@rex0220/kintone-sql-tools 3.0.0 → 3.2.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
@@ -117,6 +117,7 @@ var KEYWORDS = /* @__PURE__ */ new Map([
117
117
  ["LTRIM", "LTRIM" /* LTRIM */],
118
118
  ["RTRIM", "RTRIM" /* RTRIM */],
119
119
  ["LENGTH", "LENGTH" /* LENGTH */],
120
+ ["LENGTH_CHAR", "LENGTH_CHAR" /* LENGTH_CHAR */],
120
121
  ["SUBSTRING", "SUBSTRING" /* SUBSTRING */],
121
122
  ["SUBSTR", "SUBSTR" /* SUBSTR */],
122
123
  ["CONCAT", "CONCAT" /* CONCAT */],
@@ -129,6 +130,7 @@ var KEYWORDS = /* @__PURE__ */ new Map([
129
130
  ["LEAST", "LEAST" /* LEAST */],
130
131
  ["LPAD", "LPAD" /* LPAD */],
131
132
  ["RPAD", "RPAD" /* RPAD */],
133
+ ["TRANSLATE", "TRANSLATE" /* TRANSLATE */],
132
134
  ["CAST", "CAST" /* CAST */],
133
135
  ["CONVERT", "CONVERT" /* CONVERT */],
134
136
  ["FORMAT", "FORMAT" /* FORMAT */],
@@ -488,10 +490,12 @@ var FUNC_CALL_PREFIX_KINDS = /* @__PURE__ */ new Set([
488
490
  "LTRIM" /* LTRIM */,
489
491
  "RTRIM" /* RTRIM */,
490
492
  "LENGTH" /* LENGTH */,
493
+ "LENGTH_CHAR" /* LENGTH_CHAR */,
491
494
  "SUBSTRING" /* SUBSTRING */,
492
495
  "SUBSTR" /* SUBSTR */,
493
496
  "CONCAT" /* CONCAT */,
494
497
  "REPLACE" /* REPLACE */,
498
+ "TRANSLATE" /* TRANSLATE */,
495
499
  "COALESCE" /* COALESCE */,
496
500
  "NULLIF" /* NULLIF */,
497
501
  "ISNULL" /* ISNULL */,
@@ -543,6 +547,7 @@ var ParseError = class extends Error {
543
547
  var Parser = class {
544
548
  constructor(tokens) {
545
549
  this.tokens = tokens;
550
+ this.allowUnaryPlusNumber = false;
546
551
  this.pos = 0;
547
552
  /** WITH 句で定義された CTE 名のセット(parseTableRef で参照) */
548
553
  this.cteNames = /* @__PURE__ */ new Set();
@@ -1311,8 +1316,16 @@ var Parser = class {
1311
1316
  this.expect(")" /* RPAREN */);
1312
1317
  return expr;
1313
1318
  }
1319
+ if (this.allowUnaryPlusNumber && this.peek().kind === "+" /* PLUS */) {
1320
+ this.advance();
1321
+ const number = this.expect("NUMBER" /* NUMBER */, "\u5358\u9805 + \u306E\u76F4\u5F8C\u306B\u306F\u6570\u5024\u30EA\u30C6\u30E9\u30EB\u304C\u5FC5\u8981\u3067\u3059");
1322
+ return { type: "NUMBER", value: Number(number.value) };
1323
+ }
1314
1324
  if (this.peek().kind === "-" /* MINUS */) {
1315
1325
  this.advance();
1326
+ if (this.peek().kind === "-" /* MINUS */ || this.peek().kind === "+" /* PLUS */) {
1327
+ throw new ParseError("\u5358\u9805\u7B26\u53F7\u3092\u91CD\u306D\u3066\u6307\u5B9A\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093", this.peek());
1328
+ }
1316
1329
  const operand = this.parseArithPrimary();
1317
1330
  if (operand.type === "NUMBER") return { type: "NUMBER", value: -operand.value };
1318
1331
  return { type: "ARITH", left: { type: "NUMBER", value: 0 }, op: "-", right: operand };
@@ -1422,10 +1435,12 @@ var Parser = class {
1422
1435
  ["LTRIM" /* LTRIM */]: "LTRIM",
1423
1436
  ["RTRIM" /* RTRIM */]: "RTRIM",
1424
1437
  ["LENGTH" /* LENGTH */]: "LENGTH",
1438
+ ["LENGTH_CHAR" /* LENGTH_CHAR */]: "LENGTH_CHAR",
1425
1439
  ["SUBSTRING" /* SUBSTRING */]: "SUBSTRING",
1426
1440
  ["SUBSTR" /* SUBSTR */]: "SUBSTRING",
1427
1441
  ["CONCAT" /* CONCAT */]: "CONCAT",
1428
1442
  ["REPLACE" /* REPLACE */]: "REPLACE",
1443
+ ["TRANSLATE" /* TRANSLATE */]: "TRANSLATE",
1429
1444
  ["COALESCE" /* COALESCE */]: "COALESCE",
1430
1445
  ["NULLIF" /* NULLIF */]: "NULLIF",
1431
1446
  ["ISNULL" /* ISNULL */]: "ISNULL",
@@ -2186,6 +2201,11 @@ var Parser = class {
2186
2201
  } else if (this.peek().kind === "IF" /* IF */) {
2187
2202
  const expr = this.parseIfExpr();
2188
2203
  row.push({ type: "CASE_VALUE", expr });
2204
+ } else if (this.peek().kind === "-" /* MINUS */ || this.peek().kind === "+" /* PLUS */) {
2205
+ const sign = this.advance();
2206
+ const number = this.expect("NUMBER" /* NUMBER */, "INSERT \u306E\u5358\u9805\u7B26\u53F7\u306E\u76F4\u5F8C\u306B\u306F\u6570\u5024\u30EA\u30C6\u30E9\u30EB\u304C\u5FC5\u8981\u3067\u3059");
2207
+ const value = Number(number.value);
2208
+ row.push({ type: "NUMBER", value: sign.kind === "-" /* MINUS */ ? -value : value });
2189
2209
  } else {
2190
2210
  const tok = this.advance();
2191
2211
  if (tok.kind === "STRING" /* STRING */) {
@@ -2215,6 +2235,12 @@ var Parser = class {
2215
2235
  const { appId, subtableCode } = extractTableRef(name, this.prev());
2216
2236
  this.expect("SET" /* SET */);
2217
2237
  const assignments = this.parseAssignments();
2238
+ if (subtableCode && assignments.some((a) => a.value.type === "STRING_FUNC")) {
2239
+ throw new ParseError(
2240
+ "\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB UPDATE SET \u3067\u306F\u6587\u5B57\u5217\u95A2\u6570\u3092\u76F4\u63A5\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093",
2241
+ this.prev()
2242
+ );
2243
+ }
2218
2244
  let from = null;
2219
2245
  if (this.consume("FROM" /* FROM */)) {
2220
2246
  const table = this.parseTableRef();
@@ -2258,7 +2284,14 @@ var Parser = class {
2258
2284
  from.targetFilter = decomposed.targetFilter;
2259
2285
  } else if (assignments.some((a) => a.value.type === "SOURCE_FIELD")) {
2260
2286
  throw new ParseError(
2261
- "SET \u306E\u5024\u306B\u306F\u30EA\u30C6\u30E9\u30EB\u30FB\u7B97\u8853\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u306E\u307F\u306F\u4E0D\u53EF\uFF09",
2287
+ "SET \u306E\u5024\u306B\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u3092\u5358\u72EC\u3067\u6307\u5B9A\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093",
2288
+ whereTok
2289
+ );
2290
+ } else if (assignments.some(
2291
+ (a) => a.value.type === "STRING_FUNC" && this.nodeContainsAnyQualifier(a.value)
2292
+ )) {
2293
+ throw new ParseError(
2294
+ "UPDATE SET \u306E\u6587\u5B57\u5217\u95A2\u6570\u3067\u306F\u66F4\u65B0\u5148\u30D5\u30A3\u30FC\u30EB\u30C9\u3092\u4FEE\u98FE\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044",
2262
2295
  whereTok
2263
2296
  );
2264
2297
  }
@@ -2325,6 +2358,12 @@ var Parser = class {
2325
2358
  }
2326
2359
  validateUpdateFromAssignments(assignments, sourceAlias, tok) {
2327
2360
  for (const assignment of assignments) {
2361
+ if (assignment.value.type === "STRING_FUNC") {
2362
+ throw new ParseError(
2363
+ "UPDATE ... FROM \u306E SET \u3067\u306F\u6587\u5B57\u5217\u95A2\u6570\u3092\u76F4\u63A5\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093",
2364
+ tok
2365
+ );
2366
+ }
2328
2367
  if (assignment.value.type === "SOURCE_FIELD") {
2329
2368
  if (assignment.value.alias.toLowerCase() !== sourceAlias.toLowerCase()) {
2330
2369
  throw new ParseError(`UPDATE ... FROM \u306E SET \u53C2\u7167\u306F\u30BD\u30FC\u30B9 alias ${sourceAlias} \u3067\u4FEE\u98FE\u3057\u3066\u304F\u3060\u3055\u3044`, tok);
@@ -2464,9 +2503,17 @@ var Parser = class {
2464
2503
  this.expect(")" /* RPAREN */);
2465
2504
  return { type: "SCALAR_SUBQUERY", query };
2466
2505
  }
2467
- const node = this.parseArithAddSub();
2506
+ const previousAllowUnaryPlusNumber = this.allowUnaryPlusNumber;
2507
+ this.allowUnaryPlusNumber = true;
2508
+ let node;
2509
+ try {
2510
+ node = this.parseArithAddSub();
2511
+ } finally {
2512
+ this.allowUnaryPlusNumber = previousAllowUnaryPlusNumber;
2513
+ }
2468
2514
  if (node.type === "NUMBER") return node;
2469
2515
  if (node.type === "ARITH") return node;
2516
+ if (node.type === "STRING_FUNC") return node;
2470
2517
  if (node.type === "FIELD_REF") {
2471
2518
  const dot = node.field.indexOf(".");
2472
2519
  if (dot > 0 && dot < node.field.length - 1) {
@@ -2474,7 +2521,7 @@ var Parser = class {
2474
2521
  }
2475
2522
  }
2476
2523
  throw new ParseError(
2477
- "SET \u306E\u5024\u306B\u306F\u30EA\u30C6\u30E9\u30EB\u30FB\u7B97\u8853\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u306E\u307F\u306F\u4E0D\u53EF\uFF09",
2524
+ "SET \u306E\u5024\u306B\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u3092\u5358\u72EC\u3067\u6307\u5B9A\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093",
2478
2525
  tok
2479
2526
  );
2480
2527
  }
@@ -4499,6 +4546,43 @@ function applyRoundOp(op, num, digits) {
4499
4546
  if (digits > 0) return String(parseFloat(raw.toFixed(digits)));
4500
4547
  return String(raw);
4501
4548
  }
4549
+ function isHighSurrogate(codeUnit) {
4550
+ return codeUnit >= 55296 && codeUnit <= 56319;
4551
+ }
4552
+ function isLowSurrogate(codeUnit) {
4553
+ return codeUnit >= 56320 && codeUnit <= 57343;
4554
+ }
4555
+ function splitsSurrogatePair(value, index) {
4556
+ return index > 0 && index < value.length && isHighSurrogate(value.charCodeAt(index - 1)) && isLowSurrogate(value.charCodeAt(index));
4557
+ }
4558
+ function normalizeSliceIndex(index, length) {
4559
+ if (Number.isNaN(index) || index === Number.NEGATIVE_INFINITY) return 0;
4560
+ if (index === Number.POSITIVE_INFINITY) return length;
4561
+ const integer = Math.trunc(index);
4562
+ return integer < 0 ? Math.max(length + integer, 0) : Math.min(integer, length);
4563
+ }
4564
+ function sliceSafePrefix(value, budget) {
4565
+ let end = Math.min(Math.max(0, budget), value.length);
4566
+ if (splitsSurrogatePair(value, end)) end -= 1;
4567
+ return value.slice(0, end);
4568
+ }
4569
+ function sliceSafeSuffix(value, budget) {
4570
+ let start = Math.max(0, value.length - budget);
4571
+ if (splitsSurrogatePair(value, start)) start += 1;
4572
+ return value.slice(start);
4573
+ }
4574
+ function sliceSafeRange(value, rawStart, rawEnd) {
4575
+ let start = normalizeSliceIndex(rawStart, value.length);
4576
+ let end = normalizeSliceIndex(rawEnd, value.length);
4577
+ if (end <= start) return "";
4578
+ if (splitsSurrogatePair(value, start)) start += 1;
4579
+ if (splitsSurrogatePair(value, end)) end -= 1;
4580
+ return value.slice(start, Math.max(start, end));
4581
+ }
4582
+ function makeSafePadding(pad, gap) {
4583
+ const repeated = pad.repeat(Math.ceil(gap / pad.length));
4584
+ return sliceSafePrefix(repeated, gap);
4585
+ }
4502
4586
  function evalStringFunc(expr, row) {
4503
4587
  const args = expr.args.map((a) => evalStringFuncArg(a, row));
4504
4588
  switch (expr.func) {
@@ -4514,23 +4598,26 @@ function evalStringFunc(expr, row) {
4514
4598
  return (args[0] ?? "").trimEnd();
4515
4599
  case "LENGTH":
4516
4600
  return String((args[0] ?? "").length);
4601
+ case "LENGTH_CHAR":
4602
+ assertArity("LENGTH_CHAR", args, 1, 1);
4603
+ return String([...args[0] ?? ""].length);
4517
4604
  case "SUBSTRING": {
4518
4605
  const str = args[0] ?? "";
4519
4606
  const start = Math.max(0, Number(args[1] ?? "1") - 1);
4520
4607
  const len = args[2] !== void 0 ? Number(args[2]) : void 0;
4521
- return len !== void 0 ? str.slice(start, start + len) : str.slice(start);
4608
+ return sliceSafeRange(str, start, len !== void 0 ? start + len : str.length);
4522
4609
  }
4523
4610
  case "LEFT": {
4524
4611
  assertArity("LEFT", args, 2, 2);
4525
4612
  const str = args[0];
4526
4613
  const n = Math.trunc(Number(args[1]));
4527
- return Number.isNaN(n) || n <= 0 ? "" : str.slice(0, n);
4614
+ return Number.isNaN(n) || n <= 0 ? "" : sliceSafePrefix(str, n);
4528
4615
  }
4529
4616
  case "RIGHT": {
4530
4617
  assertArity("RIGHT", args, 2, 2);
4531
4618
  const str = args[0];
4532
4619
  const n = Math.trunc(Number(args[1]));
4533
- return Number.isNaN(n) || n <= 0 ? "" : str.slice(Math.max(0, str.length - n));
4620
+ return Number.isNaN(n) || n <= 0 ? "" : sliceSafeSuffix(str, n);
4534
4621
  }
4535
4622
  case "INSTR":
4536
4623
  assertArity("INSTR", args, 2, 2);
@@ -4541,10 +4628,11 @@ function evalStringFunc(expr, row) {
4541
4628
  const str = args[0];
4542
4629
  const n = Math.trunc(Number(args[1]));
4543
4630
  if (Number.isNaN(n) || n <= 0) return "";
4544
- if (str.length >= n) return str.slice(0, n);
4631
+ if (str.length >= n) return sliceSafePrefix(str, n);
4545
4632
  const pad = args[2] ?? " ";
4546
4633
  if (pad === "") return str;
4547
- return expr.func === "LPAD" ? str.padStart(n, pad) : str.padEnd(n, pad);
4634
+ const padding = makeSafePadding(pad, n - str.length);
4635
+ return expr.func === "LPAD" ? padding + str : str + padding;
4548
4636
  }
4549
4637
  case "GREATEST":
4550
4638
  case "LEAST":
@@ -4558,6 +4646,21 @@ function evalStringFunc(expr, row) {
4558
4646
  const to = args[2] ?? "";
4559
4647
  return from === "" ? str : str.split(from).join(to);
4560
4648
  }
4649
+ case "TRANSLATE": {
4650
+ assertArity("TRANSLATE", args, 3, 3);
4651
+ const from = [...args[1]];
4652
+ const to = [...args[2]];
4653
+ if (from.length !== to.length) {
4654
+ throw new Error(
4655
+ `ArgumentError: TRANSLATE \u306E from \u3068 to \u306F\u540C\u3058\u6587\u5B57\u6570\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\uFF08from=${from.length}, to=${to.length}\uFF09`
4656
+ );
4657
+ }
4658
+ const map = /* @__PURE__ */ new Map();
4659
+ from.forEach((ch, i) => {
4660
+ if (!map.has(ch)) map.set(ch, to[i]);
4661
+ });
4662
+ return [...args[0]].map((ch) => map.get(ch) ?? ch).join("");
4663
+ }
4561
4664
  case "COALESCE":
4562
4665
  return args.find((a) => a !== "") ?? "";
4563
4666
  case "NULLIF":
@@ -4795,6 +4898,7 @@ function evalOp(op, leftStr, right, row, fieldType, resolveFieldType, semantics
4795
4898
  }
4796
4899
  var NUMERIC_STRING_FUNCTIONS = /* @__PURE__ */ new Set([
4797
4900
  "LENGTH",
4901
+ "LENGTH_CHAR",
4798
4902
  "INSTR",
4799
4903
  "ROUND",
4800
4904
  "FLOOR",
@@ -5042,7 +5146,7 @@ function updateToPutBatches(stmt, ids, fieldTypes = /* @__PURE__ */ new Map()) {
5042
5146
  function buildUpdateRecord(assignments, fieldTypes) {
5043
5147
  const record = {};
5044
5148
  for (const { field, value } of assignments) {
5045
- if (value.type === "ARITH" || value.type === "CASE_VALUE" || value.type === "SOURCE_FIELD") continue;
5149
+ if (value.type === "ARITH" || value.type === "CASE_VALUE" || value.type === "STRING_FUNC" || value.type === "SOURCE_FIELD") continue;
5046
5150
  record[field] = { value: toKintoneValue(value, fieldTypes.get(field)) };
5047
5151
  }
5048
5152
  return record;
@@ -5052,12 +5156,19 @@ function hasArithAssignment(stmt) {
5052
5156
  (a) => a.value.type === "ARITH" || a.value.type === "CASE_VALUE"
5053
5157
  );
5054
5158
  }
5159
+ function hasRowDependentAssignment(stmt) {
5160
+ return stmt.assignments.some(
5161
+ (a) => a.value.type === "ARITH" || a.value.type === "CASE_VALUE" || a.value.type === "STRING_FUNC"
5162
+ );
5163
+ }
5055
5164
  function updateToGetQueryForArith(stmt) {
5056
5165
  assertDmlWhereIsSafe(stmt.where);
5057
5166
  const refFields = /* @__PURE__ */ new Set();
5058
5167
  for (const { value } of stmt.assignments) {
5059
5168
  if (value.type === "ARITH") {
5060
5169
  collectArithFields2(value, refFields);
5170
+ } else if (value.type === "STRING_FUNC") {
5171
+ collectStringFuncFields2(value, refFields);
5061
5172
  } else if (value.type === "CASE_VALUE") {
5062
5173
  collectCaseFields(value.expr, refFields);
5063
5174
  }
@@ -5144,6 +5255,8 @@ function updateToPutBatchesArith(stmt, records, fieldTypes = /* @__PURE__ */ new
5144
5255
  for (const { field, value } of stmt.assignments) {
5145
5256
  if (value.type === "ARITH") {
5146
5257
  record[field] = { value: String(evalArith(value, raw)) };
5258
+ } else if (value.type === "STRING_FUNC") {
5259
+ record[field] = { value: evalStringFunc(value, row) };
5147
5260
  } else if (value.type === "CASE_VALUE") {
5148
5261
  record[field] = { value: evalCaseWhenValue(value.expr, row, fieldTypes.get(field)) };
5149
5262
  } else if (value.type === "SOURCE_FIELD") {
@@ -5192,6 +5305,8 @@ function updateFromToPutBatches(stmt, matched, fieldTypes = /* @__PURE__ */ new
5192
5305
  throw new DmlConvertError(`\u6570\u5024\u30D5\u30A3\u30FC\u30EB\u30C9 ${field} \u306B\u5909\u63DB\u3067\u304D\u306A\u3044\u5024\u3067\u3059: ${raw}`);
5193
5306
  }
5194
5307
  record[field] = { value: toKintoneValue({ type: "STRING", value: raw }, fieldType) };
5308
+ } else if (value.type === "STRING_FUNC") {
5309
+ throw new DmlConvertError("UPDATE ... FROM \u306E SET \u3067\u306F\u6587\u5B57\u5217\u95A2\u6570\u3092\u76F4\u63A5\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093");
5195
5310
  } else if (value.type === "ARITH") {
5196
5311
  record[field] = { value: String(evalArith(value, target)) };
5197
5312
  } else if (value.type === "CASE_VALUE") {
@@ -5643,7 +5758,7 @@ var KORDER_NATIVE_FIELD_TYPES = /* @__PURE__ */ new Set([
5643
5758
  "CREATOR",
5644
5759
  "MODIFIER"
5645
5760
  ]);
5646
- function planKorderNative(input) {
5761
+ function planKorder(input) {
5647
5762
  const { stmt } = input;
5648
5763
  const reasons = [];
5649
5764
  if (stmt.orderMode !== "KINTONE_NATIVE") reasons.push("KORDER_MODE_REQUIRED");
@@ -5673,29 +5788,120 @@ function planKorderNative(input) {
5673
5788
  reasons.push(`KORDER_TYPE_UNSUPPORTED(field=${name}, type=${semantics.fieldType})`);
5674
5789
  }
5675
5790
  }
5676
- if (stmt.limit === null || stmt.limit < 0 || stmt.limit > 500) {
5791
+ if (stmt.limit === null || !Number.isSafeInteger(stmt.limit) || stmt.limit < 0) {
5677
5792
  reasons.push(`KORDER_LIMIT_INVALID(limit=${String(stmt.limit)})`);
5678
5793
  }
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
5794
  const offset = stmt.offset ?? 0;
5683
- if (offset < 0 || offset > 1e4) reasons.push(`KORDER_OFFSET_INVALID(offset=${offset})`);
5795
+ if (!Number.isSafeInteger(offset) || offset < 0) {
5796
+ reasons.push(`KORDER_OFFSET_INVALID(offset=${offset})`);
5797
+ }
5798
+ const scanRows = stmt.limit === null ? Number.NaN : offset + stmt.limit;
5799
+ if (stmt.limit !== null && !Number.isSafeInteger(scanRows)) {
5800
+ reasons.push(`KORDER_SCAN_ROWS_INVALID(offset=${offset}, limit=${stmt.limit})`);
5801
+ }
5684
5802
  const unique = [...new Set(reasons)];
5685
5803
  if (unique.length > 0) {
5686
5804
  throw new Error(
5687
5805
  `ArgumentError: KORDER BY cannot be executed (mode=KINTONE_NATIVE; ${unique.join(", ")}). Use ORDER BY for canonical local ordering or simplify the query.`
5688
5806
  );
5689
5807
  }
5808
+ const native = stmt.limit <= 500 && offset <= 1e4 && stmt.limit <= input.maxRecords;
5809
+ if (!native && scanRows > input.maxRecords) {
5810
+ throw new Error(
5811
+ `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.`
5812
+ );
5813
+ }
5690
5814
  return {
5691
- kind: "KORDER_NATIVE",
5815
+ kind: native ? "KORDER_NATIVE" : "KORDER_CURSOR",
5692
5816
  requiresCompleteInput: false,
5693
5817
  localOrderBy: false,
5694
5818
  applyLocalOffsetLimit: false,
5695
- reasonCodes: []
5819
+ reasonCodes: [],
5820
+ scanRows
5696
5821
  };
5697
5822
  }
5698
5823
 
5824
+ // src/core/errors/cursorErrors.ts
5825
+ var CursorCapacityError = class extends Error {
5826
+ constructor(host, limit, waitMs) {
5827
+ 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`);
5828
+ this.name = "CursorCapacityError";
5829
+ }
5830
+ };
5831
+ var CursorCreateOutcomeUnknownError = class extends Error {
5832
+ constructor(cause) {
5833
+ 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");
5834
+ this.name = "CursorCreateOutcomeUnknownError";
5835
+ this.cause = cause;
5836
+ }
5837
+ };
5838
+ var CursorCleanupWarning = class extends Error {
5839
+ constructor(cause) {
5840
+ const detail = cause instanceof Error ? cause.message : String(cause);
5841
+ 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}`);
5842
+ this.name = "CursorCleanupWarning";
5843
+ this.cause = cause;
5844
+ }
5845
+ };
5846
+
5847
+ // src/core/optimization/korderCursorExecutor.ts
5848
+ async function executeKorderCursor(input) {
5849
+ const handle = await input.client.openCursor({
5850
+ app: input.app,
5851
+ fields: input.fields.length > 0 ? input.fields : void 0,
5852
+ query: input.query,
5853
+ size: 500
5854
+ });
5855
+ const records = [];
5856
+ let seen = 0;
5857
+ let primaryError;
5858
+ let cleanupWarning;
5859
+ try {
5860
+ if (handle.totalCount > input.offset) {
5861
+ while (records.length < input.limit) {
5862
+ const page = await handle.nextPage();
5863
+ for (const record of page.records) {
5864
+ if (seen < input.offset) seen += 1;
5865
+ else if (records.length < input.limit) records.push(record);
5866
+ else break;
5867
+ }
5868
+ if (!page.next) break;
5869
+ }
5870
+ }
5871
+ } catch (error) {
5872
+ primaryError = error;
5873
+ throw error;
5874
+ } finally {
5875
+ try {
5876
+ await handle.close();
5877
+ } catch (cleanupError) {
5878
+ if (primaryError && primaryError instanceof Error) {
5879
+ Object.defineProperty(primaryError, "cursorCleanupError", {
5880
+ value: cleanupError,
5881
+ configurable: true
5882
+ });
5883
+ } else {
5884
+ cleanupWarning = new CursorCleanupWarning(cleanupError).message;
5885
+ }
5886
+ }
5887
+ }
5888
+ return { records, cleanupWarning };
5889
+ }
5890
+
5891
+ // src/converter/korderCursorQuery.ts
5892
+ function buildKorderCursorQuery(stmt) {
5893
+ const parts = [];
5894
+ if (stmt.where) parts.push(whereToKintone(stmt.where));
5895
+ const order = stmt.orderBy.map((item) => {
5896
+ if (item.key.type !== "FIELD_NAME") {
5897
+ throw new Error("ArgumentError: KORDER cursor key must be a direct field.");
5898
+ }
5899
+ return `${item.key.name} ${item.direction === "ASC" ? "asc" : "desc"}`;
5900
+ });
5901
+ parts.push(`order by ${order.join(", ")}`);
5902
+ return parts.join(" ");
5903
+ }
5904
+
5699
5905
  // src/engine/process.ts
5700
5906
  function flatten(record, alias) {
5701
5907
  const row = {};
@@ -5992,6 +6198,7 @@ function compareSortKeys(a, b, meta) {
5992
6198
  }
5993
6199
  var NUMERIC_ORDER_FUNCTIONS = /* @__PURE__ */ new Set([
5994
6200
  "LENGTH",
6201
+ "LENGTH_CHAR",
5995
6202
  "INSTR",
5996
6203
  "ROUND",
5997
6204
  "FLOOR",
@@ -6836,6 +7043,15 @@ function createEmptyMetrics() {
6836
7043
  fieldCalls: 0,
6837
7044
  appsCalls: 0,
6838
7045
  processStatusCalls: 0,
7046
+ cursorCreateCalls: 0,
7047
+ cursorGetCalls: 0,
7048
+ cursorDeleteCalls: 0,
7049
+ cursorRecordsScanned: 0,
7050
+ cursorActiveCurrent: 0,
7051
+ cursorActivePeak: 0,
7052
+ cursorCleanupFailures: 0,
7053
+ cursorCreateOutcomeUnknown: 0,
7054
+ cursorQuarantinedCurrent: 0,
6839
7055
  fetchedRows: 0,
6840
7056
  elapsedMs: 0
6841
7057
  };
@@ -6848,6 +7064,48 @@ function wrapClientWithMetrics(client, metrics) {
6848
7064
  metrics.fetchedRows += res.records.length;
6849
7065
  return res;
6850
7066
  },
7067
+ openCursor: async (params) => {
7068
+ metrics.cursorCreateCalls += 1;
7069
+ let handle;
7070
+ try {
7071
+ handle = await client.openCursor(params);
7072
+ } catch (error) {
7073
+ if (error instanceof Error && error.name === "CursorCreateOutcomeUnknownError") {
7074
+ metrics.cursorCreateOutcomeUnknown += 1;
7075
+ metrics.cursorQuarantinedCurrent += 1;
7076
+ }
7077
+ throw error;
7078
+ }
7079
+ metrics.cursorActiveCurrent += 1;
7080
+ metrics.cursorActivePeak = Math.max(metrics.cursorActivePeak, metrics.cursorActiveCurrent);
7081
+ let released = false;
7082
+ const markReleased = () => {
7083
+ if (released) return;
7084
+ released = true;
7085
+ metrics.cursorActiveCurrent -= 1;
7086
+ };
7087
+ return {
7088
+ totalCount: handle.totalCount,
7089
+ nextPage: async () => {
7090
+ metrics.cursorGetCalls += 1;
7091
+ const page = await handle.nextPage();
7092
+ metrics.cursorRecordsScanned += page.records.length;
7093
+ if (!page.next) markReleased();
7094
+ return page;
7095
+ },
7096
+ close: async () => {
7097
+ if (!released) metrics.cursorDeleteCalls += 1;
7098
+ try {
7099
+ await handle.close();
7100
+ markReleased();
7101
+ } catch (error) {
7102
+ metrics.cursorCleanupFailures += 1;
7103
+ metrics.cursorQuarantinedCurrent += 1;
7104
+ throw error;
7105
+ }
7106
+ }
7107
+ };
7108
+ },
6851
7109
  postRecords: (params) => {
6852
7110
  metrics.postCalls += 1;
6853
7111
  return client.postRecords(params);
@@ -6887,6 +7145,37 @@ function wrapClientWithSearchAbort(client, collector, failClosed) {
6887
7145
  }
6888
7146
  };
6889
7147
  }
7148
+ function wrapClientWithCursorScope(client) {
7149
+ const active = /* @__PURE__ */ new Set();
7150
+ return {
7151
+ client: {
7152
+ ...client,
7153
+ openCursor: async (params) => {
7154
+ const handle = await client.openCursor(params);
7155
+ active.add(handle);
7156
+ const remove = () => active.delete(handle);
7157
+ return {
7158
+ totalCount: handle.totalCount,
7159
+ async nextPage() {
7160
+ const page = await handle.nextPage();
7161
+ if (!page.next) remove();
7162
+ return page;
7163
+ },
7164
+ async close() {
7165
+ try {
7166
+ await handle.close();
7167
+ } finally {
7168
+ remove();
7169
+ }
7170
+ }
7171
+ };
7172
+ }
7173
+ },
7174
+ closeActive: async () => {
7175
+ await Promise.all([...active].map((handle) => handle.close().catch(() => void 0)));
7176
+ }
7177
+ };
7178
+ }
6890
7179
  function isSelectLikeStatement(stmt) {
6891
7180
  return stmt.type === "SELECT" || stmt.type === "UNION" || stmt.type === "WITH";
6892
7181
  }
@@ -6937,7 +7226,13 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
6937
7226
  case "DESCRIBE":
6938
7227
  return executeDescribe(stmt, client, cacheContext);
6939
7228
  case "EXPLAIN":
6940
- return executeExplain(stmt, client, cacheContext, options.maxRecords ?? 1e4);
7229
+ return executeExplain(
7230
+ stmt,
7231
+ client,
7232
+ cacheContext,
7233
+ options.maxRecords ?? 1e4,
7234
+ options.cursorMaxActive ?? 2
7235
+ );
6941
7236
  // 一時テーブルはバッチスコープのため単文実行では拒否する(executeBatch を使う)
6942
7237
  case "CREATE_TEMP_TABLE":
6943
7238
  throw new Error("ArgumentError: CREATE TEMP TABLE requires a batch (temp tables are batch-scoped).");
@@ -7044,9 +7339,11 @@ async function executeBatch(sql, client, options = {}) {
7044
7339
  searchAbortCollector,
7045
7340
  info.statementType !== "SELECT" && info.statementType !== "UNION" && info.statementType !== "WITH"
7046
7341
  );
7342
+ const cursorScope = wrapClientWithCursorScope(statementClient);
7047
7343
  const outcome = await runWithDeadline(
7048
- executeBatchStatement(statements[i], info, statementClient, stmtOptions, cacheContext, tempTables, variables),
7049
- remaining
7344
+ executeBatchStatement(statements[i], info, cursorScope.client, stmtOptions, cacheContext, tempTables, variables),
7345
+ remaining,
7346
+ cursorScope.closeActive
7050
7347
  );
7051
7348
  if (outcome.result) {
7052
7349
  outcome.result = attachSearchAbortWarning(outcome.result, searchAbortCollector);
@@ -7204,19 +7501,47 @@ async function runSelectLike(query, client, options, cacheContext, tempTables) {
7204
7501
  }
7205
7502
  return executeQueryWithCte(query, client, options, tempTables, cacheContext, true);
7206
7503
  }
7207
- async function runWithDeadline(work, remainingMs) {
7504
+ async function runWithDeadline(work, remainingMs, onTimeout) {
7208
7505
  if (remainingMs === null) return work;
7209
7506
  if (remainingMs <= 0) {
7507
+ if (onTimeout) await onTimeout();
7210
7508
  void work.catch(() => {
7211
7509
  });
7212
7510
  throw new BatchTimeoutError();
7213
7511
  }
7214
7512
  let timer;
7513
+ let timedOut = false;
7514
+ const guardedWork = work.then(
7515
+ (value) => timedOut ? new Promise(() => void 0) : value,
7516
+ (error) => {
7517
+ if (timedOut) return new Promise(() => void 0);
7518
+ throw error;
7519
+ }
7520
+ );
7215
7521
  try {
7216
7522
  return await Promise.race([
7217
- work,
7523
+ guardedWork,
7218
7524
  new Promise((_, reject) => {
7219
- timer = setTimeout(() => reject(new BatchTimeoutError()), remainingMs);
7525
+ timer = setTimeout(() => {
7526
+ timedOut = true;
7527
+ void (async () => {
7528
+ if (onTimeout) {
7529
+ let cleanupTimer;
7530
+ try {
7531
+ await Promise.race([
7532
+ onTimeout(),
7533
+ new Promise((resolve2) => {
7534
+ cleanupTimer = setTimeout(resolve2, 5e3);
7535
+ cleanupTimer.unref?.();
7536
+ })
7537
+ ]);
7538
+ } finally {
7539
+ if (cleanupTimer) clearTimeout(cleanupTimer);
7540
+ }
7541
+ }
7542
+ reject(new BatchTimeoutError());
7543
+ })();
7544
+ }, remainingMs);
7220
7545
  })
7221
7546
  ]);
7222
7547
  } catch (e) {
@@ -7576,7 +7901,7 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
7576
7901
  const staticMode = resolveSelectMode(stmt);
7577
7902
  const mode = whereCapability.capability === "EXACT_PUSHDOWN" ? staticMode : "FULL_SCAN";
7578
7903
  const orderMeta = await buildOrderByMetaForSelect(stmt, client, cacheContext, cteCache);
7579
- const orderPlan = hasCanonicalOrder(stmt) ? (stmt.orderMode === "KINTONE_NATIVE" ? planKorderNative : planCanonicalOrder)({
7904
+ const orderPlan = hasCanonicalOrder(stmt) ? (stmt.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
7580
7905
  stmt,
7581
7906
  staticMode: mode,
7582
7907
  whereCapability: whereCapability.capability,
@@ -7590,7 +7915,7 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
7590
7915
  client,
7591
7916
  cacheContext
7592
7917
  );
7593
- const completeInputRequired = orderPlan?.kind === "CANONICAL_REST_TOP_N" || orderPlan?.kind === "KORDER_NATIVE" ? requiresCompleteInput({ ...stmt, orderBy: [] }) : requiresCompleteInput(stmt);
7918
+ const completeInputRequired = orderPlan?.kind === "CANONICAL_REST_TOP_N" || orderPlan?.kind === "KORDER_NATIVE" || orderPlan?.kind === "KORDER_CURSOR" ? requiresCompleteInput({ ...stmt, orderBy: [] }) : requiresCompleteInput(stmt);
7594
7919
  const truncateWasDisabled = completeInputRequired && options.onLimitReached === "truncate";
7595
7920
  const effectiveOptions = truncateWasDisabled ? { ...options, onLimitReached: "error" } : options;
7596
7921
  try {
@@ -7691,12 +8016,23 @@ async function executeSimpleSelect(stmt, client, options, cacheContext, orderPla
7691
8016
  const warnings = /* @__PURE__ */ new Set();
7692
8017
  const onLimit = options.onLimitReached ?? "error";
7693
8018
  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;
8019
+ 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
8020
  const needed = stmt.limit === null ? null : (stmt.offset ?? 0) + stmt.limit;
7696
8021
  const stopAfter = stmt.orderBy.length === 0 && needed !== null && needed <= maxRecords && !whereHasKlike(stmt.where) ? needed : void 0;
7697
8022
  let records;
7698
- if (orderPlan?.kind === "KORDER_NATIVE" && stmt.limit === 0) {
8023
+ if ((orderPlan?.kind === "KORDER_NATIVE" || orderPlan?.kind === "KORDER_CURSOR") && stmt.limit === 0) {
7699
8024
  records = [];
8025
+ } else if (orderPlan?.kind === "KORDER_CURSOR") {
8026
+ const cursorResult = await executeKorderCursor({
8027
+ client,
8028
+ app: params.app,
8029
+ fields: params.fields,
8030
+ query: buildKorderCursorQuery(stmt),
8031
+ offset: stmt.offset ?? 0,
8032
+ limit: stmt.limit
8033
+ });
8034
+ records = cursorResult.records;
8035
+ if (cursorResult.cleanupWarning) warnings.add(cursorResult.cleanupWarning);
7700
8036
  } else if (useRestWindow) {
7701
8037
  const res = await client.getRecords({
7702
8038
  app: params.app,
@@ -8083,6 +8419,7 @@ function systemColumnMeta(field) {
8083
8419
  }
8084
8420
  var NUMBER_RETURNING_STRING_FUNCTIONS = /* @__PURE__ */ new Set([
8085
8421
  "LENGTH",
8422
+ "LENGTH_CHAR",
8086
8423
  "INSTR",
8087
8424
  "ROUND",
8088
8425
  "FLOOR",
@@ -8488,7 +8825,7 @@ async function executeFullScanWithCte(stmt, client, options, cteCache, cacheCont
8488
8825
  }
8489
8826
  const orderMeta = await buildOrderByMetaForSelect(stmt, client, cacheContext, cteCache);
8490
8827
  if (hasCanonicalOrder(stmt)) {
8491
- (stmt.orderMode === "KINTONE_NATIVE" ? planKorderNative : planCanonicalOrder)({
8828
+ (stmt.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
8492
8829
  stmt,
8493
8830
  staticMode: "FULL_SCAN",
8494
8831
  whereCapability: whereCapability.capability,
@@ -9050,6 +9387,28 @@ var NON_WRITABLE_FIELD_TYPES = /* @__PURE__ */ new Set([
9050
9387
  "CATEGORY",
9051
9388
  "REFERENCE_TABLE"
9052
9389
  ]);
9390
+ function assertWritableTopLevelDmlFields(appId, targetFields, fieldInfos) {
9391
+ const infoByCode = new Map(fieldInfos.map((field) => [field.code, field]));
9392
+ for (const code of targetFields) {
9393
+ const info = infoByCode.get(code);
9394
+ if (!info) {
9395
+ throw new Error(`ArgumentError: DML target field ${code} does not exist.`);
9396
+ }
9397
+ if (info.inSubtable) {
9398
+ throw new Error(
9399
+ `ArgumentError: DML target field ${code} is inside a subtable. Use subtable DML syntax (for example, APP${appId}$\u30C6\u30FC\u30D6\u30EB).`
9400
+ );
9401
+ }
9402
+ if (info.writable === false || NON_WRITABLE_FIELD_TYPES.has(info.fieldType)) {
9403
+ throw new Error(`ArgumentError: DML target field ${code} is not writable (${info.fieldType}).`);
9404
+ }
9405
+ }
9406
+ }
9407
+ async function loadWritableTopLevelDmlFields(appId, targetFields, client, cacheContext) {
9408
+ const fieldInfos = await getFieldsCached(appId, client, cacheContext);
9409
+ assertWritableTopLevelDmlFields(appId, targetFields, fieldInfos);
9410
+ return fieldInfos;
9411
+ }
9053
9412
  async function executeDmlValidation(stmt, client, options, cacheContext, tempTables, statementNumber) {
9054
9413
  return (await prepareDmlValidation(stmt, client, options, cacheContext, tempTables, statementNumber)).result;
9055
9414
  }
@@ -9061,24 +9420,22 @@ var RejectLimitExceededError = class extends Error {
9061
9420
  }
9062
9421
  };
9063
9422
  async function prepareDmlValidation(stmt, client, options, cacheContext, tempTables, statementNumber) {
9064
- if (stmt.type === "UPDATE") {
9065
- await assertDmlWhereCapability(stmt, client, cacheContext);
9066
- }
9067
9423
  const operation = stmt.type === "UPDATE" ? "UPDATE" : stmt.type.startsWith("UPSERT") ? "UPSERT" : "INSERT";
9068
9424
  const payloadFields = stmt.type === "UPDATE" ? ["$id", ...stmt.assignments.map((a) => a.field)] : [...stmt.fields];
9069
9425
  if (new Set(payloadFields).size !== payloadFields.length) {
9070
9426
  throw new Error("ArgumentError: DML target fields contain duplicates.");
9071
9427
  }
9072
- const fieldInfos = await getFieldsCached(stmt.appId, client, cacheContext);
9073
- const infoByCode = new Map(fieldInfos.map((field) => [field.code, field]));
9074
9428
  const targetFields = stmt.type === "UPDATE" ? stmt.assignments.map((a) => a.field) : stmt.fields;
9075
- for (const code of targetFields) {
9076
- const info = infoByCode.get(code);
9077
- if (!info) throw new Error(`ArgumentError: DML target field ${code} does not exist.`);
9078
- if (info.writable === false || NON_WRITABLE_FIELD_TYPES.has(info.fieldType)) {
9079
- throw new Error(`ArgumentError: DML target field ${code} is not writable (${info.fieldType}).`);
9080
- }
9429
+ const fieldInfos = await loadWritableTopLevelDmlFields(
9430
+ stmt.appId,
9431
+ targetFields,
9432
+ client,
9433
+ cacheContext
9434
+ );
9435
+ if (stmt.type === "UPDATE") {
9436
+ await assertDmlWhereCapability(stmt, client, cacheContext);
9081
9437
  }
9438
+ const infoByCode = new Map(fieldInfos.map((field) => [field.code, field]));
9082
9439
  const candidates = await materializeValidationCandidates(stmt, operation, client, options, cacheContext, tempTables, infoByCode);
9083
9440
  const { errors, invalidRows, invalidRowNumbers } = validateDmlCandidates(
9084
9441
  candidates,
@@ -9247,7 +9604,7 @@ async function materializeUpdateValidationCandidates(stmt, client, options, cach
9247
9604
  await resolveSetSubqueries(stmt.assignments, client, options, cacheContext);
9248
9605
  const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
9249
9606
  let records;
9250
- if (hasArithAssignment(stmt)) {
9607
+ if (hasRowDependentAssignment(stmt)) {
9251
9608
  const getParams = updateToGetQueryForArith(stmt);
9252
9609
  const resolved = await fetchRecordsForSharedPlan(client.getRecords, getParams.app, getParams.query, [...getParams.fields], {
9253
9610
  maxRecords: options.maxRecords ?? 1e4,
@@ -9456,6 +9813,7 @@ async function executeInsert(stmt, client, options, cacheContext) {
9456
9813
  if (stmt.subtableCode) {
9457
9814
  return executeInsertSubtable(stmt, client, options, cacheContext);
9458
9815
  }
9816
+ await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
9459
9817
  const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
9460
9818
  const batches = insertToPostBatches(stmt, fieldTypes);
9461
9819
  const createdIds = [];
@@ -9470,6 +9828,7 @@ async function executeInsert(stmt, client, options, cacheContext) {
9470
9828
  };
9471
9829
  }
9472
9830
  async function executeInsertSelect(stmt, client, options, cacheContext, cteCache) {
9831
+ await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
9473
9832
  const selectResult = cteCache !== void 0 && cteCache.size > 0 ? await executeQueryWithCte(stmt.select, client, options, cteCache, cacheContext) : await executeSelect(stmt.select, client, options, cacheContext);
9474
9833
  const { rows, columns } = selectResult;
9475
9834
  if (columns.length !== stmt.fields.length) {
@@ -9504,17 +9863,24 @@ async function executeInsertSelect(stmt, client, options, cacheContext, cteCache
9504
9863
  };
9505
9864
  }
9506
9865
  async function executeUpdate(stmt, client, options, cacheContext, tempTables) {
9507
- await assertDmlWhereCapability(stmt, client, cacheContext);
9508
9866
  if (stmt.subtableCode) {
9867
+ await assertDmlWhereCapability(stmt, client, cacheContext);
9509
9868
  return executeUpdateSubtable(stmt, client, options, cacheContext);
9510
9869
  }
9870
+ await loadWritableTopLevelDmlFields(
9871
+ stmt.appId,
9872
+ stmt.assignments.map((assignment) => assignment.field),
9873
+ client,
9874
+ cacheContext
9875
+ );
9876
+ await assertDmlWhereCapability(stmt, client, cacheContext);
9511
9877
  if (stmt.from != null) {
9512
9878
  return executeUpdateFrom(stmt, stmt.from, client, options, cacheContext, tempTables);
9513
9879
  }
9514
9880
  const maxRecords = options.maxRecords ?? 1e4;
9515
9881
  await resolveSetSubqueries(stmt.assignments, client, options, cacheContext);
9516
9882
  const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
9517
- if (hasArithAssignment(stmt)) {
9883
+ if (hasRowDependentAssignment(stmt)) {
9518
9884
  const getParams2 = updateToGetQueryForArith(stmt);
9519
9885
  const resolved2 = await fetchRecordsForSharedPlan(
9520
9886
  client.getRecords,
@@ -9608,6 +9974,7 @@ async function executeDelete(stmt, client, options, cacheContext) {
9608
9974
  return { type: "DELETE", deletedCount: ids.length };
9609
9975
  }
9610
9976
  async function executeUpsert(stmt, client, options, cacheContext) {
9977
+ await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
9611
9978
  const toInsert = [];
9612
9979
  const toUpdate = [];
9613
9980
  const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
@@ -10034,6 +10401,7 @@ function evalOrderKeyForRow(key, row) {
10034
10401
  }
10035
10402
  }
10036
10403
  async function executeUpsertSelect(stmt, client, options, cacheContext, cteCache) {
10404
+ await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
10037
10405
  const selectResult = cteCache !== void 0 && cteCache.size > 0 ? await executeQueryWithCte(stmt.select, client, options, cteCache, cacheContext) : await executeSelect(stmt.select, client, options, cacheContext);
10038
10406
  const { rows, columns } = selectResult;
10039
10407
  if (columns.length !== stmt.fields.length) {
@@ -10265,7 +10633,7 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
10265
10633
  const hasUnmaterializedSource = [select.from, ...select.joins.map((join2) => join2.table)].some((table) => table.cteName !== null);
10266
10634
  if (hasCanonicalOrder(select) && !hasUnmaterializedSource) {
10267
10635
  const mode = capability.capability === "EXACT_PUSHDOWN" ? resolveSelectMode(select) : "FULL_SCAN";
10268
- orderPlans.set(select, (select.orderMode === "KINTONE_NATIVE" ? planKorderNative : planCanonicalOrder)({
10636
+ orderPlans.set(select, (select.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
10269
10637
  stmt: select,
10270
10638
  staticMode: mode,
10271
10639
  whereCapability: capability.capability,
@@ -10295,7 +10663,7 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
10295
10663
  capabilities.set(inlined, capability);
10296
10664
  if (hasCanonicalOrder(inlined)) {
10297
10665
  const meta = await buildOrderByMetaForSelect(inlined, tracedClient, cacheContext);
10298
- orderPlans.set(inlined, (inlined.orderMode === "KINTONE_NATIVE" ? planKorderNative : planCanonicalOrder)({
10666
+ orderPlans.set(inlined, (inlined.orderMode === "KINTONE_NATIVE" ? planKorder : planCanonicalOrder)({
10299
10667
  stmt: inlined,
10300
10668
  staticMode: capability.capability === "EXACT_PUSHDOWN" ? resolveSelectMode(inlined) : "FULL_SCAN",
10301
10669
  whereCapability: capability.capability,
@@ -10313,7 +10681,7 @@ function explainMetadataLines(analysis) {
10313
10681
  ...[...analysis.processStatusApps].sort((a, b) => a - b).map((appId) => ` metadata API: process status APP${appId}`)
10314
10682
  ];
10315
10683
  }
10316
- async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4) {
10684
+ async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4, cursorMaxActive = 2) {
10317
10685
  const statements = parseSqlBatch(sql);
10318
10686
  const analysis = analyzeBatch(statements);
10319
10687
  validateDeclaredBatchVariables(statements, injectedVariables);
@@ -10324,12 +10692,12 @@ async function buildBatchExplainPlans(sql, client, injectedVariables, cacheConte
10324
10692
  const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr: resolveVariableRefs(stmt.expr, variables) } : stmt : resolveVariableRefs(stmt, variables);
10325
10693
  validateKlikeStatement(planStmt);
10326
10694
  const whereAnalysis = await buildExplainWhereAnalysis(planStmt, client, cacheContext, maxRecords);
10327
- const statementPlan = buildBatchStatementPlan(
10695
+ const statementPlan = addCursorConcurrency(buildBatchStatementPlan(
10328
10696
  planStmt,
10329
10697
  analysis.statements[i],
10330
10698
  whereAnalysis.capabilities,
10331
10699
  whereAnalysis.orderPlans
10332
- );
10700
+ ), cursorMaxActive);
10333
10701
  const metadataPlan = explainMetadataLines(whereAnalysis);
10334
10702
  plans.push({
10335
10703
  index: i,
@@ -10435,11 +10803,14 @@ function buildPlanForBatchQuery(query, info, capabilities, orderPlans) {
10435
10803
  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
10804
  return lines;
10437
10805
  }
10438
- async function executeExplain(stmt, client, cacheContext, maxRecords) {
10806
+ async function executeExplain(stmt, client, cacheContext, maxRecords, cursorMaxActive) {
10439
10807
  const analysis = await buildExplainWhereAnalysis(stmt.query, client, cacheContext, maxRecords);
10440
10808
  const lines = [
10441
10809
  ...explainMetadataLines(analysis),
10442
- ...buildExplainPlan(stmt.query, void 0, analysis.capabilities, analysis.orderPlans)
10810
+ ...addCursorConcurrency(
10811
+ buildExplainPlan(stmt.query, void 0, analysis.capabilities, analysis.orderPlans),
10812
+ cursorMaxActive
10813
+ )
10443
10814
  ];
10444
10815
  return {
10445
10816
  type: "SELECT",
@@ -10448,6 +10819,17 @@ async function executeExplain(stmt, client, cacheContext, maxRecords) {
10448
10819
  rowCount: lines.length
10449
10820
  };
10450
10821
  }
10822
+ function addCursorConcurrency(lines, cursorMaxActive) {
10823
+ const result = [];
10824
+ for (const line of lines) {
10825
+ result.push(line);
10826
+ if (line.trim() === "cursor page size: 500") {
10827
+ const indent = line.match(/^\s*/)?.[0] ?? "";
10828
+ result.push(`${indent}cursor concurrency: ${cursorMaxActive} per domain (process-local)`);
10829
+ }
10830
+ }
10831
+ return result;
10832
+ }
10451
10833
  function buildExplainPlan(query, label, capabilities, orderPlans) {
10452
10834
  if (query.type === "UNION") return buildUnionPlan(query, capabilities, orderPlans);
10453
10835
  if (query.type === "WITH") return buildWithPlan(query, capabilities, orderPlans);
@@ -10477,6 +10859,11 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans) {
10477
10859
  if (orderPlan.kind === "KORDER_NATIVE") {
10478
10860
  lines.push(" order semantics: kintone native (not kSQL canonical)");
10479
10861
  lines.push(" REST execution: single GET");
10862
+ } else if (orderPlan.kind === "KORDER_CURSOR") {
10863
+ lines.push(" order semantics: kintone native (not kSQL canonical)");
10864
+ lines.push(" fetch API: POST/GET/DELETE records/cursor.json");
10865
+ lines.push(" cursor page size: 500");
10866
+ lines.push(` scan rows: ${orderPlan.scanRows}`);
10480
10867
  }
10481
10868
  }
10482
10869
  if (orderPlan?.requiresCompleteInput ?? requiresCompleteInput(stmt)) {
@@ -10488,7 +10875,8 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans) {
10488
10875
  if (mode === "SIMPLE") {
10489
10876
  const params = selectToKintoneParams(orderPlan?.kind === "CANONICAL_REST_TOP_N" ? withCanonicalRestTie(stmt) : stmt);
10490
10877
  lines.push(` app: APP${stmt.from.appId} (${stmt.from.appId})`);
10491
- lines.push(` kintone query: ${params.query || "(\u306A\u3057)"}`);
10878
+ const displayedQuery = orderPlan?.kind === "KORDER_CURSOR" ? buildKorderCursorQuery(stmt) : params.query;
10879
+ lines.push(` kintone query: ${displayedQuery || "(\u306A\u3057)"}`);
10492
10880
  lines.push(` fields: ${params.fields.length === 0 ? "(\u5168\u30D5\u30A3\u30FC\u30EB\u30C9)" : params.fields.join(", ")}`);
10493
10881
  } else {
10494
10882
  const pushdownPlan = buildKlikePushdownPlan(stmt);
@@ -10649,6 +11037,8 @@ function buildInsertSelectPlan(stmt, label, capabilities, orderPlans) {
10649
11037
  }
10650
11038
  function buildUpdatePlan(stmt, label, capabilities, orderPlans) {
10651
11039
  const isArith = hasArithAssignment(stmt);
11040
+ const isStringFunc = stmt.assignments.some((a) => a.value.type === "STRING_FUNC");
11041
+ const isRowDependent = hasRowDependentAssignment(stmt);
10652
11042
  const isSubq = stmt.assignments.some((a) => a.value.type === "SCALAR_SUBQUERY");
10653
11043
  const lines = [];
10654
11044
  if (label) lines.push(label);
@@ -10665,10 +11055,11 @@ function buildUpdatePlan(stmt, label, capabilities, orderPlans) {
10665
11055
  lines.push(` api: GET /k/v1/records.json \u2192 PUT /k/v1/records.json`);
10666
11056
  const setTypes = [];
10667
11057
  if (isArith) setTypes.push("\u7B97\u8853 SET\uFF08\u73FE\u5728\u5024\u3092\u53D6\u5F97\u3057\u3066\u8A08\u7B97\uFF09");
11058
+ if (isStringFunc) setTypes.push("\u6587\u5B57\u5217\u95A2\u6570 SET\uFF08\u73FE\u5728\u5024\u3092\u53D6\u5F97\u3057\u3066\u8A55\u4FA1\uFF09");
10668
11059
  if (isSubq) setTypes.push("\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA SET");
10669
- if (!isArith && !isSubq) setTypes.push("\u5358\u7D14 SET");
11060
+ if (!isRowDependent && !isSubq) setTypes.push("\u5358\u7D14 SET");
10670
11061
  lines.push(` set type: ${setTypes.join(", ")}`);
10671
- if (isArith) {
11062
+ if (isRowDependent) {
10672
11063
  const refFields = collectArithRefFields(stmt);
10673
11064
  if (refFields.length > 0) {
10674
11065
  lines.push(` ref fields: ${refFields.join(", ")}\uFF08GET \u306B\u542B\u3081\u308B\uFF09`);
@@ -10754,6 +11145,7 @@ function collectArithRefFields(stmt) {
10754
11145
  const refs = /* @__PURE__ */ new Set();
10755
11146
  for (const { value } of stmt.assignments) {
10756
11147
  if (value.type === "ARITH") collectArithNodeRefs(value, refs);
11148
+ if (value.type === "STRING_FUNC") collectArithNodeRefs(value, refs);
10757
11149
  }
10758
11150
  return [...refs];
10759
11151
  }
@@ -10766,6 +11158,13 @@ function collectArithNodeRefs(node, out) {
10766
11158
  collectArithNodeRefs(node.left, out);
10767
11159
  collectArithNodeRefs(node.right, out);
10768
11160
  }
11161
+ if (node.type === "STRING_FUNC") {
11162
+ for (const arg of node.args) {
11163
+ if (arg.type !== "STRING" && arg.type !== "AGG_REF" && arg.type !== "AGG_ARITH") {
11164
+ collectArithNodeRefs(arg, out);
11165
+ }
11166
+ }
11167
+ }
10769
11168
  }
10770
11169
  function formatAssignment(a) {
10771
11170
  const v = a.value;
@@ -10773,6 +11172,7 @@ function formatAssignment(a) {
10773
11172
  if (v.type === "NUMBER") return `${a.field} = ${v.value}`;
10774
11173
  if (v.type === "ARITH") return `${a.field} = ${formatArithExprStr(v)}`;
10775
11174
  if (v.type === "CASE_VALUE") return `${a.field} = CASE WHEN ...`;
11175
+ if (v.type === "STRING_FUNC") return `${a.field} = ${v.func}(...)`;
10776
11176
  if (v.type === "SCALAR_SUBQUERY") return `${a.field} = (SELECT ...)`;
10777
11177
  if (v.type === "SOURCE_FIELD") return `${a.field} = ${v.alias}.${v.field}`;
10778
11178
  return `${a.field} = (${v.type})`;
@@ -11056,6 +11456,12 @@ function validateKsqlConfig(config) {
11056
11456
  }
11057
11457
  const logicalApps = normalizeLogicalApps(profileName, profile.logicalApps);
11058
11458
  if (logicalApps !== void 0) profile.logicalApps = logicalApps;
11459
+ if (profile.query?.cursorMaxActive !== void 0) {
11460
+ const value = profile.query.cursorMaxActive;
11461
+ if (!Number.isSafeInteger(value) || value < 1 || value > 5) {
11462
+ throw argumentError(`query.cursorMaxActive for profile "${profileName}" must be an integer from 1 to 5.`);
11463
+ }
11464
+ }
11059
11465
  }
11060
11466
  return config;
11061
11467
  }
@@ -11200,6 +11606,10 @@ var RequestGate = class {
11200
11606
  async runMutation(fn) {
11201
11607
  return this.withSlot(fn);
11202
11608
  }
11609
+ /** Cursor Create/Get/Delete: セマフォのみ。GETでも位置を進めるため再試行しない。 */
11610
+ async runCursorStep(fn) {
11611
+ return this.withSlot(fn);
11612
+ }
11203
11613
  async withSlot(fn) {
11204
11614
  await this.acquire();
11205
11615
  try {
@@ -11231,6 +11641,14 @@ var RequestGate = class {
11231
11641
  function withRequestGate(client, gate) {
11232
11642
  return {
11233
11643
  getRecords: (params) => gate.runReadOnly(() => client.getRecords(params)),
11644
+ openCursor: async (params) => {
11645
+ const handle = await gate.runCursorStep(() => client.openCursor(params));
11646
+ return {
11647
+ totalCount: handle.totalCount,
11648
+ nextPage: () => gate.runCursorStep(() => handle.nextPage()),
11649
+ close: () => gate.runCursorStep(() => handle.close())
11650
+ };
11651
+ },
11234
11652
  getApps: () => gate.runReadOnly(() => client.getApps()),
11235
11653
  getFields: (appId) => gate.runReadOnly(() => client.getFields(appId)),
11236
11654
  getProcessStatuses: (appId) => gate.runReadOnly(() => client.getProcessStatuses(appId)),
@@ -11345,7 +11763,218 @@ function normalizeProcessStatusStates(states) {
11345
11763
  });
11346
11764
  }
11347
11765
 
11766
+ // src/api/kintoneCursor.ts
11767
+ function isAlreadyReleasedCursorError(error) {
11768
+ const shaped = error;
11769
+ return shaped?.status === 404 && shaped.code === "GAIA_CN01";
11770
+ }
11771
+ async function deleteCursorWithConfirmation(deleteCursor, sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)), isAlreadyReleased = isAlreadyReleasedCursorError) {
11772
+ try {
11773
+ await deleteCursor();
11774
+ return;
11775
+ } catch (firstError) {
11776
+ if (isAlreadyReleased(firstError)) return;
11777
+ }
11778
+ await sleep(250);
11779
+ try {
11780
+ await deleteCursor();
11781
+ } catch (confirmationError) {
11782
+ if (isAlreadyReleased(confirmationError)) return;
11783
+ throw confirmationError;
11784
+ }
11785
+ }
11786
+ async function withTimeout(promise, timeoutMs) {
11787
+ let timer;
11788
+ const timeout = new Promise((_resolve, reject) => {
11789
+ timer = setTimeout(() => reject(new Error(`CursorCleanupTimeoutError: cleanup exceeded ${timeoutMs}ms.`)), timeoutMs);
11790
+ timer.unref?.();
11791
+ });
11792
+ try {
11793
+ return await Promise.race([promise, timeout]);
11794
+ } finally {
11795
+ if (timer) clearTimeout(timer);
11796
+ }
11797
+ }
11798
+ function createKintoneCursorHandle(totalCount, operations) {
11799
+ let released = false;
11800
+ let closing = false;
11801
+ let pageTail = Promise.resolve();
11802
+ let closePromise = null;
11803
+ const nextPage = () => {
11804
+ if (closing || released) return Promise.resolve({ records: [], next: false });
11805
+ const result = pageTail.then(async () => {
11806
+ if (closing || released) return { records: [], next: false };
11807
+ const page = await operations.get();
11808
+ if (!page.next) {
11809
+ released = true;
11810
+ operations.onReleased?.();
11811
+ }
11812
+ return page;
11813
+ });
11814
+ pageTail = result.then(() => void 0, () => void 0);
11815
+ return result;
11816
+ };
11817
+ const close = () => {
11818
+ if (released) return Promise.resolve();
11819
+ if (closePromise) return closePromise;
11820
+ closing = true;
11821
+ closePromise = pageTail.then(async () => {
11822
+ if (released) return;
11823
+ try {
11824
+ await withTimeout(
11825
+ deleteCursorWithConfirmation(
11826
+ operations.delete,
11827
+ operations.sleep,
11828
+ operations.isAlreadyReleasedError
11829
+ ),
11830
+ operations.cleanupTimeoutMs ?? 5e3
11831
+ );
11832
+ released = true;
11833
+ operations.onReleased?.();
11834
+ } catch (error) {
11835
+ operations.onReleaseUnknown?.();
11836
+ throw error;
11837
+ }
11838
+ });
11839
+ return closePromise;
11840
+ };
11841
+ return { totalCount, nextPage, close };
11842
+ }
11843
+
11844
+ // src/api/cursorLeaseManager.ts
11845
+ var DEFAULT_MAX_ACTIVE = 2;
11846
+ var MAX_ACTIVE = 5;
11847
+ var DEFAULT_WAIT_MS = 3e4;
11848
+ var DEFAULT_QUARANTINE_MS = 10 * 6e4 + 3e4;
11849
+ var CursorLeaseManager = class {
11850
+ constructor(host, options = {}) {
11851
+ this.host = host;
11852
+ this.active = 0;
11853
+ this.peak = 0;
11854
+ this.quarantined = 0;
11855
+ this.waiters = [];
11856
+ this.createTail = Promise.resolve();
11857
+ const maxActive = options.maxActive ?? DEFAULT_MAX_ACTIVE;
11858
+ if (!Number.isSafeInteger(maxActive) || maxActive < 1 || maxActive > MAX_ACTIVE) {
11859
+ throw new Error(`ArgumentError: cursorMaxActive must be an integer from 1 to ${MAX_ACTIVE}.`);
11860
+ }
11861
+ this.maxActive = maxActive;
11862
+ this.waitTimeoutMs = options.waitTimeoutMs ?? DEFAULT_WAIT_MS;
11863
+ this.quarantineMs = options.quarantineMs ?? DEFAULT_QUARANTINE_MS;
11864
+ }
11865
+ acquire() {
11866
+ if (this.active < this.maxActive) {
11867
+ this.active += 1;
11868
+ this.peak = Math.max(this.peak, this.active);
11869
+ return Promise.resolve(this.makeLease());
11870
+ }
11871
+ return new Promise((resolve2, reject) => {
11872
+ const waiter = {};
11873
+ waiter.resolve = resolve2;
11874
+ waiter.reject = reject;
11875
+ waiter.timer = setTimeout(() => {
11876
+ const index = this.waiters.indexOf(waiter);
11877
+ if (index >= 0) this.waiters.splice(index, 1);
11878
+ reject(new CursorCapacityError(this.host, this.maxActive, this.waitTimeoutMs));
11879
+ }, this.waitTimeoutMs);
11880
+ waiter.timer.unref?.();
11881
+ this.waiters.push(waiter);
11882
+ });
11883
+ }
11884
+ /**
11885
+ * 同一hostを共有する後続surfaceの設定を反映する。
11886
+ * 縮小時は既存leaseを強制終了せず、activeが新上限を下回るまで新規取得だけを止める。
11887
+ */
11888
+ setMaxActive(maxActive) {
11889
+ this.validateMaxActive(maxActive);
11890
+ if (this.maxActive === maxActive) return;
11891
+ this.maxActive = maxActive;
11892
+ this.dispatchWaiters();
11893
+ }
11894
+ async runCreate(fn) {
11895
+ const previous = this.createTail;
11896
+ let unlock;
11897
+ this.createTail = new Promise((resolve2) => {
11898
+ unlock = resolve2;
11899
+ });
11900
+ await previous;
11901
+ try {
11902
+ return await fn();
11903
+ } finally {
11904
+ unlock();
11905
+ }
11906
+ }
11907
+ snapshot() {
11908
+ return {
11909
+ active: this.active,
11910
+ peak: this.peak,
11911
+ quarantined: this.quarantined,
11912
+ waiting: this.waiters.length,
11913
+ limit: this.maxActive
11914
+ };
11915
+ }
11916
+ makeLease() {
11917
+ let done = false;
11918
+ return {
11919
+ release: () => {
11920
+ if (done) return;
11921
+ done = true;
11922
+ this.returnPermit();
11923
+ },
11924
+ quarantine: (durationMs = this.quarantineMs) => {
11925
+ if (done) return;
11926
+ done = true;
11927
+ this.quarantined += 1;
11928
+ const timer = setTimeout(() => {
11929
+ this.quarantined -= 1;
11930
+ this.returnPermit();
11931
+ }, durationMs);
11932
+ timer.unref?.();
11933
+ }
11934
+ };
11935
+ }
11936
+ returnPermit() {
11937
+ this.active -= 1;
11938
+ this.dispatchWaiters();
11939
+ }
11940
+ dispatchWaiters() {
11941
+ while (this.active < this.maxActive) {
11942
+ const waiter = this.waiters.shift();
11943
+ if (!waiter) return;
11944
+ clearTimeout(waiter.timer);
11945
+ this.active += 1;
11946
+ this.peak = Math.max(this.peak, this.active);
11947
+ waiter.resolve(this.makeLease());
11948
+ }
11949
+ }
11950
+ validateMaxActive(maxActive) {
11951
+ if (!Number.isSafeInteger(maxActive) || maxActive < 1 || maxActive > MAX_ACTIVE) {
11952
+ throw new Error(`ArgumentError: cursorMaxActive must be an integer from 1 to ${MAX_ACTIVE}.`);
11953
+ }
11954
+ }
11955
+ };
11956
+ var managers = /* @__PURE__ */ new Map();
11957
+ function getCursorLeaseManager(host, maxActive = DEFAULT_MAX_ACTIVE) {
11958
+ const key = host.toLowerCase();
11959
+ let manager = managers.get(key);
11960
+ if (!manager) {
11961
+ manager = new CursorLeaseManager(key, { maxActive });
11962
+ managers.set(key, manager);
11963
+ } else {
11964
+ manager.setMaxActive(maxActive);
11965
+ }
11966
+ return manager;
11967
+ }
11968
+
11348
11969
  // src/cli/nodeKintoneClient.ts
11970
+ var KintoneApiError = class extends Error {
11971
+ constructor(status, code, bodyText) {
11972
+ super(`kintone API error ${status}: ${bodyText}`);
11973
+ this.status = status;
11974
+ this.code = code;
11975
+ this.name = "KintoneApiError";
11976
+ }
11977
+ };
11349
11978
  var SEARCH_ABORTED_HEADER_VALUE = "Filter aborted because of too many search results";
11350
11979
  function createNodeKintoneClient(baseUrl, tokenResolver) {
11351
11980
  const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
@@ -11393,7 +12022,13 @@ function createNodeKintoneClient(baseUrl, tokenResolver) {
11393
12022
  if (tokenResolver.debug) {
11394
12023
  tokenResolver.log?.(`[debug] response status=${res.status} body=${bodyText}`);
11395
12024
  }
11396
- throw new Error(`kintone API error ${res.status}: ${bodyText}`);
12025
+ let code;
12026
+ try {
12027
+ const body = JSON.parse(bodyText);
12028
+ if (typeof body.code === "string") code = body.code;
12029
+ } catch {
12030
+ }
12031
+ throw new KintoneApiError(res.status, code, bodyText);
11397
12032
  }
11398
12033
  if (tokenResolver.debug) {
11399
12034
  tokenResolver.log?.(`[debug] response status=${res.status}`);
@@ -11460,6 +12095,48 @@ function createNodeKintoneClient(baseUrl, tokenResolver) {
11460
12095
  return response.searchAborted ? { ...response.body, searchAborted: true } : response.body;
11461
12096
  }
11462
12097
  },
12098
+ async openCursor(params) {
12099
+ const manager = getCursorLeaseManager(new URL(normalizedBaseUrl).host, tokenResolver.cursorMaxActive);
12100
+ const lease = await manager.acquire();
12101
+ let created;
12102
+ try {
12103
+ created = await manager.runCreate(() => requestJson(
12104
+ `${apiBasePath}/records/cursor.json`,
12105
+ {
12106
+ method: "POST",
12107
+ body: JSON.stringify({
12108
+ app: params.app,
12109
+ query: params.query,
12110
+ size: params.size,
12111
+ fields: params.fields && params.fields.length > 0 ? params.fields : void 0
12112
+ })
12113
+ },
12114
+ params.app
12115
+ ));
12116
+ } catch (error) {
12117
+ if (error instanceof KintoneApiError) {
12118
+ lease.release();
12119
+ throw error;
12120
+ }
12121
+ lease.quarantine();
12122
+ throw new CursorCreateOutcomeUnknownError(error);
12123
+ }
12124
+ const cursorId = created.id;
12125
+ return createKintoneCursorHandle(Number(created.totalCount), {
12126
+ get: () => requestJson(
12127
+ `${apiBasePath}/records/cursor.json?id=${encodeURIComponent(cursorId)}`,
12128
+ { method: "GET" },
12129
+ params.app
12130
+ ),
12131
+ delete: () => requestJson(
12132
+ `${apiBasePath}/records/cursor.json`,
12133
+ { method: "DELETE", body: JSON.stringify({ id: cursorId }) },
12134
+ params.app
12135
+ ),
12136
+ onReleased: () => lease.release(),
12137
+ onReleaseUnknown: () => lease.quarantine()
12138
+ });
12139
+ },
11463
12140
  async postRecords(_params) {
11464
12141
  const res = await requestJson(
11465
12142
  `${apiBasePath}/records.json`,
@@ -12017,6 +12694,7 @@ Options:
12017
12694
  --timeout <ms> Request timeout in milliseconds (default: 30000)
12018
12695
  --max-concurrent <n> Max concurrent kintone requests: 1-50 (default: 10)
12019
12696
  (process-wide; fixed at first resolution; KSQL_MAX_CONCURRENT wins)
12697
+ --cursor-max-active <n> Max active cursors per host: 1-5 (default: 2; KSQL_CURSOR_MAX_ACTIVE wins)
12020
12698
  --retry <n> GET retry count: 0-10, 0 disables (default: 3; KSQL_RETRY wins)
12021
12699
  --retry-base-delay <ms> GET retry backoff base delay (default: 500)
12022
12700
  --retry-max-delay <ms> GET retry backoff max delay (default: 8000)
@@ -12097,6 +12775,7 @@ function parseArgs(argv) {
12097
12775
  continueOnError: false,
12098
12776
  dmlMaxRows: null,
12099
12777
  maxConcurrent: null,
12778
+ cursorMaxActive: null,
12100
12779
  retry: null,
12101
12780
  retryBaseDelay: null,
12102
12781
  retryMaxDelay: null,
@@ -12353,6 +13032,13 @@ function parseArgs(argv) {
12353
13032
  i++;
12354
13033
  continue;
12355
13034
  }
13035
+ if (a === "--cursor-max-active") {
13036
+ const n = Number(v);
13037
+ if (!Number.isInteger(n) || n < 1 || n > 5) throw new Error("ArgumentError: --cursor-max-active must be an integer between 1 and 5.");
13038
+ out.cursorMaxActive = n;
13039
+ i++;
13040
+ continue;
13041
+ }
12356
13042
  if (a === "--retry") {
12357
13043
  const n = Number(v);
12358
13044
  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 +13334,7 @@ function createDryRunClient() {
12648
13334
  };
12649
13335
  return {
12650
13336
  getRecords: notUsed,
13337
+ openCursor: notUsed,
12651
13338
  postRecords: notUsed,
12652
13339
  putRecords: notUsed,
12653
13340
  deleteRecords: notUsed,
@@ -12790,6 +13477,7 @@ function buildReplExecArgv(base, sql, dryRun, format) {
12790
13477
  pushOpt(argv, "--attachment-format", base.attachmentFormat);
12791
13478
  pushOpt(argv, "--dml-max-rows", base.dmlMaxRows);
12792
13479
  pushOpt(argv, "--max-concurrent", base.maxConcurrent);
13480
+ pushOpt(argv, "--cursor-max-active", base.cursorMaxActive);
12793
13481
  pushOpt(argv, "--retry", base.retry);
12794
13482
  pushOpt(argv, "--retry-base-delay", base.retryBaseDelay);
12795
13483
  pushOpt(argv, "--retry-max-delay", base.retryMaxDelay);
@@ -13357,6 +14045,11 @@ async function run() {
13357
14045
  const onLimit = args.onLimit ?? envOnLimit2("KSQL_ON_LIMIT") ?? profile.query?.onLimit ?? "error";
13358
14046
  const timeout = args.timeout ?? envInt2("KSQL_TIMEOUT") ?? profile.query?.timeout ?? 3e4;
13359
14047
  const tempTableMaxRows = args.tempTableMaxRows ?? envInt2("KSQL_TEMP_TABLE_MAX_ROWS") ?? profile.query?.tempTableMaxRows ?? void 0;
14048
+ const cursorMaxActive = args.cursorMaxActive ?? envInt2("KSQL_CURSOR_MAX_ACTIVE") ?? profile.query?.cursorMaxActive ?? 2;
14049
+ if (!Number.isSafeInteger(cursorMaxActive) || cursorMaxActive < 1 || cursorMaxActive > 5) {
14050
+ process.stderr.write("ArgumentError: cursorMaxActive must be an integer from 1 to 5.\n");
14051
+ return 2;
14052
+ }
13360
14053
  if (!Number.isInteger(fetchParallel) || fetchParallel < 1 || fetchParallel > 10) {
13361
14054
  process.stderr.write("ArgumentError: fetch-parallel must be an integer between 1 and 10.\n");
13362
14055
  return 2;
@@ -13487,6 +14180,7 @@ async function run() {
13487
14180
  }
13488
14181
  profileClientMap.set(pName, createNodeKintoneClient(baseUrl, {
13489
14182
  guestSpaceId,
14183
+ cursorMaxActive,
13490
14184
  timeoutMs: timeout,
13491
14185
  debug,
13492
14186
  debugHeaders,
@@ -13519,6 +14213,7 @@ async function run() {
13519
14213
  missingAppProfiles.push(...resolvedTokens.missing);
13520
14214
  profileClientMap.set(pName, createNodeKintoneClient(baseUrl, {
13521
14215
  guestSpaceId,
14216
+ cursorMaxActive,
13522
14217
  timeoutMs: timeout,
13523
14218
  debug,
13524
14219
  debugHeaders,
@@ -13630,6 +14325,12 @@ async function run() {
13630
14325
  if (!routed) throw new Error(`AuthError: profile "${pName}" is not resolved for APP${params.app}.`);
13631
14326
  return routed.getRecords({ ...params, app: binding.appId });
13632
14327
  },
14328
+ openCursor: (params) => {
14329
+ const binding = appBindingByMappedApp.get(params.app) ?? { appId: params.app, profile: profileName.toLowerCase() };
14330
+ const routed = profileClientMap.get(binding.profile);
14331
+ if (!routed) throw new Error(`AuthError: profile "${binding.profile}" is not resolved for APP${params.app}.`);
14332
+ return routed.openCursor({ ...params, app: binding.appId });
14333
+ },
13633
14334
  postRecords: (params) => {
13634
14335
  const binding = appBindingByMappedApp.get(params.app) ?? { appId: params.app, profile: profileName.toLowerCase() };
13635
14336
  const pName = binding.profile;
@@ -13678,7 +14379,14 @@ async function run() {
13678
14379
  }
13679
14380
  if (isBatchSql && args.dryRun) {
13680
14381
  try {
13681
- const plans = await buildBatchExplainPlans(sql, client, args.variables, cacheContext, maxRecords);
14382
+ const plans = await buildBatchExplainPlans(
14383
+ sql,
14384
+ client,
14385
+ args.variables,
14386
+ cacheContext,
14387
+ maxRecords,
14388
+ cursorMaxActive
14389
+ );
13682
14390
  const out = [];
13683
14391
  const restoredStatements = sqlDiagnosticContext ? restoreSqlDiagnosticValue(plans.statements, sqlDiagnosticContext.appBindingByMappedApp) : plans.statements;
13684
14392
  restoredStatements.forEach((p) => {
@@ -13742,6 +14450,7 @@ query=${label}`);
13742
14450
  continueOnError: args.continueOnError,
13743
14451
  tempTableMaxRows,
13744
14452
  timeoutMs: timeout,
14453
+ cursorMaxActive,
13745
14454
  variables: args.variables,
13746
14455
  confirm: batchContainsDml ? async (count, operation) => {
13747
14456
  if (count > dmlMaxRows) {
@@ -13752,12 +14461,18 @@ query=${label}`);
13752
14461
  });
13753
14462
  return writeBatchOutput(batchResult, { format, noHeader, pretty, displayOptions, outputPath, quiet });
13754
14463
  }
13755
- let result = args.dryRun ? await execute(`EXPLAIN ${sql}`, client, { maxRecords, onLimitReached: onLimit, cacheContext }) : await execute(sql, client, {
14464
+ let result = args.dryRun ? await execute(`EXPLAIN ${sql}`, client, {
14465
+ maxRecords,
14466
+ onLimitReached: onLimit,
14467
+ cacheContext,
14468
+ cursorMaxActive
14469
+ }) : await execute(sql, client, {
13756
14470
  maxRecords,
13757
14471
  fetchParallel,
13758
14472
  onLimitReached: effectiveOnLimit,
13759
14473
  confirm: isDmlStatement ? confirm : void 0,
13760
- cacheContext
14474
+ cacheContext,
14475
+ cursorMaxActive
13761
14476
  });
13762
14477
  if (args.dryRun && sqlDiagnosticContext) {
13763
14478
  result = restoreSqlDiagnosticValue(result, sqlDiagnosticContext.appBindingByMappedApp);