@rex0220/kintone-sql-tools 2.1.2 → 2.3.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
@@ -629,7 +629,22 @@ var Parser = class {
629
629
  throw new ParseError("SET \u306E\u53F3\u8FBA\u3067 NULL \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF08Phase 1a\uFF09", tok);
630
630
  }
631
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);
632
+ this.advance();
633
+ const query = this.parseSelect();
634
+ this.expect(")" /* RPAREN */);
635
+ if (this.isArithOp(this.peek().kind)) {
636
+ throw new ParseError(
637
+ "\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",
638
+ this.peek()
639
+ );
640
+ }
641
+ const hasWildcard = query.columns.some(
642
+ (c) => c.type === "WILDCARD" || c.type === "PARENT_WILDCARD"
643
+ );
644
+ if (!hasWildcard && query.columns.length !== 1) {
645
+ throw new ParseError("scalar subquery in SET must return exactly 1 column.", tok);
646
+ }
647
+ return { type: "SCALAR_SUBQUERY", query };
633
648
  }
634
649
  if (tok.kind === "STRING" /* STRING */) {
635
650
  this.advance();
@@ -2492,6 +2507,28 @@ function analyzeBatch(statements) {
2492
2507
  };
2493
2508
  }
2494
2509
 
2510
+ // src/core/scalarCompare.ts
2511
+ function compareScalarValues(op, leftStr, rightStr) {
2512
+ if (op === "=") return leftStr === rightStr;
2513
+ if (op === "!=" || op === "<>") return leftStr !== rightStr;
2514
+ const rightNum = Number(rightStr);
2515
+ if (leftStr === "" && rightStr !== "" && Number.isFinite(rightNum)) {
2516
+ return op === "<" || op === "<=";
2517
+ }
2518
+ const leftNum = Number(leftStr);
2519
+ const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
2520
+ switch (op) {
2521
+ case ">":
2522
+ return numeric ? leftNum > rightNum : leftStr > rightStr;
2523
+ case "<":
2524
+ return numeric ? leftNum < rightNum : leftStr < rightStr;
2525
+ case ">=":
2526
+ return numeric ? leftNum >= rightNum : leftStr >= rightStr;
2527
+ case "<=":
2528
+ return numeric ? leftNum <= rightNum : leftStr <= rightStr;
2529
+ }
2530
+ }
2531
+
2495
2532
  // src/engine/pushDownNot.ts
