@rex0220/kintone-sql-tools 3.0.0 → 3.1.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
@@ -5643,7 +5643,7 @@ var KORDER_NATIVE_FIELD_TYPES = /* @__PURE__ */ new Set([
5643
5643
  "CREATOR",
5644
5644
  "MODIFIER"
5645
5645
  ]);
5646
- function planKorderNative(input) {
5646
+ function planKorder(input) {
5647
5647
  const { stmt } = input;
5648
5648
  const reasons = [];
5649
5649
  if (stmt.orderMode !== "KINTONE_NATIVE") reasons.push("KORDER_MODE_REQUIRED");
@@ -5673,29 +5673,120 @@ function planKorderNative(input) {
5673
5673
  reasons.push(`KORDER_TYPE_UNSUPPORTED(field=${name}, type=${semantics.fieldType})`);
5674
5674
  }
5675
5675
  }
5676
- if (stmt.limit === null || stmt.limit < 0 || stmt.limit > 500) {
5676
+ if (stmt.limit === null || !Number.isSafeInteger(stmt.limit) || stmt.limit < 0) {
5677
5677
  reasons.push(`KORDER_LIMIT_INVALID(limit=${String(stmt.limit)})`);
5678
5678
  }
5679
- if (stmt.limit !== null && stmt.limit > input.maxRecords) {
5680
- reasons.push(`KORDER_LIMIT_EXCEEDS_MAX_RECORDS(limit=${stmt.limit}, maxRecords=${input.maxRecords})`);
5681
- }
5682
5679
  const offset = stmt.offset ?? 0;
5683
- if (offset < 0 || offset > 1e4) reasons.push(`KORDER_OFFSET_INVALID(offset=${offset})`);
5680
+ if (!Number.isSafeInteger(offset) || offset < 0) {
5681
+ reasons.push(`KORDER_OFFSET_INVALID(offset=${offset})`);
5682
+ }
5683
+ const scanRows = stmt.limit === null ? Number.NaN : offset + stmt.limit;
5684
+ if (stmt.limit !== null && !Number.isSafeInteger(scanRows)) {
5685
+ reasons.push(`KORDER_SCAN_ROWS_INVALID(offset=${offset}, limit=${stmt.limit})`);
5686
+ }
5684
5687
  const unique = [...new Set(reasons)];
5685
5688
  if (unique.length > 0) {
5686
5689
  throw new Error(
5687
5690
  `ArgumentError: KORDER BY cannot be executed (mode=KINTONE_NATIVE; ${unique.join(", ")}). Use ORDER BY for canonical local ordering or simplify the query.`
5688
5691
  );
5689
5692
  }
5693
+ const native = stmt.limit <= 500 && offset <= 1e4 && stmt.limit <= input.maxRecords;
5694
+ if (!native && scanRows > input.maxRecords) {
5695
+ throw new Error(
5696
+ `ArgumentError: KORDER BY cannot be executed (mode=KINTONE_NATIVE; KORDER_SCAN_ROWS_EXCEEDS_MAX_RECORDS(scanRows=${scanRows}, maxRecords=${input.maxRecords})). Use ORDER BY for canonical local ordering, raise maxRecords, or reduce LIMIT/OFFSET.`
5697
+ );
5698
+ }
5690
5699
  return {
5691
- kind: "KORDER_NATIVE",
5700
+ kind: native ? "KORDER_NATIVE" : "KORDER_CURSOR",
5692
5701
  requiresCompleteInput: false,
5693
5702
  localOrderBy: false,
5694
5703
  applyLocalOffsetLimit: false,
5695
- reasonCodes: []
5704
+ reasonCodes: [],
5705
+ scanRows
5696
5706
  };
5697
5707
  }
5698
5708
 
5709
+ // src/core/errors/cursorErrors.ts
5710
+ var CursorCapacityError = class extends Error {
5711
+ constructor(host, limit, waitMs) {
5712
+ super(`CursorCapacityError: host=${host} \u306E active cursor \u4E0A\u9650 ${limit} \u306B ${waitMs}ms \u4EE5\u5185\u3067\u7A7A\u304D\u304C\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002`);
5713
+ this.name = "CursorCapacityError";
5714
+ }
5715
+ };
5716
+ var CursorCreateOutcomeUnknownError = class extends Error {
5717
+ constructor(cause) {
5718
+ super("CursorCreateOutcomeUnknownError: Create Cursor \u306E\u6210\u5426\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3002\u81EA\u52D5\u518D\u8A66\u884C\u305B\u305A\u3001\u6700\u592710\u5206+\u5B89\u5168\u4F59\u88D5\u306E\u9593\u306F\u67A0\u3092\u9694\u96E2\u3057\u307E\u3059\u3002");
5719
+ this.name = "CursorCreateOutcomeUnknownError";
5720
+ this.cause = cause;
5721
+ }
5722
+ };
5723
+ var CursorCleanupWarning = class extends Error {
5724
+ constructor(cause) {
5725
+ const detail = cause instanceof Error ? cause.message : String(cause);
5726
+ super(`CursorCleanupWarning: Cursor \u306E\u89E3\u653E\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3002\u7D50\u679C\u306F\u6709\u52B9\u3067\u3059\u304C\u3001\u6700\u592710\u5206+\u5B89\u5168\u4F59\u88D5\u306E\u9593\u306F\u67A0\u3092\u9694\u96E2\u3057\u307E\u3059\u3002\u8A73\u7D30: ${detail}`);
5727
+ this.name = "CursorCleanupWarning";
5728
+ this.cause = cause;
5729
+ }
5730
+ };
5731
+
5732
+ // src/core/optimization/korderCursorExecutor.ts
5733
+ async function executeKorderCursor(input) {
5734
+ const handle = await input.client.openCursor({
5735
+ app: input.app,
5736
+ fields: input.fields.length > 0 ? input.fields : void 0,
5737
+ query: input.query,
5738
+ size: 500
5739
+ });
5740
+ const records = [];
5741
+ let seen = 0;
5742
+ let primaryError;
5743
+ let cleanupWarning;
5744
+ try {
5745
+ if (handle.totalCount > input.offset) {
5746
+ while (records.length < input.limit) {
5747
+ const page = await handle.nextPage();
5748
+ for (const record of page.records) {
5749
+ if (seen < input.offset) seen += 1;
5750
+ else if (records.length < input.limit) records.push(record);
5751
+ else break;
5752
+ }
5753
+ if (!page.next) break;
5754
+ }
5755
+ }
5756
+ } catch (error) {
5757
+ primaryError = error;
5758
+ throw error;
5759
+ } finally {
5760
+ try {
5761
+ await handle.close();
5762
+ } catch (cleanupError) {
5763
+ if (primaryError && primaryError instanceof Error) {
5764
+ Object.defineProperty(primaryError, "cursorCleanupError", {
5765
+ value: cleanupError,
5766
+ configurable: true
5767
+ });
5768
+ } else {
5769
+ cleanupWarning = new CursorCleanupWarning(cleanupError).message;
5770
+ }
5771
+ }
5772
+ }
5773
+ return { records, cleanupWarning };
5774
+ }
5775
+
5776
+ // src/converter/korderCursorQuery.ts
5777
+ function buildKorderCursorQuery(stmt) {
5778
+ const parts = [];
5779
+ if (stmt.where) parts.push(whereToKintone(stmt.where));
5780
+ const order = stmt.orderBy.map((item) => {
5781
+ if (item.key.type !== "FIELD_NAME") {
5782
+ throw new Error("ArgumentError: KORDER cursor key must be a direct field.");
5783
+ }
5784
+ return `${item.key.name} ${item.direction === "ASC" ? "asc" : "desc"}`;
5785
+ });
5786
+ parts.push(`order by ${order.join(", ")}`);
5787
+ return parts.join(" ");
5788
+ }
5789
+
5699
5790
  // src/engine/process.ts
5700
5791
  function flatten(record, alias) {
5701
5792
  const row = {};
@@ -6836,6 +6927,15 @@ function createEmptyMetrics() {
6836
6927
  fieldCalls: 0,
6837
6928
  appsCalls: 0,
6838
6929
  processStatusCalls: 0,
6930
+ cursorCreateCalls: 0,
6931
+ cursorGetCalls: 0,
6932
+ cursorDeleteCalls: 0,
6933
+ cursorRecordsScanned: 0,
6934
+ cursorActiveCurrent: 0,
6935
+ cursorActivePeak: 0,
6936
+ cursorCleanupFailures: 0,
6937
+ cursorCreateOutcomeUnknown: 0,
6938
+ cursorQuarantinedCurrent: 0,
6839
6939
  fetchedRows: 0,
6840
6940
  elapsedMs: 0
6841
6941
  };
@@ -6848,6 +6948,48 @@ function wrapClientWithMetrics(client, metrics) {
6848
6948
  metrics.fetchedRows += res.records.length;
6849
6949
  return res;
6850
6950
  },
6951
+ openCursor: async (params) => {
6952
+ metrics.cursorCreateCalls += 1;
6953
+ let handle;
6954
+ try {
6955
+ handle = await client.openCursor(params);
6956
+ } catch (error) {
6957
+ if (error instanceof Error && error.name === "CursorCreateOutcomeUnknownError") {
6958
+ metrics.cursorCreateOutcomeUnknown += 1;
6959
+ metrics.cursorQuarantinedCurrent += 1;
6960
+ }
6961
+ throw error;
6962
+ }
6963
+ metrics.cursorActiveCurrent += 1;
6964
+ metrics.cursorActivePeak = Math.max(metrics.cursorActivePeak, metrics.cursorActiveCurrent);
6965
+ let released = false;
6966
+ const markReleased = () => {
6967
+ if (released) return;
6968
+ released = true;
6969
+ metrics.cursorActiveCurrent -= 1;
6970
+ };
6971
+ return {
6972
+ totalCount: handle.totalCount,
6973
+ nextPage: async () => {
6974
+ metrics.cursorGetCalls += 1;
6975
+ const page = await handle.nextPage();
6976
+ metrics.cursorRecordsScanned += page.records.length;
6977
+ if (!page.next) markReleased();
6978
+ return page;
6979
+ },
6980
+ close: async () => {
6981
+ if (!released) metrics.cursorDeleteCalls += 1;
6982
+ try {
6983
+ await handle.close();
6984
+ markReleased();
6985
+ } catch (error) {
6986
+ metrics.cursorCleanupFailures += 1;
6987
+ metrics.cursorQuarantinedCurrent += 1;
6988
+ throw error;
6989
+ }
6990
+ }
6991
+ };
6992
+ },
6851
6993
  postRecords: (params) => {
6852
6994
  metrics.postCalls += 1;
6853
6995
  return client.postRecords(params);
@@ -6887,6 +7029,37 @@ function wrapClientWithSearchAbort(client, collector, failClosed) {
6887
7029
  }
6888
7030
  };
6889
7031
  }
7032
+ function wrapClientWithCursorScope(client) {
7033
+ const active = /* @__PURE__ */ new Set();
7034
+ return {
7035
+ client: {
7036
+ ...client,
7037
+ openCursor: async (params) => {
7038
+ const handle = await client.openCursor(params);
7039
+ active.add(handle);
7040
+ const remove = () => active.delete(handle);
7041
+ return {
7042
+ totalCount: handle.totalCount,
7043
+ async nextPage() {
7044
+ const page = await handle.nextPage();
7045
+ if (!page.next) remove();
7046
+ return page;
7047
+ },
7048
+ async close() {
7049
+ try {
7050
+ await handle.close();
7051
+ } finally {
7052
+ remove();
7053
+ }
7054
+ }
7055
+ };
7056
+ }
7057
+ },
7058
+ closeActive: async () => {
7059
+ await Promise.all([...active].map((handle) => handle.close().catch(() => void 0)));
7060
+ }
7061
+ };
7062
+ }
6890
7063
  function isSelectLikeStatement(stmt) {
6891
7064
  return stmt.type === "SELECT" || stmt.type === "UNION" || stmt.type === "WITH";
6892
7065
  }
@@ -6937,7 +7110,13 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
6937
7110
  case "DESCRIBE":
6938
7111
  return executeDescribe(stmt, client, cacheContext);
6939
7112
  case "EXPLAIN":
6940
- return executeExplain(stmt, client, cacheContext, options.maxRecords ?? 1e4);
7113
+ return executeExplain(
7114
+ stmt,
7115
+ client,
7116
+ cacheContext,
7117
+ options.maxRecords ?? 1e4,
7118
+ options.cursorMaxActive ?? 2
7119
+ );
6941
7120
  // 一時テーブルはバッチスコープのため単文実行では拒否する(executeBatch を使う)
6942
7121
  case "CREATE_TEMP_TABLE":
6943
7122
  throw new Error("ArgumentError: CREATE TEMP TABLE requires a batch (temp tables are batch-scoped).");
@@ -7044,9 +7223,11 @@ async function executeBatch(sql, client, options = {}) {
7044
7223
  searchAbortCollector,
7045
7224
  info.statementType !== "SELECT" && info.statementType !== "UNION" && info.statementType !== "WITH"
7046
7225
  );
7226
+ const cursorScope = wrapClientWithCursorScope(statementClient);
7047
7227
  const outcome = await runWithDeadline(
7048
- executeBatchStatement(statements[i], info, statementClient, stmtOptions, cacheContext, tempTables, variables),
7049
- remaining
7228
+ executeBatchStatement(statements[i], info, cursorScope.client, stmtOptions, cacheContext, tempTables, variables),
7229
+ remaining,
7230
+ cursorScope.closeActive
7050
7231
  );
7051
7232
  if (outcome.result) {
7052
7233
  outcome.result = attachSearchAbortWarning(outcome.result, searchAbortCollector);
@@ -7204,19 +7385,47 @@ async function runSelectLike(query, client, options, cacheContext, tempTables) {
7204
7385
  }
7205
7386
  return executeQueryWithCte(query, client, options, tempTables, cacheContext, true);
7206
7387
  }
7207
- async function runWithDeadline(work, remainingMs) {
7388
+ async function runWithDeadline(work, remainingMs, onTimeout) {
7208
7389
  if (remainingMs === null) return work;
7209
7390
  if (remainingMs <= 0) {
7391
+ if (onTimeout) await onTimeout();
7210
7392
  void work.catch(() => {
7211
7393
  });
7212
7394
  throw new BatchTimeoutError();
7213
7395
  }
7214
7396
  let timer;
7397
+ let timedOut = false;
7398
+ const guardedWork = work.then(
7399
+ (value) => timedOut ? new Promise(() => void 0) : value,
7400
+ (error) => {
7401
+ if (timedOut) return new Promise(() => void 0);
7402
+ throw error;
7403
+ }
7404
+ );
7215
7405
  try {
7216
7406
  return await Promise.race([
7217
- work,
7407
+ guardedWork,
7218
7408
  new Promise((_, reject) => {
7219
- timer = setTimeout(() => reject(new BatchTimeoutError()), remainingMs);
7409
+ timer = setTimeout(() => {
7410
+ timedOut = true;
7411
+ void (async () => {
7412
+ if (onTimeout) {
7413
+ let cleanupTimer;
7414
+ try {
7415
+ await Promise.race([
7416
+ onTimeout(),
7417
+ new Promise((resolve2) => {
7418
+ cleanupTimer = setTimeout(resolve2, 5e3);
7419
+ cleanupTimer.unref?.();
7420
+ })
7421
+ ]);
7422
+ } finally {
7423
+ if (cleanupTimer) clearTimeout(cleanupTimer);
7424
+ }
7425
+ }
7426
+ reject(new BatchTimeoutError());
7427
+ })();
7428
+ }, remainingMs);
7220
7429
  })
7221
7430
  ]);
7222
7431
  } catch (e) {
@@ -7576,7 +7785,7 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
7576
7785
  const staticMode = resolveSelectMode(stmt);
7577
7786
  const mode = whereCapability.capability === "EXACT_PUSHDOWN" ? staticMode : "FULL_SCAN";
7578
7787
  const orderMeta = await buildOrderByMetaForSelect(stmt, client, cacheContext, cteCache);
7579
- const orderPlan = hasCanonicalOrder(stmt) ? (stmt.orderMode === "KINTONE_NATIVE" ? planKorderNative : planCanonicalOrder)({
7788
+ const orderPlan = hasCanonicalOrder(stmt) ? (stmt.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
7580
7789
  stmt,
7581
7790
  staticMode: mode,
7582
7791
  whereCapability: whereCapability.capability,
@@ -7590,7 +7799,7 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
7590
7799
  client,
7591
7800
  cacheContext
7592
7801
  );
7593
- const completeInputRequired = orderPlan?.kind === "CANONICAL_REST_TOP_N" || orderPlan?.kind === "KORDER_NATIVE" ? requiresCompleteInput({ ...stmt, orderBy: [] }) : requiresCompleteInput(stmt);
7802
+ const completeInputRequired = orderPlan?.kind === "CANONICAL_REST_TOP_N" || orderPlan?.kind === "KORDER_NATIVE" || orderPlan?.kind === "KORDER_CURSOR" ? requiresCompleteInput({ ...stmt, orderBy: [] }) : requiresCompleteInput(stmt);
7594
7803
  const truncateWasDisabled = completeInputRequired && options.onLimitReached === "truncate";
7595
7804
  const effectiveOptions = truncateWasDisabled ? { ...options, onLimitReached: "error" } : options;
7596
7805
  try {
@@ -7691,12 +7900,23 @@ async function executeSimpleSelect(stmt, client, options, cacheContext, orderPla
7691
7900
  const warnings = /* @__PURE__ */ new Set();
7692
7901
  const onLimit = options.onLimitReached ?? "error";
7693
7902
  const parallel = options.fetchParallel ?? 1;
7694
- const useRestWindow = stmt.orderBy.length > 0 ? orderPlan?.kind === "CANONICAL_REST_TOP_N" || orderPlan?.kind === "KORDER_NATIVE" : stmt.limit !== null && stmt.limit <= 500;
7903
+ const useRestWindow = stmt.orderBy.length > 0 ? orderPlan?.kind === "CANONICAL_REST_TOP_N" || orderPlan?.kind === "KORDER_NATIVE" || orderPlan?.kind === "KORDER_CURSOR" : stmt.limit !== null && stmt.limit <= 500;
7695
7904
  const needed = stmt.limit === null ? null : (stmt.offset ?? 0) + stmt.limit;
7696
7905
  const stopAfter = stmt.orderBy.length === 0 && needed !== null && needed <= maxRecords && !whereHasKlike(stmt.where) ? needed : void 0;
7697
7906
  let records;
7698
- if (orderPlan?.kind === "KORDER_NATIVE" && stmt.limit === 0) {
7907
+ if ((orderPlan?.kind === "KORDER_NATIVE" || orderPlan?.kind === "KORDER_CURSOR") && stmt.limit === 0) {
7699
7908
  records = [];
7909
+ } else if (orderPlan?.kind === "KORDER_CURSOR") {
7910
+ const cursorResult = await executeKorderCursor({
7911
+ client,
7912
+ app: params.app,
7913
+ fields: params.fields,
7914
+ query: buildKorderCursorQuery(stmt),
7915
+ offset: stmt.offset ?? 0,
7916
+ limit: stmt.limit
7917
+ });
7918
+ records = cursorResult.records;
7919
+ if (cursorResult.cleanupWarning) warnings.add(cursorResult.cleanupWarning);
7700
7920
  } else if (useRestWindow) {
7701
7921
  const res = await client.getRecords({
7702
7922
  app: params.app,
@@ -8488,7 +8708,7 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
8488
8708
  }
8489
8709
  const orderMeta = await buildOrderByMetaForSelect(stmt, client, cacheContext, cteCache);
8490
8710
  if (hasCanonicalOrder(stmt)) {
8491
- (stmt.orderMode === "KINTONE_NATIVE" ? planKorderNative : planCanonicalOrder)({
8711
+ (stmt.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
8492
8712
  stmt,
8493
8713
  staticMode: "FULL_SCAN",
8494
8714
  whereCapability: whereCapability.capability,
@@ -10265,7 +10485,7 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
10265
10485
  const hasUnmaterializedSource = [select.from, ...select.joins.map((join2) => join2.table)].some((table) => table.cteName !== null);
10266
10486
  if (hasCanonicalOrder(select) && !hasUnmaterializedSource) {
10267
10487
  const mode = capability.capability === "EXACT_PUSHDOWN" ? resolveSelectMode(select) : "FULL_SCAN";
10268
- orderPlans.set(select, (select.orderMode === "KINTONE_NATIVE" ? planKorderNative : planCanonicalOrder)({
10488
+ orderPlans.set(select, (select.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
10269
10489
  stmt: select,
10270
10490
  staticMode: mode,
10271
10491
  whereCapability: capability.capability,
@@ -10295,7 +10515,7 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
10295
10515
  capabilities.set(inlined, capability);
10296
10516
  if (hasCanonicalOrder(inlined)) {
10297
10517
  const meta = await buildOrderByMetaForSelect(inlined, tracedClient, cacheContext);
10298
- orderPlans.set(inlined, (inlined.orderMode === "KINTONE_NATIVE" ? planKorderNative : planCanonicalOrder)({
10518
+ orderPlans.set(inlined, (inlined.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
10299
10519
  stmt: inlined,
10300
10520
  staticMode: capability.capability === "EXACT_PUSHDOWN" ? resolveSelectMode(inlined) : "FULL_SCAN",
10301
10521
  whereCapability: capability.capability,
@@ -10313,7 +10533,7 @@ function explainMetadataLines(analysis) {
10313
10533
  ...[...analysis.processStatusApps].sort((a, b) => a - b).map((appId) => ` metadata API: process status APP${appId}`)
10314
10534
  ];
10315
10535
  }
10316
- async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4) {
10536
+ async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4, cursorMaxActive = 2) {
10317
10537
  const statements = parseSqlBatch(sql);
10318
10538
  const analysis = analyzeBatch(statements);
10319
10539
  validateDeclaredBatchVariables(statements, injectedVariables);
@@ -10324,12 +10544,12 @@ async function buildBatchExplainPlans(sql, client, injectedVariables, cacheConte
10324
10544
  const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr: resolveVariableRefs(stmt.expr, variables) } : stmt : resolveVariableRefs(stmt, variables);
10325
10545
  validateKlikeStatement(planStmt);
10326
10546
  const whereAnalysis = await buildExplainWhereAnalysis(planStmt, client, cacheContext, maxRecords);
10327
- const statementPlan = buildBatchStatementPlan(
10547
+ const statementPlan = addCursorConcurrency(buildBatchStatementPlan(
10328
10548
  planStmt,
10329
10549
  analysis.statements[i],
10330
10550
  whereAnalysis.capabilities,
10331
10551
  whereAnalysis.orderPlans
10332
- );
10552
+ ), cursorMaxActive);
10333
10553
  const metadataPlan = explainMetadataLines(whereAnalysis);
10334
10554
  plans.push({
10335
10555
  index: i,
@@ -10435,11 +10655,14 @@ function buildPlanForBatchQuery(query, info, capabilities, orderPlans) {
10435
10655
  lines.push(" note: \u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u3078\u306E WHERE \u30D7\u30C3\u30B7\u30E5\u30C0\u30A6\u30F3\u306F\u884C\u308F\u308C\u306A\u3044");
10436
10656
  return lines;
10437
10657
  }
10438
- async function executeExplain(stmt, client, cacheContext, maxRecords) {
10658
+ async function executeExplain(stmt, client, cacheContext, maxRecords, cursorMaxActive) {
10439
10659
  const analysis = await buildExplainWhereAnalysis(stmt.query, client, cacheContext, maxRecords);
10440
10660
  const lines = [
10441
10661
  ...explainMetadataLines(analysis),
10442
- ...buildExplainPlan(stmt.query, void 0, analysis.capabilities, analysis.orderPlans)
10662
+ ...addCursorConcurrency(
10663
+ buildExplainPlan(stmt.query, void 0, analysis.capabilities, analysis.orderPlans),
10664
+ cursorMaxActive
10665
+ )
10443
10666
  ];
10444
10667
  return {
10445
10668
  type: "SELECT",
@@ -10448,6 +10671,17 @@ async function executeExplain(stmt, client, cacheContext, maxRecords) {
10448
10671
  rowCount: lines.length
10449
10672
  };
10450
10673
  }
10674
+ function addCursorConcurrency(lines, cursorMaxActive) {
10675
+ const result = [];
10676
+ for (const line of lines) {
10677
+ result.push(line);
10678
+ if (line.trim() === "cursor page size: 500") {
10679
+ const indent = line.match(/^\s*/)?.[0] ?? "";
10680
+ result.push(`${indent}cursor concurrency: ${cursorMaxActive} per domain (process-local)`);
10681
+ }
10682
+ }
10683
+ return result;
10684
+ }
10451
10685
  function buildExplainPlan(query, label, capabilities, orderPlans) {
10452
10686
  if (query.type === "UNION") return buildUnionPlan(query, capabilities, orderPlans);
10453
10687
  if (query.type === "WITH") return buildWithPlan(query, capabilities, orderPlans);
@@ -10477,6 +10711,11 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans) {
10477
10711
  if (orderPlan.kind === "KORDER_NATIVE") {
10478
10712
  lines.push(" order semantics: kintone native (not kSQL canonical)");
10479
10713
  lines.push(" REST execution: single GET");
10714
+ } else if (orderPlan.kind === "KORDER_CURSOR") {
10715
+ lines.push(" order semantics: kintone native (not kSQL canonical)");
10716
+ lines.push(" fetch API: POST/GET/DELETE records/cursor.json");
10717
+ lines.push(" cursor page size: 500");
10718
+ lines.push(` scan rows: ${orderPlan.scanRows}`);
10480
10719
  }
10481
10720
  }
10482
10721
  if (orderPlan?.requiresCompleteInput ?? requiresCompleteInput(stmt)) {
@@ -10488,7 +10727,8 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans) {
10488
10727
  if (mode === "SIMPLE") {
10489
10728
  const params = selectToKintoneParams(orderPlan?.kind === "CANONICAL_REST_TOP_N" ? withCanonicalRestTie(stmt) : stmt);
10490
10729
  lines.push(` app: APP${stmt.from.appId} (${stmt.from.appId})`);
10491
- lines.push(` kintone query: ${params.query || "(\u306A\u3057)"}`);
10730
+ const displayedQuery = orderPlan?.kind === "KORDER_CURSOR" ? buildKorderCursorQuery(stmt) : params.query;
10731
+ lines.push(` kintone query: ${displayedQuery || "(\u306A\u3057)"}`);
10492
10732
  lines.push(` fields: ${params.fields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : params.fields.join(", ")}`);
10493
10733
  } else {
10494
10734
  const pushdownPlan = buildKlikePushdownPlan(stmt);
@@ -11056,6 +11296,12 @@ function validateKsqlConfig(config) {
11056
11296
  }
11057
11297
  const logicalApps = normalizeLogicalApps(profileName, profile.logicalApps);
11058
11298
  if (logicalApps !== void 0) profile.logicalApps = logicalApps;
11299
+ if (profile.query?.cursorMaxActive !== void 0) {
11300
+ const value = profile.query.cursorMaxActive;
11301
+ if (!Number.isSafeInteger(value) || value < 1 || value > 5) {
11302
+ throw argumentError(`query.cursorMaxActive for profile "${profileName}" must be an integer from 1 to 5.`);
11303
+ }
11304
+ }
11059
11305
  }
11060
11306
  return config;
11061
11307
  }
@@ -11200,6 +11446,10 @@ var RequestGate = class {
11200
11446
  async runMutation(fn) {
11201
11447
  return this.withSlot(fn);
11202
11448
  }
11449
+ /** Cursor Create/Get/Delete: セマフォのみ。GETでも位置を進めるため再試行しない。 */
11450
+ async runCursorStep(fn) {
11451
+ return this.withSlot(fn);
11452
+ }
11203
11453
  async withSlot(fn) {
11204
11454
  await this.acquire();
11205
11455
  try {
@@ -11231,6 +11481,14 @@ var RequestGate = class {
11231
11481
  function withRequestGate(client, gate) {
11232
11482
  return {
11233
11483
  getRecords: (params) => gate.runReadOnly(() => client.getRecords(params)),
11484
+ openCursor: async (params) => {
11485
+ const handle = await gate.runCursorStep(() => client.openCursor(params));
11486
+ return {
11487
+ totalCount: handle.totalCount,
11488
+ nextPage: () => gate.runCursorStep(() => handle.nextPage()),
11489
+ close: () => gate.runCursorStep(() => handle.close())
11490
+ };
11491
+ },
11234
11492
  getApps: () => gate.runReadOnly(() => client.getApps()),
11235
11493
  getFields: (appId) => gate.runReadOnly(() => client.getFields(appId)),
11236
11494
  getProcessStatuses: (appId) => gate.runReadOnly(() => client.getProcessStatuses(appId)),
@@ -11345,7 +11603,218 @@ function normalizeProcessStatusStates(states) {
11345
11603
  });
11346
11604
  }
11347
11605
 
11606
+ // src/api/kintoneCursor.ts
11607
+ function isAlreadyReleasedCursorError(error) {
11608
+ const shaped = error;
11609
+ return shaped?.status === 404 && shaped.code === "GAIA_CN01";
11610
+ }
11611
+ async function deleteCursorWithConfirmation(deleteCursor, sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)), isAlreadyReleased = isAlreadyReleasedCursorError) {
11612
+ try {
11613
+ await deleteCursor();
11614
+ return;
11615
+ } catch (firstError) {
11616
+ if (isAlreadyReleased(firstError)) return;
11617
+ }
11618
+ await sleep(250);
11619
+ try {
11620
+ await deleteCursor();
11621
+ } catch (confirmationError) {
11622
+ if (isAlreadyReleased(confirmationError)) return;
11623
+ throw confirmationError;
11624
+ }
11625
+ }
11626
+ async function withTimeout(promise, timeoutMs) {
11627
+ let timer;
11628
+ const timeout = new Promise((_resolve, reject) => {
11629
+ timer = setTimeout(() => reject(new Error(`CursorCleanupTimeoutError: cleanup exceeded ${timeoutMs}ms.`)), timeoutMs);
11630
+ timer.unref?.();
11631
+ });
11632
+ try {
11633
+ return await Promise.race([promise, timeout]);
11634
+ } finally {
11635
+ if (timer) clearTimeout(timer);
11636
+ }
11637
+ }
11638
+ function createKintoneCursorHandle(totalCount, operations) {
11639
+ let released = false;
11640
+ let closing = false;
11641
+ let pageTail = Promise.resolve();
11642
+ let closePromise = null;
11643
+ const nextPage = () => {
11644
+ if (closing || released) return Promise.resolve({ records: [], next: false });
11645
+ const result = pageTail.then(async () => {
11646
+ if (closing || released) return { records: [], next: false };
11647
+ const page = await operations.get();
11648
+ if (!page.next) {
11649
+ released = true;
11650
+ operations.onReleased?.();
11651
+ }
11652
+ return page;
11653
+ });
11654
+ pageTail = result.then(() => void 0, () => void 0);
11655
+ return result;
11656
+ };
11657
+ const close = () => {
11658
+ if (released) return Promise.resolve();
11659
+ if (closePromise) return closePromise;
11660
+ closing = true;
11661
+ closePromise = pageTail.then(async () => {
11662
+ if (released) return;
11663
+ try {
11664
+ await withTimeout(
11665
+ deleteCursorWithConfirmation(
11666
+ operations.delete,
11667
+ operations.sleep,
11668
+ operations.isAlreadyReleasedError
11669
+ ),
11670
+ operations.cleanupTimeoutMs ?? 5e3
11671
+ );
11672
+ released = true;
11673
+ operations.onReleased?.();
11674
+ } catch (error) {
11675
+ operations.onReleaseUnknown?.();
11676
+ throw error;
11677
+ }
11678
+ });
11679
+ return closePromise;
11680
+ };
11681
+ return { totalCount, nextPage, close };
11682
+ }
11683
+
11684
+ // src/api/cursorLeaseManager.ts
11685
+ var DEFAULT_MAX_ACTIVE = 2;
11686
+ var MAX_ACTIVE = 5;
11687
+ var DEFAULT_WAIT_MS = 3e4;
11688
+ var DEFAULT_QUARANTINE_MS = 10 * 6e4 + 3e4;
11689
+ var CursorLeaseManager = class {
11690
+ constructor(host, options = {}) {
11691
+ this.host = host;
11692
+ this.active = 0;
11693
+ this.peak = 0;
11694
+ this.quarantined = 0;
11695
+ this.waiters = [];
11696
+ this.createTail = Promise.resolve();
11697
+ const maxActive = options.maxActive ?? DEFAULT_MAX_ACTIVE;
11698
+ if (!Number.isSafeInteger(maxActive) || maxActive < 1 || maxActive > MAX_ACTIVE) {
11699
+ throw new Error(`ArgumentError: cursorMaxActive must be an integer from 1 to ${MAX_ACTIVE}.`);
11700
+ }
11701
+ this.maxActive = maxActive;
11702
+ this.waitTimeoutMs = options.waitTimeoutMs ?? DEFAULT_WAIT_MS;
11703
+ this.quarantineMs = options.quarantineMs ?? DEFAULT_QUARANTINE_MS;
11704
+ }
11705
+ acquire() {
11706
+ if (this.active < this.maxActive) {
11707
+ this.active += 1;
11708
+ this.peak = Math.max(this.peak, this.active);
11709
+ return Promise.resolve(this.makeLease());
11710
+ }
11711
+ return new Promise((resolve2, reject) => {
11712
+ const waiter = {};
11713
+ waiter.resolve = resolve2;
11714
+ waiter.reject = reject;
11715
+ waiter.timer = setTimeout(() => {
11716
+ const index = this.waiters.indexOf(waiter);
11717
+ if (index >= 0) this.waiters.splice(index, 1);
11718
+ reject(new CursorCapacityError(this.host, this.maxActive, this.waitTimeoutMs));
11719
+ }, this.waitTimeoutMs);
11720
+ waiter.timer.unref?.();
11721
+ this.waiters.push(waiter);
11722
+ });
11723
+ }
11724
+ /**
11725
+ * 同一hostを共有する後続surfaceの設定を反映する。
11726
+ * 縮小時は既存leaseを強制終了せず、activeが新上限を下回るまで新規取得だけを止める。
11727
+ */
11728
+ setMaxActive(maxActive) {
11729
+ this.validateMaxActive(maxActive);
11730
+ if (this.maxActive === maxActive) return;
11731
+ this.maxActive = maxActive;
11732
+ this.dispatchWaiters();
11733
+ }
11734
+ async runCreate(fn) {
11735
+ const previous = this.createTail;
11736
+ let unlock;
11737
+ this.createTail = new Promise((resolve2) => {
11738
+ unlock = resolve2;
11739
+ });
11740
+ await previous;
11741
+ try {
11742
+ return await fn();
11743
+ } finally {
11744
+ unlock();
11745
+ }
11746
+ }
11747
+ snapshot() {
11748
+ return {
11749
+ active: this.active,
11750
+ peak: this.peak,
11751
+ quarantined: this.quarantined,
11752
+ waiting: this.waiters.length,
11753
+ limit: this.maxActive
11754
+ };
11755
+ }
11756
+ makeLease() {
11757
+ let done = false;
11758
+ return {
11759
+ release: () => {
11760
+ if (done) return;
11761
+ done = true;
11762
+ this.returnPermit();
11763
+ },
11764
+ quarantine: (durationMs = this.quarantineMs) => {
11765
+ if (done) return;
11766
+ done = true;
11767
+ this.quarantined += 1;
11768
+ const timer = setTimeout(() => {
11769
+ this.quarantined -= 1;
11770
+ this.returnPermit();
11771
+ }, durationMs);
11772
+ timer.unref?.();
11773
+ }
11774
+ };
11775
+ }
11776
+ returnPermit() {
11777
+ this.active -= 1;
11778
+ this.dispatchWaiters();
11779
+ }
11780
+ dispatchWaiters() {
11781
+ while (this.active < this.maxActive) {
11782
+ const waiter = this.waiters.shift();
11783
+ if (!waiter) return;
11784
+ clearTimeout(waiter.timer);
11785
+ this.active += 1;
11786
+ this.peak = Math.max(this.peak, this.active);
11787
+ waiter.resolve(this.makeLease());
11788
+ }
11789
+ }
11790
+ validateMaxActive(maxActive) {
11791
+ if (!Number.isSafeInteger(maxActive) || maxActive < 1 || maxActive > MAX_ACTIVE) {
11792
+ throw new Error(`ArgumentError: cursorMaxActive must be an integer from 1 to ${MAX_ACTIVE}.`);
11793
+ }
11794
+ }
11795
+ };
11796
+ var managers = /* @__PURE__ */ new Map();
11797
+ function getCursorLeaseManager(host, maxActive = DEFAULT_MAX_ACTIVE) {
11798
+ const key = host.toLowerCase();
11799
+ let manager = managers.get(key);
11800
+ if (!manager) {
11801
+ manager = new CursorLeaseManager(key, { maxActive });
11802
+ managers.set(key, manager);
11803
+ } else {
11804
+ manager.setMaxActive(maxActive);
11805
+ }
11806
+ return manager;
11807
+ }
11808
+
11348
11809
  // src/cli/nodeKintoneClient.ts
11810
+ var KintoneApiError = class extends Error {
11811
+ constructor(status, code, bodyText) {
11812
+ super(`kintone API error ${status}: ${bodyText}`);
11813
+ this.status = status;
11814
+ this.code = code;
11815
+ this.name = "KintoneApiError";
11816
+ }
11817
+ };
11349
11818
  var SEARCH_ABORTED_HEADER_VALUE = "Filter aborted because of too many search results";
11350
11819
  function createNodeKintoneClient(baseUrl, tokenResolver) {
11351
11820
  const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
@@ -11393,7 +11862,13 @@ function createNodeKintoneClient(baseUrl, tokenResolver) {
11393
11862
  if (tokenResolver.debug) {
11394
11863
  tokenResolver.log?.(`[debug] response status=${res.status} body=${bodyText}`);
11395
11864
  }
11396
- throw new Error(`kintone API error ${res.status}: ${bodyText}`);
11865
+ let code;
11866
+ try {
11867
+ const body = JSON.parse(bodyText);
11868
+ if (typeof body.code === "string") code = body.code;
11869
+ } catch {
11870
+ }
11871
+ throw new KintoneApiError(res.status, code, bodyText);
11397
11872
  }
11398
11873
  if (tokenResolver.debug) {
11399
11874
  tokenResolver.log?.(`[debug] response status=${res.status}`);
@@ -11460,6 +11935,48 @@ function createNodeKintoneClient(baseUrl, tokenResolver) {
11460
11935
  return response.searchAborted ? { ...response.body, searchAborted: true } : response.body;
11461
11936
  }
11462
11937
  },
11938
+ async openCursor(params) {
11939
+ const manager = getCursorLeaseManager(new URL(normalizedBaseUrl).host, tokenResolver.cursorMaxActive);
11940
+ const lease = await manager.acquire();
11941
+ let created;
11942
+ try {
11943
+ created = await manager.runCreate(() => requestJson(
11944
+ `${apiBasePath}/records/cursor.json`,
11945
+ {
11946
+ method: "POST",
11947
+ body: JSON.stringify({
11948
+ app: params.app,
11949
+ query: params.query,
11950
+ size: params.size,
11951
+ fields: params.fields && params.fields.length > 0 ? params.fields : void 0
11952
+ })
11953
+ },
11954
+ params.app
11955
+ ));
11956
+ } catch (error) {
11957
+ if (error instanceof KintoneApiError) {
11958
+ lease.release();
11959
+ throw error;
11960
+ }
11961
+ lease.quarantine();
11962
+ throw new CursorCreateOutcomeUnknownError(error);
11963
+ }
11964
+ const cursorId = created.id;
11965
+ return createKintoneCursorHandle(Number(created.totalCount), {
11966
+ get: () => requestJson(
11967
+ `${apiBasePath}/records/cursor.json?id=${encodeURIComponent(cursorId)}`,
11968
+ { method: "GET" },
11969
+ params.app
11970
+ ),
11971
+ delete: () => requestJson(
11972
+ `${apiBasePath}/records/cursor.json`,
11973
+ { method: "DELETE", body: JSON.stringify({ id: cursorId }) },
11974
+ params.app
11975
+ ),
11976
+ onReleased: () => lease.release(),
11977
+ onReleaseUnknown: () => lease.quarantine()
11978
+ });
11979
+ },
11463
11980
  async postRecords(_params) {
11464
11981
  const res = await requestJson(
11465
11982
  `${apiBasePath}/records.json`,
@@ -12017,6 +12534,7 @@ Options:
12017
12534
  --timeout <ms> Request timeout in milliseconds (default: 30000)
12018
12535
  --max-concurrent <n> Max concurrent kintone requests: 1-50 (default: 10)
12019
12536
  (process-wide; fixed at first resolution; KSQL_MAX_CONCURRENT wins)
12537
+ --cursor-max-active <n> Max active cursors per host: 1-5 (default: 2; KSQL_CURSOR_MAX_ACTIVE wins)
12020
12538
  --retry <n> GET retry count: 0-10, 0 disables (default: 3; KSQL_RETRY wins)
12021
12539
  --retry-base-delay <ms> GET retry backoff base delay (default: 500)
12022
12540
  --retry-max-delay <ms> GET retry backoff max delay (default: 8000)
@@ -12097,6 +12615,7 @@ function parseArgs(argv) {
12097
12615
  continueOnError: false,
12098
12616
  dmlMaxRows: null,
12099
12617
  maxConcurrent: null,
12618
+ cursorMaxActive: null,
12100
12619
  retry: null,
12101
12620
  retryBaseDelay: null,
12102
12621
  retryMaxDelay: null,
@@ -12353,6 +12872,13 @@ function parseArgs(argv) {
12353
12872
  i++;
12354
12873
  continue;
12355
12874
  }
12875
+ if (a === "--cursor-max-active") {
12876
+ const n = Number(v);
12877
+ if (!Number.isInteger(n) || n < 1 || n > 5) throw new Error("ArgumentError: --cursor-max-active must be an integer between 1 and 5.");
12878
+ out.cursorMaxActive = n;
12879
+ i++;
12880
+ continue;
12881
+ }
12356
12882
  if (a === "--retry") {
12357
12883
  const n = Number(v);
12358
12884
  if (!Number.isInteger(n) || n < 0 || n > 10) throw new Error("ArgumentError: --retry must be an integer between 0 and 10 (0 disables retry).");
@@ -12648,6 +13174,7 @@ function createDryRunClient() {
12648
13174
  };
12649
13175
  return {
12650
13176
  getRecords: notUsed,
13177
+ openCursor: notUsed,
12651
13178
  postRecords: notUsed,
12652
13179
  putRecords: notUsed,
12653
13180
  deleteRecords: notUsed,
@@ -12790,6 +13317,7 @@ function buildReplExecArgv(base, sql, dryRun, format) {
12790
13317
  pushOpt(argv, "--attachment-format", base.attachmentFormat);
12791
13318
  pushOpt(argv, "--dml-max-rows", base.dmlMaxRows);
12792
13319
  pushOpt(argv, "--max-concurrent", base.maxConcurrent);
13320
+ pushOpt(argv, "--cursor-max-active", base.cursorMaxActive);
12793
13321
  pushOpt(argv, "--retry", base.retry);
12794
13322
  pushOpt(argv, "--retry-base-delay", base.retryBaseDelay);
12795
13323
  pushOpt(argv, "--retry-max-delay", base.retryMaxDelay);
@@ -13357,6 +13885,11 @@ async function run() {
13357
13885
  const onLimit = args.onLimit ?? envOnLimit2("KSQL_ON_LIMIT") ?? profile.query?.onLimit ?? "error";
13358
13886
  const timeout = args.timeout ?? envInt2("KSQL_TIMEOUT") ?? profile.query?.timeout ?? 3e4;
13359
13887
  const tempTableMaxRows = args.tempTableMaxRows ?? envInt2("KSQL_TEMP_TABLE_MAX_ROWS") ?? profile.query?.tempTableMaxRows ?? void 0;
13888
+ const cursorMaxActive = args.cursorMaxActive ?? envInt2("KSQL_CURSOR_MAX_ACTIVE") ?? profile.query?.cursorMaxActive ?? 2;
13889
+ if (!Number.isSafeInteger(cursorMaxActive) || cursorMaxActive < 1 || cursorMaxActive > 5) {
13890
+ process.stderr.write("ArgumentError: cursorMaxActive must be an integer from 1 to 5.\n");
13891
+ return 2;
13892
+ }
13360
13893
  if (!Number.isInteger(fetchParallel) || fetchParallel < 1 || fetchParallel > 10) {
13361
13894
  process.stderr.write("ArgumentError: fetch-parallel must be an integer between 1 and 10.\n");
13362
13895
  return 2;
@@ -13487,6 +14020,7 @@ async function run() {
13487
14020
  }
13488
14021
  profileClientMap.set(pName, createNodeKintoneClient(baseUrl, {
13489
14022
  guestSpaceId,
14023
+ cursorMaxActive,
13490
14024
  timeoutMs: timeout,
13491
14025
  debug,
13492
14026
  debugHeaders,
@@ -13519,6 +14053,7 @@ async function run() {
13519
14053
  missingAppProfiles.push(...resolvedTokens.missing);
13520
14054
  profileClientMap.set(pName, createNodeKintoneClient(baseUrl, {
13521
14055
  guestSpaceId,
14056
+ cursorMaxActive,
13522
14057
  timeoutMs: timeout,
13523
14058
  debug,
13524
14059
  debugHeaders,
@@ -13630,6 +14165,12 @@ async function run() {
13630
14165
  if (!routed) throw new Error(`AuthError: profile "${pName}" is not resolved for APP${params.app}.`);
13631
14166
  return routed.getRecords({ ...params, app: binding.appId });
13632
14167
  },
14168
+ openCursor: (params) => {
14169
+ const binding = appBindingByMappedApp.get(params.app) ?? { appId: params.app, profile: profileName.toLowerCase() };
14170
+ const routed = profileClientMap.get(binding.profile);
14171
+ if (!routed) throw new Error(`AuthError: profile "${binding.profile}" is not resolved for APP${params.app}.`);
14172
+ return routed.openCursor({ ...params, app: binding.appId });
14173
+ },
13633
14174
  postRecords: (params) => {
13634
14175
  const binding = appBindingByMappedApp.get(params.app) ?? { appId: params.app, profile: profileName.toLowerCase() };
13635
14176
  const pName = binding.profile;
@@ -13678,7 +14219,14 @@ async function run() {
13678
14219
  }
13679
14220
  if (isBatchSql && args.dryRun) {
13680
14221
  try {
13681
- const plans = await buildBatchExplainPlans(sql, client, args.variables, cacheContext, maxRecords);
14222
+ const plans = await buildBatchExplainPlans(
14223
+ sql,
14224
+ client,
14225
+ args.variables,
14226
+ cacheContext,
14227
+ maxRecords,
14228
+ cursorMaxActive
14229
+ );
13682
14230
  const out = [];
13683
14231
  const restoredStatements = sqlDiagnosticContext ? restoreSqlDiagnosticValue(plans.statements, sqlDiagnosticContext.appBindingByMappedApp) : plans.statements;
13684
14232
  restoredStatements.forEach((p) => {
@@ -13742,6 +14290,7 @@ query=${label}`);
13742
14290
  continueOnError: args.continueOnError,
13743
14291
  tempTableMaxRows,
13744
14292
  timeoutMs: timeout,
14293
+ cursorMaxActive,
13745
14294
  variables: args.variables,
13746
14295
  confirm: batchContainsDml ? async (count, operation) => {
13747
14296
  if (count > dmlMaxRows) {
@@ -13752,12 +14301,18 @@ query=${label}`);
13752
14301
  });
13753
14302
  return writeBatchOutput(batchResult, { format, noHeader, pretty, displayOptions, outputPath, quiet });
13754
14303
  }
13755
- let result = args.dryRun ? await execute(`EXPLAIN ${sql}`, client, { maxRecords, onLimitReached: onLimit, cacheContext }) : await execute(sql, client, {
14304
+ let result = args.dryRun ? await execute(`EXPLAIN ${sql}`, client, {
14305
+ maxRecords,
14306
+ onLimitReached: onLimit,
14307
+ cacheContext,
14308
+ cursorMaxActive
14309
+ }) : await execute(sql, client, {
13756
14310
  maxRecords,
13757
14311
  fetchParallel,
13758
14312
  onLimitReached: effectiveOnLimit,
13759
14313
  confirm: isDmlStatement ? confirm : void 0,
13760
- cacheContext
14314
+ cacheContext,
14315
+ cursorMaxActive
13761
14316
  });
13762
14317
  if (args.dryRun && sqlDiagnosticContext) {
13763
14318
  result = restoreSqlDiagnosticValue(result, sqlDiagnosticContext.appBindingByMappedApp);