M7 code review (2026-11-01)
This is a maximum-effort review of the M7 code: git diff c7cefe3..HEAD -- src test, about 3.5k lines. It covers the memory store and TEMP tables, spilling, columnar base tables, morsel parallelism and materialized-view matching (ADR 0010).
Method. Ten independent finder passes, then a verification pass and a gap sweep:
- Finder passes: line-by-line (twice), removed behavior, cross-file callers, language and SQL-semantics pitfalls, wrapper correctness, reuse, simplification, efficiency, altitude and conventions.
- Verification: each candidate not already reproduced by its finder got a verifier that tried to reproduce it against the real engine. Reproductions used MemoryVfs file databases and the Node server harness; none changed the repository.
- Gap sweep: a final pass looked only for defects not yet on the list.
There is no CLAUDE.md, so no conventions findings were possible.
Status: all 25 confirmed defects (S1, S2, S3) are fixed, each with regression tests (test/s1-regressions.test.ts, test/s2-regressions.test.ts, test/s3-regressions.test.ts); the performance and cleanup items are open. Each item below records how it was reproduced and the suggested direction. Severity: S1 corrupts data or returns wrong results silently; S2 wrong results or errors in specific shapes; S3 lifecycle, robustness and edge cases; P performance; C cleanup.
Confirmed defects
S1: data corruption and silent wrong results
- Fixed. A re-run INSERT on a columnar table reuses rowids after compaction.
- Where:
exec/columnar-table.ts:143anddatabase.ts:2614. - Cause:
ColumnarTableWritercachessegMax(and itsColumnStore) forever. The INSERT program keeps one writer across executions (writer ??= this.writerFor(table)). Compaction (appendDelta) does not bump the schema version, andresetStatementis not overridden. - Repro: run
prepare('INSERT INTO c VALUES (?)')8200 times on an empty columnar table.count(*) = 8200butcount(DISTINCT rowid) = 8192. An explicit INSERT of an id already in the segments is also accepted. - After a rollback:
dropColumnarCaches()leaves the writer holding the oldColumnStoreobject. - Fix (done):
ColumnarTableWriter.resetStatement()clearssegMaxand re-reads the table's current segment reader each execution.
- Where:
- Fixed. A failed
CREATE UNIQUE INDEXon a TEMP table corrupts it.- Where:
database.ts:1862. - Cause: the index is registered (
table.indexes.push,this.indexes.set) beforecheckUniqueIndex. For TEMP tablesdirtySchemais false, andmemDirtyis set only after the check. When the check fails, the pages roll back but the phantom index stays. - Repro:
CREATE TEMP TABLE tt(x); INSERT INTO tt VALUES (1),(1); CREATE UNIQUE INDEX ui ON tt(x)fails, as it should. Afterwards everyINSERT INTO ttandPRAGMA integrity_checkfails with "page 2 out of range", anduiis still listed. This also happens on:memory:databases, wherememBeforeis null. - Fix (done): temp index creation sets
memDirtybefore changing state, and every write statement now takes a memory snapshot (a shared empty one when there are no memory objects), so:memory:databases restore too.
- Where:
- Fixed. Morsel and MV merges accept aggregates they cannot merge.
- Where:
exec/morsel.ts:173, also reached throughmv-match.tsrollUp. - Cause: only bare-column arguments of non-mergeable aggregates are rejected.
group_concat/string_agg/json_group_*/user aggregates over constants or GROUP BY keys are copied into the merge query and aggregated over partial rows. - Repro:
SELECT g, length(group_concat('x','')) FROM t GROUP BY g(1000 rows) gives[[0,500],[1,500]]normally and[[0,4],[1,4]]throughqueryMorsels. - Fix (done): any aggregate call outside the mergeable set rejects the query, using the planner's
isAggregateCall, which the view matcher now also uses instead of its own list.
- Where:
- Fixed. Materialized views stay "fresh" when they are not.
- Where:
database.ts:1645(touchTable) and:1619(selectDeps). - Cause: freshness tracks base tables by name, with dependencies cached in
m.deps ??=. Withmatview_rewrite = 1this gives stale or unrelated answers. - Repros:
DROP TABLE t; CREATE TABLE t(x):SELECT count(*) FROM treturns the old count 3 instead of 0. The same happens withALTER TABLE u RENAME TO t.- Redefining a plain view the MV reads, once its dependencies are cached, leaves the MV fresh, and writes to the new base table never make it stale.
WITH t AS (SELECT * FROM t WHERE …) SELECT … FROM tgets empty dependencies (CTE names are hidden globally, including inside the CTE body), so inserts never make it stale.- A TEMP table shadowing
tis answered from the main table's MV.
- Fix (done):
- DROP TABLE, ALTER (including RENAME onto the name), CREATE and DROP VIEW and DROP MATERIALIZED VIEW mark dependent views stale and clear cached dependencies.
- Dependencies now include view names, and CTE names are scoped (visible in their own body only when RECURSIVE).
- View rewrite skips any view whose dependencies a TEMP table or view shadows.
- Where:
- Fixed.
main.-qualified statements reach a shadowing TEMP table.- Where:
database.ts:2398(targetTable) and:2086(DROP TABLE). - Repro: main
twith 2 rows, thenCREATE TEMP TABLE t, thenDROP TABLE main.t. This destroys the temp table's trees but the main table's catalog row. Maintis gone, andintegrity_checkreports2 in use + 0 free != 3 totalpermanently. - Also:
INSERT INTO main.twrites into the temp table. - Fix (done):
- Shadowed main tables are kept in a
shadowedMainmap, andgetTable(name, schema)resolvesmain.andtemp.against the right one. - The planner (FROM items, the
count(*)shortcut, decorrelation) and DML targets pass the schema through. - DROP TABLE removes the right entry, and its foreign-key cascade names
main.. - Scans carry their table, so a fused scan never re-resolves it by name.
- Shadowed main tables are kept in a
- Where:
S2: wrong results or errors in specific shapes
- Fixed. The server's morsel path breaks request scheduling.
- Where:
server.ts:398(toMorselsfinish). - Deadlock: a local query with a finite credit, while another session runs BEGIN and then INSERT. The query receives only
columns, and the INSERT never answers: the stream waits forallowed(), and the INSERT waits forstreams.length === 0. - Rejected write: a write pipelined after the same session's own morsel query fails with
SMALT_BUSY"query cursor is open". Before M7 it succeeded. - Closed database: after
close,finish()still merges and streams from the closedDatabase. - Fix:
- start the merged stream only when its session is allowed (queue it otherwise);
- let the requesting session's own writes wait (or treat its morsel job like its own stream);
- cancel pending morsel jobs on close, and check
this.db === dbinfinish; - use a read-hold counter instead of a fake
RowCursor.
- Fix (done): Merged morsel results are precomputed rows that hold no reads, so writers don't wait for their client. A write during pending morsel work waits in the queue, the requesting session's own included. A reader failure re-queues the query to run normally on the owner, under the usual scheduling. Close fails pending morsel queries, and
finish()never answers from a closed database.
- Where:
- Fixed. min/max merges ignore column collation.
- Where:
exec/morsel.ts:163. - Repro:
ci TEXT COLLATE NOCASEholding'b','a','B','C'.SELECT min(ci), max(ci)gives['a','C']normally and['B','b']through morsels. The MV roll-up has the same bug. - Fix: reject non-BINARY
min/maxarguments, or carry the collation into the merge. - Fix (done): a min/max argument compared with a non-BINARY collation declines the split.
- Where:
- Fixed. An MV roll-up
countreturns NULL instead of 0 when no view rows match.- Where:
exec/mv-match.ts:151. - Cause: an ungrouped query merges
countassumover the view's group rows, which are zero when nothing matches. - Repro:
SELECT count(*) FROM t WHERE g = 999returns[[null]]. An empty base table gives the same. - Fix: merge
countascoalesce(sum(n), 0), ortotal(n)cast to integer. - Fix (done):
countmerges ascoalesce(sum(n), 0).
- Where:
- Fixed. The morsel range filter binds to a user column named
rowid.- Where:
exec/morsel.ts:249. - Cause: the filter uses a column reference named
rowid, whilerowidBoundssplits by true B+tree rowids. - Repro:
CREATE TABLE t(rowid INT, v INT)with userrowidvalues outside the true range.sum(v), count(*)gives[[500500,1000]]normally and[[null,0]]through morsels. The same happens with columnar tables and columnar views. - Fix: refuse morsels when a column is named
rowid,_rowid_oroid, or filter on an internal rowid reference. - Fix (done): the range filter uses the first of
rowid/_rowid_/oidthat no user column takes, and declines when all three are taken.
- Where:
- Fixed. The morsel merge loses REAL typing.
- Where:
database.ts:560. - Cause:
smalt_partialscolumns are typedany. - Repro:
x REALholding2.0, 4.0, ….typeof(max(x))andmin(x) || ''givereal/2.0normally andinteger/2through morsels. - Fix: type the partial columns from the partial query's output types.
- Where:
- Fix (done):
smalt_partials()columns are typed from the partial query's output types.
- Fixed. A HAVING alias that matches a table column breaks morsel queries.
- Where:
exec/morsel.ts:171. - Repro:
SELECT g, sum(a) AS a FROM t GROUP BY g HAVING a > 5works normally but fails through morsels with "misuse of aggregate function sum()". - Fix: resolve HAVING names the way the planner does (table column first), or decline when an alias shadows a column.
- Where:
- Fix (done): in HAVING, a table column takes precedence over an output alias, as the planner resolves it; the query then declines.
- Fixed. Rewriting a query to an MV breaks
t.*and renamesSELECT *columns.- Where:
exec/mv-match.tsproject. - Repros:
SELECT t.* FROM tfails with "no such table: t": the star's qualifier is not normalized.SELECT * FROM tagainstCREATE MATERIALIZED VIEW mv(x, y) AS SELECT * FROM treturns columnsx, yinstead ofa, b.
- Where:
- Fix (done):
*andt.*are spelled out from the view's columns under the base table's names (MatviewCandidate.baseColumns), which also fixes column mapping for renamedSELECT *views.
- Fixed. ALTER TABLE on a TEMP table does nothing, or rewrites the main table.
- Where:
database.ts:2146. - Repros:
ALTER TABLE tmp ADD COLUMN y DEFAULT 3succeeds without effect.- With main
tand TEMPt(a,b),ALTER TABLE t ADD COLUMN cwritesa,b,cinto maint's catalog row over records shaped(a).
- Fix: support ALTER for temp tables in memory, or reject it.
- Where:
- Fix (done): ALTER resolves
main./temp.and rejects TEMP tables (SMALT_UNSUPPORTED).ALTER TABLE main.talters a shadowed main table.
- Fixed. Persistent views and MVs may read TEMP tables.
- Where:
database.ts:986(createMatview) andcreateView. - Repro:
CREATE MATERIALIZED VIEW m AS SELECT x FROM tt(ttis TEMP) persists this connection's private rows to the file, visible to readers and other tabs. After reopen,REFRESHfails with "no such table: tt". - Fix: reject persistent objects with temp dependencies (SQLite makes such views TEMP, or rejects them when
mainis explicit).
- Where:
- Fix (done): a materialized view over a TEMP object is rejected. A plain view over one becomes a TEMP view, as in SQLite, or is rejected when written
main..
- Fixed. TEMP views are not treated as connection-local.
- Where:
database.ts:298(usesMemoryObjects). - Repro:
CREATE TEMP VIEW tv AS SELECT * FROM t, thenSELECT * FROM tv. It can be routed to a parallel reader, which fails with "no such table: tv".
- Where:
- Fix (done): TEMP views count as connection-local.
- Fixed. ANALYZE includes TEMP tables.
- Where:
database.ts:1664. - Effect: their statistics are written into
smalt_statin the file and survive reopen. A TEMPtshadowing maintoverwrites maint's statistics, with the wrong column count.
- Where:
- Fix (done): ANALYZE skips TEMP tables, includes shadowed main tables, and loads statistics onto main tables only.
- Fixed. A TEMP table hides the main table from information_schema.
- Where:
catalog/information-schema.ts:56anddatabase.ts:587. - Repro: with main
tand TEMPt,information_schema.tablesand.columnslose maintentirely.
- Where:
- Fix (done):
information_schemaincludes shadowed main tables.
- Fixed. A columnar table wraps past the maximum rowid.
- Where:
exec/columnar-table.ts:146. - Cause: there is no
I64_MAXcheck, unlikeTableWriter.newRowid. - Repro: with segments holding 9223372036854775807, an INSERT succeeds with rowid 2^63, which reads back as -9223372036854775808. A row table raises
SMALT_FULL.
- Where:
- Fix (done):
SMALT_FULLpast the maximum rowid, as for row tables.
- Fixed. TEMP and main indexes can share a name.
- Where:
database.tscreateIndexandloadSchema. - Cause:
checkNewNamechecks only the persisted catalog, so a main index may reuse a TEMP index's name. - Effect: depending on whether the schema was reloaded,
DROP INDEX idrops one of the two, and the other can then never be dropped.
- Where:
- Fix (done): main indexes share one namespace with TEMP indexes.
Found while fixing S2 (fixed in the same change):
- View flattening:
flatten.tsre-emitted a view's base tables asmain.<name>. Since the S1 schema fix, that made views and subqueries over TEMP tables fail with "no such table: main.x". It now uses the resolved table's own schema. - Integrity check:
PRAGMA integrity_checkskipped main tables shadowed by a TEMP table, so their pages looked leaked ("page accounting"). Shadowed main tables are now checked. - HAVING alias of an aggregate (fixed):
SELECT g, sum(a) AS s FROM t GROUP BY g HAVING s > 100failed with "misuse of aggregate function sum()", also on the normal path. This was an older planner gap, not an M7 bug; SQLite accepts it. Select-list aliases now expand in the aggregate scope of their query (Scope.aliasScope), sosrefers to the aggregate. A table column still takes precedence over an alias of the same name. An alias of a GROUP BY term now reads the group value, which also lets such queries use the fused aggregate. - Correlated subqueries over aggregate rows (fixed, found with the fix above): in an aggregate query, a correlated subquery in the result, HAVING or ORDER BY read the outer columns as NULL, a silent wrong result:
SELECT g, (SELECT max(w) FROM u WHERE u.g = t.g) FROM t GROUP BY greturned NULL for every group, andHAVING EXISTS (… u.g = t.g …)returned no rows. Aggregate output rows only held the group keys and aggregates. Outer columns that a subquery reads are now carried by the aggregate like bare columns (Scope.onOuterRef), on both the decorrelated and the per-row path. Tests:test/having-alias.test.ts(compared with SQLite).
S3: lifecycle, robustness and edge cases
- Fixed. Reopening a database with memory views leaves writes uncommitted in the main pager.
- Where:
database.ts:228. - Cause:
rebuildMemoryViewsruns outside a transaction.fillMatviewcallstouchTable, which writessmalt_matviewand bumps the cookie on the main pager when a file MV depends on a memory MV, but only the memory pager is committed. - Effect: after open,
pager.inTransactionis true. The next failing write silently discards that metadata, the next successful write commits it, andcheckpoint()throws while the WAL is non-empty. - Ordering: views rebuild in catalog order, so after DROP and re-CREATE of a base memory view, a memory view over it stays unpopulated after reopen.
- Where:
- Fix (done): the rebuild at open runs in dependency order (a memory view over pending memory views waits for them) and marks nothing stale. It recomputes the same rows from the same committed data, so the main file is not written and
pager.inTransactionstays false.
- Fixed. A re-opened fused GROUP BY can leak a spill hold.
- Where:
exec/operators.ts:2350. - Cause:
FusedGroupAggregate.openresetsdelegatedwithout closing the fallbackHashAggregate. - Trigger: a correlated derived table on the inner side of a nested loop, where one re-open spills and the next does not. Reproduced only with a non-deterministic filter (
random()). - Effect: the
-spillfile is never truncated and keeps growing. - Fix: close the fallback first in
open().
- Where:
- Fix (done):
open()closes a fallback still delegated from the previous open.
- Fixed. The multi-pass external sort is not stable.
- Where:
exec/operators.ts(Sort merge passes). - Cause: with more than 64 runs, the merged run holding the earliest input is appended after later runs, so ties favor later input. The code comment claims stability.
- Fix: replace the merged group at the front of
runs, not the end.
- Where:
- Fix (done): merge passes replace each group of runs in place, keeping input order.
- Fixed. Two writer handles on one VFS without locking share the scratch file.
- Where:
database.ts:245,exec/spill.ts. - Effect: the second handle truncates the shared
db-spillfile, and the first fails with "spill file: short read". This is not reachable with OPFS: exclusive handles make the second spill open fall back to no spilling.
- Where:
- Fix (done): scratch file names are registered per VFS. A second handle (or a worker whose exclusive OPFS open fails) takes
<db>-spill-2,-3, … and the name is freed on close.
- Fixed.
Database.close()never closes the memory pager. Only cleanup is affected: memory is reclaimed by the garbage collector.
- Fix (done):
close()closes the memory pager.
- Fixed. The two-pager transaction facade has no compensation.
- Where:
database.ts:352–391. - Risk: if the main pager commits and the auto-checkpoint after it throws, the memory pager never commits. This was not reproduced; it is an ordering hazard.
- Where:
- Fix (done): the automatic checkpoint after a WAL commit is best-effort (counted in
stats.failedCheckpoints, retried at the next commit and on close). A committed transaction no longer surfaces as an error, and the memory store's commit always follows the main one.
Refuted on verification
- Re-run INSERTs keep a refreshed MV fresh. Refuted: REFRESH bumps the schema version, so the INSERT recompiles with a new writer.
- Mixed number and bigint rowids in one row group are missed. Refuted: a bigint forces the generic chunk encoding without
numMax, so lookups scan. - Bigint rowids break generated code ("Cannot mix BigInt"). Not reproducible: fused aggregates matched a row table both with and without codegen.
Performance
Status: all eight items are addressed. Measured with npx tsx bench/micro/m7-perf.ts (Node, file database on MemoryVfs, median of 5). Invariant tests are in test/perf-invariants.test.ts.
| Issue | Fix | Before | After |
|---|---|---|---|
Every write statement to a columnar table runs count() over the delta and deletion trees | Exact running counts kept by the writer (dropped on rollback, recounted when missing) | 8000 single-row INSERTs: 354 ms | 309 ms (a row table takes 282–309 ms: the rest is the per-statement WAL commit) |
UPDATE and DELETE of segment rows run locateInSegments with a linear rowid search | Binary search in row groups whose rowids ascend (normal after compaction; checked once per chunk) | UPDATE / DELETE 5k segment rows: 56.7 / 42.6 ms | 42.4 / 30.8 ms |
Every columnar scan rebuilds the deleted-rowid Set and the per-row-group bitmaps | Set cached per table, keyed by commit counter, local deletion version and deletion root. Bitmaps cached per set; row groups whose rowid range holds no deletion are skipped | GROUP BY / selective filter with 5k deletions: 14.5 / 0.91 ms | 5.3 / 0.31 ms |
snapshotMem() runs before every write statement | Already reduced in the S1 fixes: a shared empty snapshot when there are no memory objects | — | — |
rowBytes computed for every sorted row | Sampled every 16th row | Sort 300k rows: 328 ms | 322 ms (the accounting was a small share) |
touchTable loops over all tables on every DML writer | Loops over a per-schema-version list of materialized views only. (Marking a view stale still recompiles cached statements once per refresh cycle; left as is.) | 5000 INSERT + UPDATE with 20 tables: 62.7 ms | 62.4 ms (the loop was a small share) |
usesMemoryObjects walks the AST per request; morselQuery cache-clears at 64 entries; merge statements compiled per run | Memoized per statement and schema version. The morsel cache is an LRU. Partial column types and the merge statement are compiled once per query text and schema; smalt_partials() reads the current rows at run time | 20000 checks with a TEMP table present: 25.2 ms | 0.04 ms |
| TEMP-table growth never triggers re-planning | Statements re-check row assumptions when the memory pager's commit counter moves too (Database.changeStamp) | Plan kept after a TEMP table grew 1→20000 rows | Re-planned |
Regression check after all fixes
main after the fixes (f42b0bc) against the last commit before them (6ec52a7), run back to back on the same machine. A difference counts only when the min–max ranges of the two runs do not overlap.
| Suite | Result |
|---|---|
Storage micro-benchmarks (npm run bench) | Flat. Sequential B-tree insert: 60.2 ms on both (median of 3 runs). |
Joins vs SQLite in Node (bench/micro/joins.ts, J1–J7) | Flat. J4 about 67 ms on both (6 runs each). |
bench/micro/m7-perf.ts | Every case the same or faster (table above). |
| Browser, OPFS (47 workloads) | Nothing slower. Loading the join tables got faster: 1732 → 1330 ms. Geometric mean of the medians 0.0%. |
Browser, CPU only (smaltdb-mem, 45 workloads) | Nothing slower. Geometric mean −0.2% over 9 repetitions, against −0.7% for SQLite on the same runs. |
The first run of analytic: group by came out 3–19% slower in individual browser runs, never outside the ranges. Measured directly in Node (15 fresh databases each) it is equal: 16.0–16.5 ms against 16.1–16.7 ms.
Saved baselines: bench/results/2026-11-01-chromium-f42b0bc.json and bench/results/2026-11-01-chromium-f42b0bc-cpu.json.
Regression check after the follow-ups
main after the HAVING / correlated-subquery fix, reader PRAGMA settings and nested materialized-view matching (e05378c) against the baseline above (f42b0bc), run back to back on the same machine, same rule.
| Suite | Result |
|---|---|
| Storage micro-benchmarks | Flat (the new commit came out faster on every B-tree case; no storage code changed). |
| Joins vs SQLite in Node (J1–J7) | Flat. J6 9.0–9.8 ms against 8.8–9.9 ms (4 runs each). |
bench/micro/m7-perf.ts | Flat. |
| Browser, OPFS (47 workloads) | Geometric mean +1.2%, against −0.7% for SQLite on the same runs. |
| Browser, CPU only (45 workloads) | Geometric mean +1.5%, against +2.5% for SQLite. |
One case came out outside the ranges once: index: create on 2 columns on OPFS, 91.0 → 97.7 ms. It did not hold up: the CPU-only runs give 63.0 → 61.6 ms, the saved runs 93.1 → 94.3 ms (OPFS, overlapping) and 60.9 → 60.9 ms (CPU only), and Node (11 fresh databases each, two rounds) 66.6–67.6 ms against 65.2–65.6 ms. None of the changes touch index builds. With matview_rewrite off (the default, and in every benchmark) the new matching hook is not installed.
Saved baselines: bench/results/2026-11-01-chromium-e05378c.json and bench/results/2026-11-01-chromium-e05378c-cpu.json.
Cleanup
Status: done, except two items kept on purpose and two that are behavior changes (follow-up features). Behavior is unchanged: all suites pass, and bench/micro/m7-perf.ts plus an interpreted GROUP BY benchmark show no regression.
- Reuse:
- AST helpers: a new
sql/ast-util.tsholdsexprKey,anyNode,andandandAll.mv-match.tsusesconjunctsfromexpr.ts, the sharedandAllandanyNode, and the planner'sisAggregateCall; its own copies are gone.flatten.tsuses the sharedandandanyNode, keeping its behavior of not entering nested SELECTs.
- Expression keys:
exprKey(morsels, MV matching) and the planner'sexprShapeKeyare now one canonical function with options (foldNames,ignoreQualifiers). Kept:exprEqualsstays a separate, allocation-free comparator, because it is used on planner hot paths where building a key string would cost more. - Walkers: the deep walkers (
walkin morsel,containsin mv-match,anyNodein flatten) are replaced by the sharedanyNode. - Metadata rows:
writeMatviewMetaandwriteColumnarMetashareupsertMetaRow, which also closes its cursor on errors. - Pragmas: boolean parsing and setters use
pragmaBool()andsetter(). - Varints: the spill writer uses the B+tree module's
writeVarint(same encoding).
- AST helpers: a new
- Simplification:
- Column stores: a single
storeOf(t)replaces the three ways of getting a table'sColumnStore, and oneliveRows(t)is shared by the planner (throughPlannerEnv) andparallelSplit. - Unused fields removed:
spilledRuns,spilledPartitionsandColumnScan.skipped. - Columnar scans: the interpreted
ColumnScanand the generated loops shareColumnScanRun(zone-map test, deleted flags, stored rowids per row group). - Worker jobs: reader jobs are a tagged union (
{ kind: 'query' } | { kind: 'partial' }) instead of fake jobs with negative ids. - Kept:
HashAggregate.stepandstepRecordstay separate. Routing in-memory rows through the spill record format cost about 20% on an interpreted GROUP BY (34 → 41 ms over 300k rows), sostepreads registers directly and a comment ties the two together.record()now sharesinputs()with the spill path. - Kept:
MemViewstill mirrors the refresh state ofMatViewInfo. It has to outliveTableSchemaobjects, which are rebuilt on every schema reload. Folding the two together would touch every refresh path for little gain.
- Column stores: a single
- Altitude:
- Columnar checks: the hand-written
t.matview?.storage === 'columnar' || t.columnarchecks are nowisColumnar(t). - Implicit rowid zone (-1): kept, and the stated consequence does not occur. A materialized view's storage layout never declares an INTEGER PRIMARY KEY, so views have no rowid alias that could go unpruned.
- Follow-up features, not cleanup (they change behavior):
- MV rewrite inside subqueries, CTEs, views and INSERT … SELECT, by hooking it into
planSelect. Done: every SELECT the planner plans can be matched, except correlated ones, CTE names and statements that write a view's base tables (ADR 0010, section 5). Tests:test/mv-rewrite-nested.test.ts; - passing PRAGMA settings (
work_mem,matview_rewrite) to parallel readers. Done: the owner sends its settings (Database.readerSettings) with each query and morsel handed to a reader, which applies them first (applyReaderSettings). Tests:test/reader-settings.test.ts, plus a browser check that a reader answers from a materialized view once the owner turnsmatview_rewriteon.
- MV rewrite inside subqueries, CTEs, views and INSERT … SELECT, by hooking it into
- Columnar checks: the hand-written
Suggested order of work
- S1 items 1–5, which corrupt data or return wrong results silently. Each gets a regression test that reproduces it first.
- Morsel and MV merge correctness, items 3 and 7–12:
- tighten
analyzeMorsels(non-mergeable aggregates, collation,rowidcolumns, HAVING resolution); - fix the
countmerge; - type the partial columns;
- add a differential test that runs a large set of query shapes through both the normal path and morsels.
- tighten
- Server scheduling, item 6: the morsel-completion path needs to respect the queue and transaction ownership.
- TEMP-table isolation, items 13–17 and 19: keep main and temp schema maps separate. That root cause underlies several items.
- S3 items 20–25, the performance items, then the cleanup.