@rex0220/kintone-sql-tools 3.26.0 → 3.28.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
@@ -5957,6 +5957,133 @@ function collectKlikes(where, out) {
5957
5957
  }
5958
5958
  }
5959
5959
 
5960
+ // src/core/optimization/relativeDateFullScanExactPlan.ts
5961
+ function buildRelativeDateFullScanExactPlan(input) {
5962
+ const {
5963
+ select,
5964
+ selectMode,
5965
+ capability,
5966
+ context,
5967
+ serializedWholeWhere,
5968
+ relativeFunctionNames
5969
+ } = input;
5970
+ if (select.where === null) return null;
5971
+ if (select.from.appId <= 0 || select.from.cteName !== null) return null;
5972
+ if (select.from.subtableCode) return null;
5973
+ if (select.joins.length > 0) return null;
5974
+ if (!context.allowFullScanExact) return null;
5975
+ if (select.orderMode === "KINTONE_NATIVE") return null;
5976
+ const hasCanonicalOrder2 = select.orderMode === "CANONICAL" && select.orderBy.length > 0;
5977
+ if (selectMode !== "FULL_SCAN" && !hasCanonicalOrder2) return null;
5978
+ if (capability.capability !== "EXACT_PUSHDOWN") return null;
5979
+ const occurrences = serverOnlyFunctionOccurrencesInWhere(select.where);
5980
+ if (occurrences.length === 0) return null;
5981
+ if (!sameOccurrenceList(occurrences, relativeFunctionNames)) return null;
5982
+ if (serializedWholeWhere === null || !serializedMultisetContains(serializedWholeWhere, occurrences)) {
5983
+ return null;
5984
+ }
5985
+ const prefilterPlan = {
5986
+ prefilterWhere: select.where,
5987
+ residualWhere: null,
5988
+ exactRelativeLeaves: collectExactServerFunctionLeaves(select.where),
5989
+ relativeFunctionNames: new Set(occurrences),
5990
+ appliedKlikes: /* @__PURE__ */ new Set(),
5991
+ capability: capability.capability,
5992
+ reasons: capability.reasons
5993
+ };
5994
+ const plan = {
5995
+ allowForm: "FULL_SCAN_EXACT",
5996
+ clientWhereEvaluation: false,
5997
+ serializedWholeWhere,
5998
+ prefilterPlan
5999
+ };
6000
+ assertRelativeDateFullScanExactPlan(plan, input, occurrences);
6001
+ return plan;
6002
+ }
6003
+ function assertRelativeDateFullScanExactPlan(plan, input, occurrences) {
6004
+ if (plan.allowForm !== "FULL_SCAN_EXACT") {
6005
+ throw new Error("FULL_SCAN_EXACT invariant: allowForm");
6006
+ }
6007
+ if (plan.clientWhereEvaluation !== false) {
6008
+ throw new Error("FULL_SCAN_EXACT invariant: clientWhereEvaluation");
6009
+ }
6010
+ if (input.capability.capability !== "EXACT_PUSHDOWN") {
6011
+ throw new Error("FULL_SCAN_EXACT invariant: capability");
6012
+ }
6013
+ if (input.select.where === null || plan.prefilterPlan.prefilterWhere !== input.select.where) {
6014
+ throw new Error("FULL_SCAN_EXACT invariant: whole WHERE identity");
6015
+ }
6016
+ if (plan.prefilterPlan.residualWhere !== null) {
6017
+ throw new Error("FULL_SCAN_EXACT invariant: residualWhere");
6018
+ }
6019
+ if (plan.prefilterPlan.capability !== "EXACT_PUSHDOWN") {
6020
+ throw new Error("FULL_SCAN_EXACT invariant: transport capability");
6021
+ }
6022
+ if (!serializedMultisetContains(plan.serializedWholeWhere, occurrences)) {
6023
+ throw new Error("FULL_SCAN_EXACT invariant: relative occurrence serialization");
6024
+ }
6025
+ }
6026
+ function serverOnlyFunctionOccurrencesInWhere(where) {
6027
+ const names = [];
6028
+ const visit = (node) => {
6029
+ if (Array.isArray(node)) {
6030
+ node.forEach(visit);
6031
+ return;
6032
+ }
6033
+ if (node === null || typeof node !== "object") return;
6034
+ const value = node;
6035
+ if (value["type"] === "SELECT") return;
6036
+ if (value["type"] === "KINTONE_FUNC" && typeof value["name"] === "string" && isServerOnlyWhereFunctionName(value["name"])) {
6037
+ names.push(value["name"]);
6038
+ return;
6039
+ }
6040
+ Object.values(value).forEach(visit);
6041
+ };
6042
+ visit(where);
6043
+ return names;
6044
+ }
6045
+ function collectExactServerFunctionLeaves(where) {
6046
+ const leaves = [];
6047
+ const visit = (node) => {
6048
+ switch (node.type) {
6049
+ case "BINARY":
6050
+ if (node.right.type === "KINTONE_FUNC" && isServerOnlyWhereFunctionName(node.right.name) || node.right.type === "IN_LIST" && node.right.values.length === 1 && node.right.values[0].type === "KINTONE_FUNC" && isServerOnlyWhereFunctionName(node.right.values[0].name)) {
6051
+ leaves.push(node);
6052
+ }
6053
+ return;
6054
+ case "LOGICAL":
6055
+ visit(node.left);
6056
+ visit(node.right);
6057
+ return;
6058
+ case "NOT":
6059
+ case "GROUP":
6060
+ visit(node.expr);
6061
+ return;
6062
+ case "EXISTS":
6063
+ case "NULL_CHECK":
6064
+ case "BOOLEAN":
6065
+ return;
6066
+ }
6067
+ };
6068
+ visit(where);
6069
+ return leaves;
6070
+ }
6071
+ function sameOccurrenceList(actual, expected) {
6072
+ return actual.length === expected.length && actual.every((name, index) => name === expected[index]);
6073
+ }
6074
+ function serializedMultisetContains(query, expectedNames) {
6075
+ const expected = /* @__PURE__ */ new Map();
6076
+ for (const name of expectedNames) {
6077
+ if (!isServerOnlyWhereFunctionName(name)) return false;
6078
+ expected.set(name, (expected.get(name) ?? 0) + 1);
6079
+ }
6080
+ for (const [name, count] of expected) {
6081
+ const matches = query.match(new RegExp(`\\b${name}\\s*\\(`, "g"));
6082
+ if ((matches?.length ?? 0) < count) return false;
6083
+ }
6084
+ return true;
6085
+ }
6086
+
5960
6087
  // src/core/klikeValidation.ts
