@rex0220/kintone-sql-tools 2.2.0 → 2.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 CHANGED
@@ -16,13 +16,13 @@ kintone アプリを SQL 風の構文で操作するツールセットです。
16
16
  - プラグイン: read-only バッチのみ(最終結果を表示)
17
17
  - **`ASSERT`(実行時ゲート。DML 前の件数ガード / CLI ヘルスチェック)**(v1.10.0)
18
18
  - サブテーブル仮想テーブル(`APP100$明細`)
19
- - CLI 拡張 `APP@profile`
20
- - 同一 SQL 内で同一 APP の profile 混在を許可
21
- - `INSERT/UPDATE/UPSERT` 対応、`DELETE` は未対応
22
- - CLI / MCP の論理アプリ参照 `LAPP_<NAME>`
23
- - profile ごとの `logicalApps` で、同じ SQL を異なる物理アプリ ID へ安全に解決
24
- - `APP100` は常に物理 ID 100 のまま(暗黙変換なし)
25
- - `allowPhysicalAppRefs: false` で、その profile の物理 `APPxxx` 直接参照を禁止可能
19
+ - CLI 拡張 `APP@profile`
20
+ - 同一 SQL 内で同一 APP の profile 混在を許可
21
+ - `INSERT/UPDATE/UPSERT` 対応、`DELETE` は未対応
22
+ - CLI / MCP の論理アプリ参照 `LAPP_<NAME>`
23
+ - profile ごとの `logicalApps` で、同じ SQL を異なる物理アプリ ID へ安全に解決
24
+ - `APP100` は常に物理 ID 100 のまま(暗黙変換なし)
25
+ - `allowPhysicalAppRefs: false` で、その profile の物理 `APPxxx` 直接参照を禁止可能
26
26
  - `FROM` 省略 SELECT(例: `SELECT 'xxx' AS a`)
27
27
 
28
28
  ## インストール
@@ -50,11 +50,11 @@ npm run build:plugin
50
50
 
51
51
  ## 使い分け(CLI / Plugin)
52
52
 
53
- - CLI を使うケース:
53
+ - CLI を使うケース:
54
54
  - DML を含むバッチ実行・SQL ファイル実行(`-f`)
55
55
  - CI/CD 連携
56
- - `APP@profile` を使った環境切替
57
- - `LAPP_<NAME>` を使った配置非依存 SQL
56
+ - `APP@profile` を使った環境切替
57
+ - `LAPP_<NAME>` を使った配置非依存 SQL
58
58
  - `--dry-run` / `EXPLAIN` による安全確認
59
59
 
60
60
  - Plugin を使うケース:
@@ -62,14 +62,14 @@ npm run build:plugin
62
62
  - 非エンジニア向けの運用
63
63
  - UI で結果確認したい場合
64
64
 
65
- - MCP を使うケース:
65
+ - MCP を使うケース:
66
66
  - Claude 等の AI クライアントから kintone を照会・更新
67
- - 一時テーブルで中間結果をサーバー内に保持し、AI のコンテキスト消費を抑えたい場合
68
- - validation / EXPLAIN で論理名から最終的な物理アプリ ID への解決を確認したい場合
67
+ - 一時テーブルで中間結果をサーバー内に保持し、AI のコンテキスト消費を抑えたい場合
68
+ - validation / EXPLAIN で論理名から最終的な物理アプリ ID への解決を確認したい場合
69
69
 
70
70
  注意:
71
71
 
72
- - `APP@profile` と `LAPP_<NAME>[@profile]` は Node.js runtime(CLI / MCP)の拡張です。plugin 側では非対応です。
72
+ - `APP@profile` と `LAPP_<NAME>[@profile]` は Node.js runtime(CLI / MCP)の拡張です。plugin 側では非対応です。
73
73
 
74
74
  ## 最短実行例(CLI)
75
75
 
@@ -104,39 +104,39 @@ node dist-cli/ksql.js --console --base-url https://example.cybozu.com --token xx
104
104
  - 既定: `./ksql.config.json`
105
105
  - profile 切替: `--profile <name>`
106
106
 
107
- 例:
107
+ 例:
108
108
 
