sqlc 命名参数(Named Parameters)包深度解析:从 `sqlc.arg()` 到原生占位符的编译管线
开发工具代码生成数据库【免费下载链接】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),仅供参考

相关新闻

GraalVM TRegex 版本演进与语言集成指南:从 Changelog 到源码的完整解读

GraalVM TRegex 版本演进与语言集成指南:从 Changelog 到源码的完整解读

GraalVM TRegex 版本演进与语言集成指南:从 Changelog 到源码的完整解读 【免费下载链接】graal GraalVM compiles applications into native executables that start instantly, scale fast, and use fewer compute resources 🚀 项目地址: https://g…

2026/9/21 15:20:24 阅读更多 →
Plotly Treemap 图表完全指南:从 px.treemap 到 go.Treemap 的分层数据可视化

Plotly Treemap 图表完全指南:从 px.treemap 到 go.Treemap 的分层数据可视化

Plotly Treemap 图表完全指南:从 px.treemap 到 go.Treemap 的分层数据可视化 【免费下载链接】plotly.py The interactive graphing library for Python :sparkles: 项目地址: https://gitcode.com/gh_mirrors/pl/plotly.py Treemap(树状矩形图&…

2026/9/21 15:20:24 阅读更多 →
MXNet CPU pip 包安装指南:平台支持、libquadmath 依赖与安装验证

MXNet CPU pip 包安装指南:平台支持、libquadmath 依赖与安装验证

MXNet CPU pip 包安装指南:平台支持、libquadmath 依赖与安装验证 【免费下载链接】mxnet Lightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript…

2026/9/21 15:20:24 阅读更多 →

最新新闻

3个实战项目教你避开范冰冰的微博接口报错

3个实战项目教你避开范冰冰的微博接口报错

3个实战项目教你避开范冰冰的微博接口报错 刚把那个爬取范冰冰微博历史数据的脚本跑起来,控制台直接喷了一屏幕的红色 StackTrace。看着那一串 ConnectionError , TimeoutError , 还有莫名其妙的…

2026/9/22 17:05:25 阅读更多 →
3个腹部穴位定位坑点,面试必问的实战排查指南

3个腹部穴位定位坑点,面试必问的实战排查指南

3个腹部穴位定位坑点,面试必问的实战排查指南 版本升级后 API 全变了,你盯着屏幕上的 NullPointerException…

2026/9/22 17:05:25 阅读更多 →
3分钟搞定AirPods序列号校验,图解原理拒绝报错

3分钟搞定AirPods序列号校验,图解原理拒绝报错

3分钟搞定AirPods序列号校验,图解原理拒绝报错 看了一堆教程还是不会写项目?别急,这次我们用图解原理彻底讲透。 很多开发者拿到一批 AirPods…

2026/9/22 17:05:25 阅读更多 →
3个狠招让老汉播放器流畅运行,2026最新性能优化实战

3个狠招让老汉播放器流畅运行,2026最新性能优化实战

3个狠招让老汉播放器流畅运行,2026最新性能优化实战 面试被问“为什么你的视频播放器在低端机上卡顿严重”,你支支吾吾答不上来,心里发虚。 2026最新的技术迭代已经让“能播”不再是及格线,“丝滑”才是硬道理。…

2026/9/22 17:05:25 阅读更多 →
3个CD Key生成坑导致崩溃?源码解析教你避坑

3个CD Key生成坑导致崩溃?源码解析教你避坑

3个CD Key生成坑导致崩溃?源码解析教你避坑 版本升级后 API 全变了,原本能跑通的 License 校验逻辑突然报 403 Forbidden,后端日志里全是 Signature Mismatch…

2026/9/22 17:05:25 阅读更多 →
测验全流程解析与完整示例

测验全流程解析与完整示例

测验全流程解析与完整示例 版本升级后 API 全变了,老代码直接跑不通,这种痛谁懂?别慌,今天不整虚的,直接上 完整示例 ,把【测验】这块硬骨头掰碎了揉烂了讲透。…

2026/9/22 17:04:25 阅读更多 →

日新闻

3台商务办公笔记本实测:手写实现环境配置,告别卡半天

3台商务办公笔记本实测:手写实现环境配置,告别卡半天

3台商务办公笔记本实测:手写实现环境配置,告别卡半天 配置环境就卡半天?别怪机器慢,多半是你没选对工具链。在Java、Go或Python的项目现场, 手写实现…

2026/9/22 0:00:41 阅读更多 →
剑帝加点速查手册:3分钟搞懂核心逻辑

剑帝加点速查手册:3分钟搞懂核心逻辑

剑帝加点速查手册:3分钟搞懂核心逻辑 面试被问原理答不上来,是不是常态?别慌。很多开发者对着 GitHub 开源仓库里的代码发呆,看似简单实则暗藏玄机。今天这份【剑帝加点】速查手册,直接带你拆解核心实现,把面试必考的原理讲透。…

2026/9/22 0:00:41 阅读更多 →
手写实现图片压缩网站核心:搞定WebP转换与质量调优

手写实现图片压缩网站核心:搞定WebP转换与质量调优

手写实现图片压缩网站核心:搞定WebP转换与质量调优 复制来的代码跑不通不知道怎么调?别慌,这种“复制粘贴地狱”在开发圈太常见了。尤其是做 图片压缩网站…

2026/9/22 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/22 4:32:41 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/22 4:38:57 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/22 8:51:04 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/21 15:36:51 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/21 15:36:51 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/22 2:43:42 阅读更多 →