@rex0220/kintone-sql-tools 2.1.1 → 2.2.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
@@ -1033,14 +1033,20 @@ var Parser = class {
1033
1033
  }
1034
1034
  const aggFunc = this.tryAggregateFunc();
1035
1035
  if (aggFunc !== null) {
1036
- const aggCol = this.parseAggregateColumn(aggFunc);
1036
+ const ref = this.parseAggregateRef(aggFunc);
1037
1037
  if (this.isArithOp(this.peek().kind)) {
1038
- const left = { type: "AGG_REF", func: aggCol.func, distinct: aggCol.distinct, arg: aggCol.arg };
1039
- const expr = this.continueAggArith(left);
1040
- const alias2 = this.consume("AS" /* AS */) ? this.parseAliasName() : null;
1041
- return { type: "ARITH_AGG_COL", expr, alias: alias2 };
1038
+ const expr = this.continueAggArith(ref);
1039
+ const alias3 = this.consume("AS" /* AS */) ? this.parseAliasName() : null;
1040
+ return { type: "ARITH_AGG_COL", expr, alias: alias3 };
1042
1041
  }
1043
- return aggCol;
1042
+ const alias2 = this.consume("AS" /* AS */) ? this.parseAliasName() : null;
1043
+ return {
1044
+ type: "AGGREGATE",
1045
+ func: ref.func,
1046
+ distinct: ref.distinct,
1047
+ arg: ref.arg,
1048
+ alias: alias2
1049
+ };
1044
1050
  }
1045
1051
  if (this.peek().kind === "(" /* LPAREN */ && this.peekAt(1).kind === "SELECT" /* SELECT */) {
1046
1052
  this.advance();
@@ -1124,8 +1130,7 @@ var Parser = class {
1124
1130
  }
1125
1131
  const aggFunc = this.tryAggregateFunc();
1126
1132
  if (aggFunc !== null) {
1127
- const aggCol = this.parseAggregateColumn(aggFunc);
1128
- return { type: "AGG_REF", func: aggCol.func, distinct: aggCol.distinct, arg: aggCol.arg };
1133
+ return this.parseAggregateRef(aggFunc);
1129
1134
  }
1130
1135
  throw new ParseError("\u96C6\u8A08\u7B97\u8853\u5F0F\u306B\u306F\u96C6\u8A08\u95A2\u6570\u307E\u305F\u306F\u6570\u5024\u304C\u5FC5\u8981\u3067\u3059", this.peek());
1131
1136
  }
@@ -1416,7 +1421,8 @@ var Parser = class {
1416
1421
  const kind = this.peek().kind;
1417
1422
  return map[kind] ?? null;
1418
1423
  }
1419
- parseAggregateColumn(func) {
1424
+ /** 集計関数参照を読む。SELECT 列の alias は呼び出し側で式全体の後に処理する。 */
1425
+ parseAggregateRef(func) {
1420
1426
  this.advance();
1421
1427
  this.expect("(" /* LPAREN */);
1422
1428
  const distinct = this.consume("DISTINCT" /* DISTINCT */);
@@ -1427,8 +1433,7 @@ var Parser = class {
1427
1433
  arg = this.parseArithAddSub();
1428
1434
  }
1429
1435
  this.expect(")" /* RPAREN */);
1430
- const alias = this.consume("AS" /* AS */) ? this.parseAliasName() : null;
1431
- return { type: "AGGREGATE", func, distinct, arg, alias };
1436
+ return { type: "AGG_REF", func, distinct, arg };
1432
1437
  }
1433
1438
  // ----------------------------------------------------------
1434
1439
  // FROM / JOIN
@@ -2487,6 +2492,28 @@ function analyzeBatch(statements) {
2487
2492
  };
2488
2493
  }
2489
2494
 
2495
+ // src/core/scalarCompare.ts
2496
+ function compareScalarValues(op, leftStr, rightStr) {
2497
+ if (op === "=") return leftStr === rightStr;
2498
+ if (op === "!=" || op === "<>") return leftStr !== rightStr;
2499
+ const rightNum = Number(rightStr);
2500
+ if (leftStr === "" && rightStr !== "" && Number.isFinite(rightNum)) {
2501
+ return op === "<" || op === "<=";
2502
+ }
2503
+ const leftNum = Number(leftStr);
2504
+ const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
2505
+ switch (op) {
2506
+ case ">":
2507
+ return numeric ? leftNum > rightNum : leftStr > rightStr;
2508
+ case "<":
2509
+ return numeric ? leftNum < rightNum : leftStr < rightStr;
2510
+ case ">=":
2511
+ return numeric ? leftNum >= rightNum : leftStr >= rightStr;
2512
+ case "<=":
2513
+ return numeric ? leftNum <= rightNum : leftStr <= rightStr;
2514
+ }
2515
+ }
2516
+
2490
2517
  // src/engine/pushDownNot.ts
