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/README.md +173 -0
- package/README.zh-CN.md +162 -0
- package/llms-full.txt +71 -0
- package/llms.txt +1 -0
- package/package.json +2 -2
- package/src/ask.js +358 -0
- package/src/ask_knowledge.md +250 -0
- package/src/crud/write.js +61 -5
- package/src/ddl.js +250 -12
- package/src/executors/index.js +15 -2
- package/src/executors/mongo.js +18 -3
- package/src/executors/mysql.js +5 -1
- package/src/executors/sqlite.js +7 -2
- package/src/feedback.js +6 -1
- package/src/index.js +82 -25
- package/src/llm.js +157 -0
- package/src/permission.js +58 -0
- package/src/profile.js +35 -0
- package/src/schema.js +19 -2
- package/src/workflow.js +734 -0
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
|
-
|
|
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
|
|
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
|
-
* -
|
|
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
|
-
|
|
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 === '
|
|
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
|
|
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
|
-
|
|
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 };
|
package/src/executors/index.js
CHANGED
|
@@ -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
|
-
|
|
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':
|
package/src/executors/mongo.js
CHANGED
|
@@ -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
|
-
|
|
124
|
-
|
|
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':
|
package/src/executors/mysql.js
CHANGED
|
@@ -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
|
/**
|
package/src/executors/sqlite.js
CHANGED
|
@@ -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
|
-
|
|
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 };
|