@rex0220/kintone-sql-tools 2.1.2 → 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
@@ -2492,6 +2492,28 @@ function analyzeBatch(statements) {
2492
2492
  };
2493
2493
  }
2494
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
+
2495
2517
  // src/engine/pushDownNot.ts
2496
2518
  function pushDownNot(expr) {
2497
2519
  switch (expr.type) {
@@ -3466,24 +3488,7 @@ function evalOp(op, leftStr, right, row) {
3466
3488
  return !matchLike(leftStr, pattern);
3467
3489
  }
3468
3490
  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
- }
3491
+ return compareScalarValues(op, leftStr, rightStr);
3487
3492
  }
3488
3493
  function assertResolvedInListValues2(values) {
3489
3494
  const unresolved = values.find((item) => item.type === "VARIABLE");
@@ -4086,60 +4091,59 @@ async function resolveDmlTargetIds(getRecords, app, query, options) {
4086
4091
  }
4087
4092
 
4088
4093
  // src/core/optimization/wherePredicatePushdown.ts
4089
- 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) {
4090
4101
  switch (where.type) {
4091
4102
  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;
4103
+ return accept(where) ? where : null;
4099
4104
  case "LOGICAL":
4100
- if (where.op === "AND") {
4101
- const left = extractTableCondition(where.left, tableAlias);
4102
- 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);
4103
4109
  if (left && right) return { ...where, left, right };
4104
4110
  return left ?? right ?? null;
4105
4111
  }
4106
- return referencesOnlyTable(where, tableAlias) ? where : null;
4107
- case "NOT":
4108
4112
  case "GROUP":
4109
- return referencesOnlyTable(where, tableAlias) ? where : null;
4113
+ return extractAndLeaves(where.expr, accept);
4114
+ case "NULL_CHECK":
4115
+ case "NOT":
4110
4116
  case "EXISTS":
4111
4117
  return null;
4112
4118
  }
4113
4119
  }
4114
- function isSingleTableField(field, tableAlias) {
4115
- if (field.type !== "FIELD") return false;
4116
- 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";
4117
4124
  }
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
- }
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 === "<=";
4128
4129
  }
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
- }
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;
4143
4147
  }
4144
4148
 
4145
4149
  // src/engine/process.ts
@@ -5087,7 +5091,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
5087
5091
  }
5088
5092
  const low = await evalAssertOperand(stmt.low, client, options, cacheContext, tempTables);
5089
5093
  const high = await evalAssertOperand(stmt.high, client, options, cacheContext, tempTables);
5090
- if (!compareAssertValues(">=", left, low) || !compareAssertValues("<=", left, high)) {
5094
+ if (!compareScalarValues(">=", left, low) || !compareScalarValues("<=", left, high)) {
5091
5095
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
5092
5096
  }
5093
5097
  return { type: "ASSERT", condition: stmt.text };
@@ -5096,7 +5100,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
5096
5100
  throw new Error("ArgumentError: malformed ASSERT statement.");
5097
5101
  }
5098
5102
  const right = await evalAssertOperand(stmt.right, client, options, cacheContext, tempTables);
5099
- if (!compareAssertValues(stmt.op, left, right)) {
5103
+ if (!compareScalarValues(stmt.op, left, right)) {
5100
5104
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
5101
5105
  }
5102
5106
  return { type: "ASSERT", condition: stmt.text };
@@ -5156,26 +5160,6 @@ function evalAssertArith(node) {
5156
5160
  }
5157
5161
  throw new Error(`ArgumentError: unsupported operand in ASSERT expression: ${node.type}`);
5158
5162
  }
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
5163
  async function executeSelect(stmt, client, options, cacheContext, cteCache) {
5180
5164
  if (isNoFromSelect(stmt)) {
5181
5165
  return executeNoFromSelect(stmt);
@@ -5309,6 +5293,44 @@ async function validateSelectFieldCodes(stmt, mode, client, cacheContext) {
5309
5293
  }
5310
5294
  }
5311
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
+ }
5312
5334
  async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache) {
5313
5335
  const maxRecords = options.maxRecords ?? 1e4;
5314
5336
  const warnings = /* @__PURE__ */ new Set();
@@ -5317,20 +5339,22 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
5317
5339
  resolveSubqueries(stmt.where, client, options, cacheContext, cteCache),
5318
5340
  resolveSubqueries(stmt.having, client, options, cacheContext, cteCache)
5319
5341
  ]);
5342
+ const pushdownFieldTypes = await loadNumericPushdownFieldTypes(stmt, client, cacheContext);
5343
+ const mainPushDown = extractMainSafePushdown(
5344
+ stmt,
5345
+ pushdownFieldTypes.get(stmt.from.appId)
5346
+ );
5320
5347
  const tableConditions = /* @__PURE__ */ new Map();
