@rex0220/kintone-sql-tools 1.13.2 → 2.1.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/dist-cli/ksql.js CHANGED
@@ -183,6 +183,7 @@ var Lexer = class {
183
183
  if (opTok) return opTok;
184
184
  if (isIdentStart(ch)) return this.readIdentOrKeyword(start);
185
185
  if (ch === "#") return this.readHashIdent(start);
186
+ if (ch === "@") return this.readVariable(start);
186
187
  throw new LexError(
187
188
  `\u4E88\u671F\u3057\u306A\u3044\u6587\u5B57 \u300C${ch}\u300D \u3067\u3059`,
188
189
  this.pos,
@@ -362,6 +363,23 @@ var Lexer = class {
362
363
  }
363
364
  return this.makeToken("IDENT" /* IDENT */, value, start);
364
365
  }
366
+ /** バッチ変数: @[A-Za-z_][A-Za-z0-9_]{0,63} */
367
+ readVariable(start) {
368
+ this.pos++;
369
+ const first = this.input[this.pos] ?? "";
370
+ if (!/[A-Za-z_]/.test(first)) {
371
+ throw new LexError("\u300C@\u300D \u306E\u76F4\u5F8C\u306B\u306F\u82F1\u5B57\u307E\u305F\u306F _ \u3067\u59CB\u307E\u308B\u5909\u6570\u540D\u304C\u5FC5\u8981\u3067\u3059", start, this.input);
372
+ }
373
+ this.pos++;
374
+ while (this.pos < this.input.length && /[A-Za-z0-9_]/.test(this.input[this.pos])) {
375
+ this.pos++;
376
+ }
377
+ const nameLength = this.pos - start - 1;
378
+ if (nameLength > 64) {
379
+ throw new LexError("\u5909\u6570\u540D\u306F 64 \u6587\u5B57\u4EE5\u5185\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044", start, this.input);
380
+ }
381
+ return this.makeToken("VARIABLE" /* VARIABLE */, this.input.slice(start, this.pos), start);
382
+ }
365
383
  // ----------------------------------------------------------
366
384
  // 空白・コメントをスキップ
367
385
  // ----------------------------------------------------------
@@ -573,6 +591,8 @@ var Parser = class {
573
591
  return this.parseDescribe();
574
592
  case "EXPLAIN" /* EXPLAIN */:
575
593
  return this.parseExplain();
594
+ case "SET" /* SET */:
595
+ return this.parseSetVariable();
576
596
  case "ASSERT" /* ASSERT */:
577
597
  return this.parseAssert();
578
598
  case "IDENT" /* IDENT */: {
@@ -585,11 +605,65 @@ var Parser = class {
585
605
  break;
586
606
  }
587
607
  throw new ParseError(
588
- "SELECT / INSERT / UPDATE / DELETE / REORDER / WITH / SHOW / DESCRIBE / EXPLAIN / CREATE TEMP TABLE / DROP TEMP TABLE / ASSERT \u306E\u3044\u305A\u308C\u304B\u3067\u59CB\u307E\u308B SQL \u6587\u304C\u5FC5\u8981\u3067\u3059",
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",
589
609
  tok
590
610
  );
591
611
  }
592
612
  // ----------------------------------------------------------
613
+ // SET @name = <ScalarExpr>
614
+ // ----------------------------------------------------------
615
+ parseSetVariable() {
616
+ this.expect("SET" /* SET */);
617
+ const variable = this.expect("VARIABLE" /* VARIABLE */, "SET \u306E\u5F8C\u306B\u306F\u5909\u6570\u540D\uFF08\u4F8B: @name\uFF09\u304C\u5FC5\u8981\u3067\u3059");
618
+ this.expect("=" /* EQ */);
619
+ const expr = this.parseScalarExpr();
620
+ return { type: "SET_VARIABLE", name: variable.value.slice(1).toLowerCase(), expr };
621
+ }
622
+ /** SET RHS 専用。既存式パーサーで構文を読み、フィールド参照を明示的に拒否する。 */
623
+ parseScalarExpr() {
624
+ const tok = this.peek();
625
+ 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);
627
+ }
628
+ 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);
630
+ }
631
+ 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);
633
+ }
634
+ if (tok.kind === "STRING" /* STRING */) {
635
+ this.advance();
636
+ return { type: "STRING", value: tok.value };
637
+ }
638
+ if (tok.kind === "LOGINUSER" /* LOGINUSER */) {
639
+ 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",
641
+ tok
642
+ );
643
+ }
644
+ if (tok.kind === "TODAY" /* TODAY */ || tok.kind === "NOW" /* NOW */) {
645
+ return this.parseSqlValue();
646
+ }
647
+ const expr = this.parseArithAddSub();
648
+ this.rejectNonScalarExpr(expr, tok);
649
+ 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);
651
+ }
652
+ rejectNonScalarExpr(node, tok) {
653
+ if (node.type === "STRING" || node.type === "NUMBER") return;
654
+ 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);
656
+ }
657
+ if (node.type === "ARITH" || node.type === "AGG_ARITH") {
658
+ this.rejectNonScalarExpr(node.left, tok);
659
+ this.rejectNonScalarExpr(node.right, tok);
660
+ return;
661
+ }
662
+ if (node.type === "STRING_FUNC") {
663
+ for (const arg of node.args) this.rejectNonScalarExpr(arg, tok);
664
+ }
665
+ }
666
+ // ----------------------------------------------------------
593
667
  // CREATE TEMP TABLE / DROP TEMP TABLE(バッチ内一時テーブル)
