Skip to main content

problemreductions/models/misc/
conjunctive_boolean_query.rs

1//! Conjunctive Boolean Query problem implementation.
2//!
3//! Given a finite domain `D = {0, ..., domain_size-1}`, a collection of
4//! relations `R`, and a conjunctive Boolean query
5//! `Q = (exists y_1, ..., y_l)(A_1 /\ ... /\ A_r)`, determine whether `Q` is
6//! true over `R` and `D`.
7//!
8//! Each conjunct `A_i` applies a relation to a tuple of arguments, where each
9//! argument is either an existentially quantified variable or a constant from
10//! the domain. The query is satisfiable iff there exists an assignment to the
11//! variables such that every conjunct's resolved tuple belongs to its relation.
12
13use crate::registry::{CreateSpec, ProblemSchemaEntry};
14use crate::traits::Problem;
15use serde::{Deserialize, Serialize};
16
17inventory::submit! {
18    ProblemSchemaEntry {
19        name: "ConjunctiveBooleanQuery",
20        display_name: "Conjunctive Boolean Query",
21        aliases: &["CBQ"],
22        dimensions: &[],
23        category: crate::registry::ProblemCategory::Misc,
24        module_path: module_path!(),
25        description: "Evaluate a conjunctive Boolean query over a relational database",
26        fields: ConjunctiveBooleanQueryCreateSpec::FIELDS,
27    }
28}
29
30/// A relation with fixed arity and a set of tuples over a finite domain.
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct Relation {
33    /// The arity (number of columns) of this relation.
34    pub arity: usize,
35    /// The set of tuples; each tuple has length == arity, entries in `0..domain_size`.
36    pub tuples: Vec<Vec<usize>>,
37}
38
39/// An argument in a conjunctive query atom.
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub enum QueryArg {
42    /// A reference to existential variable `y_i` (0-indexed).
43    Variable(usize),
44    /// A constant value from the domain `D`.
45    Constant(usize),
46}
47
48/// The Conjunctive Boolean Query problem.
49///
50/// Given a finite domain `D = {0, ..., domain_size-1}`, a collection of
51/// relations `R`, and a conjunctive Boolean query
52/// `Q = (exists y_1, ..., y_l)(A_1 /\ ... /\ A_r)`, determine whether `Q` is
53/// true over `R` and `D`.
54///
55/// # Representation
56///
57/// The configuration is a vector of length `num_variables`, where each entry is
58/// a value in `{0, ..., domain_size-1}` representing an assignment to the
59/// existentially quantified variables.
60///
61/// # Example
62///
63/// ```
64/// use problemreductions::models::misc::{ConjunctiveBooleanQuery, CbqRelation, QueryArg};
65/// use problemreductions::{Problem, BruteForce};
66///
67/// let relations = vec![
68///     CbqRelation { arity: 2, tuples: vec![vec![0, 3], vec![1, 3]] },
69/// ];
70/// let conjuncts = vec![
71///     (0, vec![QueryArg::Variable(0), QueryArg::Constant(3)]),
72/// ];
73/// let problem = ConjunctiveBooleanQuery::new(6, relations, 1, conjuncts);
74/// let solver = BruteForce::new();
75/// let solution = solver.solve(&problem).unwrap();
76/// assert!(solution.is_some());
77/// ```
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79#[serde(try_from = "ConjunctiveBooleanQueryData")]
80pub struct ConjunctiveBooleanQuery {
81    domain_size: usize,
82    relations: Vec<Relation>,
83    num_variables: usize,
84    conjuncts: Vec<(usize, Vec<QueryArg>)>,
85}
86
87#[derive(Deserialize)]
88struct ConjunctiveBooleanQueryData {
89    domain_size: usize,
90    relations: Vec<Relation>,
91    num_variables: usize,
92    conjuncts: Vec<(usize, Vec<QueryArg>)>,
93}
94
95impl TryFrom<ConjunctiveBooleanQueryData> for ConjunctiveBooleanQuery {
96    type Error = crate::registry::ConstructionError;
97    fn try_from(data: ConjunctiveBooleanQueryData) -> Result<Self, Self::Error> {
98        Self::try_new(
99            data.domain_size,
100            data.relations,
101            data.num_variables,
102            data.conjuncts,
103        )
104    }
105}
106
107#[derive(Debug, Deserialize, crate::CreateSpec)]
108struct ConjunctiveBooleanQueryCreateSpec {
109    /// Size of the finite domain.
110    domain_size: usize,
111    /// Relations evaluated by the query.
112    #[create(codec = "json")]
113    relations: Vec<Relation>,
114    /// Query atoms; the number of variables is inferred from their arguments.
115    #[create(codec = "json")]
116    conjuncts: Vec<(usize, Vec<QueryArg>)>,
117}
118
119impl TryFrom<ConjunctiveBooleanQueryCreateSpec> for ConjunctiveBooleanQuery {
120    type Error = crate::registry::ConstructionError;
121
122    fn try_from(spec: ConjunctiveBooleanQueryCreateSpec) -> Result<Self, Self::Error> {
123        let mut num_variables = 0_usize;
124        for (_, args) in &spec.conjuncts {
125            for arg in args {
126                if let QueryArg::Variable(variable) = arg {
127                    let count = variable
128                        .checked_add(1)
129                        .ok_or_else(|| "number of query variables overflows usize".to_string())?;
130                    num_variables = num_variables.max(count);
131                }
132            }
133        }
134
135        Self::try_new(
136            spec.domain_size,
137            spec.relations,
138            num_variables,
139            spec.conjuncts,
140        )
141    }
142}
143
144impl ConjunctiveBooleanQuery {
145    /// Create a new ConjunctiveBooleanQuery instance.
146    ///
147    /// # Panics
148    ///
149    /// Panics if:
150    /// - Any relation's tuples have incorrect arity
151    /// - Any tuple entry is >= domain_size
152    /// - Any conjunct references a non-existent relation
153    /// - Any `Variable(i)` has `i >= num_variables`
154    /// - Any `Constant(c)` has `c >= domain_size`
155    /// - Any conjunct's argument count does not match the referenced relation's arity
156    pub fn new(
157        domain_size: usize,
158        relations: Vec<Relation>,
159        num_variables: usize,
160        conjuncts: Vec<(usize, Vec<QueryArg>)>,
161    ) -> Self {
162        Self::try_new(domain_size, relations, num_variables, conjuncts)
163            .unwrap_or_else(|error| panic!("{error}"))
164    }
165
166    fn try_new(
167        domain_size: usize,
168        relations: Vec<Relation>,
169        num_variables: usize,
170        conjuncts: Vec<(usize, Vec<QueryArg>)>,
171    ) -> Result<Self, crate::registry::ConstructionError> {
172        for (i, rel) in relations.iter().enumerate() {
173            for (j, tuple) in rel.tuples.iter().enumerate() {
174                if tuple.len() != rel.arity {
175                    return Err(format!(
176                        "Relation {i}: tuple {j} has length {}, expected arity {}",
177                        tuple.len(),
178                        rel.arity
179                    )
180                    .into());
181                }
182                for (k, &val) in tuple.iter().enumerate() {
183                    if val >= domain_size {
184                        return Err(format!(
185                            "Relation {i}: tuple {j}, entry {k} is {val}, must be < {domain_size}"
186                        )
187                        .into());
188                    }
189                }
190            }
191        }
192        for (i, (rel_idx, args)) in conjuncts.iter().enumerate() {
193            if *rel_idx >= relations.len() {
194                return Err(format!(
195                    "Conjunct {i}: relation index {rel_idx} out of range (have {} relations)",
196                    relations.len()
197                )
198                .into());
199            }
200            if args.len() != relations[*rel_idx].arity {
201                return Err(format!(
202                    "Conjunct {i}: has {} args, expected arity {}",
203                    args.len(),
204                    relations[*rel_idx].arity
205                )
206                .into());
207            }
208            for (k, arg) in args.iter().enumerate() {
209                match arg {
210                    QueryArg::Variable(v) => {
211                        if *v >= num_variables {
212                            return Err(format!("Conjunct {i}, arg {k}: Variable({v}) >= num_variables ({num_variables})").into());
213                        }
214                    }
215                    QueryArg::Constant(c) => {
216                        if *c >= domain_size {
217                            return Err(format!("Conjunct {i}, arg {k}: Constant({c}) >= domain_size ({domain_size})").into());
218                        }
219                    }
220                }
221            }
222        }
223        Ok(Self {
224            domain_size,
225            relations,
226            num_variables,
227            conjuncts,
228        })
229    }
230
231    /// Returns the size of the finite domain.
232    pub fn domain_size(&self) -> usize {
233        self.domain_size
234    }
235
236    /// Returns the number of relations.
237    pub fn num_relations(&self) -> usize {
238        self.relations.len()
239    }
240
241    /// Returns the number of existentially quantified variables.
242    pub fn num_variables(&self) -> usize {
243        self.num_variables
244    }
245
246    /// Returns the number of conjuncts in the query.
247    pub fn num_conjuncts(&self) -> usize {
248        self.conjuncts.len()
249    }
250
251    /// Returns the relations.
252    pub fn relations(&self) -> &[Relation] {
253        &self.relations
254    }
255
256    /// Returns the conjuncts.
257    pub fn conjuncts(&self) -> &[(usize, Vec<QueryArg>)] {
258        &self.conjuncts
259    }
260}
261
262impl Problem for ConjunctiveBooleanQuery {
263    const NAME: &'static str = "ConjunctiveBooleanQuery";
264    type Solution = Vec<usize>;
265    type Value = crate::types::Or;
266
267    crate::problem_parameters![
268        ("domain_size", domain_size),
269        ("num_conjuncts", num_conjuncts),
270        ("num_relations", num_relations),
271        ("num_variables", num_variables),
272    ];
273
274    fn variant() -> Vec<(&'static str, &'static str)> {
275        crate::variant_params![]
276    }
277
278    fn evaluate(
279        &self,
280        config: &Self::Solution,
281    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
282        Ok({
283            crate::types::Or({
284                if config.len() != self.num_variables {
285                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
286                        "variable assignment length does not match the query".into(),
287                    ));
288                }
289                if config.iter().any(|&v| v >= self.domain_size) {
290                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
291                        "variable assignment contains an out-of-range domain value".into(),
292                    ));
293                }
294                self.conjuncts.iter().all(|(rel_idx, args)| {
295                    let tuple: Vec<usize> = args
296                        .iter()
297                        .map(|arg| match arg {
298                            QueryArg::Variable(i) => config[*i],
299                            QueryArg::Constant(c) => *c,
300                        })
301                        .collect();
302                    self.relations[*rel_idx].tuples.contains(&tuple)
303                })
304            })
305        })
306    }
307}
308
309impl crate::solvers::BruteForceProblem for ConjunctiveBooleanQuery {
310    fn dimensions(&self) -> Vec<usize> {
311        vec![self.domain_size; self.num_variables]
312    }
313}
314
315crate::declare_variants! {
316    default ConjunctiveBooleanQuery => "domain_size ^ num_variables" create ConjunctiveBooleanQueryCreateSpec,
317}
318
319crate::register_brute_force! {
320    ConjunctiveBooleanQuery,
321}
322
323#[cfg(feature = "example-db")]
324pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
325    vec![crate::example_db::specs::ModelExampleSpec {
326        id: "conjunctive_boolean_query",
327        // D={0..5}, 2 relations (binary R0, ternary R1), 3 atoms, 2 variables.
328        // Satisfying assignment: y0=0, y1=1.
329        instance: Box::new(ConjunctiveBooleanQuery::new(
330            6,
331            vec![
332                Relation {
333                    arity: 2,
334                    tuples: vec![vec![0, 3], vec![1, 3], vec![2, 4], vec![3, 4], vec![4, 5]],
335                },
336                Relation {
337                    arity: 3,
338                    tuples: vec![vec![0, 1, 5], vec![1, 2, 5], vec![2, 3, 4], vec![0, 4, 3]],
339                },
340            ],
341            2,
342            vec![
343                (0, vec![QueryArg::Variable(0), QueryArg::Constant(3)]),
344                (0, vec![QueryArg::Variable(1), QueryArg::Constant(3)]),
345                (
346                    1,
347                    vec![
348                        QueryArg::Variable(0),
349                        QueryArg::Variable(1),
350                        QueryArg::Constant(5),
351                    ],
352                ),
353            ],
354        )),
355        optimal_config: serde_json::json!(vec![0, 1]),
356        optimal_value: serde_json::json!(true),
357    }]
358}
359
360#[cfg(test)]
361#[path = "../../unit_tests/models/misc/conjunctive_boolean_query.rs"]
362mod tests;