psycopg2 的 AsIs 建表写法,交给 Codex 复查前先接 TaoToken,打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 拿一把 Key,再把 Codex 的执行通道换到兼容地址,就能让它逐行读那段 execute。原文的诉求其实很清楚:PostgreSQL 创建表时,schema 和表名都想当参数传进来,不在代码里写死public.user这种字符串。为了达到这个效果,作者用了AsIs(schema)和AsIs(table),把两个变量原样拼进create table %s.%s(name text)。这版代码能跑通,但「参数化」这三个字在这里被理解偏了——psycopg2 的%s占位符只对值生效,对表名、schema、列名这类标识符不生效。这篇不重写你的业务流程,只做一件事:把那段 execute 送进 Codex 做一次结构化复查,顺带把接入配置讲透。
1. AsIs 把 schema 和表名当参数,究竟绕过了什么
1.1 psycopg2 的 %s 只管值,不管标识符
写惯了cur.execute("insert into t values(%s)", (v,))的人,很容易顺推出「表名也能这么传」。但 PostgreSQL 的协议层面,参数化只覆盖数据值,不覆盖 SQL 语法树里的对象名。你写select * from %s,驱动不会把%s替换成一个被正确引用的表名,它会直接报语法错误,因为解析器在看到%s那一刻还没拿到参数。
所以标识符要动态化,只有两条路:要么自己拼字符串并负责加引号、转义,要么用驱动提供的标识符包装类型。psycopg2 给了两个:sql.Identifier和extensions.AsIs。名字上AsIs更短、用起来更直接,于是很多示例代码都选了它。
1.2 AsIs 的行为是「原样塞进去」,不是「安全地传进去」
看原始写法:
from psycopg2.extensions import AsIs cur.execute( "create table %s.%s(name text)", (AsIs(schema), AsIs(table)), )AsIs的名字就是它的全部行为:as-is,原样。它会把这个 Python 字符串直接写进 SQL 文本,不加双引号,不做标识符转义。这意味着三件事同时成立:
- 拼接结果完全由
schema和table的取值决定,只要这两个值来自外部(比如租户配置表、前端参数、URL 路径),注入面就敞开了; - 大小写敏感问题回来了。PostgreSQL 会把未加引号的标识符折叠成小写,你的
create table TenantA.UserB实际建出来的是tenanta.userb,事后按原样去查会找不到; - 保留字冲突。表名恰好是
order、user、group这类词时,语句直接语法错误。
1.3 多租户建表更容易放大这个问题
单库单 schema 的项目,表名通常是常量,AsIs看起来只是「省了个拼接」。真正危险的是多租户场景:schema 一般等于租户 ID,表名往往跟着业务模板走,这两者经常来自配置文件或数据库里的元数据表。你信任元数据表的内容,但元数据表本身也可能被写入脏数据;一旦某个租户的 schema 名里带了分号或者双引号,这条create table就跑出你预期之外的东西了。
这不是说AsIs完全不能用,而是说用它等于自己接管了「标识符引用」这件事,而绝大多数人并没有真的接管——只是把风险藏起来了。
2. 在 ~/.codex/config.toml 里把 Codex 接到 TaoToken
2.1 先去官网创建一把 API Key
复查代码这件事,最省事的执行工具是 Codex:给它贴一段代码和一条明确的问题,它能把改写理由和可运行版本一起给你。前提是它得有个能稳定调到模型的通道。
打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册,进控制台创建一把 API Key,复制出来备用。这个 Key 后面要填进 Codex 的环境变量,不要在代码里硬编码,也不要提交进仓库。顺手在模型广场记一下你打算用的模型 ID,后面填进配置。如果你想先确认 Key 可用,可以在 TaoToken 模型对话 里发一条测试消息。
2.2 config.toml 里的 model_provider 与 base_url
Codex 的配置文件是~/.codex/config.toml。这里要改的是供应商和地址,不是别的东西——注意 Codex 不读ANTHROPIC_BASE_URL那套环境变量,别把 Claude Code 的配置照搬过来。
model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "chat"几点说明:
base_url填https://taotoken.net/api,末尾不要带/v1。很多 404 都是因为在这里多敲了一段路径,工具再去拼/v1/chat/completions就拼重了;model的取值以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 模型广场当时的列表为准,不要凭印象写。模型 ID 写错了通常不会给你一句人话,只会返回一个参数错误;env_key只是告诉 Codex「从这个环境变量里取密钥」,键名你自己定,保持一致即可。
2.3 把 Key 放进环境变量再启动
export TAOTOKEN_API_KEY=YOUR_API_KEY codex如果你用的是把凭据存在~/.codex/auth.json的版本,那就把文件里对应的凭据字段换成这把 Key,其余结构别动。两种方式选一种,不要同时配,否则排查起来会分不清到底读的是哪份。
启动后在终端里问一句「你当前用的是哪个 model provider」,它能正确回出配置里的名字,就说明通道通了。这一步没通之前,不要急着贴建表代码——不然你分不清是代码问题还是配置问题。
3. 把那段 execute 原样贴给 Codex,问题清单怎么问
3.1 一条能拿到可运行版本的提问模板
贴代码的时候,把上下文一起给出来,Codex 的结论会准得多。建议这样组织:
下面这段是 psycopg2 在 PostgreSQL 里动态建表的代码,schema 和 table 都来自 外部配置,目标是表名不写死。请判断: 1) AsIs 拼接在这里是否存在注入或标识符引用问题; 2) 是否应改用 psycopg2.sql.Identifier,为什么; 3) 给出一个可以直接运行的改写版本,保留不写死表名的目标; 4) 列出改写后行为上会变化的地方。 from psycopg2.extensions import AsIs cur.execute("create table %s.%s(name text)", (AsIs(schema), AsIs(table)))关键是把「目标」讲清楚:不写死表名。如果你只说「帮我看看这段代码」,很容易收到一句「建议使用参数化查询」就结束,那对你没帮助。
3.2 Codex 大概率会点出的三处
把上面那段贴过去,通常能收到三类反馈,正好对应第 1 节里的分析:
第一,AsIs不做标识符引用,等价于字符串拼接,只是换了个类型名。外部可控的 schema 或表名进来,注入面没有消失。
第二,不加引号的标识符会被 PostgreSQL 折叠成小写,含大写字母或保留字的表名在建立和查询之间会产生不一致。这一点在实际项目里比注入更常见,因为它不报错,只是「查不到」。
第三,真正的标识符参数化工具是psycopg2.sql.Identifier,它会在拼接时调用quote_ident,该加引号的地方加引号,该转义的地方转义。
3.3 sql.Identifier 版本,照着改
复查之后,把建表函数改成这样:
from psycopg2 import sql def create_table(cur, schema: str, table: str) -> None: stmt = sql.SQL( "create table if not exists {}.{}(name text)" ).format( sql.Identifier(schema), sql.Identifier(table), ) cur.execute(stmt)和AsIs版本对比,差异是结构性的:sql.SQL负责承载固定骨架,sql.Identifier负责承载动态的标识符,两者用.format()组合,不是靠%s占位符。这样既保留了「表名和 schema 都从参数来」的原始目标,又不用你手动处理引号。
顺带补了两点:if not exists让重复执行不炸,函数签名带上类型标注,方便后续在调用处看出入参来源。
如果你还需要动态列名,思路一样,再加一个sql.Identifier即可;但如果列名也多到需要循环拼接,更该考虑的是把建表语句做成模板文件,而不是在 Python 里越拼越长。
3.4 在本地执行,把结果贴回对话
这里划一条线:Codex 负责读代码、给改写版本、解释差异;真正连库执行的是你本地的 Python 脚本或 psql。不要指望它替你连上数据库跑建表语句,它也不该拿到你的库连接信息。
你可以在本地这样验证:
python -c " import psycopg2 from psycopg2 import sql conn = psycopg2.connect('dbname=test user=postgres') cur = conn.cursor() cur.execute(sql.SQL('create table if not exists {}.{}(name text)').format( sql.Identifier('tenant_a'), sql.Identifier('UserB'))) conn.commit() print('ok') "跑完去 psql 里对一下:
\dt tenant_a.* select schemaname, tablename from pg_tables where schemaname = 'tenant_a';看到UserB是带引号保留大写的形式建出来的,说明引号逻辑生效了;如果看到的是userb,说明还有地方没走Identifier。把pg_tables的查询结果贴回 Codex 的对话,让它对着真实结果再确认一次,比在脑子里推演靠谱。
4. 复查和运行阶段会撞上的报错
4.1 Codex 侧:401 与模型不存在
贴代码之前 Codex 就报错的,先看通道。返回 401 一般是 Key 有问题:复制时带了空格、Key 被删除、或者环境变量名和env_key写的不一致。回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 控制台重新创建一把,再export一次,注意重新开一个终端会话。
如果返回的是模型相关错误,大概率是model字段的取值和模型广场对不上。这类错误不会自我修复,去模型广场复制一次准确 ID 最省时间。
4.2 base_url 末尾的 /v1 是最容易被忽视的一处
base_url = "https://taotoken.net/api/v1"这种写法在别的工具里可能是对的,但在 Codex 这里会拼出重复路径,典型表现是 404 或「接口不存在」。改回https://taotoken.net/api,末尾不加/v1,不要加斜杠。切换供应商时顺手检查这一行,能省掉一轮排查。
4.3 psycopg2 侧:语法错误与表不存在
| 报错 | 常见原因 | 处理方向 |
|---|---|---|
SyntaxError: syntax error at or near "." | %s占位符被当成标识符用,或表名撞上保留字 | 换sql.Identifier,并给保留字加引号 |
relation "public.user" does not exist | 建表时未加引号被折叠成小写,查询时按原样查 | 统一两边的大小写,或统一走Identifier |
InvalidSchemaName: schema "x" does not exist | schema 没提前建,或名字大小写不一致 | 先create schema,或补if not exists |
InsufficientPrivilege: permission denied for schema | 当前连接角色没有该 schema 的建表权限 | 换角色或补授权,不要在代码里绕 |
这些报错里有相当一部分不是AsIs造成的,但都会被误记到它头上。把报错原文整段贴回 Codex 对话,让它对着你的 SQL 和pg_tables结果做一次对照,比逐条试要快。
4.4 search_path 会掩盖一部分问题
如果你的连接串里配了search_path,create table tenant_a.userb(...)在某些情况下会落到默认 schema 里去,尤其是在 schema 名和当前用户同名时。验证阶段建议显式带上 schema 前缀,别依赖search_path。这一点在改写前后都成立,但用Identifier之后行为更可预测,因为它不会因为引号缺失而发生变化。
5. 改完之后,用同一把 Key 复测并回控制台对账
5.1 在模型对话里复测一次改写结果
本地跑通之后,把改写后的create_table函数和pg_tables的查询结果一起贴给 Codex,让它确认三件事:改写是否覆盖了原来AsIs的行为、有没有引入新的引号问题、if not exists会不会掩盖 schema 不存在的错误。最后一条容易被忽略——if not exists只管表,不管 schema。
如果你换了模型想再测一遍同样的提问,去 TaoToken 模型对话 里用同一把 Key 发一轮,方便对比不同模型对sql.Identifier的解释是否一致。
5.2 回控制台看这次调用有没有记上账
Codex 跑完几轮之后,去 控制台 API Keys 对照一下调用记录。如果刚才在 Config 和对话里都发了请求,这里应该能看到对应的量。看不到就说明请求根本没走到这条通道,那就回到 4.1 和 4.2 重新看配置,而不是去怀疑建表代码。
长期用 Codex 做这类代码复查,调用频次比随手问一句高不少,可以先看 Coding Plan 的套餐能不能覆盖你的日常量,再决定要不要换。Codex 在config.toml里的字段对照,官方文档里也有更细的说明,配置项多的时候对着看比凭记忆改稳。
回到最初那段cur.execute("create table %s.%s(name text)", (AsIs(schema), AsIs(table)))——它不是不能用,只是它把「安全引用标识符」这件事留给了调用者。让 Codex 复查的价值不在于它替你做决定,而在于它会明确告诉你:你省下的那点拼接代码,换来的是自己承担quote_ident的全部责任。想清楚这笔账,再决定是保留AsIs并补上自己的转义函数,还是直接换成sql.Identifier。