@rex0220/kintone-sql-tools 1.9.0 → 1.10.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.
@@ -30999,6 +30999,7 @@ var KEYWORDS = /* @__PURE__ */ new Map([
30999
30999
  ["AVG", "AVG" /* AVG */],
31000
31000
  ["MAX", "MAX" /* MAX */],
31001
31001
  ["MIN", "MIN" /* MIN */],
31002
+ ["ASSERT", "ASSERT" /* ASSERT */],
31002
31003
  ["AND", "AND" /* AND */],
31003
31004
  ["OR", "OR" /* OR */],
31004
31005
  ["NOT", "NOT" /* NOT */],
@@ -31344,6 +31345,58 @@ function isJapanese(cp) {
31344
31345
 
31345
31346
  // src/parser/parser.ts
31346
31347
  var MAX_BATCH_STATEMENTS = 20;
31348
+ var FUNC_CALL_PREFIX_KINDS = /* @__PURE__ */ new Set([
31349
+ "IDENT" /* IDENT */,
31350
+ "BIDENT" /* BIDENT */,
31351
+ "COUNT" /* COUNT */,
31352
+ "SUM" /* SUM */,
31353
+ "AVG" /* AVG */,
31354
+ "MAX" /* MAX */,
31355
+ "MIN" /* MIN */,
31356
+ "TODAY" /* TODAY */,
31357
+ "NOW" /* NOW */,
31358
+ "LOGINUSER" /* LOGINUSER */,
31359
+ "UPPER" /* UPPER */,
31360
+ "LOWER" /* LOWER */,
31361
+ "TRIM" /* TRIM */,
31362
+ "LTRIM" /* LTRIM */,
31363
+ "RTRIM" /* RTRIM */,
31364
+ "LENGTH" /* LENGTH */,
31365
+ "SUBSTRING" /* SUBSTRING */,
31366
+ "SUBSTR" /* SUBSTR */,
31367
+ "CONCAT" /* CONCAT */,
31368
+ "REPLACE" /* REPLACE */,
31369
+ "COALESCE" /* COALESCE */,
31370
+ "NULLIF" /* NULLIF */,
31371
+ "ISNULL" /* ISNULL */,
31372
+ "CAST" /* CAST */,
31373
+ "CONVERT" /* CONVERT */,
31374
+ "FORMAT" /* FORMAT */,
31375
+ "ROUND" /* ROUND */,
31376
+ "FLOOR" /* FLOOR */,
31377
+ "CEIL" /* CEIL */,
31378
+ "CEILING" /* CEILING */,
31379
+ "ABS" /* ABS */,
31380
+ "MOD" /* MOD */,
31381
+ "POWER" /* POWER */,
31382
+ "POW" /* POW */,
31383
+ "SQRT" /* SQRT */,
31384
+ "YEAR" /* YEAR */,
31385
+ "MONTH" /* MONTH */,
31386
+ "DAY" /* DAY */,
31387
+ "DATE_FORMAT" /* DATE_FORMAT */,
31388
+ "DATEDIFF" /* DATEDIFF */,
31389
+ "DATE_ADD" /* DATE_ADD */,
31390
+ "IF" /* IF */
31391
+ ]);
31392
+ function needsSpaceBetween(prev, cur) {
31393
+ if (prev.kind === "(" /* LPAREN */ || prev.kind === "." /* DOT */) return false;
31394
+ if (cur.kind === ")" /* RPAREN */ || cur.kind === "," /* COMMA */ || cur.kind === "." /* DOT */) return false;
31395
+ if (cur.kind === "(" /* LPAREN */) {
31396
+ return !FUNC_CALL_PREFIX_KINDS.has(prev.kind);
31397
+ }
31398
+ return true;
31399
+ }
31347
31400
  var ParseError = class extends Error {
31348
31401
  constructor(message, token) {
31349
31402
  super(`${message}\uFF08\u4F4D\u7F6E ${token.pos}\u3001\u30C8\u30FC\u30AF\u30F3: \u300C${token.value}\u300D\uFF09`);
@@ -31433,6 +31486,8 @@ var Parser = class {
31433
31486
  return this.parseDescribe();
31434
31487
  case "EXPLAIN" /* EXPLAIN */:
31435
31488
  return this.parseExplain();
31489
+ case "ASSERT" /* ASSERT */:
31490
+ return this.parseAssert();
31436
31491
  case "IDENT" /* IDENT */: {
31437
31492
  const upper = tok.value.toUpperCase();
31438
31493
  if (upper === "CREATE") return this.parseCreateTempTable();
@@ -31443,7 +31498,7 @@ var Parser = class {
31443
31498
  break;
31444
31499
  }
31445
31500
  throw new ParseError(
31446
- "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",
31501
+ "SELECT / INSERT / UPDATE / DELETE / REORDER / WITH / SHOW / DESCRIBE / EXPLAIN / CREATE TEMP TABLE / DROP TEMP TABLE / ASSERT \u306E\u3044\u305A\u308C\u304B\u3067\u59CB\u307E\u308B SQL \u6587\u304C\u5FC5\u8981\u3067\u3059",
31447
31502
  tok
31448
31503
  );
31449
31504
  }
@@ -31537,6 +31592,174 @@ var Parser = class {
31537
31592
  return { type: "EXPLAIN", query };
31538
31593
  }
31539
31594
  // ----------------------------------------------------------
31595
+ // ASSERT
31596
+ //
31597
+ // ASSERT <式> <比較演算子> <式>
31598
+ // ASSERT <式> BETWEEN <式> AND <式>
31599
+ //
31600
+ // 式: リテラル / 算術式 / スカラーサブクエリ。
31601
+ // フィールド参照(FROM コンテキストがない)・AND / OR 複合条件・
31602
+ // 裸の値のみ(ASSERT 1)は ParseError。
31603
+ // ----------------------------------------------------------
31604
+ parseAssert() {
31605
+ this.expect("ASSERT" /* ASSERT */);
31606
+ const condStart = this.pos;
31607
+ const left = this.parseAssertOperand();
31608
+ const opTok = this.peek();
31609
+ if (this.consume("BETWEEN" /* BETWEEN */)) {
31610
+ const low = this.parseAssertOperand();
31611
+ this.expect(
31612
+ "AND" /* AND */,
31613
+ "ASSERT \u306E BETWEEN \u306B\u306F AND \u304C\u5FC5\u8981\u3067\u3059\uFF08\u4F8B: ASSERT (SELECT COUNT(*) FROM #t) BETWEEN 1 AND 500\uFF09"
31614
+ );
31615
+ const high = this.parseAssertOperand();
31616
+ this.rejectAssertCompound();
31617
+ return {
31618
+ type: "ASSERT",
31619
+ left,
31620
+ op: "BETWEEN",
31621
+ right: null,
31622
+ low,
31623
+ high,
31624
+ text: this.renderTokenRange(condStart, this.pos)
31625
+ };
31626
+ }
31627
+ const op = this.tryAssertCompareOp();
31628
+ if (op === null) {
31629
+ throw new ParseError(
31630
+ "ASSERT \u306B\u306F\u6BD4\u8F03\u6F14\u7B97\u5B50\uFF08= <> < <= > >=\uFF09\u307E\u305F\u306F BETWEEN \u304C\u5FC5\u8981\u3067\u3059\uFF08\u5024\u306E\u307F\u306E ASSERT \u306F\u4E0D\u53EF\uFF09",
31631
+ opTok
31632
+ );
31633
+ }
31634
+ const right = this.parseAssertOperand();
31635
+ this.rejectAssertCompound();
31636
+ return {
31637
+ type: "ASSERT",
31638
+ left,
31639
+ op,
31640
+ right,
31641
+ low: null,
31642
+ high: null,
31643
+ text: this.renderTokenRange(condStart, this.pos)
31644
+ };
31645
+ }
31646
+ /** ASSERT のオペランド: 文字列 / スカラーサブクエリ / 数値算術式 */
31647
+ parseAssertOperand() {
31648
+ const tok = this.peek();
31649
+ if (tok.kind === "STRING" /* STRING */) {
31650
+ this.advance();
31651
+ return { type: "STRING", value: tok.value };
31652
+ }
31653
+ if (tok.kind === "(" /* LPAREN */ && this.peekAt(1).kind === "SELECT" /* SELECT */) {
31654
+ this.advance();
31655
+ const query = this.parseSelect();
31656
+ this.expect(")" /* RPAREN */);
31657
+ if (this.isArithOp(this.peek().kind)) {
31658
+ throw new ParseError(
31659
+ "\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u306E\u5F8C\u306B\u7B97\u8853\u6F14\u7B97\u5B50\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\u3002\u30B5\u30D6\u30AF\u30A8\u30EA\u5185\u3067\u8A08\u7B97\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u4F8B: ASSERT (SELECT COUNT(*) * 2 FROM APP100) > 10\uFF09",
31660
+ this.peek()
31661
+ );
31662
+ }
31663
+ const hasWildcard = query.columns.some(
31664
+ (c) => c.type === "WILDCARD" || c.type === "PARENT_WILDCARD"
31665
+ );
31666
+ if (!hasWildcard && query.columns.length > 1) {
31667
+ throw new ParseError("scalar subquery in ASSERT must return exactly 1 column.", tok);
31668
+ }
31669
+ return { type: "SCALAR_SUBQUERY", query };
31670
+ }
31671
+ if (tok.kind === "NUMBER" /* NUMBER */ || tok.kind === "(" /* LPAREN */ || tok.kind === "-" /* MINUS */) {
31672
+ const expr = this.parseArithAddSub();
31673
+ this.rejectNonLiteralArith(expr, tok);
31674
+ if (expr.type === "NUMBER") return expr;
31675
+ return expr;
31676
+ }
31677
+ if (this.tryStringFuncName() !== null) {
31678
+ throw new ParseError(
31679
+ "ASSERT \u306E\u5F0F\u3067\u306F\u95A2\u6570\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF08\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u5185\u3067\u8A08\u7B97\u3057\u3066\u304F\u3060\u3055\u3044\uFF09",
31680
+ tok
31681
+ );
31682
+ }
31683
+ throw new ParseError(
31684
+ "ASSERT \u306E\u5F0F\u306B\u306F\u30EA\u30C6\u30E9\u30EB\u30FB\u7B97\u8853\u5F0F\u30FB\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF09",
31685
+ tok
31686
+ );
31687
+ }
31688
+ /** ASSERT の算術式にフィールド参照・関数呼び出しが含まれていたら拒否する */
31689
+ rejectNonLiteralArith(node, tok) {
31690
+ if (node.type === "FIELD_REF") {
31691
+ throw new ParseError(
31692
+ `ASSERT \u3067\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF08FROM \u30B3\u30F3\u30C6\u30AD\u30B9\u30C8\u304C\u3042\u308A\u307E\u305B\u3093\uFF09: ${node.field}`,
31693
+ tok
31694
+ );
31695
+ }
31696
+ if (node.type === "STRING_FUNC") {
31697
+ throw new ParseError(
31698
+ "ASSERT \u306E\u5F0F\u3067\u306F\u95A2\u6570\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF08\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u5185\u3067\u8A08\u7B97\u3057\u3066\u304F\u3060\u3055\u3044\uFF09",
31699
+ tok
31700
+ );
31701
+ }
31702
+ if (node.type === "ARITH") {
31703
+ this.rejectNonLiteralArith(node.left, tok);
31704
+ this.rejectNonLiteralArith(node.right, tok);
31705
+ }
31706
+ }
31707
+ /** ASSERT の比較演算子を読む(該当しなければ null・消費しない) */
31708
+ tryAssertCompareOp() {
31709
+ switch (this.peek().kind) {
31710
+ case "=" /* EQ */:
31711
+ this.advance();
31712
+ return "=";
31713
+ case "!=" /* NEQ */:
31714
+ this.advance();
31715
+ return "!=";
31716
+ case "<>" /* LT_GT */:
31717
+ this.advance();
31718
+ return "<>";
31719
+ case ">" /* GT */:
31720
+ this.advance();
31721
+ return ">";
31722
+ case "<" /* LT */:
31723
+ this.advance();
31724
+ return "<";
31725
+ case ">=" /* GTE */:
31726
+ this.advance();
31727
+ return ">=";
31728
+ case "<=" /* LTE */:
31729
+ this.advance();
31730
+ return "<=";
31731
+ default:
31732
+ return null;
31733
+ }
31734
+ }
31735
+ /** ASSERT は AND / OR による複合条件に対応しない(初期版仕様) */
31736
+ rejectAssertCompound() {
31737
+ const tok = this.peek();
31738
+ if (tok.kind === "AND" /* AND */ || tok.kind === "OR" /* OR */) {
31739
+ throw new ParseError(
31740
+ "ASSERT \u306F AND / OR \u306B\u3088\u308B\u8907\u5408\u6761\u4EF6\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u305B\u3093\uFF08\u8907\u6570\u306E ASSERT \u6587\u306B\u5206\u3051\u3066\u304F\u3060\u3055\u3044\uFF09",
31741
+ tok
31742
+ );
31743
+ }
31744
+ }
31745
+ /**
31746
+ * トークン列 [fromIdx, toIdx) を SQL 風テキストに再構成する。
31747
+ * AssertError の "assertion failed: <条件>" メッセージ用(正規化表示で十分)。
31748
+ */
31749
+ renderTokenRange(fromIdx, toIdx) {
31750
+ let out = "";
31751
+ for (let i = fromIdx; i < toIdx; i++) {
31752
+ const t = this.tokens[i];
31753
+ let text;
31754
+ if (t.kind === "STRING" /* STRING */) text = `'${t.value.replace(/'/g, "''")}'`;
31755
+ else if (t.kind === "BIDENT" /* BIDENT */) text = `\`${t.value}\``;
31756
+ else text = t.value;
31757
+ if (out.length > 0 && needsSpaceBetween(this.tokens[i - 1], t)) out += " ";
31758
+ out += text;
31759
+ }
31760
+ return out;
31761
+ }
31762
+ // ----------------------------------------------------------
31540
31763
  // SELECT
31541
31764
  // ----------------------------------------------------------
31542
31765
  parseSelect() {
@@ -32886,7 +33109,7 @@ function isDmlType(type) {
32886
33109
  return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
32887
33110
  }
32888
33111
  function isReadOnlyType(type) {
32889
- return type === "SELECT" || type === "UNION" || type === "WITH" || type === "EXPLAIN" || type === "SHOW_APPS" || type === "DESCRIBE" || type === "CREATE_TEMP_TABLE" || type === "DROP_TEMP_TABLE";
33112
+ return type === "SELECT" || type === "UNION" || type === "WITH" || type === "EXPLAIN" || type === "SHOW_APPS" || type === "DESCRIBE" || type === "CREATE_TEMP_TABLE" || type === "DROP_TEMP_TABLE" || type === "ASSERT";
32890
33113
  }
32891
33114
  function hasWhereClause(stmt) {
32892
33115
  if (!stmt || typeof stmt !== "object") return false;
@@ -35255,6 +35478,8 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
35255
35478
  throw new Error("ArgumentError: CREATE TEMP TABLE requires a batch (temp tables are batch-scoped).");
35256
35479
  case "DROP_TEMP_TABLE":
35257
35480
  throw new Error("ArgumentError: DROP TEMP TABLE requires a batch (temp tables are batch-scoped).");
35481
+ case "ASSERT":
35482
+ return executeAssert(stmt, client, options, cacheContext);
35258
35483
  }
35259
35484
  }
35260
35485
  var TEMP_TABLE_MAX_ROWS = 1e4;
@@ -35329,6 +35554,8 @@ async function executeBatch(sql, client, options = {}) {
35329
35554
  failed.add(i);
35330
35555
  if (e instanceof BatchTimeoutError) {
35331
35556
  aborted2 = "timeout";
35557
+ } else if (e instanceof AssertError) {
35558
+ aborted2 = "assertion";
35332
35559
  } else if (!options.continueOnError) {
35333
35560
  aborted2 = "fail-fast";
35334
35561
  }
@@ -35361,6 +35588,10 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
35361
35588
  if (stmt.type === "EXPLAIN") {
35362
35589
  return { result: await executeParsedStatement(stmt, client, options, cacheContext) };
35363
35590
  }
35591
+ if (stmt.type === "ASSERT") {
35592
+ await executeAssert(stmt, client, options, cacheContext, tempTables);
35593
+ return {};
35594
+ }
35364
35595
  if (info.tempTablesReferenced.length > 0) {
35365
35596
  if (stmt.type === "SELECT" || stmt.type === "UNION") {
35366
35597
  return { result: await executeQueryWithCte(stmt, client, options, tempTables, cacheContext) };
@@ -35437,6 +35668,107 @@ function parseSqlBatch(sql) {
35437
35668
  const tokens = new Lexer(sql).tokenize();
35438
35669
  return new Parser(tokens).parseStatements();
35439
35670
  }
35671
+ var AssertError = class extends Error {
35672
+ constructor(message) {
35673
+ super(`AssertError: ${message}`);
35674
+ this.name = "AssertError";
35675
+ }
35676
+ };
35677
+ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
35678
+ const left = await evalAssertOperand(stmt.left, client, options, cacheContext, tempTables);
35679
+ if (stmt.op === "BETWEEN") {
35680
+ if (stmt.low === null || stmt.high === null) {
35681
+ throw new Error("ArgumentError: malformed ASSERT statement.");
35682
+ }
35683
+ const low = await evalAssertOperand(stmt.low, client, options, cacheContext, tempTables);
35684
+ const high = await evalAssertOperand(stmt.high, client, options, cacheContext, tempTables);
35685
+ if (!compareAssertValues(">=", left, low) || !compareAssertValues("<=", left, high)) {
35686
+ throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
35687
+ }
35688
+ return { type: "ASSERT", condition: stmt.text };
35689
+ }
35690
+ if (stmt.right === null) {
35691
+ throw new Error("ArgumentError: malformed ASSERT statement.");
35692
+ }
35693
+ const right = await evalAssertOperand(stmt.right, client, options, cacheContext, tempTables);
35694
+ if (!compareAssertValues(stmt.op, left, right)) {
35695
+ throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
35696
+ }
35697
+ return { type: "ASSERT", condition: stmt.text };
35698
+ }
35699
+ async function evalAssertOperand(operand, client, options, cacheContext, tempTables) {
35700
+ switch (operand.type) {
35701
+ case "NUMBER":
35702
+ return String(operand.value);
35703
+ case "STRING":
35704
+ return operand.value;
35705
+ case "ARITH":
35706
+ return String(evalAssertArith(operand));
35707
+ case "SCALAR_SUBQUERY": {
35708
+ const { query, probed } = withScalarProbeLimit(operand.query);
35709
+ const result = await runSubquery(query, client, options, cacheContext, tempTables);
35710
+ if (result.columns.length > 1) {
35711
+ throw new AssertError(
35712
+ `scalar subquery returned ${result.columns.length} columns (expected 1 column).`
35713
+ );
35714
+ }
35715
+ if (result.rowCount === 0) {
35716
+ throw new AssertError("scalar subquery returned no rows (expected 1 row).");
35717
+ }
35718
+ if (result.rowCount > 1) {
35719
+ const rows = probed && result.rowCount === 2 ? "2 or more rows" : `${result.rowCount} rows`;
35720
+ throw new AssertError(`scalar subquery returned ${rows} (expected 1 row).`);
35721
+ }
35722
+ const col = result.columns[0] ?? "";
35723
+ return result.rows[0]?.[col] ?? "";
35724
+ }
35725
+ }
35726
+ }
35727
+ function withScalarProbeLimit(query) {
35728
+ const hasAgg = query.groupBy.length > 0 || query.columns.some((c) => c.type === "AGGREGATE" || c.type === "ARITH_AGG_COL");
35729
+ if (hasAgg || query.distinct || query.limit !== null) return { query, probed: false };
35730
+ return { query: { ...query, limit: 2 }, probed: true };
35731
+ }
35732
+ function evalAssertArith(node) {
35733
+ if (node.type === "NUMBER") return node.value;
35734
+ if (node.type === "ARITH") {
35735
+ const left = evalAssertArith(node.left);
35736
+ const right = evalAssertArith(node.right);
35737
+ switch (node.op) {
35738
+ case "+":
35739
+ return left + right;
35740
+ case "-":
35741
+ return left - right;
35742
+ case "*":
35743
+ return left * right;
35744
+ case "/":
35745
+ return left / right;
35746
+ case "%":
35747
+ return left % right;
35748
+ }
35749
+ }
35750
+ throw new Error(`ArgumentError: unsupported operand in ASSERT expression: ${node.type}`);
35751
+ }
35752
+ function compareAssertValues(op, leftStr, rightStr) {
35753
+ const leftNum = Number(leftStr);
35754
+ const rightNum = Number(rightStr);
35755
+ const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
35756
+ switch (op) {
35757
+ case "=":
35758
+ return leftStr === rightStr;
35759
+ case "!=":
35760
+ case "<>":
35761
+ return leftStr !== rightStr;
35762
+ case ">":
35763
+ return numeric ? leftNum > rightNum : leftStr > rightStr;
35764
+ case "<":
35765
+ return numeric ? leftNum < rightNum : leftStr < rightStr;
35766
+ case ">=":
35767
+ return numeric ? leftNum >= rightNum : leftStr >= rightStr;
35768
+ case "<=":
35769
+ return numeric ? leftNum <= rightNum : leftStr <= rightStr;
35770
+ }
35771
+ }
35440
35772
  async function executeSelect(stmt, client, options, cacheContext, cteCache) {
35441
35773
  if (isNoFromSelect(stmt)) {
35442
35774
  return executeNoFromSelect(stmt);
@@ -36894,8 +37226,33 @@ function buildBatchStatementPlan(stmt, info) {
36894
37226
  if (stmt.type === "SHOW_APPS") return ["SHOW APPS\uFF08\u30A2\u30D7\u30EA\u4E00\u89A7\u306E\u53D6\u5F97\uFF09"];
36895
37227
  if (stmt.type === "DESCRIBE") return [`DESCRIBE APP${stmt.appId}\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u5B9A\u7FA9\u306E\u53D6\u5F97\uFF09`];
36896
37228
  if (stmt.type === "EXPLAIN") return buildPlanForBatchQuery(stmt.query, info);
37229
+ if (stmt.type === "ASSERT") {
37230
+ const lines = [
37231
+ `ASSERT ${stmt.text}`,
37232
+ " check: \u5B9F\u884C\u6642\u306B\u6761\u4EF6\u8A55\u4FA1\uFF08\u4E0D\u6210\u7ACB\u306F AssertError \u3067\u30D0\u30C3\u30C1\u505C\u6B62\u3001\u4EE5\u964D\u306E\u6587\u306F skipped\uFF09"
37233
+ ];
37234
+ const subqueries = [stmt.left, stmt.right, stmt.low, stmt.high].filter(
37235
+ (o) => o !== null && o.type === "SCALAR_SUBQUERY"
37236
+ );
37237
+ subqueries.forEach((sq, i) => {
37238
+ lines.push(subqueries.length > 1 ? ` subquery[${i + 1}]:` : " subquery:");
37239
+ const subInfo = hasTempTableRef(sq.query) ? info : { ...info, tempTablesReferenced: [] };
37240
+ lines.push(...buildPlanForBatchQuery(sq.query, subInfo).map((l) => ` ${l}`));
37241
+ });
37242
+ return lines;
37243
+ }
36897
37244
  return buildPlanForBatchQuery(stmt, info);
36898
37245
  }
37246
+ function hasTempTableRef(node) {
37247
+ if (Array.isArray(node)) return node.some(hasTempTableRef);
37248
+ if (node !== null && typeof node === "object") {
37249
+ const obj = node;
37250
+ const cte = obj["cteName"];
37251
+ if (typeof cte === "string" && cte.startsWith("#")) return true;
37252
+ return Object.values(obj).some(hasTempTableRef);
37253
+ }
37254
+ return false;
37255
+ }
36899
37256
  function buildPlanForBatchQuery(query, info) {
36900
37257
  if (info.tempTablesReferenced.length === 0) {
36901
37258
  return buildExplainPlan(query);
@@ -37248,6 +37605,62 @@ function parseSqlStatements(sql) {
37248
37605
  return new Parser(tokens).parseStatements();
37249
37606
  }
37250
37607
 
37608
+ // src/output/batchEnvelope.ts
37609
+ function toMutationSummary(result) {
37610
+ if (result.type === "INSERT") {
37611
+ return { insertedCount: result.insertedCount, createdIds: result.createdIds };
37612
+ }
37613
+ if (result.type === "UPDATE") return { updatedCount: result.updatedCount };
37614
+ if (result.type === "DELETE") return { deletedCount: result.deletedCount };
37615
+ if (result.type === "UPSERT") {
37616
+ return { insertedCount: result.insertedCount, updatedCount: result.updatedCount };
37617
+ }
37618
+ return { reorderedParentCount: result.reorderedParentCount };
37619
+ }
37620
+ function buildBatchEnvelope(batch, options = {}) {
37621
+ const { maxTotalRecords } = options;
37622
+ const results = [];
37623
+ let totalRows = 0;
37624
+ const statements = batch.statements.map((s) => {
37625
+ const entry = {
37626
+ index: s.index,
37627
+ type: s.type,
37628
+ status: s.status
37629
+ };
37630
+ if (s.status === "error" && s.error) entry.error = s.error;
37631
+ if (s.status === "skipped" && s.skippedReason) entry.skippedReason = s.skippedReason;
37632
+ if (s.tempTable !== void 0) entry.tempTable = s.tempTable;
37633
+ if (s.rowCount !== void 0) entry.rowCount = s.rowCount;
37634
+ if (s.status === "success" && s.result?.type === "SELECT") {
37635
+ totalRows += s.result.rowCount;
37636
+ if (maxTotalRecords !== void 0 && totalRows > maxTotalRecords) {
37637
+ throw new Error(
37638
+ `ArgumentError: batch total rows (${totalRows}) exceed maxTotalRecords (${maxTotalRecords}).`
37639
+ );
37640
+ }
37641
+ entry.resultIndex = results.length;
37642
+ results.push({
37643
+ columns: s.result.columns,
37644
+ rows: s.result.rows,
37645
+ rowCount: s.result.rowCount,
37646
+ warnings: s.result.warnings ?? []
37647
+ });
37648
+ } else if (s.status === "success" && s.result && s.result.type !== "SELECT" && s.result.type !== "ASSERT") {
37649
+ Object.assign(entry, toMutationSummary(s.result));
37650
+ }
37651
+ return entry;
37652
+ });
37653
+ return {
37654
+ ok: batch.ok,
37655
+ batch: true,
37656
+ statementCount: batch.statementCount,
37657
+ statements,
37658
+ results,
37659
+ // バッチ全体の警告(仕様 §6.2)。文ごとの警告は results[].warnings に入る
37660
+ warnings: []
37661
+ };
37662
+ }
37663
+
37251
37664
  // src/node/appProfiles.ts
37252
37665
  function parseTokenMap(raw) {
37253
37666
  const out = {};
@@ -37453,6 +37866,21 @@ function envInt(name) {
37453
37866
  if (!Number.isInteger(n) || n <= 0) return null;
37454
37867
  return n;
37455
37868
  }
37869
+ function envNonNegativeInt(name) {
37870
+ const v = envString(name);
37871
+ if (v === null) return null;
37872
+ const n = Number(v);
37873
+ if (!Number.isInteger(n) || n < 0) return null;
37874
+ return n;
37875
+ }
37876
+ function resolveRequestGateOptions(base) {
37877
+ return {
37878
+ ...base,
37879
+ maxConcurrent: envInt("KSQL_MAX_CONCURRENT") ?? base.maxConcurrent,
37880
+ // KSQL_RETRY=0(リトライ無効)は有効値のため envNonNegativeInt で読む
37881
+ maxRetries: envNonNegativeInt("KSQL_RETRY") ?? base.maxRetries
37882
+ };
37883
+ }
37456
37884
  function envOnLimit(name) {
37457
37885
  const v = envString(name);
37458
37886
  if (v === "error" || v === "truncate") return v;
@@ -37493,8 +37921,11 @@ var RequestGate = class {
37493
37921
  this.waiters = [];
37494
37922
  this.maxConcurrent = clampInt(options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT, 1, 50);
37495
37923
  this.maxRetries = clampInt(options.maxRetries ?? DEFAULT_MAX_RETRIES, 0, 10);
37496
- this.baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
37497
- this.maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
37924
+ this.baseDelayMs = clampInt(options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS, 1, 6e4);
37925
+ this.maxDelayMs = Math.max(
37926
+ clampInt(options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS, 1, 6e5),
37927
+ this.baseDelayMs
37928
+ );
37498
37929
  this.sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
37499
37930
  this.random = options.random ?? Math.random;
37500
37931
  }
@@ -37505,6 +37936,18 @@ var RequestGate = class {
37505
37936
  get limit() {
37506
37937
  return this.maxConcurrent;
37507
37938
  }
37939
+ /** 解決済みの GET リトライ回数(テスト・診断用) */
37940
+ get retries() {
37941
+ return this.maxRetries;
37942
+ }
37943
+ /** 解決済みのバックオフ初期値ミリ秒(テスト・診断用) */
37944
+ get retryBaseDelayMs() {
37945
+ return this.baseDelayMs;
37946
+ }
37947
+ /** 解決済みのバックオフ上限ミリ秒(テスト・診断用) */
37948
+ get retryMaxDelayMs() {
37949
+ return this.maxDelayMs;
37950
+ }
37508
37951
  /** GET 系: セマフォ + リトライ付きで実行する */
37509
37952
  async runReadOnly(fn) {
37510
37953
  let attempt = 0;
@@ -37561,11 +38004,11 @@ function withRequestGate(client, gate) {
37561
38004
  };
37562
38005
  }
37563
38006
  var globalGate = null;
37564
- function getGlobalRequestGate(limitHint) {
38007
+ function getGlobalRequestGate(options) {
37565
38008
  if (globalGate === null) {
37566
- const envValue = Number(process.env.KSQL_MAX_CONCURRENT);
37567
- const limit = Number.isInteger(envValue) && envValue > 0 ? envValue : limitHint;
37568
- globalGate = new RequestGate({ maxConcurrent: limit });
38009
+ globalGate = new RequestGate(
38010
+ typeof options === "number" ? { maxConcurrent: options } : options ?? {}
38011
+ );
37569
38012
  }
37570
38013
  return globalGate;
37571
38014
  }
@@ -37918,7 +38361,12 @@ async function createKsqlRuntime(serverOptions, input) {
37918
38361
  };
37919
38362
  const gatedClient = withRequestGate(
37920
38363
  routedClient,
37921
- getGlobalRequestGate(profile2.query?.maxConcurrent)
38364
+ getGlobalRequestGate(resolveRequestGateOptions({
38365
+ maxConcurrent: profile2.query?.maxConcurrent,
38366
+ maxRetries: profile2.query?.retry,
38367
+ baseDelayMs: profile2.query?.retryBaseDelayMs,
38368
+ maxDelayMs: profile2.query?.retryMaxDelayMs
38369
+ }))
37922
38370
  );
37923
38371
  return {
37924
38372
  sql,
@@ -38175,57 +38623,11 @@ function toSelectPayload(result) {
38175
38623
  warnings: result.warnings ?? []
38176
38624
  };
38177
38625
  }
38178
- function toMutationSummary(result) {
38179
- if (result.type === "INSERT") {
38180
- return { insertedCount: result.insertedCount, createdIds: result.createdIds };
38181
- }
38182
- if (result.type === "UPDATE") return { updatedCount: result.updatedCount };
38183
- if (result.type === "DELETE") return { deletedCount: result.deletedCount };
38184
- if (result.type === "UPSERT") {
38185
- return { insertedCount: result.insertedCount, updatedCount: result.updatedCount };
38186
- }
38187
- return { reorderedParentCount: result.reorderedParentCount };
38188
- }
38189
- function toBatchQueryPayload(batch, maxTotalRecords) {
38190
- const results = [];
38191
- let totalRows = 0;
38192
- const statements = batch.statements.map((s) => {
38193
- const entry = {
38194
- index: s.index,
38195
- type: s.type,
38196
- status: s.status
38197
- };
38198
- if (s.status === "error" && s.error) entry.error = s.error;
38199
- if (s.status === "skipped" && s.skippedReason) entry.skippedReason = s.skippedReason;
38200
- if (s.tempTable !== void 0) entry.tempTable = s.tempTable;
38201
- if (s.rowCount !== void 0) entry.rowCount = s.rowCount;
38202
- if (s.status === "success" && s.result?.type === "SELECT") {
38203
- totalRows += s.result.rowCount;
38204
- if (maxTotalRecords !== void 0 && totalRows > maxTotalRecords) {
38205
- throw new Error(
38206
- `ArgumentError: batch total rows (${totalRows}) exceed maxTotalRecords (${maxTotalRecords}).`
38207
- );
38208
- }
38209
- entry.resultIndex = results.length;
38210
- results.push({
38211
- columns: s.result.columns,
38212
- rows: s.result.rows,
38213
- rowCount: s.result.rowCount,
38214
- warnings: s.result.warnings ?? []
38215
- });
38216
- } else if (s.status === "success" && s.result && s.result.type !== "SELECT") {
38217
- Object.assign(entry, toMutationSummary(s.result));
38218
- }
38219
- return entry;
38220
- });
38626
+ function toAssertPayload(result) {
38221
38627
  return {
38222
- ok: batch.ok,
38223
- batch: true,
38224
- statementCount: batch.statementCount,
38225
- statements,
38226
- results,
38227
- // バッチ全体の警告(仕様 §6.2)。文ごとの警告は results[].warnings に入る
38228
- warnings: []
38628
+ ok: true,
38629
+ type: result.type,
38630
+ condition: result.condition
38229
38631
  };
38230
38632
  }
38231
38633
  function toMutationPayload(result) {
@@ -38428,7 +38830,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
38428
38830
  // HTTP クライアント側の per-request タイムアウトと同値になる
38429
38831
  timeoutMs: runtime2.timeout
38430
38832
  });
38431
- return toBatchQueryPayload(batchResult, input.maxTotalRecords);
38833
+ return { ...buildBatchEnvelope(batchResult, { maxTotalRecords: input.maxTotalRecords }) };
38432
38834
  }
38433
38835
  if (!validation.isReadOnly) {
38434
38836
  throw new Error(`ArgumentError: ${validation.statementType} is not allowed by ksql_query. Use ksql_mutate.`);
@@ -38441,6 +38843,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
38441
38843
  onLimitReached: input.onLimit ?? DEFAULT_ON_LIMIT,
38442
38844
  cacheContext: validation.cacheContext
38443
38845
  });
38846
+ if (result2.type === "ASSERT") return toAssertPayload(result2);
38444
38847
  if (result2.type !== "SELECT") {
38445
38848
  throw new Error(`ArgumentError: read-only query returned unexpected result type ${result2.type}.`);
38446
38849
  }
@@ -38460,6 +38863,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
38460
38863
  onLimitReached: runtime.onLimit,
38461
38864
  cacheContext: runtime.cacheContext
38462
38865
  });
38866
+ if (result.type === "ASSERT") return toAssertPayload(result);
38463
38867
  if (result.type !== "SELECT") {
38464
38868
  throw new Error(`ArgumentError: read-only query returned unexpected result type ${result.type}.`);
38465
38869
  }
@@ -38521,7 +38925,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
38521
38925
  return true;
38522
38926
  }
38523
38927
  });
38524
- const payload = toBatchQueryPayload(batchResult);
38928
+ const payload = buildBatchEnvelope(batchResult);
38525
38929
  if (selectBasedDml) {
38526
38930
  for (const entry of payload.statements) {
38527
38931
  if (entry.type !== "INSERT_SELECT" && entry.type !== "UPSERT_SELECT") continue;
@@ -38531,7 +38935,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
38531
38935
  entry.error = { ...error51, message: `${error51.message} ${SELECT_BASED_DML_READ_LIMIT_HINT}` };
38532
38936
  }
38533
38937
  }
38534
- return payload;
38938
+ return { ...payload };
38535
38939
  }
38536
38940
  async function mutate(input) {
38537
38941
  const dmlMaxRows = requireDmlApproval(input, "ksql_mutate");
@@ -38576,7 +38980,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
38576
38980
  } catch (err) {
38577
38981
  throw selectBasedDml ? appendSelectBasedDmlReadLimitHint(err) : err;
38578
38982
  }
38579
- if (result.type === "SELECT") {
38983
+ if (result.type === "SELECT" || result.type === "ASSERT") {
38580
38984
  throw new Error(`ArgumentError: ksql_mutate returned unexpected result type ${result.type}.`);
38581
38985
  }
38582
38986
  return toMutationPayload(result);
@@ -38854,7 +39258,7 @@ Options:
38854
39258
  -h, --help Show help
38855
39259
  `);
38856
39260
  }
38857
- var SERVER_VERSION = true ? "1.9.0" : "0.0.0-dev";
39261
+ var SERVER_VERSION = true ? "1.10.0" : "0.0.0-dev";
38858
39262
  function createServer(args) {
38859
39263
  const server = new McpServer({
38860
39264
  name: "ksql-mcp",
@@ -38876,7 +39280,7 @@ function createServer(args) {
38876
39280
  }, tools.explainTool);
38877
39281
  server.registerTool("ksql_query", {
38878
39282
  title: "Run read-only kSQL",
38879
- description: "Execute read-only kSQL: SELECT, WITH, UNION, EXPLAIN, SHOW APPS, DESCRIBE. Supports multi-statement batches with temp tables (CREATE TEMP TABLE #t AS SELECT ...; SELECT ... FROM #t;). DML is rejected.",
39283
+ description: "Execute read-only kSQL: SELECT, WITH, UNION, EXPLAIN, SHOW APPS, DESCRIBE, ASSERT. Supports multi-statement batches with temp tables (CREATE TEMP TABLE #t AS SELECT ...; SELECT ... FROM #t;). ASSERT <expr> <op> <expr> (or BETWEEN) is a runtime gate: on failure it raises AssertError and always stops the batch. DML is rejected.",
38880
39284
  inputSchema: queryInputShape
38881
39285
  }, tools.queryTool);
38882
39286
  server.registerTool("ksql_mutate", {