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;