Skip to main content

problemreductions/models/misc/
conjunctive_query_foldability.rs

1//! Conjunctive Query Foldability problem implementation.
2//!
3//! Given two conjunctive queries Q1 and Q2 over a finite domain with relations,
4//! the problem asks whether there exists a substitution of undistinguished variables
5//! that transforms Q1 into Q2. NP-complete (Chandra & Merlin, 1977).
6
7use crate::registry::{FieldInfo, ProblemSchemaEntry};
8use crate::traits::Problem;
9use serde::{Deserialize, Serialize};
10use std::collections::HashSet;
11
12inventory::submit! {
13    ProblemSchemaEntry {
14        name: "ConjunctiveQueryFoldability",
15        display_name: "Conjunctive Query Foldability",
16        aliases: &[],
17        dimensions: &[],
18        module_path: module_path!(),
19        description: "Determine if one conjunctive query can be folded into another by substituting undistinguished variables",
20        fields: &[
21            FieldInfo { name: "domain_size", type_name: "usize", description: "Size of the finite domain D" },
22            FieldInfo { name: "num_distinguished", type_name: "usize", description: "Number of distinguished variables X" },
23            FieldInfo { name: "num_undistinguished", type_name: "usize", description: "Number of undistinguished variables Y" },
24            FieldInfo { name: "relation_arities", type_name: "Vec<usize>", description: "Arity of each relation" },
25            FieldInfo { name: "query1_conjuncts", type_name: "Vec<(usize, Vec<Term>)>", description: "Atoms of query Q1" },
26            FieldInfo { name: "query2_conjuncts", type_name: "Vec<(usize, Vec<Term>)>", description: "Atoms of query Q2" },
27        ],
28    }
29}
30
31/// A term in a conjunctive query atom.
32#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(tag = "type", content = "index")]
34pub enum Term {
35    /// A domain constant `D[i]`.
36    Constant(usize),
37    /// A distinguished variable `X[i]`.
38    Distinguished(usize),
39    /// An undistinguished variable `Y[i]`.
40    Undistinguished(usize),
41}
42
43/// The Conjunctive Query Foldability problem.
44///
45/// Given a finite domain `D`, a set of relation symbols with fixed arities,
46/// distinguished variables `X`, undistinguished variables `Y`, and two
47/// conjunctive queries Q1 and Q2 (over `X ∪ Y ∪ D`), this problem asks:
48/// does there exist a substitution `σ: Y → X ∪ Y ∪ D` that maps every atom
49/// of Q1 to an atom of Q2?
50///
51/// This is equivalent to the *query containment* problem for conjunctive queries
52/// and is NP-complete (Chandra & Merlin, 1977; Garey & Johnson A4 SR30).
53///
54/// # Representation
55///
56/// - Each undistinguished variable `Y[i]` is a configuration variable whose
57///   value encodes its substitution target:
58///   - `0..domain_size` → `Constant(v)`
59///   - `domain_size..domain_size+num_distinguished` → `Distinguished(v - domain_size)`
60///   - `domain_size+num_distinguished..` → `Undistinguished(v - domain_size - num_distinguished)`
61/// - The problem is satisfiable iff applying `σ` to all atoms of Q1 yields
62///   exactly the multiset (treated as a set) of atoms in Q2.
63///
64/// # Example
65///
66/// ```
67/// use problemreductions::models::misc::{ConjunctiveQueryFoldability, Term};
68/// use problemreductions::{Problem, Solver, BruteForce};
69///
70/// // Q1: R(x, u) ∧ R(u, u)    Q2: R(x, x)  (single atom; duplicates are irrelevant)
71/// // σ: u → x (index = domain_size + 0 = 0) folds Q1 to Q2
72/// let problem = ConjunctiveQueryFoldability::new(
73///     0, 1, 1,
74///     vec![2],
75///     vec![
76///         (0, vec![Term::Distinguished(0), Term::Undistinguished(0)]),
77///         (0, vec![Term::Undistinguished(0), Term::Undistinguished(0)]),
78///     ],
79///     vec![
80///         (0, vec![Term::Distinguished(0), Term::Distinguished(0)]),
81///     ],
82/// );
83/// let solver = BruteForce::new();
84/// let solution = solver.find_witness(&problem);
85/// assert!(solution.is_some());
86/// ```
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct ConjunctiveQueryFoldability {
89    /// Size of the finite domain D.
90    domain_size: usize,
91    /// Number of distinguished variables X.
92    num_distinguished: usize,
93    /// Number of undistinguished variables Y.
94    num_undistinguished: usize,
95    /// Arity of each relation symbol.
96    relation_arities: Vec<usize>,
97    /// Atoms of query Q1: each atom is `(relation_index, argument_list)`.
98    query1_conjuncts: Vec<(usize, Vec<Term>)>,
99    /// Atoms of query Q2: each atom is `(relation_index, argument_list)`.
100    query2_conjuncts: Vec<(usize, Vec<Term>)>,
101}
102
103impl ConjunctiveQueryFoldability {
104    /// Create a new `ConjunctiveQueryFoldability` instance.
105    ///
106    /// # Arguments
107    ///
108    /// * `domain_size` – Number of domain constants `|D|`.
109    /// * `num_distinguished` – Number of distinguished variables `|X|`.
110    /// * `num_undistinguished` – Number of undistinguished variables `|Y|`.
111    /// * `relation_arities` – Arity of each relation symbol.
112    /// * `query1_conjuncts` – Atoms of Q1 as `(relation_index, args)` pairs.
113    /// * `query2_conjuncts` – Atoms of Q2 as `(relation_index, args)` pairs.
114    ///
115    /// # Panics
116    ///
117    /// Panics if:
118    /// - Any atom references a relation index out of range.
119    /// - Any atom has the wrong number of arguments for its relation's arity.
120    /// - Any `Constant(i)` has `i >= domain_size`.
121    /// - Any `Distinguished(i)` has `i >= num_distinguished`.
122    /// - Any `Undistinguished(i)` has `i >= num_undistinguished`.
123    pub fn new(
124        domain_size: usize,
125        num_distinguished: usize,
126        num_undistinguished: usize,
127        relation_arities: Vec<usize>,
128        query1_conjuncts: Vec<(usize, Vec<Term>)>,
129        query2_conjuncts: Vec<(usize, Vec<Term>)>,
130    ) -> Self {
131        let instance = Self {
132            domain_size,
133            num_distinguished,
134            num_undistinguished,
135            relation_arities,
136            query1_conjuncts,
137            query2_conjuncts,
138        };
139        instance.validate();
140        instance
141    }
142
143    /// Validate the instance, panicking on any inconsistency.
144    fn validate(&self) {
145        for (query_name, conjuncts) in [
146            ("Q1", &self.query1_conjuncts),
147            ("Q2", &self.query2_conjuncts),
148        ] {
149            for (atom_idx, (rel_idx, args)) in conjuncts.iter().enumerate() {
150                assert!(
151                    *rel_idx < self.relation_arities.len(),
152                    "Atom {atom_idx} of {query_name}: relation index {rel_idx} out of range \
153                     (num_relations = {})",
154                    self.relation_arities.len()
155                );
156                let arity = self.relation_arities[*rel_idx];
157                assert_eq!(
158                    args.len(),
159                    arity,
160                    "Atom {atom_idx} of {query_name}: relation {rel_idx} has arity {arity} \
161                     but got {} arguments",
162                    args.len()
163                );
164                for term in args {
165                    match term {
166                        Term::Constant(i) => assert!(
167                            *i < self.domain_size,
168                            "Atom {atom_idx} of {query_name}: Constant({i}) out of range \
169                             (domain_size = {})",
170                            self.domain_size
171                        ),
172                        Term::Distinguished(i) => assert!(
173                            *i < self.num_distinguished,
174                            "Atom {atom_idx} of {query_name}: Distinguished({i}) out of range \
175                             (num_distinguished = {})",
176                            self.num_distinguished
177                        ),
178                        Term::Undistinguished(i) => assert!(
179                            *i < self.num_undistinguished,
180                            "Atom {atom_idx} of {query_name}: Undistinguished({i}) out of range \
181                             (num_undistinguished = {})",
182                            self.num_undistinguished
183                        ),
184                    }
185                }
186            }
187        }
188    }
189
190    /// Returns the size of the finite domain D.
191    pub fn domain_size(&self) -> usize {
192        self.domain_size
193    }
194
195    /// Returns the number of distinguished variables X.
196    pub fn num_distinguished(&self) -> usize {
197        self.num_distinguished
198    }
199
200    /// Returns the number of undistinguished variables Y.
201    pub fn num_undistinguished(&self) -> usize {
202        self.num_undistinguished
203    }
204
205    /// Returns the number of conjuncts (atoms) in Q1.
206    pub fn num_conjuncts_q1(&self) -> usize {
207        self.query1_conjuncts.len()
208    }
209
210    /// Returns the number of conjuncts (atoms) in Q2.
211    pub fn num_conjuncts_q2(&self) -> usize {
212        self.query2_conjuncts.len()
213    }
214
215    /// Returns the number of relation symbols.
216    pub fn num_relations(&self) -> usize {
217        self.relation_arities.len()
218    }
219
220    /// Returns the arities of the relation symbols.
221    pub fn relation_arities(&self) -> &[usize] {
222        &self.relation_arities
223    }
224
225    /// Returns the atoms (conjuncts) of query Q1.
226    pub fn query1_conjuncts(&self) -> &[(usize, Vec<Term>)] {
227        &self.query1_conjuncts
228    }
229
230    /// Returns the atoms (conjuncts) of query Q2.
231    pub fn query2_conjuncts(&self) -> &[(usize, Vec<Term>)] {
232        &self.query2_conjuncts
233    }
234
235    /// Decode a config index into the [`Term`] it represents under σ.
236    ///
237    /// The mapping is:
238    /// - `0..domain_size` → `Constant(v)`
239    /// - `domain_size..domain_size+num_distinguished` → `Distinguished(v - domain_size)`
240    /// - `domain_size+num_distinguished..` → `Undistinguished(v - domain_size - num_distinguished)`
241    fn decode_substitution(&self, v: usize) -> Term {
242        if v < self.domain_size {
243            Term::Constant(v)
244        } else if v < self.domain_size + self.num_distinguished {
245            Term::Distinguished(v - self.domain_size)
246        } else {
247            Term::Undistinguished(v - self.domain_size - self.num_distinguished)
248        }
249    }
250
251    /// Apply substitution `σ` (given as a slice of config values) to a single term.
252    ///
253    /// Distinguished variables and constants are left unchanged; undistinguished
254    /// variable `Y[i]` is replaced by `decode_substitution(config[i])`.
255    fn apply_substitution(&self, term: &Term, config: &[usize]) -> Term {
256        match term {
257            Term::Undistinguished(i) => self.decode_substitution(config[*i]),
258            other => other.clone(),
259        }
260    }
261}
262
263impl Problem for ConjunctiveQueryFoldability {
264    const NAME: &'static str = "ConjunctiveQueryFoldability";
265    type Value = crate::types::Or;
266
267    fn variant() -> Vec<(&'static str, &'static str)> {
268        crate::variant_params![]
269    }
270
271    /// Returns the configuration space dimensions.
272    ///
273    /// Each of the `num_undistinguished` variables can map to any element of
274    /// `D ∪ X ∪ Y`, giving `domain_size + num_distinguished + num_undistinguished`
275    /// choices per variable.  When `num_undistinguished == 0` the vector is empty
276    /// (Q1 contains no variables to substitute; the problem is trivially decided
277    /// by checking set equality of Q1 and Q2 at evaluation time).
278    fn dims(&self) -> Vec<usize> {
279        let range = self.domain_size + self.num_distinguished + self.num_undistinguished;
280        vec![range; self.num_undistinguished]
281    }
282
283    /// Evaluate whether configuration `config` represents a folding of Q1 into Q2.
284    ///
285    /// Returns `true` iff applying the substitution encoded by `config` to every
286    /// atom of Q1 produces exactly the set of atoms in Q2.
287    fn evaluate(&self, config: &[usize]) -> crate::types::Or {
288        crate::types::Or({
289            if config.len() != self.num_undistinguished {
290                return crate::types::Or(false);
291            }
292            let range = self.domain_size + self.num_distinguished + self.num_undistinguished;
293            if config.iter().any(|&v| v >= range) {
294                return crate::types::Or(false);
295            }
296
297            // Apply σ to every atom of Q1.
298            let substituted: HashSet<(usize, Vec<Term>)> = self
299                .query1_conjuncts
300                .iter()
301                .map(|(rel_idx, args)| {
302                    let new_args = args
303                        .iter()
304                        .map(|term| self.apply_substitution(term, config))
305                        .collect();
306                    (*rel_idx, new_args)
307                })
308                .collect();
309
310            // Collect Q2 as a set.
311            let q2_set: HashSet<(usize, Vec<Term>)> =
312                self.query2_conjuncts.iter().cloned().collect();
313
314            substituted == q2_set
315        })
316    }
317}
318
319crate::declare_variants! {
320    default ConjunctiveQueryFoldability => "(num_distinguished + num_undistinguished + domain_size)^num_undistinguished * num_conjuncts_q1",
321}
322
323#[cfg(feature = "example-db")]
324pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
325    // YES instance: triangle + self-loop folds to lollipop.
326    //
327    // Q1: R(x, u) ∧ R(u, v) ∧ R(v, x) ∧ R(u, u)
328    // Q2: R(x, a) ∧ R(a, a) ∧ R(a, x)
329    //
330    // The substitution σ: U(0) → U(2), U(1) → U(2), U(2) → U(2)
331    // maps Q1 → Q2 (as a set). Config = [3, 3, 3].
332    vec![crate::example_db::specs::ModelExampleSpec {
333        id: "conjunctive_query_foldability",
334        instance: Box::new(ConjunctiveQueryFoldability::new(
335            0,       // domain_size
336            1,       // num_distinguished (x)
337            3,       // num_undistinguished (u, v, a)
338            vec![2], // one binary relation R
339            vec![
340                (0, vec![Term::Distinguished(0), Term::Undistinguished(0)]), // R(x, u)
341                (0, vec![Term::Undistinguished(0), Term::Undistinguished(1)]), // R(u, v)
342                (0, vec![Term::Undistinguished(1), Term::Distinguished(0)]), // R(v, x)
343                (0, vec![Term::Undistinguished(0), Term::Undistinguished(0)]), // R(u, u)
344            ],
345            vec![
346                (0, vec![Term::Distinguished(0), Term::Undistinguished(2)]), // R(x, a)
347                (0, vec![Term::Undistinguished(2), Term::Undistinguished(2)]), // R(a, a)
348                (0, vec![Term::Undistinguished(2), Term::Distinguished(0)]), // R(a, x)
349            ],
350        )),
351        optimal_config: vec![3, 3, 3],
352        optimal_value: serde_json::json!(true),
353    }]
354}
355
356#[cfg(test)]
357#[path = "../../unit_tests/models/misc/conjunctive_query_foldability.rs"]
358mod tests;