@rex0220/kintone-sql-tools 3.20.0 → 3.21.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
@@ -11929,7 +11929,8 @@ function runFullScan(input) {
11929
11929
  });
11930
11930
  knownColumns = mergeKnownColumns(knownColumns, rightColumns, rows);
11931
11931
  }
11932
- rows = applyFilter(rows, stmt.where, fieldTypeResolver, appliedKlikes, fieldSemanticsResolver);
11932
+ const filterWhere = input.residualWhere !== void 0 ? input.residualWhere : stmt.where;
11933
+ rows = applyFilter(rows, filterWhere, fieldTypeResolver, appliedKlikes, fieldSemanticsResolver);
11933
11934
  const grouping = normalizeGroupingSpec(stmt);
11934
11935
  if (grouping.type === "GROUPING_SETS") {
11935
11936
  if (!resolvedGroupingSpec) {
@@ -12452,7 +12453,7 @@ function nestedSelects(node, root) {
12452
12453
  visit(node);
12453
12454
  return found;
12454
12455
  }
12455
- function collectSelect(select, path, candidates, forceForbidden) {
12456
+ function collectSelect(select, path, candidates, forceForbidden, allowPhase2 = true) {
12456
12457
  const functionNames = relativeDateFunctionNamesInWhere(select.where);
12457
12458
  if (functionNames.length > 0) {
12458
12459
  candidates.push({
@@ -12460,11 +12461,18 @@ function collectSelect(select, path, candidates, forceForbidden) {
12460
12461
  source: select,
12461
12462
  where: select.where,
12462
12463
  functionNames,
12463
- path
12464
+ path,
12465
+ allowPhase2Prefilter: allowPhase2
12464
12466
  });
12465
12467
  }
12466
12468
  nestedSelects(select, select).forEach(
12467
- (nested, index) => collectSelect(nested, `${path}.select-source[${index}]`, candidates, forceForbidden)
12469
+ (nested, index) => collectSelect(
12470
+ nested,
12471
+ `${path}.select-source[${index}]`,
12472
+ candidates,
12473
+ forceForbidden,
12474
+ allowPhase2
12475
+ )
12468
12476
  );
12469
12477
  }
12470
12478
  function collectUnion(union, path, candidates, forceForbidden) {
@@ -12552,19 +12560,34 @@ function collectStatement(statement, path, candidates, forceForbidden = false) {
12552
12560
  }
12553
12561
  }
12554
12562
  nestedSelects(statement, statement).forEach(
12555
- (select, index) => collectSelect(select, `${path}.select-source[${index}]`, candidates, forceForbidden)
12563
+ (select, index) => collectSelect(
12564
+ select,
12565
+ `${path}.select-source[${index}]`,
12566
+ candidates,
12567
+ forceForbidden,
12568
+ false
12569
+ )
12556
12570
  );
12557
12571
  return;
12558
12572
  }
12559
12573
  default:
12560
12574
  nestedSelects(statement, statement).forEach(
12561
- (select, index) => collectSelect(select, `${path}.select-source[${index}]`, candidates, forceForbidden)
12575
+ (select, index) => collectSelect(
12576
+ select,
12577
+ `${path}.select-source[${index}]`,
12578
+ candidates,
12579
+ forceForbidden,
12580
+ false
12581
+ )
12562
12582
  );
12563
12583
  }
12564
12584
  }
12565
12585
  function serializationContainsFunctions(query, names) {
12566
12586
  return names.every((name) => new RegExp(`\\b${name}\\s*\\(`).test(query));
12567
12587
  }
12588
+ function allowRelativeDatePrefilterPlan(select, decomposition) {
12589
+ return decomposition.eligible === true && resolveSelectMode(select) === "FULL_SCAN" && select.orderMode !== "KINTONE_NATIVE" && select.from.cteName === null && !select.from.subtableCode && select.joins.length === 0;
12590
+ }
12568
12591
  function rejectedNode(candidate) {
12569
12592
  return {
12570
12593
  kind: candidate.kind,
@@ -12621,7 +12644,17 @@ async function buildRelativeDatePushdownPlan(statement, resolver) {
12621
12644
  } catch {
12622
12645
  restQuery2 = "";
12623
12646
  }
12624
- const allowed2 = physicalTopLevel && selectMode === "SIMPLE" && (select.orderBy.length === 0 || select.orderMode === "KINTONE_NATIVE") && capability2.capability === "EXACT_PUSHDOWN" && serializationContainsFunctions(restQuery2, candidate.functionNames);
12647
+ let allowed2 = physicalTopLevel && selectMode === "SIMPLE" && (select.orderBy.length === 0 || select.orderMode === "KINTONE_NATIVE") && capability2.capability === "EXACT_PUSHDOWN" && serializationContainsFunctions(restQuery2, candidate.functionNames);
12648
+ let prefilterPlan;
12649
+ let phase2PrefilterEligible;
12650
+ if (!allowed2 && candidate.allowPhase2Prefilter !== false && capability2.capability === "SUPERSET_PREFILTER" && resolver.prefilterDecomposition) {
12651
+ const decomposition = await resolver.prefilterDecomposition(select);
12652
+ if (decomposition?.eligible === true && allowRelativeDatePrefilterPlan(select, decomposition)) {
12653
+ prefilterPlan = decomposition.plan;
12654
+ phase2PrefilterEligible = true;
12655
+ allowed2 = true;
12656
+ }
12657
+ }
12625
12658
  const node2 = {
12626
12659
  kind: candidate.kind,
12627
12660
  source: candidate.source,
@@ -12630,6 +12663,7 @@ async function buildRelativeDatePushdownPlan(statement, resolver) {
12630
12663
  selectMode,
12631
12664
  capability: capability2,
12632
12665
  restQuery: restQuery2,
12666
+ ...prefilterPlan ? { prefilterPlan, phase2PrefilterEligible } : {},
12633
12667
  clientWhereEvaluation: !allowed2,
12634
12668
  allowed: allowed2
12635
12669
  };
@@ -12690,6 +12724,312 @@ function assertRelativeDatePushdownPlan(plan) {
12690
12724
  }
12691
12725
  }
12692
12726
 
12727
+ // src/core/optimization/relativeDatePrefilterPlan.ts
12728
+ function decomposeRelativeDatePrefilter(stmt, resolveField2, testSeam = {}) {
12729
+ const capability = classifyWhereCapability(stmt.where, resolveField2);
12730
+ const reject = (reasonCodes, disposition = "INELIGIBLE") => ({
12731
+ eligible: false,
12732
+ disposition,
12733
+ reasonCodes,
12734
+ capability: capability.capability,
12735
+ reasons: capability.reasons
12736
+ });
12737
+ if (stmt.where === null) return reject(["NO_WHERE"]);
12738
+ if (stmt.from.subtableCode) return reject(["SUBTABLE_UNSUPPORTED"]);
12739
+ if (stmt.joins.length > 0) return reject(["JOIN_UNSUPPORTED"]);
12740
+ if (stmt.from.cteName !== null || stmt.from.appId <= 0) {
12741
+ return reject(["NOT_DIRECT_PHYSICAL_APP"]);
12742
+ }
12743
+ const occurrences = collectRelativeOccurrences(stmt.where);
12744
+ if (occurrences.length === 0) return reject(["NO_RELATIVE_DATE"]);
12745
+ const spine = collectRelativeLeavesOnAndSpine(stmt.where, resolveField2);
12746
+ if (!spine.ok) return reject(spine.reasonCodes);
12747
+ if (!sameRelativeMultiset(occurrences, spine.leaves)) {
12748
+ return reject(["RELATIVE_DATE_LEAF_COUNT_MISMATCH"]);
12749
+ }
12750
+ if (capability.capability === "EXACT_PUSHDOWN") {
12751
+ return reject(["DEFER_TO_PHASE1"], "DEFER_PHASE1");
12752
+ }
12753
+ if (capability.capability !== "SUPERSET_PREFILTER") {
12754
+ return reject(["CAPABILITY_NOT_SUPERSET_PREFILTER"]);
12755
+ }
12756
+ const fieldMetadata = collectFieldMetadata(stmt.where, resolveField2);
12757
+ const safePlan = buildSingleTableKlikePushdownPlan(stmt.where, {
12758
+ tableAlias: stmt.from.alias ?? void 0,
12759
+ allowUnqualifiedFields: true,
12760
+ allowKlike: true,
12761
+ fieldTypes: fieldMetadata.fieldTypes,
12762
+ fieldOptions: fieldMetadata.fieldOptions
12763
+ });
12764
+ const serialize = testSeam.serialize ?? whereToKintone;
12765
+ const containsFunctions = testSeam.containsFunctions ?? serializationContainsFunctions;
12766
+ const confirmedLeaves = [];
12767
+ for (const leaf of spine.leaves) {
12768
+ const name = relativeNameOf(leaf);
12769
+ if (name === null) return reject(["RELATIVE_DATE_LEAF_COUNT_MISMATCH"]);
12770
+ let query;
12771
+ try {
12772
+ query = serialize(leaf);
12773
+ } catch {
12774
+ return reject(["PREFILTER_SERIALIZATION_FAILED"]);
12775
+ }
12776
+ if (!containsFunctions(query, [name])) {
12777
+ return reject(["PREFILTER_FUNCTION_MISSING"]);
12778
+ }
12779
+ confirmedLeaves.push(leaf);
12780
+ }
12781
+ const safeLeaves = collectBinaryIdentities(safePlan.condition);
12782
+ const adoptedLeaves = new Set(confirmedLeaves);
12783
+ const prefilterWhere = selectPrefilterInOriginalOrder(
12784
+ stmt.where,
12785
+ adoptedLeaves,
12786
+ safeLeaves
12787
+ );
12788
+ if (prefilterWhere === null) {
12789
+ return reject(["RELATIVE_DATE_LEAF_COUNT_MISMATCH"]);
12790
+ }
12791
+ let prefilterQuery;
12792
+ try {
12793
+ prefilterQuery = serialize(prefilterWhere);
12794
+ } catch {
12795
+ return reject(["PREFILTER_SERIALIZATION_FAILED"]);
12796
+ }
12797
+ const expectedNames = confirmedLeaves.map((leaf) => relativeNameOf(leaf));
12798
+ if (!containsFunctions(prefilterQuery, [...new Set(expectedNames)]) || !serializedMultisetContains(prefilterQuery, expectedNames)) {
12799
+ return reject(["PREFILTER_FUNCTION_MISSING"]);
12800
+ }
12801
+ const residualWhere = testSeam.rewriteResidual ? testSeam.rewriteResidual(stmt.where, adoptedLeaves) : replaceAdoptedLeaves(stmt.where, adoptedLeaves);
12802
+ if (residualWhere !== null && collectRelativeOccurrences(residualWhere).length > 0) {
12803
+ return reject(["RESIDUAL_RELATIVE_DATE_REMAINED"]);
12804
+ }
12805
+ if (residualWhere === null) {
12806
+ return reject(["DEFER_TO_PHASE1"], "DEFER_PHASE1");
12807
+ }
12808
+ const relativeFunctionNames = /* @__PURE__ */ new Set();
12809
+ for (const name of expectedNames) relativeFunctionNames.add(name);
12810
+ return {
12811
+ eligible: true,
12812
+ plan: {
12813
+ prefilterWhere,
12814
+ residualWhere,
12815
+ exactRelativeLeaves: confirmedLeaves,
12816
+ relativeFunctionNames,
12817
+ appliedKlikes: safePlan.appliedKlikes,
12818
+ capability: capability.capability,
12819
+ reasons: capability.reasons
12820
+ }
12821
+ };
12822
+ }
12823
+ function collectRelativeLeavesOnAndSpine(where, resolveField2) {
12824
+ const leaves = [];
12825
+ let failure = null;
12826
+ const visit = (node) => {
12827
+ if (failure !== null) return;
12828
+ switch (node.type) {
12829
+ case "GROUP":
12830
+ visit(node.expr);
12831
+ return;
12832
+ case "LOGICAL":
12833
+ if (node.op === "AND") {
12834
+ visit(node.left);
12835
+ visit(node.right);
12836
+ return;
12837
+ }
12838
+ if (collectRelativeOccurrences(node).length > 0) {
12839
+ failure = "RELATIVE_DATE_CONTEXT_UNSUPPORTED";
12840
+ }
12841
+ return;
12842
+ case "NOT":
12843
+ if (collectRelativeOccurrences(node).length > 0) {
12844
+ failure = "RELATIVE_DATE_CONTEXT_UNSUPPORTED";
12845
+ }
12846
+ return;
12847
+ case "BINARY": {
12848
+ const name = relativeNameOf(node);
12849
+ if (name === null) return;
12850
+ const result = classifyRelativeDateBinary(
12851
+ node.op,
12852
+ node.left,
12853
+ node.right,
12854
+ resolveField2
12855
+ );
12856
+ if (result.capability !== "EXACT_PUSHDOWN") {
12857
+ failure = "RELATIVE_DATE_LEAF_NOT_EXACT";
12858
+ return;
12859
+ }
12860
+ leaves.push(node);
12861
+ return;
12862
+ }
12863
+ case "EXISTS":
12864
+ if (collectRelativeOccurrences(node).length > 0) {
12865
+ failure = "RELATIVE_DATE_CONTEXT_UNSUPPORTED";
12866
+ }
12867
+ return;
12868
+ case "NULL_CHECK":
12869
+ case "BOOLEAN":
12870
+ return;
12871
+ }
12872
+ };
12873
+ visit(where);
12874
+ return failure === null ? { ok: true, leaves } : { ok: false, reasonCodes: [failure] };
12875
+ }
12876
+ function relativeNameOf(leaf) {
12877
+ return leaf.right.type === "KINTONE_FUNC" && isRelativeDateFunctionName(leaf.right.name) ? leaf.right.name : null;
12878
+ }
12879
+ function collectRelativeOccurrences(where) {
12880
+ const found = [];
12881
+ const visitWhere = (node) => {
12882
+ if (node.type === "BINARY" && relativeNameOf(node) !== null) {
12883
+ found.push(node);
12884
+ return;
12885
+ }
12886
+ switch (node.type) {
12887
+ case "LOGICAL":
12888
+ visitWhere(node.left);
12889
+ visitWhere(node.right);
12890
+ return;
12891
+ case "NOT":
12892
+ case "GROUP":
12893
+ visitWhere(node.expr);
12894
+ return;
12895
+ case "EXISTS":
12896
+ if (node.query.where !== null) visitWhere(node.query.where);
12897
+ return;
12898
+ case "BINARY":
12899
+ case "NULL_CHECK":
12900
+ case "BOOLEAN":
12901
+ return;
12902
+ }
12903
+ };
12904
+ visitWhere(where);
12905
+ return found;
12906
+ }
12907
+ function sameRelativeMultiset(occurrences, candidates) {
12908
+ if (occurrences.length !== candidates.length) return false;
12909
+ const remaining = new Set(candidates);
12910
+ for (const occurrence of occurrences) {
12911
+ if (!remaining.delete(occurrence)) return false;
12912
+ }
12913
+ return remaining.size === 0;
12914
+ }
12915
+ function collectBinaryIdentities(where) {
12916
+ const found = /* @__PURE__ */ new Set();
12917
+ const visit = (node) => {
12918
+ switch (node.type) {
12919
+ case "BINARY":
12920
+ found.add(node);
12921
+ return;
12922
+ case "LOGICAL":
12923
+ visit(node.left);
12924
+ visit(node.right);
12925
+ return;
12926
+ case "NOT":
12927
+ case "GROUP":
12928
+ visit(node.expr);
12929
+ return;
12930
+ case "NULL_CHECK":
12931
+ case "EXISTS":
12932
+ case "BOOLEAN":
12933
+ return;
12934
+ }
12935
+ };
12936
+ if (where !== null) visit(where);
12937
+ return found;
12938
+ }
12939
+ function selectPrefilterInOriginalOrder(where, relativeLeaves, safeLeaves) {
12940
+ switch (where.type) {
12941
+ case "BINARY":
12942
+ return relativeLeaves.has(where) || safeLeaves.has(where) ? where : null;
12943
+ case "LOGICAL":
12944
+ if (where.op !== "AND") return null;
12945
+ {
12946
+ const left = selectPrefilterInOriginalOrder(where.left, relativeLeaves, safeLeaves);
12947
+ const right = selectPrefilterInOriginalOrder(where.right, relativeLeaves, safeLeaves);
12948
+ if (left !== null && right !== null) return { ...where, left, right };
12949
+ return left ?? right;
12950
+ }
12951
+ case "GROUP": {
12952
+ const expr = selectPrefilterInOriginalOrder(where.expr, relativeLeaves, safeLeaves);
12953
+ return expr === null ? null : { ...where, expr };
12954
+ }
12955
+ case "NULL_CHECK":
12956
+ case "NOT":
12957
+ case "EXISTS":
12958
+ case "BOOLEAN":
12959
+ return null;
12960
+ }
12961
+ }
12962
+ var TRUE_PREDICATE = { type: "BOOLEAN", value: true };
12963
+ function replaceAdoptedLeaves(where, adoptedLeaves) {
12964
+ if (where.type === "BINARY" && adoptedLeaves.has(where)) return TRUE_PREDICATE;
12965
+ switch (where.type) {
12966
+ case "LOGICAL": {
12967
+ if (where.op !== "AND") return where;
12968
+ const left = replaceAdoptedLeaves(where.left, adoptedLeaves) ?? TRUE_PREDICATE;
12969
+ const right = replaceAdoptedLeaves(where.right, adoptedLeaves) ?? TRUE_PREDICATE;
12970
+ if (isTrue(left)) return isTrue(right) ? null : right;
12971
+ if (isTrue(right)) return left;
12972
+ if (left === where.left && right === where.right) return where;
12973
+ return { ...where, left, right };
12974
+ }
12975
+ case "GROUP": {
12976
+ const expr = replaceAdoptedLeaves(where.expr, adoptedLeaves) ?? TRUE_PREDICATE;
12977
+ if (isTrue(expr)) return TRUE_PREDICATE;
12978
+ return expr === where.expr ? where : { ...where, expr };
12979
+ }
12980
+ case "BINARY":
12981
+ case "NULL_CHECK":
12982
+ case "NOT":
12983
+ case "EXISTS":
12984
+ case "BOOLEAN":
12985
+ return where;
12986
+ }
12987
+ }
12988
+ function isTrue(where) {
12989
+ return where.type === "BOOLEAN" && where.value;
12990
+ }
12991
+ function collectFieldMetadata(where, resolveField2) {
12992
+ const fields = /* @__PURE__ */ new Map();
12993
+ const visitValue = (value) => {
12994
+ if (value === null || typeof value !== "object") return;
12995
+ if (Array.isArray(value)) {
12996
+ value.forEach(visitValue);
12997
+ return;
12998
+ }
12999
+ const record = value;
13000
+ if (record["type"] === "SELECT") return;
13001
+ if (record["type"] === "FIELD" && typeof record["field"] === "string" && typeof record["tableAlias"] !== "undefined") {
13002
+ fields.set(record["field"], value);
13003
+ return;
13004
+ }
13005
+ Object.values(record).forEach(visitValue);
13006
+ };
13007
+ visitValue(where);
13008
+ const fieldTypes = /* @__PURE__ */ new Map();
13009
+ const fieldOptions = /* @__PURE__ */ new Map();
13010
+ for (const [fieldCode, field] of fields) {
13011
+ const semantics = resolveField2(field);
13012
+ if (!semantics) continue;
13013
+ fieldTypes.set(fieldCode, semantics.fieldType);
13014
+ if (semantics.optionOrder) {
13015
+ fieldOptions.set(fieldCode, new Set(semantics.optionOrder.keys()));
13016
+ }
13017
+ }
13018
+ return { fieldTypes, fieldOptions };
13019
+ }
13020
+ function serializedMultisetContains(query, expectedNames) {
13021
+ const expected = /* @__PURE__ */ new Map();
13022
+ for (const name of expectedNames) {
13023
+ if (!RELATIVE_DATE_FUNCTION_NAMES.has(name)) return false;
13024
+ expected.set(name, (expected.get(name) ?? 0) + 1);
13025
+ }
13026
+ for (const [name, count] of expected) {
13027
+ const matches = query.match(new RegExp(`\\b${name}\\s*\\(`, "g"));
13028
+ if ((matches?.length ?? 0) < count) return false;
13029
+ }
13030
+ return true;
13031
+ }
13032
+
12693
13033
  // src/import/sourceLoader.ts
12694
13034
  var IMPORT_MAX_BYTES = 10 * 1024 * 1024;
12695
13035
  var ImportSourceError = class extends Error {
@@ -13753,7 +14093,16 @@ async function assertRelativeDateExecutionPlan(stmt, client, cacheContext) {
13753
14093
  async function resolveRelativeDateExecutionPlan(stmt, client, cacheContext) {
13754
14094
  return buildRelativeDatePushdownPlan(stmt, {
13755
14095
  select: (select) => resolveSelectWhereCapability(select, client, cacheContext),
13756
- dml: (dml) => resolveDmlWhereCapability(dml, client, cacheContext)
14096
+ dml: (dml) => resolveDmlWhereCapability(dml, client, cacheContext),
14097
+ prefilterDecomposition: async (select) => {
14098
+ if (select.where === null) return null;
14099
+ const resolver = await buildWhereFieldSemanticsResolver(
14100
+ select,
14101
+ client,
14102
+ cacheContext
14103
+ );
14104
+ return decomposeRelativeDatePrefilter(select, resolver);
14105
+ }
13757
14106
  });
13758
14107
  }
13759
14108
  async function executeParsedStatement(stmt, client, options, cacheContext) {
@@ -14862,6 +15211,14 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
14862
15211
  if (whereCapability.capability === "UNSUPPORTED") {
14863
15212
  throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(whereCapability)}).`);
14864
15213
  }
15214
+ let prefilterPlan;
15215
+ if (whereCapability.capability === "SUPERSET_PREFILTER") {
15216
+ const resolver = await buildWhereFieldSemanticsResolver(stmt, client, cacheContext, cteCache);
15217
+ const decomposition = decomposeRelativeDatePrefilter(stmt, resolver);
15218
+ if (decomposition.eligible && allowRelativeDatePrefilterPlan(stmt, decomposition)) {
15219
+ prefilterPlan = decomposition.plan;
15220
+ }
15221
+ }
14865
15222
  const staticMode = resolveSelectMode(stmt);
14866
15223
  const mode = whereCapability.capability === "EXACT_PUSHDOWN" ? staticMode : "FULL_SCAN";
14867
15224
  const orderMeta = await buildOrderByMetaForSelect(stmt, client, cacheContext, cteCache);
@@ -14892,7 +15249,8 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
14892
15249
  cacheContext,
14893
15250
  cteCache,
14894
15251
  whereCapability.capability === "EXACT_PUSHDOWN",
14895
- orderMeta
15252
+ orderMeta,
15253
+ prefilterPlan
14896
15254
  );
14897
15255
  }
14898
15256
  } catch (error) {
@@ -15810,7 +16168,7 @@ function buildSelectFieldTypeResolvers(stmt, fieldTypesByApp) {
15810
16168
  };
15811
16169
  return { row, having };
15812
16170
  }
15813
- async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache, allowOriginalWherePushdown = true, preloadedOrderMeta) {
16171
+ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache, allowOriginalWherePushdown = true, preloadedOrderMeta, prefilterPlan) {
15814
16172
  const maxRecords = options.maxRecords ?? 1e4;
15815
16173
  const warnings = /* @__PURE__ */ new Set();
15816
16174
  const parallel = options.fetchParallel ?? 1;
@@ -15836,6 +16194,10 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
15836
16194
  validateKlikePushdownPlan(pushdownPlan);
15837
16195
  const mainPushDown = pushdownPlan.mainCondition;
15838
16196
  const tableConditions = pushdownPlan.joinConditions;
16197
+ if (prefilterPlan && allowOriginalWherePushdown) {
16198
+ throw new Error("internal error: relative-date prefilter must disable original WHERE pushdown.");
16199
+ }
16200
+ const mainFetchCondition = prefilterPlan ? prefilterPlan.prefilterWhere : mainPushDown;
15839
16201
  const constantFalse = isConstantFalseWhere(stmt.where);
15840
16202
  const mainFetch = constantFalse ? Promise.resolve([]) : fetchTableRecordsForFullScan(
15841
16203
  stmt,
@@ -15846,7 +16208,7 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
15846
16208
  true,
15847
16209
  options.onLimitReached ?? "error",
15848
16210
  warnings,
15849
- mainPushDown,
16211
+ mainFetchCondition,
15850
16212
  allowOriginalWherePushdown
15851
16213
  );
15852
16214
  const parallelJoins = [];
@@ -15927,7 +16289,8 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
15927
16289
  havingFieldTypeResolver: fieldTypeResolvers.having,
15928
16290
  havingFieldSemanticsResolver,
15929
16291
  aggregateSortKindResolver,
15930
- appliedKlikes: pushdownPlan.appliedKlikes,
16292
+ appliedKlikes: prefilterPlan?.appliedKlikes ?? pushdownPlan.appliedKlikes,
16293
+ ...prefilterPlan ? { residualWhere: prefilterPlan.residualWhere } : {},
15931
16294
  resolvedGroupingSpec: resolvedGroupingSpecs.get(stmt)
15932
16295
  });
15933
16296
  return { type: "SELECT", rows, columns, rowCount: rows.length, warnings: [...warnings] };
@@ -19732,6 +20095,102 @@ function explainMetadataLines(analysis) {
19732
20095
  ...[...analysis.numberPrecisionApps].sort((a, b) => a - b).map((appId) => ` metadata API: number precision APP${appId}`)
19733
20096
  ];
19734
20097
  }
20098
+ function renderResidualOperator(op) {
20099
+ switch (op) {
20100
+ case "NOT_LIKE":
20101
+ return "NOT LIKE";
20102
+ case "NOT_KLIKE":
20103
+ return "NOT KLIKE";
20104
+ case "NOT_IN":
20105
+ return "NOT IN";
20106
+ case "LIKE":
20107
+ case "KLIKE":
20108
+ case "IN":
20109
+ case "=":
20110
+ case "!=":
20111
+ case "<>":
20112
+ case ">":
20113
+ case "<":
20114
+ case ">=":
20115
+ case "<=":
20116
+ return op;
20117
+ default:
20118
+ return "<op>";
20119
+ }
20120
+ }
20121
+ function relativeReasonOperator(op) {
20122
+ return op === "<>" ? "!=" : op;
20123
+ }
20124
+ function renderResidualValue(node) {
20125
+ if (node === null || typeof node !== "object") return "<expr>";
20126
+ const value = node;
20127
+ switch (value["type"]) {
20128
+ case "FIELD":
20129
+ return typeof value["field"] === "string" ? `${typeof value["tableAlias"] === "string" ? `${value["tableAlias"]}.` : ""}${value["field"]}` : "<expr>";
20130
+ case "FIELD_REF":
20131
+ return typeof value["field"] === "string" ? value["field"] : "<expr>";
20132
+ case "NUMBER":
20133
+ return typeof value["raw"] === "string" ? value["raw"] : typeof value["value"] === "number" ? String(value["value"]) : "<expr>";
20134
+ case "STRING":
20135
+ return typeof value["value"] === "string" ? `'${value["value"].replace(/'/g, "''")}'` : "<expr>";
20136
+ case "VARIABLE":
20137
+ return typeof value["name"] === "string" ? `@${value["name"]}` : "<expr>";
20138
+ case "VARIABLE_IN_LIST":
20139
+ return typeof value["name"] === "string" ? `@${value["name"]}` : "<expr>";
20140
+ case "STRING_FUNC": {
20141
+ if (typeof value["func"] !== "string" || !Array.isArray(value["args"])) return "<expr>";
20142
+ return `${value["func"]}(${value["args"].map(renderResidualValue).join(", ")})`;
20143
+ }
20144
+ case "FUNC_FIELD":
20145
+ case "ARITH_FIELD":
20146
+ case "CASE_FIELD":
20147
+ case "ARITH_VALUE":
20148
+ case "CASE_VALUE":
20149
+ return renderResidualValue(value["expr"]);
20150
+ case "GROUPING_FIELD":
20151
+ return `GROUPING(${renderResidualValue(value["ref"])})`;
20152
+ case "GROUPING_REF":
20153
+ return renderResidualValue(value["field"]);
20154
+ case "ARITH":
20155
+ case "SCALAR_ARITH":
20156
+ case "CONCAT_OP":
20157
+ return `(${renderResidualValue(value["left"])} ${typeof value["op"] === "string" ? value["op"] : value["type"] === "CONCAT_OP" ? "||" : "<op>"} ${renderResidualValue(value["right"])})`;
20158
+ case "KINTONE_FUNC":
20159
+ return typeof value["name"] === "string" ? `${value["name"]}(...)` : "<expr>";
20160
+ case "IN_LIST":
20161
+ return Array.isArray(value["values"]) ? `(${value["values"].map(renderResidualValue).join(", ")})` : "<expr>";
20162
+ case "ARRAY":
20163
+ return Array.isArray(value["elements"]) ? `[${value["elements"].map(renderResidualValue).join(", ")}]` : "<expr>";
20164
+ case "CASE":
20165
+ case "CASE_WHEN":
20166
+ return "CASE ... END";
20167
+ default:
20168
+ return "<expr>";
20169
+ }
20170
+ }
20171
+ function renderRelativeDateResidualWhere(where) {
20172
+ try {
20173
+ const node = where;
20174
+ switch (node["type"]) {
20175
+ case "BINARY":
20176
+ return `${renderResidualValue(node["left"])} ${renderResidualOperator(node["op"])} ${renderResidualValue(node["right"])}`;
20177
+ case "NULL_CHECK":
20178
+ return `${renderResidualValue(node["field"])} IS ${node["not"] === true ? "NOT " : ""}NULL`;
20179
+ case "LOGICAL":
20180
+ return `(${renderRelativeDateResidualWhere(node["left"])} ${node["op"] === "AND" || node["op"] === "OR" ? node["op"] : "<op>"} ${renderRelativeDateResidualWhere(node["right"])})`;
20181
+ case "NOT":
20182
+ return `NOT (${renderRelativeDateResidualWhere(node["expr"])})`;
20183
+ case "GROUP":
20184
+ return `(${renderRelativeDateResidualWhere(node["expr"])})`;
20185
+ case "BOOLEAN":
20186
+ return node["value"] === true ? "TRUE" : node["value"] === false ? "FALSE" : "<expr>";
20187
+ default:
20188
+ return "<expr>";
20189
+ }
20190
+ } catch {
20191
+ return "<expr>";
20192
+ }
20193
+ }
19735
20194
  function relativeDateExplainLines(plan) {
19736
20195
  if (!plan.hasRelativeDate) return [];
19737
20196
  if (!plan.allowed && plan.rejection) {
@@ -19745,6 +20204,32 @@ function relativeDateExplainLines(plan) {
19745
20204
  }
19746
20205
  const lines = [];
19747
20206
  for (const node of plan.nodes) {
20207
+ const prefilterPlan = node.prefilterPlan;
20208
+ if (node.allowed && prefilterPlan?.prefilterWhere && prefilterPlan.residualWhere) {
20209
+ for (const leaf of prefilterPlan.exactRelativeLeaves) {
20210
+ const functionName = leaf.right.type === "KINTONE_FUNC" ? leaf.right.name : "(unknown)";
20211
+ const field = leaf.left.type === "FIELD" ? leaf.left.field : void 0;
20212
+ const operator = relativeReasonOperator(leaf.op);
20213
+ const detail = node.capability?.reasons.find(
20214
+ (reason) => reason.functionName === functionName && (field === void 0 || reason.field === field) && reason.operator === operator
20215
+ );
20216
+ lines.push(
20217
+ ` relative date function: ${functionName}`,
20218
+ " relative date evaluation: kintone server exact prefilter",
20219
+ ` field: ${detail?.field ?? field ?? "(unknown)"} (${detail?.fieldType ?? "unknown"})`,
20220
+ ` operator: ${detail?.operator ?? operator}`
20221
+ );
20222
+ }
20223
+ const serverPrefilter = whereToKintone(prefilterPlan.prefilterWhere);
20224
+ lines.push(
20225
+ " where capability: SUPERSET_PREFILTER",
20226
+ ` server prefilter: ${serverPrefilter}`,
20227
+ ` client residual: ${renderRelativeDateResidualWhere(prefilterPlan.residualWhere)}`,
20228
+ " relative date client evaluations: 0",
20229
+ ` kintone query: ${serverPrefilter}`
20230
+ );
20231
+ continue;
20232
+ }
19748
20233
  for (const functionName of node.functionNames) {
19749
20234
  const detail = node.capability?.reasons.find(
19750
20235
  (reason) => reason.functionName === functionName