2491
2518
  function pushDownNot(expr) {
2492
2519
  switch (expr.type) {
@@ -3461,24 +3488,7 @@ function evalOp(op, leftStr, right, row) {
3461
3488
  return !matchLike(leftStr, pattern);
3462
3489
  }
3463
3490
  const rightStr = resolveValue(right, row);
3464
- const leftNum = Number(leftStr);
3465
- const rightNum = Number(rightStr);
3466
- const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
3467
- switch (op) {
3468
- case "=":
3469
- return leftStr === rightStr;
3470
- case "!=":
3471
- case "<>":
3472
- return leftStr !== rightStr;
3473
- case ">":
3474
- return numeric ? leftNum > rightNum : leftStr > rightStr;
3475
- case "<":
3476
- return numeric ? leftNum < rightNum : leftStr < rightStr;
3477
- case ">=":
3478
- return numeric ? leftNum >= rightNum : leftStr >= rightStr;
3479
- case "<=":
3480
- return numeric ? leftNum <= rightNum : leftStr <= rightStr;
3481
- }
3491
+ return compareScalarValues(op, leftStr, rightStr);
3482
3492
  }
3483
3493
  function assertResolvedInListValues2(values) {
3484
3494
  const unresolved = values.find((item) => item.type === "VARIABLE");
@@ -4081,60 +4091,59 @@ async function resolveDmlTargetIds(getRecords, app, query, options) {
4081
4091
  }
4082
4092
 
4083
4093
  // src/core/optimization/wherePredicatePushdown.ts
4084
- function extractTableCondition(where, tableAlias) {
4094
+ function extractSafePushdownLeaves(where, options = {}) {
4095
+ return extractAndLeaves(where, (expr) => isSafeComparison(expr, options));
4096
+ }
4097
+ function extractNumericPushdownCandidates(where, options = {}) {
4098
+ return extractAndLeaves(where, (expr) => isNumericCandidate(expr, options));
4099
+ }
4100
+ function extractAndLeaves(where, accept) {
4085
4101
  switch (where.type) {
4086
4102
  case "BINARY":
4087
- if (isLike(where)) return null;
4088
- if (!isSingleTableField(where.left, tableAlias)) return null;
4089
- if (!isPushDownableRight(where.right)) return null;
4090
- return where;
4091
- case "NULL_CHECK":
4092
- if (!isSingleTableField(where.field, tableAlias)) return null;
4093
- return where;
4103
+ return accept(where) ? where : null;
4094
4104
  case "LOGICAL":
4095
- if (where.op === "AND") {
4096
- const left = extractTableCondition(where.left, tableAlias);
4097
- const right = extractTableCondition(where.right, tableAlias);
4105
+ if (where.op !== "AND") return null;
4106
+ {
4107
+ const left = extractAndLeaves(where.left, accept);
4108
+ const right = extractAndLeaves(where.right, accept);
4098
4109
  if (left && right) return { ...where, left, right };
4099
4110
  return left ?? right ?? null;
4100
4111
  }
4101
- return referencesOnlyTable(where, tableAlias) ? where : null;
4102
- case "NOT":
4103
4112
  case "GROUP":
4104
- return referencesOnlyTable(where, tableAlias) ? where : null;
4113
+ return extractAndLeaves(where.expr, accept);
4114
+ case "NULL_CHECK":
4115
+ case "NOT":
4105
4116
  case "EXISTS":
4106
4117
  return null;
4107
4118
  }
4108
4119
  }
4109
- function isSingleTableField(field, tableAlias) {
4110
- if (field.type !== "FIELD") return false;
4111
- return field.tableAlias === tableAlias;
4120
+ function isSafeComparison(expr, options) {
4121
+ if (isSafeIdComparison(expr, options)) return true;
4122
+ if (!isNumericCandidate(expr, options)) return false;
4123
+ return options.fieldTypes?.get(expr.left.field) === "NUMBER";
4112
4124
  }
4113
- function isPushDownableRight(value) {
4114
- switch (value.type) {
4115
- case "STRING":
4116
- case "NUMBER":
4117
- case "KINTONE_FUNC":
4118
- case "IN_LIST":
4119
- return true;
4120
- default:
4121
- return false;
4122
- }
4125
+ function isSafeIdComparison(expr, options) {
4126
+ if (!isTargetIdField(expr.left, options)) return false;
4127
+ if (expr.right.type !== "NUMBER") return false;
4128
+ return expr.op === "=" || expr.op === ">" || expr.op === "<" || expr.op === ">=" || expr.op === "<=";
4123
4129
  }
4124
- function referencesOnlyTable(expr, tableAlias) {
4125
- switch (expr.type) {
4126
- case "BINARY":
4127
- return !isLike(expr) && isSingleTableField(expr.left, tableAlias) && isPushDownableRight(expr.right);
4128
- case "NULL_CHECK":
4129
- return isSingleTableField(expr.field, tableAlias);
4130
- case "LOGICAL":
4131
- return referencesOnlyTable(expr.left, tableAlias) && referencesOnlyTable(expr.right, tableAlias);
4132
- case "NOT":
4133
- case "GROUP":
4134
- return referencesOnlyTable(expr.expr, tableAlias);
4135
- case "EXISTS":
4136
- return false;
4137
- }
4130
+ function isTargetIdField(field, options) {
4131
+ if (field.type !== "FIELD" || field.field !== "$id") return false;
4132
+ const targetAlias = options.tableAlias ?? null;
4133
+ if (field.tableAlias === targetAlias) return true;
4134
+ return options.allowUnqualifiedFields === true && field.tableAlias === null;
4135
+ }
4136
+ function isNumericCandidate(expr, options) {
4137
+ if (expr.left.type !== "FIELD" || expr.left.field === "$id") return false;
4138
+ if (!isTargetField(expr.left, options)) return false;
4139
+ if (expr.right.type !== "NUMBER") return false;
4140
+ if (expr.op === "=") return true;
4141
+ return (expr.op === "<" || expr.op === ">") && Number.isSafeInteger(expr.right.value);
4142
+ }
4143
+ function isTargetField(field, options) {
4144
+ const targetAlias = options.tableAlias ?? null;
4145
+ if (field.tableAlias === targetAlias) return true;
4146
+ return options.allowUnqualifiedFields === true && field.tableAlias === null;
4138
4147
  }
4139
4148
 
4140
4149
  // src/engine/process.ts
@@ -5082,7 +5091,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
5082
5091
  }
5083
5092
  const low = await evalAssertOperand(stmt.low, client, options, cacheContext, tempTables);
5084
5093
  const high = await evalAssertOperand(stmt.high, client, options, cacheContext, tempTables);
5085
- if (!compareAssertValues(">=", left, low) || !compareAssertValues("<=", left, high)) {
5094
+ if (!compareScalarValues(">=", left, low) || !compareScalarValues("<=", left, high)) {
5086
5095
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
5087
5096
  }
5088
5097
  return { type: "ASSERT", condition: stmt.text };
@@ -5091,7 +5100,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
5091
5100
  throw new Error("ArgumentError: malformed ASSERT statement.");
5092
5101
  }
5093
5102
  const right = await evalAssertOperand(stmt.right, client, options, cacheContext, tempTables);
5094
- if (!compareAssertValues(stmt.op, left, right)) {
5103
+ if (!compareScalarValues(stmt.op, left, right)) {
5095
5104
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
5096
5105
  }
5097
5106
  return { type: "ASSERT", condition: stmt.text };
@@ -5151,26 +5160,6 @@ function evalAssertArith(node) {
5151
5160
  }
5152
5161
  throw new Error(`ArgumentError: unsupported operand in ASSERT expression: ${node.type}`);
5153
5162
  }
5154
- function compareAssertValues(op, leftStr, rightStr) {
5155
- const leftNum = Number(leftStr);
5156
- const rightNum = Number(rightStr);
5157
- const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
5158
- switch (op) {
5159
- case "=":
5160
- return leftStr === rightStr;
5161
- case "!=":
5162
- case "<>":
5163
- return leftStr !== rightStr;
5164
- case ">":
5165
- return numeric ? leftNum > rightNum : leftStr > rightStr;
5166
- case "<":
5167
- return numeric ? leftNum < rightNum : leftStr < rightStr;
5168
- case ">=":
5169
- return numeric ? leftNum >= rightNum : leftStr >= rightStr;
5170
- case "<=":
5171
- return numeric ? leftNum <= rightNum : leftStr <= rightStr;
5172
- }
5173
- }
5174
5163
  async function executeSelect(stmt, client, options, cacheContext, cteCache) {
5175
5164
  if (isNoFromSelect(stmt)) {
5176
5165
  return executeNoFromSelect(stmt);
@@ -5304,6 +5293,44 @@ async function validateSelectFieldCodes(stmt, mode, client, cacheContext) {
5304
5293
  }
5305
5294
  }
5306
5295
  }
5296
+ function extractMainSafePushdown(stmt, fieldTypes) {
5297
+ if (stmt.where === null || stmt.from.subtableCode || stmt.from.cteName !== null) return null;
5298
+ if (stmt.joins.length === 0) {
5299
+ return extractSafePushdownLeaves(stmt.where, {
5300
+ tableAlias: stmt.from.alias ?? void 0,
5301
+ allowUnqualifiedFields: true,
5302
+ fieldTypes
5303
+ });
5304
+ }
5305
+ if (!stmt.from.alias) return null;
5306
+ return extractSafePushdownLeaves(stmt.where, { tableAlias: stmt.from.alias, fieldTypes });
5307
+ }
5308
+ function extractMainNumericPushdownCandidate(stmt) {
5309
+ if (stmt.where === null || stmt.from.subtableCode || stmt.from.cteName !== null) return null;
5310
+ if (stmt.joins.length === 0) {
5311
+ return extractNumericPushdownCandidates(stmt.where, {
5312
+ tableAlias: stmt.from.alias ?? void 0,
5313
+ allowUnqualifiedFields: true
5314
+ });
5315
+ }
5316
+ if (!stmt.from.alias) return null;
5317
+ return extractNumericPushdownCandidates(stmt.where, { tableAlias: stmt.from.alias });
5318
+ }
5319
+ async function loadNumericPushdownFieldTypes(stmt, client, cacheContext) {
5320
+ const appIds = /* @__PURE__ */ new Set();
5321
+ if (extractMainNumericPushdownCandidate(stmt) !== null) appIds.add(stmt.from.appId);
5322
+ if (stmt.where !== null) {
5323
+ for (const join2 of stmt.joins) {
5324
+ if (!join2.table.alias || join2.table.subtableCode || join2.table.cteName !== null) continue;
5325
+ const candidate = extractNumericPushdownCandidates(stmt.where, {
5326
+ tableAlias: join2.table.alias
5327
+ });
5328
+ if (candidate !== null) appIds.add(join2.table.appId);
5329
+ }
5330
+ }
5331
+ const entries = await Promise.all([...appIds].map(async (appId) => [appId, await getFieldTypeMap(appId, client, cacheContext)]));
5332
+ return new Map(entries);
5333
+ }
5307
5334
  async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache) {
5308
5335
  const maxRecords = options.maxRecords ?? 1e4;
5309
5336
  const warnings = /* @__PURE__ */ new Set();
@@ -5312,20 +5339,22 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
5312
5339
  resolveSubqueries(stmt.where, client, options, cacheContext, cteCache),
5313
5340
  resolveSubqueries(stmt.having, client, options, cacheContext, cteCache)
5314
5341
  ]);
5342
+ const pushdownFieldTypes = await loadNumericPushdownFieldTypes(stmt, client, cacheContext);
5343
+ const mainPushDown = extractMainSafePushdown(
5344
+ stmt,
5345
+ pushdownFieldTypes.get(stmt.from.appId)
5346
+ );
5315
5347
  const tableConditions = /* @__PURE__ */ new Map();
5316
5348
  if (stmt.where !== null) {
5317
- if (stmt.from.alias) {
5318
- const cond = extractTableCondition(stmt.where, stmt.from.alias);
5319
- if (cond) tableConditions.set(stmt.from.alias, cond);
5320
- }
5321
5349
  for (const join2 of stmt.joins) {
5322
- if (join2.table.alias) {
5323
- const cond = extractTableCondition(stmt.where, join2.table.alias);
5324
- if (cond) tableConditions.set(join2.table.alias, cond);
5325
- }
5350
+ if (!join2.table.alias || join2.table.subtableCode || join2.table.cteName !== null) continue;
5351
+ const cond = extractSafePushdownLeaves(stmt.where, {
5352
+ tableAlias: join2.table.alias,
5353
+ fieldTypes: pushdownFieldTypes.get(join2.table.appId)
5354
+ });
5355
+ if (cond) tableConditions.set(join2.table.alias, cond);
5326
5356
  }
5327
5357
  }
5328
- const mainPushDown = stmt.from.alias ? tableConditions.get(stmt.from.alias) ?? null : null;
5329
5358
  const mainFetch = fetchTableRecordsForFullScan(
5330
5359
  stmt,
5331
5360
  stmt.from,
@@ -6736,19 +6765,27 @@ function buildSelectPlan(stmt, label) {
6736
6765
  } else {
6737
6766
  const mainFields = selectToFetchAllFields(stmt, stmt.from);
6738
6767
  const mainAliasStr = stmt.from.alias ? ` AS ${stmt.from.alias}` : "";
6739
- const mainPushDown = stmt.from.alias && stmt.where ? extractTableCondition(stmt.where, stmt.from.alias) : null;
6768
+ const mainPushDown = extractMainSafePushdown(stmt);
6769
+ const mainCandidate = extractMainNumericPushdownCandidate(stmt);
6740
6770
  const mainQ = mainPushDown !== null ? whereToKintone(mainPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
6741
6771
  lines.push(` app: APP${stmt.from.appId}${mainAliasStr} (${stmt.from.appId})`);
6742
6772
  lines.push(` kintone query: ${mainQ}`);
6773
+ if (mainCandidate !== null) {
6774
+ lines.push(` pushdown candidate: ${whereToKintone(mainCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u78BA\u8A8D\u5F85\u3061\uFF09`);
6775
+ }
6743
6776
  lines.push(` fields: ${mainFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : mainFields.join(", ")}`);
6744
6777
  for (const join2 of stmt.joins) {
6745
6778
  const joinFields = selectToFetchAllFields(stmt, join2.table);
6746
6779
  const joinAliasStr = join2.table.alias ? ` AS ${join2.table.alias}` : "";
6747
6780
  const joinType = join2.type === "INNER" ? "JOIN" : `${join2.type} JOIN`;
6748
- const joinPushDown = join2.table.alias && stmt.where ? extractTableCondition(stmt.where, join2.table.alias) : null;
6781
+ const joinPushDown = join2.table.alias && !join2.table.subtableCode && join2.table.cteName === null && stmt.where ? extractSafePushdownLeaves(stmt.where, { tableAlias: join2.table.alias }) : null;
6782
+ const joinCandidate = join2.table.alias && !join2.table.subtableCode && join2.table.cteName === null && stmt.where ? extractNumericPushdownCandidates(stmt.where, { tableAlias: join2.table.alias }) : null;
6749
6783
  const joinQ = joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
6750
6784
  lines.push(` ${joinType}: APP${join2.table.appId}${joinAliasStr} (${join2.table.appId})`);
6751
6785
  lines.push(` kintone query: ${joinQ}`);
6786
+ if (joinCandidate !== null) {
6787
+ lines.push(` pushdown candidate: ${whereToKintone(joinCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u78BA\u8A8D\u5F85\u3061\uFF09`);
6788
+ }
6752
6789
  lines.push(` fields: ${joinFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : joinFields.join(", ")}`);
6753
6790
  }
6754
6791
  }
@@ -31946,14 +31946,20 @@ var Parser = class {
31946
31946
  }
31947
31947
  const aggFunc = this.tryAggregateFunc();
31948
31948
  if (aggFunc !== null) {
31949
- const aggCol = this.parseAggregateColumn(aggFunc);
31949
+ const ref = this.parseAggregateRef(aggFunc);
31950
31950
  if (this.isArithOp(this.peek().kind)) {
31951
- const left = { type: "AGG_REF", func: aggCol.func, distinct: aggCol.distinct, arg: aggCol.arg };
31952
- const expr = this.continueAggArith(left);
31953
- const alias2 = this.consume("AS" /* AS */) ? this.parseAliasName() : null;
31954
- return { type: "ARITH_AGG_COL", expr, alias: alias2 };
31951
+ const expr = this.continueAggArith(ref);
31952
+ const alias3 = this.consume("AS" /* AS */) ? this.parseAliasName() : null;
31953
+ return { type: "ARITH_AGG_COL", expr, alias: alias3 };
31955
31954
  }
31956
- return aggCol;
31955
+ const alias2 = this.consume("AS" /* AS */) ? this.parseAliasName() : null;
31956
+ return {
31957
+ type: "AGGREGATE",
31958
+ func: ref.func,
31959
+ distinct: ref.distinct,
31960
+ arg: ref.arg,
31961
+ alias: alias2
31962
+ };
31957
31963
  }
31958
31964
  if (this.peek().kind === "(" /* LPAREN */ && this.peekAt(1).kind === "SELECT" /* SELECT */) {
31959
31965
  this.advance();
@@ -32037,8 +32043,7 @@ var Parser = class {
32037
32043
  }
32038
32044
  const aggFunc = this.tryAggregateFunc();
32039
32045
  if (aggFunc !== null) {
32040
- const aggCol = this.parseAggregateColumn(aggFunc);
32041
- return { type: "AGG_REF", func: aggCol.func, distinct: aggCol.distinct, arg: aggCol.arg };
32046
+ return this.parseAggregateRef(aggFunc);
32042
32047
  }
32043
32048
  throw new ParseError("\u96C6\u8A08\u7B97\u8853\u5F0F\u306B\u306F\u96C6\u8A08\u95A2\u6570\u307E\u305F\u306F\u6570\u5024\u304C\u5FC5\u8981\u3067\u3059", this.peek());
32044
32049
  }
@@ -32329,7 +32334,8 @@ var Parser = class {
32329
32334
  const kind = this.peek().kind;
32330
32335
  return map2[kind] ?? null;
32331
32336
  }
32332
- parseAggregateColumn(func) {
32337
+ /** 集計関数参照を読む。SELECT 列の alias は呼び出し側で式全体の後に処理する。 */
32338
+ parseAggregateRef(func) {
32333
32339
  this.advance();
32334
32340
  this.expect("(" /* LPAREN */);
32335
32341
  const distinct = this.consume("DISTINCT" /* DISTINCT */);
@@ -32340,8 +32346,7 @@ var Parser = class {
32340
32346
  arg = this.parseArithAddSub();
32341
32347
  }
32342
32348
  this.expect(")" /* RPAREN */);
32343
- const alias = this.consume("AS" /* AS */) ? this.parseAliasName() : null;
32344
- return { type: "AGGREGATE", func, distinct, arg, alias };
32349
+ return { type: "AGG_REF", func, distinct, arg };
32345
32350
  }
32346
32351
  // ----------------------------------------------------------
32347
32352
  // FROM / JOIN
@@ -33388,6 +33393,28 @@ function analyzeBatch(statements) {
33388
33393
  };
33389
33394
  }
33390
33395
 
33396
+ // src/core/scalarCompare.ts
33397
+ function compareScalarValues(op, leftStr, rightStr) {
33398
+ if (op === "=") return leftStr === rightStr;
33399
+ if (op === "!=" || op === "<>") return leftStr !== rightStr;
33400
+ const rightNum = Number(rightStr);
33401
+ if (leftStr === "" && rightStr !== "" && Number.isFinite(rightNum)) {
33402
+ return op === "<" || op === "<=";
33403
+ }
33404
+ const leftNum = Number(leftStr);
33405
+ const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
33406
+ switch (op) {
33407
+ case ">":
33408
+ return numeric ? leftNum > rightNum : leftStr > rightStr;
33409
+ case "<":
33410
+ return numeric ? leftNum < rightNum : leftStr < rightStr;
33411
+ case ">=":
33412
+ return numeric ? leftNum >= rightNum : leftStr >= rightStr;
33413
+ case "<=":
33414
+ return numeric ? leftNum <= rightNum : leftStr <= rightStr;
33415
+ }
33416
+ }
33417
+
33391
33418
  // src/engine/pushDownNot.ts
33392
33419
  function pushDownNot(expr) {
33393
33420
  switch (expr.type) {
@@ -34362,24 +34389,7 @@ function evalOp(op, leftStr, right, row) {
34362
34389
  return !matchLike(leftStr, pattern);
34363
34390
  }
34364
34391
  const rightStr = resolveValue(right, row);
34365
- const leftNum = Number(leftStr);
34366
- const rightNum = Number(rightStr);
34367
- const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
34368
- switch (op) {
34369
- case "=":
34370
- return leftStr === rightStr;
34371
- case "!=":
34372
- case "<>":
34373
- return leftStr !== rightStr;
34374
- case ">":
34375
- return numeric ? leftNum > rightNum : leftStr > rightStr;
34376
- case "<":
34377
- return numeric ? leftNum < rightNum : leftStr < rightStr;
34378
- case ">=":
34379
- return numeric ? leftNum >= rightNum : leftStr >= rightStr;
34380
- case "<=":
34381
- return numeric ? leftNum <= rightNum : leftStr <= rightStr;
34382
- }
34392
+ return compareScalarValues(op, leftStr, rightStr);
34383
34393
  }
34384
34394
  function assertResolvedInListValues2(values) {
34385
34395
  const unresolved = values.find((item) => item.type === "VARIABLE");
@@ -34982,60 +34992,59 @@ async function resolveDmlTargetIds(getRecords, app, query, options) {
34982
34992
  }
34983
34993
 
34984
34994
  // src/core/optimization/wherePredicatePushdown.ts
34985
- function extractTableCondition(where, tableAlias) {
34995
+ function extractSafePushdownLeaves(where, options = {}) {
34996
+ return extractAndLeaves(where, (expr) => isSafeComparison(expr, options));
34997
+ }
34998
+ function extractNumericPushdownCandidates(where, options = {}) {
34999
+ return extractAndLeaves(where, (expr) => isNumericCandidate(expr, options));
35000
+ }
35001
+ function extractAndLeaves(where, accept) {
34986
35002
  switch (where.type) {
34987
35003
  case "BINARY":
34988
- if (isLike(where)) return null;
34989
- if (!isSingleTableField(where.left, tableAlias)) return null;
34990
- if (!isPushDownableRight(where.right)) return null;
34991
- return where;
34992
- case "NULL_CHECK":
34993
- if (!isSingleTableField(where.field, tableAlias)) return null;
34994
- return where;
35004
+ return accept(where) ? where : null;
34995
35005
  case "LOGICAL":
34996
- if (where.op === "AND") {
34997
- const left = extractTableCondition(where.left, tableAlias);
34998
- const right = extractTableCondition(where.right, tableAlias);
35006
+ if (where.op !== "AND") return null;
35007
+ {
35008
+ const left = extractAndLeaves(where.left, accept);
35009
+ const right = extractAndLeaves(where.right, accept);
34999
35010
  if (left && right) return { ...where, left, right };
35000
35011
  return left ?? right ?? null;
35001
35012
  }
35002
- return referencesOnlyTable(where, tableAlias) ? where : null;
35003
- case "NOT":
35004
35013
  case "GROUP":
35005
- return referencesOnlyTable(where, tableAlias) ? where : null;
35014
+ return extractAndLeaves(where.expr, accept);
35015
+ case "NULL_CHECK":
35016
+ case "NOT":
35006
35017
  case "EXISTS":
35007
35018
  return null;
35008
35019
  }
35009
35020
  }
35010
- function isSingleTableField(field, tableAlias) {
35011
- if (field.type !== "FIELD") return false;
35012
- return field.tableAlias === tableAlias;
35021
+ function isSafeComparison(expr, options) {
35022
+ if (isSafeIdComparison(expr, options)) return true;
35023
+ if (!isNumericCandidate(expr, options)) return false;
35024
+ return options.fieldTypes?.get(expr.left.field) === "NUMBER";
35013
35025
  }
35014
- function isPushDownableRight(value) {
35015
- switch (value.type) {
35016
- case "STRING":
35017
- case "NUMBER":
35018
- case "KINTONE_FUNC":
35019
- case "IN_LIST":
35020
- return true;
35021
- default:
35022
- return false;
35023
- }
35026
+ function isSafeIdComparison(expr, options) {
35027
+ if (!isTargetIdField(expr.left, options)) return false;
35028
+ if (expr.right.type !== "NUMBER") return false;
35029
+ return expr.op === "=" || expr.op === ">" || expr.op === "<" || expr.op === ">=" || expr.op === "<=";
35024
35030
  }
35025
- function referencesOnlyTable(expr, tableAlias) {
35026
- switch (expr.type) {
35027
- case "BINARY":
35028
- return !isLike(expr) && isSingleTableField(expr.left, tableAlias) && isPushDownableRight(expr.right);
35029
- case "NULL_CHECK":
35030
- return isSingleTableField(expr.field, tableAlias);
35031
- case "LOGICAL":
35032
- return referencesOnlyTable(expr.left, tableAlias) && referencesOnlyTable(expr.right, tableAlias);
35033
- case "NOT":
35034
- case "GROUP":
35035
- return referencesOnlyTable(expr.expr, tableAlias);
35036
- case "EXISTS":
35037
- return false;
35038
- }
35031
+ function isTargetIdField(field, options) {
35032
+ if (field.type !== "FIELD" || field.field !== "$id") return false;
35033
+ const targetAlias = options.tableAlias ?? null;
35034
+ if (field.tableAlias === targetAlias) return true;
35035
+ return options.allowUnqualifiedFields === true && field.tableAlias === null;
35036
+ }
35037
+ function isNumericCandidate(expr, options) {
35038
+ if (expr.left.type !== "FIELD" || expr.left.field === "$id") return false;
35039
+ if (!isTargetField(expr.left, options)) return false;
35040
+ if (expr.right.type !== "NUMBER") return false;
35041
+ if (expr.op === "=") return true;
35042
+ return (expr.op === "<" || expr.op === ">") && Number.isSafeInteger(expr.right.value);
35043
+ }
35044
+ function isTargetField(field, options) {
35045
+ const targetAlias = options.tableAlias ?? null;
35046
+ if (field.tableAlias === targetAlias) return true;
35047
+ return options.allowUnqualifiedFields === true && field.tableAlias === null;
35039
35048
  }
35040
35049
 
35041
35050
  // src/engine/process.ts
@@ -35983,7 +35992,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
35983
35992
  }
35984
35993
  const low = await evalAssertOperand(stmt.low, client, options, cacheContext, tempTables);
35985
35994
  const high = await evalAssertOperand(stmt.high, client, options, cacheContext, tempTables);
35986
- if (!compareAssertValues(">=", left, low) || !compareAssertValues("<=", left, high)) {
35995
+ if (!compareScalarValues(">=", left, low) || !compareScalarValues("<=", left, high)) {
35987
35996
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
35988
35997
  }
35989
35998
  return { type: "ASSERT", condition: stmt.text };
@@ -35992,7 +36001,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
35992
36001
  throw new Error("ArgumentError: malformed ASSERT statement.");
35993
36002
  }
35994
36003
  const right = await evalAssertOperand(stmt.right, client, options, cacheContext, tempTables);
35995
- if (!compareAssertValues(stmt.op, left, right)) {
36004
+ if (!compareScalarValues(stmt.op, left, right)) {
35996
36005
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
35997
36006
  }
35998
36007
  return { type: "ASSERT", condition: stmt.text };
@@ -36052,26 +36061,6 @@ function evalAssertArith(node) {
36052
36061
  }
36053
36062
  throw new Error(`ArgumentError: unsupported operand in ASSERT expression: ${node.type}`);
36054
36063
  }
36055
- function compareAssertValues(op, leftStr, rightStr) {
36056
- const leftNum = Number(leftStr);
36057
- const rightNum = Number(rightStr);
36058
- const numeric = !Number.isNaN(leftNum) && !Number.isNaN(rightNum);
36059
- switch (op) {
36060
- case "=":
36061
- return leftStr === rightStr;
36062
- case "!=":
36063
- case "<>":
36064
- return leftStr !== rightStr;
36065
- case ">":
36066
- return numeric ? leftNum > rightNum : leftStr > rightStr;
36067
- case "<":
36068
- return numeric ? leftNum < rightNum : leftStr < rightStr;
36069
- case ">=":
36070
- return numeric ? leftNum >= rightNum : leftStr >= rightStr;
36071
- case "<=":
36072
- return numeric ? leftNum <= rightNum : leftStr <= rightStr;
36073
- }
36074
- }
36075
36064
  async function executeSelect(stmt, client, options, cacheContext, cteCache) {
36076
36065
  if (isNoFromSelect(stmt)) {
36077
36066
  return executeNoFromSelect(stmt);
@@ -36205,6 +36194,44 @@ async function validateSelectFieldCodes(stmt, mode, client, cacheContext) {
36205
36194
  }
36206
36195
  }
36207
36196
  }
36197
+ function extractMainSafePushdown(stmt, fieldTypes) {
36198
+ if (stmt.where === null || stmt.from.subtableCode || stmt.from.cteName !== null) return null;
36199
+ if (stmt.joins.length === 0) {
36200
+ return extractSafePushdownLeaves(stmt.where, {
36201
+ tableAlias: stmt.from.alias ?? void 0,
36202
+ allowUnqualifiedFields: true,
36203
+ fieldTypes
36204
+ });
36205
+ }
36206
+ if (!stmt.from.alias) return null;
36207
+ return extractSafePushdownLeaves(stmt.where, { tableAlias: stmt.from.alias, fieldTypes });
36208
+ }
36209
+ function extractMainNumericPushdownCandidate(stmt) {
36210
+ if (stmt.where === null || stmt.from.subtableCode || stmt.from.cteName !== null) return null;
36211
+ if (stmt.joins.length === 0) {
36212
+ return extractNumericPushdownCandidates(stmt.where, {
36213
+ tableAlias: stmt.from.alias ?? void 0,
36214
+ allowUnqualifiedFields: true
36215
+ });
36216
+ }
36217
+ if (!stmt.from.alias) return null;
36218
+ return extractNumericPushdownCandidates(stmt.where, { tableAlias: stmt.from.alias });
36219
+ }
36220
+ async function loadNumericPushdownFieldTypes(stmt, client, cacheContext) {
36221
+ const appIds = /* @__PURE__ */ new Set();
36222
+ if (extractMainNumericPushdownCandidate(stmt) !== null) appIds.add(stmt.from.appId);
36223
+ if (stmt.where !== null) {
36224
+ for (const join of stmt.joins) {
36225
+ if (!join.table.alias || join.table.subtableCode || join.table.cteName !== null) continue;
36226
+ const candidate = extractNumericPushdownCandidates(stmt.where, {
36227
+ tableAlias: join.table.alias
36228
+ });
36229
+ if (candidate !== null) appIds.add(join.table.appId);
36230
+ }
36231
+ }
36232
+ const entries = await Promise.all([...appIds].map(async (appId) => [appId, await getFieldTypeMap(appId, client, cacheContext)]));
36233
+ return new Map(entries);
36234
+ }
36208
36235
  async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache) {
36209
36236
  const maxRecords2 = options.maxRecords ?? 1e4;
36210
36237
  const warnings = /* @__PURE__ */ new Set();
@@ -36213,20 +36240,22 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
36213
36240
  resolveSubqueries(stmt.where, client, options, cacheContext, cteCache),
36214
36241
  resolveSubqueries(stmt.having, client, options, cacheContext, cteCache)
36215
36242
  ]);
36243
+ const pushdownFieldTypes = await loadNumericPushdownFieldTypes(stmt, client, cacheContext);
36244
+ const mainPushDown = extractMainSafePushdown(
36245
+ stmt,
36246
+ pushdownFieldTypes.get(stmt.from.appId)
36247
+ );
36216
36248
  const tableConditions = /* @__PURE__ */ new Map();
36217
36249
  if (stmt.where !== null) {
36218
- if (stmt.from.alias) {
36219
- const cond = extractTableCondition(stmt.where, stmt.from.alias);
36220
- if (cond) tableConditions.set(stmt.from.alias, cond);
36221
- }
36222
36250
  for (const join of stmt.joins) {
36223
- if (join.table.alias) {
36224
- const cond = extractTableCondition(stmt.where, join.table.alias);
36225
- if (cond) tableConditions.set(join.table.alias, cond);
36226
- }
36251
+ if (!join.table.alias || join.table.subtableCode || join.table.cteName !== null) continue;
36252
+ const cond = extractSafePushdownLeaves(stmt.where, {
36253
+ tableAlias: join.table.alias,
36254
+ fieldTypes: pushdownFieldTypes.get(join.table.appId)
36255
+ });
36256
+ if (cond) tableConditions.set(join.table.alias, cond);
36227
36257
  }
36228
36258
  }
36229
- const mainPushDown = stmt.from.alias ? tableConditions.get(stmt.from.alias) ?? null : null;
36230
36259
  const mainFetch = fetchTableRecordsForFullScan(
36231
36260
  stmt,
36232
36261
  stmt.from,
@@ -37637,19 +37666,27 @@ function buildSelectPlan(stmt, label) {
37637
37666
  } else {
37638
37667
  const mainFields = selectToFetchAllFields(stmt, stmt.from);
37639
37668
  const mainAliasStr = stmt.from.alias ? ` AS ${stmt.from.alias}` : "";
37640
- const mainPushDown = stmt.from.alias && stmt.where ? extractTableCondition(stmt.where, stmt.from.alias) : null;
37669
+ const mainPushDown = extractMainSafePushdown(stmt);
37670
+ const mainCandidate = extractMainNumericPushdownCandidate(stmt);
37641
37671
  const mainQ = mainPushDown !== null ? whereToKintone(mainPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
37642
37672
  lines.push(` app: APP${stmt.from.appId}${mainAliasStr} (${stmt.from.appId})`);
37643
37673
  lines.push(` kintone query: ${mainQ}`);
37674
+ if (mainCandidate !== null) {
37675
+ lines.push(` pushdown candidate: ${whereToKintone(mainCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u78BA\u8A8D\u5F85\u3061\uFF09`);
37676
+ }
37644
37677
  lines.push(` fields: ${mainFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : mainFields.join(", ")}`);
37645
37678
  for (const join of stmt.joins) {
37646
37679
  const joinFields = selectToFetchAllFields(stmt, join.table);
37647
37680
  const joinAliasStr = join.table.alias ? ` AS ${join.table.alias}` : "";
37648
37681
  const joinType = join.type === "INNER" ? "JOIN" : `${join.type} JOIN`;
37649
- const joinPushDown = join.table.alias && stmt.where ? extractTableCondition(stmt.where, join.table.alias) : null;
37682
+ const joinPushDown = join.table.alias && !join.table.subtableCode && join.table.cteName === null && stmt.where ? extractSafePushdownLeaves(stmt.where, { tableAlias: join.table.alias }) : null;
37683
+ const joinCandidate = join.table.alias && !join.table.subtableCode && join.table.cteName === null && stmt.where ? extractNumericPushdownCandidates(stmt.where, { tableAlias: join.table.alias }) : null;
37650
37684
  const joinQ = joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
37651
37685
  lines.push(` ${joinType}: APP${join.table.appId}${joinAliasStr} (${join.table.appId})`);
37652
37686
  lines.push(` kintone query: ${joinQ}`);
37687
+ if (joinCandidate !== null) {
37688
+ lines.push(` pushdown candidate: ${whereToKintone(joinCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u78BA\u8A8D\u5F85\u3061\uFF09`);
37689
+ }
37653
37690
  lines.push(` fields: ${joinFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : joinFields.join(", ")}`);
37654
37691
  }
37655
37692
  }
@@ -39977,7 +40014,7 @@ Options:
39977
40014
  -h, --help Show help
39978
40015
  `);
39979
40016
  }
39980
- var SERVER_VERSION = true ? "2.1.1" : "0.0.0-dev";
40017
+ var SERVER_VERSION = true ? "2.2.0" : "0.0.0-dev";
39981
40018
  function createServer(args) {
39982
40019
  const server = new McpServer({
39983
40020
  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.1",
3
+ "version": "2.2.0",
4
4
  "description": "kintone SQL plugin, CLI, and MCP tools (ksql)",
5
5
  "publishConfig": {
6
6
  "access": "public"