@rex0220/kintone-sql-tools 1.2.0 → 1.4.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 +14 -2
- package/dist-cli/ksql.js +1617 -516
- package/dist-mcp/ksql-mcp.js +1315 -243
- package/dist-mcpb/ksql-mcp.mcpb +0 -0
- package/package.json +2 -2
package/dist-cli/ksql.js
CHANGED
|
@@ -22,6 +22,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
24
|
HELP_TEXT: () => HELP_TEXT,
|
|
25
|
+
buildBatchDmlConfirmMessage: () => buildBatchDmlConfirmMessage,
|
|
26
|
+
buildBatchStatementSummary: () => buildBatchStatementSummary,
|
|
25
27
|
buildOutput: () => buildOutput,
|
|
26
28
|
extractAppIds: () => extractAppIds,
|
|
27
29
|
normalizeAppKey: () => normalizeAppKey,
|
|
@@ -134,11 +136,12 @@ var KEYWORDS = /* @__PURE__ */ new Map([
|
|
|
134
136
|
|
|
135
137
|
// src/lexer/lexer.ts
|
|
136
138
|
var LexError = class extends Error {
|
|
137
|
-
constructor(message, pos, input) {
|
|
139
|
+
constructor(message, pos, input, unterminated = false) {
|
|
138
140
|
const around = input.slice(Math.max(0, pos - 10), pos + 10);
|
|
139
141
|
super(`${message}\uFF08\u4F4D\u7F6E ${pos}\u3001\u524D\u5F8C: \u300C${around}\u300D\uFF09`);
|
|
140
142
|
this.pos = pos;
|
|
141
143
|
this.input = input;
|
|
144
|
+
this.unterminated = unterminated;
|
|
142
145
|
this.name = "LexError";
|
|
143
146
|
}
|
|
144
147
|
};
|
|
@@ -175,6 +178,7 @@ var Lexer = class {
|
|
|
175
178
|
const opTok = this.tryReadOperator(start);
|
|
176
179
|
if (opTok) return opTok;
|
|
177
180
|
if (isIdentStart(ch)) return this.readIdentOrKeyword(start);
|
|
181
|
+
if (ch === "#") return this.readHashIdent(start);
|
|
178
182
|
throw new LexError(
|
|
179
183
|
`\u4E88\u671F\u3057\u306A\u3044\u6587\u5B57 \u300C${ch}\u300D \u3067\u3059`,
|
|
180
184
|
this.pos,
|
|
@@ -203,7 +207,7 @@ var Lexer = class {
|
|
|
203
207
|
this.pos++;
|
|
204
208
|
}
|
|
205
209
|
}
|
|
206
|
-
throw new LexError("\u6587\u5B57\u5217\u30EA\u30C6\u30E9\u30EB\u304C\u9589\u3058\u3089\u308C\u3066\u3044\u307E\u305B\u3093", start, this.input);
|
|
210
|
+
throw new LexError("\u6587\u5B57\u5217\u30EA\u30C6\u30E9\u30EB\u304C\u9589\u3058\u3089\u308C\u3066\u3044\u307E\u305B\u3093", start, this.input, true);
|
|
207
211
|
}
|
|
208
212
|
// ----------------------------------------------------------
|
|
209
213
|
// バッククォート識別子: `field name`
|
|
@@ -223,7 +227,8 @@ var Lexer = class {
|
|
|
223
227
|
throw new LexError(
|
|
224
228
|
"\u30D0\u30C3\u30AF\u30AF\u30A9\u30FC\u30C8\u8B58\u5225\u5B50\u304C\u9589\u3058\u3089\u308C\u3066\u3044\u307E\u305B\u3093",
|
|
225
229
|
start,
|
|
226
|
-
this.input
|
|
230
|
+
this.input,
|
|
231
|
+
true
|
|
227
232
|
);
|
|
228
233
|
}
|
|
229
234
|
// ----------------------------------------------------------
|
|
@@ -330,6 +335,30 @@ var Lexer = class {
|
|
|
330
335
|
return this.makeToken(kind, value, start);
|
|
331
336
|
}
|
|
332
337
|
// ----------------------------------------------------------
|
|
338
|
+
// 一時テーブル識別子: #temp
|
|
339
|
+
// # は先頭のみ有効。isIdentStart に # を加えると isIdentContinue 経由で
|
|
340
|
+
// 識別子の途中(APP#x 等)にも許容されてしまうため、専用分岐で読む。
|
|
341
|
+
// ----------------------------------------------------------
|
|
342
|
+
readHashIdent(start) {
|
|
343
|
+
this.pos++;
|
|
344
|
+
const next = this.input[this.pos] ?? "";
|
|
345
|
+
if (!isIdentStart(next)) {
|
|
346
|
+
throw new LexError("\u300C#\u300D \u306E\u76F4\u5F8C\u306B\u306F\u8B58\u5225\u5B50\u304C\u5FC5\u8981\u3067\u3059", start, this.input);
|
|
347
|
+
}
|
|
348
|
+
while (this.pos < this.input.length && isIdentContinue(this.input[this.pos])) {
|
|
349
|
+
this.pos++;
|
|
350
|
+
}
|
|
351
|
+
const value = this.input.slice(start, this.pos);
|
|
352
|
+
if (this.input[this.pos] === "@") {
|
|
353
|
+
throw new LexError(
|
|
354
|
+
`@profile is not allowed on temp table ${value}.`,
|
|
355
|
+
this.pos,
|
|
356
|
+
this.input
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return this.makeToken("IDENT" /* IDENT */, value, start);
|
|
360
|
+
}
|
|
361
|
+
// ----------------------------------------------------------
|
|
333
362
|
// 空白・コメントをスキップ
|
|
334
363
|
// ----------------------------------------------------------
|
|
335
364
|
skipWhitespaceAndComments() {
|
|
@@ -346,14 +375,25 @@ var Lexer = class {
|
|
|
346
375
|
continue;
|
|
347
376
|
}
|
|
348
377
|
if (ch === "/" && this.input[this.pos + 1] === "*") {
|
|
378
|
+
const commentStart = this.pos;
|
|
349
379
|
this.pos += 2;
|
|
380
|
+
let closed = false;
|
|
350
381
|
while (this.pos < this.input.length) {
|
|
351
382
|
if (this.input[this.pos] === "*" && this.input[this.pos + 1] === "/") {
|
|
352
383
|
this.pos += 2;
|
|
384
|
+
closed = true;
|
|
353
385
|
break;
|
|
354
386
|
}
|
|
355
387
|
this.pos++;
|
|
356
388
|
}
|
|
389
|
+
if (!closed) {
|
|
390
|
+
throw new LexError(
|
|
391
|
+
"\u30D6\u30ED\u30C3\u30AF\u30B3\u30E1\u30F3\u30C8\u304C\u9589\u3058\u3089\u308C\u3066\u3044\u307E\u305B\u3093",
|
|
392
|
+
commentStart,
|
|
393
|
+
this.input,
|
|
394
|
+
true
|
|
395
|
+
);
|
|
396
|
+
}
|
|
357
397
|
continue;
|
|
358
398
|
}
|
|
359
399
|
break;
|
|
@@ -387,6 +427,7 @@ function isJapanese(cp) {
|
|
|
387
427
|
}
|
|
388
428
|
|
|
389
429
|
// src/parser/parser.ts
|
|
430
|
+
var MAX_BATCH_STATEMENTS = 20;
|
|
390
431
|
var ParseError = class extends Error {
|
|
391
432
|
constructor(message, token) {
|
|
392
433
|
super(`${message}\uFF08\u4F4D\u7F6E ${token.pos}\u3001\u30C8\u30FC\u30AF\u30F3: \u300C${token.value}\u300D\uFF09`);
|
|
@@ -400,15 +441,54 @@ var Parser = class {
|
|
|
400
441
|
this.pos = 0;
|
|
401
442
|
/** WITH 句で定義された CTE 名のセット(parseTableRef で参照) */
|
|
402
443
|
this.cteNames = /* @__PURE__ */ new Set();
|
|
444
|
+
/** パース中に出現した一時テーブル参照(#name)のトークン。単文 API での拒否に使う */
|
|
445
|
+
this.tempTableRefs = [];
|
|
403
446
|
}
|
|
404
447
|
// ----------------------------------------------------------
|
|
405
448
|
// 公開 API
|
|
406
449
|
// ----------------------------------------------------------
|
|
450
|
+
/** 単文をパースする(従来 API。複文が渡されたらエラー) */
|
|
407
451
|
parse() {
|
|
408
|
-
const
|
|
409
|
-
if (
|
|
452
|
+
const stmts = this.parseStatements();
|
|
453
|
+
if (stmts.length === 0) {
|
|
454
|
+
throw new ParseError("SQL \u6587\u304C\u3042\u308A\u307E\u305B\u3093", this.peek());
|
|
455
|
+
}
|
|
456
|
+
if (stmts.length > 1) {
|
|
457
|
+
throw new ParseError(
|
|
458
|
+
"\u3053\u306E API \u306F\u5358\u6587\u306E\u307F\u53D7\u3051\u4ED8\u3051\u307E\u3059\uFF08\u8907\u6587\u306F\u30D0\u30C3\u30C1\u5B9F\u884C API \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\uFF09",
|
|
459
|
+
this.peek()
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (this.tempTableRefs.length > 0) {
|
|
463
|
+
const tok = this.tempTableRefs[0];
|
|
464
|
+
throw new ParseError(
|
|
465
|
+
`temp table ${tok.value} is not defined in this batch.`,
|
|
466
|
+
tok
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return stmts[0];
|
|
470
|
+
}
|
|
471
|
+
/** 複文(`;` 区切り)をパースする。空文はスキップする */
|
|
472
|
+
parseStatements() {
|
|
473
|
+
const stmts = [];
|
|
474
|
+
while (true) {
|
|
475
|
+
while (this.peek().kind === ";" /* SEMICOLON */) this.advance();
|
|
476
|
+
if (this.peek().kind === "EOF" /* EOF */) break;
|
|
477
|
+
const startTok = this.peek();
|
|
478
|
+
stmts.push(this.parseStatement());
|
|
479
|
+
if (stmts.length > MAX_BATCH_STATEMENTS) {
|
|
480
|
+
throw new ParseError(
|
|
481
|
+
`batch exceeds ${MAX_BATCH_STATEMENTS} statements.`,
|
|
482
|
+
startTok
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
const after = this.peek();
|
|
486
|
+
if (after.kind !== ";" /* SEMICOLON */ && after.kind !== "EOF" /* EOF */) {
|
|
487
|
+
throw new ParseError("\u6587\u306E\u533A\u5207\u308A\u306B\u306F ; \u304C\u5FC5\u8981\u3067\u3059", after);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
410
490
|
this.expect("EOF" /* EOF */);
|
|
411
|
-
return
|
|
491
|
+
return stmts;
|
|
412
492
|
}
|
|
413
493
|
// ----------------------------------------------------------
|
|
414
494
|
// Statement ディスパッチ
|
|
@@ -437,12 +517,63 @@ var Parser = class {
|
|
|
437
517
|
return this.parseDescribe();
|
|
438
518
|
case "EXPLAIN" /* EXPLAIN */:
|
|
439
519
|
return this.parseExplain();
|
|
520
|
+
case "IDENT" /* IDENT */: {
|
|
521
|
+
const upper = tok.value.toUpperCase();
|
|
522
|
+
if (upper === "CREATE") return this.parseCreateTempTable();
|
|
523
|
+
if (upper === "DROP") return this.parseDropTempTable();
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
440
526
|
default:
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
throw new ParseError(
|
|
530
|
+
"SELECT / INSERT / UPDATE / DELETE / REORDER / WITH / SHOW / DESCRIBE / EXPLAIN / CREATE TEMP TABLE / DROP TEMP TABLE \u306E\u3044\u305A\u308C\u304B\u3067\u59CB\u307E\u308B SQL \u6587\u304C\u5FC5\u8981\u3067\u3059",
|
|
531
|
+
tok
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
// ----------------------------------------------------------
|
|
535
|
+
// CREATE TEMP TABLE / DROP TEMP TABLE(バッチ内一時テーブル)
|
|
536
|
+
// CREATE / DROP / TEMP / TABLE は予約語にしない(ソフトキーワード)
|
|
537
|
+
// ----------------------------------------------------------
|
|
538
|
+
parseCreateTempTable() {
|
|
539
|
+
this.advance();
|
|
540
|
+
this.expectSoftKeyword("TEMP", "CREATE \u306E\u5F8C\u306B\u306F TEMP TABLE \u304C\u5FC5\u8981\u3067\u3059\uFF08\u4F8B: CREATE TEMP TABLE #temp AS SELECT ...\uFF09");
|
|
541
|
+
this.expectSoftKeyword("TABLE", "CREATE TEMP \u306E\u5F8C\u306B\u306F TABLE \u304C\u5FC5\u8981\u3067\u3059");
|
|
542
|
+
const name = this.parseTempTableName();
|
|
543
|
+
this.expect("AS" /* AS */, "CREATE TEMP TABLE \u306B\u306F AS SELECT \u304C\u5FC5\u8981\u3067\u3059");
|
|
544
|
+
const tok = this.peek();
|
|
545
|
+
let query;
|
|
546
|
+
if (tok.kind === "WITH" /* WITH */) {
|
|
547
|
+
query = this.parseWith();
|
|
548
|
+
} else if (tok.kind === "SELECT" /* SELECT */) {
|
|
549
|
+
query = this.tryParseUnionChain(this.parseSelect());
|
|
550
|
+
} else {
|
|
551
|
+
throw new ParseError("CREATE TEMP TABLE ... AS \u306E\u5F8C\u306B\u306F SELECT / WITH \u304C\u5FC5\u8981\u3067\u3059", tok);
|
|
445
552
|
}
|
|
553
|
+
return { type: "CREATE_TEMP_TABLE", name, query };
|
|
554
|
+
}
|
|
555
|
+
parseDropTempTable() {
|
|
556
|
+
this.advance();
|
|
557
|
+
this.expectSoftKeyword("TEMP", "DROP \u306E\u5F8C\u306B\u306F TEMP TABLE \u304C\u5FC5\u8981\u3067\u3059\uFF08\u4F8B: DROP TEMP TABLE #temp\uFF09");
|
|
558
|
+
this.expectSoftKeyword("TABLE", "DROP TEMP \u306E\u5F8C\u306B\u306F TABLE \u304C\u5FC5\u8981\u3067\u3059");
|
|
559
|
+
const name = this.parseTempTableName();
|
|
560
|
+
return { type: "DROP_TEMP_TABLE", name };
|
|
561
|
+
}
|
|
562
|
+
expectSoftKeyword(word, msg) {
|
|
563
|
+
const tok = this.peek();
|
|
564
|
+
if (tok.kind === "IDENT" /* IDENT */ && tok.value.toUpperCase() === word) {
|
|
565
|
+
this.advance();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
throw new ParseError(msg, tok);
|
|
569
|
+
}
|
|
570
|
+
parseTempTableName() {
|
|
571
|
+
const tok = this.peek();
|
|
572
|
+
if (tok.kind === "IDENT" /* IDENT */ && tok.value.startsWith("#")) {
|
|
573
|
+
this.advance();
|
|
574
|
+
return tok.value;
|
|
575
|
+
}
|
|
576
|
+
throw new ParseError("\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u540D\u306F # \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\uFF08\u4F8B: #temp\uFF09", tok);
|
|
446
577
|
}
|
|
447
578
|
parseShow() {
|
|
448
579
|
this.advance();
|
|
@@ -999,24 +1130,38 @@ var Parser = class {
|
|
|
999
1130
|
// FROM / JOIN
|
|
1000
1131
|
// ----------------------------------------------------------
|
|
1001
1132
|
parseTableRef() {
|
|
1002
|
-
const
|
|
1133
|
+
const nameTok = this.peek();
|
|
1134
|
+
const name = this.parseTableName();
|
|
1135
|
+
if (nameTok.kind === "IDENT" /* IDENT */ && name.startsWith("#")) {
|
|
1136
|
+
this.tempTableRefs.push(this.prev());
|
|
1137
|
+
const alias2 = this.consume("AS" /* AS */) ? this.parseTableAliasName() : this.tryParseImplicitAlias();
|
|
1138
|
+
return { appId: 0, alias: alias2, cteName: name };
|
|
1139
|
+
}
|
|
1003
1140
|
if (this.cteNames.has(name)) {
|
|
1004
|
-
const alias2 = this.consume("AS" /* AS */) ? this.
|
|
1141
|
+
const alias2 = this.consume("AS" /* AS */) ? this.parseTableAliasName() : this.tryParseImplicitAlias();
|
|
1005
1142
|
return { appId: 0, alias: alias2, cteName: name };
|
|
1006
1143
|
}
|
|
1007
1144
|
const { appId, subtableCode } = extractTableRef(name, this.prev());
|
|
1008
1145
|
if (subtableCode) {
|
|
1009
|
-
const alias2 = this.consume("AS" /* AS */) ? this.
|
|
1146
|
+
const alias2 = this.consume("AS" /* AS */) ? this.parseTableAliasName() : this.tryParseImplicitAlias();
|
|
1010
1147
|
return { appId, alias: alias2, cteName: null, subtableCode };
|
|
1011
1148
|
}
|
|
1012
1149
|
const implicit = this.tryParseImplicitAlias();
|
|
1013
|
-
const alias = this.consume("AS" /* AS */) ? this.
|
|
1150
|
+
const alias = this.consume("AS" /* AS */) ? this.parseTableAliasName() : implicit ?? name;
|
|
1014
1151
|
return { appId, alias, cteName: null };
|
|
1015
1152
|
}
|
|
1153
|
+
// テーブル alias 名を読む(IDENT / BIDENT)。alias 位置の # は BIDENT でも拒否
|
|
1154
|
+
parseTableAliasName() {
|
|
1155
|
+
const tok = this.peek();
|
|
1156
|
+
if ((tok.kind === "IDENT" /* IDENT */ || tok.kind === "BIDENT" /* BIDENT */) && tok.value.startsWith("#")) {
|
|
1157
|
+
throw new ParseError("\u30A8\u30A4\u30EA\u30A2\u30B9\u540D\u306B # \u3067\u59CB\u307E\u308B\u540D\u524D\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
|
|
1158
|
+
}
|
|
1159
|
+
return this.parseIdentifier();
|
|
1160
|
+
}
|
|
1016
1161
|
tryParseImplicitAlias() {
|
|
1017
1162
|
const k = this.peek().kind;
|
|
1018
1163
|
if (k === "IDENT" /* IDENT */ || k === "BIDENT" /* BIDENT */) {
|
|
1019
|
-
return this.
|
|
1164
|
+
return this.parseTableAliasName();
|
|
1020
1165
|
}
|
|
1021
1166
|
return null;
|
|
1022
1167
|
}
|
|
@@ -1421,6 +1566,7 @@ var Parser = class {
|
|
|
1421
1566
|
parseInsert() {
|
|
1422
1567
|
this.expect("INSERT" /* INSERT */);
|
|
1423
1568
|
this.expect("INTO" /* INTO */);
|
|
1569
|
+
this.rejectTempTableDml();
|
|
1424
1570
|
const name = this.parseIdentifier();
|
|
1425
1571
|
const { appId, subtableCode } = extractTableRef(name, this.prev());
|
|
1426
1572
|
this.expect("(" /* LPAREN */);
|
|
@@ -1446,6 +1592,7 @@ var Parser = class {
|
|
|
1446
1592
|
parseUpsert() {
|
|
1447
1593
|
this.expect("UPSERT" /* UPSERT */);
|
|
1448
1594
|
this.expect("INTO" /* INTO */);
|
|
1595
|
+
this.rejectTempTableDml();
|
|
1449
1596
|
const name = this.parseIdentifier();
|
|
1450
1597
|
const { appId, subtableCode } = extractTableRef(name, this.prev());
|
|
1451
1598
|
if (subtableCode) {
|
|
@@ -1530,6 +1677,7 @@ var Parser = class {
|
|
|
1530
1677
|
// ----------------------------------------------------------
|
|
1531
1678
|
parseUpdate() {
|
|
1532
1679
|
this.expect("UPDATE" /* UPDATE */);
|
|
1680
|
+
this.rejectTempTableDml();
|
|
1533
1681
|
const name = this.parseIdentifier();
|
|
1534
1682
|
const { appId, subtableCode } = extractTableRef(name, this.prev());
|
|
1535
1683
|
this.expect("SET" /* SET */);
|
|
@@ -1605,6 +1753,7 @@ var Parser = class {
|
|
|
1605
1753
|
parseDelete() {
|
|
1606
1754
|
this.expect("DELETE" /* DELETE */);
|
|
1607
1755
|
this.expect("FROM" /* FROM */);
|
|
1756
|
+
this.rejectTempTableDml();
|
|
1608
1757
|
const name = this.parseIdentifier();
|
|
1609
1758
|
const { appId, subtableCode } = extractTableRef(name, this.prev());
|
|
1610
1759
|
const whereTok = this.peek();
|
|
@@ -1623,6 +1772,7 @@ var Parser = class {
|
|
|
1623
1772
|
parseReorder() {
|
|
1624
1773
|
this.expect("REORDER" /* REORDER */);
|
|
1625
1774
|
const all = this.consume("ALL" /* ALL */);
|
|
1775
|
+
this.rejectTempTableDml();
|
|
1626
1776
|
const name = this.parseIdentifier();
|
|
1627
1777
|
const { appId, subtableCode } = extractTableRef(name, this.prev());
|
|
1628
1778
|
if (!subtableCode) {
|
|
@@ -1704,8 +1854,29 @@ var Parser = class {
|
|
|
1704
1854
|
}
|
|
1705
1855
|
return n;
|
|
1706
1856
|
}
|
|
1707
|
-
// 識別子(IDENT / BIDENT
|
|
1857
|
+
// 識別子(IDENT / BIDENT)を読む。# 始まりの一時テーブル名は不可
|
|
1858
|
+
//(temp マーカーはレキサが生成する IDENT のみ。`#field` のような
|
|
1859
|
+
// バッククォート識別子は # で始まる通常フィールド名として許容する)
|
|
1708
1860
|
parseIdentifier() {
|
|
1861
|
+
const tok = this.peek();
|
|
1862
|
+
if (tok.kind === "IDENT" /* IDENT */ || tok.kind === "BIDENT" /* BIDENT */) {
|
|
1863
|
+
if (tok.kind === "IDENT" /* IDENT */ && tok.value.startsWith("#")) {
|
|
1864
|
+
throw new ParseError(
|
|
1865
|
+
"\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u540D\uFF08# \u3067\u59CB\u307E\u308B\u540D\u524D\uFF09\u306F FROM / JOIN / CREATE / DROP TEMP TABLE \u3067\u306E\u307F\u4F7F\u7528\u3067\u304D\u307E\u3059",
|
|
1866
|
+
tok
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
this.advance();
|
|
1870
|
+
return tok.value;
|
|
1871
|
+
}
|
|
1872
|
+
throw new ParseError(
|
|
1873
|
+
"\u30D5\u30A3\u30FC\u30EB\u30C9\u540D\u307E\u305F\u306F\u30C6\u30FC\u30D6\u30EB\u540D\u304C\u5FC5\u8981\u3067\u3059",
|
|
1874
|
+
tok
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
// テーブル名(IDENT / BIDENT)を読む。# 始まりの一時テーブル名を許容する
|
|
1878
|
+
//(一時テーブルを受理してよいのはテーブル参照位置のみ。他は parseIdentifier を使う)
|
|
1879
|
+
parseTableName() {
|
|
1709
1880
|
const tok = this.peek();
|
|
1710
1881
|
if (tok.kind === "IDENT" /* IDENT */ || tok.kind === "BIDENT" /* BIDENT */) {
|
|
1711
1882
|
this.advance();
|
|
@@ -1716,10 +1887,23 @@ var Parser = class {
|
|
|
1716
1887
|
tok
|
|
1717
1888
|
);
|
|
1718
1889
|
}
|
|
1890
|
+
// DML の対象テーブル位置に一時テーブルが指定されていたら拒否する
|
|
1891
|
+
rejectTempTableDml() {
|
|
1892
|
+
const tok = this.peek();
|
|
1893
|
+
if (tok.kind === "IDENT" /* IDENT */ && tok.value.startsWith("#")) {
|
|
1894
|
+
throw new ParseError(
|
|
1895
|
+
`DML on temp table ${tok.value} is not supported.`,
|
|
1896
|
+
tok
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1719
1900
|
// エイリアス名: IDENT / BIDENT に加え、キーワードも許容する
|
|
1720
1901
|
// 例: SELECT SUM(金額) AS avg → "avg" は AVG キーワードだが alias として有効
|
|
1721
1902
|
parseAliasName() {
|
|
1722
1903
|
const tok = this.peek();
|
|
1904
|
+
if (tok.value.startsWith("#")) {
|
|
1905
|
+
throw new ParseError("\u30A8\u30A4\u30EA\u30A2\u30B9\u540D\u306B # \u3067\u59CB\u307E\u308B\u540D\u524D\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
|
|
1906
|
+
}
|
|
1723
1907
|
if (tok.kind === "IDENT" /* IDENT */ || tok.kind === "BIDENT" /* BIDENT */ || KEYWORDS.has(tok.value.toUpperCase())) {
|
|
1724
1908
|
this.advance();
|
|
1725
1909
|
return tok.value.toLowerCase();
|
|
@@ -1776,6 +1960,171 @@ function extractTableRef(name, tok) {
|
|
|
1776
1960
|
return { appId: Number(m[1]), subtableCode: m[2] ?? null };
|
|
1777
1961
|
}
|
|
1778
1962
|
|
|
1963
|
+
// src/core/dmlGuard.ts
|
|
1964
|
+
function getStatementType(stmt) {
|
|
1965
|
+
if (!stmt || typeof stmt !== "object") return "UNKNOWN";
|
|
1966
|
+
const obj = stmt;
|
|
1967
|
+
return typeof obj.type === "string" ? obj.type : "UNKNOWN";
|
|
1968
|
+
}
|
|
1969
|
+
function isDmlType(type) {
|
|
1970
|
+
return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
|
|
1971
|
+
}
|
|
1972
|
+
function isReadOnlyType(type) {
|
|
1973
|
+
return type === "SELECT" || type === "UNION" || type === "WITH" || type === "EXPLAIN" || type === "SHOW_APPS" || type === "DESCRIBE" || type === "CREATE_TEMP_TABLE" || type === "DROP_TEMP_TABLE";
|
|
1974
|
+
}
|
|
1975
|
+
function hasWhereClause(stmt) {
|
|
1976
|
+
if (!stmt || typeof stmt !== "object") return false;
|
|
1977
|
+
const obj = stmt;
|
|
1978
|
+
return obj.where !== null && obj.where !== void 0;
|
|
1979
|
+
}
|
|
1980
|
+
function isNoFromSelectStatement(stmt) {
|
|
1981
|
+
if (!stmt || typeof stmt !== "object") return false;
|
|
1982
|
+
const obj = stmt;
|
|
1983
|
+
return obj.type === "SELECT" && obj.from?.appId === 0 && obj.from?.cteName === "__NO_FROM__";
|
|
1984
|
+
}
|
|
1985
|
+
function getInsertValuesCount(stmt) {
|
|
1986
|
+
if (!stmt || typeof stmt !== "object") return null;
|
|
1987
|
+
const obj = stmt;
|
|
1988
|
+
if (obj.type !== "INSERT") return null;
|
|
1989
|
+
return Array.isArray(obj.values) ? obj.values.length : null;
|
|
1990
|
+
}
|
|
1991
|
+
function collectDmlTargetFields(stmt) {
|
|
1992
|
+
if (!stmt || typeof stmt !== "object") return [];
|
|
1993
|
+
const obj = stmt;
|
|
1994
|
+
if (!obj.type) return [];
|
|
1995
|
+
if (obj.type === "UPDATE") {
|
|
1996
|
+
return (obj.assignments ?? []).map((a) => a.field).filter((f) => Boolean(f));
|
|
1997
|
+
}
|
|
1998
|
+
if (obj.type === "INSERT" || obj.type === "INSERT_SELECT" || obj.type === "UPSERT" || obj.type === "UPSERT_SELECT") {
|
|
1999
|
+
return [...obj.fields ?? [], ...obj.keyFields ?? []];
|
|
2000
|
+
}
|
|
2001
|
+
return [];
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// src/core/batch.ts
|
|
2005
|
+
var MAX_TEMP_TABLES = 16;
|
|
2006
|
+
var BatchAnalysisError = class extends Error {
|
|
2007
|
+
constructor(message, statementIndex) {
|
|
2008
|
+
super(message);
|
|
2009
|
+
this.statementIndex = statementIndex;
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
function collectRefs(node, tempRefs, appIds) {
|
|
2013
|
+
if (Array.isArray(node)) {
|
|
2014
|
+
for (const v of node) collectRefs(v, tempRefs, appIds);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
if (node !== null && typeof node === "object") {
|
|
2018
|
+
const obj = node;
|
|
2019
|
+
const cte = obj["cteName"];
|
|
2020
|
+
if (typeof cte === "string" && cte.startsWith("#")) tempRefs.add(cte);
|
|
2021
|
+
const appId = obj["appId"];
|
|
2022
|
+
if (typeof appId === "number" && appId > 0) appIds.add(appId);
|
|
2023
|
+
for (const v of Object.values(obj)) collectRefs(v, tempRefs, appIds);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
function analyzeBatch(statements) {
|
|
2027
|
+
if (statements.length === 0) {
|
|
2028
|
+
throw new BatchAnalysisError("ArgumentError: SQL is empty.", 0);
|
|
2029
|
+
}
|
|
2030
|
+
if (statements.length === 1) {
|
|
2031
|
+
const t = statements[0].type;
|
|
2032
|
+
if (t === "CREATE_TEMP_TABLE" || t === "DROP_TEMP_TABLE") {
|
|
2033
|
+
const verb = t === "CREATE_TEMP_TABLE" ? "CREATE TEMP TABLE" : "DROP TEMP TABLE";
|
|
2034
|
+
throw new BatchAnalysisError(
|
|
2035
|
+
`ArgumentError: ${verb} requires a batch (temp tables are batch-scoped).`,
|
|
2036
|
+
0
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
const defined = /* @__PURE__ */ new Map();
|
|
2041
|
+
const createdOrder = [];
|
|
2042
|
+
const results = [];
|
|
2043
|
+
statements.forEach((stmt, index) => {
|
|
2044
|
+
const statementType = getStatementType(stmt);
|
|
2045
|
+
const created = [];
|
|
2046
|
+
const dropped = [];
|
|
2047
|
+
const refs = /* @__PURE__ */ new Set();
|
|
2048
|
+
const stmtAppIds = /* @__PURE__ */ new Set();
|
|
2049
|
+
const dependsOn = /* @__PURE__ */ new Set();
|
|
2050
|
+
if (stmt.type === "CREATE_TEMP_TABLE") {
|
|
2051
|
+
collectRefs(stmt.query, refs, stmtAppIds);
|
|
2052
|
+
} else if (stmt.type === "DROP_TEMP_TABLE") {
|
|
2053
|
+
} else {
|
|
2054
|
+
collectRefs(stmt, refs, stmtAppIds);
|
|
2055
|
+
}
|
|
2056
|
+
let tempOnlySource = false;
|
|
2057
|
+
if (stmt.type === "INSERT_SELECT" || stmt.type === "UPSERT_SELECT") {
|
|
2058
|
+
const srcTemp = /* @__PURE__ */ new Set();
|
|
2059
|
+
const srcApps = /* @__PURE__ */ new Set();
|
|
2060
|
+
collectRefs(stmt.select, srcTemp, srcApps);
|
|
2061
|
+
tempOnlySource = srcTemp.size > 0 && srcApps.size === 0;
|
|
2062
|
+
}
|
|
2063
|
+
for (const name of refs) {
|
|
2064
|
+
const at = defined.get(name);
|
|
2065
|
+
if (at === void 0) {
|
|
2066
|
+
throw new BatchAnalysisError(
|
|
2067
|
+
`ParseError: temp table ${name} is not defined in this batch.`,
|
|
2068
|
+
index
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
dependsOn.add(at);
|
|
2072
|
+
}
|
|
2073
|
+
if (stmt.type === "CREATE_TEMP_TABLE") {
|
|
2074
|
+
if (defined.has(stmt.name)) {
|
|
2075
|
+
throw new BatchAnalysisError(
|
|
2076
|
+
`ParseError: temp table ${stmt.name} is already defined.`,
|
|
2077
|
+
index
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
defined.set(stmt.name, index);
|
|
2081
|
+
createdOrder.push(stmt.name);
|
|
2082
|
+
created.push(stmt.name);
|
|
2083
|
+
if (defined.size > MAX_TEMP_TABLES) {
|
|
2084
|
+
throw new BatchAnalysisError(
|
|
2085
|
+
`ParseError: batch exceeds ${MAX_TEMP_TABLES} temp tables.`,
|
|
2086
|
+
index
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
if (stmt.type === "DROP_TEMP_TABLE") {
|
|
2091
|
+
const at = defined.get(stmt.name);
|
|
2092
|
+
if (at === void 0) {
|
|
2093
|
+
throw new BatchAnalysisError(
|
|
2094
|
+
`ParseError: temp table ${stmt.name} is not defined in this batch.`,
|
|
2095
|
+
index
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
dependsOn.add(at);
|
|
2099
|
+
dropped.push(stmt.name);
|
|
2100
|
+
defined.delete(stmt.name);
|
|
2101
|
+
}
|
|
2102
|
+
results.push({
|
|
2103
|
+
index,
|
|
2104
|
+
statementType,
|
|
2105
|
+
isDml: isDmlType(statementType),
|
|
2106
|
+
isReadOnly: isReadOnlyType(statementType),
|
|
2107
|
+
hasWhere: hasWhereClause(stmt),
|
|
2108
|
+
insertValuesCount: getInsertValuesCount(stmt),
|
|
2109
|
+
appIds: [...stmtAppIds].sort((a, b) => a - b),
|
|
2110
|
+
tempTablesCreated: created,
|
|
2111
|
+
tempTablesReferenced: [...refs],
|
|
2112
|
+
tempTablesDropped: dropped,
|
|
2113
|
+
dependsOn: [...dependsOn].sort((a, b) => a - b),
|
|
2114
|
+
tempOnlySource,
|
|
2115
|
+
targetAppId: isDmlType(statementType) && typeof stmt.appId === "number" ? stmt.appId : null
|
|
2116
|
+
});
|
|
2117
|
+
});
|
|
2118
|
+
const containsDml = results.some((r) => r.isDml);
|
|
2119
|
+
return {
|
|
2120
|
+
statementCount: statements.length,
|
|
2121
|
+
isReadOnlyBatch: !containsDml && results.every((r) => r.isReadOnly),
|
|
2122
|
+
containsDml,
|
|
2123
|
+
tempTables: createdOrder,
|
|
2124
|
+
statements: results
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
|
|
1779
2128
|
// src/engine/pushDownNot.ts
|
|
1780
2129
|
function pushDownNot(expr) {
|
|
1781
2130
|
switch (expr.type) {
|
|
@@ -2804,23 +3153,31 @@ function resolveKintoneFunc(name) {
|
|
|
2804
3153
|
return "";
|
|
2805
3154
|
}
|
|
2806
3155
|
}
|
|
3156
|
+
var likeRegexCache = /* @__PURE__ */ new Map();
|
|
3157
|
+
var LIKE_REGEX_CACHE_MAX = 200;
|
|
2807
3158
|
function matchLike(value, pattern) {
|
|
2808
3159
|
if (!pattern.includes("%") && !pattern.includes("_")) {
|
|
2809
3160
|
return value.includes(pattern);
|
|
2810
3161
|
}
|
|
2811
|
-
let
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3162
|
+
let regex = likeRegexCache.get(pattern);
|
|
3163
|
+
if (!regex) {
|
|
3164
|
+
let regexStr = "^";
|
|
3165
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
3166
|
+
const ch = pattern[i];
|
|
3167
|
+
if (ch === "%") {
|
|
3168
|
+
regexStr += ".*";
|
|
3169
|
+
} else if (ch === "_") {
|
|
3170
|
+
regexStr += ".";
|
|
3171
|
+
} else {
|
|
3172
|
+
regexStr += ch.replace(/[.+*?^${}()|[\]\\]/g, "\\$&");
|
|
3173
|
+
}
|
|
2820
3174
|
}
|
|
3175
|
+
regexStr += "$";
|
|
3176
|
+
regex = new RegExp(regexStr, "u");
|
|
3177
|
+
if (likeRegexCache.size >= LIKE_REGEX_CACHE_MAX) likeRegexCache.clear();
|
|
3178
|
+
likeRegexCache.set(pattern, regex);
|
|
2821
3179
|
}
|
|
2822
|
-
|
|
2823
|
-
return new RegExp(regexStr, "u").test(value);
|
|
3180
|
+
return regex.test(value);
|
|
2824
3181
|
}
|
|
2825
3182
|
|
|
2826
3183
|
// src/converter/dmlToKintone.ts
|
|
@@ -3248,10 +3605,12 @@ async function fetchPage(fetcher, app, query, fields, pageSize, offset) {
|
|
|
3248
3605
|
return fetcher({ app, query: pageQuery, fields });
|
|
3249
3606
|
}
|
|
3250
3607
|
function buildCursorQuery(baseQuery, cursorId) {
|
|
3251
|
-
if (cursorId <= 0) return baseQuery.trimEnd();
|
|
3252
|
-
const cursor = `$id > ${cursorId} order by $id asc`;
|
|
3253
3608
|
const base = baseQuery.trimEnd();
|
|
3254
|
-
|
|
3609
|
+
if (cursorId <= 0) {
|
|
3610
|
+
return base ? `${base} order by $id asc` : "order by $id asc";
|
|
3611
|
+
}
|
|
3612
|
+
const cursor = `$id > ${cursorId} order by $id asc`;
|
|
3613
|
+
return base ? `(${base}) and ${cursor}` : cursor;
|
|
3255
3614
|
}
|
|
3256
3615
|
function buildPageQuery(query, pageSize, offset) {
|
|
3257
3616
|
const base = query.trimEnd();
|
|
@@ -3404,6 +3763,8 @@ function applyJoin(leftRows, rightRows, join2) {
|
|
|
3404
3763
|
else rightIndex.set(k, [rRow]);
|
|
3405
3764
|
}
|
|
3406
3765
|
const result = [];
|
|
3766
|
+
const emptyRight = {};
|
|
3767
|
+
for (const key of Object.keys(rightRows[0] ?? {})) emptyRight[key] = "";
|
|
3407
3768
|
for (const lRow of leftRows) {
|
|
3408
3769
|
const k = lRow[leftKey] ?? "";
|
|
3409
3770
|
const matched = rightIndex.get(k) ?? [];
|
|
@@ -3412,8 +3773,6 @@ function applyJoin(leftRows, rightRows, join2) {
|
|
|
3412
3773
|
result.push({ ...lRow, ...rRow });
|
|
3413
3774
|
}
|
|
3414
3775
|
} else if (joinType === "LEFT") {
|
|
3415
|
-
const emptyRight = {};
|
|
3416
|
-
for (const key of Object.keys(rightRows[0] ?? {})) emptyRight[key] = "";
|
|
3417
3776
|
result.push({ ...lRow, ...emptyRight });
|
|
3418
3777
|
}
|
|
3419
3778
|
}
|
|
@@ -3443,8 +3802,10 @@ function applyGroupBy(rows, groupByKeys, columns) {
|
|
|
3443
3802
|
}
|
|
3444
3803
|
for (const col of columns) {
|
|
3445
3804
|
if (col.type === "AGGREGATE") {
|
|
3446
|
-
const
|
|
3447
|
-
|
|
3805
|
+
const syntheticKey = aggregateSyntheticName2(col.func, col.distinct, col.arg);
|
|
3806
|
+
const value = String(evalAggregate(col.func, col.distinct, col.arg, groupRows));
|
|
3807
|
+
outRow[col.alias ?? syntheticKey] = value;
|
|
3808
|
+
if (col.alias) outRow[syntheticKey] = value;
|
|
3448
3809
|
} else if (col.type === "ARITH_AGG_COL") {
|
|
3449
3810
|
const outputKey = col.alias ?? aggArithDefaultKey(col.expr);
|
|
3450
3811
|
outRow[outputKey] = String(evalAggArithExpr(col.expr, groupRows));
|
|
@@ -3489,12 +3850,23 @@ function evalAggregate(func, distinct, arg, rows) {
|
|
|
3489
3850
|
return nums.reduce((a, b) => a + b, 0);
|
|
3490
3851
|
case "AVG":
|
|
3491
3852
|
return nums.length === 0 ? 0 : nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
3853
|
+
// Math.max(...nums) は要素数が多いと RangeError になるためループで求める
|
|
3492
3854
|
case "MAX":
|
|
3493
|
-
return nums.length === 0 ? 0 :
|
|
3855
|
+
return nums.length === 0 ? 0 : maxOf(nums);
|
|
3494
3856
|
case "MIN":
|
|
3495
|
-
return nums.length === 0 ? 0 :
|
|
3857
|
+
return nums.length === 0 ? 0 : minOf(nums);
|
|
3496
3858
|
}
|
|
3497
3859
|
}
|
|
3860
|
+
function maxOf(nums) {
|
|
3861
|
+
let m = nums[0];
|
|
3862
|
+
for (const n of nums) if (n > m) m = n;
|
|
3863
|
+
return m;
|
|
3864
|
+
}
|
|
3865
|
+
function minOf(nums) {
|
|
3866
|
+
let m = nums[0];
|
|
3867
|
+
for (const n of nums) if (n < m) m = n;
|
|
3868
|
+
return m;
|
|
3869
|
+
}
|
|
3498
3870
|
function evalAggArithExpr(node, rows) {
|
|
3499
3871
|
if (node.type === "NUMBER") return node.value;
|
|
3500
3872
|
if (node.type === "AGG_REF") return evalAggregate(node.func, node.distinct, node.arg, rows);
|
|
@@ -3531,43 +3903,89 @@ function applyHaving(rows, having) {
|
|
|
3531
3903
|
return rows.filter((row) => evalWhere(having, row));
|
|
3532
3904
|
}
|
|
3533
3905
|
function applyDistinct(rows, columns) {
|
|
3906
|
+
if (rows.length === 0) return rows;
|
|
3907
|
+
const keyFor = buildDistinctKeyBuilder(rows, columns);
|
|
3534
3908
|
const seen = /* @__PURE__ */ new Set();
|
|
3535
3909
|
return rows.filter((row) => {
|
|
3536
|
-
const key =
|
|
3910
|
+
const key = keyFor(row);
|
|
3537
3911
|
if (seen.has(key)) return false;
|
|
3538
3912
|
seen.add(key);
|
|
3539
3913
|
return true;
|
|
3540
3914
|
});
|
|
3541
3915
|
}
|
|
3542
|
-
function
|
|
3916
|
+
function buildDistinctKeyBuilder(rows, columns) {
|
|
3543
3917
|
if (columns.some((c) => c.type === "WILDCARD")) {
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3918
|
+
const allKeys = /* @__PURE__ */ new Set();
|
|
3919
|
+
for (const row of rows) {
|
|
3920
|
+
for (const k of Object.keys(row)) allKeys.add(k);
|
|
3921
|
+
}
|
|
3922
|
+
const keys = [...allKeys].sort();
|
|
3923
|
+
return (row) => JSON.stringify(keys.map((k) => row[k] !== void 0 ? row[k] : null));
|
|
3924
|
+
}
|
|
3925
|
+
let sortedParentKeys = [];
|
|
3926
|
+
if (columns.some((c) => c.type === "PARENT_WILDCARD")) {
|
|
3927
|
+
const parentKeys = /* @__PURE__ */ new Set();
|
|
3928
|
+
for (const row of rows) {
|
|
3929
|
+
for (const k of Object.keys(row)) {
|
|
3930
|
+
if (k.startsWith("_p.")) parentKeys.add(k);
|
|
3555
3931
|
}
|
|
3556
3932
|
}
|
|
3933
|
+
sortedParentKeys = [...parentKeys].sort();
|
|
3557
3934
|
}
|
|
3558
|
-
return
|
|
3935
|
+
return (row) => {
|
|
3936
|
+
const values = [];
|
|
3937
|
+
for (const col of columns) {
|
|
3938
|
+
if (col.type === "FIELD") {
|
|
3939
|
+
values.push(row[col.field] ?? "");
|
|
3940
|
+
continue;
|
|
3941
|
+
}
|
|
3942
|
+
if (col.type === "PARENT_WILDCARD") {
|
|
3943
|
+
for (const k of sortedParentKeys) {
|
|
3944
|
+
values.push(row[k] !== void 0 ? row[k] : null);
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
return JSON.stringify(values);
|
|
3949
|
+
};
|
|
3559
3950
|
}
|
|
3560
3951
|
function applyOrderBy(rows, orderBy, optionOrders, sortKinds) {
|
|
3561
3952
|
if (orderBy.length === 0) return rows;
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3953
|
+
const keyMeta = orderBy.map(({ key }) => ({
|
|
3954
|
+
orderMap: key.type === "FIELD_NAME" ? optionOrders?.get(key.name) : void 0,
|
|
3955
|
+
sortKind: key.type === "FIELD_NAME" ? sortKinds?.get(key.name) : void 0
|
|
3956
|
+
}));
|
|
3957
|
+
const decorated = rows.map((row) => ({
|
|
3958
|
+
row,
|
|
3959
|
+
keys: orderBy.map(({ key }, i) => {
|
|
3960
|
+
const s = evalOrderKey(key, row);
|
|
3961
|
+
const n = Number(s);
|
|
3962
|
+
const orderMap = keyMeta[i].orderMap;
|
|
3963
|
+
return {
|
|
3964
|
+
s,
|
|
3965
|
+
n,
|
|
3966
|
+
isNum: !Number.isNaN(n),
|
|
3967
|
+
rank: orderMap ? minChoiceIndex(parseChoiceValues(s), orderMap) : 0
|
|
3968
|
+
};
|
|
3969
|
+
})
|
|
3970
|
+
}));
|
|
3971
|
+
decorated.sort((a, b) => {
|
|
3972
|
+
for (let i = 0; i < orderBy.length; i++) {
|
|
3973
|
+
const cmp = compareSortKeys(a.keys[i], b.keys[i], keyMeta[i]);
|
|
3974
|
+
if (cmp !== 0) return orderBy[i].direction === "ASC" ? cmp : -cmp;
|
|
3568
3975
|
}
|
|
3569
3976
|
return 0;
|
|
3570
3977
|
});
|
|
3978
|
+
return decorated.map((d) => d.row);
|
|
3979
|
+
}
|
|
3980
|
+
function compareSortKeys(a, b, meta) {
|
|
3981
|
+
if (meta.orderMap) {
|
|
3982
|
+
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
3983
|
+
return a.s.localeCompare(b.s, "ja");
|
|
3984
|
+
}
|
|
3985
|
+
if (meta.sortKind === "string") {
|
|
3986
|
+
return a.s.localeCompare(b.s, "ja");
|
|
3987
|
+
}
|
|
3988
|
+
return a.isNum && b.isNum ? a.n - b.n : a.s.localeCompare(b.s, "ja");
|
|
3571
3989
|
}
|
|
3572
3990
|
function evalOrderKey(key, row) {
|
|
3573
3991
|
switch (key.type) {
|
|
@@ -3579,32 +3997,6 @@ function evalOrderKey(key, row) {
|
|
|
3579
3997
|
return evalStringFunc(key.expr, row);
|
|
3580
3998
|
}
|
|
3581
3999
|
}
|
|
3582
|
-
function compareOrderValues(av, bv, key, optionOrders, sortKinds) {
|
|
3583
|
-
if (key.type === "FIELD_NAME") {
|
|
3584
|
-
const orderMap = optionOrders?.get(key.name);
|
|
3585
|
-
if (orderMap) {
|
|
3586
|
-
const ac = compareByChoiceOrder(av, bv, orderMap);
|
|
3587
|
-
if (ac !== 0) return ac;
|
|
3588
|
-
return av.localeCompare(bv, "ja");
|
|
3589
|
-
}
|
|
3590
|
-
const sortKind = sortKinds?.get(key.name);
|
|
3591
|
-
if (sortKind === "number") {
|
|
3592
|
-
return compareAsNumber(av, bv);
|
|
3593
|
-
}
|
|
3594
|
-
if (sortKind === "string") {
|
|
3595
|
-
return av.localeCompare(bv, "ja");
|
|
3596
|
-
}
|
|
3597
|
-
}
|
|
3598
|
-
return compareAuto(av, bv);
|
|
3599
|
-
}
|
|
3600
|
-
function compareByChoiceOrder(av, bv, orderMap) {
|
|
3601
|
-
const aValues = parseChoiceValues(av);
|
|
3602
|
-
const bValues = parseChoiceValues(bv);
|
|
3603
|
-
const aRank = minChoiceIndex(aValues, orderMap);
|
|
3604
|
-
const bRank = minChoiceIndex(bValues, orderMap);
|
|
3605
|
-
if (aRank !== bRank) return aRank - bRank;
|
|
3606
|
-
return 0;
|
|
3607
|
-
}
|
|
3608
4000
|
function parseChoiceValues(raw) {
|
|
3609
4001
|
const trimmed = raw.trim();
|
|
3610
4002
|
if (trimmed === "") return [""];
|
|
@@ -3628,18 +4020,6 @@ function minChoiceIndex(values, orderMap) {
|
|
|
3628
4020
|
}
|
|
3629
4021
|
return min;
|
|
3630
4022
|
}
|
|
3631
|
-
function compareAsNumber(av, bv) {
|
|
3632
|
-
const an = Number(av);
|
|
3633
|
-
const bn = Number(bv);
|
|
3634
|
-
const numeric = !Number.isNaN(an) && !Number.isNaN(bn);
|
|
3635
|
-
return numeric ? an - bn : av.localeCompare(bv, "ja");
|
|
3636
|
-
}
|
|
3637
|
-
function compareAuto(av, bv) {
|
|
3638
|
-
const an = Number(av);
|
|
3639
|
-
const bn = Number(bv);
|
|
3640
|
-
const numeric = !Number.isNaN(an) && !Number.isNaN(bn);
|
|
3641
|
-
return numeric ? an - bn : av.localeCompare(bv, "ja");
|
|
3642
|
-
}
|
|
3643
4023
|
function applyLimit(rows, limit, offset) {
|
|
3644
4024
|
const start = offset ?? 0;
|
|
3645
4025
|
if (limit === null) return rows.slice(start);
|
|
@@ -3884,8 +4264,61 @@ function toFlatString(value) {
|
|
|
3884
4264
|
|
|
3885
4265
|
// src/execute.ts
|
|
3886
4266
|
async function execute(sql, client, options = {}) {
|
|
4267
|
+
const metrics = createEmptyMetrics();
|
|
4268
|
+
const countedClient = wrapClientWithMetrics(client, metrics);
|
|
4269
|
+
const startedAt = Date.now();
|
|
4270
|
+
const result = await executeStatement(sql, countedClient, options);
|
|
4271
|
+
metrics.elapsedMs = Date.now() - startedAt;
|
|
4272
|
+
return { ...result, metrics };
|
|
4273
|
+
}
|
|
4274
|
+
function createEmptyMetrics() {
|
|
4275
|
+
return {
|
|
4276
|
+
getCalls: 0,
|
|
4277
|
+
postCalls: 0,
|
|
4278
|
+
putCalls: 0,
|
|
4279
|
+
deleteCalls: 0,
|
|
4280
|
+
fieldCalls: 0,
|
|
4281
|
+
appsCalls: 0,
|
|
4282
|
+
fetchedRows: 0,
|
|
4283
|
+
elapsedMs: 0
|
|
4284
|
+
};
|
|
4285
|
+
}
|
|
4286
|
+
function wrapClientWithMetrics(client, metrics) {
|
|
4287
|
+
return {
|
|
4288
|
+
getRecords: async (params) => {
|
|
4289
|
+
metrics.getCalls += 1;
|
|
4290
|
+
const res = await client.getRecords(params);
|
|
4291
|
+
metrics.fetchedRows += res.records.length;
|
|
4292
|
+
return res;
|
|
4293
|
+
},
|
|
4294
|
+
postRecords: (params) => {
|
|
4295
|
+
metrics.postCalls += 1;
|
|
4296
|
+
return client.postRecords(params);
|
|
4297
|
+
},
|
|
4298
|
+
putRecords: (params) => {
|
|
4299
|
+
metrics.putCalls += 1;
|
|
4300
|
+
return client.putRecords(params);
|
|
4301
|
+
},
|
|
4302
|
+
deleteRecords: (params) => {
|
|
4303
|
+
metrics.deleteCalls += 1;
|
|
4304
|
+
return client.deleteRecords(params);
|
|
4305
|
+
},
|
|
4306
|
+
getApps: () => {
|
|
4307
|
+
metrics.appsCalls += 1;
|
|
4308
|
+
return client.getApps();
|
|
4309
|
+
},
|
|
4310
|
+
getFields: (appId) => {
|
|
4311
|
+
metrics.fieldCalls += 1;
|
|
4312
|
+
return client.getFields(appId);
|
|
4313
|
+
}
|
|
4314
|
+
};
|
|
4315
|
+
}
|
|
4316
|
+
async function executeStatement(sql, client, options) {
|
|
3887
4317
|
const cacheContext = options.cacheContext ?? "default";
|
|
3888
4318
|
const stmt = parseSql(sql);
|
|
4319
|
+
return executeParsedStatement(stmt, client, options, cacheContext);
|
|
4320
|
+
}
|
|
4321
|
+
async function executeParsedStatement(stmt, client, options, cacheContext) {
|
|
3889
4322
|
switch (stmt.type) {
|
|
3890
4323
|
case "SELECT":
|
|
3891
4324
|
return executeSelect(stmt, client, options, cacheContext);
|
|
@@ -3894,7 +4327,7 @@ async function execute(sql, client, options = {}) {
|
|
|
3894
4327
|
case "WITH":
|
|
3895
4328
|
return executeWith(stmt, client, options, cacheContext);
|
|
3896
4329
|
case "INSERT":
|
|
3897
|
-
return executeInsert(stmt, client, cacheContext);
|
|
4330
|
+
return executeInsert(stmt, client, options, cacheContext);
|
|
3898
4331
|
case "INSERT_SELECT":
|
|
3899
4332
|
return executeInsertSelect(stmt, client, options, cacheContext);
|
|
3900
4333
|
case "UPSERT":
|
|
@@ -3913,9 +4346,181 @@ async function execute(sql, client, options = {}) {
|
|
|
3913
4346
|
return executeDescribe(stmt, client, cacheContext);
|
|
3914
4347
|
case "EXPLAIN":
|
|
3915
4348
|
return executeExplain(stmt);
|
|
4349
|
+
// 一時テーブルはバッチスコープのため単文実行では拒否する(executeBatch を使う)
|
|
4350
|
+
case "CREATE_TEMP_TABLE":
|
|
4351
|
+
throw new Error("ArgumentError: CREATE TEMP TABLE requires a batch (temp tables are batch-scoped).");
|
|
4352
|
+
case "DROP_TEMP_TABLE":
|
|
4353
|
+
throw new Error("ArgumentError: DROP TEMP TABLE requires a batch (temp tables are batch-scoped).");
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4356
|
+
var TEMP_TABLE_MAX_ROWS = 1e4;
|
|
4357
|
+
var BatchTimeoutError = class extends Error {
|
|
4358
|
+
constructor() {
|
|
4359
|
+
super("TimeoutError: batch timeout exceeded.");
|
|
4360
|
+
}
|
|
4361
|
+
};
|
|
4362
|
+
async function executeBatch(sql, client, options = {}) {
|
|
4363
|
+
const statements = parseSqlBatch(sql);
|
|
4364
|
+
const analysis = analyzeBatch(statements);
|
|
4365
|
+
if (options.continueOnError && analysis.containsDml) {
|
|
4366
|
+
throw new Error("ArgumentError: continueOnError is not allowed for batches containing DML.");
|
|
4367
|
+
}
|
|
4368
|
+
for (const s of analysis.statements) {
|
|
4369
|
+
if (!s.isDml || s.tempTablesReferenced.length === 0) continue;
|
|
4370
|
+
if (s.statementType === "INSERT_SELECT" && s.tempOnlySource) continue;
|
|
4371
|
+
throw new BatchAnalysisError(
|
|
4372
|
+
s.statementType === "INSERT_SELECT" ? `ArgumentError: INSERT_SELECT in a batch must select from temp tables only. (statement ${s.index})` : `ArgumentError: temp table references in ${s.statementType} are not supported yet.`,
|
|
4373
|
+
s.index
|
|
4374
|
+
);
|
|
4375
|
+
}
|
|
4376
|
+
const metrics = createEmptyMetrics();
|
|
4377
|
+
const countedClient = wrapClientWithMetrics(client, metrics);
|
|
4378
|
+
const startedAt = Date.now();
|
|
4379
|
+
const deadline = options.timeoutMs != null ? startedAt + options.timeoutMs : null;
|
|
4380
|
+
const cacheContext = options.cacheContext ?? "default";
|
|
4381
|
+
const tempTables = /* @__PURE__ */ new Map();
|
|
4382
|
+
const results = [];
|
|
4383
|
+
const failed = /* @__PURE__ */ new Set();
|
|
4384
|
+
let aborted = null;
|
|
4385
|
+
for (let i = 0; i < statements.length; i++) {
|
|
4386
|
+
const info = analysis.statements[i];
|
|
4387
|
+
const base = { index: i, type: info.statementType };
|
|
4388
|
+
if (aborted) {
|
|
4389
|
+
results.push({ ...base, status: "skipped", skippedReason: aborted });
|
|
4390
|
+
failed.add(i);
|
|
4391
|
+
continue;
|
|
4392
|
+
}
|
|
4393
|
+
const brokenDep = info.dependsOn.find((d) => failed.has(d));
|
|
4394
|
+
if (brokenDep !== void 0) {
|
|
4395
|
+
const depName = analysis.statements[brokenDep].tempTablesCreated[0] ?? `statement ${brokenDep}`;
|
|
4396
|
+
results.push({ ...base, status: "skipped", skippedReason: `dependency: ${depName}` });
|
|
4397
|
+
failed.add(i);
|
|
4398
|
+
continue;
|
|
4399
|
+
}
|
|
4400
|
+
if (deadline !== null && Date.now() >= deadline) {
|
|
4401
|
+
results.push({ ...base, status: "skipped", skippedReason: "timeout" });
|
|
4402
|
+
failed.add(i);
|
|
4403
|
+
aborted = "timeout";
|
|
4404
|
+
continue;
|
|
4405
|
+
}
|
|
4406
|
+
try {
|
|
4407
|
+
const remaining = deadline !== null ? deadline - Date.now() : null;
|
|
4408
|
+
const outcome = await runWithDeadline(
|
|
4409
|
+
executeBatchStatement(statements[i], info, countedClient, options, cacheContext, tempTables),
|
|
4410
|
+
remaining
|
|
4411
|
+
);
|
|
4412
|
+
results.push({ ...base, status: "success", ...outcome });
|
|
4413
|
+
} catch (e) {
|
|
4414
|
+
results.push({ ...base, status: "error", error: toBatchStatementError(e) });
|
|
4415
|
+
failed.add(i);
|
|
4416
|
+
if (e instanceof BatchTimeoutError) {
|
|
4417
|
+
aborted = "timeout";
|
|
4418
|
+
} else if (!options.continueOnError) {
|
|
4419
|
+
aborted = "fail-fast";
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
metrics.elapsedMs = Date.now() - startedAt;
|
|
4424
|
+
return {
|
|
4425
|
+
ok: results.every((r) => r.status === "success"),
|
|
4426
|
+
statementCount: statements.length,
|
|
4427
|
+
statements: results,
|
|
4428
|
+
analysis,
|
|
4429
|
+
metrics
|
|
4430
|
+
};
|
|
4431
|
+
}
|
|
4432
|
+
async function executeBatchStatement(stmt, info, client, options, cacheContext, tempTables) {
|
|
4433
|
+
if (stmt.type === "CREATE_TEMP_TABLE") {
|
|
4434
|
+
const materializeOptions = {
|
|
4435
|
+
...options,
|
|
4436
|
+
maxRecords: options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
4437
|
+
onLimitReached: "error"
|
|
4438
|
+
};
|
|
4439
|
+
const result = await runSelectLike(stmt.query, client, materializeOptions, cacheContext, tempTables);
|
|
4440
|
+
tempTables.set(stmt.name, result.rows);
|
|
4441
|
+
return { tempTable: stmt.name, rowCount: result.rows.length };
|
|
4442
|
+
}
|
|
4443
|
+
if (stmt.type === "DROP_TEMP_TABLE") {
|
|
4444
|
+
tempTables.delete(stmt.name);
|
|
4445
|
+
return { tempTable: stmt.name };
|
|
3916
4446
|
}
|
|
4447
|
+
if (stmt.type === "EXPLAIN") {
|
|
4448
|
+
return { result: await executeParsedStatement(stmt, client, options, cacheContext) };
|
|
4449
|
+
}
|
|
4450
|
+
if (info.tempTablesReferenced.length > 0) {
|
|
4451
|
+
if (stmt.type === "SELECT" || stmt.type === "UNION") {
|
|
4452
|
+
return { result: await executeQueryWithCte(stmt, client, options, tempTables, cacheContext) };
|
|
4453
|
+
}
|
|
4454
|
+
if (stmt.type === "WITH") {
|
|
4455
|
+
return { result: await executeWith(stmt, client, options, cacheContext, tempTables) };
|
|
4456
|
+
}
|
|
4457
|
+
if (stmt.type === "INSERT_SELECT") {
|
|
4458
|
+
return { result: await executeInsertSelect(stmt, client, options, cacheContext, tempTables) };
|
|
4459
|
+
}
|
|
4460
|
+
throw new Error(`ArgumentError: temp table references in ${stmt.type} are not supported yet.`);
|
|
4461
|
+
}
|
|
4462
|
+
return { result: await executeParsedStatement(stmt, client, options, cacheContext) };
|
|
4463
|
+
}
|
|
4464
|
+
async function runSelectLike(query, client, options, cacheContext, tempTables) {
|
|
4465
|
+
if (query.type === "WITH") {
|
|
4466
|
+
return executeWith(query, client, options, cacheContext, tempTables);
|
|
4467
|
+
}
|
|
4468
|
+
return executeQueryWithCte(query, client, options, tempTables, cacheContext);
|
|
4469
|
+
}
|
|
4470
|
+
async function runWithDeadline(work, remainingMs) {
|
|
4471
|
+
if (remainingMs === null) return work;
|
|
4472
|
+
if (remainingMs <= 0) {
|
|
4473
|
+
void work.catch(() => {
|
|
4474
|
+
});
|
|
4475
|
+
throw new BatchTimeoutError();
|
|
4476
|
+
}
|
|
4477
|
+
let timer;
|
|
4478
|
+
try {
|
|
4479
|
+
return await Promise.race([
|
|
4480
|
+
work,
|
|
4481
|
+
new Promise((_, reject) => {
|
|
4482
|
+
timer = setTimeout(() => reject(new BatchTimeoutError()), remainingMs);
|
|
4483
|
+
})
|
|
4484
|
+
]);
|
|
4485
|
+
} catch (e) {
|
|
4486
|
+
if (e instanceof BatchTimeoutError) {
|
|
4487
|
+
void work.catch(() => {
|
|
4488
|
+
});
|
|
4489
|
+
}
|
|
4490
|
+
throw e;
|
|
4491
|
+
} finally {
|
|
4492
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
function toBatchStatementError(e) {
|
|
4496
|
+
if (e instanceof Error) {
|
|
4497
|
+
const name = e.name !== "Error" ? e.name : null;
|
|
4498
|
+
return { code: name ?? codeFromMessagePrefix(e.message), message: e.message };
|
|
4499
|
+
}
|
|
4500
|
+
if (e !== null && typeof e === "object") {
|
|
4501
|
+
const obj = e;
|
|
4502
|
+
const message2 = typeof obj.message === "string" && obj.message.length > 0 ? obj.message : safeJsonStringify(e);
|
|
4503
|
+
const code = typeof obj.code === "string" && obj.code.length > 0 ? obj.code : codeFromMessagePrefix(message2);
|
|
4504
|
+
return { code, message: message2 };
|
|
4505
|
+
}
|
|
4506
|
+
const message = String(e);
|
|
4507
|
+
return { code: codeFromMessagePrefix(message), message };
|
|
4508
|
+
}
|
|
4509
|
+
function codeFromMessagePrefix(message) {
|
|
4510
|
+
return message.match(/^([A-Za-z]+Error):/)?.[1] ?? "Error";
|
|
4511
|
+
}
|
|
4512
|
+
function safeJsonStringify(v) {
|
|
4513
|
+
try {
|
|
4514
|
+
return JSON.stringify(v) ?? String(v);
|
|
4515
|
+
} catch {
|
|
4516
|
+
return String(v);
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
function parseSqlBatch(sql) {
|
|
4520
|
+
const tokens = new Lexer(sql).tokenize();
|
|
4521
|
+
return new Parser(tokens).parseStatements();
|
|
3917
4522
|
}
|
|
3918
|
-
async function executeSelect(stmt, client, options, cacheContext) {
|
|
4523
|
+
async function executeSelect(stmt, client, options, cacheContext, cteCache) {
|
|
3919
4524
|
if (isNoFromSelect(stmt)) {
|
|
3920
4525
|
return executeNoFromSelect(stmt);
|
|
3921
4526
|
}
|
|
@@ -3924,7 +4529,7 @@ async function executeSelect(stmt, client, options, cacheContext) {
|
|
|
3924
4529
|
if (mode === "SIMPLE") {
|
|
3925
4530
|
return executeSimpleSelect(stmt, client, options, cacheContext);
|
|
3926
4531
|
} else {
|
|
3927
|
-
return executeFullScanSelect(stmt, client, options, cacheContext);
|
|
4532
|
+
return executeFullScanSelect(stmt, client, options, cacheContext, cteCache);
|
|
3928
4533
|
}
|
|
3929
4534
|
}
|
|
3930
4535
|
function isNoFromSelect(stmt) {
|
|
@@ -4009,8 +4614,7 @@ async function executeSimpleSelect(stmt, client, options, cacheContext) {
|
|
|
4009
4614
|
}
|
|
4010
4615
|
let rows = records.map((r) => flatten(r, null));
|
|
4011
4616
|
if (!useSingleGet) {
|
|
4012
|
-
const optionOrders = await
|
|
4013
|
-
const sortKinds = await buildSortKindsForSelect(stmt, client, cacheContext);
|
|
4617
|
+
const { optionOrders, sortKinds } = await buildOrderByMetaForSelect(stmt, client, cacheContext);
|
|
4014
4618
|
rows = applyOrderBy(rows, stmt.orderBy, optionOrders, sortKinds);
|
|
4015
4619
|
rows = applyLimit(rows, stmt.limit, stmt.offset);
|
|
4016
4620
|
}
|
|
@@ -4038,22 +4642,25 @@ async function validateSelectFieldCodes(stmt, mode, client, cacheContext) {
|
|
|
4038
4642
|
}
|
|
4039
4643
|
}
|
|
4040
4644
|
for (const [appId, fields] of appToFields.entries()) {
|
|
4041
|
-
|
|
4645
|
+
const userFields = [...fields].filter((f) => !isSystemLikeFieldCode(f));
|
|
4646
|
+
if (userFields.length === 0) continue;
|
|
4042
4647
|
const defs = await getFieldsCached(appId, client, cacheContext);
|
|
4043
4648
|
if (defs.length === 0) continue;
|
|
4044
4649
|
const validCodes = new Set(defs.map((d) => d.code));
|
|
4045
|
-
const unknown =
|
|
4650
|
+
const unknown = userFields.filter((f) => !validCodes.has(f));
|
|
4046
4651
|
if (unknown.length > 0) {
|
|
4047
4652
|
throw new Error(`ArgumentError: unknown field code(s): ${unknown.join(", ")} (APP${appId})`);
|
|
4048
4653
|
}
|
|
4049
4654
|
}
|
|
4050
4655
|
}
|
|
4051
|
-
async function executeFullScanSelect(stmt, client, options, cacheContext) {
|
|
4656
|
+
async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache) {
|
|
4052
4657
|
const maxRecords = options.maxRecords ?? 1e4;
|
|
4053
4658
|
const warnings = /* @__PURE__ */ new Set();
|
|
4054
4659
|
const parallel = options.fetchParallel ?? 1;
|
|
4055
|
-
await
|
|
4056
|
-
|
|
4660
|
+
await Promise.all([
|
|
4661
|
+
resolveSubqueries(stmt.where, client, options, cacheContext, cteCache),
|
|
4662
|
+
resolveSubqueries(stmt.having, client, options, cacheContext, cteCache)
|
|
4663
|
+
]);
|
|
4057
4664
|
const tableConditions = /* @__PURE__ */ new Map();
|
|
4058
4665
|
if (stmt.where !== null) {
|
|
4059
4666
|
if (stmt.from.alias) {
|
|
@@ -4102,6 +4709,12 @@ async function executeFullScanSelect(stmt, client, options, cacheContext) {
|
|
|
4102
4709
|
onOptJoins.push(join2);
|
|
4103
4710
|
}
|
|
4104
4711
|
}
|
|
4712
|
+
const scalarCachePromise = resolveScalarColumns(stmt.columns, client, options, cacheContext, cteCache);
|
|
4713
|
+
const orderByMetaPromise = buildOrderByMetaForSelect(stmt, client, cacheContext);
|
|
4714
|
+
scalarCachePromise.catch(() => {
|
|
4715
|
+
});
|
|
4716
|
+
orderByMetaPromise.catch(() => {
|
|
4717
|
+
});
|
|
4105
4718
|
const mainRecords = await mainFetch;
|
|
4106
4719
|
const tables = /* @__PURE__ */ new Map();
|
|
4107
4720
|
tables.set(stmt.from.alias, mainRecords);
|
|
@@ -4133,15 +4746,16 @@ async function executeFullScanSelect(stmt, client, options, cacheContext) {
|
|
|
4133
4746
|
);
|
|
4134
4747
|
tables.set(join2.table.alias, joinRecords);
|
|
4135
4748
|
}));
|
|
4136
|
-
const scalarCache = await
|
|
4137
|
-
const optionOrders = await
|
|
4138
|
-
const sortKinds = await buildSortKindsForSelect(stmt, client, cacheContext);
|
|
4749
|
+
const scalarCache = await scalarCachePromise;
|
|
4750
|
+
const { optionOrders, sortKinds } = await orderByMetaPromise;
|
|
4139
4751
|
const { rows, columns } = runFullScan({ tables, stmt, scalarCache, optionOrders, sortKinds });
|
|
4140
4752
|
return { type: "SELECT", rows, columns, rowCount: rows.length, warnings: [...warnings] };
|
|
4141
4753
|
}
|
|
4142
4754
|
async function executeUnion(stmt, client, options, cacheContext) {
|
|
4143
|
-
const leftResult =
|
|
4144
|
-
|
|
4755
|
+
const [leftResult, rightResult] = await Promise.all([
|
|
4756
|
+
stmt.left.type === "UNION" ? executeUnion(stmt.left, client, options, cacheContext) : executeSelect(stmt.left, client, options, cacheContext),
|
|
4757
|
+
executeSelect(stmt.right, client, options, cacheContext)
|
|
4758
|
+
]);
|
|
4145
4759
|
const leftCols = leftResult.columns;
|
|
4146
4760
|
const rightCols = rightResult.columns;
|
|
4147
4761
|
const remappedRight = rightResult.rows.map((row) => {
|
|
@@ -4158,17 +4772,17 @@ async function executeUnion(stmt, client, options, cacheContext) {
|
|
|
4158
4772
|
function deduplicateRows(rows, columns) {
|
|
4159
4773
|
const seen = /* @__PURE__ */ new Set();
|
|
4160
4774
|
return rows.filter((row) => {
|
|
4161
|
-
const key = columns.map((c) => row[c] ?? "")
|
|
4775
|
+
const key = JSON.stringify(columns.map((c) => row[c] ?? ""));
|
|
4162
4776
|
if (seen.has(key)) return false;
|
|
4163
4777
|
seen.add(key);
|
|
4164
4778
|
return true;
|
|
4165
4779
|
});
|
|
4166
4780
|
}
|
|
4167
|
-
async function executeWith(stmt, client, options, cacheContext) {
|
|
4168
|
-
if (canInlineSingleCte(stmt)) {
|
|
4781
|
+
async function executeWith(stmt, client, options, cacheContext, seed) {
|
|
4782
|
+
if ((seed == null || seed.size === 0) && canInlineSingleCte(stmt)) {
|
|
4169
4783
|
return executeSelect(buildInlinedQuery(stmt), client, options, cacheContext);
|
|
4170
4784
|
}
|
|
4171
|
-
const cteCache =
|
|
4785
|
+
const cteCache = new Map(seed ?? []);
|
|
4172
4786
|
for (const cte of stmt.ctes) {
|
|
4173
4787
|
let result;
|
|
4174
4788
|
if (cte.query.type === "SHOW_APPS") {
|
|
@@ -4261,8 +4875,10 @@ function stripCteAliasFromFieldValue(fv, alias) {
|
|
|
4261
4875
|
}
|
|
4262
4876
|
async function executeQueryWithCte(query, client, options, cteCache, cacheContext) {
|
|
4263
4877
|
if (query.type === "UNION") {
|
|
4264
|
-
const leftResult = await
|
|
4265
|
-
|
|
4878
|
+
const [leftResult, rightResult] = await Promise.all([
|
|
4879
|
+
executeQueryWithCte(query.left, client, options, cteCache, cacheContext),
|
|
4880
|
+
executeQueryWithCte(query.right, client, options, cteCache, cacheContext)
|
|
4881
|
+
]);
|
|
4266
4882
|
const leftCols = leftResult.columns;
|
|
4267
4883
|
const rightCols = rightResult.columns;
|
|
4268
4884
|
const remapped = rightResult.rows.map((row) => {
|
|
@@ -4278,7 +4894,7 @@ async function executeQueryWithCte(query, client, options, cteCache, cacheContex
|
|
|
4278
4894
|
}
|
|
4279
4895
|
const hasCteRef = query.from.cteName != null || query.joins.some((j) => j.table.cteName != null);
|
|
4280
4896
|
if (!hasCteRef) {
|
|
4281
|
-
return executeSelect(query, client, options, cacheContext);
|
|
4897
|
+
return executeSelect(query, client, options, cacheContext, cteCache);
|
|
4282
4898
|
}
|
|
4283
4899
|
return executeFullScanWithCte(query, client, options, cteCache, cacheContext);
|
|
4284
4900
|
}
|
|
@@ -4286,9 +4902,17 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
|
|
|
4286
4902
|
const maxRecords = options.maxRecords ?? 1e4;
|
|
4287
4903
|
const warnings = /* @__PURE__ */ new Set();
|
|
4288
4904
|
const parallel = options.fetchParallel ?? 1;
|
|
4289
|
-
await
|
|
4290
|
-
|
|
4291
|
-
|
|
4905
|
+
await Promise.all([
|
|
4906
|
+
resolveSubqueries(stmt.where, client, options, cacheContext, cteCache),
|
|
4907
|
+
resolveSubqueries(stmt.having, client, options, cacheContext, cteCache)
|
|
4908
|
+
]);
|
|
4909
|
+
const scalarCachePromise = resolveScalarColumns(stmt.columns, client, options, cacheContext, cteCache);
|
|
4910
|
+
const orderByMetaPromise = buildOrderByMetaForSelect(stmt, client, cacheContext);
|
|
4911
|
+
scalarCachePromise.catch(() => {
|
|
4912
|
+
});
|
|
4913
|
+
orderByMetaPromise.catch(() => {
|
|
4914
|
+
});
|
|
4915
|
+
const tables = /* @__PURE__ */ new Map();
|
|
4292
4916
|
if (stmt.from.cteName != null) {
|
|
4293
4917
|
const rows2 = cteCache.get(stmt.from.cteName) ?? [];
|
|
4294
4918
|
tables.set(stmt.from.alias, rows2.map(processRowToKintoneRecord));
|
|
@@ -4334,9 +4958,8 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
|
|
|
4334
4958
|
}
|
|
4335
4959
|
});
|
|
4336
4960
|
await Promise.all(joinFetches);
|
|
4337
|
-
const scalarCache = await
|
|
4338
|
-
const optionOrders = await
|
|
4339
|
-
const sortKinds = await buildSortKindsForSelect(stmt, client, cacheContext);
|
|
4961
|
+
const scalarCache = await scalarCachePromise;
|
|
4962
|
+
const { optionOrders, sortKinds } = await orderByMetaPromise;
|
|
4340
4963
|
const { rows, columns } = runFullScan({ tables, stmt, scalarCache, optionOrders, sortKinds });
|
|
4341
4964
|
return { type: "SELECT", rows, columns, rowCount: rows.length, warnings: [...warnings] };
|
|
4342
4965
|
}
|
|
@@ -4372,6 +4995,78 @@ async function fetchTableRecordsForFullScan(stmt, table, client, maxRecords, par
|
|
|
4372
4995
|
const parentRecords = parentResolved.records;
|
|
4373
4996
|
return expandSubtableRecords(parentRecords, table.subtableCode);
|
|
4374
4997
|
}
|
|
4998
|
+
var UPSERT_IN_CHUNK_SIZE = 50;
|
|
4999
|
+
function normalizeKeyPart(v) {
|
|
5000
|
+
const t = v.trim();
|
|
5001
|
+
if (t !== "" && !Number.isNaN(Number(t))) return String(Number(t));
|
|
5002
|
+
return v;
|
|
5003
|
+
}
|
|
5004
|
+
function upsertCompositeKey(parts) {
|
|
5005
|
+
return JSON.stringify(parts);
|
|
5006
|
+
}
|
|
5007
|
+
function upsertNormalizedKey(parts, numericKey) {
|
|
5008
|
+
return JSON.stringify(parts.map((p, i) => numericKey[i] ? normalizeKeyPart(p) : p));
|
|
5009
|
+
}
|
|
5010
|
+
function lookupUpsertTarget(index, keyParts) {
|
|
5011
|
+
const exact = index.raw.get(upsertCompositeKey(keyParts));
|
|
5012
|
+
if (exact !== void 0) return exact;
|
|
5013
|
+
if (!index.numericKey.some(Boolean)) return void 0;
|
|
5014
|
+
return index.normalized.get(upsertNormalizedKey(keyParts, index.numericKey));
|
|
5015
|
+
}
|
|
5016
|
+
async function resolveUpsertTargets(appId, keyFields, rowKeyValues, client, options, fieldTypes) {
|
|
5017
|
+
const maxRecords = options.maxRecords ?? 1e4;
|
|
5018
|
+
const parallel = options.fetchParallel ?? 1;
|
|
5019
|
+
const numericKey = keyFields.map((f) => fieldTypes.get(f) === "NUMBER");
|
|
5020
|
+
const index = { raw: /* @__PURE__ */ new Map(), normalized: /* @__PURE__ */ new Map(), numericKey };
|
|
5021
|
+
const setMax = (map, key, id) => {
|
|
5022
|
+
const cur = map.get(key);
|
|
5023
|
+
if (cur === void 0 || id > cur) map.set(key, id);
|
|
5024
|
+
};
|
|
5025
|
+
const addRecordToIndex = (parts, id) => {
|
|
5026
|
+
setMax(index.raw, upsertCompositeKey(parts), id);
|
|
5027
|
+
if (numericKey.some(Boolean)) {
|
|
5028
|
+
setMax(index.normalized, upsertNormalizedKey(parts, numericKey), id);
|
|
5029
|
+
}
|
|
5030
|
+
};
|
|
5031
|
+
const batchFirstKeys = /* @__PURE__ */ new Set();
|
|
5032
|
+
const perRowKeys = [];
|
|
5033
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5034
|
+
for (const parts of rowKeyValues) {
|
|
5035
|
+
const composite = upsertCompositeKey(parts);
|
|
5036
|
+
if (seen.has(composite)) continue;
|
|
5037
|
+
seen.add(composite);
|
|
5038
|
+
if (parts.some((p) => p === "")) perRowKeys.push(parts);
|
|
5039
|
+
else batchFirstKeys.add(parts[0]);
|
|
5040
|
+
}
|
|
5041
|
+
const fields = ["$id", ...keyFields];
|
|
5042
|
+
for (const chunk2 of splitChunks([...batchFirstKeys], UPSERT_IN_CHUNK_SIZE)) {
|
|
5043
|
+
const query = `${keyFields[0]} in (${chunk2.map(sqlQuote).join(",")})`;
|
|
5044
|
+
const records = await fetchAll(client.getRecords, appId, query, fields, { maxRecords, parallel });
|
|
5045
|
+
for (const rec of records) {
|
|
5046
|
+
const id = Number(rec["$id"]?.value);
|
|
5047
|
+
if (!Number.isFinite(id)) continue;
|
|
5048
|
+
addRecordToIndex(keyFields.map((f) => toScalarText(rec[f]?.value)), id);
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
for (const parts of perRowKeys) {
|
|
5052
|
+
const query = keyFields.map((f, i) => `${f} = ${sqlQuote(parts[i])}`).join(" and ");
|
|
5053
|
+
const existing = await fetchAll(client.getRecords, appId, query, ["$id"], { maxRecords, parallel });
|
|
5054
|
+
if (existing.length === 0) continue;
|
|
5055
|
+
addRecordToIndex(parts, maxRecordId(existing));
|
|
5056
|
+
}
|
|
5057
|
+
return index;
|
|
5058
|
+
}
|
|
5059
|
+
function maxRecordId(records) {
|
|
5060
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
5061
|
+
for (const r of records) {
|
|
5062
|
+
const n = Number(r["$id"]?.value);
|
|
5063
|
+
if (Number.isFinite(n) && n > max) max = n;
|
|
5064
|
+
}
|
|
5065
|
+
if (!Number.isFinite(max)) {
|
|
5066
|
+
throw new Error("\u30EC\u30B3\u30FC\u30C9\u306B\u6570\u5024\u306E $id \u304C\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093\u3002");
|
|
5067
|
+
}
|
|
5068
|
+
return max;
|
|
5069
|
+
}
|
|
4375
5070
|
function toScalarText(value) {
|
|
4376
5071
|
if (typeof value === "string") return value;
|
|
4377
5072
|
if (value === null || value === void 0) return "";
|
|
@@ -4509,6 +5204,16 @@ async function getSortKindMapByApp(appId, client, cacheContext) {
|
|
|
4509
5204
|
setScopedCacheValue(sortKindCache, cacheContext, appId, map);
|
|
4510
5205
|
return map;
|
|
4511
5206
|
}
|
|
5207
|
+
async function buildOrderByMetaForSelect(stmt, client, cacheContext) {
|
|
5208
|
+
if (stmt.orderBy.length === 0) {
|
|
5209
|
+
return { optionOrders: /* @__PURE__ */ new Map(), sortKinds: /* @__PURE__ */ new Map() };
|
|
5210
|
+
}
|
|
5211
|
+
const [optionOrders, sortKinds] = await Promise.all([
|
|
5212
|
+
buildOptionOrdersForSelect(stmt, client, cacheContext),
|
|
5213
|
+
buildSortKindsForSelect(stmt, client, cacheContext)
|
|
5214
|
+
]);
|
|
5215
|
+
return { optionOrders, sortKinds };
|
|
5216
|
+
}
|
|
4512
5217
|
async function buildOptionOrdersForSelect(stmt, client, cacheContext) {
|
|
4513
5218
|
const optionOrders = /* @__PURE__ */ new Map();
|
|
4514
5219
|
const tables = [stmt.from, ...stmt.joins.map((j) => j.table)];
|
|
@@ -4582,9 +5287,9 @@ function convertProcessRowValue(raw, dstFieldType) {
|
|
|
4582
5287
|
}
|
|
4583
5288
|
return raw;
|
|
4584
5289
|
}
|
|
4585
|
-
async function executeInsert(stmt, client, cacheContext) {
|
|
5290
|
+
async function executeInsert(stmt, client, options, cacheContext) {
|
|
4586
5291
|
if (stmt.subtableCode) {
|
|
4587
|
-
return executeInsertSubtable(stmt, client, cacheContext);
|
|
5292
|
+
return executeInsertSubtable(stmt, client, options, cacheContext);
|
|
4588
5293
|
}
|
|
4589
5294
|
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
4590
5295
|
const batches = insertToPostBatches(stmt, fieldTypes);
|
|
@@ -4599,14 +5304,18 @@ async function executeInsert(stmt, client, cacheContext) {
|
|
|
4599
5304
|
insertedCount: createdIds.flat().length
|
|
4600
5305
|
};
|
|
4601
5306
|
}
|
|
4602
|
-
async function executeInsertSelect(stmt, client, options, cacheContext) {
|
|
4603
|
-
const selectResult = await executeSelect(stmt.select, client, options, cacheContext);
|
|
5307
|
+
async function executeInsertSelect(stmt, client, options, cacheContext, cteCache) {
|
|
5308
|
+
const selectResult = cteCache !== void 0 && cteCache.size > 0 ? await executeQueryWithCte(stmt.select, client, options, cteCache, cacheContext) : await executeSelect(stmt.select, client, options, cacheContext);
|
|
4604
5309
|
const { rows, columns } = selectResult;
|
|
4605
5310
|
if (columns.length !== stmt.fields.length) {
|
|
4606
5311
|
throw new Error(
|
|
4607
5312
|
`SELECT \u306E\u5217\u6570\uFF08${columns.length}\uFF09\u3068 INSERT \u306E\u30D5\u30A3\u30FC\u30EB\u30C9\u6570\uFF08${stmt.fields.length}\uFF09\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093`
|
|
4608
5313
|
);
|
|
4609
5314
|
}
|
|
5315
|
+
if (options.confirm) {
|
|
5316
|
+
const ok = await options.confirm(rows.length, "INSERT");
|
|
5317
|
+
if (!ok) throw new OperationCancelledError("INSERT", rows.length);
|
|
5318
|
+
}
|
|
4610
5319
|
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
4611
5320
|
const allRecords = rows.map((row) => {
|
|
4612
5321
|
const record = {};
|
|
@@ -4699,26 +5408,19 @@ async function executeDelete(stmt, client, options, cacheContext) {
|
|
|
4699
5408
|
return { type: "DELETE", deletedCount: ids.length };
|
|
4700
5409
|
}
|
|
4701
5410
|
async function executeUpsert(stmt, client, options, cacheContext) {
|
|
4702
|
-
const maxRecords = options.maxRecords ?? 1e4;
|
|
4703
5411
|
const toInsert = [];
|
|
4704
5412
|
const toUpdate = [];
|
|
4705
5413
|
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
4706
|
-
|
|
4707
|
-
|
|
5414
|
+
const rowKeyValues = stmt.values.map(
|
|
5415
|
+
(row) => stmt.keyFields.map((key) => {
|
|
4708
5416
|
const idx = stmt.fields.indexOf(key);
|
|
4709
5417
|
if (idx === -1) throw new Error(`ON DUPLICATE \u306E\u30AD\u30FC\u300C${key}\u300D\u304C INSERT \u30D5\u30A3\u30FC\u30EB\u30C9\u306B\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093`);
|
|
4710
5418
|
const val = row[idx];
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
client.getRecords,
|
|
4717
|
-
stmt.appId,
|
|
4718
|
-
query,
|
|
4719
|
-
["$id"],
|
|
4720
|
-
{ maxRecords, parallel: options.fetchParallel ?? 1 }
|
|
4721
|
-
);
|
|
5419
|
+
return val.type === "STRING" ? val.value : val.type === "NUMBER" ? String(val.value) : val.type === "CASE_VALUE" ? evalCaseWhen(val.expr, {}) : val.elements.map((e) => e.value).join(",");
|
|
5420
|
+
})
|
|
5421
|
+
);
|
|
5422
|
+
const targetIndex = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeyValues, client, options, fieldTypes);
|
|
5423
|
+
stmt.values.forEach((row, rowIdx) => {
|
|
4722
5424
|
const record = {};
|
|
4723
5425
|
stmt.fields.forEach((field, i) => {
|
|
4724
5426
|
const val = row[i];
|
|
@@ -4728,13 +5430,13 @@ async function executeUpsert(stmt, client, options, cacheContext) {
|
|
|
4728
5430
|
record[field] = { value: toKintoneValue(val, fieldTypes.get(field)) };
|
|
4729
5431
|
}
|
|
4730
5432
|
});
|
|
4731
|
-
|
|
4732
|
-
|
|
5433
|
+
const id = lookupUpsertTarget(targetIndex, rowKeyValues[rowIdx]);
|
|
5434
|
+
if (id !== void 0) {
|
|
4733
5435
|
toUpdate.push({ id, record });
|
|
4734
5436
|
} else {
|
|
4735
5437
|
toInsert.push(record);
|
|
4736
5438
|
}
|
|
4737
|
-
}
|
|
5439
|
+
});
|
|
4738
5440
|
if (options.confirm && toInsert.length + toUpdate.length > 0) {
|
|
4739
5441
|
const total = toInsert.length + toUpdate.length;
|
|
4740
5442
|
const ok = await options.confirm(total, "UPDATE");
|
|
@@ -4754,18 +5456,17 @@ async function executeUpsert(stmt, client, options, cacheContext) {
|
|
|
4754
5456
|
updatedCount: toUpdate.length
|
|
4755
5457
|
};
|
|
4756
5458
|
}
|
|
4757
|
-
async function executeInsertSubtable(stmt, client, _cacheContext) {
|
|
5459
|
+
async function executeInsertSubtable(stmt, client, options, _cacheContext) {
|
|
4758
5460
|
const subtableCode = stmt.subtableCode;
|
|
4759
5461
|
const pidIndex = stmt.fields.indexOf("_pid");
|
|
4760
5462
|
if (pidIndex < 0) {
|
|
4761
5463
|
throw new Error("\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB INSERT \u306B\u306F _pid \u304C\u5FC5\u9808\u3067\u3059");
|
|
4762
5464
|
}
|
|
4763
|
-
const parents = await fetchAll(client.getRecords, stmt.appId, "", [], {
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
}
|
|
5465
|
+
const parents = await fetchAll(client.getRecords, stmt.appId, "", [], {
|
|
5466
|
+
maxRecords: options.maxRecords ?? 1e4,
|
|
5467
|
+
parallel: options.fetchParallel ?? 1
|
|
5468
|
+
});
|
|
5469
|
+
const parentMap = buildParentIdMap(parents);
|
|
4769
5470
|
const insertsByParent = /* @__PURE__ */ new Map();
|
|
4770
5471
|
for (const rowValues of stmt.values) {
|
|
4771
5472
|
const pid = valueToString(rowValues[pidIndex]);
|
|
@@ -4832,8 +5533,9 @@ async function executeUpdateSubtable(stmt, client, options, _cacheContext) {
|
|
|
4832
5533
|
}
|
|
4833
5534
|
byRid.set(t.rowId, updates);
|
|
4834
5535
|
}
|
|
5536
|
+
const parentById = buildParentIdMap(parents);
|
|
4835
5537
|
for (const [pid, updateMap] of updatesByParent.entries()) {
|
|
4836
|
-
const parent =
|
|
5538
|
+
const parent = parentById.get(pid);
|
|
4837
5539
|
if (!parent) continue;
|
|
4838
5540
|
const currentRows = getMutableTableRows(parent, subtableCode);
|
|
4839
5541
|
const payloadRows = currentRows.map((row) => {
|
|
@@ -4873,8 +5575,9 @@ async function executeDeleteSubtable(stmt, client, options, _cacheContext) {
|
|
|
4873
5575
|
if (bucket) bucket.push(t.rowIndex);
|
|
4874
5576
|
else byParent.set(t.parentId, [t.rowIndex]);
|
|
4875
5577
|
}
|
|
5578
|
+
const parentById = buildParentIdMap(parents);
|
|
4876
5579
|
for (const [pid, idxs] of byParent.entries()) {
|
|
4877
|
-
const parent =
|
|
5580
|
+
const parent = parentById.get(pid);
|
|
4878
5581
|
if (!parent) continue;
|
|
4879
5582
|
const rows = getMutableTableRows(parent, subtableCode);
|
|
4880
5583
|
const rm = new Set(idxs);
|
|
@@ -4883,6 +5586,14 @@ async function executeDeleteSubtable(stmt, client, options, _cacheContext) {
|
|
|
4883
5586
|
}
|
|
4884
5587
|
return { type: "DELETE", deletedCount: targets.length };
|
|
4885
5588
|
}
|
|
5589
|
+
function buildParentIdMap(parents) {
|
|
5590
|
+
const map = /* @__PURE__ */ new Map();
|
|
5591
|
+
for (const p of parents) {
|
|
5592
|
+
const pid = String(p["$id"]?.value ?? "");
|
|
5593
|
+
if (pid) map.set(pid, p);
|
|
5594
|
+
}
|
|
5595
|
+
return map;
|
|
5596
|
+
}
|
|
4886
5597
|
function expandRowsForSubtableDml(parents, subtableCode) {
|
|
4887
5598
|
const out = [];
|
|
4888
5599
|
for (const parent of parents) {
|
|
@@ -5018,8 +5729,9 @@ async function executeReorder(stmt, client, options, _cacheContext) {
|
|
|
5018
5729
|
const ok = await options.confirm(targetParentIds.size, "UPDATE");
|
|
5019
5730
|
if (!ok) throw new OperationCancelledError("UPDATE", targetParentIds.size);
|
|
5020
5731
|
}
|
|
5732
|
+
const parentById = buildParentIdMap(parents);
|
|
5021
5733
|
for (const pid of targetParentIds) {
|
|
5022
|
-
const parent =
|
|
5734
|
+
const parent = parentById.get(pid);
|
|
5023
5735
|
if (!parent) continue;
|
|
5024
5736
|
const rows = getMutableTableRows(parent, stmt.subtableCode);
|
|
5025
5737
|
const sortable = rows.map((row, i) => ({ row, i, flat: buildFlatRowForSort(parent, stmt.subtableCode, row, i) }));
|
|
@@ -5067,7 +5779,6 @@ function evalOrderKeyForRow(key, row) {
|
|
|
5067
5779
|
}
|
|
5068
5780
|
}
|
|
5069
5781
|
async function executeUpsertSelect(stmt, client, options, cacheContext) {
|
|
5070
|
-
const maxRecords = options.maxRecords ?? 1e4;
|
|
5071
5782
|
const selectResult = await executeSelect(stmt.select, client, options, cacheContext);
|
|
5072
5783
|
const { rows, columns } = selectResult;
|
|
5073
5784
|
if (columns.length !== stmt.fields.length) {
|
|
@@ -5082,29 +5793,26 @@ async function executeUpsertSelect(stmt, client, options, cacheContext) {
|
|
|
5082
5793
|
}
|
|
5083
5794
|
const toInsert = [];
|
|
5084
5795
|
const toUpdate = [];
|
|
5085
|
-
|
|
5796
|
+
const records = rows.map((row) => {
|
|
5086
5797
|
const record = {};
|
|
5087
5798
|
stmt.fields.forEach((field, i) => {
|
|
5088
5799
|
record[field] = { value: row[columns[i]] ?? "" };
|
|
5089
5800
|
});
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
{
|
|
5101
|
-
);
|
|
5102
|
-
if (existing.length > 0) {
|
|
5103
|
-
toUpdate.push({ id: Number(existing[0]["$id"].value), record });
|
|
5801
|
+
return record;
|
|
5802
|
+
});
|
|
5803
|
+
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
5804
|
+
const rowKeyValues = records.map(
|
|
5805
|
+
(record) => stmt.keyFields.map((key) => String(record[key]?.value ?? ""))
|
|
5806
|
+
);
|
|
5807
|
+
const targetIndex = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeyValues, client, options, fieldTypes);
|
|
5808
|
+
records.forEach((record, rowIdx) => {
|
|
5809
|
+
const id = lookupUpsertTarget(targetIndex, rowKeyValues[rowIdx]);
|
|
5810
|
+
if (id !== void 0) {
|
|
5811
|
+
toUpdate.push({ id, record });
|
|
5104
5812
|
} else {
|
|
5105
5813
|
toInsert.push(record);
|
|
5106
5814
|
}
|
|
5107
|
-
}
|
|
5815
|
+
});
|
|
5108
5816
|
if (options.confirm && toInsert.length + toUpdate.length > 0) {
|
|
5109
5817
|
const total = toInsert.length + toUpdate.length;
|
|
5110
5818
|
const ok = await options.confirm(total, "UPDATE");
|
|
@@ -5155,37 +5863,51 @@ function parseSql(sql) {
|
|
|
5155
5863
|
throw e;
|
|
5156
5864
|
}
|
|
5157
5865
|
}
|
|
5158
|
-
async function resolveSubqueries(where, client, options, cacheContext) {
|
|
5866
|
+
async function resolveSubqueries(where, client, options, cacheContext, cteCache) {
|
|
5867
|
+
const tasks = [];
|
|
5868
|
+
collectSubqueryTasks(where, client, options, cacheContext, tasks, cteCache);
|
|
5869
|
+
await Promise.all(tasks);
|
|
5870
|
+
}
|
|
5871
|
+
function runSubquery(query, client, options, cacheContext, cteCache) {
|
|
5872
|
+
if (cteCache !== void 0 && cteCache.size > 0) {
|
|
5873
|
+
return executeQueryWithCte(query, client, options, cteCache, cacheContext);
|
|
5874
|
+
}
|
|
5875
|
+
return executeSelect(query, client, options, cacheContext);
|
|
5876
|
+
}
|
|
5877
|
+
function collectSubqueryTasks(where, client, options, cacheContext, tasks, cteCache) {
|
|
5159
5878
|
if (where === null) return;
|
|
5160
5879
|
switch (where.type) {
|
|
5161
5880
|
case "BINARY": {
|
|
5162
5881
|
const right = where.right;
|
|
5163
5882
|
if (right.type === "SUBQUERY_IN_LIST") {
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5883
|
+
tasks.push(runSubquery(right.query, client, options, cacheContext, cteCache).then((result) => {
|
|
5884
|
+
const col = right.column ?? (result.columns[0] ?? "");
|
|
5885
|
+
right.resolved = new Set(result.rows.map((r) => r[col] ?? ""));
|
|
5886
|
+
}));
|
|
5168
5887
|
}
|
|
5169
5888
|
if (right.type === "SCALAR_SUBQUERY") {
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5889
|
+
tasks.push(runSubquery(right.query, client, options, cacheContext, cteCache).then((result) => {
|
|
5890
|
+
if (result.rowCount === 0) throw new Error("\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u304C\u5024\u3092\u8FD4\u3057\u307E\u305B\u3093\u3067\u3057\u305F");
|
|
5891
|
+
if (result.rowCount > 1) throw new Error("\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u304C\u8907\u6570\u884C\u3092\u8FD4\u3057\u307E\u3057\u305F\uFF081\u884C\u306E\u307F\u8A31\u53EF\uFF09");
|
|
5892
|
+
const col = result.columns[0] ?? "";
|
|
5893
|
+
right.resolved = result.rows[0]?.[col] ?? "";
|
|
5894
|
+
}));
|
|
5175
5895
|
}
|
|
5176
5896
|
break;
|
|
5177
5897
|
}
|
|
5178
5898
|
case "LOGICAL":
|
|
5179
|
-
|
|
5180
|
-
|
|
5899
|
+
collectSubqueryTasks(where.left, client, options, cacheContext, tasks, cteCache);
|
|
5900
|
+
collectSubqueryTasks(where.right, client, options, cacheContext, tasks, cteCache);
|
|
5181
5901
|
break;
|
|
5182
5902
|
case "NOT":
|
|
5183
5903
|
case "GROUP":
|
|
5184
|
-
|
|
5904
|
+
collectSubqueryTasks(where.expr, client, options, cacheContext, tasks, cteCache);
|
|
5185
5905
|
break;
|
|
5186
5906
|
case "EXISTS": {
|
|
5187
|
-
const
|
|
5188
|
-
|
|
5907
|
+
const node = where;
|
|
5908
|
+
tasks.push(runSubquery(node.query, client, options, cacheContext, cteCache).then((result) => {
|
|
5909
|
+
node.resolved = result.rowCount > 0;
|
|
5910
|
+
}));
|
|
5189
5911
|
break;
|
|
5190
5912
|
}
|
|
5191
5913
|
}
|
|
@@ -5201,19 +5923,83 @@ async function resolveSetSubqueries(assignments, client, options, cacheContext)
|
|
|
5201
5923
|
a.value = { type: "STRING", value: resolved };
|
|
5202
5924
|
}
|
|
5203
5925
|
}
|
|
5204
|
-
async function resolveScalarColumns(columns, client, options, cacheContext) {
|
|
5205
|
-
const
|
|
5926
|
+
async function resolveScalarColumns(columns, client, options, cacheContext, cteCache) {
|
|
5927
|
+
const byQuery = /* @__PURE__ */ new Map();
|
|
5928
|
+
const pending = [];
|
|
5206
5929
|
for (let i = 0; i < columns.length; i++) {
|
|
5207
5930
|
const col = columns[i];
|
|
5208
5931
|
if (col.type !== "SCALAR_SUBQUERY_COL") continue;
|
|
5209
|
-
const
|
|
5210
|
-
|
|
5211
|
-
if (
|
|
5212
|
-
|
|
5213
|
-
|
|
5932
|
+
const key = JSON.stringify(col.query);
|
|
5933
|
+
let promise = byQuery.get(key);
|
|
5934
|
+
if (!promise) {
|
|
5935
|
+
promise = runSubquery(col.query, client, options, cacheContext, cteCache).then((result) => {
|
|
5936
|
+
if (result.rowCount === 0) throw new Error("\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u304C\u5024\u3092\u8FD4\u3057\u307E\u305B\u3093\u3067\u3057\u305F");
|
|
5937
|
+
if (result.rowCount > 1) throw new Error("\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u304C\u8907\u6570\u884C\u3092\u8FD4\u3057\u307E\u3057\u305F\uFF081\u884C\u306E\u307F\u8A31\u53EF\uFF09");
|
|
5938
|
+
const firstCol = result.columns[0] ?? "";
|
|
5939
|
+
return result.rows[0]?.[firstCol] ?? "";
|
|
5940
|
+
});
|
|
5941
|
+
byQuery.set(key, promise);
|
|
5942
|
+
}
|
|
5943
|
+
pending.push([i, promise]);
|
|
5214
5944
|
}
|
|
5945
|
+
const values = await Promise.all(pending.map(([, promise]) => promise));
|
|
5946
|
+
const cache = /* @__PURE__ */ new Map();
|
|
5947
|
+
pending.forEach(([i], idx) => cache.set(i, values[idx]));
|
|
5215
5948
|
return cache;
|
|
5216
5949
|
}
|
|
5950
|
+
function buildBatchExplainPlans(sql) {
|
|
5951
|
+
const statements = parseSqlBatch(sql);
|
|
5952
|
+
const analysis = analyzeBatch(statements);
|
|
5953
|
+
return {
|
|
5954
|
+
statementCount: statements.length,
|
|
5955
|
+
statements: statements.map((stmt, i) => ({
|
|
5956
|
+
index: i,
|
|
5957
|
+
type: analysis.statements[i].statementType,
|
|
5958
|
+
plan: buildBatchStatementPlan(stmt, analysis.statements[i])
|
|
5959
|
+
}))
|
|
5960
|
+
};
|
|
5961
|
+
}
|
|
5962
|
+
function buildBatchStatementPlan(stmt, info) {
|
|
5963
|
+
if (stmt.type === "CREATE_TEMP_TABLE") {
|
|
5964
|
+
return [
|
|
5965
|
+
`CREATE TEMP TABLE ${stmt.name}`,
|
|
5966
|
+
` scope: batch\uFF08\u30D0\u30C3\u30C1\u7D42\u4E86\u6642\u306B\u81EA\u52D5\u7834\u68C4\uFF09`,
|
|
5967
|
+
` rows: \u5B9F\u4F53\u5316\u524D\u306E\u305F\u3081\u4E0D\u660E\uFF08\u4E0A\u9650 ${TEMP_TABLE_MAX_ROWS} \u884C\u3001\u8D85\u904E\u306F\u30A8\u30E9\u30FC\uFF09`,
|
|
5968
|
+
...buildPlanForBatchQuery(stmt.query, info).map((l) => ` ${l}`)
|
|
5969
|
+
];
|
|
5970
|
+
}
|
|
5971
|
+
if (stmt.type === "DROP_TEMP_TABLE") {
|
|
5972
|
+
return [
|
|
5973
|
+
`DROP TEMP TABLE ${stmt.name}`,
|
|
5974
|
+
" \u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u30B9\u30C8\u30A2\u306E\u89E3\u653E\u306E\u307F\uFF08kintone \u30A2\u30AF\u30BB\u30B9\u306A\u3057\uFF09"
|
|
5975
|
+
];
|
|
5976
|
+
}
|
|
5977
|
+
if (stmt.type === "SHOW_APPS") return ["SHOW APPS\uFF08\u30A2\u30D7\u30EA\u4E00\u89A7\u306E\u53D6\u5F97\uFF09"];
|
|
5978
|
+
if (stmt.type === "DESCRIBE") return [`DESCRIBE APP${stmt.appId}\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u5B9A\u7FA9\u306E\u53D6\u5F97\uFF09`];
|
|
5979
|
+
if (stmt.type === "EXPLAIN") return buildPlanForBatchQuery(stmt.query, info);
|
|
5980
|
+
return buildPlanForBatchQuery(stmt, info);
|
|
5981
|
+
}
|
|
5982
|
+
function buildPlanForBatchQuery(query, info) {
|
|
5983
|
+
if (info.tempTablesReferenced.length === 0) {
|
|
5984
|
+
return buildExplainPlan(query);
|
|
5985
|
+
}
|
|
5986
|
+
const lines = [];
|
|
5987
|
+
if (query.type === "INSERT_SELECT") {
|
|
5988
|
+
lines.push(
|
|
5989
|
+
`INSERT INTO APP${query.appId} ... SELECT\uFF08\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u30BD\u30FC\u30B9\u3002\u5B9F\u884C\u6642\u306B\u4EF6\u6570\u78BA\u5B9A \u2192 dmlMaxRows \u9069\u7528\uFF09`
|
|
5990
|
+
);
|
|
5991
|
+
}
|
|
5992
|
+
lines.push(" mode: FULL_SCAN\uFF08\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u53C2\u7167\uFF09");
|
|
5993
|
+
lines.push(
|
|
5994
|
+
` temp: ${info.tempTablesReferenced.join(", ")}\uFF08\u30A4\u30F3\u30E1\u30E2\u30EA\u8D70\u67FB\u3002\u5B9F\u4F53\u5316\u524D\u306E\u305F\u3081\u884C\u6570\u4E0D\u660E\uFF09`
|
|
5995
|
+
);
|
|
5996
|
+
const apps = info.appIds.filter((a) => query.type !== "INSERT_SELECT" || a !== query.appId);
|
|
5997
|
+
if (apps.length > 0) {
|
|
5998
|
+
lines.push(` app: ${apps.map((a) => `APP${a}`).join(", ")}`);
|
|
5999
|
+
}
|
|
6000
|
+
lines.push(" note: \u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u3078\u306E WHERE \u30D7\u30C3\u30B7\u30E5\u30C0\u30A6\u30F3\u306F\u884C\u308F\u308C\u306A\u3044");
|
|
6001
|
+
return lines;
|
|
6002
|
+
}
|
|
5217
6003
|
function executeExplain(stmt) {
|
|
5218
6004
|
const lines = buildExplainPlan(stmt.query);
|
|
5219
6005
|
return {
|
|
@@ -5534,6 +6320,10 @@ function parseSqlStatement(sql) {
|
|
|
5534
6320
|
const tokens = new Lexer(sql).tokenize();
|
|
5535
6321
|
return new Parser(tokens).parse();
|
|
5536
6322
|
}
|
|
6323
|
+
function parseSqlStatements(sql) {
|
|
6324
|
+
const tokens = new Lexer(sql).tokenize();
|
|
6325
|
+
return new Parser(tokens).parseStatements();
|
|
6326
|
+
}
|
|
5537
6327
|
|
|
5538
6328
|
// src/core/displayFormat.ts
|
|
5539
6329
|
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -5619,207 +6409,6 @@ function isSubtableRow(v) {
|
|
|
5619
6409
|
return typeof obj.id === "string" && typeof obj.value === "object" && obj.value !== null;
|
|
5620
6410
|
}
|
|
5621
6411
|
|
|
5622
|
-
// src/cli/nodeKintoneClient.ts
|
|
5623
|
-
function createNodeKintoneClient(baseUrl, tokenResolver) {
|
|
5624
|
-
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
5625
|
-
const apiBasePath = tokenResolver.guestSpaceId && tokenResolver.guestSpaceId > 0 ? `/k/guest/${tokenResolver.guestSpaceId}/v1` : "/k/v1";
|
|
5626
|
-
async function requestJson(path, init, appIdForToken) {
|
|
5627
|
-
const headers = new Headers(init.headers ?? {});
|
|
5628
|
-
if (tokenResolver.auth.type === "token") {
|
|
5629
|
-
headers.set("X-Cybozu-API-Token", tokenResolver.auth.resolveToken(appIdForToken));
|
|
5630
|
-
} else {
|
|
5631
|
-
const credentials = `${tokenResolver.auth.username}:${tokenResolver.auth.password}`;
|
|
5632
|
-
const encoded = Buffer.from(credentials, "utf-8").toString("base64");
|
|
5633
|
-
headers.set("X-Cybozu-Authorization", encoded);
|
|
5634
|
-
}
|
|
5635
|
-
headers.set("Accept", "application/json");
|
|
5636
|
-
const method = String(init.method ?? "GET").toUpperCase();
|
|
5637
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
5638
|
-
headers.set("Content-Type", "application/json");
|
|
5639
|
-
}
|
|
5640
|
-
const timeoutMs = tokenResolver.timeoutMs ?? 3e4;
|
|
5641
|
-
const url = `${normalizedBaseUrl}${path}`;
|
|
5642
|
-
if (tokenResolver.debug) {
|
|
5643
|
-
tokenResolver.log?.(`[debug] request ${String(init.method ?? "GET")} ${url}`);
|
|
5644
|
-
if (tokenResolver.debugHeaders) {
|
|
5645
|
-
const authHeader = headers.get("X-Cybozu-API-Token") ? "X-Cybozu-API-Token=***" : headers.get("X-Cybozu-Authorization") ? "X-Cybozu-Authorization=***" : "Auth=(none)";
|
|
5646
|
-
tokenResolver.log?.(
|
|
5647
|
-
`[debug] request-headers ${authHeader} Content-Type=${headers.get("Content-Type") ?? "(none)"} Accept=${headers.get("Accept") ?? "(none)"}`
|
|
5648
|
-
);
|
|
5649
|
-
}
|
|
5650
|
-
}
|
|
5651
|
-
const res = await fetch(url, {
|
|
5652
|
-
...init,
|
|
5653
|
-
headers,
|
|
5654
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
5655
|
-
});
|
|
5656
|
-
if (!res.ok) {
|
|
5657
|
-
const bodyText = await res.text();
|
|
5658
|
-
if (tokenResolver.debug) {
|
|
5659
|
-
tokenResolver.log?.(`[debug] response status=${res.status} body=${bodyText}`);
|
|
5660
|
-
}
|
|
5661
|
-
throw new Error(`kintone API error ${res.status}: ${bodyText}`);
|
|
5662
|
-
}
|
|
5663
|
-
if (tokenResolver.debug) {
|
|
5664
|
-
tokenResolver.log?.(`[debug] response status=${res.status}`);
|
|
5665
|
-
}
|
|
5666
|
-
return await res.json();
|
|
5667
|
-
}
|
|
5668
|
-
function shouldRetryWithRecordNumberOrder(path, bodyText) {
|
|
5669
|
-
if (!path.includes("/v1/records.json?")) return false;
|
|
5670
|
-
if (!bodyText.includes('"code":"CB_IL02"')) return false;
|
|
5671
|
-
const queryPart = path.split("query=")[1] ?? "";
|
|
5672
|
-
const query = decodeURIComponent(queryPart.split("&")[0] ?? "");
|
|
5673
|
-
if (!query.includes("limit")) return false;
|
|
5674
|
-
if (!query.includes("offset")) return false;
|
|
5675
|
-
if (query.toLowerCase().includes("order by")) return false;
|
|
5676
|
-
return true;
|
|
5677
|
-
}
|
|
5678
|
-
function rewriteQueryWithRecordNumberOrder(path) {
|
|
5679
|
-
const [base, rest] = path.split("query=");
|
|
5680
|
-
if (!rest) return path;
|
|
5681
|
-
const [encodedQuery, ...tail] = rest.split("&");
|
|
5682
|
-
const query = decodeURIComponent(encodedQuery ?? "");
|
|
5683
|
-
const rewritten = `order by \u30EC\u30B3\u30FC\u30C9\u756A\u53F7 asc ${query}`.trim();
|
|
5684
|
-
const nextQuery = encodeURIComponent(rewritten);
|
|
5685
|
-
return `${base}query=${nextQuery}${tail.length > 0 ? `&${tail.join("&")}` : ""}`;
|
|
5686
|
-
}
|
|
5687
|
-
return {
|
|
5688
|
-
async getRecords(params) {
|
|
5689
|
-
const queryPart = `query=${encodeURIComponent(params.query)}`;
|
|
5690
|
-
const appPart = `app=${encodeURIComponent(String(params.app))}`;
|
|
5691
|
-
const fieldParts = params.fields.map((f) => `fields[]=${encodeURIComponent(f)}`);
|
|
5692
|
-
const qs = [appPart, queryPart, ...fieldParts].join("&");
|
|
5693
|
-
if (tokenResolver.debug) {
|
|
5694
|
-
tokenResolver.log?.(
|
|
5695
|
-
`[debug] getRecords app=${params.app} query="${params.query}" fields=${params.fields.length > 0 ? params.fields.join(",") : "(all)"} auth=${tokenResolver.auth.type}`
|
|
5696
|
-
);
|
|
5697
|
-
}
|
|
5698
|
-
const path = `${apiBasePath}/records.json?${qs}`;
|
|
5699
|
-
try {
|
|
5700
|
-
return await requestJson(
|
|
5701
|
-
path,
|
|
5702
|
-
{ method: "GET" },
|
|
5703
|
-
params.app
|
|
5704
|
-
);
|
|
5705
|
-
} catch (err) {
|
|
5706
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5707
|
-
if (!shouldRetryWithRecordNumberOrder(path, msg)) throw err;
|
|
5708
|
-
const retryPath = rewriteQueryWithRecordNumberOrder(path);
|
|
5709
|
-
if (tokenResolver.debug) {
|
|
5710
|
-
tokenResolver.log?.("[debug] retry with fallback query order by \u30EC\u30B3\u30FC\u30C9\u756A\u53F7 asc");
|
|
5711
|
-
}
|
|
5712
|
-
return await requestJson(
|
|
5713
|
-
retryPath,
|
|
5714
|
-
{ method: "GET" },
|
|
5715
|
-
params.app
|
|
5716
|
-
);
|
|
5717
|
-
}
|
|
5718
|
-
},
|
|
5719
|
-
async postRecords(_params) {
|
|
5720
|
-
const res = await requestJson(
|
|
5721
|
-
`${apiBasePath}/records.json`,
|
|
5722
|
-
{
|
|
5723
|
-
method: "POST",
|
|
5724
|
-
body: JSON.stringify({
|
|
5725
|
-
app: _params.app,
|
|
5726
|
-
records: _params.records
|
|
5727
|
-
})
|
|
5728
|
-
},
|
|
5729
|
-
_params.app
|
|
5730
|
-
);
|
|
5731
|
-
return { ids: res.ids };
|
|
5732
|
-
},
|
|
5733
|
-
async putRecords(_params) {
|
|
5734
|
-
await requestJson(
|
|
5735
|
-
`${apiBasePath}/records.json`,
|
|
5736
|
-
{
|
|
5737
|
-
method: "PUT",
|
|
5738
|
-
body: JSON.stringify({
|
|
5739
|
-
app: _params.app,
|
|
5740
|
-
records: _params.records
|
|
5741
|
-
})
|
|
5742
|
-
},
|
|
5743
|
-
_params.app
|
|
5744
|
-
);
|
|
5745
|
-
},
|
|
5746
|
-
async deleteRecords(_params) {
|
|
5747
|
-
await requestJson(
|
|
5748
|
-
`${apiBasePath}/records.json`,
|
|
5749
|
-
{
|
|
5750
|
-
method: "DELETE",
|
|
5751
|
-
body: JSON.stringify({
|
|
5752
|
-
app: _params.app,
|
|
5753
|
-
ids: _params.ids
|
|
5754
|
-
})
|
|
5755
|
-
},
|
|
5756
|
-
_params.app
|
|
5757
|
-
);
|
|
5758
|
-
},
|
|
5759
|
-
async getApps() {
|
|
5760
|
-
const PAGE = 100;
|
|
5761
|
-
const all = [];
|
|
5762
|
-
let offset = 0;
|
|
5763
|
-
while (true) {
|
|
5764
|
-
const qs = new URLSearchParams();
|
|
5765
|
-
qs.set("limit", String(PAGE));
|
|
5766
|
-
qs.set("offset", String(offset));
|
|
5767
|
-
const res = await requestJson(
|
|
5768
|
-
`${apiBasePath}/apps.json?${qs.toString()}`,
|
|
5769
|
-
{ method: "GET" },
|
|
5770
|
-
0
|
|
5771
|
-
);
|
|
5772
|
-
for (const app of res.apps) {
|
|
5773
|
-
all.push({
|
|
5774
|
-
appId: Number(app.appId),
|
|
5775
|
-
name: app.name,
|
|
5776
|
-
description: app.description
|
|
5777
|
-
});
|
|
5778
|
-
}
|
|
5779
|
-
if (res.apps.length < PAGE) break;
|
|
5780
|
-
offset += PAGE;
|
|
5781
|
-
}
|
|
5782
|
-
return all;
|
|
5783
|
-
},
|
|
5784
|
-
async getFields(appId) {
|
|
5785
|
-
const qs = new URLSearchParams();
|
|
5786
|
-
qs.set("app", String(appId));
|
|
5787
|
-
const res = await requestJson(
|
|
5788
|
-
`${apiBasePath}/app/form/fields.json?${qs.toString()}`,
|
|
5789
|
-
{ method: "GET" },
|
|
5790
|
-
appId
|
|
5791
|
-
);
|
|
5792
|
-
return Object.values(res.properties).map((f) => ({
|
|
5793
|
-
code: f.code,
|
|
5794
|
-
label: f.label,
|
|
5795
|
-
fieldType: f.type,
|
|
5796
|
-
optionOrder: toOptionOrderMap(f.options),
|
|
5797
|
-
sortKind: detectSortKind(f.type, f.format)
|
|
5798
|
-
}));
|
|
5799
|
-
}
|
|
5800
|
-
};
|
|
5801
|
-
}
|
|
5802
|
-
function toOptionOrderMap(options) {
|
|
5803
|
-
if (!options || typeof options !== "object") return void 0;
|
|
5804
|
-
const order = {};
|
|
5805
|
-
let hasAny = false;
|
|
5806
|
-
for (const [label, meta] of Object.entries(options)) {
|
|
5807
|
-
const n = Number(meta?.index);
|
|
5808
|
-
if (!Number.isFinite(n)) continue;
|
|
5809
|
-
order[label] = n;
|
|
5810
|
-
hasAny = true;
|
|
5811
|
-
}
|
|
5812
|
-
return hasAny ? order : void 0;
|
|
5813
|
-
}
|
|
5814
|
-
function detectSortKind(fieldType, calcFormat) {
|
|
5815
|
-
if (fieldType === "NUMBER" || fieldType === "RECORD_NUMBER") return "number";
|
|
5816
|
-
if (fieldType === "CALC") {
|
|
5817
|
-
if (calcFormat === "NUMBER" || calcFormat === "NUMBER_DIGIT") return "number";
|
|
5818
|
-
return "string";
|
|
5819
|
-
}
|
|
5820
|
-
return void 0;
|
|
5821
|
-
}
|
|
5822
|
-
|
|
5823
6412
|
// src/node/appProfiles.ts
|
|
5824
6413
|
var import_fs = require("fs");
|
|
5825
6414
|
function parseTokenMap(raw) {
|
|
@@ -5940,119 +6529,450 @@ function collectAppProfileTokens(sql) {
|
|
|
5940
6529
|
i += 2;
|
|
5941
6530
|
break;
|
|
5942
6531
|
}
|
|
5943
|
-
i++;
|
|
6532
|
+
i++;
|
|
6533
|
+
}
|
|
6534
|
+
continue;
|
|
6535
|
+
}
|
|
6536
|
+
const parsed = tryParseAppProfileToken(sql, i);
|
|
6537
|
+
if (!parsed) {
|
|
6538
|
+
i++;
|
|
6539
|
+
continue;
|
|
6540
|
+
}
|
|
6541
|
+
tokens.push(parsed);
|
|
6542
|
+
i = parsed.fullEnd;
|
|
6543
|
+
}
|
|
6544
|
+
return tokens;
|
|
6545
|
+
}
|
|
6546
|
+
function nextVirtualAppId(used) {
|
|
6547
|
+
let id = 9e8;
|
|
6548
|
+
while (used.has(id)) id++;
|
|
6549
|
+
used.add(id);
|
|
6550
|
+
return id;
|
|
6551
|
+
}
|
|
6552
|
+
function normalizeSqlAppProfiles(sql, defaultProfile = "dev") {
|
|
6553
|
+
const tokens = collectAppProfileTokens(sql);
|
|
6554
|
+
const hasProfileSyntax = tokens.some((t) => t.profile !== null);
|
|
6555
|
+
const profilesByApp = /* @__PURE__ */ new Map();
|
|
6556
|
+
const normalizedProfile = (profile) => profile ?? defaultProfile;
|
|
6557
|
+
for (const t of tokens) {
|
|
6558
|
+
const p = normalizedProfile(t.profile);
|
|
6559
|
+
let set = profilesByApp.get(t.appId);
|
|
6560
|
+
if (!set) {
|
|
6561
|
+
set = /* @__PURE__ */ new Set();
|
|
6562
|
+
profilesByApp.set(t.appId, set);
|
|
6563
|
+
}
|
|
6564
|
+
set.add(p.toLowerCase());
|
|
6565
|
+
}
|
|
6566
|
+
const usedAppIds = new Set(tokens.map((t) => t.appId));
|
|
6567
|
+
const pairToMapped = /* @__PURE__ */ new Map();
|
|
6568
|
+
const appBindingByMappedApp = /* @__PURE__ */ new Map();
|
|
6569
|
+
for (const [appId, pSet] of profilesByApp.entries()) {
|
|
6570
|
+
const profiles = [...pSet].sort();
|
|
6571
|
+
if (profiles.length <= 1) continue;
|
|
6572
|
+
for (const pLower of profiles) {
|
|
6573
|
+
const mapped = nextVirtualAppId(usedAppIds);
|
|
6574
|
+
pairToMapped.set(`${appId}@${pLower}`, mapped);
|
|
6575
|
+
appBindingByMappedApp.set(mapped, { appId, profile: pLower });
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
const out = [];
|
|
6579
|
+
let cursor = 0;
|
|
6580
|
+
for (const t of tokens) {
|
|
6581
|
+
const p = normalizedProfile(t.profile);
|
|
6582
|
+
const pLower = p.toLowerCase();
|
|
6583
|
+
const mapped = pairToMapped.get(`${t.appId}@${pLower}`) ?? t.appId;
|
|
6584
|
+
appBindingByMappedApp.set(mapped, { appId: t.appId, profile: pLower });
|
|
6585
|
+
out.push(sql.slice(cursor, t.start));
|
|
6586
|
+
out.push(sql.slice(t.start, t.digitStart));
|
|
6587
|
+
out.push(String(mapped));
|
|
6588
|
+
out.push(sql.slice(t.digitEnd, t.appEnd));
|
|
6589
|
+
cursor = t.fullEnd;
|
|
6590
|
+
}
|
|
6591
|
+
out.push(sql.slice(cursor));
|
|
6592
|
+
return {
|
|
6593
|
+
normalizedSql: out.join(""),
|
|
6594
|
+
hasProfileSyntax,
|
|
6595
|
+
appBindingByMappedApp
|
|
6596
|
+
};
|
|
6597
|
+
}
|
|
6598
|
+
function buildCacheContext(defaultProfile, appBindingByMappedApp) {
|
|
6599
|
+
if (appBindingByMappedApp.size === 0) return `default:${defaultProfile.toLowerCase()}`;
|
|
6600
|
+
const pairs = [...appBindingByMappedApp.entries()].sort((a, b) => a[0] - b[0]).map(([mappedAppId, b]) => `M${mappedAppId}=APP${b.appId}@${b.profile}`);
|
|
6601
|
+
return `apps:${pairs.join(",")}`;
|
|
6602
|
+
}
|
|
6603
|
+
function formatResolvedAppProfiles(sql, defaultProfile) {
|
|
6604
|
+
const parsed = normalizeSqlAppProfiles(sql, defaultProfile);
|
|
6605
|
+
if (parsed.appBindingByMappedApp.size === 0) return "(none)";
|
|
6606
|
+
return [...parsed.appBindingByMappedApp.values()].map((b) => `APP${b.appId}->${b.profile}`).join(", ");
|
|
6607
|
+
}
|
|
6608
|
+
|
|
6609
|
+
// src/cli/consoleInput.ts
|
|
6610
|
+
function decideConsoleInput(buffer, line) {
|
|
6611
|
+
const t = line.trim();
|
|
6612
|
+
if (t.startsWith(":")) return { kind: "meta" };
|
|
6613
|
+
const newBuffer = buffer.length > 0 ? `${buffer}
|
|
6614
|
+
${line}` : line;
|
|
6615
|
+
if (newBuffer.trim().length === 0) return { kind: "ignore" };
|
|
6616
|
+
if (isBatchConstruction(newBuffer)) return { kind: "continue", buffer: newBuffer };
|
|
6617
|
+
if (!t.endsWith(";")) return { kind: "continue", buffer: newBuffer };
|
|
6618
|
+
const parsed = tryParseStatements(newBuffer);
|
|
6619
|
+
if (parsed.kind === "ok") {
|
|
6620
|
+
if (parsed.count === 0) return { kind: "ignore" };
|
|
6621
|
+
if (parsed.hasTempTable) return { kind: "continue", buffer: newBuffer };
|
|
6622
|
+
return parsed.count === 1 ? { kind: "execute-single", sql: newBuffer } : { kind: "execute-batch", sql: newBuffer };
|
|
6623
|
+
}
|
|
6624
|
+
if (parsed.continuable) return { kind: "continue", buffer: newBuffer };
|
|
6625
|
+
return { kind: "error", message: parsed.message };
|
|
6626
|
+
}
|
|
6627
|
+
function decideRun(buffer) {
|
|
6628
|
+
if (buffer.trim().length === 0) {
|
|
6629
|
+
return { kind: "error", message: "ArgumentError: input buffer is empty (nothing to :run)" };
|
|
6630
|
+
}
|
|
6631
|
+
const parsed = tryParseStatements(buffer);
|
|
6632
|
+
if (parsed.kind === "fail") return { kind: "error", message: parsed.message };
|
|
6633
|
+
return { kind: "execute-batch", sql: buffer };
|
|
6634
|
+
}
|
|
6635
|
+
function isBatchConstruction(buffer) {
|
|
6636
|
+
return /^create\s+temp\s+table\b/i.test(stripLeadingCommentsAndWs(buffer));
|
|
6637
|
+
}
|
|
6638
|
+
function stripLeadingCommentsAndWs(sql) {
|
|
6639
|
+
let s = sql;
|
|
6640
|
+
while (true) {
|
|
6641
|
+
const before = s;
|
|
6642
|
+
s = s.replace(/^\s+/, "");
|
|
6643
|
+
s = s.replace(/^--[^\n]*(\n|$)/, "");
|
|
6644
|
+
s = s.replace(/^\/\*[\s\S]*?\*\//, "");
|
|
6645
|
+
if (s === before) return s;
|
|
6646
|
+
}
|
|
6647
|
+
}
|
|
6648
|
+
function toParseInput(sql) {
|
|
6649
|
+
try {
|
|
6650
|
+
return normalizeSqlAppProfiles(sql, "console").normalizedSql;
|
|
6651
|
+
} catch {
|
|
6652
|
+
return sql;
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
function tryParseStatements(sql) {
|
|
6656
|
+
try {
|
|
6657
|
+
const stmts = new Parser(new Lexer(toParseInput(sql)).tokenize()).parseStatements();
|
|
6658
|
+
return {
|
|
6659
|
+
kind: "ok",
|
|
6660
|
+
count: stmts.length,
|
|
6661
|
+
hasTempTable: stmts.some(
|
|
6662
|
+
(s) => s.type === "CREATE_TEMP_TABLE" || s.type === "DROP_TEMP_TABLE"
|
|
6663
|
+
)
|
|
6664
|
+
};
|
|
6665
|
+
} catch (e) {
|
|
6666
|
+
if (e instanceof LexError) {
|
|
6667
|
+
return { kind: "fail", continuable: e.unterminated, message: e.message };
|
|
6668
|
+
}
|
|
6669
|
+
if (e instanceof ParseError) {
|
|
6670
|
+
return { kind: "fail", continuable: e.token.kind === "EOF" /* EOF */, message: e.message };
|
|
6671
|
+
}
|
|
6672
|
+
throw e;
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6675
|
+
|
|
6676
|
+
// src/api/requestGate.ts
|
|
6677
|
+
var DEFAULT_MAX_CONCURRENT = 10;
|
|
6678
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
6679
|
+
var DEFAULT_BASE_DELAY_MS = 500;
|
|
6680
|
+
var DEFAULT_MAX_DELAY_MS = 8e3;
|
|
6681
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([408, 429, 502, 503, 504]);
|
|
6682
|
+
function isRetryableError(err) {
|
|
6683
|
+
if (!(err instanceof Error)) return false;
|
|
6684
|
+
const status = err.message.match(/^kintone API error (\d{3}):/);
|
|
6685
|
+
if (status) return RETRYABLE_STATUSES.has(Number(status[1]));
|
|
6686
|
+
if (err.name === "AbortError" || err.name === "TimeoutError") return true;
|
|
6687
|
+
if (/fetch failed/i.test(err.message)) return true;
|
|
6688
|
+
return false;
|
|
6689
|
+
}
|
|
6690
|
+
var RequestGate = class {
|
|
6691
|
+
constructor(options = {}) {
|
|
6692
|
+
this.active = 0;
|
|
6693
|
+
this.waiters = [];
|
|
6694
|
+
this.maxConcurrent = clampInt(options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT, 1, 50);
|
|
6695
|
+
this.maxRetries = clampInt(options.maxRetries ?? DEFAULT_MAX_RETRIES, 0, 10);
|
|
6696
|
+
this.baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
6697
|
+
this.maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
6698
|
+
this.sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
6699
|
+
this.random = options.random ?? Math.random;
|
|
6700
|
+
}
|
|
6701
|
+
/** 現在の同時実行数(テスト・診断用) */
|
|
6702
|
+
get activeCount() {
|
|
6703
|
+
return this.active;
|
|
6704
|
+
}
|
|
6705
|
+
get limit() {
|
|
6706
|
+
return this.maxConcurrent;
|
|
6707
|
+
}
|
|
6708
|
+
/** GET 系: セマフォ + リトライ付きで実行する */
|
|
6709
|
+
async runReadOnly(fn) {
|
|
6710
|
+
let attempt = 0;
|
|
6711
|
+
while (true) {
|
|
6712
|
+
try {
|
|
6713
|
+
return await this.withSlot(fn);
|
|
6714
|
+
} catch (err) {
|
|
6715
|
+
if (attempt >= this.maxRetries || !isRetryableError(err)) throw err;
|
|
6716
|
+
await this.sleep(this.backoffDelay(attempt));
|
|
6717
|
+
attempt += 1;
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
/** 書き込み系: セマフォのみ(リトライしない — 二重実行防止) */
|
|
6722
|
+
async runMutation(fn) {
|
|
6723
|
+
return this.withSlot(fn);
|
|
6724
|
+
}
|
|
6725
|
+
async withSlot(fn) {
|
|
6726
|
+
await this.acquire();
|
|
6727
|
+
try {
|
|
6728
|
+
return await fn();
|
|
6729
|
+
} finally {
|
|
6730
|
+
this.release();
|
|
6731
|
+
}
|
|
6732
|
+
}
|
|
6733
|
+
async acquire() {
|
|
6734
|
+
if (this.active < this.maxConcurrent) {
|
|
6735
|
+
this.active += 1;
|
|
6736
|
+
return;
|
|
6737
|
+
}
|
|
6738
|
+
await new Promise((resolve2) => this.waiters.push(resolve2));
|
|
6739
|
+
this.active += 1;
|
|
6740
|
+
}
|
|
6741
|
+
release() {
|
|
6742
|
+
this.active -= 1;
|
|
6743
|
+
const next = this.waiters.shift();
|
|
6744
|
+
if (next) next();
|
|
6745
|
+
}
|
|
6746
|
+
/** 指数バックオフ + ジッタ(attempt: 0 始まり) */
|
|
6747
|
+
backoffDelay(attempt) {
|
|
6748
|
+
const base = Math.min(this.baseDelayMs * 2 ** attempt, this.maxDelayMs);
|
|
6749
|
+
const jitter = 1 + (this.random() - 0.5) * 0.5;
|
|
6750
|
+
return Math.round(base * jitter);
|
|
6751
|
+
}
|
|
6752
|
+
};
|
|
6753
|
+
function withRequestGate(client, gate) {
|
|
6754
|
+
return {
|
|
6755
|
+
getRecords: (params) => gate.runReadOnly(() => client.getRecords(params)),
|
|
6756
|
+
getApps: () => gate.runReadOnly(() => client.getApps()),
|
|
6757
|
+
getFields: (appId) => gate.runReadOnly(() => client.getFields(appId)),
|
|
6758
|
+
postRecords: (params) => gate.runMutation(() => client.postRecords(params)),
|
|
6759
|
+
putRecords: (params) => gate.runMutation(() => client.putRecords(params)),
|
|
6760
|
+
deleteRecords: (params) => gate.runMutation(() => client.deleteRecords(params))
|
|
6761
|
+
};
|
|
6762
|
+
}
|
|
6763
|
+
var globalGate = null;
|
|
6764
|
+
function getGlobalRequestGate(limitHint) {
|
|
6765
|
+
if (globalGate === null) {
|
|
6766
|
+
const envValue = Number(process.env.KSQL_MAX_CONCURRENT);
|
|
6767
|
+
const limit = Number.isInteger(envValue) && envValue > 0 ? envValue : limitHint;
|
|
6768
|
+
globalGate = new RequestGate({ maxConcurrent: limit });
|
|
6769
|
+
}
|
|
6770
|
+
return globalGate;
|
|
6771
|
+
}
|
|
6772
|
+
function clampInt(v, min, max) {
|
|
6773
|
+
if (!Number.isFinite(v)) return min;
|
|
6774
|
+
return Math.max(min, Math.min(max, Math.trunc(v)));
|
|
6775
|
+
}
|
|
6776
|
+
|
|
6777
|
+
// src/cli/nodeKintoneClient.ts
|
|
6778
|
+
function createNodeKintoneClient(baseUrl, tokenResolver) {
|
|
6779
|
+
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
|
|
6780
|
+
const apiBasePath = tokenResolver.guestSpaceId && tokenResolver.guestSpaceId > 0 ? `/k/guest/${tokenResolver.guestSpaceId}/v1` : "/k/v1";
|
|
6781
|
+
async function requestJson(path, init, appIdForToken) {
|
|
6782
|
+
const headers = new Headers(init.headers ?? {});
|
|
6783
|
+
if (tokenResolver.auth.type === "token") {
|
|
6784
|
+
headers.set("X-Cybozu-API-Token", tokenResolver.auth.resolveToken(appIdForToken));
|
|
6785
|
+
} else {
|
|
6786
|
+
const credentials = `${tokenResolver.auth.username}:${tokenResolver.auth.password}`;
|
|
6787
|
+
const encoded = Buffer.from(credentials, "utf-8").toString("base64");
|
|
6788
|
+
headers.set("X-Cybozu-Authorization", encoded);
|
|
6789
|
+
}
|
|
6790
|
+
headers.set("Accept", "application/json");
|
|
6791
|
+
const method = String(init.method ?? "GET").toUpperCase();
|
|
6792
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
6793
|
+
headers.set("Content-Type", "application/json");
|
|
6794
|
+
}
|
|
6795
|
+
const timeoutMs = tokenResolver.timeoutMs ?? 3e4;
|
|
6796
|
+
const url = `${normalizedBaseUrl}${path}`;
|
|
6797
|
+
if (tokenResolver.debug) {
|
|
6798
|
+
tokenResolver.log?.(`[debug] request ${String(init.method ?? "GET")} ${url}`);
|
|
6799
|
+
if (tokenResolver.debugHeaders) {
|
|
6800
|
+
const authHeader = headers.get("X-Cybozu-API-Token") ? "X-Cybozu-API-Token=***" : headers.get("X-Cybozu-Authorization") ? "X-Cybozu-Authorization=***" : "Auth=(none)";
|
|
6801
|
+
tokenResolver.log?.(
|
|
6802
|
+
`[debug] request-headers ${authHeader} Content-Type=${headers.get("Content-Type") ?? "(none)"} Accept=${headers.get("Accept") ?? "(none)"}`
|
|
6803
|
+
);
|
|
6804
|
+
}
|
|
6805
|
+
}
|
|
6806
|
+
const res = await fetch(url, {
|
|
6807
|
+
...init,
|
|
6808
|
+
headers,
|
|
6809
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
6810
|
+
});
|
|
6811
|
+
if (!res.ok) {
|
|
6812
|
+
const bodyText = await res.text();
|
|
6813
|
+
if (tokenResolver.debug) {
|
|
6814
|
+
tokenResolver.log?.(`[debug] response status=${res.status} body=${bodyText}`);
|
|
6815
|
+
}
|
|
6816
|
+
throw new Error(`kintone API error ${res.status}: ${bodyText}`);
|
|
6817
|
+
}
|
|
6818
|
+
if (tokenResolver.debug) {
|
|
6819
|
+
tokenResolver.log?.(`[debug] response status=${res.status}`);
|
|
6820
|
+
}
|
|
6821
|
+
return await res.json();
|
|
6822
|
+
}
|
|
6823
|
+
function shouldRetryWithRecordNumberOrder(path, bodyText) {
|
|
6824
|
+
if (!path.includes("/v1/records.json?")) return false;
|
|
6825
|
+
if (!bodyText.includes('"code":"CB_IL02"')) return false;
|
|
6826
|
+
const queryPart = path.split("query=")[1] ?? "";
|
|
6827
|
+
const query = decodeURIComponent(queryPart.split("&")[0] ?? "");
|
|
6828
|
+
if (!query.includes("limit")) return false;
|
|
6829
|
+
if (!query.includes("offset")) return false;
|
|
6830
|
+
if (query.toLowerCase().includes("order by")) return false;
|
|
6831
|
+
return true;
|
|
6832
|
+
}
|
|
6833
|
+
function rewriteQueryWithRecordNumberOrder(path) {
|
|
6834
|
+
const [base, rest] = path.split("query=");
|
|
6835
|
+
if (!rest) return path;
|
|
6836
|
+
const [encodedQuery, ...tail] = rest.split("&");
|
|
6837
|
+
const query = decodeURIComponent(encodedQuery ?? "");
|
|
6838
|
+
const rewritten = `order by \u30EC\u30B3\u30FC\u30C9\u756A\u53F7 asc ${query}`.trim();
|
|
6839
|
+
const nextQuery = encodeURIComponent(rewritten);
|
|
6840
|
+
return `${base}query=${nextQuery}${tail.length > 0 ? `&${tail.join("&")}` : ""}`;
|
|
6841
|
+
}
|
|
6842
|
+
return {
|
|
6843
|
+
async getRecords(params) {
|
|
6844
|
+
const queryPart = `query=${encodeURIComponent(params.query)}`;
|
|
6845
|
+
const appPart = `app=${encodeURIComponent(String(params.app))}`;
|
|
6846
|
+
const fieldParts = params.fields.map((f) => `fields[]=${encodeURIComponent(f)}`);
|
|
6847
|
+
const qs = [appPart, queryPart, ...fieldParts].join("&");
|
|
6848
|
+
if (tokenResolver.debug) {
|
|
6849
|
+
tokenResolver.log?.(
|
|
6850
|
+
`[debug] getRecords app=${params.app} query="${params.query}" fields=${params.fields.length > 0 ? params.fields.join(",") : "(all)"} auth=${tokenResolver.auth.type}`
|
|
6851
|
+
);
|
|
6852
|
+
}
|
|
6853
|
+
const path = `${apiBasePath}/records.json?${qs}`;
|
|
6854
|
+
try {
|
|
6855
|
+
return await requestJson(
|
|
6856
|
+
path,
|
|
6857
|
+
{ method: "GET" },
|
|
6858
|
+
params.app
|
|
6859
|
+
);
|
|
6860
|
+
} catch (err) {
|
|
6861
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6862
|
+
if (!shouldRetryWithRecordNumberOrder(path, msg)) throw err;
|
|
6863
|
+
const retryPath = rewriteQueryWithRecordNumberOrder(path);
|
|
6864
|
+
if (tokenResolver.debug) {
|
|
6865
|
+
tokenResolver.log?.("[debug] retry with fallback query order by \u30EC\u30B3\u30FC\u30C9\u756A\u53F7 asc");
|
|
6866
|
+
}
|
|
6867
|
+
return await requestJson(
|
|
6868
|
+
retryPath,
|
|
6869
|
+
{ method: "GET" },
|
|
6870
|
+
params.app
|
|
6871
|
+
);
|
|
6872
|
+
}
|
|
6873
|
+
},
|
|
6874
|
+
async postRecords(_params) {
|
|
6875
|
+
const res = await requestJson(
|
|
6876
|
+
`${apiBasePath}/records.json`,
|
|
6877
|
+
{
|
|
6878
|
+
method: "POST",
|
|
6879
|
+
body: JSON.stringify({
|
|
6880
|
+
app: _params.app,
|
|
6881
|
+
records: _params.records
|
|
6882
|
+
})
|
|
6883
|
+
},
|
|
6884
|
+
_params.app
|
|
6885
|
+
);
|
|
6886
|
+
return { ids: res.ids };
|
|
6887
|
+
},
|
|
6888
|
+
async putRecords(_params) {
|
|
6889
|
+
await requestJson(
|
|
6890
|
+
`${apiBasePath}/records.json`,
|
|
6891
|
+
{
|
|
6892
|
+
method: "PUT",
|
|
6893
|
+
body: JSON.stringify({
|
|
6894
|
+
app: _params.app,
|
|
6895
|
+
records: _params.records
|
|
6896
|
+
})
|
|
6897
|
+
},
|
|
6898
|
+
_params.app
|
|
6899
|
+
);
|
|
6900
|
+
},
|
|
6901
|
+
async deleteRecords(_params) {
|
|
6902
|
+
await requestJson(
|
|
6903
|
+
`${apiBasePath}/records.json`,
|
|
6904
|
+
{
|
|
6905
|
+
method: "DELETE",
|
|
6906
|
+
body: JSON.stringify({
|
|
6907
|
+
app: _params.app,
|
|
6908
|
+
ids: _params.ids
|
|
6909
|
+
})
|
|
6910
|
+
},
|
|
6911
|
+
_params.app
|
|
6912
|
+
);
|
|
6913
|
+
},
|
|
6914
|
+
async getApps() {
|
|
6915
|
+
const PAGE = 100;
|
|
6916
|
+
const all = [];
|
|
6917
|
+
let offset = 0;
|
|
6918
|
+
while (true) {
|
|
6919
|
+
const qs = new URLSearchParams();
|
|
6920
|
+
qs.set("limit", String(PAGE));
|
|
6921
|
+
qs.set("offset", String(offset));
|
|
6922
|
+
const res = await requestJson(
|
|
6923
|
+
`${apiBasePath}/apps.json?${qs.toString()}`,
|
|
6924
|
+
{ method: "GET" },
|
|
6925
|
+
0
|
|
6926
|
+
);
|
|
6927
|
+
for (const app of res.apps) {
|
|
6928
|
+
all.push({
|
|
6929
|
+
appId: Number(app.appId),
|
|
6930
|
+
name: app.name,
|
|
6931
|
+
description: app.description
|
|
6932
|
+
});
|
|
6933
|
+
}
|
|
6934
|
+
if (res.apps.length < PAGE) break;
|
|
6935
|
+
offset += PAGE;
|
|
5944
6936
|
}
|
|
5945
|
-
|
|
5946
|
-
}
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
}
|
|
5963
|
-
function normalizeSqlAppProfiles(sql, defaultProfile = "dev") {
|
|
5964
|
-
const tokens = collectAppProfileTokens(sql);
|
|
5965
|
-
const hasProfileSyntax = tokens.some((t) => t.profile !== null);
|
|
5966
|
-
const profilesByApp = /* @__PURE__ */ new Map();
|
|
5967
|
-
const normalizedProfile = (profile) => profile ?? defaultProfile;
|
|
5968
|
-
for (const t of tokens) {
|
|
5969
|
-
const p = normalizedProfile(t.profile);
|
|
5970
|
-
let set = profilesByApp.get(t.appId);
|
|
5971
|
-
if (!set) {
|
|
5972
|
-
set = /* @__PURE__ */ new Set();
|
|
5973
|
-
profilesByApp.set(t.appId, set);
|
|
5974
|
-
}
|
|
5975
|
-
set.add(p.toLowerCase());
|
|
5976
|
-
}
|
|
5977
|
-
const usedAppIds = new Set(tokens.map((t) => t.appId));
|
|
5978
|
-
const pairToMapped = /* @__PURE__ */ new Map();
|
|
5979
|
-
const appBindingByMappedApp = /* @__PURE__ */ new Map();
|
|
5980
|
-
for (const [appId, pSet] of profilesByApp.entries()) {
|
|
5981
|
-
const profiles = [...pSet].sort();
|
|
5982
|
-
if (profiles.length <= 1) continue;
|
|
5983
|
-
for (const pLower of profiles) {
|
|
5984
|
-
const mapped = nextVirtualAppId(usedAppIds);
|
|
5985
|
-
pairToMapped.set(`${appId}@${pLower}`, mapped);
|
|
5986
|
-
appBindingByMappedApp.set(mapped, { appId, profile: pLower });
|
|
6937
|
+
return all;
|
|
6938
|
+
},
|
|
6939
|
+
async getFields(appId) {
|
|
6940
|
+
const qs = new URLSearchParams();
|
|
6941
|
+
qs.set("app", String(appId));
|
|
6942
|
+
const res = await requestJson(
|
|
6943
|
+
`${apiBasePath}/app/form/fields.json?${qs.toString()}`,
|
|
6944
|
+
{ method: "GET" },
|
|
6945
|
+
appId
|
|
6946
|
+
);
|
|
6947
|
+
return Object.values(res.properties).map((f) => ({
|
|
6948
|
+
code: f.code,
|
|
6949
|
+
label: f.label,
|
|
6950
|
+
fieldType: f.type,
|
|
6951
|
+
optionOrder: toOptionOrderMap(f.options),
|
|
6952
|
+
sortKind: detectSortKind(f.type, f.format)
|
|
6953
|
+
}));
|
|
5987
6954
|
}
|
|
5988
|
-
}
|
|
5989
|
-
const out = [];
|
|
5990
|
-
let cursor = 0;
|
|
5991
|
-
for (const t of tokens) {
|
|
5992
|
-
const p = normalizedProfile(t.profile);
|
|
5993
|
-
const pLower = p.toLowerCase();
|
|
5994
|
-
const mapped = pairToMapped.get(`${t.appId}@${pLower}`) ?? t.appId;
|
|
5995
|
-
appBindingByMappedApp.set(mapped, { appId: t.appId, profile: pLower });
|
|
5996
|
-
out.push(sql.slice(cursor, t.start));
|
|
5997
|
-
out.push(sql.slice(t.start, t.digitStart));
|
|
5998
|
-
out.push(String(mapped));
|
|
5999
|
-
out.push(sql.slice(t.digitEnd, t.appEnd));
|
|
6000
|
-
cursor = t.fullEnd;
|
|
6001
|
-
}
|
|
6002
|
-
out.push(sql.slice(cursor));
|
|
6003
|
-
return {
|
|
6004
|
-
normalizedSql: out.join(""),
|
|
6005
|
-
hasProfileSyntax,
|
|
6006
|
-
appBindingByMappedApp
|
|
6007
6955
|
};
|
|
6008
6956
|
}
|
|
6009
|
-
function
|
|
6010
|
-
if (
|
|
6011
|
-
const
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
}
|
|
6019
|
-
|
|
6020
|
-
// src/node/dmlGuard.ts
|
|
6021
|
-
function getStatementType(stmt) {
|
|
6022
|
-
if (!stmt || typeof stmt !== "object") return "UNKNOWN";
|
|
6023
|
-
const obj = stmt;
|
|
6024
|
-
return typeof obj.type === "string" ? obj.type : "UNKNOWN";
|
|
6025
|
-
}
|
|
6026
|
-
function isDmlType(type) {
|
|
6027
|
-
return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
|
|
6028
|
-
}
|
|
6029
|
-
function hasWhereClause(stmt) {
|
|
6030
|
-
if (!stmt || typeof stmt !== "object") return false;
|
|
6031
|
-
const obj = stmt;
|
|
6032
|
-
return obj.where !== null && obj.where !== void 0;
|
|
6033
|
-
}
|
|
6034
|
-
function isNoFromSelectStatement(stmt) {
|
|
6035
|
-
if (!stmt || typeof stmt !== "object") return false;
|
|
6036
|
-
const obj = stmt;
|
|
6037
|
-
return obj.type === "SELECT" && obj.from?.appId === 0 && obj.from?.cteName === "__NO_FROM__";
|
|
6038
|
-
}
|
|
6039
|
-
function getInsertValuesCount(stmt) {
|
|
6040
|
-
if (!stmt || typeof stmt !== "object") return null;
|
|
6041
|
-
const obj = stmt;
|
|
6042
|
-
if (obj.type !== "INSERT") return null;
|
|
6043
|
-
return Array.isArray(obj.values) ? obj.values.length : null;
|
|
6044
|
-
}
|
|
6045
|
-
function collectDmlTargetFields(stmt) {
|
|
6046
|
-
if (!stmt || typeof stmt !== "object") return [];
|
|
6047
|
-
const obj = stmt;
|
|
6048
|
-
if (!obj.type) return [];
|
|
6049
|
-
if (obj.type === "UPDATE") {
|
|
6050
|
-
return (obj.assignments ?? []).map((a) => a.field).filter((f) => Boolean(f));
|
|
6957
|
+
function toOptionOrderMap(options) {
|
|
6958
|
+
if (!options || typeof options !== "object") return void 0;
|
|
6959
|
+
const order = {};
|
|
6960
|
+
let hasAny = false;
|
|
6961
|
+
for (const [label, meta] of Object.entries(options)) {
|
|
6962
|
+
const n = Number(meta?.index);
|
|
6963
|
+
if (!Number.isFinite(n)) continue;
|
|
6964
|
+
order[label] = n;
|
|
6965
|
+
hasAny = true;
|
|
6051
6966
|
}
|
|
6052
|
-
|
|
6053
|
-
|
|
6967
|
+
return hasAny ? order : void 0;
|
|
6968
|
+
}
|
|
6969
|
+
function detectSortKind(fieldType, calcFormat) {
|
|
6970
|
+
if (fieldType === "NUMBER" || fieldType === "RECORD_NUMBER") return "number";
|
|
6971
|
+
if (fieldType === "CALC") {
|
|
6972
|
+
if (calcFormat === "NUMBER" || calcFormat === "NUMBER_DIGIT") return "number";
|
|
6973
|
+
return "string";
|
|
6054
6974
|
}
|
|
6055
|
-
return
|
|
6975
|
+
return void 0;
|
|
6056
6976
|
}
|
|
6057
6977
|
|
|
6058
6978
|
// src/cli/index.ts
|
|
@@ -6070,6 +6990,7 @@ Options:
|
|
|
6070
6990
|
--dry-run Parse and show execution plan only
|
|
6071
6991
|
--format <type> Output format: table | json | jsonl | csv | markdown | md
|
|
6072
6992
|
--max-records <n> Max records to fetch (default: 500)
|
|
6993
|
+
--fetch-parallel <n> Parallel page fetches per query: 1-10 (default: 3)
|
|
6073
6994
|
--on-limit <mode> On record limit: error | truncate
|
|
6074
6995
|
--timeout <ms> Request timeout in milliseconds (default: 30000)
|
|
6075
6996
|
--config <path> Config file path (default: ./ksql.config.json)
|
|
@@ -6102,6 +7023,7 @@ Options:
|
|
|
6102
7023
|
--yes Skip DML confirmation prompt
|
|
6103
7024
|
--allow-without-where Allow UPDATE/DELETE without WHERE
|
|
6104
7025
|
--dml-max-rows <n> Max affected rows for DML guard (default: 100)
|
|
7026
|
+
--continue-on-error Batch: keep executing after a statement error (read-only batch only)
|
|
6105
7027
|
-h, --help Show help
|
|
6106
7028
|
-v, --version Show version
|
|
6107
7029
|
`;
|
|
@@ -6115,6 +7037,7 @@ function parseArgs(argv) {
|
|
|
6115
7037
|
dryRun: false,
|
|
6116
7038
|
format: null,
|
|
6117
7039
|
maxRecords: null,
|
|
7040
|
+
fetchParallel: null,
|
|
6118
7041
|
onLimit: null,
|
|
6119
7042
|
timeout: null,
|
|
6120
7043
|
configPath: null,
|
|
@@ -6141,6 +7064,7 @@ function parseArgs(argv) {
|
|
|
6141
7064
|
allowDml: false,
|
|
6142
7065
|
yes: false,
|
|
6143
7066
|
allowWithoutWhere: false,
|
|
7067
|
+
continueOnError: false,
|
|
6144
7068
|
dmlMaxRows: null,
|
|
6145
7069
|
userFormat: null,
|
|
6146
7070
|
arrayFormat: null,
|
|
@@ -6210,6 +7134,10 @@ function parseArgs(argv) {
|
|
|
6210
7134
|
out.allowWithoutWhere = true;
|
|
6211
7135
|
continue;
|
|
6212
7136
|
}
|
|
7137
|
+
if (a === "--continue-on-error") {
|
|
7138
|
+
out.continueOnError = true;
|
|
7139
|
+
continue;
|
|
7140
|
+
}
|
|
6213
7141
|
const v = argv[i + 1];
|
|
6214
7142
|
if (a === "-e" || a === "--execute") {
|
|
6215
7143
|
out.executeSql = v ?? "";
|
|
@@ -6329,6 +7257,13 @@ function parseArgs(argv) {
|
|
|
6329
7257
|
i++;
|
|
6330
7258
|
continue;
|
|
6331
7259
|
}
|
|
7260
|
+
if (a === "--fetch-parallel") {
|
|
7261
|
+
const n = Number(v);
|
|
7262
|
+
if (!Number.isInteger(n) || n < 1 || n > 10) throw new Error("ArgumentError: --fetch-parallel must be an integer between 1 and 10.");
|
|
7263
|
+
out.fetchParallel = n;
|
|
7264
|
+
i++;
|
|
7265
|
+
continue;
|
|
7266
|
+
}
|
|
6332
7267
|
if (a === "--timeout") {
|
|
6333
7268
|
const n = Number(v);
|
|
6334
7269
|
if (!Number.isInteger(n) || n <= 0) throw new Error("ArgumentError: --timeout must be a positive integer.");
|
|
@@ -6545,6 +7480,50 @@ function toExitCodeFromError(err) {
|
|
|
6545
7480
|
if (msg.startsWith("AuthError:")) return 3;
|
|
6546
7481
|
return 1;
|
|
6547
7482
|
}
|
|
7483
|
+
function buildBatchStatementSummary(s) {
|
|
7484
|
+
const parts = [`[${s.index + 1}] ${s.type}`, s.status];
|
|
7485
|
+
if (s.tempTable) parts.push(`temp=${s.tempTable}`);
|
|
7486
|
+
if (s.rowCount !== void 0) parts.push(`rows=${s.rowCount}`);
|
|
7487
|
+
if (s.status === "success" && s.result?.type === "SELECT") parts.push(`rowCount=${s.result.rowCount}`);
|
|
7488
|
+
if (s.status === "success" && s.result && s.result.type !== "SELECT") {
|
|
7489
|
+
const r = s.result;
|
|
7490
|
+
if (r.type === "INSERT") parts.push(`inserted=${r.insertedCount}`);
|
|
7491
|
+
else if (r.type === "UPDATE") parts.push(`updated=${r.updatedCount}`);
|
|
7492
|
+
else if (r.type === "DELETE") parts.push(`deleted=${r.deletedCount}`);
|
|
7493
|
+
else if (r.type === "UPSERT") parts.push(`inserted=${r.insertedCount} updated=${r.updatedCount}`);
|
|
7494
|
+
else parts.push(`reordered=${r.reorderedParentCount}`);
|
|
7495
|
+
}
|
|
7496
|
+
if (s.status === "error" && s.error) parts.push(s.error.message);
|
|
7497
|
+
if (s.status === "skipped" && s.skippedReason) parts.push(`reason=${s.skippedReason}`);
|
|
7498
|
+
return parts.join(" ");
|
|
7499
|
+
}
|
|
7500
|
+
function buildBatchDmlConfirmMessage(analysis) {
|
|
7501
|
+
const lines = ["[DML Confirm] batch"];
|
|
7502
|
+
for (const s of analysis.statements) {
|
|
7503
|
+
if (!s.isDml) continue;
|
|
7504
|
+
const app = s.targetAppId !== null ? `APP${s.targetAppId}` : "-";
|
|
7505
|
+
lines.push(` [${s.index + 1}] ${s.statementType} app=${app} where=${s.hasWhere ? "yes" : "no"}`);
|
|
7506
|
+
}
|
|
7507
|
+
return lines.join("\n");
|
|
7508
|
+
}
|
|
7509
|
+
function writeBatchOutput(batch, opts) {
|
|
7510
|
+
const outputs = [];
|
|
7511
|
+
for (const s of batch.statements) {
|
|
7512
|
+
if (!opts.quiet) process.stderr.write(`${buildBatchStatementSummary(s)}
|
|
7513
|
+
`);
|
|
7514
|
+
if (s.status === "success" && s.result?.type === "SELECT") {
|
|
7515
|
+
outputs.push(buildOutput(s.result, opts.format, opts.noHeader, opts.pretty, opts.displayOptions));
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7518
|
+
const output = outputs.join("\n\n");
|
|
7519
|
+
if (opts.outputPath) (0, import_fs2.writeFileSync)(opts.outputPath, `${output}
|
|
7520
|
+
`, "utf-8");
|
|
7521
|
+
else if (output) process.stdout.write(`${output}
|
|
7522
|
+
`);
|
|
7523
|
+
if (batch.ok) return 0;
|
|
7524
|
+
const firstError = batch.statements.find((s) => s.status === "error");
|
|
7525
|
+
return firstError?.error ? toExitCodeFromError(new Error(firstError.error.message)) : 1;
|
|
7526
|
+
}
|
|
6548
7527
|
function shouldExitOnEmpty(dryRun, exitOnEmpty, rowCount) {
|
|
6549
7528
|
if (dryRun) return false;
|
|
6550
7529
|
return exitOnEmpty && rowCount === 0;
|
|
@@ -6609,6 +7588,7 @@ function parseConsoleMetaCommand(line) {
|
|
|
6609
7588
|
if (!t.startsWith(":")) return { kind: "none" };
|
|
6610
7589
|
if (t === ":help") return { kind: "help" };
|
|
6611
7590
|
if (t === ":exit" || t === ":quit") return { kind: "exit" };
|
|
7591
|
+
if (t === ":run") return { kind: "run" };
|
|
6612
7592
|
if (t === ":clear") return { kind: "clear" };
|
|
6613
7593
|
if (t === ":last") return { kind: "show-last" };
|
|
6614
7594
|
if (t === ":buffer") return { kind: "show-buffer" };
|
|
@@ -6680,6 +7660,7 @@ function buildReplExecArgv(base, sql, dryRun, format) {
|
|
|
6680
7660
|
pushOpt(argv, "--token-file", base.tokenFile);
|
|
6681
7661
|
pushOpt(argv, "--app", base.app);
|
|
6682
7662
|
pushOpt(argv, "--max-records", base.maxRecords);
|
|
7663
|
+
pushOpt(argv, "--fetch-parallel", base.fetchParallel);
|
|
6683
7664
|
pushOpt(argv, "--on-limit", base.onLimit);
|
|
6684
7665
|
pushOpt(argv, "--timeout", base.timeout);
|
|
6685
7666
|
pushOpt(argv, "--output", base.outputPath);
|
|
@@ -6702,6 +7683,7 @@ function buildReplExecArgv(base, sql, dryRun, format) {
|
|
|
6702
7683
|
if (base.allowDml) argv.push("--yes");
|
|
6703
7684
|
if (base.allowDml) argv.push("--allow-dml");
|
|
6704
7685
|
if (base.allowWithoutWhere) argv.push("--allow-without-where");
|
|
7686
|
+
if (base.continueOnError) argv.push("--continue-on-error");
|
|
6705
7687
|
return argv;
|
|
6706
7688
|
}
|
|
6707
7689
|
function normalizeConsoleInputLine(line) {
|
|
@@ -6773,7 +7755,21 @@ async function confirmDmlInConsole(sql, opts, queue, defaultProfile = "dev") {
|
|
|
6773
7755
|
if (!opts.allowDml || opts.yes || opts.dryRun) return true;
|
|
6774
7756
|
try {
|
|
6775
7757
|
const normalized = normalizeSqlAppProfiles(sql, defaultProfile);
|
|
6776
|
-
const
|
|
7758
|
+
const statements = parseSqlStatements(normalized.normalizedSql);
|
|
7759
|
+
if (statements.length > 1) {
|
|
7760
|
+
const analysis = analyzeBatch(statements);
|
|
7761
|
+
if (!analysis.containsDml) return true;
|
|
7762
|
+
const message = buildBatchDmlConfirmMessage(analysis);
|
|
7763
|
+
if (queue) {
|
|
7764
|
+
process.stdout.write(`${message}
|
|
7765
|
+
Proceed? (yes/no): `);
|
|
7766
|
+
const input = await queue.next();
|
|
7767
|
+
if (input.kind !== "line") return false;
|
|
7768
|
+
return parseConfirmAnswer(input.line);
|
|
7769
|
+
}
|
|
7770
|
+
return await promptDmlConfirm(message);
|
|
7771
|
+
}
|
|
7772
|
+
const stmt = statements[0];
|
|
6777
7773
|
const stmtType = getStatementType(stmt);
|
|
6778
7774
|
if (!isDmlType(stmtType)) return true;
|
|
6779
7775
|
const compact = sql.replace(/\s+/g, " ").trim();
|
|
@@ -6885,17 +7881,47 @@ async function runConsole(base) {
|
|
|
6885
7881
|
const line = normalizeConsoleInputLine(input.line);
|
|
6886
7882
|
const t = line.trim();
|
|
6887
7883
|
emptyPromptSigintArmed = false;
|
|
6888
|
-
if (
|
|
7884
|
+
if (t.startsWith(":")) {
|
|
6889
7885
|
const meta = parseConsoleMetaCommand(t);
|
|
6890
7886
|
if (meta.kind === "none") {
|
|
6891
7887
|
} else if (meta.kind === "exit") {
|
|
6892
7888
|
return 0;
|
|
7889
|
+
} else if (meta.kind === "run") {
|
|
7890
|
+
const runDecision = decideRun(buffer);
|
|
7891
|
+
if (runDecision.kind === "error") {
|
|
7892
|
+
process.stderr.write(`${runDecision.message}
|
|
7893
|
+
`);
|
|
7894
|
+
continue;
|
|
7895
|
+
}
|
|
7896
|
+
const sql2 = runDecision.sql.trim();
|
|
7897
|
+
{
|
|
7898
|
+
const ok = await confirmDmlInConsole(sql2, {
|
|
7899
|
+
allowDml: base.allowDml,
|
|
7900
|
+
yes: base.yes,
|
|
7901
|
+
dryRun
|
|
7902
|
+
}, queue, profile ?? "dev");
|
|
7903
|
+
if (!ok) {
|
|
7904
|
+
process.stderr.write("DML was cancelled by user.\n");
|
|
7905
|
+
continue;
|
|
7906
|
+
}
|
|
7907
|
+
}
|
|
7908
|
+
buffer = "";
|
|
7909
|
+
lastSql = sql2;
|
|
7910
|
+
lastResolvedProfiles = formatResolvedAppProfiles(sql2, profile ?? "dev");
|
|
7911
|
+
history.push(sql2);
|
|
7912
|
+
appendHistory(sql2);
|
|
7913
|
+
const { code: code2, stdout: stdout2 } = await runWithArgvCapture(buildReplExecArgvWithProfile(base, sql2, dryRun, format, profile));
|
|
7914
|
+
lastOutput = stdout2;
|
|
7915
|
+
if (code2 !== 0) process.stderr.write(`(last exit code: ${code2})
|
|
7916
|
+
`);
|
|
7917
|
+
continue;
|
|
6893
7918
|
} else if (meta.kind === "help") {
|
|
6894
7919
|
process.stdout.write(
|
|
6895
7920
|
[
|
|
6896
7921
|
"console commands:",
|
|
6897
7922
|
" :help",
|
|
6898
7923
|
" :exit | :quit",
|
|
7924
|
+
" :run",
|
|
6899
7925
|
" :clear",
|
|
6900
7926
|
" :last",
|
|
6901
7927
|
" :buffer",
|
|
@@ -7043,10 +8069,22 @@ async function runConsole(base) {
|
|
|
7043
8069
|
continue;
|
|
7044
8070
|
}
|
|
7045
8071
|
}
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
if (
|
|
7049
|
-
|
|
8072
|
+
const decision = decideConsoleInput(buffer, line);
|
|
8073
|
+
if (decision.kind === "ignore") continue;
|
|
8074
|
+
if (decision.kind === "continue") {
|
|
8075
|
+
buffer = decision.buffer;
|
|
8076
|
+
continue;
|
|
8077
|
+
}
|
|
8078
|
+
if (decision.kind === "error") {
|
|
8079
|
+
buffer = "";
|
|
8080
|
+
process.stderr.write(`${decision.message}
|
|
8081
|
+
(input buffer cleared)
|
|
8082
|
+
`);
|
|
8083
|
+
continue;
|
|
8084
|
+
}
|
|
8085
|
+
if (decision.kind === "meta") continue;
|
|
8086
|
+
const isBatchExec = decision.kind === "execute-batch";
|
|
8087
|
+
const sql = isBatchExec ? decision.sql.trim() : decision.sql.replace(/;\s*$/, "").trim();
|
|
7050
8088
|
buffer = "";
|
|
7051
8089
|
if (!sql) continue;
|
|
7052
8090
|
{
|
|
@@ -7122,6 +8160,9 @@ async function run() {
|
|
|
7122
8160
|
let hasWhere = true;
|
|
7123
8161
|
let insertValuesCount = null;
|
|
7124
8162
|
let isDmlStatement = false;
|
|
8163
|
+
let isBatchSql = false;
|
|
8164
|
+
let batchContainsDml = false;
|
|
8165
|
+
let batchAnalysis = null;
|
|
7125
8166
|
if (args.diagRecordId === null) {
|
|
7126
8167
|
sql = args.executeSql;
|
|
7127
8168
|
if (!sql && args.filePath) sql = (0, import_fs2.readFileSync)(args.filePath, "utf-8");
|
|
@@ -7140,17 +8181,24 @@ async function run() {
|
|
|
7140
8181
|
return 2;
|
|
7141
8182
|
}
|
|
7142
8183
|
try {
|
|
7143
|
-
const
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
8184
|
+
const statements = parseSqlStatements(sql);
|
|
8185
|
+
if (statements.length > 1) {
|
|
8186
|
+
batchAnalysis = analyzeBatch(statements);
|
|
8187
|
+
isBatchSql = true;
|
|
8188
|
+
batchContainsDml = batchAnalysis.containsDml;
|
|
8189
|
+
} else {
|
|
8190
|
+
const stmt = parseSqlStatement(sql);
|
|
8191
|
+
parsedStmt = stmt;
|
|
8192
|
+
stmtType = getStatementType(stmt);
|
|
8193
|
+
isDmlStatement = isDmlType(stmtType);
|
|
8194
|
+
hasWhere = hasWhereClause(stmt);
|
|
8195
|
+
insertValuesCount = getInsertValuesCount(stmt);
|
|
8196
|
+
const supported = stmtType === "SELECT" || stmtType === "UNION" || stmtType === "WITH" || stmtType === "EXPLAIN" || stmtType === "SHOW_APPS" || stmtType === "DESCRIBE" || isDmlStatement;
|
|
8197
|
+
if (!supported) {
|
|
8198
|
+
process.stderr.write(`ArgumentError: unsupported statement type in CLI: ${stmtType}
|
|
7152
8199
|
`);
|
|
7153
|
-
|
|
8200
|
+
return 2;
|
|
8201
|
+
}
|
|
7154
8202
|
}
|
|
7155
8203
|
} catch (err) {
|
|
7156
8204
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
@@ -7159,8 +8207,13 @@ async function run() {
|
|
|
7159
8207
|
}
|
|
7160
8208
|
}
|
|
7161
8209
|
const maxRecords = args.maxRecords ?? envInt("KSQL_MAX_RECORDS") ?? profile.query?.maxRecords ?? 500;
|
|
8210
|
+
const fetchParallel = args.fetchParallel ?? envInt("KSQL_FETCH_PARALLEL") ?? profile.query?.fetchParallel ?? 3;
|
|
7162
8211
|
const onLimit = args.onLimit ?? envOnLimit("KSQL_ON_LIMIT") ?? profile.query?.onLimit ?? "error";
|
|
7163
8212
|
const timeout = args.timeout ?? envInt("KSQL_TIMEOUT") ?? profile.query?.timeout ?? 3e4;
|
|
8213
|
+
if (!Number.isInteger(fetchParallel) || fetchParallel < 1 || fetchParallel > 10) {
|
|
8214
|
+
process.stderr.write("ArgumentError: fetch-parallel must be an integer between 1 and 10.\n");
|
|
8215
|
+
return 2;
|
|
8216
|
+
}
|
|
7164
8217
|
const rawFormat = args.format ?? envFormat("KSQL_FORMAT") ?? profile.output?.format ?? "table";
|
|
7165
8218
|
const format = normalizeOutputFormat(rawFormat);
|
|
7166
8219
|
if (!format) {
|
|
@@ -7199,6 +8252,24 @@ async function run() {
|
|
|
7199
8252
|
process.stderr.write("ArgumentError: no APPxxx found in SQL and --app is not set.\n");
|
|
7200
8253
|
return 2;
|
|
7201
8254
|
}
|
|
8255
|
+
if (isBatchSql) {
|
|
8256
|
+
if (batchContainsDml && !allowDml) {
|
|
8257
|
+
process.stderr.write("ArgumentError: DML is disabled. Use --allow-dml to enable UPDATE/DELETE/INSERT/UPSERT/REORDER.\n");
|
|
8258
|
+
return 2;
|
|
8259
|
+
}
|
|
8260
|
+
if (args.dryRun) {
|
|
8261
|
+
const plans = buildBatchExplainPlans(sql);
|
|
8262
|
+
const out = [];
|
|
8263
|
+
plans.statements.forEach((p) => {
|
|
8264
|
+
if (p.index > 0) out.push("");
|
|
8265
|
+
out.push(`[${p.index + 1}] ${p.type}`);
|
|
8266
|
+
out.push(...p.plan);
|
|
8267
|
+
});
|
|
8268
|
+
process.stdout.write(`${out.join("\n")}
|
|
8269
|
+
`);
|
|
8270
|
+
return 0;
|
|
8271
|
+
}
|
|
8272
|
+
}
|
|
7202
8273
|
if (isDmlStatement) {
|
|
7203
8274
|
if (hasProfileSyntax && stmtType === "DELETE") {
|
|
7204
8275
|
process.stderr.write("ArgumentError: @profile is not supported for DELETE yet.\n");
|
|
@@ -7446,6 +8517,9 @@ async function run() {
|
|
|
7446
8517
|
getApps: () => defaultClient.getApps()
|
|
7447
8518
|
};
|
|
7448
8519
|
}
|
|
8520
|
+
if (!args.dryRun) {
|
|
8521
|
+
client = withRequestGate(client, getGlobalRequestGate(profile.query?.maxConcurrent));
|
|
8522
|
+
}
|
|
7449
8523
|
try {
|
|
7450
8524
|
if (isDmlStatement && !args.dryRun) {
|
|
7451
8525
|
const stmtAppId = parsedStmt && typeof parsedStmt === "object" && typeof parsedStmt.appId === "number" ? parsedStmt.appId : appIds[0];
|
|
@@ -7473,8 +8547,33 @@ async function run() {
|
|
|
7473
8547
|
return await promptDmlConfirm(`[DML Confirm] type=${operation} estimatedRows=${count}
|
|
7474
8548
|
query=${label}`);
|
|
7475
8549
|
};
|
|
8550
|
+
if (isBatchSql) {
|
|
8551
|
+
if (batchContainsDml && !yes && batchAnalysis) {
|
|
8552
|
+
const ok = await promptDmlConfirm(buildBatchDmlConfirmMessage(batchAnalysis));
|
|
8553
|
+
if (!ok) {
|
|
8554
|
+
process.stderr.write("DML was cancelled by user.\n");
|
|
8555
|
+
return 2;
|
|
8556
|
+
}
|
|
8557
|
+
}
|
|
8558
|
+
const batchResult = await executeBatch(sql, client, {
|
|
8559
|
+
maxRecords,
|
|
8560
|
+
fetchParallel,
|
|
8561
|
+
onLimitReached: onLimit,
|
|
8562
|
+
cacheContext,
|
|
8563
|
+
continueOnError: args.continueOnError,
|
|
8564
|
+
timeoutMs: timeout,
|
|
8565
|
+
confirm: batchContainsDml ? async (count, operation) => {
|
|
8566
|
+
if (count > dmlMaxRows) {
|
|
8567
|
+
throw new Error(`ArgumentError: ${operation} affected rows (${count}) exceed --dml-max-rows (${dmlMaxRows}).`);
|
|
8568
|
+
}
|
|
8569
|
+
return true;
|
|
8570
|
+
} : void 0
|
|
8571
|
+
});
|
|
8572
|
+
return writeBatchOutput(batchResult, { format, noHeader, pretty, displayOptions, outputPath, quiet });
|
|
8573
|
+
}
|
|
7476
8574
|
const result = args.dryRun ? await execute(`EXPLAIN ${sql}`, client, { maxRecords, onLimitReached: onLimit, cacheContext }) : await execute(sql, client, {
|
|
7477
8575
|
maxRecords,
|
|
8576
|
+
fetchParallel,
|
|
7478
8577
|
onLimitReached: onLimit,
|
|
7479
8578
|
confirm: isDmlStatement ? confirm : void 0,
|
|
7480
8579
|
cacheContext
|
|
@@ -7521,6 +8620,8 @@ if (isDirectCliRun()) {
|
|
|
7521
8620
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7522
8621
|
0 && (module.exports = {
|
|
7523
8622
|
HELP_TEXT,
|
|
8623
|
+
buildBatchDmlConfirmMessage,
|
|
8624
|
+
buildBatchStatementSummary,
|
|
7524
8625
|
buildOutput,
|
|
7525
8626
|
extractAppIds,
|
|
7526
8627
|
normalizeAppKey,
|