594
668
  // CREATE / DROP / TEMP / TABLE は予約語にしない(ソフトキーワード)
595
669
  // ----------------------------------------------------------
@@ -733,6 +807,10 @@ var Parser = class {
733
807
  /** ASSERT のオペランド: 文字列 / スカラーサブクエリ / 数値算術式 */
734
808
  parseAssertOperand() {
735
809
  const tok = this.peek();
810
+ if (tok.kind === "VARIABLE" /* VARIABLE */) {
811
+ this.advance();
812
+ return { type: "VARIABLE", name: tok.value.slice(1).toLowerCase() };
813
+ }
736
814
  if (tok.kind === "STRING" /* STRING */) {
737
815
  this.advance();
738
816
  return { type: "STRING", value: tok.value };
@@ -1640,6 +1718,10 @@ var Parser = class {
1640
1718
  // 右辺の値
1641
1719
  parseSqlValue() {
1642
1720
  const tok = this.peek();
1721
+ if (tok.kind === "VARIABLE" /* VARIABLE */) {
1722
+ this.advance();
1723
+ return { type: "VARIABLE", name: tok.value.slice(1).toLowerCase() };
1724
+ }
1643
1725
  if (tok.kind === "STRING" /* STRING */) {
1644
1726
  this.advance();
1645
1727
  return { type: "STRING", value: tok.value };
@@ -1702,8 +1784,10 @@ var Parser = class {
1702
1784
  values.push({ type: "STRING", value: tok.value });
1703
1785
  } else if (tok.kind === "NUMBER" /* NUMBER */) {
1704
1786
  values.push({ type: "NUMBER", value: Number(tok.value) });
1787
+ } else if (tok.kind === "VARIABLE" /* VARIABLE */) {
1788
+ values.push({ type: "VARIABLE", name: tok.value.slice(1).toLowerCase() });
1705
1789
  } else {
1706
- throw new ParseError("IN \u30EA\u30B9\u30C8\u306B\u306F\u6587\u5B57\u5217\u307E\u305F\u306F\u6570\u5024\u304C\u5FC5\u8981\u3067\u3059", tok);
1790
+ throw new ParseError("IN \u30EA\u30B9\u30C8\u306B\u306F\u6587\u5B57\u5217\u3001\u6570\u5024\u3001\u307E\u305F\u306F\u30D0\u30C3\u30C1\u5909\u6570\u304C\u5FC5\u8981\u3067\u3059", tok);
1707
1791
  }
1708
1792
  } while (this.consume("," /* COMMA */));
1709
1793
  return values;
@@ -1935,6 +2019,7 @@ var Parser = class {
1935
2019
  */
1936
2020
  parseAssignmentValue() {
1937
2021
  const tok = this.peek();
2022
+ if (tok.kind === "VARIABLE" /* VARIABLE */) return this.parseSqlValue();
1938
2023
  if (tok.kind === "STRING" /* STRING */) return this.parseSqlValue();
1939
2024
  if (tok.kind === "TODAY" /* TODAY */ || tok.kind === "NOW" /* NOW */ || tok.kind === "LOGINUSER" /* LOGINUSER */) return this.parseSqlValue();
1940
2025
  if (tok.kind === "IN" /* IN */) return this.parseSqlValue();
@@ -2196,7 +2281,7 @@ function isDmlType(type) {
2196
2281
  return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
2197
2282
  }
2198
2283
  function isReadOnlyType(type) {
2199
- return type === "SELECT" || type === "UNION" || type === "WITH" || type === "EXPLAIN" || type === "SHOW_APPS" || type === "DESCRIBE" || type === "CREATE_TEMP_TABLE" || type === "DROP_TEMP_TABLE" || type === "ASSERT";
2284
+ 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";
2200
2285
  }
2201
2286
  function hasWhereClause(stmt) {
2202
2287
  if (!stmt || typeof stmt !== "object") return false;
@@ -2229,6 +2314,7 @@ function collectDmlTargetFields(stmt) {
2229
2314
 
2230
2315
  // src/core/batch.ts
2231
2316
  var MAX_TEMP_TABLES = 16;
2317
+ var MAX_BATCH_VARIABLES = 64;
2232
2318
  var BatchAnalysisError = class extends Error {
2233
2319
  constructor(message, statementIndex) {
2234
2320
  super(message);
@@ -2249,12 +2335,29 @@ function collectRefs(node, tempRefs, appIds) {
2249
2335
  for (const v of Object.values(obj)) collectRefs(v, tempRefs, appIds);
2250
2336
  }
2251
2337
  }
2338
+ function collectVariableRefs(node, refs) {
2339
+ if (Array.isArray(node)) {
2340
+ for (const v of node) collectVariableRefs(v, refs);
2341
+ return;
2342
+ }
2343
+ if (node !== null && typeof node === "object") {
2344
+ const obj = node;
2345
+ if (obj["type"] === "VARIABLE" && typeof obj["name"] === "string") {
2346
+ refs.add(obj["name"]);
2347
+ return;
2348
+ }
2349
+ for (const v of Object.values(obj)) collectVariableRefs(v, refs);
2350
+ }
2351
+ }
2252
2352
  function analyzeBatch(statements) {
2253
2353
  if (statements.length === 0) {
2254
2354
  throw new BatchAnalysisError("ArgumentError: SQL is empty.", 0);
2255
2355
  }
2256
2356
  if (statements.length === 1) {
2257
2357
  const t = statements[0].type;
2358
+ if (t === "SET_VARIABLE") {
2359
+ throw new BatchAnalysisError("ArgumentError: SET variable requires a batch.", 0);
2360
+ }
2258
2361
  if (t === "CREATE_TEMP_TABLE" || t === "DROP_TEMP_TABLE") {
2259
2362
  const verb = t === "CREATE_TEMP_TABLE" ? "CREATE TEMP TABLE" : "DROP TEMP TABLE";
2260
2363
  throw new BatchAnalysisError(
@@ -2266,6 +2369,8 @@ function analyzeBatch(statements) {
2266
2369
  const defined = /* @__PURE__ */ new Map();
2267
2370
  const createdOrder = [];
2268
2371
  const results = [];
2372
+ const variableDefs = /* @__PURE__ */ new Map();
2373
+ const variableOrder = [];
2269
2374
  statements.forEach((stmt, index) => {
2270
2375
  const statementType = getStatementType(stmt);
2271
2376
  const created = [];
@@ -2273,6 +2378,31 @@ function analyzeBatch(statements) {
2273
2378
  const refs = /* @__PURE__ */ new Set();
2274
2379
  const stmtAppIds = /* @__PURE__ */ new Set();
2275
2380
  const dependsOn = /* @__PURE__ */ new Set();
2381
+ const variableRefs = /* @__PURE__ */ new Set();
2382
+ collectVariableRefs(stmt, variableRefs);
2383
+ for (const name of variableRefs) {
2384
+ const def = variableDefs.get(name);
2385
+ if (def === void 0) {
2386
+ throw new BatchAnalysisError(
2387
+ `ParseError: variable @${name} is not defined before statement ${index + 1}.`,
2388
+ index
2389
+ );
2390
+ }
2391
+ def.referencedBy.push(index);
2392
+ }
2393
+ if (stmt.type === "SET_VARIABLE") {
2394
+ if (variableDefs.has(stmt.name)) {
2395
+ throw new BatchAnalysisError(`ParseError: variable @${stmt.name} is already defined.`, index);
2396
+ }
2397
+ variableDefs.set(stmt.name, { index, referencedBy: [] });
2398
+ variableOrder.push(stmt.name);
2399
+ if (variableOrder.length > MAX_BATCH_VARIABLES) {
2400
+ throw new BatchAnalysisError(
2401
+ `ParseError: batch exceeds ${MAX_BATCH_VARIABLES} variables.`,
2402
+ index
2403
+ );
2404
+ }
2405
+ }
2276
2406
  if (stmt.type === "CREATE_TEMP_TABLE") {
2277
2407
  collectRefs(stmt.query, refs, stmtAppIds);
2278
2408
  } else if (stmt.type === "DROP_TEMP_TABLE") {
@@ -2342,11 +2472,17 @@ function analyzeBatch(statements) {
2342
2472
  });
2343
2473
  });
2344
2474
  const containsDml = results.some((r) => r.isDml);
2475
+ const variables = variableOrder.map((name) => ({
2476
+ name,
2477
+ referencedBy: [...variableDefs.get(name).referencedBy]
2478
+ }));
2345
2479
  return {
2346
2480
  statementCount: statements.length,
2347
2481
  isReadOnlyBatch: !containsDml && results.every((r) => r.isReadOnly),
2348
2482
  containsDml,
2349
2483
  tempTables: createdOrder,
2484
+ variables,
2485
+ warnings: variables.filter((v) => v.referencedBy.length === 0).map((v) => `variable @${v.name} is never used.`),
2350
2486
  statements: results
2351
2487
  };
2352
2488
  }
@@ -2407,6 +2543,29 @@ function negateOp(op) {
2407
2543
  }
2408
2544
  }
2409
2545
 
2546
+ // src/core/like.ts
2547
+ function likePatternHasWildcard(pattern) {
2548
+ return pattern.includes("%") || pattern.includes("_");
2549
+ }
2550
+ function isLike(where) {
2551
+ return where.type === "BINARY" && (where.op === "LIKE" || where.op === "NOT_LIKE");
2552
+ }
2553
+ function whereHasLike(where) {
2554
+ if (where === null) return false;
2555
+ if (isLike(where)) return true;
2556
+ switch (where.type) {
2557
+ case "LOGICAL":
2558
+ return whereHasLike(where.left) || whereHasLike(where.right);
2559
+ case "NOT":
2560
+ case "GROUP":
2561
+ return whereHasLike(where.expr);
2562
+ case "BINARY":
2563
+ case "NULL_CHECK":
2564
+ case "EXISTS":
2565
+ return false;
2566
+ }
2567
+ }
2568
+
2410
2569
  // src/converter/whereToKintone.ts
2411
2570
  function whereToKintone(expr) {
2412
2571
  switch (expr.type) {
@@ -2425,6 +2584,11 @@ function whereToKintone(expr) {
2425
2584
  }
2426
2585
  }
2427
2586
  function convertBinary(expr) {
2587
+ if (isLike(expr)) {
2588
+ throw new KintoneQueryError(
2589
+ "LIKE / NOT LIKE \u306F kintone \u30AF\u30A8\u30EA\u306B\u5909\u63DB\u3067\u304D\u307E\u305B\u3093\uFF08\u5E38\u306B JS \u8A55\u4FA1\u304C\u5FC5\u8981\u3067\u3059\uFF09"
2590
+ );
2591
+ }
2428
2592
  const left = convertField(expr.left);
2429
2593
  const op = convertOp(expr.op);
2430
2594
  const right = convertValue(expr.right, expr.op);
@@ -2489,6 +2653,8 @@ function convertField(field) {
2489
2653
  }
2490
2654
  function convertValue(value, op) {
2491
2655
  switch (value.type) {
2656
+ case "VARIABLE":
2657
+ throw new KintoneQueryError(`\u672A\u89E3\u6C7A\u306E\u30D0\u30C3\u30C1\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
2492
2658
  case "STRING":
2493
2659
  return convertString(value);
2494
2660
  case "NUMBER":
@@ -2519,11 +2685,18 @@ function convertInList(v, op) {
2519
2685
  if (op !== "IN" && op !== "NOT_IN") {
2520
2686
  throw new KintoneQueryError("IN_LIST \u306F IN / NOT IN \u6F14\u7B97\u5B50\u3067\u306E\u307F\u4F7F\u7528\u3067\u304D\u307E\u3059");
2521
2687
  }
2688
+ assertResolvedInListValues(v.values);
2522
2689
  const values = v.values.map(
2523
2690
  (item) => item.type === "STRING" ? convertString(item) : String(item.value)
2524
2691
  ).join(",");
2525
2692
  return `(${values})`;
2526
2693
  }
2694
+ function assertResolvedInListValues(values) {
2695
+ const unresolved = values.find((item) => item.type === "VARIABLE");
2696
+ if (unresolved?.type === "VARIABLE") {
2697
+ throw new KintoneQueryError(`\u672A\u89E3\u6C7A\u306E\u30D0\u30C3\u30C1\u5909\u6570 @${unresolved.name} \u304C\u3042\u308A\u307E\u3059`);
2698
+ }
2699
+ }
2527
2700
  function quoteIdentifier(name) {
2528
2701
  if (/^[\w$\u3000-\u9FFF]+$/u.test(name)) {
2529
2702
  return name;
@@ -2550,22 +2723,22 @@ function resolveSelectMode(stmt) {
2550
2723
  if (stmt.columns.some(
2551
2724
  (c) => c.type === "AGGREGATE" || c.type === "ARITH_AGG_COL" || c.type === "SCALAR_SUBQUERY_COL" || c.type === "STRFUNC_COL" && hasAggregateInStringFuncExpr(c.expr)
2552
2725
  )) return "FULL_SCAN";
2553
- if (hasWhereFunc(stmt.where)) return "FULL_SCAN";
2726
+ if (whereRequiresJsEval(stmt.where)) return "FULL_SCAN";
2554
2727
  if (stmt.orderBy.some((o) => o.key.type !== "FIELD_NAME")) return "FULL_SCAN";
2555
2728
  return "SIMPLE";
2556
2729
  }
2557
- function hasWhereFunc(where) {
2730
+ function whereRequiresJsEval(where) {
2558
2731
  if (where === null) return false;
2559
2732
  switch (where.type) {
2560
2733
  case "BINARY":
2561
- return isFunc(where.left) || where.right.type === "ARITH_VALUE" || where.right.type === "CASE_VALUE" || where.right.type === "SUBQUERY_IN_LIST" || where.right.type === "SCALAR_SUBQUERY";
2734
+ return isFunc(where.left) || where.right.type === "ARITH_VALUE" || where.right.type === "CASE_VALUE" || where.right.type === "SUBQUERY_IN_LIST" || where.right.type === "SCALAR_SUBQUERY" || isLike(where);
2562
2735
  case "NULL_CHECK":
2563
2736
  return isFunc(where.field);
2564
2737
  case "LOGICAL":
2565
- return hasWhereFunc(where.left) || hasWhereFunc(where.right);
2738
+ return whereRequiresJsEval(where.left) || whereRequiresJsEval(where.right);
2566
2739
  case "NOT":
2567
2740
  case "GROUP":
2568
- return hasWhereFunc(where.expr);
2741
+ return whereRequiresJsEval(where.expr);
2569
2742
  case "EXISTS":
2570
2743
  return true;
2571
2744
  }
@@ -2601,7 +2774,7 @@ function selectToKintoneParams(stmt) {
2601
2774
  }
2602
2775
  function selectToFetchAllParams(stmt, appId) {
2603
2776
  const queryParts = [];
2604
- if (stmt.where !== null && stmt.joins.length === 0 && !hasWhereFunc(stmt.where)) {
2777
+ if (stmt.where !== null && stmt.joins.length === 0 && !whereRequiresJsEval(stmt.where)) {
2605
2778
  queryParts.push(whereToKintone(stmt.where));
2606
2779
  }
2607
2780
  return {
@@ -3267,15 +3440,17 @@ function evalBinary(expr, row) {
3267
3440
  return evalOp(expr.op, left, expr.right, row);
3268
3441
  }
3269
3442
  function evalOp(op, leftStr, right, row) {
3270
- if (op === "IN") {
3271
- if (right.type === "IN_LIST") return right.values.some((v) => leftStr === String(v.value));
3272
- if (right.type === "SUBQUERY_IN_LIST") return right.resolved.has(leftStr);
3273
- return false;
3274
- }
3275
- if (op === "NOT_IN") {
3276
- if (right.type === "IN_LIST") return right.values.every((v) => leftStr !== String(v.value));
3277
- if (right.type === "SUBQUERY_IN_LIST") return !right.resolved.has(leftStr);
3278
- return true;
3443
+ if (op === "IN" || op === "NOT_IN") {
3444
+ if (right.type === "IN_LIST") {
3445
+ assertResolvedInListValues2(right.values);
3446
+ const contains = right.values.some((v) => leftStr === String(v.value));
3447
+ return op === "IN" ? contains : !contains;
3448
+ }
3449
+ if (right.type === "SUBQUERY_IN_LIST") {
3450
+ const contains = right.resolved.has(leftStr);
3451
+ return op === "IN" ? contains : !contains;
3452
+ }
3453
+ return op === "NOT_IN";
3279
3454
  }
3280
3455
  if (op === "LIKE") {
3281
3456
  const pattern = resolveValue(right, row);
@@ -3305,6 +3480,12 @@ function evalOp(op, leftStr, right, row) {
3305
3480
  return numeric ? leftNum <= rightNum : leftStr <= rightStr;
3306
3481
  }
3307
3482
  }
3483
+ function assertResolvedInListValues2(values) {
3484
+ const unresolved = values.find((item) => item.type === "VARIABLE");
3485
+ if (unresolved?.type === "VARIABLE") {
3486
+ throw new Error(`ParseError: unresolved batch variable @${unresolved.name}.`);
3487
+ }
3488
+ }
3308
3489
  function evalNullCheck(expr, row) {
3309
3490
  const val = resolveField(expr.field, row);
3310
3491
  return expr.not ? val !== "" : val === "";
@@ -3324,6 +3505,8 @@ function resolveField(field, row) {
3324
3505
  }
3325
3506
  function resolveValue(value, row) {
3326
3507
  switch (value.type) {
3508
+ case "VARIABLE":
3509
+ throw new Error(`ParseError: unresolved batch variable @${value.name}.`);
3327
3510
  case "STRING":
3328
3511
  return value.value;
3329
3512
  case "NUMBER":
@@ -3339,6 +3522,8 @@ function resolveValue(value, row) {
3339
3522
  case "SCALAR_SUBQUERY":
3340
3523
  return value.resolved;
3341
3524
  case "ARITH_VALUE":
3525
+ if (value.expr.type === "FIELD_REF") return resolveFieldRef(row, value.expr.field);
3526
+ if (value.expr.type === "STRING_FUNC") return evalStringFunc(value.expr, row);
3342
3527
  return String(evalArithExpr(value.expr, row));
3343
3528
  case "CASE_VALUE":
3344
3529
  return evalCaseWhen(value.expr, row);
@@ -3382,7 +3567,7 @@ function resolveKintoneFunc(name) {
3382
3567
  var likeRegexCache = /* @__PURE__ */ new Map();
3383
3568
  var LIKE_REGEX_CACHE_MAX = 200;
3384
3569
  function matchLike(value, pattern) {
3385
- if (!pattern.includes("%") && !pattern.includes("_")) {
3570
+ if (!likePatternHasWildcard(pattern)) {
3386
3571
  return value.includes(pattern);
3387
3572
  }
3388
3573
  let regex = likeRegexCache.get(pattern);
@@ -3407,6 +3592,12 @@ function matchLike(value, pattern) {
3407
3592
  }
3408
3593
 
3409
3594
  // src/converter/dmlToKintone.ts
3595
+ function assertDmlWhereIsSafe(where) {
3596
+ if (!whereHasLike(where)) return;
3597
+ throw new DmlConvertError(
3598
+ "UPDATE / DELETE \u306E WHERE \u306B LIKE / NOT LIKE \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\u3002LIKE \u306F kSQL \u306E\u610F\u5473\u8AD6\u306B\u5F93\u3063\u3066 JS \u3067\u8A55\u4FA1\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u304C\u3001\u89AA\u30EC\u30B3\u30FC\u30C9 DML \u306B\u306F JS \u8A55\u4FA1\u7D4C\u8DEF\u304C\u306A\u3044\u305F\u3081\u3001\u5B89\u5168\u4E0A\u62D2\u5426\u3057\u307E\u3057\u305F\u3002SELECT \u3067\u5BFE\u8C61\u30EC\u30B3\u30FC\u30C9\u756A\u53F7\u3092\u78BA\u8A8D\u3057\u3001IN \u307E\u305F\u306F\u5B8C\u5168\u4E00\u81F4\u3067\u5BFE\u8C61\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
3599
+ );
3600
+ }
3410
3601
  var USER_TYPES = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
3411
3602
  var ARRAY_TYPES = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
3412
3603
  function insertToPostBatches(stmt, fieldTypes = /* @__PURE__ */ new Map()) {
@@ -3431,6 +3622,7 @@ function buildInsertRecord(fields, row, fieldTypes) {
3431
3622
  return record;
3432
3623
  }
3433
3624
  function updateToGetQuery(stmt) {
3625
+ assertDmlWhereIsSafe(stmt.where);
3434
3626
  return {
3435
3627
  app: stmt.appId,
3436
3628
  query: whereToKintone(stmt.where),
@@ -3459,6 +3651,7 @@ function hasArithAssignment(stmt) {
3459
3651
  );
3460
3652
  }
3461
3653
  function updateToGetQueryForArith(stmt) {
3654
+ assertDmlWhereIsSafe(stmt.where);
3462
3655
  const refFields = /* @__PURE__ */ new Set();
3463
3656
  for (const { value } of stmt.assignments) {
3464
3657
  if (value.type === "ARITH") {
@@ -3604,6 +3797,7 @@ function resolveArithOperand(operand, raw) {
3604
3797
  return n;
3605
3798
  }
3606
3799
  function deleteToGetQuery(stmt) {
3800
+ assertDmlWhereIsSafe(stmt.where);
3607
3801
  return {
3608
3802
  app: stmt.appId,
3609
3803
  query: whereToKintone(stmt.where),
@@ -3676,6 +3870,8 @@ function evalCaseWhenValue(expr, row, fieldType) {
3676
3870
  }
3677
3871
  function toKintoneValue(value, fieldType) {
3678
3872
  switch (value.type) {
3873
+ case "VARIABLE":
3874
+ throw new DmlConvertError(`\u672A\u89E3\u6C7A\u306E\u30D0\u30C3\u30C1\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
3679
3875
  case "STRING":
3680
3876
  return convertString2(value.value, fieldType);
3681
3877
  case "NUMBER":
@@ -3888,6 +4084,7 @@ async function resolveDmlTargetIds(getRecords, app, query, options) {
3888
4084
  function extractTableCondition(where, tableAlias) {
3889
4085
  switch (where.type) {
3890
4086
  case "BINARY":
4087
+ if (isLike(where)) return null;
3891
4088
  if (!isSingleTableField(where.left, tableAlias)) return null;
3892
4089
  if (!isPushDownableRight(where.right)) return null;
3893
4090
  return where;
@@ -3927,7 +4124,7 @@ function isPushDownableRight(value) {
3927
4124
  function referencesOnlyTable(expr, tableAlias) {
3928
4125
  switch (expr.type) {
3929
4126
  case "BINARY":
3930
- return isSingleTableField(expr.left, tableAlias) && isPushDownableRight(expr.right);
4127
+ return !isLike(expr) && isSingleTableField(expr.left, tableAlias) && isPushDownableRight(expr.right);
3931
4128
  case "NULL_CHECK":
3932
4129
  return isSingleTableField(expr.field, tableAlias);
3933
4130
  case "LOGICAL":
@@ -4550,6 +4747,10 @@ async function executeStatement(sql, client, options) {
4550
4747
  return executeParsedStatement(stmt, client, options, cacheContext);
4551
4748
  }
4552
4749
  async function executeParsedStatement(stmt, client, options, cacheContext) {
4750
+ const unresolved = findVariableRef(stmt);
4751
+ if (unresolved !== null) {
4752
+ throw new Error(`ParseError: variable @${unresolved} is not defined in a batch.`);
4753
+ }
4553
4754
  switch (stmt.type) {
4554
4755
  case "SELECT":
4555
4756
  return executeSelect(stmt, client, options, cacheContext);
@@ -4582,6 +4783,8 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
4582
4783
  throw new Error("ArgumentError: CREATE TEMP TABLE requires a batch (temp tables are batch-scoped).");
4583
4784
  case "DROP_TEMP_TABLE":
4584
4785
  throw new Error("ArgumentError: DROP TEMP TABLE requires a batch (temp tables are batch-scoped).");
4786
+ case "SET_VARIABLE":
4787
+ throw new Error("ArgumentError: SET variable requires a batch.");
4585
4788
  case "ASSERT":
4586
4789
  return executeAssert(stmt, client, options, cacheContext);
4587
4790
  }
@@ -4612,6 +4815,7 @@ async function executeBatch(sql, client, options = {}) {
4612
4815
  const deadline = options.timeoutMs != null ? startedAt + options.timeoutMs : null;
4613
4816
  const cacheContext = options.cacheContext ?? "default";
4614
4817
  const tempTables = /* @__PURE__ */ new Map();
4818
+ const variables = /* @__PURE__ */ new Map();
4615
4819
  const results = [];
4616
4820
  const failed = /* @__PURE__ */ new Set();
4617
4821
  let aborted = null;
@@ -4649,7 +4853,7 @@ async function executeBatch(sql, client, options = {}) {
4649
4853
  })
4650
4854
  } : options;
4651
4855
  const outcome = await runWithDeadline(
4652
- executeBatchStatement(statements[i], info, countedClient, stmtOptions, cacheContext, tempTables),
4856
+ executeBatchStatement(statements[i], info, countedClient, stmtOptions, cacheContext, tempTables, variables),
4653
4857
  remaining
4654
4858
  );
4655
4859
  results.push({ ...base, status: "success", ...outcome });
@@ -4660,6 +4864,8 @@ async function executeBatch(sql, client, options = {}) {
4660
4864
  aborted = "timeout";
4661
4865
  } else if (e instanceof AssertError) {
4662
4866
  aborted = "assertion";
4867
+ } else if (info.statementType === "SET_VARIABLE") {
4868
+ aborted = "fail-fast";
4663
4869
  } else if (!options.continueOnError) {
4664
4870
  aborted = "fail-fast";
4665
4871
  }
@@ -4674,44 +4880,49 @@ async function executeBatch(sql, client, options = {}) {
4674
4880
  metrics
4675
4881
  };
4676
4882
  }
4677
- async function executeBatchStatement(stmt, info, client, options, cacheContext, tempTables) {
4678
- if (stmt.type === "CREATE_TEMP_TABLE") {
4883
+ async function executeBatchStatement(stmt, info, client, options, cacheContext, tempTables, variables) {
4884
+ if (stmt.type === "SET_VARIABLE") {
4885
+ variables.set(stmt.name, evaluateScalarExpr(stmt.expr));
4886
+ return {};
4887
+ }
4888
+ const resolvedStmt = resolveVariableRefs(stmt, variables);
4889
+ if (resolvedStmt.type === "CREATE_TEMP_TABLE") {
4679
4890
  const materializeOptions = {
4680
4891
  ...options,
4681
4892
  maxRecords: options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
4682
4893
  onLimitReached: "error"
4683
4894
  };
4684
- const result = await runSelectLike(stmt.query, client, materializeOptions, cacheContext, tempTables);
4685
- tempTables.set(stmt.name, result.rows);
4686
- return { tempTable: stmt.name, rowCount: result.rows.length };
4895
+ const result = await runSelectLike(resolvedStmt.query, client, materializeOptions, cacheContext, tempTables);
4896
+ tempTables.set(resolvedStmt.name, result.rows);
4897
+ return { tempTable: resolvedStmt.name, rowCount: result.rows.length };
4687
4898
  }
4688
4899
  if (stmt.type === "DROP_TEMP_TABLE") {
4689
4900
  tempTables.delete(stmt.name);
4690
4901
  return { tempTable: stmt.name };
4691
4902
  }
4692
4903
  if (stmt.type === "EXPLAIN") {
4693
- return { result: await executeParsedStatement(stmt, client, options, cacheContext) };
4904
+ return { result: await executeParsedStatement(resolvedStmt, client, options, cacheContext) };
4694
4905
  }
4695
- if (stmt.type === "ASSERT") {
4696
- await executeAssert(stmt, client, options, cacheContext, tempTables);
4906
+ if (resolvedStmt.type === "ASSERT") {
4907
+ await executeAssert(resolvedStmt, client, options, cacheContext, tempTables);
4697
4908
  return {};
4698
4909
  }
4699
4910
  if (info.tempTablesReferenced.length > 0) {
4700
- if (stmt.type === "SELECT" || stmt.type === "UNION") {
4701
- return { result: await executeQueryWithCte(stmt, client, options, tempTables, cacheContext) };
4911
+ if (resolvedStmt.type === "SELECT" || resolvedStmt.type === "UNION") {
4912
+ return { result: await executeQueryWithCte(resolvedStmt, client, options, tempTables, cacheContext) };
4702
4913
  }
4703
- if (stmt.type === "WITH") {
4704
- return { result: await executeWith(stmt, client, options, cacheContext, tempTables) };
4914
+ if (resolvedStmt.type === "WITH") {
4915
+ return { result: await executeWith(resolvedStmt, client, options, cacheContext, tempTables) };
4705
4916
  }
4706
- if (stmt.type === "INSERT_SELECT") {
4707
- return { result: await executeInsertSelect(stmt, client, options, cacheContext, tempTables) };
4917
+ if (resolvedStmt.type === "INSERT_SELECT") {
4918
+ return { result: await executeInsertSelect(resolvedStmt, client, options, cacheContext, tempTables) };
4708
4919
  }
4709
- if (stmt.type === "UPSERT_SELECT") {
4710
- return { result: await executeUpsertSelect(stmt, client, options, cacheContext, tempTables) };
4920
+ if (resolvedStmt.type === "UPSERT_SELECT") {
4921
+ return { result: await executeUpsertSelect(resolvedStmt, client, options, cacheContext, tempTables) };
4711
4922
  }
4712
4923
  throw new Error(`ArgumentError: temp table references in ${stmt.type} are not supported yet.`);
4713
4924
  }
4714
- return { result: await executeParsedStatement(stmt, client, options, cacheContext) };
4925
+ return { result: await executeParsedStatement(resolvedStmt, client, options, cacheContext) };
4715
4926
  }
4716
4927
  async function runSelectLike(query, client, options, cacheContext, tempTables) {
4717
4928
  if (query.type === "WITH") {
@@ -4772,6 +4983,62 @@ function parseSqlBatch(sql) {
4772
4983
  const tokens = new Lexer(sql).tokenize();
4773
4984
  return new Parser(tokens).parseStatements();
4774
4985
  }
4986
+ function evaluateScalarExpr(expr) {
4987
+ switch (expr.type) {
4988
+ case "STRING":
4989
+ return { type: "string", value: expr.value };
4990
+ case "NUMBER":
4991
+ return { type: "number", value: expr.value };
4992
+ case "KINTONE_FUNC":
4993
+ return { type: "string", value: resolveKintoneFunc(expr.name) };
4994
+ case "STRING_FUNC":
4995
+ return { type: "string", value: evalStringFunc(expr, {}) };
4996
+ case "ARITH": {
4997
+ const value = evalArithExpr(expr, {});
4998
+ if (!Number.isFinite(value)) {
4999
+ throw new Error("ArgumentError: SET scalar arithmetic produced a non-finite number.");
5000
+ }
5001
+ return { type: "number", value };
5002
+ }
5003
+ }
5004
+ }
5005
+ function resolveVariableRefs(node, variables) {
5006
+ if (Array.isArray(node)) {
5007
+ return node.map((v) => resolveVariableRefs(v, variables));
5008
+ }
5009
+ if (node !== null && typeof node === "object") {
5010
+ const obj = node;
5011
+ if (obj["type"] === "VARIABLE" && typeof obj["name"] === "string") {
5012
+ const value = variables.get(obj["name"]);
5013
+ if (value === void 0) {
5014
+ throw new Error(`ParseError: variable @${obj["name"]} is not defined in this batch.`);
5015
+ }
5016
+ return value.type === "number" ? { type: "NUMBER", value: value.value } : { type: "STRING", value: value.value };
5017
+ }
5018
+ return Object.fromEntries(
5019
+ Object.entries(obj).map(([key, value]) => [key, resolveVariableRefs(value, variables)])
5020
+ );
5021
+ }
5022
+ return node;
5023
+ }
5024
+ function findVariableRef(node) {
5025
+ if (Array.isArray(node)) {
5026
+ for (const value of node) {
5027
+ const found = findVariableRef(value);
5028
+ if (found !== null) return found;
5029
+ }
5030
+ return null;
5031
+ }
5032
+ if (node !== null && typeof node === "object") {
5033
+ const obj = node;
5034
+ if (obj["type"] === "VARIABLE" && typeof obj["name"] === "string") return obj["name"];
5035
+ for (const value of Object.values(obj)) {
5036
+ const found = findVariableRef(value);
5037
+ if (found !== null) return found;
5038
+ }
5039
+ }
5040
+ return null;
5041
+ }
4775
5042
  var AssertError = class extends Error {
4776
5043
  constructor(message) {
4777
5044
  super(`AssertError: ${message}`);
@@ -4802,6 +5069,8 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
4802
5069
  }
4803
5070
  async function evalAssertOperand(operand, client, options, cacheContext, tempTables) {
4804
5071
  switch (operand.type) {
5072
+ case "VARIABLE":
5073
+ throw new Error(`ParseError: unresolved batch variable @${operand.name}.`);
4805
5074
  case "NUMBER":
4806
5075
  return String(operand.value);
4807
5076
  case "STRING":
@@ -6303,13 +6572,21 @@ async function resolveScalarColumns(columns, client, options, cacheContext, cteC
6303
6572
  function buildBatchExplainPlans(sql) {
6304
6573
  const statements = parseSqlBatch(sql);
6305
6574
  const analysis = analyzeBatch(statements);
6575
+ const variables = /* @__PURE__ */ new Map();
6306
6576
  return {
6307
6577
  statementCount: statements.length,
6308
- statements: statements.map((stmt, i) => ({
6309
- index: i,
6310
- type: analysis.statements[i].statementType,
6311
- plan: buildBatchStatementPlan(stmt, analysis.statements[i])
6312
- }))
6578
+ statements: statements.map((stmt, i) => {
6579
+ const planStmt = stmt.type === "SET_VARIABLE" ? stmt : resolveVariableRefs(stmt, variables);
6580
+ const result = {
6581
+ index: i,
6582
+ type: analysis.statements[i].statementType,
6583
+ plan: buildBatchStatementPlan(planStmt, analysis.statements[i])
6584
+ };
6585
+ if (stmt.type === "SET_VARIABLE") {
6586
+ variables.set(stmt.name, { type: "string", value: `@${stmt.name}` });
6587
+ }
6588
+ return result;
6589
+ })
6313
6590
  };
6314
6591
  }
6315
6592
  function buildBatchStatementPlan(stmt, info) {
@@ -6327,6 +6604,12 @@ function buildBatchStatementPlan(stmt, info) {
6327
6604
  " \u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u30B9\u30C8\u30A2\u306E\u89E3\u653E\u306E\u307F\uFF08kintone \u30A2\u30AF\u30BB\u30B9\u306A\u3057\uFF09"
6328
6605
  ];
6329
6606
  }
6607
+ if (stmt.type === "SET_VARIABLE") {
6608
+ return [
6609
+ `SET @${stmt.name} = <scalar expression>`,
6610
+ " 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"
6611
+ ];
6612
+ }
6330
6613
  if (stmt.type === "SHOW_APPS") return ["SHOW APPS\uFF08\u30A2\u30D7\u30EA\u4E00\u89A7\u306E\u53D6\u5F97\uFF09"];
6331
6614
  if (stmt.type === "DESCRIBE") return [`DESCRIBE APP${stmt.appId}\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u5B9A\u7FA9\u306E\u53D6\u5F97\uFF09`];
6332
6615
  if (stmt.type === "EXPLAIN") return buildPlanForBatchQuery(stmt.query, info);
@@ -6486,8 +6769,10 @@ function collectFullScanReasons(stmt) {
6486
6769
  r.push("\u96C6\u8A08\u95A2\u6570\uFF08COUNT / SUM \u7B49\uFF09\u3042\u308A");
6487
6770
  if (stmt.columns.some((c) => c.type === "SCALAR_SUBQUERY_COL"))
6488
6771
  r.push("SELECT \u5217\u306B\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA");
6489
- if (hasWhereFunc(stmt.where))
6772
+ if (whereRequiresJsEval(stmt.where))
6490
6773
  r.push("WHERE \u53E5\u306B JS \u8A55\u4FA1\u304C\u5FC5\u8981\u306A\u5F0F");
6774
+ if (whereHasLike(stmt.where))
6775
+ r.push("LIKE \u306F\u5E38\u306B JS \u8A55\u4FA1\u306E\u305F\u3081\u5168\u4EF6\u53D6\u5F97");
6491
6776
  if (stmt.orderBy.some((o) => o.key.type !== "FIELD_NAME"))
6492
6777
  r.push("ORDER BY \u306B\u5F0F");
6493
6778
  return r;