Go codegen for ClickHouse, DuckDB, Spanner and SQL Server under database/sql - #4621
Open
kyleconroy wants to merge 15 commits into
Open
kyleconroy wants to merge 15 commits into
kyleconroy wants to merge 15 commits into
Conversation
The core analyzer types every column and parameter as an expression: a name, whether a value may be null, and nested arguments, so decimal(10, 2), array(array(integer)) and struct(x: integer) survive intact. The plugin request flattened that to a single type name plus not_null, is_array, array_dims, length and unsigned, which is not enough for a type mapper to tell nvarchar(max) from nvarchar(50) or to see the element of an array or the fields of a struct. Add a recursive TypeExpr message to the plugin proto and a type_expr field on Column, populated from the core's tree in the shim. It is only set when the analysis core typed the column; the legacy path leaves it unset and the flat fields stay as they were, so existing plugins keep working. When a database analyzer overrides a column's flat type in combineAnalysis, the tree no longer describes the column and is dropped. A DuckDB codegen_json case pins the shape with decimal arguments, nested arrays, a labeled struct, a map and a sized array. The existing JSON request goldens gain the new field, unset, on every column. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
The Go generator ran for the four engines the analysis core handles but knew none of their types, so every field came out as any. Add a mapper per engine that reads the column's type expression rather than the flat name, which is what lets it see the element of an array, the key and value of a map, the fields of a struct, and the type LowCardinality or SimpleAggregateFunction wraps. Each maps to the Go types the engine's database/sql driver hands back: shopspring decimals, orb geometries and pointer-typed nullable elements for clickhouse-go; duckdb.Composite, duckdb.Decimal, duckdb.Interval and duckdb.Map for duckdb-go, with a list parameter typed as the slice itself since a Composite is not accepted as an argument; the spanner package's Null types for go-sql-spanner, whose arrays decode into them; and mssql.UniqueIdentifier for go-mssqldb. The tree-reading mappers render the whole type, arrays included, so goType leaves the dimensions to them. Model columns come from the catalog, which the core path flattened, so catalog.Column now carries the expression too and the shim forwards it. The queries for these engines pass slices straight to the driver, so pq.Array wrapping is limited to PostgreSQL under database/sql, where lib/pq needs it. Import detection follows a package qualifier into map keys and values and through any number of slice and pointer prefixes. A core_types case per engine generates the analyze_types schema and queries with database/sql and pins the output; the end-to-end module gains the drivers so the generated code compiles. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
…ew engines' examples SQL Server and Spanner name a parameter @name in the query text and ClickHouse's server-side placeholder is {name:Type}; their drivers take such an argument as sql.Named rather than by position. When every parameter of a query is named that way, pass each as sql.Named with the name the query uses. A query written with ? keeps positional arguments. Add an authors example for ClickHouse, SQL Server and Spanner beside the existing ones, and one for DuckDB in a module of its own, since its driver is cgo and the main module's builds keep cgo off. Test helpers create a database per test on the server CLICKHOUSE_SERVER_URI, MSSQL_SERVER_URI or SPANNER_SERVER_URI names and skip when none is; the DuckDB example opens a database in memory. CI runs ClickHouse and SQL Server as services, builds the end-to-end module with cgo for the DuckDB driver, and tests the DuckDB module; docker-compose gains a ClickHouse service beside the SQL Server and Spanner Omni ones for local runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
The ClickHouse image restricts the default user to connections from localhost unless a password is configured, and the test job reaches the service through a port mapping, so the ClickHouse example failed to authenticate. Set a password on the service and in the URI, in docker-compose too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
…ni in CI The examples' drivers were becoming dependencies of sqlc itself, and the DuckDB driver, being cgo, had to live in a module apart. Move every example into one module under examples/, which requires sqlc through a replace, so the drivers are its dependencies alone and cgo can stay on for all of it; sqlc's own go.mod loses them, and lib/pq with them, since only the examples imported it. The DuckDB example rejoins the authors example beside the others, and the test helpers for ClickHouse, SQL Server and Spanner move into the examples module, since the drivers they need are what kept them out of sqlc. The end-to-end suite skips the directory under examples that holds those helpers rather than treating it as an example. sqlc-test-setup now installs Spanner Omni from its standalone server release, checked against a pinned SHA-256, and starts a single server in the background on port 15000, so CI runs the Spanner example against it alongside the ClickHouse and SQL Server services. The Makefile and CI run the examples from their module. Drop the clearing of a column's type expression in combineAnalysis: the compiler builds a live-database analyzer only on the legacy path and returns before doing so on the core path, so the two never meet and the expression is never set where the analyzer runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
The examples need every database sqlc supports, while sqlc's own tests need only PostgreSQL and MySQL. Split the test job in two, and let sqlc-test-setup take the databases to install and start as arguments so each job sets up what it uses. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
The DuckDB golden imports the DuckDB driver, which is cgo, so it is left out of that build and checked by the replay suite alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
…House and SQL Server A column's type_expr is only set when the analysis core typed it, and the JSON plugin wrote every other column with an explicit null, which put a line on every column of every request the legacy path produced. Drop the member when it is unset instead, so those requests read as they did before the member existed; the goldens on the legacy path are back to what they were. sqlc-test-setup now installs and starts ClickHouse and SQL Server as well, so the examples need no service containers from CI's host. ClickHouse comes from the release tarball goldeneye pins, checked against its SHA-512 and cached where goldeneye caches it, and runs from a configuration the tool writes that gives the default user the password the tests use. SQL Server comes from Microsoft's apt repository for Ubuntu 22.04 and 24.04, set up non-interactively with the EULA accepted, and starts through systemd where there is one and in the background otherwise. The examples job in CI installs and starts every database through the tool. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
…-end module with cgo The end-to-end module is one of its own, so cgo there is no concern of sqlc's builds, and the DuckDB golden compiles with the rest again. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
A detached server holding the tool's stderr open kept anything reading the tool's output waiting after the tool had finished. Its output goes to a file of its own, the way the ClickHouse and Spanner Omni servers' does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
…g its conf On a GitHub runner the tool runs as an unprivileged user, and /var/opt/mssql is readable by the mssql user only, so stat-ing /var/opt/mssql/mssql.conf failed with permission denied even though setup had completed. Treat a successful setup as installed, and when setup fails check for the configuration through sudo before deciding whether the failure was only the service start. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
The regex that stripped the member from protojson's output was a hack around EmitUnpopulated, which has no per-field form. A column the legacy path typed now carries "type_expr": null like every other unset message field, and the goldens are regenerated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
…kHouse nullables correctly A parameter whose placeholder names it, SQL Server's and Spanner's @name and ClickHouse's {name:Type}, is now marked by the compiler: the mssql and googlesql converters carry the name on the ParamRef as the ClickHouse one already did, the analyzer keeps it whichever use of the parameter it sees first, and the plugin Parameter gains a named field. The Go codegen reads that instead of searching the query text for "@name", which bound a CAST-wrapped @A positionally, because the analyzer had named it after the column it was compared with, and matched "@id" inside "@identity_no". A row struct is scanned into and a params struct is passed as arguments, and DuckDB wants different types for the two: a list is duckdb.Composite when scanned and a plain slice when passed. Row structs used the params form. The imports for the element of a generic type such as duckdb.Composite[[]time.Time] were also missing, and sql.Named's database/sql import reached the querier interface, which does not use it. clickhouse-go dereferences a Nullable value only when Nullable is the column's outermost type, so LowCardinality(Nullable(T)) and SimpleAggregateFunction(f, Nullable(T)) come through as pointers that a sql.Null wrapper cannot scan; they take the pointer form now. sqlc-test-setup start skips SQL Server wherever install did, rather than failing on an Ubuntu release the packages are not published for. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
The name a placeholder carries, @name or {name:Type}, is what the driver binds by, so the plugin Parameter carries it as name, collected from the query's ParamRef nodes by the compiler rather than taken from the analyzer, which also names a parameter after the function it is compared with. The Go codegen passes it to sql.Named instead of the column's name. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT
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.
Summary
The Go generator already ran for the four engines the analysis core handles, but knew none of their types: every field came out as
any, and the queries passed arguments positionally against@nameplaceholders. This makes the generated code typed and runnable with each engine'sdatabase/sqldriver.Plugin request carries the type expression.
codegen.protogains a recursiveTypeExprmessage and atype_exprfield onColumn, populated from the core analyzer's tree. It is only set when the core typed the column; the legacy path leaves it unset and the flat fields stay as they were, so existing plugins keep working. Model columns come from the catalog, which the core path flattened, socatalog.Columncarries the tree too. The core path and the live-database analyzer never meet:NewCompilerreturns afterinitCorebefore any analyzer is built.A type mapper per engine reads the tree rather than the flat name, which is what lets it see the element of an array, the key and value of a map, the fields of a struct, and the type
LowCardinalityorSimpleAggregateFunctionwraps. Each maps to what the engine's driver hands back:clickhouse-go/v2): sized integers,*big.Int, shopspringdecimal.Decimal,uuid.UUID,net.IP,orb.*geometries; nested nullables as pointers, soArray(Nullable(String))is[]*stringandMap(String, Nullable(UInt8))ismap[string]*uint8.duckdb-go/v2): a list column isduckdb.Composite[[]T], which decodes the driver's[]any; the same list as a parameter is the plain slice, since a Composite is not accepted as an argument.duckdb.Decimal,duckdb.Interval,duckdb.Map; structs asmap[string]any.go-sql-spanner):sql.Null*scalars, arrays as slices of thespanner.Null*types the driver decodes by default,big.Rat/spanner.NullNumeric,civil.Date/spanner.NullDate,spanner.NullJSON.go-mssqldb): integers by width, exact numerics asstring, temporals astime.Time,mssql.UniqueIdentifier, binary and spatial types as[]byte.Named arguments. When every parameter of a query is bound by name (
@namefor SQL Server and Spanner,{name:Type}for ClickHouse) each is passed assql.Named.pq.Arraywrapping is limited to PostgreSQL underdatabase/sql, since the other drivers scan slices directly.Examples in their own module.
examples/is now a Go module that requires sqlc through areplace, so the drivers the examples run against are its dependencies alone (sqlc'sgo.modloses them, andlib/pq, which only the examples imported) and cgo stays on for the DuckDB driver. Anauthorsexample for ClickHouse, DuckDB, SQL Server and Spanner sits beside the existing ones. Test helpers inexamples/internal/localcreate a database per test on the server named byCLICKHOUSE_SERVER_URI,MSSQL_SERVER_URIorSPANNER_SERVER_URIand skip otherwise.CI. ClickHouse and SQL Server run as job services.
sqlc-test-setupinstalls Spanner Omni from its standalone server release (pinned SHA-256) and starts a single server on port 15000, so the Spanner example runs too. The end-to-end module builds with cgo for the DuckDB golden, and the examples module is tested from its own directory.docker-composegains a ClickHouse service beside the SQL Server and Spanner Omni ones.Verified
TestExamples,TestFormat, vet and gofmt pass; no existing golden changed.internal/endtoend/testdatacompiles with the drivers in itsgo.mod, including the DuckDB case under cgo.CGO_ENABLED=0.Not verified locally: the Spanner example and the Omni install/start in
sqlc-test-setup. The Omni server opens IPv6 sockets at startup, which the sandbox this was written in refuses, so CI is its first real run.Left for later
CREATE TYPE ... AS ENUM, SQL ServerCREATE TYPE ... FROM) are not in the core-path catalog and map toany.coreResultCatalogreportspublicas every engine's default schema.sqlc-gen-goneeds the same proto field before it can read the tree.Cost to note
The Spanner Omni server release is about 276 MB per CI run, and the DuckDB driver's linux/amd64 bindings about 30 MB, paid by the test job.
🤖 Generated with Claude Code
https://claude.ai/code/session_015rfiHyC4iRLyRWc3UmYEtT