לדלג לתוכן

Addendum: one array type, arrays of primitives only

Status: evaluation, 2026-10-09. Third design beside array-types-design.md (v1, typed and nested) and json-columns-design.md (v2, JSON only, REQ-1965).

Proposal (maintainer): "rdbs don't allow arrays of arbitrary types. should we just support arrays of primitives". Semantic SQL gets one array type, array<T>, a single-level array of a primitive. Array of arrays, struct, map and array of struct stay JSON columns with a schema, as in v2.

Evidence tags as in v2: [code], [doc], [probe], [mem] (unverified).

1. Is it the common denominator of the array-capable backends

Yes for the type. The element is whatever that backend's scalar of the same IR type is, so the existing scalar mapping decides each element type.

Backend One-level array of a primitive Limit that remains
PostgreSQL, Greenplum, YugabyteDB, openGauss T[] for any base type the type does not record dimensions [doc]: an int[] column can hold a 2-D value. A declared one-level column holding one is a data fault
CockroachDB T[]; "does not support nested arrays" [doc] none for this proposal
DuckDB T[] [doc] none
Trino array(T) [doc] none
Databricks, Hive ARRAY<T> [doc] 0-based; handled in translation
ClickHouse Array(T) [doc] the array itself cannot be Nullable [mem]: NULL array and empty array are one value
BigQuery ARRAY<T>; arrays of arrays not allowed [doc], which this proposal never needs a NULL array written to a table "is converted to an empty array"; a result array with a NULL element raises [doc]
Snowflake structured ARRAY(T) in standard and Iceberg tables [doc] the older ARRAY holds VARIANT elements, untyped: registers as JSON, or as array<T> only if the registrar states T
Redshift no array column type; SUPER [doc] JSON class, not array-capable

Primitives: text, smallint, integer, bigint, boolean, float, double, numeric(p,s), date, timestamp are array elements on all of them. uuid, time, interval and binary follow each backend's scalar mapping (several have no uuid type and carry text).

So two semantic gaps survive even at one level: NULL versus empty (BigQuery, ClickHouse) and NULL elements (BigQuery). Either the type says "NULL and empty are not distinguished, elements are not NULL" or those two backends differ from the rest.

2. Where array<T> meets a backend with no arrays

A source without arrays has no native array column, so the DIRECT route never meets one. The meeting points:

Meeting point Example
A replica landed in a self-only engine without arrays a PostgreSQL text[] table replicated into a region whose engine is MySQL, SQL Server, Oracle, MariaDB, TiDB, SingleStore, Exasol, Db2, SAP HANA, Fabric, Synapse, Redshift, Firebird, MonetDB, SAP ASE, SQL Anywhere, or the generic sqlalchemy kind
An API, file or stream source whose scalar list lands in that engine's cache OpenAPI items: {type: string} under a MySQL engine
Moving a model to such an engine engine swap; an environment on a different engine kind

Reading another region's replicas in place is done only by DuckDB, PostgreSQL and Trino (provisa/core/engine_kinds.py, REGION_READERS [code]), all array-capable, so that path is not a meeting point.

Of the engine kinds in ENGINE_KINDS [code], about 13 have array columns and about 17 do not.

Choice at the meeting point Catalog type = read type Provisa touches a value Model the same on every engine
A. Stored as a JSON array, served as json there holds within that deployment, but the model says array<T> and the catalog says json no no: the column's type depends on the engine kind
B. Stored as a JSON array, decoded by Provisa to a list holds yes: 0.49 s per 1M rows to Arrow, 0.82–1.27 s to Python lists [probe] yes
C. Refused: a table with an array<T> column cannot be served by an engine without arrays; the registrar registers the column as json to use it there holds no no: such a model does not move to those engines unchanged

No choice keeps all three. That is the original question ("where does this translation happen, and will provisa become a bottleneck") returning for the engines without arrays.

3. Does Provisa touch a value

Under A or C: no, as in v2. Under B: on that one route. pgwire needs an array encoder per element type, which works on values the wire layer already holds; Arrow passes list batches through.

4. What it buys over v2, and what it costs

Buys Weight
A list type on pgwire, Arrow and JDBC real for programmatic clients (drivers return lists, pandas gets list<T>). How well BI tools use array columns was not checked [mem]
Native indexes and speed without pattern recognition modest: v2's native form already sends element, length, contains and unnest to the native operation; this removes the recognition step and covers expressions v2 would leave as to-JSON
Exact numerics without string spelling real for arrays of numeric(p,s)
No schema needed for the common case real: the element type is the type
PostgreSQL array syntax (= ANY, unnest, c[1], array_agg) real for users who know it
Costs Note
Two mechanisms v2 is still needed in full for nested arrays, structs, maps and arrays of structs
pgwire work an array OID, text and binary codec per element type; pg_type rows. The three wire defects (first-row typing at provisa/pgwire/server.py:406-431, the l/item Arrow child name, to_ir spellings) must be fixed either way
Every surface twice GraphQL list and its filter, gRPC (bare repeated cannot hold NULL versus empty), grid filters, masks, fakes, each for arrays as well as JSON
Two spellings, two index bases arrays 1-based with PostgreSQL syntax; JSON 0-based with JSON functions
The NULL/empty and NULL-element gaps section 1
The meeting point section 2: one of three properties is given up on about 17 engine kinds

5. One operation vocabulary for both

Yes. The internal operations are the same five: element, length, contains, each (unnest), aggregate. The rule table is keyed by (operation, backend, operand form). v2 already has two forms, JSON and native; the native-form rows of v2 section 4.2 are exactly the array<T> rules. array<T> adds no rows, only a second way to reach the native rows, plus the cast between array and JSON (the to-JSON expressions of v2 section 3). The index and pushdown facts and the plan notes are shared. What cannot be shared is the surface spelling and the index base.

6. Recommendation

JSON only (v2), as REQ-1965 states it.

Reason: it is the only one of the three whose model is the same on every engine kind while Provisa touches no value and the catalog type equals the read type. array<primitive> gives that up on the engines without arrays, which are more than half of the kinds supported.

If a list on the wire is wanted for the array-capable engines, it can be added after v2 without changing v2: a natively one-level primitive array may be registered as array<T> instead of json, choice C at the meeting point, reusing v2's native-form rules. That is one more piece at the end of v2's build order, not a different design. Deciding it now is not required to start.

7. What would change in REQ-1965 under array<primitive> + JSON

Sentence in REQ-1965 Becomes
"Semantic SQL has no array, struct or map type" one array type: a single-level array of a primitive; no nested array, struct or map type
"JSON is the type registration suggests" for a native array array<T> for a one-level array of a primitive; JSON for nested arrays, structs, maps, arrays of structs
"Every column that is natively an array, struct or map carries a JSON Schema" only those registered as JSON
"The type a catalog lists for such a column is JSON on every surface" array<T> as a list type on pgwire, Arrow, JDBC, GraphQL for array columns
"Provisa touches no value on any route" needs the meeting-point rule: A, B or C of section 2
new what an array<T> column is on an engine without arrays; whether NULL and empty arrays are distinguished; whether elements may be NULL
"A decimal with a declared precision ... keep their exact value inside JSON" unchanged for JSON; array elements are exact natively