-
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
44
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
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"] schema[schema.sql] config[sqlc.yaml] queries[queries.sql] end subgraph sqlc_core["SQLC"] direction TB subgraph builtin_flow["Built-in Engine processing"] direction TB parser["Parse (postgresql, mysql, sqlite)"] ast[(AST)] catalog[(Catalog)] compiler[Compiler] convert_compiler_results_to_codegen[Queries + types] parser --> ast parser --> catalog schema --> parser queries --> parser ast --> compiler catalog --> compiler compiler --> convert_compiler_results_to_codegen end subgraph plugin_flow["Plugin Engine processing"] direction TB adapter["Engine process runner"] ext_parse["call Parse from external Engine"] convert_plugin_response_to_codegen["Queries + types"] adapter --> |"ParseRequest"| ext_parse schema --> adapter queries --> adapter ext_parse --> |"ParseResponse"| convert_plugin_response_to_codegen end branch --> builtin_flow["Built-in"] branch --> plugin_flow end config --> branch{"Engine Type"} convert_plugin_response_to_codegen --> codegen convert_compiler_results_to_codegen --> codegen subgraph output["Codegen"] codegen[Codegen plugin] endBuilt-in path: Parser → AST, schema → Catalog; Compiler uses both and produces Queries + types. Codegen receives that as usual.
Plugin path: Engine process runner sends one ParseRequest per query file:
sqlis the entire contents of that file, plusschema_sqlorconnection_params. The plugin returns ParseResponse.statements — one Statement per query block in the file (each with name, cmd, sql, parameters, columns). Each statement is turned into a compiler query and passed into the same ProcessResult/codegen path, so N blocks → N codegen queries (N helpers). 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 via a thin adapter that maps each Statement to a compiler query (name, cmd, sql, parameters, columns). N statements → N queries → N generated helpers.
So: for external engines, the pipeline is schema + full query file → engine plugin (Parse) → N statements → N codegen queries, 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:
sql) + schema (or connection). One Parse call per query file.repeated Statement statements. Each Statement has:name,cmd(enum::one/:many/:exec/ etc.),sql,parameters,columns. The plugin returns one Statement per query block in the file.The plugin is the single place that defines how the query is interpreted. sqlc does not parse or analyze that SQL again; it maps each Statement to a codegen query and forwards them. N blocks in the file → N statements → N generated helpers.
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.Call pattern: sqlc issues one Parse call per query file. For each file (e.g.
query.sql), the full file contents are sent in a single request. The plugin returns one Statement per query block in that file.Sent to the plugin (
ParseRequest)sql-- name: X :cmdcomments).schema_sqlschema.sql.connection_paramsExactly one of
schema_sqlorconnection_paramsis used per request, depending on project configuration.Returned from the plugin (
ParseResponse)statementsEach Statement has:
name-- name: GetUseretc.).cmdCmd(CMD_ONE,CMD_MANY,CMD_EXEC,CMD_EXEC_RESULT,CMD_EXEC_ROWS,CMD_EXEC_LAST_ID,CMD_COPY_FROM,CMD_BATCH_EXEC,CMD_BATCH_MANY,CMD_BATCH_ONE). Matches sqlc’s:one,:many,:exec, etc.sql*expanded).parameterscolumnsThere is no top-level
sql/parameters/columns; the response is onlystatements. N blocks in the file → N statements → N codegen queries (N helpers).Helpers in
pkg/enginefor plugin authorsTo split the query file and parse
"-- name: X :cmd"lines in the same way as built-in engines, the engine package provides (optional) helpers that delegate tointernal/metadata:CommentSyntax— which comment styles to accept (Dash, SlashStar, Hash).ParseNameAndCmd(line, syntax)— parses a line like"-- name: ListAuthors :many"→(name, cmd Cmd, ok).QueryBlocks(content, syntax)— splits file content into[]QueryBlock(Name, Cmd, SQL).StatementMeta(name, cmd, sql)— builds a*Statementwith name/cmd/sql set; the plugin fills parameters and columns.CmdToString(c)/CmdFromString(s)— convert between enumCmdand strings like":one",":many".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(full_query_file_content, schema_sql | connection_params) → ParseResponse{ statements }.repeated Statement statements. Each Statement hasname,cmd(enum),sql,parameters,columns. No top-level sql/parameters/columns.schema_sql(file contents) or asconnection_params(DSN) inParseRequest.engines[]+sql[].engine: <name>; existingschema/queries/codegenstay as-is.pkg/engine(ParseNameAndCmd, QueryBlocks, StatementMeta, Cmd enum) reuseinternal/metadataso plugin behavior matches built-in engines.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.