Skip to content

googlesql: Spanner schemas, arrays, and a function seed from Spanner - #4645

Open
kyleconroy wants to merge 4 commits into
mainfrom
claude/googlesql-spanner-01L8QoGi6VRtADkb4jL3BFHP
Open

kyleconroy wants to merge 4 commits into
mainfrom
claude/googlesql-spanner-01L8QoGi6VRtADkb4jL3BFHP

Conversation

@kyleconroy

@kyleconroy kyleconroy commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

With zetajones v0.2.0 parsing Spanner's DDL, this fills the GoogleSQL engine's remaining gaps for Spanner. A realistic Spanner schema and query set that used to fail or come back as any now generates fully typed code. The PR has four commits, one per area.

1. Schema statements

  • ALTER TABLE used to be dropped as unsupported, so columns it added, dropped or retyped never reached the catalog. It now converts ADD/DROP COLUMN, Spanner's ALTER COLUMN (which restates type and nullability), ALTER COLUMN … DROP NOT NULL, RENAME COLUMN and RENAME TO.
  • RENAME TABLE, CREATE [OR REPLACE] VIEW and DROP VIEW now convert.
  • A HIDDEN column is left out of SELECT *, as Spanner does.

2. Arrays and literals

  • Converter: typed literals (DATE, TIMESTAMP, NUMERIC, JSON, bytes), array constructors, INTERVAL, the OFFSET/ORDINAL/SAFE_* subscripts, and x IN UNNEST(arr), which becomes x = ANY(arr).
  • Core: a subscript is the array's nullable element with an integer index, and ARRAY(subquery) is an array, no longer its first value. x = ANY(array) types a placeholder as an array of x's type, which is what PostgreSQL reports; sqlc.slice() still gets the element. unnest(anyarray) → anyelement resolves to the element, and a function in FROM takes its resolved type, with its alias naming the column.

3. Functions and operators

  • functions.jsonl had only count. It now has 291 signatures across aggregates, math, strings, dates and timestamps, JSON, arrays, hashing, search and vector distance. operators.jsonl has 83. Spanner has no window functions, so none are listed.
  • GoogleSQL now propagates NULL through function arguments. Functions that return NULL from non-null input (JSON_VALUE, REGEXP_EXTRACT, the SAFE_* functions, …) are marked nullable.
  • The converter maps COALESCE/IFNULL/NULLIF/IF/GREATEST/LEAST onto the nodes the core types them by, makes SAFE.f(...) nullable, passes date parts (DAY, WEEK(MONDAY)) as strings rather than columns, and converts EXTRACT.
  • Core: an operator a dialect lists now replaces the one its arithmetic rule generates for the same operands, which is how INT64 / INT64 is FLOAT64. A placeholder passed for a polymorphic parameter takes its type from the other arguments, so ARRAY_INCLUDES(arr, @v) types @v as the element.

4. goldeneye generates the library

Spanner keeps no catalog of its functions, so goldeneye now reads them by asking.

  • goldeneye/spanner/library.jsonl lists the signatures to try. Each is compiled in PLAN mode against a probe database, and the ones Spanner accepts are written to functions.jsonl with the type Spanner reports. A polymorphic signature is tried with INT64 and STRING, and its result must hold for both.
  • Calls a select list can't make are compiled the way they can be: SEARCH in a WHERE clause, SCORE over a search index, PENDING_COMMIT_TIMESTAMP in an INSERT.
  • operators.jsonl is every binary operator Spanner accepts over the operands tried. types.jsonl is the types the dialect lists, each checked by a cast. The GoogleSQL types only BigQuery has (bignumeric, datetime, time, geography, range, map, …) are kept as they are, since the engine is shared with BigQuery.
  • Nullability is the one thing Spanner doesn't report, so library.jsonl states it.
  • The generated files hold exactly the records the seed in commit 3 had. TestDialect now checks all four files against Spanner.

Effect on other engines

The core changes reach every engine that goes through the analysis core. One existing golden changes: in analyze_types/bigquery, [1, 2] is now typed ARRAY<INT64> where it used to be untyped.

