@rex0220/kintone-sql-tools 3.27.0 → 3.29.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;
@@ -11233,6 +11369,30 @@ function classifyJoinPushdownLeaf(predicate, sources) {
11233
11369
  const relation = classifySupportedLeaf(predicate, owner, fieldType);
11234
11370
  return relation === "unsafe" ? unsafe() : Object.freeze({ relation, owner });
11235
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
+ }
11236
11396
  function buildJoinPushdownPlan(where, sources) {
11237
11397
  const allKlikes = /* @__PURE__ */ new Set();
11238
11398
  collectKlikes2(where, allKlikes);
@@ -11243,15 +11403,370 @@ function buildJoinPushdownPlan(where, sources) {
11243
11403
  predicate: fragment.predicate,
11244
11404
  relation: fragment.relation
11245
11405
  }));
11406
+ const serverFunctionFoundation = buildServerFunctionFoundation(where, sources);
11246
11407
  const appliedKlikes = /* @__PURE__ */ new Set();
11247
- 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
+ }
11248
11413
  return Object.freeze({
11249
11414
  items: Object.freeze(items),
11250
11415
  appliedKlikes,
11251
11416
  allKlikes: Object.freeze([...allKlikes]),
11252
- 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
11253
11494
  });
11254
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
+ }
11255
11770
  function collectRejections(where, sources) {
11256
11771
  if (where === null) return [];
11257
11772
  const reasons = /* @__PURE__ */ new Set();
@@ -11322,6 +11837,9 @@ function assertPredicateOwnership(predicate, item, sources) {
11322
11837
  case "GROUP":
11323
11838
  assertPredicateOwnership(predicate.expr, item, sources);
11324
11839
  return;
11840
+ case "NOT":
11841
+ assertPredicateOwnership(predicate.expr, item, sources);
11842
+ return;
11325
11843
  case "BINARY": {
11326
11844
  if (predicate.left.type !== "FIELD" || containsFieldReference(predicate.right)) {
11327
11845
  throw joinOwnershipError(item, "predicate contains a non-target or RHS field reference");
@@ -11332,7 +11850,6 @@ function assertPredicateOwnership(predicate, item, sources) {
11332
11850
  }
11333
11851
  return;
11334
11852
  }
11335
- case "NOT":
11336
11853
  case "NULL_CHECK":
11337
11854
  case "EXISTS":
11338
11855
  case "BOOLEAN":
@@ -13138,133 +13655,6 @@ function deepClone(value, seen = /* @__PURE__ */ new Map()) {
13138
13655
  return clone;
13139
13656
  }
13140
13657
 
13141
- // src/core/optimization/relativeDateFullScanExactPlan.ts
13142
- function buildRelativeDateFullScanExactPlan(input) {
13143
- const {
13144
- select,
13145
- selectMode,
13146
- capability,
13147
- context,
13148
- serializedWholeWhere,
13149
- relativeFunctionNames
13150
- } = input;
13151
- if (select.where === null) return null;
13152
- if (select.from.appId <= 0 || select.from.cteName !== null) return null;
13153
- if (select.from.subtableCode) return null;
13154
- if (select.joins.length > 0) return null;
13155
- if (!context.allowFullScanExact) return null;
13156
- if (select.orderMode === "KINTONE_NATIVE") return null;
13157
- const hasCanonicalOrder2 = select.orderMode === "CANONICAL" && select.orderBy.length > 0;
13158
- if (selectMode !== "FULL_SCAN" && !hasCanonicalOrder2) return null;
13159
- if (capability.capability !== "EXACT_PUSHDOWN") return null;
13160
- const occurrences = serverOnlyFunctionOccurrencesInWhere(select.where);
13161
- if (occurrences.length === 0) return null;
13162
- if (!sameOccurrenceList(occurrences, relativeFunctionNames)) return null;
13163
- if (serializedWholeWhere === null || !serializedMultisetContains(serializedWholeWhere, occurrences)) {
13164
- return null;
13165
- }
13166
- const prefilterPlan = {
13167
- prefilterWhere: select.where,
13168
- residualWhere: null,
13169
- exactRelativeLeaves: collectExactServerFunctionLeaves(select.where),
13170
- relativeFunctionNames: new Set(occurrences),
13171
- appliedKlikes: /* @__PURE__ */ new Set(),
13172
- capability: capability.capability,
13173
- reasons: capability.reasons
13174
- };
13175
- const plan = {
13176
- allowForm: "FULL_SCAN_EXACT",
13177
- clientWhereEvaluation: false,
13178
- serializedWholeWhere,
13179
- prefilterPlan
13180
- };
13181
- assertRelativeDateFullScanExactPlan(plan, input, occurrences);
13182
- return plan;
13183
- }
13184
- function assertRelativeDateFullScanExactPlan(plan, input, occurrences) {
13185
- if (plan.allowForm !== "FULL_SCAN_EXACT") {
13186
- throw new Error("FULL_SCAN_EXACT invariant: allowForm");
13187
- }
13188
- if (plan.clientWhereEvaluation !== false) {
13189
- throw new Error("FULL_SCAN_EXACT invariant: clientWhereEvaluation");
13190
- }
13191
- if (input.capability.capability !== "EXACT_PUSHDOWN") {
13192
- throw new Error("FULL_SCAN_EXACT invariant: capability");
13193
- }
13194
- if (input.select.where === null || plan.prefilterPlan.prefilterWhere !== input.select.where) {
13195
- throw new Error("FULL_SCAN_EXACT invariant: whole WHERE identity");
13196
- }
13197
- if (plan.prefilterPlan.residualWhere !== null) {
13198
- throw new Error("FULL_SCAN_EXACT invariant: residualWhere");
13199
- }
13200
- if (plan.prefilterPlan.capability !== "EXACT_PUSHDOWN") {
13201
- throw new Error("FULL_SCAN_EXACT invariant: transport capability");
13202
- }
13203
- if (!serializedMultisetContains(plan.serializedWholeWhere, occurrences)) {
13204
- throw new Error("FULL_SCAN_EXACT invariant: relative occurrence serialization");
13205
- }
13206
- }
13207
- function serverOnlyFunctionOccurrencesInWhere(where) {
13208
- const names = [];
13209
- const visit = (node) => {
13210
- if (Array.isArray(node)) {
13211
- node.forEach(visit);
13212
- return;
13213
- }
13214
- if (node === null || typeof node !== "object") return;
13215
- const value = node;
13216
- if (value["type"] === "SELECT") return;
13217
- if (value["type"] === "KINTONE_FUNC" && typeof value["name"] === "string" && isServerOnlyWhereFunctionName(value["name"])) {
13218
- names.push(value["name"]);
13219
- return;
13220
- }
13221
- Object.values(value).forEach(visit);
13222
- };
13223
- visit(where);
13224
- return names;
13225
- }
13226
- function collectExactServerFunctionLeaves(where) {
13227
- const leaves = [];
13228
- const visit = (node) => {
13229
- switch (node.type) {
13230
- case "BINARY":
13231
- 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)) {
13232
- leaves.push(node);
13233
- }
13234
- return;
13235
- case "LOGICAL":
13236
- visit(node.left);
13237
- visit(node.right);
13238
- return;
13239
- case "NOT":
13240
- case "GROUP":
13241
- visit(node.expr);
13242
- return;
13243
- case "EXISTS":
13244
- case "NULL_CHECK":
13245
- case "BOOLEAN":
13246
- return;
13247
- }
13248
- };
13249
- visit(where);
13250
- return leaves;
13251
- }
13252
- function sameOccurrenceList(actual, expected) {
13253
- return actual.length === expected.length && actual.every((name, index) => name === expected[index]);
13254
- }
13255
- function serializedMultisetContains(query, expectedNames) {
13256
- const expected = /* @__PURE__ */ new Map();
13257
- for (const name of expectedNames) {
13258
- if (!isServerOnlyWhereFunctionName(name)) return false;
13259
- expected.set(name, (expected.get(name) ?? 0) + 1);
13260
- }
13261
- for (const [name, count] of expected) {
13262
- const matches = query.match(new RegExp(`\\b${name}\\s*\\(`, "g"));
13263
- if ((matches?.length ?? 0) < count) return false;
13264
- }
13265
- return true;
13266
- }
13267
-
13268
13658
  // src/core/optimization/relativeDatePushdownGuard.ts
13269
13659
  function relativeDateFunctionNamesInNode(node, stopAtNestedSelect) {
13270
13660
  const names = [];
@@ -13485,6 +13875,11 @@ function serializationContainsFunctions(query, names) {
13485
13875
  function allowRelativeDatePrefilterPlan(select, decomposition) {
13486
13876
  return decomposition.eligible === true && resolveSelectMode(select) === "FULL_SCAN" && select.orderMode !== "KINTONE_NATIVE" && select.from.cteName === null && !select.from.subtableCode && select.joins.length === 0;
13487
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
+ }
13488
13883
  function rejectedNode(candidate) {
13489
13884
  return {
13490
13885
  kind: candidate.kind,
@@ -13584,6 +13979,14 @@ async function buildRelativeDatePushdownPlan(statement, resolver) {
13584
13979
  allowed2 = true;
13585
13980
  }
13586
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
+ }
13587
13990
  const node2 = {
13588
13991
  kind: candidate.kind,
13589
13992
  source: candidate.source,
@@ -13594,6 +13997,11 @@ async function buildRelativeDatePushdownPlan(statement, resolver) {
13594
13997
  restQuery: restQuery2,
13595
13998
  ...prefilterPlan ? { prefilterPlan, phase2PrefilterEligible } : {},
13596
13999
  ...fullScanExactPlan ? { fullScanExactPlan, allowForm: fullScanExactPlan.allowForm } : {},
14000
+ ...joinServerFunctionPlan ? {
14001
+ joinServerFunctionPlan,
14002
+ allowForm: "JOIN_SERVER_FUNCTION_EXACT",
14003
+ joinServerFunctionVariant: joinServerFunctionPlan.serverFunctionCandidate.variant
14004
+ } : {},
13597
14005
  clientWhereEvaluation: !allowed2,
13598
14006
  allowed: allowed2
13599
14007
  };
@@ -13685,7 +14093,7 @@ function decomposeRelativeDatePrefilter(stmt, resolveField2, testSeam = {}) {
13685
14093
  }
13686
14094
  const occurrences = collectServerFunctionOccurrences(stmt.where);
13687
14095
  if (occurrences.length === 0) return reject(["NO_RELATIVE_DATE"]);
13688
- const spine = collectServerFunctionLeavesOnAndSpine(stmt.where, resolveField2);
14096
+ const spine = collectServerFunctionLeavesOnAndSpine2(stmt.where, resolveField2);
13689
14097
  if (!spine.ok) return reject(spine.reasonCodes);
13690
14098
  if (!sameRelativeMultiset(occurrences, spine.leaves)) {
13691
14099
  return reject(["RELATIVE_DATE_LEAF_COUNT_MISMATCH"]);
@@ -13763,7 +14171,7 @@ function decomposeRelativeDatePrefilter(stmt, resolveField2, testSeam = {}) {
13763
14171
  }
13764
14172
  };
13765
14173
  }
13766
- function collectServerFunctionLeavesOnAndSpine(where, resolveField2) {
14174
+ function collectServerFunctionLeavesOnAndSpine2(where, resolveField2) {
13767
14175
  const leaves = [];
13768
14176
  let failure = null;
13769
14177
  const visit = (node) => {
@@ -15056,6 +15464,15 @@ async function resolveRelativeDateExecutionPlan(stmt, client, cacheContext) {
15056
15464
  cacheContext
15057
15465
  );
15058
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;
15059
15476
  }
15060
15477
  });
15061
15478
  }
@@ -15741,21 +16158,28 @@ async function runWithDeadline(work, remainingMs, onTimeout) {
15741
16158
  }
15742
16159
  }
15743
16160
  function toBatchStatementError(e) {
16161
+ let error;
15744
16162
  if (e instanceof ApplyWritePartialFailureError) {
15745
- return { code: e.name, message: e.message, partialSuccess: e.partialSuccess };
15746
- }
15747
- if (e instanceof Error) {
16163
+ error = { code: e.name, message: e.message, partialSuccess: e.partialSuccess };
16164
+ } else if (e instanceof Error) {
15748
16165
  const name = e.name !== "Error" ? e.name : null;
15749
- return { code: name ?? codeFromMessagePrefix(e.message), message: e.message };
15750
- }
15751
- if (e !== null && typeof e === "object") {
16166
+ error = { code: name ?? codeFromMessagePrefix(e.message), message: e.message };
16167
+ } else if (e !== null && typeof e === "object") {
15752
16168
  const obj = e;
15753
- const message2 = typeof obj.message === "string" && obj.message.length > 0 ? obj.message : safeJsonStringify(e);
15754
- const code = typeof obj.code === "string" && obj.code.length > 0 ? obj.code : codeFromMessagePrefix(message2);
15755
- return { code, message: message2 };
16169
+ const message = typeof obj.message === "string" && obj.message.length > 0 ? obj.message : safeJsonStringify(e);
16170
+ const code = typeof obj.code === "string" && obj.code.length > 0 ? obj.code : codeFromMessagePrefix(message);
16171
+ error = { code, message };
16172
+ } else {
16173
+ const message = String(e);
16174
+ error = { code: codeFromMessagePrefix(message), message };
15756
16175
  }
15757
- const message = String(e);
15758
- return { code: codeFromMessagePrefix(message), message };
16176
+ Object.defineProperty(error, "cause", {
16177
+ value: e,
16178
+ enumerable: false,
16179
+ configurable: false,
16180
+ writable: false
16181
+ });
16182
+ return error;
15759
16183
  }
15760
16184
  function codeFromMessagePrefix(message) {
15761
16185
  return message.match(/^([A-Za-z]+Error):/)?.[1] ?? "Error";
@@ -16680,6 +17104,7 @@ function extractMainTypedPushdownCandidate(stmt) {
16680
17104
  if (!stmt.from.alias) return null;
16681
17105
  return extractTypedPushdownCandidates(stmt.where, { tableAlias: stmt.from.alias });
16682
17106
  }
17107
+ var boundJoinRuntimePlans = /* @__PURE__ */ new WeakMap();
16683
17108
  function buildRuntimeJoinPushdownPlan(stmt, metadata) {
16684
17109
  if (stmt.joins.length === 0 || stmt.where === null || stmt.joins.some((join2) => join2.type !== "INNER")) {
16685
17110
  return null;
@@ -16700,11 +17125,17 @@ function buildRuntimeJoinPushdownPlan(stmt, metadata) {
16700
17125
  ]),
16701
17126
  fieldOptions: metadata.fieldOptionsByApp.get(table.appId)
16702
17127
  }));
16703
- const plan = buildJoinPushdownPlan(stmt.where, sources);
17128
+ const staticPlan = buildJoinPushdownPlan(stmt.where, sources);
17129
+ const plan = bindJoinServerFunctionFetches(staticPlan, sources);
16704
17130
  const conditionsByAlias = /* @__PURE__ */ new Map();
16705
17131
  const queriesByAlias = /* @__PURE__ */ new Map();
17132
+ for (const [alias, query] of plan.fetchQueriesByAlias) {
17133
+ queriesByAlias.set(alias, query);
17134
+ }
16706
17135
  for (const item of plan.items) {
16707
- queriesByAlias.set(item.targetAlias, serializeJoinPushdownItem(item, sources));
17136
+ if (!queriesByAlias.has(item.targetAlias)) {
17137
+ queriesByAlias.set(item.targetAlias, serializeJoinPushdownItem(item, sources));
17138
+ }
16708
17139
  conditionsByAlias.set(item.targetAlias, item.predicate);
16709
17140
  }
16710
17141
  const mainAlias = stmt.from.alias;
@@ -17346,15 +17777,24 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
17346
17777
  whereNeedsFieldMetadata(stmt.having) || selectCaseConditionsNeedFieldMetadata(stmt)
17347
17778
  );
