@rex0220/kintone-sql-tools 3.59.0 → 3.61.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
@@ -3187,7 +3187,7 @@ var Parser = class {
3187
3187
  return { type: "BINARY", op: "KLIKE", left: field, right: pattern };
3188
3188
  }
3189
3189
  const op = this.parseCompareOp();
3190
- const right = this.parseWhereSqlValue();
3190
+ const right = this.parseWhereSqlValue(op !== "LIKE");
3191
3191
  return { type: "BINARY", op, left: field, right };
3192
3192
  }
3193
3193
  parseCompareOp() {
@@ -3310,8 +3310,13 @@ var Parser = class {
3310
3310
  return { type: "FIELD", tableAlias: qi.tableAlias, field: qi.field };
3311
3311
  }
3312
3312
  // 右辺の値
3313
- parseWhereSqlValue() {
3313
+ parseWhereSqlValue(allowUnaryPlusNumberLiteral = true) {
3314
3314
  const tok = this.peek();
3315
+ if (allowUnaryPlusNumberLiteral && tok.kind === "+" /* PLUS */) {
3316
+ this.advance();
3317
+ const number = this.expect("NUMBER" /* NUMBER */, "\u5358\u9805 + \u306E\u76F4\u5F8C\u306B\u306F\u6570\u5024\u30EA\u30C6\u30E9\u30EB\u304C\u5FC5\u8981\u3067\u3059");
3318
+ return makeNumberLiteral(`+${number.value}`);
3319
+ }
3315
3320
  if (this.allowRelativeDateFunctions && tok.kind === "IDENT" /* IDENT */ && this.peekAt(1).kind === "(" /* LPAREN */ && isRelativeDateFunctionName(tok.value.toUpperCase())) {
3316
3321
  return this.parseRelativeDateFunction();
3317
3322
  }
@@ -12037,414 +12042,191 @@ function isOuterJoinNonPreservedTable(statement, table, isMainTable) {
12037
12042
  return false;
12038
12043
  }
12039
12044
 
12040
- // src/core/explainMetadata.ts
12041
- function buildGroupingExplainMetadata(statement, canonicalItemCount) {
12042
- const grouping = normalizeGroupingSpec(statement);
12043
- if (grouping.type !== "GROUPING_SETS") return null;
12044
- return {
12045
- source: grouping.source,
12046
- expandedSetCount: grouping.sets.length,
12047
- groupingItemCount: canonicalItemCount ?? grouping.allItems.length,
12048
- setLimit: B65_MAX_GROUPING_SETS,
12049
- itemLimit: B65_MAX_GROUPING_ITEMS,
12050
- outputRowLimit: B65_MAX_GENERATED_ROWS
12051
- };
12045
+ // src/core/optimization/joinDateTimeLiteralPolicy.ts
12046
+ function isCanonicalJoinDate(value) {
12047
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
12048
+ if (!match) return false;
12049
+ const year = Number(match[1]);
12050
+ const month = Number(match[2]);
12051
+ const day = Number(match[3]);
12052
+ if (year < 1 || year > 9999) return false;
12053
+ const date = /* @__PURE__ */ new Date(0);
12054
+ date.setUTCFullYear(year, month - 1, day);
12055
+ date.setUTCHours(0, 0, 0, 0);
12056
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
12052
12057
  }
12053
- function whereNeedsFieldMetadata(where) {
12054
- if (where === null) return false;
12055
- switch (where.type) {
12056
- case "BINARY":
12057
- return valueNeedsFieldMetadata(where.left);
12058
- case "NULL_CHECK":
12059
- return valueNeedsFieldMetadata(where.field);
12060
- case "LOGICAL":
12061
- return whereNeedsFieldMetadata(where.left) || whereNeedsFieldMetadata(where.right);
12062
- case "NOT":
12063
- case "GROUP":
12064
- return whereNeedsFieldMetadata(where.expr);
12065
- case "EXISTS":
12066
- case "BOOLEAN":
12067
- return false;
12068
- }
12058
+ function isCanonicalJoinTime(value) {
12059
+ const match = /^(\d{2}):(\d{2})$/.exec(value);
12060
+ return match !== null && Number(match[1]) <= 23 && Number(match[2]) <= 59;
12069
12061
  }
12070
- function valueNeedsFieldMetadata(value) {
12071
- if (Array.isArray(value)) return value.some(valueNeedsFieldMetadata);
12072
- if (value === null || typeof value !== "object") return false;
12073
- const item = value;
12074
- if (item["type"] === "FIELD") return item["field"] !== "$id";
12075
- if (item["type"] === "SELECT") return false;
12076
- return Object.values(item).some(valueNeedsFieldMetadata);
12062
+ function isCanonicalJoinDateTime(value) {
12063
+ const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/.exec(value);
12064
+ if (!match || !isCanonicalJoinDate(match[1])) return false;
12065
+ return Number(match[2]) <= 23 && Number(match[3]) <= 59 && Number(match[4]) <= 59;
12077
12066
  }
12078
- function selectNeedsOwnMetadata(statement) {
12079
- return whereNeedsFieldMetadata(statement.where) || statement.groupBy.length > 0 || normalizeGroupingSpec(statement).type === "GROUPING_SETS" || statement.orderBy.length > 0 || statement.columns.some(
12080
- (column) => column.type === "WINDOW_COL" && column.orderBy.length > 0
12081
- );
12067
+
12068
+ // src/core/optimization/whereCapability.ts
12069
+ var RANGE_AND_EQUALITY = ["=", "!=", ">", "<", ">=", "<="];
12070
+ var RELATIVE_DATE_FIELD_TYPES = /* @__PURE__ */ new Set([
12071
+ "DATE",
12072
+ "DATETIME",
12073
+ "CREATED_TIME",
12074
+ "UPDATED_TIME"
12075
+ ]);
12076
+ var RELATIVE_DATE_OPERATORS = new Set(RANGE_AND_EQUALITY);
12077
+ var LEGACY_KINTONE_FUNCTION_FIELD_TYPES = /* @__PURE__ */ new Map([
12078
+ ["TODAY", /* @__PURE__ */ new Set(["DATE", "DATETIME", "CREATED_TIME", "UPDATED_TIME"])],
12079
+ ["NOW", /* @__PURE__ */ new Set(["DATETIME", "CREATED_TIME", "UPDATED_TIME"])],
12080
+ ["LOGINUSER", /* @__PURE__ */ new Set(["CREATOR", "MODIFIER", "USER_SELECT"])],
12081
+ ["PRIMARY_ORGANIZATION", /* @__PURE__ */ new Set(["ORGANIZATION_SELECT"])]
12082
+ ]);
12083
+ var LEGACY_KINTONE_FUNCTION_OPERATORS = /* @__PURE__ */ new Map([
12084
+ ["TODAY", new Set(RANGE_AND_EQUALITY)],
12085
+ ["NOW", new Set(RANGE_AND_EQUALITY)],
12086
+ ["LOGINUSER", /* @__PURE__ */ new Set(["in", "not in"])],
12087
+ ["PRIMARY_ORGANIZATION", /* @__PURE__ */ new Set(["in", "not in"])]
12088
+ ]);
12089
+ var EQUALITY_IN = ["=", "!=", "in", "not in"];
12090
+ var NATIVE_OPERATORS = /* @__PURE__ */ new Map([
12091
+ ["RECORD_NUMBER", /* @__PURE__ */ new Set([...RANGE_AND_EQUALITY, "in", "not in"])],
12092
+ ["__ID__", /* @__PURE__ */ new Set([...RANGE_AND_EQUALITY, "in", "not in"])],
12093
+ ["CREATOR", /* @__PURE__ */ new Set(["in", "not in"])],
12094
+ ["MODIFIER", /* @__PURE__ */ new Set(["in", "not in"])],
12095
+ ["CREATED_TIME", new Set(RANGE_AND_EQUALITY)],
12096
+ ["UPDATED_TIME", new Set(RANGE_AND_EQUALITY)],
12097
+ ["DATE", new Set(RANGE_AND_EQUALITY)],
12098
+ ["TIME", new Set(RANGE_AND_EQUALITY)],
12099
+ ["DATETIME", new Set(RANGE_AND_EQUALITY)],
12100
+ ["SINGLE_LINE_TEXT", /* @__PURE__ */ new Set(["=", "!=", "in", "not in", "like", "not like"])],
12101
+ ["LINK", /* @__PURE__ */ new Set(["=", "!=", "in", "not in", "like", "not like"])],
12102
+ ["NUMBER", /* @__PURE__ */ new Set([...RANGE_AND_EQUALITY, "in", "not in"])],
12103
+ ["CALC", /* @__PURE__ */ new Set([...RANGE_AND_EQUALITY, "in", "not in"])],
12104
+ ["MULTI_LINE_TEXT", /* @__PURE__ */ new Set(["like", "not like"])],
12105
+ ["RICH_TEXT", /* @__PURE__ */ new Set(["like", "not like"])],
12106
+ ["CHECK_BOX", /* @__PURE__ */ new Set(["in", "not in"])],
12107
+ ["RADIO_BUTTON", /* @__PURE__ */ new Set(["in", "not in"])],
12108
+ ["DROP_DOWN", /* @__PURE__ */ new Set(["in", "not in"])],
12109
+ ["MULTI_SELECT", /* @__PURE__ */ new Set(["in", "not in"])],
12110
+ ["FILE", /* @__PURE__ */ new Set(["like", "not like"])],
12111
+ ["USER_SELECT", /* @__PURE__ */ new Set(["in", "not in"])],
12112
+ ["ORGANIZATION_SELECT", /* @__PURE__ */ new Set(["in", "not in"])],
12113
+ ["GROUP_SELECT", /* @__PURE__ */ new Set(["in", "not in"])],
12114
+ ["STATUS", new Set(EQUALITY_IN)],
12115
+ ["STATUS_ASSIGNEE", /* @__PURE__ */ new Set(["in", "not in"])]
12116
+ ]);
12117
+ var LOCAL_VALID_OPERATORS = /* @__PURE__ */ new Map([
12118
+ ["CREATOR", /* @__PURE__ */ new Set(["in", "not in"])],
12119
+ ["MODIFIER", /* @__PURE__ */ new Set(["in", "not in"])],
12120
+ ["CHECK_BOX", /* @__PURE__ */ new Set(["in", "not in"])],
12121
+ ["MULTI_SELECT", /* @__PURE__ */ new Set(["in", "not in"])]
12122
+ ]);
12123
+ var LOCAL_SCALAR_TYPES = /* @__PURE__ */ new Set([
12124
+ "RECORD_NUMBER",
12125
+ "__ID__",
12126
+ "CREATOR",
12127
+ "MODIFIER",
12128
+ "CREATED_TIME",
12129
+ "UPDATED_TIME",
12130
+ "DATE",
12131
+ "TIME",
12132
+ "DATETIME",
12133
+ "SINGLE_LINE_TEXT",
12134
+ "LINK",
12135
+ "NUMBER",
12136
+ "CALC",
12137
+ "MULTI_LINE_TEXT",
12138
+ "RICH_TEXT",
12139
+ "RADIO_BUTTON",
12140
+ "DROP_DOWN",
12141
+ "STATUS",
12142
+ // 一時表・CTE・式列は kintone REST へは送らず、共有ローカル評価器で扱う。
12143
+ "KSQL_STRING",
12144
+ "KSQL_NUMBER",
12145
+ "KSQL_BOOLEAN"
12146
+ ]);
12147
+ var LOCAL_COLLECTION_TYPES = /* @__PURE__ */ new Set([
12148
+ "CHECK_BOX",
12149
+ "MULTI_SELECT",
12150
+ "FILE",
12151
+ "USER_SELECT",
12152
+ "ORGANIZATION_SELECT",
12153
+ "GROUP_SELECT",
12154
+ "STATUS_ASSIGNEE",
12155
+ "CATEGORY"
12156
+ ]);
12157
+ function nativeWhereOperatorsForType(fieldType) {
12158
+ return NATIVE_OPERATORS.get(fieldType) ?? /* @__PURE__ */ new Set();
12082
12159
  }
12083
- function explainNeedsAppMetadata(statement) {
12084
- const seen = /* @__PURE__ */ new Set();
12160
+ function normalizeChoiceEquality(where, resolveField2) {
12161
+ const rewrites = [];
12085
12162
  const visit = (node) => {
12086
- if (node === null || typeof node !== "object") return false;
12087
- if (seen.has(node)) return false;
12088
- seen.add(node);
12089
- if (Array.isArray(node)) return node.some(visit);
12090
- const item = node;
12091
- if (item["type"] === "VALIDATE") return true;
12092
- if (item["type"] === "SELECT" && selectNeedsOwnMetadata(node)) {
12093
- return true;
12163
+ if (node.type === "BINARY") {
12164
+ if ((node.op === "=" || node.op === "!=" || node.op === "<>") && node.left.type === "FIELD" && node.right.type === "STRING" && node.right.value !== "") {
12165
+ const semantics = resolveField2(node.left);
12166
+ if (semantics !== void 0 && semantics.compareMode === "option" && LOCAL_SCALAR_TYPES.has(semantics.fieldType) && nativeWhereOperatorsForType(semantics.fieldType).has("in") && semantics.optionOrder?.has(node.right.value) === true) {
12167
+ const normalizedOperator = node.op === "=" ? "IN" : "NOT_IN";
12168
+ rewrites.push({
12169
+ field: node.left,
12170
+ originalOperator: node.op,
12171
+ normalizedOperator,
12172
+ value: node.right.value
12173
+ });
12174
+ return {
12175
+ ...node,
12176
+ op: normalizedOperator,
12177
+ right: { type: "IN_LIST", values: [node.right] }
12178
+ };
12179
+ }
12180
+ }
12181
+ return node;
12094
12182
  }
12095
- if ((item["type"] === "UPDATE" || item["type"] === "DELETE") && whereNeedsFieldMetadata(node.where)) {
12096
- return true;
12183
+ if (node.type === "LOGICAL") {
12184
+ const left = visit(node.left);
12185
+ const right = visit(node.right);
12186
+ return left === node.left && right === node.right ? node : { ...node, left, right };
12097
12187
  }
12098
- if (item["type"] === "UPDATE" && Array.isArray(item["applyBlocks"]) && item["applyBlocks"].length > 0) return true;
12099
- return Object.values(item).some(visit);
12188
+ if (node.type === "GROUP" || node.type === "NOT") {
12189
+ const expr = visit(node.expr);
12190
+ return expr === node.expr ? node : { ...node, expr };
12191
+ }
12192
+ return node;
12100
12193
  };
12101
- return visit(statement);
12194
+ return { normalizedWhere: visit(where), rewrites };
12102
12195
  }
12103
-
12104
- // src/api/fetchAll.ts
12105
- async function fetchAll(fetcher, app, query, fields, options = {}) {
12106
- const pageSize = options.pageSize ?? PAGE_SIZE_DEFAULT;
12107
- const parallel = Math.max(1, options.parallel ?? PARALLEL_DEFAULT);
12108
- const maxRecords = options.maxRecords ?? MAX_RECORDS_DEFAULT;
12109
- const onLimit = options.onLimit ?? "error";
12110
- const stopAfter = options.stopAfter;
12111
- if (stopAfter !== void 0 && (!Number.isSafeInteger(stopAfter) || stopAfter <= 0 || stopAfter > maxRecords)) {
12112
- throw new RangeError("stopAfter must be a positive safe integer <= maxRecords");
12113
- }
12114
- const fetchCap = stopAfter ?? maxRecords;
12115
- const fetchFields = fields.length > 0 && !fields.includes("$id") ? [...fields, "$id"] : fields;
12116
- const allRecords = [];
12117
- let notified = false;
12118
- const limitMessage = `\u53D6\u5F97\u4EF6\u6570\u304C\u4E0A\u9650\uFF08${maxRecords} \u4EF6\uFF09\u3092\u8D85\u3048\u307E\u3057\u305F\u3002WHERE \u53E5\u3067\u7D5E\u308A\u8FBC\u3080\u304B\u3001maxRecords \u3092\u5F15\u304D\u4E0A\u3052\u3066\u304F\u3060\u3055\u3044\u3002`;
12119
- let cursorId = 0;
12120
- let windowOffset = 0;
12121
- const cursorQuery0 = buildCursorQuery(query, cursorId);
12122
- const first = await fetchPage(fetcher, app, cursorQuery0, fetchFields, pageSize, windowOffset);
12123
- notifySearchAborted(first, options);
12124
- allRecords.push(...first.records);
12125
- if (stopAfter !== void 0 && allRecords.length >= stopAfter) {
12126
- return allRecords.slice(0, stopAfter);
12196
+ function classifyWhereCapability(where, resolveField2) {
12197
+ if (where === null) {
12198
+ return { capability: "EXACT_PUSHDOWN", reasons: [{ code: "WHERE_EXACT" }] };
12127
12199
  }
12128
- if (allRecords.length > maxRecords) {
12129
- if (onLimit === "truncate") {
12130
- if (!notified && options.onTruncate) {
12131
- options.onTruncate(maxRecords);
12132
- notified = true;
12200
+ return classifyNode(where, resolveField2);
12201
+ }
12202
+ function classifyNode(where, resolveField2) {
12203
+ switch (where.type) {
12204
+ case "BOOLEAN":
12205
+ return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
12206
+ case "BINARY":
12207
+ return classifyBinary(where.op, where.left, where.right, resolveField2);
12208
+ case "NULL_CHECK":
12209
+ if (where.field.type !== "FIELD") return localExpression();
12210
+ return classifyLocalOnlyField(where.field, where.not ? "IS NOT NULL" : "IS NULL", resolveField2);
12211
+ case "EXISTS":
12212
+ return localExpression();
12213
+ case "GROUP":
12214
+ return classifyNode(where.expr, resolveField2);
12215
+ case "NOT": {
12216
+ const inner = classifyNode(where.expr, resolveField2);
12217
+ if (inner.capability !== "SUPERSET_PREFILTER") return inner;
12218
+ if (!hasRelativeDateReason(inner.reasons)) {
12219
+ return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
12133
12220
  }
12134
- return allRecords.slice(0, maxRecords);
12135
- }
12136
- throw new FetchAllLimitError(limitMessage);
12137
- }
12138
- if (first.records.length < pageSize) return allRecords;
12139
- windowOffset += pageSize;
12140
- if (windowOffset >= KINTONE_MAX_OFFSET) {
12141
- cursorId = getLastId(first.records);
12142
- windowOffset = 0;
12143
- }
12144
- while (true) {
12145
- if (stopAfter !== void 0 && allRecords.length >= stopAfter) {
12146
- return allRecords.slice(0, stopAfter);
12147
- }
12148
- if (allRecords.length >= maxRecords) {
12149
- if (onLimit === "truncate") {
12150
- if (!notified && options.onTruncate) {
12151
- options.onTruncate(maxRecords);
12152
- notified = true;
12153
- }
12154
- return allRecords.slice(0, maxRecords);
12155
- }
12156
- throw new FetchAllLimitError(limitMessage);
12157
- }
12158
- const remaining = fetchCap - allRecords.length;
12159
- const maxPagesByLimit = Math.ceil(remaining / pageSize);
12160
- const batchParallel = Math.max(1, Math.min(parallel, maxPagesByLimit));
12161
- const batchOffsets = [];
12162
- for (let i = 0; i < batchParallel; i++) {
12163
- const off = windowOffset + i * pageSize;
12164
- if (off >= KINTONE_MAX_OFFSET) break;
12165
- batchOffsets.push(off);
12166
- }
12167
- const cq = buildCursorQuery(query, cursorId);
12168
- const responses = await Promise.all(
12169
- batchOffsets.map(
12170
- (offset) => fetchPage(fetcher, app, cq, fetchFields, pageSize, offset)
12171
- )
12172
- );
12173
- for (const response of responses) notifySearchAborted(response, options);
12174
- let done = false;
12175
- for (const res of responses) {
12176
- allRecords.push(...res.records);
12177
- if (stopAfter !== void 0 && allRecords.length >= stopAfter) {
12178
- return allRecords.slice(0, stopAfter);
12179
- }
12180
- if (allRecords.length > maxRecords) {
12181
- if (onLimit === "truncate") {
12182
- if (!notified && options.onTruncate) {
12183
- options.onTruncate(maxRecords);
12184
- notified = true;
12185
- }
12186
- return allRecords.slice(0, maxRecords);
12187
- }
12188
- throw new FetchAllLimitError(limitMessage);
12189
- }
12190
- if (res.records.length < pageSize) {
12191
- done = true;
12192
- break;
12193
- }
12194
- }
12195
- if (done) break;
12196
- windowOffset += pageSize * batchOffsets.length;
12197
- if (windowOffset >= KINTONE_MAX_OFFSET) {
12198
- const lastRes = responses[responses.length - 1];
12199
- cursorId = getLastId(lastRes.records);
12200
- windowOffset = 0;
12201
- }
12202
- }
12203
- return allRecords;
12204
- }
12205
- function notifySearchAborted(response, options) {
12206
- if (response.searchAborted) options.onSearchAborted?.();
12207
- }
12208
- function extractIds(records) {
12209
- return records.map((r) => {
12210
- const raw = r["$id"]?.value;
12211
- if (raw === void 0) {
12212
- throw new Error(
12213
- '\u30EC\u30B3\u30FC\u30C9\u306B $id \u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093\u3002fields \u306B "$id" \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002'
12214
- );
12215
- }
12216
- const id = Number(raw);
12217
- if (!Number.isFinite(id)) {
12218
- throw new Error(`$id \u306E\u5024\u304C\u6570\u5024\u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${raw}`);
12219
- }
12220
- return id;
12221
- });
12222
- }
12223
- var PAGE_SIZE_DEFAULT = 500;
12224
- var PARALLEL_DEFAULT = 1;
12225
- var MAX_RECORDS_DEFAULT = 1e4;
12226
- var KINTONE_MAX_OFFSET = 1e4;
12227
- async function fetchPage(fetcher, app, query, fields, pageSize, offset) {
12228
- const pageQuery = buildPageQuery(query, pageSize, offset);
12229
- return fetcher({ app, query: pageQuery, fields });
12230
- }
12231
- function buildCursorQuery(baseQuery, cursorId) {
12232
- const base = baseQuery.trimEnd();
12233
- if (cursorId <= 0) {
12234
- return base ? `${base} order by $id asc` : "order by $id asc";
12235
- }
12236
- const cursor = `$id > ${cursorId} order by $id asc`;
12237
- return base ? `(${base}) and ${cursor}` : cursor;
12238
- }
12239
- function buildPageQuery(query, pageSize, offset) {
12240
- const base = query.trimEnd();
12241
- const suffix = `limit ${pageSize} offset ${offset}`;
12242
- return base ? `${base} ${suffix}` : suffix;
12243
- }
12244
- function getLastId(records) {
12245
- const last = records[records.length - 1];
12246
- const raw = last?.["$id"]?.value;
12247
- if (raw === void 0) return 0;
12248
- const id = Number(raw);
12249
- return Number.isFinite(id) ? id : 0;
12250
- }
12251
- var FetchAllLimitError = class extends Error {
12252
- constructor(message, completeInputWrapped = false) {
12253
- super(message);
12254
- this.completeInputWrapped = completeInputWrapped;
12255
- this.name = "FetchAllLimitError";
12256
- }
12257
- };
12258
-
12259
- // src/core/optimization/sharedPlanner.ts
12260
- async function fetchRecordsForSharedPlan(getRecords, app, query, fields, options) {
12261
- const records = await fetchAll(getRecords, app, query, fields, {
12262
- maxRecords: options.maxRecords,
12263
- parallel: options.parallel,
12264
- onLimit: options.onLimit ?? "error",
12265
- onTruncate: options.onTruncate,
12266
- onSearchAborted: options.onSearchAborted
12267
- });
12268
- return {
12269
- records,
12270
- metrics: { fetchedRows: records.length }
12271
- };
12272
- }
12273
- async function resolveDmlTargetIds(getRecords, app, query, options) {
12274
- const { records, metrics } = await fetchRecordsForSharedPlan(
12275
- getRecords,
12276
- app,
12277
- query,
12278
- ["$id"],
12279
- options
12280
- );
12281
- return {
12282
- ids: extractIds(records),
12283
- metrics
12284
- };
12285
- }
12286
-
12287
- // src/core/optimization/whereCapability.ts
12288
- var RANGE_AND_EQUALITY = ["=", "!=", ">", "<", ">=", "<="];
12289
- var RELATIVE_DATE_FIELD_TYPES = /* @__PURE__ */ new Set([
12290
- "DATE",
12291
- "DATETIME",
12292
- "CREATED_TIME",
12293
- "UPDATED_TIME"
12294
- ]);
12295
- var RELATIVE_DATE_OPERATORS = new Set(RANGE_AND_EQUALITY);
12296
- var LEGACY_KINTONE_FUNCTION_FIELD_TYPES = /* @__PURE__ */ new Map([
12297
- ["TODAY", /* @__PURE__ */ new Set(["DATE", "DATETIME", "CREATED_TIME", "UPDATED_TIME"])],
12298
- ["NOW", /* @__PURE__ */ new Set(["DATETIME", "CREATED_TIME", "UPDATED_TIME"])],
12299
- ["LOGINUSER", /* @__PURE__ */ new Set(["CREATOR", "MODIFIER", "USER_SELECT"])],
12300
- ["PRIMARY_ORGANIZATION", /* @__PURE__ */ new Set(["ORGANIZATION_SELECT"])]
12301
- ]);
12302
- var LEGACY_KINTONE_FUNCTION_OPERATORS = /* @__PURE__ */ new Map([
12303
- ["TODAY", new Set(RANGE_AND_EQUALITY)],
12304
- ["NOW", new Set(RANGE_AND_EQUALITY)],
12305
- ["LOGINUSER", /* @__PURE__ */ new Set(["in", "not in"])],
12306
- ["PRIMARY_ORGANIZATION", /* @__PURE__ */ new Set(["in", "not in"])]
12307
- ]);
12308
- var EQUALITY_IN = ["=", "!=", "in", "not in"];
12309
- var NATIVE_OPERATORS = /* @__PURE__ */ new Map([
12310
- ["RECORD_NUMBER", /* @__PURE__ */ new Set([...RANGE_AND_EQUALITY, "in", "not in"])],
12311
- ["__ID__", /* @__PURE__ */ new Set([...RANGE_AND_EQUALITY, "in", "not in"])],
12312
- ["CREATOR", /* @__PURE__ */ new Set(["in", "not in"])],
12313
- ["MODIFIER", /* @__PURE__ */ new Set(["in", "not in"])],
12314
- ["CREATED_TIME", new Set(RANGE_AND_EQUALITY)],
12315
- ["UPDATED_TIME", new Set(RANGE_AND_EQUALITY)],
12316
- ["DATE", new Set(RANGE_AND_EQUALITY)],
12317
- ["TIME", new Set(RANGE_AND_EQUALITY)],
12318
- ["DATETIME", new Set(RANGE_AND_EQUALITY)],
12319
- ["SINGLE_LINE_TEXT", /* @__PURE__ */ new Set(["=", "!=", "in", "not in", "like", "not like"])],
12320
- ["LINK", /* @__PURE__ */ new Set(["=", "!=", "in", "not in", "like", "not like"])],
12321
- ["NUMBER", /* @__PURE__ */ new Set([...RANGE_AND_EQUALITY, "in", "not in"])],
12322
- ["CALC", /* @__PURE__ */ new Set([...RANGE_AND_EQUALITY, "in", "not in"])],
12323
- ["MULTI_LINE_TEXT", /* @__PURE__ */ new Set(["like", "not like"])],
12324
- ["RICH_TEXT", /* @__PURE__ */ new Set(["like", "not like"])],
12325
- ["CHECK_BOX", /* @__PURE__ */ new Set(["in", "not in"])],
12326
- ["RADIO_BUTTON", /* @__PURE__ */ new Set(["in", "not in"])],
12327
- ["DROP_DOWN", /* @__PURE__ */ new Set(["in", "not in"])],
12328
- ["MULTI_SELECT", /* @__PURE__ */ new Set(["in", "not in"])],
12329
- ["FILE", /* @__PURE__ */ new Set(["like", "not like"])],
12330
- ["USER_SELECT", /* @__PURE__ */ new Set(["in", "not in"])],
12331
- ["ORGANIZATION_SELECT", /* @__PURE__ */ new Set(["in", "not in"])],
12332
- ["GROUP_SELECT", /* @__PURE__ */ new Set(["in", "not in"])],
12333
- ["STATUS", new Set(EQUALITY_IN)]
12334
- ]);
12335
- var LOCAL_VALID_OPERATORS = /* @__PURE__ */ new Map([
12336
- ["CREATOR", /* @__PURE__ */ new Set(["in", "not in"])],
12337
- ["MODIFIER", /* @__PURE__ */ new Set(["in", "not in"])],
12338
- ["CHECK_BOX", /* @__PURE__ */ new Set(["in", "not in"])],
12339
- ["MULTI_SELECT", /* @__PURE__ */ new Set(["in", "not in"])]
12340
- ]);
12341
- var LOCAL_SCALAR_TYPES = /* @__PURE__ */ new Set([
12342
- "RECORD_NUMBER",
12343
- "__ID__",
12344
- "CREATOR",
12345
- "MODIFIER",
12346
- "CREATED_TIME",
12347
- "UPDATED_TIME",
12348
- "DATE",
12349
- "TIME",
12350
- "DATETIME",
12351
- "SINGLE_LINE_TEXT",
12352
- "LINK",
12353
- "NUMBER",
12354
- "CALC",
12355
- "MULTI_LINE_TEXT",
12356
- "RICH_TEXT",
12357
- "RADIO_BUTTON",
12358
- "DROP_DOWN",
12359
- "STATUS",
12360
- // 一時表・CTE・式列は kintone REST へは送らず、共有ローカル評価器で扱う。
12361
- "KSQL_STRING",
12362
- "KSQL_NUMBER",
12363
- "KSQL_BOOLEAN"
12364
- ]);
12365
- var LOCAL_COLLECTION_TYPES = /* @__PURE__ */ new Set([
12366
- "CHECK_BOX",
12367
- "MULTI_SELECT",
12368
- "FILE",
12369
- "USER_SELECT",
12370
- "ORGANIZATION_SELECT",
12371
- "GROUP_SELECT",
12372
- "STATUS_ASSIGNEE",
12373
- "CATEGORY"
12374
- ]);
12375
- function nativeWhereOperatorsForType(fieldType) {
12376
- return NATIVE_OPERATORS.get(fieldType) ?? /* @__PURE__ */ new Set();
12377
- }
12378
- function normalizeChoiceEquality(where, resolveField2) {
12379
- const rewrites = [];
12380
- const visit = (node) => {
12381
- if (node.type === "BINARY") {
12382
- if ((node.op === "=" || node.op === "!=" || node.op === "<>") && node.left.type === "FIELD" && node.right.type === "STRING" && node.right.value !== "") {
12383
- const semantics = resolveField2(node.left);
12384
- if (semantics !== void 0 && semantics.compareMode === "option" && LOCAL_SCALAR_TYPES.has(semantics.fieldType) && nativeWhereOperatorsForType(semantics.fieldType).has("in") && semantics.optionOrder?.has(node.right.value) === true) {
12385
- const normalizedOperator = node.op === "=" ? "IN" : "NOT_IN";
12386
- rewrites.push({
12387
- field: node.left,
12388
- originalOperator: node.op,
12389
- normalizedOperator,
12390
- value: node.right.value
12391
- });
12392
- return {
12393
- ...node,
12394
- op: normalizedOperator,
12395
- right: { type: "IN_LIST", values: [node.right] }
12396
- };
12397
- }
12398
- }
12399
- return node;
12400
- }
12401
- if (node.type === "LOGICAL") {
12402
- const left = visit(node.left);
12403
- const right = visit(node.right);
12404
- return left === node.left && right === node.right ? node : { ...node, left, right };
12405
- }
12406
- if (node.type === "GROUP" || node.type === "NOT") {
12407
- const expr = visit(node.expr);
12408
- return expr === node.expr ? node : { ...node, expr };
12409
- }
12410
- return node;
12411
- };
12412
- return { normalizedWhere: visit(where), rewrites };
12413
- }
12414
- function classifyWhereCapability(where, resolveField2) {
12415
- if (where === null) {
12416
- return { capability: "EXACT_PUSHDOWN", reasons: [{ code: "WHERE_EXACT" }] };
12417
- }
12418
- return classifyNode(where, resolveField2);
12419
- }
12420
- function classifyNode(where, resolveField2) {
12421
- switch (where.type) {
12422
- case "BOOLEAN":
12423
- return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
12424
- case "BINARY":
12425
- return classifyBinary(where.op, where.left, where.right, resolveField2);
12426
- case "NULL_CHECK":
12427
- if (where.field.type !== "FIELD") return localExpression();
12428
- return classifyLocalOnlyField(where.field, where.not ? "IS NOT NULL" : "IS NULL", resolveField2);
12429
- case "EXISTS":
12430
- return localExpression();
12431
- case "GROUP":
12432
- return classifyNode(where.expr, resolveField2);
12433
- case "NOT": {
12434
- const inner = classifyNode(where.expr, resolveField2);
12435
- if (inner.capability !== "SUPERSET_PREFILTER") return inner;
12436
- if (!hasRelativeDateReason(inner.reasons)) {
12437
- return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
12438
- }
12439
- return requireExactFunctionPushdown({
12440
- capability: "LOCAL_ONLY",
12441
- reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }, ...inner.reasons]
12442
- });
12443
- }
12444
- case "LOGICAL": {
12445
- const left = classifyNode(where.left, resolveField2);
12446
- const right = classifyNode(where.right, resolveField2);
12447
- return combineLogical(where.op, left, right);
12221
+ return requireExactFunctionPushdown({
12222
+ capability: "LOCAL_ONLY",
12223
+ reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }, ...inner.reasons]
12224
+ });
12225
+ }
12226
+ case "LOGICAL": {
12227
+ const left = classifyNode(where.left, resolveField2);
12228
+ const right = classifyNode(where.right, resolveField2);
12229
+ return combineLogical(where.op, left, right);
12448
12230
  }
12449
12231
  }
12450
12232
  }
@@ -12505,24 +12287,68 @@ function classifyBinary(op, left, right, resolveField2) {
12505
12287
  };
12506
12288
  }
12507
12289
  return {
12508
- capability: "LOCAL_ONLY",
12290
+ capability: "LOCAL_ONLY",
12291
+ reasons: [{
12292
+ code: "WHERE_RESIDUAL",
12293
+ field: left.field,
12294
+ fieldType: semantics.fieldType,
12295
+ operator: nativeOp
12296
+ }]
12297
+ };
12298
+ }
12299
+ function isLegacyKintoneFunction(value) {
12300
+ return value.type === "KINTONE_FUNC" && LEGACY_KINTONE_FUNCTION_NAMES.has(value.name);
12301
+ }
12302
+ function classifyLegacyKintoneFunctionBinary(op, left, right, resolveField2) {
12303
+ const operator = normalizeOperator(op);
12304
+ const functionName = right.name;
12305
+ if (!LEGACY_KINTONE_FUNCTION_NAMES.has(functionName) || left.type !== "FIELD") {
12306
+ return legacyKintoneFunctionUnsupported(
12307
+ "WHERE_KINTONE_FUNCTION_CONTEXT_UNSUPPORTED",
12308
+ functionName,
12309
+ void 0,
12310
+ void 0,
12311
+ operator
12312
+ );
12313
+ }
12314
+ const semantics = resolveField2(left);
12315
+ const validFieldTypes = LEGACY_KINTONE_FUNCTION_FIELD_TYPES.get(functionName);
12316
+ if (!semantics || !validFieldTypes.has(semantics.fieldType) || semantics.inSubtable || semantics.requiresCollectionOperators) {
12317
+ return legacyKintoneFunctionUnsupported(
12318
+ "WHERE_KINTONE_FUNCTION_FIELD_TYPE_UNSUPPORTED",
12319
+ functionName,
12320
+ left.field,
12321
+ semantics?.fieldType,
12322
+ operator
12323
+ );
12324
+ }
12325
+ const validOperators = LEGACY_KINTONE_FUNCTION_OPERATORS.get(functionName);
12326
+ if (!validOperators.has(operator)) {
12327
+ return legacyKintoneFunctionUnsupported(
12328
+ "WHERE_KINTONE_FUNCTION_OPERATOR_UNSUPPORTED",
12329
+ functionName,
12330
+ left.field,
12331
+ semantics.fieldType,
12332
+ operator
12333
+ );
12334
+ }
12335
+ return {
12336
+ capability: "EXACT_PUSHDOWN",
12509
12337
  reasons: [{
12510
- code: "WHERE_RESIDUAL",
12338
+ code: "WHERE_EXACT",
12339
+ functionName,
12511
12340
  field: left.field,
12512
12341
  fieldType: semantics.fieldType,
12513
- operator: nativeOp
12342
+ operator
12514
12343
  }]
12515
12344
  };
12516
12345
  }
12517
- function isLegacyKintoneFunction(value) {
12518
- return value.type === "KINTONE_FUNC" && LEGACY_KINTONE_FUNCTION_NAMES.has(value.name);
12519
- }
12520
- function classifyLegacyKintoneFunctionBinary(op, left, right, resolveField2) {
12346
+ function classifyRelativeDateBinary(op, left, right, resolveField2) {
12521
12347
  const operator = normalizeOperator(op);
12522
12348
  const functionName = right.name;
12523
- if (!LEGACY_KINTONE_FUNCTION_NAMES.has(functionName) || left.type !== "FIELD") {
12524
- return legacyKintoneFunctionUnsupported(
12525
- "WHERE_KINTONE_FUNCTION_CONTEXT_UNSUPPORTED",
12349
+ if (left.type !== "FIELD") {
12350
+ return relativeDateUnsupported(
12351
+ "WHERE_RELATIVE_DATE_CONTEXT_UNSUPPORTED",
12526
12352
  functionName,
12527
12353
  void 0,
12528
12354
  void 0,
@@ -12530,258 +12356,567 @@ function classifyLegacyKintoneFunctionBinary(op, left, right, resolveField2) {
12530
12356
  );
12531
12357
  }
12532
12358
  const semantics = resolveField2(left);
12533
- const validFieldTypes = LEGACY_KINTONE_FUNCTION_FIELD_TYPES.get(functionName);
12534
- if (!semantics || !validFieldTypes.has(semantics.fieldType) || semantics.inSubtable || semantics.requiresCollectionOperators) {
12535
- return legacyKintoneFunctionUnsupported(
12536
- "WHERE_KINTONE_FUNCTION_FIELD_TYPE_UNSUPPORTED",
12359
+ if (!semantics) {
12360
+ return relativeDateUnsupported(
12361
+ "WHERE_RELATIVE_DATE_FIELD_TYPE_UNSUPPORTED",
12537
12362
  functionName,
12538
12363
  left.field,
12539
- semantics?.fieldType,
12364
+ void 0,
12365
+ operator
12366
+ );
12367
+ }
12368
+ if (!hasValidRelativeDateArguments(right)) {
12369
+ return relativeDateUnsupported(
12370
+ "WHERE_RELATIVE_DATE_ARGUMENT_INVALID",
12371
+ functionName,
12372
+ left.field,
12373
+ semantics.fieldType,
12374
+ operator
12375
+ );
12376
+ }
12377
+ if (!RELATIVE_DATE_OPERATORS.has(operator)) {
12378
+ return relativeDateUnsupported(
12379
+ "WHERE_RELATIVE_DATE_OPERATOR_UNSUPPORTED",
12380
+ functionName,
12381
+ left.field,
12382
+ semantics.fieldType,
12383
+ operator
12384
+ );
12385
+ }
12386
+ if (!RELATIVE_DATE_FIELD_TYPES.has(semantics.fieldType) || semantics.inSubtable || semantics.requiresCollectionOperators) {
12387
+ return relativeDateUnsupported(
12388
+ "WHERE_RELATIVE_DATE_FIELD_TYPE_UNSUPPORTED",
12389
+ functionName,
12390
+ left.field,
12391
+ semantics.fieldType,
12392
+ operator
12393
+ );
12394
+ }
12395
+ return {
12396
+ capability: "EXACT_PUSHDOWN",
12397
+ reasons: [{
12398
+ code: "WHERE_EXACT",
12399
+ functionName,
12400
+ field: left.field,
12401
+ fieldType: semantics.fieldType,
12402
+ operator
12403
+ }]
12404
+ };
12405
+ }
12406
+ function hasValidRelativeDateArguments(value) {
12407
+ if (!("args" in value) || !value.args) return false;
12408
+ switch (value.name) {
12409
+ case "YESTERDAY":
12410
+ case "TOMORROW":
12411
+ case "THIS_YEAR":
12412
+ case "LAST_YEAR":
12413
+ case "NEXT_YEAR":
12414
+ return value.args.kind === "NONE";
12415
+ case "FROM_TODAY":
12416
+ 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");
12417
+ case "THIS_WEEK":
12418
+ case "LAST_WEEK":
12419
+ case "NEXT_WEEK":
12420
+ 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");
12421
+ case "THIS_MONTH":
12422
+ case "LAST_MONTH":
12423
+ case "NEXT_MONTH":
12424
+ 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);
12425
+ default:
12426
+ return false;
12427
+ }
12428
+ }
12429
+ function classifyLocalOnlyField(field, operator, resolveField2) {
12430
+ const semantics = resolveField2(field);
12431
+ if (!semantics) return unsupported2("WHERE_FIELD_UNRESOLVED", field.field, void 0, operator);
12432
+ if (!isLocallyValidOperator(semantics.fieldType, operator)) {
12433
+ return unsupported2(
12434
+ "WHERE_OPERATOR_INVALID_FOR_FIELD_TYPE",
12435
+ field.field,
12436
+ semantics.fieldType,
12540
12437
  operator
12541
12438
  );
12542
12439
  }
12543
- const validOperators = LEGACY_KINTONE_FUNCTION_OPERATORS.get(functionName);
12544
- if (!validOperators.has(operator)) {
12545
- return legacyKintoneFunctionUnsupported(
12546
- "WHERE_KINTONE_FUNCTION_OPERATOR_UNSUPPORTED",
12547
- functionName,
12548
- left.field,
12549
- semantics.fieldType,
12550
- operator
12551
- );
12440
+ if (!LOCAL_SCALAR_TYPES.has(semantics.fieldType) && !LOCAL_COLLECTION_TYPES.has(semantics.fieldType)) {
12441
+ return unsupported2("WHERE_OPERATOR_UNSUPPORTED", field.field, semantics.fieldType, operator);
12442
+ }
12443
+ return {
12444
+ capability: "LOCAL_ONLY",
12445
+ reasons: [{ code: "WHERE_RESIDUAL", field: field.field, fieldType: semantics.fieldType, operator }]
12446
+ };
12447
+ }
12448
+ function isLocallyValidOperator(fieldType, operator) {
12449
+ const policy = LOCAL_VALID_OPERATORS.get(fieldType);
12450
+ return policy === void 0 || policy.has(operator);
12451
+ }
12452
+ function hasLocalContract(fieldType, op) {
12453
+ if (LOCAL_SCALAR_TYPES.has(fieldType)) return true;
12454
+ if (!LOCAL_COLLECTION_TYPES.has(fieldType)) return false;
12455
+ return op === "=" || op === "!=" || op === "<>" || op === "IN" || op === "NOT_IN" || op === "LIKE" || op === "NOT_LIKE" || op === "KLIKE" || op === "NOT_KLIKE";
12456
+ }
12457
+ function normalizeOperator(op) {
12458
+ switch (op) {
12459
+ case "<>":
12460
+ return "!=";
12461
+ case "IN":
12462
+ return "in";
12463
+ case "NOT_IN":
12464
+ return "not in";
12465
+ case "LIKE":
12466
+ case "KLIKE":
12467
+ return "like";
12468
+ case "NOT_LIKE":
12469
+ case "NOT_KLIKE":
12470
+ return "not like";
12471
+ default:
12472
+ return op;
12473
+ }
12474
+ }
12475
+ function combineLogical(op, left, right) {
12476
+ const reasons = [...left.reasons, ...right.reasons];
12477
+ if (left.capability === "UNSUPPORTED" || right.capability === "UNSUPPORTED") {
12478
+ return requireExactFunctionPushdown({ capability: "UNSUPPORTED", reasons });
12479
+ }
12480
+ if (left.capability === "EXACT_PUSHDOWN" && right.capability === "EXACT_PUSHDOWN") {
12481
+ return { capability: "EXACT_PUSHDOWN", reasons };
12482
+ }
12483
+ if (op === "AND" && (left.capability === "EXACT_PUSHDOWN" || right.capability === "EXACT_PUSHDOWN" || left.capability === "SUPERSET_PREFILTER" || right.capability === "SUPERSET_PREFILTER")) {
12484
+ return requireExactFunctionPushdown({
12485
+ capability: "SUPERSET_PREFILTER",
12486
+ reasons: [{ code: "WHERE_SUPERSET_PREFILTER" }, ...reasons]
12487
+ });
12488
+ }
12489
+ return requireExactFunctionPushdown({ capability: "LOCAL_ONLY", reasons });
12490
+ }
12491
+ function localExpression() {
12492
+ return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
12493
+ }
12494
+ function unsupported2(code, field, fieldType, operator) {
12495
+ return { capability: "UNSUPPORTED", reasons: [{ code, field, fieldType, operator }] };
12496
+ }
12497
+ function legacyKintoneFunctionUnsupported(code, functionName, field, fieldType, operator) {
12498
+ return requireExactFunctionPushdown({
12499
+ capability: "UNSUPPORTED",
12500
+ reasons: [{ code, functionName, field, fieldType, operator }]
12501
+ });
12502
+ }
12503
+ function relativeDateUnsupported(code, functionName, field, fieldType, operator) {
12504
+ return requireExactRelativeDatePushdown({
12505
+ capability: "UNSUPPORTED",
12506
+ reasons: [{ code, functionName, field, fieldType, operator }]
12507
+ });
12508
+ }
12509
+ function hasRelativeDateReason(reasons) {
12510
+ return reasons.some(
12511
+ (reason) => reason.code.startsWith("WHERE_RELATIVE_DATE_") || reason.functionName !== void 0 && isRelativeDateFunctionName(reason.functionName)
12512
+ );
12513
+ }
12514
+ function hasLegacyKintoneFunctionReason(reasons) {
12515
+ return reasons.some(
12516
+ (reason) => reason.code.startsWith("WHERE_KINTONE_FUNCTION_") || reason.functionName !== void 0 && LEGACY_KINTONE_FUNCTION_NAMES.has(reason.functionName)
12517
+ );
12518
+ }
12519
+ function requireExactRelativeDatePushdown(result) {
12520
+ if (result.capability === "EXACT_PUSHDOWN" || !hasRelativeDateReason(result.reasons) || result.reasons.some((reason) => reason.code === "WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN")) {
12521
+ return result;
12522
+ }
12523
+ const relative = result.reasons.find(
12524
+ (reason) => reason.code.startsWith("WHERE_RELATIVE_DATE_") || reason.functionName !== void 0 && isRelativeDateFunctionName(reason.functionName)
12525
+ );
12526
+ return {
12527
+ capability: result.capability,
12528
+ reasons: [
12529
+ ...result.reasons,
12530
+ {
12531
+ code: "WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN",
12532
+ functionName: relative.functionName,
12533
+ field: relative.field,
12534
+ fieldType: relative.fieldType,
12535
+ operator: relative.operator
12536
+ }
12537
+ ]
12538
+ };
12539
+ }
12540
+ function requireExactLegacyKintoneFunctionPushdown(result) {
12541
+ if (result.capability === "EXACT_PUSHDOWN" || !hasLegacyKintoneFunctionReason(result.reasons) || result.reasons.some(
12542
+ (reason) => reason.code === "WHERE_KINTONE_FUNCTION_REQUIRES_EXACT_PUSHDOWN"
12543
+ )) {
12544
+ return result;
12545
+ }
12546
+ const legacy = result.reasons.find(
12547
+ (reason) => reason.code.startsWith("WHERE_KINTONE_FUNCTION_") || reason.functionName !== void 0 && LEGACY_KINTONE_FUNCTION_NAMES.has(reason.functionName)
12548
+ );
12549
+ return {
12550
+ capability: result.capability,
12551
+ reasons: [
12552
+ ...result.reasons,
12553
+ {
12554
+ code: "WHERE_KINTONE_FUNCTION_REQUIRES_EXACT_PUSHDOWN",
12555
+ functionName: legacy.functionName,
12556
+ field: legacy.field,
12557
+ fieldType: legacy.fieldType,
12558
+ operator: legacy.operator
12559
+ }
12560
+ ]
12561
+ };
12562
+ }
12563
+ function requireExactFunctionPushdown(result) {
12564
+ return requireExactLegacyKintoneFunctionPushdown(
12565
+ requireExactRelativeDatePushdown(result)
12566
+ );
12567
+ }
12568
+
12569
+ // src/core/optimization/joinKeyPrefilter.ts
12570
+ var JOIN_KEY_IN_CHUNK_SIZE = 50;
12571
+ function buildJoinKeyPrefilterQueries(plan, field, quoteValue) {
12572
+ if (plan.kind === "RANGE") {
12573
+ return [`${field} >= ${quoteValue(plan.min)} and ${field} <= ${quoteValue(plan.max)}`];
12574
+ }
12575
+ if (plan.kind !== "IN") return [];
12576
+ const queries = [];
12577
+ for (let offset = 0; offset < plan.values.length; offset += JOIN_KEY_IN_CHUNK_SIZE) {
12578
+ const chunk3 = plan.values.slice(offset, offset + JOIN_KEY_IN_CHUNK_SIZE);
12579
+ queries.push(`${field} in (${chunk3.map(quoteValue).join(",")})`);
12580
+ }
12581
+ return queries;
12582
+ }
12583
+ var DATETIME_TYPES = /* @__PURE__ */ new Set(["DATETIME", "CREATED_TIME", "UPDATED_TIME"]);
12584
+ var JOIN_KEY_EMPTY_IN_FIELD_TYPES = /* @__PURE__ */ new Set([
12585
+ "SINGLE_LINE_TEXT",
12586
+ "LINK",
12587
+ "NUMBER",
12588
+ "CALC",
12589
+ "DROP_DOWN",
12590
+ "RADIO_BUTTON",
12591
+ "CHECK_BOX",
12592
+ "MULTI_SELECT",
12593
+ "STATUS"
12594
+ ]);
12595
+ function canonicalFor(fieldType, value) {
12596
+ if (fieldType === "DATE") return isCanonicalJoinDate(value);
12597
+ if (fieldType === "TIME") return isCanonicalJoinTime(value);
12598
+ if (DATETIME_TYPES.has(fieldType)) return isCanonicalJoinDateTime(value);
12599
+ return false;
12600
+ }
12601
+ function semanticsMatch(fieldType, semantics) {
12602
+ if (!semantics || semantics.compareMode !== "string") return false;
12603
+ if (fieldType === "DATE") return semantics.fieldType === "DATE";
12604
+ if (fieldType === "TIME") return semantics.fieldType === "TIME";
12605
+ if (DATETIME_TYPES.has(fieldType)) return DATETIME_TYPES.has(semantics.fieldType);
12606
+ return false;
12607
+ }
12608
+ function planJoinKeyPrefilter(input) {
12609
+ if (input.sourceRowCount === 0) return { kind: "EMPTY_SOURCE" };
12610
+ if (!input.fieldType) {
12611
+ return { kind: "FALLBACK", reason: "JOIN_KEY_FIELD_TYPE_UNRESOLVED" };
12612
+ }
12613
+ const operators = nativeWhereOperatorsForType(input.fieldType);
12614
+ if (operators.has("in")) {
12615
+ if (input.values === void 0) {
12616
+ return { kind: "FALLBACK", reason: "JOIN_KEY_VALUES_RUNTIME" };
12617
+ }
12618
+ if (input.hasEmptyValue && !JOIN_KEY_EMPTY_IN_FIELD_TYPES.has(input.fieldType)) {
12619
+ return { kind: "FALLBACK", reason: "JOIN_KEY_EMPTY_VALUE" };
12620
+ }
12621
+ const values2 = [...new Set(input.values)];
12622
+ if (values2.length === 0) return { kind: "EMPTY_SOURCE" };
12623
+ if (values2.length > input.maxInKeys) {
12624
+ return { kind: "FALLBACK", reason: "JOIN_KEY_LIMIT_EXCEEDED" };
12625
+ }
12626
+ return { kind: "IN", values: values2, relation: "exact" };
12552
12627
  }
12553
- return {
12554
- capability: "EXACT_PUSHDOWN",
12555
- reasons: [{
12556
- code: "WHERE_EXACT",
12557
- functionName,
12558
- field: left.field,
12559
- fieldType: semantics.fieldType,
12560
- operator
12561
- }]
12562
- };
12563
- }
12564
- function classifyRelativeDateBinary(op, left, right, resolveField2) {
12565
- const operator = normalizeOperator(op);
12566
- const functionName = right.name;
12567
- if (left.type !== "FIELD") {
12568
- return relativeDateUnsupported(
12569
- "WHERE_RELATIVE_DATE_CONTEXT_UNSUPPORTED",
12570
- functionName,
12571
- void 0,
12572
- void 0,
12573
- operator
12574
- );
12628
+ if (!operators.has(">=") || !operators.has("<=")) {
12629
+ return { kind: "FALLBACK", reason: "JOIN_KEY_OPERATOR_UNAVAILABLE" };
12575
12630
  }
12576
- const semantics = resolveField2(left);
12577
- if (!semantics) {
12578
- return relativeDateUnsupported(
12579
- "WHERE_RELATIVE_DATE_FIELD_TYPE_UNSUPPORTED",
12580
- functionName,
12581
- left.field,
12582
- void 0,
12583
- operator
12584
- );
12631
+ if (input.values === void 0) {
12632
+ return { kind: "RANGE_CANDIDATE", relation: "superset", reason: "JOIN_KEY_VALUES_RUNTIME" };
12585
12633
  }
12586
- if (!hasValidRelativeDateArguments(right)) {
12587
- return relativeDateUnsupported(
12588
- "WHERE_RELATIVE_DATE_ARGUMENT_INVALID",
12589
- functionName,
12590
- left.field,
12591
- semantics.fieldType,
12592
- operator
12593
- );
12634
+ if (!semanticsMatch(input.fieldType, input.sourceSemantics)) {
12635
+ return { kind: "FALLBACK", reason: "JOIN_KEY_SEMANTICS_UNRESOLVED" };
12594
12636
  }
12595
- if (!RELATIVE_DATE_OPERATORS.has(operator)) {
12596
- return relativeDateUnsupported(
12597
- "WHERE_RELATIVE_DATE_OPERATOR_UNSUPPORTED",
12598
- functionName,
12599
- left.field,
12600
- semantics.fieldType,
12601
- operator
12602
- );
12637
+ const sourceSemantics = input.sourceSemantics;
12638
+ if (input.hasEmptyValue) {
12639
+ return { kind: "FALLBACK", reason: "JOIN_KEY_EMPTY_VALUE" };
12603
12640
  }
12604
- if (!RELATIVE_DATE_FIELD_TYPES.has(semantics.fieldType) || semantics.inSubtable || semantics.requiresCollectionOperators) {
12605
- return relativeDateUnsupported(
12606
- "WHERE_RELATIVE_DATE_FIELD_TYPE_UNSUPPORTED",
12607
- functionName,
12608
- left.field,
12609
- semantics.fieldType,
12610
- operator
12611
- );
12641
+ const values = [...new Set(input.values)];
12642
+ if (values.some((value) => !canonicalFor(input.fieldType, value))) {
12643
+ return { kind: "FALLBACK", reason: "JOIN_KEY_NON_CANONICAL_VALUE" };
12644
+ }
12645
+ if (values.length === 0) {
12646
+ return { kind: "FALLBACK", reason: "JOIN_KEY_EMPTY_VALUE" };
12612
12647
  }
12648
+ let min = values[0];
12649
+ let max = values[0];
12650
+ for (let i = 1; i < values.length; i += 1) {
12651
+ const value = values[i];
12652
+ if (compareCanonicalValues(value, min, sourceSemantics) < 0) min = value;
12653
+ if (compareCanonicalValues(value, max, sourceSemantics) > 0) max = value;
12654
+ }
12655
+ return { kind: "RANGE", min, max, relation: "superset" };
12656
+ }
12657
+
12658
+ // src/core/explainMetadata.ts
12659
+ function buildGroupingExplainMetadata(statement, canonicalItemCount) {
12660
+ const grouping = normalizeGroupingSpec(statement);
12661
+ if (grouping.type !== "GROUPING_SETS") return null;
12613
12662
  return {
12614
- capability: "EXACT_PUSHDOWN",
12615
- reasons: [{
12616
- code: "WHERE_EXACT",
12617
- functionName,
12618
- field: left.field,
12619
- fieldType: semantics.fieldType,
12620
- operator
12621
- }]
12663
+ source: grouping.source,
12664
+ expandedSetCount: grouping.sets.length,
12665
+ groupingItemCount: canonicalItemCount ?? grouping.allItems.length,
12666
+ setLimit: B65_MAX_GROUPING_SETS,
12667
+ itemLimit: B65_MAX_GROUPING_ITEMS,
12668
+ outputRowLimit: B65_MAX_GENERATED_ROWS
12622
12669
  };
12623
12670
  }
12624
- function hasValidRelativeDateArguments(value) {
12625
- if (!("args" in value) || !value.args) return false;
12626
- switch (value.name) {
12627
- case "YESTERDAY":
12628
- case "TOMORROW":
12629
- case "THIS_YEAR":
12630
- case "LAST_YEAR":
12631
- case "NEXT_YEAR":
12632
- return value.args.kind === "NONE";
12633
- case "FROM_TODAY":
12634
- 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");
12635
- case "THIS_WEEK":
12636
- case "LAST_WEEK":
12637
- case "NEXT_WEEK":
12638
- 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");
12639
- case "THIS_MONTH":
12640
- case "LAST_MONTH":
12641
- case "NEXT_MONTH":
12642
- 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);
12643
- default:
12671
+ function whereNeedsFieldMetadata(where) {
12672
+ if (where === null) return false;
12673
+ switch (where.type) {
12674
+ case "BINARY":
12675
+ return valueNeedsFieldMetadata(where.left);
12676
+ case "NULL_CHECK":
12677
+ return valueNeedsFieldMetadata(where.field);
12678
+ case "LOGICAL":
12679
+ return whereNeedsFieldMetadata(where.left) || whereNeedsFieldMetadata(where.right);
12680
+ case "NOT":
12681
+ case "GROUP":
12682
+ return whereNeedsFieldMetadata(where.expr);
12683
+ case "EXISTS":
12684
+ case "BOOLEAN":
12644
12685
  return false;
12645
12686
  }
12646
12687
  }
12647
- function classifyLocalOnlyField(field, operator, resolveField2) {
12648
- const semantics = resolveField2(field);
12649
- if (!semantics) return unsupported2("WHERE_FIELD_UNRESOLVED", field.field, void 0, operator);
12650
- if (!isLocallyValidOperator(semantics.fieldType, operator)) {
12651
- return unsupported2(
12652
- "WHERE_OPERATOR_INVALID_FOR_FIELD_TYPE",
12653
- field.field,
12654
- semantics.fieldType,
12655
- operator
12656
- );
12688
+ function valueNeedsFieldMetadata(value) {
12689
+ if (Array.isArray(value)) return value.some(valueNeedsFieldMetadata);
12690
+ if (value === null || typeof value !== "object") return false;
12691
+ const item = value;
12692
+ if (item["type"] === "FIELD") return item["field"] !== "$id";
12693
+ if (item["type"] === "SELECT") return false;
12694
+ return Object.values(item).some(valueNeedsFieldMetadata);
12695
+ }
12696
+ function selectNeedsOwnMetadata(statement) {
12697
+ return whereNeedsFieldMetadata(statement.where) || statement.groupBy.length > 0 || normalizeGroupingSpec(statement).type === "GROUPING_SETS" || statement.orderBy.length > 0 || statement.joins.some(
12698
+ (join2) => join2.table.appId > 0 && join2.table.cteName === null && (join2.on.left.field !== "$id" || join2.on.right.field !== "$id")
12699
+ ) || statement.columns.some(
12700
+ (column) => column.type === "WINDOW_COL" && column.orderBy.length > 0
12701
+ );
12702
+ }
12703
+ function explainNeedsAppMetadata(statement) {
12704
+ const seen = /* @__PURE__ */ new Set();
12705
+ const visit = (node) => {
12706
+ if (node === null || typeof node !== "object") return false;
12707
+ if (seen.has(node)) return false;
12708
+ seen.add(node);
12709
+ if (Array.isArray(node)) return node.some(visit);
12710
+ const item = node;
12711
+ if (item["type"] === "VALIDATE") return true;
12712
+ if (item["type"] === "SELECT" && selectNeedsOwnMetadata(node)) {
12713
+ return true;
12714
+ }
12715
+ if ((item["type"] === "UPDATE" || item["type"] === "DELETE") && whereNeedsFieldMetadata(node.where)) {
12716
+ return true;
12717
+ }
12718
+ if (item["type"] === "UPDATE" && Array.isArray(item["applyBlocks"]) && item["applyBlocks"].length > 0) return true;
12719
+ return Object.values(item).some(visit);
12720
+ };
12721
+ return visit(statement);
12722
+ }
12723
+
12724
+ // src/api/fetchAll.ts
12725
+ async function fetchAll(fetcher, app, query, fields, options = {}) {
12726
+ const pageSize = options.pageSize ?? PAGE_SIZE_DEFAULT;
12727
+ const parallel = Math.max(1, options.parallel ?? PARALLEL_DEFAULT);
12728
+ const maxRecords = options.maxRecords ?? MAX_RECORDS_DEFAULT;
12729
+ const onLimit = options.onLimit ?? "error";
12730
+ const stopAfter = options.stopAfter;
12731
+ if (stopAfter !== void 0 && (!Number.isSafeInteger(stopAfter) || stopAfter <= 0 || stopAfter > maxRecords)) {
12732
+ throw new RangeError("stopAfter must be a positive safe integer <= maxRecords");
12657
12733
  }
12658
- if (!LOCAL_SCALAR_TYPES.has(semantics.fieldType) && !LOCAL_COLLECTION_TYPES.has(semantics.fieldType)) {
12659
- return unsupported2("WHERE_OPERATOR_UNSUPPORTED", field.field, semantics.fieldType, operator);
12734
+ const fetchCap = stopAfter ?? maxRecords;
12735
+ const fetchFields = fields.length > 0 && !fields.includes("$id") ? [...fields, "$id"] : fields;
12736
+ const allRecords = [];
12737
+ let notified = false;
12738
+ const limitMessage = `\u53D6\u5F97\u4EF6\u6570\u304C\u4E0A\u9650\uFF08${maxRecords} \u4EF6\uFF09\u3092\u8D85\u3048\u307E\u3057\u305F\u3002WHERE \u53E5\u3067\u7D5E\u308A\u8FBC\u3080\u304B\u3001maxRecords \u3092\u5F15\u304D\u4E0A\u3052\u3066\u304F\u3060\u3055\u3044\u3002`;
12739
+ let cursorId = 0;
12740
+ let windowOffset = 0;
12741
+ const cursorQuery0 = buildCursorQuery(query, cursorId);
12742
+ const first = await fetchPage(fetcher, app, cursorQuery0, fetchFields, pageSize, windowOffset);
12743
+ notifySearchAborted(first, options);
12744
+ allRecords.push(...first.records);
12745
+ if (stopAfter !== void 0 && allRecords.length >= stopAfter) {
12746
+ return allRecords.slice(0, stopAfter);
12747
+ }
12748
+ if (allRecords.length > maxRecords) {
12749
+ if (onLimit === "truncate") {
12750
+ if (!notified && options.onTruncate) {
12751
+ options.onTruncate(maxRecords);
12752
+ notified = true;
12753
+ }
12754
+ return allRecords.slice(0, maxRecords);
12755
+ }
12756
+ throw new FetchAllLimitError(limitMessage);
12757
+ }
12758
+ if (first.records.length < pageSize) return allRecords;
12759
+ windowOffset += pageSize;
12760
+ if (windowOffset >= KINTONE_MAX_OFFSET) {
12761
+ cursorId = getLastId(first.records);
12762
+ windowOffset = 0;
12763
+ }
12764
+ while (true) {
12765
+ if (stopAfter !== void 0 && allRecords.length >= stopAfter) {
12766
+ return allRecords.slice(0, stopAfter);
12767
+ }
12768
+ if (allRecords.length >= maxRecords) {
12769
+ if (onLimit === "truncate") {
12770
+ if (!notified && options.onTruncate) {
12771
+ options.onTruncate(maxRecords);
12772
+ notified = true;
12773
+ }
12774
+ return allRecords.slice(0, maxRecords);
12775
+ }
12776
+ throw new FetchAllLimitError(limitMessage);
12777
+ }
12778
+ const remaining = fetchCap - allRecords.length;
12779
+ const maxPagesByLimit = Math.ceil(remaining / pageSize);
12780
+ const batchParallel = Math.max(1, Math.min(parallel, maxPagesByLimit));
12781
+ const batchOffsets = [];
12782
+ for (let i = 0; i < batchParallel; i++) {
12783
+ const off = windowOffset + i * pageSize;
12784
+ if (off >= KINTONE_MAX_OFFSET) break;
12785
+ batchOffsets.push(off);
12786
+ }
12787
+ const cq = buildCursorQuery(query, cursorId);
12788
+ const responses = await Promise.all(
12789
+ batchOffsets.map(
12790
+ (offset) => fetchPage(fetcher, app, cq, fetchFields, pageSize, offset)
12791
+ )
12792
+ );
12793
+ for (const response of responses) notifySearchAborted(response, options);
12794
+ let done = false;
12795
+ for (const res of responses) {
12796
+ allRecords.push(...res.records);
12797
+ if (stopAfter !== void 0 && allRecords.length >= stopAfter) {
12798
+ return allRecords.slice(0, stopAfter);
12799
+ }
12800
+ if (allRecords.length > maxRecords) {
12801
+ if (onLimit === "truncate") {
12802
+ if (!notified && options.onTruncate) {
12803
+ options.onTruncate(maxRecords);
12804
+ notified = true;
12805
+ }
12806
+ return allRecords.slice(0, maxRecords);
12807
+ }
12808
+ throw new FetchAllLimitError(limitMessage);
12809
+ }
12810
+ if (res.records.length < pageSize) {
12811
+ done = true;
12812
+ break;
12813
+ }
12814
+ }
12815
+ if (done) break;
12816
+ windowOffset += pageSize * batchOffsets.length;
12817
+ if (windowOffset >= KINTONE_MAX_OFFSET) {
12818
+ const lastRes = responses[responses.length - 1];
12819
+ cursorId = getLastId(lastRes.records);
12820
+ windowOffset = 0;
12821
+ }
12660
12822
  }
12661
- return {
12662
- capability: "LOCAL_ONLY",
12663
- reasons: [{ code: "WHERE_RESIDUAL", field: field.field, fieldType: semantics.fieldType, operator }]
12664
- };
12823
+ return allRecords;
12665
12824
  }
12666
- function isLocallyValidOperator(fieldType, operator) {
12667
- const policy = LOCAL_VALID_OPERATORS.get(fieldType);
12668
- return policy === void 0 || policy.has(operator);
12825
+ function notifySearchAborted(response, options) {
12826
+ if (response.searchAborted) options.onSearchAborted?.();
12669
12827
  }
12670
- function hasLocalContract(fieldType, op) {
12671
- if (LOCAL_SCALAR_TYPES.has(fieldType)) return true;
12672
- if (!LOCAL_COLLECTION_TYPES.has(fieldType)) return false;
12673
- return op === "=" || op === "!=" || op === "<>" || op === "IN" || op === "NOT_IN" || op === "LIKE" || op === "NOT_LIKE" || op === "KLIKE" || op === "NOT_KLIKE";
12828
+ function extractIds(records) {
12829
+ return records.map((r) => {
12830
+ const raw = r["$id"]?.value;
12831
+ if (raw === void 0) {
12832
+ throw new Error(
12833
+ '\u30EC\u30B3\u30FC\u30C9\u306B $id \u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093\u3002fields \u306B "$id" \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002'
12834
+ );
12835
+ }
12836
+ const id = Number(raw);
12837
+ if (!Number.isFinite(id)) {
12838
+ throw new Error(`$id \u306E\u5024\u304C\u6570\u5024\u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${raw}`);
12839
+ }
12840
+ return id;
12841
+ });
12674
12842
  }
12675
- function normalizeOperator(op) {
12676
- switch (op) {
12677
- case "<>":
12678
- return "!=";
12679
- case "IN":
12680
- return "in";
12681
- case "NOT_IN":
12682
- return "not in";
12683
- case "LIKE":
12684
- case "KLIKE":
12685
- return "like";
12686
- case "NOT_LIKE":
12687
- case "NOT_KLIKE":
12688
- return "not like";
12689
- default:
12690
- return op;
12691
- }
12843
+ var PAGE_SIZE_DEFAULT = 500;
12844
+ var PARALLEL_DEFAULT = 1;
12845
+ var MAX_RECORDS_DEFAULT = 1e4;
12846
+ var KINTONE_MAX_OFFSET = 1e4;
12847
+ async function fetchPage(fetcher, app, query, fields, pageSize, offset) {
12848
+ const pageQuery = buildPageQuery(query, pageSize, offset);
12849
+ return fetcher({ app, query: pageQuery, fields });
12692
12850
  }
12693
- function combineLogical(op, left, right) {
12694
- const reasons = [...left.reasons, ...right.reasons];
12695
- if (left.capability === "UNSUPPORTED" || right.capability === "UNSUPPORTED") {
12696
- return requireExactFunctionPushdown({ capability: "UNSUPPORTED", reasons });
12697
- }
12698
- if (left.capability === "EXACT_PUSHDOWN" && right.capability === "EXACT_PUSHDOWN") {
12699
- return { capability: "EXACT_PUSHDOWN", reasons };
12700
- }
12701
- if (op === "AND" && (left.capability === "EXACT_PUSHDOWN" || right.capability === "EXACT_PUSHDOWN" || left.capability === "SUPERSET_PREFILTER" || right.capability === "SUPERSET_PREFILTER")) {
12702
- return requireExactFunctionPushdown({
12703
- capability: "SUPERSET_PREFILTER",
12704
- reasons: [{ code: "WHERE_SUPERSET_PREFILTER" }, ...reasons]
12705
- });
12851
+ function buildCursorQuery(baseQuery, cursorId) {
12852
+ const base = baseQuery.trimEnd();
12853
+ if (cursorId <= 0) {
12854
+ return base ? `${base} order by $id asc` : "order by $id asc";
12706
12855
  }
12707
- return requireExactFunctionPushdown({ capability: "LOCAL_ONLY", reasons });
12708
- }
12709
- function localExpression() {
12710
- return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
12711
- }
12712
- function unsupported2(code, field, fieldType, operator) {
12713
- return { capability: "UNSUPPORTED", reasons: [{ code, field, fieldType, operator }] };
12714
- }
12715
- function legacyKintoneFunctionUnsupported(code, functionName, field, fieldType, operator) {
12716
- return requireExactFunctionPushdown({
12717
- capability: "UNSUPPORTED",
12718
- reasons: [{ code, functionName, field, fieldType, operator }]
12719
- });
12720
- }
12721
- function relativeDateUnsupported(code, functionName, field, fieldType, operator) {
12722
- return requireExactRelativeDatePushdown({
12723
- capability: "UNSUPPORTED",
12724
- reasons: [{ code, functionName, field, fieldType, operator }]
12725
- });
12856
+ const cursor = `$id > ${cursorId} order by $id asc`;
12857
+ return base ? `(${base}) and ${cursor}` : cursor;
12726
12858
  }
12727
- function hasRelativeDateReason(reasons) {
12728
- return reasons.some(
12729
- (reason) => reason.code.startsWith("WHERE_RELATIVE_DATE_") || reason.functionName !== void 0 && isRelativeDateFunctionName(reason.functionName)
12730
- );
12859
+ function buildPageQuery(query, pageSize, offset) {
12860
+ const base = query.trimEnd();
12861
+ const suffix = `limit ${pageSize} offset ${offset}`;
12862
+ return base ? `${base} ${suffix}` : suffix;
12731
12863
  }
12732
- function hasLegacyKintoneFunctionReason(reasons) {
12733
- return reasons.some(
12734
- (reason) => reason.code.startsWith("WHERE_KINTONE_FUNCTION_") || reason.functionName !== void 0 && LEGACY_KINTONE_FUNCTION_NAMES.has(reason.functionName)
12735
- );
12864
+ function getLastId(records) {
12865
+ const last = records[records.length - 1];
12866
+ const raw = last?.["$id"]?.value;
12867
+ if (raw === void 0) return 0;
12868
+ const id = Number(raw);
12869
+ return Number.isFinite(id) ? id : 0;
12736
12870
  }
12737
- function requireExactRelativeDatePushdown(result) {
12738
- if (result.capability === "EXACT_PUSHDOWN" || !hasRelativeDateReason(result.reasons) || result.reasons.some((reason) => reason.code === "WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN")) {
12739
- return result;
12871
+ var FetchAllLimitError = class extends Error {
12872
+ constructor(message, completeInputWrapped = false) {
12873
+ super(message);
12874
+ this.completeInputWrapped = completeInputWrapped;
12875
+ this.name = "FetchAllLimitError";
12740
12876
  }
12741
- const relative = result.reasons.find(
12742
- (reason) => reason.code.startsWith("WHERE_RELATIVE_DATE_") || reason.functionName !== void 0 && isRelativeDateFunctionName(reason.functionName)
12743
- );
12877
+ };
12878
+
12879
+ // src/core/optimization/sharedPlanner.ts
12880
+ async function fetchRecordsForSharedPlan(getRecords, app, query, fields, options) {
12881
+ const records = await fetchAll(getRecords, app, query, fields, {
12882
+ maxRecords: options.maxRecords,
12883
+ parallel: options.parallel,
12884
+ onLimit: options.onLimit ?? "error",
12885
+ onTruncate: options.onTruncate,
12886
+ onSearchAborted: options.onSearchAborted
12887
+ });
12744
12888
  return {
12745
- capability: result.capability,
12746
- reasons: [
12747
- ...result.reasons,
12748
- {
12749
- code: "WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN",
12750
- functionName: relative.functionName,
12751
- field: relative.field,
12752
- fieldType: relative.fieldType,
12753
- operator: relative.operator
12754
- }
12755
- ]
12889
+ records,
12890
+ metrics: { fetchedRows: records.length }
12756
12891
  };
12757
12892
  }
12758
- function requireExactLegacyKintoneFunctionPushdown(result) {
12759
- if (result.capability === "EXACT_PUSHDOWN" || !hasLegacyKintoneFunctionReason(result.reasons) || result.reasons.some(
12760
- (reason) => reason.code === "WHERE_KINTONE_FUNCTION_REQUIRES_EXACT_PUSHDOWN"
12761
- )) {
12762
- return result;
12763
- }
12764
- const legacy = result.reasons.find(
12765
- (reason) => reason.code.startsWith("WHERE_KINTONE_FUNCTION_") || reason.functionName !== void 0 && LEGACY_KINTONE_FUNCTION_NAMES.has(reason.functionName)
12893
+ async function resolveDmlTargetIds(getRecords, app, query, options) {
12894
+ const { records, metrics } = await fetchRecordsForSharedPlan(
12895
+ getRecords,
12896
+ app,
12897
+ query,
12898
+ ["$id"],
12899
+ options
12766
12900
  );
12767
12901
  return {
12768
- capability: result.capability,
12769
- reasons: [
12770
- ...result.reasons,
12771
- {
12772
- code: "WHERE_KINTONE_FUNCTION_REQUIRES_EXACT_PUSHDOWN",
12773
- functionName: legacy.functionName,
12774
- field: legacy.field,
12775
- fieldType: legacy.fieldType,
12776
- operator: legacy.operator
12777
- }
12778
- ]
12902
+ ids: extractIds(records),
12903
+ metrics
12779
12904
  };
12780
12905
  }
12781
- function requireExactFunctionPushdown(result) {
12782
- return requireExactLegacyKintoneFunctionPushdown(
12783
- requireExactRelativeDatePushdown(result)
12784
- );
12906
+
12907
+ // src/core/optimization/joinNumberLiteralPolicy.ts
12908
+ function isJoinNumberLiteralSupported(literal) {
12909
+ const source = literal.raw ?? String(literal.value);
12910
+ const decimal = parseExactDecimal(source);
12911
+ if (decimal === null) return false;
12912
+ if (decimal.sign === 0) return numberLiteralText(literal) === "0";
12913
+ const fractionDigits = Math.max(decimal.scale, 0);
12914
+ const integerDigits = Math.max(decimal.coefficient.length - decimal.scale, 0);
12915
+ if (fractionDigits > 10 || integerDigits + fractionDigits > 30) {
12916
+ return false;
12917
+ }
12918
+ const canonical = formatPlainDecimal(decimal);
12919
+ return numberLiteralText(literal) === canonical;
12785
12920
  }
12786
12921
 
12787
12922
  // src/core/optimization/joinPredicatePushdown.ts
@@ -12792,6 +12927,14 @@ var SELECTION_TYPES = /* @__PURE__ */ new Set([
12792
12927
  "MULTI_SELECT",
12793
12928
  "STATUS"
12794
12929
  ]);
12930
+ var USER_CODE_TYPES = /* @__PURE__ */ new Set([
12931
+ "CREATOR",
12932
+ "MODIFIER",
12933
+ "USER_SELECT",
12934
+ "ORGANIZATION_SELECT",
12935
+ "GROUP_SELECT",
12936
+ "STATUS_ASSIGNEE"
12937
+ ]);
12795
12938
  var KLIKE_TYPES = /* @__PURE__ */ new Set([
12796
12939
  "SINGLE_LINE_TEXT",
12797
12940
  "LINK",
@@ -12799,7 +12942,7 @@ var KLIKE_TYPES = /* @__PURE__ */ new Set([
12799
12942
  "RICH_TEXT",
12800
12943
  "FILE"
12801
12944
  ]);
12802
- var DATETIME_EQUALITY_TYPES = /* @__PURE__ */ new Set([
12945
+ var DATETIME_TYPES2 = /* @__PURE__ */ new Set([
12803
12946
  "DATETIME",
12804
12947
  "CREATED_TIME",
12805
12948
  "UPDATED_TIME"
@@ -13395,21 +13538,45 @@ function classifySupportedLeaf(predicate, owner, fieldType) {
13395
13538
  return isPositiveSafeInteger(predicate.right) && (predicate.op === "=" || predicate.op === "<" || predicate.op === ">" || predicate.op === "<=" || predicate.op === ">=") ? "exact" : "unsafe";
13396
13539
  }
13397
13540
  if (fieldType === "RECORD_NUMBER") {
13398
- return "unsafe";
13541
+ return classifySupersetScalarOrListLiteral(predicate);
13399
13542
  }
13400
13543
  if (fieldType === "NUMBER") {
13401
- if (predicate.right.type !== "NUMBER") return "unsafe";
13402
- if (predicate.op === "=") return "superset";
13403
- return (predicate.op === "<" || predicate.op === ">") && isSafeIntegerLiteral(predicate.right) ? "superset" : "unsafe";
13544
+ if ((predicate.op === "IN" || predicate.op === "NOT_IN") && predicate.right.type === "IN_LIST" && predicate.right.values.length > 0 && predicate.right.values.every(
13545
+ (value) => value.type === "NUMBER" && isJoinNumberLiteralSupported(value)
13546
+ )) {
13547
+ return "exact";
13548
+ }
13549
+ if ((predicate.op === "=" || predicate.op === "!=" || predicate.op === "<>" || predicate.op === "<" || predicate.op === ">" || predicate.op === "<=" || predicate.op === ">=") && predicate.right.type === "NUMBER" && isJoinNumberLiteralSupported(predicate.right)) {
13550
+ return "exact";
13551
+ }
13552
+ return "unsafe";
13553
+ }
13554
+ if (fieldType === "CALC") {
13555
+ return classifySupersetScalarOrListLiteral(predicate);
13556
+ }
13557
+ if (USER_CODE_TYPES.has(fieldType)) {
13558
+ if (predicate.op !== "IN" && predicate.op !== "NOT_IN" || predicate.right.type !== "IN_LIST" || predicate.right.values.length === 0) return "unsafe";
13559
+ return predicate.right.values.every(
13560
+ (value) => value.type === "STRING" && value.value !== ""
13561
+ ) ? "exact" : "unsafe";
13404
13562
  }
13405
- if (fieldType === "SINGLE_LINE_TEXT") {
13406
- return predicate.op === "=" && predicate.right.type === "STRING" && predicate.right.value !== "" ? "superset" : "unsafe";
13563
+ if (fieldType === "SINGLE_LINE_TEXT" || fieldType === "LINK") {
13564
+ if ((predicate.op === "IN" || predicate.op === "NOT_IN") && predicate.right.type === "IN_LIST" && predicate.right.values.length > 0 && predicate.right.values.every(
13565
+ (value) => value.type === "STRING" && value.value !== ""
13566
+ )) {
13567
+ return "exact";
13568
+ }
13569
+ return (predicate.op === "=" || predicate.op === "!=" || predicate.op === "<>") && predicate.right.type === "STRING" && predicate.right.value !== "" ? "exact" : "unsafe";
13407
13570
  }
13408
- if (fieldType === "DATE" || fieldType === "TIME" || DATETIME_EQUALITY_TYPES.has(fieldType)) {
13409
- if (predicate.op !== "=" || predicate.right.type !== "STRING") return "unsafe";
13410
- if (fieldType === "DATE") return isCanonicalDate(predicate.right.value) ? "superset" : "unsafe";
13411
- if (fieldType === "TIME") return isCanonicalTime(predicate.right.value) ? "superset" : "unsafe";
13412
- return isCanonicalDateTime(predicate.right.value) ? "superset" : "unsafe";
13571
+ if (fieldType === "DATE" || fieldType === "TIME" || DATETIME_TYPES2.has(fieldType)) {
13572
+ if (predicate.op !== "=" && predicate.op !== "!=" && predicate.op !== "<>" && predicate.op !== "<" && predicate.op !== ">" && predicate.op !== "<=" && predicate.op !== ">=" || predicate.right.type !== "STRING") return "unsafe";
13573
+ if (fieldType === "DATE") {
13574
+ return isCanonicalJoinDate(predicate.right.value) ? "exact" : "unsafe";
13575
+ }
13576
+ if (fieldType === "TIME") {
13577
+ return isCanonicalJoinTime(predicate.right.value) ? "exact" : "unsafe";
13578
+ }
13579
+ return isCanonicalJoinDateTime(predicate.right.value) ? "exact" : "unsafe";
13413
13580
  }
13414
13581
  if (SELECTION_TYPES.has(fieldType)) {
13415
13582
  if (predicate.op !== "IN" && predicate.op !== "NOT_IN" || predicate.right.type !== "IN_LIST" || predicate.right.values.length === 0) return "unsafe";
@@ -13421,6 +13588,19 @@ function classifySupportedLeaf(predicate, owner, fieldType) {
13421
13588
  }
13422
13589
  return "unsafe";
13423
13590
  }
13591
+ function classifySupersetScalarOrListLiteral(predicate) {
13592
+ const supportedLiteral = (value) => value.type === "NUMBER" && isJoinNumberLiteralSupported(value) || value.type === "STRING" && value.value !== "";
13593
+ if ((predicate.op === "IN" || predicate.op === "NOT_IN") && predicate.right.type === "IN_LIST") {
13594
+ const values = predicate.right.values;
13595
+ if (values.length > 0 && values.every(supportedLiteral) && values.every((value) => value.type === values[0].type)) {
13596
+ return "superset";
13597
+ }
13598
+ }
13599
+ if ((predicate.op === "=" || predicate.op === "!=" || predicate.op === "<>" || predicate.op === "<" || predicate.op === ">" || predicate.op === "<=" || predicate.op === ">=") && supportedLiteral(predicate.right)) {
13600
+ return "superset";
13601
+ }
13602
+ return "unsafe";
13603
+ }
13424
13604
  function owned(source, fieldCode) {
13425
13605
  return Object.freeze({
13426
13606
  status: "OWNED",
@@ -13445,24 +13625,6 @@ function isPositiveSafeInteger(value) {
13445
13625
  function isSafeIntegerLiteral(value) {
13446
13626
  return /^-?\d+$/.test(numberLiteralText(value)) && Number.isSafeInteger(value.value);
13447
13627
  }
13448
- function isCanonicalDate(value) {
13449
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
13450
- if (!match) return false;
13451
- const year = Number(match[1]);
13452
- const month = Number(match[2]);
13453
- const day = Number(match[3]);
13454
- const date = new Date(Date.UTC(year, month - 1, day));
13455
- return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
13456
- }
13457
- function isCanonicalTime(value) {
13458
- const match = /^(\d{2}):(\d{2})$/.exec(value);
13459
- return match !== null && Number(match[1]) <= 23 && Number(match[2]) <= 59;
13460
- }
13461
- function isCanonicalDateTime(value) {
13462
- const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/.exec(value);
13463
- if (!match || !isCanonicalDate(match[1])) return false;
13464
- return Number(match[2]) <= 23 && Number(match[3]) <= 59 && Number(match[4]) <= 59;
13465
- }
13466
13628
  function collectKlikes2(where, out) {
13467
13629
  if (where === null) return;
13468
13630
  if (isKlike(where)) {
@@ -20174,6 +20336,8 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
20174
20336
  parallel,
20175
20337
  options.onLimitReached ?? "error",
20176
20338
  warnings,
20339
+ cacheContext,
20340
+ cteCache,
20177
20341
  null,
20178
20342
  plainGroupByPlan,
20179
20343
  ""
@@ -20562,6 +20726,8 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
20562
20726
  parallel,
20563
20727
  effectiveOptions.onLimitReached ?? "error",
20564
20728
  warnings,
20729
+ cacheContext,
20730
+ cteCache,
20565
20731
  pushDownCond,
20566
20732
  plainGroupByPlan
20567
20733
  );
@@ -20754,10 +20920,9 @@ function splitChunks(items, size) {
20754
20920
  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
20755
20921
  return out;
20756
20922
  }
20757
- var JOIN_IN_CHUNK_SIZE = 50;
20758
20923
  var JOIN_IN_MAX_CHUNKS = 6;
20759
- var JOIN_IN_MAX_KEYS = JOIN_IN_CHUNK_SIZE * JOIN_IN_MAX_CHUNKS;
20760
- async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxRecords, parallel, onLimit, warnings, pushDownCond = null, plainGroupByPlan, additionalPushQuery = "") {
20924
+ var JOIN_IN_MAX_KEYS = JOIN_KEY_IN_CHUNK_SIZE * JOIN_IN_MAX_CHUNKS;
20925
+ async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxRecords, parallel, onLimit, warnings, cacheContext, materializedTables, pushDownCond = null, plainGroupByPlan, additionalPushQuery = "") {
20761
20926
  if (join2.type !== "INNER") return null;
20762
20927
  if (!join2.table.alias) return null;
20763
20928
  if (join2.table.subtableCode) return null;
@@ -20780,17 +20945,47 @@ async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxR
20780
20945
  }
20781
20946
  const sourceRows2 = tables.get(sourceAlias);
20782
20947
  if (!sourceRows2) return null;
20783
- const keys = /* @__PURE__ */ new Set();
20948
+ const sourceTable = [stmt.from, ...stmt.joins.map((candidate) => candidate.table)].find((table) => effectiveTableAlias(table) === sourceAlias);
20949
+ const targetInfo = (await getFieldsCached(join2.table.appId, client, cacheContext)).find((info) => info.code === fieldCodeForTypeLookup(join2.table, joinField));
20950
+ const targetMeta = targetInfo ? materializedMetaFromFieldInfo(targetInfo, join2.table.appId) : systemColumnMeta(joinField);
20951
+ let sourceMeta;
20952
+ if (sourceTable?.cteName !== null && sourceTable?.cteName !== void 0) {
20953
+ sourceMeta = materializedTables?.get(sourceTable.cteName)?.columnMeta?.get(sourceField);
20954
+ } else if (sourceTable) {
20955
+ const sourceInfo = (await getFieldsCached(sourceTable.appId, client, cacheContext)).find((info) => info.code === fieldCodeForTypeLookup(sourceTable, sourceField));
20956
+ sourceMeta = sourceInfo ? materializedMetaFromFieldInfo(sourceInfo, sourceTable.appId) : systemColumnMeta(sourceField);
20957
+ }
20958
+ const keys = [];
20959
+ let hasEmptyValue = false;
20784
20960
  for (const row of sourceRows2) {
20785
20961
  const raw = row[sourceField]?.value;
20786
- const txt = toScalarText(raw).trim();
20787
- if (txt.length > 0) keys.add(txt);
20962
+ const txt = toScalarText(raw);
20963
+ if (raw === null || raw === void 0 || txt.length === 0) hasEmptyValue = true;
20964
+ keys.push(txt);
20965
+ }
20966
+ const keyPlan = planJoinKeyPrefilter({
20967
+ fieldType: targetMeta?.fieldType,
20968
+ sourceSemantics: sourceMeta?.semantics,
20969
+ sourceRowCount: sourceRows2.length,
20970
+ values: keys,
20971
+ hasEmptyValue,
20972
+ maxInKeys: JOIN_IN_MAX_KEYS
20973
+ });
20974
+ if (keyPlan.kind === "EMPTY_SOURCE") return [];
20975
+ if (keyPlan.kind === "FALLBACK") {
20976
+ if (keyPlan.reason === "JOIN_KEY_LIMIT_EXCEEDED") {
20977
+ const count = new Set(keys).size;
20978
+ warnings.add(
20979
+ `JOIN\u30AD\u30FC\u304C ${count} \u4EF6\u306E\u305F\u3081 ON \u6700\u9069\u5316\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u3001JOIN\u5148\u3092\u5168\u4EF6\u53D6\u5F97\u3057\u307E\u3059\uFF08\u4E0A\u9650 ${JOIN_IN_MAX_KEYS} \u4EF6\uFF09\u3002`
20980
+ );
20981
+ }
20982
+ return null;
20788
20983
  }
20789
- const values = [...keys];
20790
- if (values.length === 0) return [];
20791
- if (values.length > JOIN_IN_MAX_KEYS) {
20984
+ if (keyPlan.kind === "RANGE_CANDIDATE") return null;
20985
+ const prefilterQueries = buildJoinKeyPrefilterQueries(keyPlan, joinField, sqlQuote);
20986
+ if (prefilterQueries.length === 0) {
20792
20987
  warnings.add(
20793
- `JOIN\u30AD\u30FC\u304C ${values.length} \u4EF6\u306E\u305F\u3081 ON \u6700\u9069\u5316\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u3001JOIN\u5148\u3092\u5168\u4EF6\u53D6\u5F97\u3057\u307E\u3059\uFF08\u4E0A\u9650 ${JOIN_IN_MAX_KEYS} \u4EF6\uFF09\u3002`
20988
+ `JOIN\u30AD\u30FC\u304C 0 \u4EF6\u306E\u305F\u3081 ON \u6700\u9069\u5316\u3092\u30B9\u30AD\u30C3\u30D7\u3057\u3001JOIN\u5148\u3092\u5168\u4EF6\u53D6\u5F97\u3057\u307E\u3059\uFF08\u4E0A\u9650 ${JOIN_IN_MAX_KEYS} \u4EF6\uFF09\u3002`
20794
20989
  );
20795
20990
  return null;
20796
20991
  }
@@ -20799,18 +20994,16 @@ async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxR
20799
20994
  warnings.add(`\u53D6\u5F97\u4E0A\u9650\uFF08${max} \u4EF6\uFF09\u306B\u9054\u3057\u305F\u305F\u3081\u3001${max} \u4EF6\u3067\u6253\u3061\u5207\u3063\u3066\u8868\u793A\u3057\u3066\u3044\u307E\u3059\u3002`);
20800
20995
  markLimitReached(client, join2.table.appId);
20801
20996
  };
20802
- const chunks = splitChunks(values, JOIN_IN_CHUNK_SIZE);
20803
20997
  const merged = [];
20804
20998
  const seen = /* @__PURE__ */ new Set();
20805
- for (const chunk3 of chunks) {
20806
- const inClause = `${joinField} in (${chunk3.map(sqlQuote).join(",")})`;
20999
+ for (const joinKeyQuery of prefilterQueries) {
20807
21000
  const pushQueries = [
20808
21001
  pushDownCond !== null ? whereToKintone(pushDownCond) : "",
20809
21002
  additionalPushQuery
20810
21003
  ].filter((query2) => query2 !== "");
20811
21004
  const query = pushQueries.reduce(
20812
21005
  (combined, pushQuery) => `(${combined}) and (${pushQuery})`,
20813
- inClause
21006
+ joinKeyQuery
20814
21007
  );
20815
21008
  const resolved = await fetchRecordsForSharedPlan(client.getRecords, join2.table.appId, query, fields, {
20816
21009
  parallel,
@@ -24050,6 +24243,7 @@ async function resolveScalarColumns(columns, client, options, cacheContext, cteC
24050
24243
  return cache;
24051
24244
  }
24052
24245
  var explainJoinPushdownPlans = /* @__PURE__ */ new WeakMap();
24246
+ var explainJoinKeyPrefilters = /* @__PURE__ */ new WeakMap();
24053
24247
  var explainChoiceEqualityRewrites = /* @__PURE__ */ new WeakMap();
24054
24248
  var validateExplainInfo = /* @__PURE__ */ new WeakMap();
24055
24249
  var applyParentExplainPlan = /* @__PURE__ */ new WeakMap();
@@ -24081,6 +24275,7 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
24081
24275
  (node) => node.source === source || JSON.stringify(node.source) === JSON.stringify(source)
24082
24276
  );
24083
24277
  const explainRelations = new Map(initialRelations ?? []);
24278
+ const staticExplainRelations = new Set(initialRelations?.keys() ?? []);
24084
24279
  const explainSourceColumns = async (select) => {
24085
24280
  const tables = [select.from, ...select.joins.map((join2) => join2.table)];
24086
24281
  if (tables.length > 1 && select.columns.some(
@@ -24160,6 +24355,17 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
24160
24355
  const withStatement = node;
24161
24356
  for (const cte of withStatement.ctes) {
24162
24357
  await preflightExplainRelations(cte.query);
24358
+ if (cte.query.type === "GENERATE_SERIES") {
24359
+ const generated = executeGenerateSeries(cte.query);
24360
+ explainRelations.set(cte.name, {
24361
+ rows: generated.rows,
24362
+ columns: generated.columns,
24363
+ columnMeta: materializedMetaBySelectResult.get(generated),
24364
+ uniqueGeneratedColumn: cte.query.columnAlias
24365
+ });
24366
+ staticExplainRelations.add(cte.name);
24367
+ continue;
24368
+ }
24163
24369
  const columns = await inferExplainRelationColumns(cte.query);
24164
24370
  explainRelations.set(cte.name, { rows: [], columns });
24165
24371
  }
@@ -24240,6 +24446,71 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
24240
24446
  await loadTypedPushdownMeta(select, tracedClient, cacheContext)
24241
24447
  );
24242
24448
  if (joinPushdownPlan) explainJoinPushdownPlans.set(select, joinPushdownPlan);
24449
+ const joinKeyPlans = /* @__PURE__ */ new Map();
24450
+ for (const join2 of select.joins) {
24451
+ const joinAlias = join2.table.alias;
24452
+ if (join2.type !== "INNER" || !joinAlias || join2.table.subtableCode) continue;
24453
+ const leftAlias = join2.on.left.tableAlias;
24454
+ const rightAlias = join2.on.right.tableAlias;
24455
+ if (!leftAlias || !rightAlias) continue;
24456
+ const sourceAlias = leftAlias === joinAlias && rightAlias !== joinAlias ? rightAlias : rightAlias === joinAlias && leftAlias !== joinAlias ? leftAlias : void 0;
24457
+ const sourceField = leftAlias === joinAlias ? join2.on.right.field : join2.on.left.field;
24458
+ const joinField = leftAlias === joinAlias ? join2.on.left.field : join2.on.right.field;
24459
+ if (!sourceAlias) continue;
24460
+ const sourceTable = [select.from, ...select.joins.map((candidate) => candidate.table)].find((table) => effectiveTableAlias(table) === sourceAlias);
24461
+ const targetInfos = await getFieldsCached(join2.table.appId, tracedClient, cacheContext);
24462
+ const targetInfo = targetInfos.find((info) => info.code === fieldCodeForTypeLookup(join2.table, joinField));
24463
+ const targetMeta = targetInfo ? materializedMetaFromFieldInfo(targetInfo, join2.table.appId) : systemColumnMeta(joinField);
24464
+ let sourceMeta;
24465
+ let values;
24466
+ let sourceRowCount;
24467
+ let hasEmptyValue;
24468
+ if (sourceTable?.cteName !== null && sourceTable?.cteName !== void 0) {
24469
+ const relation = explainRelations.get(sourceTable.cteName);
24470
+ sourceMeta = relation?.columnMeta?.get(sourceField);
24471
+ if (staticExplainRelations.has(sourceTable.cteName) && relation) {
24472
+ sourceRowCount = relation.rows.length;
24473
+ values = relation.rows.map((row) => toScalarText(row[sourceField]));
24474
+ hasEmptyValue = values.some((value) => value.length === 0);
24475
+ }
24476
+ } else if (sourceTable) {
24477
+ const info = (await getFieldsCached(sourceTable.appId, tracedClient, cacheContext)).find((candidate) => candidate.code === fieldCodeForTypeLookup(sourceTable, sourceField));
24478
+ sourceMeta = info ? materializedMetaFromFieldInfo(info, sourceTable.appId) : systemColumnMeta(sourceField);
24479
+ }
24480
+ const plan = planJoinKeyPrefilter({
24481
+ fieldType: targetMeta?.fieldType,
24482
+ sourceSemantics: sourceMeta?.semantics,
24483
+ sourceRowCount,
24484
+ values,
24485
+ hasEmptyValue,
24486
+ maxInKeys: JOIN_IN_MAX_KEYS
24487
+ });
24488
+ if (plan.kind === "FALLBACK" && plan.reason === "JOIN_KEY_VALUES_RUNTIME") {
24489
+ const runtimePlanConsumesJoin = joinPushdownPlan?.joinPlan.items.some(
24490
+ (item) => item.targetAlias === joinAlias
24491
+ ) || joinPushdownPlan?.joinPlan.serverFunctionConsumptions.some(
24492
+ (consumption) => consumption.targetAlias === joinAlias
24493
+ );
24494
+ if (runtimePlanConsumesJoin) continue;
24495
+ }
24496
+ const queries = buildJoinKeyPrefilterQueries(plan, joinField, sqlQuote);
24497
+ const additionalCandidate = select.where ? extractTypedPushdownCandidates(select.where, { tableAlias: joinAlias }) : null;
24498
+ let additionalQuery;
24499
+ let additionalRelation;
24500
+ if (additionalCandidate) {
24501
+ const infoByCode = new Map(targetInfos.map((info) => [info.code, info]));
24502
+ const additionalCapability = classifyWhereCapability(additionalCandidate, (field) => {
24503
+ const info = infoByCode.get(field.field);
24504
+ return info?.semantics ?? (info ? resolveFieldSemantics(info) : systemColumnMeta(field.field)?.semantics);
24505
+ });
24506
+ if (additionalCapability.capability === "EXACT_PUSHDOWN" || additionalCapability.capability === "SUPERSET_PREFILTER") {
24507
+ additionalQuery = whereToKintone(additionalCandidate);
24508
+ additionalRelation = additionalCapability.capability === "EXACT_PUSHDOWN" ? "exact" : "superset";
24509
+ }
24510
+ }
24511
+ joinKeyPlans.set(joinAlias, { plan, queries, additionalQuery, additionalRelation });
24512
+ }
24513
+ if (joinKeyPlans.size > 0) explainJoinKeyPrefilters.set(select, joinKeyPlans);
24243
24514
  if (select.orderBy.length > 0 || select.columns.some((column) => column.type === "WINDOW_COL" && column.orderBy.length > 0)) {
24244
24515
  const meta = await buildOrderByMetaForSelect(select, tracedClient, cacheContext);
24245
24516
  if (select.orderMode !== "KINTONE_NATIVE") {
@@ -25356,9 +25627,13 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
25356
25627
  const joinPushDown = join2.table.alias ? pushdownPlan.joinConditions.get(join2.table.alias) ?? null : null;
25357
25628
  const joinCandidate = join2.table.alias && !join2.table.subtableCode && join2.table.cteName === null && stmt.where ? extractTypedPushdownCandidates(stmt.where, { tableAlias: join2.table.alias }) : null;
25358
25629
  const joinBoundQuery = join2.table.alias ? runtimeJoinPlan?.queriesByAlias.get(join2.table.alias) : void 0;
25359
- const joinQ = joinBoundQuery || (joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)");
25630
+ const joinKey = join2.table.alias ? explainJoinKeyPrefilters.get(stmt)?.get(join2.table.alias) : void 0;
25631
+ const baseJoinQ = joinBoundQuery || (joinPushDown !== null ? whereToKintone(joinPushDown) : joinKey?.additionalQuery ? joinKey.additionalQuery : "(\u5168\u4EF6\u53D6\u5F97)");
25632
+ const runtimeJoinKeyCandidate = joinKey?.plan.kind === "RANGE_CANDIDATE" || joinKey?.plan.kind === "FALLBACK" && joinKey.plan.reason === "JOIN_KEY_VALUES_RUNTIME";
25633
+ const joinQueries = joinKey && joinKey.queries.length > 0 ? joinKey.queries.map((query) => baseJoinQ !== "(\u5168\u4EF6\u53D6\u5F97)" ? `(${query}) and (${baseJoinQ})` : query) : [runtimeJoinKeyCandidate ? "(runtime source keys)" : baseJoinQ];
25634
+ const joinQ = joinQueries.join(" | ");
25360
25635
  lines.push(` ${joinType}: APP${join2.table.appId}${joinAliasStr} (${join2.table.appId})`);
25361
- lines.push(` kintone query: ${joinQ}`);
25636
+ for (const query of joinQueries) lines.push(` kintone query: ${query}`);
25362
25637
  const joinPlanItem = runtimeJoinPlan?.joinPlan.items.find(
25363
25638
  (item) => item.targetAlias === join2.table.alias
25364
25639
  );
@@ -25366,8 +25641,8 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
25366
25641
  (consumption) => consumption.targetAlias === join2.table.alias
25367
25642
  );
25368
25643
  if (emitFetch && join2.table.cteName === null) {
25369
- const joinPending = !runtimeJoinPlan && joinCandidate !== null;
25370
- const joinFetchScope = joinQ === "(\u5168\u4EF6\u53D6\u5F97)" ? "ALL" : joinPending ? "PREFILTERED" : joinPlanItem?.relation === "exact" || joinFunctionConsumption ? "EXACT" : "PREFILTERED";
25644
+ const joinPending = runtimeJoinKeyCandidate || !joinKey && !runtimeJoinPlan && joinCandidate !== null;
25645
+ const joinFetchScope = joinQ === "(\u5168\u4EF6\u53D6\u5F97)" ? "ALL" : joinPending ? "PREFILTERED" : joinKey?.plan.kind === "IN" && joinPlanItem?.relation !== "superset" && joinKey.additionalRelation !== "superset" ? "EXACT" : joinPlanItem?.relation === "exact" || joinFunctionConsumption ? "EXACT" : "PREFILTERED";
25371
25646
  lines.push(renderFetchScope(createExplainFetchSource(
25372
25647
  collector,
25373
25648
  join2.table.appId,
@@ -25378,7 +25653,25 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
25378
25653
  joinPending
25379
25654
  )));
25380
25655
  }
25381
- if (joinPlanItem || joinFunctionConsumption) {
25656
+ if (joinKey) {
25657
+ if (joinKey.plan.kind === "RANGE") {
25658
+ lines.push(" join key prefilter: range");
25659
+ for (const query of joinQueries) lines.push(` pushdown applied: ${query}`);
25660
+ lines.push(" relation: superset");
25661
+ } else if (joinKey.plan.kind === "IN") {
25662
+ const relation = joinPlanItem?.relation === "superset" || joinKey.additionalRelation === "superset" ? "superset" : "exact";
25663
+ lines.push(" join key prefilter: in");
25664
+ for (const query of joinQueries) lines.push(` pushdown applied: ${query}`);
25665
+ lines.push(` relation: ${relation}`);
25666
+ } else if (joinKey.plan.kind === "RANGE_CANDIDATE") {
25667
+ lines.push(" join key prefilter: range candidate");
25668
+ lines.push(" relation: superset");
25669
+ lines.push(` join key prefilter reason: ${joinKey.plan.reason}`);
25670
+ } else if (joinKey.plan.kind === "FALLBACK") {
25671
+ lines.push(joinKey.plan.reason === "JOIN_KEY_VALUES_RUNTIME" ? " join key prefilter: runtime candidate" : " join key prefilter: not applied");
25672
+ lines.push(` join key prefilter reason: ${joinKey.plan.reason}`);
25673
+ }
25674
+ } else if (joinPlanItem || joinFunctionConsumption) {
25382
25675
  lines.push(` pushdown applied: ${joinBoundQuery}`);
25383
25676
  lines.push(` relation: ${joinPlanItem?.relation ?? "exact"}`);
25384
25677
  } else if (!runtimeJoinPlan && joinCandidate !== null) {