109
109
  ```bash
110
- node dist-cli/ksql.js --config ./ksql.config.json --profile dev -e "SELECT * FROM APP100"
111
- ```
112
-
113
- 論理アプリ参照を使う場合、profile ごとに論理名と物理 ID を定義します。
114
-
115
- ```json
116
- {
117
- "defaultProfile": "dev",
118
- "profiles": {
119
- "dev": {
120
- "baseUrl": "https://dev.example.cybozu.com",
121
- "logicalApps": { "ORDERS": 100 },
122
- "tokenMap": { "APP100": "env:DEV_ORDERS_TOKEN" }
123
- },
124
- "prod": {
125
- "baseUrl": "https://prod.example.cybozu.com",
126
- "allowPhysicalAppRefs": false,
127
- "logicalApps": { "ORDERS": 1200 },
128
- "tokenMap": { "APP1200": "env:PROD_ORDERS_TOKEN" }
129
- }
130
- }
131
- }
132
- ```
133
-
134
- ```bash
135
- node dist-cli/ksql.js --config ./ksql.config.json --profile dev -e "SELECT * FROM LAPP_ORDERS"
136
- node dist-cli/ksql.js --config ./ksql.config.json --profile prod -e "SELECT * FROM LAPP_ORDERS"
137
- ```
138
-
139
- どちらも同じ SQL ですが、前者は `APP100`、後者は `APP1200` に解決されます。`logicalApps` のキーは `LAPP_` を付けない ASCII 論理名です。`APP100`、`100`、`LAPP_ORDERS` は設定キーとして拒否されます。
110
+ node dist-cli/ksql.js --config ./ksql.config.json --profile dev -e "SELECT * FROM APP100"
111
+ ```
112
+
113
+ 論理アプリ参照を使う場合、profile ごとに論理名と物理 ID を定義します。
114
+
115
+ ```json
116
+ {
117
+ "defaultProfile": "dev",
118
+ "profiles": {
119
+ "dev": {
120
+ "baseUrl": "https://dev.example.cybozu.com",
121
+ "logicalApps": { "ORDERS": 100 },
122
+ "tokenMap": { "APP100": "env:DEV_ORDERS_TOKEN" }
123
+ },
124
+ "prod": {
125
+ "baseUrl": "https://prod.example.cybozu.com",
126
+ "allowPhysicalAppRefs": false,
127
+ "logicalApps": { "ORDERS": 1200 },
128
+ "tokenMap": { "APP1200": "env:PROD_ORDERS_TOKEN" }
129
+ }
130
+ }
131
+ }
132
+ ```
133
+
134
+ ```bash
135
+ node dist-cli/ksql.js --config ./ksql.config.json --profile dev -e "SELECT * FROM LAPP_ORDERS"
136
+ node dist-cli/ksql.js --config ./ksql.config.json --profile prod -e "SELECT * FROM LAPP_ORDERS"
137
+ ```
138
+
139
+ どちらも同じ SQL ですが、前者は `APP100`、後者は `APP1200` に解決されます。`logicalApps` のキーは `LAPP_` を付けない ASCII 論理名です。`APP100`、`100`、`LAPP_ORDERS` は設定キーとして拒否されます。
140
140
 
141
141
  ## CLI オプション
142
142
 
@@ -154,6 +154,7 @@ Options:
154
154
  -f, --file <path> Execute SQL file
155
155
  --console Start interactive console mode
156
156
  --dry-run Parse and show execution plan only
157
+ --var <name=value> Override a DECLARE variable (repeatable; not for secrets)
157
158
  --format <type> Output format: table | json | jsonl | csv | markdown | md
158
159
  (batch + json: prints one JSON envelope for the whole batch)
159
160
  --max-records <n> Max records to fetch (default: 500)
@@ -236,7 +237,7 @@ Options:
236
237
  - [CLI / Console 仕様](docs/ksql_cli_console_spec.md)
237
238
  - [バッチ実行・一時テーブル仕様](docs/ksql_batch_temp_table_spec.md)
238
239
  - [MCP サーバー仕様](docs/ksql_mcp_server_spec.md) / [Claude Desktop への導入(MCPB)](docs/ksql_mcpb_claude_desktop_install.md)
239
- - [APP@profile 仕様](docs/cli_app_profile_spec.md)
240
+ - [APP@profile 仕様](docs/cli_app_profile_spec.md)
240
241
  - [公開前チェックリスト](docs/internal/public_release_checklist.md)
241
242
 
242
243
  ## ライセンス
package/dist-cli/ksql.js CHANGED
@@ -599,13 +599,14 @@ var Parser = class {
599
599
  const upper = tok.value.toUpperCase();
600
600
  if (upper === "CREATE") return this.parseCreateTempTable();
601
601
  if (upper === "DROP") return this.parseDropTempTable();
602
+ if (upper === "DECLARE") return this.parseDeclareVariable();
602
603
  break;
603
604
  }
604
605
  default:
605
606
  break;
606
607
  }
607
608
  throw new ParseError(
608
- "SELECT / INSERT / UPDATE / DELETE / REORDER / WITH / SHOW / DESCRIBE / EXPLAIN / CREATE TEMP TABLE / DROP TEMP TABLE / SET / ASSERT \u306E\u3044\u305A\u308C\u304B\u3067\u59CB\u307E\u308B SQL \u6587\u304C\u5FC5\u8981\u3067\u3059",
609
+ "SELECT / INSERT / UPDATE / DELETE / REORDER / WITH / SHOW / DESCRIBE / EXPLAIN / CREATE TEMP TABLE / DROP TEMP TABLE / SET / DECLARE / ASSERT \u306E\u3044\u305A\u308C\u304B\u3067\u59CB\u307E\u308B SQL \u6587\u304C\u5FC5\u8981\u3067\u3059",
609
610
  tok
610
611
  );
611
612
  }
@@ -616,20 +617,48 @@ var Parser = class {
616
617
  this.expect("SET" /* SET */);
617
618
  const variable = this.expect("VARIABLE" /* VARIABLE */, "SET \u306E\u5F8C\u306B\u306F\u5909\u6570\u540D\uFF08\u4F8B: @name\uFF09\u304C\u5FC5\u8981\u3067\u3059");
618
619
  this.expect("=" /* EQ */);
619
- const expr = this.parseScalarExpr();
620
+ const expr = this.parseScalarExpr("SET", true);
620
621
  return { type: "SET_VARIABLE", name: variable.value.slice(1).toLowerCase(), expr };
621
622
  }
622
- /** SET RHS 専用。既存式パーサーで構文を読み、フィールド参照を明示的に拒否する。 */
623
- parseScalarExpr() {
623
+ parseDeclareVariable() {
624
+ this.advance();
625
+ const variable = this.expect("VARIABLE" /* VARIABLE */, "DECLARE \u306E\u5F8C\u306B\u306F\u5909\u6570\u540D\uFF08\u4F8B: @name\uFF09\u304C\u5FC5\u8981\u3067\u3059");
626
+ this.expect("=" /* EQ */);
627
+ const expr = this.parseScalarExpr("DECLARE", false);
628
+ if (expr.type === "SCALAR_SUBQUERY") {
629
+ throw new ParseError("DECLARE \u306E\u65E2\u5B9A\u5024\u306B\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", this.peek());
630
+ }
631
+ return { type: "DECLARE_VARIABLE", name: variable.value.slice(1).toLowerCase(), default: expr };
632
+ }
633
+ /** SET / DECLARE RHS 専用。既存式パーサーで構文を読み、フィールド参照を明示的に拒否する。 */
634
+ parseScalarExpr(context, allowScalarSubquery) {
624
635
  const tok = this.peek();
625
636
  if (tok.kind === "VARIABLE" /* VARIABLE */) {
626
- throw new ParseError("SET \u306E\u53F3\u8FBA\u3067\u306F\u4ED6\u306E\u5909\u6570\u3092\u53C2\u7167\u3067\u304D\u307E\u305B\u3093\uFF08Phase 1a\uFF09", tok);
637
+ throw new ParseError(`${context} \u306E\u53F3\u8FBA\u3067\u306F\u4ED6\u306E\u5909\u6570\u3092\u53C2\u7167\u3067\u304D\u307E\u305B\u3093`, tok);
627
638
  }
628
639
  if (tok.kind === "NULL" /* NULL */) {
629
- throw new ParseError("SET \u306E\u53F3\u8FBA\u3067 NULL \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF08Phase 1a\uFF09", tok);
640
+ throw new ParseError(`${context} \u306E\u53F3\u8FBA\u3067 NULL \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093`, tok);
630
641
  }
631
642
  if (tok.kind === "(" /* LPAREN */ && this.peekAt(1).kind === "SELECT" /* SELECT */) {
632
- throw new ParseError("SET \u306E\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u4EE3\u5165\u306F Phase 1b \u3067\u5BFE\u5FDC\u4E88\u5B9A\u3067\u3059", tok);
643
+ if (!allowScalarSubquery) {
644
+ throw new ParseError("DECLARE \u306E\u65E2\u5B9A\u5024\u306B\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
645
+ }
646
+ this.advance();
647
+ const query = this.parseSelect();
648
+ this.expect(")" /* RPAREN */);
649
+ if (this.isArithOp(this.peek().kind)) {
650
+ throw new ParseError(
651
+ "\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",
652
+ this.peek()
653
+ );
654
+ }
655
+ const hasWildcard = query.columns.some(
656
+ (c) => c.type === "WILDCARD" || c.type === "PARENT_WILDCARD"
657
+ );
658
+ if (!hasWildcard && query.columns.length !== 1) {
659
+ throw new ParseError("scalar subquery in SET must return exactly 1 column.", tok);
660
+ }
661
+ return { type: "SCALAR_SUBQUERY", query };
633
662
  }
634
663
  if (tok.kind === "STRING" /* STRING */) {
635
664
  this.advance();
@@ -637,7 +666,7 @@ var Parser = class {
637
666
  }
638
667
  if (tok.kind === "LOGINUSER" /* LOGINUSER */) {
639
668
  throw new ParseError(
640
- "SET \u306E\u53F3\u8FBA\u3067 LOGINUSER() \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF08\u5B9F\u884C\u74B0\u5883\u5171\u901A\u306E\u30ED\u30B0\u30A4\u30F3\u30E6\u30FC\u30B6\u30FC\u89E3\u6C7A\u306F\u672A\u5BFE\u5FDC\u3067\u3059\uFF09",
669
+ `${context} \u306E\u53F3\u8FBA\u3067 LOGINUSER() \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF08\u5B9F\u884C\u74B0\u5883\u5171\u901A\u306E\u30ED\u30B0\u30A4\u30F3\u30E6\u30FC\u30B6\u30FC\u89E3\u6C7A\u306F\u672A\u5BFE\u5FDC\u3067\u3059\uFF09`,
641
670
  tok
642
671
  );
643
672
  }
@@ -645,22 +674,22 @@ var Parser = class {
645
674
  return this.parseSqlValue();
646
675
  }
647
676
  const expr = this.parseArithAddSub();
648
- this.rejectNonScalarExpr(expr, tok);
677
+ this.rejectNonScalarExpr(expr, tok, context);
649
678
  if (expr.type === "NUMBER" || expr.type === "STRING_FUNC" || expr.type === "ARITH") return expr;
650
- throw new ParseError("SET \u306E\u53F3\u8FBA\u306B\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u3092\u542B\u307E\u306A\u3044\u30B9\u30AB\u30E9\u30FC\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044", tok);
679
+ throw new ParseError(`${context} \u306E\u53F3\u8FBA\u306B\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u3092\u542B\u307E\u306A\u3044\u30B9\u30AB\u30E9\u30FC\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044`, tok);
651
680
  }
652
- rejectNonScalarExpr(node, tok) {
681
+ rejectNonScalarExpr(node, tok, context) {
653
682
  if (node.type === "STRING" || node.type === "NUMBER") return;
654
683
  if (node.type === "FIELD_REF" || node.type === "AGG_REF") {
655
- throw new ParseError("SET \u306E\u53F3\u8FBA\u3067\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u30FB\u96C6\u8A08\u95A2\u6570\u3092\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
684
+ throw new ParseError(`${context} \u306E\u53F3\u8FBA\u3067\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u30FB\u96C6\u8A08\u95A2\u6570\u3092\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093`, tok);
656
685
  }
657
686
  if (node.type === "ARITH" || node.type === "AGG_ARITH") {
658
- this.rejectNonScalarExpr(node.left, tok);
659
- this.rejectNonScalarExpr(node.right, tok);
687
+ this.rejectNonScalarExpr(node.left, tok, context);
688
+ this.rejectNonScalarExpr(node.right, tok, context);
660
689
  return;
661
690
  }
662
691
  if (node.type === "STRING_FUNC") {
663
- for (const arg of node.args) this.rejectNonScalarExpr(arg, tok);
692
+ for (const arg of node.args) this.rejectNonScalarExpr(arg, tok, context);
664
693
  }
665
694
  }
666
695
  // ----------------------------------------------------------
@@ -2286,7 +2315,7 @@ function isDmlType(type) {
2286
2315
  return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
2287
2316
  }
2288
2317
  function isReadOnlyType(type) {
2289
- return type === "SELECT" || type === "UNION" || type === "WITH" || type === "EXPLAIN" || type === "SHOW_APPS" || type === "DESCRIBE" || type === "CREATE_TEMP_TABLE" || type === "DROP_TEMP_TABLE" || type === "SET_VARIABLE" || type === "ASSERT";
2318
+ return type === "SELECT" || type === "UNION" || type === "WITH" || type === "EXPLAIN" || type === "SHOW_APPS" || type === "DESCRIBE" || type === "CREATE_TEMP_TABLE" || type === "DROP_TEMP_TABLE" || type === "SET_VARIABLE" || type === "DECLARE_VARIABLE" || type === "ASSERT";
2290
2319
  }
2291
2320
  function hasWhereClause(stmt) {
2292
2321
  if (!stmt || typeof stmt !== "object") return false;
@@ -2360,8 +2389,9 @@ function analyzeBatch(statements) {
2360
2389
  }
2361
2390
  if (statements.length === 1) {
2362
2391
  const t = statements[0].type;
2363
- if (t === "SET_VARIABLE") {
2364
- throw new BatchAnalysisError("ArgumentError: SET variable requires a batch.", 0);
2392
+ if (t === "SET_VARIABLE" || t === "DECLARE_VARIABLE") {
2393
+ const verb = t === "SET_VARIABLE" ? "SET" : "DECLARE";
2394
+ throw new BatchAnalysisError(`ArgumentError: ${verb} variable requires a batch.`, 0);
2365
2395
  }
2366
2396
  if (t === "CREATE_TEMP_TABLE" || t === "DROP_TEMP_TABLE") {
2367
2397
  const verb = t === "CREATE_TEMP_TABLE" ? "CREATE TEMP TABLE" : "DROP TEMP TABLE";
@@ -2395,7 +2425,7 @@ function analyzeBatch(statements) {
2395
2425
  }
2396
2426
  def.referencedBy.push(index);
2397
2427
  }
2398
- if (stmt.type === "SET_VARIABLE") {
2428
+ if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
2399
2429
  if (variableDefs.has(stmt.name)) {
2400
2430
  throw new BatchAnalysisError(`ParseError: variable @${stmt.name} is already defined.`, index);
2401
2431
  }
@@ -2492,6 +2522,40 @@ function analyzeBatch(statements) {
2492
2522
  };
2493
2523
  }
2494
2524
 
2525
+ // src/core/batchVariables.ts
2526
+ var VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
2527
+ function normalizeBatchVariableName(name) {
2528
+ if (!VARIABLE_NAME_RE.test(name)) {
2529
+ throw new Error(
2530
+ `ArgumentError: invalid variable name "${name}". Use a name without @ matching [A-Za-z_][A-Za-z0-9_]{0,63}.`
2531
+ );
2532
+ }
2533
+ return name.toLowerCase();
2534
+ }
2535
+ function normalizeBatchVariables(input) {
2536
+ const normalized = /* @__PURE__ */ Object.create(null);
2537
+ for (const [rawName, value] of Object.entries(input ?? {})) {
2538
+ const name = normalizeBatchVariableName(rawName);
2539
+ if (Object.prototype.hasOwnProperty.call(normalized, name)) {
2540
+ throw new Error(`ArgumentError: variable "${rawName}" is specified more than once.`);
2541
+ }
2542
+ normalized[name] = value;
2543
+ }
2544
+ return normalized;
2545
+ }
2546
+ function validateDeclaredBatchVariables(statements, input) {
2547
+ const normalized = normalizeBatchVariables(input);
2548
+ const declared = new Set(
2549
+ statements.filter((stmt) => stmt.type === "DECLARE_VARIABLE").map((stmt) => stmt.name)
2550
+ );
2551
+ for (const name of Object.keys(normalized)) {
2552
+ if (!declared.has(name)) {
2553
+ throw new Error(`ArgumentError: injected variable @${name} is not declared.`);
2554
+ }
2555
+ }
2556
+ return normalized;
2557
+ }
2558
+
2495
2559
  // src/core/scalarCompare.ts
2496
2560
  function compareScalarValues(op, leftStr, rightStr) {
2497
2561
  if (op === "=") return leftStr === rightStr;
@@ -4823,6 +4887,8 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
4823
4887
  throw new Error("ArgumentError: DROP TEMP TABLE requires a batch (temp tables are batch-scoped).");
4824
4888
  case "SET_VARIABLE":
4825
4889
  throw new Error("ArgumentError: SET variable requires a batch.");
4890
+ case "DECLARE_VARIABLE":
4891
+ throw new Error("ArgumentError: DECLARE variable requires a batch.");
4826
4892
  case "ASSERT":
4827
4893
  return executeAssert(stmt, client, options, cacheContext);
4828
4894
  }
@@ -4836,6 +4902,8 @@ var BatchTimeoutError = class extends Error {
4836
4902
  async function executeBatch(sql, client, options = {}) {
4837
4903
  const statements = parseSqlBatch(sql);
4838
4904
  const analysis = analyzeBatch(statements);
4905
+ const injectedVariables = validateDeclaredBatchVariables(statements, options.variables);
4906
+ const batchOptions = { ...options, variables: injectedVariables };
4839
4907
  if (options.continueOnError && analysis.containsDml) {
4840
4908
  throw new Error("ArgumentError: continueOnError is not allowed for batches containing DML.");
4841
4909
  }
@@ -4880,16 +4948,16 @@ async function executeBatch(sql, client, options = {}) {
4880
4948
  }
4881
4949
  try {
4882
4950
  const remaining = deadline !== null ? deadline - Date.now() : null;
4883
- const userConfirm = options.confirm;
4951
+ const userConfirm = batchOptions.confirm;
4884
4952
  const stmtOptions = userConfirm ? {
4885
- ...options,
4953
+ ...batchOptions,
4886
4954
  confirm: (count, operation) => userConfirm(count, operation, {
4887
4955
  statementIndex: i,
4888
4956
  statementCount: statements.length,
4889
4957
  statementType: info.statementType,
4890
4958
  targetAppId: info.targetAppId
4891
4959
  })
4892
- } : options;
4960
+ } : batchOptions;
4893
4961
  const outcome = await runWithDeadline(
4894
4962
  executeBatchStatement(statements[i], info, countedClient, stmtOptions, cacheContext, tempTables, variables),
4895
4963
  remaining
@@ -4902,7 +4970,7 @@ async function executeBatch(sql, client, options = {}) {
4902
4970
  aborted = "timeout";
4903
4971
  } else if (e instanceof AssertError) {
4904
4972
  aborted = "assertion";
4905
- } else if (info.statementType === "SET_VARIABLE") {
4973
+ } else if (info.statementType === "SET_VARIABLE" || info.statementType === "DECLARE_VARIABLE") {
4906
4974
  aborted = "fail-fast";
4907
4975
  } else if (!options.continueOnError) {
4908
4976
  aborted = "fail-fast";
@@ -4920,7 +4988,36 @@ async function executeBatch(sql, client, options = {}) {
4920
4988
  }
4921
4989
  async function executeBatchStatement(stmt, info, client, options, cacheContext, tempTables, variables) {
4922
4990
  if (stmt.type === "SET_VARIABLE") {
4923
- variables.set(stmt.name, evaluateScalarExpr(stmt.expr));
4991
+ const resolvedStmt2 = resolveVariableRefs(stmt, variables);
4992
+ if (resolvedStmt2.expr.type === "SCALAR_SUBQUERY") {
4993
+ try {
4994
+ const value = await evaluateScalarSubquery(
4995
+ resolvedStmt2.expr.query,
4996
+ client,
4997
+ options,
4998
+ cacheContext,
4999
+ tempTables
5000
+ );
5001
+ variables.set(stmt.name, { type: "string", value });
5002
+ } catch (e) {
5003
+ if (e instanceof ScalarSubqueryError) {
5004
+ throw new Error(`ArgumentError: ${e.message}`);
5005
+ }
5006
+ throw e;
5007
+ }
5008
+ } else {
5009
+ variables.set(stmt.name, evaluateScalarExpr(resolvedStmt2.expr));
5010
+ }
5011
+ return {};
5012
+ }
5013
+ if (stmt.type === "DECLARE_VARIABLE") {
5014
+ const injected = options.variables ?? {};
5015
+ if (Object.prototype.hasOwnProperty.call(injected, stmt.name)) {
5016
+ variables.set(stmt.name, { type: "string", value: injected[stmt.name] });
5017
+ } else {
5018
+ const value = evaluateScalarExpr(stmt.default);
5019
+ variables.set(stmt.name, { type: "string", value: String(value.value) });
5020
+ }
4924
5021
  return {};
4925
5022
  }
4926
5023
  const resolvedStmt = resolveVariableRefs(stmt, variables);
@@ -5083,6 +5180,12 @@ var AssertError = class extends Error {
5083
5180
  this.name = "AssertError";
5084
5181
  }
5085
5182
  };
5183
+ var ScalarSubqueryError = class extends Error {
5184
+ constructor(message) {
5185
+ super(message);
5186
+ this.name = "ScalarSubqueryError";
5187
+ }
5188
+ };
5086
5189
  async function executeAssert(stmt, client, options, cacheContext, tempTables) {
5087
5190
  const left = await evalAssertOperand(stmt.left, client, options, cacheContext, tempTables);
5088
5191
  if (stmt.op === "BETWEEN") {
@@ -5116,25 +5219,38 @@ async function evalAssertOperand(operand, client, options, cacheContext, tempTab
5116
5219
  case "ARITH":
5117
5220
  return String(evalAssertArith(operand));
5118
5221
  case "SCALAR_SUBQUERY": {
5119
- const { query, probed } = withScalarProbeLimit(operand.query);
5120
- const result = await runSubquery(query, client, options, cacheContext, tempTables);
5121
- if (result.columns.length > 1) {
5122
- throw new AssertError(
5123
- `scalar subquery returned ${result.columns.length} columns (expected 1 column).`
5222
+ try {
5223
+ return await evaluateScalarSubquery(
5224
+ operand.query,
5225
+ client,
5226
+ options,
5227
+ cacheContext,
5228
+ tempTables
5124
5229
  );
5230
+ } catch (e) {
5231
+ if (e instanceof ScalarSubqueryError) throw new AssertError(e.message);
5232
+ throw e;
5125
5233
  }
5126
- if (result.rowCount === 0) {
5127
- throw new AssertError("scalar subquery returned no rows (expected 1 row).");
5128
- }
5129
- if (result.rowCount > 1) {
5130
- const rows = probed && result.rowCount === 2 ? "2 or more rows" : `${result.rowCount} rows`;
5131
- throw new AssertError(`scalar subquery returned ${rows} (expected 1 row).`);
5132
- }
5133
- const col = result.columns[0] ?? "";
5134
- return result.rows[0]?.[col] ?? "";
5135
5234
  }
5136
5235
  }
5137
5236
  }
5237
+ async function evaluateScalarSubquery(sourceQuery, client, options, cacheContext, tempTables) {
5238
+ const { query, probed } = withScalarProbeLimit(sourceQuery);
5239
+ const result = await runSubquery(query, client, options, cacheContext, tempTables);
5240
+ if (result.columns.length !== 1) {
5241
+ throw new ScalarSubqueryError(
5242
+ `scalar subquery returned ${result.columns.length} columns (expected 1 column).`
5243
+ );
5244
+ }
5245
+ if (result.rowCount === 0) {
5246
+ throw new ScalarSubqueryError("scalar subquery returned no rows (expected 1 row).");
5247
+ }
5248
+ if (result.rowCount > 1) {
5249
+ const rows = probed && result.rowCount === 2 ? "2 or more rows" : `${result.rowCount} rows`;
5250
+ throw new ScalarSubqueryError(`scalar subquery returned ${rows} (expected 1 row).`);
5251
+ }
5252
+ return result.rows[0]?.[result.columns[0]] ?? "";
5253
+ }
5138
5254
  function withScalarProbeLimit(query) {
5139
5255
  const hasAgg = query.groupBy.length > 0 || query.columns.some((c) => c.type === "AGGREGATE" || c.type === "ARITH_AGG_COL");
5140
5256
  if (hasAgg || query.distinct || query.limit !== null) return { query, probed: false };
@@ -6629,20 +6745,21 @@ async function resolveScalarColumns(columns, client, options, cacheContext, cteC
6629
6745
  pending.forEach(([i], idx) => cache.set(i, values[idx]));
6630
6746
  return cache;
6631
6747
  }
6632
- function buildBatchExplainPlans(sql) {
6748
+ function buildBatchExplainPlans(sql, injectedVariables) {
6633
6749
  const statements = parseSqlBatch(sql);
6634
6750
  const analysis = analyzeBatch(statements);
6751
+ validateDeclaredBatchVariables(statements, injectedVariables);
6635
6752
  const variables = /* @__PURE__ */ new Map();
6636
6753
  return {
6637
6754
  statementCount: statements.length,
6638
6755
  statements: statements.map((stmt, i) => {
6639
- const planStmt = stmt.type === "SET_VARIABLE" ? stmt : resolveVariableRefs(stmt, variables);
6756
+ const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr: resolveVariableRefs(stmt.expr, variables) } : stmt : resolveVariableRefs(stmt, variables);
6640
6757
  const result = {
6641
6758
  index: i,
6642
6759
  type: analysis.statements[i].statementType,
6643
6760
  plan: buildBatchStatementPlan(planStmt, analysis.statements[i])
6644
6761
  };
6645
- if (stmt.type === "SET_VARIABLE") {
6762
+ if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
6646
6763
  variables.set(stmt.name, { type: "string", value: `@${stmt.name}` });
6647
6764
  }
6648
6765
  return result;
@@ -6665,11 +6782,26 @@ function buildBatchStatementPlan(stmt, info) {
6665
6782
  ];
6666
6783
  }
6667
6784
  if (stmt.type === "SET_VARIABLE") {
6785
+ if (stmt.expr.type === "SCALAR_SUBQUERY") {
6786
+ const subInfo = hasTempTableRef(stmt.expr.query) ? info : { ...info, tempTablesReferenced: [] };
6787
+ return [
6788
+ `SET @${stmt.name} = (SELECT ...)`,
6789
+ " value: \u30B5\u30D6\u30AF\u30A8\u30EA\u3092\u5B9F\u884C\u6642\u306B1\u56DE\u8A55\u4FA1\uFF081\u884C1\u5217\u30FB\u30D0\u30C3\u30C1\u5185\u5B9A\u6570\u30FB\u7D50\u679C\u30E1\u30BF\u30C7\u30FC\u30BF\u306B\u306F\u975E\u516C\u958B\uFF09",
6790
+ " subquery:",
6791
+ ...buildPlanForBatchQuery(stmt.expr.query, subInfo).map((l) => ` ${l}`)
6792
+ ];
6793
+ }
6668
6794
  return [
6669
6795
  `SET @${stmt.name} = <scalar expression>`,
6670
6796
  " value: \u5B9F\u884C\u6642\u306B1\u56DE\u8A55\u4FA1\uFF08\u30D0\u30C3\u30C1\u5185\u5B9A\u6570\u30FB\u7D50\u679C\u30E1\u30BF\u30C7\u30FC\u30BF\u306B\u306F\u975E\u516C\u958B\uFF09"
6671
6797
  ];
6672
6798
  }
6799
+ if (stmt.type === "DECLARE_VARIABLE") {
6800
+ return [
6801
+ `DECLARE @${stmt.name} = <default scalar expression>`,
6802
+ " value: \u5916\u90E8\u6CE8\u5165\u304C\u3042\u308C\u3070\u63A1\u7528\u3001\u306A\u3051\u308C\u3070\u65E2\u5B9A\u5024\u3092\u5B9F\u884C\u6642\u306B1\u56DE\u8A55\u4FA1\uFF08\u5024\u306F\u975E\u516C\u958B\uFF09"
6803
+ ];
6804
+ }
6673
6805
  if (stmt.type === "SHOW_APPS") return ["SHOW APPS\uFF08\u30A2\u30D7\u30EA\u4E00\u89A7\u306E\u53D6\u5F97\uFF09"];
6674
6806
  if (stmt.type === "DESCRIBE") return [`DESCRIBE APP${stmt.appId}\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u5B9A\u7FA9\u306E\u53D6\u5F97\uFF09`];
6675
6807
  if (stmt.type === "EXPLAIN") return buildPlanForBatchQuery(stmt.query, info);
@@ -8131,6 +8263,7 @@ Options:
8131
8263
  -f, --file <path> Execute SQL file
8132
8264
  --console Start interactive console mode
8133
8265
  --dry-run Parse and show execution plan only
8266
+ --var <name=value> Override a DECLARE variable (repeatable; not for secrets)
8134
8267
  --format <type> Output format: table | json | jsonl | csv | markdown | md
8135
8268
  (batch + json: prints one JSON envelope for the whole batch)
8136
8269
  --max-records <n> Max records to fetch (default: 500)
@@ -8200,6 +8333,8 @@ function parseArgs(argv) {
8200
8333
  password: null,
8201
8334
  token: null,
8202
8335
  tokenMap: {},
8336
+ // `__proto__` も有効な変数名なので prototype のない辞書で保持する。
8337
+ variables: /* @__PURE__ */ Object.create(null),
8203
8338
  tokenFile: null,
8204
8339
  app: null,
8205
8340
  diagRecordId: null,
@@ -8294,6 +8429,19 @@ function parseArgs(argv) {
8294
8429
  continue;
8295
8430
  }
8296
8431
  const v = argv[i + 1];
8432
+ if (a === "--var") {
8433
+ const raw = v ?? "";
8434
+ const eq = raw.indexOf("=");
8435
+ if (eq < 0) throw new Error("ArgumentError: --var must use name=value.");
8436
+ const rawName = raw.slice(0, eq);
8437
+ const name = normalizeBatchVariableName(rawName);
8438
+ if (Object.prototype.hasOwnProperty.call(out.variables, name)) {
8439
+ throw new Error(`ArgumentError: variable "${rawName}" is specified more than once.`);
8440
+ }
8441
+ out.variables[name] = raw.slice(eq + 1);
8442
+ i++;
8443
+ continue;
8444
+ }
8297
8445
  if (a === "-e" || a === "--execute") {
8298
8446
  out.executeSql = v ?? "";
8299
8447
  i++;
@@ -8850,6 +8998,7 @@ function buildReplExecArgv(base, sql, dryRun, format) {
8850
8998
  const argv = ["-e", sql];
8851
8999
  if (dryRun) argv.push("--dry-run");
8852
9000
  if (format) argv.push("--format", format);
9001
+ for (const [name, value] of Object.entries(base.variables)) argv.push("--var", `${name}=${value}`);
8853
9002
  pushOpt(argv, "--config", base.configPath);
8854
9003
  pushOpt(argv, "--profile", base.profile);
8855
9004
  pushOpt(argv, "--base-url", base.baseUrl);
@@ -9473,6 +9622,10 @@ async function run() {
9473
9622
  attachmentFormat: args.attachmentFormat ?? profile.output?.attachmentFormat ?? "full"
9474
9623
  };
9475
9624
  const appIds = sql ? extractAppIds(sql) : [];
9625
+ if (!isBatchSql && Object.keys(args.variables).length > 0) {
9626
+ process.stderr.write("ArgumentError: --var requires a batch containing DECLARE.\n");
9627
+ return 2;
9628
+ }
9476
9629
  const defaultApp = args.app ?? envInt2("KSQL_APP") ?? profile.app ?? null;
9477
9630
  if (appIds.length === 0 && defaultApp !== null) appIds.push(defaultApp);
9478
9631
  const allowNoFromSelect = isNoFromSelectStatement(parsedStmt) || stmtType === "SHOW_APPS";
@@ -9488,7 +9641,7 @@ async function run() {
9488
9641
  if (args.dryRun) {
9489
9642
  let plans;
9490
9643
  try {
9491
- plans = buildBatchExplainPlans(sql);
9644
+ plans = buildBatchExplainPlans(sql, args.variables);
9492
9645
  } catch (err) {
9493
9646
  const restored = sourceSql && sqlDiagnosticContext ? restoreSqlContextError(err, sourceSql, {
9494
9647
  bindings: sqlDiagnosticContext.appBindingByMappedApp,
@@ -9805,6 +9958,7 @@ query=${label}`);
9805
9958
  continueOnError: args.continueOnError,