Testing

  • New end-to-end cases:
    • googlesql variants of create_view, ddl_alter_table_add_column, ddl_alter_table_alter_type, ddl_alter_table_drop_column and ddl_alter_table_rename.
    • ddl_spanner, a realistic schema with change streams, roles, grants, identity columns and vector indexes.
    • analyze_arrays, array_unnest, analyze_functions and func_nullability.
  • Every new case's schema was checked against Spanner Omni. The new analyze_arrays and analyze_functions cases pass goldeneye's check against live Spanner. Nullability, string lengths and UNNEST lineage, which Spanner's wire types can't report, are pinned in the generate cases.
  • go test ./internal/endtoend -run TestReplay/base passes in full. SQLC_TEST_CORE=1 … -run TestReplay/core has no new failures against main's baseline. The internal/core, internal/compiler, internal/engine and internal/cmd tests pass.
  • goldeneye's spanner.TestDialect passes against Omni.
  • The examples/authors/googlesql example passes against Spanner Omni, and its generated code is unchanged.
  • goldeneye's analyze_types/googlesql still fails, as it does on main. sqlc names unattributed parameters after their @name, and goldeneye's Spanner answer leaves them blank.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L8QoGi6VRtADkb4jL3BFHP

ALTER TABLE was dropped as unsupported, so columns it added, dropped or
retyped never reached the catalog. It now converts ADD/DROP COLUMN,
Spanner's ALTER COLUMN (which restates the type and nullability), ALTER
COLUMN ... DROP NOT NULL, RENAME COLUMN and RENAME TO; RENAME TABLE
converts too. CREATE [OR REPLACE] VIEW and DROP VIEW convert, and a
HIDDEN column is left out of SELECT * as Spanner does.

The new googlesql cases' schemas were checked against Spanner Omni.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8QoGi6VRtADkb4jL3BFHP
The converter now handles typed literals (DATE, TIMESTAMP, NUMERIC, JSON,
bytes), array constructors, INTERVAL, the OFFSET/ORDINAL/SAFE_* subscripts
and "x IN UNNEST(array)", which becomes "x = ANY(array)".

In the analysis core:
- A subscript is the array's element, nullable, with an integer index.
- ARRAY(subquery) is an array of the column, not its first value.
- "x = ANY(array)" types a placeholder as an array of x's type, as
  PostgreSQL does; sqlc.slice() still gets the element.
- A polymorphic function taking anyarray and returning anyelement, as
  unnest does, returns the array's element, and a function in FROM takes
  that resolved type, with its alias naming the column.

GoogleSQL gains an unnest seed. analyze_arrays is checked against Spanner
Omni; the rest are in array_unnest, where Spanner's wire types lose the
nullability and lengths the golden pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8QoGi6VRtADkb4jL3BFHP
functions.jsonl held only count, so every other function typed as any.
It now lists 291 signatures across aggregates, math, strings, dates and
timestamps, JSON, arrays, hashing, search and vector distance. Their
return types, and those of the operators, were read from Spanner Omni by
compiling each call in PLAN mode; the search functions and
PENDING_COMMIT_TIMESTAMP, which cannot be compiled in a select list, are
typed from Spanner's reference. Spanner has no window functions, so none
are listed.

GoogleSQL now propagates NULL through function arguments, with the
functions that return NULL from non-null input marked nullable.

The converter maps COALESCE, IFNULL, NULLIF, IF, GREATEST and LEAST onto
the nodes the core types them by, makes SAFE.f(...) nullable, passes date
parts (DAY, WEEK(MONDAY)) as strings rather than columns, and converts
EXTRACT.

In the core, an operator a dialect lists now replaces the one its
arithmetic rule generates for the same operands, which is how INT64 /
INT64 is a FLOAT64; and a placeholder passed for a polymorphic parameter
takes its type from the other arguments, so the value in
array_includes(array, @v) is the array's element.

analyze_functions is checked against Spanner Omni; func_nullability
covers what Spanner's wire types cannot report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8QoGi6VRtADkb4jL3BFHP
Spanner keeps no catalog of its library, so goldeneye now reads it by
asking. spanner/library.jsonl lists the function signatures to try; each
is compiled in PLAN mode against a probe database, and the ones Spanner
accepts are written to functions.jsonl with the type it reports. A
polymorphic signature is tried with INT64 and STRING and its result must
hold for both. Calls a select list cannot make are compiled the way they
can be: SEARCH in a WHERE clause, SCORE over a search index,
PENDING_COMMIT_TIMESTAMP in an INSERT. operators.jsonl is every binary
operator Spanner accepts over the operands tried, and types.jsonl the
types the dialect lists, each one Spanner has checked by a cast; the
GoogleSQL types only BigQuery has are kept as they are.

The generated files hold the same records the seed already did; they
are rewritten in goldeneye's compact form, and TestDialect now checks
all four files against Spanner. The dialect package's Function gains
never_null, which the seed already reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8QoGi6VRtADkb4jL3BFHP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants