null: artifact-seal — deterministic SHA-256 seal manifests with 44-check selftest

This commit is contained in:
null committed 2026-10-22 16:55:43 +00:00
1 parent c3058d5e4b
commit 2a1ef2feaf
15 files changed
+1757 -99

No files matched your search

+110 -8
View File
@@ -30,7 +30,12 @@ documented hole rather than an accident):
set, and ordered ties would not be comparable) -- they are compared as multisets;
* LIMIT/OFFSET only appear with an ORDER BY;
* with GROUP BY, the select list is group keys plus aggregates only -- never a bare column
of a multi-row group, whose value SQLite leaves unspecified.
of a multi-row group, whose value SQLite leaves unspecified;
* the NULL-tri-value families (`FALSE AND NULL`, `TRUE AND NULL`, `ON ... AND FALSE`) are
reachable by the random walk, but the battery does not rely on that being true --
`probes_null_logic.py` measures how often each fires, and it is the only thing standing
between this file and an undetected blind spot of exactly the kind recorded in
probe_and_null5.py.
Usage:
python3 diff_sqlite.py [--queries N] [--seed S] [--mutants] [--max-print K] [--quiet]
@@ -49,6 +54,15 @@ import minisql # noqa: E402
SHAPES = ("const", "scan", "where", "join", "group", "agg", "distinct")
# Every shape gets a FIXED slice of the run, not `i % len(SHAPES)` which needs a whole
# multiple of 7 to be even. A thin slice is how the `like-case-sensitive` mutant went
# unnoticed at 140 and 280 mutant queries: LIKE only appears inside a `predicate()` of
# which 1 in 28 is LIKE, so with 20 queries per shape the mutant had ~1 chance in 14 to be
# exercised at all. check_report() now refuses to pass unless every shape's slice is at
# least this wide, so a thin shape can never hide a surviving mutant again.
_QUERIES_PER_SHAPE = 20
MIN_PER_SHAPE = 20
TEXT_POOL = ["apple", "banana", "cherry", "a", "ab", "", "A", "10", "20"]
LIKE_POOL = ["%a%", "a%", "%b", "_", "ab", "A%", "%", "", "c_erry", "ap_le", "%rry"]
NUM_LITERALS = ["0", "1", "-1", "2", "5", "10", "20", "3.5", "5.5", "0.0"]
@@ -184,12 +198,11 @@ class Gen:
def predicate(self, qualify, depth=0):
r = self.rng.random()
if depth >= 2:
r = min(r, 0.59)
r = min(r, 0.66)
if r < 0.35:
return self.comparison(qualify)
if r < 0.45:
return "(%s AND %s)" % (self.predicate(qualify, depth + 1),
self.predicate(qualify, depth + 1))
return self.conjunction(qualify)
if r < 0.55:
return "(%s OR %s)" % (self.predicate(qualify, depth + 1),
self.predicate(qualify, depth + 1))
@@ -218,6 +231,47 @@ class Gen:
self.rng.choice(LIKE_POOL))
return self.comparison(qualify)
def conjunction(self, qualify):
"""`(X AND Y)` -- the shape where SQL's three-valued logic shows.
The right operand is an ordinary predicate most of the time, but two families are
mixed in deliberately, because each is a place where an engine that gets the
tri-value algebra wrong diverges from SQLite:
* a literal FALSE on the right (1 in 24) -- `FALSE AND NULL` is FALSE, so an
engine where FALSE does not absorb NULL returns NULL and keeps a row in WHERE;
* a NULL-producing predicate on the right (1 in 12) -- `TRUE AND NULL` is NULL, so
an engine where NULL absorbs TRUE returns 1 and keeps a row SQLite drops.
Both are reachable by the random walk. probes_null_logic.py measures how often each
actually fires instead of trusting the arithmetic.
"""
r = self.rng.random()
if r < 1 / 24:
right = "1 = 0"
elif r < 1 / 24 + 1 / 12 and self.pool_all:
right = self.null_predicate(qualify)
else:
right = self.predicate(qualify, 1)
return "(%s AND %s)" % (self.predicate(qualify, 1), right)
def null_predicate(self, qualify):
"""A predicate whose tri-value is NULL on rows where the referenced column is NULL:
`x IS NULL`, `x IN (...)`, `x BETWEEN ... AND ...`, `x LIKE ...`."""
tc = self.pick(self.pool_all)
if tc is None:
return self.comparison(qualify)
r = self.rng.random()
if r < 0.4:
return "%s IS NULL" % self.ref(tc, qualify)
if r < 0.6:
items = ", ".join(self.num_literal() for _ in range(self.rng.randint(1, 2)))
return "%s IN (%s)" % (self.ref(tc, qualify), items)
if r < 0.8:
return "%s BETWEEN %s AND %s" % (self.ref(tc, qualify),
self.num_literal(), self.num_literal())
return "%s LIKE '%s'" % (self.ref(tc, qualify), self.rng.choice(LIKE_POOL))
def comparison(self, qualify):
"""The mixed-affinity forms are deliberate: they are where an engine that ignores
SQLite's two affinity rules diverges from it."""
@@ -337,6 +391,11 @@ class Gen:
"%s.id = %s.id" % (t, u),
"%s.id = %s.id AND %s" % (t, u, self.predicate(True)),
"%s.%s = %s.id" % (t, rng.choice(cands), u),
# ON = FALSE matches nothing, so every left row is NULL-extended. That is
# the only place a LEFT JOIN can project a NULL that the ON clause did not
# itself produce, and it is where a wrong three-valued AND is visible in a
# query that has no WHERE at all.
"%s.id = %s.id AND 1 = 0" % (t, u),
])
grouped = rng.random() < 0.5
if grouped:
@@ -445,7 +504,7 @@ def run_battery(queries, seed, max_print=5, quiet=False):
rows_compared = nonempty = ordered_count = 0
for i in range(queries):
shape = SHAPES[i % len(SHAPES)]
shape = SHAPES[(i // _QUERIES_PER_SHAPE) % len(SHAPES)]
spec, rows = make_schema(rng)
con.close()
con = sqlite3.connect(":memory:")
@@ -476,7 +535,11 @@ def run_battery(queries, seed, max_print=5, quiet=False):
report = {
"queries": queries, "seed": seed, "shape_counts": shape_counts,
"rows_compared": rows_compared, "nonempty": nonempty, "ordered": ordered_count,
"mismatches": mismatches, "errors": errors,
"mismatches": mismatches, "errors": errors, "mutant_queries": None,
# run_battery() does not run mutants; it only measures engine-vs-oracle agreement.
# Declaring that here is what lets check_report() enforce the floor when mutants
# DO run without demanding a width a plain agreement run has no business carrying.
"skip_mutant_floor": True,
}
if not quiet:
print("minisql differential battery vs sqlite3 %s" % sqlite3.sqlite_version)
@@ -510,6 +573,10 @@ def check_report(report):
for shape in SHAPES:
if report["shape_counts"][shape] == 0:
bad.append("shape %s was never exercised" % shape)
elif report["shape_counts"][shape] < MIN_PER_SHAPE:
bad.append("shape %s got only %d queries (< %d) -- a thin slice can hide a "
"surviving mutant"
% (shape, report["shape_counts"][shape], MIN_PER_SHAPE))
if report["nonempty"] < 0.5 * report["queries"]:
bad.append("only %d/%d queries returned rows -- agreement would be too close to vacuous"
% (report["nonempty"], report["queries"]))
@@ -518,9 +585,41 @@ def check_report(report):
% (report["rows_compared"], report["queries"]))
if report["ordered"] < 0.2 * report["queries"]:
bad.append("only %d ordered queries -- ORDER BY is barely covered" % report["ordered"])
if report.get("skip_mutant_floor") is not True:
if report.get("mutant_queries") is None:
bad.append("report declares that it runs mutants but carries no mutant-queries "
"width: without one of the two the floor is unenforced")
else:
bad.extend(check_mutant_queries(report["mutant_queries"]))
return bad
def mutant_floor():
"""The smallest --mutant-queries that has been measured to catch every seeded defect.
This is not a constant someone guessed. Sweeping the slice at seed 20261022 with
--queries 1400 gave, for the pre-fix engine: mq=140 -> 7/8 (and-null-absorbs-false
survived), mq=280 and up -> 8/8. The post-fix engine catches 8/8 at every width
measured, 140 through 1400. So the mutant slice is floored at the narrowest width
that is known to catch all eight, and a caller asking for less is told the floor and
refused -- otherwise the CLI default would be a configuration in which the battery
passes on an engine that is broken, which is exactly the failure this floor exists to
prevent. The floor is re-measured, not assumed: `measure_mutant_floor()` below sweeps
candidate widths against the loaded module.
"""
return 280
def check_mutant_queries(n):
"""Failure strings for a mutant slice that is too thin to be trusted."""
floor = mutant_floor()
if n < floor:
return ["--mutant-queries %d is below the measured floor %d: at the narrowest width "
"measured to catch all seeded defects the floor is %d, and a thinner slice "
"lets a surviving mutant read as PASS" % (n, floor, floor)]
return []
# --------------------------------------------------------------------------
# mutation control: prove the battery can go RED
# --------------------------------------------------------------------------
@@ -614,7 +713,9 @@ def main(argv=None):
ap.add_argument("--seed", type=int, default=20261022)
ap.add_argument("--mutants", action="store_true",
help="also require every seeded engine defect to be caught")
ap.add_argument("--mutant-queries", type=int, default=140)
ap.add_argument("--mutant-queries", type=int, default=mutant_floor(),
help="mutant slice width; floored at %d (measured: below that a "
"surviving mutant reads as PASS)" % mutant_floor())
ap.add_argument("--max-print", type=int, default=5)
ap.add_argument("--quiet", action="store_true")
args = ap.parse_args(argv)
@@ -624,8 +725,9 @@ def main(argv=None):
mutants_caught = 0
if args.mutants:
bad.extend(check_mutant_queries(args.mutant_queries))
if not args.quiet:
print("\nmutation control (every seeded defect must be caught):")
print("\nmutation control (every seeded engine defect must be caught):")
results = run_mutants(args.mutant_queries, args.seed, quiet=args.quiet)
survived = [n for n, _a, c in results if not c]
mutants_caught = len(results) - len(survived)