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)]
89#[serde(try_from = "ConjunctiveQueryFoldabilityData")]
90pub struct ConjunctiveQueryFoldability {
91    /// Size of the finite domain D.
92    domain_size: usize,
93    /// Number of distinguished variables X.
94    num_distinguished: usize,
95    /// Number of undistinguished variables Y.
96    num_undistinguished: usize,
97    /// Arity of each relation symbol.
98    relation_arities: Vec<usize>,
99    /// Atoms of query Q1: each atom is `(relation_index, argument_list)`.
100    query1_conjuncts: Vec<(usize, Vec<Term>)>,
101    /// Atoms of query Q2: each atom is `(relation_index, argument_list)`.
102    query2_conjuncts: Vec<(usize, Vec<Term>)>,
103}
104
105#[derive(Deserialize)]
106struct ConjunctiveQueryFoldabilityData {
107    domain_size: usize,
108    num_distinguished: usize,
109    num_undistinguished: usize,
110    relation_arities: Vec<usize>,
111    query1_conjuncts: Vec<(usize, Vec<Term>)>,
112    query2_conjuncts: Vec<(usize, Vec<Term>)>,
113}
114
115impl TryFrom<ConjunctiveQueryFoldabilityData> for ConjunctiveQueryFoldability {
116    type Error = crate::registry::ConstructionError;
117    fn try_from(data: ConjunctiveQueryFoldabilityData) -> Result<Self, Self::Error> {
118        Self::try_new(
119            data.domain_size,
120            data.num_distinguished,
121            data.num_undistinguished,
122            data.relation_arities,
123            data.query1_conjuncts,
124            data.query2_conjuncts,
125        )
126    }
127}
128
129impl ConjunctiveQueryFoldability {
130    /// Create a new `ConjunctiveQueryFoldability` instance.
131    ///
132    /// # Arguments
133    ///
134    /// * `domain_size` – Number of domain constants `|D|`.
135    /// * `num_distinguished` – Number of distinguished variables `|X|`.
136    /// * `num_undistinguished` – Number of undistinguished variables `|Y|`.
137    /// * `relation_arities` – Arity of each relation symbol.
138    /// * `query1_conjuncts` – Atoms of Q1 as `(relation_index, args)` pairs.
139    /// * `query2_conjuncts` – Atoms of Q2 as `(relation_index, args)` pairs.
140    ///
141    /// # Panics
142    ///
143    /// Panics if:
144    /// - Any atom references a relation index out of range.
145    /// - Any atom has the wrong number of arguments for its relation's arity.
146    /// - Any `Constant(i)` has `i >= domain_size`.
147    /// - Any `Distinguished(i)` has `i >= num_distinguished`.
148    /// - Any `Undistinguished(i)` has `i >= num_undistinguished`.
149    pub fn new(
150        domain_size: usize,
151        num_distinguished: usize,
152        num_undistinguished: usize,
153        relation_arities: Vec<usize>,
154        query1_conjuncts: Vec<(usize, Vec<Term>)>,
155        query2_conjuncts: Vec<(usize, Vec<Term>)>,
156    ) -> Self {
157        Self::try_new(
158            domain_size,
159            num_distinguished,
160            num_undistinguished,
161            relation_arities,
162            query1_conjuncts,
163            query2_conjuncts,
164        )
165        .unwrap_or_else(|error| panic!("{error}"))
166    }
167
168    fn try_new(
169        domain_size: usize,
170        num_distinguished: usize,
171        num_undistinguished: usize,
172        relation_arities: Vec<usize>,
173        query1_conjuncts: Vec<(usize, Vec<Term>)>,
174        query2_conjuncts: Vec<(usize, Vec<Term>)>,
175    ) -> Result<Self, crate::registry::ConstructionError> {
176        let instance = Self {
177            domain_size,
178            num_distinguished,
179            num_undistinguished,
180            relation_arities,
181            query1_conjuncts,
182            query2_conjuncts,
183        };
184        instance.validate()?;
185        Ok(instance)
186    }
187
188    /// Check relation arities and argument indices.
189    fn validate(&self) -> Result<(), crate::registry::ConstructionError> {
190        for (query_name, conjuncts) in [
191            ("Q1", &self.query1_conjuncts),
192            ("Q2", &self.query2_conjuncts),
193        ] {
194            for (atom_idx, (rel_idx, args)) in conjuncts.iter().enumerate() {
195                if *rel_idx >= self.relation_arities.len() {
196                    return Err(format!(
197                        "Atom {atom_idx} of {query_name}: relation index {rel_idx} out of range \
198                     (num_relations = {})",
199                        self.relation_arities.len()
200                    )
201                    .into());
202                };
203                let arity = self.relation_arities[*rel_idx];
204                if args.len() != arity {
205                    return Err(format!(
206                        "Atom {atom_idx} of {query_name}: relation {rel_idx} has arity {arity} \
207                     but got {} arguments",
208                        args.len()
209                    )
210                    .into());
211                };
212                for term in args {
213                    match term {
214                        Term::Constant(i) => {
215                            if *i >= self.domain_size {
216                                return Err(format!(
217                                    "Atom {atom_idx} of {query_name}: Constant({i}) out of range \
218                             (domain_size = {})",
219                                    self.domain_size
220                                )
221                                .into());
222                            }
223                        }
224                        Term::Distinguished(i) => {
225                            if *i >= self.num_distinguished {
226                                return Err(format!("Atom {atom_idx} of {query_name}: Distinguished({i}) out of range \
227                             (num_distinguished = {})", self.num_distinguished).into());
228                            }
229                        }
230                        Term::Undistinguished(i) => {
231                            if *i >= self.num_undistinguished {
232                                return Err(format!("Atom {atom_idx} of {query_name}: Undistinguished({i}) out of range \
233                             (num_undistinguished = {})", self.num_undistinguished).into());
234                            }
235                        }
236                    }
237                }
238            }
239        }
240        Ok(())
241    }
242
243    /// Returns the size of the finite domain D.
244    pub fn domain_size(&self) -> usize {
245        self.domain_size
246    }
247
248    /// Returns the number of distinguished variables X.
249    pub fn num_distinguished(&self) -> usize {
250        self.num_distinguished
251    }
252
253    /// Returns the number of undistinguished variables Y.
254    pub fn num_undistinguished(&self) -> usize {
255        self.num_undistinguished
256    }
257
258    /// Returns the number of conjuncts (atoms) in Q1.
259    pub fn num_conjuncts_q1(&self) -> usize {
260        self.query1_conjuncts.len()
261    }
262
263    /// Returns the number of conjuncts (atoms) in Q2.
264    pub fn num_conjuncts_q2(&self) -> usize {
265        self.query2_conjuncts.len()
266    }
267
268    /// Returns the number of relation symbols.
269    pub fn num_relations(&self) -> usize {
270        self.relation_arities.len()
271    }
272
273    /// Returns the arities of the relation symbols.
274    pub fn relation_arities(&self) -> &[usize] {
275        &self.relation_arities
276    }
277
278    /// Returns the atoms (conjuncts) of query Q1.
279    pub fn query1_conjuncts(&self) -> &[(usize, Vec<Term>)] {
280        &self.query1_conjuncts
281    }
282
283    /// Returns the atoms (conjuncts) of query Q2.
284    pub fn query2_conjuncts(&self) -> &[(usize, Vec<Term>)] {
285        &self.query2_conjuncts
286    }
287
288    /// Decode a config index into the [`Term`] it represents under σ.
289    ///
290    /// The mapping is:
291    /// - `0..domain_size` → `Constant(v)`
292    /// - `domain_size..domain_size+num_distinguished` → `Distinguished(v - domain_size)`
293    /// - `domain_size+num_distinguished..` → `Undistinguished(v - domain_size - num_distinguished)`
294    fn decode_substitution(&self, v: usize) -> Term {
295        if v < self.domain_size {
296            Term::Constant(v)
297        } else if v < self.domain_size + self.num_distinguished {
298            Term::Distinguished(v - self.domain_size)
299        } else {
300            Term::Undistinguished(v - self.domain_size - self.num_distinguished)
301        }
302    }
303
304    /// Apply substitution `σ` (given as a slice of config values) to a single term.
305    ///
306    /// Distinguished variables and constants are left unchanged; undistinguished
307    /// variable `Y[i]` is replaced by `decode_substitution(config[i])`.
308    fn apply_substitution(&self, term: &Term, config: &[usize]) -> Term {
309        match term {
310            Term::Undistinguished(i) => self.decode_substitution(config[*i]),
311            other => other.clone(),
312        }
313    }
314}
315
316impl Problem for ConjunctiveQueryFoldability {
317    const NAME: &'static str = "ConjunctiveQueryFoldability";
318    type Solution = Vec<usize>;
319    type Value = crate::types::Or;
320
321    crate::problem_parameters![
322        ("domain_size", domain_size),
323        ("num_distinguished", num_distinguished),
324        ("num_undistinguished", num_undistinguished),
325        ("num_conjuncts_q1", num_conjuncts_q1),
326        ("num_conjuncts_q2", num_conjuncts_q2),
327        ("num_relations", num_relations),
328    ];
329
330    fn variant() -> Vec<(&'static str, &'static str)> {
331        crate::variant_params![]
332    }
333
334    /// Evaluate whether configuration `config` represents a folding of Q1 into Q2.
335    ///
336    /// Returns `true` iff applying the substitution encoded by `config` to every
337    /// atom of Q1 produces exactly the set of atoms in Q2.
338    fn evaluate(
339        &self,
340        config: &Self::Solution,
341    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
342        Ok({
343            crate::types::Or({
344                if config.len() != self.num_undistinguished {
345                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
346                        "folding-map length does not match the query variables".into(),
347                    ));
348                }
349                let range = self.domain_size + self.num_distinguished + self.num_undistinguished;
350                if config.iter().any(|&value| value >= range) {
351                    return Err(crate::traits::EvaluationError::InvalidConfiguration(
352                        "folding map contains an out-of-range value".into(),
353                    ));
354                }
355                // Apply σ to every atom of Q1.
356                let substituted: HashSet<(usize, Vec<Term>)> = self
357                    .query1_conjuncts
358                    .iter()
359                    .map(|(rel_idx, args)| {
360                        let new_args = args
361                            .iter()
362                            .map(|term| self.apply_substitution(term, config))
363                            .collect();
364                        (*rel_idx, new_args)
365                    })
366                    .collect();
367
368                // Collect Q2 as a set.
369                let q2_set: HashSet<(usize, Vec<Term>)> =
370                    self.query2_conjuncts.iter().cloned().collect();
371
372                substituted == q2_set
373            })
374        })
375    }
376}
377
378impl crate::solvers::BruteForceProblem for ConjunctiveQueryFoldability {
379    /// Each undistinguished variable can map to any element of `D ∪ X ∪ Y`.
380    fn dimensions(&self) -> Vec<usize> {
381        let range = self.domain_size + self.num_distinguished + self.num_undistinguished;
382        vec![range; self.num_undistinguished]
383    }
384}
385
386crate::declare_variants! {
387    default ConjunctiveQueryFoldability => "(num_distinguished + num_undistinguished + domain_size)^num_undistinguished * num_conjuncts_q1",
388}
389
390crate::register_brute_force! {
391    ConjunctiveQueryFoldability,
392}
393
394#[cfg(feature = "example-db")]
395pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
396    // YES instance: triangle + self-loop folds to lollipop.
397    //
398    // Q1: R(x, u) ∧ R(u, v) ∧ R(v, x) ∧ R(u, u)
399    // Q2: R(x, a) ∧ R(a, a) ∧ R(a, x)
400    //
401    // The substitution σ: U(0) → U(2), U(1) → U(2), U(2) → U(2)
402    // maps Q1 → Q2 (as a set). Config = [3, 3, 3].
403    vec![crate::example_db::specs::ModelExampleSpec {
404        id: "conjunctive_query_foldability",
405        instance: Box::new(ConjunctiveQueryFoldability::new(
406            0,       // domain_size
407            1,       // num_distinguished (x)
408            3,       // num_undistinguished (u, v, a)
409            vec![2], // one binary relation R
410            vec![
411                (0, vec![Term::Distinguished(0), Term::Undistinguished(0)]), // R(x, u)
412                (0, vec![Term::Undistinguished(0), Term::Undistinguished(1)]), // R(u, v)
413                (0, vec![Term::Undistinguished(1), Term::Distinguished(0)]), // R(v, x)
414                (0, vec![Term::Undistinguished(0), Term::Undistinguished(0)]), // R(u, u)
415            ],
416            vec![
417                (0, vec![Term::Distinguished(0), Term::Undistinguished(2)]), // R(x, a)
418                (0, vec![Term::Undistinguished(2), Term::Undistinguished(2)]), // R(a, a)
419                (0, vec![Term::Undistinguished(2), Term::Distinguished(0)]), // R(a, x)
420            ],
421        )),
422        optimal_config: serde_json::json!(vec![3, 3, 3]),
423        optimal_value: serde_json::json!(true),
424    }]
425}
426
427#[cfg(test)]
428#[path = "../../unit_tests/models/misc/conjunctive_query_foldability.rs"]
429mod tests;