@rex0220/kintone-sql-tools 2.12.0 → 2.13.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.
@@ -32416,6 +32416,7 @@ var Parser = class {
32416
32416
  tryParseImplicitAlias() {
32417
32417
  const k = this.peek().kind;
32418
32418
  if (k === "IDENT" /* IDENT */ || k === "BIDENT" /* BIDENT */) {
32419
+ if (k === "IDENT" /* IDENT */ && this.peek().value.toUpperCase() === "VALIDATE" && this.peekAt(1).kind === "IDENT" /* IDENT */ && this.peekAt(1).value.toUpperCase() === "ONLY") return null;
32419
32420
  return this.parseTableAliasName();
32420
32421
  }
32421
32422
  return null;
@@ -32862,7 +32863,8 @@ var Parser = class {
32862
32863
  if (subtableCode) {
32863
32864
  throw new ParseError("INSERT INTO ... SELECT \u306F\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u4EEE\u60F3\u30C6\u30FC\u30D6\u30EB\u3067\u306F\u672A\u5BFE\u5FDC\u3067\u3059", this.prev());
32864
32865
  }
32865
- return { type: "INSERT_SELECT", appId, fields, select };
32866
+ const validation2 = this.parseDmlControlSuffix();
32867
+ return { type: "INSERT_SELECT", appId, fields, select, ...validation2 };
32866
32868
  }
32867
32869
  this.expect("VALUES" /* VALUES */);
32868
32870
  const values = [];
@@ -32872,7 +32874,11 @@ var Parser = class {
32872
32874
  this.expect(")" /* RPAREN */);
32873
32875
  values.push(row);
32874
32876
  } while (this.consume("," /* COMMA */));
32875
- return subtableCode ? { type: "INSERT", appId, subtableCode, fields, values } : { type: "INSERT", appId, fields, values };
32877
+ const validation = this.parseDmlControlSuffix();
32878
+ if (subtableCode && (validation.validateOnly || validation.onErrorSkip)) {
32879
+ throw new ParseError("VALIDATE ONLY / ON ERROR SKIP \u306F\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB INSERT \u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u305B\u3093", this.prev());
32880
+ }
32881
+ return subtableCode ? { type: "INSERT", appId, subtableCode, fields, values, ...validation } : { type: "INSERT", appId, fields, values, ...validation };
32876
32882
  }
32877
32883
  parseUpsert() {
32878
32884
  this.expect("UPSERT" /* UPSERT */);
@@ -32889,7 +32895,8 @@ var Parser = class {
32889
32895
  if (this.peek().kind === "SELECT" /* SELECT */) {
32890
32896
  const select = this.parseSelect();
32891
32897
  const keyFields2 = this.parseOnDuplicate();
32892
- return { type: "UPSERT_SELECT", appId, fields, select, keyFields: keyFields2 };
32898
+ const validation2 = this.parseDmlControlSuffix();
32899
+ return { type: "UPSERT_SELECT", appId, fields, select, keyFields: keyFields2, ...validation2 };
32893
32900
  }
32894
32901
  this.expect("VALUES" /* VALUES */);
32895
32902
  const values = [];
@@ -32899,7 +32906,8 @@ var Parser = class {
32899
32906
  this.expect(")" /* RPAREN */);
32900
32907
  } while (this.consume("," /* COMMA */));
32901
32908
  const keyFields = this.parseOnDuplicate();
32902
- return { type: "UPSERT", appId, fields, values, keyFields };
32909
+ const validation = this.parseDmlControlSuffix();
32910
+ return { type: "UPSERT", appId, fields, values, keyFields, ...validation };
32903
32911
  }
32904
32912
  parseOnDuplicate() {
32905
32913
  this.expectKeyword("ON" /* ON */, "UPSERT \u306B\u306F ON DUPLICATE (\u30AD\u30FC\u30D5\u30A3\u30FC\u30EB\u30C9) \u304C\u5FC5\u8981\u3067\u3059");
@@ -32979,10 +32987,14 @@ var Parser = class {
32979
32987
  if (!table.alias) {
32980
32988
  throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u306B\u306F\u30A8\u30A4\u30EA\u30A2\u30B9\u304C\u5FC5\u8981\u3067\u3059", this.prev());
32981
32989
  }
32990
+ if (table.alias.toLowerCase() === `app${appId}`.toLowerCase()) {
32991
+ throw new ParseError(`UPDATE ... FROM \u306E\u30BD\u30FC\u30B9 alias \u306F\u66F4\u65B0\u5148 APP${appId} \u3068\u540C\u540D\u306B\u3067\u304D\u307E\u305B\u3093`, this.prev());
32992
+ }
32982
32993
  from = {
32983
32994
  appId: table.appId,
32984
32995
  cteName: table.cteName,
32985
32996
  alias: table.alias,
32997
+ targetJoinField: "",
32986
32998
  joinKeyField: "",
32987
32999
  targetFilter: null
32988
33000
  };
@@ -33001,6 +33013,7 @@ var Parser = class {
33001
33013
  }
33002
33014
  this.validateUpdateFromAssignments(assignments, from.alias, whereTok);
33003
33015
  const decomposed = this.decomposeUpdateFromWhere(where, appId, from.alias, whereTok);
33016
+ from.targetJoinField = decomposed.targetJoinField;
33004
33017
  from.joinKeyField = decomposed.joinKeyField;
33005
33018
  from.targetFilter = decomposed.targetFilter;
33006
33019
  } else if (assignments.some((a) => a.value.type === "SOURCE_FIELD")) {
@@ -33009,8 +33022,66 @@ var Parser = class {
33009
33022
  whereTok
33010
33023
  );
33011
33024
  }
33012
- if (from !== null) return { type: "UPDATE", appId, assignments, where, from };
33013
- return subtableCode ? { type: "UPDATE", appId, subtableCode, assignments, where } : { type: "UPDATE", appId, assignments, where };
33025
+ const validation = this.parseDmlControlSuffix();
33026
+ if (subtableCode && (validation.validateOnly || validation.onErrorSkip)) {
33027
+ throw new ParseError("VALIDATE ONLY / ON ERROR SKIP \u306F\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB UPDATE \u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u305B\u3093", this.prev());
33028
+ }
33029
+ if (from !== null) return { type: "UPDATE", appId, assignments, where, from, ...validation };
33030
+ return subtableCode ? { type: "UPDATE", appId, subtableCode, assignments, where, ...validation } : { type: "UPDATE", appId, assignments, where, ...validation };
33031
+ }
33032
+ /** DML末尾の VALIDATE ONLY または ON ERROR SKIP。各語はsoft keyword。 */
33033
+ parseDmlControlSuffix() {
33034
+ if (this.peek().kind === "ON" /* ON */) return this.parseOnErrorSkipSuffix();
33035
+ if (this.isSoftKeyword("REJECT")) {
33036
+ throw new ParseError("REJECT LIMIT \u306B\u306F ON ERROR SKIP INTO \u304C\u5FC5\u8981\u3067\u3059", this.peek());
33037
+ }
33038
+ if (!this.isSoftKeyword("VALIDATE")) return {};
33039
+ const validateTok = this.advance();
33040
+ if (!this.isSoftKeyword("ONLY")) {
33041
+ throw new ParseError("VALIDATE \u306E\u5F8C\u306B\u306F ONLY \u304C\u5FC5\u8981\u3067\u3059", this.peek());
33042
+ }
33043
+ this.advance();
33044
+ let validationErrorTable = null;
33045
+ if (this.consume("INTO" /* INTO */)) {
33046
+ const tok = this.peek();
33047
+ if (tok.kind !== "IDENT" /* IDENT */ || !tok.value.startsWith("#")) {
33048
+ throw new ParseError("VALIDATE ONLY INTO \u306B\u306F # \u3067\u59CB\u307E\u308B\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u540D\u304C\u5FC5\u8981\u3067\u3059", tok);
33049
+ }
33050
+ validationErrorTable = this.parseTableName();
33051
+ }
33052
+ if (this.peek().kind === "ON" /* ON */ || this.isSoftKeyword("REJECT")) {
33053
+ throw new ParseError("VALIDATE ONLY \u3068 ON ERROR / REJECT LIMIT \u306F\u4F75\u8A18\u3067\u304D\u307E\u305B\u3093", validateTok);
33054
+ }
33055
+ return { validateOnly: true, validationErrorTable };
33056
+ }
33057
+ parseOnErrorSkipSuffix() {
33058
+ const onTok = this.advance();
33059
+ if (!this.isSoftKeyword("ERROR")) throw new ParseError("ON \u306E\u5F8C\u306B\u306F ERROR \u304C\u5FC5\u8981\u3067\u3059", this.peek());
33060
+ this.advance();
33061
+ if (!this.isSoftKeyword("SKIP")) throw new ParseError("ON ERROR \u306E\u5F8C\u306B\u306F SKIP \u304C\u5FC5\u8981\u3067\u3059", this.peek());
33062
+ this.advance();
33063
+ this.expect("INTO" /* INTO */, "ON ERROR SKIP \u306B\u306F INTO #\u4E00\u6642\u30C6\u30FC\u30D6\u30EB \u304C\u5FC5\u8981\u3067\u3059");
33064
+ const tableTok = this.peek();
33065
+ if (tableTok.kind !== "IDENT" /* IDENT */ || !tableTok.value.startsWith("#")) {
33066
+ throw new ParseError("ON ERROR SKIP INTO \u306B\u306F # \u3067\u59CB\u307E\u308B\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u540D\u304C\u5FC5\u8981\u3067\u3059", tableTok);
33067
+ }
33068
+ const errorTable = this.parseTableName();
33069
+ let rejectLimit = null;
33070
+ if (this.isSoftKeyword("REJECT")) {
33071
+ this.advance();
33072
+ this.expect("LIMIT" /* LIMIT */, "REJECT \u306E\u5F8C\u306B\u306F LIMIT \u304C\u5FC5\u8981\u3067\u3059");
33073
+ const tok = this.expect("NUMBER" /* NUMBER */, "REJECT LIMIT \u306B\u306F 0 \u4EE5\u4E0A\u306E\u6574\u6570\u304C\u5FC5\u8981\u3067\u3059");
33074
+ if (!/^\d+$/.test(tok.value)) throw new ParseError("REJECT LIMIT \u306F 0 \u4EE5\u4E0A\u306E\u5B89\u5168\u306A\u6574\u6570\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044", tok);
33075
+ rejectLimit = Number(tok.value);
33076
+ if (!Number.isSafeInteger(rejectLimit)) throw new ParseError("REJECT LIMIT \u306F 0 \u4EE5\u4E0A\u306E\u5B89\u5168\u306A\u6574\u6570\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044", tok);
33077
+ }
33078
+ if (this.isSoftKeyword("REJECT") || this.isSoftKeyword("VALIDATE") || this.peek().kind === "ON" /* ON */) {
33079
+ throw new ParseError("ON ERROR SKIP \u306E\u53E5\u304C\u91CD\u8907\u307E\u305F\u306F\u7AF6\u5408\u3057\u3066\u3044\u307E\u3059", onTok);
33080
+ }
33081
+ return { onErrorSkip: true, errorTable, rejectLimit };
33082
+ }
33083
+ isSoftKeyword(value) {
33084
+ return this.peek().kind === "IDENT" /* IDENT */ && this.peek().value.toUpperCase() === value;
33014
33085
  }
33015
33086
  validateUpdateFromAssignments(assignments, sourceAlias, tok) {
33016
33087
  for (const assignment of assignments) {
@@ -33035,11 +33106,11 @@ var Parser = class {
33035
33106
  const leaves = this.flattenTopLevelAnd(where);
33036
33107
  const joins = [];
33037
33108
  leaves.forEach((leaf, index) => {
33038
- const sourceField = this.matchUpdateFromJoin(leaf, targetAppId, sourceAlias);
33039
- if (sourceField !== null) joins.push({ index, sourceField });
33109
+ const matched = this.matchUpdateFromJoin(leaf, targetAppId, sourceAlias);
33110
+ if (matched !== null) joins.push({ index, ...matched });
33040
33111
  });
33041
33112
  if (joins.length !== 1) {
33042
- throw new ParseError("UPDATE ... FROM \u306E WHERE \u306B\u306F target.$id = source.key \u306E\u7D50\u5408\u7B49\u5024\u304C\u3061\u3087\u3046\u30691\u3064\u5FC5\u8981\u3067\u3059", tok);
33113
+ throw new ParseError("UPDATE ... FROM \u306E WHERE \u306B\u306F target.key = source.key \u306E\u7D50\u5408\u7B49\u5024\u304C\u3061\u3087\u3046\u30691\u3064\u5FC5\u8981\u3067\u3059", tok);
33043
33114
  }
33044
33115
  const join = joins[0];
33045
33116
  for (let i = 0; i < leaves.length; i++) {
@@ -33055,7 +33126,7 @@ var Parser = class {
33055
33126
  (acc, expr) => acc === null ? expr : { type: "LOGICAL", op: "AND", left: acc, right: expr },
33056
33127
  null
33057
33128
  );
33058
- return { joinKeyField: join.sourceField, targetFilter };
33129
+ return { targetJoinField: join.targetField, joinKeyField: join.sourceField, targetFilter };
33059
33130
  }
33060
33131
  flattenTopLevelAnd(expr) {
33061
33132
  if (expr.type === "GROUP") return this.flattenTopLevelAnd(expr.expr);
@@ -33069,16 +33140,20 @@ var Parser = class {
33069
33140
  const right = expr.right.type === "ARITH_VALUE" && expr.right.expr.type === "FIELD_REF" ? this.splitQualifiedField(expr.right.expr.field) : null;
33070
33141
  if (right === null) return null;
33071
33142
  const left = { alias: expr.left.tableAlias, field: expr.left.field };
33072
- if (this.isTargetIdRef(left, targetAppId) && this.isSourceRef(right, sourceAlias)) return right.field;
33073
- if (this.isSourceRef(left, sourceAlias) && this.isTargetIdRef(right, targetAppId)) return left.field;
33143
+ if (this.isTargetRef(left, targetAppId) && this.isSourceRef(right, sourceAlias)) {
33144
+ return { targetField: left.field, sourceField: right.field };
33145
+ }
33146
+ if (this.isSourceRef(left, sourceAlias) && this.isTargetRef(right, targetAppId)) {
33147
+ return { targetField: right.field, sourceField: left.field };
33148
+ }
33074
33149
  return null;
33075
33150
  }
33076
33151
  splitQualifiedField(field) {
33077
33152
  const dot = field.indexOf(".");
33078
33153
  return dot < 0 ? { alias: null, field } : { alias: field.slice(0, dot), field: field.slice(dot + 1) };
33079
33154
  }
33080
- isTargetIdRef(ref, appId) {
33081
- return ref.field === "$id" && (ref.alias === null || ref.alias.toLowerCase() === `app${appId}`.toLowerCase());
33155
+ isTargetRef(ref, appId) {
33156
+ return ref.alias === null || ref.alias.toLowerCase() === `app${appId}`.toLowerCase();
33082
33157
  }
33083
33158
  isSourceRef(ref, alias) {
33084
33159
  return ref.alias?.toLowerCase() === alias.toLowerCase();
@@ -33405,6 +33480,15 @@ function isDmlType(type) {
33405
33480
  function isReadOnlyType(type) {
33406
33481
  return type === "SELECT" || type === "UNION" || type === "WITH" || type === "EXPLAIN" || type === "SHOW_APPS" || type === "DESCRIBE" || type === "CREATE_TEMP_TABLE" || type === "DROP_TEMP_TABLE" || type === "SET_VARIABLE" || type === "DECLARE_VARIABLE" || type === "ASSERT";
33407
33482
  }
33483
+ function writesKintone(stmt) {
33484
+ return isDmlType(stmt.type) && !("validateOnly" in stmt && stmt.validateOnly === true);
33485
+ }
33486
+ function isReadOnlyStatement(stmt) {
33487
+ return !writesKintone(stmt) && (isReadOnlyType(stmt.type) || isDmlType(stmt.type));
33488
+ }
33489
+ function requiresCompleteInput(stmt) {
33490
+ return isDmlType(stmt.type);
33491
+ }
33408
33492
  function hasWhereClause(stmt) {
33409
33493
  if (!stmt || typeof stmt !== "object") return false;
33410
33494
  const obj = stmt;
@@ -34632,11 +34716,17 @@ function analyzeBatch(statements) {
34632
34716
  }
34633
34717
  }
34634
34718
  const defined = /* @__PURE__ */ new Map();
34719
+ const validationSchemas = /* @__PURE__ */ new Map();
34635
34720
  const createdOrder = [];
34636
34721
  const results = [];
34637
34722
  const variableDefs = /* @__PURE__ */ new Map();
34638
34723
  const variableOrder = [];
34639
34724
  statements.forEach((stmt, index) => {
34725
+ const validationTable = "validationErrorTable" in stmt && stmt.validationErrorTable ? stmt.validationErrorTable : "onErrorSkip" in stmt && stmt.onErrorSkip ? stmt.errorTable ?? null : null;
34726
+ if (statements.length === 1 && validationTable) {
34727
+ const message = "onErrorSkip" in stmt && stmt.onErrorSkip ? "ArgumentError: ON ERROR SKIP requires a batch." : "ArgumentError: VALIDATE ONLY INTO requires a batch.";
34728
+ throw new BatchAnalysisError(message, index);
34729
+ }
34640
34730
  const statementType = getStatementType(stmt);
34641
34731
  const created = [];
34642
34732
  const dropped = [];
@@ -34691,6 +34781,28 @@ function analyzeBatch(statements) {
34691
34781
  }
34692
34782
  dependsOn.add(at);
34693
34783
  }
34784
+ if (validationTable) {
34785
+ const payloadFields = stmt.type === "UPDATE" ? ["$id", ...stmt.assignments.map((a) => a.field)] : "fields" in stmt ? stmt.fields : [];
34786
+ const signature = JSON.stringify(payloadFields);
34787
+ const at = defined.get(validationTable);
34788
+ if (at === void 0) {
34789
+ defined.set(validationTable, index);
34790
+ validationSchemas.set(validationTable, signature);
34791
+ createdOrder.push(validationTable);
34792
+ created.push(validationTable);
34793
+ if (defined.size > MAX_TEMP_TABLES) {
34794
+ throw new BatchAnalysisError(`ParseError: batch exceeds ${MAX_TEMP_TABLES} temp tables.`, index);
34795
+ }
34796
+ } else {
34797
+ if (validationSchemas.get(validationTable) !== signature) {
34798
+ throw new BatchAnalysisError(
34799
+ `ParseError: validation error table ${validationTable} has a different payload schema.`,
34800
+ index
34801
+ );
34802
+ }
34803
+ dependsOn.add(at);
34804
+ }
34805
+ }
34694
34806
  if (stmt.type === "CREATE_TEMP_TABLE") {
34695
34807
  if (defined.has(stmt.name)) {
34696
34808
  throw new BatchAnalysisError(
@@ -34723,8 +34835,8 @@ function analyzeBatch(statements) {
34723
34835
  results.push({
34724
34836
  index,
34725
34837
  statementType,
34726
- isDml: isDmlType(statementType),
34727
- isReadOnly: isReadOnlyType(statementType),
34838
+ isDml: writesKintone(stmt),
34839
+ isReadOnly: isReadOnlyStatement(stmt),
34728
34840
  hasWhere: hasWhereClause(stmt),
34729
34841
  insertValuesCount: getInsertValuesCount(stmt),
34730
34842
  appIds: [...stmtAppIds].sort((a, b) => a - b),
@@ -34734,10 +34846,15 @@ function analyzeBatch(statements) {
34734
34846
  dependsOn: [...dependsOn].sort((a, b) => a - b),
34735
34847
  tempOnlySource,
34736
34848
  targetAppId: isDmlType(statementType) && typeof stmt.appId === "number" ? stmt.appId : null,
34737
- isUpdateFrom: stmt.type === "UPDATE" && stmt.from != null
34849
+ isUpdateFrom: stmt.type === "UPDATE" && stmt.from != null,
34850
+ isValidationOnly: "validateOnly" in stmt && stmt.validateOnly === true,
34851
+ isOnErrorSkip: "onErrorSkip" in stmt && stmt.onErrorSkip === true,
34852
+ requiresCompleteInput: requiresCompleteInput(stmt)
34738
34853
  });
34739
34854
  });
34740
34855
  const containsDml = results.some((r) => r.isDml);
34856
+ const containsValidationOnly = results.some((r) => r.isValidationOnly);
34857
+ const needsCompleteInput = results.some((r) => r.requiresCompleteInput);
34741
34858
  const variables = variableOrder.map((name) => ({
34742
34859
  name,
34743
34860
  referencedBy: [...variableDefs.get(name).referencedBy]
@@ -34746,6 +34863,8 @@ function analyzeBatch(statements) {
34746
34863
  statementCount: statements.length,
34747
34864
  isReadOnlyBatch: !containsDml && results.every((r) => r.isReadOnly),
34748
34865
  containsDml,
34866
+ containsValidationOnly,
34867
+ requiresCompleteInput: needsCompleteInput,
34749
34868
  tempTables: createdOrder,
34750
34869
  variables,
34751
34870
  warnings: variables.filter((v) => v.referencedBy.length === 0).map((v) => `variable @${v.name} is never used.`),
@@ -35555,6 +35674,18 @@ function evalCaseWhenValue(expr, row, fieldType) {
35555
35674
  return "";
35556
35675
  }
35557
35676
  function toKintoneValue(value, fieldType) {
35677
+ const result = normalizeDmlSqlValue(value, fieldType);
35678
+ if (!result.ok) throw new DmlConvertError(result.message);
35679
+ return result.value;
35680
+ }
35681
+ function normalizeDmlSqlValue(value, fieldType) {
35682
+ try {
35683
+ return { ok: true, value: convertDmlSqlValue(value, fieldType) };
35684
+ } catch (e) {
35685
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
35686
+ }
35687
+ }
35688
+ function convertDmlSqlValue(value, fieldType) {
35558
35689
  switch (value.type) {
35559
35690
  case "VARIABLE":
35560
35691
  throw new DmlConvertError(`\u672A\u89E3\u6C7A\u306E\u30D0\u30C3\u30C1\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
@@ -36395,6 +36526,228 @@ function toFlatString(value) {
36395
36526
  }
36396
36527
  }
36397
36528
 
36529
+ // src/core/dmlValidation.ts
36530
+ var ARRAY_TYPES2 = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
36531
+ var CHOICE_TYPES = /* @__PURE__ */ new Set(["DROP_DOWN", "RADIO_BUTTON", "CHECK_BOX", "MULTI_SELECT"]);
36532
+ function validateAndNormalizeDmlValue(raw, field) {
36533
+ if (field.fieldType === "DATE" || field.fieldType === "TIME" || field.fieldType === "DATETIME") {
36534
+ const original = rawScalarText(raw);
36535
+ if (original !== "" && !isValidTemporalInput(original, field.fieldType)) {
36536
+ return { ok: false, code: "ERR_TYPE_DATE", message: `${field.code} \u306E\u65E5\u4ED8\u30FB\u6642\u523B\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059` };
36537
+ }
36538
+ }
36539
+ let value;
36540
+ try {
36541
+ value = normalizeRaw(raw, field.fieldType);
36542
+ } catch (e) {
36543
+ const message = e instanceof Error ? e.message : String(e);
36544
+ return { ok: false, code: typeCode(field.fieldType), message };
36545
+ }
36546
+ if (field.required && isEmpty(value)) {
36547
+ return { ok: false, code: "ERR_REQUIRED", message: `${field.code} \u306F\u5FC5\u9808\u3067\u3059` };
36548
+ }
36549
+ if (!isEmpty(value) && field.fieldType === "NUMBER") {
36550
+ const text = String(value);
36551
+ if (!isFiniteDecimal(text)) {
36552
+ return { ok: false, code: "ERR_TYPE_NUMBER", message: `${field.code} \u306F\u6570\u5024\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044` };
36553
+ }
36554
+ if (field.minValue != null && compareDecimal(text, field.minValue) < 0) {
36555
+ return { ok: false, code: "ERR_RANGE_MIN", message: `${field.code} \u306F ${field.minValue} \u4EE5\u4E0A\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044` };
36556
+ }
36557
+ if (field.maxValue != null && compareDecimal(text, field.maxValue) > 0) {
36558
+ return { ok: false, code: "ERR_RANGE_MAX", message: `${field.code} \u306F ${field.maxValue} \u4EE5\u4E0B\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044` };
36559
+ }
36560
+ }
36561
+ if (!isEmpty(value) && (field.fieldType === "DATE" || field.fieldType === "TIME" || field.fieldType === "DATETIME")) {
36562
+ if (!isValidTemporal(String(value), field.fieldType)) {
36563
+ return { ok: false, code: "ERR_TYPE_DATE", message: `${field.code} \u306E\u65E5\u4ED8\u30FB\u6642\u523B\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059` };
36564
+ }
36565
+ }
36566
+ if (typeof value === "string") {
36567
+ const length = value.length;
36568
+ const min = field.minLength == null ? null : Number(field.minLength);
36569
+ const max = field.maxLength == null ? null : Number(field.maxLength);
36570
+ if (Number.isFinite(min) && length < min) {
36571
+ return { ok: false, code: "ERR_LENGTH_MIN", message: `${field.code} \u306F ${min} \u6587\u5B57\u4EE5\u4E0A\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044` };
36572
+ }
36573
+ if (Number.isFinite(max) && length > max) {
36574
+ return { ok: false, code: "ERR_LENGTH_MAX", message: `${field.code} \u306F ${max} \u6587\u5B57\u4EE5\u4E0B\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044` };
36575
+ }
36576
+ }
36577
+ if (CHOICE_TYPES.has(field.fieldType) && field.optionOrder) {
36578
+ const selected = Array.isArray(value) ? value.map(String) : [String(value)];
36579
+ if (selected.some((choice) => !(choice in field.optionOrder))) {
36580
+ return { ok: false, code: "ERR_CHOICE_INVALID", message: `${field.code} \u306B\u5B9A\u7FA9\u5916\u306E\u9078\u629E\u80A2\u304C\u3042\u308A\u307E\u3059` };
36581
+ }
36582
+ }
36583
+ return { ok: true, value };
36584
+ }
36585
+ function rawScalarText(raw) {
36586
+ if (raw == null) return "";
36587
+ if (isSqlValue(raw) && (raw.type === "STRING" || raw.type === "NUMBER")) return String(raw.value);
36588
+ return typeof raw === "string" || typeof raw === "number" ? String(raw) : "";
36589
+ }
36590
+ function isValidTemporalInput(value, type) {
36591
+ if (type === "DATE") return isValidTemporal(value.replace(/\//g, "-"), "DATE");
36592
+ if (type === "TIME") return isValidTemporal(value, "TIME");
36593
+ let normalized = value.replace(/\//g, "-").replace(" ", "T");
36594
+ if (/T\d{2}:\d{2}$/.test(normalized)) normalized += ":00";
36595
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(normalized)) {
36596
+ return isValidTemporal(normalized.slice(0, 10), "DATE") && isValidTemporal(normalized.slice(11), "TIME");
36597
+ }
36598
+ return isValidTemporal(normalized, "DATETIME");
36599
+ }
36600
+ function normalizeRaw(raw, fieldType) {
36601
+ if (isSqlValue(raw)) {
36602
+ const normalized = normalizeDmlSqlValue(raw, fieldType);
36603
+ if (!normalized.ok) throw new Error(normalized.message);
36604
+ return normalized.value;
36605
+ }
36606
+ if (Array.isArray(raw)) return raw.map((v) => typeof v === "object" && v !== null && "code" in v ? String(v.code) : String(v));
36607
+ const text = raw == null ? "" : String(raw);
36608
+ if (ARRAY_TYPES2.has(fieldType)) {
36609
+ if (text === "") return [];
36610
+ try {
36611
+ const parsed = JSON.parse(text);
36612
+ if (Array.isArray(parsed)) return parsed.map(String);
36613
+ } catch {
36614
+ }
36615
+ return text.split(",").map((v) => v.trim());
36616
+ }
36617
+ return text;
36618
+ }
36619
+ function isSqlValue(value) {
36620
+ return typeof value === "object" && value !== null && typeof value.type === "string";
36621
+ }
36622
+ function isEmptyDmlValue(value) {
36623
+ if (value == null || value === "") return true;
36624
+ if (Array.isArray(value)) return value.length === 0;
36625
+ if (isSqlValue(value)) {
36626
+ if (value.type === "STRING") return value.value === "";
36627
+ if (value.type === "ARRAY") return value.elements.length === 0;
36628
+ }
36629
+ return false;
36630
+ }
36631
+ function isEmpty(value) {
36632
+ return value === "" || Array.isArray(value) && value.length === 0;
36633
+ }
36634
+ function typeCode(type) {
36635
+ return type === "NUMBER" ? "ERR_TYPE_NUMBER" : "ERR_TYPE_DATE";
36636
+ }
36637
+ function isFiniteDecimal(value) {
36638
+ return /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(value.trim());
36639
+ }
36640
+ function compareDecimal(left, right) {
36641
+ const normalize = (input) => {
36642
+ let s = input.trim();
36643
+ let sign = 1;
36644
+ if (s.startsWith("-")) {
36645
+ sign = -1;
36646
+ s = s.slice(1);
36647
+ } else if (s.startsWith("+")) s = s.slice(1);
36648
+ let [whole, fraction = ""] = s.split(".");
36649
+ whole = (whole || "0").replace(/^0+(?=\d)/, "");
36650
+ fraction = fraction.replace(/0+$/, "");
36651
+ if (/^0*$/.test(whole) && fraction === "") sign = 1;
36652
+ return { sign, whole, fraction };
36653
+ };
36654
+ const a = normalize(left);
36655
+ const b = normalize(right);
36656
+ if (a.sign !== b.sign) return a.sign < b.sign ? -1 : 1;
36657
+ const direction = a.sign;
36658
+ if (a.whole.length !== b.whole.length) return a.whole.length < b.whole.length ? -direction : direction;
36659
+ if (a.whole !== b.whole) return a.whole < b.whole ? -direction : direction;
36660
+ const width = Math.max(a.fraction.length, b.fraction.length);
36661
+ const af = a.fraction.padEnd(width, "0");
36662
+ const bf = b.fraction.padEnd(width, "0");
36663
+ return af === bf ? 0 : af < bf ? -direction : direction;
36664
+ }
36665
+ function isValidTemporal(value, type) {
36666
+ if (type === "TIME") {
36667
+ const m2 = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value);
36668
+ return m2 !== null && Number(m2[1]) <= 23 && Number(m2[2]) <= 59 && Number(m2[3] ?? 0) <= 59;
36669
+ }
36670
+ const datePart = type === "DATE" ? value : value.slice(0, 10);
36671
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(datePart);
36672
+ if (!m) return false;
36673
+ const year = Number(m[1]);
36674
+ const month = Number(m[2]);
36675
+ const day = Number(m[3]);
36676
+ const date5 = new Date(Date.UTC(year, month - 1, day));
36677
+ if (date5.getUTCFullYear() !== year || date5.getUTCMonth() !== month - 1 || date5.getUTCDate() !== day) return false;
36678
+ if (type === "DATE") return true;
36679
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})$/.test(value) && isValidTemporal(value.slice(11, value.endsWith("Z") ? -1 : value.length - 6), "TIME");
36680
+ }
36681
+
36682
+ // src/core/dmlValidationCandidates.ts
36683
+ var VALIDATION_META_COLUMNS = [
36684
+ "$err_statement",
36685
+ "$err_operation",
36686
+ "$err_row",
36687
+ "$err_field",
36688
+ "$err_code",
36689
+ "$err_message"
36690
+ ];
36691
+ function validateDmlCandidates(candidates, operation, payloadFields, targetFields, fieldInfos, statementNumber) {
36692
+ const infoByCode = new Map(fieldInfos.map((field) => [field.code, field]));
36693
+ const errors = [];
36694
+ const invalid = /* @__PURE__ */ new Set();
36695
+ for (const candidate of candidates) {
36696
+ candidate.record ??= {};
36697
+ const rowErrors = [...candidate.preErrors];
36698
+ for (const code of targetFields) {
36699
+ const result = validateAndNormalizeDmlValue(candidate.payload.get(code), infoByCode.get(code));
36700
+ if (!result.ok) rowErrors.push({ field: code, code: result.code, message: result.message });
36701
+ else candidate.record[code] = { value: result.value };
36702
+ }
36703
+ if (candidate.mode === "create") {
36704
+ for (const info of fieldInfos) {
36705
+ if (info.inSubtable) continue;
36706
+ if (candidate.payload.has(info.code)) continue;
36707
+ const emptyDefault = isEmptyDmlValue(info.defaultValue);
36708
+ if (!emptyDefault) {
36709
+ const defaultResult = validateAndNormalizeDmlValue(info.defaultValue, info);
36710
+ if (!defaultResult.ok) rowErrors.push({
36711
+ field: info.code,
36712
+ code: defaultResult.code,
36713
+ message: `\u65E2\u5B9A\u5024: ${defaultResult.message}`
36714
+ });
36715
+ } else {
36716
+ const emptyResult = validateAndNormalizeDmlValue("", info);
36717
+ if (!emptyResult.ok) {
36718
+ rowErrors.push({ field: info.code, code: emptyResult.code, message: emptyResult.message });
36719
+ } else if (info.required) {
36720
+ rowErrors.push({ field: info.code, code: "ERR_REQUIRED", message: `${info.code} \u306F\u5FC5\u9808\u3067\u3059` });
36721
+ }
36722
+ }
36723
+ }
36724
+ }
36725
+ if (rowErrors.length > 0) invalid.add(candidate.rowNumber);
36726
+ for (const error51 of rowErrors) {
36727
+ const row = {};
36728
+ for (const field of payloadFields) row[field] = renderValidationValue(candidate.payload.get(field));
36729
+ row["$err_statement"] = String(statementNumber);
36730
+ row["$err_operation"] = operation;
36731
+ row["$err_row"] = String(candidate.rowNumber);
36732
+ row["$err_field"] = error51.field;
36733
+ row["$err_code"] = error51.code;
36734
+ row["$err_message"] = error51.message;
36735
+ errors.push(row);
36736
+ }
36737
+ }
36738
+ return { errors, invalidRows: invalid.size, invalidRowNumbers: invalid };
36739
+ }
36740
+ function renderValidationValue(value) {
36741
+ if (value == null) return "";
36742
+ if (typeof value === "object" && "type" in value) {
36743
+ const sql = value;
36744
+ if (sql.type === "STRING" || sql.type === "NUMBER") return String(sql.value ?? "");
36745
+ if (sql.type === "ARRAY") return JSON.stringify(sql.elements?.map((e) => e.value) ?? []);
36746
+ }
36747
+ if (Array.isArray(value)) return JSON.stringify(value);
36748
+ return String(value);
36749
+ }
36750
+
36398
36751
  // src/execute.ts
36399
36752
  var SEARCH_ABORTED_WARNING = "\u691C\u7D22\u304C 10 \u4E07\u4EF6\u3067\u6253\u3061\u5207\u3089\u308C\u3001\u7D50\u679C\u304C\u6B20\u843D\u3057\u305F\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002";
36400
36753
  var SearchAbortedError = class extends Error {
@@ -36498,6 +36851,15 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
36498
36851
  throw new Error(`ParseError: variable @${unresolved} is not defined in a batch.`);
36499
36852
  }
36500
36853
  validateKlikeStatement(stmt);
36854
+ if ("validateOnly" in stmt && stmt.validateOnly === true) {
36855
+ if (stmt.validationErrorTable) {
36856
+ throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
36857
+ }
36858
+ return executeDmlValidation(stmt, client, { ...options, onLimitReached: "error" }, cacheContext, void 0, 1);
36859
+ }
36860
+ if ("onErrorSkip" in stmt && stmt.onErrorSkip === true) {
36861
+ throw new Error("ArgumentError: ON ERROR SKIP requires a batch.");
36862
+ }
36501
36863
  switch (stmt.type) {
36502
36864
  case "SELECT":
36503
36865
  return executeSelect(stmt, client, options, cacheContext);
@@ -36539,6 +36901,17 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
36539
36901
  }
36540
36902
  }
36541
36903
  var TEMP_TABLE_MAX_ROWS = 1e4;
36904
+ function appendValidationErrors(tempTables, name, columns, rows, maxRows) {
36905
+ const current = tempTables.get(name);
36906
+ if (current && (current.columns.length !== columns.length || current.columns.some((c, i) => c !== columns[i]))) {
36907
+ throw new Error(`ArgumentError: validation error table ${name} has a different schema.`);
36908
+ }
36909
+ const existingRows = current?.rows ?? [];
36910
+ if (existingRows.length + rows.length > maxRows) {
36911
+ throw new Error(`ArgumentError: temp table ${name} exceeds max rows (${maxRows}).`);
36912
+ }
36913
+ tempTables.set(name, { columns: [...columns], rows: [...existingRows, ...rows] });
36914
+ }
36542
36915
  var BatchTimeoutError = class extends Error {
36543
36916
  constructor() {
36544
36917
  super("TimeoutError: batch timeout exceeded.");
@@ -36620,7 +36993,12 @@ async function executeBatch(sql, client, options = {}) {
36620
36993
  }
36621
36994
  results.push({ ...base, status: "success", ...outcome });
36622
36995
  } catch (e) {
36623
- results.push({ ...base, status: "error", error: toBatchStatementError(e) });
36996
+ results.push({
36997
+ ...base,
36998
+ status: "error",
36999
+ error: toBatchStatementError(e),
37000
+ ...e instanceof RejectLimitExceededError ? { result: e.diagnostic } : {}
37001
+ });
36624
37002
  failed.add(i);
36625
37003
  if (e instanceof BatchTimeoutError) {
36626
37004
  aborted2 = "timeout";
@@ -36679,6 +37057,38 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
36679
37057
  }
36680
37058
  const resolvedStmt = resolveVariableRefs(stmt, variables);
36681
37059
  validateKlikeStatement(resolvedStmt);
37060
+ if ("validateOnly" in resolvedStmt && resolvedStmt.validateOnly === true) {
37061
+ const result = await executeDmlValidation(
37062
+ resolvedStmt,
37063
+ client,
37064
+ { ...options, onLimitReached: "error" },
37065
+ cacheContext,
37066
+ tempTables,
37067
+ info.index + 1
37068
+ );
37069
+ if (resolvedStmt.validationErrorTable) {
37070
+ appendValidationErrors(
37071
+ tempTables,
37072
+ resolvedStmt.validationErrorTable,
37073
+ result.columns,
37074
+ result.errors,
37075
+ options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS
37076
+ );
37077
+ }
37078
+ return { result };
37079
+ }
37080
+ if ("onErrorSkip" in resolvedStmt && resolvedStmt.onErrorSkip === true) {
37081
+ return {
37082
+ result: await executeOnErrorSkip(
37083
+ resolvedStmt,
37084
+ client,
37085
+ { ...options, onLimitReached: "error" },
37086
+ cacheContext,
37087
+ tempTables,
37088
+ info.index + 1
37089
+ )
37090
+ };
37091
+ }
36682
37092
  if (resolvedStmt.type === "CREATE_TEMP_TABLE") {
36683
37093
  const materializeOptions = {
36684
37094
  ...options,
@@ -37827,7 +38237,7 @@ async function buildSortKindsForSelect(stmt, client, cacheContext) {
37827
38237
  }
37828
38238
  function convertProcessRowValue(raw, dstFieldType) {
37829
38239
  const USER_TYPES2 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
37830
- const ARRAY_TYPES2 = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
38240
+ const ARRAY_TYPES3 = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
37831
38241
  if (USER_TYPES2.has(dstFieldType ?? "")) {
37832
38242
  if (raw === "") return [];
37833
38243
  try {
@@ -37839,7 +38249,7 @@ function convertProcessRowValue(raw, dstFieldType) {
37839
38249
  }
37840
38250
  return raw.split(",").map((c) => ({ code: c.trim() }));
37841
38251
  }
37842
- if (ARRAY_TYPES2.has(dstFieldType ?? "")) {
38252
+ if (ARRAY_TYPES3.has(dstFieldType ?? "")) {
37843
38253
  if (raw === "") return [];
37844
38254
  try {
37845
38255
  const parsed = JSON.parse(raw);
@@ -37850,6 +38260,396 @@ function convertProcessRowValue(raw, dstFieldType) {
37850
38260
  }
37851
38261
  return raw;
37852
38262
  }
38263
+ var NON_WRITABLE_FIELD_TYPES = /* @__PURE__ */ new Set([
38264
+ "CALC",
38265
+ "RECORD_NUMBER",
38266
+ "CREATOR",
38267
+ "CREATED_TIME",
38268
+ "MODIFIER",
38269
+ "UPDATED_TIME",
38270
+ "STATUS",
38271
+ "STATUS_ASSIGNEE",
38272
+ "CATEGORY",
38273
+ "REFERENCE_TABLE"
38274
+ ]);
38275
+ async function executeDmlValidation(stmt, client, options, cacheContext, tempTables, statementNumber) {
38276
+ return (await prepareDmlValidation(stmt, client, options, cacheContext, tempTables, statementNumber)).result;
38277
+ }
38278
+ var RejectLimitExceededError = class extends Error {
38279
+ constructor(message, diagnostic) {
38280
+ super(`RejectLimitExceededError: ${message}`);
38281
+ this.diagnostic = diagnostic;
38282
+ this.name = "RejectLimitExceededError";
38283
+ }
38284
+ };
38285
+ async function prepareDmlValidation(stmt, client, options, cacheContext, tempTables, statementNumber) {
38286
+ const operation = stmt.type === "UPDATE" ? "UPDATE" : stmt.type.startsWith("UPSERT") ? "UPSERT" : "INSERT";
38287
+ const payloadFields = stmt.type === "UPDATE" ? ["$id", ...stmt.assignments.map((a) => a.field)] : [...stmt.fields];
38288
+ if (new Set(payloadFields).size !== payloadFields.length) {
38289
+ throw new Error("ArgumentError: DML target fields contain duplicates.");
38290
+ }
38291
+ const fieldInfos = await getFieldsCached(stmt.appId, client, cacheContext);
38292
+ const infoByCode = new Map(fieldInfos.map((field) => [field.code, field]));
38293
+ const targetFields = stmt.type === "UPDATE" ? stmt.assignments.map((a) => a.field) : stmt.fields;
38294
+ for (const code of targetFields) {
38295
+ const info = infoByCode.get(code);
38296
+ if (!info) throw new Error(`ArgumentError: DML target field ${code} does not exist.`);
38297
+ if (info.writable === false || NON_WRITABLE_FIELD_TYPES.has(info.fieldType)) {
38298
+ throw new Error(`ArgumentError: DML target field ${code} is not writable (${info.fieldType}).`);
38299
+ }
38300
+ }
38301
+ const candidates = await materializeValidationCandidates(stmt, operation, client, options, cacheContext, tempTables, infoByCode);
38302
+ const { errors, invalidRows, invalidRowNumbers } = validateDmlCandidates(
38303
+ candidates,
38304
+ operation,
38305
+ payloadFields,
38306
+ targetFields,
38307
+ fieldInfos,
38308
+ statementNumber
38309
+ );
38310
+ const columns = [...payloadFields, ...VALIDATION_META_COLUMNS];
38311
+ const result = {
38312
+ type: "VALIDATION",
38313
+ operation,
38314
+ validatedRows: candidates.length,
38315
+ validRows: candidates.length - invalidRows,
38316
+ invalidRows,
38317
+ errorCount: errors.length,
38318
+ columns,
38319
+ errors,
38320
+ ...stmt.validationErrorTable ? { errTable: stmt.validationErrorTable } : stmt.onErrorSkip && stmt.errorTable ? { errTable: stmt.errorTable } : {}
38321
+ };
38322
+ return { result, candidates, invalidRowNumbers };
38323
+ }
38324
+ async function executeOnErrorSkip(stmt, client, options, cacheContext, tempTables, statementNumber) {
38325
+ const prepared = await prepareDmlValidation(
38326
+ stmt,
38327
+ client,
38328
+ options,
38329
+ cacheContext,
38330
+ tempTables,
38331
+ statementNumber
38332
+ );
38333
+ const errTable = stmt.errorTable;
38334
+ if (!errTable) throw new Error("ArgumentError: ON ERROR SKIP requires INTO #error_table.");
38335
+ appendValidationErrors(
38336
+ tempTables,
38337
+ errTable,
38338
+ prepared.result.columns,
38339
+ prepared.result.errors,
38340
+ options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS
38341
+ );
38342
+ const rejectLimit = stmt.rejectLimit ?? null;
38343
+ if (rejectLimit !== null && prepared.result.invalidRows > rejectLimit) {
38344
+ throw new RejectLimitExceededError(
38345
+ `rejected rows (${prepared.result.invalidRows}) exceed REJECT LIMIT (${rejectLimit}).`,
38346
+ prepared.result
38347
+ );
38348
+ }
38349
+ const valid = prepared.candidates.filter((candidate) => !prepared.invalidRowNumbers.has(candidate.rowNumber));
38350
+ if (options.confirm) {
38351
+ const operation = stmt.type.startsWith("INSERT") ? "INSERT" : "UPDATE";
38352
+ const ok = await options.confirm(valid.length, operation);
38353
+ if (!ok) throw new OperationCancelledError(operation, valid.length);
38354
+ }
38355
+ const common = {
38356
+ affectedRows: valid.length,
38357
+ skippedRows: prepared.result.invalidRows,
38358
+ rejectLimit,
38359
+ errTable
38360
+ };
38361
+ if (stmt.type === "INSERT" || stmt.type === "INSERT_SELECT") {
38362
+ const createdIds = [];
38363
+ for (let i = 0; i < valid.length; i += 100) {
38364
+ const response = await client.postRecords({ app: stmt.appId, records: valid.slice(i, i + 100).map((c) => c.record) });
38365
+ createdIds.push(response.ids);
38366
+ }
38367
+ return { type: "INSERT", createdIds, insertedCount: createdIds.flat().length, ...common };
38368
+ }
38369
+ if (stmt.type === "UPDATE") {
38370
+ const updates2 = valid.map((candidate) => {
38371
+ if (candidate.targetId === void 0) throw new Error("InternalError: prepared UPDATE candidate has no targetId.");
38372
+ return { id: candidate.targetId, record: candidate.record };
38373
+ });
38374
+ for (let i = 0; i < updates2.length; i += 100) {
38375
+ await client.putRecords({ app: stmt.appId, records: updates2.slice(i, i + 100) });
38376
+ }
38377
+ return { type: "UPDATE", updatedCount: updates2.length, ...common };
38378
+ }
38379
+ const inserts = valid.filter((candidate) => candidate.mode === "create");
38380
+ const updates = valid.filter((candidate) => candidate.mode === "update").map((candidate) => {
38381
+ if (candidate.targetId === void 0) throw new Error("InternalError: prepared UPSERT candidate has no targetId.");
38382
+ return { id: candidate.targetId, record: candidate.record };
38383
+ });
38384
+ let insertedCount = 0;
38385
+ for (let i = 0; i < inserts.length; i += 100) {
38386
+ const response = await client.postRecords({ app: stmt.appId, records: inserts.slice(i, i + 100).map((c) => c.record) });
38387
+ insertedCount += response.ids.length;
38388
+ }
38389
+ for (let i = 0; i < updates.length; i += 100) {
38390
+ await client.putRecords({ app: stmt.appId, records: updates.slice(i, i + 100) });
38391
+ }
38392
+ return { type: "UPSERT", insertedCount, updatedCount: updates.length, ...common };
38393
+ }
38394
+ async function materializeValidationCandidates(stmt, operation, client, options, cacheContext, tempTables, infoByCode) {
38395
+ if (stmt.type === "UPDATE") return materializeUpdateValidationCandidates(stmt, client, options, cacheContext, tempTables);
38396
+ let rows;
38397
+ if (stmt.type === "INSERT" || stmt.type === "UPSERT") {
38398
+ rows = stmt.values.map((row) => row.map(
38399
+ (value, i) => value.type === "CASE_VALUE" ? evalCaseWhenValue(value.expr, {}, infoByCode.get(stmt.fields[i])?.fieldType) : value
38400
+ ));
38401
+ } else {
38402
+ const selectResult = tempTables && tempTables.size > 0 ? await executeQueryWithCte(stmt.select, client, { ...options, onLimitReached: "error" }, tempTables, cacheContext) : await executeSelect(stmt.select, client, { ...options, onLimitReached: "error" }, cacheContext);
38403
+ if (selectResult.columns.length !== stmt.fields.length) {
38404
+ throw new Error(`SELECT \u306E\u5217\u6570\uFF08${selectResult.columns.length}\uFF09\u3068 DML \u306E\u30D5\u30A3\u30FC\u30EB\u30C9\u6570\uFF08${stmt.fields.length}\uFF09\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093`);
38405
+ }
38406
+ rows = selectResult.rows.map((row) => selectResult.columns.map((column) => row[column] ?? ""));
38407
+ }
38408
+ const candidates = rows.map((values, index) => ({
38409
+ rowNumber: index + 1,
38410
+ operation,
38411
+ mode: "create",
38412
+ payload: new Map(stmt.fields.map((field, i) => [field, values[i]])),
38413
+ preErrors: [],
38414
+ record: {}
38415
+ }));
38416
+ if (stmt.type !== "UPSERT" && stmt.type !== "UPSERT_SELECT") return candidates;
38417
+ for (const key of stmt.keyFields) {
38418
+ if (!stmt.fields.includes(key)) throw new Error(`ON DUPLICATE \u306E\u30AD\u30FC\u300C${key}\u300D\u304C UPSERT \u30D5\u30A3\u30FC\u30EB\u30C9\u306B\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093`);
38419
+ }
38420
+ const fieldTypes = new Map([...infoByCode].map(([code, info]) => [code, info.fieldType]));
38421
+ const rowKeys = candidates.map((candidate) => stmt.keyFields.map((key) => renderValidationValue(candidate.payload.get(key))));
38422
+ const targets = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeys, client, options, fieldTypes);
38423
+ const numeric = stmt.keyFields.map((key) => fieldTypes.get(key) === "NUMBER");
38424
+ const keyCounts = /* @__PURE__ */ new Map();
38425
+ for (const parts of rowKeys) {
38426
+ const key = upsertNormalizedKey(parts, numeric);
38427
+ keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
38428
+ }
38429
+ candidates.forEach((candidate, index) => {
38430
+ const parts = rowKeys[index];
38431
+ const targetId = lookupUpsertTarget(targets, parts);
38432
+ candidate.mode = targetId === void 0 ? "create" : "update";
38433
+ if (targetId !== void 0) candidate.targetId = targetId;
38434
+ stmt.keyFields.forEach((key, keyIndex) => {
38435
+ if (parts[keyIndex] === "") candidate.preErrors.push({ field: key, code: "ERR_KEY_EMPTY", message: `UPSERT \u30AD\u30FC ${key} \u306F\u7A7A\u306B\u3067\u304D\u307E\u305B\u3093` });
38436
+ });
38437
+ if ((keyCounts.get(upsertNormalizedKey(parts, numeric)) ?? 0) > 1) {
38438
+ candidate.preErrors.push({ field: stmt.keyFields[0], code: "ERR_KEY_DUP_SOURCE", message: "UPSERT \u30BD\u30FC\u30B9\u5185\u3067\u30AD\u30FC\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059" });
38439
+ }
38440
+ });
38441
+ return candidates;
38442
+ }
38443
+ async function materializeUpdateValidationCandidates(stmt, client, options, cacheContext, tempTables) {
38444
+ if (stmt.from) return materializeUpdateFromValidationCandidates(stmt, stmt.from, client, options, cacheContext, tempTables);
38445
+ await resolveSetSubqueries(stmt.assignments, client, options, cacheContext);
38446
+ const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
38447
+ let records;
38448
+ if (hasArithAssignment(stmt)) {
38449
+ const getParams = updateToGetQueryForArith(stmt);
38450
+ const resolved = await fetchRecordsForSharedPlan(client.getRecords, getParams.app, getParams.query, [...getParams.fields], {
38451
+ maxRecords: options.maxRecords ?? 1e4,
38452
+ parallel: options.fetchParallel ?? 1,
38453
+ onLimit: "error"
38454
+ });
38455
+ records = updateToPutBatchesArith(stmt, resolved.records, fieldTypes).flatMap((batch) => batch.records);
38456
+ } else {
38457
+ const getParams = updateToGetQuery(stmt);
38458
+ const resolved = await resolveDmlTargetIds(client.getRecords, getParams.app, getParams.query, {
38459
+ maxRecords: options.maxRecords ?? 1e4,
38460
+ parallel: options.fetchParallel ?? 1
38461
+ });
38462
+ records = updateToPutBatches(stmt, resolved.ids, fieldTypes).flatMap((batch) => batch.records);
38463
+ }
38464
+ return records.sort((a, b) => a.id - b.id).map((entry, index) => ({
38465
+ rowNumber: index + 1,
38466
+ operation: "UPDATE",
38467
+ mode: "update",
38468
+ payload: new Map([["$id", String(entry.id)], ...stmt.assignments.map((a) => [a.field, entry.record[a.field]?.value ?? ""])]),
38469
+ preErrors: [],
38470
+ record: entry.record,
38471
+ targetId: entry.id
38472
+ }));
38473
+ }
38474
+ async function materializeUpdateFromValidationCandidates(stmt, from, client, options, cacheContext, tempTables) {
38475
+ const matched = await resolveUpdateFromMatchedRecords(stmt, from, client, options, cacheContext, tempTables);
38476
+ const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
38477
+ const records = updateFromToPutBatches(stmt, matched, fieldTypes).flatMap((batch) => batch.records);
38478
+ return records.sort((a, b) => a.id - b.id).map((entry, index) => ({
38479
+ rowNumber: index + 1,
38480
+ operation: "UPDATE",
38481
+ mode: "update",
38482
+ payload: new Map([["$id", String(entry.id)], ...stmt.assignments.map((a) => [a.field, entry.record[a.field]?.value ?? ""])]),
38483
+ preErrors: [],
38484
+ record: entry.record,
38485
+ targetId: entry.id
38486
+ }));
38487
+ }
38488
+ var UPDATE_FROM_KEY_CHUNK_SIZE = UPSERT_IN_CHUNK_SIZE;
38489
+ var UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES = /* @__PURE__ */ new Set([
38490
+ "CHECK_BOX",
38491
+ "MULTI_SELECT",
38492
+ "USER_SELECT",
38493
+ "ORGANIZATION_SELECT",
38494
+ "GROUP_SELECT",
38495
+ "FILE"
38496
+ ]);
38497
+ async function resolveUpdateFromMatchedRecords(stmt, from, client, options, cacheContext, tempTables) {
38498
+ const joinKind = await resolveUpdateFromTargetJoinKind(stmt, from, client, cacheContext);
38499
+ const sourceFields = [...new Set(stmt.assignments.filter((a) => a.value.type === "SOURCE_FIELD").map((a) => a.value.type === "SOURCE_FIELD" ? a.value.field : ""))];
38500
+ const requiredSourceFields = [.../* @__PURE__ */ new Set([from.joinKeyField, ...sourceFields])];
38501
+ const sourceRows = await loadUpdateFromSourceRows(
38502
+ from,
38503
+ requiredSourceFields,
38504
+ sourceFields,
38505
+ client,
38506
+ options,
38507
+ cacheContext,
38508
+ tempTables
38509
+ );
38510
+ const sourceByKey = /* @__PURE__ */ new Map();
38511
+ for (const row of sourceRows) {
38512
+ if (!Object.prototype.hasOwnProperty.call(row, from.joinKeyField)) {
38513
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${from.joinKeyField} does not exist.`);
38514
+ }
38515
+ const key = normalizeUpdateFromJoinKey(row[from.joinKeyField], joinKind, "source");
38516
+ if (sourceByKey.has(key)) {
38517
+ throw new Error(`ArgumentError: UPDATE ... FROM source has multiple rows for normalized key ${key}.`);
38518
+ }
38519
+ sourceByKey.set(key, row);
38520
+ }
38521
+ if (sourceByKey.size === 0) return [];
38522
+ const maxRecords2 = options.maxRecords ?? 1e4;
38523
+ const targetFields = collectUpdateFromTargetFields(stmt);
38524
+ const filterQuery = from.targetFilter === null ? "" : updateToGetQuery({ ...stmt, from: null, where: from.targetFilter }).query;
38525
+ const targetRecords = [];
38526
+ const seenTargetIds = /* @__PURE__ */ new Set();
38527
+ let fetchedTargetCount = 0;
38528
+ for (const keys of splitChunks([...sourceByKey.keys()], UPDATE_FROM_KEY_CHUNK_SIZE)) {
38529
+ const keyQuery = `${from.targetJoinField} in (${keys.map(sqlQuote).join(",")})`;
38530
+ const query = filterQuery ? `(${keyQuery}) and (${filterQuery})` : keyQuery;
38531
+ const resolved = await fetchRecordsForSharedPlan(
38532
+ client.getRecords,
38533
+ stmt.appId,
38534
+ query,
38535
+ targetFields,
38536
+ { maxRecords: maxRecords2, parallel: options.fetchParallel ?? 1, onLimit: "error" }
38537
+ );
38538
+ fetchedTargetCount += resolved.records.length;
38539
+ if (fetchedTargetCount > maxRecords2) {
38540
+ throw new FetchAllLimitError(
38541
+ `\u53D6\u5F97\u4EF6\u6570\u304C\u4E0A\u9650\uFF08${maxRecords2} \u4EF6\uFF09\u3092\u8D85\u3048\u307E\u3057\u305F\u3002WHERE \u53E5\u3067\u7D5E\u308A\u8FBC\u3080\u304B\u3001maxRecords \u3092\u5F15\u304D\u4E0A\u3052\u3066\u304F\u3060\u3055\u3044\u3002`
38542
+ );
38543
+ }
38544
+ for (const record2 of resolved.records) {
38545
+ const id = record2["$id"]?.value;
38546
+ if (typeof id !== "string" || id === "") {
38547
+ throw new Error("ArgumentError: UPDATE ... FROM target record does not contain a valid $id.");
38548
+ }
38549
+ if (seenTargetIds.has(id)) continue;
38550
+ seenTargetIds.add(id);
38551
+ targetRecords.push(record2);
38552
+ }
38553
+ }
38554
+ const matched = [];
38555
+ for (const target of targetRecords) {
38556
+ const raw = target[from.targetJoinField]?.value;
38557
+ const key = normalizeUpdateFromJoinKey(raw, joinKind, "target");
38558
+ if (key === null) continue;
38559
+ const source = sourceByKey.get(key);
38560
+ if (source !== void 0) matched.push({ target, source });
38561
+ }
38562
+ return matched;
38563
+ }
38564
+ async function resolveUpdateFromTargetJoinKind(stmt, from, client, cacheContext) {
38565
+ if (from.targetJoinField === "$id") return "id";
38566
+ const info = (await getFieldsCached(stmt.appId, client, cacheContext)).find((field) => field.code === from.targetJoinField);
38567
+ if (!info) {
38568
+ throw new Error(`ArgumentError: UPDATE ... FROM target column ${from.targetJoinField} does not exist.`);
38569
+ }
38570
+ if (info.inSubtable || info.writable === false || info.fieldType !== "SINGLE_LINE_TEXT" && info.fieldType !== "NUMBER") {
38571
+ throw new Error(
38572
+ `ArgumentError: UPDATE ... FROM does not support target join field type ${info.fieldType} (${from.targetJoinField}).`
38573
+ );
38574
+ }
38575
+ return info.fieldType === "NUMBER" ? "number" : "string";
38576
+ }
38577
+ async function loadUpdateFromSourceRows(from, requiredSourceFields, sourceValueFields, client, options, cacheContext, tempTables) {
38578
+ if (from.cteName !== null) {
38579
+ const table = tempTables?.get(from.cteName);
38580
+ if (!table) throw new Error(`ArgumentError: temp table ${from.cteName} is not available.`);
38581
+ for (const field of requiredSourceFields) {
38582
+ if (!table.columns.includes(field)) {
38583
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
38584
+ }
38585
+ }
38586
+ return table.rows;
38587
+ }
38588
+ const sourceTypes = await getFieldTypeMap(from.appId, client, cacheContext);
38589
+ const joinType = from.joinKeyField === "$id" ? "RECORD_NUMBER" : sourceTypes.get(from.joinKeyField);
38590
+ if (joinType === void 0) {
38591
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${from.joinKeyField} does not exist.`);
38592
+ }
38593
+ if (from.joinKeyField !== "$id" && joinType !== "SINGLE_LINE_TEXT" && joinType !== "NUMBER") {
38594
+ throw new Error(
38595
+ `ArgumentError: UPDATE ... FROM does not support source join field type ${joinType} (${from.joinKeyField}).`
38596
+ );
38597
+ }
38598
+ for (const field of sourceValueFields) {
38599
+ if (field !== "$id" && !sourceTypes.has(field)) {
38600
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
38601
+ }
38602
+ const type = field === "$id" ? "RECORD_NUMBER" : sourceTypes.get(field);
38603
+ if (UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES.has(type ?? "")) {
38604
+ throw new Error(`ArgumentError: UPDATE ... FROM does not support source field type ${type} (${field}).`);
38605
+ }
38606
+ }
38607
+ const resolved = await fetchRecordsForSharedPlan(
38608
+ client.getRecords,
38609
+ from.appId,
38610
+ "",
38611
+ requiredSourceFields,
38612
+ { maxRecords: options.maxRecords ?? 1e4, parallel: options.fetchParallel ?? 1, onLimit: "error" }
38613
+ );
38614
+ return resolved.records.map((record2) => flatten(record2, null));
38615
+ }
38616
+ function normalizeUpdateFromJoinKey(raw, kind, side) {
38617
+ if (typeof raw !== "string") {
38618
+ throw new Error(`ArgumentError: UPDATE ... FROM ${side} key must be a scalar string: ${String(raw)}`);
38619
+ }
38620
+ if (kind === "string") {
38621
+ if (raw === "") {
38622
+ if (side === "target") return null;
38623
+ throw new Error("ArgumentError: UPDATE ... FROM source key must not be empty.");
38624
+ }
38625
+ return raw;
38626
+ }
38627
+ if (kind === "number" && side === "target" && raw === "") return null;
38628
+ if (kind === "id") {
38629
+ const text2 = raw.trim();
38630
+ const id = Number(text2);
38631
+ if (text2 === "" || !Number.isSafeInteger(id) || id <= 0) {
38632
+ throw new Error(`ArgumentError: UPDATE ... FROM ${side} key must be a positive safe integer: ${raw}`);
38633
+ }
38634
+ return String(id);
38635
+ }
38636
+ const text = raw.trim();
38637
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(text)) {
38638
+ throw new Error(`ArgumentError: UPDATE ... FROM ${side} key must be a finite decimal: ${raw}`);
38639
+ }
38640
+ let unsigned = text;
38641
+ let negative = false;
38642
+ if (unsigned.startsWith("-") || unsigned.startsWith("+")) {
38643
+ negative = unsigned[0] === "-";
38644
+ unsigned = unsigned.slice(1);
38645
+ }
38646
+ let [whole, fraction = ""] = unsigned.split(".");
38647
+ whole = (whole || "0").replace(/^0+(?=\d)/, "");
38648
+ fraction = fraction.replace(/0+$/, "");
38649
+ const zero = /^0*$/.test(whole) && fraction === "";
38650
+ const canonical = fraction === "" ? whole : `${whole}.${fraction}`;
38651
+ return negative && !zero ? `-${canonical}` : canonical;
38652
+ }
37853
38653
  async function executeInsert(stmt, client, options, cacheContext) {
37854
38654
  if (stmt.subtableCode) {
37855
38655
  return executeInsertSubtable(stmt, client, options, cacheContext);
@@ -37949,98 +38749,20 @@ async function executeUpdate(stmt, client, options, cacheContext, tempTables) {
37949
38749
  }
37950
38750
  return { type: "UPDATE", updatedCount: ids.length };
37951
38751
  }
37952
- var UPDATE_FROM_ID_CHUNK_SIZE = 50;
37953
- var UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES = /* @__PURE__ */ new Set([
37954
- "CHECK_BOX",
37955
- "MULTI_SELECT",
37956
- "USER_SELECT",
37957
- "ORGANIZATION_SELECT",
37958
- "GROUP_SELECT",
37959
- "FILE"
37960
- ]);
37961
38752
  async function executeUpdateFrom(stmt, from, client, options, cacheContext, tempTables) {
37962
- const sourceFields = [...new Set(stmt.assignments.filter((a) => a.value.type === "SOURCE_FIELD").map((a) => a.value.type === "SOURCE_FIELD" ? a.value.field : ""))];
37963
- const requiredSourceFields = [.../* @__PURE__ */ new Set([from.joinKeyField, ...sourceFields])];
37964
- let sourceRows;
37965
- if (from.cteName !== null) {
37966
- const table = tempTables?.get(from.cteName);
37967
- if (!table) throw new Error(`ArgumentError: temp table ${from.cteName} is not available.`);
37968
- for (const field of requiredSourceFields) {
37969
- if (!table.columns.includes(field)) {
37970
- throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
37971
- }
37972
- }
37973
- sourceRows = table.rows;
37974
- } else {
37975
- const sourceTypes = await getFieldTypeMap(from.appId, client, cacheContext);
37976
- for (const field of requiredSourceFields) {
37977
- if (field !== "$id" && !sourceTypes.has(field)) {
37978
- throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
37979
- }
37980
- const type = sourceTypes.get(field);
37981
- if (UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES.has(type ?? "")) {
37982
- throw new Error(`ArgumentError: UPDATE ... FROM does not support source field type ${type} (${field}).`);
37983
- }
37984
- }
37985
- const maxRecords2 = options.maxRecords ?? 1e4;
37986
- const resolved = await fetchRecordsForSharedPlan(
37987
- client.getRecords,
37988
- from.appId,
37989
- "",
37990
- requiredSourceFields,
37991
- { maxRecords: maxRecords2, parallel: options.fetchParallel ?? 1, onLimit: "error" }
37992
- );
37993
- sourceRows = resolved.records.map((record2) => flatten(record2, null));
37994
- }
37995
- const sourceById = /* @__PURE__ */ new Map();
37996
- for (const row of sourceRows) {
37997
- if (!Object.prototype.hasOwnProperty.call(row, from.joinKeyField)) {
37998
- throw new Error(`ArgumentError: UPDATE ... FROM source column ${from.joinKeyField} does not exist.`);
37999
- }
38000
- const raw = row[from.joinKeyField];
38001
- const text = typeof raw === "string" ? raw.trim() : "";
38002
- const id = Number(text);
38003
- if (text === "" || !Number.isSafeInteger(id) || id <= 0) {
38004
- throw new Error(`ArgumentError: UPDATE ... FROM source key must be a positive safe integer: ${String(raw)}`);
38005
- }
38006
- if (sourceById.has(id)) {
38007
- throw new Error(`ArgumentError: UPDATE ... FROM source has multiple rows for target $id ${id}.`);
38008
- }
38009
- sourceById.set(id, row);
38010
- }
38011
- const targetIds = [...sourceById.keys()];
38012
- const targetFields = collectUpdateFromTargetFields(stmt);
38013
- const filterQuery = from.targetFilter === null ? "" : updateToGetQuery({ ...stmt, from: null, where: from.targetFilter }).query;
38014
- const targetRecords = [];
38015
- for (const ids of splitChunks(targetIds, UPDATE_FROM_ID_CHUNK_SIZE)) {
38016
- const idQuery = `$id in (${ids.map((id) => sqlQuote(String(id))).join(",")})`;
38017
- const query = filterQuery ? `(${idQuery}) and (${filterQuery})` : idQuery;
38018
- const resolved = await fetchRecordsForSharedPlan(
38019
- client.getRecords,
38020
- stmt.appId,
38021
- query,
38022
- targetFields,
38023
- { maxRecords: Math.max(ids.length, 1), parallel: options.fetchParallel ?? 1, onLimit: "error" }
38024
- );
38025
- targetRecords.push(...resolved.records);
38026
- }
38753
+ const matched = await resolveUpdateFromMatchedRecords(stmt, from, client, options, cacheContext, tempTables);
38027
38754
  if (options.confirm) {
38028
- const ok = await options.confirm(targetRecords.length, "UPDATE");
38029
- if (!ok) throw new OperationCancelledError("UPDATE", targetRecords.length);
38755
+ const ok = await options.confirm(matched.length, "UPDATE");
38756
+ if (!ok) throw new OperationCancelledError("UPDATE", matched.length);
38030
38757
  }
38031
38758
  const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
38032
- const matched = targetRecords.map((target) => {
38033
- const id = Number(target["$id"]?.value);
38034
- const source = sourceById.get(id);
38035
- if (!source) throw new Error(`ArgumentError: UPDATE ... FROM could not resolve source row for target $id ${id}.`);
38036
- return { target, source };
38037
- });
38038
38759
  const batches = updateFromToPutBatches(stmt, matched, fieldTypes);
38039
38760
  for (const batch of batches) await client.putRecords(batch);
38040
- return { type: "UPDATE", updatedCount: targetRecords.length };
38761
+ return { type: "UPDATE", updatedCount: matched.length };
38041
38762
  }
38042
38763
  function collectUpdateFromTargetFields(stmt) {
38043
38764
  const fields = /* @__PURE__ */ new Set(["$id"]);
38765
+ if (stmt.from) fields.add(stmt.from.targetJoinField);
38044
38766
  const visit = (node) => {
38045
38767
  if (Array.isArray(node)) {
38046
38768
  node.forEach(visit);
@@ -38986,7 +39708,7 @@ function buildUpdatePlan(stmt, label) {
38986
39708
  if (stmt.from) {
38987
39709
  const source = stmt.from.cteName ?? `APP${stmt.from.appId}`;
38988
39710
  lines.push(` source: ${source} AS ${stmt.from.alias}`);
38989
- lines.push(` join: APP${stmt.appId}.$id = ${stmt.from.alias}.${stmt.from.joinKeyField}`);
39711
+ lines.push(` join: APP${stmt.appId}.${stmt.from.targetJoinField} = ${stmt.from.alias}.${stmt.from.joinKeyField}`);
38990
39712
  lines.push(` target filter: ${stmt.from.targetFilter ? safeWhereToKintone(stmt.from.targetFilter) : "(none)"}`);
38991
39713
  } else {
38992
39714
  lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
@@ -39143,12 +39865,32 @@ function parseSqlStatements(sql) {
39143
39865
  // src/output/batchEnvelope.ts
39144
39866
  function toMutationSummary(result) {
39145
39867
  if (result.type === "INSERT") {
39146
- return { insertedCount: result.insertedCount, createdIds: result.createdIds };
39868
+ return {
39869
+ insertedCount: result.insertedCount,
39870
+ createdIds: result.createdIds,
39871
+ ...result.affectedRows !== void 0 ? { affectedRows: result.affectedRows } : {},
39872
+ ...result.skippedRows !== void 0 ? { skippedRows: result.skippedRows } : {},
39873
+ ...result.rejectLimit !== void 0 ? { rejectLimit: result.rejectLimit } : {},
39874
+ ...result.errTable !== void 0 ? { errTable: result.errTable } : {}
39875
+ };
39147
39876
  }
39148
- if (result.type === "UPDATE") return { updatedCount: result.updatedCount };
39877
+ if (result.type === "UPDATE") return {
39878
+ updatedCount: result.updatedCount,
39879
+ ...result.affectedRows !== void 0 ? { affectedRows: result.affectedRows } : {},
39880
+ ...result.skippedRows !== void 0 ? { skippedRows: result.skippedRows } : {},
39881
+ ...result.rejectLimit !== void 0 ? { rejectLimit: result.rejectLimit } : {},
39882
+ ...result.errTable !== void 0 ? { errTable: result.errTable } : {}
39883
+ };
39149
39884
  if (result.type === "DELETE") return { deletedCount: result.deletedCount };
39150
39885
  if (result.type === "UPSERT") {
39151
- return { insertedCount: result.insertedCount, updatedCount: result.updatedCount };
39886
+ return {
39887
+ insertedCount: result.insertedCount,
39888
+ updatedCount: result.updatedCount,
39889
+ ...result.affectedRows !== void 0 ? { affectedRows: result.affectedRows } : {},
39890
+ ...result.skippedRows !== void 0 ? { skippedRows: result.skippedRows } : {},
39891
+ ...result.rejectLimit !== void 0 ? { rejectLimit: result.rejectLimit } : {},
39892
+ ...result.errTable !== void 0 ? { errTable: result.errTable } : {}
39893
+ };
39152
39894
  }
39153
39895
  return { reorderedParentCount: result.reorderedParentCount };
39154
39896
  }
@@ -39175,11 +39917,31 @@ function buildBatchEnvelope(batch, options = {}) {
39175
39917
  }
39176
39918
  entry.resultIndex = results.length;
39177
39919
  results.push({
39920
+ type: "SELECT",
39178
39921
  columns: s.result.columns,
39179
39922
  rows: s.result.rows,
39180
39923
  rowCount: s.result.rowCount,
39181
39924
  warnings: s.result.warnings ?? []
39182
39925
  });
39926
+ } else if (s.result?.type === "VALIDATION") {
39927
+ totalRows += s.result.errorCount;
39928
+ if (maxTotalRecords !== void 0 && totalRows > maxTotalRecords) {
39929
+ throw new Error(`ArgumentError: batch total rows (${totalRows}) exceed maxTotalRecords (${maxTotalRecords}).`);
39930
+ }
39931
+ entry.resultIndex = results.length;
39932
+ results.push({
39933
+ type: "VALIDATION",
39934
+ columns: s.result.columns,
39935
+ rows: s.result.errors,
39936
+ rowCount: s.result.errorCount,
39937
+ warnings: [],
39938
+ operation: s.result.operation,
39939
+ validatedRows: s.result.validatedRows,
39940
+ validRows: s.result.validRows,
39941
+ invalidRows: s.result.invalidRows,
39942
+ errorCount: s.result.errorCount,
39943
+ ...s.result.errTable ? { errTable: s.result.errTable } : {}
39944
+ });
39183
39945
  } else if (s.status === "success" && s.result && s.result.type !== "SELECT" && s.result.type !== "ASSERT") {
39184
39946
  Object.assign(entry, toMutationSummary(s.result));
39185
39947
  }
@@ -39537,6 +40299,9 @@ function clampInt(v, min, max) {
39537
40299
 
39538
40300
  // src/core/formFieldInfo.ts
39539
40301
  function flattenFormFieldProperties(properties) {
40302
+ return flattenFields(properties, collectLookupCopyFields(properties));
40303
+ }
40304
+ function flattenFields(properties, lookupCopyFields, inSubtable = false) {
39540
40305
  const out = [];
39541
40306
  for (const field of Object.values(properties)) {
39542
40307
  out.push({
@@ -39544,12 +40309,49 @@ function flattenFormFieldProperties(properties) {
39544
40309
  label: field.label,
39545
40310
  fieldType: field.type,
39546
40311
  optionOrder: toOptionOrderMap(field.options),
39547
- sortKind: detectSortKind(field.type, field.format)
40312
+ sortKind: detectSortKind(field.type, field.format),
40313
+ required: field.required,
40314
+ minValue: normalizeConstraintValue(field.minValue),
40315
+ maxValue: normalizeConstraintValue(field.maxValue),
40316
+ minLength: normalizeConstraintValue(field.minLength),
40317
+ maxLength: normalizeConstraintValue(field.maxLength),
40318
+ defaultValue: field.defaultValue,
40319
+ inSubtable,
40320
+ writable: !lookupCopyFields.has(field.code) && !NON_WRITABLE_FIELD_TYPES2.has(field.type)
39548
40321
  });
39549
- if (field.fields) out.push(...flattenFormFieldProperties(field.fields));
40322
+ if (field.fields) out.push(...flattenFields(field.fields, lookupCopyFields, true));
39550
40323
  }
39551
40324
  return out;
39552
40325
  }
40326
+ var NON_WRITABLE_FIELD_TYPES2 = /* @__PURE__ */ new Set([
40327
+ "CALC",
40328
+ "RECORD_NUMBER",
40329
+ "CREATOR",
40330
+ "CREATED_TIME",
40331
+ "MODIFIER",
40332
+ "UPDATED_TIME",
40333
+ "STATUS",
40334
+ "STATUS_ASSIGNEE",
40335
+ "CATEGORY",
40336
+ "REFERENCE_TABLE",
40337
+ "SUBTABLE"
40338
+ ]);
40339
+ function collectLookupCopyFields(properties) {
40340
+ const result = /* @__PURE__ */ new Set();
40341
+ const visit = (fields) => {
40342
+ for (const field of Object.values(fields)) {
40343
+ for (const mapping of field.lookup?.fieldMappings ?? []) {
40344
+ if (mapping.field) result.add(mapping.field);
40345
+ }
40346
+ if (field.fields) visit(field.fields);
40347
+ }
40348
+ };
40349
+ visit(properties);
40350
+ return result;
40351
+ }
40352
+ function normalizeConstraintValue(value) {
40353
+ return value == null || value === "" ? void 0 : value;
40354
+ }
39553
40355
  function toOptionOrderMap(options) {
39554
40356
  if (!options || typeof options !== "object") return void 0;
39555
40357
  const order = {};
@@ -40573,20 +41375,42 @@ function toAssertPayload(result) {
40573
41375
  condition: result.condition
40574
41376
  };
40575
41377
  }
41378
+ function toDmlValidationPayload(result) {
41379
+ return {
41380
+ ok: true,
41381
+ type: result.type,
41382
+ operation: result.operation,
41383
+ validatedRows: result.validatedRows,
41384
+ validRows: result.validRows,
41385
+ invalidRows: result.invalidRows,
41386
+ errorCount: result.errorCount,
41387
+ columns: result.columns,
41388
+ errors: result.errors,
41389
+ ...result.errTable ? { errTable: result.errTable } : {}
41390
+ };
41391
+ }
40576
41392
  function toMutationPayload(result) {
40577
41393
  if (result.type === "INSERT") {
40578
41394
  return {
40579
41395
  ok: true,
40580
41396
  type: result.type,
40581
41397
  insertedCount: result.insertedCount,
40582
- createdIds: result.createdIds
41398
+ createdIds: result.createdIds,
41399
+ ...result.affectedRows !== void 0 ? { affectedRows: result.affectedRows } : {},
41400
+ ...result.skippedRows !== void 0 ? { skippedRows: result.skippedRows } : {},
41401
+ ...result.rejectLimit !== void 0 ? { rejectLimit: result.rejectLimit } : {},
41402
+ ...result.errTable !== void 0 ? { errTable: result.errTable } : {}
40583
41403
  };
40584
41404
  }
40585
41405
  if (result.type === "UPDATE") {
40586
41406
  return {
40587
41407
  ok: true,
40588
41408
  type: result.type,
40589
- updatedCount: result.updatedCount
41409
+ updatedCount: result.updatedCount,
41410
+ ...result.affectedRows !== void 0 ? { affectedRows: result.affectedRows } : {},
41411
+ ...result.skippedRows !== void 0 ? { skippedRows: result.skippedRows } : {},
41412
+ ...result.rejectLimit !== void 0 ? { rejectLimit: result.rejectLimit } : {},
41413
+ ...result.errTable !== void 0 ? { errTable: result.errTable } : {}
40590
41414
  };
40591
41415
  }
40592
41416
  if (result.type === "DELETE") {
@@ -40601,7 +41425,11 @@ function toMutationPayload(result) {
40601
41425
  ok: true,
40602
41426
  type: result.type,
40603
41427
  insertedCount: result.insertedCount,
40604
- updatedCount: result.updatedCount
41428
+ updatedCount: result.updatedCount,
41429
+ ...result.affectedRows !== void 0 ? { affectedRows: result.affectedRows } : {},
41430
+ ...result.skippedRows !== void 0 ? { skippedRows: result.skippedRows } : {},
41431
+ ...result.rejectLimit !== void 0 ? { rejectLimit: result.rejectLimit } : {},
41432
+ ...result.errTable !== void 0 ? { errTable: result.errTable } : {}
40605
41433
  };
40606
41434
  }
40607
41435
  return {
@@ -40637,7 +41465,7 @@ function requireDmlApproval(input, toolName, suffix = "") {
40637
41465
  }
40638
41466
  function containsSelectBasedDml(statements) {
40639
41467
  return statements.some(
40640
- (s) => s.statementType === "INSERT_SELECT" || s.statementType === "UPSERT_SELECT" || s.isUpdateFrom === true
41468
+ (s) => s.statementType === "INSERT_SELECT" || s.statementType === "UPSERT_SELECT" || s.isUpdateFrom === true || s.isOnErrorSkip === true
40641
41469
  );
40642
41470
  }
40643
41471
  function resolveMutateRuntimeMaxRecords(statements, dmlMaxRows) {
@@ -40700,13 +41528,18 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
40700
41528
  tempTablesDropped: s2.tempTablesDropped,
40701
41529
  tempOnlySource: s2.tempOnlySource,
40702
41530
  targetAppId: s2.targetAppId,
40703
- isUpdateFrom: s2.isUpdateFrom
41531
+ isUpdateFrom: s2.isUpdateFrom,
41532
+ isValidationOnly: s2.isValidationOnly,
41533
+ isOnErrorSkip: s2.isOnErrorSkip,
41534
+ requiresCompleteInput: s2.requiresCompleteInput
40704
41535
  }));
40705
41536
  const common = {
40706
41537
  ok: true,
40707
41538
  statementCount: analysis.statementCount,
40708
41539
  isReadOnlyBatch: analysis.isReadOnlyBatch,
40709
41540
  containsDml: analysis.containsDml,
41541
+ containsValidationOnly: analysis.containsValidationOnly,
41542
+ requiresCompleteInput: analysis.requiresCompleteInput,
40710
41543
  tempTables: analysis.tempTables,
40711
41544
  canRunWithQueryTool: analysis.isReadOnlyBatch,
40712
41545
  requiresMutationTool: analysis.containsDml,
@@ -40780,7 +41613,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
40780
41613
  profile: input.profile,
40781
41614
  maxRecords: input.maxRecords,
40782
41615
  fetchParallel: input.fetchParallel,
40783
- onLimit: input.onLimit,
41616
+ onLimit: validation.requiresCompleteInput ? "error" : input.onLimit,
40784
41617
  timeout: input.timeout,
40785
41618
  tempTableMaxRows: input.tempTableMaxRows
40786
41619
  });
@@ -40813,6 +41646,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
40813
41646
  cacheContext: validation.cacheContext
40814
41647
  });
40815
41648
  if (result2.type === "ASSERT") return toAssertPayload(result2);
41649
+ if (result2.type === "VALIDATION") return toDmlValidationPayload(result2);
40816
41650
  if (result2.type !== "SELECT") {
40817
41651
  throw new Error(`ArgumentError: read-only query returned unexpected result type ${result2.type}.`);
40818
41652
  }
@@ -40824,7 +41658,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
40824
41658
  profile: input.profile,
40825
41659
  maxRecords: input.maxRecords,
40826
41660
  fetchParallel: input.fetchParallel,
40827
- onLimit: input.onLimit,
41661
+ onLimit: validation.requiresCompleteInput ? "error" : input.onLimit,
40828
41662
  timeout: input.timeout
40829
41663
  });
40830
41664
  const result = await executeSql(runtime.sql, runtime.client, {
@@ -40834,6 +41668,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
40834
41668
  cacheContext: runtime.cacheContext
40835
41669
  });
40836
41670
  if (result.type === "ASSERT") return toAssertPayload(result);
41671
+ if (result.type === "VALIDATION") return toDmlValidationPayload(result);
40837
41672
  if (result.type !== "SELECT") {
40838
41673
  throw new Error(`ArgumentError: read-only query returned unexpected result type ${result.type}.`);
40839
41674
  }
@@ -40850,12 +41685,12 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
40850
41685
  if ((s.statementType === "UPDATE" || s.statementType === "DELETE") && !s.hasWhere) {
40851
41686
  throw new Error(`ArgumentError: ${s.statementType} without WHERE is blocked by ksql_mutate.${at}`);
40852
41687
  }
40853
- if (s.insertValuesCount !== null && s.insertValuesCount > dmlMaxRows) {
41688
+ if (!s.isOnErrorSkip && s.insertValuesCount !== null && s.insertValuesCount > dmlMaxRows) {
40854
41689
  throw new Error(
40855
41690
  `ArgumentError: INSERT rows (${s.insertValuesCount}) exceed dmlMaxRows (${dmlMaxRows}).${at}`
40856
41691
  );
40857
41692
  }
40858
- staticInsertTotal += s.insertValuesCount ?? 0;
41693
+ if (!s.isOnErrorSkip) staticInsertTotal += s.insertValuesCount ?? 0;
40859
41694
  }
40860
41695
  const dmlTotalMaxRows = input.dmlTotalMaxRows;
40861
41696
  if (dmlTotalMaxRows !== void 0 && staticInsertTotal > dmlTotalMaxRows) {
@@ -40960,7 +41795,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
40960
41795
  } catch (err) {
40961
41796
  throw selectBasedDml ? appendSelectBasedDmlReadLimitHint(err) : err;
40962
41797
  }
40963
- if (result.type === "SELECT" || result.type === "ASSERT") {
41798
+ if (result.type === "SELECT" || result.type === "ASSERT" || result.type === "VALIDATION") {
40964
41799
  throw new Error(`ArgumentError: ksql_mutate returned unexpected result type ${result.type}.`);
40965
41800
  }
40966
41801
  return toMutationPayload(result);
@@ -41119,7 +41954,7 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
41119
41954
  var profile = external_exports.string().min(1).describe("kintone connection profile name from ksql.config.json (default: the server's default profile).").optional();
41120
41955
  var maxRecords = external_exports.number().int().positive().describe("Maximum records fetched per SELECT (default 500).").optional();
41121
41956
  var fetchParallel = external_exports.number().int().min(1).max(10).describe("Number of parallel kintone record-fetch requests (1-10).").optional();
41122
- var onLimit = external_exports.enum(["error", "truncate"]).describe("Behavior when maxRecords is exceeded: 'error' rejects, 'truncate' returns the first maxRecords rows (default 'error').").optional();
41957
+ var onLimit = external_exports.enum(["error", "truncate"]).describe("Behavior when maxRecords is exceeded: 'error' rejects, 'truncate' returns the first maxRecords rows (default 'error'). VALIDATE ONLY always requires complete input and therefore overrides 'truncate' to 'error'.").optional();
41123
41958
  var tempTableMaxRows = external_exports.number().int().positive().describe("Per-temp-table cap on materialized rows for CREATE TEMP TABLE ... AS SELECT (default 10000). Overflow always errors \u2014 'truncate' never applies to temp tables, so downstream statements never see silently truncated data. Raising this increases memory use (up to 16 temp tables per batch); prefer narrowing the SELECT with WHERE.").optional();
41124
41959
  var timeout = external_exports.number().int().positive().describe("Request timeout in milliseconds. For multi-statement batches this also acts as the total batch deadline.").optional();
41125
41960
  var savedQueryName = external_exports.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/).describe("Saved query name (alphanumeric, '_' and '-', up to 64 chars).");
@@ -41243,7 +42078,7 @@ Options:
41243
42078
  -h, --help Show help
41244
42079
  `);
41245
42080
  }
41246
- var SERVER_VERSION = true ? "2.12.0" : "0.0.0-dev";
42081
+ var SERVER_VERSION = true ? "2.13.0" : "0.0.0-dev";
41247
42082
  function createServer(args) {
41248
42083
  const server = new McpServer({
41249
42084
  name: "ksql-mcp",
@@ -41265,12 +42100,12 @@ function createServer(args) {
41265
42100
  }, tools.explainTool);
41266
42101
  server.registerTool("ksql_query", {
41267
42102
  title: "Run read-only kSQL",
41268
- description: "Execute read-only kSQL: SELECT, WITH, UNION, EXPLAIN, SHOW APPS, DESCRIBE, ASSERT. Supports multi-statement batches with temp tables (CREATE TEMP TABLE #t AS SELECT ...; SELECT ... FROM #t;). ASSERT <expr> <op> <expr> (or BETWEEN) is a runtime gate: on failure it raises AssertError and always stops the batch. DML is rejected.",
42103
+ description: "Execute read-only kSQL: SELECT, WITH, UNION, EXPLAIN, SHOW APPS, DESCRIBE, ASSERT, and INSERT/UPSERT/UPDATE ... VALIDATE ONLY. ASSERT failure always stops the batch. VALIDATE ONLY performs local Tier-0 validation with zero write API calls; it always requires complete input, so onLimit=truncate is ignored and treated as error. Supports multi-statement batches with temp tables, including VALIDATE ONLY INTO #err for later SELECT. Mutating DML is rejected.",
41269
42104
  inputSchema: queryInputShape
41270
42105
  }, tools.queryTool);
41271
42106
  server.registerTool("ksql_mutate", {
41272
42107
  title: "Run mutating kSQL",
41273
- description: "Execute DML kSQL with explicit allowDml, confirmText, and dmlMaxRows safety controls. Supports multi-statement DML batches with temp tables. INSERT/UPSERT INTO app ... SELECT supports app sources, temp tables, or joins of both. UPDATE ... FROM supports copying scalar fields from an app or temp table by matching target $id to one source key. For UPSERT, dmlMaxRows counts inserts + updates. dmlMaxRows caps affected rows only, not source reads: source SELECT and UPDATE ... FROM app reads use the runtime maxRecords (KSQL_MAX_RECORDS / profile query.maxRecords, default 500); temp tables hold at most 10000 rows by default (adjustable via tempTableMaxRows).",
42108
+ description: "Execute DML kSQL with explicit allowDml, confirmText, and dmlMaxRows safety controls. Supports multi-statement DML batches with temp tables. ON ERROR SKIP INTO #err optionally isolates local Tier-0 validation failures and writes only valid rows; REJECT LIMIT stops with zero writes while returning diagnostics. INSERT/UPSERT INTO app ... SELECT supports app sources, temp tables, or joins of both. UPDATE ... FROM supports copying scalar fields from an app or temp table by matching target $id or a single-line-text/number business key to one source key. For UPSERT, dmlMaxRows counts inserts + updates. dmlMaxRows caps affected rows only, not source reads: source SELECT, ON ERROR SKIP candidates, and UPDATE ... FROM app reads use the runtime maxRecords (KSQL_MAX_RECORDS / profile query.maxRecords, default 500); temp tables hold at most 10000 rows by default (adjustable via tempTableMaxRows).",
41274
42109
  inputSchema: mutateInputShape
41275
42110
  }, tools.mutateTool);
41276
42111
  server.registerTool("ksql_describe_app", {