17348
17779
  const havingFieldSemanticsResolver = buildHavingFieldSemanticsResolver(stmt, fieldSemanticsResolver);
17349
- const pushdownPlan = buildRuntimeJoinPushdownPlan(stmt, pushdownMeta) ?? buildKlikePushdownPlan(stmt, pushdownMeta);
17780
+ const preboundJoinPlan = boundJoinRuntimePlans.get(stmt);
17781
+ if (preboundJoinPlan && !isJoinServerFunctionFetchPlan(preboundJoinPlan.joinPlan)) {
17782
+ throw new Error("InternalError: JOIN server-function fetch binding changed before records API.");
17783
+ }
17784
+ const pushdownPlan = preboundJoinPlan ?? buildRuntimeJoinPushdownPlan(stmt, pushdownMeta) ?? buildKlikePushdownPlan(stmt, pushdownMeta);
17350
17785
  validateKlikePushdownPlan(pushdownPlan);
17786
+ const runtimeJoinPlan = "joinPlan" in pushdownPlan ? pushdownPlan : null;
17787
+ const boundServerFunctionPlan = runtimeJoinPlan && isJoinServerFunctionFetchPlan(runtimeJoinPlan.joinPlan) ? runtimeJoinPlan : null;
17351
17788
  const fetchClient = client;
17352
17789
  const mainPushDown = pushdownPlan.mainCondition;
17353
17790
  const tableConditions = pushdownPlan.joinConditions;
17354
17791
  if (prefilterPlan && allowOriginalWherePushdown) {
17355
17792
  throw new Error("internal error: relative-date prefilter must disable original WHERE pushdown.");
17356
17793
  }
17357
- const mainFetchCondition = prefilterPlan ? prefilterPlan.prefilterWhere : mainPushDown;
17794
+ const mainFetchCondition = prefilterPlan ? prefilterPlan.prefilterWhere : boundServerFunctionPlan ? null : mainPushDown;
17795
+ const mainBoundQuery = boundServerFunctionPlan?.queriesByAlias.get(
17796
+ stmt.from.alias
17797
+ ) ?? "";
17358
17798
  const scalarCache = await resolveScalarColumns(stmt.columns, client, options, cacheContext, cteCache);
17359
17799
  const constantFalse = isConstantFalseWhere(stmt.where);
17360
17800
  const mainFetch = constantFalse ? Promise.resolve([]) : fetchTableRecordsForFullScan(
@@ -17367,8 +17807,9 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
17367
17807
  options.onLimitReached ?? "error",
17368
17808
  warnings,
17369
17809
  mainFetchCondition,
17370
- allowOriginalWherePushdown,
17371
- plainGroupByPlan
17810
+ boundServerFunctionPlan ? false : allowOriginalWherePushdown,
17811
+ plainGroupByPlan,
17812
+ mainBoundQuery
17372
17813
  );
