@rex0220/kintone-sql-tools 3.60.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
@@ -12042,251 +12042,27 @@ function isOuterJoinNonPreservedTable(statement, table, isMainTable) {
12042
12042
  return false;
12043
12043
  }
12044
12044
 
12045
- // src/core/explainMetadata.ts
12046
- function buildGroupingExplainMetadata(statement, canonicalItemCount) {
12047
- const grouping = normalizeGroupingSpec(statement);
12048
- if (grouping.type !== "GROUPING_SETS") return null;
12049
- return {
12050
- source: grouping.source,
12051
- expandedSetCount: grouping.sets.length,
12052
- groupingItemCount: canonicalItemCount ?? grouping.allItems.length,
12053
- setLimit: B65_MAX_GROUPING_SETS,
12054
- itemLimit: B65_MAX_GROUPING_ITEMS,
12055
- outputRowLimit: B65_MAX_GENERATED_ROWS
12056
- };
12057
- }
12058
- function whereNeedsFieldMetadata(where) {
12059
- if (where === null) return false;
12060
- switch (where.type) {
12061
- case "BINARY":
12062
- return valueNeedsFieldMetadata(where.left);
12063
- case "NULL_CHECK":
12064
- return valueNeedsFieldMetadata(where.field);
12065
- case "LOGICAL":
12066
- return whereNeedsFieldMetadata(where.left) || whereNeedsFieldMetadata(where.right);
12067
- case "NOT":
12068
- case "GROUP":
12069
- return whereNeedsFieldMetadata(where.expr);
12070
- case "EXISTS":
12071
- case "BOOLEAN":
12072
- return false;
12073
- }
12074
- }
12075
- function valueNeedsFieldMetadata(value) {
12076
- if (Array.isArray(value)) return value.some(valueNeedsFieldMetadata);
12077
- if (value === null || typeof value !== "object") return false;
12078
- const item = value;
12079
- if (item["type"] === "FIELD") return item["field"] !== "$id";
12080
- if (item["type"] === "SELECT") return false;
12081
- return Object.values(item).some(valueNeedsFieldMetadata);
12082
- }
12083
- function selectNeedsOwnMetadata(statement) {
12084
- return whereNeedsFieldMetadata(statement.where) || statement.groupBy.length > 0 || normalizeGroupingSpec(statement).type === "GROUPING_SETS" || statement.orderBy.length > 0 || statement.columns.some(
12085
- (column) => column.type === "WINDOW_COL" && column.orderBy.length > 0
12086
- );
12087
- }
12088
- function explainNeedsAppMetadata(statement) {
12089
- const seen = /* @__PURE__ */ new Set();
12090
- const visit = (node) => {
12091
- if (node === null || typeof node !== "object") return false;
12092
- if (seen.has(node)) return false;
12093
- seen.add(node);
12094
- if (Array.isArray(node)) return node.some(visit);
12095
- const item = node;
12096
- if (item["type"] === "VALIDATE") return true;
12097
- if (item["type"] === "SELECT" && selectNeedsOwnMetadata(node)) {
12098
- return true;
12099
- }
12100
- if ((item["type"] === "UPDATE" || item["type"] === "DELETE") && whereNeedsFieldMetadata(node.where)) {
12101
- return true;
12102
- }
12103
- if (item["type"] === "UPDATE" && Array.isArray(item["applyBlocks"]) && item["applyBlocks"].length > 0) return true;
12104
- return Object.values(item).some(visit);
12105
- };
12106
- return visit(statement);
12107
- }
12108
-
12109
- // src/api/fetchAll.ts
12110
- async function fetchAll(fetcher, app, query, fields, options = {}) {
12111
- const pageSize = options.pageSize ?? PAGE_SIZE_DEFAULT;
12112
- const parallel = Math.max(1, options.parallel ?? PARALLEL_DEFAULT);
12113
- const maxRecords = options.maxRecords ?? MAX_RECORDS_DEFAULT;
12114
- const onLimit = options.onLimit ?? "error";
12115
- const stopAfter = options.stopAfter;
12116
- if (stopAfter !== void 0 && (!Number.isSafeInteger(stopAfter) || stopAfter <= 0 || stopAfter > maxRecords)) {
12117
- throw new RangeError("stopAfter must be a positive safe integer <= maxRecords");
12118
- }
12119
- const fetchCap = stopAfter ?? maxRecords;
12120
- const fetchFields = fields.length > 0 && !fields.includes("$id") ? [...fields, "$id"] : fields;
12121
- const allRecords = [];
12122
- let notified = false;
12123
- 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`;
12124
- let cursorId = 0;
12125
- let windowOffset = 0;
12126
- const cursorQuery0 = buildCursorQuery(query, cursorId);
12127
- const first = await fetchPage(fetcher, app, cursorQuery0, fetchFields, pageSize, windowOffset);
12128
- notifySearchAborted(first, options);
12129
- allRecords.push(...first.records);
12130
- if (stopAfter !== void 0 && allRecords.length >= stopAfter) {
12131
- return allRecords.slice(0, stopAfter);
12132
- }
12133
- if (allRecords.length > maxRecords) {
12134
- if (onLimit === "truncate") {
12135
- if (!notified && options.onTruncate) {
12136
- options.onTruncate(maxRecords);
12137
- notified = true;
12138
- }
12139
- return allRecords.slice(0, maxRecords);
12140
- }
12141
- throw new FetchAllLimitError(limitMessage);
12142
- }
12143
- if (first.records.length < pageSize) return allRecords;
12144
- windowOffset += pageSize;
12145
- if (windowOffset >= KINTONE_MAX_OFFSET) {
12146
- cursorId = getLastId(first.records);
12147
- windowOffset = 0;
12148
- }
12149
- while (true) {
12150
- if (stopAfter !== void 0 && allRecords.length >= stopAfter) {
12151
- return allRecords.slice(0, stopAfter);
12152
- }
12153
- if (allRecords.length >= maxRecords) {
12154
- if (onLimit === "truncate") {
12155
- if (!notified && options.onTruncate) {
12156
- options.onTruncate(maxRecords);
12157
- notified = true;
12158
- }
12159
- return allRecords.slice(0, maxRecords);
12160
- }
12161
- throw new FetchAllLimitError(limitMessage);
12162
- }
12163
- const remaining = fetchCap - allRecords.length;
12164
- const maxPagesByLimit = Math.ceil(remaining / pageSize);
12165
- const batchParallel = Math.max(1, Math.min(parallel, maxPagesByLimit));
12166
- const batchOffsets = [];
12167
- for (let i = 0; i < batchParallel; i++) {
12168
- const off = windowOffset + i * pageSize;
12169
- if (off >= KINTONE_MAX_OFFSET) break;
12170
- batchOffsets.push(off);
12171
- }
12172
- const cq = buildCursorQuery(query, cursorId);
12173
- const responses = await Promise.all(
12174
- batchOffsets.map(
12175
- (offset) => fetchPage(fetcher, app, cq, fetchFields, pageSize, offset)
12176
- )
12177
- );
12178
- for (const response of responses) notifySearchAborted(response, options);
12179
- let done = false;
12180
- for (const res of responses) {
12181
- allRecords.push(...res.records);
12182
- if (stopAfter !== void 0 && allRecords.length >= stopAfter) {
12183
- return allRecords.slice(0, stopAfter);
12184
- }
12185
- if (allRecords.length > maxRecords) {
12186
- if (onLimit === "truncate") {
12187
- if (!notified && options.onTruncate) {
12188
- options.onTruncate(maxRecords);
12189
- notified = true;
12190
- }
12191
- return allRecords.slice(0, maxRecords);
12192
- }
12193
- throw new FetchAllLimitError(limitMessage);
12194
- }
12195
- if (res.records.length < pageSize) {
12196
- done = true;
12197
- break;
12198
- }
12199
- }
12200
- if (done) break;
12201
- windowOffset += pageSize * batchOffsets.length;
12202
- if (windowOffset >= KINTONE_MAX_OFFSET) {
12203
- const lastRes = responses[responses.length - 1];
12204
- cursorId = getLastId(lastRes.records);
12205
- windowOffset = 0;
12206
- }
12207
- }
12208
- return allRecords;
12209
- }
12210
- function notifySearchAborted(response, options) {
12211
- if (response.searchAborted) options.onSearchAborted?.();
12212
- }
12213
- function extractIds(records) {
12214
- return records.map((r) => {
12215
- const raw = r["$id"]?.value;
12216
- if (raw === void 0) {
12217
- throw new Error(
12218
- '\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'
12219
- );
12220
- }
12221
- const id = Number(raw);
12222
- if (!Number.isFinite(id)) {
12223
- throw new Error(`$id \u306E\u5024\u304C\u6570\u5024\u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${raw}`);
12224
- }
12225
- return id;
12226
- });
12227
- }
12228
- var PAGE_SIZE_DEFAULT = 500;
12229
- var PARALLEL_DEFAULT = 1;
12230
- var MAX_RECORDS_DEFAULT = 1e4;
12231
- var KINTONE_MAX_OFFSET = 1e4;
12232
- async function fetchPage(fetcher, app, query, fields, pageSize, offset) {
12233
- const pageQuery = buildPageQuery(query, pageSize, offset);
12234
- return fetcher({ app, query: pageQuery, fields });
12235
- }
12236
- function buildCursorQuery(baseQuery, cursorId) {
12237
- const base = baseQuery.trimEnd();
12238
- if (cursorId <= 0) {
12239
- return base ? `${base} order by $id asc` : "order by $id asc";
12240
- }
12241
- const cursor = `$id > ${cursorId} order by $id asc`;
12242
- return base ? `(${base}) and ${cursor}` : cursor;
12243
- }
12244
- function buildPageQuery(query, pageSize, offset) {
12245
- const base = query.trimEnd();
12246
- const suffix = `limit ${pageSize} offset ${offset}`;
12247
- return base ? `${base} ${suffix}` : suffix;
12248
- }
12249
- function getLastId(records) {
12250
- const last = records[records.length - 1];
12251
- const raw = last?.["$id"]?.value;
12252
- if (raw === void 0) return 0;
12253
- const id = Number(raw);
12254
- return Number.isFinite(id) ? id : 0;
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;
12255
12057
  }
12256
- var FetchAllLimitError = class extends Error {
12257
- constructor(message, completeInputWrapped = false) {
12258
- super(message);
12259
- this.completeInputWrapped = completeInputWrapped;
12260
- this.name = "FetchAllLimitError";
12261
- }
12262
- };
12263
-
12264
- // src/core/optimization/sharedPlanner.ts
12265
- async function fetchRecordsForSharedPlan(getRecords, app, query, fields, options) {
12266
- const records = await fetchAll(getRecords, app, query, fields, {
12267
- maxRecords: options.maxRecords,
12268
- parallel: options.parallel,
12269
- onLimit: options.onLimit ?? "error",
12270
- onTruncate: options.onTruncate,
12271
- onSearchAborted: options.onSearchAborted
12272
- });
12273
- return {
12274
- records,
12275
- metrics: { fetchedRows: records.length }
12276
- };
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;
12277
12061
  }
12278
- async function resolveDmlTargetIds(getRecords, app, query, options) {
12279
- const { records, metrics } = await fetchRecordsForSharedPlan(
12280
- getRecords,
12281
- app,
12282
- query,
12283
- ["$id"],
12284
- options
12285
- );
12286
- return {
12287
- ids: extractIds(records),
12288
- metrics
12289
- };
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;
12290
12066
  }
12291
12067
 
12292
12068
  // src/core/optimization/whereCapability.ts
@@ -12701,117 +12477,432 @@ function combineLogical(op, left, right) {
12701
12477
  if (left.capability === "UNSUPPORTED" || right.capability === "UNSUPPORTED") {
12702
12478
  return requireExactFunctionPushdown({ capability: "UNSUPPORTED", reasons });
12703
12479
  }
12704
- if (left.capability === "EXACT_PUSHDOWN" && right.capability === "EXACT_PUSHDOWN") {
12705
- return { capability: "EXACT_PUSHDOWN", reasons };
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" };
12627
+ }
12628
+ if (!operators.has(">=") || !operators.has("<=")) {
12629
+ return { kind: "FALLBACK", reason: "JOIN_KEY_OPERATOR_UNAVAILABLE" };
12630
+ }
12631
+ if (input.values === void 0) {
12632
+ return { kind: "RANGE_CANDIDATE", relation: "superset", reason: "JOIN_KEY_VALUES_RUNTIME" };
12633
+ }
12634
+ if (!semanticsMatch(input.fieldType, input.sourceSemantics)) {
12635
+ return { kind: "FALLBACK", reason: "JOIN_KEY_SEMANTICS_UNRESOLVED" };
12636
+ }
12637
+ const sourceSemantics = input.sourceSemantics;
12638
+ if (input.hasEmptyValue) {
12639
+ return { kind: "FALLBACK", reason: "JOIN_KEY_EMPTY_VALUE" };
12640
+ }
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" };
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;
12662
+ return {
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
12669
+ };
12670
+ }
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":
12685
+ return false;
12686
+ }
12687
+ }
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");
12733
+ }
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;
12706
12763
  }
12707
- if (op === "AND" && (left.capability === "EXACT_PUSHDOWN" || right.capability === "EXACT_PUSHDOWN" || left.capability === "SUPERSET_PREFILTER" || right.capability === "SUPERSET_PREFILTER")) {
12708
- return requireExactFunctionPushdown({
12709
- capability: "SUPERSET_PREFILTER",
12710
- reasons: [{ code: "WHERE_SUPERSET_PREFILTER" }, ...reasons]
12711
- });
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
+ }
12712
12822
  }
12713
- return requireExactFunctionPushdown({ capability: "LOCAL_ONLY", reasons });
12714
- }
12715
- function localExpression() {
12716
- return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
12823
+ return allRecords;
12717
12824
  }
12718
- function unsupported2(code, field, fieldType, operator) {
12719
- return { capability: "UNSUPPORTED", reasons: [{ code, field, fieldType, operator }] };
12825
+ function notifySearchAborted(response, options) {
12826
+ if (response.searchAborted) options.onSearchAborted?.();
12720
12827
  }
12721
- function legacyKintoneFunctionUnsupported(code, functionName, field, fieldType, operator) {
12722
- return requireExactFunctionPushdown({
12723
- capability: "UNSUPPORTED",
12724
- reasons: [{ code, functionName, field, fieldType, operator }]
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;
12725
12841
  });
12726
12842
  }
12727
- function relativeDateUnsupported(code, functionName, field, fieldType, operator) {
12728
- return requireExactRelativeDatePushdown({
12729
- capability: "UNSUPPORTED",
12730
- reasons: [{ code, functionName, field, fieldType, operator }]
12731
- });
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 });
12732
12850
  }
12733
- function hasRelativeDateReason(reasons) {
12734
- return reasons.some(
12735
- (reason) => reason.code.startsWith("WHERE_RELATIVE_DATE_") || reason.functionName !== void 0 && isRelativeDateFunctionName(reason.functionName)
12736
- );
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";
12855
+ }
12856
+ const cursor = `$id > ${cursorId} order by $id asc`;
12857
+ return base ? `(${base}) and ${cursor}` : cursor;
12737
12858
  }
12738
- function hasLegacyKintoneFunctionReason(reasons) {
12739
- return reasons.some(
12740
- (reason) => reason.code.startsWith("WHERE_KINTONE_FUNCTION_") || reason.functionName !== void 0 && LEGACY_KINTONE_FUNCTION_NAMES.has(reason.functionName)
12741
- );
12859
+ function buildPageQuery(query, pageSize, offset) {
12860
+ const base = query.trimEnd();
12861
+ const suffix = `limit ${pageSize} offset ${offset}`;
12862
+ return base ? `${base} ${suffix}` : suffix;
12742
12863
  }
12743
- function requireExactRelativeDatePushdown(result) {
12744
- if (result.capability === "EXACT_PUSHDOWN" || !hasRelativeDateReason(result.reasons) || result.reasons.some((reason) => reason.code === "WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN")) {
12745
- return result;
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;
12870
+ }
12871
+ var FetchAllLimitError = class extends Error {
12872
+ constructor(message, completeInputWrapped = false) {
12873
+ super(message);
12874
+ this.completeInputWrapped = completeInputWrapped;
12875
+ this.name = "FetchAllLimitError";
12746
12876
  }
12747
- const relative = result.reasons.find(
12748
- (reason) => reason.code.startsWith("WHERE_RELATIVE_DATE_") || reason.functionName !== void 0 && isRelativeDateFunctionName(reason.functionName)
12749
- );
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
+ });
12750
12888
  return {
12751
- capability: result.capability,
12752
- reasons: [
12753
- ...result.reasons,
12754
- {
12755
- code: "WHERE_RELATIVE_DATE_REQUIRES_EXACT_PUSHDOWN",
12756
- functionName: relative.functionName,
12757
- field: relative.field,
12758
- fieldType: relative.fieldType,
12759
- operator: relative.operator
12760
- }
12761
- ]
12889
+ records,
12890
+ metrics: { fetchedRows: records.length }
12762
12891
  };
12763
12892
  }
12764
- function requireExactLegacyKintoneFunctionPushdown(result) {
12765
- if (result.capability === "EXACT_PUSHDOWN" || !hasLegacyKintoneFunctionReason(result.reasons) || result.reasons.some(
12766
- (reason) => reason.code === "WHERE_KINTONE_FUNCTION_REQUIRES_EXACT_PUSHDOWN"
12767
- )) {
12768
- return result;
12769
- }
12770
- const legacy = result.reasons.find(
12771
- (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
12772
12900
  );
12773
12901
  return {
12774
- capability: result.capability,
12775
- reasons: [
12776
- ...result.reasons,
12777
- {
12778
- code: "WHERE_KINTONE_FUNCTION_REQUIRES_EXACT_PUSHDOWN",
12779
- functionName: legacy.functionName,
12780
- field: legacy.field,
12781
- fieldType: legacy.fieldType,
12782
- operator: legacy.operator
12783
- }
12784
- ]
12902
+ ids: extractIds(records),
12903
+ metrics
12785
12904
  };
12786
12905
  }
12787
- function requireExactFunctionPushdown(result) {
12788
- return requireExactLegacyKintoneFunctionPushdown(
12789
- requireExactRelativeDatePushdown(result)
12790
- );
12791
- }
12792
-
12793
- // src/core/optimization/joinDateTimeLiteralPolicy.ts
12794
- function isCanonicalJoinDate(value) {
12795
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
12796
- if (!match) return false;
12797
- const year = Number(match[1]);
12798
- const month = Number(match[2]);
12799
- const day = Number(match[3]);
12800
- if (year < 1 || year > 9999) return false;
12801
- const date = /* @__PURE__ */ new Date(0);
12802
- date.setUTCFullYear(year, month - 1, day);
12803
- date.setUTCHours(0, 0, 0, 0);
12804
- return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
12805
- }
12806
- function isCanonicalJoinTime(value) {
12807
- const match = /^(\d{2}):(\d{2})$/.exec(value);
12808
- return match !== null && Number(match[1]) <= 23 && Number(match[2]) <= 59;
12809
- }
12810
- function isCanonicalJoinDateTime(value) {
12811
- const match = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/.exec(value);
12812
- if (!match || !isCanonicalJoinDate(match[1])) return false;
12813
- return Number(match[2]) <= 23 && Number(match[3]) <= 59 && Number(match[4]) <= 59;
12814
- }
12815
12906
 
12816
12907
  // src/core/optimization/joinNumberLiteralPolicy.ts
12817
12908
  function isJoinNumberLiteralSupported(literal) {
@@ -12851,7 +12942,7 @@ var KLIKE_TYPES = /* @__PURE__ */ new Set([
12851
12942
  "RICH_TEXT",
12852
12943
  "FILE"
12853
12944
  ]);
12854
- var DATETIME_TYPES = /* @__PURE__ */ new Set([
12945
+ var DATETIME_TYPES2 = /* @__PURE__ */ new Set([
12855
12946
  "DATETIME",
12856
12947
  "CREATED_TIME",
12857
12948
  "UPDATED_TIME"
@@ -13477,7 +13568,7 @@ function classifySupportedLeaf(predicate, owner, fieldType) {
13477
13568
  }
13478
13569
  return (predicate.op === "=" || predicate.op === "!=" || predicate.op === "<>") && predicate.right.type === "STRING" && predicate.right.value !== "" ? "exact" : "unsafe";
13479
13570
  }
13480
- if (fieldType === "DATE" || fieldType === "TIME" || DATETIME_TYPES.has(fieldType)) {
13571
+ if (fieldType === "DATE" || fieldType === "TIME" || DATETIME_TYPES2.has(fieldType)) {
13481
13572
  if (predicate.op !== "=" && predicate.op !== "!=" && predicate.op !== "<>" && predicate.op !== "<" && predicate.op !== ">" && predicate.op !== "<=" && predicate.op !== ">=" || predicate.right.type !== "STRING") return "unsafe";
13482
13573
  if (fieldType === "DATE") {
13483
13574
  return isCanonicalJoinDate(predicate.right.value) ? "exact" : "unsafe";
@@ -20245,6 +20336,8 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
20245
20336
  parallel,
20246
20337
  options.onLimitReached ?? "error",
20247
20338
  warnings,
20339
+ cacheContext,
20340
+ cteCache,
20248
20341
  null,
20249
20342
  plainGroupByPlan,
20250
20343
  ""
@@ -20633,6 +20726,8 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
20633
20726
  parallel,
20634
20727
  effectiveOptions.onLimitReached ?? "error",
20635
20728
  warnings,
20729
+ cacheContext,
20730
+ cteCache,
20636
20731
  pushDownCond,
20637
20732
  plainGroupByPlan
20638
20733
  );
@@ -20825,10 +20920,9 @@ function splitChunks(items, size) {
20825
20920
  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
20826
20921
  return out;
20827
20922
  }
20828
- var JOIN_IN_CHUNK_SIZE = 50;
20829
20923
  var JOIN_IN_MAX_CHUNKS = 6;
20830
- var JOIN_IN_MAX_KEYS = JOIN_IN_CHUNK_SIZE * JOIN_IN_MAX_CHUNKS;
20831
- 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 = "") {
20832
20926
  if (join2.type !== "INNER") return null;
20833
20927
  if (!join2.table.alias) return null;
20834
20928
  if (join2.table.subtableCode) return null;
@@ -20851,17 +20945,47 @@ async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxR
20851
20945
  }
20852
20946
  const sourceRows2 = tables.get(sourceAlias);
20853
20947
  if (!sourceRows2) return null;
20854
- 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;
20855
20960
  for (const row of sourceRows2) {
20856
20961
  const raw = row[sourceField]?.value;
20857
- const txt = toScalarText(raw).trim();
20858
- 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;
20859
20983
  }
20860
- const values = [...keys];
20861
- if (values.length === 0) return [];
20862
- 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) {
20863
20987
  warnings.add(
20864
- `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`
20865
20989
  );
20866
20990
  return null;
20867
20991
  }
@@ -20870,18 +20994,16 @@ async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxR
20870
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`);
20871
20995
  markLimitReached(client, join2.table.appId);
