Skip to content

feat(array): SQL relations, bulk load, region aggregates, narrow types and SRIDs; fix SQL output, redaction, audit and Calvin refusals - #422

Merged
farhan-syah merged 29 commits into
mainfrom
fix/eo-array-path
Oct 18, 2026
Merged

farhan-syah merged 29 commits into
mainfrom
fix/eo-array-path

Conversation

@farhan-syah

@farhan-syah farhan-syah commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Summary

Makes the Array engine usable end to end for raster and gridded data: bulk load, query as a SQL relation, aggregate over a region, and project between CRSs. Also fixes the defects a full demo rehearsal found across SQL output, redaction, audit, pgwire, and the Calvin write path.

Array and spatial

  • ARRAY_SLICE returns a typed relation (dims and attrs as columns), so WHERE, expressions, GROUP BY, ORDER BY, LIMIT, joins and CTEs apply. ARRAY_PROJECT plans as a whole-domain slice.
  • ARRAY_AGG takes a box and a polygon zone; group takes its dim's type; non-numeric reducers are refused at plan time.
  • ARRAY_ELEMENTWISE, ARRAY_FLUSH, ARRAY_COMPACT and the vector prefilter run across every shard and core. Array primaries spread over all cores.
  • COPY <array> FROM STDIN and native ArrayPutBatch. Bulk load costs about 35.6k instructions per cell (was 112k).
  • Narrow attr types: UINT8, INT16, UINT16, INT32, UINT32, FLOAT32. A numeric NULL stays NULL through flush.
  • Cells in auto-flushed segments are visible to reads; scans decode each tile once.
  • Geometries carry an SRID. ST_Transform covers 4326, 32647, 32648, 3375, 3376. Projected geometries measure in planar metres. Mixed SRIDs are refused (22000).

SQL and pgwire

  • Computed columns evaluate on spatial, columnar, search, and index-lookup paths.
  • Timeseries aggregates keep select-list names, order and types. Scalar expressions over aggregates evaluate (ROUND(AVG(x), 1)).
  • An ungrouped column is refused with 42803.
  • Parse reports planning errors with their SQLSTATE.
  • An unknown startup database is refused with FATAL 3D000. Docs use psql ... -d default.
  • Sparse search applies its WHERE filter before the top-k cut. A filtered vector search keeps its filter and k.

Security and audit

  • Any read of a redacted column outside a plain projection is refused with 42501: expressions, filters, sorts, group keys, key lookups. Every plan variant is named in plan_util/.
  • Audit: reads see every recorded entry; the buffer never drops an entry; every site writes the audit WAL; recorders share fsyncs; VERIFY AUDIT CHAIN checks the whole chain.
  • The Event Plane hands audit records to a Control Plane recorder.
  • Every OIDC and JWT login records auth_success or auth_failure; bearer refusals do not feed lockout.
  • SHOW AUDIT prints snake_case event names and filters by event_type.

Write path

  • A Calvin-routed write answers its participant's typed refusal (23000, 23502), not 40000.
  • A new Calvin scheduler waits behind every write admitted before it started.

Breaking changes (pre-1.0, no migration)

  • On disk: array tile attr column format, surrogate map tables (surrogate_pk_v4, scope table), audit catalog rows. Data directories from main do not open cleanly.
  • Wire: ArrayOp, ClusterArrayOp, VectorOp::SparseSearch, TimeseriesOp::Scan, QueryOp::Aggregate, and Calvin sequencer entries gained fields.
  • SHOW AUDIT event names are snake_case.
  • nodedb-lite needs follow-up changes before it can move to these crates.

Known limits

  • A unique-key violation on INSERT still shows whether a redacted key exists.
  • HTTP JWT and API-key requests write one audit entry per request.

Test plan

  • cargo nextest run --workspace --exclude nodedb-cluster-tests --all-features: 22431 passed
  • cargo nextest run -p nodedb-cluster-tests --all-features: 511 passed
  • cargo clippy --workspace --all-targets --all-features -- -D warnings, plus the three nodedb-client feature sets
  • cargo fmt --all -- --check, cargo deny check
  • scripts/ci/ gates: Calvin determinism, format expects, plane separation, warm storage, Raft transport boundary
  • wasm32 check of the codec, columnar and strict crates; fuzz crate builds with its lock

…n names

Document psql superuser authentication, the 0.6 release status, and the
engines that carry bitemporal columns. Clarify that AS OF SYSTEM TIME
NULL works on ARRAY_SLICE but not ARRAY_AGG.

