ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
classify_query.c
Go to the documentation of this file.
1/**
2 * @file classify_query.c
3 * @brief Query-time TID / BID / OPAQUE classifier.
4 *
5 * Invoked by @c provsql_planner on the top-level @c Query when the
6 * @c provsql.classify_top_level GUC is on. Emits a @c NOTICE carrying
7 * the certified kind and the set of provenance-tracked base relations
8 * the query touches.
9 *
10 * Scope :
11 *
12 * - Single-source classification : a flat @c fromlist of
13 * @c RangeTblRefs, with no kind-altering features (@c SubLinks,
14 * modifying @c CTEs, @c cteList, @c DISTINCT, @c GROUP BY,
15 * @c HAVING, aggregates, window functions, set-returning
16 * functions in the target list, an @c ORDER @c BY ... @c LIMIT not
17 * marked @c plain()). Either zero or one
18 * provenance-tracked base relations are reached either directly
19 * (@c RTE_RELATION) or through any depth of subqueries
20 * (@c RTE_SUBQUERY -- view bodies after PG rewriting and inline
21 * @c FROM subqueries). The recorded kind of the sole tracked
22 * source (TID / BID / OPAQUE) is preserved verbatim ; zero
23 * tracked sources is trivially TID.
24 * - @c UNION @c ALL specialisation : a fully-@c UNION-ALL tree of
25 * leg subqueries each of which classifies independently as TID
26 * over a base-relid set that is disjoint from every other leg's.
27 * The union is then TID with the cumulative source list.
28 * Anything that doesn't fit (@c INTERSECT, @c EXCEPT,
29 * @c UNION @c DISTINCT, mixed kinds, overlapping leg sources)
30 * falls back to OPAQUE.
31 *
32 * Everything else is reported as OPAQUE. Independent-TID join
33 * inference, BID block-key preservation under projection and
34 * @c GROUP @c BY, and the per-relation base-ancestor registry the
35 * disjointness check consults all live in this same file ; see
36 * the helpers below.
37 */
38#include "postgres.h"
39
40#include "lib/stringinfo.h"
41#include "nodes/bitmapset.h"
42#include "nodes/nodeFuncs.h"
43#include "nodes/nodes.h"
44#include "nodes/parsenodes.h"
45#include "nodes/pg_list.h"
46#if PG_VERSION_NUM >= 120000
47#include "optimizer/optimizer.h" /* get_sortgroupclause_tle */
48#else
49#include "optimizer/tlist.h" /* get_sortgroupclause_tle (PG <12) */
50#endif
51#include "utils/builtins.h"
52#include "utils/lsyscache.h"
53
54#include "classify_query.h"
55#include "provsql_utils.h"
56
57/** @brief Backing storage for the @c provsql.classify_top_level GUC. */
59
60/** @brief Map a @c provsql_table_kind to its uppercase user-facing label. */
61static const char *kind_label(provsql_table_kind k) {
62 switch (k) {
63 case PROVSQL_TABLE_TID: return "TID";
64 case PROVSQL_TABLE_BID: return "BID";
65 case PROVSQL_TABLE_OPAQUE: return "OPAQUE";
66 }
67 return "?";
68}
69
70/**
71 * @brief Decide whether a @c jointree fromlist entry has a shape the
72 * classifier can certify : a plain @c RangeTblRef, or a
73 * @c JoinExpr with @c jointype @c == @c JOIN_INNER whose
74 * @c larg and @c rarg recursively satisfy the same predicate.
75 *
76 * ANSI-syntax inner joins (@c INNER @c JOIN, @c CROSS @c JOIN; PG
77 * normalises both to @c JOIN_INNER) preserve TID per-row independence
78 * because every output row corresponds to exactly one pair of source
79 * rows -- the row's provenance is the AND of the two tokens. Outer
80 * joins (@c LEFT / @c RIGHT / @c FULL) introduce NULL-padding rows
81 * whose provenance is the negation of the inner-match disjunction, so
82 * they break the per-row TID property and stay OPAQUE. Semi / anti
83 * joins are also rejected (the planner uses @c JOIN_SEMI / @c JOIN_ANTI
84 * for sublink-driven joins, which our @c hasSubLinks shape gate has
85 * already filtered out upstream, but the explicit check here keeps
86 * the predicate self-contained).
87 */
88static bool classify_fromlist_shape_ok(Node *n) {
89 if (n == NULL)
90 return false;
91 if (IsA(n, RangeTblRef))
92 return true;
93 if (IsA(n, JoinExpr)) {
94 JoinExpr *je = (JoinExpr *) n;
95 if (je->jointype != JOIN_INNER)
96 return false;
97 return classify_fromlist_shape_ok(je->larg)
98 && classify_fromlist_shape_ok(je->rarg);
99 }
100 return false;
101}
102
103/**
104 * @brief Whether the ORDER BY ... LIMIT / OFFSET of @p q makes the lineage
105 * of each row depend on the rows before it.
106 *
107 * Over tracked relations, such a clause keeps the rows that the rank filter
108 * of the rewriter selects in each world, so a row's annotation reads the
109 * rows sorted before it. A clause marked @c plain() truncates the actual
110 * result and leaves lineages alone, as a LIMIT without ORDER BY does.
111 * Conservative: a clause the rewriter leaves a truncation is reported too.
112 */
113static bool limit_is_rank_filter(const Query *q) {
114 constants_t constants;
115 Node *nodes[2];
116 int i;
117
118 if (q->sortClause == NIL ||
119 (q->limitOffset == NULL &&
120 (q->limitCount == NULL ||
121 (IsA(q->limitCount, Const) && ((Const *) q->limitCount)->constisnull))))
122 return false;
123 constants = get_constants(false);
124 if (!constants.ok || !OidIsValid(constants.OID_FUNCTION_PLAIN))
125 return true;
126 nodes[0] = q->limitCount;
127 nodes[1] = q->limitOffset;
128 for (i = 0; i < 2; ++i) {
129 Node *n = nodes[i] ? strip_implicit_coercions(nodes[i]) : NULL;
130 if (n != NULL && IsA(n, FuncExpr) &&
131 ((FuncExpr *) n)->funcid == constants.OID_FUNCTION_PLAIN)
132 return false;
133 }
134 return true;
135}
136
137/**
138 * @brief Recursive walker shared by the top-level entry point and the
139 * @c RTE_SUBQUERY descent.
140 *
141 * The shape gate, source enumeration, and recursion live here so the
142 * outer entry point can decide TID / BID / OPAQUE from the cumulative
143 * @c n_meta / @c sole_relid pair after the whole tree has been
144 * walked. Tracked @c RTE_RELATION entries reachable through any
145 * depth of @c RTE_SUBQUERY (view bodies after PG rewriting, inline
146 * @c FROM-clause subqueries) contribute to the accumulator.
147 *
148 * The recursion is stack-bounded by the SQL parser's own nesting
149 * limit ; no explicit depth cap is needed at this layer.
150 */
151static void classify_walk(Query *q,
153 bool *shape_ok,
154 int *n_meta,
155 Oid *sole_relid) {
156 ListCell *lc;
157
158 if (q == NULL || q->commandType != CMD_SELECT) {
159 *shape_ok = false;
160 return;
161 }
162
163 /* Shape gate at this level. Anything that turns row lineage
164 * into a composite (aggregates, GROUP BY, HAVING, DISTINCT,
165 * window functions, SRFs in the target list, a LIMIT that is the
166 * filter of a rank) breaks the
167 * per-row independent-atom property TID demands, so we refuse
168 * to certify. Hidden subqueries, modifying CTEs, named CTEs,
169 * and set operations are conservative rejects from the original
170 * scope (set operations have a dedicated UNION ALL path higher
171 * up the dispatcher). A FROM-less @c SELECT (e.g.
172 * @c SELECT @c 1) keeps the gate open because there are no
173 * sub-structures to inspect; it ends up trivially TID at the
174 * outer level. */
175 if (q->hasSubLinks
176 || q->hasModifyingCTE
177 || q->cteList != NIL
178 || q->setOperations != NULL
179 || q->distinctClause != NIL
180 || q->groupClause != NIL
181 || q->groupingSets != NIL
182 || q->havingQual != NULL
183 || q->hasAggs
184 || q->hasWindowFuncs
185 || q->hasTargetSRFs
187 *shape_ok = false;
188
189 if (*shape_ok && q->jointree != NULL && q->jointree->fromlist != NIL) {
190 foreach (lc, q->jointree->fromlist) {
191 if (!classify_fromlist_shape_ok((Node *) lfirst(lc))) {
192 *shape_ok = false;
193 break;
194 }
195 }
196 }
197
198 /* Walk the range table. RTE_RELATION entries with metadata are
199 * collected as sources; @c RTE_SUBQUERY recurses (view bodies and
200 * inline subqueries) so the underlying base relations join the
201 * accumulator. @c RTE_JOIN entries (one per @c JoinExpr in the
202 * fromlist) are synthetic union-aliases over the join's combined
203 * column list ; they carry no source on their own and pass through.
204 * The fromlist shape gate above already constrains them to
205 * @c JOIN_INNER, so seeing one here does not change the
206 * per-row independence story. Any other @c rtekind (@c RTE_VALUES,
207 * @c RTE_CTE, @c RTE_FUNCTION, the PG 18 synthetic @c RTE_GROUP,
208 * ...) trips the shape gate so we conservatively report OPAQUE.
209 * @c RTE_GROUP appears alongside the user RTEs when @c q->groupClause
210 * is non-empty -- our shape gate already rejects @c groupClause
211 * upstream, so by the time control reaches this loop on a GROUP BY
212 * query @c *shape_ok is already false and the catch-all here is
213 * just re-asserting it without enumerating @c RTE_GROUP as a
214 * source. Treating it as a generic non-source rtekind keeps the
215 * file source-compatible across PG 12-18+ without a version
216 * guard. */
217 foreach (lc, q->rtable) {
218 RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
220
221 if (rte->rtekind == RTE_RELATION) {
222 if (provsql_lookup_table_info(rte->relid, &info)) {
223 out->source_relids = lappend_oid(out->source_relids, rte->relid);
224 *sole_relid = rte->relid;
225 (*n_meta)++;
226 }
227 } else if (rte->rtekind == RTE_SUBQUERY) {
228 /* Descend. The same shape gate is applied to the inner
229 * @c Query : a @c SubLink or set operation inside the
230 * subquery propagates opacity to the outer level, while the
231 * visible inner @c RTE_RELATION sources are still added to
232 * the accumulator for diagnostic purposes. */
233 classify_walk(rte->subquery, out, shape_ok, n_meta, sole_relid);
234 } else if (rte->rtekind == RTE_JOIN) {
235 /* Synthetic RTE for a JoinExpr; the underlying source RTEs
236 * appear separately in the same rtable. No-op here. */
237 } else {
238 *shape_ok = false;
239 }
240 }
241}
242
243/**
244 * @brief Walk a @c SetOperationStmt tree, collecting each leaf
245 * leg's @c Query body into @c legs.
246 *
247 * The tree is a binary structure : interior @c SetOperationStmt
248 * nodes carry the operator and an @c all flag, leaf @c RangeTblRefs
249 * point at @c RTE_SUBQUERY entries in @c parent->rtable whose
250 * @c subquery field is the leg's parsed @c Query. Returns false
251 * if any interior node is not a @c UNION @c ALL or any leaf is not
252 * a subquery RTE, so that the dispatcher can fall back to OPAQUE
253 * (only fully-UNION-ALL trees qualify for the TID promotion).
254 */
255static bool collect_union_all_legs(Node *node, Query *parent,
256 List **legs) {
257 if (node == NULL)
258 return false;
259 if (IsA(node, RangeTblRef)) {
260 int rtindex = ((RangeTblRef *) node)->rtindex;
261 RangeTblEntry *rte;
262 if (rtindex < 1 || rtindex > list_length(parent->rtable))
263 return false;
264 rte = (RangeTblEntry *) list_nth(parent->rtable, rtindex - 1);
265 if (rte->rtekind != RTE_SUBQUERY || rte->subquery == NULL)
266 return false;
267 *legs = lappend(*legs, rte->subquery);
268 return true;
269 }
270 if (IsA(node, SetOperationStmt)) {
271 SetOperationStmt *s = (SetOperationStmt *) node;
272 if (s->op != SETOP_UNION || !s->all)
273 return false;
274 return collect_union_all_legs(s->larg, parent, legs)
275 && collect_union_all_legs(s->rarg, parent, legs);
276 }
277 return false;
278}
279
280/**
281 * @brief Promote a fully-UNION-ALL @c Query to TID when each leg
282 * classifies as TID and the leg source-relid sets are
283 * pairwise disjoint.
284 *
285 * Returns true and populates @c out with TID + the cumulative
286 * source list on success ; returns false to let the dispatcher
287 * fall through to the conservative OPAQUE path on any failure
288 * (non-UNION-ALL operator, a leg that classifies as BID/OPAQUE,
289 * or overlapping leg sources -- the gate-level atoms of a relid
290 * that appears in two legs are not disjoint, so the multiset
291 * union no longer satisfies the TID property).
292 *
293 * Pairwise disjointness is checked at the relid level only. A
294 * future correlation registry will refine this by also rejecting
295 * legs whose base ancestors overlap (e.g. two views sharing the
296 * same underlying TID table), which the syntactic check cannot
297 * detect.
298 *
299 * BID legs are deliberately not promoted, even when every leg is
300 * BID with the same block-key column projected at the same target-
301 * list position. The UNION ALL output's "rows with the same
302 * block-key value" set spans both legs, but a row from leg A and a
303 * row from leg B sharing a block-key value are independent (the
304 * legs are different relations), not mutually exclusive. Calling
305 * the result BID under that column would falsely advertise an
306 * invariant the rows don't satisfy. Recovering BID-ness would
307 * require either certifying disjoint block-key values between
308 * legs (not knowable from the query text) or emitting a synthetic
309 * composite block key @c (leg_id, k) in the output and recording
310 * it in @c provsql_table_info ; neither is implemented, so the
311 * classifier stays conservative here by design.
312 */
313static bool try_classify_union_all(Query *q,
315 List *legs = NIL;
316 List *seen = NIL;
317 ListCell *lc;
318
319 if (q->setOperations == NULL || !IsA(q->setOperations, SetOperationStmt))
320 return false;
321
322 if (!collect_union_all_legs(q->setOperations, q, &legs)) {
323 list_free(legs);
324 return false;
325 }
326
327 foreach (lc, legs) {
328 Query *leg_query = (Query *) lfirst(lc);
330 ListCell *src_lc;
331
332 provsql_classify_query(leg_query, &leg);
333 if (leg.kind != PROVSQL_TABLE_TID) {
334 list_free(leg.source_relids);
335 list_free(seen);
336 list_free(legs);
337 return false;
338 }
339 foreach (src_lc, leg.source_relids) {
340 Oid relid = lfirst_oid(src_lc);
341 if (list_member_oid(seen, relid)) {
342 list_free(leg.source_relids);
343 list_free(seen);
344 list_free(legs);
345 return false;
346 }
347 seen = lappend_oid(seen, relid);
348 }
349 list_free(leg.source_relids);
350 }
351 list_free(legs);
352
353 out->kind = PROVSQL_TABLE_TID;
354 out->source_relids = seen;
355 return true;
356}
357
358/**
359 * @brief Conservative multi-source promotion: when every tracked
360 * source in @p out->source_relids is TID and the registered
361 * ancestor sets are pairwise disjoint, promote the
362 * classification to TID.
363 *
364 * Mirrors the disjointness check the safe-query rewriter runs in
365 * @c is_safe_query_candidate, just at the classifier layer so a
366 * multi-source query no longer collapses to OPAQUE before the
367 * rewriter even sees it. The hierarchical-CQ structure is NOT
368 * inspected here -- the rewriter runs the full check downstream,
369 * and the classifier's job is only to certify the per-row
370 * independence the user-visible NOTICE pill advertises.
371 *
372 * Returns @c true and sets @c out->kind on success. On failure
373 * (any source non-TID, no registry entry, or any pair of ancestor
374 * sets overlapping), returns @c false and leaves @p out unchanged
375 * so the caller falls through to OPAQUE.
376 */
378 ListCell *lc;
379 int i, j, n;
380 uint16 *anc_n;
382
383 n = list_length(out->source_relids);
384 if (n < 2)
385 return false;
386
387 anc_n = palloc0(n * sizeof(uint16));
388 anc = palloc(n * sizeof(*anc));
389
390 i = 0;
391 foreach (lc, out->source_relids) {
392 Oid relid = lfirst_oid(lc);
394 if (!provsql_lookup_table_info(relid, &info)
395 || info.kind != PROVSQL_TABLE_TID) {
396 pfree(anc);
397 pfree(anc_n);
398 return false;
399 }
400 if (!provsql_lookup_ancestry(relid, &anc_n[i], anc[i])) {
401 /* Defensive : add_provenance / repair_key seed {self} eagerly,
402 * so a tracked relation should always have a non-empty
403 * registry entry. If somehow missing, fall back to {self}
404 * (matches the safe-query rewriter's same fallback). */
405 anc[i][0] = relid;
406 anc_n[i] = 1;
407 }
408 i++;
409 }
410
411 for (i = 0; i < n; i++)
412 for (j = i + 1; j < n; j++) {
413 uint16 a, b;
414 for (a = 0; a < anc_n[i]; a++)
415 for (b = 0; b < anc_n[j]; b++)
416 if (anc[i][a] == anc[j][b]) {
417 pfree(anc);
418 pfree(anc_n);
419 return false;
420 }
421 }
422
423 pfree(anc);
424 pfree(anc_n);
425 out->kind = PROVSQL_TABLE_TID;
426 return true;
427}
428
429/**
430 * @brief Resolve a base-level (@p varno, @p attno) pair in @p q
431 * transitively through @c RTE_SUBQUERY layers until reaching
432 * the underlying @c RTE_RELATION column. Returns @c true on
433 * success and writes the base relid / base attno to
434 * @p out_relid / @p out_attno. Returns @c false when any
435 * intermediate TLE is not a plain @c Var (possibly through
436 * @c RelabelType wrappers), when an outer-scope reference is
437 * hit, or when the chain ends on a non-relation rtekind.
438 */
439static bool resolve_var_to_base(Query *q, Index varno, AttrNumber attno,
440 Oid *out_relid, AttrNumber *out_attno) {
441 RangeTblEntry *rte;
442 if (q == NULL || varno < 1
443 || (int) varno > list_length(q->rtable))
444 return false;
445 rte = (RangeTblEntry *) list_nth(q->rtable, varno - 1);
446 if (rte->rtekind == RTE_RELATION) {
447 *out_relid = rte->relid;
448 *out_attno = attno;
449 return true;
450 }
451 if (rte->rtekind == RTE_SUBQUERY && rte->subquery != NULL) {
452 Query *sub = rte->subquery;
453 TargetEntry *te;
454 Node *e;
455 Var *v;
456 ListCell *lc;
457 /* Match by resno : a TLE may carry a Var resjunk-tagged or
458 * reordered, so scan rather than @c list_nth blindly. */
459 te = NULL;
460 foreach (lc, sub->targetList) {
461 TargetEntry *t = (TargetEntry *) lfirst(lc);
462 if (t->resno == attno && !t->resjunk) {
463 te = t;
464 break;
465 }
466 }
467 if (te == NULL)
468 return false;
469 e = (Node *) te->expr;
470 while (e != NULL && IsA(e, RelabelType))
471 e = (Node *) ((RelabelType *) e)->arg;
472 if (e == NULL || !IsA(e, Var))
473 return false;
474 v = (Var *) e;
475 if (v->varlevelsup != 0)
476 return false;
477 return resolve_var_to_base(sub, v->varno, v->varattno,
478 out_relid, out_attno);
479 }
480 return false;
481}
482
483/**
484 * @brief Decide whether every block-key column of @p info survives
485 * in @p q's target list -- resolved transitively through
486 * @c RTE_SUBQUERY descents so the same check works on
487 * @c SELECT @c k @c FROM @c bid_t and on
488 * @c SELECT @c k @c FROM @c (SELECT @c k @c FROM @c bid_t).
489 * Renamed projections (@c SELECT @c k @c AS @c b ...) still
490 * count as preserving -- the match is on the underlying
491 * @c Var, not the output column's name. @c resjunk entries
492 * in the outer @c targetList are ignored.
493 */
494static bool bid_block_key_preserved(Query *q, Oid source_relid,
495 const ProvenanceTableInfo *info) {
496 for (uint16 i = 0; i < info->block_key_n; ++i) {
497 AttrNumber bk = info->block_key[i];
498 bool found = false;
499 ListCell *lc;
500 foreach (lc, q->targetList) {
501 TargetEntry *te = (TargetEntry *) lfirst(lc);
502 Node *e = (Node *) te->expr;
503 Var *v;
504 Oid base_relid;
505 AttrNumber base_attno;
506 if (te->resjunk)
507 continue;
508 while (e != NULL && IsA(e, RelabelType))
509 e = (Node *) ((RelabelType *) e)->arg;
510 if (e == NULL || !IsA(e, Var))
511 continue;
512 v = (Var *) e;
513 if (v->varlevelsup != 0)
514 continue;
515 if (!resolve_var_to_base(q, v->varno, v->varattno,
516 &base_relid, &base_attno))
517 continue;
518 if (base_relid == source_relid && base_attno == bk) {
519 found = true;
520 break;
521 }
522 }
523 if (!found)
524 return false;
525 }
526 return true;
527}
528
529/**
530 * @brief PG 18+ helper: when @p q has a synthetic @c RTE_GROUP
531 * entry (set @c parseCheckAggregates() appends it for every
532 * @c GROUP @c BY query), Vars in @c groupClause's TLE
533 * expressions point at the @c RTE_GROUP rather than the
534 * underlying source. Resolve them through the @c RTE_GROUP's
535 * @c groupexprs list so the BID-block-key check below sees
536 * the source Var. No-op on PG &lt; 18 and on queries without
537 * @c hasGroupRTE.
538 */
539static Node *resolve_through_group_rte(Query *q, Node *e) {
540#if PG_VERSION_NUM >= 180000
541 ListCell *lc;
542 Index group_rtindex = 0;
543 Index idx = 1;
544 List *groupexprs = NIL;
545 Var *v;
546
547 if (e == NULL || !q->hasGroupRTE)
548 return e;
549 foreach (lc, q->rtable) {
550 RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
551 if (rte->rtekind == RTE_GROUP) {
552 group_rtindex = idx;
553 groupexprs = rte->groupexprs;
554 break;
555 }
556 idx++;
557 }
558 if (group_rtindex == 0)
559 return e;
560
561 while (e != NULL && IsA(e, RelabelType))
562 e = (Node *) ((RelabelType *) e)->arg;
563 if (e == NULL || !IsA(e, Var))
564 return e;
565 v = (Var *) e;
566 if (v->varlevelsup != 0 || v->varno != group_rtindex)
567 return e;
568 if (v->varattno < 1 || v->varattno > list_length(groupexprs))
569 return e;
570 return (Node *) list_nth(groupexprs, v->varattno - 1);
571#else
572 (void) q;
573 return e;
574#endif
575}
576
577/**
578 * @brief Pre-dispatch special case for @c GROUP @c BY on a single
579 * BID source's block-key columns.
580 *
581 * The generic shape gate rejects @c groupClause @c != @c NIL up
582 * front, but @c SELECT @c k @c FROM @c bid_t @c GROUP @c BY @c k
583 * (and the multi-column-key generalisation) has a well-defined
584 * per-row provenance : each output row's @c block_key value
585 * uniquely identifies one BID block, and the OR over that block's
586 * mulinput slots reduces to the block's key token (an independent
587 * @c gate_input). So the output is per-row independent -- TID --
588 * with the cumulative source list narrowed to the single BID
589 * source.
590 *
591 * Conservative: requires no aggregates / window functions /
592 * sublinks / SRFs / CTEs / set operations / HAVING / DISTINCT /
593 * sortClause-with-side-effects, no @c LIMIT / @c OFFSET, a flat
594 * fromlist of exactly one @c RangeTblRef pointing at a BID
595 * @c RTE_RELATION, and a @c groupClause whose resolved Vars match
596 * the source's block-key set exactly (no extra columns, no
597 * missing ones). When all met, returns @c true with
598 * @p out populated. Any failure leaves @p out untouched ; the
599 * caller proceeds to the generic dispatcher path.
600 */
603 RangeTblRef *rtr;
604 RangeTblEntry *rte;
606 Bitmapset *resolved = NULL;
607 ListCell *lc;
608
609 if (q->groupClause == NIL)
610 return false;
611 if (q->hasAggs || q->hasWindowFuncs || q->hasTargetSRFs
612 || q->hasSubLinks || q->hasModifyingCTE || q->hasDistinctOn)
613 return false;
614 if (q->cteList != NIL || q->groupingSets != NIL
615 || q->havingQual != NULL || q->setOperations != NULL
616 || q->distinctClause != NIL)
617 return false;
618 if (q->jointree == NULL
619 || list_length(q->jointree->fromlist) != 1)
620 return false;
621 if (!IsA(linitial(q->jointree->fromlist), RangeTblRef))
622 return false;
623 rtr = (RangeTblRef *) linitial(q->jointree->fromlist);
624 if (rtr->rtindex < 1 || rtr->rtindex > list_length(q->rtable))
625 return false;
626 rte = (RangeTblEntry *) list_nth(q->rtable, rtr->rtindex - 1);
627 if (rte->rtekind != RTE_RELATION)
628 return false;
629 if (!provsql_lookup_table_info(rte->relid, &info))
630 return false;
631 if (info.kind != PROVSQL_TABLE_BID)
632 return false;
633 if (info.block_key_n == 0)
634 return false; /* whole-table BID : "GROUP BY {}" doesn't exist */
635
636 foreach (lc, q->groupClause) {
637 SortGroupClause *sgc = (SortGroupClause *) lfirst(lc);
638 TargetEntry *te;
639 Node *e;
640 Var *v;
641 bool attno_in_block_key = false;
642 uint16 i;
643 te = get_sortgroupclause_tle(sgc, q->targetList);
644 if (te == NULL) { bms_free(resolved); return false; }
645 e = (Node *) te->expr;
646 /* On PG 18+, grouped Vars in the targetList point at the
647 * synthetic @c RTE_GROUP entry ; resolve through its
648 * @c groupexprs list back to the source-relation Var. */
650 while (e != NULL && IsA(e, RelabelType))
651 e = (Node *) ((RelabelType *) e)->arg;
652 if (e == NULL || !IsA(e, Var)) { bms_free(resolved); return false; }
653 v = (Var *) e;
654 if (v->varlevelsup != 0 || v->varno != rtr->rtindex) {
655 bms_free(resolved); return false;
656 }
657 for (i = 0; i < info.block_key_n; ++i)
658 if (info.block_key[i] == v->varattno) {
659 attno_in_block_key = true; break;
660 }
661 if (!attno_in_block_key) { bms_free(resolved); return false; }
662 resolved = bms_add_member(resolved, v->varattno);
663 }
664 /* Each block-key column must appear exactly once in groupClause. */
665 for (uint16 i = 0; i < info.block_key_n; ++i)
666 if (!bms_is_member(info.block_key[i], resolved)) {
667 bms_free(resolved); return false;
668 }
669 bms_free(resolved);
670
671 out->kind = PROVSQL_TABLE_TID;
672 out->source_relids = list_make1_oid(rte->relid);
673 return true;
674}
675
677 bool shape_ok = true;
678 int n_meta = 0;
679 Oid sole_relid = InvalidOid;
680
682 out->source_relids = NIL;
683
684 if (q == NULL || q->commandType != CMD_SELECT)
685 return;
686
687 /* UNION ALL specialisation : a fully-UNION-ALL tree of subquery
688 * legs each TID over a relid set disjoint from the other legs
689 * promotes to TID with the cumulative source list. Anything
690 * else (INTERSECT, EXCEPT, UNION DISTINCT, mixed kinds,
691 * overlapping leg sources) falls through to @c classify_walk,
692 * which trips the shape gate on @c q->setOperations != NULL and
693 * reports OPAQUE while still enumerating visible sources for
694 * diagnostics. */
695 if (q->setOperations != NULL && try_classify_union_all(q, out))
696 return;
697
698 /* GROUP BY on a single BID source's block-key columns reduces
699 * the output to one row per block ; each row's provenance
700 * collapses to the block's key token (an independent input
701 * gate), so the result is TID. Handled as a pre-dispatch
702 * special case because the generic shape gate refuses
703 * @c groupClause @c != @c NIL up front. */
705 return;
706
707 classify_walk(q, out, &shape_ok, &n_meta, &sole_relid);
708
709 if (!shape_ok) {
710 /* Conservative : when we cannot fully see the query, we cannot
711 * certify TID-ness even if the visible RTEs carry no metadata --
712 * a hidden subquery might pull in correlated rows. */
714 } else if (n_meta == 0) {
715 /* Fully visible and no provenance-tracked source : the result is
716 * deterministic, hence trivially TID. */
717 out->kind = PROVSQL_TABLE_TID;
718 } else if (n_meta == 1) {
720 if (provsql_lookup_table_info(sole_relid, &info)) {
721 if (info.kind == PROVSQL_TABLE_BID) {
722 /* BID : the output is BID iff every block-key column of the
723 * source survives in the outer target list (matched by Var
724 * resolution through any @c RTE_SUBQUERY descent, not by
725 * output column name). Otherwise the mutually-exclusive
726 * partitioning the user could observe is lost -- downgrade
727 * to OPAQUE. Whole-table BID (@c block_key_n @c == @c 0)
728 * is trivially preserved. */
729 if (info.block_key_n == 0
730 || bid_block_key_preserved(q, sole_relid, &info))
731 out->kind = PROVSQL_TABLE_BID;
732 else
734 } else {
735 out->kind = (provsql_table_kind) info.kind;
736 }
737 }
738 /* If the lookup races and disappears between the two calls,
739 * fall back to OPAQUE. */
740 } else {
741 /* Multiple tracked sources : promote to TID when every source
742 * is TID and the registered ancestor sets are pairwise
743 * disjoint. Any failure (non-TID source, missing registry
744 * entry, ancestor overlap) leaves @c out->kind at OPAQUE,
745 * which is the conservative default already set above. */
748 }
749}
750
752 StringInfoData buf;
753 ListCell *lc;
754 bool first = true;
755
756 initStringInfo(&buf);
757 appendStringInfo(&buf, "query result is %s", kind_label(c->kind));
758
759 /* TID / BID : the source list is complete and tells the user which
760 * tracked relations contribute the per-row uncertainty. OPAQUE :
761 * we deliberately omit sources -- when the shape gate trips on a
762 * sublink, a set operation, GROUP BY, etc., the rtable walk only
763 * reaches the syntactically visible sources, so the list would
764 * be partial and misleadingly suggest completeness. The user
765 * already has the query text in front of them and can identify
766 * which relations are involved without our help. */
767 if (c->kind != PROVSQL_TABLE_OPAQUE) {
768 if (c->source_relids == NIL) {
769 appendStringInfoString(&buf, " (no provenance-tracked sources)");
770 } else {
771 appendStringInfoString(&buf, " (sources: ");
772 foreach (lc, c->source_relids) {
773 Oid relid = lfirst_oid(lc);
774 char *nspname = get_namespace_name(get_rel_namespace(relid));
775 char *relname = get_rel_name(relid);
776
777 if (!first)
778 appendStringInfoString(&buf, ", ");
779 first = false;
780
781 if (nspname != NULL && relname != NULL)
782 appendStringInfo(&buf, "%s.%s",
783 quote_identifier(nspname),
784 quote_identifier(relname));
785 else if (relname != NULL)
786 appendStringInfoString(&buf, quote_identifier(relname));
787 else
788 appendStringInfo(&buf, "<oid %u>", relid);
789 }
790 appendStringInfoChar(&buf, ')');
791 }
792 }
793
794 provsql_notice("%s", buf.data);
795 pfree(buf.data);
796}
provsql_table_kind
How the provenance leaves of a tracked relation are correlated.
@ PROVSQL_TABLE_TID
@ PROVSQL_TABLE_BID
@ PROVSQL_TABLE_OPAQUE
#define PROVSQL_TABLE_INFO_MAX_ANCESTORS
Cap on the number of base ancestors recorded per relation.
static void classify_walk(Query *q, ProvSQLClassification *out, bool *shape_ok, int *n_meta, Oid *sole_relid)
Recursive walker shared by the top-level entry point and the RTE_SUBQUERY descent.
bool provsql_classify_top_level
Backing storage for the provsql.classify_top_level GUC.
static bool collect_union_all_legs(Node *node, Query *parent, List **legs)
Walk a SetOperationStmt tree, collecting each leaf leg's Query body into legs.
static bool try_classify_multi_source_tid(ProvSQLClassification *out)
Conservative multi-source promotion: when every tracked source in out->source_relids is TID and the r...
static bool bid_block_key_preserved(Query *q, Oid source_relid, const ProvenanceTableInfo *info)
Decide whether every block-key column of info survives in q's target list – resolved transitively thr...
void provsql_classify_emit_notice(const ProvSQLClassification *c)
Render the result of provsql_classify_query as a NOTICE.
static bool try_classify_groupby_block_key(Query *q, ProvSQLClassification *out)
Pre-dispatch special case for GROUP BY on a single BID source's block-key columns.
static bool try_classify_union_all(Query *q, ProvSQLClassification *out)
Promote a fully-UNION-ALL Query to TID when each leg classifies as TID and the leg source-relid sets ...
static bool limit_is_rank_filter(const Query *q)
Whether the ORDER BY ... LIMIT / OFFSET of q makes the lineage of each row depend on the rows before ...
static Node * resolve_through_group_rte(Query *q, Node *e)
PG 18+ helper: when q has a synthetic RTE_GROUP entry (set parseCheckAggregates() appends it for ever...
static bool resolve_var_to_base(Query *q, Index varno, AttrNumber attno, Oid *out_relid, AttrNumber *out_attno)
Resolve a base-level (varno, attno) pair in q transitively through RTE_SUBQUERY layers until reaching...
static bool classify_fromlist_shape_ok(Node *n)
Decide whether a jointree fromlist entry has a shape the classifier can certify : a plain RangeTblRef...
static const char * kind_label(provsql_table_kind k)
Map a provsql_table_kind to its uppercase user-facing label.
void provsql_classify_query(Query *q, ProvSQLClassification *out)
Classify the result relation of a parsed top-level Query.
Public surface of the query-time TID / BID / OPAQUE classifier.
#define provsql_notice(fmt,...)
Emit a ProvSQL informational notice (execution continues).
bool provsql_lookup_ancestry(Oid relid, uint16 *ancestor_n_out, Oid *ancestors_out)
Look up the base-ancestor set of a tracked relation.
bool provsql_lookup_table_info(Oid relid, ProvenanceTableInfo *out)
Look up per-table provenance metadata with a backend-local cache.
constants_t get_constants(bool failure_if_not_possible)
Retrieve the cached OID constants for the current database.
Core types, constants, and utilities shared across ProvSQL.
Result of provsql_classify_query.
provsql_table_kind kind
Per-relation metadata for the safe-query optimisation.
AttrNumber block_key[PROVSQL_TABLE_INFO_MAX_BLOCK_KEY]
Block-key column numbers.
uint16_t block_key_n
Number of valid entries in block_key.
uint8_t kind
One of provsql_table_kind.
Structure to store the value of various constants.
Oid OID_FUNCTION_PLAIN
OID of the marker plain(), which keeps a LIMIT a truncation of the actual result; InvalidOid on a sch...
bool ok
true if constants were loaded