base-link-core 0.3.14 → 0.3.16

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.
@@ -0,0 +1,896 @@
1
+ /**
2
+ * #955 · 数据族引擎最小路径:单表查询单的解析、目录校验与结果集装配。
3
+ * #956 · 加连接一支:显式 `on` 的两表连接(`join` 长度只许 1);`via`
4
+ * 预定义关联路径只留语法位,出现即按项拒(后续加法式扩)。
5
+ * #957 · 加分组聚合一支:`groupBy`+`agg`(`sum`/`avg`/`count`/`min`/`max`,
6
+ * 聚合列按 `as` 起名);聚合结果同样是结果集的一行行。业务派生量(缺口、
7
+ * 连续记录天数、达标率……)不在这族——本件不提供表达式、窗口函数与 `having`,
8
+ * 这类写法按项拒;将来扶正为业务量另票且落在视图族。
9
+ * #958 · 加分页续取一支:`page.size` 单次截断 + `next` 不透明凭据续取;
10
+ * 行查询走游标(`ORDER BY` 全序 + `rowid` 系链,`NULL` 安全),聚合查询走偏移;
11
+ * `total` 为全量命中数(不再等于当页行数),末页不再回 `next`。
12
+ *
13
+ * 住公共层(`base-link-core`)的理由:`docs/agents/数据族-规格.md` §六——
14
+ * 查询单解析、目录校验、结果集装配住公共层;表清单住各技能自己的包。
15
+ * 目录校验复用同一件 `readDataSchema`(与目录命令返回的是同一份,不会两处漂移)。
16
+ * 分页续取同住(规格 §四③④):凭据编解码与游标装配住 `data-page.ts`,
17
+ * 本件只做接线(解析 → 指纹 → 取数 → 截断 → 凭据)。
18
+ *
19
+ * 范围:单表取行(选字段、条件、排序)+显式 `on` 两表连接+单表分组聚合,
20
+ * 以上三支均可分页续取;空集合法(空分组 → `rows: []`、`total: 0`);
21
+ * 非法表名/非法字段/非法凭据按项拒并点名。批量语义留后续票(#959):
22
+ * 本件遇到批量外形态不静默忽略(请求级仍须 `{ queries: [...] }` 非空数组)。
23
+ *
24
+ * 零运行时依赖:只 import 同包的 `errors.js`、`data-schema.js` 与 `data-page.js`;
25
+ * 库句柄只按形状收(`DataQueryDb`),`DatabaseSync` 按形状相容。
26
+ * 类型上写得出具体形状:无 `any`,未知输入先收窄再读。
27
+ * 只读:只跑 `SELECT` 与 `PRAGMA`,不建表、不迁移、不写库、不产文件。
28
+ */
29
+ import { LinkCoreError } from './errors.js';
30
+ import { readDataSchema } from './data-schema.js';
31
+ import { buildCursorCondition, decodeAggToken, decodeRowToken, encodeAggToken, encodeRowToken, fingerprintQuery, readPage, } from './data-page.js';
32
+ const OPS = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'like', 'in', 'isNull', 'isNotNull'];
33
+ function isRecord(v) {
34
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
35
+ }
36
+ function fail(code, message) {
37
+ throw new LinkCoreError(code, message);
38
+ }
39
+ /** 标识符引用:`"` 转义后包起来(表名/列名已对目录校验过,仍不裸拼)。 */
40
+ function ident(name) {
41
+ return '"' + name.replace(/"/g, '""') + '"';
42
+ }
43
+ /** 预留:历史上曾在此拒 `page`(#958 已落地分页,本函数不再拒任何键,保留作扩展点)。 */
44
+ function assertNoUnsupported(_raw, _from) { }
45
+ /** 分页隐列别名前缀(`__data_page_o<序号>__`:排序列隐带,供游标取值,不进 `fields`/`rows`)。 */
46
+ function orderHiddenAlias(i) {
47
+ return '__data_page_o' + i + '__';
48
+ }
49
+ /** 单表 `rowid` 隐列别名(供游标系链,不进 `fields`/`rows`)。 */
50
+ const SINGLE_ROWID_ALIAS = '__data_page_rowid__';
51
+ /** 连接 `rowid` 隐列别名(两表各一链,不进 `fields`/`rows`)。 */
52
+ const FROM_ROWID_ALIAS = '__data_page_from_rowid__';
53
+ const JOIN_ROWID_ALIAS = '__data_page_join_rowid__';
54
+ /** 取全量命中数(一行 `COUNT(*)`,行数非 1 或缺 `cnt` 即内部错)。 */
55
+ function readCount(db, sql, params, from) {
56
+ const rows = db.prepare(sql).all(...params);
57
+ if (rows.length !== 1 || !isRecord(rows[0])) {
58
+ fail('DATA_INTERNAL', from + ' 的计数查询未回单行');
59
+ }
60
+ const cnt = rows[0]['cnt'];
61
+ if (typeof cnt !== 'number' || !Number.isInteger(cnt) || cnt < 0) {
62
+ fail('DATA_INTERNAL', from + ' 的计数非法:' + String(cnt));
63
+ }
64
+ return cnt;
65
+ }
66
+ /** `表.列` 限定引用的形状(`on` 左右与 join 下的 `表.列` 写法走这里)。 */
67
+ function splitQualified(ref) {
68
+ const dot = ref.indexOf('.');
69
+ if (dot <= 0 || dot === ref.length - 1)
70
+ return null;
71
+ if (ref.indexOf('.', dot + 1) !== -1)
72
+ return null;
73
+ return { table: ref.slice(0, dot), column: ref.slice(dot + 1) };
74
+ }
75
+ function readJoin(raw, from, allowTables, colOf) {
76
+ const j = raw['join'];
77
+ if (j === undefined)
78
+ return null;
79
+ if (!Array.isArray(j) || j.length !== 1) {
80
+ fail('DATA_QUERY_INVALID', from + ' 的 join 须为长度 1 的数组(本票只做两表连接)');
81
+ }
82
+ const entry = j[0];
83
+ if (!isRecord(entry))
84
+ fail('DATA_QUERY_INVALID', from + ' 的 join[0] 须为对象');
85
+ const rec = entry;
86
+ if (rec['via'] !== undefined) {
87
+ fail('DATA_QUERY_INVALID', from + ' 不支持 via 预定义关联路径(后续加法式扩,本票只做显式 on)');
88
+ }
89
+ const table = rec['table'];
90
+ if (typeof table !== 'string' || table.length === 0) {
91
+ fail('DATA_QUERY_INVALID', from + ' 的 join.table 须为非空字符串');
92
+ }
93
+ if (!allowTables.includes(table)) {
94
+ fail('DATA_UNKNOWN_TABLE', '未知表:' + String(table) + '(允许:' + allowTables.join('/') + ')');
95
+ }
96
+ if (table === from) {
97
+ fail('DATA_QUERY_INVALID', from + ' 的 join.table 不得与 from 同表:' + String(table));
98
+ }
99
+ const typeRaw = rec['type'];
100
+ const kind = typeof typeRaw === 'string' && typeRaw.toLowerCase() === 'inner'
101
+ ? 'INNER'
102
+ : typeof typeRaw === 'string' && typeRaw.toLowerCase() === 'left'
103
+ ? 'LEFT'
104
+ : null;
105
+ if (kind === null) {
106
+ fail('DATA_QUERY_INVALID', from + ' 的 join.type 须显式写 inner/left(实际:' + String(typeRaw) + ')');
107
+ }
108
+ const on = rec['on'];
109
+ if (!Array.isArray(on) || on.length === 0) {
110
+ fail('DATA_QUERY_INVALID', from + ' 的 join.on 须为非空数组(显式连接条件)');
111
+ }
112
+ const scope = new Map([
113
+ [from, colOf(from)],
114
+ [table, colOf(table)],
115
+ ]);
116
+ const pairs = [];
117
+ for (const item of on) {
118
+ if (!isRecord(item))
119
+ fail('DATA_QUERY_INVALID', from + ' 的 join.on 元素须为对象(含 left/right)');
120
+ const left = item['left'];
121
+ const right = item['right'];
122
+ if (typeof left !== 'string' || left.length === 0 || typeof right !== 'string' || right.length === 0) {
123
+ fail('DATA_QUERY_INVALID', from + ' 的 join.on 元素 left/right 须为非空字符串');
124
+ }
125
+ const l = splitQualified(left);
126
+ const r = splitQualified(right);
127
+ if (!l)
128
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + String(left) + '(连接条件须写成 表.列)');
129
+ if (!r)
130
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + String(right) + '(连接条件须写成 表.列)');
131
+ for (const [ref, part] of [
132
+ [left, l],
133
+ [right, r],
134
+ ]) {
135
+ const cols = scope.get(part.table);
136
+ if (!cols)
137
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + String(ref) + '(连接条件须引用 ' + from + '/' + String(table) + ' 两表之列)');
138
+ if (!cols.has(part.column)) {
139
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + String(ref));
140
+ }
141
+ }
142
+ if (l.table === r.table) {
143
+ fail('DATA_QUERY_INVALID', from + ' 的连接条件须跨两表(左右须各属一表):' + String(left) + ' = ' + String(right));
144
+ }
145
+ pairs.push({
146
+ leftTable: l.table,
147
+ leftCol: l.column,
148
+ rightTable: r.table,
149
+ rightCol: r.column,
150
+ });
151
+ }
152
+ return { joinTable: table, joinKind: kind, pairs };
153
+ }
154
+ /** 有连接时 select/where/orderBy 共用的字段归属:裸名须在两表间无歧义,`表.列` 须落在两表内。 */
155
+ function resolveScopedField(field, from, joinTable, colOf, ownersOf, ctx) {
156
+ if (typeof field !== 'string' || field.length === 0) {
157
+ fail('DATA_QUERY_INVALID', ctx + ' field 须为非空字符串');
158
+ }
159
+ const name = field;
160
+ if (name.includes('.')) {
161
+ const part = splitQualified(name);
162
+ if (!part)
163
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + name);
164
+ const cols = part.table === from || part.table === joinTable ? colOf(part.table) : null;
165
+ if (!cols)
166
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + name + '(须引用 ' + from + '/' + joinTable + ' 两表之列)');
167
+ const type = cols.get(part.column);
168
+ if (!type)
169
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + name);
170
+ return { table: part.table, column: part.column, type: type };
171
+ }
172
+ const owners = ownersOf(name);
173
+ if (owners.length === 0)
174
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + from + '.' + name + '(两表中均无此列)');
175
+ if (owners.length > 1) {
176
+ fail('DATA_QUERY_INVALID', ctx + ' 字段歧义:' + name + '(' + owners.join('/') + ' 两表均有此列,请写成 表.列)');
177
+ }
178
+ return { table: owners[0], column: name, type: colOf(owners[0]).get(name) };
179
+ }
180
+ function readId(raw) {
181
+ const id = raw['id'];
182
+ if (id === undefined)
183
+ return undefined;
184
+ if (typeof id !== 'string')
185
+ fail('DATA_QUERY_INVALID', '查询单 id 须为字符串');
186
+ return id;
187
+ }
188
+ function readFrom(raw, allowTables) {
189
+ const from = raw['from'];
190
+ if (typeof from !== 'string' || from.length === 0) {
191
+ fail('DATA_QUERY_INVALID', '查询单 from 须为非空字符串');
192
+ }
193
+ if (!allowTables.includes(from)) {
194
+ fail('DATA_UNKNOWN_TABLE', '未知表:' + String(from) + '(允许:' + allowTables.join('/') + ')');
195
+ }
196
+ return from;
197
+ }
198
+ function readSelect(raw, from, cols) {
199
+ const colByName = new Map(cols.map((c) => [c.name, c]));
200
+ const sel = raw['select'];
201
+ if (sel === undefined)
202
+ return cols.map((c) => ({ name: c.name, type: c.type }));
203
+ if (!Array.isArray(sel) || sel.length === 0) {
204
+ fail('DATA_QUERY_INVALID', from + ' 的 select 须为非空数组');
205
+ }
206
+ const out = [];
207
+ const seen = new Set();
208
+ for (const name of sel) {
209
+ if (typeof name !== 'string' || name.length === 0) {
210
+ fail('DATA_QUERY_INVALID', from + ' 的 select 元素须为非空字符串');
211
+ }
212
+ const hit = colByName.get(name);
213
+ if (!hit)
214
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + from + '.' + String(name));
215
+ if (seen.has(name))
216
+ fail('DATA_QUERY_INVALID', from + ' 的 select 字段重复:' + String(name));
217
+ seen.add(name);
218
+ out.push({ name: hit.name, type: hit.type });
219
+ }
220
+ return out;
221
+ }
222
+ function isScalar(v) {
223
+ return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
224
+ }
225
+ function checkLeafValue(from, field, op, value, colType) {
226
+ if (op === 'isNull' || op === 'isNotNull') {
227
+ if (value !== undefined)
228
+ fail('DATA_QUERY_INVALID', from + '.' + field + ' 的 ' + op + ' 不带 value');
229
+ return;
230
+ }
231
+ if (op === 'in') {
232
+ if (!Array.isArray(value) || value.length === 0) {
233
+ fail('DATA_QUERY_INVALID', from + '.' + field + ' 的 in 须为非空数组');
234
+ }
235
+ for (const v of value) {
236
+ if (!isScalar(v))
237
+ fail('DATA_QUERY_INVALID', from + '.' + field + ' 的 in 元素须为字符串/数字/布尔/null');
238
+ }
239
+ return;
240
+ }
241
+ if (op === 'like') {
242
+ if (typeof value !== 'string') {
243
+ fail('DATA_QUERY_INVALID', from + '.' + field + ' 的 like 值须为字符串');
244
+ }
245
+ const t = colType.toUpperCase();
246
+ if (!t.includes('TEXT') && !t.includes('CHAR') && !t.includes('CLOB')) {
247
+ fail('DATA_QUERY_INVALID', from + '.' + field + ' 的 like 只支持文本列(该列类型 ' + colType + ')');
248
+ }
249
+ return;
250
+ }
251
+ if (op === 'eq' || op === 'ne') {
252
+ if (!isScalar(value)) {
253
+ fail('DATA_QUERY_INVALID', from + '.' + field + ' 的 ' + op + ' 值须为字符串/数字/布尔/null');
254
+ }
255
+ return;
256
+ }
257
+ if (value === null || value === undefined || typeof value === 'object' || typeof value === 'boolean') {
258
+ fail('DATA_QUERY_INVALID', from + '.' + field + ' 的 ' + op + ' 值须为字符串/数字');
259
+ }
260
+ }
261
+ function checkWhere(node, from, colByName) {
262
+ if (!isRecord(node))
263
+ fail('DATA_QUERY_INVALID', from + ' 的 where 须为对象');
264
+ const rec = node;
265
+ const hasAnd = rec['and'] !== undefined;
266
+ const hasOr = rec['or'] !== undefined;
267
+ if (hasAnd || hasOr) {
268
+ if (hasAnd && hasOr)
269
+ fail('DATA_QUERY_INVALID', from + ' 的 where 里 and 与 or 不可同层并存');
270
+ const key = hasAnd ? 'and' : 'or';
271
+ const list = rec[key];
272
+ if (!Array.isArray(list) || list.length === 0) {
273
+ fail('DATA_QUERY_INVALID', from + ' 的 where.' + key + ' 须为非空数组');
274
+ }
275
+ for (const sub of list)
276
+ checkWhere(sub, from, colByName);
277
+ return;
278
+ }
279
+ const field = rec['field'];
280
+ const op = rec['op'];
281
+ if (typeof field !== 'string' || field.length === 0) {
282
+ fail('DATA_QUERY_INVALID', from + ' 的条件 field 须为非空字符串');
283
+ }
284
+ const colType = colByName.get(field);
285
+ if (!colType)
286
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + from + '.' + String(field));
287
+ if (typeof op !== 'string' || !OPS.includes(op)) {
288
+ fail('DATA_QUERY_INVALID', from + '.' + String(field) + ' 的 op 非法:' + String(op) + '(允许:' + OPS.join('/') + ')');
289
+ }
290
+ checkLeafValue(from, field, op, rec['value'], colType);
291
+ }
292
+ function checkOrderBy(raw, from, colByName) {
293
+ const ob = raw['orderBy'];
294
+ if (ob === undefined)
295
+ return [];
296
+ if (!Array.isArray(ob) || ob.length === 0) {
297
+ fail('DATA_QUERY_INVALID', from + ' 的 orderBy 须为非空数组');
298
+ }
299
+ const out = [];
300
+ for (const item of ob) {
301
+ if (!isRecord(item))
302
+ fail('DATA_QUERY_INVALID', from + ' 的 orderBy 元素须为对象');
303
+ const field = item['field'];
304
+ const dir = item['dir'];
305
+ if (typeof field !== 'string' || field.length === 0) {
306
+ fail('DATA_QUERY_INVALID', from + ' 的 orderBy.field 须为非空字符串');
307
+ }
308
+ if (!colByName.has(field))
309
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + from + '.' + String(field));
310
+ if (typeof dir !== 'string' || (dir.toLowerCase() !== 'asc' && dir.toLowerCase() !== 'desc')) {
311
+ fail('DATA_QUERY_INVALID', from + '.' + String(field) + ' 的排序方向须为 asc/desc');
312
+ }
313
+ out.push({ field: field, dir: dir.toLowerCase() === 'desc' ? 'DESC' : 'ASC' });
314
+ }
315
+ return out;
316
+ }
317
+ const AGG_FNS = ['sum', 'avg', 'count', 'min', 'max'];
318
+ function readGroupBy(raw, from, colByName) {
319
+ const gb = raw['groupBy'];
320
+ if (!Array.isArray(gb) || gb.length === 0) {
321
+ fail('DATA_QUERY_INVALID', from + ' 的 groupBy 须为非空数组(分组与聚合须成对出现,见 #957)');
322
+ }
323
+ const out = [];
324
+ const seen = new Set();
325
+ for (const entry of gb) {
326
+ if (typeof entry !== 'string' || entry.length === 0) {
327
+ fail('DATA_QUERY_INVALID', from + ' 的 groupBy 元素须为非空字符串');
328
+ }
329
+ if (!colByName.has(entry))
330
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + from + '.' + String(entry));
331
+ if (seen.has(entry))
332
+ fail('DATA_QUERY_INVALID', from + ' 的 groupBy 字段重复:' + String(entry));
333
+ seen.add(entry);
334
+ out.push(entry);
335
+ }
336
+ return out;
337
+ }
338
+ function readAgg(raw, from, colByName) {
339
+ const ag = raw['agg'];
340
+ if (!Array.isArray(ag) || ag.length === 0) {
341
+ fail('DATA_QUERY_INVALID', from + ' 的 agg 须为非空数组(分组与聚合须成对出现,见 #957)');
342
+ }
343
+ const out = [];
344
+ const seenAs = new Set();
345
+ for (const entry of ag) {
346
+ if (!isRecord(entry))
347
+ fail('DATA_QUERY_INVALID', from + ' 的 agg 元素须为对象(含 fn/field/as)');
348
+ const rec = entry;
349
+ const fnRaw = rec['fn'];
350
+ const fn = typeof fnRaw === 'string' && AGG_FNS.includes(fnRaw.toLowerCase())
351
+ ? fnRaw.toLowerCase()
352
+ : null;
353
+ if (fn === null) {
354
+ fail('DATA_QUERY_INVALID', from + ' 的 agg.fn 非法:' + String(fnRaw) + '(允许:' + AGG_FNS.join('/') + ')');
355
+ }
356
+ const field = rec['field'];
357
+ if (typeof field !== 'string' || field.length === 0) {
358
+ fail('DATA_QUERY_INVALID', from + ' 的 agg.field 须为非空字符串');
359
+ }
360
+ const f = field;
361
+ if (!(fn === 'count' && f === '*') && !colByName.has(f)) {
362
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + from + '.' + String(field));
363
+ }
364
+ const asRaw = rec['as'];
365
+ if (typeof asRaw !== 'string' || asRaw.length === 0 || asRaw.trim().length === 0) {
366
+ fail('DATA_QUERY_INVALID', from + ' 的 agg.as 须为非空字符串(聚合列按此名出现在 fields 里)');
367
+ }
368
+ const as = asRaw;
369
+ if (seenAs.has(as))
370
+ fail('DATA_QUERY_INVALID', from + ' 的 agg.as 重复:' + as);
371
+ if (colByName.has(as)) {
372
+ fail('DATA_QUERY_INVALID', from + ' 的 agg.as 不得与表列同名:' + as + '(防遮蔽,请另起名)');
373
+ }
374
+ seenAs.add(as);
375
+ const inputType = f === '*' ? 'INTEGER' : colByName.get(f);
376
+ const type = fn === 'count' ? 'INTEGER' : fn === 'avg' ? 'REAL' : inputType;
377
+ out.push({ fn: fn, field: f, as, type });
378
+ }
379
+ return out;
380
+ }
381
+ function checkOrderByAgg(raw, from, groupBy, agg, colByName) {
382
+ const ob = raw['orderBy'];
383
+ if (ob === undefined)
384
+ return [];
385
+ if (!Array.isArray(ob) || ob.length === 0) {
386
+ fail('DATA_QUERY_INVALID', from + ' 的 orderBy 须为非空数组');
387
+ }
388
+ const groups = new Set(groupBy);
389
+ const aliases = new Set(agg.map((a) => a.as));
390
+ const out = [];
391
+ for (const item of ob) {
392
+ if (!isRecord(item))
393
+ fail('DATA_QUERY_INVALID', from + ' 的 orderBy 元素须为对象');
394
+ const field = item['field'];
395
+ const dir = item['dir'];
396
+ if (typeof field !== 'string' || field.length === 0) {
397
+ fail('DATA_QUERY_INVALID', from + ' 的 orderBy.field 须为非空字符串');
398
+ }
399
+ const name = field;
400
+ if (!groups.has(name) && !aliases.has(name)) {
401
+ if (colByName.has(name)) {
402
+ fail('DATA_QUERY_INVALID', from + ' 的聚合查询 orderBy 须为分组列或聚合别名:' + name);
403
+ }
404
+ fail('DATA_UNKNOWN_FIELD', '未知字段:' + from + '.' + String(field));
405
+ }
406
+ if (typeof dir !== 'string' || (dir.toLowerCase() !== 'asc' && dir.toLowerCase() !== 'desc')) {
407
+ fail('DATA_QUERY_INVALID', from + '.' + String(field) + ' 的排序方向须为 asc/desc');
408
+ }
409
+ out.push({ field: name, dir: dir.toLowerCase() === 'desc' ? 'DESC' : 'ASC' });
410
+ }
411
+ return out;
412
+ }
413
+ function aggExpr(a) {
414
+ const fn = a.fn.toUpperCase();
415
+ if (a.fn === 'count' && a.field === '*')
416
+ return 'COUNT(*)';
417
+ return fn + '(' + ident(a.field) + ')';
418
+ }
419
+ function toParam(v) {
420
+ if (typeof v === 'boolean')
421
+ return v ? 1 : 0;
422
+ return v;
423
+ }
424
+ function readSelectScoped(raw, from, joinTable, fromCols, colOf, ownersOf) {
425
+ const sel = raw['select'];
426
+ if (sel === undefined)
427
+ return fromCols.map((c) => ({ name: c.name, table: from, column: c.name, type: c.type }));
428
+ if (!Array.isArray(sel) || sel.length === 0) {
429
+ fail('DATA_QUERY_INVALID', from + ' 的 select 须为非空数组');
430
+ }
431
+ const out = [];
432
+ const seen = new Set();
433
+ for (const entry of sel) {
434
+ const r = resolveScopedField(entry, from, joinTable, colOf, ownersOf, from + ' 的 select');
435
+ if (seen.has(r.table + '.' + r.column)) {
436
+ fail('DATA_QUERY_INVALID', from + ' 的 select 字段重复:' + String(entry));
437
+ }
438
+ if (out.some((o) => o.name === r.column)) {
439
+ fail('DATA_QUERY_INVALID', from + ' 的 select 输出名重复:' + r.column + '(两表同名列须后续别名票,本票请避开同名双选)');
440
+ }
441
+ seen.add(r.table + '.' + r.column);
442
+ out.push({ name: r.column, table: r.table, column: r.column, type: r.type });
443
+ }
444
+ return out;
445
+ }
446
+ function checkWhereScoped(node, from, joinTable, colOf, ownersOf) {
447
+ if (!isRecord(node))
448
+ fail('DATA_QUERY_INVALID', from + ' 的 where 须为对象');
449
+ const rec = node;
450
+ const hasAnd = rec['and'] !== undefined;
451
+ const hasOr = rec['or'] !== undefined;
452
+ if (hasAnd || hasOr) {
453
+ if (hasAnd && hasOr)
454
+ fail('DATA_QUERY_INVALID', from + ' 的 where 里 and 与 or 不可同层并存');
455
+ const key = hasAnd ? 'and' : 'or';
456
+ const list = rec[key];
457
+ if (!Array.isArray(list) || list.length === 0) {
458
+ fail('DATA_QUERY_INVALID', from + ' 的 where.' + key + ' 须为非空数组');
459
+ }
460
+ for (const sub of list)
461
+ checkWhereScoped(sub, from, joinTable, colOf, ownersOf);
462
+ return;
463
+ }
464
+ const field = rec['field'];
465
+ const op = rec['op'];
466
+ const r = resolveScopedField(field, from, joinTable, colOf, ownersOf, from + ' 的条件');
467
+ if (typeof op !== 'string' || !OPS.includes(op)) {
468
+ fail('DATA_QUERY_INVALID', r.table + '.' + r.column + ' 的 op 非法:' + String(op) + '(允许:' + OPS.join('/') + ')');
469
+ }
470
+ checkLeafValue(r.table, r.column, op, rec['value'], r.type);
471
+ }
472
+ function checkOrderByScoped(raw, from, joinTable, colOf, ownersOf) {
473
+ const ob = raw['orderBy'];
474
+ if (ob === undefined)
475
+ return [];
476
+ if (!Array.isArray(ob) || ob.length === 0) {
477
+ fail('DATA_QUERY_INVALID', from + ' 的 orderBy 须为非空数组');
478
+ }
479
+ const out = [];
480
+ for (const item of ob) {
481
+ if (!isRecord(item))
482
+ fail('DATA_QUERY_INVALID', from + ' 的 orderBy 元素须为对象');
483
+ const rec = item;
484
+ const r = resolveScopedField(rec['field'], from, joinTable, colOf, ownersOf, from + ' 的 orderBy');
485
+ const dir = rec['dir'];
486
+ if (typeof dir !== 'string' || (dir.toLowerCase() !== 'asc' && dir.toLowerCase() !== 'desc')) {
487
+ fail('DATA_QUERY_INVALID', r.table + '.' + r.column + ' 的排序方向须为 asc/desc');
488
+ }
489
+ out.push({ table: r.table, column: r.column, dir: dir.toLowerCase() === 'desc' ? 'DESC' : 'ASC' });
490
+ }
491
+ return out;
492
+ }
493
+ function buildLeafScoped(table, field, op, value) {
494
+ const c = ident(table) + '.' + ident(field);
495
+ if (op === 'isNull')
496
+ return { sql: c + ' IS NULL', params: [] };
497
+ if (op === 'isNotNull')
498
+ return { sql: c + ' IS NOT NULL', params: [] };
499
+ if (op === 'in') {
500
+ const list = value;
501
+ return { sql: c + ' IN (' + list.map(() => '?').join(', ') + ')', params: list.map(toParam) };
502
+ }
503
+ if (op === 'like')
504
+ return { sql: c + " LIKE ? ESCAPE '\\'", params: [value] };
505
+ if (op === 'eq') {
506
+ if (value === null)
507
+ return { sql: c + ' IS NULL', params: [] };
508
+ return { sql: c + ' = ?', params: [toParam(value)] };
509
+ }
510
+ if (op === 'ne') {
511
+ if (value === null)
512
+ return { sql: c + ' IS NOT NULL', params: [] };
513
+ return { sql: c + ' <> ?', params: [toParam(value)] };
514
+ }
515
+ const sym = op === 'gt' ? '>' : op === 'gte' ? '>=' : op === 'lt' ? '<' : '<=';
516
+ return { sql: c + ' ' + sym + ' ?', params: [toParam(value)] };
517
+ }
518
+ function buildWhereScoped(node, from, joinTable, colOf, ownersOf) {
519
+ const rec = node;
520
+ if (rec['and'] !== undefined || rec['or'] !== undefined) {
521
+ const key = rec['and'] !== undefined ? 'and' : 'or';
522
+ const joiner = key === 'and' ? ' AND ' : ' OR ';
523
+ const parts = [];
524
+ const params = [];
525
+ for (const sub of rec[key]) {
526
+ const b = buildWhereScoped(sub, from, joinTable, colOf, ownersOf);
527
+ parts.push('(' + b.sql + ')');
528
+ params.push(...b.params);
529
+ }
530
+ return { sql: parts.join(joiner), params };
531
+ }
532
+ const leaf = node;
533
+ const r = resolveScopedField(leaf.field, from, joinTable, colOf, ownersOf, from + ' 的条件');
534
+ return buildLeafScoped(r.table, r.column, leaf.op, leaf.value);
535
+ }
536
+ function buildLeaf(field, op, value) {
537
+ const c = ident(field);
538
+ if (op === 'isNull')
539
+ return { sql: c + ' IS NULL', params: [] };
540
+ if (op === 'isNotNull')
541
+ return { sql: c + ' IS NOT NULL', params: [] };
542
+ if (op === 'in') {
543
+ const list = value;
544
+ return { sql: c + ' IN (' + list.map(() => '?').join(', ') + ')', params: list.map(toParam) };
545
+ }
546
+ if (op === 'like')
547
+ return { sql: c + " LIKE ? ESCAPE '\\'", params: [value] };
548
+ if (op === 'eq') {
549
+ if (value === null)
550
+ return { sql: c + ' IS NULL', params: [] };
551
+ return { sql: c + ' = ?', params: [toParam(value)] };
552
+ }
553
+ if (op === 'ne') {
554
+ if (value === null)
555
+ return { sql: c + ' IS NOT NULL', params: [] };
556
+ return { sql: c + ' <> ?', params: [toParam(value)] };
557
+ }
558
+ const sym = op === 'gt' ? '>' : op === 'gte' ? '>=' : op === 'lt' ? '<' : '<=';
559
+ return { sql: c + ' ' + sym + ' ?', params: [toParam(value)] };
560
+ }
561
+ function buildWhere(node) {
562
+ const rec = node;
563
+ if (rec['and'] !== undefined || rec['or'] !== undefined) {
564
+ const key = rec['and'] !== undefined ? 'and' : 'or';
565
+ const joiner = key === 'and' ? ' AND ' : ' OR ';
566
+ const parts = [];
567
+ const params = [];
568
+ for (const sub of rec[key]) {
569
+ const b = buildWhere(sub);
570
+ parts.push('(' + b.sql + ')');
571
+ params.push(...b.params);
572
+ }
573
+ return { sql: parts.join(joiner), params };
574
+ }
575
+ const leaf = node;
576
+ return buildLeaf(leaf.field, leaf.op, leaf.value);
577
+ }
578
+ /**
579
+ * 跑一批查询单:请求级非法(不是对象/`queries` 不是非空数组)即抛;
580
+ * 单项的表/字段/形状/执行问题收进那一项的 `{ ok: false, error }`,其余项照常。
581
+ * 返回与请求同序对应的 `results`(调用方按此装 envelope)。
582
+ */
583
+ export function executeDataQueries(db, allowTables, request) {
584
+ if (!isRecord(request))
585
+ fail('DATA_QUERY_INVALID', '数据查询请求须为对象(含 queries 数组)');
586
+ const queries = request['queries'];
587
+ if (!Array.isArray(queries) || queries.length === 0) {
588
+ fail('DATA_QUERY_INVALID', '数据查询请求须含非空 queries 数组');
589
+ }
590
+ const schemas = readDataSchema(db, allowTables);
591
+ const colMap = new Map(schemas.map((s) => [s.table, s.fields]));
592
+ const out = [];
593
+ for (const raw of queries) {
594
+ if (!isRecord(raw)) {
595
+ out.push({
596
+ ok: false,
597
+ query: raw,
598
+ error: { code: 'DATA_QUERY_INVALID', message: '查询单须为对象' },
599
+ });
600
+ continue;
601
+ }
602
+ const rec = raw;
603
+ try {
604
+ const id = readId(rec);
605
+ const from = readFrom(rec, allowTables);
606
+ assertNoUnsupported(rec, from);
607
+ const cols = colMap.get(from) ?? [];
608
+ const colByName = new Map(cols.map((c) => [c.name, c.type]));
609
+ const colOf = (t) => {
610
+ if (t === from)
611
+ return colByName;
612
+ const list = colMap.get(t) ?? [];
613
+ return new Map(list.map((c) => [c.name, c.type]));
614
+ };
615
+ const parsedJoin = readJoin(rec, from, allowTables, colOf);
616
+ const hasGroupBy = rec['groupBy'] !== undefined;
617
+ const hasAgg = rec['agg'] !== undefined;
618
+ if (hasGroupBy || hasAgg) {
619
+ if (!hasGroupBy || !hasAgg) {
620
+ fail('DATA_QUERY_INVALID', from + ' 的 groupBy 与 agg 须成对出现(分组聚合见 #957)');
621
+ }
622
+ if (parsedJoin !== null) {
623
+ fail('DATA_QUERY_INVALID', from + ' 的聚合暂不支持连接组合(join+agg 另票,本票只做单表分组聚合)');
624
+ }
625
+ if (rec['select'] !== undefined) {
626
+ fail('DATA_QUERY_INVALID', from + ' 的 select 与 agg 不可同存(聚合输出列由 groupBy+agg.as 决定)');
627
+ }
628
+ const groupBy = readGroupBy(rec, from, colByName);
629
+ const agg = readAgg(rec, from, colByName);
630
+ if (rec['where'] !== undefined)
631
+ checkWhere(rec['where'], from, colByName);
632
+ const order = checkOrderByAgg(rec, from, groupBy, agg, colByName);
633
+ const page = readPage(rec['page'], from);
634
+ const fingerprint = fingerprintQuery(rec);
635
+ let offset = 0;
636
+ if (page.next !== null) {
637
+ offset = decodeAggToken(page.next, fingerprint);
638
+ }
639
+ const selectList = [...groupBy.map((g) => ident(g)), ...agg.map((a) => aggExpr(a) + ' AS ' + ident(a.as))].join(', ');
640
+ let whereSql = '';
641
+ let whereParams = [];
642
+ if (rec['where'] !== undefined) {
643
+ const w = buildWhere(rec['where']);
644
+ whereSql = ' WHERE ' + w.sql;
645
+ whereParams = w.params;
646
+ }
647
+ const groupSql = ' GROUP BY ' + groupBy.map((g) => ident(g)).join(', ');
648
+ const orderedFields = new Set(order.map((o) => o.field));
649
+ const tiebreakers = groupBy.filter((g) => !orderedFields.has(g)).map((g) => ident(g) + ' ASC');
650
+ const orderTerms = [
651
+ ...order.map((o) => ident(o.field) + ' ' + o.dir),
652
+ ...tiebreakers,
653
+ ];
654
+ const orderSql = orderTerms.length > 0 ? ' ORDER BY ' + orderTerms.join(', ') : '';
655
+ const countSql = 'SELECT COUNT(*) AS cnt FROM (SELECT 1 FROM ' + ident(from) + whereSql + groupSql + ')';
656
+ const total = readCount(db, countSql, whereParams, from);
657
+ const fetchSql = 'SELECT ' +
658
+ selectList +
659
+ ' FROM ' +
660
+ ident(from) +
661
+ whereSql +
662
+ groupSql +
663
+ orderSql +
664
+ ' LIMIT ' +
665
+ (page.size + 1) +
666
+ ' OFFSET ' +
667
+ offset;
668
+ const rows = db.prepare(fetchSql).all(...whereParams);
669
+ const fields = [
670
+ ...groupBy.map((g) => ({ name: g, type: colByName.get(g) })),
671
+ ...agg.map((a) => ({ name: a.as, type: a.type })),
672
+ ];
673
+ const projected = [];
674
+ const limit = rows.length <= page.size ? rows.length : page.size;
675
+ for (let i = 0; i < limit; i += 1) {
676
+ const row = rows[i];
677
+ if (!isRecord(row))
678
+ fail('DATA_INTERNAL', from + ' 的行不是对象');
679
+ const obj = {};
680
+ for (const f of fields)
681
+ obj[f.name] = row[f.name] ?? null;
682
+ projected.push(obj);
683
+ }
684
+ const base = {
685
+ ok: true,
686
+ query: raw,
687
+ fields: fields.map((f) => ({ name: f.name, type: f.type })),
688
+ rows: projected,
689
+ total,
690
+ };
691
+ if (rows.length <= page.size) {
692
+ out.push(id === undefined ? base : { id, ...base });
693
+ }
694
+ else {
695
+ const next = encodeAggToken(fingerprint, offset + page.size);
696
+ out.push(id === undefined ? { ...base, next } : { id, ...base, next });
697
+ }
698
+ continue;
699
+ }
700
+ if (parsedJoin === null) {
701
+ const fields = readSelect(rec, from, cols);
702
+ if (rec['where'] !== undefined)
703
+ checkWhere(rec['where'], from, colByName);
704
+ const order = checkOrderBy(rec, from, colByName);
705
+ const page = readPage(rec['page'], from);
706
+ const fingerprint = fingerprintQuery(rec);
707
+ let whereSql = '';
708
+ let whereParams = [];
709
+ if (rec['where'] !== undefined) {
710
+ const w = buildWhere(rec['where']);
711
+ whereSql = ' WHERE ' + w.sql;
712
+ whereParams = w.params;
713
+ }
714
+ const total = readCount(db, 'SELECT COUNT(*) AS cnt FROM ' + ident(from) + whereSql, whereParams, from);
715
+ const hiddenSelects = [
716
+ ...order.map((o, i) => ident(o.field) + ' AS ' + ident(orderHiddenAlias(i))),
717
+ 'rowid AS ' + ident(SINGLE_ROWID_ALIAS),
718
+ ];
719
+ const orderSql = ' ORDER BY ' +
720
+ [...order.map((o) => ident(o.field) + ' ' + o.dir), 'rowid ASC'].join(', ');
721
+ let cursorSql = '';
722
+ let cursorParams = [];
723
+ let orderingValues = [];
724
+ let rowids = [];
725
+ if (page.next !== null) {
726
+ const decoded = decodeRowToken(page.next, fingerprint, order.length, 1);
727
+ if (typeof decoded.rowids[0] !== 'number' || !Number.isInteger(decoded.rowids[0])) {
728
+ fail('DATA_QUERY_INVALID', '续取凭据无效:行号须为整数');
729
+ }
730
+ orderingValues = decoded.orderingValues;
731
+ rowids = decoded.rowids;
732
+ const cursor = buildCursorCondition(order.map((o, i) => ({ table: null, column: o.field, dir: o.dir, lastValue: orderingValues[i] })), [{ table: null, lastRowid: rowids[0] }]);
733
+ cursorSql = cursor.sql;
734
+ cursorParams = cursor.params;
735
+ }
736
+ const fetchWhere = whereSql + (cursorSql === '' ? '' : (whereSql === '' ? ' WHERE ' : ' AND ') + '(' + cursorSql + ')');
737
+ const fetchSql = 'SELECT ' +
738
+ [...fields.map((f) => ident(f.name)), ...hiddenSelects].join(', ') +
739
+ ' FROM ' +
740
+ ident(from) +
741
+ fetchWhere +
742
+ orderSql +
743
+ ' LIMIT ' +
744
+ (page.size + 1);
745
+ const rows = db.prepare(fetchSql).all(...whereParams, ...cursorParams);
746
+ const projected = [];
747
+ const limit = rows.length <= page.size ? rows.length : page.size;
748
+ for (let i = 0; i < limit; i += 1) {
749
+ const row = rows[i];
750
+ if (!isRecord(row))
751
+ fail('DATA_INTERNAL', from + ' 的行不是对象');
752
+ const obj = {};
753
+ for (const f of fields)
754
+ obj[f.name] = row[f.name] ?? null;
755
+ projected.push(obj);
756
+ }
757
+ const base = {
758
+ ok: true,
759
+ query: raw,
760
+ fields: fields.map((f) => ({ name: f.name, type: f.type })),
761
+ rows: projected,
762
+ total,
763
+ };
764
+ if (rows.length <= page.size) {
765
+ out.push(id === undefined ? base : { id, ...base });
766
+ }
767
+ else {
768
+ const last = rows[page.size - 1];
769
+ const nextOrdering = order.map((_, i) => last[orderHiddenAlias(i)] ?? null);
770
+ const nextRowid = last[SINGLE_ROWID_ALIAS];
771
+ if (typeof nextRowid !== 'number' || !Number.isInteger(nextRowid)) {
772
+ fail('DATA_INTERNAL', from + ' 的游标行号非法');
773
+ }
774
+ const next = encodeRowToken(fingerprint, nextOrdering, [nextRowid]);
775
+ out.push(id === undefined ? { ...base, next } : { id, ...base, next });
776
+ }
777
+ continue;
778
+ }
779
+ const jt = parsedJoin.joinTable;
780
+ const fromCols = colMap.get(from) ?? [];
781
+ const ownersOf = (column) => [from, jt].filter((t) => colOf(t).has(column));
782
+ const fields = readSelectScoped(rec, from, jt, fromCols, colOf, ownersOf);
783
+ if (rec['where'] !== undefined)
784
+ checkWhereScoped(rec['where'], from, jt, colOf, ownersOf);
785
+ const order = checkOrderByScoped(rec, from, jt, colOf, ownersOf);
786
+ const page = readPage(rec['page'], from);
787
+ const fingerprint = fingerprintQuery(rec);
788
+ const joinOn = parsedJoin.pairs
789
+ .map((p) => ident(p.leftTable) + '.' + ident(p.leftCol) + ' = ' + ident(p.rightTable) + '.' + ident(p.rightCol))
790
+ .join(' AND ');
791
+ const fromSql = ident(from) + ' ' + parsedJoin.joinKind + ' JOIN ' + ident(jt) + ' ON ' + joinOn;
792
+ let whereSql = '';
793
+ let whereParams = [];
794
+ if (rec['where'] !== undefined) {
795
+ const w = buildWhereScoped(rec['where'], from, jt, colOf, ownersOf);
796
+ whereSql = ' WHERE ' + w.sql;
797
+ whereParams = w.params;
798
+ }
799
+ const total = readCount(db, 'SELECT COUNT(*) AS cnt FROM ' + fromSql + whereSql, whereParams, from);
800
+ const hiddenSelects = [
801
+ ...order.map((o, i) => ident(o.table) + '.' + ident(o.column) + ' AS ' + ident(orderHiddenAlias(i))),
802
+ ident(from) + '.rowid AS ' + ident(FROM_ROWID_ALIAS),
803
+ ident(jt) + '.rowid AS ' + ident(JOIN_ROWID_ALIAS),
804
+ ];
805
+ const orderSql = ' ORDER BY ' +
806
+ [
807
+ ...order.map((o) => ident(o.table) + '.' + ident(o.column) + ' ' + o.dir),
808
+ ident(from) + '.rowid ASC',
809
+ ident(jt) + '.rowid ASC',
810
+ ].join(', ');
811
+ let cursorSql = '';
812
+ let cursorParams = [];
813
+ if (page.next !== null) {
814
+ const decoded = decodeRowToken(page.next, fingerprint, order.length, 2);
815
+ if (typeof decoded.rowids[0] !== 'number' || !Number.isInteger(decoded.rowids[0])) {
816
+ fail('DATA_QUERY_INVALID', '续取凭据无效:行号须为整数');
817
+ }
818
+ const cursor = buildCursorCondition(order.map((o, i) => ({
819
+ table: o.table,
820
+ column: o.column,
821
+ dir: o.dir,
822
+ lastValue: decoded.orderingValues[i],
823
+ })), [
824
+ { table: from, lastRowid: decoded.rowids[0] },
825
+ { table: jt, lastRowid: decoded.rowids[1] },
826
+ ]);
827
+ cursorSql = cursor.sql;
828
+ cursorParams = cursor.params;
829
+ }
830
+ const fetchWhere = whereSql + (cursorSql === '' ? '' : (whereSql === '' ? ' WHERE ' : ' AND ') + '(' + cursorSql + ')');
831
+ const fetchSql = 'SELECT ' +
832
+ [
833
+ ...fields.map((f) => ident(f.table) + '.' + ident(f.column) + ' AS ' + ident(f.name)),
834
+ ...hiddenSelects,
835
+ ].join(', ') +
836
+ ' FROM ' +
837
+ fromSql +
838
+ fetchWhere +
839
+ orderSql +
840
+ ' LIMIT ' +
841
+ (page.size + 1);
842
+ const rows = db.prepare(fetchSql).all(...whereParams, ...cursorParams);
843
+ const projected = [];
844
+ const limit = rows.length <= page.size ? rows.length : page.size;
845
+ for (let i = 0; i < limit; i += 1) {
846
+ const row = rows[i];
847
+ if (!isRecord(row))
848
+ fail('DATA_INTERNAL', from + ' 的行不是对象');
849
+ const obj = {};
850
+ for (const f of fields)
851
+ obj[f.name] = row[f.name] ?? null;
852
+ projected.push(obj);
853
+ }
854
+ const base = {
855
+ ok: true,
856
+ query: raw,
857
+ fields: fields.map((f) => ({ name: f.name, type: f.type })),
858
+ rows: projected,
859
+ total,
860
+ };
861
+ if (rows.length <= page.size) {
862
+ out.push(id === undefined ? base : { id, ...base });
863
+ }
864
+ else {
865
+ const last = rows[page.size - 1];
866
+ const nextOrdering = order.map((_, i) => last[orderHiddenAlias(i)] ?? null);
867
+ const nextFrom = last[FROM_ROWID_ALIAS];
868
+ const nextJoin = last[JOIN_ROWID_ALIAS];
869
+ if (typeof nextFrom !== 'number' || !Number.isInteger(nextFrom)) {
870
+ fail('DATA_INTERNAL', from + ' 的游标行号非法');
871
+ }
872
+ if (nextJoin !== null && (typeof nextJoin !== 'number' || !Number.isInteger(nextJoin))) {
873
+ fail('DATA_INTERNAL', from + ' 的游标行号非法');
874
+ }
875
+ const next = encodeRowToken(fingerprint, nextOrdering, [
876
+ nextFrom,
877
+ nextJoin,
878
+ ]);
879
+ out.push(id === undefined ? { ...base, next } : { id, ...base, next });
880
+ }
881
+ }
882
+ catch (e) {
883
+ const code = e instanceof LinkCoreError ? e.code : 'DATA_INTERNAL';
884
+ const message = e instanceof Error ? e.message : String(e);
885
+ const id = typeof rec['id'] === 'string' ? rec['id'] : undefined;
886
+ const item = {
887
+ ok: false,
888
+ query: raw,
889
+ error: { code, message },
890
+ };
891
+ out.push(id === undefined ? item : { id, ...item });
892
+ }
893
+ }
894
+ return out;
895
+ }
896
+ //# sourceMappingURL=data-query.js.map