17373
17814
  const parallelJoins = [];
17374
17815
  const onOptJoins = [];
@@ -17377,8 +17818,9 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
17377
17818
  parallelJoins.push({ join: join2, promise: Promise.resolve([]) });
17378
17819
  continue;
17379
17820
  }
17380
- const jCond = join2.table.alias ? tableConditions.get(join2.table.alias) ?? null : null;
17381
- if (jCond !== null) {
17821
+ const boundQuery = join2.table.alias ? boundServerFunctionPlan?.queriesByAlias.get(join2.table.alias) ?? "" : "";
17822
+ const jCond = boundServerFunctionPlan ? null : join2.table.alias ? tableConditions.get(join2.table.alias) ?? null : null;
17823
+ if (jCond !== null || boundQuery !== "") {
17382
17824
  parallelJoins.push({
17383
17825
  join: join2,
17384
17826
  promise: fetchTableRecordsForFullScan(
@@ -17392,7 +17834,8 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
17392
17834
  warnings,
17393
17835
  jCond,
17394
17836
  true,
17395
- plainGroupByPlan
17837
+ plainGroupByPlan,
17838
+ boundQuery
17396
17839
  )
17397
17840
  });
17398
17841
  } else {
@@ -17452,7 +17895,7 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
17452
17895
  havingFieldSemanticsResolver,
17453
17896
  aggregateSortKindResolver,
17454
17897
  appliedKlikes: prefilterPlan?.appliedKlikes ?? pushdownPlan.appliedKlikes,
17455
- ...prefilterPlan ? { residualWhere: prefilterPlan.residualWhere } : {},
17898
+ ...prefilterPlan ? { residualWhere: prefilterPlan.residualWhere } : boundServerFunctionPlan ? { residualWhere: boundServerFunctionPlan.joinPlan.residualWhere } : {},
17456
17899
  resolvedGroupingSpec: resolvedGroupingSpecs.get(stmt),
17457
17900
  plainGroupByPlan
17458
17901
  });
