@rex0220/kintone-sql-tools 3.19.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
@@ -783,6 +783,26 @@ function aggregateOperandLabel(node) {
783
783
  return `${aggregateOperandLabel(node.left)}${node.op}${aggregateOperandLabel(node.right)}`;
784
784
  }
785
785
 
786
+ // src/core/relativeDateFunction.ts
787
+ var RELATIVE_DATE_FUNCTION_NAMES = /* @__PURE__ */ new Set([
788
+ "YESTERDAY",
789
+ "TOMORROW",
790
+ "FROM_TODAY",
791
+ "THIS_WEEK",
792
+ "LAST_WEEK",
793
+ "NEXT_WEEK",
794
+ "THIS_MONTH",
795
+ "LAST_MONTH",
796
+ "NEXT_MONTH",
797
+ "THIS_YEAR",
798
+ "LAST_YEAR",
799
+ "NEXT_YEAR"
800
+ ]);
801
+ var WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN = "WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN";
802
+ function isRelativeDateFunctionName(name) {
803
+ return RELATIVE_DATE_FUNCTION_NAMES.has(name);
804
+ }
805
+
786
806
  // src/parser/parser.ts
787
807
  var MAX_BATCH_STATEMENTS = 20;
788
808
  var PARSER_SCALAR_FUNCTION_TOKEN_MAP = Object.freeze({
@@ -883,12 +903,35 @@ var PARSER_CONTEXTUAL_FUNCTION_TOKEN_MAP = Object.freeze({
883
903
  function isContextualFunctionToken(kind) {
884
904
  return PARSER_CONTEXTUAL_FUNCTION_TOKEN_MAP[kind] !== void 0;
885
905
  }
906
+ var PARSER_IDENT_RELATIVE_DATE_FUNCTIONS = Object.freeze(
907
+ [...RELATIVE_DATE_FUNCTION_NAMES]
908
+ );
909
+ var RELATIVE_DATE_PERIOD_UNITS = /* @__PURE__ */ new Set(["DAYS", "WEEKS", "MONTHS", "YEARS"]);
910
+ var RELATIVE_DATE_WEEKDAYS = /* @__PURE__ */ new Set([
911
+ "SUNDAY",
912
+ "MONDAY",
913
+ "TUESDAY",
914
+ "WEDNESDAY",
915
+ "THURSDAY",
916
+ "FRIDAY",
917
+ "SATURDAY"
918
+ ]);
919
+ function isRelativeDatePeriodUnit(value) {
920
+ return RELATIVE_DATE_PERIOD_UNITS.has(value);
921
+ }
922
+ function isRelativeDateWeekday(value) {
923
+ return RELATIVE_DATE_WEEKDAYS.has(value);
924
+ }
925
+ function isRelativeDateMonthDay(value) {
926
+ return Number.isInteger(value) && value >= 1 && value <= 31;
927
+ }
886
928
  var PARSER_FUNCTION_SPELLINGS = Object.freeze(Array.from(/* @__PURE__ */ new Set([
887
929
  ...Object.keys(PARSER_SCALAR_FUNCTION_TOKEN_MAP),
888
930
  ...PARSER_IDENT_SCALAR_FUNCTIONS,
889
931
  ...Object.keys(PARSER_AGGREGATE_FUNCTION_TOKEN_MAP),
890
932
  ...Object.keys(PARSER_WINDOW_FUNCTION_TOKEN_MAP),
891
933
  ...Object.keys(PARSER_CONTEXTUAL_FUNCTION_TOKEN_MAP),
934
+ ...PARSER_IDENT_RELATIVE_DATE_FUNCTIONS,
892
935
  "IF"
893
936
  ])));
894
937
  var FUNC_CALL_PREFIX_KINDS = /* @__PURE__ */ new Set([
@@ -989,6 +1032,8 @@ var Parser = class {
989
1032
  this.insideAggregateArg = 0;
990
1033
  /** GROUPING(field) is limited to the explicitly selected query context. */
991
1034
  this.groupingFieldContext = "FORBIDDEN";
1035
+ /** True only while parsing an actual SQL WHERE clause (including nested groups). */
1036
+ this.allowRelativeDateFunctions = false;
992
1037
  /** WITH 句で定義された CTE 名のセット(parseTableRef で参照) */
993
1038
  this.cteNames = /* @__PURE__ */ new Set();
994
1039
  /** パース中に出現した一時テーブル参照(#name)のトークン。単文 API での拒否に使う */
@@ -1500,7 +1545,7 @@ var Parser = class {
1500
1545
  this.advance();
1501
1546
  summary = true;
1502
1547
  }
1503
- const where = this.consume("WHERE" /* WHERE */) ? this.parseWhereExpr() : null;
1548
+ const where = this.consume("WHERE" /* WHERE */) ? this.parseWhereExpr(void 0, true) : null;
1504
1549
  const checks = this.parseCheckGroups();
1505
1550
  let errorTable;
1506
1551
  if (this.consume("INTO" /* INTO */)) {
@@ -1719,7 +1764,7 @@ var Parser = class {
1719
1764
  const hasFrom = this.consume("FROM" /* FROM */);
1720
1765
  const from = hasFrom ? this.parseTableRef() : { appId: 0, alias: null, cteName: NO_FROM_CTE_NAME };
1721
1766
  const joins = hasFrom ? this.parseJoins() : [];
1722
- const where = this.consume("WHERE" /* WHERE */) ? this.parseWhereExpr() : null;
1767
+ const where = this.consume("WHERE" /* WHERE */) ? this.parseWhereExpr(void 0, true) : null;
1723
1768
  let groupBy = [];
1724
1769
  let grouping;
1725
1770
  let having = null;
@@ -2586,13 +2631,16 @@ var Parser = class {
2586
2631
  // ----------------------------------------------------------
2587
2632
  // WHERE 式(再帰下降・優先順位付き)
2588
2633
  // ----------------------------------------------------------
2589
- parseWhereExpr(groupingFieldContext = this.groupingFieldContext) {
2634
+ parseWhereExpr(groupingFieldContext = this.groupingFieldContext, allowRelativeDateFunctions = this.allowRelativeDateFunctions) {
2590
2635
  const previousContext = this.groupingFieldContext;
2591
- this.groupingFieldContext = groupingFieldContext;
2636
+ const previousAllowRelativeDateFunctions = this.allowRelativeDateFunctions;
2637
+ this.groupingFieldContext = groupingFieldContext ?? previousContext;
2638
+ this.allowRelativeDateFunctions = allowRelativeDateFunctions;
2592
2639
  try {
2593
2640
  return this.parseOrExpr();
2594
2641
  } finally {
2595
2642
  this.groupingFieldContext = previousContext;
2643
+ this.allowRelativeDateFunctions = previousAllowRelativeDateFunctions;
2596
2644
  }
2597
2645
  }
2598
2646
  // OR(最低優先度)
@@ -2671,9 +2719,9 @@ var Parser = class {
2671
2719
  return { type: "NULL_CHECK", field, not };
2672
2720
  }
2673
2721
  if (this.consume("BETWEEN" /* BETWEEN */)) {
2674
- const low = this.parseSqlValue();
2722
+ const low = this.parseWhereSqlValue();
2675
2723
  this.expect("AND" /* AND */);
2676
- const high = this.parseSqlValue();
2724
+ const high = this.parseWhereSqlValue();
2677
2725
  return {
2678
2726
  type: "LOGICAL",
2679
2727
  op: "AND",
@@ -2708,7 +2756,7 @@ var Parser = class {
2708
2756
  return { type: "BINARY", op: "KLIKE", left: field, right: pattern };
2709
2757
  }
2710
2758
  const op = this.parseCompareOp();
2711
- const right = this.parseSqlValue();
2759
+ const right = this.parseWhereSqlValue();
2712
2760
  return { type: "BINARY", op, left: field, right };
2713
2761
  }
2714
2762
  parseCompareOp() {
@@ -2813,6 +2861,105 @@ var Parser = class {
2813
2861
  return { type: "FIELD", tableAlias: qi.tableAlias, field: qi.field };
2814
2862
  }
2815
2863
  // 右辺の値
2864
+ parseWhereSqlValue() {
2865
+ const tok = this.peek();
2866
+ if (this.allowRelativeDateFunctions && tok.kind === "IDENT" /* IDENT */ && this.peekAt(1).kind === "(" /* LPAREN */ && isRelativeDateFunctionName(tok.value.toUpperCase())) {
2867
+ return this.parseRelativeDateFunction();
2868
+ }
2869
+ return this.parseSqlValue();
2870
+ }
2871
+ parseRelativeDateFunction() {
2872
+ const nameToken = this.expect("IDENT" /* IDENT */);
2873
+ const name = nameToken.value.toUpperCase();
2874
+ this.expect("(" /* LPAREN */);
2875
+ switch (name) {
2876
+ case "YESTERDAY":
2877
+ case "TOMORROW":
2878
+ case "THIS_YEAR":
2879
+ case "LAST_YEAR":
2880
+ case "NEXT_YEAR":
2881
+ this.expect(")" /* RPAREN */, `${name}() \u306F\u5F15\u6570\u3092\u53D7\u3051\u53D6\u308A\u307E\u305B\u3093`);
2882
+ return { type: "KINTONE_FUNC", name, args: { kind: "NONE" } };
2883
+ case "FROM_TODAY": {
2884
+ let sign = "";
2885
+ if (this.consume("-" /* MINUS */)) sign = "-";
2886
+ if (this.peek().kind === "+" /* PLUS */) {
2887
+ throw new ParseError("FROM_TODAY \u306E offset \u306B + \u7B26\u53F7\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", this.peek());
2888
+ }
2889
+ const offsetToken = this.expect(
2890
+ "NUMBER" /* NUMBER */,
2891
+ "FROM_TODAY \u306B\u306F\u6574\u6570 offset \u3068\u5358\u4F4D\u304C\u5FC5\u8981\u3067\u3059"
2892
+ );
2893
+ if (!/^\d+$/.test(offsetToken.value)) {
2894
+ throw new ParseError("FROM_TODAY \u306E offset \u306F10\u9032\u6574\u6570\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044", offsetToken);
2895
+ }
2896
+ const rawOffset = `${sign}${offsetToken.value}`;
2897
+ const offset = Number(rawOffset);
2898
+ if (!Number.isSafeInteger(offset)) {
2899
+ throw new ParseError("FROM_TODAY \u306E offset \u306F\u5B89\u5168\u306A\u6574\u6570\u306E\u7BC4\u56F2\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044", offsetToken);
2900
+ }
2901
+ this.expect("," /* COMMA */, "FROM_TODAY \u306E offset \u3068\u5358\u4F4D\u306F\u30AB\u30F3\u30DE\u3067\u533A\u5207\u3063\u3066\u304F\u3060\u3055\u3044");
2902
+ const unitToken = this.expect(
2903
+ "IDENT" /* IDENT */,
2904
+ "FROM_TODAY \u306E\u5358\u4F4D\u306B\u306F\u975E\u5F15\u7528\u306E DAYS / WEEKS / MONTHS / YEARS \u304C\u5FC5\u8981\u3067\u3059"
2905
+ );
2906
+ const unit = unitToken.value.toUpperCase();
2907
+ if (!isRelativeDatePeriodUnit(unit)) {
2908
+ throw new ParseError("FROM_TODAY \u306E\u5358\u4F4D\u306B\u306F DAYS / WEEKS / MONTHS / YEARS \u304C\u5FC5\u8981\u3067\u3059", unitToken);
2909
+ }
2910
+ this.expect(")" /* RPAREN */, "FROM_TODAY \u306F offset \u3068\u5358\u4F4D\u306E2\u5F15\u6570\u3060\u3051\u3092\u53D7\u3051\u53D6\u308A\u307E\u3059");
2911
+ const offsetText = String(offset === 0 ? 0 : offset);
2912
+ return {
2913
+ type: "KINTONE_FUNC",
2914
+ name,
2915
+ args: { kind: "FROM_TODAY", offset, offsetText, unit }
2916
+ };
2917
+ }
2918
+ case "THIS_WEEK":
2919
+ case "LAST_WEEK":
2920
+ case "NEXT_WEEK": {
2921
+ let weekday = null;
2922
+ if (this.peek().kind !== ")" /* RPAREN */) {
2923
+ const weekdayToken = this.expect(
2924
+ "IDENT" /* IDENT */,
2925
+ `${name} \u306E\u66DC\u65E5\u306B\u306F\u975E\u5F15\u7528\u306E SUNDAY ... SATURDAY \u304C\u5FC5\u8981\u3067\u3059`
2926
+ );
2927
+ const candidate = weekdayToken.value.toUpperCase();
2928
+ if (!isRelativeDateWeekday(candidate)) {
2929
+ throw new ParseError(`${name} \u306E\u66DC\u65E5\u304C\u4E0D\u6B63\u3067\u3059`, weekdayToken);
2930
+ }
2931
+ weekday = candidate;
2932
+ }
2933
+ this.expect(")" /* RPAREN */, `${name} \u306F\u66DC\u65E5\u3092\u6700\u59271\u500B\u3060\u3051\u53D7\u3051\u53D6\u308A\u307E\u3059`);
2934
+ return { type: "KINTONE_FUNC", name, args: { kind: "WEEK", weekday } };
2935
+ }
2936
+ case "THIS_MONTH":
2937
+ case "LAST_MONTH":
2938
+ case "NEXT_MONTH": {
2939
+ let day = null;
2940
+ if (this.peek().kind !== ")" /* RPAREN */) {
2941
+ const dayToken = this.peek();
2942
+ if (dayToken.kind === "IDENT" /* IDENT */ && dayToken.value.toUpperCase() === "LAST") {
2943
+ this.advance();
2944
+ day = "LAST";
2945
+ } else if (dayToken.kind === "NUMBER" /* NUMBER */ && /^\d+$/.test(dayToken.value)) {
2946
+ this.advance();
2947
+ const candidate = Number(dayToken.value);
2948
+ if (!isRelativeDateMonthDay(candidate)) {
2949
+ throw new ParseError(`${name} \u306E\u65E5\u306F 1 \u304B\u3089 31 \u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044`, dayToken);
2950
+ }
2951
+ day = candidate;
2952
+ } else {
2953
+ throw new ParseError(`${name} \u306E\u65E5\u306F\u975E\u5F15\u7528\u306E LAST \u307E\u305F\u306F 1 \u304B\u3089 31 \u304C\u5FC5\u8981\u3067\u3059`, dayToken);
2954
+ }
2955
+ }
2956
+ this.expect(")" /* RPAREN */, `${name} \u306F\u65E5\u3092\u6700\u59271\u500B\u3060\u3051\u53D7\u3051\u53D6\u308A\u307E\u3059`);
2957
+ return { type: "KINTONE_FUNC", name, args: { kind: "MONTH", day } };
2958
+ }
2959
+ default:
2960
+ throw new ParseError(`\u672A\u77E5\u306E\u76F8\u5BFE\u65E5\u4ED8\u95A2\u6570 ${name} \u3067\u3059`, nameToken);
2961
+ }
2962
+ }
2816
2963
  parseSqlValue() {
2817
2964
  const tok = this.peek();
2818
2965
  if (tok.kind === "VARIABLE" /* VARIABLE */) {
@@ -3382,7 +3529,7 @@ var Parser = class {
3382
3529
  whereTok
3383
3530
  );
3384
3531
  }
3385
- const where = this.parseWhereExpr();
3532
+ const where = this.parseWhereExpr(void 0, true);
3386
3533
  const applyBlocks = [];
3387
3534
  while (this.isApplyBlockStart()) applyBlocks.push(this.parseApplyBlock());
3388
3535
  if (this.isSoftKeyword("APPLY") && this.peekAt(1).kind === "IDENT" /* IDENT */ && this.peekAt(1).value.toUpperCase() === "SUBTABLE") {
@@ -3514,7 +3661,7 @@ var Parser = class {
3514
3661
  throw new ParseError("APPLY \u306E\u64CD\u4F5C\u306B\u306F PATCH / APPEND / REMOVE / ADD \u304C\u5FC5\u8981\u3067\u3059", this.peek());
3515
3662
  }
3516
3663
  parseApplyRowSelector() {
3517
- if (this.consume("WHERE" /* WHERE */)) return { kind: "WHERE", where: this.parseWhereExpr() };
3664
+ if (this.consume("WHERE" /* WHERE */)) return { kind: "WHERE", where: this.parseWhereExpr(void 0, true) };
3518
3665
  if (this.consume("ALL" /* ALL */)) {
3519
3666
  if (!this.isSoftKeyword("ROWS")) throw new ParseError("ALL \u306E\u5F8C\u306B\u306F ROWS \u304C\u5FC5\u8981\u3067\u3059", this.peek());
3520
3667
  this.advance();
@@ -3834,7 +3981,7 @@ var Parser = class {
3834
3981
  whereTok
3835
3982
  );
3836
3983
  }
3837
- const where = this.parseWhereExpr();
3984
+ const where = this.parseWhereExpr(void 0, true);
3838
3985
  return subtableCode ? { type: "DELETE", appId, subtableCode, where } : { type: "DELETE", appId, where };
3839
3986
  }
3840
3987
  // ----------------------------------------------------------
@@ -3864,7 +4011,7 @@ var Parser = class {
3864
4011
  if (!this.consume("WHERE" /* WHERE */)) {
3865
4012
  throw new ParseError("REORDER \u306B\u306F WHERE \u53E5\u304C\u5FC5\u9808\u3067\u3059\uFF08\u8AA4\u64CD\u4F5C\u9632\u6B62\uFF09", whereTok);
3866
4013
  }
3867
- where = this.parseWhereExpr();
4014
+ where = this.parseWhereExpr(void 0, true);
3868
4015
  }
3869
4016
  return {
3870
4017
  type: "REORDER",
@@ -4591,7 +4738,9 @@ function assertSafeParentPredicateNode(node, allowKlike) {
4591
4738
  if ((type === "FIELD" || type === "FIELD_REF") && typeof item["field"] === "string" && /^(count|sum|avg|min|max|group_concat)\s*\(/i.test(item["field"])) {
4592
4739
  unsupported("aggregate or window expressions in parent WHERE");
4593
4740
  }
4594
- if (type === "KINTONE_FUNC") unsupported("non-deterministic kintone functions in parent WHERE");
4741
+ if (type === "KINTONE_FUNC" && !(typeof item["name"] === "string" && isRelativeDateFunctionName(item["name"]))) {
4742
+ unsupported("non-deterministic kintone functions in parent WHERE");
4743
+ }
4595
4744
  for (const value of Object.values(item)) assertSafeParentPredicateNode(value, allowKlike);
4596
4745
  }
4597
4746
  function assertSafeChildPredicate(where, idxSelectors) {
@@ -4612,7 +4761,9 @@ function assertSafeApplyNode(node, context, allowIdx = false) {
4612
4761
  unsupported(`subqueries in ${context}`);
4613
4762
  }
4614
4763
  if (type === "WINDOW_COL" || type === "AGGREGATE" || type === "AGG_REF" || type === "AGG_ARITH" || type === "ARITH_AGG_COL") unsupported(`aggregate or window expressions in ${context}`);
4615
- if (type === "KINTONE_FUNC") unsupported(`non-deterministic kintone functions in ${context}`);
4764
+ if (type === "KINTONE_FUNC" && !(typeof item["name"] === "string" && isRelativeDateFunctionName(item["name"]))) {
4765
+ unsupported(`non-deterministic kintone functions in ${context}`);
4766
+ }
4616
4767
  if (type === "FIELD") {
4617
4768
  const alias = item["tableAlias"];
4618
4769
  const field = item["field"];
@@ -4864,7 +5015,67 @@ function convertString(v) {
4864
5015
  return `"${v.value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
4865
5016
  }
4866
5017
  function convertKintoneFunc(v) {
4867
- return `${v.name}()`;
5018
+ switch (v.name) {
5019
+ case "TODAY":
5020
+ case "NOW":
5021
+ case "LOGINUSER":
5022
+ return `${v.name}()`;
5023
+ }
5024
+ return convertRelativeDateFunction(v);
5025
+ }
5026
+ function convertRelativeDateFunction(v) {
5027
+ switch (v.name) {
5028
+ case "YESTERDAY":
5029
+ case "TOMORROW":
5030
+ case "THIS_YEAR":
5031
+ case "LAST_YEAR":
5032
+ case "NEXT_YEAR":
5033
+ if (v.args?.kind !== "NONE") return failInvalidRelativeDateFunction();
5034
+ return `${v.name}()`;
5035
+ case "FROM_TODAY":
5036
+ if (v.args?.kind !== "FROM_TODAY" || !isValidRelativeDateOffset(v.args.offset, v.args.offsetText) || !RELATIVE_DATE_PERIOD_UNITS2.has(v.args.unit)) {
5037
+ return failInvalidRelativeDateFunction();
5038
+ }
5039
+ return `${v.name}(${v.args.offsetText}, ${v.args.unit})`;
5040
+ case "THIS_WEEK":
5041
+ case "LAST_WEEK":
5042
+ case "NEXT_WEEK":
5043
+ if (v.args?.kind !== "WEEK" || v.args.weekday !== null && !RELATIVE_DATE_WEEKDAYS2.has(v.args.weekday)) {
5044
+ return failInvalidRelativeDateFunction();
5045
+ }
5046
+ return v.args.weekday === null ? `${v.name}()` : `${v.name}(${v.args.weekday})`;
5047
+ case "THIS_MONTH":
5048
+ case "LAST_MONTH":
5049
+ case "NEXT_MONTH":
5050
+ if (v.args?.kind !== "MONTH" || !isValidRelativeDateMonthDay(v.args.day)) {
5051
+ return failInvalidRelativeDateFunction();
5052
+ }
5053
+ return v.args.day === null ? `${v.name}()` : `${v.name}(${v.args.day})`;
5054
+ }
5055
+ return failInvalidRelativeDateFunction();
5056
+ }
5057
+ var RELATIVE_DATE_PERIOD_UNITS2 = /* @__PURE__ */ new Set(["DAYS", "WEEKS", "MONTHS", "YEARS"]);
5058
+ var RELATIVE_DATE_WEEKDAYS2 = /* @__PURE__ */ new Set([
5059
+ "SUNDAY",
5060
+ "MONDAY",
5061
+ "TUESDAY",
5062
+ "WEDNESDAY",
5063
+ "THURSDAY",
5064
+ "FRIDAY",
5065
+ "SATURDAY"
5066
+ ]);
5067
+ function isValidRelativeDateOffset(offset, offsetText) {
5068
+ if (!Number.isSafeInteger(offset)) return false;
5069
+ if (!/^(?:0|[1-9]\d*|-[1-9]\d*)$/.test(offsetText)) return false;
5070
+ return Number(offsetText) === offset;
5071
+ }
5072
+ function isValidRelativeDateMonthDay(day) {
5073
+ return day === null || day === "LAST" || typeof day === "number" && Number.isInteger(day) && day >= 1 && day <= 31;
5074
+ }
5075
+ function failInvalidRelativeDateFunction() {
5076
+ throw new KintoneQueryError(
5077
+ "internal error: invalid relative date function AST reached kintone query conversion"
5078
+ );
4868
5079
  }
4869
5080
  function convertInList(v, op) {
4870
5081
  if (op !== "IN" && op !== "NOT_IN") {
@@ -7489,7 +7700,7 @@ function resolveValue(value, row, resolveFieldType, resolveFieldSemantics2) {
7489
7700
  case "NUMBER":
7490
7701
  return numberLiteralText(value);
7491
7702
  case "KINTONE_FUNC":
7492
- return resolveKintoneFunc(value.name);
7703
+ return resolveKintoneFuncValue(value.name);
7493
7704
  case "IN_LIST":
7494
7705
  return "";
7495
7706
  // IN は evalOp で別処理
@@ -7558,6 +7769,21 @@ function resolveKintoneFunc(name) {
7558
7769
  return "";
7559
7770
  }
7560
7771
  }
7772
+ function resolveKintoneFuncValue(name) {
7773
+ if (isRelativeDateFunctionName(name)) {
7774
+ throw new Error(
7775
+ `${name}: ${WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN}`
7776
+ );
7777
+ }
7778
+ switch (name) {
7779
+ case "TODAY":
7780
+ case "NOW":
7781
+ case "LOGINUSER":
7782
+ return resolveKintoneFunc(name);
7783
+ default:
7784
+ throw new Error(`InternalError: unexpected KINTONE_FUNC name: ${name}`);
7785
+ }
7786
+ }
7561
7787
  var likeRegexCache = /* @__PURE__ */ new Map();
7562
7788
  var LIKE_REGEX_CACHE_MAX = 200;
7563
7789
  function matchLike(value, pattern) {
@@ -10535,7 +10761,10 @@ function planKorder(input) {
10535
10761
  if (stmt.from.cteName !== null || stmt.from.subtableCode || input.staticMode !== "SIMPLE") {
10536
10762
  reasons.push("KORDER_QUERY_SHAPE_UNSUPPORTED");
10537
10763
  }
10538
- if (input.whereCapability !== "EXACT_PUSHDOWN") reasons.push("KORDER_WHERE_NOT_EXACT");
10764
+ if (input.whereCapability !== "EXACT_PUSHDOWN") {
10765
+ reasons.push(...(input.whereReasons ?? []).filter((reason) => reason.functionName !== void 0).map((reason) => reason.code));
10766
+ reasons.push("KORDER_WHERE_NOT_EXACT");
10767
+ }
10539
10768
  if (input.hasKlike) reasons.push("KORDER_KLIKE_UNSUPPORTED");
10540
10769
  if (stmt.orderBy.length === 0) reasons.push("KORDER_KEY_REQUIRED");
10541
10770
  for (const item of stmt.orderBy) {
@@ -11700,7 +11929,8 @@ function runFullScan(input) {
11700
11929
  });
11701
11930
  knownColumns = mergeKnownColumns(knownColumns, rightColumns, rows);
11702
11931
  }
11703
- 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);
11704
11934
  const grouping = normalizeGroupingSpec(stmt);
11705
11935
  if (grouping.type === "GROUPING_SETS") {
11706
11936
  if (!resolvedGroupingSpec) {
@@ -11861,6 +12091,13 @@ function deepClone(value, seen = /* @__PURE__ */ new Map()) {
11861
12091
 
11862
12092
  // src/core/optimization/whereCapability.ts
11863
12093
  var RANGE_AND_EQUALITY = ["=", "!=", ">", "<", ">=", "<="];
12094
+ var RELATIVE_DATE_FIELD_TYPES = /* @__PURE__ */ new Set([
12095
+ "DATE",
12096
+ "DATETIME",
12097
+ "CREATED_TIME",
12098
+ "UPDATED_TIME"
12099
+ ]);
12100
+ var RELATIVE_DATE_OPERATORS = new Set(RANGE_AND_EQUALITY);
11864
12101
  var EQUALITY_IN = ["=", "!=", "in", "not in"];
11865
12102
  var NATIVE_OPERATORS = /* @__PURE__ */ new Map([
11866
12103
  ["RECORD_NUMBER", /* @__PURE__ */ new Set([...RANGE_AND_EQUALITY, "in", "not in"])],
@@ -11936,7 +12173,7 @@ function classifyNode(where, resolveField2) {
11936
12173
  case "BOOLEAN":
11937
12174
  return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
11938
12175
  case "BINARY":
11939
- return classifyBinary(where.op, where.left, where.right.type, resolveField2);
12176
+ return classifyBinary(where.op, where.left, where.right, resolveField2);
11940
12177
  case "NULL_CHECK":
11941
12178
  if (where.field.type !== "FIELD") return localExpression();
11942
12179
  return classifyLocalOnlyField(where.field, where.not ? "IS NOT NULL" : "IS NULL", resolveField2);
@@ -11946,7 +12183,14 @@ function classifyNode(where, resolveField2) {
11946
12183
  return classifyNode(where.expr, resolveField2);
11947
12184
  case "NOT": {
11948
12185
  const inner = classifyNode(where.expr, resolveField2);
11949
- return inner.capability === "SUPERSET_PREFILTER" ? { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] } : inner;
12186
+ if (inner.capability !== "SUPERSET_PREFILTER") return inner;
12187
+ if (!hasRelativeDateReason(inner.reasons)) {
12188
+ return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
12189
+ }
12190
+ return requireExactRelativeDatePushdown({
12191
+ capability: "LOCAL_ONLY",
12192
+ reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }, ...inner.reasons]
12193
+ });
11950
12194
  }
11951
12195
  case "LOGICAL": {
11952
12196
  const left = classifyNode(where.left, resolveField2);
@@ -11955,7 +12199,10 @@ function classifyNode(where, resolveField2) {
11955
12199
  }
11956
12200
  }
11957
12201
  }
11958
- function classifyBinary(op, left, rightType, resolveField2) {
12202
+ function classifyBinary(op, left, right, resolveField2) {
12203
+ if (right.type === "KINTONE_FUNC" && isRelativeDateFunctionName(right.name)) {
12204
+ return classifyRelativeDateBinary(op, left, right, resolveField2);
12205
+ }
11959
12206
  if (left.type !== "FIELD") return localExpression();
11960
12207
  const semantics = resolveField2(left);
11961
12208
  if (!semantics) {
@@ -11966,7 +12213,7 @@ function classifyBinary(op, left, rightType, resolveField2) {
11966
12213
  }
11967
12214
  const nativeOp = normalizeOperator(op);
11968
12215
  const native = nativeWhereOperatorsForType(semantics.fieldType);
11969
- const rightCanPush = rightType === "STRING" || rightType === "NUMBER" || rightType === "IN_LIST" || rightType === "KINTONE_FUNC";
12216
+ const rightCanPush = right.type === "STRING" || right.type === "NUMBER" || right.type === "IN_LIST" || isLegacyKintoneFunction(right);
11970
12217
  const structureAllows = !semantics.requiresCollectionOperators || nativeOp !== "=" && nativeOp !== "!=";
11971
12218
  const sqlLikeIsResidual = op === "LIKE" || op === "NOT_LIKE";
11972
12219
  if (rightCanPush && structureAllows && native.has(nativeOp) && !sqlLikeIsResidual) {
@@ -11990,6 +12237,92 @@ function classifyBinary(op, left, rightType, resolveField2) {
11990
12237
  }]
11991
12238
  };
11992
12239
  }
12240
+ function isLegacyKintoneFunction(value) {
12241
+ return value.type === "KINTONE_FUNC" && (value.name === "TODAY" || value.name === "NOW" || value.name === "LOGINUSER");
12242
+ }
12243
+ function classifyRelativeDateBinary(op, left, right, resolveField2) {
12244
+ const operator = normalizeOperator(op);
12245
+ const functionName = right.name;
12246
+ if (left.type !== "FIELD") {
12247
+ return relativeDateUnsupported(
12248
+ "WHERE_RELATIVE_DATE_CONTEXT_UNSUPPORTED",
12249
+ functionName,
12250
+ void 0,
12251
+ void 0,
12252
+ operator
12253
+ );
12254
+ }
12255
+ const semantics = resolveField2(left);
12256
+ if (!semantics) {
12257
+ return relativeDateUnsupported(
12258
+ "WHERE_RELATIVE_DATE_FIELD_TYPE_UNSUPPORTED",
12259
+ functionName,
12260
+ left.field,
12261
+ void 0,
12262
+ operator
12263
+ );
12264
+ }
12265
+ if (!hasValidRelativeDateArguments(right)) {
12266
+ return relativeDateUnsupported(
12267
+ "WHERE_RELATIVE_DATE_ARGUMENT_INVALID",
12268
+ functionName,
12269
+ left.field,
12270
+ semantics.fieldType,
12271
+ operator
12272
+ );
12273
+ }
12274
+ if (!RELATIVE_DATE_OPERATORS.has(operator)) {
12275
+ return relativeDateUnsupported(
12276
+ "WHERE_RELATIVE_DATE_OPERATOR_UNSUPPORTED",
12277
+ functionName,
12278
+ left.field,
12279
+ semantics.fieldType,
12280
+ operator
12281
+ );
12282
+ }
12283
+ if (!RELATIVE_DATE_FIELD_TYPES.has(semantics.fieldType) || semantics.inSubtable || semantics.requiresCollectionOperators) {
12284
+ return relativeDateUnsupported(
12285
+ "WHERE_RELATIVE_DATE_FIELD_TYPE_UNSUPPORTED",
12286
+ functionName,
12287
+ left.field,
12288
+ semantics.fieldType,
12289
+ operator
12290
+ );
12291
+ }
12292
+ return {
12293
+ capability: "EXACT_PUSHDOWN",
12294
+ reasons: [{
12295
+ code: "WHERE_EXACT",
12296
+ functionName,
12297
+ field: left.field,
12298
+ fieldType: semantics.fieldType,
12299
+ operator
12300
+ }]
12301
+ };
12302
+ }
12303
+ function hasValidRelativeDateArguments(value) {
12304
+ if (!("args" in value) || !value.args) return false;
12305
+ switch (value.name) {
12306
+ case "YESTERDAY":
12307
+ case "TOMORROW":
12308
+ case "THIS_YEAR":
12309
+ case "LAST_YEAR":
12310
+ case "NEXT_YEAR":
12311
+ return value.args.kind === "NONE";
12312
+ case "FROM_TODAY":
12313
+ return value.args.kind === "FROM_TODAY" && Number.isSafeInteger(value.args.offset) && value.args.offsetText === String(value.args.offset === 0 ? 0 : value.args.offset) && (value.args.unit === "DAYS" || value.args.unit === "WEEKS" || value.args.unit === "MONTHS" || value.args.unit === "YEARS");
12314
+ case "THIS_WEEK":
12315
+ case "LAST_WEEK":
12316
+ case "NEXT_WEEK":
12317
+ return value.args.kind === "WEEK" && (value.args.weekday === null || value.args.weekday === "SUNDAY" || value.args.weekday === "MONDAY" || value.args.weekday === "TUESDAY" || value.args.weekday === "WEDNESDAY" || value.args.weekday === "THURSDAY" || value.args.weekday === "FRIDAY" || value.args.weekday === "SATURDAY");
12318
+ case "THIS_MONTH":
12319
+ case "LAST_MONTH":
12320
+ case "NEXT_MONTH":
12321
+ return value.args.kind === "MONTH" && (value.args.day === null || value.args.day === "LAST" || Number.isInteger(value.args.day) && value.args.day >= 1 && value.args.day <= 31);
12322
+ default:
12323
+ return false;
12324
+ }
12325
+ }
11993
12326
  function classifyLocalOnlyField(field, operator, resolveField2) {
11994
12327
  const semantics = resolveField2(field);
11995
12328
  if (!semantics) return unsupported2("WHERE_FIELD_UNRESOLVED", field.field, void 0, operator);
@@ -12027,18 +12360,18 @@ function normalizeOperator(op) {
12027
12360
  function combineLogical(op, left, right) {
12028
12361
  const reasons = [...left.reasons, ...right.reasons];
12029
12362
  if (left.capability === "UNSUPPORTED" || right.capability === "UNSUPPORTED") {
12030
- return { capability: "UNSUPPORTED", reasons };
12363
+ return requireExactRelativeDatePushdown({ capability: "UNSUPPORTED", reasons });
12031
12364
  }
12032
12365
  if (left.capability === "EXACT_PUSHDOWN" && right.capability === "EXACT_PUSHDOWN") {
12033
12366
  return { capability: "EXACT_PUSHDOWN", reasons };
12034
12367
  }
12035
12368
  if (op === "AND" && (left.capability === "EXACT_PUSHDOWN" || right.capability === "EXACT_PUSHDOWN" || left.capability === "SUPERSET_PREFILTER" || right.capability === "SUPERSET_PREFILTER")) {
12036
- return {
12369
+ return requireExactRelativeDatePushdown({
12037
12370
  capability: "SUPERSET_PREFILTER",
12038
12371
  reasons: [{ code: "WHERE_SUPERSET_PREFILTER" }, ...reasons]
12039
- };
12372
+ });
12040
12373
  }
12041
- return { capability: "LOCAL_ONLY", reasons };
12374
+ return requireExactRelativeDatePushdown({ capability: "LOCAL_ONLY", reasons });
12042
12375
  }
12043
12376
  function localExpression() {
12044
12377
  return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
@@ -12046,6 +12379,656 @@ function localExpression() {
12046
12379
  function unsupported2(code, field, fieldType, operator) {
12047
12380
  return { capability: "UNSUPPORTED", reasons: [{ code, field, fieldType, operator }] };
12048
12381
  }
12382
+ function relativeDateUnsupported(code, functionName, field, fieldType, operator) {
12383
+ return requireExactRelativeDatePushdown({
12384
+ capability: "UNSUPPORTED",
12385
+ reasons: [{ code, functionName, field, fieldType, operator }]
12386
+ });
12387
+ }
12388
+ function hasRelativeDateReason(reasons) {
12389
+ return reasons.some((reason) => reason.functionName !== void 0);
12390
+ }
12391
+ function requireExactRelativeDatePushdown(result) {
12392
+ if (result.capability === "EXACT_PUSHDOWN" || !hasRelativeDateReason(result.reasons) || result.reasons.some((reason) => reason.code === "WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN")) {
12393
+ return result;
12394
+ }
12395
+ const relative = result.reasons.find((reason) => reason.functionName !== void 0);
12396
+ return {
12397
+ capability: result.capability,
12398
+ reasons: [
12399
+ ...result.reasons,
12400
+ {
12401
+ code: "WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN",
12402
+ functionName: relative.functionName,
12403
+ field: relative.field,
12404
+ fieldType: relative.fieldType,
12405
+ operator: relative.operator
12406
+ }
12407
+ ]
12408
+ };
12409
+ }
12410
+
12411
+ // src/core/optimization/relativeDatePushdownGuard.ts
12412
+ function relativeDateFunctionNamesInNode(node, stopAtNestedSelect) {
12413
+ const names = [];
12414
+ const seen = /* @__PURE__ */ new Set();
12415
+ const visit = (valueNode) => {
12416
+ if (Array.isArray(valueNode)) {
12417
+ valueNode.forEach(visit);
12418
+ return;
12419
+ }
12420
+ if (valueNode === null || typeof valueNode !== "object") return;
12421
+ const value = valueNode;
12422
+ if (value["type"] === "KINTONE_FUNC" && typeof value["name"] === "string" && isRelativeDateFunctionName(value["name"]) && !seen.has(value["name"])) {
12423
+ seen.add(value["name"]);
12424
+ names.push(value["name"]);
12425
+ return;
12426
+ }
12427
+ if (stopAtNestedSelect && value["type"] === "SELECT") return;
12428
+ Object.values(value).forEach(visit);
12429
+ };
12430
+ visit(node);
12431
+ return names;
12432
+ }
12433
+ function relativeDateFunctionNamesInWhere(where) {
12434
+ return relativeDateFunctionNamesInNode(where, true);
12435
+ }
12436
+ function nestedSelects(node, root) {
12437
+ const found = [];
12438
+ const seen = /* @__PURE__ */ new Set();
12439
+ const visit = (value) => {
12440
+ if (Array.isArray(value)) {
12441
+ value.forEach(visit);
12442
+ return;
12443
+ }
12444
+ if (value === null || typeof value !== "object" || seen.has(value)) return;
12445
+ seen.add(value);
12446
+ const object = value;
12447
+ if (object["type"] === "SELECT" && value !== root) {
12448
+ found.push(value);
12449
+ return;
12450
+ }
12451
+ Object.values(object).forEach(visit);
12452
+ };
12453
+ visit(node);
12454
+ return found;
12455
+ }
12456
+ function collectSelect(select, path, candidates, forceForbidden, allowPhase2 = true) {
12457
+ const functionNames = relativeDateFunctionNamesInWhere(select.where);
12458
+ if (functionNames.length > 0) {
12459
+ candidates.push({
12460
+ kind: forceForbidden ? "FORBIDDEN" : "SELECT",
12461
+ source: select,
12462
+ where: select.where,
12463
+ functionNames,
12464
+ path,
12465
+ allowPhase2Prefilter: allowPhase2
12466
+ });
12467
+ }
12468
+ nestedSelects(select, select).forEach(
12469
+ (nested, index) => collectSelect(
12470
+ nested,
12471
+ `${path}.select-source[${index}]`,
12472
+ candidates,
12473
+ forceForbidden,
12474
+ allowPhase2
12475
+ )
12476
+ );
12477
+ }
12478
+ function collectUnion(union, path, candidates, forceForbidden) {
12479
+ if (union.left.type === "UNION") collectUnion(union.left, `${path}.left`, candidates, forceForbidden);
12480
+ else collectSelect(union.left, `${path}.left`, candidates, forceForbidden);
12481
+ collectSelect(union.right, `${path}.right`, candidates, forceForbidden);
12482
+ }
12483
+ function collectWith(statement, path, candidates, inheritedForbidden) {
12484
+ if (!inheritedForbidden && canInlineSingleCte(statement)) {
12485
+ collectSelect(buildInlinedQuery(statement), `${path}.inlined`, candidates, false);
12486
+ return;
12487
+ }
12488
+ statement.ctes.forEach((cte, index) => {
12489
+ if (cte.query.type === "SELECT") {
12490
+ collectSelect(cte.query, `${path}.cte[${index}]`, candidates, true);
12491
+ } else if (cte.query.type === "UNION") {
12492
+ collectUnion(cte.query, `${path}.cte[${index}]`, candidates, true);
12493
+ }
12494
+ });
12495
+ if (statement.query.type === "SELECT") {
12496
+ collectSelect(statement.query, `${path}.main`, candidates, true);
12497
+ } else {
12498
+ collectUnion(statement.query, `${path}.main`, candidates, true);
12499
+ }
12500
+ }
12501
+ function collectStatement(statement, path, candidates, forceForbidden = false) {
12502
+ switch (statement.type) {
12503
+ case "SELECT":
12504
+ collectSelect(statement, path, candidates, forceForbidden);
12505
+ return;
12506
+ case "UNION":
12507
+ collectUnion(statement, path, candidates, forceForbidden);
12508
+ return;
12509
+ case "WITH":
12510
+ collectWith(statement, path, candidates, forceForbidden);
12511
+ return;
12512
+ case "CREATE_TEMP_TABLE":
12513
+ if (statement.query.type === "WITH") collectWith(statement.query, `${path}.query`, candidates, true);
12514
+ else if (statement.query.type === "UNION") collectUnion(statement.query, `${path}.query`, candidates, true);
12515
+ else collectSelect(statement.query, `${path}.query`, candidates, true);
12516
+ return;
12517
+ case "EXPLAIN":
12518
+ collectStatement(statement.query, `${path}.query`, candidates, forceForbidden);
12519
+ return;
12520
+ case "VALIDATE":
12521
+ case "REORDER": {
12522
+ const functionNames = relativeDateFunctionNamesInWhere(statement.where);
12523
+ if (functionNames.length > 0) {
12524
+ candidates.push({
12525
+ kind: "FORBIDDEN",
12526
+ source: statement,
12527
+ where: statement.where,
12528
+ functionNames,
12529
+ path
12530
+ });
12531
+ }
12532
+ nestedSelects(statement, statement).forEach(
12533
+ (select, index) => collectSelect(select, `${path}.select-source[${index}]`, candidates, true)
12534
+ );
12535
+ return;
12536
+ }
12537
+ case "UPDATE":
12538
+ case "DELETE": {
12539
+ const functionNames = relativeDateFunctionNamesInWhere(statement.where);
12540
+ if (functionNames.length > 0) {
12541
+ const forbidden = forceForbidden || Boolean(statement.subtableCode) || statement.type === "UPDATE" && (statement.from != null || Boolean(statement.applyBlocks?.length));
12542
+ candidates.push({
12543
+ kind: forbidden ? "FORBIDDEN" : "DML",
12544
+ source: statement,
12545
+ where: statement.where,
12546
+ functionNames,
12547
+ path
12548
+ });
12549
+ }
12550
+ if (statement.type === "UPDATE" && statement.applyBlocks?.length) {
12551
+ const applyFunctions = relativeDateFunctionNamesInNode(statement.applyBlocks, true);
12552
+ if (applyFunctions.length > 0) {
12553
+ candidates.push({
12554
+ kind: "FORBIDDEN",
12555
+ source: statement,
12556
+ where: null,
12557
+ functionNames: applyFunctions,
12558
+ path: `${path}.apply`
12559
+ });
12560
+ }
12561
+ }
12562
+ nestedSelects(statement, statement).forEach(
12563
+ (select, index) => collectSelect(
12564
+ select,
12565
+ `${path}.select-source[${index}]`,
12566
+ candidates,
12567
+ forceForbidden,
12568
+ false
12569
+ )
12570
+ );
12571
+ return;
12572
+ }
12573
+ default:
12574
+ nestedSelects(statement, statement).forEach(
12575
+ (select, index) => collectSelect(
12576
+ select,
12577
+ `${path}.select-source[${index}]`,
12578
+ candidates,
12579
+ forceForbidden,
12580
+ false
12581
+ )
12582
+ );
12583
+ }
12584
+ }
12585
+ function serializationContainsFunctions(query, names) {
12586
+ return names.every((name) => new RegExp(`\\b${name}\\s*\\(`).test(query));
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
+ }
12591
+ function rejectedNode(candidate) {
12592
+ return {
12593
+ kind: candidate.kind,
12594
+ source: candidate.source,
12595
+ functionNames: candidate.functionNames,
12596
+ path: candidate.path,
12597
+ clientWhereEvaluation: true,
12598
+ allowed: false
12599
+ };
12600
+ }
12601
+ function relativeDateReasonCodes(capability) {
12602
+ const codes = (capability?.reasons ?? []).filter(
12603
+ (reason) => reason.functionName !== void 0
12604
+ ).map((reason) => reason.code).filter((code) => code.startsWith("WHERE_RELATIVE_DATE_"));
12605
+ if (!codes.includes(WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN)) {
12606
+ codes.push(WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN);
12607
+ }
12608
+ return [...new Set(codes)];
12609
+ }
12610
+ function rejectionFor(candidate, capability) {
12611
+ const reasonCodes = relativeDateReasonCodes(capability);
12612
+ return {
12613
+ functionName: candidate.functionNames[0],
12614
+ path: candidate.path,
12615
+ code: reasonCodes.find(
12616
+ (code) => code !== WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN
12617
+ ) ?? WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN,
12618
+ reasonCodes
12619
+ };
12620
+ }
12621
+ async function buildRelativeDatePushdownPlan(statement, resolver) {
12622
+ const candidates = [];
12623
+ collectStatement(statement, "statement", candidates);
12624
+ const nodes = [];
12625
+ for (const candidate of candidates) {
12626
+ if (candidate.kind === "FORBIDDEN") {
12627
+ const node2 = rejectedNode(candidate);
12628
+ nodes.push(node2);
12629
+ return {
12630
+ hasRelativeDate: true,
12631
+ nodes,
12632
+ allowed: false,
12633
+ rejection: rejectionFor(candidate)
12634
+ };
12635
+ }
12636
+ if (candidate.kind === "SELECT") {
12637
+ const select = candidate.source;
12638
+ const physicalTopLevel = select.from.cteName === null && !select.from.subtableCode && select.joins.length === 0;
12639
+ const selectMode = resolveSelectMode(select);
12640
+ const capability2 = await resolver.select(select);
12641
+ let restQuery2 = "";
12642
+ try {
12643
+ restQuery2 = candidate.where === null ? "" : whereToKintone(candidate.where);
12644
+ } catch {
12645
+ restQuery2 = "";
12646
+ }
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
+ }
12658
+ const node2 = {
12659
+ kind: candidate.kind,
12660
+ source: candidate.source,
12661
+ functionNames: candidate.functionNames,
12662
+ path: candidate.path,
12663
+ selectMode,
12664
+ capability: capability2,
12665
+ restQuery: restQuery2,
12666
+ ...prefilterPlan ? { prefilterPlan, phase2PrefilterEligible } : {},
12667
+ clientWhereEvaluation: !allowed2,
12668
+ allowed: allowed2
12669
+ };
12670
+ nodes.push(node2);
12671
+ if (!allowed2) {
12672
+ return {
12673
+ hasRelativeDate: true,
12674
+ nodes,
12675
+ allowed: false,
12676
+ rejection: rejectionFor(candidate, capability2)
12677
+ };
12678
+ }
12679
+ continue;
12680
+ }
12681
+ const dml = candidate.source;
12682
+ const capability = await resolver.dml(dml);
12683
+ let restQuery = "";
12684
+ try {
12685
+ restQuery = candidate.where === null ? "" : whereToKintone(candidate.where);
12686
+ } catch {
12687
+ restQuery = "";
12688
+ }
12689
+ const allowed = capability.capability === "EXACT_PUSHDOWN" && serializationContainsFunctions(restQuery, candidate.functionNames);
12690
+ const node = {
12691
+ kind: candidate.kind,
12692
+ source: candidate.source,
12693
+ functionNames: candidate.functionNames,
12694
+ path: candidate.path,
12695
+ capability,
12696
+ restQuery,
12697
+ clientWhereEvaluation: !allowed,
12698
+ allowed
12699
+ };
12700
+ nodes.push(node);
12701
+ if (!allowed) {
12702
+ return {
12703
+ hasRelativeDate: true,
12704
+ nodes,
12705
+ allowed: false,
12706
+ rejection: rejectionFor(candidate, capability)
12707
+ };
12708
+ }
12709
+ }
12710
+ return {
12711
+ hasRelativeDate: candidates.length > 0,
12712
+ nodes,
12713
+ allowed: true
12714
+ };
12715
+ }
12716
+ function assertRelativeDatePushdownPlan(plan) {
12717
+ if (!plan.allowed && plan.rejection) {
12718
+ const details = plan.rejection.reasonCodes.filter(
12719
+ (code) => code !== WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN
12720
+ );
12721
+ throw new Error(
12722
+ `${plan.rejection.functionName}: ${WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN}${details.length > 0 ? ` (reason=${details.join(", ")})` : ""} (path=${plan.rejection.path})`
12723
+ );
12724
+ }
12725
+ }
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
+ }
12049
13032
 
12050
13033
  // src/import/sourceLoader.ts
12051
13034
  var IMPORT_MAX_BYTES = 10 * 1024 * 1024;
@@ -13102,7 +14085,29 @@ function attachSearchAbortWarning(result, collector) {
13102
14085
  warnings.add(SEARCH_ABORTED_WARNING);
13103
14086
  return { ...result, warnings: [...warnings] };
13104
14087
  }
14088
+ async function assertRelativeDateExecutionPlan(stmt, client, cacheContext) {
14089
+ const plan = await resolveRelativeDateExecutionPlan(stmt, client, cacheContext);
14090
+ assertRelativeDatePushdownPlan(plan);
14091
+ return plan;
14092
+ }
14093
+ async function resolveRelativeDateExecutionPlan(stmt, client, cacheContext) {
14094
+ return buildRelativeDatePushdownPlan(stmt, {
14095
+ select: (select) => resolveSelectWhereCapability(select, 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
+ }
14106
+ });
14107
+ }
13105
14108
  async function executeParsedStatement(stmt, client, options, cacheContext) {
14109
+ const relativeDatePlan = await resolveRelativeDateExecutionPlan(stmt, client, cacheContext);
14110
+ if (stmt.type !== "EXPLAIN") assertRelativeDatePushdownPlan(relativeDatePlan);
13106
14111
  const unresolved = findVariableRef(stmt);
13107
14112
  if (unresolved !== null && !isApplyParentKlikeStatement(stmt)) {
13108
14113
  throw new Error(`ParseError: variable @${unresolved} is not defined in a batch.`);
@@ -13164,7 +14169,8 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
13164
14169
  options.maxRecords ?? 1e4,
13165
14170
  options.cursorMaxActive ?? 2,
13166
14171
  stmt.query.type === "UPDATE" && stmt.query.applyBlocks?.length ? resolveApplyGuardLimit(options.dmlMaxRows, "dmlMaxRows", DEFAULT_APPLY_MAX_ROWS) : DEFAULT_APPLY_MAX_ROWS,
13167
- stmt.query.type === "UPDATE" && stmt.query.applyBlocks?.length ? resolveApplyGuardLimit(options.dmlMaxSubtableRows, "dmlMaxSubtableRows", DEFAULT_APPLY_MAX_SUBTABLE_ROWS) : DEFAULT_APPLY_MAX_SUBTABLE_ROWS
14172
+ stmt.query.type === "UPDATE" && stmt.query.applyBlocks?.length ? resolveApplyGuardLimit(options.dmlMaxSubtableRows, "dmlMaxSubtableRows", DEFAULT_APPLY_MAX_SUBTABLE_ROWS) : DEFAULT_APPLY_MAX_SUBTABLE_ROWS,
14173
+ relativeDatePlan
13168
14174
  );
13169
14175
  // 一時テーブルはバッチスコープのため単文実行では拒否する(executeBatch を使う)
13170
14176
  case "CREATE_TEMP_TABLE":
@@ -13548,6 +14554,7 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
13548
14554
  if (stmt.type === "SET_VARIABLE") {
13549
14555
  const resolvedStmt2 = resolveBatchVariableReferences(stmt, variables);
13550
14556
  validateKlikeStatement(resolvedStmt2);
14557
+ await assertRelativeDateExecutionPlan(resolvedStmt2, client, cacheContext);
13551
14558
  if (resolvedStmt2.expr.type === "ARRAY") {
13552
14559
  variables.set(stmt.name, {
13553
14560
  type: "array",
@@ -13604,6 +14611,7 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
13604
14611
  assertApplyScope("phase15b", resolvedStmt);
13605
14612
  assertApplyExecutionScope("phase15b", resolvedStmt);
13606
14613
  validateKlikeStatement(resolvedStmt);
14614
+ await assertRelativeDateExecutionPlan(resolvedStmt, client, cacheContext);
13607
14615
  if (resolvedStmt.type === "VALIDATE") {
13608
14616
  const result = await executeExistingRecordValidationCore(
13609
14617
  resolvedStmt,
@@ -14161,16 +15169,27 @@ function hasCanonicalOrder(stmt) {
14161
15169
  (column) => column.type === "WINDOW_COL" && column.orderBy.length > 0
14162
15170
  );
14163
15171
  }
14164
- async function assertDmlWhereCapability(stmt, client, cacheContext) {
14165
- if (stmt.subtableCode || stmt.type === "UPDATE" && stmt.from != null) return;
15172
+ async function resolveDmlWhereCapability(stmt, client, cacheContext) {
15173
+ if (stmt.subtableCode || stmt.type === "UPDATE" && stmt.from != null) {
15174
+ return {
15175
+ capability: "LOCAL_ONLY",
15176
+ reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }]
15177
+ };
15178
+ }
14166
15179
  const fields = whereNeedsFieldMetadata(stmt.where) ? await getFieldsCached(stmt.appId, client, cacheContext) : [];
14167
15180
  const byCode = new Map(fields.map((field) => [field.code, field]));
14168
- if (stmt.where.type === "BOOLEAN" && stmt.where.value === false) return;
14169
- const result = classifyWhereCapability(stmt.where, (field) => {
15181
+ if (stmt.where.type === "BOOLEAN" && stmt.where.value === false) {
15182
+ return classifyWhereCapability(null, () => void 0);
15183
+ }
15184
+ return classifyWhereCapability(stmt.where, (field) => {
14170
15185
  if (field.field === "$id") return resolveFieldSemantics({ fieldType: "__ID__" });
14171
15186
  const info = byCode.get(field.field);
14172
15187
  return info?.semantics ?? (info ? resolveFieldSemantics(info) : void 0);
14173
15188
  });
15189
+ }
15190
+ async function assertDmlWhereCapability(stmt, client, cacheContext) {
15191
+ if (stmt.subtableCode || stmt.type === "UPDATE" && stmt.from != null) return;
15192
+ const result = await resolveDmlWhereCapability(stmt, client, cacheContext);
14174
15193
  if (result.capability !== "EXACT_PUSHDOWN") {
14175
15194
  throw new DmlConvertError(
14176
15195
  `WHERE predicate cannot be represented by kintone REST (${formatWhereCapabilityFailure(result)})`
@@ -14192,6 +15211,14 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
14192
15211
  if (whereCapability.capability === "UNSUPPORTED") {
14193
15212
  throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(whereCapability)}).`);
14194
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
+ }
14195
15222
  const staticMode = resolveSelectMode(stmt);
14196
15223
  const mode = whereCapability.capability === "EXACT_PUSHDOWN" ? staticMode : "FULL_SCAN";
14197
15224
  const orderMeta = await buildOrderByMetaForSelect(stmt, client, cacheContext, cteCache);
@@ -14199,6 +15226,7 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
14199
15226
  stmt,
14200
15227
  staticMode: mode,
14201
15228
  whereCapability: whereCapability.capability,
15229
+ whereReasons: whereCapability.reasons,
14202
15230
  orderSemantics: orderMeta.semantics,
14203
15231
  maxRecords: options.maxRecords ?? 1e4,
14204
15232
  hasKlike: whereHasKlike(stmt.where)
@@ -14221,7 +15249,8 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
14221
15249
  cacheContext,
14222
15250
  cteCache,
14223
15251
  whereCapability.capability === "EXACT_PUSHDOWN",
14224
- orderMeta
15252
+ orderMeta,
15253
+ prefilterPlan
14225
15254
  );
14226
15255
  }
14227
15256
  } catch (error) {
@@ -15139,7 +16168,7 @@ function buildSelectFieldTypeResolvers(stmt, fieldTypesByApp) {
15139
16168
  };
15140
16169
  return { row, having };
15141
16170
  }
15142
- async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache, allowOriginalWherePushdown = true, preloadedOrderMeta) {
16171
+ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache, allowOriginalWherePushdown = true, preloadedOrderMeta, prefilterPlan) {
15143
16172
  const maxRecords = options.maxRecords ?? 1e4;
15144
16173
  const warnings = /* @__PURE__ */ new Set();
15145
16174
  const parallel = options.fetchParallel ?? 1;
@@ -15165,6 +16194,10 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
15165
16194
  validateKlikePushdownPlan(pushdownPlan);
15166
16195
  const mainPushDown = pushdownPlan.mainCondition;
15167
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;
15168
16201
  const constantFalse = isConstantFalseWhere(stmt.where);
15169
16202
  const mainFetch = constantFalse ? Promise.resolve([]) : fetchTableRecordsForFullScan(
15170
16203
  stmt,
@@ -15175,7 +16208,7 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
15175
16208
  true,
15176
16209
  options.onLimitReached ?? "error",
15177
16210
  warnings,
15178
- mainPushDown,
16211
+ mainFetchCondition,
15179
16212
  allowOriginalWherePushdown
15180
16213
  );
15181
16214
  const parallelJoins = [];
@@ -15256,7 +16289,8 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
15256
16289
  havingFieldTypeResolver: fieldTypeResolvers.having,
15257
16290
  havingFieldSemanticsResolver,
15258
16291
  aggregateSortKindResolver,
15259
- appliedKlikes: pushdownPlan.appliedKlikes,
16292
+ appliedKlikes: prefilterPlan?.appliedKlikes ?? pushdownPlan.appliedKlikes,
16293
+ ...prefilterPlan ? { residualWhere: prefilterPlan.residualWhere } : {},
15260
16294
  resolvedGroupingSpec: resolvedGroupingSpecs.get(stmt)
15261
16295
  });
15262
16296
  return { type: "SELECT", rows, columns, rowCount: rows.length, warnings: [...warnings] };
@@ -15394,6 +16428,7 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
15394
16428
  stmt,
15395
16429
  staticMode: "FULL_SCAN",
15396
16430
  whereCapability: whereCapability.capability,
16431
+ whereReasons: whereCapability.reasons,
15397
16432
  orderSemantics: orderMeta.semantics,
15398
16433
  maxRecords,
15399
16434
  hasKlike: whereHasKlike(stmt.where)
@@ -18875,7 +19910,7 @@ async function resolveScalarColumns(columns, client, options, cacheContext, cteC
18875
19910
  }
18876
19911
  var validateExplainInfo = /* @__PURE__ */ new WeakMap();
18877
19912
  var applyParentExplainPlan = /* @__PURE__ */ new WeakMap();
18878
- async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords = 1e4) {
19913
+ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords = 1e4, relativeDatePlan) {
18879
19914
  const fieldApps = /* @__PURE__ */ new Set();
18880
19915
  const processStatusApps = /* @__PURE__ */ new Set();
18881
19916
  const numberPrecisionApps = /* @__PURE__ */ new Set();
@@ -18897,6 +19932,10 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
18897
19932
  const capabilities = /* @__PURE__ */ new Map();
18898
19933
  const orderPlans = /* @__PURE__ */ new Map();
18899
19934
  const seen = /* @__PURE__ */ new Set();
19935
+ const sharedRelativeDatePlan = relativeDatePlan ?? await resolveRelativeDateExecutionPlan(query, tracedClient, cacheContext);
19936
+ const relativeNodeFor = (source) => sharedRelativeDatePlan.nodes.find(
19937
+ (node) => node.source === source || JSON.stringify(node.source) === JSON.stringify(source)
19938
+ );
18900
19939
  const visit = async (node) => {
18901
19940
  if (node === null || typeof node !== "object") return;
18902
19941
  if (seen.has(node)) return;
@@ -18915,7 +19954,8 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
18915
19954
  physicalApps.forEach((appId) => fieldApps.add(appId));
18916
19955
  }
18917
19956
  const capability = await resolveSelectWhereCapability(select, tracedClient, cacheContext);
18918
- if (capability.capability === "UNSUPPORTED") {
19957
+ const relativeNode = relativeNodeFor(select);
19958
+ if (capability.capability === "UNSUPPORTED" && !relativeNode) {
18919
19959
  throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
18920
19960
  }
18921
19961
  capabilities.set(select, capability);
@@ -18929,12 +19969,13 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
18929
19969
  }
18930
19970
  }
18931
19971
  const hasUnmaterializedSource = [select.from, ...select.joins.map((join2) => join2.table)].some((table) => table.cteName !== null);
18932
- if (hasCanonicalOrder(select) && !hasUnmaterializedSource) {
19972
+ if (hasCanonicalOrder(select) && !hasUnmaterializedSource && relativeNode?.allowed !== false) {
18933
19973
  const mode = capability.capability === "EXACT_PUSHDOWN" ? resolveSelectMode(select) : "FULL_SCAN";
18934
19974
  orderPlans.set(select, (select.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
18935
19975
  stmt: select,
18936
19976
  staticMode: mode,
18937
19977
  whereCapability: capability.capability,
19978
+ whereReasons: capability.reasons,
18938
19979
  orderSemantics: meta.semantics,
18939
19980
  maxRecords,
18940
19981
  hasKlike: whereHasKlike(select.where)
@@ -19010,7 +20051,7 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
19010
20051
  }
19011
20052
  }
19012
20053
  const dml = node;
19013
- if (dml.type !== "UPDATE" || !usesApplyParentResidualSelection(dml)) {
20054
+ if ((dml.type !== "UPDATE" || !usesApplyParentResidualSelection(dml)) && relativeNodeFor(dml)?.allowed !== false) {
19014
20055
  await assertDmlWhereCapability(dml, tracedClient, cacheContext);
19015
20056
  }
19016
20057
  }
@@ -19020,23 +20061,32 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
19020
20061
  if (typeof query === "object" && query !== null && query.type === "WITH" && canInlineSingleCte(query)) {
19021
20062
  const inlined = buildInlinedQuery(query);
19022
20063
  const capability = await resolveSelectWhereCapability(inlined, tracedClient, cacheContext);
19023
- if (capability.capability === "UNSUPPORTED") {
20064
+ const relativeNode = relativeNodeFor(inlined);
20065
+ if (capability.capability === "UNSUPPORTED" && !relativeNode) {
19024
20066
  throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
19025
20067
  }
19026
20068
  capabilities.set(inlined, capability);
19027
- if (hasCanonicalOrder(inlined)) {
20069
+ if (hasCanonicalOrder(inlined) && relativeNode?.allowed !== false) {
19028
20070
  const meta = await buildOrderByMetaForSelect(inlined, tracedClient, cacheContext);
19029
20071
  orderPlans.set(inlined, (inlined.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
19030
20072
  stmt: inlined,
19031
20073
  staticMode: capability.capability === "EXACT_PUSHDOWN" ? resolveSelectMode(inlined) : "FULL_SCAN",
19032
20074
  whereCapability: capability.capability,
20075
+ whereReasons: capability.reasons,
19033
20076
  orderSemantics: meta.semantics,
19034
20077
  maxRecords,
19035
20078
  hasKlike: whereHasKlike(inlined.where)
19036
20079
  }));
19037
20080
  }
19038
20081
  }
19039
- return { capabilities, orderPlans, fieldApps, processStatusApps, numberPrecisionApps };
20082
+ return {
20083
+ capabilities,
20084
+ orderPlans,
20085
+ fieldApps,
20086
+ processStatusApps,
20087
+ numberPrecisionApps,
20088
+ relativeDatePlan: sharedRelativeDatePlan
20089
+ };
19040
20090
  }
19041
20091
  function explainMetadataLines(analysis) {
19042
20092
  return [
@@ -19045,6 +20095,158 @@ function explainMetadataLines(analysis) {
19045
20095
  ...[...analysis.numberPrecisionApps].sort((a, b) => a - b).map((appId) => ` metadata API: number precision APP${appId}`)
19046
20096
  ];
19047
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
+ }
20194
+ function relativeDateExplainLines(plan) {
20195
+ if (!plan.hasRelativeDate) return [];
20196
+ if (!plan.allowed && plan.rejection) {
20197
+ return [
20198
+ ` relative date function: ${plan.rejection.functionName}`,
20199
+ " plan status: rejected",
20200
+ ` reason: ${plan.rejection.reasonCodes.join(", ")}`,
20201
+ " client evaluation: forbidden",
20202
+ " records/cursor/mutation API during EXPLAIN: none"
20203
+ ];
20204
+ }
20205
+ const lines = [];
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
+ }
20233
+ for (const functionName of node.functionNames) {
20234
+ const detail = node.capability?.reasons.find(
20235
+ (reason) => reason.functionName === functionName
20236
+ );
20237
+ lines.push(
20238
+ ` relative date function: ${functionName}`,
20239
+ " evaluation: kintone server",
20240
+ ` field: ${detail?.field ?? "(unknown)"} (${detail?.fieldType ?? "unknown"})`,
20241
+ ` operator: ${detail?.operator ?? "(unknown)"}`,
20242
+ ` where capability: ${node.capability?.capability ?? "(unknown)"}`,
20243
+ " client evaluation: forbidden",
20244
+ ` kintone query: ${node.restQuery || "(\u306A\u3057)"}`
20245
+ );
20246
+ }
20247
+ }
20248
+ return lines;
20249
+ }
19048
20250
  async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4, cursorMaxActive = 2, enableImport = false, dmlMaxRows = 100, dmlMaxSubtableRows = DEFAULT_APPLY_MAX_SUBTABLE_ROWS) {
19049
20251
  const statements = parseSqlBatch(sql, enableImport);
19050
20252
  const analysis = analyzeBatch(statements);
@@ -19055,15 +20257,25 @@ async function buildBatchExplainPlans(sql, client, injectedVariables, cacheConte
19055
20257
  const stmt = statements[i];
19056
20258
  const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr: resolveBatchVariableReferences(stmt.expr, variables) } : stmt : resolveBatchVariableReferences(stmt, variables);
19057
20259
  validateKlikeStatement(planStmt);
19058
- const whereAnalysis = await buildExplainWhereAnalysis(planStmt, client, cacheContext, maxRecords);
19059
- const statementPlan = addCursorConcurrency(buildBatchStatementPlan(
20260
+ const relativeDatePlan = await resolveRelativeDateExecutionPlan(planStmt, client, cacheContext);
20261
+ const whereAnalysis = await buildExplainWhereAnalysis(
19060
20262
  planStmt,
19061
- analysis.statements[i],
19062
- whereAnalysis.capabilities,
19063
- whereAnalysis.orderPlans,
19064
- dmlMaxRows,
19065
- dmlMaxSubtableRows
19066
- ), cursorMaxActive);
20263
+ client,
20264
+ cacheContext,
20265
+ maxRecords,
20266
+ relativeDatePlan
20267
+ );
20268
+ const statementPlan = relativeDatePlan.hasRelativeDate && !relativeDatePlan.allowed ? relativeDateExplainLines(relativeDatePlan) : [
20269
+ ...relativeDateExplainLines(relativeDatePlan),
20270
+ ...addCursorConcurrency(buildBatchStatementPlan(
20271
+ planStmt,
20272
+ analysis.statements[i],
20273
+ whereAnalysis.capabilities,
20274
+ whereAnalysis.orderPlans,
20275
+ dmlMaxRows,
20276
+ dmlMaxSubtableRows
20277
+ ), cursorMaxActive)
20278
+ ];
19067
20279
  const metadataPlan = explainMetadataLines(whereAnalysis);
19068
20280
  plans.push({
19069
20281
  index: i,
@@ -19172,10 +20384,19 @@ function buildPlanForBatchQuery(query, info, capabilities, orderPlans) {
19172
20384
  lines.push(" note: \u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u3078\u306E WHERE \u30D7\u30C3\u30B7\u30E5\u30C0\u30A6\u30F3\u306F\u884C\u308F\u308C\u306A\u3044");
19173
20385
  return lines;
19174
20386
  }
19175
- async function executeExplain(stmt, client, cacheContext, maxRecords, cursorMaxActive, dmlMaxRows = 100, dmlMaxSubtableRows = DEFAULT_APPLY_MAX_SUBTABLE_ROWS) {
19176
- const analysis = await buildExplainWhereAnalysis(stmt.query, client, cacheContext, maxRecords);
19177
- const lines = [
20387
+ async function executeExplain(stmt, client, cacheContext, maxRecords, cursorMaxActive, dmlMaxRows = 100, dmlMaxSubtableRows = DEFAULT_APPLY_MAX_SUBTABLE_ROWS, relativeDatePlan) {
20388
+ const sharedPlan = relativeDatePlan ?? await resolveRelativeDateExecutionPlan(stmt.query, client, cacheContext);
20389
+ const analysis = await buildExplainWhereAnalysis(
20390
+ stmt.query,
20391
+ client,
20392
+ cacheContext,
20393
+ maxRecords,
20394
+ sharedPlan
20395
+ );
20396
+ const relativeLines = relativeDateExplainLines(sharedPlan);
20397
+ const lines = sharedPlan.hasRelativeDate && !sharedPlan.allowed ? [...explainMetadataLines(analysis), ...relativeLines] : [
19178
20398
  ...explainMetadataLines(analysis),
20399
+ ...relativeLines,
19179
20400
  ...addCursorConcurrency(
19180
20401
  buildExplainPlan(
19181
20402
  stmt.query,