5321
5348
  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
5349
  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
- }
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);
5331
5356
  }
5332
5357
  }
5333
- const mainPushDown = stmt.from.alias ? tableConditions.get(stmt.from.alias) ?? null : null;
5334
5358
  const mainFetch = fetchTableRecordsForFullScan(
5335
5359
  stmt,
5336
5360
  stmt.from,
@@ -6741,19 +6765,27 @@ function buildSelectPlan(stmt, label) {
6741
6765
  } else {
6742
6766
  const mainFields = selectToFetchAllFields(stmt, stmt.from);
6743
6767
  const mainAliasStr = stmt.from.alias ? ` AS ${stmt.from.alias}` : "";
6744
- const mainPushDown = stmt.from.alias && stmt.where ? extractTableCondition(stmt.where, stmt.from.alias) : null;
6768
+ const mainPushDown = extractMainSafePushdown(stmt);
6769
+ const mainCandidate = extractMainNumericPushdownCandidate(stmt);
6745
6770
  const mainQ = mainPushDown !== null ? whereToKintone(mainPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
6746
6771
  lines.push(` app: APP${stmt.from.appId}${mainAliasStr} (${stmt.from.appId})`);
6747
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
+ }
6748
6776
  lines.push(` fields: ${mainFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : mainFields.join(", ")}`);
6749
6777
  for (const join2 of stmt.joins) {
6750
6778
  const joinFields = selectToFetchAllFields(stmt, join2.table);
6751
6779
  const joinAliasStr = join2.table.alias ? ` AS ${join2.table.alias}` : "";
6752
6780
  const joinType = join2.type === "INNER" ? "JOIN" : `${join2.type} JOIN`;
6753
- 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;
6754
6783
  const joinQ = joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
6755
6784
  lines.push(` ${joinType}: APP${join2.table.appId}${joinAliasStr} (${join2.table.appId})`);
6756
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
+ }
6757
6789
  lines.push(` fields: ${joinFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : joinFields.join(", ")}`);
6758
6790
  }
6759
6791
  }
@@ -33393,6 +33393,28 @@ function analyzeBatch(statements) {
33393
33393
  };
33394
33394
  }
33395
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
+
33396
33418
  // src/engine/pushDownNot.ts
33397
33419
  function pushDownNot(expr) {
33398
33420
  switch (expr.type) {
@@ -34367,24 +34389,7 @@ function evalOp(op, leftStr, right, row) {
34367
34389
  return !matchLike(leftStr, pattern);
34368
34390
  }
34369
34391
  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
- }
34392
+ return compareScalarValues(op, leftStr, rightStr);
34388
34393
  }
34389
34394
  function assertResolvedInListValues2(values) {
34390
34395
  const unresolved = values.find((item) => item.type === "VARIABLE");
@@ -34987,60 +34992,59 @@ async function resolveDmlTargetIds(getRecords, app, query, options) {
34987
34992
  }
34988
34993
 
34989
34994
  // src/core/optimization/wherePredicatePushdown.ts
34990
- 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) {
34991
35002
  switch (where.type) {
34992
35003
  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;
35004
+ return accept(where) ? where : null;
35000
35005
  case "LOGICAL":
35001
- if (where.op === "AND") {
35002
- const left = extractTableCondition(where.left, tableAlias);
35003
- 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);
35004
35010
  if (left && right) return { ...where, left, right };
35005
35011
  return left ?? right ?? null;
35006
35012
  }
35007
- return referencesOnlyTable(where, tableAlias) ? where : null;
35008
- case "NOT":
35009
35013
  case "GROUP":
35010
- return referencesOnlyTable(where, tableAlias) ? where : null;
35014
+ return extractAndLeaves(where.expr, accept);
35015
+ case "NULL_CHECK":
35016
+ case "NOT":
35011
35017
  case "EXISTS":
35012
35018
  return null;
35013
35019
  }
35014
35020
  }
35015
- function isSingleTableField(field, tableAlias) {
35016
- if (field.type !== "FIELD") return false;
35017
- 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";
35018
35025
  }
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
- }
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 === "<=";
35029
35030
  }
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
- }
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;
35044
35048
  }
35045
35049
 
35046
35050
  // src/engine/process.ts
@@ -35988,7 +35992,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
35988
35992
  }
35989
35993
  const low = await evalAssertOperand(stmt.low, client, options, cacheContext, tempTables);
35990
35994
  const high = await evalAssertOperand(stmt.high, client, options, cacheContext, tempTables);