@@ -21160,8 +21603,11 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
21160
21603
  throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
21161
21604
  }
21162
21605
  capabilities.set(select, capability);
21163
- const joinPushdownMeta = await loadTypedPushdownMeta(select, tracedClient, cacheContext);
21164
- const joinPushdownPlan = buildRuntimeJoinPushdownPlan(select, joinPushdownMeta);
21606
+ const preboundJoinPushdownPlan = boundJoinRuntimePlans.get(select);
21607
+ const joinPushdownPlan = preboundJoinPushdownPlan ?? buildRuntimeJoinPushdownPlan(
21608
+ select,
21609
+ await loadTypedPushdownMeta(select, tracedClient, cacheContext)
21610
+ );
21165
21611
  if (joinPushdownPlan) explainJoinPushdownPlans.set(select, joinPushdownPlan);
21166
21612
  if (select.orderBy.length > 0 || select.columns.some((column) => column.type === "WINDOW_COL" && column.orderBy.length > 0)) {
21167
21613
  const meta = await buildOrderByMetaForSelect(select, tracedClient, cacheContext);
@@ -21399,9 +21845,15 @@ function relativeDateExplainLines(plan) {
21399
21845
  if (!plan.hasServerOnlyWhereFunction) return [];
21400
21846
  if (!plan.allowed && plan.rejection) {
21401
21847
  const label = isRelativeDateFunctionName(plan.rejection.functionName) ? "relative date function" : "kintone function";
21848
+ const rejectedNode2 = plan.nodes[plan.nodes.length - 1];
21849
+ const detail = rejectedNode2?.capability?.reasons.find(
21850
+ (reason) => reason.functionName === plan.rejection.functionName
21851
+ );
21852
+ const target = rejectedNode2 ? findServerFunctionExplainTarget(rejectedNode2.source, plan.rejection.functionName) : null;
21402
21853
  return [
21403
21854
  ` ${label}: ${plan.rejection.functionName}`,
21404
21855
  " plan status: rejected",
21856
+ ` target alias / field: ${target?.alias ?? "(unknown)"} / ${detail?.field ?? target?.field ?? "(unknown)"}`,
21405
21857
  ` reason: ${plan.rejection.reasonCodes.join(", ")}`,
21406
21858
  " client evaluation: forbidden",
21407
21859
  " records/cursor/mutation API during EXPLAIN: none"
@@ -21409,6 +21861,41 @@ function relativeDateExplainLines(plan) {
21409
21861
  }
21410
21862
  const lines = [];
21411
21863
  for (const node of plan.nodes) {
21864
+ const joinServerFunctionPlan = node.joinServerFunctionPlan;
21865
+ if (node.allowed && node.allowForm === "JOIN_SERVER_FUNCTION_EXACT" && joinServerFunctionPlan) {
21866
+ const variant = node.joinServerFunctionVariant === "WHOLE_WHERE_EXACT" ? "whole-WHERE" : "leaf";
21867
+ lines.push(
21868
+ ` allow form: JOIN_SERVER_FUNCTION_EXACT (${variant})`
21869
+ );
21870
+ for (const consumption of joinServerFunctionPlan.serverFunctionConsumptions) {
21871
+ for (const leaf of consumption.functionLeaves) {
21872
+ const functionName = serverFunctionNameOfExplainLeaf(leaf);
21873
+ const field = leaf.left.type === "FIELD" ? leaf.left.field : "(unknown)";
21874
+ lines.push(
21875
+ ` ${serverFunctionLabel(functionName)}: ${functionName}`,
21876
+ ` ${serverFunctionEvaluationLabel(functionName)}: kintone server exact JOIN prefilter`,
21877
+ ` target alias / APP: ${consumption.targetAlias} / APP${consumption.appId}`,
21878
+ ` field: ${field}`,
21879
+ ` function leaf relation: ${consumption.relation}`,
21880
+ ` consumption: ${consumption.consumption}`
21881
+ );
21882
+ }
21883
+ }
21884
+ lines.push(
21885
+ ` client residual: ${joinServerFunctionPlan.residualWhere === null ? "(none)" : renderRelativeDateResidualWhere(joinServerFunctionPlan.residualWhere)}`
21886
+ );
21887
+ if (joinServerFunctionPlan.adoptedServerFunctionOccurrences.some(
21888
+ isRelativeDateFunctionName
21889
+ )) {
21890
+ lines.push(" relative date client evaluations: 0");
21891
+ }
21892
+ if (joinServerFunctionPlan.adoptedServerFunctionOccurrences.some(
21893
+ (name) => !isRelativeDateFunctionName(name)
21894
+ )) {
21895
+ lines.push(" kintone function client evaluations: 0");
21896
+ }
21897
+ continue;
21898
+ }
21412
21899
  const fullScanExactPlan = node.fullScanExactPlan;
21413
21900
  if (node.allowed && node.allowForm === "FULL_SCAN_EXACT" && fullScanExactPlan) {
21414
21901
  for (const leaf of fullScanExactPlan.prefilterPlan.exactRelativeLeaves) {
@@ -21493,6 +21980,36 @@ function relativeDateExplainLines(plan) {
21493
21980
  }
21494
21981
  return lines;
21495
21982
  }
21983
+ function findServerFunctionExplainTarget(source, functionName) {
21984
+ const where = "where" in source ? source.where : null;
21985
+ let found = null;
21986
+ const visit = (value) => {
21987
+ if (found || value === null || typeof value !== "object") return;
21988
+ if (Array.isArray(value)) {
21989
+ value.forEach(visit);
21990
+ return;
21991
+ }
21992
+ const node = value;
21993
+ if (node["type"] === "BINARY") {
21994
+ const left = node["left"];
21995
+ const right = node["right"];
21996
+ const directlyMatches = right?.["type"] === "KINTONE_FUNC" && right["name"] === functionName;
21997
+ const listMatches = right?.["type"] === "IN_LIST" && Array.isArray(right["values"]) && right["values"].some(
21998
+ (entry) => entry !== null && typeof entry === "object" && entry["type"] === "KINTONE_FUNC" && entry["name"] === functionName
21999
+ );
22000
+ if ((directlyMatches || listMatches) && left?.["type"] === "FIELD" && typeof left["field"] === "string") {
22001
+ found = {
22002
+ alias: typeof left["tableAlias"] === "string" ? left["tableAlias"] : null,
22003
+ field: left["field"]
22004
+ };
22005
+ return;
22006
+ }
22007
+ }
22008
+ Object.values(node).forEach(visit);
22009
+ };
22010
+ visit(where);
22011
+ return found;
22012
+ }
21496
22013
  function serverFunctionLabel(functionName) {
21497
22014
  return isRelativeDateFunctionName(functionName) ? "relative date function" : "kintone function";
21498
22015
  }
@@ -21910,10 +22427,22 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
21910
22427
  lines.push(" join pushdown plan: applied (runtime metadata resolved)");
21911
22428
  lines.push(" runtime plan timing: variables/subqueries resolved -> metadata resolved -> immutable plan");
21912
22429
  lines.push(" EXPLAIN unresolved subqueries: not applied (records API is not called)");
21913
- lines.push(" residual: original WHERE");
22430
+ if (runtimeJoinPlan.joinPlan.serverFunctionCandidate) {
22431
+ lines.push(
22432
+ ` allow form: JOIN_SERVER_FUNCTION_EXACT (${runtimeJoinPlan.joinPlan.serverFunctionCandidate.variant === "WHOLE_WHERE_EXACT" ? "whole-WHERE" : "leaf"})`
22433
+ );
22434
+ }
22435
+ lines.push(
22436
+ ` client residual: ${runtimeJoinPlan.joinPlan.residualWhere === null ? "(none)" : renderRelativeDateResidualWhere(runtimeJoinPlan.joinPlan.residualWhere)}`
22437
+ );
21914
22438
  lines.push(` KLIKE applied nodes: ${runtimeJoinPlan.joinPlan.appliedKlikes.size}`);
21915
- for (const rejection of runtimeJoinPlan.joinPlan.rejections) {
21916
- lines.push(` join pushdown not applied: ${rejection.reason}`);
22439
+ lines.push(
22440
+ ` KLIKE unapplied nodes: ${runtimeJoinPlan.joinPlan.allKlikes.length - runtimeJoinPlan.joinPlan.appliedKlikes.size}`
22441
+ );
22442
+ if (runtimeJoinPlan.joinPlan.residualWhere !== null) {
22443
+ for (const rejection of runtimeJoinPlan.joinPlan.rejections) {
22444
+ lines.push(` join pushdown not applied: ${rejection.reason}`);
22445
+ }
21917
22446
  }
21918
22447
  } else {
21919
22448
  const reason = stmt.joins.some((join2) => join2.type !== "INNER") ? "OUTER_JOIN" : [stmt.from, ...stmt.joins.map((join2) => join2.table)].some(
@@ -21928,15 +22457,19 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
21928
22457
  const mainPushDown = pushdownPlan.mainCondition;
21929
22458
  const mainCandidate = extractMainTypedPushdownCandidate(stmt);
21930
22459
  const exactOriginalWhere = stmt.joins.length === 0 && whereCapability?.capability === "EXACT_PUSHDOWN" && stmt.where !== null && !whereRequiresJsEval(stmt.where) ? whereToKintone(stmt.where) : "";
21931
- const mainQ = mainPushDown !== null ? whereToKintone(mainPushDown) : exactOriginalWhere || "(\u5168\u4EF6\u53D6\u5F97)";
22460
+ const mainBoundQuery = stmt.from.alias ? runtimeJoinPlan?.queriesByAlias.get(stmt.from.alias) : void 0;
22461
+ const mainQ = mainBoundQuery || (mainPushDown !== null ? whereToKintone(mainPushDown) : exactOriginalWhere || "(\u5168\u4EF6\u53D6\u5F97)");
21932
22462
  lines.push(` app: APP${stmt.from.appId}${mainAliasStr} (${stmt.from.appId})`);
21933
22463
  lines.push(` kintone query: ${mainQ}`);
21934
22464
  const mainJoinItem = runtimeJoinPlan?.joinPlan.items.find(
21935
22465
  (item) => item.targetAlias === stmt.from.alias
21936
22466
  );
21937
- if (mainJoinItem) {
21938
- lines.push(` pushdown applied: ${runtimeJoinPlan.queriesByAlias.get(mainJoinItem.targetAlias)}`);
21939
- lines.push(` relation: ${mainJoinItem.relation}`);
22467
+ const mainFunctionConsumption = runtimeJoinPlan?.joinPlan.serverFunctionConsumptions.find(
22468
+ (consumption) => consumption.targetAlias === stmt.from.alias
22469
+ );
22470
+ if (mainJoinItem || mainFunctionConsumption) {
22471
+ lines.push(` pushdown applied: ${mainBoundQuery}`);
22472
+ lines.push(` relation: ${mainJoinItem?.relation ?? "exact"}`);
21940
22473
  } else if (!runtimeJoinPlan && mainCandidate !== null) {
21941
22474
  lines.push(` pushdown candidate: ${whereToKintone(mainCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u30FB\u5B9F\u5728\u78BA\u8A8D\u5F85\u3061\uFF09`);
21942
22475
  }
@@ -21947,15 +22480,19 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
21947
22480
  const joinType = join2.type === "INNER" ? "JOIN" : `${join2.type} JOIN`;
21948
22481
  const joinPushDown = join2.table.alias ? pushdownPlan.joinConditions.get(join2.table.alias) ?? null : null;
21949
22482
  const joinCandidate = join2.table.alias && !join2.table.subtableCode && join2.table.cteName === null && stmt.where ? extractTypedPushdownCandidates(stmt.where, { tableAlias: join2.table.alias }) : null;
21950
- const joinQ = joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)";
22483
+ const joinBoundQuery = join2.table.alias ? runtimeJoinPlan?.queriesByAlias.get(join2.table.alias) : void 0;
22484
+ const joinQ = joinBoundQuery || (joinPushDown !== null ? whereToKintone(joinPushDown) : "(\u5168\u4EF6\u53D6\u5F97)");
21951
22485
  lines.push(` ${joinType}: APP${join2.table.appId}${joinAliasStr} (${join2.table.appId})`);
21952
22486
  lines.push(` kintone query: ${joinQ}`);
21953
22487
  const joinPlanItem = runtimeJoinPlan?.joinPlan.items.find(
21954
22488
  (item) => item.targetAlias === join2.table.alias
21955
22489
  );
21956
- if (joinPlanItem) {
21957
- lines.push(` pushdown applied: ${runtimeJoinPlan.queriesByAlias.get(joinPlanItem.targetAlias)}`);
21958
- lines.push(` relation: ${joinPlanItem.relation}`);
22490
+ const joinFunctionConsumption = runtimeJoinPlan?.joinPlan.serverFunctionConsumptions.find(
22491
+ (consumption) => consumption.targetAlias === join2.table.alias
22492
+ );
22493
+ if (joinPlanItem || joinFunctionConsumption) {
22494
+ lines.push(` pushdown applied: ${joinBoundQuery}`);
22495
+ lines.push(` relation: ${joinPlanItem?.relation ?? "exact"}`);
21959
22496
  } else if (!runtimeJoinPlan && joinCandidate !== null) {
21960
22497
  lines.push(` pushdown candidate: ${whereToKintone(joinCandidate)}\uFF08\u5B9F\u884C\u6642\u306E\u578B\u30FB\u5B9F\u5728\u78BA\u8A8D\u5F85\u3061\uFF09`);
21961
22498
  }