@morlay/rdbkit 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,10 +84,22 @@ await database.setSqlitePragma("user_version", 4); // 写库身份(值只允
84
84
  ```
85
85
 
86
86
 
87
+ ## PostgreSQL schema
88
+
89
+ 连接配置里的 `schema` 是**唯一事实来源**:结构探测、DDL、表句柄的读写、迁移账本与 `objectNames` 都从
90
+ `Database.schema` 取,一律渲染成 `"<schema>"."<表>"`(SQLite 没有 schema 概念,照旧不带前缀)。省略即
91
+ `public`,不依赖连接串的 `search_path`。schema 本身要由部署方先建好(`CREATE SCHEMA ...`),rdbkit 不替你建。
92
+
93
+ ```ts
94
+ const database = await openDatabase({ dialect: "postgres", connectionString, schema: "dsh_test" });
95
+ await ensureSchema(database, catalog); // CREATE TABLE "dsh_test"."t_sessions" ...
96
+ await runMigrations(database, { catalog, migrations }); // 账本也在 dsh_test
97
+ ```
98
+
87
99
  ## 迁移
88
100
 
89
101
  结构(建表 / 补列 / 补索引)由 `ensureSchema` 从表定义算差异;**数据迁移**走注册表,账本是内置的
90
- `t_schema_migrations`:
102
+ `t_schema_migrations`(与业务表同 schema,见上节):
91
103
 
92
104
  ```ts
93
105
  const migrations = new MigrationRegistry().register({
package/dist/index.mjs CHANGED
@@ -98,8 +98,8 @@ function postgresDialect() {
98
98
  }
99
99
  };
100
100
  }
