@rex0220/kintone-sql-tools 3.31.0 → 3.32.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
@@ -12530,11 +12530,11 @@ var EMPTY_WILDCARD_FIELD_TYPE_POLICY = {
12530
12530
  UPDATED_TIME: "RECORD",
12531
12531
  USER_SELECT: "RECORD"
12532
12532
  };
12533
- function fieldPolicy(fieldType) {
12533
+ function fieldPolicy(fieldType, fieldCode) {
12534
12534
  const policy = EMPTY_WILDCARD_FIELD_TYPE_POLICY[fieldType];
12535
12535
  if (policy === void 0) {
12536
12536
  throw new Error(
12537
- `InternalError: empty SELECT * schema policy is not defined for field type ${fieldType}.`
12537
+ `ArgumentError: getFields returned unknown fieldType "${fieldType}" for field "${fieldCode}". getFields must return only the fields from /k/v1/app/form/fields.json; $id and $revision are synthesized by the engine and must not be added.`
12538
12538
  );
12539
12539
  }
12540
12540
  return policy;
@@ -12549,10 +12549,10 @@ async function deriveEmptyWildcardColumns(fields, subtableCode, loadProcessStatu
12549
12549
  ];
12550
12550
  }
12551
12551
  const topLevel = fields.filter((field) => !field.inSubtable);
12552
- const needsProcessSettings = topLevel.some((field) => fieldPolicy(field.fieldType) === "PROCESS");
12552
+ const needsProcessSettings = topLevel.some((field) => fieldPolicy(field.fieldType, field.code) === "PROCESS");
12553
12553
  const processEnabled = needsProcessSettings ? (await loadProcessStatuses()).enable : false;
12554
12554
  const columns = topLevel.filter((field) => {
12555
- const policy = fieldPolicy(field.fieldType);
12555
+ const policy = fieldPolicy(field.fieldType, field.code);
12556
12556
  return policy === "RECORD" || policy === "PROCESS" && processEnabled;
12557
12557
  }).map((field) => field.code);
12558
12558
  return [...columns, "$revision", "$id"];
@@ -15448,11 +15448,21 @@ function createEmptyMetrics() {
15448
15448
  cursorCreateOutcomeUnknown: 0,
15449
15449
  cursorQuarantinedCurrent: 0,
15450
15450
  fetchedRows: 0,
15451
+ limitReached: false,
15452
+ limitReachedApps: [],
15451
15453
  elapsedMs: 0
15452
15454
  };
15453
15455
  }