5961
6088
  var KlikeValidationError = class extends Error {
5962
6089
  constructor(message) {
@@ -6115,6 +6242,10 @@ function validateSelect(stmt) {
6115
6242
  validateNestedSelects(stmt);
6116
6243
  return;
6117
6244
  }
6245
+ if (canDeferJoinWholeWhereKlikeValidation(stmt)) {
6246
+ validateNestedSelects(stmt);
6247
+ return;
6248
+ }
6118
6249
  throw new KlikeValidationError(
6119
6250
  "FULL_SCAN \u306E KLIKE / NOT KLIKE \u306F\u3001\u7269\u7406\u30C6\u30FC\u30D6\u30EB\u306B\u5BFE\u3059\u308B AND \u30EA\u30FC\u30D5\u3068\u3057\u3066\u5FC5\u305A\u62BC\u3057\u4E0B\u3052\u3089\u308C\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002OR / NOT \u914D\u4E0B\u3001CTE\u30FB\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u3001LEFT / RIGHT JOIN \u3067\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093"
6120
6251
  );
@@ -6122,6 +6253,11 @@ function validateSelect(stmt) {
6122
6253
  }
6123
6254
  validateNestedSelects(stmt);
6124
6255
  }
6256
+ function canDeferJoinWholeWhereKlikeValidation(stmt) {
6257
+ return stmt.where !== null && serverOnlyFunctionOccurrencesInWhere(stmt.where).length > 0 && stmt.joins.length > 0 && stmt.joins.every((join2) => join2.type === "INNER") && [stmt.from, ...stmt.joins.map((join2) => join2.table)].every(
6258
+ (table) => table.alias !== null && table.cteName === null && !table.subtableCode
6259
+ );
6260
+ }
6125
6261
  function validateOwnKlikeExpressions(stmt) {
6126
6262
  walkWithoutNestedSelects(stmt, (where) => {
6127
6263
  if (!isKlike(where)) return;
@@ -10453,6 +10589,28 @@ function validateDeclaredBatchVariables(statements, input) {
10453
10589
  return normalized;
10454
10590
  }
10455
10591
 
10592
+ // src/core/outerJoinSearchAbortGuard.ts
10593
+ function isOuterJoinSelect(value) {
10594
+ if (value["type"] !== "SELECT" || !Array.isArray(value["joins"])) return false;
10595
+ return value["joins"].some((join2) => {
10596
+ if (join2 === null || typeof join2 !== "object") return false;
10597
+ const type = join2["type"];
10598
+ return type === "LEFT" || type === "RIGHT";
10599
+ });
10600
+ }
10601
+ function statementContainsOuterJoin(statement) {
10602
+ const seen = /* @__PURE__ */ new Set();
10603
+ const visit = (value) => {
10604
+ if (Array.isArray(value)) return value.some(visit);
10605
+ if (value === null || typeof value !== "object" || seen.has(value)) return false;
10606
+ seen.add(value);
10607
+ const object = value;
10608
+ if (isOuterJoinSelect(object)) return true;
10609
+ return Object.values(object).some(visit);
10610
+ };
10611
+ return visit(statement);
10612
+ }
10613
+
10456
10614
  // src/core/explainMetadata.ts
10457
10615
  function buildGroupingExplainMetadata(statement, canonicalItemCount) {
10458
10616
  const grouping = normalizeGroupingSpec(statement);
@@ -11211,6 +11369,30 @@ function classifyJoinPushdownLeaf(predicate, sources) {
11211
11369
  const relation = classifySupportedLeaf(predicate, owner, fieldType);
11212
11370
  return relation === "unsafe" ? unsafe() : Object.freeze({ relation, owner });
11213
11371
  }
11372
+ function classifyJoinServerFunctionLeaf(predicate, sources) {
11373
+ const functionOccurrences = serverOnlyFunctionOccurrencesInWhere(predicate);
11374
+ if (functionOccurrences.length === 0 || predicate.left.type !== "FIELD") {
11375
+ return Object.freeze({ relation: "unsafe" });
11376
+ }
11377
+ const owner = resolveJoinFieldOwner(predicate.left, sources);
11378
+ if (owner.status !== "OWNED" || owner.source.sourceKind !== "APP") {
11379
+ return Object.freeze({ relation: "unsafe" });
11380
+ }
11381
+ const capability = classifyWhereCapability(predicate, (field) => {
11382
+ const resolved = resolveJoinFieldOwner(field, sources);
11383
+ if (resolved.status !== "OWNED" || resolved.source !== owner.source) return void 0;
11384
+ const type = resolved.source.fieldTypes.get(resolved.fieldCode);
11385
+ return type === void 0 ? void 0 : resolveFieldSemantics({ fieldType: type });
11386
+ });
11387
+ if (capability.capability !== "EXACT_PUSHDOWN") {
11388
+ return Object.freeze({ relation: "unsafe" });
11389
+ }
11390
+ return Object.freeze({
11391
+ relation: "function-leaf-exact",
11392
+ owner,
11393
+ functionOccurrences: Object.freeze([...functionOccurrences])
11394
+ });
11395
+ }
11214
11396
  function buildJoinPushdownPlan(where, sources) {
11215
11397
  const allKlikes = /* @__PURE__ */ new Set();
11216
11398
  collectKlikes2(where, allKlikes);
@@ -11221,15 +11403,370 @@ function buildJoinPushdownPlan(where, sources) {
11221
11403
  predicate: fragment.predicate,
11222
11404
  relation: fragment.relation
11223
11405
  }));
11406
+ const serverFunctionFoundation = buildServerFunctionFoundation(where, sources);
11224
11407
  const appliedKlikes = /* @__PURE__ */ new Set();
11225
- for (const item of items) collectKlikes2(item.predicate, appliedKlikes);
11408
+ if (serverFunctionFoundation.serverFunctionCandidate?.variant === "WHOLE_WHERE_EXACT") {
11409
+ for (const klike of allKlikes) appliedKlikes.add(klike);
11410
+ } else {
11411
+ for (const item of items) collectKlikes2(item.predicate, appliedKlikes);
11412
+ }
11226
11413
  return Object.freeze({
11227
11414
  items: Object.freeze(items),
11228
11415
  appliedKlikes,
11229
11416
  allKlikes: Object.freeze([...allKlikes]),
11230
- rejections: Object.freeze(collectRejections(where, sources))
11417
+ rejections: Object.freeze(collectRejections(where, sources)),
11418
+ fetchQueriesByAlias: /* @__PURE__ */ new Map(),
11419
+ ...serverFunctionFoundation
11420
+ });
11421
+ }
11422
+ function bindJoinServerFunctionFetches(plan, sources) {
11423
+ if (plan.serverFunctionCandidate?.staticContract !== "CONFIRMED" || plan.serverFunctionConsumptions.length === 0 || plan.residualServerFunctionOccurrences.length !== 0 || !sameStringMultiset(
11424
+ plan.allServerFunctionOccurrences,
11425
+ plan.adoptedServerFunctionOccurrences
11426
+ ) || plan.allKlikes.some((klike) => !plan.appliedKlikes.has(klike))) {
11427
+ return plan;
11428
+ }
11429
+ const queryPartsByAlias = /* @__PURE__ */ new Map();
11430
+ const append = (alias, query) => {
11431
+ const parts = queryPartsByAlias.get(alias);
11432
+ if (parts) parts.push(query);
11433
+ else queryPartsByAlias.set(alias, [query]);
11434
+ };
11435
+ try {
11436
+ if (plan.serverFunctionCandidate.variant === "EXACT_LEAF") {
11437
+ for (const item of plan.items) {
11438
+ append(item.targetAlias, serializeJoinPushdownItem(item, sources));
11439
+ }
11440
+ }
11441
+ for (const consumption of plan.serverFunctionConsumptions) {
11442
+ const source = sources.find(
11443
+ (candidate) => candidate.alias === consumption.targetAlias && candidate.appId === consumption.appId && candidate.sourceKind === "APP"
11444
+ );
11445
+ if (!source) return plan;
11446
+ const serialized = serializeJoinPushdownItem({
11447
+ targetAlias: consumption.targetAlias,
11448
+ appId: consumption.appId,
11449
+ predicate: consumption.predicate,
11450
+ relation: "exact"
11451
+ }, sources);
11452
+ if (serialized !== consumption.serializedPredicate || !sameStringMultiset(
11453
+ serverFunctionOccurrencesInSerializedQuery(serialized),
11454
+ consumption.functionOccurrences
11455
+ )) {
11456
+ return plan;
11457
+ }
11458
+ append(consumption.targetAlias, serialized);
11459
+ }
11460
+ } catch {
11461
+ return plan;
11462
+ }
11463
+ const fetchQueriesByAlias = /* @__PURE__ */ new Map();
11464
+ for (const [alias, parts] of queryPartsByAlias) {
11465
+ fetchQueriesByAlias.set(
11466
+ alias,
11467
+ parts.length === 1 ? parts[0] : parts.map((part) => `(${part})`).join(" and ")
11468
+ );
11469
+ }
11470
+ const boundConsumptions = plan.serverFunctionConsumptions.map((consumption) => {
11471
+ const query = fetchQueriesByAlias.get(consumption.targetAlias);
11472
+ if (!query) return null;
11473
+ return Object.freeze({
11474
+ ...consumption,
11475
+ fetchBinding: Object.freeze({
11476
+ status: "BOUND_TO_TARGET_FETCH",
11477
+ targetAlias: consumption.targetAlias,
11478
+ appId: consumption.appId,
11479
+ query
11480
+ })
11481
+ });
11482
+ });
11483
+ if (boundConsumptions.some((consumption) => consumption === null)) return plan;
11484
+ return Object.freeze({
11485
+ ...plan,
11486
+ serverFunctionCandidate: Object.freeze({
11487
+ ...plan.serverFunctionCandidate,
11488
+ fetchContract: "CONFIRMED"
11489
+ }),
11490
+ serverFunctionConsumptions: Object.freeze(
11491
+ boundConsumptions
11492
+ ),
11493
+ fetchQueriesByAlias
11231
11494
  });
11232
11495
  }
11496
+ function isJoinServerFunctionFetchPlan(plan) {
11497
+ if (plan.serverFunctionCandidate === null || plan.serverFunctionCandidate.staticContract !== "CONFIRMED" || plan.serverFunctionCandidate.fetchContract !== "CONFIRMED" || plan.serverFunctionConsumptions.length === 0 || plan.residualServerFunctionOccurrences.length !== 0 || !sameStringMultiset(
11498
+ plan.allServerFunctionOccurrences,
11499
+ plan.adoptedServerFunctionOccurrences
11500
+ )) {
11501
+ return false;
11502
+ }
11503
+ if (!plan.serverFunctionConsumptions.every((consumption) => {
11504
+ const binding = consumption.fetchBinding;
11505
+ return binding !== "PENDING_STEP_2" && binding.status === "BOUND_TO_TARGET_FETCH" && binding.targetAlias === consumption.targetAlias && binding.appId === consumption.appId && binding.query === plan.fetchQueriesByAlias.get(consumption.targetAlias) && binding.query.includes(consumption.serializedPredicate);
11506
+ })) {
11507
+ return false;
11508
+ }
11509
+ for (const [alias, query] of plan.fetchQueriesByAlias) {
11510
+ const expected = plan.serverFunctionConsumptions.filter((consumption) => consumption.targetAlias === alias).flatMap((consumption) => [...consumption.functionOccurrences]);
11511
+ if (expected.length > 0 && !sameStringMultiset(
11512
+ serverFunctionOccurrencesInSerializedQuery(query),
11513
+ expected
11514
+ )) {
11515
+ return false;
11516
+ }
11517
+ }
11518
+ if (plan.serverFunctionCandidate.variant === "WHOLE_WHERE_EXACT" && (plan.serverFunctionConsumptions.length !== 1 || plan.serverFunctionConsumptions[0].consumption !== "whole-where" || plan.residualWhere !== null || plan.allKlikes.some((klike) => !plan.appliedKlikes.has(klike)) || countSerializedKlikes(queryForWholeWhere(plan)) !== plan.allKlikes.length)) {
11519
+ return false;
11520
+ }
11521
+ return true;
11522
+ }
11523
+ function buildServerFunctionFoundation(where, sources) {
11524
+ if (where === null) return emptyServerFunctionFoundation(null);
11525
+ const allOccurrences = Object.freeze([
11526
+ ...serverOnlyFunctionOccurrencesInWhere(where)
11527
+ ]);
11528
+ if (allOccurrences.length === 0) return emptyServerFunctionFoundation(where);
11529
+ const wholeWhere = buildWholeWhereServerFunctionFoundation(
11530
+ where,
11531
+ sources,
11532
+ allOccurrences
11533
+ );
11534
+ if (wholeWhere !== null) return wholeWhere;
11535
+ const candidates = collectServerFunctionLeavesOnAndSpine(where);
11536
+ const consumptions = [];
11537
+ const adoptedLeaves = /* @__PURE__ */ new Set();
11538
+ for (const leaf of candidates) {
11539
+ const classification = classifyJoinServerFunctionLeaf(leaf, sources);
11540
+ if (classification.relation !== "function-leaf-exact") continue;
11541
+ const guardItem = {
11542
+ targetAlias: classification.owner.alias,
11543
+ appId: classification.owner.appId,
11544
+ predicate: leaf,
11545
+ relation: "exact"
11546
+ };
11547
+ let serializedPredicate;
11548
+ try {
11549
+ serializedPredicate = serializeJoinPushdownItem(guardItem, sources);
11550
+ } catch {
11551
+ continue;
11552
+ }
11553
+ const serializedOccurrences = serverFunctionOccurrencesInSerializedQuery(serializedPredicate);
11554
+ if (!sameStringMultiset(serializedOccurrences, classification.functionOccurrences)) {
11555
+ continue;
11556
+ }
11557
+ adoptedLeaves.add(leaf);
11558
+ consumptions.push(Object.freeze({
11559
+ targetAlias: classification.owner.alias,
11560
+ appId: classification.owner.appId,
11561
+ predicate: leaf,
11562
+ functionLeaves: Object.freeze([leaf]),
11563
+ functionOccurrences: classification.functionOccurrences,
11564
+ relation: classification.relation,
11565
+ consumption: "leaf",
11566
+ serializedPredicate,
11567
+ staticProof: Object.freeze({
11568
+ classifier: "EXACT_PUSHDOWN",
11569
+ ownership: "OWNED",
11570
+ serialization: "OCCURRENCE_MULTISET_EXACT",
11571
+ residualIdentityConsumption: "CONFIRMED"
11572
+ }),
11573
+ fetchBinding: "PENDING_STEP_2"
11574
+ }));
11575
+ }
11576
+ const residualWhere = removeAdoptedLeavesFromAndSpine(where, adoptedLeaves);
11577
+ const residualOccurrences = Object.freeze(residualWhere === null ? [] : [...serverOnlyFunctionOccurrencesInWhere(residualWhere)]);
11578
+ const adoptedOccurrences = Object.freeze(consumptions.flatMap(
11579
+ (consumption) => [...consumption.functionOccurrences]
11580
+ ));
11581
+ const staticContract = sameStringMultiset(allOccurrences, adoptedOccurrences) && residualOccurrences.length === 0 ? "CONFIRMED" : "INCOMPLETE";
11582
+ return {
11583
+ serverFunctionCandidate: Object.freeze({
11584
+ variant: "EXACT_LEAF",
11585
+ staticContract,
11586
+ fetchContract: "PENDING_STEP_2"
11587
+ }),
11588
+ serverFunctionConsumptions: Object.freeze(consumptions),
11589
+ allServerFunctionOccurrences: allOccurrences,
11590
+ adoptedServerFunctionOccurrences: adoptedOccurrences,
11591
+ residualWhere,
11592
+ residualServerFunctionOccurrences: residualOccurrences
11593
+ };
11594
+ }
11595
+ function buildWholeWhereServerFunctionFoundation(where, sources, allOccurrences) {
11596
+ const owner = classifyWholeWhereExactOwner(where, sources);
11597
+ if (owner === null) return null;
11598
+ const item = {
11599
+ targetAlias: owner.alias,
11600
+ appId: owner.appId,
11601
+ predicate: where,
11602
+ relation: "exact"
11603
+ };
11604
+ let serializedPredicate;
11605
+ try {
11606
+ serializedPredicate = serializeJoinPushdownItem(item, sources);
11607
+ } catch {
11608
+ return null;
11609
+ }
11610
+ if (!sameStringMultiset(
11611
+ serverFunctionOccurrencesInSerializedQuery(serializedPredicate),
11612
+ allOccurrences
11613
+ )) {
11614
+ return null;
11615
+ }
11616
+ const allKlikes = /* @__PURE__ */ new Set();
11617
+ collectKlikes2(where, allKlikes);
11618
+ if (countSerializedKlikes(serializedPredicate) !== allKlikes.size) return null;
11619
+ const functionLeaves = Object.freeze(collectServerFunctionLeaves(where));
11620
+ const consumption = Object.freeze({
11621
+ targetAlias: owner.alias,
11622
+ appId: owner.appId,
11623
+ predicate: where,
11624
+ functionLeaves,
11625
+ functionOccurrences: Object.freeze([...allOccurrences]),
11626
+ relation: "function-leaf-exact",
11627
+ consumption: "whole-where",
11628
+ serializedPredicate,
11629
+ staticProof: Object.freeze({
11630
+ classifier: "EXACT_PUSHDOWN",
11631
+ ownership: "OWNED",
11632
+ serialization: "OCCURRENCE_MULTISET_EXACT",
11633
+ residualIdentityConsumption: "CONFIRMED"
11634
+ }),
11635
+ fetchBinding: "PENDING_STEP_2"
11636
+ });
11637
+ return {
11638
+ serverFunctionCandidate: Object.freeze({
11639
+ variant: "WHOLE_WHERE_EXACT",
11640
+ staticContract: "CONFIRMED",
11641
+ fetchContract: "PENDING_STEP_2"
11642
+ }),
11643
+ serverFunctionConsumptions: Object.freeze([consumption]),
11644
+ allServerFunctionOccurrences: Object.freeze([...allOccurrences]),
11645
+ adoptedServerFunctionOccurrences: Object.freeze([...allOccurrences]),
11646
+ residualWhere: null,
11647
+ residualServerFunctionOccurrences: Object.freeze([])
11648
+ };
11649
+ }
11650
+ function classifyWholeWhereExactOwner(where, sources) {
11651
+ switch (where.type) {
11652
+ case "BINARY": {
11653
+ const functionOccurrences = serverOnlyFunctionOccurrencesInWhere(where);
11654
+ if (functionOccurrences.length > 0) {
11655
+ const classification2 = classifyJoinServerFunctionLeaf(where, sources);
11656
+ return classification2.relation === "function-leaf-exact" ? classification2.owner : null;
11657
+ }
11658
+ const classification = classifyJoinPushdownLeaf(where, sources);
11659
+ return classification.relation === "exact" && classification.owner !== void 0 ? classification.owner : null;
11660
+ }
11661
+ case "LOGICAL": {
11662
+ const left = classifyWholeWhereExactOwner(where.left, sources);
11663
+ const right = classifyWholeWhereExactOwner(where.right, sources);
11664
+ return left !== null && right !== null && sameOwner(left, right) ? left : null;
11665
+ }
11666
+ case "GROUP":
11667
+ case "NOT":
11668
+ return classifyWholeWhereExactOwner(where.expr, sources);
11669
+ case "NULL_CHECK":
11670
+ case "EXISTS":
11671
+ case "BOOLEAN":
11672
+ return null;
11673
+ }
11674
+ }
11675
+ function collectServerFunctionLeaves(where) {
11676
+ switch (where.type) {
11677
+ case "BINARY":
11678
+ return serverOnlyFunctionOccurrencesInWhere(where).length > 0 ? [where] : [];
11679
+ case "LOGICAL":
11680
+ return [
11681
+ ...collectServerFunctionLeaves(where.left),
11682
+ ...collectServerFunctionLeaves(where.right)
11683
+ ];
11684
+ case "GROUP":
11685
+ case "NOT":
11686
+ return collectServerFunctionLeaves(where.expr);
11687
+ case "NULL_CHECK":
11688
+ case "EXISTS":
11689
+ case "BOOLEAN":
11690
+ return [];
11691
+ }
11692
+ }
11693
+ function emptyServerFunctionFoundation(where) {
11694
+ const occurrences = where === null ? [] : serverOnlyFunctionOccurrencesInWhere(where);
11695
+ return {
11696
+ serverFunctionCandidate: null,
11697
+ serverFunctionConsumptions: Object.freeze([]),
11698
+ allServerFunctionOccurrences: Object.freeze([...occurrences]),
11699
+ adoptedServerFunctionOccurrences: Object.freeze([]),
11700
+ residualWhere: where,
11701
+ residualServerFunctionOccurrences: Object.freeze([...occurrences])
11702
+ };
11703
+ }
11704
+ function collectServerFunctionLeavesOnAndSpine(where) {
11705
+ switch (where.type) {
11706
+ case "BINARY":
11707
+ return serverOnlyFunctionOccurrencesInWhere(where).length > 0 ? [where] : [];
11708
+ case "LOGICAL":
11709
+ return where.op === "AND" ? [
11710
+ ...collectServerFunctionLeavesOnAndSpine(where.left),
11711
+ ...collectServerFunctionLeavesOnAndSpine(where.right)
11712
+ ] : [];
11713
+ case "GROUP":
11714
+ return collectServerFunctionLeavesOnAndSpine(where.expr);
11715
+ case "NOT":
11716
+ case "NULL_CHECK":
11717
+ case "EXISTS":
11718
+ case "BOOLEAN":
11719
+ return [];
11720
+ }
11721
+ }
11722
+ function removeAdoptedLeavesFromAndSpine(where, adoptedLeaves) {
11723
+ if (where.type === "BINARY") return adoptedLeaves.has(where) ? null : where;
11724
+ if (where.type === "LOGICAL") {
11725
+ if (where.op !== "AND") return where;
11726
+ const left = removeAdoptedLeavesFromAndSpine(where.left, adoptedLeaves);
11727
+ const right = removeAdoptedLeavesFromAndSpine(where.right, adoptedLeaves);
11728
+ if (left === null) return right;
11729
+ if (right === null) return left;
11730
+ if (left === where.left && right === where.right) return where;
11731
+ return { ...where, left, right };
11732
+ }
11733
+ if (where.type === "GROUP") {
11734
+ const expr = removeAdoptedLeavesFromAndSpine(where.expr, adoptedLeaves);
11735
+ if (expr === null) return null;
11736
+ return expr === where.expr ? where : { ...where, expr };
11737
+ }
11738
+ return where;
11739
+ }
11740
+ function serverFunctionOccurrencesInSerializedQuery(query) {
11741
+ const matches = [];
11742
+ for (const name of SERVER_ONLY_WHERE_FUNCTION_NAMES) {
11743
+ const pattern = new RegExp(`\\b${name}\\s*\\(`, "g");
11744
+ let match;
11745
+ while ((match = pattern.exec(query)) !== null) {
11746
+ matches.push({ name, index: match.index });
11747
+ }
11748
+ }
11749
+ return matches.sort((left, right) => left.index - right.index).map((match) => match.name);
11750
+ }
11751
+ function sameStringMultiset(left, right) {
11752
+ if (left.length !== right.length) return false;
11753
+ const counts = /* @__PURE__ */ new Map();
11754
+ for (const value of left) counts.set(value, (counts.get(value) ?? 0) + 1);
11755
+ for (const value of right) {
11756
+ const count = counts.get(value);
11757
+ if (count === void 0) return false;
11758
+ if (count === 1) counts.delete(value);
11759
+ else counts.set(value, count - 1);
11760
+ }
11761
+ return counts.size === 0;
11762
+ }
11763
+ function countSerializedKlikes(query) {
11764
+ return [...query.matchAll(/\s(?:not\s+)?like\s/g)].length;
11765
+ }
11766
+ function queryForWholeWhere(plan) {
11767
+ const consumption = plan.serverFunctionConsumptions[0];
11768
+ return consumption === void 0 ? "" : plan.fetchQueriesByAlias.get(consumption.targetAlias) ?? "";
11769
+ }
11233
11770
  function collectRejections(where, sources) {
11234
11771
  if (where === null) return [];
11235
11772
  const reasons = /* @__PURE__ */ new Set();
@@ -11300,6 +11837,9 @@ function assertPredicateOwnership(predicate, item, sources) {
11300
11837
  case "GROUP":
11301
11838
  assertPredicateOwnership(predicate.expr, item, sources);
11302
11839
  return;
11840
+ case "NOT":
11841
+ assertPredicateOwnership(predicate.expr, item, sources);
11842
+ return;
11303
11843
  case "BINARY": {
11304
11844
  if (predicate.left.type !== "FIELD" || containsFieldReference(predicate.right)) {
11305
11845
  throw joinOwnershipError(item, "predicate contains a non-target or RHS field reference");
@@ -11310,7 +11850,6 @@ function assertPredicateOwnership(predicate, item, sources) {
11310
11850
  }
11311
11851
  return;
11312
11852
  }
11313
- case "NOT":
11314
11853
  case "NULL_CHECK":
11315
11854
  case "EXISTS":
11316
11855
  case "BOOLEAN":
@@ -13116,133 +13655,6 @@ function deepClone(value, seen = /* @__PURE__ */ new Map()) {
13116
13655
  return clone;
13117
13656
  }
13118
13657
 
13119
- // src/core/optimization/relativeDateFullScanExactPlan.ts
13120
- function buildRelativeDateFullScanExactPlan(input) {
13121
- const {
13122
- select,
13123
- selectMode,
13124
- capability,
13125
- context,
13126
- serializedWholeWhere,
13127
- relativeFunctionNames
13128
- } = input;
13129
- if (select.where === null) return null;
13130
- if (select.from.appId <= 0 || select.from.cteName !== null) return null;
13131
- if (select.from.subtableCode) return null;
13132
- if (select.joins.length > 0) return null;
13133
- if (!context.allowFullScanExact) return null;
13134
- if (select.orderMode === "KINTONE_NATIVE") return null;
13135
- const hasCanonicalOrder2 = select.orderMode === "CANONICAL" && select.orderBy.length > 0;
13136
- if (selectMode !== "FULL_SCAN" && !hasCanonicalOrder2) return null;
13137
- if (capability.capability !== "EXACT_PUSHDOWN") return null;
13138
- const occurrences = serverOnlyFunctionOccurrencesInWhere(select.where);
13139
- if (occurrences.length === 0) return null;
13140
- if (!sameOccurrenceList(occurrences, relativeFunctionNames)) return null;
13141
- if (serializedWholeWhere === null || !serializedMultisetContains(serializedWholeWhere, occurrences)) {
13142
- return null;
13143
- }
13144
- const prefilterPlan = {
13145
- prefilterWhere: select.where,
13146
- residualWhere: null,
13147
- exactRelativeLeaves: collectExactServerFunctionLeaves(select.where),
13148
- relativeFunctionNames: new Set(occurrences),
13149
- appliedKlikes: /* @__PURE__ */ new Set(),
13150
- capability: capability.capability,
13151
- reasons: capability.reasons
13152
- };
13153
- const plan = {
13154
- allowForm: "FULL_SCAN_EXACT",
13155
- clientWhereEvaluation: false,
13156
- serializedWholeWhere,
13157
- prefilterPlan
13158
- };
13159
- assertRelativeDateFullScanExactPlan(plan, input, occurrences);
13160
- return plan;
13161
- }
13162
- function assertRelativeDateFullScanExactPlan(plan, input, occurrences) {
13163
- if (plan.allowForm !== "FULL_SCAN_EXACT") {
13164
- throw new Error("FULL_SCAN_EXACT invariant: allowForm");
13165
- }
13166
- if (plan.clientWhereEvaluation !== false) {
13167
- throw new Error("FULL_SCAN_EXACT invariant: clientWhereEvaluation");
13168
- }
13169
- if (input.capability.capability !== "EXACT_PUSHDOWN") {
13170
- throw new Error("FULL_SCAN_EXACT invariant: capability");
13171
- }
13172
- if (input.select.where === null || plan.prefilterPlan.prefilterWhere !== input.select.where) {
13173
- throw new Error("FULL_SCAN_EXACT invariant: whole WHERE identity");
13174
- }
13175
- if (plan.prefilterPlan.residualWhere !== null) {
13176
- throw new Error("FULL_SCAN_EXACT invariant: residualWhere");
13177
- }
13178
- if (plan.prefilterPlan.capability !== "EXACT_PUSHDOWN") {
13179
- throw new Error("FULL_SCAN_EXACT invariant: transport capability");
13180
- }
13181
- if (!serializedMultisetContains(plan.serializedWholeWhere, occurrences)) {
13182
- throw new Error("FULL_SCAN_EXACT invariant: relative occurrence serialization");
13183
- }
13184
- }
13185
- function serverOnlyFunctionOccurrencesInWhere(where) {
13186
- const names = [];
13187
- const visit = (node) => {
13188
- if (Array.isArray(node)) {
13189
- node.forEach(visit);
13190
- return;
13191
- }
13192
- if (node === null || typeof node !== "object") return;
13193
- const value = node;
13194
- if (value["type"] === "SELECT") return;
13195
- if (value["type"] === "KINTONE_FUNC" && typeof value["name"] === "string" && isServerOnlyWhereFunctionName(value["name"])) {
13196
- names.push(value["name"]);
13197
- return;
13198
- }
13199
- Object.values(value).forEach(visit);
13200
- };
13201
- visit(where);
13202
- return names;
13203
- }
13204
- function collectExactServerFunctionLeaves(where) {
13205
- const leaves = [];
13206
- const visit = (node) => {
13207
- switch (node.type) {
13208
- case "BINARY":
13209
- if (node.right.type === "KINTONE_FUNC" && isServerOnlyWhereFunctionName(node.right.name) || node.right.type === "IN_LIST" && node.right.values.length === 1 && node.right.values[0].type === "KINTONE_FUNC" && isServerOnlyWhereFunctionName(node.right.values[0].name)) {
13210
- leaves.push(node);
13211
- }
13212
- return;
13213
- case "LOGICAL":
13214
- visit(node.left);
13215
- visit(node.right);
13216
- return;
13217
- case "NOT":
13218
- case "GROUP":
13219
- visit(node.expr);
13220
- return;
13221
- case "EXISTS":
13222
- case "NULL_CHECK":
13223
- case "BOOLEAN":
13224
- return;
13225
- }
13226
- };
13227
- visit(where);
13228
- return leaves;
13229
- }
13230
- function sameOccurrenceList(actual, expected) {
13231
- return actual.length === expected.length && actual.every((name, index) => name === expected[index]);
13232
- }
13233
- function serializedMultisetContains(query, expectedNames) {
13234
- const expected = /* @__PURE__ */ new Map();
13235
- for (const name of expectedNames) {
13236
- if (!isServerOnlyWhereFunctionName(name)) return false;
13237
- expected.set(name, (expected.get(name) ?? 0) + 1);
13238
- }
13239
- for (const [name, count] of expected) {
13240
- const matches = query.match(new RegExp(`\\b${name}\\s*\\(`, "g"));
13241
- if ((matches?.length ?? 0) < count) return false;
13242
- }
13243
- return true;
13244
- }
13245
-
13246
13658
  // src/core/optimization/relativeDatePushdownGuard.ts
13247
13659
  function relativeDateFunctionNamesInNode(node, stopAtNestedSelect) {
13248
13660
  const names = [];
@@ -13463,6 +13875,11 @@ function serializationContainsFunctions(query, names) {
13463
13875
  function allowRelativeDatePrefilterPlan(select, decomposition) {
13464
13876
  return decomposition.eligible === true && resolveSelectMode(select) === "FULL_SCAN" && select.orderMode !== "KINTONE_NATIVE" && select.from.cteName === null && !select.from.subtableCode && select.joins.length === 0;
13465
13877
  }
13878
+ function allowJoinServerFunctionPlan(select, plan) {
13879
+ return select.where !== null && select.joins.length > 0 && select.joins.every((join2) => join2.type === "INNER") && [select.from, ...select.joins.map((join2) => join2.table)].every(
13880
+ (table) => table.alias !== null && table.cteName === null && !table.subtableCode
13881
+ ) && isJoinServerFunctionFetchPlan(plan);
13882
+ }
13466
13883
  function rejectedNode(candidate) {
13467
13884
  return {
13468
13885
  kind: candidate.kind,
@@ -13562,6 +13979,14 @@ async function buildRelativeDatePushdownPlan(statement, resolver) {
13562
13979
  allowed2 = true;
13563
13980
  }
13564
13981
  }
13982
+ let joinServerFunctionPlan;
13983
+ if (!allowed2 && resolver.joinServerFunctionPlan) {
13984
+ const candidatePlan = await resolver.joinServerFunctionPlan(select);
13985
+ if (candidatePlan !== null && allowJoinServerFunctionPlan(select, candidatePlan)) {
13986
+ joinServerFunctionPlan = candidatePlan;
13987
+ allowed2 = true;
13988
+ }
13989
+ }
13565
13990
  const node2 = {
13566
13991
  kind: candidate.kind,
13567
13992
  source: candidate.source,
@@ -13572,6 +13997,11 @@ async function buildRelativeDatePushdownPlan(statement, resolver) {
13572
13997
  restQuery: restQuery2,
13573
13998
  ...prefilterPlan ? { prefilterPlan, phase2PrefilterEligible } : {},
13574
13999
  ...fullScanExactPlan ? { fullScanExactPlan, allowForm: fullScanExactPlan.allowForm } : {},
14000
+ ...joinServerFunctionPlan ? {
14001
+ joinServerFunctionPlan,
14002
+ allowForm: "JOIN_SERVER_FUNCTION_EXACT",
14003
+ joinServerFunctionVariant: joinServerFunctionPlan.serverFunctionCandidate.variant
14004
+ } : {},
13575
14005
  clientWhereEvaluation: !allowed2,
13576
14006
  allowed: allowed2
13577
14007
  };
@@ -13663,7 +14093,7 @@ function decomposeRelativeDatePrefilter(stmt, resolveField2, testSeam = {}) {
13663
14093
  }
13664
14094
  const occurrences = collectServerFunctionOccurrences(stmt.where);
13665
14095
  if (occurrences.length === 0) return reject(["NO_RELATIVE_DATE"]);
13666
- const spine = collectServerFunctionLeavesOnAndSpine(stmt.where, resolveField2);
14096
+ const spine = collectServerFunctionLeavesOnAndSpine2(stmt.where, resolveField2);
13667
14097
  if (!spine.ok) return reject(spine.reasonCodes);
13668
14098
  if (!sameRelativeMultiset(occurrences, spine.leaves)) {
13669
14099
  return reject(["RELATIVE_DATE_LEAF_COUNT_MISMATCH"]);
@@ -13741,7 +14171,7 @@ function decomposeRelativeDatePrefilter(stmt, resolveField2, testSeam = {}) {
13741
14171
  }
13742
14172
  };
13743
14173
  }
13744
- function collectServerFunctionLeavesOnAndSpine(where, resolveField2) {
14174
+ function collectServerFunctionLeavesOnAndSpine2(where, resolveField2) {
13745
14175
  const leaves = [];
13746
14176
  let failure = null;
13747
14177
  const visit = (node) => {
@@ -14840,7 +15270,7 @@ async function execute(sql, client, options = {}) {
14840
15270
  const guardedClient = wrapClientWithSearchAbort(
14841
15271
  countedClient,
14842
15272
  collector,
14843
- !isSelectLikeStatement(stmt)
15273
+ !isSelectLikeStatement(stmt) || statementContainsOuterJoin(stmt)
14844
15274
  );
14845
15275
  const result = await executeParsedStatement(
14846
15276
  stmt,
@@ -14959,9 +15389,14 @@ function wrapClientWithMetrics(client, metrics) {
14959
15389
  }
14960
15390
  };
14961
15391
  }
15392
+ var SEARCH_ABORT_FAIL_CLOSED = /* @__PURE__ */ Symbol("searchAbortFailClosed");
14962
15393
  function wrapClientWithSearchAbort(client, collector, failClosed) {
15394
+ if (failClosed && client[SEARCH_ABORT_FAIL_CLOSED]) {
15395
+ return client;
15396
+ }
14963
15397
  return {
14964
15398
  ...client,
15399
+ ...failClosed ? { [SEARCH_ABORT_FAIL_CLOSED]: true } : {},
14965
15400
  getRecords: async (params) => {
14966
15401
  const response = await client.getRecords(params);
14967
15402
  if (response.searchAborted) {
@@ -15029,6 +15464,15 @@ async function resolveRelativeDateExecutionPlan(stmt, client, cacheContext) {
15029
15464
  cacheContext
15030
15465
  );
15031
15466
  return decomposeRelativeDatePrefilter(select, resolver);
15467
+ },
15468
+ joinServerFunctionPlan: async (select) => {
15469
+ const metadata = await loadTypedPushdownMeta(select, client, cacheContext);
15470
+ const runtimePlan = buildRuntimeJoinPushdownPlan(select, metadata);
15471
+ if (runtimePlan === null) return null;
15472
+ if (isJoinServerFunctionFetchPlan(runtimePlan.joinPlan)) {
15473
+ boundJoinRuntimePlans.set(select, runtimePlan);
15474
+ }
15475
+ return runtimePlan.joinPlan;
15032
15476
  }
15033
15477
  });
15034
15478
  }
@@ -15446,7 +15890,7 @@ async function executeBatch(sql, client, options = {}) {
15446
15890
  const statementClient = wrapClientWithSearchAbort(
15447
15891
  countedClient,
15448
15892
  searchAbortCollector,
15449
- info.statementType !== "SELECT" && info.statementType !== "UNION" && info.statementType !== "WITH"
15893
+ info.statementType !== "SELECT" && info.statementType !== "UNION" && info.statementType !== "WITH" || statementContainsOuterJoin(statements[i])
15450
15894
  );
15451
15895
  const cursorScope = wrapClientWithCursorScope(statementClient);
15452
15896
  const outcome = await runWithDeadline(
@@ -16653,6 +17097,7 @@ function extractMainTypedPushdownCandidate(stmt) {
16653
17097
  if (!stmt.from.alias) return null;
16654
17098
  return extractTypedPushdownCandidates(stmt.where, { tableAlias: stmt.from.alias });
16655
17099
  }
17100
+ var boundJoinRuntimePlans = /* @__PURE__ */ new WeakMap();
16656
17101
  function buildRuntimeJoinPushdownPlan(stmt, metadata) {
16657
17102
  if (stmt.joins.length === 0 || stmt.where === null || stmt.joins.some((join2) => join2.type !== "INNER")) {
16658
17103
  return null;
@@ -16673,11 +17118,17 @@ function buildRuntimeJoinPushdownPlan(stmt, metadata) {
16673
17118
  ]),
16674
17119
  fieldOptions: metadata.fieldOptionsByApp.get(table.appId)
16675
17120
  }));
16676
- const plan = buildJoinPushdownPlan(stmt.where, sources);
17121
+ const staticPlan = buildJoinPushdownPlan(stmt.where, sources);
17122
+ const plan = bindJoinServerFunctionFetches(staticPlan, sources);
16677
17123
  const conditionsByAlias = /* @__PURE__ */ new Map();
16678
17124
  const queriesByAlias = /* @__PURE__ */ new Map();
17125
+ for (const [alias, query] of plan.fetchQueriesByAlias) {
17126
+ queriesByAlias.set(alias, query);
17127
+ }
16679
17128
  for (const item of plan.items) {
16680
- queriesByAlias.set(item.targetAlias, serializeJoinPushdownItem(item, sources));
17129
+ if (!queriesByAlias.has(item.targetAlias)) {
17130
+ queriesByAlias.set(item.targetAlias, serializeJoinPushdownItem(item, sources));
17131
+ }
16681
17132
  conditionsByAlias.set(item.targetAlias, item.predicate);
16682
17133
  }
16683
17134
  const mainAlias = stmt.from.alias;
@@ -17319,15 +17770,24 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
17319
17770
  whereNeedsFieldMetadata(stmt.having) || selectCaseConditionsNeedFieldMetadata(stmt)
17320
17771
  );
17321
17772
  const havingFieldSemanticsResolver = buildHavingFieldSemanticsResolver(stmt, fieldSemanticsResolver);
17322
- const pushdownPlan = buildRuntimeJoinPushdownPlan(stmt, pushdownMeta) ?? buildKlikePushdownPlan(stmt, pushdownMeta);
17773
+ const preboundJoinPlan = boundJoinRuntimePlans.get(stmt);
17774
+ if (preboundJoinPlan && !isJoinServerFunctionFetchPlan(preboundJoinPlan.joinPlan)) {
17775
+ throw new Error("InternalError: JOIN server-function fetch binding changed before records API.");
17776
+ }
17777
+ const pushdownPlan = preboundJoinPlan ?? buildRuntimeJoinPushdownPlan(stmt, pushdownMeta) ?? buildKlikePushdownPlan(stmt, pushdownMeta);
17323
17778
  validateKlikePushdownPlan(pushdownPlan);
17779
+ const runtimeJoinPlan = "joinPlan" in pushdownPlan ? pushdownPlan : null;
17780
+ const boundServerFunctionPlan = runtimeJoinPlan && isJoinServerFunctionFetchPlan(runtimeJoinPlan.joinPlan) ? runtimeJoinPlan : null;
17324
17781
  const fetchClient = client;
17325
17782
  const mainPushDown = pushdownPlan.mainCondition;
17326
17783
  const tableConditions = pushdownPlan.joinConditions;
17327
17784
  if (prefilterPlan && allowOriginalWherePushdown) {
17328
17785
  throw new Error("internal error: relative-date prefilter must disable original WHERE pushdown.");
17329
17786
  }
17330
- const mainFetchCondition = prefilterPlan ? prefilterPlan.prefilterWhere : mainPushDown;
17787
+ const mainFetchCondition = prefilterPlan ? prefilterPlan.prefilterWhere : boundServerFunctionPlan ? null : mainPushDown;
17788
+ const mainBoundQuery = boundServerFunctionPlan?.queriesByAlias.get(
17789
+ stmt.from.alias
17790
+ ) ?? "";
17331
17791
  const scalarCache = await resolveScalarColumns(stmt.columns, client, options, cacheContext, cteCache);
17332
17792
  const constantFalse = isConstantFalseWhere(stmt.where);
17333
17793
  const mainFetch = constantFalse ? Promise.resolve([]) : fetchTableRecordsForFullScan(
@@ -17340,8 +17800,9 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
17340
17800
  options.onLimitReached ?? "error",
17341
17801
  warnings,
17342
17802
  mainFetchCondition,
17343
- allowOriginalWherePushdown,
17344
- plainGroupByPlan
17803
+ boundServerFunctionPlan ? false : allowOriginalWherePushdown,
17804
+ plainGroupByPlan,
17805
+ mainBoundQuery
17345
17806
  );
17346
17807
  const parallelJoins = [];
17347
17808
  const onOptJoins = [];
@@ -17350,8 +17811,9 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
17350
17811
  parallelJoins.push({ join: join2, promise: Promise.resolve([]) });
17351
17812
  continue;
17352
17813
  }
17353
- const jCond = join2.table.alias ? tableConditions.get(join2.table.alias) ?? null : null;
17354
- if (jCond !== null) {
17814
+ const boundQuery = join2.table.alias ? boundServerFunctionPlan?.queriesByAlias.get(join2.table.alias) ?? "" : "";
17815
+ const jCond = boundServerFunctionPlan ? null : join2.table.alias ? tableConditions.get(join2.table.alias) ?? null : null;
17816
+ if (jCond !== null || boundQuery !== "") {
17355
17817
  parallelJoins.push({
17356
17818
  join: join2,
17357
17819
  promise: fetchTableRecordsForFullScan(
@@ -17365,7 +17827,8 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
17365
17827
  warnings,
17366
17828
  jCond,
17367
17829
  true,
17368
- plainGroupByPlan
17830
+ plainGroupByPlan,
17831
+ boundQuery
17369
17832
  )
17370
17833
  });
17371
17834
  } else {
@@ -17425,7 +17888,7 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
17425
17888
  havingFieldSemanticsResolver,
17426
17889
  aggregateSortKindResolver,
17427
17890
  appliedKlikes: prefilterPlan?.appliedKlikes ?? pushdownPlan.appliedKlikes,
17428
- ...prefilterPlan ? { residualWhere: prefilterPlan.residualWhere } : {},
17891
+ ...prefilterPlan ? { residualWhere: prefilterPlan.residualWhere } : boundServerFunctionPlan ? { residualWhere: boundServerFunctionPlan.joinPlan.residualWhere } : {},
17429
17892
  resolvedGroupingSpec: resolvedGroupingSpecs.get(stmt),
17430
17893
  plainGroupByPlan
17431
17894
  });
@@ -21133,8 +21596,11 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
21133
21596
  throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
21134
21597
  }
21135
21598
  capabilities.set(select, capability);
21136
- const joinPushdownMeta = await loadTypedPushdownMeta(select, tracedClient, cacheContext);
21137
- const joinPushdownPlan = buildRuntimeJoinPushdownPlan(select, joinPushdownMeta);
21599
+ const preboundJoinPushdownPlan = boundJoinRuntimePlans.get(select);
21600
+ const joinPushdownPlan = preboundJoinPushdownPlan ?? buildRuntimeJoinPushdownPlan(
21601
+ select,
21602
+ await loadTypedPushdownMeta(select, tracedClient, cacheContext)
21603
+ );
21138
21604
  if (joinPushdownPlan) explainJoinPushdownPlans.set(select, joinPushdownPlan);
21139
21605
  if (select.orderBy.length > 0 || select.columns.some((column) => column.type === "WINDOW_COL" && column.orderBy.length > 0)) {
21140
21606
  const meta = await buildOrderByMetaForSelect(select, tracedClient, cacheContext);
@@ -21372,9 +21838,15 @@ function relativeDateExplainLines(plan) {
21372
21838
  if (!plan.hasServerOnlyWhereFunction) return [];
21373
21839
  if (!plan.allowed && plan.rejection) {
21374
21840
  const label = isRelativeDateFunctionName(plan.rejection.functionName) ? "relative date function" : "kintone function";
21841
+ const rejectedNode2 = plan.nodes[plan.nodes.length - 1];
21842
+ const detail = rejectedNode2?.capability?.reasons.find(
21843
+ (reason) => reason.functionName === plan.rejection.functionName
21844
+ );
21845
+ const target = rejectedNode2 ? findServerFunctionExplainTarget(rejectedNode2.source, plan.rejection.functionName) : null;
21375
21846
  return [
21376
21847
  ` ${label}: ${plan.rejection.functionName}`,
21377
21848
  " plan status: rejected",
21849
+ ` target alias / field: ${target?.alias ?? "(unknown)"} / ${detail?.field ?? target?.field ?? "(unknown)"}`,
21378
21850
  ` reason: ${plan.rejection.reasonCodes.join(", ")}`,
21379
21851
  " client evaluation: forbidden",
21380
21852
  " records/cursor/mutation API during EXPLAIN: none"
@@ -21382,6 +21854,41 @@ function relativeDateExplainLines(plan) {
21382
21854
  }
21383
21855
  const lines = [];
21384
21856
  for (const node of plan.nodes) {
21857
+ const joinServerFunctionPlan = node.joinServerFunctionPlan;
21858
+ if (node.allowed && node.allowForm === "JOIN_SERVER_FUNCTION_EXACT" && joinServerFunctionPlan) {
21859
+ const variant = node.joinServerFunctionVariant === "WHOLE_WHERE_EXACT" ? "whole-WHERE" : "leaf";
21860
+ lines.push(
21861
+ ` allow form: JOIN_SERVER_FUNCTION_EXACT (${variant})`
21862
+ );
21863
+ for (const consumption of joinServerFunctionPlan.serverFunctionConsumptions) {
21864
+ for (const leaf of consumption.functionLeaves) {
21865
+ const functionName = serverFunctionNameOfExplainLeaf(leaf);
21866
+ const field = leaf.left.type === "FIELD" ? leaf.left.field : "(unknown)";
21867
+ lines.push(
21868
+ ` ${serverFunctionLabel(functionName)}: ${functionName}`,
21869
+ ` ${serverFunctionEvaluationLabel(functionName)}: kintone server exact JOIN prefilter`,
21870
+ ` target alias / APP: ${consumption.targetAlias} / APP${consumption.appId}`,
21871
+ ` field: ${field}`,
21872
+ ` function leaf relation: ${consumption.relation}`,
21873
+ ` consumption: ${consumption.consumption}`
21874
+ );
21875
+ }
21876
+ }
21877
+ lines.push(
21878
+ ` client residual: ${joinServerFunctionPlan.residualWhere === null ? "(none)" : renderRelativeDateResidualWhere(joinServerFunctionPlan.residualWhere)}`
21879
+ );
21880
+ if (joinServerFunctionPlan.adoptedServerFunctionOccurrences.some(
21881
+ isRelativeDateFunctionName
21882
+ )) {
21883
+ lines.push(" relative date client evaluations: 0");
21884
+ }
21885
+ if (joinServerFunctionPlan.adoptedServerFunctionOccurrences.some(
21886
+ (name) => !isRelativeDateFunctionName(name)
21887
+ )) {
21888
+ lines.push(" kintone function client evaluations: 0");
21889
+ }
21890
+ continue;
21891
+ }
21385
21892
  const fullScanExactPlan = node.fullScanExactPlan;
21386
21893
  if (node.allowed && node.allowForm === "FULL_SCAN_EXACT" && fullScanExactPlan) {
21387
21894
  for (const leaf of fullScanExactPlan.prefilterPlan.exactRelativeLeaves) {
@@ -21466,6 +21973,36 @@ function relativeDateExplainLines(plan) {
21466
21973
  }
21467
21974
  return lines;
21468
21975
  }
21976
+ function findServerFunctionExplainTarget(source, functionName) {
21977
+ const where = "where" in source ? source.where : null;
21978
+ let found = null;
21979
+ const visit = (value) => {
21980
+ if (found || value === null || typeof value !== "object") return;
21981
+ if (Array.isArray(value)) {
21982
+ value.forEach(visit);
21983
+ return;
21984
+ }
21985
+ const node = value;
21986
+ if (node["type"] === "BINARY") {
21987
+ const left = node["left"];
21988
+ const right = node["right"];
21989
+ const directlyMatches = right?.["type"] === "KINTONE_FUNC" && right["name"] === functionName;
21990
+ const listMatches = right?.["type"] === "IN_LIST" && Array.isArray(right["values"]) && right["values"].some(
21991
+ (entry) => entry !== null && typeof entry === "object" && entry["type"] === "KINTONE_FUNC" && entry["name"] === functionName
21992
+ );
21993
+ if ((directlyMatches || listMatches) && left?.["type"] === "FIELD" && typeof left["field"] === "string") {
21994
+ found = {
21995
+ alias: typeof left["tableAlias"] === "string" ? left["tableAlias"] : null,
21996
+ field: left["field"]
21997
+ };
21998
+ return;
21999
+ }
22000
+ }
22001
+ Object.values(node).forEach(visit);
22002
+ };
22003
+ visit(where);
22004
+ return found;
22005
+ }
21469
22006
  function serverFunctionLabel(functionName) {
21470
22007
  return isRelativeDateFunctionName(functionName) ? "relative date function" : "kintone function";
21471
22008
  }
@@ -21883,10 +22420,22 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
21883
22420
  lines.push(" join pushdown plan: applied (runtime metadata resolved)");
21884
22421
  lines.push(" runtime plan timing: variables/subqueries resolved -> metadata resolved -> immutable plan");
21885
22422
  lines.push(" EXPLAIN unresolved subqueries: not applied (records API is not called)");
21886
- lines.push(" residual: original WHERE");
22423
+ if (runtimeJoinPlan.joinPlan.serverFunctionCandidate) {
22424
+ lines.push(
22425
+ ` allow form: JOIN_SERVER_FUNCTION_EXACT (${runtimeJoinPlan.joinPlan.serverFunctionCandidate.variant === "WHOLE_WHERE_EXACT" ? "whole-WHERE" : "leaf"})`
22426
+ );
22427
+ }
22428
+ lines.push(
22429
+ ` client residual: ${runtimeJoinPlan.joinPlan.residualWhere === null ? "(none)" : renderRelativeDateResidualWhere(runtimeJoinPlan.joinPlan.residualWhere)}`
22430
+ );
21887
22431
  lines.push(` KLIKE applied nodes: ${runtimeJoinPlan.joinPlan.appliedKlikes.size}`);
21888
- for (const rejection of runtimeJoinPlan.joinPlan.rejections) {
21889
- lines.push(` join pushdown not applied: ${rejection.reason}`);
22432
+ lines.push(
22433
+ ` KLIKE unapplied nodes: ${runtimeJoinPlan.joinPlan.allKlikes.length - runtimeJoinPlan.joinPlan.appliedKlikes.size}`
22434
+ );
22435
+ if (runtimeJoinPlan.joinPlan.residualWhere !== null) {
22436
+ for (const rejection of runtimeJoinPlan.joinPlan.rejections) {
22437
+ lines.push(` join pushdown not applied: ${rejection.reason}`);
22438
+ }
21890
22439
  }
21891
22440
  } else {
21892
22441
  const reason = stmt.joins.some((join2) => join2.type !== "INNER") ? "OUTER_JOIN" : [stmt.from, ...stmt.joins.map((join2) => join2.table)].some(
@@ -21901,15 +22450,19 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
21901
22450
  const mainPushDown = pushdownPlan.mainCondition;
21902
22451
  const mainCandidate = extractMainTypedPushdownCandidate(stmt);
21903
22452
  const exactOriginalWhere = stmt.joins.length === 0 && whereCapability?.capability === "EXACT_PUSHDOWN" && stmt.where !== null && !whereRequiresJsEval(stmt.where) ? whereToKintone(stmt.where) : "";
21904
- const mainQ = mainPushDown !== null ? whereToKintone(mainPushDown) : exactOriginalWhere || "(\u5168\u4EF6\u53D6\u5F97)";
22453
+ const mainBoundQuery = stmt.from.alias ? runtimeJoinPlan?.queriesByAlias.get(stmt.from.alias) : void 0;
22454
+ const mainQ = mainBoundQuery || (mainPushDown !== null ? whereToKintone(mainPushDown) : exactOriginalWhere || "(\u5168\u4EF6\u53D6\u5F97)");
21905
22455
  lines.push(` app: APP${stmt.from.appId}${mainAliasStr} (${stmt.from.appId})`);
21906
22456
  lines.push(` kintone query: ${mainQ}`);
21907
22457
  const mainJoinItem = runtimeJoinPlan?.joinPlan.items.find(
21908
22458
  (item) => item.targetAlias === stmt.from.alias
21909
22459
  );
21910
- if (mainJoinItem) {
21911
- lines.push(` pushdown applied: ${runtimeJoinPlan.queriesByAlias.get(mainJoinItem.targetAlias)}`);
21912
- lines.push(` relation: ${mainJoinItem.relation}`);
22460
+ const mainFunctionConsumption = runtimeJoinPlan?.joinPlan.serverFunctionConsumptions.find(
22461
+ (consumption) => consumption.targetAlias === stmt.from.alias
22462
+ );
22463
+ if (mainJoinItem || mainFunctionConsumption) {
22464
+ lines.push(` pushdown applied: ${mainBoundQuery}`);
22465
+ lines.push(` relation: ${mainJoinItem?.relation ?? "exact"}`);
21913
22466
  } else if (!runtimeJoinPlan && mainCandidate !== null) {
21914
22467
  lines.push(` pushdown candidate: ${whereToKintone(mainCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u30FB\u5B9F\u5728\u78BA\u8A8D\u5F85\u3061\uFF09`);
21915
22468
  }
@@ -21920,15 +22473,19 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
21920
22473
  const joinType = join2.type === "INNER" ? "JOIN" : `${join2.type} JOIN`;
21921
22474
  const joinPushDown = join2.table.alias ? pushdownPlan.joinConditions.get(join2.table.alias) ?? null : null;
21922
22475
  const joinCandidate = join2.table.alias && !join2.table.subtableCode && join2.table.cteName === null && stmt.where ? extractTypedPushdownCandidates(stmt.where, { tableAlias: join2.table.alias }) : null;
21923
- const joinQ = joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
22476
+ const joinBoundQuery = join2.table.alias ? runtimeJoinPlan?.queriesByAlias.get(join2.table.alias) : void 0;
22477
+ const joinQ = joinBoundQuery || (joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)");
21924
22478
  lines.push(` ${joinType}: APP${join2.table.appId}${joinAliasStr} (${join2.table.appId})`);
21925
22479
  lines.push(` kintone query: ${joinQ}`);
21926
22480
  const joinPlanItem = runtimeJoinPlan?.joinPlan.items.find(
21927
22481
  (item) => item.targetAlias === join2.table.alias
21928
22482
  );
21929
- if (joinPlanItem) {
21930
- lines.push(` pushdown applied: ${runtimeJoinPlan.queriesByAlias.get(joinPlanItem.targetAlias)}`);
21931
- lines.push(` relation: ${joinPlanItem.relation}`);
22483
+ const joinFunctionConsumption = runtimeJoinPlan?.joinPlan.serverFunctionConsumptions.find(
22484
+ (consumption) => consumption.targetAlias === join2.table.alias
22485
+ );
22486
+ if (joinPlanItem || joinFunctionConsumption) {
22487
+ lines.push(` pushdown applied: ${joinBoundQuery}`);
22488
+ lines.push(` relation: ${joinPlanItem?.relation ?? "exact"}`);
21932
22489
  } else if (!runtimeJoinPlan && joinCandidate !== null) {
21933
22490
  lines.push(` pushdown candidate: ${whereToKintone(joinCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u30FB\u5B9F\u5728\u78BA\u8A8D\u5F85\u3061\uFF09`);
21934
22491
  }