开发工具代码生成数据库【免费下载链接】sqlcGenerate type-safe code from SQL项目地址https://gitcode.com/gh_mirrors/sq/sqlc点击查看免费下载导读在 sqlc 中查询参数可以通过sqlc.arg()、sqlc.narg()、sqlc.slice()以及name等 sqlc 专有语法书写也可以在编译期由编译器根据模式schema推断可空性。internal/sql/named包是这条管线上的参数模型层它把参数的名字、可空性、是否为sqlc.slice()参数统一建模为Param并用ParamSet把占位符编号与参数一一映射供编译器的类型推断和代码生成使用。阅读本文后你将理解 sqlc 的参数是如何在预处理阶段被重写为各引擎原生占位符的、可空性合并的优先级规则、不同方言下同名参数编号行为的差异以及 MySQL 用户变量var与 sqlc 命名参数param的关键区别。本文以 internal/sql/named/CLAUDE.md 为主线结合 param.go、param_set.go、preprocess.go 等源码展开。包的定位参数模型的单一事实来源按照internal/sql/named/CLAUDE.md的说明internal/sql/named包只负责建模一条查询语句的参数它们的名字name、可空性nullability以及是否来自sqlc.slice()。它不再关心 sqlc 的任何语法细节——sqlc.arg()、sqlc.narg()、sqlc.slice()和name都会在引擎解析器运行之前由 internal/sql/preprocess 重写为原生占位符而ParamSet正是在该包重写的过程中被构建出来的。也就是说包内没有任何 SQL 解析逻辑它向上承接预处理器的重写结果向下为编译器提供查询参数的类型推断输入是整个 sqlc 参数处理链条中的数据模型层。包内文件结构如下internal/sql/named/ ├── CLAUDE.md # 包设计说明本文主线 ├── param.go # Param 与可空性建模 ├── param_set.go # ParamSet 占位符编号映射 ├── param_test.go # Param 可空性合并测试 └── param_set_test.go# ParamSet.Add 编号分配测试Param一个参数的完整画像Paramparam.go代表查询的一个输入参数它既可以来自位置参数如$1也可以来自命名参数运算符param还可以来自命名参数函数调用sqlc.arg(param)。其结构体由三个字段组成type Param struct { name string nullability nullability isSqlcSlice bool }name用户可见的参数名nullability可空性位掩码见下节isSqlcSlice是否为sqlc.slice()参数。四种构造器包提供了四种公开构造器对应参数的不同来源构造器来源初始可空性NewParam(name)sqlc.arg()或name未指定nullUnspecifiedNewUserNullableParam(name)sqlc.narg()始终可空nullableNewSqlcSlice(name)sqlc.slice()未指定isSqlcSlice trueNewInferredParam(name, notNull)编译器根据 schema 推断inferredNotNull/inferredNull从源码可以看到NewInferredParam根据notNull布尔值把可空性设为inferredNotNull或inferredNull而NewSqlcSlice则只是把未指定可空性的参数标记为 slice 参数。可空性的位掩码表示nullability是一个int类型的位掩码这正是可以按位 OR 合并设计的关键param.goconst ( nullUnspecified nullability 0b0000 inferredNull nullability 0b0001 inferredNotNull nullability 0b0010 nullable nullability 0b0100 notNullable nullability 0b1000 )五种状态分别表示未指定、推断为可空、推断为不可空、用户指定可空、用户指定不可空。由于每个状态独占一个 bit任意两个来源的 nullability 都可以通过按位 OR 无损合并。NotNull 的裁决顺序Param.NotNull()param.go按照用户指定优先于推断的优先级链做出最终裁决用户指定不可空notNullable→ 返回true用户指定可空nullable→ 返回false推断不可空inferredNotNull→ 返回true推断可空inferredNull→ 返回false完全未指定 → 默认返回false可空这与大多数数据库的默认行为一致。也就是说sqlc.narg()强制可空、推断为 NOT NULL 的参数遇到用户指定的可空性时以用户为准用户的可空性要求永远压过编译器的推断。mergeParam无顺序依赖的可空性合并mergeParam(a, b)param.go把两个部分指定的参数合并成一个完整参数名字优先取a的名字若a为空则取b的TestMergeParamName验证了非空名字优先的规则可空性直接按位 ORa.nullability | b.nullability因此两个来源到达的顺序不影响结果——这是TestMergeParamNullability中再尝试Combine(b, a)应得到相同结果这一断言的理论基础isSqlcSlice只要任一方为 slice 参数即为 true。param_test.go中的TestMergeParamNullability覆盖了关键场景尤其是推断与用户定义冲突的情形合并场景结果未指定 推断不可空不可空未指定 推断可空可空未指定 用户可空可空推断可空 用户可空可空推断不可空 用户可空可空用户优先ParamSet占位符编号 ↔ 参数映射表ParamSetparam_set.go为单条语句维护占位符编号到Param的映射。其内部包含四张结构hasNamedSupport该引擎是否支持命名参数namedParams map[string]Param当前跟踪的命名参数集合namedLocs map[string][]int每个名字出现过的位置列表positionToName map[int]string编号到名字的反向映射argn已检查过的位置参数计数。使用方式ps : named.NewParamSet(numbersAlreadyUsed, hasNamedSupport) n : ps.Add(named.NewParam(author_id)) // 返回占位符编号NewParamSet的第一个参数是已经用掉的编号集合如用户手写的$1、$5这些位置会被预填为无名参数编号分配从 1 开始向后寻找空位nextArgNum第二个参数hasNamedSupport决定同名参数是否共享编号。Add 的编号分配语义Addparam_set.go的分配策略直接体现方言差异支持命名参数hasNamedSupport true重复出现的同一个名字直接返回第一次分配到的编号namedLocs[name][0]即同名共享一个编号不支持命名参数hasNamedSupport false每次出现都通过nextArgNum拿到新的编号即每次出现各占一个编号。param_set_test.go中的TestParamSet_Add用表格测试完整验证了这一行为命名集named重复添加p1两次都返回 1p2返回 2非命名集unnamedp1分别返回 1、2p2返回 3、4——每次出现都递增预填编号集populatedNamed已用 1/2/4/5/6p1返回 3p2返回 7且重复添加结果不变预填编号的非命名集populatedUnnamedp1返回 3、7p2返回 8、9。编译器读回的两个 API编译器通过两个方法把ParamSet读回NameFor(number)返回某占位符编号对应的用户可见名字FetchMerge(number, inferred)把编号对应的已记录参数与编译器推断出的默认参数合并返回合并后的参数以及它是否为命名参数的布尔值。FetchMergeparam_set.go在编号不存在或对应无名参数时直接返回传入的mergeP与false——这正是 resolve.go 中addUnknownParam的用法用NewInferredParam(ref.name, false)作为默认值合并后若isNamed为真则标记IsNamedParam。方言差异hasNamedSupport的由来为什么会有hasNamedSupport这个开关dialect.go 中的注释给出了答案// hasNamedSupport reports whether repeated uses of the same parameter name can // share a single placeholder. MySQL sends an argument per ?, so every // occurrence needs its own number. func (d Dialect) hasNamedSupport() bool { return d.Style ! StyleQuestion }MySQL、ClickHouse采用StyleQuestion每个?单独传一个参数因此hasNamedSupport false名字的每次出现各占一个编号PostgreSQL$1、SQLite?1等编号方言同名参数可以共享同一个编号hasNamedSupport true。number函数preprocess.go正是据此分支?方言从空集开始、按源码顺序把所有出现含原生?与重写出的 sqlc 参数统一编号编号方言则保留用户手写的编号$1、$5等用NewParamSet(numbs, d.hasNamedSupport())填平缺口后再为sqlc.arg()/sqlc.narg()/sqlc.slice()分配编号。预处理器如何构建 ParamSet尽管本包不处理语法理解ParamSet的产生过程有助于把握整个管线。internal/sql/preprocess在引擎解析器之前把 sqlc 语法重写为原生 SQL详见 internal/sql/preprocess/CLAUDE.md转换关系如下sqlc 语法重写为sqlc.arg(name)/sqlc.narg(name)方言的原生占位符sqlc.slice(name)包裹/*SLICE:name*/的占位符sqlc.embed(table)table.*name原生占位符为 sqlc 语法的方言各引擎的原生占位符与name处理引擎占位符namepostgresql$1sqlc 语法mysql?用户变量原样保留sqlite?1sqlc 语法在 preprocess.go 的Dialected流程中number(d, occs)为每条语句构建Statement.Params一个*named.ParamSetparam(occ)则根据 occurrence 的种类选择构造器func param(occ *occurrence) named.Param { switch occ.kind { case kindNarg: return named.NewUserNullableParam(occ.name) case kindSlice: return named.NewSqlcSlice(occ.name) default: return named.NewParam(occ.name) } }即sqlc.narg()产生用户可空参数、sqlc.slice()产生 slice 参数、sqlc.arg()/name产生未指定可空性的普通参数。语句预处理失败时也会构建一个空的ParamSetNewParamSet(nil, d.hasNamedSupport())保证下游调用方拿到的永远是有效对象preprocess.go。编译器侧的回读FetchMerge 的实际调用FetchMerge在编译器中被广泛使用。resolve.go 在解析各类表达式时都会调用它把预处理器记录的参数信息与编译器针对具体表达式推断出的默认参数合并limitOffset/limitCount默认推断为offset/limitnotNull true数据类型integer未知参数addUnknownParam默认推断为可空数据类型any二元表达式默认推断||为text类型等。合并后的p.NotNull()、p.IsSqlcSlice()、p.Name()分别决定最终参数的不可空性、slice 标记与用户可见名字resolve.go。而在 parse_core.go 中FetchMerge被用于合并 schema 列推断出的可空性if param, isNamed : params.FetchMerge(p.Number, named.NewInferredParam(col.Name, p.NotNull)); isNamed { ... col.IsSqlcSlice param.IsSqlcSlice() }最终analyze.go 把IsSqlcSlice透传到分析结果供代码生成阶段决定参数是否以 slice展开为IN (...)多值形式处理。MySQLvariable与 sqlcparam的边界internal/sql/named/CLAUDE.md特别强调了一个易混淆点name只有在预处理器的方言声明它是 sqlc 语法时才是 sqlc 语法。在 dialect.go 中MySQL 的AtSign被设为false而 PostgreSQL、SQLite 为trueGoogleSQL 虽未在dialects表中列出但按 CLAUDE.md 说明同样把name视为命名参数。于是同一个写法在不同引擎下含义截然不同-- PostgreSQL 中 是 sqlc 语法 SELECT * FROM users WHERE id user_id -- 预处理后为: SELECT * FROM users WHERE id $1 -- MySQL 中 是用户变量 SELECT * FROM users WHERE id ! user_id -- 保持不变: SELECT * FROM users WHERE id ! user_idMySQL 场景下user_id会被转换为ast.VariableExpr原样到达解析器绝不参与 sqlc 的参数重写。这一设计避免了 sqlc 语法与 MySQL 原生用户变量语义的冲突——在 MySQL 中请改用sqlc.arg(user_id)来书写命名参数。小结与扩展阅读internal/sql/named以极小的表面积两个核心类型、四种构造器、两个回读 API完成了 sqlc 参数建模的全部工作Param用位掩码可空性统一了用户指定与编译器推断两种来源且用户优先级更高mergeParam的按位 OR 语义让合并顺序无关保证可空性裁决确定可复现ParamSet通过hasNamedSupport优雅地适配了?方言与编号方言在同名参数是否共享编号上的本质差异预处理器构建、编译器消费FetchMerge/NameFor是两者间的唯二接口。想深入验证以上行为可以直接运行本包的单元测试go test ./internal/sql/named -v go test ./internal/sql/preprocess -update # 重新生成预处理 golden 文件仅开发时使用进一步阅读预处理器的完整设计见 internal/sql/preprocess/CLAUDE.md编译器对参数的消费见 internal/compiler/resolve.go 与 internal/compiler/parse_core.go端到端测试语料位于 internal/endtoend/testdata如sqlc_arg、sqlc_narg、sqlc_slice等目录。赞分享开发工具代码生成数据库【免费下载链接】sqlcGenerate type-safe code from SQL项目地址https://gitcode.com/gh_mirrors/sq/sqlc点击查看免费下载相关推荐libpqxx 语句参数Statement Parameters深度指南从 $1 占位符到动态参数列表libpqxx 语句参数Statement Parameters深度指南从 $1 占位符到动态参数列表 导读 本文以 libpqxx 7.7.3 官方文档网络通信后端babel/plugin-transform-parameters 深度解析默认参数与 Rest 参数到 ES5 的编译实现babel/plugin transform parameters 深度解析默认参数与 Rest 参数到 ES5 的编译实现 本指南以 packages/b编译器开发工具Meteor TypeScript 包深度解析从编译管线到特性边界Meteor TypeScript 包深度解析从编译管线到特性边界 导读 本篇文章围绕 Meteor 官方 typescript 编译器插件包展开讲解它在后端前端开发工具移动开发上一篇攻克Nuxt项目中的TypeScript配置兼容性难题从报错到丝滑开发的实战指南下一篇Toast-Swift样式自定义完全手册打造独一无二的toast外观创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考