9806
9959
  tempTableMaxRows,
9807
9960
  timeoutMs: timeout,
9961
+ variables: args.variables,
9808
9962
  confirm: batchContainsDml ? async (count, operation) => {
9809
9963
  if (count > dmlMaxRows) {
9810
9964
  throw new Error(`ArgumentError: ${operation} affected rows (${count}) exceed --dml-max-rows (${dmlMaxRows}).`);
@@ -31512,13 +31512,14 @@ var Parser = class {
31512
31512
  const upper = tok.value.toUpperCase();
31513
31513
  if (upper === "CREATE") return this.parseCreateTempTable();
31514
31514
  if (upper === "DROP") return this.parseDropTempTable();
31515
+ if (upper === "DECLARE") return this.parseDeclareVariable();
31515
31516
  break;
31516
31517
  }
31517
31518
  default:
31518
31519
  break;
31519
31520
  }
31520
31521
  throw new ParseError(
31521
- "SELECT / INSERT / UPDATE / DELETE / REORDER / WITH / SHOW / DESCRIBE / EXPLAIN / CREATE TEMP TABLE / DROP TEMP TABLE / SET / ASSERT \u306E\u3044\u305A\u308C\u304B\u3067\u59CB\u307E\u308B SQL \u6587\u304C\u5FC5\u8981\u3067\u3059",
31522
+ "SELECT / INSERT / UPDATE / DELETE / REORDER / WITH / SHOW / DESCRIBE / EXPLAIN / CREATE TEMP TABLE / DROP TEMP TABLE / SET / DECLARE / ASSERT \u306E\u3044\u305A\u308C\u304B\u3067\u59CB\u307E\u308B SQL \u6587\u304C\u5FC5\u8981\u3067\u3059",
31522
31523
  tok
31523
31524
  );
31524
31525
  }
