@rex0220/kintone-sql-tools 3.18.0 → 3.20.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) {
@@ -11861,6 +12090,13 @@ function deepClone(value, seen = /* @__PURE__ */ new Map()) {
11861
12090
 
11862
12091
  // src/core/optimization/whereCapability.ts
11863
12092
  var RANGE_AND_EQUALITY = ["=", "!=", ">", "<", ">=", "<="];
12093
+ var RELATIVE_DATE_FIELD_TYPES = /* @__PURE__ */ new Set([
12094
+ "DATE",
12095
+ "DATETIME",
12096
+ "CREATED_TIME",
12097
+ "UPDATED_TIME"
12098
+ ]);
12099
+ var RELATIVE_DATE_OPERATORS = new Set(RANGE_AND_EQUALITY);
11864
12100
  var EQUALITY_IN = ["=", "!=", "in", "not in"];
11865
12101
  var NATIVE_OPERATORS = /* @__PURE__ */ new Map([
11866
12102
  ["RECORD_NUMBER", /* @__PURE__ */ new Set([...RANGE_AND_EQUALITY, "in", "not in"])],
@@ -11936,7 +12172,7 @@ function classifyNode(where, resolveField2) {
11936
12172
  case "BOOLEAN":
11937
12173
  return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
11938
12174
  case "BINARY":
11939
- return classifyBinary(where.op, where.left, where.right.type, resolveField2);
12175
+ return classifyBinary(where.op, where.left, where.right, resolveField2);
11940
12176
  case "NULL_CHECK":
11941
12177
  if (where.field.type !== "FIELD") return localExpression();
11942
12178
  return classifyLocalOnlyField(where.field, where.not ? "IS NOT NULL" : "IS NULL", resolveField2);
@@ -11946,7 +12182,14 @@ function classifyNode(where, resolveField2) {
11946
12182
  return classifyNode(where.expr, resolveField2);
11947
12183
  case "NOT": {
11948
12184
  const inner = classifyNode(where.expr, resolveField2);
11949
- return inner.capability === "SUPERSET_PREFILTER" ? { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] } : inner;
12185
+ if (inner.capability !== "SUPERSET_PREFILTER") return inner;
12186
+ if (!hasRelativeDateReason(inner.reasons)) {
12187
+ return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
12188
+ }
12189
+ return requireExactRelativeDatePushdown({
12190
+ capability: "LOCAL_ONLY",
12191
+ reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }, ...inner.reasons]
12192
+ });
11950
12193
  }
11951
12194
  case "LOGICAL": {
11952
12195
  const left = classifyNode(where.left, resolveField2);
@@ -11955,7 +12198,10 @@ function classifyNode(where, resolveField2) {
11955
12198
  }
11956
12199
  }
11957
12200
  }
11958
- function classifyBinary(op, left, rightType, resolveField2) {
12201
+ function classifyBinary(op, left, right, resolveField2) {
12202
+ if (right.type === "KINTONE_FUNC" && isRelativeDateFunctionName(right.name)) {
12203
+ return classifyRelativeDateBinary(op, left, right, resolveField2);
12204
+ }
11959
12205
  if (left.type !== "FIELD") return localExpression();
11960
12206
  const semantics = resolveField2(left);
11961
12207
  if (!semantics) {
@@ -11966,7 +12212,7 @@ function classifyBinary(op, left, rightType, resolveField2) {
11966
12212
  }
11967
12213
  const nativeOp = normalizeOperator(op);
11968
12214
  const native = nativeWhereOperatorsForType(semantics.fieldType);
11969
- const rightCanPush = rightType === "STRING" || rightType === "NUMBER" || rightType === "IN_LIST" || rightType === "KINTONE_FUNC";
12215
+ const rightCanPush = right.type === "STRING" || right.type === "NUMBER" || right.type === "IN_LIST" || isLegacyKintoneFunction(right);
11970
12216
  const structureAllows = !semantics.requiresCollectionOperators || nativeOp !== "=" && nativeOp !== "!=";
11971
12217
  const sqlLikeIsResidual = op === "LIKE" || op === "NOT_LIKE";
11972
12218
  if (rightCanPush && structureAllows && native.has(nativeOp) && !sqlLikeIsResidual) {
@@ -11990,6 +12236,92 @@ function classifyBinary(op, left, rightType, resolveField2) {
11990
12236
  }]
11991
12237
  };
11992
12238
  }
12239
+ function isLegacyKintoneFunction(value) {
12240
+ return value.type === "KINTONE_FUNC" && (value.name === "TODAY" || value.name === "NOW" || value.name === "LOGINUSER");
12241
+ }
12242
+ function classifyRelativeDateBinary(op, left, right, resolveField2) {
12243
+ const operator = normalizeOperator(op);
12244
+ const functionName = right.name;
12245
+ if (left.type !== "FIELD") {
12246
+ return relativeDateUnsupported(
12247
+ "WHERE_RELATIVE_DATE_CONTEXT_UNSUPPORTED",
12248
+ functionName,
12249
+ void 0,
12250
+ void 0,
12251
+ operator
12252
+ );
12253
+ }
12254
+ const semantics = resolveField2(left);
12255
+ if (!semantics) {
12256
+ return relativeDateUnsupported(
12257
+ "WHERE_RELATIVE_DATE_FIELD_TYPE_UNSUPPORTED",
12258
+ functionName,
12259
+ left.field,
12260
+ void 0,
12261
+ operator
12262
+ );
12263
+ }
12264
+ if (!hasValidRelativeDateArguments(right)) {
12265
+ return relativeDateUnsupported(
12266
+ "WHERE_RELATIVE_DATE_ARGUMENT_INVALID",
12267
+ functionName,
12268
+ left.field,
12269
+ semantics.fieldType,
12270
+ operator
12271
+ );
12272
+ }
12273
+ if (!RELATIVE_DATE_OPERATORS.has(operator)) {
12274
+ return relativeDateUnsupported(
12275
+ "WHERE_RELATIVE_DATE_OPERATOR_UNSUPPORTED",
12276
+ functionName,
12277
+ left.field,
12278
+ semantics.fieldType,
12279
+ operator
12280
+ );
12281
+ }
12282
+ if (!RELATIVE_DATE_FIELD_TYPES.has(semantics.fieldType) || semantics.inSubtable || semantics.requiresCollectionOperators) {
12283
+ return relativeDateUnsupported(
12284
+ "WHERE_RELATIVE_DATE_FIELD_TYPE_UNSUPPORTED",
12285
+ functionName,
12286
+ left.field,
12287
+ semantics.fieldType,
12288
+ operator
12289
+ );
12290
+ }
12291
+ return {
12292
+ capability: "EXACT_PUSHDOWN",
12293
+ reasons: [{
12294
+ code: "WHERE_EXACT",
12295
+ functionName,
12296
+ field: left.field,
12297
+ fieldType: semantics.fieldType,
12298
+ operator
12299
+ }]
12300
+ };
12301
+ }
12302
+ function hasValidRelativeDateArguments(value) {
12303
+ if (!("args" in value) || !value.args) return false;
12304
+ switch (value.name) {
12305
+ case "YESTERDAY":
12306
+ case "TOMORROW":
12307
+ case "THIS_YEAR":
12308
+ case "LAST_YEAR":
12309
+ case "NEXT_YEAR":
12310
+ return value.args.kind === "NONE";
12311
+ case "FROM_TODAY":
12312
+ 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");
12313
+ case "THIS_WEEK":
12314
+ case "LAST_WEEK":
12315
+ case "NEXT_WEEK":
12316
+ 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");
12317
+ case "THIS_MONTH":
12318
+ case "LAST_MONTH":
12319
+ case "NEXT_MONTH":
12320
+ 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);
12321
+ default:
12322
+ return false;
12323
+ }
12324
+ }
11993
12325
  function classifyLocalOnlyField(field, operator, resolveField2) {
11994
12326
  const semantics = resolveField2(field);
11995
12327
  if (!semantics) return unsupported2("WHERE_FIELD_UNRESOLVED", field.field, void 0, operator);
@@ -12027,18 +12359,18 @@ function normalizeOperator(op) {
12027
12359
  function combineLogical(op, left, right) {
12028
12360
  const reasons = [...left.reasons, ...right.reasons];
12029
12361
  if (left.capability === "UNSUPPORTED" || right.capability === "UNSUPPORTED") {
12030
- return { capability: "UNSUPPORTED", reasons };
12362
+ return requireExactRelativeDatePushdown({ capability: "UNSUPPORTED", reasons });
12031
12363
  }
12032
12364
  if (left.capability === "EXACT_PUSHDOWN" && right.capability === "EXACT_PUSHDOWN") {
12033
12365
  return { capability: "EXACT_PUSHDOWN", reasons };
12034
12366
  }
12035
12367
  if (op === "AND" && (left.capability === "EXACT_PUSHDOWN" || right.capability === "EXACT_PUSHDOWN" || left.capability === "SUPERSET_PREFILTER" || right.capability === "SUPERSET_PREFILTER")) {
12036
- return {
12368
+ return requireExactRelativeDatePushdown({
12037
12369
  capability: "SUPERSET_PREFILTER",
12038
12370
  reasons: [{ code: "WHERE_SUPERSET_PREFILTER" }, ...reasons]
12039
- };
12371
+ });
12040
12372
  }
12041
- return { capability: "LOCAL_ONLY", reasons };
12373
+ return requireExactRelativeDatePushdown({ capability: "LOCAL_ONLY", reasons });
12042
12374
  }
12043
12375
  function localExpression() {
12044
12376
  return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
@@ -12046,6 +12378,317 @@ function localExpression() {
12046
12378
  function unsupported2(code, field, fieldType, operator) {
12047
12379
  return { capability: "UNSUPPORTED", reasons: [{ code, field, fieldType, operator }] };
12048
12380
  }
12381
+ function relativeDateUnsupported(code, functionName, field, fieldType, operator) {
12382
+ return requireExactRelativeDatePushdown({
12383
+ capability: "UNSUPPORTED",
12384
+ reasons: [{ code, functionName, field, fieldType, operator }]
12385
+ });
12386
+ }
12387
+ function hasRelativeDateReason(reasons) {
12388
+ return reasons.some((reason) => reason.functionName !== void 0);
12389
+ }
12390
+ function requireExactRelativeDatePushdown(result) {
12391
+ if (result.capability === "EXACT_PUSHDOWN" || !hasRelativeDateReason(result.reasons) || result.reasons.some((reason) => reason.code === "WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN")) {
12392
+ return result;
12393
+ }
12394
+ const relative = result.reasons.find((reason) => reason.functionName !== void 0);
12395
+ return {
12396
+ capability: result.capability,
12397
+ reasons: [
12398
+ ...result.reasons,
12399
+ {
12400
+ code: "WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN",
12401
+ functionName: relative.functionName,
12402
+ field: relative.field,
12403
+ fieldType: relative.fieldType,
12404
+ operator: relative.operator
12405
+ }
12406
+ ]
12407
+ };
12408
+ }
12409
+
12410
+ // src/core/optimization/relativeDatePushdownGuard.ts
12411
+ function relativeDateFunctionNamesInNode(node, stopAtNestedSelect) {
12412
+ const names = [];
12413
+ const seen = /* @__PURE__ */ new Set();
12414
+ const visit = (valueNode) => {
12415
+ if (Array.isArray(valueNode)) {
12416
+ valueNode.forEach(visit);
12417
+ return;
12418
+ }
12419
+ if (valueNode === null || typeof valueNode !== "object") return;
12420
+ const value = valueNode;
12421
+ if (value["type"] === "KINTONE_FUNC" && typeof value["name"] === "string" && isRelativeDateFunctionName(value["name"]) && !seen.has(value["name"])) {
12422
+ seen.add(value["name"]);
12423
+ names.push(value["name"]);
12424
+ return;
12425
+ }
12426
+ if (stopAtNestedSelect && value["type"] === "SELECT") return;
12427
+ Object.values(value).forEach(visit);
12428
+ };
12429
+ visit(node);
12430
+ return names;
12431
+ }
12432
+ function relativeDateFunctionNamesInWhere(where) {
12433
+ return relativeDateFunctionNamesInNode(where, true);
12434
+ }
12435
+ function nestedSelects(node, root) {
12436
+ const found = [];
12437
+ const seen = /* @__PURE__ */ new Set();
12438
+ const visit = (value) => {
12439
+ if (Array.isArray(value)) {
12440
+ value.forEach(visit);
12441
+ return;
12442
+ }
12443
+ if (value === null || typeof value !== "object" || seen.has(value)) return;
12444
+ seen.add(value);
12445
+ const object = value;
12446
+ if (object["type"] === "SELECT" && value !== root) {
12447
+ found.push(value);
12448
+ return;
12449
+ }
12450
+ Object.values(object).forEach(visit);
12451
+ };
12452
+ visit(node);
12453
+ return found;
12454
+ }
12455
+ function collectSelect(select, path, candidates, forceForbidden) {
12456
+ const functionNames = relativeDateFunctionNamesInWhere(select.where);
12457
+ if (functionNames.length > 0) {
12458
+ candidates.push({
12459
+ kind: forceForbidden ? "FORBIDDEN" : "SELECT",
12460
+ source: select,
12461
+ where: select.where,
12462
+ functionNames,
12463
+ path
12464
+ });
12465
+ }
12466
+ nestedSelects(select, select).forEach(
12467
+ (nested, index) => collectSelect(nested, `${path}.select-source[${index}]`, candidates, forceForbidden)
12468
+ );
12469
+ }
12470
+ function collectUnion(union, path, candidates, forceForbidden) {
12471
+ if (union.left.type === "UNION") collectUnion(union.left, `${path}.left`, candidates, forceForbidden);
12472
+ else collectSelect(union.left, `${path}.left`, candidates, forceForbidden);
12473
+ collectSelect(union.right, `${path}.right`, candidates, forceForbidden);
12474
+ }
12475
+ function collectWith(statement, path, candidates, inheritedForbidden) {
12476
+ if (!inheritedForbidden && canInlineSingleCte(statement)) {
12477
+ collectSelect(buildInlinedQuery(statement), `${path}.inlined`, candidates, false);
12478
+ return;
12479
+ }
12480
+ statement.ctes.forEach((cte, index) => {
12481
+ if (cte.query.type === "SELECT") {
12482
+ collectSelect(cte.query, `${path}.cte[${index}]`, candidates, true);
12483
+ } else if (cte.query.type === "UNION") {
12484
+ collectUnion(cte.query, `${path}.cte[${index}]`, candidates, true);
12485
+ }
12486
+ });
12487
+ if (statement.query.type === "SELECT") {
12488
+ collectSelect(statement.query, `${path}.main`, candidates, true);
12489
+ } else {
12490
+ collectUnion(statement.query, `${path}.main`, candidates, true);
12491
+ }
12492
+ }
12493
+ function collectStatement(statement, path, candidates, forceForbidden = false) {
12494
+ switch (statement.type) {
12495
+ case "SELECT":
12496
+ collectSelect(statement, path, candidates, forceForbidden);
12497
+ return;
12498
+ case "UNION":
12499
+ collectUnion(statement, path, candidates, forceForbidden);
12500
+ return;
12501
+ case "WITH":
12502
+ collectWith(statement, path, candidates, forceForbidden);
12503
+ return;
12504
+ case "CREATE_TEMP_TABLE":
12505
+ if (statement.query.type === "WITH") collectWith(statement.query, `${path}.query`, candidates, true);
12506
+ else if (statement.query.type === "UNION") collectUnion(statement.query, `${path}.query`, candidates, true);
12507
+ else collectSelect(statement.query, `${path}.query`, candidates, true);
12508
+ return;
12509
+ case "EXPLAIN":
12510
+ collectStatement(statement.query, `${path}.query`, candidates, forceForbidden);
12511
+ return;
12512
+ case "VALIDATE":
12513
+ case "REORDER": {
12514
+ const functionNames = relativeDateFunctionNamesInWhere(statement.where);
12515
+ if (functionNames.length > 0) {
12516
+ candidates.push({
12517
+ kind: "FORBIDDEN",
12518
+ source: statement,
12519
+ where: statement.where,
12520
+ functionNames,
12521
+ path
12522
+ });
12523
+ }
12524
+ nestedSelects(statement, statement).forEach(
12525
+ (select, index) => collectSelect(select, `${path}.select-source[${index}]`, candidates, true)
12526
+ );
12527
+ return;
12528
+ }
12529
+ case "UPDATE":
12530
+ case "DELETE": {
12531
+ const functionNames = relativeDateFunctionNamesInWhere(statement.where);
12532
+ if (functionNames.length > 0) {
12533
+ const forbidden = forceForbidden || Boolean(statement.subtableCode) || statement.type === "UPDATE" && (statement.from != null || Boolean(statement.applyBlocks?.length));
12534
+ candidates.push({
12535
+ kind: forbidden ? "FORBIDDEN" : "DML",
12536
+ source: statement,
12537
+ where: statement.where,
12538
+ functionNames,
12539
+ path
12540
+ });
12541
+ }
12542
+ if (statement.type === "UPDATE" && statement.applyBlocks?.length) {
12543
+ const applyFunctions = relativeDateFunctionNamesInNode(statement.applyBlocks, true);
12544
+ if (applyFunctions.length > 0) {
12545
+ candidates.push({
12546
+ kind: "FORBIDDEN",
12547
+ source: statement,
12548
+ where: null,
12549
+ functionNames: applyFunctions,
12550
+ path: `${path}.apply`
12551
+ });
12552
+ }
12553
+ }
12554
+ nestedSelects(statement, statement).forEach(
12555
+ (select, index) => collectSelect(select, `${path}.select-source[${index}]`, candidates, forceForbidden)
12556
+ );
12557
+ return;
12558
+ }
12559
+ default:
12560
+ nestedSelects(statement, statement).forEach(
12561
+ (select, index) => collectSelect(select, `${path}.select-source[${index}]`, candidates, forceForbidden)
12562
+ );
12563
+ }
12564
+ }
12565
+ function serializationContainsFunctions(query, names) {
12566
+ return names.every((name) => new RegExp(`\\b${name}\\s*\\(`).test(query));
12567
+ }
12568
+ function rejectedNode(candidate) {
12569
+ return {
12570
+ kind: candidate.kind,
12571
+ source: candidate.source,
12572
+ functionNames: candidate.functionNames,
12573
+ path: candidate.path,
12574
+ clientWhereEvaluation: true,
12575
+ allowed: false
12576
+ };
12577
+ }
12578
+ function relativeDateReasonCodes(capability) {
12579
+ const codes = (capability?.reasons ?? []).filter(
12580
+ (reason) => reason.functionName !== void 0
12581
+ ).map((reason) => reason.code).filter((code) => code.startsWith("WHERE_RELATIVE_DATE_"));
12582
+ if (!codes.includes(WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN)) {
12583
+ codes.push(WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN);
12584
+ }
12585
+ return [...new Set(codes)];
12586
+ }
12587
+ function rejectionFor(candidate, capability) {
12588
+ const reasonCodes = relativeDateReasonCodes(capability);
12589
+ return {
12590
+ functionName: candidate.functionNames[0],
12591
+ path: candidate.path,
12592
+ code: reasonCodes.find(
12593
+ (code) => code !== WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN
12594
+ ) ?? WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN,
12595
+ reasonCodes
12596
+ };
12597
+ }
12598
+ async function buildRelativeDatePushdownPlan(statement, resolver) {
12599
+ const candidates = [];
12600
+ collectStatement(statement, "statement", candidates);
12601
+ const nodes = [];
12602
+ for (const candidate of candidates) {
12603
+ if (candidate.kind === "FORBIDDEN") {
12604
+ const node2 = rejectedNode(candidate);
12605
+ nodes.push(node2);
12606
+ return {
12607
+ hasRelativeDate: true,
12608
+ nodes,
12609
+ allowed: false,
12610
+ rejection: rejectionFor(candidate)
12611
+ };
12612
+ }
12613
+ if (candidate.kind === "SELECT") {
12614
+ const select = candidate.source;
12615
+ const physicalTopLevel = select.from.cteName === null && !select.from.subtableCode && select.joins.length === 0;
12616
+ const selectMode = resolveSelectMode(select);
12617
+ const capability2 = await resolver.select(select);
12618
+ let restQuery2 = "";
12619
+ try {
12620
+ restQuery2 = candidate.where === null ? "" : whereToKintone(candidate.where);
12621
+ } catch {
12622
+ restQuery2 = "";
12623
+ }
12624
+ const allowed2 = physicalTopLevel && selectMode === "SIMPLE" && (select.orderBy.length === 0 || select.orderMode === "KINTONE_NATIVE") && capability2.capability === "EXACT_PUSHDOWN" && serializationContainsFunctions(restQuery2, candidate.functionNames);
12625
+ const node2 = {
12626
+ kind: candidate.kind,
12627
+ source: candidate.source,
12628
+ functionNames: candidate.functionNames,
12629
+ path: candidate.path,
12630
+ selectMode,
12631
+ capability: capability2,
12632
+ restQuery: restQuery2,
12633
+ clientWhereEvaluation: !allowed2,
12634
+ allowed: allowed2
12635
+ };
12636
+ nodes.push(node2);
12637
+ if (!allowed2) {
12638
+ return {
12639
+ hasRelativeDate: true,
12640
+ nodes,
12641
+ allowed: false,
12642
+ rejection: rejectionFor(candidate, capability2)
12643
+ };
12644
+ }
12645
+ continue;
12646
+ }
12647
+ const dml = candidate.source;
12648
+ const capability = await resolver.dml(dml);
12649
+ let restQuery = "";
12650
+ try {
12651
+ restQuery = candidate.where === null ? "" : whereToKintone(candidate.where);
12652
+ } catch {
12653
+ restQuery = "";
12654
+ }
12655
+ const allowed = capability.capability === "EXACT_PUSHDOWN" && serializationContainsFunctions(restQuery, candidate.functionNames);
12656
+ const node = {
12657
+ kind: candidate.kind,
12658
+ source: candidate.source,
12659
+ functionNames: candidate.functionNames,
12660
+ path: candidate.path,
12661
+ capability,
12662
+ restQuery,
12663
+ clientWhereEvaluation: !allowed,
12664
+ allowed
12665
+ };
12666
+ nodes.push(node);
12667
+ if (!allowed) {
12668
+ return {
12669
+ hasRelativeDate: true,
12670
+ nodes,
12671
+ allowed: false,
12672
+ rejection: rejectionFor(candidate, capability)
12673
+ };
12674
+ }
12675
+ }
12676
+ return {
12677
+ hasRelativeDate: candidates.length > 0,
12678
+ nodes,
12679
+ allowed: true
12680
+ };
12681
+ }
12682
+ function assertRelativeDatePushdownPlan(plan) {
12683
+ if (!plan.allowed && plan.rejection) {
12684
+ const details = plan.rejection.reasonCodes.filter(
12685
+ (code) => code !== WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN
12686
+ );
12687
+ throw new Error(
12688
+ `${plan.rejection.functionName}: ${WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN}${details.length > 0 ? ` (reason=${details.join(", ")})` : ""} (path=${plan.rejection.path})`
12689
+ );
12690
+ }
12691
+ }
12049
12692
 
12050
12693
  // src/import/sourceLoader.ts
12051
12694
  var IMPORT_MAX_BYTES = 10 * 1024 * 1024;
@@ -13102,7 +13745,20 @@ function attachSearchAbortWarning(result, collector) {
13102
13745
  warnings.add(SEARCH_ABORTED_WARNING);
13103
13746
  return { ...result, warnings: [...warnings] };
13104
13747
  }
13748
+ async function assertRelativeDateExecutionPlan(stmt, client, cacheContext) {
13749
+ const plan = await resolveRelativeDateExecutionPlan(stmt, client, cacheContext);
13750
+ assertRelativeDatePushdownPlan(plan);
13751
+ return plan;
13752
+ }
13753
+ async function resolveRelativeDateExecutionPlan(stmt, client, cacheContext) {
13754
+ return buildRelativeDatePushdownPlan(stmt, {
13755
+ select: (select) => resolveSelectWhereCapability(select, client, cacheContext),
13756
+ dml: (dml) => resolveDmlWhereCapability(dml, client, cacheContext)
13757
+ });
13758
+ }
13105
13759
  async function executeParsedStatement(stmt, client, options, cacheContext) {
13760
+ const relativeDatePlan = await resolveRelativeDateExecutionPlan(stmt, client, cacheContext);
13761
+ if (stmt.type !== "EXPLAIN") assertRelativeDatePushdownPlan(relativeDatePlan);
13106
13762
  const unresolved = findVariableRef(stmt);
13107
13763
  if (unresolved !== null && !isApplyParentKlikeStatement(stmt)) {
13108
13764
  throw new Error(`ParseError: variable @${unresolved} is not defined in a batch.`);
@@ -13164,7 +13820,8 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
13164
13820
  options.maxRecords ?? 1e4,
13165
13821
  options.cursorMaxActive ?? 2,
13166
13822
  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
13823
+ stmt.query.type === "UPDATE" && stmt.query.applyBlocks?.length ? resolveApplyGuardLimit(options.dmlMaxSubtableRows, "dmlMaxSubtableRows", DEFAULT_APPLY_MAX_SUBTABLE_ROWS) : DEFAULT_APPLY_MAX_SUBTABLE_ROWS,
13824
+ relativeDatePlan
13168
13825
  );
13169
13826
  // 一時テーブルはバッチスコープのため単文実行では拒否する(executeBatch を使う)
13170
13827
  case "CREATE_TEMP_TABLE":
@@ -13548,6 +14205,7 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
13548
14205
  if (stmt.type === "SET_VARIABLE") {
13549
14206
  const resolvedStmt2 = resolveBatchVariableReferences(stmt, variables);
13550
14207
  validateKlikeStatement(resolvedStmt2);
14208
+ await assertRelativeDateExecutionPlan(resolvedStmt2, client, cacheContext);
13551
14209
  if (resolvedStmt2.expr.type === "ARRAY") {
13552
14210
  variables.set(stmt.name, {
13553
14211
  type: "array",
@@ -13604,6 +14262,7 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
13604
14262
  assertApplyScope("phase15b", resolvedStmt);
13605
14263
  assertApplyExecutionScope("phase15b", resolvedStmt);
13606
14264
  validateKlikeStatement(resolvedStmt);
14265
+ await assertRelativeDateExecutionPlan(resolvedStmt, client, cacheContext);
13607
14266
  if (resolvedStmt.type === "VALIDATE") {
13608
14267
  const result = await executeExistingRecordValidationCore(
13609
14268
  resolvedStmt,
@@ -14161,16 +14820,27 @@ function hasCanonicalOrder(stmt) {
14161
14820
  (column) => column.type === "WINDOW_COL" && column.orderBy.length > 0
14162
14821
  );
14163
14822
  }
14164
- async function assertDmlWhereCapability(stmt, client, cacheContext) {
14165
- if (stmt.subtableCode || stmt.type === "UPDATE" && stmt.from != null) return;
14823
+ async function resolveDmlWhereCapability(stmt, client, cacheContext) {
14824
+ if (stmt.subtableCode || stmt.type === "UPDATE" && stmt.from != null) {
14825
+ return {
14826
+ capability: "LOCAL_ONLY",
14827
+ reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }]
14828
+ };
14829
+ }
14166
14830
  const fields = whereNeedsFieldMetadata(stmt.where) ? await getFieldsCached(stmt.appId, client, cacheContext) : [];
14167
14831
  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) => {
14832
+ if (stmt.where.type === "BOOLEAN" && stmt.where.value === false) {
14833
+ return classifyWhereCapability(null, () => void 0);
14834
+ }
14835
+ return classifyWhereCapability(stmt.where, (field) => {
14170
14836
  if (field.field === "$id") return resolveFieldSemantics({ fieldType: "__ID__" });
14171
14837
  const info = byCode.get(field.field);
14172
14838
  return info?.semantics ?? (info ? resolveFieldSemantics(info) : void 0);
14173
14839
  });
14840
+ }
14841
+ async function assertDmlWhereCapability(stmt, client, cacheContext) {
14842
+ if (stmt.subtableCode || stmt.type === "UPDATE" && stmt.from != null) return;
14843
+ const result = await resolveDmlWhereCapability(stmt, client, cacheContext);
14174
14844
  if (result.capability !== "EXACT_PUSHDOWN") {
14175
14845
  throw new DmlConvertError(
14176
14846
  `WHERE predicate cannot be represented by kintone REST (${formatWhereCapabilityFailure(result)})`
@@ -14199,6 +14869,7 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
14199
14869
  stmt,
14200
14870
  staticMode: mode,
14201
14871
  whereCapability: whereCapability.capability,
14872
+ whereReasons: whereCapability.reasons,
14202
14873
  orderSemantics: orderMeta.semantics,
14203
14874
  maxRecords: options.maxRecords ?? 1e4,
14204
14875
  hasKlike: whereHasKlike(stmt.where)
@@ -15394,6 +16065,7 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
15394
16065
  stmt,
15395
16066
  staticMode: "FULL_SCAN",
15396
16067
  whereCapability: whereCapability.capability,
16068
+ whereReasons: whereCapability.reasons,
15397
16069
  orderSemantics: orderMeta.semantics,
15398
16070
  maxRecords,
15399
16071
  hasKlike: whereHasKlike(stmt.where)
@@ -18875,7 +19547,7 @@ async function resolveScalarColumns(columns, client, options, cacheContext, cteC
18875
19547
  }
18876
19548
  var validateExplainInfo = /* @__PURE__ */ new WeakMap();
18877
19549
  var applyParentExplainPlan = /* @__PURE__ */ new WeakMap();
18878
- async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords = 1e4) {
19550
+ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords = 1e4, relativeDatePlan) {
18879
19551
  const fieldApps = /* @__PURE__ */ new Set();
18880
19552
  const processStatusApps = /* @__PURE__ */ new Set();
18881
19553
  const numberPrecisionApps = /* @__PURE__ */ new Set();
@@ -18897,6 +19569,10 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
18897
19569
  const capabilities = /* @__PURE__ */ new Map();
18898
19570
  const orderPlans = /* @__PURE__ */ new Map();
18899
19571
  const seen = /* @__PURE__ */ new Set();
19572
+ const sharedRelativeDatePlan = relativeDatePlan ?? await resolveRelativeDateExecutionPlan(query, tracedClient, cacheContext);
19573
+ const relativeNodeFor = (source) => sharedRelativeDatePlan.nodes.find(
19574
+ (node) => node.source === source || JSON.stringify(node.source) === JSON.stringify(source)
19575
+ );
18900
19576
  const visit = async (node) => {
18901
19577
  if (node === null || typeof node !== "object") return;
18902
19578
  if (seen.has(node)) return;
@@ -18915,7 +19591,8 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
18915
19591
  physicalApps.forEach((appId) => fieldApps.add(appId));
18916
19592
  }
18917
19593
  const capability = await resolveSelectWhereCapability(select, tracedClient, cacheContext);
18918
- if (capability.capability === "UNSUPPORTED") {
19594
+ const relativeNode = relativeNodeFor(select);
19595
+ if (capability.capability === "UNSUPPORTED" && !relativeNode) {
18919
19596
  throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
18920
19597
  }
18921
19598
  capabilities.set(select, capability);
@@ -18929,12 +19606,13 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
18929
19606
  }
18930
19607
  }
18931
19608
  const hasUnmaterializedSource = [select.from, ...select.joins.map((join2) => join2.table)].some((table) => table.cteName !== null);
18932
- if (hasCanonicalOrder(select) && !hasUnmaterializedSource) {
19609
+ if (hasCanonicalOrder(select) && !hasUnmaterializedSource && relativeNode?.allowed !== false) {
18933
19610
  const mode = capability.capability === "EXACT_PUSHDOWN" ? resolveSelectMode(select) : "FULL_SCAN";
18934
19611
  orderPlans.set(select, (select.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
18935
19612
  stmt: select,
18936
19613
  staticMode: mode,
18937
19614
  whereCapability: capability.capability,
19615
+ whereReasons: capability.reasons,
18938
19616
  orderSemantics: meta.semantics,
18939
19617
  maxRecords,
18940
19618
  hasKlike: whereHasKlike(select.where)
@@ -19010,7 +19688,7 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
19010
19688
  }
19011
19689
  }
19012
19690
  const dml = node;
19013
- if (dml.type !== "UPDATE" || !usesApplyParentResidualSelection(dml)) {
19691
+ if ((dml.type !== "UPDATE" || !usesApplyParentResidualSelection(dml)) && relativeNodeFor(dml)?.allowed !== false) {
19014
19692
  await assertDmlWhereCapability(dml, tracedClient, cacheContext);
19015
19693
  }
19016
19694
  }
@@ -19020,23 +19698,32 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
19020
19698
  if (typeof query === "object" && query !== null && query.type === "WITH" && canInlineSingleCte(query)) {
19021
19699
  const inlined = buildInlinedQuery(query);
19022
19700
  const capability = await resolveSelectWhereCapability(inlined, tracedClient, cacheContext);
19023
- if (capability.capability === "UNSUPPORTED") {
19701
+ const relativeNode = relativeNodeFor(inlined);
19702
+ if (capability.capability === "UNSUPPORTED" && !relativeNode) {
19024
19703
  throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
19025
19704
  }
19026
19705
  capabilities.set(inlined, capability);
19027
- if (hasCanonicalOrder(inlined)) {
19706
+ if (hasCanonicalOrder(inlined) && relativeNode?.allowed !== false) {
19028
19707
  const meta = await buildOrderByMetaForSelect(inlined, tracedClient, cacheContext);
19029
19708
  orderPlans.set(inlined, (inlined.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
19030
19709
  stmt: inlined,
19031
19710
  staticMode: capability.capability === "EXACT_PUSHDOWN" ? resolveSelectMode(inlined) : "FULL_SCAN",
19032
19711
  whereCapability: capability.capability,
19712
+ whereReasons: capability.reasons,
19033
19713
  orderSemantics: meta.semantics,
19034
19714
  maxRecords,
19035
19715
  hasKlike: whereHasKlike(inlined.where)
19036
19716
  }));
19037
19717
  }
19038
19718
  }
19039
- return { capabilities, orderPlans, fieldApps, processStatusApps, numberPrecisionApps };
19719
+ return {
19720
+ capabilities,
19721
+ orderPlans,
19722
+ fieldApps,
19723
+ processStatusApps,
19724
+ numberPrecisionApps,
19725
+ relativeDatePlan: sharedRelativeDatePlan
19726
+ };
19040
19727
  }
19041
19728
  function explainMetadataLines(analysis) {
19042
19729
  return [
@@ -19045,6 +19732,36 @@ function explainMetadataLines(analysis) {
19045
19732
  ...[...analysis.numberPrecisionApps].sort((a, b) => a - b).map((appId) => ` metadata API: number precision APP${appId}`)
19046
19733
  ];
19047
19734
  }
19735
+ function relativeDateExplainLines(plan) {
19736
+ if (!plan.hasRelativeDate) return [];
19737
+ if (!plan.allowed && plan.rejection) {
19738
+ return [
19739
+ ` relative date function: ${plan.rejection.functionName}`,
19740
+ " plan status: rejected",
19741
+ ` reason: ${plan.rejection.reasonCodes.join(", ")}`,
19742
+ " client evaluation: forbidden",
19743
+ " records/cursor/mutation API during EXPLAIN: none"
19744
+ ];
19745
+ }
19746
+ const lines = [];
19747
+ for (const node of plan.nodes) {
19748
+ for (const functionName of node.functionNames) {
19749
+ const detail = node.capability?.reasons.find(
19750
+ (reason) => reason.functionName === functionName
19751
+ );
19752
+ lines.push(
19753
+ ` relative date function: ${functionName}`,
19754
+ " evaluation: kintone server",
19755
+ ` field: ${detail?.field ?? "(unknown)"} (${detail?.fieldType ?? "unknown"})`,
19756
+ ` operator: ${detail?.operator ?? "(unknown)"}`,
19757
+ ` where capability: ${node.capability?.capability ?? "(unknown)"}`,
19758
+ " client evaluation: forbidden",
19759
+ ` kintone query: ${node.restQuery || "(\u306A\u3057)"}`
19760
+ );
19761
+ }
19762
+ }
19763
+ return lines;
19764
+ }
19048
19765
  async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4, cursorMaxActive = 2, enableImport = false, dmlMaxRows = 100, dmlMaxSubtableRows = DEFAULT_APPLY_MAX_SUBTABLE_ROWS) {
19049
19766
  const statements = parseSqlBatch(sql, enableImport);
19050
19767
  const analysis = analyzeBatch(statements);
@@ -19055,15 +19772,25 @@ async function buildBatchExplainPlans(sql, client, injectedVariables, cacheConte
19055
19772
  const stmt = statements[i];
19056
19773
  const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr: resolveBatchVariableReferences(stmt.expr, variables) } : stmt : resolveBatchVariableReferences(stmt, variables);
19057
19774
  validateKlikeStatement(planStmt);
19058
- const whereAnalysis = await buildExplainWhereAnalysis(planStmt, client, cacheContext, maxRecords);
19059
- const statementPlan = addCursorConcurrency(buildBatchStatementPlan(
19775
+ const relativeDatePlan = await resolveRelativeDateExecutionPlan(planStmt, client, cacheContext);
19776
+ const whereAnalysis = await buildExplainWhereAnalysis(
19060
19777
  planStmt,
19061
- analysis.statements[i],
19062
- whereAnalysis.capabilities,
19063
- whereAnalysis.orderPlans,
19064
- dmlMaxRows,
19065
- dmlMaxSubtableRows
19066
- ), cursorMaxActive);
19778
+ client,
19779
+ cacheContext,
19780
+ maxRecords,
19781
+ relativeDatePlan
19782
+ );
19783
+ const statementPlan = relativeDatePlan.hasRelativeDate && !relativeDatePlan.allowed ? relativeDateExplainLines(relativeDatePlan) : [
19784
+ ...relativeDateExplainLines(relativeDatePlan),
19785
+ ...addCursorConcurrency(buildBatchStatementPlan(
19786
+ planStmt,
19787
+ analysis.statements[i],
19788
+ whereAnalysis.capabilities,
19789
+ whereAnalysis.orderPlans,
19790
+ dmlMaxRows,
19791
+ dmlMaxSubtableRows
19792
+ ), cursorMaxActive)
19793
+ ];
19067
19794
  const metadataPlan = explainMetadataLines(whereAnalysis);
19068
19795
  plans.push({
19069
19796
  index: i,
@@ -19172,10 +19899,19 @@ function buildPlanForBatchQuery(query, info, capabilities, orderPlans) {
19172
19899
  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
19900
  return lines;
19174
19901
  }
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 = [
19902
+ async function executeExplain(stmt, client, cacheContext, maxRecords, cursorMaxActive, dmlMaxRows = 100, dmlMaxSubtableRows = DEFAULT_APPLY_MAX_SUBTABLE_ROWS, relativeDatePlan) {
19903
+ const sharedPlan = relativeDatePlan ?? await resolveRelativeDateExecutionPlan(stmt.query, client, cacheContext);
19904
+ const analysis = await buildExplainWhereAnalysis(
19905
+ stmt.query,
19906
+ client,
19907
+ cacheContext,
19908
+ maxRecords,
19909
+ sharedPlan
19910
+ );
19911
+ const relativeLines = relativeDateExplainLines(sharedPlan);
19912
+ const lines = sharedPlan.hasRelativeDate && !sharedPlan.allowed ? [...explainMetadataLines(analysis), ...relativeLines] : [
19178
19913
  ...explainMetadataLines(analysis),
19914
+ ...relativeLines,
19179
19915
  ...addCursorConcurrency(
19180
19916
  buildExplainPlan(
19181
19917
  stmt.query,