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)]
79pub struct ConjunctiveBooleanQuery {
80    domain_size: usize,
81    relations: Vec<Relation>,
82    num_variables: usize,
83    conjuncts: Vec<(usize, Vec<QueryArg>)>,
84}
85
86#[derive(Debug, Deserialize, crate::CreateSpec)]
87struct ConjunctiveBooleanQueryCreateSpec {
88    /// Size of the finite domain.
89    domain_size: usize,
90    /// Relations evaluated by the query.
91    #[create(codec = "json")]
92    relations: Vec<Relation>,
93    /// Query atoms; the number of variables is inferred from their arguments.
94    #[create(codec = "json")]
95    conjuncts: Vec<(usize, Vec<QueryArg>)>,
96}
97
98impl TryFrom<ConjunctiveBooleanQueryCreateSpec> for ConjunctiveBooleanQuery {
99    type Error = crate::registry::ConstructionError;
100
101    fn try_from(spec: ConjunctiveBooleanQueryCreateSpec) -> Result<Self, Self::Error> {
102        let mut num_variables = 0_usize;
103        for (_, args) in &spec.conjuncts {
104            for arg in args {
105                if let QueryArg::Variable(variable) = arg {
106                    let count = variable
107                        .checked_add(1)
108                        .ok_or_else(|| "number of query variables overflows usize".to_string())?;
109                    num_variables = num_variables.max(count);
110                }
111            }
112        }
113
114        for (relation_index, relation) in spec.relations.iter().enumerate() {
115            for (tuple_index, tuple) in relation.tuples.iter().enumerate() {
116                if tuple.len() != relation.arity {
117                    return Err(format!(
118                        "relation {relation_index} tuple {tuple_index} has length {}, expected arity {}",
119                        tuple.len(),
120                        relation.arity
121                    ).into());
122                }
123                for (entry_index, &value) in tuple.iter().enumerate() {
124                    if value >= spec.domain_size {
125                        return Err(format!(
126                            "relation {relation_index} tuple {tuple_index} entry {entry_index} is {value}, must be less than domain size {}",
127                            spec.domain_size
128                        ).into());
129                    }
130                }
131            }
132        }
133
134        for (conjunct_index, (relation_index, args)) in spec.conjuncts.iter().enumerate() {
135            let relation = spec.relations.get(*relation_index).ok_or_else(|| {
136                format!(
137                    "conjunct {conjunct_index} relation index {relation_index} is out of range for {} relations",
138                    spec.relations.len()
139                )
140            })?;
141            if args.len() != relation.arity {
142                return Err(format!(
143                    "conjunct {conjunct_index} has {} arguments, expected arity {}",
144                    args.len(),
145                    relation.arity
146                )
147                .into());
148            }
149            for (argument_index, arg) in args.iter().enumerate() {
150                if let QueryArg::Constant(value) = arg {
151                    if *value >= spec.domain_size {
152                        return Err(format!(
153                            "conjunct {conjunct_index} argument {argument_index} constant {value} must be less than domain size {}",
154                            spec.domain_size
155                        ).into());
156                    }
157                }
158            }
159        }
160
161        Ok(Self {
162            domain_size: spec.domain_size,
163            relations: spec.relations,
164            num_variables,
165            conjuncts: spec.conjuncts,
166        })
167    }
168}
169
170impl ConjunctiveBooleanQuery {
171    /// Create a new ConjunctiveBooleanQuery instance.
172    ///
173    /// # Panics
174    ///
175    /// Panics if:
176    /// - Any relation's tuples have incorrect arity
177    /// - Any tuple entry is >= domain_size
178    /// - Any conjunct references a non-existent relation
179    /// - Any `Variable(i)` has `i >= num_variables`
180    /// - Any `Constant(c)` has `c >= domain_size`
181    /// - Any conjunct's argument count does not match the referenced relation's arity
182    pub fn new(
183        domain_size: usize,
184        relations: Vec<Relation>,
185        num_variables: usize,
186        conjuncts: Vec<(usize, Vec<QueryArg>)>,
187    ) -> Self {
188        for (i, rel) in relations.iter().enumerate() {
189            for (j, tuple) in rel.tuples.iter().enumerate() {
190                assert!(
191                    tuple.len() == rel.arity,
192                    "Relation {i}: tuple {j} has length {}, expected arity {}",
193                    tuple.len(),
194                    rel.arity
195                );
196                for (k, &val) in tuple.iter().enumerate() {
197                    assert!(
198                        val < domain_size,
199                        "Relation {i}: tuple {j}, entry {k} is {val}, must be < {domain_size}"
200                    );
201                }
202            }
203        }
204        for (i, (rel_idx, args)) in conjuncts.iter().enumerate() {
205            assert!(
206                *rel_idx < relations.len(),
207                "Conjunct {i}: relation index {rel_idx} out of range (have {} relations)",
208                relations.len()
209            );
210            assert!(
211                args.len() == relations[*rel_idx].arity,
212                "Conjunct {i}: has {} args, expected arity {}",
213                args.len(),
214                relations[*rel_idx].arity
215            );
216            for (k, arg) in args.iter().enumerate() {
217                match arg {
218                    QueryArg::Variable(v) => {
219                        assert!(
220                            *v < num_variables,
221                            "Conjunct {i}, arg {k}: Variable({v}) >= num_variables ({num_variables})"
222                        );
223                    }
224                    QueryArg::Constant(c) => {
225                        assert!(
226                            *c < domain_size,
227                            "Conjunct {i}, arg {k}: Constant({c}) >= domain_size ({domain_size})"
228                        );
229                    }
230                }
231            }
232        }
233        Self {
234            domain_size,
235            relations,
236            num_variables,
237            conjuncts,
238        }
239    }
240
241    /// Returns the size of the finite domain.
242    pub fn domain_size(&self) -> usize {
243        self.domain_size
244    }
245
246    /// Returns the number of relations.
247    pub fn num_relations(&self) -> usize {
248        self.relations.len()
249    }
250
251    /// Returns the number of existentially quantified variables.
252    pub fn num_variables(&self) -> usize {
253        self.num_variables
254    }
255
256    /// Returns the number of conjuncts in the query.
257    pub fn num_conjuncts(&self) -> usize {
258        self.conjuncts.len()
259    }
260
261    /// Returns the relations.
262    pub fn relations(&self) -> &[Relation] {
263        &self.relations
264    }
265
266    /// Returns the conjuncts.
267    pub fn conjuncts(&self) -> &[(usize, Vec<QueryArg>)] {
268        &self.conjuncts
269    }
270}
271
272impl Problem for ConjunctiveBooleanQuery {
273    const NAME: &'static str = "ConjunctiveBooleanQuery";
274    type Solution = Vec<usize>;
275    type Value = crate::types::Or;
276
277    crate::problem_parameters![
278        ("domain_size", domain_size),
279        ("num_conjuncts", num_conjuncts),
280        ("num_relations", num_relations),
281        ("num_variables", num_variables),
282    ];
283
284    fn variant() -> Vec<(&'static str, &'static str)> {
285        crate::variant_params![]
286    }
287
288    fn evaluate(
289        &self,
290        config: &Self::Solution,
291    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
292        Ok({
293            crate::types::Or({
294                if config.len() != self.num_variables {
295                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
296                        "variable assignment length does not match the query".into(),
297                    ));
298                }
299                if config.iter().any(|&v| v >= self.domain_size) {
300                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
301                        "variable assignment contains an out-of-range domain value".into(),
302                    ));
303                }
304                self.conjuncts.iter().all(|(rel_idx, args)| {
305                    let tuple: Vec<usize> = args
306                        .iter()
307                        .map(|arg| match arg {
308                            QueryArg::Variable(i) => config[*i],
309                            QueryArg::Constant(c) => *c,
310                        })
311                        .collect();
312                    self.relations[*rel_idx].tuples.contains(&tuple)
313                })
314            })
315        })
316    }
317}
318
319impl crate::solvers::BruteForceProblem for ConjunctiveBooleanQuery {
320    fn dimensions(&self) -> Vec<usize> {
321        vec![self.domain_size; self.num_variables]
322    }
323}
324
325crate::declare_variants! {
326    default ConjunctiveBooleanQuery => "domain_size ^ num_variables" create ConjunctiveBooleanQueryCreateSpec,
327}
328
329crate::register_brute_force! {
330    ConjunctiveBooleanQuery,
331}
332
333#[cfg(feature = "example-db")]
334pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
335    vec![crate::example_db::specs::ModelExampleSpec {
336        id: "conjunctive_boolean_query",
337        // D={0..5}, 2 relations (binary R0, ternary R1), 3 atoms, 2 variables.
338        // Satisfying assignment: y0=0, y1=1.
339        instance: Box::new(ConjunctiveBooleanQuery::new(
340            6,
341            vec![
342                Relation {
343                    arity: 2,
344                    tuples: vec![vec![0, 3], vec![1, 3], vec![2, 4], vec![3, 4], vec![4, 5]],
345                },
346                Relation {
347                    arity: 3,
348                    tuples: vec![vec![0, 1, 5], vec![1, 2, 5], vec![2, 3, 4], vec![0, 4, 3]],
349                },
350            ],
351            2,
352            vec![
353                (0, vec![QueryArg::Variable(0), QueryArg::Constant(3)]),
354                (0, vec![QueryArg::Variable(1), QueryArg::Constant(3)]),
355                (
356                    1,
357                    vec![
358                        QueryArg::Variable(0),
359                        QueryArg::Variable(1),
360                        QueryArg::Constant(5),
361                    ],
362                ),
363            ],
364        )),
365        optimal_config: serde_json::json!(vec![0, 1]),
366        optimal_value: serde_json::json!(true),
367    }]
368}
369
370#[cfg(test)]
371#[path = "../../unit_tests/models/misc/conjunctive_boolean_query.rs"]
372mod tests;