nodejs-store 2.4.0 → 2.6.0

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/src/ddl.js CHANGED
@@ -10,7 +10,9 @@
10
10
  * - 每表必建 __present 哨兵列(形态 ,f1,f2,;同 core write/insert.rs::present_value);
11
11
  * - timestamps !== false → 追加 createdAt / updatedAt(同 core schema/registry.rs::add_timestamp_fields);
12
12
  * - 归档表 <collection>_deleted 由 registry 自动派生,本模块按已注册 def 逐表生成(不特判);
13
- * - 不生成 CREATE INDEX(SQL 后端不建索引,schema.indexes 仅元数据,铁律 6)。
13
+ * - schema.indexes(Mongo 形态 {keys: {f: 1|-1}, options/inline})→ CREATE [UNIQUE] INDEX
14
+ * (阶段 3 索引落地;原「仅元数据不建索引」铁律 6 子项按用户裁决放开,见
15
+ * common-store/事务型能力增补执行文档.md 附录 D)。与 py_store/ddl.py 逐字节对齐。
14
16
  *
15
17
  * 生成器只产出文本、不执行 —— 不违反铁律 6(绝不写 DDL 回库)。
16
18
  * 对齐 py_store/ddl.py(两端输出逐字节一致)。
@@ -38,6 +40,10 @@ const NON_COLUMN = ['object', 'array'];
38
40
  // object/array 字段的列类型(JSON 文本列;同 core Backend::json_type_name)
39
41
  const JSON_TYPE = ['JSON', 'jsonb', 'TEXT'];
40
42
  const ID_TYPE = ['VARCHAR(64)', 'TEXT', 'TEXT'];
43
+ // 阶段2:`_id` 声明 strategy=autoincrement 时的自增列类型(MySQL AUTO_INCREMENT 列
44
+ // 须被索引 —— 表级 PRIMARY KEY 满足;SQLite 语法要求 PRIMARY KEY AUTOINCREMENT 相邻,
45
+ // 由 createTable 的 pk+auto 分支拼接;PG 用 SERIAL)。与 py_store/ddl.py 逐字节对齐。
46
+ const ID_AUTO_TYPE = ['INT AUTO_INCREMENT', 'SERIAL', 'INTEGER'];
41
47
  const PRESENT_TYPE = ['VARCHAR(255)', 'TEXT', 'TEXT'];
42
48
  const TIMESTAMP_FIELDS = ['createdAt', 'updatedAt'];
43
49
  const MYSQL_PRESENT_MAX = 255;
@@ -56,7 +62,7 @@ function declaredType(fieldDef) {
56
62
  return fieldDef && typeof fieldDef === 'object' ? fieldDef.type : fieldDef;
57
63
  }
58
64
 
59
- /** 返回 [[name, sqlType, pk]],顺序:声明的字段(标量 / object·array JSON 列)→ timestamps → __present */
65
+ /** 返回 [[name, sqlType, pk, auto]],顺序:声明的字段(标量 / object·array JSON 列)→ timestamps → __present */
60
66
  function columns(defn, backend) {
61
67
  const i = idx(backend);
62
68
  const cols = [];
@@ -64,12 +70,14 @@ function columns(defn, backend) {
64
70
  for (const [name, fdef] of Object.entries(fields)) {
65
71
  const ftype = declaredType(fdef);
66
72
  if (name === '_id') {
67
- cols.push([name, ID_TYPE[i], true]);
73
+ const strategy = fdef && typeof fdef === 'object' ? fdef.strategy : undefined;
74
+ cols.push([name, strategy === 'autoincrement' ? ID_AUTO_TYPE[i] : ID_TYPE[i], true,
75
+ strategy === 'autoincrement']);
68
76
  continue;
69
77
  }
70
78
  if (NON_COLUMN.includes(ftype)) {
71
79
  // object/array → 单列 JSON 文本(同 core field_column_ref::Json)
72
- cols.push([name, JSON_TYPE[i], false]);
80
+ cols.push([name, JSON_TYPE[i], false, false]);
73
81
  continue;
74
82
  }
75
83
  if (!Object.prototype.hasOwnProperty.call(TYPES, ftype)) {
@@ -77,17 +85,17 @@ function columns(defn, backend) {
77
85
  `DDL 生成:字段 "${defn.name}.${name}" 类型 ${JSON.stringify(ftype)} 未知,支持 ${Object.keys(TYPES).sort()}`,
78
86
  );
79
87
  }
80
- cols.push([name, TYPES[ftype][i], false]);
88
+ cols.push([name, TYPES[ftype][i], false, false]);
81
89
  }
82
90
  if (!cols.some((c) => c[2])) {
83
91
  throw new Error(`DDL 生成:schema "${defn.name}" 缺少 _id 字段`);
84
92
  }
85
93
  if (defn.timestamps !== false) {
86
94
  for (const ts of TIMESTAMP_FIELDS) {
87
- if (!cols.some((c) => c[0] === ts)) cols.push([ts, TYPES.number[i], false]);
95
+ if (!cols.some((c) => c[0] === ts)) cols.push([ts, TYPES.number[i], false, false]);
88
96
  }
89
97
  }
90
- cols.push(['__present', PRESENT_TYPE[i], false]);
98
+ cols.push(['__present', PRESENT_TYPE[i], false, false]);
91
99
  return cols;
92
100
  }
93
101
 
@@ -106,13 +114,35 @@ function warnPresentOverflow(defn, cols) {
106
114
  }
107
115
  }
