ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
joint_width_query.c
Go to the documentation of this file.
1/**
2 * @file joint_width_query.c
3 * @brief Implementation of the planner-time UCQ recogniser (descriptor
4 * extraction). See @c joint_width_query.h.
5 */
6#include "postgres.h"
7#include "nodes/parsenodes.h"
8#include "nodes/nodeFuncs.h"
9#include "nodes/makefuncs.h"
10#include "nodes/pg_list.h"
11#include "catalog/pg_type.h"
12#include "lib/stringinfo.h"
13#if PG_VERSION_NUM >= 120000
14#include "optimizer/optimizer.h"
15#else
16#include "optimizer/clauses.h" /* make_ands_explicit (PG <12) */
17#endif
18#include "parser/parsetree.h"
19#include "rewrite/rewriteManip.h"
20#include "utils/builtins.h"
21#include "utils/lsyscache.h"
22#include "utils/ruleutils.h"
23
24#include "joint_width_query.h"
25#include "provsql_utils.h"
26#include "qual_classify.h"
27
28/** @brief A column reference (range-table index, attribute number). */
29typedef struct ColRef {
30 Index rtindex;
31 AttrNumber attno;
32 int parent; /* union-find parent index into the ColRef array */
33} ColRef;
34
35/** @brief Find-or-add a (rtindex, attno) column node; returns its index. */
36static int colref_get(ColRef *cols, int *ncols, Index rtindex, AttrNumber attno)
37{
38 int i;
39 for (i = 0; i < *ncols; i++)
40 if (cols[i].rtindex == rtindex && cols[i].attno == attno)
41 return i;
42 cols[*ncols].rtindex = rtindex;
43 cols[*ncols].attno = attno;
44 cols[*ncols].parent = *ncols;
45 return (*ncols)++;
46}
47
48/** @brief Union-find root with path halving. */
49static int uf_find(ColRef *cols, int i)
50{
51 while (cols[i].parent != i) {
52 cols[i].parent = cols[cols[i].parent].parent;
53 i = cols[i].parent;
54 }
55 return i;
56}
57
58static void uf_union(ColRef *cols, int a, int b)
59{
60 int ra = uf_find(cols, a), rb = uf_find(cols, b);
61 if (ra != rb)
62 cols[ra].parent = rb;
63}
64
65/**
66 * @brief Collect the equality structure of a WHERE / ON qual tree, over
67 * the shared @c qual_classify recognisers.
68 *
69 * Every conjunct of the (AND-only) qual must be either a @c Var=Var join
70 * (its two columns unioned, recorded in @p pairs) or a @c Var=Const
71 * selection. A selection is recorded in @p const_cols / @p const_vals
72 * when @p const_cols is non-NULL (the caller pins the column's variable to
73 * the value); when @c NULL it makes the recogniser decline (the UNION arms,
74 * which cannot pin per disjunct, pass @c NULL). The constant must share
75 * the column's exact type: the pin is applied through the gather's text
76 * element dictionary, so a cross-type literal could mis-match its column's
77 * text form. Any other qual shape declines.
78 */
79static bool collect_equalities(Node *qual, ColRef *cols, int *ncols,
80 int **pairs, int *npairs,
81 int *const_cols, Const **const_vals, int *nconst)
82{
83 List *conjuncts = NIL;
84 ListCell *lc;
85
86 qc_flatten_and(qual, &conjuncts);
87 foreach (lc, conjuncts) {
88 Expr *c = (Expr *) lfirst(lc);
89 Var *lv, *rv;
90 Const *k;
91 if (qc_is_var_eq(c, &lv, &rv)) {
92 (*pairs)[(*npairs)++] = colref_get(cols, ncols, lv->varno, lv->varattno);
93 (*pairs)[(*npairs)++] = colref_get(cols, ncols, rv->varno, rv->varattno);
94 } else if (qc_is_var_const_eq(c, &lv, &k)) {
95 if (const_cols == NULL || k->consttype != lv->vartype)
96 return false; /* caller cannot pin, or a cross-type literal */
97 const_cols[*nconst] = colref_get(cols, ncols, lv->varno, lv->varattno);
98 const_vals[*nconst] = k;
99 (*nconst)++;
100 } else {
101 return false; /* not an equijoin or a value selection: decline */
102 }
103 }
104 return true;
105}
106
107/** @brief Append @p s as a JSON string literal (escaping @c " and @c \\‍). */
108static void append_json_string(StringInfo buf, const char *s)
109{
110 appendStringInfoChar(buf, '"');
111 for (; *s; s++) {
112 if (*s == '"' || *s == '\\') {
113 appendStringInfoChar(buf, '\\');
114 appendStringInfoChar(buf, *s);
115 } else if (*s == '\n') {
116 appendStringInfoString(buf, "\\n");
117 } else {
118 appendStringInfoChar(buf, *s);
119 }
120 }
121 appendStringInfoChar(buf, '"');
122}
123
124/** @brief Walker context: does any target-list Var reach a tracked atom? */
125typedef struct {
126 Index *rtis;
128 bool found;
129} ExistCtx;
130
131static bool exist_walker(Node *node, void *vctx)
132{
133 ExistCtx *ctx = (ExistCtx *) vctx;
134 if (node == NULL)
135 return false;
136 if (IsA(node, Var)) {
137 Var *v = (Var *) node;
138 int i;
139 for (i = 0; i < ctx->natoms; i++)
140 if (v->varno == ctx->rtis[i]) {
141 ctx->found = true;
142 return true;
143 }
144 return false;
145 }
146 return expression_tree_walker(node, exist_walker, vctx);
147}
148
149/**
150 * @brief Recursively collect tracked base-relation atoms and the ON quals
151 * of a jointree item (a @c RangeTblRef or an inner @c JoinExpr).
152 *
153 * Flattens @c a @c JOIN @c b @c ON ... (and nested inner joins) to the
154 * same atom set + equality quals as the comma/@c WHERE form. Declines
155 * (returns @c false) on an outer join, a non-relation RTE, an untracked
156 * relation, or any other item shape.
157 */
158static bool collect_jointree(Node *n, Query *q, Index *atom_rti,
159 Oid *atom_relid, int *natoms, List **qual_list)
160{
161 if (n == NULL)
162 return true;
163 if (IsA(n, RangeTblRef)) {
164 Index rti = ((RangeTblRef *) n)->rtindex;
165 RangeTblEntry *rte = rt_fetch(rti, q->rtable);
166 if (rte->rtekind != RTE_RELATION)
167 return false;
168 if (get_attnum(rte->relid, PROVSQL_COLUMN_NAME) == InvalidAttrNumber)
169 return false;
170 atom_rti[*natoms] = rti;
171 atom_relid[*natoms] = rte->relid;
172 (*natoms)++;
173 return true;
174 }
175 if (IsA(n, JoinExpr)) {
176 JoinExpr *je = (JoinExpr *) n;
177 if (je->jointype != JOIN_INNER)
178 return false; /* outer joins change the semantics: decline */
179 if (!collect_jointree(je->larg, q, atom_rti, atom_relid, natoms, qual_list))
180 return false;
181 if (!collect_jointree(je->rarg, q, atom_rti, atom_relid, natoms, qual_list))
182 return false;
183 if (je->quals != NULL)
184 *qual_list = lappend(*qual_list, je->quals); /* the ON condition */
185 return true;
186 }
187 return false; /* FromExpr / subquery / other: outside scope */
188}
189
190/**
191 * @brief A merge of relations across the disjuncts of a UCQ: distinct
192 * relation OIDs (first-seen order) with their element column-name
193 * lists. The atoms of every disjunct reference relations by their
194 * index here, so a relation shared between disjuncts (a self-join
195 * across a UNION) is gathered once.
196 */
197typedef struct {
198 Oid *oids;
199 List **colnames; /* element column names (raw), per relation */
200 int n;
201 int cap;
202} RelMerge;
203
204/** @brief Find-or-add a relation; returns its global index. */
205static int relmerge_get(RelMerge *m, Oid oid, List *colnames)
206{
207 int i;
208 for (i = 0; i < m->n; i++)
209 if (m->oids[i] == oid)
210 return i;
211 m->oids[m->n] = oid;
212 m->colnames[m->n] = colnames;
213 return m->n++;
214}
215
216/**
217 * @brief Does @p cq expose no tracked-atom variable in its target list
218 * (i.e. it computes the Boolean existence of its CQ)?
219 *
220 * @p atom_rti / @p natoms describe the tracked range-table entries of
221 * @p cq (as collected by @c collect_jointree).
222 */
223static bool cq_is_existential(Query *cq, Index *atom_rti, int natoms)
224{
225 ListCell *lc;
226 ExistCtx ec;
227 ec.rtis = atom_rti;
228 ec.natoms = natoms;
229 ec.found = false;
230 foreach (lc, cq->targetList) {
231 TargetEntry *te = (TargetEntry *) lfirst(lc);
232 if (te->resjunk)
233 continue;
234 if (te->resname != NULL && strcmp(te->resname, PROVSQL_COLUMN_NAME) == 0)
235 continue; /* the auto-added provenance column of a processed arm */
236 exist_walker((Node *) te->expr, &ec);
237 }
238 return !ec.found;
239}
240
241/**
242 * @brief Extract one conjunctive query (a flat join over tracked base
243 * relations) as a UCQ disjunct, appending its
244 * @c {"n_vars":N,"atoms":[...]} object to @p dbuf and registering
245 * its relations in @p m (so atom @c "rel" indices are global).
246 *
247 * The per-disjunct variable space is local (variables 0..N-1 of this
248 * disjunct); the element dictionary that binds them to data values is
249 * shared across the whole UCQ by the gather.
250 *
251 * @p head_resno (length @p n_heads) gives the arm output-column positions
252 * (1-based @c resno, aligned across the UNION's arms) of the per-answer
253 * head variables, in head order. Each is numbered CANONICALLY -- head
254 * @e h becomes query variable @e h in every arm -- so a single
255 * @c head_vars = [0,1,…] pins the head in every disjunct (the SQL
256 * @c ucq_joint_provenance_answer uses one head-variable index across all
257 * disjuncts). With @p n_heads == 0 the arm must be Boolean-existential.
258 * Returns @c false to decline.
259 */
260static bool emit_cq_disjunct(const constants_t *constants, Query *cq,
261 RelMerge *m, StringInfo dbuf,
262 const int *head_resno, int n_heads)
263{
264 ListCell *lc;
265 int natoms = 0;
266 Index *atom_rti;
267 Oid *atom_relid;
268 ColRef *cols;
269 int ncols = 0;
270 int *pairs;
271 int npairs = 0;
272 int maxcols;
273 List *qual_list = NIL;
274 int *root_var;
275 int nvars = 0;
276 int a, i, h;
277
278 (void) constants;
279
280 /* Same shape gate as the top-level single-CQ path, per arm. */
281 if (cq->commandType != CMD_SELECT || cq->setOperations != NULL ||
282 cq->jointree == NULL || cq->hasAggs || cq->havingQual != NULL ||
283 cq->hasWindowFuncs || cq->hasSubLinks || cq->cteList != NIL ||
284 cq->jointree->fromlist == NIL)
285 return false;
286
287 atom_rti = palloc(list_length(cq->rtable) * sizeof(Index));
288 atom_relid = palloc(list_length(cq->rtable) * sizeof(Oid));
289 foreach (lc, cq->jointree->fromlist)
290 if (!collect_jointree((Node *) lfirst(lc), cq, atom_rti, atom_relid,
291 &natoms, &qual_list))
292 return false;
293 if (natoms == 0)
294 return false;
295 if (cq->jointree->quals != NULL)
296 qual_list = lappend(qual_list, cq->jointree->quals);
297
298 /* A Boolean disjunct must expose no tracked head; a per-answer disjunct
299 * exposes exactly the head columns (resolved below). */
300 if (n_heads == 0 && !cq_is_existential(cq, atom_rti, natoms))
301 return false;
302
303 maxcols = 0;
304 for (a = 0; a < natoms; a++) {
305 RangeTblEntry *rte = rt_fetch(atom_rti[a], cq->rtable);
306 maxcols += list_length(rte->eref->colnames);
307 }
308 cols = palloc(maxcols * sizeof(ColRef));
309 pairs = palloc(2 * (list_length(cq->rtable) * 8 + 64) * sizeof(int));
310
311 for (a = 0; a < natoms; a++) {
312 RangeTblEntry *rte = rt_fetch(atom_rti[a], cq->rtable);
313 AttrNumber attno = 0;
314 ListCell *c;
315 foreach (c, rte->eref->colnames) {
316 char *name = strVal(lfirst(c));
317 attno++;
318 if (name[0] == '\0' || strcmp(name, PROVSQL_COLUMN_NAME) == 0)
319 continue;
320 (void) colref_get(cols, &ncols, atom_rti[a], attno);
321 }
322 }
323
324 foreach (lc, qual_list)
325 if (!collect_equalities((Node *) lfirst(lc), cols, &ncols, &pairs, &npairs,
326 NULL, NULL, NULL)) /* a UNION arm cannot pin a const */
327 return false;
328 for (i = 0; i < npairs; i += 2)
329 uf_union(cols, pairs[i], pairs[i + 1]);
330
331 root_var = palloc(ncols * sizeof(int));
332 for (i = 0; i < ncols; i++)
333 root_var[i] = -1;
334
335 /* Canonical head numbering: head h -> variable h. Resolve each head
336 * output column to its base Var, then its equivalence class. */
337 for (h = 0; h < n_heads; h++) {
338 TargetEntry *te = NULL;
339 ListCell *tc;
340 Node *e;
341 Var *hv;
342 bool tracked = false;
343 int node, root;
344 foreach (tc, cq->targetList) {
345 TargetEntry *t = (TargetEntry *) lfirst(tc);
346 if (t->resno == head_resno[h]) { te = t; break; }
347 }
348 if (te == NULL || te->resjunk)
349 return false;
350 e = (Node *) te->expr;
351 while (IsA(e, RelabelType))
352 e = (Node *) ((RelabelType *) e)->arg;
353 if (!IsA(e, Var) || ((Var *) e)->varlevelsup != 0)
354 return false; /* a head must be a bare base column to be pinnable */
355 hv = (Var *) e;
356 for (a = 0; a < natoms; a++)
357 if (atom_rti[a] == hv->varno) { tracked = true; break; }
358 if (!tracked)
359 return false;
360 node = colref_get(cols, &ncols, hv->varno, hv->varattno);
361 root = uf_find(cols, node);
362 if (root_var[root] >= 0 && root_var[root] != h)
363 return false; /* two heads collapse to one variable: decline */
364 root_var[root] = h;
365 }
366 nvars = n_heads;
367 for (i = 0; i < ncols; i++) {
368 int r = uf_find(cols, i);
369 if (root_var[r] < 0)
370 root_var[r] = nvars++;
371 }
372
373 appendStringInfo(dbuf, "{\"n_vars\":%d,\"atoms\":[", nvars);
374 for (a = 0; a < natoms; a++) {
375 RangeTblEntry *rte = rt_fetch(atom_rti[a], cq->rtable);
376 AttrNumber attno = 0;
377 ListCell *c;
378 bool firstv = true;
379 List *elem_colnames = NIL;
380 int gidx;
381 /* Element column names of this atom's relation (raw, in order). */
382 foreach (c, rte->eref->colnames) {
383 char *name = strVal(lfirst(c));
384 if (name[0] == '\0' || strcmp(name, PROVSQL_COLUMN_NAME) == 0)
385 continue;
386 elem_colnames = lappend(elem_colnames, makeString(name));
387 }
388 gidx = relmerge_get(m, atom_relid[a], elem_colnames);
389 if (a) appendStringInfoChar(dbuf, ',');
390 appendStringInfo(dbuf, "{\"rel\":%d,\"vars\":[", gidx);
391 attno = 0;
392 foreach (c, rte->eref->colnames) {
393 char *name = strVal(lfirst(c));
394 attno++;
395 if (name[0] == '\0' || strcmp(name, PROVSQL_COLUMN_NAME) == 0)
396 continue;
397 {
398 int node = colref_get(cols, &ncols, atom_rti[a], attno);
399 int var = root_var[uf_find(cols, node)];
400 if (!firstv) appendStringInfoChar(dbuf, ',');
401 appendStringInfo(dbuf, "%d", var);
402 firstv = false;
403 }
404 }
405 appendStringInfoString(dbuf, "]}");
406 }
407 appendStringInfoString(dbuf, "]}");
408 return true;
409}
410
411/**
412 * @brief Collect the leaf arm subqueries of a UNION set-operation tree.
413 *
414 * Walks @p node (a @c SetOperationStmt tree); every internal node must be
415 * a @c SETOP_UNION (UNION or UNION ALL -- both compute the same Boolean
416 * existence), and every leaf a @c RangeTblRef to an @c RTE_SUBQUERY arm.
417 * Appends each arm @c Query* to @p arms. Returns @c false to decline
418 * (INTERSECT / EXCEPT, or an unexpected leaf).
419 */
420static bool collect_union_arms(Node *node, Query *parent, List **arms)
421{
422 if (node == NULL)
423 return false;
424 if (IsA(node, SetOperationStmt)) {
425 SetOperationStmt *so = (SetOperationStmt *) node;
426 if (so->op != SETOP_UNION)
427 return false;
428 return collect_union_arms(so->larg, parent, arms) &&
429 collect_union_arms(so->rarg, parent, arms);
430 }
431 if (IsA(node, RangeTblRef)) {
432 Index rti = ((RangeTblRef *) node)->rtindex;
433 RangeTblEntry *rte = rt_fetch(rti, parent->rtable);
434 if (rte->rtekind != RTE_SUBQUERY || rte->subquery == NULL)
435 return false;
436 *arms = lappend(*arms, rte->subquery);
437 return true;
438 }
439 return false;
440}
441
442/**
443 * @brief Classify a UNION output column (1-based @p resno): is it a
444 * per-answer head or a constant projection?
445 *
446 * @return @c 1 if every arm exposes a bare @c Var over a tracked base
447 * relation there (a real head, pinnable), @c 0 if every arm
448 * exposes a constant / non-tracked expression there (a Boolean
449 * projection like @c SELECT @c 1, to be ignored), @c -1 on a mixed
450 * or malformed column (the recogniser then declines).
451 */
452static int union_position_kind(Query *unionq, int resno)
453{
454 List *arms = NIL;
455 ListCell *lc;
456 int kind = -2; /* unset */
457
458 if (!collect_union_arms(unionq->setOperations, unionq, &arms))
459 return -1;
460 foreach (lc, arms) {
461 Query *arm = (Query *) lfirst(lc);
462 TargetEntry *te = NULL;
463 ListCell *tc;
464 Node *e;
465 int k;
466 foreach (tc, arm->targetList) {
467 TargetEntry *t = (TargetEntry *) lfirst(tc);
468 if (t->resno == resno) { te = t; break; }
469 }
470 if (te == NULL)
471 return -1;
472 e = (Node *) te->expr;
473 while (IsA(e, RelabelType))
474 e = (Node *) ((RelabelType *) e)->arg;
475 k = 0;
476 if (IsA(e, Var) && ((Var *) e)->varlevelsup == 0) {
477 RangeTblEntry *rte = rt_fetch(((Var *) e)->varno, arm->rtable);
478 if (rte->rtekind == RTE_RELATION &&
479 get_attnum(rte->relid, PROVSQL_COLUMN_NAME) != InvalidAttrNumber)
480 k = 1;
481 }
482 if (kind == -2)
483 kind = k;
484 else if (kind != k)
485 return -1; /* tracked in one arm, constant in another: decline */
486 }
487 return kind < 0 ? 0 : kind;
488}
489
490/**
491 * @brief Build the descriptor for a UNION of conjunctive queries (a UCQ
492 * with more than one disjunct): one disjunct per arm, relations
493 * merged across arms. @p head_resno (length @p n_heads) gives the
494 * per-answer head output positions (canonically numbered in every
495 * arm); @p n_heads == 0 is the Boolean existence case. Returns
496 * @c NULL to decline.
497 */
498static char *build_union_descriptor(const constants_t *constants, Query *unionq,
499 const int *head_resno, int n_heads)
500{
501 List *arms = NIL;
502 ListCell *lc;
503 RelMerge m;
504 StringInfoData dbuf;
505 StringInfoData buf;
506 int i;
507 bool first = true;
508
509 if (!collect_union_arms(unionq->setOperations, unionq, &arms))
510 return NULL;
511 if (list_length(arms) < 2)
512 return NULL;
513
514 m.cap = 0;
515 foreach (lc, arms)
516 m.cap += list_length(((Query *) lfirst(lc))->rtable);
517 m.oids = palloc(m.cap * sizeof(Oid));
518 m.colnames = palloc(m.cap * sizeof(List *));
519 m.n = 0;
520
521 initStringInfo(&dbuf);
522 foreach (lc, arms) {
523 if (!first) appendStringInfoChar(&dbuf, ',');
524 if (!emit_cq_disjunct(constants, (Query *) lfirst(lc), &m, &dbuf,
525 head_resno, n_heads))
526 return NULL;
527 first = false;
528 }
529
530 initStringInfo(&buf);
531 appendStringInfo(&buf, "{\"disjuncts\":[%s],\"relations\":[", dbuf.data);
532 for (i = 0; i < m.n; i++) {
533 char *nsp = get_namespace_name(get_rel_namespace(m.oids[i]));
534 char *rel = get_rel_name(m.oids[i]);
535 StringInfoData qn;
536 initStringInfo(&qn);
537 appendStringInfo(&qn, "%s.%s", quote_identifier(nsp), quote_identifier(rel));
538 if (i) appendStringInfoChar(&buf, ',');
539 append_json_string(&buf, qn.data);
540 pfree(qn.data);
541 }
542 appendStringInfoString(&buf, "],\"elem_cols\":[");
543 for (i = 0; i < m.n; i++) {
544 ListCell *c;
545 bool firstc = true;
546 if (i) appendStringInfoChar(&buf, ',');
547 appendStringInfoChar(&buf, '[');
548 foreach (c, m.colnames[i]) {
549 if (!firstc) appendStringInfoChar(&buf, ',');
550 append_json_string(&buf, strVal(lfirst(c)));
551 firstc = false;
552 }
553 appendStringInfoChar(&buf, ']');
554 }
555 appendStringInfoString(&buf, "]}");
556 return buf.data;
557}
558
559char *provsql_joint_width_descriptor(const constants_t *constants, Query *q,
560 bool *all_existential,
561 List **head_var_idx, List **head_exprs)
562{
563 ListCell *lc;
564 int natoms = 0;
565 Index *atom_rti; /* per atom: range-table index */
566 Oid *atom_relid; /* per atom: relation OID */
567 char **atom_filter; /* per atom: deparsed single-relation selection, or NULL */
568 int *atom_scanidx; /* per atom: index into the distinct scans */
569 Oid *scan_relid; /* per scan: base relation OID */
570 char **scan_filter; /* per scan: filter text (NULL = unfiltered scan) */
571 int nscan = 0;
572 ColRef *cols;
573 int ncols = 0;
574 int *pairs;
575 int npairs = 0;
576 List **per_atom; /* qc_split_quals output, indexed by rtindex-1 */
577 Node *residual = NULL;
578 int maxcols;
579 List *qual_list = NIL;
580 StringInfoData buf;
581 int i, a;
582
583 if (all_existential)
584 *all_existential = false;
585 if (head_var_idx)
586 *head_var_idx = NIL;
587 if (head_exprs)
588 *head_exprs = NIL;
589
590 /* UNION of conjunctive queries: a genuine multi-disjunct UCQ. Reached
591 * when this is the body of an aggregated subquery (the recursion from the
592 * subquery branch below); a top-level UNION is combined at SR_PLUS, where
593 * the recogniser is not invoked. Boolean existence only (per-answer
594 * UNION heads decline inside emit_cq_disjunct). */
595 if (q->setOperations != NULL) {
596 char *desc = build_union_descriptor(constants, q, NULL, 0);
597 if (desc != NULL && all_existential)
598 *all_existential = true;
599 return desc;
600 }
601
602 /* Shape gate: a plain SELECT, single CQ (no aggregates), with a jointree.
603 * DISTINCT / GROUP BY are allowed (they are how the existence / per-answer
604 * questions are written). */
605 if (q->commandType != CMD_SELECT ||
606 q->jointree == NULL || q->hasAggs ||
607 q->havingQual != NULL ||
608 q->hasWindowFuncs || q->hasSubLinks || q->cteList != NIL)
609 return NULL;
610
611 /* Subquery form: the sole FROM item is a subquery whose body is the UCQ
612 * (SELECT head, probability_evaluate(provenance())
613 * FROM (SELECT base.head FROM ... WHERE ...) s GROUP BY head, and the
614 * DISTINCT variant). Recurse into the subquery for the descriptor and
615 * its output heads, then map the outer output columns through the
616 * subquery target list (the outer Var becomes the per-group head value). */
617 if (list_length(q->jointree->fromlist) == 1 && q->jointree->quals == NULL) {
618 Node *only = (Node *) linitial(q->jointree->fromlist);
619 if (IsA(only, RangeTblRef)) {
620 Index subrti = ((RangeTblRef *) only)->rtindex;
621 RangeTblEntry *subrte = rt_fetch(subrti, q->rtable);
622 if (subrte->rtekind == RTE_SUBQUERY &&
623 subrte->subquery->setOperations != NULL) {
624 /* UNION subquery: a multi-disjunct UCQ. The per-answer heads are the
625 * GROUP BY keys (Vars over the subquery); the SELECT list is
626 * arbitrary, and the provenance() aggregate is not a head. With no
627 * GROUP BY it is the Boolean existence. A constant union column
628 * (SELECT 1) is ignored. */
629 int *head_resno = palloc((list_length(q->targetList) +
630 list_length(q->groupClause)) * sizeof(int));
631 List *hexprs = NIL;
632 int n_heads = 0;
633 char *udesc;
634 List *cand = NIL; /* candidate head target-entries */
635 if (q->groupClause != NIL) {
636 ListCell *gl, *tc;
637 foreach (gl, q->groupClause) {
638 SortGroupClause *sgc = (SortGroupClause *) lfirst(gl);
639 foreach (tc, q->targetList) {
640 TargetEntry *t = (TargetEntry *) lfirst(tc);
641 if (t->ressortgroupref == sgc->tleSortGroupRef) {
642 cand = lappend(cand, t); break;
643 }
644 }
645 }
646 } else {
647 foreach (lc, q->targetList) {
648 TargetEntry *t = (TargetEntry *) lfirst(lc);
649 if (!t->resjunk)
650 cand = lappend(cand, t);
651 }
652 }
653 foreach (lc, cand) {
654 TargetEntry *te = (TargetEntry *) lfirst(lc);
655 Node *e;
656 Var *v;
657 TargetEntry *ste = NULL;
658 ListCell *sc;
659 e = (Node *) te->expr;
660 while (IsA(e, RelabelType))
661 e = (Node *) ((RelabelType *) e)->arg;
662 if (!(IsA(e, Var) && ((Var *) e)->varno == subrti &&
663 ((Var *) e)->varlevelsup == 0)) {
664 if (q->groupClause != NIL)
665 return NULL; /* an un-pinnable group key: decline */
666 continue; /* the provenance() aggregate or a constant */
667 }
668 v = (Var *) e;
669 foreach (sc, subrte->subquery->targetList) {
670 TargetEntry *t = (TargetEntry *) lfirst(sc);
671 if (t->resno == v->varattno) { ste = t; break; }
672 }
673 if (ste != NULL && ste->resname != NULL &&
674 strcmp(ste->resname, PROVSQL_COLUMN_NAME) == 0)
675 continue; /* the subquery's provenance column, not a head */
676 {
677 int kind = union_position_kind(subrte->subquery, v->varattno);
678 if (kind == -1)
679 return NULL; /* malformed / mixed head column */
680 if (kind == 0)
681 continue; /* a constant projection (SELECT 1 d): ignore */
682 }
683 {
684 bool dup = false; int z;
685 for (z = 0; z < n_heads; z++)
686 if (head_resno[z] == v->varattno) { dup = true; break; }
687 if (dup) continue;
688 }
689 head_resno[n_heads] = v->varattno;
690 hexprs = lappend(hexprs, e);
691 n_heads++;
692 }
693 udesc = build_union_descriptor(constants, subrte->subquery,
694 head_resno, n_heads);
695 if (udesc == NULL)
696 return NULL;
697 if (n_heads == 0) {
698 if (all_existential)
699 *all_existential = true;
700 } else if (head_var_idx != NULL) {
701 int h;
702 *head_var_idx = NIL;
703 for (h = 0; h < n_heads; h++)
704 *head_var_idx = lappend_int(*head_var_idx, h); /* canonical */
705 if (head_exprs != NULL)
706 *head_exprs = hexprs;
707 }
708 return udesc;
709 }
710 if (subrte->rtekind == RTE_SUBQUERY) {
711 List *sub_idx = NIL, *sub_exprs = NIL;
712 bool sub_all = false;
713 char *desc = provsql_joint_width_descriptor(constants, subrte->subquery,
714 &sub_all, &sub_idx,
715 &sub_exprs);
716 if (desc == NULL || sub_idx == NIL)
717 return NULL;
718 /* If the inner CQ carries a constant pin (a Const, not a Var, in its
719 * head list), it cannot be remapped through the outer target list:
720 * decline (the flat form handles constants directly). */
721 {
722 ListCell *xc;
723 foreach (xc, sub_exprs)
724 if (!IsA((Node *) lfirst(xc), Var))
725 return NULL;
726 }
727 if (head_var_idx != NULL) {
728 bool clean = true;
729 *head_var_idx = NIL;
730 if (head_exprs != NULL)
731 *head_exprs = NIL;
732 foreach (lc, q->targetList) {
733 TargetEntry *te = (TargetEntry *) lfirst(lc);
734 Node *e;
735 AttrNumber outcol;
736 TargetEntry *ste = NULL;
737 ListCell *sc, *li, *lx;
738 Node *se;
739 int hv = -1;
740 if (te->resjunk)
741 continue;
742 e = (Node *) te->expr;
743 while (IsA(e, RelabelType))
744 e = (Node *) ((RelabelType *) e)->arg;
745 if (!(IsA(e, Var) && ((Var *) e)->varno == subrti &&
746 ((Var *) e)->varlevelsup == 0))
747 continue; /* the aggregate / a constant: not a head */
748 outcol = ((Var *) e)->varattno;
749 foreach (sc, subrte->subquery->targetList) {
750 TargetEntry *t = (TargetEntry *) lfirst(sc);
751 if (t->resno == outcol) { ste = t; break; }
752 }
753 if (ste == NULL || ste->resjunk) { clean = false; break; }
754 se = (Node *) ste->expr;
755 while (IsA(se, RelabelType))
756 se = (Node *) ((RelabelType *) se)->arg;
757 if (!IsA(se, Var)) { clean = false; break; }
758 forboth (li, sub_idx, lx, sub_exprs) {
759 Var *sv = (Var *) lfirst(lx);
760 if (sv->varno == ((Var *) se)->varno &&
761 sv->varattno == ((Var *) se)->varattno) {
762 hv = lfirst_int(li);
763 break;
764 }
765 }
766 if (hv < 0) { clean = false; break; }
767 if (!list_member_int(*head_var_idx, hv)) {
768 *head_var_idx = lappend_int(*head_var_idx, hv);
769 if (head_exprs != NULL)
770 *head_exprs = lappend(*head_exprs, e);
771 }
772 }
773 if (!clean) {
774 *head_var_idx = NIL;
775 if (head_exprs != NULL)
776 *head_exprs = NIL;
777 }
778 }
779 if (all_existential)
780 *all_existential = false;
781 return desc;
782 }
783 }
784 }
785
786 /* The FROM list must be a flat list of RangeTblRefs to tracked base
787 * relations. */
788 if (q->jointree->fromlist == NIL)
789 return NULL;
790 {
791 int nrte = list_length(q->rtable);
792 atom_rti = palloc(nrte * sizeof(Index));
793 atom_relid = palloc(nrte * sizeof(Oid));
794 atom_filter = palloc(nrte * sizeof(char *));
795 atom_scanidx = palloc(nrte * sizeof(int));
796 scan_relid = palloc(nrte * sizeof(Oid));
797 scan_filter = palloc(nrte * sizeof(char *));
798 per_atom = palloc0(nrte * sizeof(List *));
799 }
800 foreach (lc, q->jointree->fromlist)
801 if (!collect_jointree((Node *) lfirst(lc), q, atom_rti, atom_relid,
802 &natoms, &qual_list))
803 return NULL;
804 if (natoms == 0)
805 return NULL;
806 /* The top-level WHERE conjoins with every collected ON condition. */
807 if (q->jointree->quals != NULL)
808 qual_list = lappend(qual_list, q->jointree->quals);
809
810 /* Split the conjunction into single-relation selections (a pre-filter,
811 * pushed into that atom's relation scan) and the cross-relation residual
812 * (which must be join equalities). This is the shared qc_split_quals the
813 * safe-query rewrite uses; here the lifted selections are deparsed back
814 * to SQL and the gather scans FROM <relation> WHERE <selection>. */
815 qc_split_quals((Node *) qual_list, list_length(q->rtable), per_atom,
816 &residual);
817
818 /* Deparse each atom's pushed selections, then key the scans by
819 * (relation, filter): atoms over the same relation with the same filter
820 * (notably the unfiltered self-join) share one scan; a self-join with
821 * disjoint constant filters gets two filtered scans. */
822 for (a = 0; a < natoms; a++) {
823 List *sel = per_atom[atom_rti[a] - 1];
824 int found = -1;
825 atom_filter[a] = NULL;
826 if (sel != NIL) {
827 Node *conj = (Node *) make_ands_explicit(sel);
828 List *dpctx;
829 conj = copyObject(conj);
830 ChangeVarNodes(conj, atom_rti[a], 1, 0); /* the scan sees the rel as varno 1 */
831 dpctx = deparse_context_for(get_rel_name(atom_relid[a]), atom_relid[a]);
832 atom_filter[a] = deparse_expression(conj, dpctx, false, false);
833 }
834 for (i = 0; i < nscan; i++)
835 if (scan_relid[i] == atom_relid[a] &&
836 ((scan_filter[i] == NULL && atom_filter[a] == NULL) ||
837 (scan_filter[i] != NULL && atom_filter[a] != NULL &&
838 strcmp(scan_filter[i], atom_filter[a]) == 0))) { found = i; break; }
839 if (found < 0) {
840 scan_relid[nscan] = atom_relid[a];
841 scan_filter[nscan] = atom_filter[a];
842 found = nscan++;
843 }
844 atom_scanidx[a] = found;
845 }
846
847 /* Upper bound on column nodes: every atom's every column. */
848 maxcols = 0;
849 for (a = 0; a < natoms; a++) {
850 RangeTblEntry *rte = rt_fetch(atom_rti[a], q->rtable);
851 maxcols += list_length(rte->eref->colnames);
852 }
853 cols = palloc(maxcols * sizeof(ColRef));
854 pairs = palloc(2 * (list_length(q->rtable) * 8 + 64) * sizeof(int));
855
856 /* Seed a column node for every element column of every atom (so even
857 * un-joined columns become singleton variables). */
858 for (a = 0; a < natoms; a++) {
859 RangeTblEntry *rte = rt_fetch(atom_rti[a], q->rtable);
860 AttrNumber attno = 0;
861 ListCell *c;
862 foreach (c, rte->eref->colnames) {
863 char *name = strVal(lfirst(c));
864 attno++;
865 if (name[0] == '\0' || strcmp(name, PROVSQL_COLUMN_NAME) == 0)
866 continue;
867 (void) colref_get(cols, &ncols, atom_rti[a], attno);
868 }
869 }
870
871 /* The cross-relation residual must be pure join equalities: each conjunct
872 * a Var=Var equijoin (-> union-find). Anything else (a non-equijoin join
873 * predicate, an OR / inequality spanning two relations, a volatile
874 * selection) is outside the fragment -- decline. */
875 {
876 List *rconj = NIL;
877 ListCell *rc;
878 qc_flatten_and(residual, &rconj);
879 foreach (rc, rconj) {
880 Var *lv, *rv;
881 if (!qc_is_var_eq((Expr *) lfirst(rc), &lv, &rv))
882 return NULL;
883 pairs[npairs++] = colref_get(cols, &ncols, lv->varno, lv->varattno);
884 pairs[npairs++] = colref_get(cols, &ncols, rv->varno, rv->varattno);
885 }
886 }
887 for (i = 0; i < npairs; i += 2)
888 uf_union(cols, pairs[i], pairs[i + 1]);
889
890 /* Assign a query-variable index to every equivalence class (by class
891 * root, first-seen order). */
892 {
893 int *root_var = palloc(ncols * sizeof(int));
894 int nvars = 0;
895 for (i = 0; i < ncols; i++)
896 root_var[i] = -1;
897 for (i = 0; i < ncols; i++) {
898 int r = uf_find(cols, i);
899 if (root_var[r] < 0)
900 root_var[r] = nvars++;
901 }
902
903 /* Emit the descriptor JSON. */
904 initStringInfo(&buf);
905 appendStringInfo(&buf, "{\"disjuncts\":[{\"n_vars\":%d,\"atoms\":[", nvars);
906 for (a = 0; a < natoms; a++) {
907 RangeTblEntry *rte = rt_fetch(atom_rti[a], q->rtable);
908 AttrNumber attno = 0;
909 ListCell *c;
910 bool firstv = true;
911 if (a) appendStringInfoChar(&buf, ',');
912 appendStringInfo(&buf, "{\"rel\":%d,\"vars\":[", atom_scanidx[a]);
913 foreach (c, rte->eref->colnames) {
914 char *name = strVal(lfirst(c));
915 attno++;
916 if (name[0] == '\0' || strcmp(name, PROVSQL_COLUMN_NAME) == 0)
917 continue;
918 {
919 int node = colref_get(cols, &ncols, atom_rti[a], attno);
920 int var = root_var[uf_find(cols, node)];
921 if (!firstv) appendStringInfoChar(&buf, ',');
922 appendStringInfo(&buf, "%d", var);
923 firstv = false;
924 }
925 }
926 appendStringInfoString(&buf, "]}");
927 }
928 /* One entry per SCAN: the qualified relation name, its pushed selection
929 * (rel_where; "" when unfiltered), and its element columns. */
930 appendStringInfoString(&buf, "]}],\"relations\":[");
931 for (i = 0; i < nscan; i++) {
932 char *nsp = get_namespace_name(get_rel_namespace(scan_relid[i]));
933 char *rel = get_rel_name(scan_relid[i]);
934 StringInfoData qn;
935 initStringInfo(&qn);
936 appendStringInfo(&qn, "%s.%s", quote_identifier(nsp),
937 quote_identifier(rel));
938 if (i) appendStringInfoChar(&buf, ',');
939 append_json_string(&buf, qn.data); /* gather uses it raw (FROM %s) */
940 pfree(qn.data);
941 }
942 appendStringInfoString(&buf, "],\"rel_where\":[");
943 for (i = 0; i < nscan; i++) {
944 if (i) appendStringInfoChar(&buf, ',');
945 append_json_string(&buf, scan_filter[i] ? scan_filter[i] : "");
946 }
947 appendStringInfoString(&buf, "],\"elem_cols\":[");
948 for (i = 0; i < nscan; i++) {
949 /* Element columns of scan i: from the first atom on it (all atoms
950 * over the same relation share its columns). */
951 RangeTblEntry *rte = NULL;
952 AttrNumber attno = 0;
953 ListCell *c;
954 bool firstc = true;
955 for (a = 0; a < natoms; a++)
956 if (atom_scanidx[a] == i) { rte = rt_fetch(atom_rti[a], q->rtable); break; }
957 if (i) appendStringInfoChar(&buf, ',');
958 appendStringInfoChar(&buf, '[');
959 foreach (c, rte->eref->colnames) {
960 char *name = strVal(lfirst(c));
961 attno++;
962 if (name[0] == '\0' || strcmp(name, PROVSQL_COLUMN_NAME) == 0)
963 continue;
964 if (!firstc) appendStringInfoChar(&buf, ',');
965 append_json_string(&buf, name); /* raw name; gather quotes with %I */
966 firstc = false;
967 }
968 appendStringInfoChar(&buf, ']');
969 }
970 appendStringInfoString(&buf, "]}");
971
972 /* Heads = the variables the query pins per answer: the GROUP BY keys,
973 * each a bare Var over a tracked atom. The SELECT list is arbitrary (a
974 * function of the keys), so what is displayed does not constrain
975 * recognition; constant selections are not heads (they are pre-filters
976 * lifted into the relation scans above). No pin at all is the Boolean
977 * existence; an un-pinnable group key (an expression) declines. Without
978 * a GROUP BY, the exposed bare tracked Vars are the heads (the
979 * un-grouped form). */
980 {
981 bool clean = true;
982 List *hv = NIL, *hx = NIL;
983 if (q->groupClause != NIL) {
984 ListCell *gl;
985 foreach (gl, q->groupClause) {
986 SortGroupClause *sgc = (SortGroupClause *) lfirst(gl);
987 TargetEntry *gte = NULL;
988 ListCell *tc;
989 Node *e;
990 Var *vv;
991 ExistCtx ec;
992 int node, var;
993 foreach (tc, q->targetList) {
994 TargetEntry *t = (TargetEntry *) lfirst(tc);
995 if (t->ressortgroupref == sgc->tleSortGroupRef) { gte = t; break; }
996 }
997 if (gte == NULL) { clean = false; break; }
998 e = (Node *) gte->expr;
999 while (IsA(e, RelabelType))
1000 e = (Node *) ((RelabelType *) e)->arg;
1001 /* A group key independent of the tracked data (a constant -- e.g. the
1002 * GROUP BY 1 that DISTINCT lowers to -- or an untracked column) is
1003 * one group, not a per-answer head: ignore it. The Boolean
1004 * existence keeps no head at all. */
1005 ec.rtis = atom_rti; ec.natoms = natoms; ec.found = false;
1006 exist_walker(e, &ec);
1007 if (!ec.found)
1008 continue;
1009 /* It partitions the tracked data: it must be a bare column to pin. */
1010 if (!(IsA(e, Var) && ((Var *) e)->varlevelsup == 0)) {
1011 clean = false; break; /* an expression over tracked cols: cannot pin */
1012 }
1013 vv = (Var *) e;
1014 node = colref_get(cols, &ncols, vv->varno, vv->varattno);
1015 var = root_var[uf_find(cols, node)];
1016 if (!list_member_int(hv, var)) {
1017 hv = lappend_int(hv, var);
1018 hx = lappend(hx, e);
1019 }
1020 }
1021 } else {
1022 foreach (lc, q->targetList) {
1023 TargetEntry *te = (TargetEntry *) lfirst(lc);
1024 Node *e;
1025 ExistCtx ec;
1026 if (te->resjunk)
1027 continue;
1028 if (te->resname != NULL &&
1029 strcmp(te->resname, PROVSQL_COLUMN_NAME) == 0)
1030 continue; /* auto-added provenance column of a processed subquery */
1031 e = (Node *) te->expr;
1032 while (IsA(e, RelabelType))
1033 e = (Node *) ((RelabelType *) e)->arg;
1034 ec.rtis = atom_rti; ec.natoms = natoms; ec.found = false;
1035 exist_walker(e, &ec);
1036 if (!ec.found)
1037 continue; /* a constant / untracked column: not a head */
1038 if (IsA(e, Var) && ((Var *) e)->varlevelsup == 0) {
1039 Var *vv = (Var *) e;
1040 int node = colref_get(cols, &ncols, vv->varno, vv->varattno);
1041 int var = root_var[uf_find(cols, node)];
1042 if (!list_member_int(hv, var)) {
1043 hv = lappend_int(hv, var);
1044 hx = lappend(hx, e);
1045 }
1046 continue;
1047 }
1048 clean = false; /* an exposed tracked column we cannot pin */
1049 break;
1050 }
1051 }
1052 if (head_var_idx != NULL) {
1053 *head_var_idx = clean ? hv : NIL;
1054 if (head_exprs != NULL)
1055 *head_exprs = clean ? hx : NIL;
1056 }
1057 /* Boolean existence iff there is no pin at all (and recognition is
1058 * clean); otherwise the per-answer / constant-pinned substitution. */
1059 if (all_existential)
1060 *all_existential = clean && (hv == NIL);
1061 }
1062 }
1063
1064 (void) constants;
1065 return buf.data;
1066}
static bool cq_is_existential(Query *cq, Index *atom_rti, int natoms)
Does cq expose no tracked-atom variable in its target list (i.e.
static void uf_union(ColRef *cols, int a, int b)
static char * build_union_descriptor(const constants_t *constants, Query *unionq, const int *head_resno, int n_heads)
Build the descriptor for a UNION of conjunctive queries (a UCQ with more than one disjunct): one disj...
static bool collect_jointree(Node *n, Query *q, Index *atom_rti, Oid *atom_relid, int *natoms, List **qual_list)
Recursively collect tracked base-relation atoms and the ON quals of a jointree item (a RangeTblRef or...
static bool exist_walker(Node *node, void *vctx)
static bool emit_cq_disjunct(const constants_t *constants, Query *cq, RelMerge *m, StringInfo dbuf, const int *head_resno, int n_heads)
Extract one conjunctive query (a flat join over tracked base relations) as a UCQ disjunct,...
static bool collect_equalities(Node *qual, ColRef *cols, int *ncols, int **pairs, int *npairs, int *const_cols, Const **const_vals, int *nconst)
Collect the equality structure of a WHERE / ON qual tree, over the shared qual_classify recognisers.
static int union_position_kind(Query *unionq, int resno)
Classify a UNION output column (1-based resno): is it a per-answer head or a constant projection?
static int uf_find(ColRef *cols, int i)
Union-find root with path halving.
static int colref_get(ColRef *cols, int *ncols, Index rtindex, AttrNumber attno)
Find-or-add a (rtindex, attno) column node; returns its index.
char * provsql_joint_width_descriptor(const constants_t *constants, Query *q, bool *all_existential, List **head_var_idx, List **head_exprs)
Build the joint-width descriptor for a recognised UCQ.
static int relmerge_get(RelMerge *m, Oid oid, List *colnames)
Find-or-add a relation; returns its global index.
static void append_json_string(StringInfo buf, const char *s)
Append s as a JSON string literal (escaping " and \‍).
static bool collect_union_arms(Node *node, Query *parent, List **arms)
Collect the leaf arm subqueries of a UNION set-operation tree.
Planner-time recognition of unsafe UCQs for the joint-width compiler.
Core types, constants, and utilities shared across ProvSQL.
#define PROVSQL_COLUMN_NAME
Canonical name of the per-row provenance column installed by add_provenance / repair_key.
void qc_flatten_and(Node *n, List **out)
Flatten the top-level AND tree of a qual into a flat list of leaf conjuncts (a bare List is an implic...
void qc_split_quals(Node *quals, int natoms, List **per_atom_out, Node **out_residual)
Partition top-level conjuncts into atom-local selections and the cross-atom residual.
bool qc_is_var_eq(Expr *qual, Var **l, Var **r)
Recognise a conjunct equating two base Vars (the canonical equality for the operand types,...
bool qc_is_var_const_eq(Expr *qual, Var **var, Const **konst)
Recognise a conjunct of shape Var=Const (either order, through RelabelType casts; non-NULL literal,...
Predicate-tree classification helpers shared by the query rewriters (the safe-query rewrite and the j...
A column reference (range-table index, attribute number).
AttrNumber attno
Walker context: does any target-list Var reach a tracked atom?
A merge of relations across the disjuncts of a UCQ: distinct relation OIDs (first-seen order) with th...
Structure to store the value of various constants.