20872
20996
  };
20873
- const chunks = splitChunks(values, JOIN_IN_CHUNK_SIZE);
20874
20997
  const merged = [];
20875
20998
  const seen = /* @__PURE__ */ new Set();
20876
- for (const chunk3 of chunks) {
20877
- const inClause = `${joinField} in (${chunk3.map(sqlQuote).join(",")})`;
20999
+ for (const joinKeyQuery of prefilterQueries) {
20878
21000
  const pushQueries = [
20879
21001
  pushDownCond !== null ? whereToKintone(pushDownCond) : "",
20880
21002
  additionalPushQuery
20881
21003
  ].filter((query2) => query2 !== "");
20882
21004
  const query = pushQueries.reduce(
20883
21005
  (combined, pushQuery) => `(${combined}) and (${pushQuery})`,
20884
- inClause
21006
+ joinKeyQuery
20885
21007
  );
20886
21008
  const resolved = await fetchRecordsForSharedPlan(client.getRecords, join2.table.appId, query, fields, {
20887
21009
  parallel,
@@ -24121,6 +24243,7 @@ async function resolveScalarColumns(columns, client, options, cacheContext, cteC
24121
24243
  return cache;
24122
24244
  }
24123
24245
  var explainJoinPushdownPlans = /* @__PURE__ */ new WeakMap();
24246
+ var explainJoinKeyPrefilters = /* @__PURE__ */ new WeakMap();
24124
24247
  var explainChoiceEqualityRewrites = /* @__PURE__ */ new WeakMap();
24125
24248
  var validateExplainInfo = /* @__PURE__ */ new WeakMap();
24126
24249
  var applyParentExplainPlan = /* @__PURE__ */ new WeakMap();
@@ -24152,6 +24275,7 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
24152
24275
  (node) => node.source === source || JSON.stringify(node.source) === JSON.stringify(source)
24153
24276
  );
24154
24277
  const explainRelations = new Map(initialRelations ?? []);
24278
+ const staticExplainRelations = new Set(initialRelations?.keys() ?? []);
24155
24279
  const explainSourceColumns = async (select) => {
24156
24280
  const tables = [select.from, ...select.joins.map((join2) => join2.table)];
24157
24281
  if (tables.length > 1 && select.columns.some(
@@ -24231,6 +24355,17 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
24231
24355
  const withStatement = node;
24232
24356
  for (const cte of withStatement.ctes) {
24233
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
+ }
24234
24369
  const columns = await inferExplainRelationColumns(cte.query);
24235
24370
  explainRelations.set(cte.name, { rows: [], columns });
24236
24371
  }
@@ -24311,6 +24446,71 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
24311
24446
  await loadTypedPushdownMeta(select, tracedClient, cacheContext)
24312
24447
  );
24313
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);
24314
24514
  if (select.orderBy.length > 0 || select.columns.some((column) => column.type === "WINDOW_COL" && column.orderBy.length > 0)) {
24315
24515
  const meta = await buildOrderByMetaForSelect(select, tracedClient, cacheContext);
24316
24516
  if (select.orderMode !== "KINTONE_NATIVE") {
@@ -25427,9 +25627,13 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
25427
25627
  const joinPushDown = join2.table.alias ? pushdownPlan.joinConditions.get(join2.table.alias) ?? null : null;
25428
25628
  const joinCandidate = join2.table.alias && !join2.table.subtableCode && join2.table.cteName === null && stmt.where ? extractTypedPushdownCandidates(stmt.where, { tableAlias: join2.table.alias }) : null;
25429
25629
  const joinBoundQuery = join2.table.alias ? runtimeJoinPlan?.queriesByAlias.get(join2.table.alias) : void 0;
25430
- 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(" | ");
25431
25635
  lines.push(` ${joinType}: APP${join2.table.appId}${joinAliasStr} (${join2.table.appId})`);
25432
- lines.push(` kintone query: ${joinQ}`);
25636
+ for (const query of joinQueries) lines.push(` kintone query: ${query}`);
25433
25637
  const joinPlanItem = runtimeJoinPlan?.joinPlan.items.find(
25434
25638
  (item) => item.targetAlias === join2.table.alias
25435
25639
  );
@@ -25437,8 +25641,8 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
25437
25641
  (consumption) => consumption.targetAlias === join2.table.alias
25438
25642
  );
25439
25643
  if (emitFetch && join2.table.cteName === null) {
25440
- const joinPending = !runtimeJoinPlan && joinCandidate !== null;
25441
- 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";
25442
25646
  lines.push(renderFetchScope(createExplainFetchSource(
25443
25647
  collector,
25444
25648
  join2.table.appId,
@@ -25449,7 +25653,25 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
25449
25653
  joinPending
25450
25654
  )));
25451
25655
  }
25452
- 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) {
25453
25675
  lines.push(` pushdown applied: ${joinBoundQuery}`);
25454
25676
  lines.push(` relation: ${joinPlanItem?.relation ?? "exact"}`);
25455
25677
  } else if (!runtimeJoinPlan && joinCandidate !== null) {