Repository navigation
fix(sql): SQL correctness across routing, copies, typeguards, aggregates and continuous aggregates - #417
Merged
Conversation
…uses Document the refresh policies, retention option, supported aggregate functions and the read-only per-bucket relation returned by a continuous aggregate.
…aggregates natively Continuous aggregates now keep persisted per-bucket state folded from flushed partitions, survive restart, and are readable as read-only relations with refresh policies and retention. Timeseries aggregation, ordering and tier selection run natively in the data plane, with partition rewrite and flush recovery. Planner and executor gain aggregate FILTER clauses, aggregates over joins, UPDATE ... FROM and MERGE row sources, unqualified join column resolution, generated columns, state-transition checks by role and typeguard defaults across write forms. PromQL gains remote read and metric registration over the shared source layer.
An index registered without backfill over existing rows lacks those rows, so an equality probe on it could miss matches. Track which indexes cover the whole collection, probe only those, and read the collection otherwise so results stay correct. Probe keys use the coerced-equality bucket so an index finds every row a WHERE = matches. Coverage is carried through checkpoint and restore.
Late rows flush into a new partition beside the one their interval already has. A maintenance pass now merges those partitions in bounded groups once the interval has been quiet, staging the merged partition and committing it through a manifest that crash recovery finishes or rolls back. Merge pacing is tuned by merge_groups_per_tick, merge_max_rows and merge_quiet_ms. Replaces the out-of-order buffer and the previous merge cycle.
CREATE and ALTER now install the collection's CRDT constraint version on the applying replicas before they return, so a delta pushed right after is not fenced with a retryable Gap. Reconcile and the metadata proposer share the install path, and a failed DDL compensates it.
A prepared statement's result column now carries the key the executor emits its value under, separate from its announced name. Join columns and aliased column references no longer come back empty from Execute.
A derived table can be a JOIN side: the planner embeds its body as the join input and qualifies its columns by the alias. CTE definitions that read earlier definitions inline in dependency order, a CTE body can be a join, and star expansion keeps aliased column names. Output column types are resolved through derived tables and CTEs.
Replace direct string slicing in the escape-string terminator check and the MATCH IN-target parser with non-panicking accessors. Stop sizing the PromQL row vector from the untrusted array header count.
Use MissedTickBehavior::Delay so a late wake no longer fires a burst of back-to-back ticks. Skip any sample taken less than one interval after the previous one and keep the older baseline, because an idle core moves its heartbeat once per poll and a shorter window reads it as stalled.
Binding N keys cost N catalog transactions, each with its own durable sync. Add batched catalog ops that write the forward and reverse rows of many keys of one collection in a single transaction, first-wins, together with a due allocator watermark row. Fresh draws, bound-key assignment, and identity binding for writes, restore, snapshot apply, and the surrogate exchange now take whole batches under one registry write lock. WAL bind records follow the commit under the same lock.
The test slept a fixed 50ms before asserting the leader had applied its no-op and the proposed entry. The log fsyncs to disk, so a loaded host could miss that window. Poll with a 5s deadline instead.
A constraint install proposed directly on the local node's Raft proxy, so a node holding no replica of the collection's data group could not install. The install is now a SetConstraints plan routed through the gateway to the group's leader, and the reconcile loop delivers the same way, keyed per database. The DDL is already committed when its install runs, so an install error no longer fails the DDL. It is recorded as a diagnostic and left to the reconcile loop. Constraint ops and refresh waits home to their collection's vShard through a shared home_collection function used by the router and the forwarded-plan executor.
REFRESH CONTINUOUS AGGREGATE proposed its cut and waited for the fold on the local node, which fails when the node does not replicate the root source's data group. The cut and the wait now run on a replica of that group: locally when this node hosts it, through the gateway to the group's leader otherwise. A wait that reaches a core not owning the root answers at once. A core holding a refresh wait or an aggregate read now folds its own backlog on every tick, so the wait no longer depends on the maintenance pass. Cluster test diagnostics report the first DDL error and the server error of a failed read.
The node a gateway task forwards to has no caller identity, so a scan of a provider relation answered no rows there. The pgwire, NDJSON and WebSocket transports now materialize provider rows where the identity is, before sending the task through the gateway.
A lease lapses when a renewal round runs long, and that round is still in flight. The grace now covers one renewal interval for the coverage wait plus the holder's read timeout for the reply, so a round the leader answers in time admits the waiting statement.
Add a harness barrier that waits until every node holds an authorization lease, and run it after cluster readiness and after leader DDL, so the next statement is not refused as retryable. The Calvin backup test now bounds each awaited step with a named deadline. The snapshot constraint test reruns reconcile passes until one delivers and waits for full apply convergence before dropping the local constraint set, so a late-applied entry cannot reinstall it.
A multi-row SQL INSERT or UPSERT now lowers to a single batched operation per collection instead of one request per row. This covers key-value (BatchPut with a per-statement conflict intent), schemaless document upserts, and primary-vector inserts. Surrogate bindings are logged as one batch record per assigner call, split to fit a double-write slot, and the double-write buffer stages slot images in memory so one flush covers many records. WAL replay, replication encode/decode, transaction staging, RLS injection and change-event extraction handle the batched forms. Wire and in-process tests cover multi-row inserts, array object literals, and restart behavior for skipped inserts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #311
Closes #318
Closes #333
Closes #349
Closes #398
#296 is a duplicate of #349 and is already closed.
What changed
Statement meaning from the AST (#349, #296)
nodedb-sql/src/parser/query_function.rs: query functions are recognized from the AST. A keyword inside a string literal or a comment no longer routes a statement.DEPTS 3,LABELS 'x').ddl_ast/parse/clause/: one clause cursor serves every DDL parser that accepted keywords anywhere.route_tokens.rs), not raw text.INSERT…SELECT, MERGE and UPDATE FROM (#311)
RowSourcereads every engine as a source: document, KV, columnar, spatial, timeseries.INSERTlowers toBatchInsert.BatchInsertchecks duplicate primary keys, withif_absentforON CONFLICT DO NOTHING.EqCoercedBucket).Typeguards and generated columns (#318)
enforcement/typeguard/: the Data Plane enforces typeguards on every write form.VALUEbecomes aGENERATEDcolumn on CONVERT.428C9.DatabaseId::DEFAULT.BY ROLEroles travel in replicated writes, the redo record and sync.SHOW/REWRITE PARTITIONSrun on the Data Plane.partition_bysplits flushes, committed through the manifest with roll-forward.LIKE,IN,BETWEENandIS NULL.Aggregates and joins (#333)
HAVING,FILTER (WHERE …),GROUP BYexpressions and grouping sets work on both.stddev/varianceuse sample semantics, as in PostgreSQL. The_popforms are population.CROSS JOIN, aggregates over joins, derived tables and CTEs as join sides. CTE columns are typed.GatheredSource) and redaction.Continuous aggregates (#398)
EngineType::ContinuousAggregate). It shares one namespace with collections.cagg.stateimage per root.REFRESH CONTINUOUS AGGREGATEis a replicated cut on the source's data group. It runs on any node.PromQL
WHERE job = …works.quantile_over_timeis fixed.Timeseries storage
o3_buffer.rs,merge/o3.rsandmerge/cycle.rs.TimeseriesWalBatchand the legacyTimeseriesMemtableare removed.KV
IndexCoverage). A numeric probe matches.Constraints
Fixes found by the full suite
control/surrogate/: surrogate binds commit once per statement, not once per row. Before this, every row cost onesystem.redbfsync and one WAL sync. A 10,500-row seed takes 5s on disk, down from 110s. This defect is onmaintoo.exchange/resolve/materialize.rs: a task forwarded through the gateway gets its provider rows filled first. Before this, a forwarded read of a catalog relation or a continuous aggregate returned no rows. It affected pgwire, WebSocket and NDJSON.auth_lease/timing.rs:lapse_gracecovers one renewal round in flight (renew_every + rpc_read_timeout). Before this, a statement was refused while a renewal that would succeed was still running.bootstrap/core_stall_monitor.rs: the monitor judges full windows only. A late wake no longer reports idle cores as stalled.sql_plan_convert/dml/kv_insert.rs: a multi-row KV INSERT is oneKvOp::BatchPut, not one Raft entry per row. A 500-row INSERT took 5s on disk and hit the 30s deadline under load.ON CONFLICT DO UPDATE, vector-primary INSERT / UPSERT, and KVON CONFLICT DO UPDATEeach lower to one replicated entry.ddl/neutral/collection/dml/parse/: multi-rowUPSERT INTO … VALUES (…), (…)and the array formINSERT INTO c [{…}, {…}]parse every row. Both returned42601onmain.ON CONFLICT DO NOTHING: WAL replay overwrote a stored row with the value the statement skipped. Skip-on-conflict writes now resolve before they are logged.event/trigger/lane/fire.rs: the trigger firing cursor committed through a metadata Raft proposal on every write. It commits on its 2s cadence when no action fired.nodedb-wal/src/record/surrogate_bind.rs: surrogate binds are one WAL record per batch (SurrogateBindBatch), split to fit a DWB slot. Before this, one record per row cost one synchronous O_DIRECT DWB write per row.nodedb-wal/src/double_write/: a record stages its DWB slot in memory. A sync writes at most the 64 slots the ring keeps. Torn-write protection is unchanged.planner/context/: sub-planners (object-literal and UPSERT handlers, triggers, procedures, COPY) carry the sequence registry. Anextvaldefault there failed with55000onmain.quinn-proto0.11.19 (RUSTSEC-2026-0338, -0340, -0343) andruzstd0.9.1 (RUSTSEC-2026-0344).deny.tomlignores RUSTSEC-2024-0449 (unsafe-libyaml, unmaintained, no fix). lindera parses YAML only from the config file inLINDERA_CONFIG_PATH.parser/: no fixed-offset string slices, and no preallocation from an untrusted count.Behaviour changes
42702. A declared side wins over a schemaless one.engine='vector'means a vector-primary collection. A contradictingprimary, or a missingvector_field/dim, returns42601.TYPE …spellings map to the engine they name. An unknown spelling returns42601.23505.42P07.23505,DO NOTHINGskips it, a merge applies to the earlier row's result."*"event.UPSERT ncommand tag.SurrogateBindisSurrogateBindBatch(code52 | 0x8000). No compatibility with older logs.Removed
Lite follow-ups
This PR changes shared-crate APIs. Lite must handle each change below.
API changes
ContinuousAggregateDef.incarnation. Struct literals need..Default::default().TimeseriesOpvariants:RefreshAggregate,ReadAggregate,Partitions.MetaOpvariants:AwaitContinuousAggregateRefresh,ContinuousAggregatesRegistered.MetaOpfields:EnforceTimeseriesRetention.aggregates,TemporalPurgeColumnar.aggregates.EngineType::ContinuousAggregate,SqlError::WrongObjectType,SqlCatalogError::Unreadable.AggregateExpr.filter/.arg_text.AggregateSpec.filter/.param(AggregateParam).TimeseriesScanPlan.having.TimeseriesOp::Scan.having/.tiering.post_group_by/post_aggregatesare removed. NestedLoop and SortMerge gainleft_input/right_input.BatchInsert.if_absent. Theinsert_selectvisitor has no limit argument.UpdateFrom.source_alias.ProviderScan.sources.FlushedSeries,FlushedKind,TimeseriesWalBatch,Value::eq_coerced_exact_text.parser::query_function.parse_search_using_fusionreturnsOption<Result<..>>.KvBatchIntent{Put,Upsert,Insert,InsertIfAbsent,Merge { updates, rls_write_check }} and fieldKvOp::BatchPut.intent. Lite'sadapter/kv/dispatch.rs:159destructuresBatchPutwithout..and does not compile until it handlesintent.DocumentOp::BatchUpsertandVectorOp::DirectBatchWrite, new structVectorDirectRow, new methodVectorOp::direct_batch_point_writes(). Lite's exhaustive matches need both arms.Behaviour to handle or check
lower_aggregatehandlesFILTERand theSTDDEV/VARIANCEfamily.42702. CTE and derived-table bodies arrive as qualified computed columns.lower_ctefails for any CTE reference.usize::MAXmeans an unbounded limit.engine='vector', and it accepts thestrict/documentaliases.sync_interop_vector.rsneedsengine='document_schemaless'.''.MetaOp::CalvinFlusharm, renamehilbert_rangetoshard_bucket, and update three comments that quote the olddist_to_nodetext.Docs (nodedb-docs, already on
main)BUCKETandAGGREGATE, and partitions.SHOW PARTITIONSfor change streams.engine='vector'collection mode.How to check
cargo nextest run --workspace --exclude nodedb-cluster-tests --all-features --retries 0: 21792 of 21792 passed.cargo nextest run -p nodedb-cluster-tests --all-features --retries 0: 504 of 504 passed.nodedb-clientwith no features,nativeandremote.scripts/ci/check_*gate passes, including--self-test.