Rename the stale NDARRAY_* references in array comments and the
ARRAY_AGG planner error message to the ARRAY_* function names.
BitReader::read_bits now takes whole byte runs instead of one bit per
step, rejects fields wider than 64 bits as corrupt, and leaves the
cursor untouched when a read runs past the buffer.
Value::Geometry now carries an SRID alongside its shape. GeoJSON reads
and writes a `crs` member for any SRID other than 4326, and the msgpack
form gains an SRID field.

nodedb-spatial gains a projection module (transverse Mercator and
oblique Mercator families) and planar measures for projected SRIDs.
WKB/EWKB handling moves into a module that preserves the SRID. R-tree
bounding boxes are computed in the geometry's own space.

SQL geometry functions read the SRID of each operand. Operands in
different SRIDs, an unsupported SRID, or a coordinate outside a
projection's domain raise a data exception (SQLSTATE 22000) rather than
transforming implicitly; ST_Transform converts explicitly. A spatial
index fixes its SRID at its first entry and refuses geometries in
another SRID.
ARRAY_SLICE and ARRAY_PROJECT now return a typed relation, one row per
cell, with the dimensions then the attributes as columns. WHERE,
computed columns, GROUP BY, aggregates, ORDER BY, window functions and
CTEs apply to those rows. Slice bounds push down into tile selection,
and tile reads skip tiles by their bounding boxes. CTE inlining moves
from the control-plane converter into the SQL planner.

Arrays bulk-load through COPY ... FROM STDIN (text and CSV, optional
header) and through a new ArrayPutBatch native opcode exposed as
NodeDb::array_put_cells. Rows are checked as they arrive and written in
bounded chunks, so memory stays flat regardless of input size. Other
engines refuse bulk load with a typed error where it does not apply.

Surrogate reservation for batched array writes is vectorised, array
WAL records and the catalog are reworked for batch appends, and auto
flush keeps freshly written cells visible to reads. Adds ordered-map
msgpack encoding so rows have a stable column order.
…e ring refuses a chunk

A refused partial chunk was dropped, so the collector could render a
short result set as success. The stream now ends in a terminal
ResourcesExhausted frame, and the collector discards the rows it holds.
…across shards and cores

Add region-restricted array aggregation and projection-aware
elementwise operations with a shared region and tile-key model in the
planner, physical plan, and data-plane executors. Route them through
the distributed array coordinator with new wire opcodes, group-key
merging, and maintenance fan-out.

Reads on the exchange path now gather per-core outcomes explicitly and
apply vector prefilters from array reads. Payload merging returns an
error on malformed msgpack instead of silently truncating, and graph
neighbor hops that exceed max_results are refused rather than answered
as a partial frame that a gather would wait on until its deadline.
Array attributes now store each cell at the declared width: UINT8,
INT16, UINT16, INT32, UINT32, and FLOAT32 join INT64 and FLOAT64, with
the usual SQL aliases. A value outside the width is refused with
SQLSTATE 22003 on INSERT, COPY, and the native bulk load rather than
wrapped or clamped.

Columns encode by value range, with f32 columns using the 32-bit
Gorilla codec. Region aggregates sum integer attributes exactly and
refuse overflow, elementwise add/sub/mul stay exact on integers, and
result columns report the matching pgwire type. Sync apply, cluster
merge, and shard dispatch carry the new cell types.
The lexer emits NULL as its own token, so expecting it as a keyword
rejected every NOT NULL attribute. Expect the token instead.
An unknown database or a catalog read error at startup was a non-fatal
error, which left the connection open. On the SCRAM path ReadyForQuery
is already sent, so the client read the error as the reply to its first
statement. Send every startup refusal as FATAL so the connection closes.

The TLS startup test now names the built-in database.
Without -d, psql asks for a database named after the user, and the
server refuses the connection with 3D000.
…sion reads

Aggregating timeseries scans now carry their SELECT-list output
columns, so group rows are renamed to the client-visible aliases after
HAVING, sort, and limit. The time key comes from the catalog. GROUP BY
key aliases on the generic aggregate are applied the same way.

Computed SELECT items over access paths that evaluate no expression
(spatial scans, secondary-index fetches, search hits) are evaluated by
a subquery tail that projects the final list.

Sparse vector search carries the residual WHERE: the Data Plane widens
the candidate window and the Control Plane keeps the top_k hits that
pass.

A read of a redacted column outside a plain projection (expression,
predicate, sort, group key) is refused with 42501 on every engine.
The plan collection helpers move into a plan_util directory.
A participant whose stage refuses its slice now votes abort with the
typed refusal, encoded as opaque bytes the sequencer carries through the
vote and the verdict. The coordinator decodes the refusal and the client
sees the SQLSTATE a direct write renders, not a bare transaction
rollback.

When participants abort with different causes, a refusal outranks an
unrefused participant error, and equal ranks resolve to the lowest
vShard so every replica picks the same cause.
…itted writes