108
116
 
117
+ /** schema.indexes → CREATE [UNIQUE] INDEX 语句列表(阶段 3 索引落地)。
118
+ * 索引名 `idx_<collection>_<f1>_<f2>`(对齐 SQL 常规命名);keys 值 1/-1 → ASC/DESC。 */
119
+ function indexStmts(defn, backend) {
120
+ const out = [];
121
+ const table = defn.collection || defn.name;
122
+ for (const idx of defn.indexes || []) {
123
+ if (!idx || typeof idx !== 'object') continue;
124
+ const keys = idx.keys;
125
+ if (!keys || typeof keys !== 'object' || !Object.keys(keys).length) continue;
126
+ const unique = Boolean(idx.unique || (idx.options && idx.options.unique));
127
+ const cols = Object.entries(keys)
128
+ .map(([k, v]) => `${q(backend, k)} ${v === -1 ? 'DESC' : 'ASC'}`)
129
+ .join(', ');
130
+ const name = 'idx_' + table + '_' + Object.keys(keys).join('_');
131
+ out.push(`CREATE ${unique ? 'UNIQUE ' : ''}INDEX ${q(backend, name)} ON ${q(backend, table)} (${cols})`);
132
+ }
133
+ return out;
134
+ }
135
+
109
136
  function createTable(defn, backend) {
110
137
  const table = defn.collection || defn.name;
111
138
  const cols = columns(defn, backend);
112
139
  if (backend === 'mysql') warnPresentOverflow(defn, cols);
113
140
  const lines = [];
114
- for (const [name, ctype, pk] of cols) {
115
- if (pk && backend === 'mysql') lines.push(` ${q(backend, name)} ${ctype} NOT NULL`);
141
+ for (const [name, ctype, pk, auto] of cols) {
142
+ if (pk && auto && backend === 'sqlite') {
143
+ // SQLite 语法要求 AUTOINCREMENT 紧跟 PRIMARY KEY
144
+ lines.push(` ${q(backend, name)} ${ctype} PRIMARY KEY AUTOINCREMENT`);
145
+ } else if (pk && backend === 'mysql') lines.push(` ${q(backend, name)} ${ctype} NOT NULL`);
116
146
  else if (pk) lines.push(` ${q(backend, name)} ${ctype} PRIMARY KEY`);
117
147
  else lines.push(` ${q(backend, name)} ${ctype}`);
118
148
  }
@@ -120,13 +150,40 @@ function createTable(defn, backend) {
120
150
  return `CREATE TABLE ${q(backend, table)} (\n` + lines.join(',\n') + '\n);';
121
151
  }
122
152
 
123
- /** 生成 DDL 文本(多表以空行分隔);backend ∈ mysql/postgres/sqlite */
153
+ /** 生成 DDL 文本(多表以空行分隔,每表 CREATE TABLE 后跟其 CREATE INDEX);backend ∈ mysql/postgres/sqlite
154
+ *
155
+ * 按表名去重:同名表只出一次 CREATE TABLE + 索引(防御 core 注册表出现重复名 ——
156
+ * 上游失守即告警,禁静默;对齐 py_store/ddl.py 的 seen_tables 防御)。 */
124
157
  function generate(backend, names) {
125
158
  if (!BACKENDS.includes(backend)) {
126
159
  throw new Error(`DDL 生成:不支持的后端 ${JSON.stringify(backend)}(支持 ${BACKENDS.join('/')})`);
127
160
  }
128
161
  const targets = names && names.length ? Array.from(names) : schema.list();
129
- return targets.map((n) => createTable(schema.get(n), backend)).join('\n\n');
162
+ const blocks = [];
163
+ const seenTables = new Set();
164
+ const dup = [];
165
+ for (const n of targets) {
166
+ const defn = schema.get(n);
167
+ const table = defn.collection || n;
168
+ if (seenTables.has(table)) {
169
+ dup.push(table);
170
+ continue;
171
+ }
172
+ seenTables.add(table);
173
+ blocks.push(createTable(defn, backend));
174
+ blocks.push(...indexStmts(defn, backend));
175
+ }
176
+ if (dup.length) {
177
+ _emitFeedback({
178
+ type: 'ddl_duplicate_table',
179
+ code: 'ddlDuplicateTable',
180
+ layer: 'host',
181
+ message: `DDL 生成:表 ${[...new Set(dup)].sort()} 重复注册,已去重`,
182
+ hint: 'schema 注册表出现重复名(见 schemaDuplicateName 告警);修复注册侧根因',
183
+ backend,
184
+ });
185
+ }
186
+ return blocks.join('\n\n');
130
187
  }
131
188
 
132
189
  module.exports = { generate };
@@ -38,6 +38,43 @@ function _scalar(rows) {
38
38
  return typeof v === 'string' ? Number(v) : v;
39
39
  }
40
40
 
41
+ /**
42
+ * 显式 checkout:返回 `{ conn, release }`;`release` 为 async 幂等函数。
43
+ *
44
+ * 池形态(pg Pool → `connect()` / mysql2 Pool → `getConnection()`)失败**直接上抛**,
45
+ * 禁「静默退回 driver 本体」——退回本体可能落到池上另一连接,使事务语义错乱;
46
+ * 单连接形态直用 driver,release 为 no-op。
47
+ * pg / mysql 执行器的显式事务句柄均以此为基础(禁第二套 checkout 路径)。
48
+ */
49
+ async function openAcquire(driver) {
50
+ if (driver && typeof driver.connect === 'function') {
51
+ const conn = await driver.connect(); // pg Pool:失败直接上抛
52
+ let released = false;
53
+ return {
54
+ conn,
55
+ release: async () => {
56
+ if (released) return;
57
+ released = true;
58
+ // pg Client(connect() 返回自身)无 release(),属单连接形态
59
+ if (typeof conn.release === 'function') conn.release();
60
+ },
61
+ };
62
+ }
63
+ if (driver && typeof driver.getConnection === 'function') {
64
+ const conn = await driver.getConnection(); // mysql2 Pool:失败直接上抛
65
+ let released = false;
66
+ return {
67
+ conn,
68
+ release: async () => {
69
+ if (released) return;
70
+ released = true;
71
+ conn.release();
72
+ },
73
+ };
74
+ }
75
+ return { conn: driver, release: async () => {} }; // 单连接形态
76
+ }
77
+
41
78
  /** 中立包络 → Mongo 驱动等价返回值 */
42
79
  function shapeResult(cmd, out) {
43
80
  switch (cmd.kind) {
@@ -49,8 +86,21 @@ function shapeResult(cmd, out) {
49
86
  return (out.docs && out.docs[0]) || null;
50
87
  case 'countDocuments':
51
88
  return _scalar(out.rows);
52
- case 'insertOne':
53
- return cmd.doc;
89
+ case 'insertOne': {
90
+ const doc = cmd.doc;
91
+ // 阶段2:autoincrement 主键 —— doc 无 `_id`(core 不注入)→ 从执行包络回读
92
+ // 自增值(PG/SQLite RETURNING 走 rows;MySQL/SQLite lastInsertRowid 走 insertId)
93
+ if (doc && !doc._id) {
94
+ let rid = null;
95
+ if (out.rows && out.rows[0] && Object.prototype.hasOwnProperty.call(out.rows[0], '_id')) {
96
+ rid = out.rows[0]._id;
97
+ } else if (out.insertId !== null && out.insertId !== undefined) {
98
+ rid = out.insertId;
99
+ }
100
+ if (rid !== null && rid !== undefined) return { ...doc, _id: rid };
101
+ }
102
+ return doc;
103
+ }
54
104
  case 'insertMany':
55
105
  return { insertedCount: (cmd.docs || []).length };
56
106
  case 'updateMany':
@@ -62,4 +112,4 @@ function shapeResult(cmd, out) {
62
112
  }
63
113
  }
64
114
 
65
- module.exports = { createConnection, shapeResult, mongo, mysql, postgres, sqlite };
115
+ module.exports = { createConnection, openAcquire, shapeResult, mongo, mysql, postgres, sqlite };
@@ -11,6 +11,10 @@
11
11
  * 同时命中「值为 null」与「字段缺失」两类文档,而本 store 的三态契约(F-07/H-09)
12
12
  * 要求 `null` 只命中显式 null、`$exists:false` 才命中缺失 —— 这与 SQL 侧
13
13
  * `col IS NULL`(显式 null)语义对齐。运算对象(`$ne:null`/`$exists`/`$gt`…)不改写。
14
+ *
15
+ * Mongo session 事务:本模块只提供原语 `openTransaction`(`startSession` +
16
+ * `startTransaction`)与 `execMongo(..., session)` 透传;事务的编排(提交/回滚/
17
+ * 降级声明)由 datasource 层负责。Mongo **无** `SAVEPOINT` 原语,故不提供保存点系列。
14
18
  */
15
19
 
16
20
  /** 递归改写 filter:`field: null`(标量等值)→ `field: {$eq: null, $exists: true}`。 */
@@ -51,52 +55,111 @@ function _normPipeline(cmd) {
51
55
  }
52
56
  }
53
57
 
54
- /** Command JSON → MongoDB 原生驱动调用 */
55
- async function execMongo(db, cmd) {
58
+ /** Mongo 连接的 client:db 实例取 .client;MongoClient 返回自身 */
59
+ function _clientOf(connection) {
60
+ if (connection && typeof connection.db === 'function' && typeof connection.collection !== 'function') {
61
+ return connection; // MongoClient
62
+ }
63
+ return (connection && connection.client) || null; // Db.client
64
+ }
65
+
66
+ /** 合并 session 到 options(session 为空时返回原 options 语义,零回归) */
67
+ function _opts(session, base) {
68
+ const o = base ? { ...base } : {};
69
+ if (session) o.session = session;
70
+ return o;
71
+ }
72
+
73
+ /**
74
+ * Mongo 事务句柄:startSession + startTransaction
75
+ * - commit/rollback 幂等;release 结束 session;
76
+ * - 无保存点原语(Mongo 不支持 SAVEPOINT),嵌套由 datasource 层降级声明。
77
+ */
78
+ async function openTransaction(connection) {
79
+ const client = _clientOf(connection);
80
+ const session = client.startSession();
81
+ session.startTransaction();
82
+ let closed = false;
83
+ return {
84
+ session,
85
+ async commit() {
86
+ if (closed) return;
87
+ closed = true;
88
+ await session.commitTransaction();
89
+ },
90
+ async rollback() {
91
+ if (closed) return;
92
+ closed = true;
93
+ await session.abortTransaction();
94
+ },
95
+ async release() {
96
+ await session.endSession();
97
+ },
98
+ };
99
+ }
100
+
101
+ /** Command JSON → MongoDB 原生驱动调用(session 非空时全部操作携带该 session) */
102
+ async function execMongo(db, cmd, session) {
56
103
  const coll = db.collection(cmd.collection);
57
104
  switch (cmd.kind) {
58
105
  case 'find': {
59
106
  _normFilter(cmd);
60
- const opts = cmd.projection ? { projection: cmd.projection } : undefined;
107
+ const opts = _opts(session, cmd.projection ? { projection: cmd.projection } : undefined);
61
108
  return coll.find(cmd.filter, opts).toArray();
62
109
  }
63
110
  case 'aggregate':
111
+ // cmd.options 为原生聚合透传项(executeNative 注入;GQL 路径无此键,零回归)
64
112
  _normPipeline(cmd);
65
- return coll.aggregate(cmd.pipeline).toArray();
113
+ return coll.aggregate(cmd.pipeline, _opts(session, cmd.options)).toArray();
66
114
  case 'countDocuments':
67
115
  _normFilter(cmd);
68
- return coll.countDocuments(cmd.filter);
116
+ return coll.countDocuments(cmd.filter, _opts(session));
69
117
  case 'findOne': {
70
118
  _normFilter(cmd);
71
- const opts = cmd.projection ? { projection: cmd.projection } : undefined;
119
+ const opts = _opts(session, cmd.projection ? { projection: cmd.projection } : undefined);
72
120
  return coll.findOne(cmd.filter, opts);
73
121
  }
74
- case 'insertOne':
75
- await coll.insertOne(cmd.doc);
76
- return cmd.doc;
122
+ case 'insertOne': {
123
+ const doc = cmd.doc || {};
124
+ // 阶段2(no-error-masking):Mongo 无自增语义 —— `_id` 缺失的文档只可能来自
125
+ // 声明 strategy=autoincrement 的 schema(常规 schema 该形态已被 core 拦截)。
126
+ // 禁止 ObjectId 静默顶替自增契约,显式报错。
127
+ if (!doc._id) {
128
+ throw new Error(
129
+ 'AUTOINCREMENT_NOT_SUPPORTED: schema 声明了 strategy="autoincrement",'
130
+ + 'MongoDB 后端无自增语义(禁 ObjectId 顶替);请使用 SQL 数据源');
131
+ }
132
+ await coll.insertOne(doc, _opts(session));
133
+ return doc;
134
+ }
77
135
  case 'insertMany':
78
136
  if (cmd.upsertById) {
79
137
  // 归档幂等(core planArchiveDocs):按 _id 逐条覆盖 —— 「归档成功但删除失败」
80
138
  // 的重试不再因 _id 冲突整批失败。SQL 侧由 dialect 的 ON CONFLICT/REPLACE 承接。
81
139
  for (const doc of cmd.docs) {
82
- await coll.replaceOne({ _id: doc._id }, doc, { upsert: true });
140
+ await coll.replaceOne({ _id: doc._id }, doc, _opts(session, { upsert: true }));
83
141
  }
84
142
  return { insertedCount: cmd.docs.length };
85
143
  }
86
- await coll.insertMany(cmd.docs);
144
+ if (cmd.docs.some((d) => !(d && d._id))) {
145
+ throw new Error(
146
+ 'AUTOINCREMENT_NOT_SUPPORTED: schema 声明了 strategy="autoincrement",'
147
+ + 'MongoDB 后端无自增语义(禁 ObjectId 顶替);请使用 SQL 数据源');
148
+ }
149
+ await coll.insertMany(cmd.docs, _opts(session));
87
150
  return { insertedCount: cmd.docs.length };
88
151
  case 'findOneAndUpdate':
89
152
  _normFilter(cmd);
90
- return coll.findOneAndUpdate(cmd.filter, cmd.update, cmd.options);
153
+ return coll.findOneAndUpdate(cmd.filter, cmd.update, _opts(session, cmd.options));
91
154
  case 'updateMany':
92
155
  _normFilter(cmd);
93
- return coll.updateMany(cmd.filter, cmd.update);
156
+ return coll.updateMany(cmd.filter, cmd.update, _opts(session));
94
157
  case 'deleteMany':
95
158
  _normFilter(cmd);
96
- return coll.deleteMany(cmd.filter);
159
+ return coll.deleteMany(cmd.filter, _opts(session));
97
160
  default:
98
161
  throw new Error(`未支持的命令: ${cmd.kind}`);
99
162
  }
100
163
  }
101
164
 
102
- module.exports = { execMongo };
165
+ module.exports = { execMongo, openTransaction };
@@ -30,6 +30,7 @@ function create(driver, _options = {}) {
30
30
  let docs = null;
31
31
  let rows = null;
32
32
  let affectedRows = 0;
33
+ let insertId = null;
33
34
  for (const stmt of plan.stmts) {
34
35
  const [raw, fields] = await conn.execute(stmt.text, stmt.params || []);
35
36
  if (Array.isArray(raw)) {
@@ -37,37 +38,75 @@ function create(driver, _options = {}) {
37
38
  if (stmt.rowShape) docs = _core.restoreRows(stmt.rowShape, rows);
38
39
  } else {
39
40
  affectedRows = Number(raw.affectedRows || 0);
41
+ // 阶段2:autoincrement 主键写后自增值回读(MySQL 无 RETURNING,insertId =
42
+ // 本连接最近一次 INSERT 生成的自增值)
43
+ insertId = Number(raw.insertId);
40
44
  }
41
45
  }
42
- return { docs, rows, affectedRows };
46
+ return { docs, rows, affectedRows, insertId };
47
+ }
48
+
49
+ /**
50
+ * 显式事务句柄:池 checkout 专用连接(失败直接上抛,无静默兜底)
51
+ * + beginTransaction + 幂等 commit/rollback;release 归还连接(单连接为 no-op)。
52
+ */
53
+ async function openTransaction() {
54
+ const { openAcquire } = require('./index'); // 延迟导入:避免与 index 的循环依赖
55
+ const { conn, release } = await openAcquire(driver);
56
+ await conn.beginTransaction();
57
+ let closed = false;
58
+ return {
59
+ exec: (plan) => runStmts(conn, plan),
60
+ async savepoint(name) {
61
+ /* 保存点(嵌套事务用);name 由 Host 生成(sp_<n>),非用户输入。
62
+ MySQL 预备语句协议不支持 SAVEPOINT → 必须走 conn.query */
63
+ await conn.query(`SAVEPOINT ${name}`);
64
+ },
65
+ async releaseSavepoint(name) {
66
+ await conn.query(`RELEASE SAVEPOINT ${name}`);
67
+ },
68
+ async rollbackToSavepoint(name) {
69
+ await conn.query(`ROLLBACK TO SAVEPOINT ${name}`);
70
+ },
71
+ async commit() {
72
+ if (closed) return;
73
+ closed = true;
74
+ await conn.commit();
75
+ },
76
+ async rollback() {
77
+ if (closed) return;
78
+ closed = true;
79
+ await conn.rollback();
80
+ },
81
+ release,
82
+ };
83
+ }
84
+
85
+ /** 事务执行:基于 openTransaction(无第二套事务路径),任一失败整体回滚;
86
+ * body(exec, tx) 第二参数为事务句柄(供上层读保存点原语),可选——旧单参写法继续可用 */
87
+ async function withTransaction(body) {
88
+ const tx = await openTransaction();
89
+ try {
90
+ const out = await body(tx.exec, tx);
91
+ await tx.commit();
92
+ return out;
93
+ } catch (e) {
94
+ try {
95
+ await tx.rollback();
96
+ } catch (_) {
97
+ /* rollback 失败不掩盖原始错误 */
98
+ }
99
+ throw e;
100
+ } finally {
101
+ await tx.release();
102
+ }
43
103
  }
44
104
 
45
105
  return {
46
106
  kind: 'mysql',
47
107
  exec: (plan) => runStmts(driver, plan),
48
- /**
49
- * 事务执行:body(executeOnTx) 的所有 plan 落在同一连接同一事务内,
50
- * 成功 commit / 失败 rollback。池自动取专用连接(结束归还)。
51
- */
52
- async withTransaction(body) {
53
- const conn =
54
- typeof driver.getConnection === 'function' ? await driver.getConnection() : driver;
55
- try {
56
- await conn.beginTransaction();
57
- const out = await body((plan) => runStmts(conn, plan));
58
- await conn.commit();
59
- return out;
60
- } catch (e) {
61
- try {
62
- await conn.rollback();
63
- } catch (_) {
64
- /* rollback 失败不掩盖原始错误 */
65
- }
66
- throw e;
67
- } finally {
68
- if (conn !== driver && typeof conn.release === 'function') conn.release();
69
- }
70
- },
108
+ withTransaction,
109
+ openTransaction,
71
110
  };
72
111
  }
73
112
 
@@ -32,44 +32,66 @@ function create(driver, _options = {}) {
32
32
  return { docs, rows, affectedRows };
33
33
  }
34
34
 
35
+ /**
36
+ * 显式事务句柄:池 checkout 专用 client(失败直接上抛,无静默兜底)
37
+ * + BEGIN + 幂等 commit/rollback;release 归还连接(单连接为 no-op)。
38
+ */
39
+ async function openTransaction() {
40
+ const { openAcquire } = require('./index'); // 延迟导入:避免与 index 的循环依赖
41
+ const { conn, release } = await openAcquire(driver);
42
+ await conn.query('BEGIN');
43
+ let closed = false;
44
+ return {
45
+ exec: (plan) => runStmts(conn, plan),
46
+ async savepoint(name) {
47
+ /* 保存点(嵌套事务用);name 由 Host 生成(sp_<n>),非用户输入 */
48
+ await conn.query(`SAVEPOINT ${name}`);
49
+ },
50
+ async releaseSavepoint(name) {
51
+ await conn.query(`RELEASE SAVEPOINT ${name}`);
52
+ },
53
+ async rollbackToSavepoint(name) {
54
+ await conn.query(`ROLLBACK TO SAVEPOINT ${name}`);
55
+ },
56
+ async commit() {
57
+ if (closed) return;
58
+ closed = true;
59
+ await conn.query('COMMIT');
60
+ },
61
+ async rollback() {
62
+ if (closed) return;
63
+ closed = true;
64
+ await conn.query('ROLLBACK');
65
+ },
66
+ release,
67
+ };
68
+ }
69
+
70
+ /** 事务执行:基于 openTransaction(无第二套事务路径),任一失败整体回滚;
71
+ * body(exec, tx) 第二参数为事务句柄(供上层读保存点原语),可选——旧单参写法继续可用 */
72
+ async function withTransaction(body) {
73
+ const tx = await openTransaction();
74
+ try {
75
+ const out = await body(tx.exec, tx);
76
+ await tx.commit();
77
+ return out;
78
+ } catch (e) {
79
+ try {
80
+ await tx.rollback();
81
+ } catch (_) {
82
+ /* rollback 失败不掩盖原始错误 */
83
+ }
84
+ throw e;
85
+ } finally {
86
+ await tx.release();
87
+ }
88
+ }
89
+
35
90
  return {
36
91
  kind: 'postgres',
37
92
  exec: (plan) => runStmts(driver, plan),
38
- /**
39
- * 事务执行:显式 BEGIN/COMMIT/ROLLBACK 包住 body 的全部 plan。
40
- * Pool 自动 checkout 专用 client(`release()` 归还);Client 直连直接用。
41
- */
42
- async withTransaction(body) {
43
- let conn = driver;
44
- let release = null;
45
- if (typeof driver.connect === 'function') {
46
- try {
47
- const c = await driver.connect();
48
- // Pool.connect() → 专用 Client(带 release);Client.connect() → 自身
49
- if (c && typeof c.query === 'function') {
50
- conn = c;
51
- if (c !== driver && typeof c.release === 'function') release = () => c.release();
52
- }
53
- } catch (_) {
54
- /* checkout 失败退回 driver 本体,事务语义由 BEGIN/COMMIT 保证 */
55
- }
56
- }
57
- try {
58
- await conn.query('BEGIN');
59
- const out = await body((plan) => runStmts(conn, plan));
60
- await conn.query('COMMIT');
61
- return out;
62
- } catch (e) {
63
- try {
64
- await conn.query('ROLLBACK');
65
- } catch (_) {
66
- /* rollback 失败不掩盖原始错误 */
67
- }
68
- throw e;
69
- } finally {
70
- if (release) release();
71
- }
72
- },
93
+ withTransaction,
94
+ openTransaction,
73
95
  };
74
96
  }
75
97
 
@@ -36,6 +36,7 @@ function create(db, _options = {}) {
36
36
  let docs = null;
37
37
  let rows = null;
38
38
  let affectedRows = 0;
39
+ let insertId = null;
39
40
  for (const stmt of plan.stmts) {
40
41
  const params = _bind(stmt.params);
41
42
  // 带 RETURNING 的写语句同样返回行 → 必须用 all() 取回;其余写语句用 run() 取影响行数
@@ -43,32 +44,67 @@ function create(db, _options = {}) {
43
44
  rows = db.prepare(stmt.text).all(...params);
44
45
  if (stmt.rowShape) docs = _core.restoreRows(stmt.rowShape, rows);
45
46
  } else {
46
- affectedRows = Number(db.prepare(stmt.text).run(...params).changes || 0);
47
+ const info = db.prepare(stmt.text).run(...params);
48
+ affectedRows = Number(info.changes || 0);
49
+ // 阶段2:autoincrement 主键写后自增值回读(非 RETURNING 的 INSERT 走
50
+ // lastInsertRowid;带 RETURNING 的写语句走上方 rows 分支)
51
+ insertId = Number(info.lastInsertRowid);
47
52
  }
48
53
  }
49
- return { docs, rows, affectedRows };
54
+ return { docs, rows, affectedRows, insertId };
50
55
  }
51
56
 
52
- return {
53
- kind: 'sqlite',
54
- exec: runStmts,
55
- /** 事务执行:显式 BEGIN/COMMIT/ROLLBACK(better-sqlite3 默认 autocommit,显式开事务安全) */
56
- async withTransaction(body) {
57
- db.exec('BEGIN');
58
- try {
59
- const out = await body(runStmts);
57
+ /** 显式事务句柄:BEGIN + 幂等 commit/rollback;release 为 no-op(单连接不归还) */
58
+ async function openTransaction() {
59
+ db.exec('BEGIN');
60
+ let closed = false;
61
+ return {
62
+ exec: runStmts,
63
+ async savepoint(name) {
64
+ /* 保存点(嵌套事务用);name 由 Host 生成(sp_<n>),非用户输入 */
65
+ db.exec(`SAVEPOINT ${name}`);
66
+ },
67
+ async releaseSavepoint(name) {
68
+ db.exec(`RELEASE SAVEPOINT ${name}`);
69
+ },
70
+ async rollbackToSavepoint(name) {
71
+ db.exec(`ROLLBACK TO SAVEPOINT ${name}`);
72
+ },
73
+ async commit() {
74
+ if (closed) return;
75
+ closed = true;
60
76
  db.exec('COMMIT');
61
- return out;
62
- } catch (e) {
63
- try {
64
- db.exec('ROLLBACK');
65
- } catch (_) {
66
- /* rollback 失败不掩盖原始错误 */
67
- }
68
- throw e;
77
+ },
78
+ async rollback() {
79
+ if (closed) return;
80
+ closed = true;
81
+ db.exec('ROLLBACK');
82
+ },
83
+ async release() {},
84
+ };
85
+ }
86
+
87
+ /** 事务执行:基于 openTransaction(无第二套事务路径),任一失败整体回滚;
88
+ * body(exec, tx) 第二参数为事务句柄(供上层读保存点原语),可选——旧单参写法继续可用 */
89
+ async function withTransaction(body) {
90
+ const tx = await openTransaction();
91
+ try {
92
+ const out = await body(tx.exec, tx);
93
+ await tx.commit();
94
+ return out;
95
+ } catch (e) {
96
+ try {
97
+ await tx.rollback();
98
+ } catch (_) {
99
+ /* rollback 失败不掩盖原始错误 */
69
100
  }
70
- },
71
- };
101
+ throw e;
102
+ } finally {
103
+ await tx.release();
104
+ }
105
+ }
106
+
107
+ return { kind: 'sqlite', exec: runStmts, withTransaction, openTransaction };
72
108
  }
73
109
 
74
110
  module.exports = { create };