@@ -31529,20 +31530,48 @@ var Parser = class {
31529
31530
  this.expect("SET" /* SET */);
31530
31531
  const variable = this.expect("VARIABLE" /* VARIABLE */, "SET \u306E\u5F8C\u306B\u306F\u5909\u6570\u540D\uFF08\u4F8B: @name\uFF09\u304C\u5FC5\u8981\u3067\u3059");
31531
31532
  this.expect("=" /* EQ */);
31532
- const expr = this.parseScalarExpr();
31533
+ const expr = this.parseScalarExpr("SET", true);
31533
31534
  return { type: "SET_VARIABLE", name: variable.value.slice(1).toLowerCase(), expr };
31534
31535
  }
31535
- /** SET RHS 専用。既存式パーサーで構文を読み、フィールド参照を明示的に拒否する。 */
31536
- parseScalarExpr() {
31536
+ parseDeclareVariable() {
31537
+ this.advance();
31538
+ const variable = this.expect("VARIABLE" /* VARIABLE */, "DECLARE \u306E\u5F8C\u306B\u306F\u5909\u6570\u540D\uFF08\u4F8B: @name\uFF09\u304C\u5FC5\u8981\u3067\u3059");
31539
+ this.expect("=" /* EQ */);
31540
+ const expr = this.parseScalarExpr("DECLARE", false);
31541
+ if (expr.type === "SCALAR_SUBQUERY") {
31542
+ throw new ParseError("DECLARE \u306E\u65E2\u5B9A\u5024\u306B\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", this.peek());
31543
+ }
31544
+ return { type: "DECLARE_VARIABLE", name: variable.value.slice(1).toLowerCase(), default: expr };
31545
+ }
31546
+ /** SET / DECLARE RHS 専用。既存式パーサーで構文を読み、フィールド参照を明示的に拒否する。 */
31547
+ parseScalarExpr(context, allowScalarSubquery) {
31537
31548
  const tok = this.peek();
31538
31549
  if (tok.kind === "VARIABLE" /* VARIABLE */) {
31539
- throw new ParseError("SET \u306E\u53F3\u8FBA\u3067\u306F\u4ED6\u306E\u5909\u6570\u3092\u53C2\u7167\u3067\u304D\u307E\u305B\u3093\uFF08Phase 1a\uFF09", tok);
31550
+ throw new ParseError(`${context} \u306E\u53F3\u8FBA\u3067\u306F\u4ED6\u306E\u5909\u6570\u3092\u53C2\u7167\u3067\u304D\u307E\u305B\u3093`, tok);
31540
31551
  }
31541
31552
  if (tok.kind === "NULL" /* NULL */) {
31542
- throw new ParseError("SET \u306E\u53F3\u8FBA\u3067 NULL \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF08Phase 1a\uFF09", tok);
31553
+ throw new ParseError(`${context} \u306E\u53F3\u8FBA\u3067 NULL \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093`, tok);
31543
31554
  }
31544
31555
  if (tok.kind === "(" /* LPAREN */ && this.peekAt(1).kind === "SELECT" /* SELECT */) {
31545
- throw new ParseError("SET \u306E\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u4EE3\u5165\u306F Phase 1b \u3067\u5BFE\u5FDC\u4E88\u5B9A\u3067\u3059", tok);
31556
+ if (!allowScalarSubquery) {
31557
+ throw new ParseError("DECLARE \u306E\u65E2\u5B9A\u5024\u306B\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
31558
+ }
31559
+ this.advance();
31560
+ const query = this.parseSelect();
31561
+ this.expect(")" /* RPAREN */);
31562
+ if (this.isArithOp(this.peek().kind)) {
31563
+ throw new ParseError(
31564
+ "\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",
31565
+ this.peek()
31566
+ );
31567
+ }
31568
+ const hasWildcard = query.columns.some(
31569
+ (c) => c.type === "WILDCARD" || c.type === "PARENT_WILDCARD"
31570
+ );
31571
+ if (!hasWildcard && query.columns.length !== 1) {
31572
+ throw new ParseError("scalar subquery in SET must return exactly 1 column.", tok);
31573
+ }
31574
+ return { type: "SCALAR_SUBQUERY", query };
31546
31575
  }
31547
31576
  if (tok.kind === "STRING" /* STRING */) {
31548
31577
  this.advance();
@@ -31550,7 +31579,7 @@ var Parser = class {
31550
31579
  }
31551
31580
  if (tok.kind === "LOGINUSER" /* LOGINUSER */) {
31552
31581
  throw new ParseError(
31553
- "SET \u306E\u53F3\u8FBA\u3067 LOGINUSER() \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF08\u5B9F\u884C\u74B0\u5883\u5171\u901A\u306E\u30ED\u30B0\u30A4\u30F3\u30E6\u30FC\u30B6\u30FC\u89E3\u6C7A\u306F\u672A\u5BFE\u5FDC\u3067\u3059\uFF09",
31582
+ `${context} \u306E\u53F3\u8FBA\u3067 LOGINUSER() \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF08\u5B9F\u884C\u74B0\u5883\u5171\u901A\u306E\u30ED\u30B0\u30A4\u30F3\u30E6\u30FC\u30B6\u30FC\u89E3\u6C7A\u306F\u672A\u5BFE\u5FDC\u3067\u3059\uFF09`,
31554
31583
  tok
31555
31584
  );
31556
31585
  }
@@ -31558,22 +31587,22 @@ var Parser = class {
31558
31587
  return this.parseSqlValue();
31559
31588
  }
31560
31589
  const expr = this.parseArithAddSub();
31561
- this.rejectNonScalarExpr(expr, tok);
31590
+ this.rejectNonScalarExpr(expr, tok, context);
31562
31591
  if (expr.type === "NUMBER" || expr.type === "STRING_FUNC" || expr.type === "ARITH") return expr;
31563
- throw new ParseError("SET \u306E\u53F3\u8FBA\u306B\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u3092\u542B\u307E\u306A\u3044\u30B9\u30AB\u30E9\u30FC\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044", tok);
31592
+ throw new ParseError(`${context} \u306E\u53F3\u8FBA\u306B\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u3092\u542B\u307E\u306A\u3044\u30B9\u30AB\u30E9\u30FC\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044`, tok);
31564
31593
  }
31565
- rejectNonScalarExpr(node, tok) {
31594
+ rejectNonScalarExpr(node, tok, context) {
31566
31595
  if (node.type === "STRING" || node.type === "NUMBER") return;
31567
31596
  if (node.type === "FIELD_REF" || node.type === "AGG_REF") {
31568
- throw new ParseError("SET \u306E\u53F3\u8FBA\u3067\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u30FB\u96C6\u8A08\u95A2\u6570\u3092\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
31597
+ throw new ParseError(`${context} \u306E\u53F3\u8FBA\u3067\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u30FB\u96C6\u8A08\u95A2\u6570\u3092\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093`, tok);
31569
31598
  }
31570
31599
  if (node.type === "ARITH" || node.type === "AGG_ARITH") {
31571
- this.rejectNonScalarExpr(node.left, tok);
31572
- this.rejectNonScalarExpr(node.right, tok);
31600
+ this.rejectNonScalarExpr(node.left, tok, context);
31601
+ this.rejectNonScalarExpr(node.right, tok, context);
31573
31602
  return;
31574
31603
  }
31575
31604
  if (node.type === "STRING_FUNC") {
31576
- for (const arg of node.args) this.rejectNonScalarExpr(arg, tok);
31605
+ for (const arg of node.args) this.rejectNonScalarExpr(arg, tok, context);
31577
31606
  }
31578
31607
  }
31579
31608
  // ----------------------------------------------------------
@@ -33199,7 +33228,7 @@ function isDmlType(type) {
33199
33228
  return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
33200
33229
  }
33201
33230
  function isReadOnlyType(type) {
33202
- return type === "SELECT" || type === "UNION" || type === "WITH" || type === "EXPLAIN" || type === "SHOW_APPS" || type === "DESCRIBE" || type === "CREATE_TEMP_TABLE" || type === "DROP_TEMP_TABLE" || type === "SET_VARIABLE" || type === "ASSERT";
33231
+ return type === "SELECT" || type === "UNION" || type === "WITH" || type === "EXPLAIN" || type === "SHOW_APPS" || type === "DESCRIBE" || type === "CREATE_TEMP_TABLE" || type === "DROP_TEMP_TABLE" || type === "SET_VARIABLE" || type === "DECLARE_VARIABLE" || type === "ASSERT";
33203
33232
  }
33204
33233
  function hasWhereClause(stmt) {
33205
33234
  if (!stmt || typeof stmt !== "object") return false;
@@ -33261,8 +33290,9 @@ function analyzeBatch(statements) {
33261
33290
  }
33262
33291
  if (statements.length === 1) {
33263
33292
  const t = statements[0].type;
33264
- if (t === "SET_VARIABLE") {
33265
- throw new BatchAnalysisError("ArgumentError: SET variable requires a batch.", 0);
33293
+ if (t === "SET_VARIABLE" || t === "DECLARE_VARIABLE") {
33294
+ const verb = t === "SET_VARIABLE" ? "SET" : "DECLARE";
33295
+ throw new BatchAnalysisError(`ArgumentError: ${verb} variable requires a batch.`, 0);
33266
33296
  }
33267
33297
  if (t === "CREATE_TEMP_TABLE" || t === "DROP_TEMP_TABLE") {
33268
33298
  const verb = t === "CREATE_TEMP_TABLE" ? "CREATE TEMP TABLE" : "DROP TEMP TABLE";
@@ -33296,7 +33326,7 @@ function analyzeBatch(statements) {
33296
33326
  }
33297
33327
  def.referencedBy.push(index);
33298
33328
  }
33299
- if (stmt.type === "SET_VARIABLE") {
33329
+ if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
33300
33330
  if (variableDefs.has(stmt.name)) {
33301
33331
  throw new BatchAnalysisError(`ParseError: variable @${stmt.name} is already defined.`, index);
33302
33332
  }
@@ -33393,6 +33423,40 @@ function analyzeBatch(statements) {
33393
33423
  };
33394
33424
  }
33395
33425
 
33426
+ // src/core/batchVariables.ts
33427
+ var VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
33428
+ function normalizeBatchVariableName(name) {
33429
+ if (!VARIABLE_NAME_RE.test(name)) {
33430
+ throw new Error(
33431
+ `ArgumentError: invalid variable name "${name}". Use a name without @ matching [A-Za-z_][A-Za-z0-9_]{0,63}.`
33432
+ );
33433
+ }
33434
+ return name.toLowerCase();
33435
+ }
33436
+ function normalizeBatchVariables(input) {
33437
+ const normalized = /* @__PURE__ */ Object.create(null);
33438
+ for (const [rawName, value] of Object.entries(input ?? {})) {
33439
+ const name = normalizeBatchVariableName(rawName);
33440
+ if (Object.prototype.hasOwnProperty.call(normalized, name)) {
33441
+ throw new Error(`ArgumentError: variable "${rawName}" is specified more than once.`);
33442
+ }
33443
+ normalized[name] = value;
33444
+ }
33445
+ return normalized;
33446
+ }
33447
+ function validateDeclaredBatchVariables(statements, input) {
33448
+ const normalized = normalizeBatchVariables(input);
33449
+ const declared = new Set(
33450
+ statements.filter((stmt) => stmt.type === "DECLARE_VARIABLE").map((stmt) => stmt.name)
33451
+ );
33452
+ for (const name of Object.keys(normalized)) {
33453
+ if (!declared.has(name)) {
33454
+ throw new Error(`ArgumentError: injected variable @${name} is not declared.`);
33455
+ }
33456
+ }
33457
+ return normalized;
33458
+ }
33459
+
33396
33460
  // src/core/scalarCompare.ts
33397
33461
  function compareScalarValues(op, leftStr, rightStr) {
33398
33462
  if (op === "=") return leftStr === rightStr;
@@ -35724,6 +35788,8 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
35724
35788
  throw new Error("ArgumentError: DROP TEMP TABLE requires a batch (temp tables are batch-scoped).");
35725
35789
  case "SET_VARIABLE":
35726
35790
  throw new Error("ArgumentError: SET variable requires a batch.");
35791
+ case "DECLARE_VARIABLE":
35792
+ throw new Error("ArgumentError: DECLARE variable requires a batch.");
35727
35793
  case "ASSERT":
35728
35794
  return executeAssert(stmt, client, options, cacheContext);
35729
35795
  }
@@ -35737,6 +35803,8 @@ var BatchTimeoutError = class extends Error {
35737
35803
  async function executeBatch(sql, client, options = {}) {
35738
35804
  const statements = parseSqlBatch(sql);
35739
35805
  const analysis = analyzeBatch(statements);
35806
+ const injectedVariables = validateDeclaredBatchVariables(statements, options.variables);
35807
+ const batchOptions = { ...options, variables: injectedVariables };
35740
35808
  if (options.continueOnError && analysis.containsDml) {
35741
35809
  throw new Error("ArgumentError: continueOnError is not allowed for batches containing DML.");
35742
35810
  }
@@ -35781,16 +35849,16 @@ async function executeBatch(sql, client, options = {}) {
35781
35849
  }
35782
35850
  try {
35783
35851
  const remaining = deadline !== null ? deadline - Date.now() : null;
35784
- const userConfirm = options.confirm;
35852
+ const userConfirm = batchOptions.confirm;
35785
35853
  const stmtOptions = userConfirm ? {
35786
- ...options,
35854
+ ...batchOptions,
35787
35855
  confirm: (count, operation) => userConfirm(count, operation, {
35788
35856
  statementIndex: i,
35789
35857
  statementCount: statements.length,
35790
35858
  statementType: info.statementType,
35791
35859
  targetAppId: info.targetAppId
35792
35860
  })
35793
- } : options;
35861
+ } : batchOptions;
35794
35862
  const outcome = await runWithDeadline(
35795
35863
  executeBatchStatement(statements[i], info, countedClient, stmtOptions, cacheContext, tempTables, variables),
35796
35864
  remaining
@@ -35803,7 +35871,7 @@ async function executeBatch(sql, client, options = {}) {
35803
35871
  aborted2 = "timeout";
35804
35872
  } else if (e instanceof AssertError) {
35805
35873
  aborted2 = "assertion";
35806
- } else if (info.statementType === "SET_VARIABLE") {
35874
+ } else if (info.statementType === "SET_VARIABLE" || info.statementType === "DECLARE_VARIABLE") {
35807
35875
  aborted2 = "fail-fast";
35808
35876
  } else if (!options.continueOnError) {
35809
35877
  aborted2 = "fail-fast";
@@ -35821,7 +35889,36 @@ async function executeBatch(sql, client, options = {}) {
35821
35889
  }
35822
35890
  async function executeBatchStatement(stmt, info, client, options, cacheContext, tempTables, variables) {
35823
35891
  if (stmt.type === "SET_VARIABLE") {
35824
- variables.set(stmt.name, evaluateScalarExpr(stmt.expr));
35892
+ const resolvedStmt2 = resolveVariableRefs(stmt, variables);
35893
+ if (resolvedStmt2.expr.type === "SCALAR_SUBQUERY") {
35894
+ try {
35895
+ const value = await evaluateScalarSubquery(
35896
+ resolvedStmt2.expr.query,
35897
+ client,
35898
+ options,
35899
+ cacheContext,
35900
+ tempTables
35901
+ );
35902
+ variables.set(stmt.name, { type: "string", value });
35903
+ } catch (e) {
35904
+ if (e instanceof ScalarSubqueryError) {
35905
+ throw new Error(`ArgumentError: ${e.message}`);
35906
+ }
35907
+ throw e;
35908
+ }
35909
+ } else {
35910
+ variables.set(stmt.name, evaluateScalarExpr(resolvedStmt2.expr));
35911
+ }
35912
+ return {};
35913
+ }
35914
+ if (stmt.type === "DECLARE_VARIABLE") {
35915
+ const injected = options.variables ?? {};
35916
+ if (Object.prototype.hasOwnProperty.call(injected, stmt.name)) {
35917
+ variables.set(stmt.name, { type: "string", value: injected[stmt.name] });
35918
+ } else {
35919
+ const value = evaluateScalarExpr(stmt.default);
35920
+ variables.set(stmt.name, { type: "string", value: String(value.value) });
35921
+ }
35825
35922
  return {};
35826
35923
  }
35827
35924
  const resolvedStmt = resolveVariableRefs(stmt, variables);
@@ -35984,6 +36081,12 @@ var AssertError = class extends Error {
35984
36081
  this.name = "AssertError";
35985
36082
  }
35986
36083
  };
36084
+ var ScalarSubqueryError = class extends Error {
36085
+ constructor(message) {
36086
+ super(message);
36087
+ this.name = "ScalarSubqueryError";
36088
+ }
36089
+ };
35987
36090
  async function executeAssert(stmt, client, options, cacheContext, tempTables) {
35988
36091
  const left = await evalAssertOperand(stmt.left, client, options, cacheContext, tempTables);
35989
36092
  if (stmt.op === "BETWEEN") {
@@ -36017,25 +36120,38 @@ async function evalAssertOperand(operand, client, options, cacheContext, tempTab
36017
36120
  case "ARITH":
36018
36121
  return String(evalAssertArith(operand));
36019
36122
  case "SCALAR_SUBQUERY": {
36020
- const { query, probed } = withScalarProbeLimit(operand.query);
36021
- const result = await runSubquery(query, client, options, cacheContext, tempTables);
36022
- if (result.columns.length > 1) {
36023
- throw new AssertError(
36024
- `scalar subquery returned ${result.columns.length} columns (expected 1 column).`
36123
+ try {
36124
+ return await evaluateScalarSubquery(
36125
+ operand.query,
36126
+ client,
36127
+ options,
36128
+ cacheContext,
36129
+ tempTables
36025
36130
  );
36131
+ } catch (e) {
36132
+ if (e instanceof ScalarSubqueryError) throw new AssertError(e.message);
36133
+ throw e;
36026
36134
  }
36027
- if (result.rowCount === 0) {
36028
- throw new AssertError("scalar subquery returned no rows (expected 1 row).");
36029
- }
36030
- if (result.rowCount > 1) {
36031
- const rows = probed && result.rowCount === 2 ? "2 or more rows" : `${result.rowCount} rows`;
36032
- throw new AssertError(`scalar subquery returned ${rows} (expected 1 row).`);
36033
- }
36034
- const col = result.columns[0] ?? "";
36035
- return result.rows[0]?.[col] ?? "";
36036
36135
  }
36037
36136
  }
36038
36137
  }
36138
+ async function evaluateScalarSubquery(sourceQuery, client, options, cacheContext, tempTables) {
36139
+ const { query, probed } = withScalarProbeLimit(sourceQuery);
36140
+ const result = await runSubquery(query, client, options, cacheContext, tempTables);
36141
+ if (result.columns.length !== 1) {
36142
+ throw new ScalarSubqueryError(
36143
+ `scalar subquery returned ${result.columns.length} columns (expected 1 column).`
36144
+ );
36145
+ }
36146
+ if (result.rowCount === 0) {
36147
+ throw new ScalarSubqueryError("scalar subquery returned no rows (expected 1 row).");
36148
+ }
36149
+ if (result.rowCount > 1) {
36150
+ const rows = probed && result.rowCount === 2 ? "2 or more rows" : `${result.rowCount} rows`;
36151
+ throw new ScalarSubqueryError(`scalar subquery returned ${rows} (expected 1 row).`);
36152
+ }
36153
+ return result.rows[0]?.[result.columns[0]] ?? "";
36154
+ }
36039
36155
  function withScalarProbeLimit(query) {
36040
36156
  const hasAgg = query.groupBy.length > 0 || query.columns.some((c) => c.type === "AGGREGATE" || c.type === "ARITH_AGG_COL");
36041
36157
  if (hasAgg || query.distinct || query.limit !== null) return { query, probed: false };
@@ -37530,20 +37646,21 @@ async function resolveScalarColumns(columns, client, options, cacheContext, cteC
37530
37646
  pending.forEach(([i], idx) => cache.set(i, values[idx]));
37531
37647
  return cache;
37532
37648
  }
37533
- function buildBatchExplainPlans(sql) {
37649
+ function buildBatchExplainPlans(sql, injectedVariables) {
37534
37650
  const statements = parseSqlBatch(sql);
37535
37651
  const analysis = analyzeBatch(statements);
37652
+ validateDeclaredBatchVariables(statements, injectedVariables);
37536
37653
  const variables = /* @__PURE__ */ new Map();
37537
37654
  return {
37538
37655
  statementCount: statements.length,
37539
37656
  statements: statements.map((stmt, i) => {
37540
- const planStmt = stmt.type === "SET_VARIABLE" ? stmt : resolveVariableRefs(stmt, variables);
37657
+ const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr: resolveVariableRefs(stmt.expr, variables) } : stmt : resolveVariableRefs(stmt, variables);
37541
37658
  const result = {
37542
37659
  index: i,
37543
37660
  type: analysis.statements[i].statementType,
37544
37661
  plan: buildBatchStatementPlan(planStmt, analysis.statements[i])
37545
37662
  };
37546
- if (stmt.type === "SET_VARIABLE") {
37663
+ if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
37547
37664
  variables.set(stmt.name, { type: "string", value: `@${stmt.name}` });
37548
37665
  }
37549
37666
  return result;
@@ -37566,11 +37683,26 @@ function buildBatchStatementPlan(stmt, info) {
37566
37683
  ];
37567
37684
  }
37568
37685
  if (stmt.type === "SET_VARIABLE") {
37686
+ if (stmt.expr.type === "SCALAR_SUBQUERY") {
37687
+ const subInfo = hasTempTableRef(stmt.expr.query) ? info : { ...info, tempTablesReferenced: [] };
37688
+ return [
37689
+ `SET @${stmt.name} = (SELECT ...)`,
37690
+ " value: \u30B5\u30D6\u30AF\u30A8\u30EA\u3092\u5B9F\u884C\u6642\u306B1\u56DE\u8A55\u4FA1\uFF081\u884C1\u5217\u30FB\u30D0\u30C3\u30C1\u5185\u5B9A\u6570\u30FB\u7D50\u679C\u30E1\u30BF\u30C7\u30FC\u30BF\u306B\u306F\u975E\u516C\u958B\uFF09",
37691
+ " subquery:",
37692
+ ...buildPlanForBatchQuery(stmt.expr.query, subInfo).map((l) => ` ${l}`)
37693
+ ];
37694
+ }
37569
37695
  return [
37570
37696
  `SET @${stmt.name} = <scalar expression>`,
37571
37697
  " value: \u5B9F\u884C\u6642\u306B1\u56DE\u8A55\u4FA1\uFF08\u30D0\u30C3\u30C1\u5185\u5B9A\u6570\u30FB\u7D50\u679C\u30E1\u30BF\u30C7\u30FC\u30BF\u306B\u306F\u975E\u516C\u958B\uFF09"
37572
37698
  ];
37573
37699
  }
37700
+ if (stmt.type === "DECLARE_VARIABLE") {
37701
+ return [
37702
+ `DECLARE @${stmt.name} = <default scalar expression>`,
37703
+ " value: \u5916\u90E8\u6CE8\u5165\u304C\u3042\u308C\u3070\u63A1\u7528\u3001\u306A\u3051\u308C\u3070\u65E2\u5B9A\u5024\u3092\u5B9F\u884C\u6642\u306B1\u56DE\u8A55\u4FA1\uFF08\u5024\u306F\u975E\u516C\u958B\uFF09"
37704
+ ];
37705
+ }
37574
37706
  if (stmt.type === "SHOW_APPS") return ["SHOW APPS\uFF08\u30A2\u30D7\u30EA\u4E00\u89A7\u306E\u53D6\u5F97\uFF09"];
37575
37707
  if (stmt.type === "DESCRIBE") return [`DESCRIBE APP${stmt.appId}\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u5B9A\u7FA9\u306E\u53D6\u5F97\uFF09`];
37576
37708
  if (stmt.type === "EXPLAIN") return buildPlanForBatchQuery(stmt.query, info);
@@ -39549,6 +39681,9 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
39549
39681
  }
39550
39682
  async function query(input, validated) {
39551
39683
  const validation = validated ?? await validate(input);
39684
+ if (!validation.batch && input.variables && Object.keys(input.variables).length > 0) {
39685
+ throw new Error("ArgumentError: variables require a batch containing DECLARE.");
39686
+ }
39552
39687
  if (validation.batch) {
39553
39688
  if (validation.containsDml) {
39554
39689
  throw new Error("ArgumentError: batch contains DML statements. Use ksql_mutate.");
@@ -39575,7 +39710,8 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
39575
39710
  // バッチでは timeout を合計タイムアウトとして扱う(仕様 §5.7)。
39576
39711
  // runtime.timeout は env / profile / 既定 30000ms を解決済みの値で、
39577
39712
  // HTTP クライアント側の per-request タイムアウトと同値になる
39578
- timeoutMs: runtime2.timeout
39713
+ timeoutMs: runtime2.timeout,
39714
+ variables: input.variables
39579
39715
  });
39580
39716
  return { ...buildBatchEnvelope(batchResult, { maxTotalRecords: input.maxTotalRecords }) };
39581
39717
  }
@@ -39664,6 +39800,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
39664
39800
  tempTableMaxRows: runtime.tempTableMaxRows,
39665
39801
  // 合計タイムアウト(解決済みの runtime.timeout。per-request と同値)
39666
39802
  timeoutMs: runtime.timeout,
39803
+ variables: input.variables,
39667
39804
  confirm: async (count, operation) => {
39668
39805
  if (count > dmlMaxRows) {
39669
39806
  throw new Error(`ArgumentError: ${operation} affected rows (${count}) exceed dmlMaxRows (${dmlMaxRows}).`);
@@ -39692,6 +39829,9 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
39692
39829
  async function mutate(input, validated) {
39693
39830
  const dmlMaxRows = requireDmlApproval(input, "ksql_mutate");
39694
39831
  const validation = validated ?? await validate(input);
39832
+ if (!validation.batch && input.variables && Object.keys(input.variables).length > 0) {
39833
+ throw new Error("ArgumentError: variables require a batch containing DECLARE.");
39834
+ }
39695
39835
  if (validation.batch) {
39696
39836
  return mutateBatch(input, validation, dmlMaxRows);
39697
39837
  }
@@ -39914,7 +40054,8 @@ var queryInputSchema = external_exports.object({
39914
40054
  tempTableMaxRows,
39915
40055
  timeout,
39916
40056
  continueOnError: external_exports.boolean().describe("Batch (multi-statement) only: keep executing subsequent statements after a runtime error (default false = fail-fast).").optional(),
39917
- maxTotalRecords: external_exports.number().int().positive().describe("Batch (multi-statement) only: cap on total rows returned across all result sets (default: unlimited).").optional()
40057
+ maxTotalRecords: external_exports.number().int().positive().describe("Batch (multi-statement) only: cap on total rows returned across all result sets (default: unlimited).").optional(),
40058
+ variables: external_exports.record(external_exports.string(), external_exports.string()).describe("Batch only: string values for variables declared with DECLARE. Keys omit @ and are case-insensitive.").optional()
39918
40059
  });
39919
40060
  var mutateInputSchema = external_exports.object({
39920
40061
  sql: external_exports.string().min(1).describe("DML kSQL text. May contain multiple ;-separated statements (batch) with temp tables, e.g. CREATE TEMP TABLE #t AS SELECT ...; INSERT INTO APPx (...) SELECT ... FROM #t;"),
@@ -39925,7 +40066,8 @@ var mutateInputSchema = external_exports.object({
39925
40066
  fetchParallel,
39926
40067
  tempTableMaxRows,
39927
40068
  timeout,
39928
- dmlTotalMaxRows: external_exports.number().int().positive().describe("Batch (multi-statement) only: cap on total affected rows across the whole batch (default: per-statement dmlMaxRows only). DML batches always run fail-fast.").optional()
40069
+ dmlTotalMaxRows: external_exports.number().int().positive().describe("Batch (multi-statement) only: cap on total affected rows across the whole batch (default: per-statement dmlMaxRows only). DML batches always run fail-fast.").optional(),
40070
+ variables: external_exports.record(external_exports.string(), external_exports.string()).describe("Batch only: string values for variables declared with DECLARE. Keys omit @ and are case-insensitive.").optional()
39929
40071
  });
39930
40072
  var describeAppInputSchema = external_exports.object({
39931
40073
  app: external_exports.number().int().positive().describe("kintone app ID to describe."),
@@ -40014,7 +40156,7 @@ Options:
40014
40156
  -h, --help Show help
40015
40157
  `);
40016
40158
  }
40017
- var SERVER_VERSION = true ? "2.2.0" : "0.0.0-dev";
40159
+ var SERVER_VERSION = true ? "2.4.0" : "0.0.0-dev";
40018
40160
  function createServer(args) {
40019
40161
  const server = new McpServer({
40020
40162
  name: "ksql-mcp",
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rex0220/kintone-sql-tools",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "kintone SQL plugin, CLI, and MCP tools (ksql)",
5
5
  "publishConfig": {
6
6
  "access": "public"