:将 sqlc.arg、sqlc.narg、sqlc.slice、sqlc.embed 改写成原生 SQL 的完整原理与实践)
sqlc 查询语法预处理Preprocess将 sqlc.arg、sqlc.narg、sqlc.slice、sqlc.embed 改写成原生 SQL 的完整原理与实践【免费下载链接】sqlcGenerate type-safe code from SQL项目地址: https://gitcode.com/gh_mirrors/sq/sqlc导读在 sqlc 中sqlc.arg(name)、sqlc.narg(name)、sqlc.slice(name)、sqlc.embed(table)以及name这些写法并不是标准 SQL而是 sqlc 定义的查询语法。本文以仓库中 internal/sql/preprocess/CLAUDE.md 为核心指南结合其源码dialect.go、lexer.go、scan.go、preprocess.go与 golden 测试用例系统讲解 preprocess 包如何在任何引擎解析器看到查询之前把这些 sqlc 语法重写为各引擎的原生占位符并记录一张副作用表side table供编译器使用。读完本文你将理解 sqlc 参数/嵌入语法的改写规则、各方言的占位符风格差异、错误处理与坐标映射机制以及如何为新增方言接入预处理、如何用测试用例验证改写结果。为什么需要预处理让引擎只看到合法 SQLsqlc 需要支持 PostgreSQL、MySQL、SQLite 等多个引擎而每个引擎都有自己独立的 SQL 解析器。历史实现中sqlc.arg()这类写法会一直存活到引擎解析器内部再通过遍历产生的 AST 来替换这意味着每个引擎都必须往返处理一个它并不理解的、带 schema 限定的函数调用。preprocess 包改变了这一局面它在解析器之前用方言参数化的 lexer扫描查询文本把每个 sqlc 构造替换成引擎的原生占位符并记录替换了哪些内容。替换完成后文本中不再残留任何 sqlc 语法引擎解析器、转换器以及astutils遍历所接触到的都是干净的合法 SQL。这一点在 preprocess.go 的包注释中写得非常明确。重写了什么sqlc 查询语法 → 原生占位符预处理负责重写的 sqlc 语法及其产物如下表sqlc 语法改写结果sqlc.arg(name)/sqlc.narg(name)该方言的原生占位符sqlc.slice(name)占位符外面包裹/*SLICE:name*/标记sqlc.embed(table)table.*name原生占位符仅在属于 sqlc 语法的方言中各引擎的原生占位符风格定义于 dialect.go 的Style类型引擎占位符namepostgresql$1sqlc 语法mysql?用户变量原样保留sqlite?1sqlc 语法需要特别强调的是GoogleSQL 与 ClickHouse 是刻意缺席的。它们自行处理参数语法因此File对它们直接返回原始源码见 preprocess.go 中DialectFor查表失败即原样返回的逻辑sqlc 语法对它们不可用——sqlc.arg()会以它看起来的样子一个函数调用原样到达解析器。这不是疏漏而是有意的设计选择。工作原理三步流水线File(engine, src)是预处理入口其完整流程见 preprocess.go 的Dialected函数如下查方言通过DialectFor(engine)从 dialect.go 的dialects表中查找该引擎的词法规则没有条目的引擎GoogleSQL、ClickHouse原样返回源码。切分语句lexer 遍历整个文件在顶层分号处切分成语句片段lexer.go 的statements每个字节恰好属于一条语句字符串或注释内的分号不会被误切。逐语句处理对每条语句执行三步——scan按源码顺序收集所有 sqlc 构造以及所有原生占位符scan.gonumber为占位符编号preprocess.go语句被重写进输出缓冲区同时一条Statement记录下改动内容。下面拆解这三步的实现细节。scan识别 sqlc 构造与原生占位符scan对单条语句逐字节扫描scan.go识别四类目标sqlc.xxx(...)调用识别到标识符sqlc大小写不敏感后调用sqlcCall解析出函数名与参数scan.go。函数名通过matchParen匹配括号、splitArgs在顶层逗号处切分参数未知函数如sqlc.argh会产生function does not exist错误。name仅当该方言的AtSign为 truePostgreSQL、SQLite时才视为 sqlc 命名参数MySQL 中name是用户变量不动。$1仅当DollarNumber为 truePostgreSQL时才视为绑定参数。?/?1仅当Question为 trueMySQL、SQLite时才识别。参数本身argument函数scan.go可以是裸标识符引用、字符串常量或带引号的标识符并处理了sqlc.embed(schema.table)这类限定引用——限定引用会被拍平成单一名称拼接时去掉点号与历史 AST 改写拼接 ColumnRef 各部分的语义保持一致。number占位符编号策略编号逻辑因方言风格而异preprocess.go?风格MySQL每个?都是独立参数因此所有占位符含用户手写的按源码顺序一起重新编号每个参数独占一个位置。编号风格PostgreSQL$n、SQLite?n保留用户写下的编号并填补空缺同时支持具名参数——同一参数名重复出现可以共享同一个占位符这正是 dialect.go 中hasNamedSupport的含义StyleQuestion之外的方言都支持。sqlc.arg、sqlc.narg、sqlc.slice分别通过 named 包登记为普通参数、用户可空参数sqlc.narg即使 schema 声明 NOT NULL 也强制可空与 slice 参数preprocess.go。replacement生成原生替换文本每个 occurrence 的替换文本由replacement决定preprocess.gosqlc.embed(table)→table.*保留用户书写的引号形式例如sqlc.embed(MyTable)会生成MyTable.*PostgreSQL$Nslice 作为单个数组参数传递无需展开标记SQLite?N但 slice 生成/*SLICE:name*/?MySQL?slice 同样生成/*SLICE:name*/?。/*SLICE:name*/标记用于告知代码生成器该参数应展开为可变参数列表该序列在 internal/codegen/golang/field.go 中亦有复刻因为模板生成替换时需要用到它preprocess.go 的注释明确说明了这一耦合。副作用表side table编译器与改写的桥梁Result不仅输出改写后的文本还保留每条语句的元数据preprocess.goResult.Statement(offset)返回改写后文本中某偏移量所属的StatementParams语句的named.ParamSet记录参数名、编号与可空性Embeds每个被改写成table.*的 embed按位置键控使编译器能区分重写产生的星号与用户手写的星号EmbedSet.Find支持传入多个位置候选因为不同引擎填充的字段不同见 preprocess.goSlices每个sqlc.slice()占位符在改写文本中的位置Numbers位置 → 参数编号。引擎在 AST 转换时以自身顺序编号绑定参数编译器用这张表恢复源码顺序Dollar/ParamErr占位符风格校验——这部分逻辑原先位于validate.ParamRef如今前移至此见 preprocess.go编号与未编号占位符混用、编号出现空洞都会被拒绝Errsqlc 语法错误未知sqlc.*函数、参数个数错误、参数不是标识符或字符串。Result.Origin(offset)把改写文本中的偏移映射回原始源码偏移preprocess.go这样报错时能指向用户实际写下的内容而不是改写后的占位符。不变式Invariants三条安全保证预处理遵守三条重要不变式均有源码与测试双重保障重写绝不增加行数。所有替换都是单行的因此从改写文本取出的行号永远不会越过原文末尾但 sqlc 调用本身若跨多行改写后行数可能变少——此时应通过Origin映射偏移而不是轻信行号。TestRewrite中专门断言了rewrite added lines不成立preprocess_test.go。非法语句原样复制。验证失败firstError命中的语句被完整抄写进输出preprocess.go引擎仍解析用户写下的原文错误按语句粒度报告而非整个文件。注释与字面量内不重写任何内容。查询注解-- name: GetAuthor :one是注释因此永远安全同理字符串、引号标识符、反引号、PostgreSQL dollar-quoted 字符串内部均被 lexer 跳过。词法细节lexer 如何安全地无知lexer 不解析 SQL它只知道足够跳过那些不能改写 sqlc 语法的区域lexer.go行注释--、MySQL 的#、块注释/* */PostgreSQL 还支持嵌套、字符串字面量、引号标识符、反引号与 PostgreSQL dollar-quoted 字符串$$...$$/$tag$...$tag$。几个值得注意的方言差异实现反斜杠转义MySQL 默认在字符串中把反斜杠当作转义符skipQuoted的backslash参数PostgreSQL 仅对E...转义字符串启用lexer.go 的isEscapeString还小心地排除了E是更长标识符尾部的情况。$1vs$tag$dollarTag识别到$1数字紧跟$会判定为绑定参数而非标签lexer.go。MySQL 的双引号DoubleQuoteString为 true 时...按字符串字面量处理除非开启 ANSI_QUOTES这会影响sqlc.arg(name)这类双引号参数的解析方式scan.go。标识符折叠FoldIdentifier为 true 的方言PostgreSQL、MySQL会把裸引用参数名小写化例如sqlc.arg(FooBar)的参数名变为foobarSQLite 则保持大小写。这些规则全部集中在 dialect.go 的dialects表中——它是唯一存放词法规则的地方。实战示例三种方言下的改写前后以下用例均来自 testdata golden 目录可以直接运行go test ./internal/sql/preprocess验证。PostgreSQLsqlc.arg输入 arg/input.sql-- name: GetUser :one SELECT id FROM users WHERE name sqlc.arg(name);输出 arg/output.sql-- name: GetUser :one SELECT id FROM users WHERE name $1;对应的 arg/side_table.json 记录了参数name编号 1、位于改写文本偏移 56named: true。PostgreSQLsqlc.embed输入 embed/input.sqlSELECT sqlc.embed(users), sqlc.embed(pets) FROM users JOIN pets ON true;改写后为SELECT users.*, pets.* FROM users JOIN pets ON true;并在 side table 中按位置登记两个 embed。MySQLsqlc.slice输入 mysql/slice/input.sqlSELECT id FROM users WHERE id IN (sqlc.slice(ids));输出 mysql/slice/output.sqlSELECT id FROM users WHERE id IN (/*SLICE:ids*/?);/*SLICE:ids*/标记让代码生成器知道该?应展开成多个参数。错误路径sqlc.argh输入非法调用sqlc.argh(name)时语句原样保留错误按语句上报。测试期望 error_unknown_function/stderr.txt 为1:8: function sqlc.argh does not exist行号与列号经由Origin映射回原始源码指向用户真正书写的位置。测试机制golden 文件与 TestRewrite测试用例以目录组织每个用例一个目录preprocess_test.go文件内容input.sql查询文件output.sql改写后的 SQLside_table.json预处理记录的一切参数、embed、slice、编号、偏移映射stderr.txt报错内容仅输入非法时存在TestRewrite为每个目录运行一个子测试。side_table.json是通过编译器使用的同一套 API读回结果渲染的因此它覆盖了参数名与可空性、embed 与 slice 的区间、占位符编号和偏移映射——每个参数的location是它在改写文本中的偏移origin是它在原始文本中的来源偏移preprocess_test.go。重新生成所有 golden 文件只需一条命令go test ./internal/sql/preprocess -update测试用例覆盖了internal/endtoend中出现的每种 sqlc 语法形态按各被预处理引擎每个函数分别对裸引用、字符串常量、带引号标识符的用法各种占位符风格以及必须原样保留的构造注释、字面量、MySQL 用户变量与$1、PostgreSQL 的与?运算符。这也是对开发者的约定当你向测试语料库新增一种写法时记得同步在此补充对应用例。新增一个方言三步完成若要让新引擎支持 sqlc 语法只需在 dialect.go 的dialects表新增一条记录代码库其他任何地方都无需改动sqlc.arg/narg/slice/embed即可对该引擎生效。字段配置取决于新引擎的词法特征是否支持、dollar-quote、反引号、#注释、嵌套块注释、反斜杠转义、标识符折叠等。但文档同时给出了明确的告诫只有引擎应当支持 sqlc 语法时才添加。将某个引擎排除在dialects之外是有意为之的选择而非遗漏——正如 GoogleSQL 与 ClickHouse 一样让它们自行处理参数语法。小结sqlc 的 preprocess 包用方言参数化 lexer 文本重写 副作用表的组合把sqlc.arg、sqlc.narg、sqlc.slice、sqlc.embed与name统一收敛到引擎解析之前完成显著简化了各引擎的职责解析器只处理合法 SQL编译器通过 side table 与Origin映射获得全部上下文。这套设计的关键文件均可在此仓库中直接查阅preprocess.go入口与流水线、lexer.go词法跳过规则、scan.go构造识别、dialect.go方言表以及 testdata覆盖三引擎全部形态的 golden 用例。【免费下载链接】sqlcGenerate type-safe code from SQL项目地址: https://gitcode.com/gh_mirrors/sq/sqlc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考