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;