Fix identifier composition, table-specific statistics, log state and random edge cases - #140
Fix identifier composition, table-specific statistics, log state and random edge cases#140roed-math wants to merge 2 commits into
Conversation
…random edge cases Four independent defects found reviewing rc2, each with regression tests that fail on rc2 and pass here. max_id/min_id formatted their table argument into the statement as text. A name needing quotes was a syntax error and a name carrying its own statement ran it; both compose an Identifier now. While there, the empty sentinel is documented: max_id returns -1, and 0 is a real id, which is what random() got wrong below. _approx_most_common read reltuples from a hard-coded public.nf_fields but read frequencies from the owning table, so every table other than nf_fields got its own frequencies scaled by an unrelated row count. The row count now comes from the table the statistics are about, looked up by name in the current schema, and the column type goes through column_type_sql rather than being concatenated. update_from_file's logging default was a shared dictionary literal that the method wrote logid and aborted into. Consecutive default calls saw the previous call's values and a caller's dictionary came back modified. An AST scan of the package confirms this was the only mutable default anywhere that is actually mutated, so nothing else needed changing. random() raised IndexError from random.choice([]) when pick_first found no values, reported a table whose only row has id 0 as empty, and discarded rows whose projection was falsy -- a table of zeros exhausted maxtries and raised "Random selection failed!". random_sample returned None for an unrecognized mode, which reads like an empty result, and reseeded the global random module when asked for a repeatable sample.
…ted array type Three follow-ups from the review of this branch. random()'s no-query branch treated every None from lucky as an id that no row has. For a projection naming one column that is also what a real row with SQL NULL in that column returns, so a table whose projected column is NULL throughout exhausted maxtries and raised "Random selection failed!" even with consecutive ids. The loop now asks whether the sampled id exists before deciding, and returns None for a row that is really there -- the value lucky would give for it. The extra query is confined to that branch: a projection returning a dictionary already distinguishes the two cases by its shape. _approx_most_common built its cast target by appending "[]" to the column declaration. A declaration may end in a COLLATE clause, which belongs to the column rather than to the type, so a column added as 'text COLLATE "C"' gave the unparseable 'text COLLATE "C"[]' -- reachable on the same table object that added the column, since add_column stores the spelling it validated. The new array_cast_type_sql builds the cast target from the validated pieces instead, dropping the collation and adding the dimension to the data type underneath it. validate_column_type now shares that decomposition rather than duplicating it. The identifier-injection test did not execute the old vulnerable path: its payload opened a double quote it never closed, and named a nonexistent relation whose failure would have aborted the transaction before the marker statement. It passed on vulnerable code for the wrong reason. The payload now starts with a table that exists and terminates that statement cleanly, which really does create the marker table on the base commit (verified against a161f5b), and the cleanup that follows is in a finally block so a run against vulnerable code leaves nothing behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
All three review items are addressed in 6c3f8d5. 1. A real row whose scalar projection is SQL
|
|
GPT signed off. |
First of the rc3 stabilization PRs, covering Track A of the review.
Four independent defects, each verified against the code before changing it and
each covered by a regression test that fails on rc2. Nothing here depends on the
later tracks, so it can land on its own.
A1.
max_id/min_idcomposed their table argument as textBoth take a public
table=argument. A name needing quotes was a syntax error,and a name carrying its own statement ran it. Both use
Identifiernow.While there I documented the empty sentinel, because A4 turned on it:
max_idreturns -1 for an empty table, and
min_idreturns 0 — which is not asentinel, since 0 is a real id.
A2.
_approx_most_commonscaled every table bynf_fieldsThe frequencies came from the owning table (
pg_stats ... tablename = %s) butthe row count came from a literal
regclass 'public.nf_fields'. So for anytable other than
nf_fieldsthe estimate was that table's frequenciesmultiplied by an unrelated row count — silently wrong, and wrong by whatever
ratio the two tables' sizes happen to have.
The count now comes from the table the statistics describe, looked up by name in
the current schema. The column type interpolated into the
unnestcast goesthrough
column_type_sql(from #133) instead of being concatenated raw.I dropped the planned "quoted table name" test for this path: since #136 a new
search table name must match the lowercase grammar, so such a table cannot be
created, and the row count is bound as a value rather than spliced.
A3.
update_from_filemutated a shared defaultConsecutive default calls saw the previous call's
logidandaborted, and acaller-supplied dictionary came back modified. Now
logging=Nonewith a freshmapping per call, copied when supplied, named
log_datainternally so it is notconfused with the
loggingmodule.Per the handoff's "do not do a large cosmetic rewrite", I ran an AST scan over
the whole package for parameters with mutable defaults that are actually mutated
(item assignment or a mutating method call, discounting rebinding). This was the
only one. Nothing else changed.
A4. Random selection edge cases
random(query, pick_first=...)calledrandom.choice([])when the querymatched nothing →
IndexErrorinstead of the documentedNone.if maxid < 1: return Nonereported a table whose single row has id 0 asempty. The sentinel is -1, so the test is
< 0.if res:discarded a row whose projection was0,False,""or[]. Atable full of them exhausted
maxtriesand raised"Random selection failed!". Nowis not None.modematched no branch andrandom_samplereturnedNone,which is indistinguishable from an empty result. It now raises
ValueErrornaming the accepted modes, before doing any work.
choicesampling called globalrandom.seed(...), so asking fora reproducible sample made every later random number in the host program
repeat. It uses a local
random.Random(repeatable).Tests
New
tests/test_correctness.py, 28 cases. 18 of them fail on rc2 — theother 10 assert adjacent behavior that was already right and guard it.
Coverage includes:
max_id/min_idagainst table names with spaces, embeddeddouble quotes,
;,--,/*and non-ASCII characters, plus an injectionattempt asserting the marker table does not exist afterwards; two tables of very
different sizes with the same distribution, checking each estimate brackets its
own row count; log-state isolation across two default calls and a caller
dictionary unchanged after both success and failure; every random edge case
above, including that the global RNG stream is bit-identical across a repeatable
sample.
Full suite: 1297 passed, 36 skipped. Ruff and the
-Wdocs build clean.🤖 Generated with Claude Code