nodejs-store 1.0.0 → 2.0.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 +115 -4
- package/package.json +11 -3
- package/src/core.js +6 -0
- package/src/crud/exec.js +28 -44
- package/src/crud/id.js +8 -3
- package/src/crud/index.js +5 -5
- package/src/crud/mutation.js +35 -22
- package/src/crud/query.js +10 -11
- package/src/crud/write.js +30 -17
- package/src/datasource.js +113 -7
- package/src/executors/_values.js +55 -0
- package/src/executors/index.js +6 -1
- package/src/executors/mongo.js +102 -0
- package/src/executors/mysql.js +40 -12
- package/src/executors/postgres.js +50 -10
- package/src/executors/sqlite.js +38 -13
- package/src/feedback.js +39 -0
- package/src/index.js +45 -15
- package/src/introspect/_shared.js +28 -0
- package/src/introspect/mysql.js +5 -15
- package/src/introspect/postgres.js +5 -15
- package/src/permission.js +51 -0
- package/src/schema.js +18 -1
package/src/datasource.js
CHANGED
|
@@ -18,13 +18,48 @@
|
|
|
18
18
|
* `{ default: db }`,保证既有单库调用零变更。
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
21
22
|
const { core: _core, get: _getSchema } = require('./schema');
|
|
23
|
+
const { emit: _emitFeedback } = require('./feedback');
|
|
22
24
|
const executors = require('./executors');
|
|
23
25
|
|
|
24
26
|
const DEFAULT_SOURCE = 'default';
|
|
25
27
|
|
|
26
28
|
let _connections = Object.create(null);
|
|
27
29
|
|
|
30
|
+
/** 事务作用域的连接覆盖:source → 事务描述符(见 runInTransaction) */
|
|
31
|
+
const _txStore = new AsyncLocalStorage();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* SQL 下推遇到无法安全翻译的组合(core 标记 unsupported)
|
|
35
|
+
*
|
|
36
|
+
* 显式报错而非静默执行「缺少该段」的 SQL(会返回错误结果);
|
|
37
|
+
* 自动反馈:触发原因见 message,修复指引见 feedback()。
|
|
38
|
+
*/
|
|
39
|
+
class PushdownUnsupportedError extends Error {
|
|
40
|
+
constructor(source, kind, codes, warnings) {
|
|
41
|
+
super(`SQL 下推不支持(${kind}): ${codes.join(', ')};${warnings.join(' / ')}`);
|
|
42
|
+
this.name = 'PushdownUnsupportedError';
|
|
43
|
+
this.source = source;
|
|
44
|
+
this.kind = kind;
|
|
45
|
+
this.codes = codes;
|
|
46
|
+
this.warnings = warnings;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 转统一反馈事件(与 feedback.emit 的事件形状一致) */
|
|
50
|
+
feedback() {
|
|
51
|
+
return {
|
|
52
|
+
type: 'sql_pushdown_unsupported',
|
|
53
|
+
code: 'pushdownUnsupported',
|
|
54
|
+
layer: 'dialect',
|
|
55
|
+
message: this.message,
|
|
56
|
+
hint: '改写查询避开该组合,或改用 Mongo 源执行该段取数',
|
|
57
|
+
source: this.source,
|
|
58
|
+
kind: this.kind,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
28
63
|
/** Mongo 形态判别:db 实例(collection 为函数)或 MongoClient(db 为函数且无 collection) */
|
|
29
64
|
function _isMongoHandle(x) {
|
|
30
65
|
return (
|
|
@@ -58,6 +93,64 @@ function getConnection(source) {
|
|
|
58
93
|
return conn;
|
|
59
94
|
}
|
|
60
95
|
|
|
96
|
+
/** 指定数据源是否已在当前连接映射中配置(辅助动作「软跳过」判定用,如 init 建索引) */
|
|
97
|
+
function hasConnection(source) {
|
|
98
|
+
return _connections[source] !== undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 连接是否为 SQL 执行器描述符(`{ kind, exec }`;Mongo 为驱动实例) */
|
|
102
|
+
function isSqlConnection(connection) {
|
|
103
|
+
return (
|
|
104
|
+
!!connection &&
|
|
105
|
+
typeof connection.kind === 'string' &&
|
|
106
|
+
typeof connection.exec === 'function'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 数据源名是否绑定 SQL 源 */
|
|
111
|
+
function isSql(source) {
|
|
112
|
+
return isSqlConnection(getConnection(source));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 当前生效连接:事务作用域内返回覆盖描述符,否则返回全局映射的连接
|
|
117
|
+
* (`exec.js#_execOn` 经此取连接,使事务内所有命令落到专用连接)
|
|
118
|
+
*/
|
|
119
|
+
function connectionFor(source) {
|
|
120
|
+
const store = _txStore.getStore();
|
|
121
|
+
if (store && store.has(source)) return store.get(source);
|
|
122
|
+
return getConnection(source);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 事务作用域:在单个 SQL 源上以「同连接 + 同事务」执行 fn 内的全部命令
|
|
127
|
+
*
|
|
128
|
+
* - fn 内经 `_exec` 路由到该 source 的命令全部落到事务连接(commit/rollback 一体);
|
|
129
|
+
* - Mongo 源 / 执行器未实现 withTransaction / 多源混合时按原样执行
|
|
130
|
+
* (跨源无法原子 —— 信任边界见 README「事务边界」),绝不静默假装已事务化;
|
|
131
|
+
* - 事务体抛错统一 rollback 后原样上抛。
|
|
132
|
+
*/
|
|
133
|
+
async function runInTransaction(source, fn) {
|
|
134
|
+
const conn = getConnection(source);
|
|
135
|
+
if (!isSqlConnection(conn) || typeof conn.withTransaction !== 'function') {
|
|
136
|
+
return fn();
|
|
137
|
+
}
|
|
138
|
+
const parent = _txStore.getStore();
|
|
139
|
+
const store = new Map(parent || []);
|
|
140
|
+
if (store.has(source)) {
|
|
141
|
+
// 同源嵌套事务:外层已持有该源的事务连接,内层并入外层(不做保存点)
|
|
142
|
+
return fn();
|
|
143
|
+
}
|
|
144
|
+
const txDescriptor = { kind: conn.kind, exec: null };
|
|
145
|
+
store.set(source, txDescriptor);
|
|
146
|
+
return _txStore.run(store, () =>
|
|
147
|
+
conn.withTransaction(async (execOnTx) => {
|
|
148
|
+
txDescriptor.exec = execOnTx;
|
|
149
|
+
return fn();
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
61
154
|
/**
|
|
62
155
|
* Mongo 源:按命令的 `namespace` 解析目标 db(两种形态,绝不猜)
|
|
63
156
|
*
|
|
@@ -117,18 +210,25 @@ function route(cmd) {
|
|
|
117
210
|
* 使上层(crud/*)对 Mongo / SQL 两条路径无感。
|
|
118
211
|
*/
|
|
119
212
|
async function execSql(source, connection, cmd) {
|
|
120
|
-
if (typeof connection.exec !== 'function') {
|
|
121
|
-
throw new Error(
|
|
122
|
-
`SQL 数据源 ${source}(${connection.kind}) 的执行器未接入(见执行文档 Phase 4)`,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
213
|
const plan = _core.dialectTranslate(connection.kind, cmd);
|
|
126
214
|
// Host 兜底:core 标记了无法安全下推的组合(如 $lookup 子 $limit 每父 top-N)时,
|
|
127
215
|
// 绝不执行「缺少该段」的 SQL(会静默返回错误结果),改为显式报错,由调用方降级重查。
|
|
216
|
+
// 先于执行器检查 —— 命令本身不可安全下推时,报下推不支持而非「执行器未接入」
|
|
217
|
+
// (对齐 py_store/datasource.py#exec_sql 的检查次序)。
|
|
128
218
|
if (Array.isArray(plan.unsupported) && plan.unsupported.length > 0) {
|
|
129
|
-
const
|
|
219
|
+
const err = new PushdownUnsupportedError(
|
|
220
|
+
source,
|
|
221
|
+
connection.kind,
|
|
222
|
+
plan.unsupported.map((u) => (u && u.code) || String(u)),
|
|
223
|
+
(plan.warnings || []).map((w) => String(w)),
|
|
224
|
+
);
|
|
225
|
+
// 自动反馈:拦截即告警(无 sink 时打 stderr),禁止静默失守
|
|
226
|
+
_emitFeedback(err.feedback());
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
if (typeof connection.exec !== 'function') {
|
|
130
230
|
throw new Error(
|
|
131
|
-
`SQL
|
|
231
|
+
`SQL 数据源 ${source}(${connection.kind}) 的执行器未接入(见执行文档 Phase 4)`,
|
|
132
232
|
);
|
|
133
233
|
}
|
|
134
234
|
const out = await connection.exec(plan);
|
|
@@ -139,10 +239,16 @@ module.exports = {
|
|
|
139
239
|
DEFAULT_SOURCE,
|
|
140
240
|
setConnections,
|
|
141
241
|
getConnection,
|
|
242
|
+
hasConnection,
|
|
243
|
+
isSqlConnection,
|
|
244
|
+
isSql,
|
|
245
|
+
connectionFor,
|
|
246
|
+
runInTransaction,
|
|
142
247
|
mongoDb,
|
|
143
248
|
sourceOfSchema,
|
|
144
249
|
connectionOfSchema,
|
|
145
250
|
dbOfSchema,
|
|
146
251
|
route,
|
|
147
252
|
execSql,
|
|
253
|
+
PushdownUnsupportedError,
|
|
148
254
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 驱动原始值归一(SQL 执行器共用)
|
|
5
|
+
*
|
|
6
|
+
* `pg` / `mysql2` 对**精确数值列**默认返回**字符串**:
|
|
7
|
+
* - `pg`:`int8`(20) / `numeric`(1700) → string;
|
|
8
|
+
* - `mysql2`:`DECIMAL` / `NEWDECIMAL`(246) → string。
|
|
9
|
+
*
|
|
10
|
+
* 而 Mongo 驱动对这些值返回 `number`。未归一 → 同一查询在 SQL 后端返回 `"8"`、Mongo 返回
|
|
11
|
+
* `8`(**静默类型失真**,`JSON.stringify` 对拍与用户代码都能看到差异)。
|
|
12
|
+
*
|
|
13
|
+
* 归一**只依据驱动给出的列类型元数据**(`pg` 的 `res.fields[].dataTypeID`、`mysql2` 的
|
|
14
|
+
* `fields[].columnType`),因此**不会**误伤真正的字符串列(如 `_id` / `status`)。
|
|
15
|
+
*
|
|
16
|
+
* 与 py-store 的 `py_store/executors/_values.py`(`Decimal` → 数值)同源同责。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** pg 的数值类型 OID(int2/int4/int8/float4/float8/numeric) */
|
|
20
|
+
const PG_NUMERIC_OIDS = new Set([20, 21, 23, 700, 701, 1700]);
|
|
21
|
+
|
|
22
|
+
/** mysql2 的数值 columnType(DECIMAL/TINY/SHORT/LONG/FLOAT/DOUBLE/LONGLONG/INT24/NEWDECIMAL) */
|
|
23
|
+
const MYSQL_NUMERIC_TYPES = new Set([0, 1, 2, 3, 4, 5, 8, 9, 246]);
|
|
24
|
+
|
|
25
|
+
/** 字符串数值 → number;非有限数值(如超长整数溢出为 Infinity)保持原样,不静默改值 */
|
|
26
|
+
function toNumber(v) {
|
|
27
|
+
if (typeof v !== 'string' || v === '') return v;
|
|
28
|
+
const n = Number(v);
|
|
29
|
+
return Number.isFinite(n) ? n : v;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 行集归一:按列类型元数据把**数值列**的字符串值转 `number`。
|
|
34
|
+
*
|
|
35
|
+
* @param {object[]} rows 驱动返回的行
|
|
36
|
+
* @param {object[]|undefined} fields 列元数据(`dataTypeID` for pg / `columnType` for mysql2)
|
|
37
|
+
* @param {'postgres'|'mysql'} kind 驱动种类
|
|
38
|
+
*/
|
|
39
|
+
function normalizeRows(rows, fields, kind) {
|
|
40
|
+
if (!rows || !rows.length || !fields || !fields.length) return rows;
|
|
41
|
+
const numeric = [];
|
|
42
|
+
for (let i = 0; i < fields.length; i += 1) {
|
|
43
|
+
const t = kind === 'postgres' ? fields[i].dataTypeID : fields[i].columnType;
|
|
44
|
+
const hit = kind === 'postgres' ? PG_NUMERIC_OIDS.has(t) : MYSQL_NUMERIC_TYPES.has(t);
|
|
45
|
+
if (hit) numeric.push(fields[i].name);
|
|
46
|
+
}
|
|
47
|
+
if (!numeric.length) return rows;
|
|
48
|
+
return rows.map((row) => {
|
|
49
|
+
const out = { ...row };
|
|
50
|
+
for (const k of numeric) out[k] = toNumber(out[k]);
|
|
51
|
+
return out;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { MYSQL_NUMERIC_TYPES, PG_NUMERIC_OIDS, normalizeRows, toNumber };
|
package/src/executors/index.js
CHANGED
|
@@ -12,12 +12,17 @@
|
|
|
12
12
|
* (`crud/query|write|mutation`)透明:上层只看到同一套返回值语义。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
const mongo = require('./mongo');
|
|
15
16
|
const mysql = require('./mysql');
|
|
16
17
|
const postgres = require('./postgres');
|
|
17
18
|
const sqlite = require('./sqlite');
|
|
18
19
|
|
|
19
20
|
const _BACKENDS = { mysql, postgres, sqlite };
|
|
20
21
|
|
|
22
|
+
// 注意:sqlite 执行器基于 better-sqlite3 同步驱动,调用期间会阻塞事件循环
|
|
23
|
+
// (设计选择,非缺陷)——高并发主链路请用 mysql/postgres/mongo,或为 SQLite
|
|
24
|
+
// 单独起独立进程;详见 ./sqlite.js 模块头「同步阻塞说明」。
|
|
25
|
+
|
|
21
26
|
/** 创建 SQL 数据源连接描述符 `{ kind, exec }`(driver 为对应驱动实例/连接) */
|
|
22
27
|
function createConnection(kind, driver, options) {
|
|
23
28
|
const mod = _BACKENDS[kind];
|
|
@@ -57,4 +62,4 @@ function shapeResult(cmd, out) {
|
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
module.exports = { createConnection, shapeResult, mysql, postgres, sqlite };
|
|
65
|
+
module.exports = { createConnection, shapeResult, mongo, mysql, postgres, sqlite };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mongo 执行器(驱动:mongodb 原生)
|
|
5
|
+
*
|
|
6
|
+
* 只做「Command JSON → 原生驱动调用」(铁律 1/3):全部纯逻辑(GQL 解析、权限、
|
|
7
|
+
* 命令规划、结果后处理)都在 Rust core。对齐 ``py-store/src/py_store/executors/mongo.py``。
|
|
8
|
+
*
|
|
9
|
+
* 唯一原生边界改写:`_explicitNull` —— 把「字段 = null」的**等值条件**编译为
|
|
10
|
+
* 「字段存在且为 null」(`{$eq: null, $exists: true}`)。理由:Mongo 原生把 `{f: null}`
|
|
11
|
+
* 同时命中「值为 null」与「字段缺失」两类文档,而本 store 的三态契约(F-07/H-09)
|
|
12
|
+
* 要求 `null` 只命中显式 null、`$exists:false` 才命中缺失 —— 这与 SQL 侧
|
|
13
|
+
* `col IS NULL`(显式 null)语义对齐。运算对象(`$ne:null`/`$exists`/`$gt`…)不改写。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 递归改写 filter:`field: null`(标量等值)→ `field: {$eq: null, $exists: true}`。 */
|
|
17
|
+
function _explicitNull(v) {
|
|
18
|
+
if (Array.isArray(v)) return v.map(_explicitNull);
|
|
19
|
+
if (v && typeof v === 'object') {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const [k, val] of Object.entries(v)) {
|
|
22
|
+
if (k.startsWith('$') && (val !== null && typeof val === 'object')) {
|
|
23
|
+
out[k] = _explicitNull(val);
|
|
24
|
+
} else if (k.startsWith('$')) {
|
|
25
|
+
out[k] = val;
|
|
26
|
+
} else if (val !== null && typeof val === 'object') {
|
|
27
|
+
out[k] = _explicitNull(val);
|
|
28
|
+
} else if (val === null) {
|
|
29
|
+
out[k] = { $eq: null, $exists: true };
|
|
30
|
+
} else {
|
|
31
|
+
out[k] = val;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
return v;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _normFilter(cmd) {
|
|
40
|
+
if (cmd.filter && typeof cmd.filter === 'object' && !Array.isArray(cmd.filter)) {
|
|
41
|
+
cmd.filter = _explicitNull(cmd.filter);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _normPipeline(cmd) {
|
|
46
|
+
if (!Array.isArray(cmd.pipeline)) return;
|
|
47
|
+
for (const stage of cmd.pipeline) {
|
|
48
|
+
if (stage && typeof stage === 'object' && stage.$match && typeof stage.$match === 'object') {
|
|
49
|
+
stage.$match = _explicitNull(stage.$match);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Command JSON → MongoDB 原生驱动调用 */
|
|
55
|
+
async function execMongo(db, cmd) {
|
|
56
|
+
const coll = db.collection(cmd.collection);
|
|
57
|
+
switch (cmd.kind) {
|
|
58
|
+
case 'find': {
|
|
59
|
+
_normFilter(cmd);
|
|
60
|
+
const opts = cmd.projection ? { projection: cmd.projection } : undefined;
|
|
61
|
+
return coll.find(cmd.filter, opts).toArray();
|
|
62
|
+
}
|
|
63
|
+
case 'aggregate':
|
|
64
|
+
_normPipeline(cmd);
|
|
65
|
+
return coll.aggregate(cmd.pipeline).toArray();
|
|
66
|
+
case 'countDocuments':
|
|
67
|
+
_normFilter(cmd);
|
|
68
|
+
return coll.countDocuments(cmd.filter);
|
|
69
|
+
case 'findOne': {
|
|
70
|
+
_normFilter(cmd);
|
|
71
|
+
const opts = cmd.projection ? { projection: cmd.projection } : undefined;
|
|
72
|
+
return coll.findOne(cmd.filter, opts);
|
|
73
|
+
}
|
|
74
|
+
case 'insertOne':
|
|
75
|
+
await coll.insertOne(cmd.doc);
|
|
76
|
+
return cmd.doc;
|
|
77
|
+
case 'insertMany':
|
|
78
|
+
if (cmd.upsertById) {
|
|
79
|
+
// 归档幂等(core planArchiveDocs):按 _id 逐条覆盖 —— 「归档成功但删除失败」
|
|
80
|
+
// 的重试不再因 _id 冲突整批失败。SQL 侧由 dialect 的 ON CONFLICT/REPLACE 承接。
|
|
81
|
+
for (const doc of cmd.docs) {
|
|
82
|
+
await coll.replaceOne({ _id: doc._id }, doc, { upsert: true });
|
|
83
|
+
}
|
|
84
|
+
return { insertedCount: cmd.docs.length };
|
|
85
|
+
}
|
|
86
|
+
await coll.insertMany(cmd.docs);
|
|
87
|
+
return { insertedCount: cmd.docs.length };
|
|
88
|
+
case 'findOneAndUpdate':
|
|
89
|
+
_normFilter(cmd);
|
|
90
|
+
return coll.findOneAndUpdate(cmd.filter, cmd.update, cmd.options);
|
|
91
|
+
case 'updateMany':
|
|
92
|
+
_normFilter(cmd);
|
|
93
|
+
return coll.updateMany(cmd.filter, cmd.update);
|
|
94
|
+
case 'deleteMany':
|
|
95
|
+
_normFilter(cmd);
|
|
96
|
+
return coll.deleteMany(cmd.filter);
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`未支持的命令: ${cmd.kind}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { execMongo };
|
package/src/executors/mysql.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { core: _core } = require('../schema');
|
|
13
|
+
const { normalizeRows } = require('./_values');
|
|
13
14
|
|
|
14
15
|
/** mysql2 返回 RowDataPacket 实例,转普通对象后再交 core(绑定层只认纯 JSON) */
|
|
15
16
|
function _plain(row) {
|
|
@@ -23,22 +24,49 @@ function create(driver, _options = {}) {
|
|
|
23
24
|
if (!driver || typeof driver.execute !== 'function') {
|
|
24
25
|
throw new TypeError('mysql 执行器需要 mysql2/promise 的连接或连接池');
|
|
25
26
|
}
|
|
27
|
+
|
|
28
|
+
/** 在指定连接上依序执行 plan.stmts(多语句 plan 由 withTransaction 包事务) */
|
|
29
|
+
async function runStmts(conn, plan) {
|
|
30
|
+
let docs = null;
|
|
31
|
+
let rows = null;
|
|
32
|
+
let affectedRows = 0;
|
|
33
|
+
for (const stmt of plan.stmts) {
|
|
34
|
+
const [raw, fields] = await conn.execute(stmt.text, stmt.params || []);
|
|
35
|
+
if (Array.isArray(raw)) {
|
|
36
|
+
rows = normalizeRows(raw.map(_plain), fields, 'mysql');
|
|
37
|
+
if (stmt.rowShape) docs = _core.restoreRows(stmt.rowShape, rows);
|
|
38
|
+
} else {
|
|
39
|
+
affectedRows = Number(raw.affectedRows || 0);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { docs, rows, affectedRows };
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
return {
|
|
27
46
|
kind: 'mysql',
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
47
|
+
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 失败不掩盖原始错误 */
|
|
39
65
|
}
|
|
66
|
+
throw e;
|
|
67
|
+
} finally {
|
|
68
|
+
if (conn !== driver && typeof conn.release === 'function') conn.release();
|
|
40
69
|
}
|
|
41
|
-
return { docs, rows, affectedRows };
|
|
42
70
|
},
|
|
43
71
|
};
|
|
44
72
|
}
|
|
@@ -10,25 +10,65 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { core: _core } = require('../schema');
|
|
13
|
+
const { normalizeRows } = require('./_values');
|
|
13
14
|
|
|
14
15
|
/** 创建执行器描述符(可直接作为 `init(connections)` 的一个 SQL 数据源连接) */
|
|
15
16
|
function create(driver, _options = {}) {
|
|
16
17
|
if (!driver || typeof driver.query !== 'function') {
|
|
17
18
|
throw new TypeError('postgres 执行器需要 pg 的 Pool/Client 实例');
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
/** 在指定连接上依序执行 plan.stmts */
|
|
22
|
+
async function runStmts(conn, plan) {
|
|
23
|
+
let docs = null;
|
|
24
|
+
let rows = null;
|
|
25
|
+
let affectedRows = 0;
|
|
26
|
+
for (const stmt of plan.stmts) {
|
|
27
|
+
const res = await conn.query(stmt.text, stmt.params || []);
|
|
28
|
+
rows = normalizeRows(res.rows || [], res.fields, 'postgres');
|
|
29
|
+
affectedRows = Number(res.rowCount || 0);
|
|
30
|
+
if (stmt.rowShape) docs = _core.restoreRows(stmt.rowShape, rows);
|
|
31
|
+
}
|
|
32
|
+
return { docs, rows, affectedRows };
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
return {
|
|
20
36
|
kind: 'postgres',
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
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();
|
|
30
71
|
}
|
|
31
|
-
return { docs, rows, affectedRows };
|
|
32
72
|
},
|
|
33
73
|
};
|
|
34
74
|
}
|
package/src/executors/sqlite.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* 产出,本模块不拼任何 SQL;带 `rowShape` 的语句结果交 core `restoreRows` 还原为
|
|
8
8
|
* 嵌套文档。返回值是中立包络 `{ docs, rows, affectedRows }`,由 `./index.js`
|
|
9
9
|
* 依 command.kind 塑形为 Mongo 驱动等价返回值。
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ 同步阻塞说明:better-sqlite3 是**同步驱动**,`prepare/all/run` 在 async 契约内
|
|
12
|
+
* 仍会**阻塞事件循环**(本模块保持同步调用是有意为之的设计选择:单连接语义最简、
|
|
13
|
+
* 无跨线程开销)。高并发主链路请改用 MySQL / PostgreSQL / MongoDB 数据源,
|
|
14
|
+
* 或为 SQLite 单独起独立进程隔离阻塞面——本执行器不适合多请求共享的事件循环热路径。
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
const { core: _core } = require('../schema');
|
|
@@ -25,23 +30,43 @@ function create(db, _options = {}) {
|
|
|
25
30
|
if (!db || typeof db.prepare !== 'function') {
|
|
26
31
|
throw new TypeError('sqlite 执行器需要 better-sqlite3 Database 实例');
|
|
27
32
|
}
|
|
33
|
+
|
|
34
|
+
/** 依序执行 plan.stmts(better-sqlite3 同步单连接;同步调用会阻塞事件循环,见模块头说明) */
|
|
35
|
+
function runStmts(plan) {
|
|
36
|
+
let docs = null;
|
|
37
|
+
let rows = null;
|
|
38
|
+
let affectedRows = 0;
|
|
39
|
+
for (const stmt of plan.stmts) {
|
|
40
|
+
const params = _bind(stmt.params);
|
|
41
|
+
// 带 RETURNING 的写语句同样返回行 → 必须用 all() 取回;其余写语句用 run() 取影响行数
|
|
42
|
+
if (!stmt.isWrite || stmt.rowShape) {
|
|
43
|
+
rows = db.prepare(stmt.text).all(...params);
|
|
44
|
+
if (stmt.rowShape) docs = _core.restoreRows(stmt.rowShape, rows);
|
|
45
|
+
} else {
|
|
46
|
+
affectedRows = Number(db.prepare(stmt.text).run(...params).changes || 0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { docs, rows, affectedRows };
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
return {
|
|
29
53
|
kind: 'sqlite',
|
|
30
|
-
exec
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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);
|
|
60
|
+
db.exec('COMMIT');
|
|
61
|
+
return out;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
try {
|
|
64
|
+
db.exec('ROLLBACK');
|
|
65
|
+
} catch (_) {
|
|
66
|
+
/* rollback 失败不掩盖原始错误 */
|
|
42
67
|
}
|
|
68
|
+
throw e;
|
|
43
69
|
}
|
|
44
|
-
return { docs, rows, affectedRows };
|
|
45
70
|
},
|
|
46
71
|
};
|
|
47
72
|
}
|
package/src/feedback.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 反馈事件通道 —— 兜底/降级/拦截触发的统一出口
|
|
5
|
+
*
|
|
6
|
+
* 事件形状(对齐 rust-store 联邦 degraded 契约)::
|
|
7
|
+
*
|
|
8
|
+
* {type, code, layer, message, hint, ...}
|
|
9
|
+
* - type : 事件类别(federation_degraded / sql_pushdown_unsupported ...)
|
|
10
|
+
* - code : 机器可读代码(crossSourceSort / pushdownUnsupported ...)
|
|
11
|
+
* - layer : 命中的防护层(federation / dialect ...)
|
|
12
|
+
* - message: 人可读描述
|
|
13
|
+
* - hint : 修复指引(供上游排查/加固)
|
|
14
|
+
*
|
|
15
|
+
* 默认无 sink 时打 stderr(向后兼容);宿主可 `setSink(fn)` 接管,
|
|
16
|
+
* 接入自动反馈闭环(允许被拦截,禁止静默失守)。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
let _sink = null;
|
|
20
|
+
|
|
21
|
+
/** 注册反馈事件回调 `fn(event)`;传 null/非函数恢复默认 stderr 行为 */
|
|
22
|
+
function setSink(fn) {
|
|
23
|
+
_sink = typeof fn === 'function' ? fn : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 产出一条反馈事件:有 sink 回调之;否则打印 stderr(允许拦截,禁止静默) */
|
|
27
|
+
function emit(event) {
|
|
28
|
+
const e = event || {};
|
|
29
|
+
if (_sink) {
|
|
30
|
+
_sink(e);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.error(
|
|
34
|
+
`[nodejs-store][${e.layer || '?'}/${e.code || '?'}] `
|
|
35
|
+
+ `${e.message || ''}(hint: ${e.hint || '-'})`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { setSink, emit };
|