多引擎 SQL 客户端
轻量级 多引擎 用于查询和测试自动化的SQL客户端——一个API横跨 PostgreSQL, MySQL/MariaDB, SQLite, SQL 服务器和甲骨文,带有流畅的查询构建器,键入结果,并且总是被参数化的查询.
这个包是 数据层:用于人类+LLM代理写作的节目SQL外观. 这是兄弟 @civitas-cerebrum/wasapi (HTTP),他们一起支持步骤API @civitas-cerebrum/element-interactions如果您正在通过此框架编写 E2E 测试规格,请使用其 steps.sql* 包件——这个包就是它们的力量, 以及你用固定装置、种子脚本、控制内部和独立的工具来达到的.
一个API,五个引擎
每个数据库驱动程序都有自己的连接仪式,占位符风格,以及结果形状. 多引擎代码通常指5个代码路径.
在( 驱动程序) 之前 :
// pg: Pool, $1 placeholders
const pool = new Pool({ connectionString });
const res = await pool.query('SELECT * FROM books WHERE genre = $1', ['Fiction']);
// mysql2: createPool, ? placeholders, [rows, fields] tuples
const [rows] = await mysqlPool.query('SELECT * FROM books WHERE genre = ?', ['Fiction']);
// mssql: request objects with named .input() bindings
const r = await pool.request().input('p1', 'Fiction')
.query('SELECT * FROM books WHERE genre = @p1');
// oracledb: outFormat option, UPPERCASE column keys, manual connection release
const conn = await pool.getConnection();
const result = await conn.execute('SELECT * FROM books WHERE genre = :1', ['Fiction'],
{ outFormat: oracledb.OUT_FORMAT_OBJECT });
await conn.close();
(@civitas-cerebrum/sql-客户端):
// Same client, same call, any engine — detected from the connection string.
const db = new SqlClient({ connectionString: process.env.DB_URL! });
const res = await db.query('SELECT * FROM books WHERE genre = $1', ['Fiction']);
// Or fully portable: the builder renders engine-correct placeholders,
// quoting, and pagination from one chain.
const top = await QueryBuilder.select('books')
.where('genre = ?', 'Fiction')
.orderBy('price', 'desc')
.limit(5)
.run(db);
* 安装
安装软件包 加引擎的驱动程序 - 驾驶员是 可选同伴依赖关系,因此您只拉取您使用的(没有自动安装,包括依赖于 sql-客户端的软件包):
# PostgreSQL
npm install @civitas-cerebrum/sql-client pg
# MySQL / MariaDB
npm install @civitas-cerebrum/sql-client mysql2
# SQLite (zero infrastructure)
npm install @civitas-cerebrum/sql-client better-sqlite3
# SQL Server
npm install @civitas-cerebrum/sql-client mssql
# Oracle (Thin mode — no Instant Client needed)
npm install @civitas-cerebrum/sql-client oracledb
要几个引擎吗? 将它们的驾驶员一起列出npm install @civitas-cerebrum/sql-client pg mysql2) (中文(简体) ). 导入软件包从不装入驱动程序; 是 require第一次使用时,一个缺失的投出一个清晰 UnsupportedEngineException 命名要安装的驱动程序.
所需经费: 节点.js 20.
| 发动机 | 连接字符串方案 | 驱动程序 | 占位符 | returning() |
|---|---|---|---|---|
| 邮递SQL | postgres:// • 妇女 postgresql:// | pg | $1 | ✅ RETURNING |
| MySQL / 马里亚DB | mysql:// • 妇女 mariadb:// | mysql2 | ? | ❌ |
| SQL 点 | sqlite: • 妇女 sqlite::memory: • 妇女 file: • 妇女 :memory: • 妇女 *.db • 妇女 *.sqlite | better-sqlite3 | ? | ✅ RETURNING |
| SQL 服务器 | mssql:// • 妇女 sqlserver:// | mssql | @p1 | ✅ OUTPUT |
| 甲骨文 | oracle:// • 妇女 oracledb:// | oracledb | :1 | ❌ |
QQ 快速启动
0基础设施 — 一个内存 SQLite 数据库, 没有 Docker, 没有服务器 。 复制复制并运行 :
import { SqlClient, QueryBuilder } from '@civitas-cerebrum/sql-client';
const db = new SqlClient({ engine: 'sqlite', connectionString: ':memory:' });
// Create a table (runScript splits a multi-statement script per engine)
await db.runScript(`
CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT, price REAL, in_stock INTEGER);
`);
// Insert (multi-row in one statement)
await QueryBuilder.insert('books')
.values([
{ title: 'Dune', price: 12.99, in_stock: true },
{ title: '1984', price: 9.5, in_stock: false },
])
.run(db);
// Read straight into a typed, coercing ResultSet
const cheap = await QueryBuilder.select('books').where('price < ?', 10).fetch(db);
console.log(cheap.column('title')); // ['1984']
const dune = await db.fetch('SELECT * FROM books WHERE title = ?', ['Dune']);
console.log(dune.one().number('price')); // 12.99
console.log(dune.one().boolean('in_stock')); // true (1 → true)
await db.end();
* 特性
- 引擎自动检测 - 从连接字符串方案推断出发动机;通过
{ engine }只有在没有计划读取时才明确. - 可选、懒散的司机 – 驱动程序是可选的对等分解程序:只安装您使用的引擎;没有强迫您(或依赖于sql-客户端的软件包). 每个都是
required 在第一次客户端构建, 和缺失一个扔UnsupportedEngineException命名要安装的驱动程序. - 总是瘫痪 —— Raw calls take a params range; 构建者需要
?标记并让引擎正确的占位符样式。 无弦插入处. sql标记模板 —— 说sql`... ${value}`安全地插入绑定参数,并编译到每个引擎正确的占位符样式;sql.id()用于识别器,碎片巢.- 流利的查询构建器 —— 说
select编号 :insert编号 :update编号 :delete带有引擎正确标识符的链条引用和页标( )LIMIT/OFFSET对OFFSET..FETCH),加 (中文(简体) )whereIn编号 :whereNull编号 :whereNotNull财务报告和审定财务报表count()编号 :exists()终端. - 一通电话读作: —— 说
fetch编号 :one编号 :maybeOne编号 :scalar直上陆地ResultSet编号 :Row从客户端或构建器链中找到 – 没有手动rows()包装. - 多行插图 —— 说
values()接受一个数组(或重复调用),并编译一个单一多行语句. - 返回行写 —— 说
returning(...cols)编译RETURNING(邮递)或OUTPUT INSERTED./DELETED.(msql);引擎在编译时没有等效的拒绝,有明显的错误. - 已打入、已规范的结果 - 每一个引擎的结果都被重塑成
{ rows, rowCount, fields },在行型上通用;Row<T>提供列名自动完成,同时仍然允许对大小写不敏感查找. - 不区分大小写、 类型强制存取器 —— 说
rows(res).one().number('price')引擎是否返回PRICE(甲骨文)或"12.99"作为字符串(postgres/mysql NUMERIC). 匹配者也比较数字,所以find({ price: 6.5 })匹配一个"6.50"电话. - 模拟行匹配者 —— 说
where编号 :find接受文字、匹配者(lt, (中文(简体) ),oneOf, (中文(简体) ),contains),或全行上游;where()返回可链条ResultSet。 。 。 . - 交易 —— 说
transaction(fn)自动COMMIT返回时ROLLBACK铸造者在五台发动机中运行qb.run(tx)) (中文(简体) ). - 脚本运行器 —— 说
runScript()编号 :splitSqlScript()拆分多语句.sql带有引擎意识规则的文件( Oracle)/, SQL 服务器GO,评论/引文注意). - 连接失败 —— 说
ping()准备状态检查和aconnectTimeoutMs选项,所以错误的主机会迅速死亡而不是被挂起. - 类型失败 —— 说
QueryFailedException显示信件中的 SQL + 参数并携带.sql, (中文(简体) ),.params驱动器.cause;占位符式错误得到跨引擎提示. - 可插载发动机 —— 说
registerEngineFactory()以自定义驱动器+方言对换. - 调试日志 —— 说
DEBUG=sql:*用参数打印每个语句( 包括内部交易), 每个引擎命名空格(sql:postgres, (中文(简体) ),sql:mysql(中文(简体) ).
使用量: SqlClient
import { SqlClient } from '@civitas-cerebrum/sql-client';
// --- PostgreSQL (engine auto-detected from the URL scheme) ---
const pg = new SqlClient({ connectionString: 'postgres://user:pass@localhost:5432/mydb' });
// --- MySQL ---
const mysql = new SqlClient({ connectionString: 'mysql://user:pass@localhost:3306/mydb' });
// --- SQLite (in-memory, zero infra) ---
const sqlite = new SqlClient({ engine: 'sqlite', connectionString: ':memory:' });
// --- SQL Server (engine-native connection object) ---
const mssql = new SqlClient({
engine: 'mssql',
connection: { server: 'localhost', port: 1433, user: 'sa', password: 'Secret!1',
database: 'mydb', options: { trustServerCertificate: true } },
});
// --- Oracle ---
const oracle = new SqlClient({ connectionString: 'oracle://user:pass@localhost:1521/FREEPDB1' });
连接会很快失败, 而不是挂在错误的主机上 – pass connectTimeoutMs 输入
配置并使用 ping() (例如在测试中) globalSetup核查前面的可达到性:
const db = new SqlClient({ connectionString: process.env.DB_URL!, connectTimeoutMs: 5000 });
await db.ping(); // throws if the engine is unreachable
原始查询
原始 SQL 中的占位符为 引擎内置 (单位:千美元)$1 编号 : ? 编号 : @p1 编号 : :1 - 参见上表:
const { rows } = await pg.query('SELECT * FROM books WHERE genre = $1', ['Fiction']);
await pg.execute('UPDATE books SET stock = 0 WHERE book_id = $1', ['b1']);
// fetch() wraps the result in a ResultSet in one step
const book = (await pg.fetch('SELECT * FROM books WHERE book_id = $1', ['b1'])).one();
sql 标记模板
喜欢 sql 便携式原始 SQL 标记: 插入在
引擎正确的占位符样式,所以相同的查询会随处可见。 使用 sql.id() 页:1
标识符(每个方言引用,从未参数化),并用巢片来作曲:
import { sql } from '@civitas-cerebrum/sql-client';
const genre = 'Fiction', max = 15;
await db.query(sql`SELECT * FROM books WHERE genre = ${genre} AND price < ${max}`);
// postgres → "... WHERE genre = $1 AND price < $2"; mysql → "... = ? AND < ?"; etc.
const onlyCheap = sql`AND price < ${10}`;
await db.query(sql`SELECT ${sql.id('title')} FROM books WHERE in_stock = ${true} ${onlyCheap}`);
交易
transaction(fn) 承诺返回和滚回抛出。 手柄带着方言,所以
QueryBuilder.run(tx) 在其中工作 sql 标记是只使用客户端的——在 tx 内部使用构建器
或原始 SQL 通过 tx.query:
await pg.transaction(async (tx) => {
await QueryBuilder.update('books').set({ stock: 0 }).where('book_id = ?', 'b1').run(tx);
await tx.execute('UPDATE books SET stock = stock - 1 WHERE book_id = $1', ['book-002']);
// throw → automatic ROLLBACK; return → automatic COMMIT
});
完成时总是关闭客户端 :
await pg.end();
QQ 查询构建器
便携式SQL从一个链- ? 标记、引用和页码都按引擎编译:
import { QueryBuilder } from '@civitas-cerebrum/sql-client';
const top = await QueryBuilder.select('books')
.columns('title', 'price')
.where('price < ?', 15)
.orderBy('price', 'desc')
.limit(5)
.run(pg);
// Multi-row INSERT — values() takes an array (or repeated calls)
await QueryBuilder.insert('books')
.values([
{ book_id: 'b1', title: 'Dune' },
{ book_id: 'b2', title: '1984' },
])
.run(pg);
// Writes can hand rows back: RETURNING (postgres/sqlite) or OUTPUT (mssql).
// mysql/oracle have no equivalent — returning() throws UnsupportedEngineException there.
const ins = await QueryBuilder.insert('books')
.values({ title: 'Dune' })
.returning('book_id', 'title') // no args → all columns
.run(pg);
// whereIn expands one placeholder per value (throws on an empty array, so you never compile
// `IN ()`); whereNull/whereNotNull avoid the `= NULL` footgun that silently matches nothing.
const some = await QueryBuilder.select('books')
.whereIn('book_id', ['b1', 'b2', 'b3'])
.whereNotNull('isbn')
.fetch(pg);
// Inspect without executing — toSql() requires an explicit dialect
const { text, values } = QueryBuilder.select('books').where('price < ?', 15).toSql(pg.dialect);
终端 通过客户端发送查询,并将结果成一个调用:
await QueryBuilder.select('books').where('book_id = ?', 'b1').one(pg); // Row (else ResultError)
await QueryBuilder.select('books').where('genre = ?', 'Fiction').fetch(pg); // ResultSet
await QueryBuilder.select('books').where('book_id = ?', 'b1').maybeOne(pg); // Row | undefined
await QueryBuilder.select('books').scalar(pg, 'title'); // first cell
await QueryBuilder.select('books').where('price < ?', 10).count(pg); // number
await QueryBuilder.select('books').where('book_id = ?', 'b1').exists(pg); // boolean
可链: columns(...cols) • 妇女 join(table, on) • 妇女 where(clause, ...params) (多次电话拨打)- whereIn(col, values) • 妇女 whereNull(col) • 妇女 whereNotNull(col) • 妇女 groupBy(...cols) • 妇女 having(clause, ...params) • 妇女 orderBy(col, dir) • 妇女 limit(n) • 妇女 offset(n) • 妇女 values(row | row[]) • 妇女 set(row) • 妇女 returning(...cols)。 。 。 .
终端 : run(client) • 妇女 fetch(client) • 妇女 one(client) • 妇女 maybeOne(client) • 妇女 scalar(client, col?) • 妇女 count(client) • 妇女 exists(client) • 妇女 toSql(dialect)。 。 。 .
说明: SQL 服务器和甲骨文需要
orderBy()开始时(OFFSET..FETCH需要它吗. 要组队吗? 把它放入一个片段:where('(genre = ? OR genre = ?)', a, b)。 。 。 .
阅读结果
按名称从行中拉出值—— 不区分大小写 (甲骨文返回 UPPERCASE 密钥)和 类型强制 (postgres/mysql 返回 NUMERIC/DECIMAL 作为字符串):
import { rows, getNumber } from '@civitas-cerebrum/sql-client';
const res = await db.query('SELECT * FROM books WHERE book_id = $1', ['book-001']);
// Fluent wrapper
const book = rows(res).one(); // exactly one row, else throws ResultError
book.string('title'); // 'To Kill a Mockingbird'
book.number('price'); // 12.99
book.boolean('in_stock'); // normalizes 1/0, 't'/'f', 'true'/'false'
rows(await db.query('SELECT count(*) FROM books')).scalar(); // first cell of first row
rows(res).column('title'); // every title
rows(res).find({ genre: 'Fiction' }); // first matching row, or undefined
rows(res).maybeOne(); // 0..1 rows, else throws ResultError
// Or standalone functions on a raw row
getNumber(res.rows[0], 'price'); // 12.99
语义学:缺栏- undefined; SQL NULL → null;否则被胁迫.
过滤行
where编号 :find 接受部分(栏-正文) 或者说 )或全行上游:
import { rows, lt, oneOf, contains } from '@civitas-cerebrum/sql-client';
const res = await db.query('SELECT * FROM books');
rows(res).where({ genre: 'Fiction', price: lt(10) }); // matchers
rows(res).find({ title: contains('1984') });
rows(res).where(row => row.number('price')! < 10); // predicate
// where() returns a ResultSet, so filtering keeps chaining:
rows(res).where({ genre: 'Fiction' }).column('title');
rows(res).where({ book_id: 'b1' }).one();
if (rows(res).where({ status: 'SOLD' }).isEmpty()) { /* none sold */ }
rows(res).where({ genre: 'Fiction' }).all().forEach(r => { /* Row[] */ });
// standalone, on raw rows
import { filterRows } from '@civitas-cerebrum/sql-client';
filterRows(res.rows, { status: oneOf(['ACTIVE', 'SOLD']) });
匹配者 : eq ne lt lte gt gte between oneOf like contains startsWith endsWith matches isNull notNull not。 。 。 .
无效/ 未定义单元格只匹配 isNull(); 比较/字符串匹配器返回 false 对于它。 平等
匹配者(eq编号 :ne编号 :oneOf)和字面部分在双方都是数字时进行数字比较,所以
6.5 查询与 "6.50" 由 postgres/ misql 返回的单元格.
API 参考
🔌 SqlClient
| 成员 | 说明 |
|---|---|
new SqlClient(config) | config数字 : { connectionString?, engine?, connection?, max?, connectTimeoutMs?, dialect? } |
query<T>(sql, params?) 编号 : query<T>(fragment) | 参数化查询(字符串或 sql ∗∗∗∗∗ Promise<SqlResult<T>> |
execute(sql, params?) 编号 : execute(fragment) | 书面陈述 – Promise<SqlResult> (受影响行的行数) |
fetch<T>(sql, params?) | query() 接下来 rows() → Promise<ResultSet<T>> |
transaction(fn) | 原子块; fn(tx) 获得 tx.query编号 :tx.execute 加号 tx.dialect编号 :tx.engine这样 QueryBuilder.run(tx) 工作内容 |
runScript(sqlText) | 拆分一个多语句脚本( 引擎意识) 并按顺序执行 |
ping() | 廉价连接探测器; 如果引擎无法到达则丢弃 |
end() | 关闭池/连接 |
engine 编号 : dialect | 解决的引擎名称和方言实例 |
📊 SqlResult<T>
{ rows: T[], rowCount: number, fields: { name, dataTypeID }[] } ——每个引擎产生的正态形状.
📖 ResultSet 编号 : Row
| 成员 | 说明 |
|---|---|
rows<T>(result | rawRows) | 将结果( 或原始数组) 包入 a ResultSet<T> |
.one() 编号 : .maybeOne() | 完全1/在 - 最一行,否则 ResultError |
.first() 编号 : .at(i) | Row 或者说 undefined |
.scalar(column?) | 第一行的第一个单元格 |
.column(name) | 所有行各一栏 |
.find(where) | 第一个匹配 Row (部分字数/对等,或 (row) => boolean(中文(简体) ) |
.where(where) | 过滤器 → 可链条 ResultSet (相同的部分/预选表格) |
.map(fn) 编号 : .all() 编号 : .raw() | 逐行绘制地图/ 全部覆盖为 Row[] / 打开到生行 |
Row<T>.get/string/number/boolean(col) | 大小写不敏感、类型强制的单元格访问; T 给列名自动完成 |
Row.has(col) 编号 : Row.raw() | 存在检查/ 内在对象 |
🏷️ sql 标记脚本( c)
| 导出 | 说明 |
|---|---|
sql`...${v}` | 构建一个 SqlFragment; 插件 ; 捆绑的活体; 筑巢的碎片 |
sql.id(name) | 插入一个标识符( 引号, 不参数化) |
splitSqlScript(sqlText, engine?) | 将多声明脚本拆分为单个声明( 引擎意识) |
· 例外情况
| 类 | 掷出时 | 额外 |
|---|---|---|
SqlException | 下方所有东西的基类 | —— 说 |
QueryFailedException | 查询/执行失败 | 消息包括 SQL + params(+ 占位符提示); .sql, (中文(简体) ), .params, (中文(简体) ), .cause |
UnsupportedEngineException | 未知引擎、 缺少驱动程序或不支持 returning() | —— 说 |
ResultError | one()编号 :maybeOne() 基本要求或失败的胁迫 | —— 说 |
扩展
| 函数 | 说明 |
|---|---|
registerEngineFactory(engine, factory) | 插入自定义 { createDriver, createDialect } 组合 |
detectEngine(connectionString) | 用于检测 SqlClient |
Dialect | 执行 placeholder, (中文(简体) ), quoteIdentifier, (中文(简体) ), compilePagination, (中文(简体) ), compileReturning |
createLogger(ns) 编号 : log | debug- 基于记录器(SqlLogger在 sql:* 命名空间 |
测试工作流程
# Light (unit + zero-infra SQLite) — no Docker needed
npm test
# Single engine (bring up just that engine's container)
docker compose --profile postgres up -d --wait && npm run test:postgres
docker compose --profile mysql up -d --wait && npm run test:mysql
docker compose --profile mssql up -d --wait && npm run test:mssql
docker compose --profile oracle up -d --wait && npm run test:oracle
# Full matrix — all five live engines
docker compose --profile all up -d --wait
npm run test:all-engines
docker compose --profile all down -v
以上命令用于局部迭代. 每个公关都由CI负责: 节点24上的单元 + SQLite 套房, 财务报告和审定财务报表 完整的四引擎活矩阵(Postgres, MySQL, SQL Server, Oracle)——每个引擎在自己的孤立容器中——所以在合并前会捕捉到引擎特有的回归.
主机端口与已经运行的东西相撞? 覆盖它们:
SQL_CLIENT_PG_PORT, (中文(简体) ), SQL_CLIENT_MYSQL_PORT, (中文(简体) ), SQL_CLIENT_MSSQL_PORT, (中文(简体) ), SQL_CLIENT_ORACLE_PORT
(并指所取所取所取所取所取所取等. SQL_TEST_URL=postgres://bookhive:bookhive@localhost:15432/bookhive npm run test:postgres) (中文(简体) ).
调试日志 : DEBUG=sql:* npm test (每个引擎命名空间) sql:postgres, (中文(简体) ), sql:mysql, (中文(简体) ), sql:sqlite, (中文(简体) ), sql:mssql, (中文(简体) ), sql:oracle;涵盖交易报表和生命周期).
发动机陷阱
跨引擎不同 包吸收你- 少数不能:
- 原始 SQL 的占位符样式不同 —— 说
$1(邮递员)?· (misql/sqlite) -@p1· (msql) ·:1(甲骨文). 使用构建器或sql标记可移植代码; 两者都使正确的样式. - Oracle 返回 UPPERCASE 列键 —— 说
Row访问者不敏感,所以row.string('title')不管怎么样,都行 仅生res.rows[i].title索引受到影响. - NUMERIC/DECIMAL 作为字符串到达 邮递/邮件
row.number()强迫,和匹配者比较数字,所以你很少注意到。 原始的细胞访问并不强制. - SQL 服务器
UNIQUE将 NULLs 视为重复 第二行为:NULL在一个UNIQUE列违反了对msql(不同于其他引擎)的限制。 在测试固定装置中使用不同的监视值 . - SQL 组合键 服务器按位置绑定参数 —— 说
@p1..@pN映射到参数阵列的顺序; 构建者和sql标记自动处理. - SQLite 没有本地日期/ 布尔 - 驱动程序正常
Date → ISO string, (中文(简体) ),boolean → 1/0, (中文(简体) ),undefined → null因此,相同的固定代码 工作与其他引擎.
非目标
故意超出范围保持精干和可预期. 为此,可使用专用工具:
- ORM / 关系/ 实体映射 这是查询客户端 不是ORM.
- 移徙和计划管理 使用你的移徙工具;
runScript()只执行您给出的 SQL . - 正在流出/ 光标 对于庞大的成果组合——成果是实实在在的数组.
- 命名参数 —— 仅定位(建造者
?编辑sql标记覆盖人类学差距. - 或组合的建树 - 单人内部的表达式 OR
where()换成碎片. - 提升助手 ——特写引擎
ON CONFLICT编号 :MERGE现在直接使用 SQL.
克劳德代码技能
货包船a 克劳德代码 技术(skills/sql-client/SKILL.md(中文(简体) )
它教毒剂 完整的API表面。 它自动安装在 npm install 改为 ~/.claude/skills/ 全球安装-g),或仅此 <project>/.claude/skills/ 对于本地的( 当工程根没有时从未写入) package.json)——包是直接安装还是到达
穿过另一个 @civitas-cerebrum 软件包。 在使用步骤-API测试的项目中
技术将测试光谱数据库的工作推迟到 database-testing 技术.
跳过副本——例如,在CI或容器中——设置 SQL_CLIENT_SKIP_SKILLS=1 在安装前.