@rex0220/kintone-sql-tools 2.3.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,19 +617,32 @@ 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 */) {
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
+ }
632
646
  this.advance();
633
647
  const query = this.parseSelect();
634
648
  this.expect(")" /* RPAREN */);
@@ -652,7 +666,7 @@ var Parser = class {
652
666
  }
653
667
  if (tok.kind === "LOGINUSER" /* LOGINUSER */) {
654
668
  throw new ParseError(
655
- "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`,
656
670
  tok
657
671
  );
658
672
  }
@@ -660,22 +674,22 @@ var Parser = class {
660
674
  return this.parseSqlValue();
661
675
  }
662
676
  const expr = this.parseArithAddSub();
663
- this.rejectNonScalarExpr(expr, tok);
677
+ this.rejectNonScalarExpr(expr, tok, context);
664
678
  if (expr.type === "NUMBER" || expr.type === "STRING_FUNC" || expr.type === "ARITH") return expr;
665
- 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);
666
680
  }
667
- rejectNonScalarExpr(node, tok) {
681
+ rejectNonScalarExpr(node, tok, context) {
668
682
  if (node.type === "STRING" || node.type === "NUMBER") return;
669
683
  if (node.type === "FIELD_REF" || node.type === "AGG_REF") {
670
- 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);
671
685
  }
672
686
  if (node.type === "ARITH" || node.type === "AGG_ARITH") {
673
- this.rejectNonScalarExpr(node.left, tok);
674
- this.rejectNonScalarExpr(node.right, tok);
687
+ this.rejectNonScalarExpr(node.left, tok, context);
688
+ this.rejectNonScalarExpr(node.right, tok, context);
675
689
  return;
676
690
  }
677
691
  if (node.type === "STRING_FUNC") {
678
- for (const arg of node.args) this.rejectNonScalarExpr(arg, tok);
692
+ for (const arg of node.args) this.rejectNonScalarExpr(arg, tok, context);
679
693
  }
680
694
  }
681
695
  // ----------------------------------------------------------
@@ -2301,7 +2315,7 @@ function isDmlType(type) {
2301
2315
  return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
2302
2316
  }
2303
2317
  function isReadOnlyType(type) {
2304
- 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";
2305
2319
  }
