@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/README.md +1 -0
- package/dist-cli/ksql.js +586 -31
- package/dist-mcp/ksql-mcp.js +586 -38
- package/dist-mcpb/ksql-mcp.mcpb +0 -0
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
7407
|
+
guardedWork,
|
|
7218
7408
|
new Promise((_, reject) => {
|
|
7219
|
-
timer = setTimeout(() =>
|
|
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" ?
|
|
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" ?
|
|
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" ?
|
|
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" ?
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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, {
|
|
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);
|