2496
2533
  function pushDownNot(expr) {
2497
2534
  switch (expr.type) {
@@ -3466,24 +3503,7 @@ function evalOp(op, leftStr, right, row) {
3466
3503
  return !matchLike(leftStr, pattern);
3467
3504
  }
3468
3505
  const rightStr = resolveValue(right, row);
3469
- const leftNum = Number(leftStr);
3470
- const rightNum = Number(rightStr);
3471
- const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
3472
- switch (op) {
3473
- case "=":
3474
- return leftStr === rightStr;
3475
- case "!=":
3476
- case "<>":
3477
- return leftStr !== rightStr;
3478
- case ">":
3479
- return numeric ? leftNum > rightNum : leftStr > rightStr;
3480
- case "<":
3481
- return numeric ? leftNum < rightNum : leftStr < rightStr;
3482
- case ">=":
3483
- return numeric ? leftNum >= rightNum : leftStr >= rightStr;
3484
- case "<=":
3485
- return numeric ? leftNum <= rightNum : leftStr <= rightStr;
3486
- }
3506
+ return compareScalarValues(op, leftStr, rightStr);
3487
3507
  }
3488
3508
  function assertResolvedInListValues2(values) {
3489
3509
  const unresolved = values.find((item) => item.type === "VARIABLE");
@@ -4086,60 +4106,59 @@ async function resolveDmlTargetIds(getRecords, app, query, options) {
4086
4106
  }
4087
4107
 
4088
4108
  // src/core/optimization/wherePredicatePushdown.ts
4089
- function extractTableCondition(where, tableAlias) {
4109
+ function extractSafePushdownLeaves(where, options = {}) {
4110
+ return extractAndLeaves(where, (expr) => isSafeComparison(expr, options));
4111
+ }
4112
+ function extractNumericPushdownCandidates(where, options = {}) {
4113
+ return extractAndLeaves(where, (expr) => isNumericCandidate(expr, options));
4114
+ }
4115
+ function extractAndLeaves(where, accept) {
4090
4116
  switch (where.type) {
4091
4117
  case "BINARY":
4092
- if (isLike(where)) return null;
4093
- if (!isSingleTableField(where.left, tableAlias)) return null;
4094
- if (!isPushDownableRight(where.right)) return null;
4095
- return where;
4096
- case "NULL_CHECK":
4097
- if (!isSingleTableField(where.field, tableAlias)) return null;
4098
- return where;
4118
+ return accept(where) ? where : null;
4099
4119
  case "LOGICAL":
4100
- if (where.op === "AND") {
4101
- const left = extractTableCondition(where.left, tableAlias);
4102
- const right = extractTableCondition(where.right, tableAlias);
4120
+ if (where.op !== "AND") return null;
4121
+ {
4122
+ const left = extractAndLeaves(where.left, accept);
4123
+ const right = extractAndLeaves(where.right, accept);
4103
4124
  if (left && right) return { ...where, left, right };
4104
4125
  return left ?? right ?? null;
4105
4126
  }
4106
- return referencesOnlyTable(where, tableAlias) ? where : null;
4107
- case "NOT":
4108
4127
  case "GROUP":
4109
- return referencesOnlyTable(where, tableAlias) ? where : null;
4128
+ return extractAndLeaves(where.expr, accept);
4129
+ case "NULL_CHECK":
4130
+ case "NOT":
4110
4131
  case "EXISTS":
4111
4132
  return null;
4112
4133
  }
4113
4134
  }
4114
- function isSingleTableField(field, tableAlias) {
4115
- if (field.type !== "FIELD") return false;
4116
- return field.tableAlias === tableAlias;
4135
+ function isSafeComparison(expr, options) {
4136
+ if (isSafeIdComparison(expr, options)) return true;
4137
+ if (!isNumericCandidate(expr, options)) return false;
4138
+ return options.fieldTypes?.get(expr.left.field) === "NUMBER";
4117
4139
  }
4118
- function isPushDownableRight(value) {
4119
- switch (value.type) {
4120
- case "STRING":
4121
- case "NUMBER":
4122
- case "KINTONE_FUNC":
4123
- case "IN_LIST":
4124
- return true;
4125
- default:
4126
- return false;
4127
- }
4140
+ function isSafeIdComparison(expr, options) {
4141
+ if (!isTargetIdField(expr.left, options)) return false;
4142
+ if (expr.right.type !== "NUMBER") return false;
4143
+ return expr.op === "=" || expr.op === ">" || expr.op === "<" || expr.op === ">=" || expr.op === "<=";
4128
4144
  }
4129
- function referencesOnlyTable(expr, tableAlias) {
4130
- switch (expr.type) {
4131
- case "BINARY":
4132
- return !isLike(expr) && isSingleTableField(expr.left, tableAlias) && isPushDownableRight(expr.right);
4133
- case "NULL_CHECK":
4134
- return isSingleTableField(expr.field, tableAlias);
4135
- case "LOGICAL":
4136
- return referencesOnlyTable(expr.left, tableAlias) && referencesOnlyTable(expr.right, tableAlias);
4137
- case "NOT":
4138
- case "GROUP":
4139
- return referencesOnlyTable(expr.expr, tableAlias);
4140
- case "EXISTS":
4141
- return false;
4142
- }
4145
+ function isTargetIdField(field, options) {
4146
+ if (field.type !== "FIELD" || field.field !== "$id") return false;
4147
+ const targetAlias = options.tableAlias ?? null;
4148
+ if (field.tableAlias === targetAlias) return true;
4149
+ return options.allowUnqualifiedFields === true && field.tableAlias === null;
4150
+ }
4151
+ function isNumericCandidate(expr, options) {
4152
+ if (expr.left.type !== "FIELD" || expr.left.field === "$id") return false;
4153
+ if (!isTargetField(expr.left, options)) return false;
4154
+ if (expr.right.type !== "NUMBER") return false;
4155
+ if (expr.op === "=") return true;
4156
+ return (expr.op === "<" || expr.op === ">") && Number.isSafeInteger(expr.right.value);
4157
+ }
4158
+ function isTargetField(field, options) {
4159
+ const targetAlias = options.tableAlias ?? null;
4160
+ if (field.tableAlias === targetAlias) return true;
4161
+ return options.allowUnqualifiedFields === true && field.tableAlias === null;
4143
4162
  }
4144
4163
 
4145
4164
  // src/engine/process.ts
@@ -4916,7 +4935,26 @@ async function executeBatch(sql, client, options = {}) {
4916
4935
  }
4917
4936
  async function executeBatchStatement(stmt, info, client, options, cacheContext, tempTables, variables) {
4918
4937
  if (stmt.type === "SET_VARIABLE") {
4919
- variables.set(stmt.name, evaluateScalarExpr(stmt.expr));
4938
+ const resolvedStmt2 = resolveVariableRefs(stmt, variables);
4939
+ if (resolvedStmt2.expr.type === "SCALAR_SUBQUERY") {
4940
+ try {
4941
+ const value = await evaluateScalarSubquery(
4942
+ resolvedStmt2.expr.query,
4943
+ client,
4944
+ options,
4945
+ cacheContext,
4946
+ tempTables
4947
+ );
4948
+ variables.set(stmt.name, { type: "string", value });
4949
+ } catch (e) {
4950
+ if (e instanceof ScalarSubqueryError) {
4951
+ throw new Error(`ArgumentError: ${e.message}`);
4952
+ }
4953
+ throw e;
4954
+ }
4955
+ } else {
4956
+ variables.set(stmt.name, evaluateScalarExpr(resolvedStmt2.expr));
4957
+ }
4920
4958
  return {};
4921
4959
  }
4922
4960
  const resolvedStmt = resolveVariableRefs(stmt, variables);
@@ -5079,6 +5117,12 @@ var AssertError = class extends Error {
5079
5117
  this.name = "AssertError";
5080
5118
  }
5081
5119
  };
5120
+ var ScalarSubqueryError = class extends Error {
5121
+ constructor(message) {
5122
+ super(message);
5123
+ this.name = "ScalarSubqueryError";
5124
+ }
5125
+ };
5082
5126
  async function executeAssert(stmt, client, options, cacheContext, tempTables) {
5083
5127
  const left = await evalAssertOperand(stmt.left, client, options, cacheContext, tempTables);
5084
5128
  if (stmt.op === "BETWEEN") {
@@ -5087,7 +5131,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
5087
5131
  }
5088
5132
  const low = await evalAssertOperand(stmt.low, client, options, cacheContext, tempTables);
5089
5133
  const high = await evalAssertOperand(stmt.high, client, options, cacheContext, tempTables);
5090
- if (!compareAssertValues(">=", left, low) || !compareAssertValues("<=", left, high)) {
5134
+ if (!compareScalarValues(">=", left, low) || !compareScalarValues("<=", left, high)) {
5091
5135
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
5092
5136
  }
5093
5137
  return { type: "ASSERT", condition: stmt.text };
@@ -5096,7 +5140,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
5096
5140
  throw new Error("ArgumentError: malformed ASSERT statement.");
5097
5141
  }
5098
5142
  const right = await evalAssertOperand(stmt.right, client, options, cacheContext, tempTables);
5099
- if (!compareAssertValues(stmt.op, left, right)) {
5143
+ if (!compareScalarValues(stmt.op, left, right)) {
5100
5144
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
5101
5145
  }
5102
5146
  return { type: "ASSERT", condition: stmt.text };
@@ -5112,25 +5156,38 @@ async function evalAssertOperand(operand, client, options, cacheContext, tempTab
5112
5156
  case "ARITH":
5113
5157
  return String(evalAssertArith(operand));
5114
5158
  case "SCALAR_SUBQUERY": {
5115
- const { query, probed } = withScalarProbeLimit(operand.query);
5116
- const result = await runSubquery(query, client, options, cacheContext, tempTables);
5117
- if (result.columns.length > 1) {
5118
- throw new AssertError(
5119
- `scalar subquery returned ${result.columns.length} columns (expected 1 column).`
5159
+ try {
5160
+ return await evaluateScalarSubquery(
5161
+ operand.query,
5162
+ client,
5163
+ options,
5164
+ cacheContext,
5165
+ tempTables
5120
5166
  );
5167
+ } catch (e) {
5168
+ if (e instanceof ScalarSubqueryError) throw new AssertError(e.message);
5169
+ throw e;
5121
5170
  }
5122
- if (result.rowCount === 0) {
5123
- throw new AssertError("scalar subquery returned no rows (expected 1 row).");
5124
- }
5125
- if (result.rowCount > 1) {
5126
- const rows = probed && result.rowCount === 2 ? "2 or more rows" : `${result.rowCount} rows`;
5127
- throw new AssertError(`scalar subquery returned ${rows} (expected 1 row).`);
5128
- }
5129
- const col = result.columns[0] ?? "";
5130
- return result.rows[0]?.[col] ?? "";
5131
5171
  }
5132
5172
  }
5133
5173
  }
5174
+ async function evaluateScalarSubquery(sourceQuery, client, options, cacheContext, tempTables) {
5175
+ const { query, probed } = withScalarProbeLimit(sourceQuery);
5176
+ const result = await runSubquery(query, client, options, cacheContext, tempTables);
5177
+ if (result.columns.length !== 1) {
5178
+ throw new ScalarSubqueryError(
5179
+ `scalar subquery returned ${result.columns.length} columns (expected 1 column).`
5180
+ );
5181
+ }
5182
+ if (result.rowCount === 0) {
5183
+ throw new ScalarSubqueryError("scalar subquery returned no rows (expected 1 row).");
5184
+ }
5185
+ if (result.rowCount > 1) {
5186
+ const rows = probed && result.rowCount === 2 ? "2 or more rows" : `${result.rowCount} rows`;
5187
+ throw new ScalarSubqueryError(`scalar subquery returned ${rows} (expected 1 row).`);
5188
+ }
5189
+ return result.rows[0]?.[result.columns[0]] ?? "";
5190
+ }
5134
5191
  function withScalarProbeLimit(query) {
5135
5192
  const hasAgg = query.groupBy.length > 0 || query.columns.some((c) => c.type === "AGGREGATE" || c.type === "ARITH_AGG_COL");
5136
5193
  if (hasAgg || query.distinct || query.limit !== null) return { query, probed: false };
@@ -5156,26 +5213,6 @@ function evalAssertArith(node) {
5156
5213
  }
5157
5214
  throw new Error(`ArgumentError: unsupported operand in ASSERT expression: ${node.type}`);
5158
5215
  }
5159
- function compareAssertValues(op, leftStr, rightStr) {
5160
- const leftNum = Number(leftStr);
5161
- const rightNum = Number(rightStr);
5162
- const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
5163
- switch (op) {
5164
- case "=":
5165
- return leftStr === rightStr;
5166
- case "!=":
5167
- case "<>":
5168
- return leftStr !== rightStr;
5169
- case ">":
5170
- return numeric ? leftNum > rightNum : leftStr > rightStr;
5171
- case "<":
5172
- return numeric ? leftNum < rightNum : leftStr < rightStr;
5173
- case ">=":
5174
- return numeric ? leftNum >= rightNum : leftStr >= rightStr;
5175
- case "<=":
5176
- return numeric ? leftNum <= rightNum : leftStr <= rightStr;
5177
- }
5178
- }
5179
5216
  async function executeSelect(stmt, client, options, cacheContext, cteCache) {
5180
5217
  if (isNoFromSelect(stmt)) {
5181
5218
  return executeNoFromSelect(stmt);
@@ -5309,6 +5346,44 @@ async function validateSelectFieldCodes(stmt, mode, client, cacheContext) {
5309
5346
  }
5310
5347
  }
5311
5348
  }
5349
+ function extractMainSafePushdown(stmt, fieldTypes) {
5350
+ if (stmt.where === null || stmt.from.subtableCode || stmt.from.cteName !== null) return null;
5351
+ if (stmt.joins.length === 0) {
5352
+ return extractSafePushdownLeaves(stmt.where, {
5353
+ tableAlias: stmt.from.alias ?? void 0,
5354
+ allowUnqualifiedFields: true,
5355
+ fieldTypes
5356
+ });
5357
+ }
5358
+ if (!stmt.from.alias) return null;
5359
+ return extractSafePushdownLeaves(stmt.where, { tableAlias: stmt.from.alias, fieldTypes });
5360
+ }
5361
+ function extractMainNumericPushdownCandidate(stmt) {
5362
+ if (stmt.where === null || stmt.from.subtableCode || stmt.from.cteName !== null) return null;
5363
+ if (stmt.joins.length === 0) {
5364
+ return extractNumericPushdownCandidates(stmt.where, {
5365
+ tableAlias: stmt.from.alias ?? void 0,
5366
+ allowUnqualifiedFields: true
5367
+ });
5368
+ }
5369
+ if (!stmt.from.alias) return null;
5370
+ return extractNumericPushdownCandidates(stmt.where, { tableAlias: stmt.from.alias });
5371
+ }
5372
+ async function loadNumericPushdownFieldTypes(stmt, client, cacheContext) {
5373
+ const appIds = /* @__PURE__ */ new Set();
5374
+ if (extractMainNumericPushdownCandidate(stmt) !== null) appIds.add(stmt.from.appId);
5375
+ if (stmt.where !== null) {
5376
+ for (const join2 of stmt.joins) {
5377
+ if (!join2.table.alias || join2.table.subtableCode || join2.table.cteName !== null) continue;
5378
+ const candidate = extractNumericPushdownCandidates(stmt.where, {
5379
+ tableAlias: join2.table.alias
5380
+ });
5381
+ if (candidate !== null) appIds.add(join2.table.appId);
5382
+ }
5383
+ }
5384
+ const entries = await Promise.all([...appIds].map(async (appId) => [appId, await getFieldTypeMap(appId, client, cacheContext)]));
5385
+ return new Map(entries);
5386
+ }
5312
5387
  async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache) {
5313
5388
  const maxRecords = options.maxRecords ?? 1e4;
5314
5389
  const warnings = /* @__PURE__ */ new Set();
@@ -5317,20 +5392,22 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
5317
5392
  resolveSubqueries(stmt.where, client, options, cacheContext, cteCache),
5318
5393
  resolveSubqueries(stmt.having, client, options, cacheContext, cteCache)
5319
5394
  ]);
5395
+ const pushdownFieldTypes = await loadNumericPushdownFieldTypes(stmt, client, cacheContext);
5396
+ const mainPushDown = extractMainSafePushdown(
5397
+ stmt,
5398
+ pushdownFieldTypes.get(stmt.from.appId)
5399
+ );
5320
5400
  const tableConditions = /* @__PURE__ */ new Map();
5321
5401
  if (stmt.where !== null) {
5322
- if (stmt.from.alias) {
5323
- const cond = extractTableCondition(stmt.where, stmt.from.alias);
5324
- if (cond) tableConditions.set(stmt.from.alias, cond);
5325
- }
5326
5402
  for (const join2 of stmt.joins) {
5327
- if (join2.table.alias) {
5328
- const cond = extractTableCondition(stmt.where, join2.table.alias);
5329
- if (cond) tableConditions.set(join2.table.alias, cond);
5330
- }
5403
+ if (!join2.table.alias || join2.table.subtableCode || join2.table.cteName !== null) continue;
5404
+ const cond = extractSafePushdownLeaves(stmt.where, {
5405
+ tableAlias: join2.table.alias,
5406
+ fieldTypes: pushdownFieldTypes.get(join2.table.appId)
5407
+ });
5408
+ if (cond) tableConditions.set(join2.table.alias, cond);
5331
5409
  }
5332
5410
  }
5333
- const mainPushDown = stmt.from.alias ? tableConditions.get(stmt.from.alias) ?? null : null;
5334
5411
  const mainFetch = fetchTableRecordsForFullScan(
5335
5412
  stmt,
5336
5413
  stmt.from,
@@ -6612,7 +6689,7 @@ function buildBatchExplainPlans(sql) {
6612
6689
  return {
6613
6690
  statementCount: statements.length,
6614
6691
  statements: statements.map((stmt, i) => {
6615
- const planStmt = stmt.type === "SET_VARIABLE" ? stmt : resolveVariableRefs(stmt, variables);
6692
+ const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr: resolveVariableRefs(stmt.expr, variables) } : stmt : resolveVariableRefs(stmt, variables);
6616
6693
  const result = {
6617
6694
  index: i,
6618
6695
  type: analysis.statements[i].statementType,
@@ -6641,6 +6718,15 @@ function buildBatchStatementPlan(stmt, info) {
6641
6718
  ];
6642
6719
  }
6643
6720
  if (stmt.type === "SET_VARIABLE") {
6721
+ if (stmt.expr.type === "SCALAR_SUBQUERY") {
6722
+ const subInfo = hasTempTableRef(stmt.expr.query) ? info : { ...info, tempTablesReferenced: [] };
6723
+ return [
6724
+ `SET @${stmt.name} = (SELECT ...)`,
6725
+ " 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",
6726
+ " subquery:",
6727
+ ...buildPlanForBatchQuery(stmt.expr.query, subInfo).map((l) => ` ${l}`)
6728
+ ];
6729
+ }
6644
6730
  return [
6645
6731
  `SET @${stmt.name} = <scalar expression>`,
6646
6732
  " 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"
@@ -6741,19 +6827,27 @@ function buildSelectPlan(stmt, label) {
6741
6827
  } else {
6742
6828
  const mainFields = selectToFetchAllFields(stmt, stmt.from);
6743
6829
  const mainAliasStr = stmt.from.alias ? ` AS ${stmt.from.alias}` : "";
6744
- const mainPushDown = stmt.from.alias && stmt.where ? extractTableCondition(stmt.where, stmt.from.alias) : null;
6830
+ const mainPushDown = extractMainSafePushdown(stmt);
6831
+ const mainCandidate = extractMainNumericPushdownCandidate(stmt);
6745
6832
  const mainQ = mainPushDown !== null ? whereToKintone(mainPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
6746
6833
  lines.push(` app: APP${stmt.from.appId}${mainAliasStr} (${stmt.from.appId})`);
6747
6834
  lines.push(` kintone query: ${mainQ}`);
6835
+ if (mainCandidate !== null) {
6836
+ lines.push(` pushdown candidate: ${whereToKintone(mainCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u78BA\u8A8D\u5F85\u3061\uFF09`);
6837
+ }
6748
6838
  lines.push(` fields: ${mainFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : mainFields.join(", ")}`);
6749
6839
  for (const join2 of stmt.joins) {
6750
6840
  const joinFields = selectToFetchAllFields(stmt, join2.table);
6751
6841
  const joinAliasStr = join2.table.alias ? ` AS ${join2.table.alias}` : "";
6752
6842
  const joinType = join2.type === "INNER" ? "JOIN" : `${join2.type} JOIN`;
6753
- const joinPushDown = join2.table.alias && stmt.where ? extractTableCondition(stmt.where, join2.table.alias) : null;
6843
+ const joinPushDown = join2.table.alias && !join2.table.subtableCode && join2.table.cteName === null && stmt.where ? extractSafePushdownLeaves(stmt.where, { tableAlias: join2.table.alias }) : null;
6844
+ const joinCandidate = join2.table.alias && !join2.table.subtableCode && join2.table.cteName === null && stmt.where ? extractNumericPushdownCandidates(stmt.where, { tableAlias: join2.table.alias }) : null;
6754
6845
  const joinQ = joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
6755
6846
  lines.push(` ${joinType}: APP${join2.table.appId}${joinAliasStr} (${join2.table.appId})`);
6756
6847
  lines.push(` kintone query: ${joinQ}`);
6848
+ if (joinCandidate !== null) {
6849
+ lines.push(` pushdown candidate: ${whereToKintone(joinCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u78BA\u8A8D\u5F85\u3061\uFF09`);
6850
+ }
6757
6851
  lines.push(` fields: ${joinFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : joinFields.join(", ")}`);
6758
6852
  }
6759
6853
  }
@@ -31542,7 +31542,22 @@ var Parser = class {
31542
31542
  throw new ParseError("SET \u306E\u53F3\u8FBA\u3067 NULL \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF08Phase 1a\uFF09", tok);
31543
31543
  }
31544
31544
  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);
31545
+ this.advance();
31546
+ const query = this.parseSelect();
31547
+ this.expect(")" /* RPAREN */);
31548
+ if (this.isArithOp(this.peek().kind)) {
31549
+ throw new ParseError(
31550
+ "\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",
31551
+ this.peek()
31552
+ );
31553
+ }
31554
+ const hasWildcard = query.columns.some(
31555
+ (c) => c.type === "WILDCARD" || c.type === "PARENT_WILDCARD"
31556
+ );
31557
+ if (!hasWildcard && query.columns.length !== 1) {
31558
+ throw new ParseError("scalar subquery in SET must return exactly 1 column.", tok);
31559
+ }
31560
+ return { type: "SCALAR_SUBQUERY", query };
31546
31561
  }
31547
31562
  if (tok.kind === "STRING" /* STRING */) {
31548
31563
  this.advance();
@@ -33393,6 +33408,28 @@ function analyzeBatch(statements) {
33393
33408
  };
33394
33409
  }
33395
33410
 
33411
+ // src/core/scalarCompare.ts
33412
+ function compareScalarValues(op, leftStr, rightStr) {
33413
+ if (op === "=") return leftStr === rightStr;
33414
+ if (op === "!=" || op === "<>") return leftStr !== rightStr;
33415
+ const rightNum = Number(rightStr);
33416
+ if (leftStr === "" && rightStr !== "" && Number.isFinite(rightNum)) {
33417
+ return op === "<" || op === "<=";
33418
+ }
33419
+ const leftNum = Number(leftStr);
33420
+ const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
33421
+ switch (op) {
33422
+ case ">":
33423
+ return numeric ? leftNum > rightNum : leftStr > rightStr;
33424
+ case "<":
33425
+ return numeric ? leftNum < rightNum : leftStr < rightStr;
33426
+ case ">=":
33427
+ return numeric ? leftNum >= rightNum : leftStr >= rightStr;
33428
+ case "<=":
33429
+ return numeric ? leftNum <= rightNum : leftStr <= rightStr;
33430
+ }
33431
+ }
33432
+
33396
33433
  // src/engine/pushDownNot.ts
33397
33434
  function pushDownNot(expr) {
33398
33435
  switch (expr.type) {
@@ -34367,24 +34404,7 @@ function evalOp(op, leftStr, right, row) {
34367
34404
  return !matchLike(leftStr, pattern);
34368
34405
  }
34369
34406
  const rightStr = resolveValue(right, row);
34370
- const leftNum = Number(leftStr);
34371
- const rightNum = Number(rightStr);
34372
- const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
34373
- switch (op) {
34374
- case "=":
34375
- return leftStr === rightStr;
34376
- case "!=":
34377
- case "<>":
34378
- return leftStr !== rightStr;
34379
- case ">":
34380
- return numeric ? leftNum > rightNum : leftStr > rightStr;
34381
- case "<":
34382
- return numeric ? leftNum < rightNum : leftStr < rightStr;
34383
- case ">=":
34384
- return numeric ? leftNum >= rightNum : leftStr >= rightStr;
34385
- case "<=":
34386
- return numeric ? leftNum <= rightNum : leftStr <= rightStr;
34387
- }
34407
+ return compareScalarValues(op, leftStr, rightStr);
34388
34408
  }
34389
34409
  function assertResolvedInListValues2(values) {
34390
34410
  const unresolved = values.find((item) => item.type === "VARIABLE");
@@ -34987,60 +35007,59 @@ async function resolveDmlTargetIds(getRecords, app, query, options) {
34987
35007
  }
34988
35008
 
34989
35009
  // src/core/optimization/wherePredicatePushdown.ts
34990
- function extractTableCondition(where, tableAlias) {
35010
+ function extractSafePushdownLeaves(where, options = {}) {
35011
+ return extractAndLeaves(where, (expr) => isSafeComparison(expr, options));
35012
+ }
35013
+ function extractNumericPushdownCandidates(where, options = {}) {
35014
+ return extractAndLeaves(where, (expr) => isNumericCandidate(expr, options));
35015
+ }
35016
+ function extractAndLeaves(where, accept) {
34991
35017
  switch (where.type) {
34992
35018
  case "BINARY":
34993
- if (isLike(where)) return null;
34994
- if (!isSingleTableField(where.left, tableAlias)) return null;
34995
- if (!isPushDownableRight(where.right)) return null;
34996
- return where;
34997
- case "NULL_CHECK":
34998
- if (!isSingleTableField(where.field, tableAlias)) return null;
34999
- return where;
35019
+ return accept(where) ? where : null;
35000
35020
  case "LOGICAL":
35001
- if (where.op === "AND") {
35002
- const left = extractTableCondition(where.left, tableAlias);
35003
- const right = extractTableCondition(where.right, tableAlias);
35021
+ if (where.op !== "AND") return null;
35022
+ {
35023
+ const left = extractAndLeaves(where.left, accept);
35024
+ const right = extractAndLeaves(where.right, accept);
35004
35025
  if (left && right) return { ...where, left, right };
35005
35026
  return left ?? right ?? null;
35006
35027
  }
35007
- return referencesOnlyTable(where, tableAlias) ? where : null;
35008
- case "NOT":
35009
35028
  case "GROUP":
35010
- return referencesOnlyTable(where, tableAlias) ? where : null;
35029
+ return extractAndLeaves(where.expr, accept);
35030
+ case "NULL_CHECK":
35031
+ case "NOT":
35011
35032
  case "EXISTS":
35012
35033
  return null;
35013
35034
  }
35014
35035
  }
35015
- function isSingleTableField(field, tableAlias) {
35016
- if (field.type !== "FIELD") return false;
35017
- return field.tableAlias === tableAlias;
35036
+ function isSafeComparison(expr, options) {
35037
+ if (isSafeIdComparison(expr, options)) return true;
35038
+ if (!isNumericCandidate(expr, options)) return false;
35039
+ return options.fieldTypes?.get(expr.left.field) === "NUMBER";
35018
35040
  }
35019
- function isPushDownableRight(value) {
35020
- switch (value.type) {
35021
- case "STRING":
35022
- case "NUMBER":
35023
- case "KINTONE_FUNC":
35024
- case "IN_LIST":
35025
- return true;
35026
- default:
35027
- return false;
35028
- }
35041
+ function isSafeIdComparison(expr, options) {
35042
+ if (!isTargetIdField(expr.left, options)) return false;
35043
+ if (expr.right.type !== "NUMBER") return false;
35044
+ return expr.op === "=" || expr.op === ">" || expr.op === "<" || expr.op === ">=" || expr.op === "<=";
35029
35045
  }
35030
- function referencesOnlyTable(expr, tableAlias) {
35031
- switch (expr.type) {
35032
- case "BINARY":
35033
- return !isLike(expr) && isSingleTableField(expr.left, tableAlias) && isPushDownableRight(expr.right);
35034
- case "NULL_CHECK":
35035
- return isSingleTableField(expr.field, tableAlias);
35036
- case "LOGICAL":
35037
- return referencesOnlyTable(expr.left, tableAlias) && referencesOnlyTable(expr.right, tableAlias);
35038
- case "NOT":
35039
- case "GROUP":
35040
- return referencesOnlyTable(expr.expr, tableAlias);
35041
- case "EXISTS":
35042
- return false;
35043
- }
35046
+ function isTargetIdField(field, options) {
35047
+ if (field.type !== "FIELD" || field.field !== "$id") return false;
35048
+ const targetAlias = options.tableAlias ?? null;
35049
+ if (field.tableAlias === targetAlias) return true;
35050
+ return options.allowUnqualifiedFields === true && field.tableAlias === null;
35051
+ }
35052
+ function isNumericCandidate(expr, options) {
35053
+ if (expr.left.type !== "FIELD" || expr.left.field === "$id") return false;
35054
+ if (!isTargetField(expr.left, options)) return false;
35055
+ if (expr.right.type !== "NUMBER") return false;
35056
+ if (expr.op === "=") return true;
35057
+ return (expr.op === "<" || expr.op === ">") && Number.isSafeInteger(expr.right.value);
35058
+ }
35059
+ function isTargetField(field, options) {
35060
+ const targetAlias = options.tableAlias ?? null;
35061
+ if (field.tableAlias === targetAlias) return true;
35062
+ return options.allowUnqualifiedFields === true && field.tableAlias === null;
35044
35063
  }
35045
35064
 
35046
35065
  // src/engine/process.ts
@@ -35817,7 +35836,26 @@ async function executeBatch(sql, client, options = {}) {
35817
35836
  }
35818
35837
  async function executeBatchStatement(stmt, info, client, options, cacheContext, tempTables, variables) {
35819
35838
  if (stmt.type === "SET_VARIABLE") {
35820
- variables.set(stmt.name, evaluateScalarExpr(stmt.expr));
35839
+ const resolvedStmt2 = resolveVariableRefs(stmt, variables);
35840
+ if (resolvedStmt2.expr.type === "SCALAR_SUBQUERY") {
35841
+ try {
35842
+ const value = await evaluateScalarSubquery(
35843
+ resolvedStmt2.expr.query,
35844
+ client,
35845
+ options,
35846
+ cacheContext,
35847
+ tempTables
35848
+ );
35849
+ variables.set(stmt.name, { type: "string", value });
35850
+ } catch (e) {
35851
+ if (e instanceof ScalarSubqueryError) {
35852
+ throw new Error(`ArgumentError: ${e.message}`);
35853
+ }
35854
+ throw e;
35855
+ }
35856
+ } else {
35857
+ variables.set(stmt.name, evaluateScalarExpr(resolvedStmt2.expr));
35858
+ }
35821
35859
  return {};
35822
35860
  }
35823
35861
  const resolvedStmt = resolveVariableRefs(stmt, variables);
@@ -35980,6 +36018,12 @@ var AssertError = class extends Error {
35980
36018
  this.name = "AssertError";
35981
36019
  }
35982
36020
  };
36021
+ var ScalarSubqueryError = class extends Error {
36022
+ constructor(message) {
36023
+ super(message);
36024
+ this.name = "ScalarSubqueryError";
36025
+ }
36026
+ };
35983
36027
  async function executeAssert(stmt, client, options, cacheContext, tempTables) {
35984
36028
  const left = await evalAssertOperand(stmt.left, client, options, cacheContext, tempTables);
35985
36029
  if (stmt.op === "BETWEEN") {
@@ -35988,7 +36032,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
35988
36032
  }
35989
36033
  const low = await evalAssertOperand(stmt.low, client, options, cacheContext, tempTables);
35990
36034
  const high = await evalAssertOperand(stmt.high, client, options, cacheContext, tempTables);
35991
- if (!compareAssertValues(">=", left, low) || !compareAssertValues("<=", left, high)) {
36035
+ if (!compareScalarValues(">=", left, low) || !compareScalarValues("<=", left, high)) {
35992
36036
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
35993
36037
  }
35994
36038
  return { type: "ASSERT", condition: stmt.text };
@@ -35997,7 +36041,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
35997
36041
  throw new Error("ArgumentError: malformed ASSERT statement.");
35998
36042
  }
35999
36043
  const right = await evalAssertOperand(stmt.right, client, options, cacheContext, tempTables);
36000
- if (!compareAssertValues(stmt.op, left, right)) {
36044
+ if (!compareScalarValues(stmt.op, left, right)) {
36001
36045
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
36002
36046
  }
36003
36047
  return { type: "ASSERT", condition: stmt.text };
@@ -36013,25 +36057,38 @@ async function evalAssertOperand(operand, client, options, cacheContext, tempTab
36013
36057
  case "ARITH":
36014
36058
  return String(evalAssertArith(operand));
36015
36059
  case "SCALAR_SUBQUERY": {
36016
- const { query, probed } = withScalarProbeLimit(operand.query);
36017
- const result = await runSubquery(query, client, options, cacheContext, tempTables);
36018
- if (result.columns.length > 1) {
36019
- throw new AssertError(
36020
- `scalar subquery returned ${result.columns.length} columns (expected 1 column).`
36060
+ try {
36061
+ return await evaluateScalarSubquery(
36062
+ operand.query,
36063
+ client,
36064
+ options,
36065
+ cacheContext,
36066
+ tempTables
36021
36067
  );
36068
+ } catch (e) {
36069
+ if (e instanceof ScalarSubqueryError) throw new AssertError(e.message);
36070
+ throw e;
36022
36071
  }
36023
- if (result.rowCount === 0) {
36024
- throw new AssertError("scalar subquery returned no rows (expected 1 row).");
36025
- }
36026
- if (result.rowCount > 1) {
36027
- const rows = probed && result.rowCount === 2 ? "2 or more rows" : `${result.rowCount} rows`;
36028
- throw new AssertError(`scalar subquery returned ${rows} (expected 1 row).`);
36029
- }
36030
- const col = result.columns[0] ?? "";
36031
- return result.rows[0]?.[col] ?? "";
36032
36072
  }
36033
36073
  }
36034
36074
  }
36075
+ async function evaluateScalarSubquery(sourceQuery, client, options, cacheContext, tempTables) {
36076
+ const { query, probed } = withScalarProbeLimit(sourceQuery);
36077
+ const result = await runSubquery(query, client, options, cacheContext, tempTables);
36078
+ if (result.columns.length !== 1) {
36079
+ throw new ScalarSubqueryError(
36080
+ `scalar subquery returned ${result.columns.length} columns (expected 1 column).`
36081
+ );
36082
+ }
36083
+ if (result.rowCount === 0) {
36084
+ throw new ScalarSubqueryError("scalar subquery returned no rows (expected 1 row).");
36085
+ }
36086
+ if (result.rowCount > 1) {
36087
+ const rows = probed && result.rowCount === 2 ? "2 or more rows" : `${result.rowCount} rows`;
36088
+ throw new ScalarSubqueryError(`scalar subquery returned ${rows} (expected 1 row).`);
36089
+ }
36090
+ return result.rows[0]?.[result.columns[0]] ?? "";
36091
+ }
36035
36092
  function withScalarProbeLimit(query) {
36036
36093
  const hasAgg = query.groupBy.length > 0 || query.columns.some((c) => c.type === "AGGREGATE" || c.type === "ARITH_AGG_COL");
36037
36094
  if (hasAgg || query.distinct || query.limit !== null) return { query, probed: false };
@@ -36057,26 +36114,6 @@ function evalAssertArith(node) {
36057
36114
  }
36058
36115
  throw new Error(`ArgumentError: unsupported operand in ASSERT expression: ${node.type}`);
36059
36116
  }
36060
- function compareAssertValues(op, leftStr, rightStr) {
36061
- const leftNum = Number(leftStr);
36062
- const rightNum = Number(rightStr);
36063
- const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
36064
- switch (op) {
36065
- case "=":
36066
- return leftStr === rightStr;
36067
- case "!=":
36068
- case "<>":
36069
- return leftStr !== rightStr;
36070
- case ">":
36071
- return numeric ? leftNum > rightNum : leftStr > rightStr;
36072
- case "<":
36073
- return numeric ? leftNum < rightNum : leftStr < rightStr;
36074
- case ">=":
36075
- return numeric ? leftNum >= rightNum : leftStr >= rightStr;
36076
- case "<=":
36077
- return numeric ? leftNum <= rightNum : leftStr <= rightStr;
36078
- }
36079
- }
36080
36117
  async function executeSelect(stmt, client, options, cacheContext, cteCache) {
36081
36118
  if (isNoFromSelect(stmt)) {
36082
36119
  return executeNoFromSelect(stmt);
@@ -36210,6 +36247,44 @@ async function validateSelectFieldCodes(stmt, mode, client, cacheContext) {
36210
36247
  }
36211
36248
  }
36212
36249
  }
36250
+ function extractMainSafePushdown(stmt, fieldTypes) {
36251
+ if (stmt.where === null || stmt.from.subtableCode || stmt.from.cteName !== null) return null;
36252
+ if (stmt.joins.length === 0) {
36253
+ return extractSafePushdownLeaves(stmt.where, {
36254
+ tableAlias: stmt.from.alias ?? void 0,
36255
+ allowUnqualifiedFields: true,
36256
+ fieldTypes
36257
+ });
36258
+ }
36259
+ if (!stmt.from.alias) return null;
36260
+ return extractSafePushdownLeaves(stmt.where, { tableAlias: stmt.from.alias, fieldTypes });
36261
+ }
36262
+ function extractMainNumericPushdownCandidate(stmt) {
36263
+ if (stmt.where === null || stmt.from.subtableCode || stmt.from.cteName !== null) return null;
36264
+ if (stmt.joins.length === 0) {
36265
+ return extractNumericPushdownCandidates(stmt.where, {
36266
+ tableAlias: stmt.from.alias ?? void 0,
36267
+ allowUnqualifiedFields: true
36268
+ });
36269
+ }
36270
+ if (!stmt.from.alias) return null;
36271
+ return extractNumericPushdownCandidates(stmt.where, { tableAlias: stmt.from.alias });
36272
+ }
36273
+ async function loadNumericPushdownFieldTypes(stmt, client, cacheContext) {
36274
+ const appIds = /* @__PURE__ */ new Set();
36275
+ if (extractMainNumericPushdownCandidate(stmt) !== null) appIds.add(stmt.from.appId);
36276
+ if (stmt.where !== null) {
36277
+ for (const join of stmt.joins) {
36278
+ if (!join.table.alias || join.table.subtableCode || join.table.cteName !== null) continue;
36279
+ const candidate = extractNumericPushdownCandidates(stmt.where, {
36280
+ tableAlias: join.table.alias
36281
+ });
36282
+ if (candidate !== null) appIds.add(join.table.appId);
36283
+ }
36284
+ }
36285
+ const entries = await Promise.all([...appIds].map(async (appId) => [appId, await getFieldTypeMap(appId, client, cacheContext)]));
36286
+ return new Map(entries);
36287
+ }
36213
36288
  async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache) {
36214
36289
  const maxRecords2 = options.maxRecords ?? 1e4;
36215
36290
  const warnings = /* @__PURE__ */ new Set();
@@ -36218,20 +36293,22 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
36218
36293
  resolveSubqueries(stmt.where, client, options, cacheContext, cteCache),
36219
36294
  resolveSubqueries(stmt.having, client, options, cacheContext, cteCache)
36220
36295
  ]);
36296
+ const pushdownFieldTypes = await loadNumericPushdownFieldTypes(stmt, client, cacheContext);
36297
+ const mainPushDown = extractMainSafePushdown(
36298
+ stmt,
36299
+ pushdownFieldTypes.get(stmt.from.appId)
36300
+ );
36221
36301
  const tableConditions = /* @__PURE__ */ new Map();
36222
36302
  if (stmt.where !== null) {
36223
- if (stmt.from.alias) {
36224
- const cond = extractTableCondition(stmt.where, stmt.from.alias);
36225
- if (cond) tableConditions.set(stmt.from.alias, cond);
36226
- }
36227
36303
  for (const join of stmt.joins) {
36228
- if (join.table.alias) {
36229
- const cond = extractTableCondition(stmt.where, join.table.alias);
36230
- if (cond) tableConditions.set(join.table.alias, cond);
36231
- }
36304
+ if (!join.table.alias || join.table.subtableCode || join.table.cteName !== null) continue;
36305
+ const cond = extractSafePushdownLeaves(stmt.where, {
36306
+ tableAlias: join.table.alias,
36307
+ fieldTypes: pushdownFieldTypes.get(join.table.appId)
36308
+ });
36309
+ if (cond) tableConditions.set(join.table.alias, cond);
36232
36310
  }
36233
36311
  }
36234
- const mainPushDown = stmt.from.alias ? tableConditions.get(stmt.from.alias) ?? null : null;
36235
36312
  const mainFetch = fetchTableRecordsForFullScan(
36236
36313
  stmt,
36237
36314
  stmt.from,
@@ -37513,7 +37590,7 @@ function buildBatchExplainPlans(sql) {
37513
37590
  return {
37514
37591
  statementCount: statements.length,
37515
37592
  statements: statements.map((stmt, i) => {
37516
- const planStmt = stmt.type === "SET_VARIABLE" ? stmt : resolveVariableRefs(stmt, variables);
37593
+ const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr: resolveVariableRefs(stmt.expr, variables) } : stmt : resolveVariableRefs(stmt, variables);
37517
37594
  const result = {
37518
37595
  index: i,
37519
37596
  type: analysis.statements[i].statementType,
@@ -37542,6 +37619,15 @@ function buildBatchStatementPlan(stmt, info) {
37542
37619
  ];
37543
37620
  }
37544
37621
  if (stmt.type === "SET_VARIABLE") {
37622
+ if (stmt.expr.type === "SCALAR_SUBQUERY") {
37623
+ const subInfo = hasTempTableRef(stmt.expr.query) ? info : { ...info, tempTablesReferenced: [] };
37624
+ return [
37625
+ `SET @${stmt.name} = (SELECT ...)`,
37626
+ " 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",
37627
+ " subquery:",
37628
+ ...buildPlanForBatchQuery(stmt.expr.query, subInfo).map((l) => ` ${l}`)
37629
+ ];
37630
+ }
37545
37631
  return [
37546
37632
  `SET @${stmt.name} = <scalar expression>`,
37547
37633
  " 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"
@@ -37642,19 +37728,27 @@ function buildSelectPlan(stmt, label) {
37642
37728
  } else {
37643
37729
  const mainFields = selectToFetchAllFields(stmt, stmt.from);
37644
37730
  const mainAliasStr = stmt.from.alias ? ` AS ${stmt.from.alias}` : "";
37645
- const mainPushDown = stmt.from.alias && stmt.where ? extractTableCondition(stmt.where, stmt.from.alias) : null;
37731
+ const mainPushDown = extractMainSafePushdown(stmt);
37732
+ const mainCandidate = extractMainNumericPushdownCandidate(stmt);
37646
37733
  const mainQ = mainPushDown !== null ? whereToKintone(mainPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
37647
37734
  lines.push(` app: APP${stmt.from.appId}${mainAliasStr} (${stmt.from.appId})`);
37648
37735
  lines.push(` kintone query: ${mainQ}`);
37736
+ if (mainCandidate !== null) {
37737
+ lines.push(` pushdown candidate: ${whereToKintone(mainCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u78BA\u8A8D\u5F85\u3061\uFF09`);
37738
+ }
37649
37739
  lines.push(` fields: ${mainFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : mainFields.join(", ")}`);
37650
37740
  for (const join of stmt.joins) {
37651
37741
  const joinFields = selectToFetchAllFields(stmt, join.table);
37652
37742
  const joinAliasStr = join.table.alias ? ` AS ${join.table.alias}` : "";
37653
37743
  const joinType = join.type === "INNER" ? "JOIN" : `${join.type} JOIN`;
37654
- const joinPushDown = join.table.alias && stmt.where ? extractTableCondition(stmt.where, join.table.alias) : null;
37744
+ const joinPushDown = join.table.alias && !join.table.subtableCode && join.table.cteName === null && stmt.where ? extractSafePushdownLeaves(stmt.where, { tableAlias: join.table.alias }) : null;
37745
+ const joinCandidate = join.table.alias && !join.table.subtableCode && join.table.cteName === null && stmt.where ? extractNumericPushdownCandidates(stmt.where, { tableAlias: join.table.alias }) : null;
37655
37746
  const joinQ = joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
37656
37747
  lines.push(` ${joinType}: APP${join.table.appId}${joinAliasStr} (${join.table.appId})`);
37657
37748
  lines.push(` kintone query: ${joinQ}`);
37749
+ if (joinCandidate !== null) {
37750
+ lines.push(` pushdown candidate: ${whereToKintone(joinCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u78BA\u8A8D\u5F85\u3061\uFF09`);
37751
+ }
37658
37752
  lines.push(` fields: ${joinFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : joinFields.join(", ")}`);
37659
37753
  }
37660
37754
  }
@@ -39982,7 +40076,7 @@ Options:
39982
40076
  -h, --help Show help
39983
40077
  `);
39984
40078
  }
39985
- var SERVER_VERSION = true ? "2.1.2" : "0.0.0-dev";
40079
+ var SERVER_VERSION = true ? "2.3.0" : "0.0.0-dev";
39986
40080
  function createServer(args) {
39987
40081
  const server = new McpServer({
39988
40082
  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.1.2",
3
+ "version": "2.3.0",
4
4
  "description": "kintone SQL plugin, CLI, and MCP tools (ksql)",
5
5
  "publishConfig": {
6
6
  "access": "public"