nodejs-store 2.5.0 → 2.7.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/crud/write.js CHANGED
@@ -24,8 +24,14 @@ async function insert(schemaName, data, routeOverride = null) {
24
24
  const plan = _call(() =>
25
25
  _core.planInsert(schemaName, data ?? null, _nowFor(schemaName), s.idPrefix ? _generateId(s) : '', _ctx(),
26
26
  routeOverride));
27
- await _exec(plan.command);
28
- return plan.returns;
27
+ const result = await _exec(plan.command);
28
+ let returns = plan.returns;
29
+ // 阶段2:autoincrement 主键 —— 执行器已回读自增值,returns 补 `_id`
30
+ if (returns && typeof returns === 'object' && !returns._id
31
+ && result && typeof result === 'object' && result._id !== undefined && result._id !== null) {
32
+ returns = { ...returns, _id: result._id };
33
+ }
34
+ return returns;
29
35
  }
30
36
 
31
37
  /** 批量插入(带权限检查,自动生成 _id 和时间戳;空数组直接返回空) */
@@ -33,6 +39,14 @@ async function insertMany(schemaName, docs, routeOverride = null) {
33
39
  if (!Array.isArray(docs) || !docs.length) return [];
34
40
 
35
41
  const s = _getSchema(schemaName);
42
+ // 阶段2(no-error-masking):autoincrement 的批量自增值回读不可靠(MySQL 批量
43
+ // insertId 仅首行、且并发插入会留间隙)→ 显式报错,不静默产出错误 _id
44
+ const idFdef = (s.fields || {})._id || {};
45
+ if (idFdef.strategy === 'autoincrement' && docs.some((d) => !(d && d._id))) {
46
+ throw new Error(
47
+ 'AUTOINCREMENT_NOT_SUPPORTED: insertMany 不支持 autoincrement schema'
48
+ + '(批量自增值回读不可靠);请逐条 insert 或显式提供 _id');
49
+ }
36
50
  const plan = _call(() => _core.planInsertMany(
37
51
  schemaName,
38
52
  docs,
@@ -78,11 +92,46 @@ async function update(schemaName, condition, data, options = null, routeOverride
78
92
  return runAtomic(sources, doRun);
79
93
  }
80
94
 
95
+ /** 执行带 `preCommand` 的命令(阶段1:mutation 关系谓词归一)。
96
+ * preCommand(aggregate 取命中 `_id`)先行执行,把 `_id` 列表回填进主命令 filter 的
97
+ * `$in` 占位(core 注入 `"__REL_PRED_IDS__"`);空集 → `$in: []`(各后端均不命中任何行)。 */
98
+ async function execWithPre(command) {
99
+ const pre = command && command.preCommand;
100
+ if (!pre) return _exec(command);
101
+ const preRows = await _exec(pre);
102
+ const ids = (preRows || []).filter((d) => d && '_id' in d).map((d) => d._id);
103
+ return fillPreIds(command, ids);
104
+ }
105
+
106
+ /** 把 preCommand 取得的 `_id` 列表回填进命令 filter 的 `$in` 占位(递归查找后执行)。
107
+ * core 注入的占位可能位于 `$and` 数组内(改写条件已有其他键时),故递归遍历。 */
108
+ function fillPreIds(command, ids) {
109
+ const main = { ...command };
110
+ delete main.preCommand;
111
+ const walk = (node) => {
112
+ if (Array.isArray(node)) {
113
+ for (const it of node) walk(it);
114
+ return;
115
+ }
116
+ if (node && typeof node === 'object') {
117
+ for (const [k, v] of Object.entries(node)) {
118
+ if (v && typeof v === 'object' && v.$in === '__REL_PRED_IDS__') {
119
+ node[k] = { $in: [...ids] };
120
+ } else {
121
+ walk(v);
122
+ }
123
+ }
124
+ }
125
+ };
126
+ if (main.filter) walk(main.filter);
127
+ return _exec(main);
128
+ }
129
+
81
130
  /** 批量更新(支持原生操作符) */
82
131
  async function updateMany(schemaName, condition, data, routeOverride = null) {
83
132
  const out = _call(() =>
84
133
  _core.planUpdateMany(schemaName, condition ?? null, data ?? null, _nowFor(schemaName), _ctx(), routeOverride));
85
- const result = await _exec(out.command);
134
+ const result = await execWithPre(out.command);
86
135
  return { modifiedCount: result.modifiedCount };
87
136
  }
88
137
 
@@ -95,8 +144,15 @@ async function remove(schemaName, condition, routeOverride = null) {
95
144
 
96
145
  const doRemove = async () => {
97
146
  let archivedCount = 0;
147
+ // 关系谓词:先执行 deleteCommand.preCommand 取命中 _id(归档 find 与删除共用同一列表)
148
+ const pre = out.deleteCommand && out.deleteCommand.preCommand;
149
+ let ids = null;
150
+ if (pre) {
151
+ const preRows = await _exec(pre);
152
+ ids = (preRows || []).filter((d) => d && '_id' in d).map((d) => d._id);
153
+ }
98
154
  if (out.findCommand) {
99
- const docs = await _exec(out.findCommand);
155
+ const docs = await (ids !== null ? fillPreIds(out.findCommand, ids) : _exec(out.findCommand));
100
156
  if (docs.length) {
101
157
  const arch = _call(() => _core.planArchiveDocs(schemaName, docs, _nowFor(schemaName), routeOverride));
102
158
  await _exec(arch.command);
@@ -104,7 +160,7 @@ async function remove(schemaName, condition, routeOverride = null) {
104
160
  }
105
161
  }
106
162
 
107
- const result = await _exec(out.deleteCommand);
163
+ const result = await (ids !== null ? fillPreIds(out.deleteCommand, ids) : _exec(out.deleteCommand));
108
164
  return { deletedCount: result.deletedCount, archivedCount };
109
165
  };
110
166
 
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,221 @@ 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');
187
+ }
188
+
189
+ // ══════════════════════════════════════════════════════════════════
190
+ // 声明式 schema 迁移(阶段 4;设计见 common-store/迁移设计文档-阶段4.md)
191
+ // 与 py_store/ddl.py 的 migration 段逐字节对齐(parity 锚:tests/migration.test.js)。
192
+ // ══════════════════════════════════════════════════════════════════
193
+
194
+ /** 类型放宽映射(首批):值域安全扩大的单向变更;跨大类不在映射内 → Err */
195
+ const WIDEN = {
196
+ int: ['long', 'float', 'double'],
197
+ long: ['float', 'double'],
198
+ float: ['double'],
199
+ };
200
+
201
+ /** 字段 def → 列 SQL 类型(与 columns 同源;object/array → JSON 文本列) */
202
+ function fieldSqlType(backend, fdef) {
203
+ const i = idx(backend);
204
+ const ftype = declaredType(fdef);
205
+ if (NON_COLUMN.includes(ftype)) return JSON_TYPE[i];
206
+ if (!Object.prototype.hasOwnProperty.call(TYPES, ftype)) {
207
+ throw new Error(
208
+ `迁移生成:字段类型 ${JSON.stringify(ftype)} 未知(支持 ${Object.keys(TYPES).sort()})`);
209
+ }
210
+ return TYPES[ftype][i];
211
+ }
212
+
213
+ /** default 字面量 → SQL 文本(仅 JSON 标量;SQLite 禁非常量表达式 DEFAULT) */
214
+ function sqlLiteral(v) {
215
+ if (typeof v === 'boolean') return v ? 'TRUE' : 'FALSE';
216
+ if (typeof v === 'number') return String(v);
217
+ if (typeof v === 'string') return `'${v.replace(/'/g, "''")}'`;
218
+ throw new Error(`迁移生成:default 仅支持 JSON 标量字面量,收到 ${typeof v}`);
219
+ }
220
+
221
+ /** 主 def → 归档表 def(列 = 主列 + deletedAt;剔除 _id 自增策略;indexes 继承) */
222
+ function archiveDefnOf(defn) {
223
+ const fields = { ...(defn.fields || {}) };
224
+ fields.deletedAt = { type: 'number' };
225
+ if (fields._id && typeof fields._id === 'object') {
226
+ const { strategy, ...rest } = fields._id;
227
+ fields._id = rest;
228
+ }
229
+ return {
230
+ ...defn,
231
+ name: `${defn.name}Deleted`,
232
+ collection: `${defn.collection || defn.name}_deleted`,
233
+ fields,
234
+ timestamps: false,
235
+ _isArchive: true,
236
+ };
237
+ }
238
+
239
+ /** 新旧 schema def 对比 → { changes, errors }(后端无关,纯函数;白名单外记入 errors) */
240
+ function diffDefs(oldDefn, newDefn) {
241
+ const changes = [];
242
+ const errors = [];
243
+ const oldFields = (oldDefn && oldDefn.fields) || {};
244
+ const newFields = newDefn.fields || {};
245
+
246
+ if (oldDefn === null || oldDefn === undefined) {
247
+ return { changes: [{ op: 'addTable', defn: newDefn }], errors };
248
+ }
249
+
250
+ for (const [name, nf] of Object.entries(newFields)) {
251
+ if (name === '_id') {
252
+ const of = oldFields._id;
253
+ if (of !== undefined && declaredType(of) !== declaredType(nf)) {
254
+ errors.push(`_id 主键类型变更不支持(${declaredType(of)} → ${declaredType(nf)})`);
255
+ }
256
+ const oStrat = of && typeof of === 'object' ? of.strategy : undefined;
257
+ const nStrat = nf && typeof nf === 'object' ? nf.strategy : undefined;
258
+ if (oStrat !== nStrat) {
259
+ errors.push(`_id 主键策略变更不支持(${JSON.stringify(oStrat)} → ${JSON.stringify(nStrat)})`);
260
+ }
261
+ continue;
262
+ }
263
+ const nfType = declaredType(nf);
264
+ if (!(name in oldFields)) {
265
+ if (!Object.prototype.hasOwnProperty.call(TYPES, nfType) && !NON_COLUMN.includes(nfType)) {
266
+ errors.push(`新列 "${name}" 类型 ${JSON.stringify(nfType)} 未知`);
267
+ continue;
268
+ }
269
+ changes.push({
270
+ op: 'addColumn', name,
271
+ field: nf && typeof nf === 'object' ? nf : { type: nf },
272
+ });
273
+ continue;
274
+ }
275
+ const of = oldFields[name];
276
+ const ofType = declaredType(of);
277
+ if (ofType === nfType) continue; // 恒等:不上报
278
+ if (nfType && (WIDEN[ofType] || []).includes(nfType)) {
279
+ changes.push({ op: 'widenColumn', name, from: ofType, to: nfType });
280
+ } else {
281
+ errors.push(
282
+ `字段 "${name}" 类型 ${JSON.stringify(ofType)} → ${JSON.stringify(nfType)} 非放宽变更`
283
+ + '(首批仅支持单向放宽:int→long/float/double、long→float/double、float→double)');
284
+ }
285
+ }
286
+ for (const name of Object.keys(oldFields)) {
287
+ if (!(name in newFields)) {
288
+ errors.push(`删除字段 "${name}" 不支持(破坏性变更;请显式走数据迁移脚本)`);
289
+ }
290
+ }
291
+
292
+ const indexKeys = (defn) => (defn.indexes || [])
293
+ .filter((ix) => ix && typeof ix === 'object' && ix.keys
294
+ && typeof ix.keys === 'object' && Object.keys(ix.keys).length);
295
+ const oldIdx = indexKeys(oldDefn);
296
+ for (const ix of indexKeys(newDefn)) {
297
+ if (!oldIdx.some((o) => JSON.stringify(o) === JSON.stringify(ix))) {
298
+ changes.push({ op: 'addIndex', index: ix });
299
+ }
300
+ }
301
+
302
+ const oldColl = oldDefn.collection || oldDefn.name;
303
+ const newColl = newDefn.collection || newDefn.name;
304
+ if (oldColl !== newColl) {
305
+ errors.push(`collection 改名不支持(${JSON.stringify(oldColl)} → ${JSON.stringify(newColl)};破坏性变更)`);
306
+ }
307
+ return { changes, errors };
308
+ }
309
+
310
+ /** 新旧 schema def → 迁移 SQL 文本列表(per-dialect;只产文本、不执行) */
311
+ function generateMigration(backend, oldDefn, newDefn) {
312
+ if (!BACKENDS.includes(backend)) {
313
+ throw new Error(`迁移生成:不支持的后端 ${JSON.stringify(backend)}(支持 ${BACKENDS.join('/')})`);
314
+ }
315
+ const plan = diffDefs(oldDefn, newDefn);
316
+ if (plan.errors.length) {
317
+ throw new Error(
318
+ 'MIGRATION_UNSUPPORTED: ' + plan.errors.join(';')
319
+ + '(首批白名单:加表/加列/类型放宽/加索引;破坏性变更请走显式数据迁移脚本)');
320
+ }
321
+
322
+ const table = newDefn.collection || newDefn.name;
323
+ const stmts = [];
324
+ const order = { addColumn: 0, widenColumn: 1, addIndex: 2 };
325
+ for (const ch of [...plan.changes].sort((a, b) => (order[a.op] ?? 9) - (order[b.op] ?? 9))) {
326
+ if (ch.op === 'addTable') {
327
+ stmts.push(createTable(newDefn, backend));
328
+ stmts.push(...indexStmts(newDefn, backend));
329
+ const arch = archiveDefnOf(newDefn);
330
+ stmts.push(createTable(arch, backend));
331
+ stmts.push(...indexStmts(arch, backend));
332
+ } else if (ch.op === 'addColumn') {
333
+ const ftype = declaredType(ch.field);
334
+ const colType = fieldSqlType(backend, ch.field);
335
+ const deflt = ch.field && typeof ch.field === 'object' ? ch.field.default : undefined;
336
+ let colSql = `${q(backend, ch.name)} ${colType}`;
337
+ if (deflt !== undefined && deflt !== null) {
338
+ if (NON_COLUMN.includes(ftype)) {
339
+ throw new Error(
340
+ `MIGRATION_UNSUPPORTED: 新列 "${ch.name}"(object/array JSON 列)不支持 DEFAULT`
341
+ + '(存量行缺失语义由 __present 哨兵表达;default 仅影响新写入)');
342
+ }
343
+ colSql += ` DEFAULT ${sqlLiteral(deflt)}`;
344
+ }
345
+ stmts.push(`ALTER TABLE ${q(backend, table)} ADD COLUMN ${colSql}`);
346
+ stmts.push(`ALTER TABLE ${q(backend, `${table}_deleted`)} ADD COLUMN ${colSql}`);
347
+ } else if (ch.op === 'widenColumn') {
348
+ const colType = fieldSqlType(backend, newDefn.fields[ch.name]);
349
+ if (backend === 'sqlite') {
350
+ throw new Error(
351
+ 'MIGRATION_UNSUPPORTED: SQLite 不支持类型变更 '
352
+ + `(${ch.from} → ${ch.to} 需重建表);加列/加索引/加表已支持`);
353
+ }
354
+ if (backend === 'mysql') {
355
+ stmts.push(`ALTER TABLE ${q(backend, table)} MODIFY COLUMN ${q(backend, ch.name)} ${colType}`);
356
+ } else {
357
+ stmts.push(
358
+ `ALTER TABLE ${q(backend, table)} ALTER COLUMN ${q(backend, ch.name)} `
359
+ + `TYPE ${colType} USING ${q(backend, ch.name)}::${colType}`);
360
+ }
361
+ } else if (ch.op === 'addIndex') {
362
+ stmts.push(...indexStmts({ collection: table, indexes: [ch.index] }, backend));
363
+ } else {
364
+ throw new Error(`MIGRATION_UNSUPPORTED: 未知变更 ${ch.op}`);
365
+ }
366
+ }
367
+ return stmts;
130
368
  }
131
369
 
132
- module.exports = { generate };
370
+ module.exports = { generate, diffDefs, generateMigration };
@@ -86,8 +86,21 @@ function shapeResult(cmd, out) {
86
86
  return (out.docs && out.docs[0]) || null;
87
87
  case 'countDocuments':
88
88
  return _scalar(out.rows);
89
- case 'insertOne':
90
- 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
+ }
91
104
  case 'insertMany':
92
105
  return { insertedCount: (cmd.docs || []).length };
93
106
  case 'updateMany':
@@ -119,9 +119,19 @@ async function execMongo(db, cmd, session) {
119
119
  const opts = _opts(session, cmd.projection ? { projection: cmd.projection } : undefined);
120
120
  return coll.findOne(cmd.filter, opts);
121
121
  }
122
- case 'insertOne':
123
- await coll.insertOne(cmd.doc, _opts(session));
124
- 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
+ }
125
135
  case 'insertMany':
126
136
  if (cmd.upsertById) {
127
137
  // 归档幂等(core planArchiveDocs):按 _id 逐条覆盖 —— 「归档成功但删除失败」
@@ -131,6 +141,11 @@ async function execMongo(db, cmd, session) {
131
141
  }
132
142
  return { insertedCount: cmd.docs.length };
133
143
  }
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
+ }
134
149
  await coll.insertMany(cmd.docs, _opts(session));
135
150
  return { insertedCount: cmd.docs.length };
136
151
  case 'findOneAndUpdate':
@@ -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,9 +38,12 @@ 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 };
43
47
  }
44
48
 
45
49
  /**
@@ -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,10 +44,14 @@ 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
57
  /** 显式事务句柄:BEGIN + 幂等 commit/rollback;release 为 no-op(单连接不归还) */
package/src/feedback.js CHANGED
@@ -23,6 +23,11 @@ function setSink(fn) {
23
23
  _sink = typeof fn === 'function' ? fn : null;
24
24
  }
25
25
 
26
+ /** 当前 sink(无则 null)——供接管方(如 ask 编排器)保存/恢复现场 */
27
+ function getSink() {
28
+ return _sink;
29
+ }
30
+
26
31
  /** 产出一条反馈事件:有 sink 回调之;否则打印 stderr(允许拦截,禁止静默) */
27
32
  function emit(event) {
28
33
  const e = event || {};
@@ -36,4 +41,4 @@ function emit(event) {
36
41
  );
37
42
  }
38
43
 
39
- module.exports = { setSink, emit };
44
+ module.exports = { setSink, getSink, emit };