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