-
Notifications
You must be signed in to change notification settings - Fork 991
Add database engine plugins (external engines) #4247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
asmyasnikov
wants to merge
43
commits into
sqlc-dev:main
Choose a base branch
from
ydb-platform:engine-plugin
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
+2,110
−67
Conversation
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
asmyasnikov
commented
Dec 28, 2025
…c with new databases (in addition to PostgreSQL, Dolphin, sqlite)
fd040bc to
6c5b9a6
Compare
a5131b5 to
7609ebc
Compare
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
asmyasnikov
commented
Jan 27, 2026
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.
This PR resolves issue #4158 and adds support for database engine plugins: external processes that implement a single
ParseRPC and allow sqlc to work with databases that are not built-in (e.g. CockroachDB, TiDB, or custom SQL dialects). The plugin contract is deliberately minimal: no AST, no compiler in the middle, and a straight path from plugin output to codegen.Motivation
sqlc is widely used, and requests to add support for new databases come in regularly: feat(clickhouse): add ClickHouse database engine support #4244, feat(sqlserver): add Microsoft SQL Server support #4243, YDB support (draft) #4090, Add ClickHouse Engine Support to sqlc #4220, Adding support for CockroachDB #4009.
Supporting new database engines increases maintenance burden on core maintainers (parsers, catalogs, dialects, and compatibility for each engine).
Exposing a way to add external database engines (Refactor project for easy adding support of new database engine without sqlc core changes #4158) lets the community ship new backends without growing sqlc’s core, and lets users adopt sqlc for more SQL dialects.
What should an engine plugin do?
internal/sql/astpublic), we risk that a new dialect cannot be expressed with the existing AST node types. This PR therefore does not have the plugin pass an AST into sqlc’s core.schema.sqlor database connection parameters into the plugin.With this design, the engine plugin system can support many SQL dialects, while validation and enrichment of SQL (e.g.
*expansion) are the plugin’s responsibility, not the core’s.Pipeline: built-in engine vs external plugin
The choice between "built-in engine" and "external plugin" is made once per
sql[]block ininternal/cmd/process.go, based onengine:in config: if it issqlite,mysql, orpostgresql→ built-in path; if it is a name listed under top-levelengines:→ plugin path. (Vet has no plugin logic; for plugin-engine blocks it fails with "unknown engine".)flowchart TB subgraph input["Input (per sql[] block)"] schema[schema.sql] queries[queries.sql] end start["processQuerySets()"] --> input input --> branch{"engine for this sql[]"} branch -->|"sqlite / mysql / postgresql"| builtin["Built-in path"] subgraph builtin_flow["Built-in path (parse → NewCompiler → Result)"] direction TB parser[Parser] ast[(AST)] catalog[(Catalog)] compiler[Compiler] codegen_input_b[Queries + types] parser --> ast schema --> parser queries --> parser schema --> catalog ast --> compiler catalog --> compiler compiler --> codegen_input_b end builtin --> builtin_flow branch -->|"name in engines"| plugin_path["Plugin path"] subgraph plugin_flow["Plugin path (runPluginQuerySet)"] direction TB adapter[engine process runner] ext_parse["Engine plugin: Parse"] response["ParseResponse: sql, parameters, columns"] adapter -->|"stdin: ParseRequest"| ext_parse ext_parse -->|"stdout: ParseResponse"| response schema --> adapter queries --> adapter end plugin_path --> plugin_flow subgraph plugin_to_result["Adapter"] adapt["pluginResponseToCompilerQuery"] response --> adapt end builtin_flow --> merge[ProcessResult → codegen] plugin_flow --> plugin_to_result plugin_to_result --> merge subgraph output["Output"] codegen[Codegen plugin] end merge --> codegenBuilt-in path: Parser → AST, schema → Catalog; Compiler uses both and produces Queries + types. Codegen receives that as usual.
Plugin path: Engine process runner sends ParseRequest (sql + schema_sql or connection_params) to the external process; plugin returns ParseResponse (sql, parameters, columns). That is passed through a thin adapter into the same ProcessResult/codegen shape. No AST, no compiler — validation and enrichment are the plugin's job.
No intermediate AST: the plugin returns already “resolved” data (SQL text, parameters, columns).
No compiler for the plugin path: type resolution,
*expansion, and validation are the plugin’s job. sqlc does not run the built-in compiler on plugin output.Data from the plugin is passed through to the codegen plugin as-is (or after a thin adapter that today still produces a synthetic
[]ast.Statementfor compatibility; the useful payload issql+parameters+columns).So: for external engines, the pipeline is effectively schema + queries → engine plugin (Parse) → (sql, parameters, columns) → codegen, with no AST and no compiler in between.
Call flow (built-in vs plugin)
The split between built-in and plugin engines happens in
internal/cmd/process.goinsideprocessQuerySets(), which branches onconfig.IsBuiltinEngine(combo.Package.Engine).Built-in path: For
engine: sqlite | mysql | postgresql,processQuerySetscallsparse()(defined ininternal/cmd/generate.go).parse()callscompiler.NewCompiler(sql, combo, parserOpts).NewCompilerininternal/compiler/engine.gohas only three cases (SQLite, MySQL, PostgreSQL) anddefault: return nil, fmt.Errorf("unknown engine: %s", conf.Engine); there is no plugin branch and noFindEnginePluginin the compiler.Plugin path: For any other
engine:(e.g. a name defined underengines:in config),processQuerySetscallsrunPluginQuerySet()ininternal/cmd/plugin_engine.go.parse()andNewCompilerare not used; the engine process runner talks to the external plugin and passes the result into the same codegen pipeline.Vet: The vet command uses its own path and calls
parse()/NewCompiler(), so vet fails for plugin engines with "unknown engine" (there is no plugin-specific branch in vet).Summary: The branch and orchestration live in
process.go. Built-in logic goes throughgenerate.go→compiler/engine.go; plugin logic is inplugin_engine.go.No intermediate AST for external plugins
The plugin does not return an AST or “statements + AST”:
sql(possibly with*expanded),parameters,columns.The plugin is the single place that defines how the query is interpreted. sqlc does not parse or analyze that SQL again; it forwards the plugin’s
ParseResponsetoward codegen. Any internal use of[]ast.Statementfor the plugin path is a compatibility shim; the semantics are driven by the plugin’ssql/parameters/columns.No compiler for external plugins
The built-in compiler (catalog, type resolution, validation, expansion of
*) is not used for external engine plugins:SELECT *if desired.parametersandcolumnsthe codegen expects.What is sent to and returned from the plugin
Invocation: one RPC,
Parse, over stdin/stdout (protobuf).Example:
sqlc-engine-mydb parsewithParseRequeston stdin andParseResponseon stdout.Sent to the plugin (
ParseRequest)sqlqueries.sqlor the current batch).schema_sqlschema.sql.connection_paramsExactly one of
schema_sqlorconnection_paramsis used per request, depending on how the project is configured (see below).Returned from the plugin (
ParseResponse)sqlSELECT *expanded to explicit columns.parametersdata_type, nullable, is_array, array_dims.columnsdata_type, nullable, is_array, array_dims, optional table/schema.These three are enough for codegen to generate type-safe code without an AST or compiler step.
How the schema is passed into the plugin
Schema is provided to the plugin in one of two ways, via
ParseRequest.schema_source:Schema-based (files)
schema: "schema.sql") and passes their contents asschema_sql(a string) inParseRequest.CREATE TABLE ...) and uses it to resolve types, expand*, etc.Database-only
connection_params(DSN + optional extra options) inParseRequest.INFORMATION_SCHEMA/pg_catalog) to resolve types and columns.So: schema is either “schema.sql as text” or “connection params to the database”; the plugin chooses how to use it.
Changes in
sqlc.yamlNew top-level
enginesPlugins are declared under
enginesand referenced by name insql[].engine:engines: list of named engines. Each hasnameand eitherprocess.cmd(and optionallyenv) or a WASM config.sql[].engine: for that SQL block, use the engine namedmydb(which triggers the plugin) instead ofpostgresql/mysql/sqlite.So the only new concept in config is “define engines (including plugins) by name, then point
sql[].engineat them.” Schema and queries are still configured persql[]block as today.Who handles sqlc placeholders in queries
Support for sqlc-style placeholders (
sqlc.arg(),sqlc.narg(),sqlc.slice(),sqlc.embed(), etc.) is entirely up to the plugin:ParseRequest.sql.parameters(and, if needed, insqlor in how it uses schema). There is no separate “sqlc placeholder” pass in the core for the plugin path.So: the database engine plugin is responsible for understanding and handling sqlc placeholders for its engine.
Summary for maintainers
Parse(sql, schema_sql | connection_params) → (sql, parameters, columns).schema_sql(file contents) or asconnection_params(DSN) inParseRequest.engines[]+sql[].engine: <name>; existingschema/queries/codegenstay as-is.This keeps the plugin API small and leaves type resolution and dialect behavior inside the plugin, while still allowing sqlc to drive generation from a single, well-defined contract.