A scheduler that starts must not stage before every write the gate
admitted for its vShard reached its core. Such a write holds no key on
the new lock table: none existed, or its keys were on a retired one. A
transaction that staged first read the state before the write, and its
redo then overwrote it.

Every admitted write now holds a ticket that ends once the write reached
its core or landed in its data group's log at a known index. A scheduler
takes a start fence after its lock table registers and keeps its stage
gate closed until the earlier tickets ended and their indexes applied.

The propose tracker also releases an entry's held keys before it answers
the proposer, so a proposer's next write never finds its own keys held.
An array aggregate now announces its columns: group, typed as the group
dim, then result, typed by the reducer. A temporal aggregate also
announces the truncated_before_horizon flag of its summary row.

A timestamp group dim wrote raw milliseconds as an integer. It now
writes the naive instant a slice row writes for the same dim, on both
the Data Plane and the cluster finalize path.
…ells

The coordinator partitioned a write into buckets of separately encoded
cells, and each shard decoded and re-encoded the bucket into the flat
array the Data Plane reads. A bucket is now written once as that flat
msgpack array, and the shard passes it through.

Cells of one tile share a tile key. A TilePrefixMemo keeps the last
tile's indices and key, so planning, routing, COPY and memtable writes
encode the space-filling curve once per run of cells in a tile. Bind
keys for a put share one buffer.
…d columns

A grouped SELECT list can now read a GROUP BY key expression and
compute over aggregates and keys. A column that is neither a key nor
inside an aggregate is refused with SQLSTATE 42803 (grouping_error)
instead of reading a value the finalized group row does not carry.

Computed select items take a numeric wire type when their operands are
integer or float: arithmetic, math functions, negation, and CASE with
branches of one type.
Parse planned a statement only to infer a result schema and swallowed
any planning error, so a driver's prepare succeeded and the error
surfaced at Execute. Parse now reports a parse-analysis error with its
SQLSTATE, as PostgreSQL does: an unknown table, column, or function, an
ambiguous column, and a column outside GROUP BY.

An error a bound value can change, and one PostgreSQL raises while
planning or executing, still surfaces at Execute. Parse strips a
per-query ON DENY clause before planning, as Execute does.
…ex surrogate scopes

Audit log
- Entries are recorded into a buffer with backpressure, hash-chained,
  and persisted through the WAL before the catalog flush. A dedicated
  flush loop runs on a period and on early-flush requests, and flushes
  once more on shutdown.
- Readers merge flushed and buffered entries in sequence order.
- A position index over DML audit rows lets the event consumer resume
  without a full scan, and a hold keeps unconsumed events replayable.
- Recorded entries export to the configured sink after the audit lock
  releases.

Surrogate map
- Bindings are keyed by a per-collection scope id in single byte-string
  keys, so a compare is one memcmp and a collection's rows are one key
  range.
- The routed exchange binds each home answer in the asking node's
  catalog, and replicated binds carry the collection with each key's
  candidate surrogate.
…up column

ARRAY_AGG now resolves its output columns fallibly. SUM, MEAN, MIN, and
MAX accept integer and float attributes only; a STRING or BYTES
attribute is refused with a type mismatch error. The group column takes
the declared type of the group dimension.
HTTP, native, sync, and OTLP bearer authentication now share a single
gate that audits each refusal and each login with the user, session, and
peer address. Audit entries can carry the authenticated user and
session. The system audit_log table prints the same snake_case event
names as SHOW AUDIT, and SHOW AUDIT IN DATABASE applies its event_type
filter.
Record calls used to append and sync the audit WAL under the audit
lock, so every recorder paid its own fsync serially. Recording now
stages entries (seq, hash chain, WAL append) under the lock, waits for
the sync with the lock released, and then publishes the entries in seq
order.

A leader/follower group commit lets concurrent recorders share one
sync, and a batch of entries needs a single sync. Readers, flushes and
export see published entries only, so none observe an entry that is not
yet durable. An entry whose append or sync failed is still published to
keep the hash chain whole, and its record call returns the error.
An attr column now decodes against the row count of its tile and is
refused as corrupt when its stored count differs, before any
allocation. The validity bitmap is borrowed from the payload instead of
expanded into a per-row vector, so a hostile count allocates nothing.
Check the stored count against the shared capacity helper before the output vector is allocated.
The approved capacity helper lives in wkb/read.rs after the module split.
@farhan-syah farhan-syah changed the title feat(array): Earth-observation array path, plus SQL, redaction, audit, and Calvin fixes feat(array): SQL relations, bulk load, region aggregates, narrow types and SRIDs; fix SQL output, redaction, audit and Calvin refusals Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-ci Opt this PR into the full test suite; re-add to force a re-run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant