Repository navigation
What's new in 1.13.0
ProvSQL 1.13.0 is a hardening and performance release. Over two months,
the rewriting was run against thousands of real-world SQL queries and
compared, possible world by possible world, with what PostgreSQL itself
answers. Every internal error and every wrong answer this turned up was
fixed, either by tracking the construct exactly or by refusing it with a
documented reason. Tracking provenance became much cheaper, long
evaluations can now be bounded and cancelled, and the circuit store became
transactional. Along the way, the rewriting learned a large part of the
SQL that real queries use and that it used to refuse or read only on the
actual data: window functions and ranking, ORDER BY … LIMIT,
INTERSECT, DISTINCT ON, grouping sets, materialized views, and
recursion over cyclic data in every semiring, with probabilities sampled
on large graphs. Two new kinds of semirings are added and shown to work
in case studies: non-commutative semirings, for the best paths in a
graph while evaluating a recursive query over it, and Boolean algebras
of sets, for the clearance levels at which a row is visible or the
purposes it may serve.
Faster provenance tracking
- Gate creation moved to C. The gate builders (
times,plus,
monus,delta, semimodule, aggregate, and comparison gates) are
written in C instead of SQL, create a gate together with its
annotations in a single message to the circuit store, and no longer
wait for an answer; the store worker buffers its reads. This cuts the
cost of tracking provenance substantially. - The backend's gate cache is sized by the new
provsql.gate_cache_size(64 MB by default), and the evaluation of
comparisons reads the gates it needs through it. - Closed forms for
HAVING. Comparisons ofMIN/MAXwith a
constant are evaluated in a single scan of the group, in every
semiring where this is exact. Two comparisons of oneCOUNT(a range
such ascount(*) BETWEEN 3 AND 5) are resolved as a Poisson-binomial
range. Several conditions on the aggregates of one group are resolved
together, in one enumeration of the group's worlds, rather than as the
product of their separate provenances. - Moments of aggregates are computed exactly over the possible worlds
of up to 20 inputs before falling back to sampling; the variance of a
SUMor aCOUNTover independent rows is computed directly from the
probability of each row, at any size. - Hashable joins. The
IS NOT DISTINCT FROMjoin conditions the
rewriting builds become a plain=where one side cannot beNULL. - Semiring evaluations run in parallel workers:
sr_*functions in a
query PostgreSQL parallelizes are evaluated by its workers. - Choosing a probability method costs less on large circuits: the
treewidth is first bounded in linear time, and the tree decomposition
is attempted only within a budget.
Bounded and interruptible evaluations
statement_timeout, a query cancel, andpg_terminate_backendnow stop
ProvSQL's long evaluations (possible-world enumerations, sampling,
moments, Shapley values, semiring evaluation) instead of waiting for
them to finish (#102).provsql.max_memorysets a per-statement memory budget for
evaluations, external tools and their children included;
provsql.max_worldsstops a condition on an aggregate whose group
has more possible worlds than allowed. Both are raised as
program_limit_exceeded, with a tag and a hint (#103).provsql.max_memoryalso covers loading the circuit, in the backend
and in the background worker that builds it from the store, which
refuses a circuit larger than what remains of the statement's budget
and gives the memory of a large circuit back once it has sent it
(#104). A text result over 1 GB, the most PostgreSQL holds in one value, stops
with the same kind of error,result-too-large, whatever the setting;
a formula that writes out a shared sub-circuit at each of its uses can
reach that size.- A rewriting that would outgrow memory (a long chain of outer joins) is
refused.
Refusals and warnings with a reason
- Tagged refusals. A query ProvSQL cannot track is refused with
SQLSTATE 0A000(feature not supported) instead of an internal error,
and every refusal names its cause on itsDETAILline,
provsql-reason: <tag>; scope: <scope>. The scope says whether the
refusal is deliberate (the shape has no provenance to give, such as
EXCEPT ALL), a gap (provenance exists and is not computed yet),
or out of scope. Most refusals carry aHINTsaying how to write
the query instead. - Untracked parts are reported. Where a part of a query is evaluated
as plain SQL (an aggregate read as a plain value, a subquery in a block
left untracked, anORDER BY … LIMITread as a truncation), a tagged
WARNINGis raised. Settingprovsql.implicit_freeze = 'error'turns
these into refusals. - Undetermined orders are reported. Where a result depends on an order
the query does not determine, aWARNINGis raised:array_agg,
string_agg, thejson_aggfamily, andxmlaggwithoutORDER BYor
with ties;lag,lead,first_value,last_value, andnth_value
over tying rows; anORDER BY … LIMITcutting through a tie. Ties are
looked for among the rows of every world, those absent from the actual
data included. plain()marks what should be read as plain SQL on purpose and
silences the warning:plain(expr)for a value,LIMIT plain(k)for
a truncation of the actual result,FROM plain(NULL::t)for a table.
Wider SQL coverage
- Window functions:
rank(),dense_rank(),row_number(),
ntile(),percent_rank(),cume_dist(), and aggregates over a
frame determined by values, are tracked in every possible world.
Other window functions still run, with a warning, and their results
are read as plain values. ORDER BY … LIMIT/FETCH/OFFSETis read in every possible
world as a selection on rank: a row is among the firstkin the
worlds where it is.DISTINCT ONis read as aLIMIT 1per group.- Set operations and grouping:
INTERSECT,DISTINCT ON,
GROUPING SETS/ROLLUP/CUBE, and set operations nested in set
operations. - Materialized views over tracked relations keep the provenance of
their rows, recomputed byREFRESH MATERIALIZED VIEW. - Outer joins: chains of outer joins are tracked, and so are outer
joins of an untracked table with a tracked one, the rows of the
untracked table being certain. - Aggregates: grouping by, or deduplicating on, an aggregate value
(each row exploded into one row per value it can take); aggregates of
aggregate results (avgof acount,maxof asum);ln,exp,
sqrt,power,round,floor,ceil,absof an aggregate
result;stddevandvariance; floating-point arithmetic over
aggregates and differences of dates;COALESCE,NULLIF,GREATEST,
LEAST, andCASEover aggregates. A comparison over an aggregate
value in the select list, or aCOALESCE(aggregate, constant), is
exploded into one row per value it takes, as is asum,min, or
maxof aggregate results. - Subqueries: scalar subqueries of more shapes are decorrelated (an
expression over an aggregate,ORDER BY … LIMIT 1, under
COALESCE), a nested
NOT EXISTSis read as the antijoin or the division it expresses,
and subquery conditions whose body groups rows are tracked. - Recursion over cyclic data, in every semiring. A recursive CTE
usingUNIONis recorded as a system of equations between its rows,
which each semiring solves according to its properties at evaluation
time: by Dijkstra's algorithm or value iteration in absorptive
semirings, by Kleene star in those that have one, and with a tagged
refusal where no method applies. Acyclic parts stay ordinary
circuits;sr_formulaprints a cyclic part as its equations, and
PROV-XML, where-provenance, and ProvSQL Studio read them too. A
recursion usingUNION ALLis the bag recursion SQL defines, one row
per derivation. TheSEARCHandCYCLEclauses are refused with a
tag; a path built as an array of the column's own type is tracked. - Probabilities of recursive queries on cyclic data. An exact
probability expands the equations into a Boolean circuit, which is
refused where it would be too large (a city's transport network). The
sampling methods ('monte-carlo','stopping-rule', and the
'additive'and'relative'guarantees) then solve the equations in
each sampled world instead, at a cost of the order of the query's
provenance, exploring the recursion only as far as the requested row
needs.
New semirings
- Paths in a graph. A product of
provenance now keeps the order of the query (for a join, the order of
theFROMclause), and three new semirings with a non-commutative
multiplication read that order.sr_regexgives the derivations of a
row as a regular expression over the labels of its input tuples, with
a Kleene star for a recursion on cyclic data.sr_tropical_pathsgives the least
cost of a derivation together with all the derivations of that cost,
as a regular expression;sr_best_pathgives the least cost with one
derivation of that cost, the one with the fewest labels, as an array of
labels: on a path query, a shortest path, step by step.sr_regexand
sr_tropical_pathsneed the recursive relation at the same place of
every recursiveFROMclause and do not support difference (outer
joins,EXCEPT);sr_best_pathhas neither restriction. All three
refuseHAVING. Case study 3 now uses them to follow journeys, with
changes, over the whole Île-de-France network. - Subset, clearance, and consent.
sr_subsetevaluates provenance
over the sets of labels of a user enum, with negation (EXCEPT,
NOT EXISTS) read exactly.sr_clearancereturns the clearance
levels at which a user sees a row (with negation, a set that need not
be upward-closed;visible_atandclearance_settlingread it);
sr_consent
returns the purposes an answer may serve given the purposes each
input tuple is consented for (consented_for,consent_purposes,
consent_conflicts). Case study 1 and the new case study 9 (a
sales-forecast dashboard) use them.
Provenance mappings
- Labels of multivalued inputs. A provenance mapping can label the
values of a multivalued input (one choice of arepair_keyblock), and
sr_formula,sr_boolexpr,view_circuit, and PROV-XML show them.
Case study 5 uses them.
Persistence
- The circuit store is transactional: the gates a rolled-back
transaction created are left as harmless orphans, and a probability it
wrote is cleared. - Per-relation metadata moved from an mmap file to a heap table,
provsql.table_info, so it follows transactions andpg_dump. The
upgrade migrates it. - A WAL resource manager lets streaming replication carry the
circuit store to a standby (PostgreSQL 15 and later;
provsql.store_wal_logging, which requires
provsql.store_synchronous_commit). - Store records replayed from the WAL before ProvSQL's worker has started
(at crash recovery, or when a standby starts) are kept and applied
once it runs. check_store()reports inconsistencies in a database's store, and
circuit_cleanup()rebuilds it keeping only what stored tokens reach.- A new chapter, Persistence, backups and replication, explains what
survives a crash, how to back up the store alongsidepg_dump, and
how to replicate it.
Diagnostics
provsql.build_id()reports the commit the loaded library was built
from.provsql.verify_rewritechecks the query after each rewriting pass,
andprovsql.trace_rewritereports which passes changed it. The
regression suite runs withverify_rewriteon in CI.- ProvSQL loads in PostgreSQL builds with assertions enabled.
Fixes
Among the many fixes, the ones most likely to have affected results:
- Memory corruption in rewritten outer joins and
EXCEPT. The join
entries the rewriting builds had column lists PostgreSQL does not
expect, which could corrupt memory and crash the server, depending on
the heap layout. - Long aggregate values. An aggregate whose value takes more than
about 4 KB as text (anarray_aggor astring_aggover a large
group) crashed the backend; it is now recorded whatever its length. - Division by zero. In a tracked query, a division by zero in a
world where the divisor is zero givesNULLthere instead of raising
for the whole query, and rows whose condition holds in no world are
dropped. - Set operations whose arms name the
provsqlcolumn differently
no longer misalign their columns. EXCEPT ALLover tracked relations, whose rows are not those of
the algebra's difference, is refused instead of answered wrongly; an
untrackedEXCEPTarm now removes rows.FILTERon aggregates is honoured in the circuit, andNULL
inputs ofarray_agg-like aggregates are kept.- Whole-row values of tracked relations no longer include the
provenance token, so equal rows compare and group as equal; a
NATURALjoin orUSINGno longer joins onprovsql. - Values defined in no world.
expected,variance, quantiles,
covariance, and entropy returnNULLfor a value that exists in no
world (an aggregate over a group that is empty in every world, a
division by a constant zero), where some of them returned NaN or an
infinity. The
support and bounds of aSUMor a groupedCOUNTare taken over the
worlds where it has a value. A NaN stored in the data now
propagates as it does in PostgreSQL instead of being skipped, a
NULLvalue in an aggregated row is skipped instead of making the
result NaN, and comparisons,MIN/MAX, and percentiles order NaN
as PostgreSQL does. - Aggregate results stored by
INSERT … SELECTare stored as the
type of their column: anarray_aggstored in aninteger[]column used to hold the
bytes of a provenance token, and crashed the server when read. - Recursive queries: the other CTEs of a
WITH RECURSIVEare
available in the body of the recursion. AGG(DISTINCT …)of a scalar aggregation withHAVINGreads its
own row instead of an empty one.- Views defined before their tables were tracked carry provenance;
a query over one used to be answered without it, and without a
warning. - An uncorrelated
EXISTSorNOT EXISTSover aWITHentry or a
derived table is tracked, as it was over a table; it used to be
refused, or, over aWITHentry, to fail with an internal error. GROUP BY ()and aHAVINGwith neither an aggregate nor a
GROUP BYare the aggregation withoutGROUP BY: one row in every
world, even over no input row,
annotated 𝟙.GROUP BY ()used to give the row the provenance of a
group, absent where the input is empty, andSELECT 'c' FROM t HAVING trueover a tracked table crashed the backend. To collapse an event
into one token, group on a column theWHEREfixes, or use
SELECT DISTINCT, as case study 8 and the conditioning chapter now do.choose()warns where its order is not determined, asarray_agg
does: ties on itsORDER BY, or noORDER BY, with different values
in some world. This covers a scalar subquery orLATERALjoin with
ORDER BY … LIMIT 1on tied rows, which used to pick one silently.max(d) - min(d)over dates is tracked, as a difference of two
integers of days; it used to fail with a cast error (#106). The same
difference read through a subquery, whose columns are aggregate
results already, is computed on the values, with a warning.- A locking clause (
FOR UPDATE,FOR SHARE,SKIP LOCKED…) in a
query over tracked relations is refused (provsql-reason: locking-clause); it used to be answered, and over an outer join to
fail with an internal error.FOR KEY SHARE, which foreign-key
checks take, is let through. - Sublinks in an outer join's
ONcondition (IN,NOT IN,
EXISTS,NOT EXISTS) are tracked; they used to make PostgreSQL's
planner fail. - Where-provenance merges the columns of a join only on its equality
conditions, not on other comparisons. identify_tokenfinds tables outside thesearch_pathand
returns them schema-qualified; it andresolve_inputskip a table
dropped or exclusively locked in the meantime instead of failing or
waiting on it.- Dropped databases: their circuit stores are closed by the worker,
and an exception in the worker is reported as an error instead of
stopping it. - Functions that sample are
VOLATILE, so that two calls in a query
are two draws and none is evaluated once at planning time; those that
cannot run in a parallel worker arePARALLEL UNSAFE. - Dozens of internal errors on real-world query shapes now give an
exact answer or a tagged refusal.
Upgrading from 1.12
Most queries that ran on 1.12 run unchanged. A few answer differently,
because 1.13 reads them in every possible world, as the semantics has
them, where 1.12 read them only on the actual data, the one world the
database stores:
ORDER BY … LIMIT k(andFETCH,OFFSET)
returns every row that is among the firstkin some world, each
annotated by that condition: possibly many more rows thank, and,
over hundreds of candidates, a circuit that costs memory and time. To
keep the reading of 1.12, the firstkrows of the actual data,
each with its own provenance, writeLIMIT plain(k), as the warning on
such a query suggests.- Rows that are answers only in other worlds are returned with
their condition: the candidates of aLIMIT, as a selection on an
aggregate value already did. To keep only the rows of the actual data, filter on
present(provenance())in an enclosing query. ALIMIT plain(k), or
aLIMITwithoutORDER BY, cuts the tracked result, such rows
included: to cut only among the actual rows, filter on
present(provenance())in theWHEREof the query theLIMITcuts,
not above it. GROUP BY ()is the aggregation withoutGROUP BY: its row is
there in every world, annotated 𝟙. To collapse an event into one
token, group on a column theWHEREfixes, or useSELECT DISTINCT.- More warnings: parts of a query read as plain values, and orders
the query does not determine (choose()included), are now reported;
plain()marks what is meant, andprovsql.implicit_freeze = 'error'
turns the warnings into refusals. - Refusals that were answers or internal errors:
EXCEPT ALL,
locking clauses (FOR UPDATE,FOR SHARE) over tracked relations,
and anINSERT … SELECTof aprovsqlcolumn into an untracked table
(storeprovenance()instead). - Probabilities are written once (see below): a script that calls
set_probagain on the same rows needsreplace_input.
Compatibility
- PostgreSQL 10 is no longer supported. ProvSQL 1.13.0 supports
PostgreSQL 11 to 18, and has been tested on PostgreSQL 19 RC1. set_probis write-once. Writing a probability on a gate that
already has a different one is refused; to give a tuple a new
probability, usereplace_input(orreplace_blockfor a
repair_keyblock), which mints a fresh input gate.
probability_is_settells whether a probability was written.rv_samplereturnsNULLfor a draw where the value does not
exist, instead of NaN.- Removed functions:
set_infosandset_extra(a gate now records
its annotations when it is created),epsilon(a probability
threshold below which a value was read as undefined; a value is now
undefined only where it exists in no world), andeval_recursive
(replaced by the equation systems). - New configuration parameters:
provsql.gate_cache_size,
provsql.implicit_freeze,provsql.max_memory,provsql.max_worlds,
provsql.store_synchronous_commit,provsql.store_wal_logging,
provsql.trace_rewrite,provsql.verify_rewrite.
ProvSQL Studio 1.9.0 is released alongside.