15456
+ var LIMIT_METRICS_SINK = /* @__PURE__ */ Symbol("limitMetricsSink");
15457
+ function markLimitReached(client, appId) {
15458
+ const metrics = client[LIMIT_METRICS_SINK];
15459
+ if (!metrics) return;
15460
+ metrics.limitReached = true;
15461
+ if (!metrics.limitReachedApps.includes(appId)) metrics.limitReachedApps.push(appId);
15462
+ }
15454
15463
  function wrapClientWithMetrics(client, metrics) {
15455
15464
  return {
15465
+ [LIMIT_METRICS_SINK]: metrics,
15456
15466
  getRecords: async (params) => {
15457
15467
  metrics.getCalls += 1;
15458
15468
  const res = await client.getRecords(params);
@@ -15652,7 +15662,7 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
15652
15662
  return executeExistingRecordValidation(stmt, client, options, cacheContext);
15653
15663
  case "SELECT":
15654
15664
  return executeSelect(
15655
- stmt,
15665
+ markCountTotalCountRoot(stmt),
15656
15666
  client,
15657
15667
  options,
15658
15668
  cacheContext,
@@ -16390,12 +16400,12 @@ function resolveBatchVariableReferencesInternal(node, variables, numericArithmet
16390
16400
  if (value.type === "array") {
16391
16401
  throw new Error(`ParseError: array variable @${obj["name"]} can only be used as IN @${obj["name"]}.`);
16392
16402
  }
16393
- if (numericArithmeticOperand && value.type !== "number") {
16403
+ if (numericArithmeticOperand && value.type !== "number" && value.placeholder !== true) {
16394
16404
  throw new Error(
16395
16405
  `ArgumentError: variable @${obj["name"]} is not numeric and cannot be used in arithmetic.`
16396
16406
  );
16397
16407
  }
16398
- return value.type === "number" ? { type: "NUMBER", value: value.value, raw: value.raw ?? String(value.value) } : { type: "STRING", value: value.value };
16408
+ return numericArithmeticOperand && value.type === "string" && value.placeholder === true ? { type: "NUMBER", value: 0, raw: value.value } : value.type === "number" ? { type: "NUMBER", value: value.value, raw: value.raw ?? String(value.value) } : { type: "STRING", value: value.value };
16399
16409
  }
16400
16410
  if (obj["type"] === "VARIABLE_COL" && typeof obj["name"] === "string" && typeof obj["alias"] === "string") {
16401
16411
  const value = variables.get(obj["name"]);
@@ -16788,7 +16798,7 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
16788
16798
  cteCache
16789
16799
  );
16790
16800
  await resolveSelectCaseSubqueries(stmt, client, options, cacheContext, cteCache);
16791
- const whereCapability = await resolveSelectWhereCapability(stmt, client, cacheContext, cteCache);
16801
+ const whereCapability = rememberSelectWhereCapability(stmt, await resolveSelectWhereCapability(stmt, client, cacheContext, cteCache));
16792
16802
  if (whereCapability.capability === "UNSUPPORTED") {
16793
16803
  throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(whereCapability)}).`);
16794
16804
  }
@@ -17214,6 +17224,7 @@ async function executeSimpleSelect(stmt, client, options, cacheContext, orderPla
17214
17224
  onLimit,
17215
17225
  onTruncate: (max) => {
17216
17226
  warnings.add(`\u53D6\u5F97\u4E0A\u9650\uFF08${max} \u4EF6\uFF09\u306B\u9054\u3057\u305F\u305F\u3081\u3001${max} \u4EF6\u3067\u6253\u3061\u5207\u3063\u3066\u8868\u793A\u3057\u3066\u3044\u307E\u3059\u3002`);
17227
+ markLimitReached(client, stmt.from.appId);
17217
17228
  }
17218
17229
  }
17219
17230
  );
@@ -18099,7 +18110,55 @@ function buildSelectFieldTypeResolvers(stmt, fieldTypesByApp) {
18099
18110
  };
18100
18111
  return { row, having };
18101
18112
  }
18113
+ var countTotalCountRootSelects = /* @__PURE__ */ new WeakSet();
18114
+ var resolvedWhereCapabilities = /* @__PURE__ */ new WeakMap();
18115
+ function markCountTotalCountRoot(stmt) {
18116
+ countTotalCountRootSelects.add(stmt);
18117
+ return stmt;
18118
+ }
18119
+ function rememberSelectWhereCapability(stmt, capability) {
18120
+ resolvedWhereCapabilities.set(stmt, capability);
18121
+ return capability;
18122
+ }
18123
+ function isCountStarTotalCountEligible(stmt, whereCapability) {
18124
+ if (whereCapability.capability !== "EXACT_PUSHDOWN") return false;
18125
+ if (stmt.columns.length !== 1) return false;
18126
+ const column = stmt.columns[0];
18127
+ if (column?.type !== "AGGREGATE" || column.func !== "COUNT" || column.distinct || column.arg.type !== "WILDCARD") {
18128
+ return false;
18129
+ }
18130
+ return !stmt.distinct && normalizeGroupingSpec(stmt).type === "NONE" && stmt.having === null && stmt.joins.length === 0 && stmt.from.cteName === null && !stmt.from.subtableCode && stmt.limit === null && stmt.offset === null;
18131
+ }
18132
+ function isValidTotalCount(value) {
18133
+ return typeof value === "string" && /^(?:0|[1-9]\d*)$/.test(value);
18134
+ }
18135
+ async function tryCountStarWithTotalCount(stmt, client) {
18136
+ const countColumn = stmt.columns[0];
18137
+ if (countColumn?.type !== "AGGREGATE") return null;
18138
+ const baseQuery = stmt.where === null ? "" : whereToKintone(stmt.where);
18139
+ const response = await client.getRecords({
18140
+ app: stmt.from.appId,
18141
+ query: `${baseQuery}${baseQuery ? " " : ""}limit 1`,
18142
+ fields: ["$id"],
18143
+ totalCount: true
18144
+ });
18145
+ if (response.searchAborted) throw new SearchAbortedError();
18146
+ if (!isValidTotalCount(response.totalCount)) return null;
18147
+ const column = countColumn.alias ?? "COUNT(*)";
18148
+ return {
18149
+ type: "SELECT",
18150
+ rows: [{ [column]: response.totalCount }],
18151
+ columns: [column],
18152
+ rowCount: 1,
18153
+ warnings: []
18154
+ };
18155
+ }
18102
18156
  async function executeFullScanSelect(stmt, client, options, cacheContext, cteCache, allowOriginalWherePushdown = true, preloadedOrderMeta, prefilterPlan, plainGroupByPlan) {
18157
+ const whereCapability = resolvedWhereCapabilities.get(stmt);
18158
+ if (countTotalCountRootSelects.has(stmt) && whereCapability !== void 0 && isCountStarTotalCountEligible(stmt, whereCapability)) {
18159
+ const totalCountResult = await tryCountStarWithTotalCount(stmt, client);
18160
+ if (totalCountResult !== null) return totalCountResult;
18161
+ }
18103
18162
  const maxRecords = options.maxRecords ?? 1e4;
18104
18163
  const warnings = /* @__PURE__ */ new Set();
18105
18164
  const parallel = options.fetchParallel ?? 1;
@@ -18540,6 +18599,7 @@ async function fetchTableRecordsForFullScan(stmt, table, client, maxRecords, par
18540
18599
  const fields = selectToFetchAllFields(stmt, table, plainGroupByPlan);
18541
18600
  const onTruncate = (max) => {
18542
18601
  warnings.add(`\u53D6\u5F97\u4E0A\u9650\uFF08${max} \u4EF6\uFF09\u306B\u9054\u3057\u305F\u305F\u3081\u3001${max} \u4EF6\u3067\u6253\u3061\u5207\u3063\u3066\u8868\u793A\u3057\u3066\u3044\u307E\u3059\u3002`);
18602
+ markLimitReached(client, table.appId);
18543
18603
  };
18544
18604
  if (!table.subtableCode) {
18545
18605
  const baseQuery = isMainTable && allowOriginalWherePushdown ? selectToFetchAllParams(stmt, table.appId).query : "";
@@ -18695,6 +18755,7 @@ async function tryFetchJoinRecordsBySourceKeys(stmt, join2, tables, client, maxR
18695
18755
  const fields = selectToFetchAllFields(stmt, join2.table, plainGroupByPlan);
18696
18756
  const onTruncate = (max) => {
18697
18757
  warnings.add(`\u53D6\u5F97\u4E0A\u9650\uFF08${max} \u4EF6\uFF09\u306B\u9054\u3057\u305F\u305F\u3081\u3001${max} \u4EF6\u3067\u6253\u3061\u5207\u3063\u3066\u8868\u793A\u3057\u3066\u3044\u307E\u3059\u3002`);
18758
+ markLimitReached(client, join2.table.appId);
18698
18759
  };
18699
18760
  const chunks = splitChunks(values, JOIN_IN_CHUNK_SIZE);
18700
18761
  const merged = [];
@@ -22446,7 +22507,7 @@ async function buildBatchExplainPlans(sql, client, injectedVariables, cacheConte
22446
22507
  plan: statementPlan.length === 0 ? metadataPlan : [statementPlan[0], ...metadataPlan, ...statementPlan.slice(1)]
22447
22508
  });
22448
22509
  if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
22449
- variables.set(stmt.name, stmt.type === "SET_VARIABLE" && stmt.expr.type === "ARRAY" ? { type: "array", elements: stmt.expr.elements.map((element) => ({ type: "string", value: element.value })) } : { type: "string", value: `@${stmt.name}` });
22510
+ variables.set(stmt.name, stmt.type === "SET_VARIABLE" && stmt.expr.type === "ARRAY" ? { type: "array", elements: stmt.expr.elements.map((element) => ({ type: "string", value: element.value })) } : { type: "string", value: `@${stmt.name}`, placeholder: true });
22450
22511
  }
22451
22512
  }
22452
22513
  return { statementCount: statements.length, statements: plans };
@@ -22714,11 +22775,12 @@ function buildValidatePlan(stmt, label) {
22714
22775
  lines.push(" records/mutation API during EXPLAIN: none; violation count unavailable");
22715
22776
  return lines;
22716
22777
  }
22717
- function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlans) {
22778
+ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlans, allowTotalCountPlan = true) {
22718
22779
  const whereCapability = capabilities?.get(stmt) ?? (capabilities ? [...capabilities].find(([candidate]) => JSON.stringify(candidate) === JSON.stringify(stmt))?.[1] : void 0);
22719
22780
  const orderPlan = orderPlans?.get(stmt) ?? (orderPlans ? [...orderPlans].find(([candidate]) => JSON.stringify(candidate) === JSON.stringify(stmt))?.[1] : void 0);
22720
22781
  const plainGroupByPlan = plainGroupByPlans?.get(stmt) ?? (plainGroupByPlans ? [...plainGroupByPlans].find(([candidate]) => JSON.stringify(candidate) === JSON.stringify(stmt))?.[1] : void 0);
22721
- const mode = orderPlan?.kind === "CANONICAL_LOCAL" ? "FULL_SCAN" : whereCapability && whereCapability.capability !== "EXACT_PUSHDOWN" ? "FULL_SCAN" : resolveSelectMode(stmt);
22782
+ const totalCountPlan = allowTotalCountPlan && whereCapability !== void 0 && isCountStarTotalCountEligible(stmt, whereCapability);
22783
+ const mode = totalCountPlan ? "COUNT_TOTAL_COUNT" : orderPlan?.kind === "CANONICAL_LOCAL" ? "FULL_SCAN" : whereCapability && whereCapability.capability !== "EXACT_PUSHDOWN" ? "FULL_SCAN" : resolveSelectMode(stmt);
22722
22784
  const reasons = collectFullScanReasons(stmt);
22723
22785
  if (whereCapability && whereCapability.capability !== "EXACT_PUSHDOWN") {
22724
22786
  reasons.push(...whereCapability.reasons.map((reason) => reason.code));
@@ -22765,6 +22827,20 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans, plainGroupByPlan
22765
22827
  }
22766
22828
  }
22767
22829
  }
22830
+ if (totalCountPlan) {
22831
+ const baseQuery = stmt.where === null ? "" : whereToKintone(stmt.where);
22832
+ lines.push(` app: APP${stmt.from.appId} (${stmt.from.appId})`);
22833
+ lines.push(
22834
+ ` kintone query: ${baseQuery}${baseQuery ? " " : ""}limit 1`
22835
+ );
22836
+ lines.push(" fields: $id");
22837
+ lines.push(" fetch API: GET records.json (totalCount=true)");
22838
+ lines.push(" REST execution: single GET");
22839
+ lines.push(" record limit: maxRecords/onLimitReached not applied");
22840
+ lines.push(" fallback: full record scan when totalCount is missing or invalid");
22841
+ lines.push(" search abort: fail-closed (SearchAbortedError)");
22842
+ return lines;
22843
+ }
22768
22844
  if (orderPlan) {
22769
22845
  lines.push(` order plan: ${orderPlan.kind}`);
22770
22846
  if (orderPlan.reasonCodes.length > 0) lines.push(` order reason: ${orderPlan.reasonCodes.join(", ")}`);
@@ -22901,7 +22977,7 @@ function buildUnionPlan(stmt, capabilities, orderPlans, plainGroupByPlans) {
22901
22977
  const lines = [];
22902
22978
  selects.forEach((sel, i) => {
22903
22979
  if (i > 0) lines.push("");
22904
- lines.push(...buildSelectPlan(sel, `[union:${i + 1}]`, capabilities, orderPlans, plainGroupByPlans));
22980
+ lines.push(...buildSelectPlan(sel, `[union:${i + 1}]`, capabilities, orderPlans, plainGroupByPlans, false));
22905
22981
  });
22906
22982
  return lines;
22907
22983
  }
@@ -22909,7 +22985,7 @@ function buildWithPlan(stmt, capabilities, orderPlans, plainGroupByPlans) {
22909
22985
  const lines = [];
22910
22986
  for (const cte of stmt.ctes) {
22911
22987
  if (cte.query.type === "SELECT") {
22912
- lines.push(...buildSelectPlan(cte.query, `[cte: ${cte.name}]`, capabilities, orderPlans, plainGroupByPlans));
22988
+ lines.push(...buildSelectPlan(cte.query, `[cte: ${cte.name}]`, capabilities, orderPlans, plainGroupByPlans, false));
22913
22989
  lines.push("");
22914
22990
  }
22915
22991
  }
@@ -22928,7 +23004,7 @@ function buildWithPlan(stmt, capabilities, orderPlans, plainGroupByPlans) {
22928
23004
  if (canInlineSingleCte(stmt)) {
22929
23005
  lines.push("");
22930
23006
  const inlined = buildInlinedQuery(stmt);
22931
- lines.push(...buildSelectPlan(inlined, "[effective: inlined CTE]", capabilities, orderPlans, plainGroupByPlans));
23007
+ lines.push(...buildSelectPlan(inlined, "[effective: inlined CTE]", capabilities, orderPlans, plainGroupByPlans, false));
22932
23008
  }
22933
23009
  return lines;
22934
23010
  }
@@ -22970,16 +23046,16 @@ function collectSubqueryPlans(stmt, capabilities, orderPlans, plainGroupByPlans)
22970
23046
  case "BINARY":
22971
23047
  if (w.right.type === "SCALAR_SUBQUERY") {
22972
23048
  lines.push("");
22973
- lines.push(...buildSelectPlan(w.right.query, `[subquery:${idx++}]`, capabilities, orderPlans, plainGroupByPlans));
23049
+ lines.push(...buildSelectPlan(w.right.query, `[subquery:${idx++}]`, capabilities, orderPlans, plainGroupByPlans, false));
22974
23050
  }
22975
23051
  if (w.right.type === "SUBQUERY_IN_LIST") {
22976
23052
  lines.push("");
22977
- lines.push(...buildSelectPlan(w.right.query, `[subquery:${idx++}]`, capabilities, orderPlans, plainGroupByPlans));
23053
+ lines.push(...buildSelectPlan(w.right.query, `[subquery:${idx++}]`, capabilities, orderPlans, plainGroupByPlans, false));
22978
23054
  }
22979
23055
  break;
22980
23056
  case "EXISTS":
22981
23057
  lines.push("");
22982
- lines.push(...buildSelectPlan(w.query, `[subquery:${idx++}]`, capabilities, orderPlans, plainGroupByPlans));
23058
+ lines.push(...buildSelectPlan(w.query, `[subquery:${idx++}]`, capabilities, orderPlans, plainGroupByPlans, false));
22983
23059
  break;
22984
23060
  case "LOGICAL":
22985
23061
  visitWhere(w.left);
@@ -22999,7 +23075,7 @@ function collectSubqueryPlans(stmt, capabilities, orderPlans, plainGroupByPlans)
22999
23075
  for (const col of stmt.columns) {
23000
23076
  if (col.type === "SCALAR_SUBQUERY_COL") {
23001
23077
  lines.push("");
23002
- lines.push(...buildSelectPlan(col.query, `[subquery:${idx++}]`, capabilities, orderPlans, plainGroupByPlans));
23078
+ lines.push(...buildSelectPlan(col.query, `[subquery:${idx++}]`, capabilities, orderPlans, plainGroupByPlans, false));
23003
23079
  }
23004
23080
  }
23005
23081
  if (stmt.having) visitWhere(stmt.having);
@@ -23025,7 +23101,7 @@ function buildInsertSelectPlan(stmt, label, capabilities, orderPlans, plainGroup
23025
23101
  lines.push(` fields: ${stmt.fields.join(", ")}`);
23026
23102
  lines.push(` api: POST /k/v1/records.json\uFF08\u4EF6\u6570\u306F SELECT \u7D50\u679C\u306B\u4F9D\u5B58\u3001100 \u4EF6\u3054\u3068\u306B\u30D0\u30C3\u30C1\uFF09`);
23027
23103
  lines.push("");
23028
- lines.push(...buildSelectPlan(stmt.select, "[source SELECT]", capabilities, orderPlans, plainGroupByPlans));
23104
+ lines.push(...buildSelectPlan(stmt.select, "[source SELECT]", capabilities, orderPlans, plainGroupByPlans, false));
23029
23105
  return lines;
23030
23106
  }
23031
23107
  function buildUpdatePlan(stmt, label, capabilities, orderPlans, dmlMaxRows = 100, dmlMaxSubtableRows = DEFAULT_APPLY_MAX_SUBTABLE_ROWS, maxRecords = 1e4) {
@@ -23072,7 +23148,7 @@ function buildUpdatePlan(stmt, label, capabilities, orderPlans, dmlMaxRows = 100
23072
23148
  for (const a of stmt.assignments) {
23073
23149
  if (a.value.type === "SCALAR_SUBQUERY") {
23074
23150
  lines.push("");
23075
- lines.push(...buildSelectPlan(a.value.query, `[subquery: ${a.field}]`, capabilities, orderPlans));
23151
+ lines.push(...buildSelectPlan(a.value.query, `[subquery: ${a.field}]`, capabilities, orderPlans, void 0, false));
23076
23152
  }
23077
23153
  }
23078
23154
  return lines;
@@ -23189,7 +23265,7 @@ function buildUpsertSelectPlan(stmt, label, capabilities, orderPlans, plainGroup
23189
23265
  ` api: GET /k/v1/records.json\uFF08\u91CD\u8907\u5224\u5B9A\uFF09\u2192 POST \u307E\u305F\u306F PUT /k/v1/records.json\uFF08100 \u4EF6\u3054\u3068\u306B\u30D0\u30C3\u30C1\uFF09`,
23190
23266
  ``
23191
23267
  ];
23192
- lines.push(...buildSelectPlan(stmt.select, "[source SELECT]", capabilities, orderPlans, plainGroupByPlans));
23268
+ lines.push(...buildSelectPlan(stmt.select, "[source SELECT]", capabilities, orderPlans, plainGroupByPlans, false));
23193
23269
  return lines;
23194
23270
  }
23195
23271
  function buildReorderPlan(stmt, label) {
@@ -24493,7 +24569,8 @@ function createNodeKintoneConnection(baseUrl, tokenResolver) {
24493
24569
  const queryPart = `query=${encodeURIComponent(params.query)}`;
24494
24570
  const appPart = `app=${encodeURIComponent(String(params.app))}`;
24495
24571
  const fieldParts = params.fields.map((f) => `fields[]=${encodeURIComponent(f)}`);
24496
- const qs = [appPart, queryPart, ...fieldParts].join("&");
24572
+ const totalCountPart = params.totalCount === true ? ["totalCount=true"] : [];
24573
+ const qs = [appPart, queryPart, ...fieldParts, ...totalCountPart].join("&");
24497
24574
  if (tokenResolver.debug) {
24498
24575
  tokenResolver.log?.(
24499
24576
  `[debug] getRecords app=${params.app} query="${params.query}" fields=${params.fields.length > 0 ? params.fields.join(",") : "(all)"} auth=${tokenResolver.auth.type}`