2306
2320
  function hasWhereClause(stmt) {
2307
2321
  if (!stmt || typeof stmt !== "object") return false;
@@ -2375,8 +2389,9 @@ function analyzeBatch(statements) {
2375
2389
  }
2376
2390
  if (statements.length === 1) {
2377
2391
  const t = statements[0].type;
2378
- if (t === "SET_VARIABLE") {
2379
- 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);
2380
2395
  }
2381
2396
  if (t === "CREATE_TEMP_TABLE" || t === "DROP_TEMP_TABLE") {
2382
2397
  const verb = t === "CREATE_TEMP_TABLE" ? "CREATE TEMP TABLE" : "DROP TEMP TABLE";
@@ -2410,7 +2425,7 @@ function analyzeBatch(statements) {
2410
2425
  }
2411
2426
  def.referencedBy.push(index);
2412
2427
  }
2413
- if (stmt.type === "SET_VARIABLE") {
2428
+ if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
2414
2429
  if (variableDefs.has(stmt.name)) {
2415
2430
  throw new BatchAnalysisError(`ParseError: variable @${stmt.name} is already defined.`, index);
2416
2431
  }
@@ -2507,6 +2522,40 @@ function analyzeBatch(statements) {
2507
2522
  };
2508
2523
  }
2509
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
+
2510
2559
  // src/core/scalarCompare.ts
2511
2560
  function compareScalarValues(op, leftStr, rightStr) {
2512
2561
  if (op === "=") return leftStr === rightStr;
@@ -4838,6 +4887,8 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
4838
4887
  throw new Error("ArgumentError: DROP TEMP TABLE requires a batch (temp tables are batch-scoped).");
4839
4888
  case "SET_VARIABLE":
4840
4889
  throw new Error("ArgumentError: SET variable requires a batch.");
4890
+ case "DECLARE_VARIABLE":
4891
+ throw new Error("ArgumentError: DECLARE variable requires a batch.");
4841
4892
  case "ASSERT":
4842
4893
  return executeAssert(stmt, client, options, cacheContext);
4843
4894
  }
@@ -4851,6 +4902,8 @@ var BatchTimeoutError = class extends Error {
4851
4902
  async function executeBatch(sql, client, options = {}) {
4852
4903
  const statements = parseSqlBatch(sql);
4853
4904
  const analysis = analyzeBatch(statements);
4905
+ const injectedVariables = validateDeclaredBatchVariables(statements, options.variables);
4906
+ const batchOptions = { ...options, variables: injectedVariables };
4854
4907
  if (options.continueOnError && analysis.containsDml) {
4855
4908
  throw new Error("ArgumentError: continueOnError is not allowed for batches containing DML.");
4856
4909
  }
@@ -4895,16 +4948,16 @@ async function executeBatch(sql, client, options = {}) {
4895
4948
  }
4896
4949
  try {
4897
4950
  const remaining = deadline !== null ? deadline - Date.now() : null;
4898
- const userConfirm = options.confirm;
4951
+ const userConfirm = batchOptions.confirm;
4899
4952
  const stmtOptions = userConfirm ? {
4900
- ...options,
4953
+ ...batchOptions,
4901
4954
  confirm: (count, operation) => userConfirm(count, operation, {
4902
4955
  statementIndex: i,
4903
4956
  statementCount: statements.length,
4904
4957
  statementType: info.statementType,
4905
4958
  targetAppId: info.targetAppId
4906
4959
  })
4907
- } : options;
4960
+ } : batchOptions;
4908
4961
  const outcome = await runWithDeadline(
4909
4962
  executeBatchStatement(statements[i], info, countedClient, stmtOptions, cacheContext, tempTables, variables),
4910
4963
  remaining
@@ -4917,7 +4970,7 @@ async function executeBatch(sql, client, options = {}) {
4917
4970
  aborted = "timeout";
4918
4971
  } else if (e instanceof AssertError) {
4919
4972
  aborted = "assertion";
4920
- } else if (info.statementType === "SET_VARIABLE") {
4973
+ } else if (info.statementType === "SET_VARIABLE" || info.statementType === "DECLARE_VARIABLE") {
4921
4974
  aborted = "fail-fast";
4922
4975
  } else if (!options.continueOnError) {
4923
4976
  aborted = "fail-fast";
@@ -4957,6 +5010,16 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
4957
5010
  }
4958
5011
  return {};
4959
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
+ }
5021
+ return {};
5022
+ }
4960
5023
  const resolvedStmt = resolveVariableRefs(stmt, variables);
4961
5024
  if (resolvedStmt.type === "CREATE_TEMP_TABLE") {
4962
5025
  const materializeOptions = {
@@ -6682,9 +6745,10 @@ async function resolveScalarColumns(columns, client, options, cacheContext, cteC
6682
6745
  pending.forEach(([i], idx) => cache.set(i, values[idx]));
6683
6746
  return cache;
6684
6747
  }
6685
- function buildBatchExplainPlans(sql) {
6748
+ function buildBatchExplainPlans(sql, injectedVariables) {
6686
6749
  const statements = parseSqlBatch(sql);
6687
6750
  const analysis = analyzeBatch(statements);
6751
+ validateDeclaredBatchVariables(statements, injectedVariables);
6688
6752
  const variables = /* @__PURE__ */ new Map();
6689
6753
  return {
6690
6754
  statementCount: statements.length,
@@ -6695,7 +6759,7 @@ function buildBatchExplainPlans(sql) {
6695
6759
  type: analysis.statements[i].statementType,
6696
6760
  plan: buildBatchStatementPlan(planStmt, analysis.statements[i])
6697
6761
  };
6698
- if (stmt.type === "SET_VARIABLE") {
6762
+ if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
6699
6763
  variables.set(stmt.name, { type: "string", value: `@${stmt.name}` });
6700
6764
  }
6701
6765
  return result;
@@ -6732,6 +6796,12 @@ function buildBatchStatementPlan(stmt, info) {
6732
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"
6733
6797
  ];
6734
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
+ }
6735
6805
  if (stmt.type === "SHOW_APPS") return ["SHOW APPS\uFF08\u30A2\u30D7\u30EA\u4E00\u89A7\u306E\u53D6\u5F97\uFF09"];
6736
6806
  if (stmt.type === "DESCRIBE") return [`DESCRIBE APP${stmt.appId}\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u5B9A\u7FA9\u306E\u53D6\u5F97\uFF09`];
6737
6807
  if (stmt.type === "EXPLAIN") return buildPlanForBatchQuery(stmt.query, info);
@@ -8193,6 +8263,7 @@ Options:
8193
8263
  -f, --file <path> Execute SQL file
8194
8264
  --console Start interactive console mode
8195
8265
  --dry-run Parse and show execution plan only
8266
+ --var <name=value> Override a DECLARE variable (repeatable; not for secrets)
8196
8267
  --format <type> Output format: table | json | jsonl | csv | markdown | md
8197
8268
  (batch + json: prints one JSON envelope for the whole batch)
8198
8269
  --max-records <n> Max records to fetch (default: 500)
@@ -8262,6 +8333,8 @@ function parseArgs(argv) {
8262
8333
  password: null,
8263
8334
  token: null,
8264
8335
  tokenMap: {},
8336
+ // `__proto__` も有効な変数名なので prototype のない辞書で保持する。
8337
+ variables: /* @__PURE__ */ Object.create(null),
8265
8338
  tokenFile: null,
8266
8339
  app: null,
8267
8340
  diagRecordId: null,
@@ -8356,6 +8429,19 @@ function parseArgs(argv) {
8356
8429
  continue;
8357
8430
  }
8358
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
+ }
8359
8445
  if (a === "-e" || a === "--execute") {
8360
8446
  out.executeSql = v ?? "";
8361
8447
  i++;
@@ -8912,6 +8998,7 @@ function buildReplExecArgv(base, sql, dryRun, format) {
8912
8998
  const argv = ["-e", sql];
8913
8999
  if (dryRun) argv.push("--dry-run");
8914
9000
  if (format) argv.push("--format", format);
9001
+ for (const [name, value] of Object.entries(base.variables)) argv.push("--var", `${name}=${value}`);
8915
9002
  pushOpt(argv, "--config", base.configPath);
8916
9003
  pushOpt(argv, "--profile", base.profile);
8917
9004
  pushOpt(argv, "--base-url", base.baseUrl);
@@ -9535,6 +9622,10 @@ async function run() {
9535
9622
  attachmentFormat: args.attachmentFormat ?? profile.output?.attachmentFormat ?? "full"
9536
9623
  };
9537
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
+ }
9538
9629
  const defaultApp = args.app ?? envInt2("KSQL_APP") ?? profile.app ?? null;
9539
9630
  if (appIds.length === 0 && defaultApp !== null) appIds.push(defaultApp);
9540
9631
  const allowNoFromSelect = isNoFromSelectStatement(parsedStmt) || stmtType === "SHOW_APPS";
@@ -9550,7 +9641,7 @@ async function run() {
9550
9641
  if (args.dryRun) {
9551
9642
  let plans;
9552
9643
  try {
9553
- plans = buildBatchExplainPlans(sql);
9644
+ plans = buildBatchExplainPlans(sql, args.variables);
9554
9645
  } catch (err) {
9555
9646
  const restored = sourceSql && sqlDiagnosticContext ? restoreSqlContextError(err, sourceSql, {
9556
9647
  bindings: sqlDiagnosticContext.appBindingByMappedApp,
@@ -9867,6 +9958,7 @@ query=${label}`);
9867
9958
  continueOnError: args.continueOnError,
9868
9959
  tempTableMaxRows,
9869
9960
  timeoutMs: timeout,
9961
+ variables: args.variables,
9870
9962
  confirm: batchContainsDml ? async (count, operation) => {
9871
9963
  if (count > dmlMaxRows) {
9872
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,19 +31530,32 @@ 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 */) {
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
+ }
31545
31559
  this.advance();
31546
31560
  const query = this.parseSelect();
31547
31561
  this.expect(")" /* RPAREN */);
@@ -31565,7 +31579,7 @@ var Parser = class {
31565
31579
  }
31566
31580
  if (tok.kind === "LOGINUSER" /* LOGINUSER */) {
31567
31581
  throw new ParseError(
31568
- "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`,
31569
31583
  tok
31570
31584
  );
31571
31585
  }
@@ -31573,22 +31587,22 @@ var Parser = class {
31573
31587
  return this.parseSqlValue();
31574
31588
  }
31575
31589
  const expr = this.parseArithAddSub();
31576
- this.rejectNonScalarExpr(expr, tok);
31590
+ this.rejectNonScalarExpr(expr, tok, context);
31577
31591
  if (expr.type === "NUMBER" || expr.type === "STRING_FUNC" || expr.type === "ARITH") return expr;
31578
- 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);
31579
31593
  }
31580
- rejectNonScalarExpr(node, tok) {
31594
+ rejectNonScalarExpr(node, tok, context) {
31581
31595
  if (node.type === "STRING" || node.type === "NUMBER") return;
31582
31596
  if (node.type === "FIELD_REF" || node.type === "AGG_REF") {
31583
- 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);
31584
31598
  }
31585
31599
  if (node.type === "ARITH" || node.type === "AGG_ARITH") {
31586
- this.rejectNonScalarExpr(node.left, tok);
31587
- this.rejectNonScalarExpr(node.right, tok);
31600
+ this.rejectNonScalarExpr(node.left, tok, context);
31601
+ this.rejectNonScalarExpr(node.right, tok, context);
31588
31602
  return;
31589
31603
  }
31590
31604
  if (node.type === "STRING_FUNC") {
31591
- for (const arg of node.args) this.rejectNonScalarExpr(arg, tok);
31605
+ for (const arg of node.args) this.rejectNonScalarExpr(arg, tok, context);
31592
31606
  }
31593
31607
  }
31594
31608
  // ----------------------------------------------------------
@@ -33214,7 +33228,7 @@ function isDmlType(type) {
33214
33228
  return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
33215
33229
  }
33216
33230
  function isReadOnlyType(type) {
33217
- 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";
33218
33232
  }
33219
33233
  function hasWhereClause(stmt) {
33220
33234
  if (!stmt || typeof stmt !== "object") return false;
@@ -33276,8 +33290,9 @@ function analyzeBatch(statements) {
33276
33290
  }
33277
33291
  if (statements.length === 1) {
33278
33292
  const t = statements[0].type;
33279
- if (t === "SET_VARIABLE") {
33280
- 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);
33281
33296
  }
33282
33297
  if (t === "CREATE_TEMP_TABLE" || t === "DROP_TEMP_TABLE") {
33283
33298
  const verb = t === "CREATE_TEMP_TABLE" ? "CREATE TEMP TABLE" : "DROP TEMP TABLE";
@@ -33311,7 +33326,7 @@ function analyzeBatch(statements) {
33311
33326
  }
33312
33327
  def.referencedBy.push(index);
33313
33328
  }
33314
- if (stmt.type === "SET_VARIABLE") {
33329
+ if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
33315
33330
  if (variableDefs.has(stmt.name)) {
33316
33331
  throw new BatchAnalysisError(`ParseError: variable @${stmt.name} is already defined.`, index);
33317
33332
  }
@@ -33408,6 +33423,40 @@ function analyzeBatch(statements) {
33408
33423
  };
33409
33424
  }
33410
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
+
33411
33460
  // src/core/scalarCompare.ts
33412
33461
  function compareScalarValues(op, leftStr, rightStr) {
33413
33462
  if (op === "=") return leftStr === rightStr;
@@ -35739,6 +35788,8 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
35739
35788
  throw new Error("ArgumentError: DROP TEMP TABLE requires a batch (temp tables are batch-scoped).");
35740
35789
  case "SET_VARIABLE":
35741
35790
  throw new Error("ArgumentError: SET variable requires a batch.");
35791
+ case "DECLARE_VARIABLE":
35792
+ throw new Error("ArgumentError: DECLARE variable requires a batch.");
35742
35793
  case "ASSERT":
35743
35794
  return executeAssert(stmt, client, options, cacheContext);
35744
35795
  }
@@ -35752,6 +35803,8 @@ var BatchTimeoutError = class extends Error {
35752
35803
  async function executeBatch(sql, client, options = {}) {
35753
35804
  const statements = parseSqlBatch(sql);
35754
35805
  const analysis = analyzeBatch(statements);
35806
+ const injectedVariables = validateDeclaredBatchVariables(statements, options.variables);
35807
+ const batchOptions = { ...options, variables: injectedVariables };
35755
35808
  if (options.continueOnError && analysis.containsDml) {
35756
35809
  throw new Error("ArgumentError: continueOnError is not allowed for batches containing DML.");
35757
35810
  }
@@ -35796,16 +35849,16 @@ async function executeBatch(sql, client, options = {}) {
35796
35849
  }
35797
35850
  try {
35798
35851
  const remaining = deadline !== null ? deadline - Date.now() : null;
35799
- const userConfirm = options.confirm;
35852
+ const userConfirm = batchOptions.confirm;
35800
35853
  const stmtOptions = userConfirm ? {
35801
- ...options,
35854
+ ...batchOptions,
35802
35855
  confirm: (count, operation) => userConfirm(count, operation, {
35803
35856
  statementIndex: i,
35804
35857
  statementCount: statements.length,
35805
35858
  statementType: info.statementType,
35806
35859
  targetAppId: info.targetAppId
35807
35860
  })
35808
- } : options;
35861
+ } : batchOptions;
35809
35862
  const outcome = await runWithDeadline(
35810
35863
  executeBatchStatement(statements[i], info, countedClient, stmtOptions, cacheContext, tempTables, variables),
35811
35864
  remaining
@@ -35818,7 +35871,7 @@ async function executeBatch(sql, client, options = {}) {
35818
35871
  aborted2 = "timeout";
35819
35872
  } else if (e instanceof AssertError) {
35820
35873
  aborted2 = "assertion";
35821
- } else if (info.statementType === "SET_VARIABLE") {
35874
+ } else if (info.statementType === "SET_VARIABLE" || info.statementType === "DECLARE_VARIABLE") {
35822
35875
  aborted2 = "fail-fast";
35823
35876
  } else if (!options.continueOnError) {
35824
35877
  aborted2 = "fail-fast";
@@ -35858,6 +35911,16 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
35858
35911
  }
35859
35912
  return {};
35860
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
+ }
35922
+ return {};
35923
+ }
35861
35924
  const resolvedStmt = resolveVariableRefs(stmt, variables);
35862
35925
  if (resolvedStmt.type === "CREATE_TEMP_TABLE") {
35863
35926
  const materializeOptions = {
@@ -37583,9 +37646,10 @@ async function resolveScalarColumns(columns, client, options, cacheContext, cteC
37583
37646
  pending.forEach(([i], idx) => cache.set(i, values[idx]));
37584
37647
  return cache;
37585
37648
  }
37586
- function buildBatchExplainPlans(sql) {
37649
+ function buildBatchExplainPlans(sql, injectedVariables) {
37587
37650
  const statements = parseSqlBatch(sql);
37588
37651
  const analysis = analyzeBatch(statements);
37652
+ validateDeclaredBatchVariables(statements, injectedVariables);
37589
37653
  const variables = /* @__PURE__ */ new Map();
37590
37654
  return {
37591
37655
  statementCount: statements.length,
@@ -37596,7 +37660,7 @@ function buildBatchExplainPlans(sql) {
37596
37660
  type: analysis.statements[i].statementType,
37597
37661
  plan: buildBatchStatementPlan(planStmt, analysis.statements[i])
37598
37662
  };
37599
- if (stmt.type === "SET_VARIABLE") {
37663
+ if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
37600
37664
  variables.set(stmt.name, { type: "string", value: `@${stmt.name}` });
37601
37665
  }
37602
37666
  return result;
@@ -37633,6 +37697,12 @@ function buildBatchStatementPlan(stmt, info) {
37633
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"
37634
37698
  ];
37635
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
+ }
37636
37706
  if (stmt.type === "SHOW_APPS") return ["SHOW APPS\uFF08\u30A2\u30D7\u30EA\u4E00\u89A7\u306E\u53D6\u5F97\uFF09"];
37637
37707
  if (stmt.type === "DESCRIBE") return [`DESCRIBE APP${stmt.appId}\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u5B9A\u7FA9\u306E\u53D6\u5F97\uFF09`];
37638
37708
  if (stmt.type === "EXPLAIN") return buildPlanForBatchQuery(stmt.query, info);
@@ -39611,6 +39681,9 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
39611
39681
  }
39612
39682
  async function query(input, validated) {
39613
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
+ }
39614
39687
  if (validation.batch) {
39615
39688
  if (validation.containsDml) {
39616
39689
  throw new Error("ArgumentError: batch contains DML statements. Use ksql_mutate.");
@@ -39637,7 +39710,8 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
39637
39710
  // バッチでは timeout を合計タイムアウトとして扱う(仕様 §5.7)。
39638
39711
  // runtime.timeout は env / profile / 既定 30000ms を解決済みの値で、
39639
39712
  // HTTP クライアント側の per-request タイムアウトと同値になる
39640
- timeoutMs: runtime2.timeout
39713
+ timeoutMs: runtime2.timeout,
39714
+ variables: input.variables
39641
39715
  });
39642
39716
  return { ...buildBatchEnvelope(batchResult, { maxTotalRecords: input.maxTotalRecords }) };
39643
39717
  }
@@ -39726,6 +39800,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
39726
39800
  tempTableMaxRows: runtime.tempTableMaxRows,
39727
39801
  // 合計タイムアウト(解決済みの runtime.timeout。per-request と同値)
39728
39802
  timeoutMs: runtime.timeout,
39803
+ variables: input.variables,
39729
39804
  confirm: async (count, operation) => {
39730
39805
  if (count > dmlMaxRows) {
39731
39806
  throw new Error(`ArgumentError: ${operation} affected rows (${count}) exceed dmlMaxRows (${dmlMaxRows}).`);
@@ -39754,6 +39829,9 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
39754
39829
  async function mutate(input, validated) {
39755
39830
  const dmlMaxRows = requireDmlApproval(input, "ksql_mutate");
39756
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
+ }
39757
39835
  if (validation.batch) {
39758
39836
  return mutateBatch(input, validation, dmlMaxRows);
39759
39837
  }
@@ -39976,7 +40054,8 @@ var queryInputSchema = external_exports.object({
39976
40054
  tempTableMaxRows,
39977
40055
  timeout,
39978
40056
  continueOnError: external_exports.boolean().describe("Batch (multi-statement) only: keep executing subsequent statements after a runtime error (default false = fail-fast).").optional(),
39979
- 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()
39980
40059
  });
39981
40060
  var mutateInputSchema = external_exports.object({
39982
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;"),
@@ -39987,7 +40066,8 @@ var mutateInputSchema = external_exports.object({
39987
40066
  fetchParallel,
39988
40067
  tempTableMaxRows,
39989
40068
  timeout,
39990
- 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()
39991
40071
  });
39992
40072
  var describeAppInputSchema = external_exports.object({
39993
40073
  app: external_exports.number().int().positive().describe("kintone app ID to describe."),
@@ -40076,7 +40156,7 @@ Options:
40076
40156
  -h, --help Show help
40077
40157
  `);
40078
40158
  }
40079
- var SERVER_VERSION = true ? "2.3.0" : "0.0.0-dev";
40159
+ var SERVER_VERSION = true ? "2.4.0" : "0.0.0-dev";
40080
40160
  function createServer(args) {
40081
40161
  const server = new McpServer({
40082
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.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "kintone SQL plugin, CLI, and MCP tools (ksql)",
5
5
  "publishConfig": {
6
6
  "access": "public"