101
- /** 一列的定义 SQL(`"name" TEXT NOT NULL ...`)。 */
102
- function columnSql$1(dialect, name, column) {
101
+ /** 一列的定义 SQL(`"name" TEXT NOT NULL ...`);外键引用跟着表名一起落在同一 schema。 */
102
+ function columnSql$1(dialect, name, column, schema) {
103
103
  assertIdentifier("column", name);
104
104
  const parts = [dialect.quote(name), dialect.columnType(column)];
105
105
  if (column.type === "serial" && dialect.name === "sqlite") parts.push("PRIMARY KEY AUTOINCREMENT");
@@ -111,7 +111,7 @@ function columnSql$1(dialect, name, column) {
111
111
  const { table, column: referenced, onDelete } = column.references;
112
112
  assertIdentifier("table", table);
113
113
  assertIdentifier("column", referenced);
114
- parts.push(`REFERENCES ${dialect.tableName(table)}(${dialect.quote(referenced)})` + (onDelete === void 0 ? "" : ` ON DELETE ${onDelete.toUpperCase()}`));
114
+ parts.push(`REFERENCES ${dialect.tableName(table, schema)}(${dialect.quote(referenced)})` + (onDelete === void 0 ? "" : ` ON DELETE ${onDelete.toUpperCase()}`));
115
115
  }
116
116
  return parts.join(" ");
117
117
  }
@@ -125,7 +125,7 @@ function literal(dialect, value, type) {
125
125
  }
126
126
  /** 建表语句(IF NOT EXISTS;列与表级约束都由定义算出来)。 */
127
127
  function createTableSql(dialect, table, schema) {
128
- const parts = Object.entries(table.columns).map(([name, column]) => columnSql$1(dialect, name, column));
128
+ const parts = Object.entries(table.columns).map(([name, column]) => columnSql$1(dialect, name, column, schema));
129
129
  const primaryKey = table.primaryKey === void 0 ? void 0 : table.primaryKey.map((name) => dialect.quote(name)).join(", ");
130
130
  if (primaryKey !== void 0) parts.push(`PRIMARY KEY (${primaryKey})`);
131
131
  for (const [name, check] of Object.entries(table.checks ?? {})) {
@@ -139,14 +139,14 @@ function createTableSql(dialect, table, schema) {
139
139
  return `CREATE TABLE IF NOT EXISTS ${dialect.tableName(table.name, schema)} (\n ${parts.join(",\n ")}\n)`;
140
140
  }
141
141
  /** 索引语句(`unique` 时是唯一索引)。 */
142
- function createIndexSql(dialect, table, name, columns, unique = false) {
142
+ function createIndexSql(dialect, table, name, columns, unique = false, schema) {
143
143
  assertIdentifier("index", name);
144
- return `CREATE ${unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${dialect.quote(name)} ON ${dialect.tableName(table.name)} (${columns.map((column) => dialect.quote(column)).join(", ")})`;
144
+ return `CREATE ${unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${dialect.quote(name)} ON ${dialect.tableName(table.name, schema)} (${columns.map((column) => dialect.quote(column)).join(", ")})`;
145
145
  }
146
146
  /** 删列(只在显式允许破坏性变更时用)。 */
147
- function dropColumnSql(dialect, table, name) {
147
+ function dropColumnSql(dialect, table, name, schema) {
148
148
  assertIdentifier("column", name);
149
- return `ALTER TABLE ${dialect.tableName(table)} DROP COLUMN ${dialect.quote(name)}`;
149
+ return `ALTER TABLE ${dialect.tableName(table, schema)} DROP COLUMN ${dialect.quote(name)}`;
150
150
  }
151
151
  /** 删索引(只在显式允许破坏性变更时用)。 */
152
152
  function dropIndexSql(dialect, name) {
@@ -154,8 +154,8 @@ function dropIndexSql(dialect, name) {
154
154
  return `DROP INDEX IF EXISTS ${dialect.quote(name)}`;
155
155
  }
156
156
  /** 加列语句(启动迁移只做「缺列则补」,不改类型、不删列)。 */
157
- function addColumnSql(dialect, table, name, column) {
158
- return `ALTER TABLE ${dialect.tableName(table)} ADD COLUMN ${columnSql$1(dialect, name, column)}`;
157
+ function addColumnSql(dialect, table, name, column, schema) {
158
+ return `ALTER TABLE ${dialect.tableName(table, schema)} ADD COLUMN ${columnSql$1(dialect, name, column, schema)}`;
159
159
  }
160
160
  // src/fragment.ts
161
161
  /** 一段纯文本;`only` 限定只在某个方言里出现(SQLite 不支持的行锁等)。 */
@@ -708,15 +708,16 @@ function selectQuery(database, table, projection) {
708
708
  if (name === table.name) return table;
709
709
  for (const join of state.joins) if ((join.alias ?? join.table.name) === name) return join.table;
710
710
  };
711
+ const targetOf = (definition) => database.dialect.tableName(definition.name, database.schema);
711
712
  const toFragment = () => {
712
713
  const items = Object.entries(projection).map(([alias, entry]) => entry.kind === "column" ? text(`${entry.table === void 0 ? "" : `"${entry.table}".`}"${entry.name}" AS "${alias}"`) : concat([exprFragment(entry), text(`AS "${alias}"`)], " "));
713
714
  const parts = [
714
715
  state.distinct ? text("SELECT DISTINCT") : text("SELECT"),
715
716
  concat(items, ", "),
716
- text(`FROM "${table.name}"`)
717
+ text(`FROM ${targetOf(table)}`)
717
718
  ];
718
719
  for (const join of state.joins) {
719
- const target = join.alias === void 0 ? `"${join.table.name}"` : `"${join.table.name}" AS "${join.alias}"`;
720
+ const target = join.alias === void 0 ? targetOf(join.table) : `${targetOf(join.table)} AS "${join.alias}"`;
720
721
  if (join.kind === "cross") {
721
722
  parts.push(text(`CROSS JOIN ${target}`));
722
723
  continue;
@@ -805,31 +806,31 @@ function selectQuery(database, table, projection) {
805
806
  function returningClause(returning) {
806
807
  return returning === void 0 || returning.length === 0 ? "" : text(`RETURNING ${returning.map((name) => `"${name}"`).join(", ")}`);
807
808
  }
808
- function insertOf(table, row, mode, conflict, update, returning) {
809
+ function insertOf(table, target, row, mode, conflict, update, returning) {
809
810
  const columns = Object.keys(row).filter((name) => table.columns[name] !== void 0);
810
811
  if (columns.length === 0) throw new Error(`rdbkit: insert into "${table.name}" has no columns`);
811
- const base = fragment(`INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, columns.map((name) => row[name]));
812
+ const base = fragment(`INSERT INTO ${target} (${columns.map((name) => `"${name}"`).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, columns.map((name) => row[name]));
812
813
  if (mode === "insert") return concat([base, returningClause(returning)], " ");
813
- const target = (conflict ?? []).map((name) => `"${name}"`).join(", ");
814
- if (mode === "ignore") return concat([base, `ON CONFLICT (${target}) DO NOTHING`], " ");
814
+ const conflictTarget = (conflict ?? []).map((name) => `"${name}"`).join(", ");
815
+ if (mode === "ignore") return concat([base, `ON CONFLICT (${conflictTarget}) DO NOTHING`], " ");
815
816
  return concat([
816
817
  base,
817
- `ON CONFLICT (${target}) ${(update ?? []).length === 0 ? "DO NOTHING" : `DO UPDATE SET ${(update ?? []).map((name) => `"${name}" = excluded."${name}"`).join(", ")}`}`,
818
+ `ON CONFLICT (${conflictTarget}) ${(update ?? []).length === 0 ? "DO NOTHING" : `DO UPDATE SET ${(update ?? []).map((name) => `"${name}" = excluded."${name}"`).join(", ")}`}`,
818
819
  returningClause(returning)
819
820
  ], " ");
820
821
  }
821
- function insertManyOf(table, rows, options = {}) {
822
+ function insertManyOf(table, target, rows, options = {}) {
822
823
  if (rows.length === 0) return fragment("SELECT 1 WHERE 1 = 0");
823
824
  const columns = Object.keys(rows[0]).filter((name) => table.columns[name] !== void 0);
824
825
  if (columns.length === 0) throw new Error(`rdbkit: insert into "${table.name}" has no columns`);
825
826
  for (const row of rows) for (const column of columns) if (row[column] === void 0) throw new Error(`rdbkit: insertMany into "${table.name}" is missing column '${column}'`);
826
827
  const tuple = `(${columns.map(() => "?").join(", ")})`;
827
- return concat([fragment(`INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")}) VALUES ${rows.map(() => tuple).join(", ")}`, rows.flatMap((row) => columns.map((name) => row[name]))), conflictClause(options)], " ");
828
+ return concat([fragment(`INSERT INTO ${target} (${columns.map((name) => `"${name}"`).join(", ")}) VALUES ${rows.map(() => tuple).join(", ")}`, rows.flatMap((row) => columns.map((name) => row[name]))), conflictClause(options)], " ");
828
829
  }
829
- function insertFromSelectOf(table, columns, source, options = {}) {
830
+ function insertFromSelectOf(table, target, columns, source, options = {}) {
830
831
  if (columns.length === 0) throw new Error(`rdbkit: insert into "${table.name}" has no columns`);
831
832
  return concat([
832
- text(`INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")})`),
833
+ text(`INSERT INTO ${target} (${columns.map((name) => `"${name}"`).join(", ")})`),
833
834
  source,
834
835
  conflictClause(options)
835
836
  ], " ");
@@ -840,17 +841,17 @@ function conflictClause(options) {
840
841
  if (conflict.length === 0) return "";
841
842
  return text(`ON CONFLICT (${conflict.map((name) => `"${name}"`).join(", ")}) ${(options.update ?? []).length === 0 ? "DO NOTHING" : `DO UPDATE SET ${(options.update ?? []).map((name) => `"${name}" = excluded."${name}"`).join(", ")}`}`);
842
843
  }
843
- function upsertExprOf(table, row, conflict, assignments) {
844
- const base = insertOf(table, row, "insert");
845
- const target = conflict.map((name) => `"${name}"`).join(", ");
844
+ function upsertExprOf(table, target, row, conflict, assignments) {
845
+ const base = insertOf(table, target, row, "insert");
846
+ const conflictTarget = conflict.map((name) => `"${name}"`).join(", ");
846
847
  const sets = Object.entries(assignments).map(([name, ref]) => concat([text(`"${name}" =`), operandSql(ref)], " "));
847
848
  return concat([
848
849
  base,
849
- text(`ON CONFLICT (${target}) DO UPDATE SET`),
850
+ text(`ON CONFLICT (${conflictTarget}) DO UPDATE SET`),
850
851
  concat(sets, ", ")
851
852
  ], " ");
852
853
  }
853
- function updateOf(table, patch, condition, returning) {
854
+ function updateOf(table, target, patch, condition, returning) {
854
855
  const columns = Object.keys(patch).filter((name) => table.columns[name] !== void 0);
855
856
  if (columns.length === 0) throw new Error(`rdbkit: update "${table.name}" has no columns`);
856
857
  const sets = columns.map((name) => {
@@ -858,15 +859,15 @@ function updateOf(table, patch, condition, returning) {
858
859
  const assigned = typeof value === "object" && value !== null && "kind" in value ? operandSql(value) : param(value);
859
860
  return concat([text(`"${name}" =`), assigned], " ");
860
861
  });
861
- const head = concat([text(`UPDATE "${table.name}" SET`), concat(sets, ", ")], " ");
862
+ const head = concat([text(`UPDATE ${target} SET`), concat(sets, ", ")], " ");
862
863
  return concat([condition === void 0 ? head : concat([
863
864
  head,
864
865
  text("WHERE"),
865
866
  condition
866
867
  ], " "), returningClause(returning)], " ");
867
868
  }
868
- function deleteOf(table, condition, returning) {
869
- const head = text(`DELETE FROM "${table.name}"`);
869
+ function deleteOf(table, target, condition, returning) {
870
+ const head = text(`DELETE FROM ${target}`);
870
871
  return concat([condition === void 0 ? head : concat([
871
872
  head,
872
873
  text("WHERE"),
@@ -876,6 +877,7 @@ function deleteOf(table, condition, returning) {
876
877
  // src/table/handle.ts
877
878
  function createTableHandle(database, definition) {
878
879
  const name = definition.name;
880
+ const target = database.dialect.tableName(name, database.schema);
879
881
  return new Proxy({
880
882
  name,
881
883
  definition,
@@ -890,29 +892,29 @@ function createTableHandle(database, definition) {
890
892
  name
891
893
  }]))),
892
894
  async insert(row, returning) {
893
- return await database.write(insertOf(definition, row, "insert", void 0, void 0, returning));
895
+ return await database.write(insertOf(definition, target, row, "insert", void 0, void 0, returning));
894
896
  },
895
897
  async insertIgnore(row, conflict) {
896
- await database.run(insertOf(definition, row, "ignore", conflict));
898
+ await database.run(insertOf(definition, target, row, "ignore", conflict));
897
899
  },
898
900
  async insertFromSelect(columns, source, options) {
899
- await database.run(insertFromSelectOf(definition, columns, source, options));
901
+ await database.run(insertFromSelectOf(definition, target, columns, source, options));
900
902
  },
901
903
  async insertMany(rows, options) {
902
- await database.run(insertManyOf(definition, rows, options));
904
+ await database.run(insertManyOf(definition, target, rows, options));
903
905
  },
904
906
  async upsert(row, conflict, update) {
905
- await database.run(insertOf(definition, row, "upsert", conflict, update));
907
+ await database.run(insertOf(definition, target, row, "upsert", conflict, update));
906
908
  },
907
909
  async update(patch, condition, returning) {
908
- return await database.write(updateOf(definition, patch, condition, returning));
910
+ return await database.write(updateOf(definition, target, patch, condition, returning));
909
911
  },
910
912
  async upsertExpr(row, conflict, assignments) {
911
- await database.run(upsertExprOf(definition, row, conflict, assignments));
913
+ await database.run(upsertExprOf(definition, target, row, conflict, assignments));
912
914
  },
913
915
  async delete(condition, returning) {
914
- if (returning !== void 0 && returning.length > 0) return await database.write(deleteOf(definition, condition, returning));
915
- await database.run(deleteOf(definition, condition));
916
+ if (returning !== void 0 && returning.length > 0) return await database.write(deleteOf(definition, target, condition, returning));
917
+ await database.run(deleteOf(definition, target, condition));
916
918
  return [];
917
919
  }
918
920
  }, { get(target, property, receiver) {
@@ -993,7 +995,7 @@ function assertPragmaValue(name, value) {
993
995
  }
994
996
  /** 现有的表与视图名(迁移探测旧表、拒绝别人的库都要它)。 */
995
997
  async function objectNames(database) {
996
- const query = database.dialect.name === "sqlite" ? fragment("SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'") : fragment("SELECT table_name AS name FROM information_schema.tables WHERE table_schema = current_schema()");
998
+ const query = database.dialect.name === "sqlite" ? fragment("SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'") : fragment("SELECT table_name AS name FROM information_schema.tables WHERE table_schema = COALESCE(?, current_schema())", [database.schema ?? null]);
997
999
  return (await database.read(query)).map((row) => row.name);
998
1000
  }
999
1001
  /** 回收空间与统计(SQLite `VACUUM` / PostgreSQL `VACUUM ANALYZE`);不得在事务内调用。 */
@@ -1197,7 +1199,10 @@ function withCatalog(config) {
1197
1199
  async function openDatabase(config) {
1198
1200
  const catalog = withCatalog(config);
1199
1201
  const executor = config.dialect === "sqlite" ? openSqlite(config) : openPostgres(config);
1200
- return createDatabase(createDialect(config.dialect), executor, catalog, config.onQuery);
1202
+ return createDatabase(createDialect(config.dialect), executor, catalog, {
1203
+ schema: config.dialect === "postgres" ? config.schema : void 0,
1204
+ onQuery: config.onQuery
1205
+ });
1201
1206
  }
1202
1207
  async function* iterateWrapped(rows, dialect) {
1203
1208
  try {
@@ -1206,7 +1211,9 @@ async function* iterateWrapped(rows, dialect) {
1206
1211
  throw classifyError(error, dialect);
1207
1212
  }
1208
1213
  }
1209
- function createDatabase(dialect, executor, catalog, onQuery) {
1214
+ function createDatabase(dialect, executor, catalog, options = {}) {
1215
+ const { onQuery } = options;
1216
+ const schema = dialect.name === "postgres" ? options.schema ?? "public" : void 0;
1210
1217
  const observe = async (query, run) => {
1211
1218
  const { text, params } = render(query, dialect.name);
1212
1219
  const started = performance.now();
@@ -1233,6 +1240,7 @@ function createDatabase(dialect, executor, catalog, onQuery) {
1233
1240
  const database = {
1234
1241
  dialect,
1235
1242
  catalog,
1243
+ schema,
1236
1244
  table: (source) => {
1237
1245
  const definition = typeof source === "string" ? catalog.get(source) : source;
1238
1246
  if (definition === void 0) throw new Error(`rdbkit: no table '${typeof source === "string" ? source : source.name}' in catalog`);
@@ -1256,7 +1264,10 @@ function createDatabase(dialect, executor, catalog, onQuery) {
1256
1264
  return observe(query, (text, params) => executor.run(text, params));
1257
1265
  },
1258
1266
  transaction(fn) {
1259
- return executor.transaction((scoped) => fn(createDatabase(dialect, scoped, catalog, onQuery)));
1267
+ return executor.transaction((scoped) => fn(createDatabase(dialect, scoped, catalog, {
1268
+ schema,
1269
+ onQuery
1270
+ })));
1260
1271
  },
1261
1272
  objectNames: () => objectNames(database),
1262
1273
  vacuum: () => vacuum(database),
@@ -1280,7 +1291,7 @@ function wrapDatabase(dialectName, ports) {
1280
1291
  close: async () => {
1281
1292
  await ports.close?.();
1282
1293
  }
1283
- }, indexCatalog([]));
1294
+ }, indexCatalog([]), { schema: ports.schema });
1284
1295
  return ports.transaction === void 0 ? database : {
1285
1296
  ...database,
1286
1297
  transaction: ports.transaction
@@ -1319,7 +1330,7 @@ function planSchema(dialect, catalog, shapes, options = {}) {
1319
1330
  changes.push({
1320
1331
  table: table.name,
1321
1332
  kind: "add-column",
1322
- statement: addColumnSql(dialect, table.name, name, assertAddable(table.name, name, column))
1333
+ statement: addColumnSql(dialect, table.name, name, assertAddable(table.name, name, column), options.schema)
1323
1334
  });
1324
1335
  }
1325
1336
  if (options.allowDestructive === true) for (const name of shape.columns) {
@@ -1327,7 +1338,7 @@ function planSchema(dialect, catalog, shapes, options = {}) {
1327
1338
  changes.push({
1328
1339
  table: table.name,
1329
1340
  kind: "drop-column",
1330
- statement: dropColumnSql(dialect, table.name, name)
1341
+ statement: dropColumnSql(dialect, table.name, name, options.schema)
1331
1342
  });
1332
1343
  }
1333
1344
  }
@@ -1336,7 +1347,7 @@ function planSchema(dialect, catalog, shapes, options = {}) {
1336
1347
  changes.push({
1337
1348
  table: table.name,
1338
1349
  kind: "create-index",
1339
- statement: createIndexSql(dialect, table, name, index.columns)
1350
+ statement: createIndexSql(dialect, table, name, index.columns, false, options.schema)
1340
1351
  });
1341
1352
  }
1342
1353
  for (const [name, unique] of Object.entries(table.uniques ?? {})) {
@@ -1344,7 +1355,7 @@ function planSchema(dialect, catalog, shapes, options = {}) {
1344
1355
  changes.push({
1345
1356
  table: table.name,
1346
1357
  kind: "create-index",
1347
- statement: createIndexSql(dialect, table, name, unique.columns, true)
1358
+ statement: createIndexSql(dialect, table, name, unique.columns, true, options.schema)
1348
1359
  });
1349
1360
  }
1350
1361
  }
@@ -1356,9 +1367,13 @@ function assertAddable(table, name, column) {
1356
1367
  }
1357
1368
  /** 让库结构与表定义对齐,并返回实际执行的语句。 */
1358
1369
  async function ensureSchema(database, catalog, options = {}) {
1370
+ const schema = options.schema ?? database.schema;
1359
1371
  const shapes = /* @__PURE__ */ new Map();
1360
- for (const table of catalog) shapes.set(table.name, await database.dialect.readShape(shapeReader(database), table.name, options.schema));
1361
- const changes = planSchema(database.dialect, catalog, shapes, options);
1372
+ for (const table of catalog) shapes.set(table.name, await database.dialect.readShape(shapeReader(database), table.name, schema));
1373
+ const changes = planSchema(database.dialect, catalog, shapes, {
1374
+ ...options,
1375
+ ...schema === void 0 ? {} : { schema }
1376
+ });
1362
1377
  for (const change of changes) {
1363
1378
  options.onStatement?.(change.statement);
1364
1379
  await database.run(fragment(change.statement));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/rdbkit",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Schema-first RDB kit for DeepSeek Harness plugins: one table definition drives the DDL, the typed table handles and the startup migration for SQLite and PostgreSQL alike; business code only ever sees an injected Database.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "engines": {
46
46
  "node": ">=24.11.0",
47
- "dsh": ">= 0.2.2-alpha.3 < 0.2.3"
47
+ "dsh": ">= 0.2.2-rc.1 < 0.2.3"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "pnpm exec tsdown"
@@ -1,8 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import {
3
- createPostgresTestDatabase,
4
- type PostgresTestDatabase,
5
- } from "../testing.ts";
2
+ import { createPostgresTestDatabase, type PostgresTestDatabase } from "../testing.ts";
6
3
  import {
7
4
  count,
8
5
  defineCatalog,
@@ -10,7 +7,9 @@ import {
10
7
  ensureSchema,
11
8
  fragment,
12
9
  openDatabase,
10
+ runMigrations,
13
11
  sum,
12
+ type Database,
14
13
  } from "../index.ts";
15
14
 
16
15
  const items = defineTable({
@@ -62,10 +61,11 @@ describe.skipIf(!process.env.TEST_PG_URL)("PostgreSQL 上同一套构建", () =>
62
61
 
63
62
  const table = database.table("t_items");
64
63
  const partTable = database.table("t_parts");
65
- const [created] = await table.insert(
66
- { f_owner: "a", f_count: 1, f_at: 1_788_852_417_123 },
67
- ["f_id", "f_owner", "f_at"],
68
- );
64
+ const [created] = await table.insert({ f_owner: "a", f_count: 1, f_at: 1_788_852_417_123 }, [
65
+ "f_id",
66
+ "f_owner",
67
+ "f_at",
68
+ ]);
69
69
  expect(created).toMatchObject({ f_owner: "a" });
70
70
  // 64 位整数读回来是 BigInt:超过 2^53 的 uint64 / int64 不会丢精度。
71
71
  expect(created?.f_at).toBe(1_788_852_417_123n);
@@ -114,6 +114,82 @@ describe.skipIf(!process.env.TEST_PG_URL)("PostgreSQL 上同一套构建", () =>
114
114
  await testDatabase.drop();
115
115
  }
116
116
  });
117
+
118
+ it("自定义 schema:DDL / 读写 / 迁移账本 / 结构探测都在同一处,public 不留副本", async () => {
119
+ const testDatabase: PostgresTestDatabase = await createPostgresTestDatabase();
120
+ const catalog = defineCatalog([items, parts]);
121
+ const config = {
122
+ dialect: "postgres" as const,
123
+ connectionString: testDatabase.connectionString,
124
+ schema: "dsh_test",
125
+ catalog,
126
+ };
127
+ const migration = {
128
+ id: "20261109-pg-schema-probe",
129
+ up: async (tx: Database) => {
130
+ await tx.table("t_items").insert({ f_owner: "migrated" });
131
+ },
132
+ };
133
+
134
+ const database = await openDatabase(config);
135
+ try {
136
+ await database.run(fragment('CREATE SCHEMA IF NOT EXISTS "dsh_test"'));
137
+
138
+ // 结构:建表与索引都限定到配置的 schema,且重复对齐是幂等的。
139
+ const statements = await ensureSchema(database, [items, parts]);
140
+ expect(statements.length).toBeGreaterThan(0);
141
+ expect(statements.every((statement) => statement.includes('"dsh_test".'))).toBe(true);
142
+ expect(await ensureSchema(database, [items, parts])).toEqual([]);
143
+
144
+ // 读写:句柄不依赖连接串的 search_path,直接落在配置的 schema。
145
+ const table = database.table("t_items");
146
+ await table.insert({ f_owner: "a", f_count: 1 });
147
+ const owners = await collect(table.select({ owner: table.column("f_owner") }));
148
+ expect(owners).toEqual([{ owner: "a" }]);
149
+
150
+ // 迁移账本:建表、读、记账都在同一 schema,重跑只跑没记过的。
151
+ expect(await runMigrations(database, { catalog, migrations: [migration] })).toEqual([
152
+ migration.id,
153
+ ]);
154
+ expect(await runMigrations(database, { catalog, migrations: [migration] })).toEqual([]);
155
+ const ledger = await database.read<{ f_id: string }>(
156
+ fragment('SELECT f_id FROM "dsh_test"."t_schema_migrations"'),
157
+ );
158
+ expect(ledger.map((row) => row.f_id)).toEqual([migration.id]);
159
+
160
+ // 结构探测跟着配置的 schema(旧数据迁移靠它判旧表在不在)。
161
+ expect(await database.objectNames()).toEqual(
162
+ expect.arrayContaining(["t_items", "t_schema_migrations"]),
163
+ );
164
+
165
+ // 表与账本都在 dsh_test,public 不留副本。
166
+ const placements = await database.read<{ table_schema: string; table_name: string }>(
167
+ fragment(
168
+ "SELECT table_schema, table_name FROM information_schema.tables " +
169
+ "WHERE table_name IN ('t_items', 't_parts', 't_schema_migrations') ORDER BY table_name",
170
+ ),
171
+ );
172
+ expect(placements.map((row) => `${row.table_schema}.${row.table_name}`)).toEqual([
173
+ "dsh_test.t_items",
174
+ "dsh_test.t_parts",
175
+ "dsh_test.t_schema_migrations",
176
+ ]);
177
+ } finally {
178
+ await database.close();
179
+ }
180
+
181
+ // 再次打开:结构已对齐、迁移已记账,数据还在。
182
+ const reopened = await openDatabase(config);
183
+ try {
184
+ expect(await ensureSchema(reopened, [items, parts])).toEqual([]);
185
+ expect(await runMigrations(reopened, { catalog, migrations: [migration] })).toEqual([]);
186
+ const [total] = await collect(reopened.table("t_items").select({ n: count() }));
187
+ expect(Number(total?.n)).toBe(2);
188
+ } finally {
189
+ await reopened.close();
190
+ await testDatabase.drop();
191
+ }
192
+ });
117
193
  });
118
194
 
119
195
  async function collect<T>(rows: AsyncIterable<T>): Promise<T[]> {
@@ -4,9 +4,15 @@ import {
4
4
  createTableSql,
5
5
  defineCatalog,
6
6
  defineTable,
7
+ E,
7
8
  ensureSchema,
8
9
  fragment,
9
10
  openDatabase,
11
+ planSchema,
12
+ render,
13
+ runMigrations,
14
+ wrapDatabase,
15
+ type Database,
10
16
  } from "../index.ts";
11
17
 
12
18
  const catalog = defineCatalog([
@@ -142,3 +148,126 @@ describe("SQLite 连接调优选项", () => {
142
148
  }
143
149
  });
144
150
  });
151
+ /** 记录 SQL 的假 PostgreSQL 执行面:不需要真服务就能看表句柄 / 迁移渲染出来的语句。 */
152
+ function recordingPostgres(schema?: string): {
153
+ database: Database;
154
+ statements: { text: string; params: readonly unknown[] }[];
155
+ reply: (rows: unknown[]) => void;
156
+ } {
157
+ const statements: { text: string; params: readonly unknown[] }[] = [];
158
+ let rows: unknown[] = [];
159
+ let database: Database;
160
+ database = wrapDatabase("postgres", {
161
+ ...(schema === undefined ? {} : { schema }),
162
+ async *iterate(text, params) {
163
+ statements.push({ text, params });
164
+ yield* rows;
165
+ },
166
+ async run(text, params) {
167
+ statements.push({ text, params });
168
+ },
169
+ transaction: (fn) => fn(database),
170
+ });
171
+ return {
172
+ database,
173
+ statements,
174
+ reply(next) {
175
+ rows = next;
176
+ },
177
+ };
178
+ }
179
+
180
+ describe("PostgreSQL 自定义 schema", () => {
181
+ const postgres = createDialect("postgres");
182
+ const items = catalog[0]!;
183
+ const links = catalog[1]!;
184
+
185
+ it("DDL 的建表 / 外键引用 / 补列 / 索引都带 schema", () => {
186
+ const createLinks = createTableSql(postgres, links, "dsh_test");
187
+ expect(createLinks).toContain('CREATE TABLE IF NOT EXISTS "dsh_test"."t_links"');
188
+ expect(createLinks).toContain('REFERENCES "dsh_test"."t_items"("f_owner")');
189
+
190
+ const existing = new Map([
191
+ [
192
+ "t_items",
193
+ { columns: new Set(["f_id", "f_owner", "f_active"]), indexes: new Set<string>() },
194
+ ],
195
+ ]);
196
+ const statements = planSchema(postgres, catalog, existing, { schema: "dsh_test" }).map(
197
+ (change) => change.statement,
198
+ );
199
+ expect(statements).toContain(
200
+ 'ALTER TABLE "dsh_test"."t_items" ADD COLUMN "f_count" INTEGER NOT NULL DEFAULT 0',
201
+ );
202
+ expect(statements).toContain(
203
+ 'CREATE INDEX IF NOT EXISTS "idx_items_owner" ON "dsh_test"."t_items" ("f_owner")',
204
+ );
205
+ expect(statements.every((statement) => statement.includes('"dsh_test".'))).toBe(true);
206
+ });
207
+
208
+ it("表句柄的读写与 join 都指向配置的 schema", async () => {
209
+ const { database, statements } = recordingPostgres("dsh_test");
210
+ const table = database.table(items);
211
+ await table.insert({ f_owner: "a", f_count: 1, f_active: true });
212
+ await table.update({ f_count: 2 }, E.eq(table.f_owner, "a"));
213
+ await table.delete(E.eq(table.f_owner, "a"));
214
+
215
+ const sql = statements.map((statement) => statement.text);
216
+ expect(sql[0]).toContain('INSERT INTO "dsh_test"."t_items"');
217
+ expect(sql[1]).toContain('UPDATE "dsh_test"."t_items"');
218
+ expect(sql[2]).toContain('DELETE FROM "dsh_test"."t_items"');
219
+ expect(render(table.select().toFragment(), "postgres").text).toContain(
220
+ 'FROM "dsh_test"."t_items"',
221
+ );
222
+ const linksTable = database.table(links);
223
+ expect(
224
+ render(
225
+ table
226
+ .select({ owner: table.f_owner })
227
+ .join(links, E.eq(table.f_owner, linksTable.f_owner))
228
+ .toFragment(),
229
+ "postgres",
230
+ ).text,
231
+ ).toContain('JOIN "dsh_test"."t_links"');
232
+ });
233
+
234
+ it("ensureSchema 与结构探测缺省用连接上的 schema", async () => {
235
+ const { database, statements } = recordingPostgres("dsh_test");
236
+ const applied = await ensureSchema(database, [items]);
237
+ expect(
238
+ applied.some((statement) =>
239
+ statement.includes('CREATE TABLE IF NOT EXISTS "dsh_test"."t_items"'),
240
+ ),
241
+ ).toBe(true);
242
+ expect(applied.every((statement) => statement.includes('"dsh_test".'))).toBe(true);
243
+ // 结构探测查的是配置的 schema,不是硬编码的 public。
244
+ expect(statements[0]!.text).toContain("information_schema.columns");
245
+ expect(statements[0]!.params).toEqual(["dsh_test", "t_items"]);
246
+ });
247
+
248
+ it("迁移账本的建表 / 读 / 记账都在配置的 schema", async () => {
249
+ const { database, statements } = recordingPostgres("dsh_test");
250
+ const migrations = [{ id: "20261109-probe", up: async () => {} }];
251
+ expect(await runMigrations(database, { catalog: [items], migrations })).toEqual([
252
+ "20261109-probe",
253
+ ]);
254
+ const sql = statements.map((statement) => statement.text);
255
+ expect(
256
+ sql.some((text) =>
257
+ text.includes('CREATE TABLE IF NOT EXISTS "dsh_test"."t_schema_migrations"'),
258
+ ),
259
+ ).toBe(true);
260
+ expect(sql.some((text) => text.includes('FROM "dsh_test"."t_schema_migrations"'))).toBe(true);
261
+ expect(sql.some((text) => text.includes('INSERT INTO "dsh_test"."t_schema_migrations"'))).toBe(
262
+ true,
263
+ );
264
+ });
265
+
266
+ it("objectNames 查配置的 schema", async () => {
267
+ const { database, statements } = recordingPostgres("dsh_test");
268
+ await database.objectNames();
269
+ expect(statements).toHaveLength(1);
270
+ expect(statements[0]!.text).toContain("information_schema.tables");
271
+ expect(statements[0]!.params).toEqual(["dsh_test"]);
272
+ });
273
+ });
@@ -29,7 +29,8 @@ export async function objectNames(database: Database): Promise<string[]> {
29
29
  "SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'",
30
30
  )
31
31
  : fragment(
32
- "SELECT table_name AS name FROM information_schema.tables WHERE table_schema = current_schema()",
32
+ "SELECT table_name AS name FROM information_schema.tables WHERE table_schema = COALESCE(?, current_schema())",
33
+ [database.schema ?? null],
33
34
  );
34
35
  const rows = await database.read<{ name: string }>(query);
35
36
  return rows.map((row) => row.name);
@@ -21,12 +21,10 @@ export async function openDatabase<const C extends Catalog = Catalog>(
21
21
  const catalog = withCatalog(config);
22
22
  const executor =
23
23
  config.dialect === "sqlite" ? openSqlite(config) : openPostgres(config);
24
- return createDatabase(
25
- createDialect(config.dialect),
26
- executor,
27
- catalog,
28
- config.onQuery,
29
- ) as Database<C>;
24
+ return createDatabase(createDialect(config.dialect), executor, catalog, {
25
+ schema: config.dialect === "postgres" ? config.schema : undefined,
26
+ onQuery: config.onQuery,
27
+ }) as Database<C>;
30
28
  }
31
29
 
32
30
  async function* iterateWrapped<T>(
@@ -44,8 +42,14 @@ export function createDatabase(
44
42
  dialect: Dialect,
45
43
  executor: Executor,
46
44
  catalog: CatalogIndex,
47
- onQuery?: (event: QueryEvent) => void,
45
+ options: {
46
+ schema?: string | undefined;
47
+ onQuery?: ((event: QueryEvent) => void) | undefined;
48
+ } = {},
48
49
  ): Database {
50
+ const { onQuery } = options;
51
+ // PostgreSQL 的目标 schema 只在这里定一次(缺省 public);SQLite 没有 schema 概念。
52
+ const schema = dialect.name === "postgres" ? (options.schema ?? "public") : undefined;
49
53
  const observe = async <T>(
50
54
  query: Fragment,
51
55
  run: (text: string, params: readonly unknown[]) => Promise<T>,
@@ -75,6 +79,7 @@ export function createDatabase(
75
79
  const database: Database = {
76
80
  dialect,
77
81
  catalog,
82
+ schema,
78
83
  table: (source: TableDef | string) => {
79
84
  const definition = typeof source === "string" ? catalog.get(source) : source;
80
85
  if (definition === undefined) {
@@ -104,7 +109,7 @@ export function createDatabase(
104
109
  transaction(fn) {
105
110
  // catalog 跟着事务走:事务里按表名取句柄与外面一致。
106
111
  return executor.transaction((scoped) =>
107
- fn(createDatabase(dialect, scoped, catalog, onQuery)),
112
+ fn(createDatabase(dialect, scoped, catalog, { schema, onQuery })),
108
113
  );
109
114
  },
110
115
  objectNames: () => objectNames(database),
@@ -14,6 +14,11 @@ export interface Database<C extends Catalog = Catalog> {
14
14
  readonly dialect: Dialect;
15
15
  /** 装配层注册的 catalog(按表名解析;没注册时只能传定义)。 */
16
16
  readonly catalog: CatalogIndex;
17
+ /**
18
+ * 生成 SQL 的目标 schema:PostgreSQL 恒有值(配置缺省 `public`),SQLite 是 `undefined`。
19
+ * DDL、DML、迁移账本与结构探测都从这里取,不各自猜。
20
+ */
21
+ readonly schema: string | undefined;
17
22
  /** 按表名取(catalog 里有定义时保留强类型)。 */
18
23
  table<K extends C[number]["name"] & string>(
19
24
  name: K,
@@ -8,6 +8,8 @@ import type { Database } from "./types.ts";
8
8
  export function wrapDatabase(
9
9
  dialectName: DialectName,
10
10
  ports: {
11
+ /** 宿主连接的目标 schema(PostgreSQL;省略即 `public`,与 rdbkit 默认一致)。 */
12
+ schema?: string | undefined;
11
13
  iterate(text: string, params: readonly unknown[]): AsyncIterable<unknown>;
12
14
  run(text: string, params: readonly unknown[]): Promise<void>;
13
15
  /** 宿主自己管事务时提供:把当前事务的执行面做成 scoped `Database`(对应 Go 的 `Tx(ctx, fn)`)。 */
@@ -30,6 +32,7 @@ export function wrapDatabase(
30
32
  },
31
33
  },
32
34
  indexCatalog([]),
35
+ { schema: ports.schema },
33
36
  );
34
37
  return ports.transaction === undefined
35
38
  ? database
package/src/dialect.ts CHANGED
@@ -115,8 +115,13 @@ function postgresDialect(): Dialect {
115
115
  };
116
116
  }
117
117
 
118
- /** 一列的定义 SQL(`"name" TEXT NOT NULL ...`)。 */
119
- export function columnSql(dialect: Dialect, name: string, column: ColumnDef): string {
118
+ /** 一列的定义 SQL(`"name" TEXT NOT NULL ...`);外键引用跟着表名一起落在同一 schema。 */
119
+ export function columnSql(
120
+ dialect: Dialect,
121
+ name: string,
122
+ column: ColumnDef,
123
+ schema?: string,
124
+ ): string {
120
125
  assertIdentifier("column", name);
121
126
  const parts = [dialect.quote(name), dialect.columnType(column)];
122
127
  if (column.type === "serial" && dialect.name === "sqlite")
@@ -131,7 +136,7 @@ export function columnSql(dialect: Dialect, name: string, column: ColumnDef): st
131
136
  assertIdentifier("table", table);
132
137
  assertIdentifier("column", referenced);
133
138
  parts.push(
134
- `REFERENCES ${dialect.tableName(table)}(${dialect.quote(referenced)})` +
139
+ `REFERENCES ${dialect.tableName(table, schema)}(${dialect.quote(referenced)})` +
135
140
  (onDelete === undefined ? "" : ` ON DELETE ${onDelete.toUpperCase()}`),
136
141
  );
137
142
  }
@@ -156,7 +161,7 @@ function literal(dialect: Dialect, value: string | number, type: ColumnDef["type
156
161
  /** 建表语句(IF NOT EXISTS;列与表级约束都由定义算出来)。 */
157
162
  export function createTableSql(dialect: Dialect, table: TableDef, schema?: string): string {
158
163
  const parts = Object.entries(table.columns).map(([name, column]) =>
159
- columnSql(dialect, name, column),
164
+ columnSql(dialect, name, column, schema),
160
165
  );
161
166
  const primaryKey =
162
167
  table.primaryKey === undefined
@@ -188,18 +193,24 @@ export function createIndexSql(
188
193
  name: string,
189
194
  columns: readonly string[],
190
195
  unique = false,
196
+ schema?: string,
191
197
  ): string {
192
198
  assertIdentifier("index", name);
193
199
  return (
194
200
  `CREATE ${unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${dialect.quote(name)} ` +
195
- `ON ${dialect.tableName(table.name)} (${columns.map((column) => dialect.quote(column)).join(", ")})`
201
+ `ON ${dialect.tableName(table.name, schema)} (${columns.map((column) => dialect.quote(column)).join(", ")})`
196
202
  );
197
203
  }
198
204
 
199
205
  /** 删列(只在显式允许破坏性变更时用)。 */
200
- export function dropColumnSql(dialect: Dialect, table: string, name: string): string {
206
+ export function dropColumnSql(
207
+ dialect: Dialect,
208
+ table: string,
209
+ name: string,
210
+ schema?: string,
211
+ ): string {
201
212
  assertIdentifier("column", name);
202
- return `ALTER TABLE ${dialect.tableName(table)} DROP COLUMN ${dialect.quote(name)}`;
213
+ return `ALTER TABLE ${dialect.tableName(table, schema)} DROP COLUMN ${dialect.quote(name)}`;
203
214
  }
204
215
 
205
216
  /** 删索引(只在显式允许破坏性变更时用)。 */
@@ -214,6 +225,7 @@ export function addColumnSql(
214
225
  table: string,
215
226
  name: string,
216
227
  column: ColumnDef,
228
+ schema?: string,
217
229
  ): string {
218
- return `ALTER TABLE ${dialect.tableName(table)} ADD COLUMN ${columnSql(dialect, name, column)}`;
230
+ return `ALTER TABLE ${dialect.tableName(table, schema)} ADD COLUMN ${columnSql(dialect, name, column, schema)}`;
219
231
  }
package/src/migrate.ts CHANGED
@@ -19,7 +19,7 @@ export interface SchemaChange {
19
19
  }
20
20
 
21
21
  export interface EnsureSchemaOptions {
22
- /** PostgreSQL 的 schema(默认 `public`);SQLite 忽略。 */
22
+ /** PostgreSQL 的 schema(省略即 `database.schema`,再缺省是 `public`);SQLite 忽略。 */
23
23
  schema?: string;
24
24
  /** 允许删掉定义里已经不存在、且确实多出来的列(默认关闭:升级不丢数据)。 */
25
25
  allowDestructive?: boolean;
@@ -64,6 +64,7 @@ export function planSchema(
64
64
  table.name,
65
65
  name,
66
66
  assertAddable(table.name, name, column),
67
+ options.schema,
67
68
  ),
68
69
  });
69
70
  }
@@ -73,7 +74,7 @@ export function planSchema(
73
74
  changes.push({
74
75
  table: table.name,
75
76
  kind: "drop-column",
76
- statement: dropColumnSql(dialect, table.name, name),
77
+ statement: dropColumnSql(dialect, table.name, name, options.schema),
77
78
  });
78
79
  }
79
80
  }
@@ -83,7 +84,7 @@ export function planSchema(
83
84
  changes.push({
84
85
  table: table.name,
85
86
  kind: "create-index",
86
- statement: createIndexSql(dialect, table, name, index.columns),
87
+ statement: createIndexSql(dialect, table, name, index.columns, false, options.schema),
87
88
  });
88
89
  }
89
90
  for (const [name, unique] of Object.entries(table.uniques ?? {})) {
@@ -92,7 +93,7 @@ export function planSchema(
92
93
  changes.push({
93
94
  table: table.name,
94
95
  kind: "create-index",
95
- statement: createIndexSql(dialect, table, name, unique.columns, true),
96
+ statement: createIndexSql(dialect, table, name, unique.columns, true, options.schema),
96
97
  });
97
98
  }
98
99
  }
@@ -115,14 +116,19 @@ export async function ensureSchema(
115
116
  catalog: Catalog,
116
117
  options: EnsureSchemaOptions = {},
117
118
  ): Promise<string[]> {
119
+ // schema 是连接上的事实:配置给了就用配置的,没给就跟着连接打开时的有效 schema。
120
+ const schema = options.schema ?? database.schema;
118
121
  const shapes = new Map<string, TableShape | undefined>();
119
122
  for (const table of catalog) {
120
123
  shapes.set(
121
124
  table.name,
122
- await database.dialect.readShape(shapeReader(database), table.name, options.schema),
125
+ await database.dialect.readShape(shapeReader(database), table.name, schema),
123
126
  );
124
127
  }
125
- const changes = planSchema(database.dialect, catalog, shapes, options);
128
+ const changes = planSchema(database.dialect, catalog, shapes, {
129
+ ...options,
130
+ ...(schema === undefined ? {} : { schema }),
131
+ });
126
132
  for (const change of changes) {
127
133
  options.onStatement?.(change.statement);
128
134
  await database.run(fragment(change.statement));
@@ -83,6 +83,8 @@ export function createTableHandle<T extends TableDef>(
83
83
  definition: T,
84
84
  ): TableHandle<T> {
85
85
  const name = definition.name;
86
+ // 目标表名按连接的方言算一次(PostgreSQL 带 schema,SQLite 就是表名)。
87
+ const target = database.dialect.tableName(name, database.schema);
86
88
  const handle: TableHandleBase<T> = {
87
89
  name,
88
90
  definition,
@@ -101,32 +103,34 @@ export function createTableHandle<T extends TableDef>(
101
103
  ) as never,
102
104
  async insert(row, returning) {
103
105
  return await database.write<RowOf<T>>(
104
- insertOf(definition, row, "insert", undefined, undefined, returning),
106
+ insertOf(definition, target, row, "insert", undefined, undefined, returning),
105
107
  );
106
108
  },
107
109
  async insertIgnore(row, conflict) {
108
- await database.run(insertOf(definition, row, "ignore", conflict));
110
+ await database.run(insertOf(definition, target, row, "ignore", conflict));
109
111
  },
110
112
  async insertFromSelect(columns, source, options) {
111
- await database.run(insertFromSelectOf(definition, columns, source, options));
113
+ await database.run(insertFromSelectOf(definition, target, columns, source, options));
112
114
  },
113
115
  async insertMany(rows, options) {
114
- await database.run(insertManyOf(definition, rows, options));
116
+ await database.run(insertManyOf(definition, target, rows, options));
115
117
  },
116
118
  async upsert(row, conflict, update) {
117
- await database.run(insertOf(definition, row, "upsert", conflict, update));
119
+ await database.run(insertOf(definition, target, row, "upsert", conflict, update));
118
120
  },
119
121
  async update(patch, condition, returning) {
120
- return await database.write<RowOf<T>>(updateOf(definition, patch, condition, returning));
122
+ return await database.write<RowOf<T>>(
123
+ updateOf(definition, target, patch, condition, returning),
124
+ );
121
125
  },
122
126
  async upsertExpr(row, conflict, assignments) {
123
- await database.run(upsertExprOf(definition, row, conflict, assignments));
127
+ await database.run(upsertExprOf(definition, target, row, conflict, assignments));
124
128
  },
125
129
  async delete(condition, returning) {
126
130
  if (returning !== undefined && returning.length > 0) {
127
- return await database.write<RowOf<T>>(deleteOf(definition, condition, returning));
131
+ return await database.write<RowOf<T>>(deleteOf(definition, target, condition, returning));
128
132
  }
129
- await database.run(deleteOf(definition, condition));
133
+ await database.run(deleteOf(definition, target, condition));
130
134
  return [];
131
135
  },
132
136
  };
@@ -10,6 +10,7 @@ export function returningClause(returning?: readonly string[]): Fragment | strin
10
10
 
11
11
  export function insertOf(
12
12
  table: TableDef,
13
+ target: string,
13
14
  row: Record<string, unknown>,
14
15
  mode: "insert" | "ignore" | "upsert",
15
16
  conflict?: readonly string[],
@@ -19,26 +20,30 @@ export function insertOf(
19
20
  const columns = Object.keys(row).filter((name) => table.columns[name] !== undefined);
20
21
  if (columns.length === 0) throw new Error(`rdbkit: insert into "${table.name}" has no columns`);
21
22
  const sql =
22
- `INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")}) ` +
23
+ `INSERT INTO ${target} (${columns.map((name) => `"${name}"`).join(", ")}) ` +
23
24
  `VALUES (${columns.map(() => "?").join(", ")})`;
24
25
  const base = fragment(
25
26
  sql,
26
27
  columns.map((name) => row[name]),
27
28
  );
28
29
  if (mode === "insert") return concat([base, returningClause(returning)], " ");
29
- const target = (conflict ?? []).map((name) => `"${name}"`).join(", ");
30
- if (mode === "ignore") return concat([base, `ON CONFLICT (${target}) DO NOTHING`], " ");
30
+ const conflictTarget = (conflict ?? []).map((name) => `"${name}"`).join(", ");
31
+ if (mode === "ignore") return concat([base, `ON CONFLICT (${conflictTarget}) DO NOTHING`], " ");
31
32
  const updates =
32
33
  (update ?? []).length === 0
33
34
  ? "DO NOTHING"
34
35
  : `DO UPDATE SET ${(update ?? [])
35
36
  .map((name) => `"${name}" = excluded."${name}"`)
36
37
  .join(", ")}`;
37
- return concat([base, `ON CONFLICT (${target}) ${updates}`, returningClause(returning)], " ");
38
+ return concat(
39
+ [base, `ON CONFLICT (${conflictTarget}) ${updates}`, returningClause(returning)],
40
+ " ",
41
+ );
38
42
  }
39
43
 
40
44
  export function insertManyOf(
41
45
  table: TableDef,
46
+ target: string,
42
47
  rows: readonly Record<string, unknown>[],
43
48
  options: { conflict?: readonly string[]; update?: readonly string[] } = {},
44
49
  ): Fragment {
@@ -54,7 +59,7 @@ export function insertManyOf(
54
59
  }
55
60
  const tuple = `(${columns.map(() => "?").join(", ")})`;
56
61
  const sql =
57
- `INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")}) ` +
62
+ `INSERT INTO ${target} (${columns.map((name) => `"${name}"`).join(", ")}) ` +
58
63
  `VALUES ${rows.map(() => tuple).join(", ")}`;
59
64
  const insert = fragment(
60
65
  sql,
@@ -65,14 +70,13 @@ export function insertManyOf(
65
70
 
66
71
  export function insertFromSelectOf(
67
72
  table: TableDef,
73
+ target: string,
68
74
  columns: readonly string[],
69
75
  source: Fragment,
70
76
  options: { conflict?: readonly string[]; update?: readonly string[] } = {},
71
77
  ): Fragment {
72
78
  if (columns.length === 0) throw new Error(`rdbkit: insert into "${table.name}" has no columns`);
73
- const head = text(
74
- `INSERT INTO "${table.name}" (${columns.map((name) => `"${name}"`).join(", ")})`,
75
- );
79
+ const head = text(`INSERT INTO ${target} (${columns.map((name) => `"${name}"`).join(", ")})`);
76
80
  return concat([head, source, conflictClause(options)], " ");
77
81
  }
78
82
 
@@ -95,20 +99,25 @@ export function conflictClause(options: {
95
99
 
96
100
  export function upsertExprOf(
97
101
  table: TableDef,
102
+ target: string,
98
103
  row: Record<string, unknown>,
99
104
  conflict: readonly string[],
100
105
  assignments: Record<string, Operand>,
101
106
  ): Fragment {
102
- const base = insertOf(table, row, "insert");
103
- const target = conflict.map((name) => `"${name}"`).join(", ");
107
+ const base = insertOf(table, target, row, "insert");
108
+ const conflictTarget = conflict.map((name) => `"${name}"`).join(", ");
104
109
  const sets = Object.entries(assignments).map(([name, ref]) =>
105
110
  concat([text(`"${name}" =`), operandSql(ref)], " "),
106
111
  );
107
- return concat([base, text(`ON CONFLICT (${target}) DO UPDATE SET`), concat(sets, ", ")], " ");
112
+ return concat(
113
+ [base, text(`ON CONFLICT (${conflictTarget}) DO UPDATE SET`), concat(sets, ", ")],
114
+ " ",
115
+ );
108
116
  }
109
117
 
110
118
  export function updateOf(
111
119
  table: TableDef,
120
+ target: string,
112
121
  patch: Record<string, unknown>,
113
122
  condition?: Condition,
114
123
  returning?: readonly string[],
@@ -124,17 +133,18 @@ export function updateOf(
124
133
  : param(value);
125
134
  return concat([text(`"${name}" =`), assigned], " ");
126
135
  });
127
- const head = concat([text(`UPDATE "${table.name}" SET`), concat(sets, ", ")], " ");
136
+ const head = concat([text(`UPDATE ${target} SET`), concat(sets, ", ")], " ");
128
137
  const body = condition === undefined ? head : concat([head, text("WHERE"), condition], " ");
129
138
  return concat([body, returningClause(returning)], " ");
130
139
  }
131
140
 
132
141
  export function deleteOf(
133
142
  table: TableDef,
143
+ target: string,
134
144
  condition?: Condition,
135
145
  returning?: readonly string[],
136
146
  ): Fragment {
137
- const head = text(`DELETE FROM "${table.name}"`);
147
+ const head = text(`DELETE FROM ${target}`);
138
148
  const body = condition === undefined ? head : concat([head, text("WHERE"), condition], " ");
139
149
  return concat([body, returningClause(returning)], " ");
140
150
  }
@@ -122,6 +122,9 @@ export function selectQuery<T>(
122
122
  return undefined;
123
123
  };
124
124
 
125
+ // 表名按连接的方言算(PostgreSQL 带 schema):FROM / JOIN 与其余引用保持同一处。
126
+ const targetOf = (definition: TableDef): string =>
127
+ database.dialect.tableName(definition.name, database.schema);
125
128
  const toFragment = (): Fragment => {
126
129
  const items = Object.entries(projection).map(([alias, entry]) =>
127
130
  entry.kind === "column"
@@ -133,13 +136,13 @@ export function selectQuery<T>(
133
136
  const parts: (Fragment | string)[] = [
134
137
  state.distinct ? text("SELECT DISTINCT") : text("SELECT"),
135
138
  concat(items, ", "),
136
- text(`FROM "${table.name}"`),
139
+ text(`FROM ${targetOf(table)}`),
137
140
  ];
138
141
  for (const join of state.joins) {
139
142
  const target =
140
143
  join.alias === undefined
141
- ? `"${join.table.name}"`
142
- : `"${join.table.name}" AS "${join.alias}"`;
144
+ ? targetOf(join.table)
145
+ : `${targetOf(join.table)} AS "${join.alias}"`;
143
146
  if (join.kind === "cross") {
144
147
  parts.push(text(`CROSS JOIN ${target}`));
145
148
  continue;