35991
- if (!compareAssertValues(">=", left, low) || !compareAssertValues("<=", left, high)) {
35995
+ if (!compareScalarValues(">=", left, low) || !compareScalarValues("<=", left, high)) {
35992
35996
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
35993
35997
  }
35994
35998
  return { type: "ASSERT", condition: stmt.text };
@@ -35997,7 +36001,7 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
35997
36001
  throw new Error("ArgumentError: malformed ASSERT statement.");
35998
36002
  }
35999
36003
  const right = await evalAssertOperand(stmt.right, client, options, cacheContext, tempTables);
36000
- if (!compareAssertValues(stmt.op, left, right)) {
36004
+ if (!compareScalarValues(stmt.op, left, right)) {
36001
36005
  throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
36002
36006
  }
36003
36007
  return { type: "ASSERT", condition: stmt.text };
@@ -36057,26 +36061,6 @@ function evalAssertArith(node) {
36057
36061
  }
36058
36062
  throw new Error(`ArgumentError: unsupported operand in ASSERT expression: ${node.type}`);
36059
36063
  }
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
36064
  async function executeSelect(stmt, client, options, cacheContext, cteCache) {
36081
36065
  if (isNoFromSelect(stmt)) {
36082
36066
  return executeNoFromSelect(stmt);
@@ -36210,6 +36194,44 @@ async function validateSelectFieldCodes(stmt, mode, client, cacheContext) {
36210
36194
  }
36211
36195
  }
36212
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
+ }
36213
36235
  async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache) {
36214
36236
  const maxRecords2 = options.maxRecords ?? 1e4;
36215
36237
  const warnings = /* @__PURE__ */ new Set();
@@ -36218,20 +36240,22 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
36218
36240
  resolveSubqueries(stmt.where, client, options, cacheContext, cteCache),
36219
36241
  resolveSubqueries(stmt.having, client, options, cacheContext, cteCache)
36220
36242
  ]);
36243
+ const pushdownFieldTypes = await loadNumericPushdownFieldTypes(stmt, client, cacheContext);
36244
+ const mainPushDown = extractMainSafePushdown(
36245
+ stmt,
36246
+ pushdownFieldTypes.get(stmt.from.appId)
36247
+ );
36221
36248
  const tableConditions = /* @__PURE__ */ new Map();
36222
36249
  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
36250
  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
- }
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);
36232
36257
  }
36233
36258
  }
36234
- const mainPushDown = stmt.from.alias ? tableConditions.get(stmt.from.alias) ?? null : null;
36235
36259
  const mainFetch = fetchTableRecordsForFullScan(
36236
36260
  stmt,
36237
36261
  stmt.from,
@@ -37642,19 +37666,27 @@ function buildSelectPlan(stmt, label) {
37642
37666
  } else {
37643
37667
  const mainFields = selectToFetchAllFields(stmt, stmt.from);
37644
37668
  const mainAliasStr = stmt.from.alias ? ` AS ${stmt.from.alias}` : "";
37645
- const mainPushDown = stmt.from.alias && stmt.where ? extractTableCondition(stmt.where, stmt.from.alias) : null;
37669
+ const mainPushDown = extractMainSafePushdown(stmt);
37670
+ const mainCandidate = extractMainNumericPushdownCandidate(stmt);
37646
37671
  const mainQ = mainPushDown !== null ? whereToKintone(mainPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
37647
37672
  lines.push(` app: APP${stmt.from.appId}${mainAliasStr} (${stmt.from.appId})`);
37648
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
+ }
37649
37677
  lines.push(` fields: ${mainFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : mainFields.join(", ")}`);
37650
37678
  for (const join of stmt.joins) {
37651
37679
  const joinFields = selectToFetchAllFields(stmt, join.table);
37652
37680
  const joinAliasStr = join.table.alias ? ` AS ${join.table.alias}` : "";
37653
37681
  const joinType = join.type === "INNER" ? "JOIN" : `${join.type} JOIN`;
37654
- 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;
37655
37684
  const joinQ = joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
37656
37685
  lines.push(` ${joinType}: APP${join.table.appId}${joinAliasStr} (${join.table.appId})`);
37657
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
+ }
37658
37690
  lines.push(` fields: ${joinFields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : joinFields.join(", ")}`);
37659
37691
  }
37660
37692
  }
@@ -39982,7 +40014,7 @@ Options:
39982
40014
  -h, --help Show help
39983
40015
  `);
39984
40016
  }
39985
- var SERVER_VERSION = true ? "2.1.2" : "0.0.0-dev";
40017
+ var SERVER_VERSION = true ? "2.2.0" : "0.0.0-dev";
39986
40018
  function createServer(args) {
39987
40019
  const server = new McpServer({
39988
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.2",
3
+ "version": "2.2.0",
4
4
  "description": "kintone SQL plugin, CLI, and MCP tools (ksql)",
5
5
  "publishConfig": {
6
6
  "access": "public"