@rex0220/kintone-sql-tools 2.8.0 → 2.9.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
@@ -3116,6 +3116,235 @@ function isAggregateSyntheticName(name) {
3116
3116
  return /^(COUNT|SUM|AVG|MAX|MIN)\(/i.test(name);
3117
3117
  }
3118
3118
 
3119
+ // src/core/cteInlining.ts
3120
+ function canInlineSingleCte(stmt) {
3121
+ if (stmt.ctes.length !== 1) return false;
3122
+ const cteDef = stmt.ctes[0];
3123
+ if (cteDef.query.type !== "SELECT" || resolveSelectMode(cteDef.query) !== "SIMPLE") return false;
3124
+ const finalQuery = stmt.query;
3125
+ if (finalQuery.type !== "SELECT") return false;
3126
+ if (finalQuery.from.cteName !== cteDef.name || finalQuery.joins.length > 0) return false;
3127
+ if (finalQuery.groupBy.length > 0 || finalQuery.distinct) return false;
3128
+ return !finalQuery.columns.some(
3129
+ (column) => column.type === "AGGREGATE" || column.type === "ARITH_AGG_COL"
3130
+ );
3131
+ }
3132
+ function buildInlinedQuery(stmt) {
3133
+ const cteBody = stmt.ctes[0].query;
3134
+ const final = stmt.query;
3135
+ const finalWhere = stripCteAlias(final.where, final.from.alias);
3136
+ const where = cteBody.where === null ? finalWhere : finalWhere === null ? cteBody.where : { type: "LOGICAL", op: "AND", left: cteBody.where, right: finalWhere };
3137
+ const columns = final.columns.every((column) => column.type === "WILDCARD") ? cteBody.columns : final.columns;
3138
+ return {
3139
+ type: "SELECT",
3140
+ from: cteBody.from,
3141
+ joins: [],
3142
+ columns,
3143
+ where,
3144
+ groupBy: [],
3145
+ having: null,
3146
+ orderBy: final.orderBy.length > 0 ? final.orderBy : cteBody.orderBy,
3147
+ limit: final.limit ?? cteBody.limit,
3148
+ offset: final.offset ?? cteBody.offset,
3149
+ distinct: false
3150
+ };
3151
+ }
3152
+ function stripCteAlias(where, alias) {
3153
+ if (where === null || alias === null) return where;
3154
+ switch (where.type) {
3155
+ case "BINARY":
3156
+ return { ...where, left: stripCteAliasFromFieldValue(where.left, alias) };
3157
+ case "NULL_CHECK":
3158
+ return { ...where, field: stripCteAliasFromFieldValue(where.field, alias) };
3159
+ case "LOGICAL":
3160
+ return {
3161
+ ...where,
3162
+ left: stripCteAlias(where.left, alias),
3163
+ right: stripCteAlias(where.right, alias)
3164
+ };
3165
+ case "NOT":
3166
+ case "GROUP":
3167
+ return { ...where, expr: stripCteAlias(where.expr, alias) };
3168
+ case "EXISTS":
3169
+ return where;
3170
+ }
3171
+ }
3172
+ function stripCteAliasFromFieldValue(value, alias) {
3173
+ if (value.type === "FIELD" && value.tableAlias === alias) {
3174
+ return { ...value, tableAlias: null };
3175
+ }
3176
+ return value;
3177
+ }
3178
+
3179
+ // src/core/optimization/wherePredicatePushdown.ts
3180
+ function extractSafePushdownLeaves(where, options = {}) {
3181
+ return extractAndLeaves(where, (expr) => isSafeComparison(expr, options));
3182
+ }
3183
+ function extractTypedPushdownCandidates(where, options = {}) {
3184
+ return extractAndLeaves(
3185
+ where,
3186
+ (expr) => isNumericCandidate(expr, options) || isSelectionInCandidate(expr, options)
3187
+ );
3188
+ }
3189
+ function extractAndLeaves(where, accept) {
3190
+ switch (where.type) {
3191
+ case "BINARY":
3192
+ return accept(where) ? where : null;
3193
+ case "LOGICAL":
3194
+ if (where.op !== "AND") return null;
3195
+ {
3196
+ const left = extractAndLeaves(where.left, accept);
3197
+ const right = extractAndLeaves(where.right, accept);
3198
+ if (left && right) return { ...where, left, right };
3199
+ return left ?? right ?? null;
3200
+ }
3201
+ case "GROUP":
3202
+ return extractAndLeaves(where.expr, accept);
3203
+ case "NULL_CHECK":
3204
+ case "NOT":
3205
+ case "EXISTS":
3206
+ return null;
3207
+ }
3208
+ }
3209
+ function isSafeComparison(expr, options) {
3210
+ if (isKlikeComparison(expr, options)) return true;
3211
+ if (isSafeIdComparison(expr, options)) return true;
3212
+ if (isNumericCandidate(expr, options)) {
3213
+ return options.fieldTypes?.get(expr.left.field) === "NUMBER";
3214
+ }
3215
+ return isSelectionInComparison(expr, options);
3216
+ }
3217
+ function isKlikeComparison(expr, options) {
3218
+ if (options.allowKlike === false) return false;
3219
+ if (expr.op !== "KLIKE" && expr.op !== "NOT_KLIKE") return false;
3220
+ if (expr.left.type !== "FIELD" || !isTargetField(expr.left, options)) return false;
3221
+ return expr.right.type === "STRING" || options.allowUnresolvedKlikeVariables === true && expr.right.type === "VARIABLE";
3222
+ }
3223
+ var SELECTION_IN_FIELD_TYPES = /* @__PURE__ */ new Set([
3224
+ "DROP_DOWN",
3225
+ "RADIO_BUTTON",
3226
+ "CHECK_BOX",
3227
+ "MULTI_SELECT",
3228
+ "STATUS"
3229
+ ]);
3230
+ function isSelectionInComparison(expr, options) {
3231
+ if (!isSelectionInCandidate(expr, options)) return false;
3232
+ if (expr.left.type !== "FIELD" || expr.right.type !== "IN_LIST") return false;
3233
+ const fieldType = options.fieldTypes?.get(expr.left.field);
3234
+ if (fieldType === void 0 || !SELECTION_IN_FIELD_TYPES.has(fieldType)) return false;
3235
+ const validOptions = options.fieldOptions?.get(expr.left.field);
3236
+ if (validOptions === void 0) return false;
3237
+ return expr.right.values.every(
3238
+ (value) => value.type === "STRING" && value.value !== "" && validOptions.has(value.value)
3239
+ );
3240
+ }
3241
+ function isSafeIdComparison(expr, options) {
3242
+ if (!isTargetIdField(expr.left, options)) return false;
3243
+ if (expr.right.type !== "NUMBER") return false;
3244
+ return expr.op === "=" || expr.op === ">" || expr.op === "<" || expr.op === ">=" || expr.op === "<=";
3245
+ }
3246
+ function isTargetIdField(field, options) {
3247
+ if (field.type !== "FIELD" || field.field !== "$id") return false;
3248
+ const targetAlias = options.tableAlias ?? null;
3249
+ if (field.tableAlias === targetAlias) return true;
3250
+ return options.allowUnqualifiedFields === true && field.tableAlias === null;
3251
+ }
3252
+ function isNumericCandidate(expr, options) {
3253
+ if (expr.left.type !== "FIELD" || expr.left.field === "$id") return false;
3254
+ if (!isTargetField(expr.left, options)) return false;
3255
+ if (expr.right.type !== "NUMBER") return false;
3256
+ if (expr.op === "=") return true;
3257
+ return (expr.op === "<" || expr.op === ">") && Number.isSafeInteger(expr.right.value);
3258
+ }
3259
+ function isSelectionInCandidate(expr, options) {
3260
+ if (expr.left.type !== "FIELD" || expr.left.field === "$id") return false;
3261
+ if (!isTargetField(expr.left, options)) return false;
3262
+ if (expr.op !== "IN" && expr.op !== "NOT_IN") return false;
3263
+ if (expr.right.type !== "IN_LIST" || expr.right.values.length === 0) return false;
3264
+ return expr.right.values.every((value) => value.type === "STRING" && value.value !== "");
3265
+ }
3266
+ function isTargetField(field, options) {
3267
+ const targetAlias = options.tableAlias ?? null;
3268
+ if (field.tableAlias === targetAlias) return true;
3269
+ return options.allowUnqualifiedFields === true && field.tableAlias === null;
3270
+ }
3271
+
3272
+ // src/core/optimization/klikePushdownPlan.ts
3273
+ function buildKlikePushdownPlan(stmt, options = {}) {
3274
+ const joinsAreSafeForKlike = stmt.joins.every((join2) => join2.type === "INNER");
3275
+ const common = {
3276
+ allowKlike: joinsAreSafeForKlike,
3277
+ allowUnresolvedKlikeVariables: options.allowUnresolvedVariables
3278
+ };
3279
+ let mainCondition = null;
3280
+ if (stmt.where !== null && !stmt.from.subtableCode && stmt.from.cteName === null) {
3281
+ if (stmt.joins.length === 0) {
3282
+ mainCondition = extractSafePushdownLeaves(stmt.where, {
3283
+ ...common,
3284
+ tableAlias: stmt.from.alias ?? void 0,
3285
+ allowUnqualifiedFields: true,
3286
+ fieldTypes: options.fieldTypesByApp?.get(stmt.from.appId),
3287
+ fieldOptions: options.fieldOptionsByApp?.get(stmt.from.appId)
3288
+ });
3289
+ } else if (stmt.from.alias) {
3290
+ mainCondition = extractSafePushdownLeaves(stmt.where, {
3291
+ ...common,
3292
+ tableAlias: stmt.from.alias,
3293
+ fieldTypes: options.fieldTypesByApp?.get(stmt.from.appId),
3294
+ fieldOptions: options.fieldOptionsByApp?.get(stmt.from.appId)
3295
+ });
3296
+ }
3297
+ }
3298
+ const joinConditions = /* @__PURE__ */ new Map();
3299
+ if (stmt.where !== null) {
3300
+ for (const join2 of stmt.joins) {
3301
+ if (!join2.table.alias || join2.table.subtableCode || join2.table.cteName !== null) continue;
3302
+ const condition = extractSafePushdownLeaves(stmt.where, {
3303
+ ...common,
3304
+ tableAlias: join2.table.alias,
3305
+ fieldTypes: options.fieldTypesByApp?.get(join2.table.appId),
3306
+ fieldOptions: options.fieldOptionsByApp?.get(join2.table.appId)
3307
+ });
3308
+ if (condition !== null) joinConditions.set(join2.table.alias, condition);
3309
+ }
3310
+ }
3311
+ const appliedKlikes = /* @__PURE__ */ new Set();
3312
+ collectKlikes(mainCondition, appliedKlikes);
3313
+ for (const condition of joinConditions.values()) collectKlikes(condition, appliedKlikes);
3314
+ const allKlikes = /* @__PURE__ */ new Set();
3315
+ collectKlikes(stmt.where, allKlikes);
3316
+ return {
3317
+ mainCondition,
3318
+ joinConditions,
3319
+ appliedKlikes,
3320
+ allKlikes: [...allKlikes]
3321
+ };
3322
+ }
3323
+ function unappliedKlikes(plan) {
3324
+ return plan.allKlikes.filter((expr) => !plan.appliedKlikes.has(expr));
3325
+ }
3326
+ function collectKlikes(where, out) {
3327
+ if (where === null) return;
3328
+ if (isKlike(where)) {
3329
+ out.add(where);
3330
+ return;
3331
+ }
3332
+ switch (where.type) {
3333
+ case "LOGICAL":
3334
+ collectKlikes(where.left, out);
3335
+ collectKlikes(where.right, out);
3336
+ return;
3337
+ case "NOT":
3338
+ case "GROUP":
3339
+ collectKlikes(where.expr, out);
3340
+ return;
3341
+ case "BINARY":
3342
+ case "NULL_CHECK":
3343
+ case "EXISTS":
3344
+ return;
3345
+ }
3346
+ }
3347
+
3119
3348
  // src/core/klikeValidation.ts
3120
3349
  var KlikeValidationError = class extends Error {
3121
3350
  constructor(message) {
@@ -3126,6 +3355,13 @@ var KlikeValidationError = class extends Error {
3126
3355
  function validateKlikeStatement(stmt) {
3127
3356
  validateStatement(stmt);
3128
3357
  }
3358
+ function validateKlikePushdownPlan(plan) {
3359
+ if (unappliedKlikes(plan).length > 0) {
3360
+ throw new KlikeValidationError(
3361
+ "FULL_SCAN \u306E KLIKE / NOT KLIKE \u3092\u5B89\u5168\u306B\u62BC\u3057\u4E0B\u3052\u3089\u308C\u307E\u305B\u3093\u3002OR / NOT \u914D\u4E0B\u3001CTE\u30FB\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u3001LEFT / RIGHT JOIN \u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044"
3362
+ );
3363
+ }
3364
+ }
3129
3365
  function validateStatement(stmt) {
3130
3366
  switch (stmt.type) {
3131
3367
  case "SELECT":
@@ -3157,7 +3393,7 @@ function validateStatement(stmt) {
3157
3393
  case "REORDER":
3158
3394
  if (containsKlike(stmt)) {
3159
3395
  throw new KlikeValidationError(
3160
- "KLIKE / NOT KLIKE \u306F v1 \u3067\u306F\u5168 DML\uFF08UPDATE / DELETE / INSERT / UPSERT / REORDER\uFF09\u3067\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093"
3396
+ "KLIKE / NOT KLIKE \u306F\u5168 DML\uFF08UPDATE / DELETE / INSERT / UPSERT / REORDER\uFF09\u3067\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093"
3161
3397
  );
3162
3398
  }
3163
3399
  return;
@@ -3177,9 +3413,8 @@ function validateUnion(stmt) {
3177
3413
  validateSelect(stmt.right);
3178
3414
  }
3179
3415
  function validateWith(stmt) {
3180
- const inlined = buildEffectiveInlineSelect(stmt);
3181
- if (inlined !== null) {
3182
- validateSelect(inlined);
3416
+ if (canInlineSingleCte(stmt)) {
3417
+ validateSelect(buildInlinedQuery(stmt));
3183
3418
  return;
3184
3419
  }
3185
3420
  for (const cte of stmt.ctes) {
@@ -3191,10 +3426,15 @@ function validateWith(stmt) {
3191
3426
  function validateSelect(stmt) {
3192
3427
  validateOwnKlikeExpressions(stmt);
3193
3428
  if (whereHasKlike(stmt.where)) {
3194
- const inMemorySource = stmt.from.cteName !== null || stmt.joins.some((join2) => join2.table.cteName !== null);
3195
- if (inMemorySource || resolveSelectMode(stmt) === "FULL_SCAN") {
3429
+ const directKintoneSimple = resolveSelectMode(stmt) === "SIMPLE" && stmt.from.cteName === null && stmt.joins.every((join2) => join2.table.cteName === null);
3430
+ if (!directKintoneSimple) {
3431
+ const plan = buildKlikePushdownPlan(stmt, { allowUnresolvedVariables: true });
3432
+ if (unappliedKlikes(plan).length === 0) {
3433
+ validateNestedSelects(stmt);
3434
+ return;
3435
+ }
3196
3436
  throw new KlikeValidationError(
3197
- "KLIKE / NOT KLIKE \u306F kintone \u3078\u62BC\u3057\u4E0B\u3052\u308B SIMPLE SELECT \u3067\u306E\u307F\u4F7F\u7528\u3067\u304D\u307E\u3059\u3002\u3053\u306E SELECT \u306F FULL_SCAN \u306B\u306A\u308A\u307E\u3059"
3437
+ "FULL_SCAN \u306E KLIKE / NOT KLIKE \u306F\u3001\u7269\u7406\u30C6\u30FC\u30D6\u30EB\u306B\u5BFE\u3059\u308B AND \u30EA\u30FC\u30D5\u3068\u3057\u3066\u5FC5\u305A\u62BC\u3057\u4E0B\u3052\u3089\u308C\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002OR / NOT \u914D\u4E0B\u3001CTE\u30FB\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u3001LEFT / RIGHT JOIN \u3067\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093"
3198
3438
  );
3199
3439
  }
3200
3440
  }
@@ -3224,33 +3464,6 @@ function validateNestedSelects(node) {
3224
3464
  if (obj.type === "SELECT") validateSelect(obj);
3225
3465
  }, true);
3226
3466
  }
3227
- function buildEffectiveInlineSelect(stmt) {
3228
- if (stmt.ctes.length !== 1) return null;
3229
- const cte = stmt.ctes[0];
3230
- if (cte.query.type !== "SELECT" || resolveSelectMode(cte.query) !== "SIMPLE") return null;
3231
- if (stmt.query.type !== "SELECT") return null;
3232
- const final = stmt.query;
3233
- if (final.from.cteName !== cte.name || final.joins.length > 0) return null;
3234
- if (final.groupBy.length > 0 || final.distinct) return null;
3235
- if (final.columns.some(
3236
- (column) => column.type === "AGGREGATE" || column.type === "ARITH_AGG_COL"
3237
- )) return null;
3238
- const where = cte.query.where === null ? final.where : final.where === null ? cte.query.where : { type: "LOGICAL", op: "AND", left: cte.query.where, right: final.where };
3239
- const columns = final.columns.every((column) => column.type === "WILDCARD") ? cte.query.columns : final.columns;
3240
- return {
3241
- type: "SELECT",
3242
- from: cte.query.from,
3243
- joins: [],
3244
- columns,
3245
- where,
3246
- groupBy: [],
3247
- having: null,
3248
- orderBy: final.orderBy.length > 0 ? final.orderBy : cte.query.orderBy,
3249
- limit: final.limit ?? cte.query.limit,
3250
- offset: final.offset ?? cte.query.offset,
3251
- distinct: false
3252
- };
3253
- }
3254
3467
  function containsKlike(node) {
3255
3468
  let found = false;
3256
3469
  walkObjects(node, (obj) => {
@@ -3755,25 +3968,29 @@ function resolveFieldRef(row, field) {
3755
3968
  }
3756
3969
 
3757
3970
  // src/engine/evalWhere.ts
3758
- function evalWhere(expr, row, resolveFieldType) {
3971
+ function evalWhere(expr, row, resolveFieldType, appliedKlikes) {
3759
3972
  switch (expr.type) {
3760
3973
  case "BINARY":
3761
- return evalBinary(expr, row, resolveFieldType);
3974
+ return evalBinary(expr, row, resolveFieldType, appliedKlikes);
3762
3975
  case "NULL_CHECK":
3763
3976
  return evalNullCheck(expr, row);
3764
3977
  case "LOGICAL":
3765
- return evalLogical(expr, row, resolveFieldType);
3978
+ return evalLogical(expr, row, resolveFieldType, appliedKlikes);
3766
3979
  case "NOT":
3767
- return !evalWhere(expr.expr, row, resolveFieldType);
3980
+ return !evalWhere(expr.expr, row, resolveFieldType, appliedKlikes);
3768
3981
  case "GROUP":
3769
- return evalWhere(expr.expr, row, resolveFieldType);
3982
+ return evalWhere(expr.expr, row, resolveFieldType, appliedKlikes);
3770
3983
  case "EXISTS": {
3771
3984
  const exists = expr.resolved;
3772
3985
  return expr.not ? !exists : exists;
3773
3986
  }
3774
3987
  }
3775
3988
  }
3776
- function evalBinary(expr, row, resolveFieldType) {
3989
+ function evalBinary(expr, row, resolveFieldType, appliedKlikes) {
3990
+ if (expr.op === "KLIKE" || expr.op === "NOT_KLIKE") {
3991
+ if (appliedKlikes?.has(expr)) return true;
3992
+ throw new Error("KLIKE / NOT KLIKE \u306F\u62BC\u3057\u4E0B\u3052\u6E08\u307F\u96C6\u5408\u306B\u542B\u307E\u308C\u306A\u3044\u305F\u3081 JavaScript \u5074\u3067\u306F\u8A55\u4FA1\u3067\u304D\u307E\u305B\u3093");
3993
+ }
3777
3994
  const left = resolveField(expr.left, row, resolveFieldType);
3778
3995
  const fieldType = expr.left.type === "FIELD" ? resolveFieldType?.(expr.left) : void 0;
3779
3996
  return evalOp(expr.op, left, expr.right, row, fieldType, resolveFieldType);
@@ -3855,11 +4072,11 @@ function evalNullCheck(expr, row) {
3855
4072
  const val = resolveField(expr.field, row);
3856
4073
  return expr.not ? val !== "" : val === "";
3857
4074
  }
3858
- function evalLogical(expr, row, resolveFieldType) {
4075
+ function evalLogical(expr, row, resolveFieldType, appliedKlikes) {
3859
4076
  if (expr.op === "AND") {
3860
- return evalWhere(expr.left, row, resolveFieldType) && evalWhere(expr.right, row, resolveFieldType);
4077
+ return evalWhere(expr.left, row, resolveFieldType, appliedKlikes) && evalWhere(expr.right, row, resolveFieldType, appliedKlikes);
3861
4078
  }
3862
- return evalWhere(expr.left, row, resolveFieldType) || evalWhere(expr.right, row, resolveFieldType);
4079
+ return evalWhere(expr.left, row, resolveFieldType, appliedKlikes) || evalWhere(expr.right, row, resolveFieldType, appliedKlikes);
3863
4080
  }
3864
4081
  function resolveField(field, row, resolveFieldType) {
3865
4082
  if (field.type === "FUNC_FIELD") return evalStringFunc(field.expr, row);
@@ -3960,7 +4177,7 @@ function matchLike(value, pattern) {
3960
4177
  function assertDmlWhereIsSafe(where) {
3961
4178
  if (whereHasKlike(where)) {
3962
4179
  throw new DmlConvertError(
3963
- "UPDATE / DELETE \u306E WHERE \u306B KLIKE / NOT KLIKE \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\u3002kintone \u30AD\u30FC\u30EF\u30FC\u30C9\u691C\u7D22\u306E\u6253\u3061\u5207\u308A\u3092\u691C\u51FA\u3067\u304D\u306A\u3044\u305F\u3081\u3001v1 \u3067\u306F\u5168 DML \u3067\u5B89\u5168\u4E0A\u62D2\u5426\u3057\u307E\u3057\u305F\u3002"
4180
+ "UPDATE / DELETE \u306E WHERE \u306B KLIKE / NOT KLIKE \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\u3002kintone \u30AD\u30FC\u30EF\u30FC\u30C9\u691C\u7D22\u306E\u6253\u3061\u5207\u308A\u3092\u691C\u51FA\u3067\u304D\u306A\u3044\u305F\u3081\u3001\u5168 DML \u3067\u5B89\u5168\u4E0A\u62D2\u5426\u3057\u3066\u3044\u307E\u3059\u3002"
3964
4181
  );
3965
4182
  }
3966
4183
  if (!whereHasLike(where)) return;
@@ -4450,92 +4667,6 @@ async function resolveDmlTargetIds(getRecords, app, query, options) {
4450
4667
  };
4451
4668
  }
4452
4669
 
4453
- // src/core/optimization/wherePredicatePushdown.ts
4454
- function extractSafePushdownLeaves(where, options = {}) {
4455
- return extractAndLeaves(where, (expr) => isSafeComparison(expr, options));
4456
- }
4457
- function extractTypedPushdownCandidates(where, options = {}) {
4458
- return extractAndLeaves(
4459
- where,
4460
- (expr) => isNumericCandidate(expr, options) || isSelectionInCandidate(expr, options)
4461
- );
4462
- }
4463
- function extractAndLeaves(where, accept) {
4464
- switch (where.type) {
4465
- case "BINARY":
4466
- return accept(where) ? where : null;
4467
- case "LOGICAL":
4468
- if (where.op !== "AND") return null;
4469
- {
4470
- const left = extractAndLeaves(where.left, accept);
4471
- const right = extractAndLeaves(where.right, accept);
4472
- if (left && right) return { ...where, left, right };
4473
- return left ?? right ?? null;
4474
- }
4475
- case "GROUP":
4476
- return extractAndLeaves(where.expr, accept);
4477
- case "NULL_CHECK":
4478
- case "NOT":
4479
- case "EXISTS":
4480
- return null;
4481
- }
4482
- }
4483
- function isSafeComparison(expr, options) {
4484
- if (isSafeIdComparison(expr, options)) return true;
4485
- if (isNumericCandidate(expr, options)) {
4486
- return options.fieldTypes?.get(expr.left.field) === "NUMBER";
4487
- }
4488
- return isSelectionInComparison(expr, options);
4489
- }
4490
- var SELECTION_IN_FIELD_TYPES = /* @__PURE__ */ new Set([
4491
- "DROP_DOWN",
4492
- "RADIO_BUTTON",
4493
- "CHECK_BOX",
4494
- "MULTI_SELECT",
4495
- "STATUS"
4496
- ]);
4497
- function isSelectionInComparison(expr, options) {
4498
- if (!isSelectionInCandidate(expr, options)) return false;
4499
- if (expr.left.type !== "FIELD" || expr.right.type !== "IN_LIST") return false;
4500
- const fieldType = options.fieldTypes?.get(expr.left.field);
4501
- if (fieldType === void 0 || !SELECTION_IN_FIELD_TYPES.has(fieldType)) return false;
4502
- const validOptions = options.fieldOptions?.get(expr.left.field);
4503
- if (validOptions === void 0) return false;
4504
- return expr.right.values.every(
4505
- (value) => value.type === "STRING" && value.value !== "" && validOptions.has(value.value)
4506
- );
4507
- }
4508
- function isSafeIdComparison(expr, options) {
4509
- if (!isTargetIdField(expr.left, options)) return false;
4510
- if (expr.right.type !== "NUMBER") return false;
4511
- return expr.op === "=" || expr.op === ">" || expr.op === "<" || expr.op === ">=" || expr.op === "<=";
4512
- }
4513
- function isTargetIdField(field, options) {
4514
- if (field.type !== "FIELD" || field.field !== "$id") return false;
4515
- const targetAlias = options.tableAlias ?? null;
4516
- if (field.tableAlias === targetAlias) return true;
4517
- return options.allowUnqualifiedFields === true && field.tableAlias === null;
4518
- }
4519
- function isNumericCandidate(expr, options) {
4520
- if (expr.left.type !== "FIELD" || expr.left.field === "$id") return false;
4521
- if (!isTargetField(expr.left, options)) return false;
4522
- if (expr.right.type !== "NUMBER") return false;
4523
- if (expr.op === "=") return true;
4524
- return (expr.op === "<" || expr.op === ">") && Number.isSafeInteger(expr.right.value);
4525
- }
4526
- function isSelectionInCandidate(expr, options) {
4527
- if (expr.left.type !== "FIELD" || expr.left.field === "$id") return false;
4528
- if (!isTargetField(expr.left, options)) return false;
4529
- if (expr.op !== "IN" && expr.op !== "NOT_IN") return false;
4530
- if (expr.right.type !== "IN_LIST" || expr.right.values.length === 0) return false;
4531
- return expr.right.values.every((value) => value.type === "STRING" && value.value !== "");
4532
- }
4533
- function isTargetField(field, options) {
4534
- const targetAlias = options.tableAlias ?? null;
4535
- if (field.tableAlias === targetAlias) return true;
4536
- return options.allowUnqualifiedFields === true && field.tableAlias === null;
4537
- }
4538
-
4539
4670
  // src/engine/process.ts
4540
4671
  function flatten(record, alias) {
4541
4672
  const row = {};
@@ -4600,9 +4731,9 @@ function applyJoin(leftRows, rightRows, join2) {
4600
4731
  }
4601
4732
  return result;
4602
4733
  }
4603
- function applyFilter(rows, where, resolveFieldType) {
4734
+ function applyFilter(rows, where, resolveFieldType, appliedKlikes) {
4604
4735
  if (where === null) return rows;
4605
- return rows.filter((row) => evalWhere(where, row, resolveFieldType));
4736
+ return rows.filter((row) => evalWhere(where, row, resolveFieldType, appliedKlikes));
4606
4737
  }
4607
4738
  function hasAggregateColumns(columns) {
4608
4739
  return columns.some(
@@ -5059,7 +5190,8 @@ function runFullScan(input) {
5059
5190
  optionOrders,
5060
5191
  sortKinds,
5061
5192
  fieldTypeResolver,
5062
- havingFieldTypeResolver
5193
+ havingFieldTypeResolver,
5194
+ appliedKlikes
5063
5195
  } = input;
5064
5196
  let rows = [];
5065
5197
  const mainAlias = stmt.from.alias;
@@ -5071,7 +5203,7 @@ function runFullScan(input) {
5071
5203
  const rightRows = rightRecords.map((r) => flatten(r, rightAlias));
5072
5204
  rows = applyJoin(rows, rightRows, join2);
5073
5205
  }
5074
- rows = applyFilter(rows, stmt.where, fieldTypeResolver);
5206
+ rows = applyFilter(rows, stmt.where, fieldTypeResolver, appliedKlikes);
5075
5207
  if (stmt.groupBy.length > 0 || hasAggregateColumns(stmt.columns)) {
5076
5208
  rows = applyGroupBy(rows, stmt.groupBy, stmt.columns);
5077
5209
  }
@@ -5759,23 +5891,6 @@ async function validateSelectFieldCodes(stmt, mode, client, cacheContext) {
5759
5891
  }
5760
5892
  }
5761
5893
  }
5762
- function extractMainSafePushdown(stmt, fieldTypes, fieldOptions) {
5763
- if (stmt.where === null || stmt.from.subtableCode || stmt.from.cteName !== null) return null;
5764
- if (stmt.joins.length === 0) {
5765
- return extractSafePushdownLeaves(stmt.where, {
5766
- tableAlias: stmt.from.alias ?? void 0,
5767
- allowUnqualifiedFields: true,
5768
- fieldTypes,
5769
- fieldOptions
5770
- });
5771
- }
5772
- if (!stmt.from.alias) return null;
5773
- return extractSafePushdownLeaves(stmt.where, {
5774
- tableAlias: stmt.from.alias,
5775
- fieldTypes,
5776
- fieldOptions
5777
- });
5778
- }
5779
5894
  function extractMainTypedPushdownCandidate(stmt) {
5780
5895
  if (stmt.where === null || stmt.from.subtableCode || stmt.from.cteName !== null) return null;
5781
5896
  if (stmt.joins.length === 0) {
@@ -5957,23 +6072,10 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
5957
6072
  loadTypedInFieldTypes(stmt, client, cacheContext)
5958
6073
  ]);
5959
6074
  const fieldTypeResolvers = buildSelectFieldTypeResolvers(stmt, typedInFieldTypes);
5960
- const mainPushDown = extractMainSafePushdown(
5961
- stmt,
5962
- pushdownMeta.fieldTypesByApp.get(stmt.from.appId),
5963
- pushdownMeta.fieldOptionsByApp.get(stmt.from.appId)
5964
- );
5965
- const tableConditions = /* @__PURE__ */ new Map();
5966
- if (stmt.where !== null) {
5967
- for (const join2 of stmt.joins) {
5968
- if (!join2.table.alias || join2.table.subtableCode || join2.table.cteName !== null) continue;
5969
- const cond = extractSafePushdownLeaves(stmt.where, {
5970
- tableAlias: join2.table.alias,
5971
- fieldTypes: pushdownMeta.fieldTypesByApp.get(join2.table.appId),
5972
- fieldOptions: pushdownMeta.fieldOptionsByApp.get(join2.table.appId)
5973
- });
5974
- if (cond) tableConditions.set(join2.table.alias, cond);
5975
- }
5976
- }
6075
+ const pushdownPlan = buildKlikePushdownPlan(stmt, pushdownMeta);
6076
+ validateKlikePushdownPlan(pushdownPlan);
6077
+ const mainPushDown = pushdownPlan.mainCondition;
6078
+ const tableConditions = pushdownPlan.joinConditions;
5977
6079
  const mainFetch = fetchTableRecordsForFullScan(
5978
6080
  stmt,
5979
6081
  stmt.from,
@@ -6054,7 +6156,8 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
6054
6156
  optionOrders,
6055
6157
  sortKinds,
6056
6158
  fieldTypeResolver: fieldTypeResolvers.row,
6057
- havingFieldTypeResolver: fieldTypeResolvers.having
6159
+ havingFieldTypeResolver: fieldTypeResolvers.having,
6160
+ appliedKlikes: pushdownPlan.appliedKlikes
6058
6161
  });
6059
6162
  return { type: "SELECT", rows, columns, rowCount: rows.length, warnings: [...warnings] };
6060
6163
  }
@@ -6103,83 +6206,6 @@ async function executeWith(stmt, client, options, cacheContext, seed) {
6103
6206
  }
6104
6207
  return executeQueryWithCte(stmt.query, client, options, cteCache, cacheContext);
6105
6208
  }
6106
- function canInlineSingleCte(stmt) {
6107
- if (stmt.ctes.length !== 1) return false;
6108
- const cteDef = stmt.ctes[0];
6109
- if (cteDef.query.type !== "SELECT") return false;
6110
- if (resolveSelectMode(cteDef.query) !== "SIMPLE") return false;
6111
- const finalQuery = stmt.query;
6112
- if (finalQuery.type !== "SELECT") return false;
6113
- if (finalQuery.from.cteName !== cteDef.name) return false;
6114
- if (finalQuery.joins.length > 0) return false;
6115
- if (finalQuery.groupBy.length > 0) return false;
6116
- if (finalQuery.distinct) return false;
6117
- if (finalQuery.columns.some(
6118
- (c) => c.type === "AGGREGATE" || c.type === "ARITH_AGG_COL"
6119
- )) return false;
6120
- return true;
6121
- }
6122
- function buildInlinedQuery(stmt) {
6123
- const cteBody = stmt.ctes[0].query;
6124
- const final = stmt.query;
6125
- const cteAlias = final.from.alias;
6126
- const finalWhere = stripCteAlias(final.where, cteAlias);
6127
- let mergedWhere;
6128
- if (cteBody.where === null) mergedWhere = finalWhere;
6129
- else if (finalWhere === null) mergedWhere = cteBody.where;
6130
- else mergedWhere = { type: "LOGICAL", op: "AND", left: cteBody.where, right: finalWhere };
6131
- const columns = final.columns.every((c) => c.type === "WILDCARD") ? cteBody.columns : final.columns;
6132
- return {
6133
- type: "SELECT",
6134
- from: cteBody.from,
6135
- joins: [],
6136
- columns,
6137
- where: mergedWhere,
6138
- groupBy: [],
6139
- having: null,
6140
- orderBy: final.orderBy.length > 0 ? final.orderBy : cteBody.orderBy,
6141
- limit: final.limit ?? cteBody.limit,
6142
- offset: final.offset ?? cteBody.offset,
6143
- distinct: false
6144
- };
6145
- }
6146
- function stripCteAlias(where, alias) {
6147
- if (where === null || alias === null) return where;
6148
- switch (where.type) {
6149
- case "BINARY":
6150
- return {
6151
- type: "BINARY",
6152
- op: where.op,
6153
- left: stripCteAliasFromFieldValue(where.left, alias),
6154
- right: where.right
6155
- };
6156
- case "NULL_CHECK":
6157
- return {
6158
- type: "NULL_CHECK",
6159
- not: where.not,
6160
- field: stripCteAliasFromFieldValue(where.field, alias)
6161
- };
6162
- case "LOGICAL":
6163
- return {
6164
- type: "LOGICAL",
6165
- op: where.op,
6166
- left: stripCteAlias(where.left, alias),
6167
- right: stripCteAlias(where.right, alias)
6168
- };
6169
- case "NOT":
6170
- return { type: "NOT", expr: stripCteAlias(where.expr, alias) };
6171
- case "GROUP":
6172
- return { type: "GROUP", expr: stripCteAlias(where.expr, alias) };
6173
- case "EXISTS":
6174
- return where;
6175
- }
6176
- }
6177
- function stripCteAliasFromFieldValue(fv, alias) {
6178
- if (fv.type === "FIELD" && fv.tableAlias === alias) {
6179
- return { type: "FIELD", field: fv.field, tableAlias: null };
6180
- }
6181
- return fv;
6182
- }
6183
6209
  async function executeQueryWithCte(query, client, options, cteCache, cacheContext) {
6184
6210
  if (query.type === "UNION") {
6185
6211
  const [leftResult, rightResult] = await Promise.all([
@@ -6214,8 +6240,13 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
6214
6240
  resolveSubqueries(stmt.having, client, options, cacheContext, cteCache),
6215
6241
  resolveSelectCaseSubqueries(stmt, client, options, cacheContext, cteCache)
6216
6242
  ]);
6217
- const typedInFieldTypes = await loadTypedInFieldTypes(stmt, client, cacheContext);
6243
+ const [pushdownMeta, typedInFieldTypes] = await Promise.all([
6244
+ loadTypedPushdownMeta(stmt, client, cacheContext),
6245
+ loadTypedInFieldTypes(stmt, client, cacheContext)
6246
+ ]);
6218
6247
  const fieldTypeResolvers = buildSelectFieldTypeResolvers(stmt, typedInFieldTypes);
6248
+ const pushdownPlan = buildKlikePushdownPlan(stmt, pushdownMeta);
6249
+ validateKlikePushdownPlan(pushdownPlan);
6219
6250
  const scalarCachePromise = resolveScalarColumns(stmt.columns, client, options, cacheContext, cteCache);
6220
6251
  const orderByMetaPromise = buildOrderByMetaForSelect(stmt, client, cacheContext);
6221
6252
  scalarCachePromise.catch(() => {
@@ -6235,7 +6266,8 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
6235
6266
  parallel,
6236
6267
  true,
6237
6268
  options.onLimitReached ?? "error",
6238
- warnings
6269
+ warnings,
6270
+ pushdownPlan.mainCondition
6239
6271
  );
6240
6272
  tables.set(stmt.from.alias, mainRecords);
6241
6273
  }
@@ -6244,6 +6276,7 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
6244
6276
  const rows2 = cteCache.get(join2.table.cteName) ?? [];
6245
6277
  tables.set(join2.table.alias, rows2.map(processRowToKintoneRecord));
6246
6278
  } else {
6279
+ const pushDownCond = join2.table.alias ? pushdownPlan.joinConditions.get(join2.table.alias) ?? null : null;
6247
6280
  const optimized = await tryFetchJoinRecordsBySourceKeys(
6248
6281
  stmt,
6249
6282
  join2,
@@ -6252,7 +6285,8 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
6252
6285
  maxRecords,
6253
6286
  parallel,
6254
6287
  options.onLimitReached ?? "error",
6255
- warnings
6288
+ warnings,
6289
+ pushDownCond
6256
6290
  );
6257
6291
  const joinRecords = optimized ?? await fetchTableRecordsForFullScan(
6258
6292
  stmt,
@@ -6262,7 +6296,8 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
6262
6296
  parallel,
6263
6297
  false,
6264
6298
  options.onLimitReached ?? "error",
6265
- warnings
6299
+ warnings,
6300
+ pushDownCond
6266
6301
  );
6267
6302
  tables.set(join2.table.alias, joinRecords);
6268
6303
  }
@@ -6277,7 +6312,8 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
6277
6312
  optionOrders,
6278
6313
  sortKinds,
6279
6314
  fieldTypeResolver: fieldTypeResolvers.row,
6280
- havingFieldTypeResolver: fieldTypeResolvers.having
6315
+ havingFieldTypeResolver: fieldTypeResolvers.having,
6316
+ appliedKlikes: pushdownPlan.appliedKlikes
6281
6317
  });
6282
6318
  return { type: "SELECT", rows, columns, rowCount: rows.length, warnings: [...warnings] };
6283
6319
  }
@@ -7486,9 +7522,10 @@ function buildSelectPlan(stmt, label) {
7486
7522
  lines.push(` kintone query: ${params.query || "(\u306A\u3057)"}`);
7487
7523
  lines.push(` fields: ${params.fields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : params.fields.join(", ")}`);
7488
7524
  } else {
7525
+ const pushdownPlan = buildKlikePushdownPlan(stmt);
7489
7526
  const mainFields = selectToFetchAllFields(stmt, stmt.from);
7490
7527
  const mainAliasStr = stmt.from.alias ? ` AS ${stmt.from.alias}` : "";
7491
- const mainPushDown = extractMainSafePushdown(stmt);
7528
+ const mainPushDown = pushdownPlan.mainCondition;
7492
7529
  const mainCandidate = extractMainTypedPushdownCandidate(stmt);
7493
7530
  const mainQ = mainPushDown !== null ? whereToKintone(mainPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
7494
7531
  lines.push(` app: APP${stmt.from.appId}${mainAliasStr} (${stmt.from.appId})`);
@@ -7501,7 +7538,7 @@ function buildSelectPlan(stmt, label) {
7501
7538
  const joinFields = selectToFetchAllFields(stmt, join2.table);
7502
7539
  const joinAliasStr = join2.table.alias ? ` AS ${join2.table.alias}` : "";
7503
7540
  const joinType = join2.type === "INNER" ? "JOIN" : `${join2.type} JOIN`;
7504
- const joinPushDown = join2.table.alias && !join2.table.subtableCode && join2.table.cteName === null && stmt.where ? extractSafePushdownLeaves(stmt.where, { tableAlias: join2.table.alias }) : null;
7541
+ const joinPushDown = join2.table.alias ? pushdownPlan.joinConditions.get(join2.table.alias) ?? null : null;
7505
7542
  const joinCandidate = join2.table.alias && !join2.table.subtableCode && join2.table.cteName === null && stmt.where ? extractTypedPushdownCandidates(stmt.where, { tableAlias: join2.table.alias }) : null;
7506
7543
  const joinQ = joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
7507
7544
  lines.push(` ${joinType}: APP${join2.table.appId}${joinAliasStr} (${join2.table.appId})`);
@@ -7544,6 +7581,10 @@ function buildWithPlan(stmt) {
7544
7581
  if (stmt.query.type === "SELECT" || stmt.query.type === "UNION") {
7545
7582
  lines.push(...buildExplainPlan(stmt.query, "[main]"));
7546
7583
  }
7584
+ if (canInlineSingleCte(stmt)) {
7585
+ lines.push("");
7586
+ lines.push(...buildSelectPlan(buildInlinedQuery(stmt), "[effective: inlined CTE]"));
7587
+ }
7547
7588
  return lines;
7548
7589
  }
7549
7590
  function collectFullScanReasons(stmt) {