@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.
package/dist-cli/ksql.js CHANGED
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  buildBatchStatementSummary: () => buildBatchStatementSummary,
27
27
  buildOutput: () => buildOutput,
28
28
  buildReplExecArgv: () => buildReplExecArgv,
29
+ buildValidationOutput: () => buildValidationOutput,
29
30
  extractAppIds: () => extractAppIds,
30
31
  normalizeAppKey: () => normalizeAppKey,
31
32
  normalizeSqlAppProfiles: () => normalizeSqlAppProfiles,
@@ -1503,6 +1504,7 @@ var Parser = class {
1503
1504
  tryParseImplicitAlias() {
1504
1505
  const k = this.peek().kind;
1505
1506
  if (k === "IDENT" /* IDENT */ || k === "BIDENT" /* BIDENT */) {
1507
+ if (k === "IDENT" /* IDENT */ && this.peek().value.toUpperCase() === "VALIDATE" && this.peekAt(1).kind === "IDENT" /* IDENT */ && this.peekAt(1).value.toUpperCase() === "ONLY") return null;
1506
1508
  return this.parseTableAliasName();
1507
1509
  }
1508
1510
  return null;
@@ -1949,7 +1951,8 @@ var Parser = class {
1949
1951
  if (subtableCode) {
1950
1952
  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());
1951
1953
  }
1952
- return { type: "INSERT_SELECT", appId, fields, select };
1954
+ const validation2 = this.parseDmlControlSuffix();
1955
+ return { type: "INSERT_SELECT", appId, fields, select, ...validation2 };
1953
1956
  }
1954
1957
  this.expect("VALUES" /* VALUES */);
1955
1958
  const values = [];
@@ -1959,7 +1962,11 @@ var Parser = class {
1959
1962
  this.expect(")" /* RPAREN */);
1960
1963
  values.push(row);
1961
1964
  } while (this.consume("," /* COMMA */));
1962
- return subtableCode ? { type: "INSERT", appId, subtableCode, fields, values } : { type: "INSERT", appId, fields, values };
1965
+ const validation = this.parseDmlControlSuffix();
1966
+ if (subtableCode && (validation.validateOnly || validation.onErrorSkip)) {
1967
+ 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());
1968
+ }
1969
+ return subtableCode ? { type: "INSERT", appId, subtableCode, fields, values, ...validation } : { type: "INSERT", appId, fields, values, ...validation };
1963
1970
  }
1964
1971
  parseUpsert() {
1965
1972
  this.expect("UPSERT" /* UPSERT */);
@@ -1976,7 +1983,8 @@ var Parser = class {
1976
1983
  if (this.peek().kind === "SELECT" /* SELECT */) {
1977
1984
  const select = this.parseSelect();
1978
1985
  const keyFields2 = this.parseOnDuplicate();
1979
- return { type: "UPSERT_SELECT", appId, fields, select, keyFields: keyFields2 };
1986
+ const validation2 = this.parseDmlControlSuffix();
1987
+ return { type: "UPSERT_SELECT", appId, fields, select, keyFields: keyFields2, ...validation2 };
1980
1988
  }
1981
1989
  this.expect("VALUES" /* VALUES */);
1982
1990
  const values = [];
@@ -1986,7 +1994,8 @@ var Parser = class {
1986
1994
  this.expect(")" /* RPAREN */);
1987
1995
  } while (this.consume("," /* COMMA */));
1988
1996
  const keyFields = this.parseOnDuplicate();
1989
- return { type: "UPSERT", appId, fields, values, keyFields };
1997
+ const validation = this.parseDmlControlSuffix();
1998
+ return { type: "UPSERT", appId, fields, values, keyFields, ...validation };
1990
1999
  }
1991
2000
  parseOnDuplicate() {
1992
2001
  this.expectKeyword("ON" /* ON */, "UPSERT \u306B\u306F ON DUPLICATE (\u30AD\u30FC\u30D5\u30A3\u30FC\u30EB\u30C9) \u304C\u5FC5\u8981\u3067\u3059");
@@ -2066,10 +2075,14 @@ var Parser = class {
2066
2075
  if (!table.alias) {
2067
2076
  throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u306B\u306F\u30A8\u30A4\u30EA\u30A2\u30B9\u304C\u5FC5\u8981\u3067\u3059", this.prev());
2068
2077
  }
2078
+ if (table.alias.toLowerCase() === `app${appId}`.toLowerCase()) {
2079
+ 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());
2080
+ }
2069
2081
  from = {
2070
2082
  appId: table.appId,
2071
2083
  cteName: table.cteName,
2072
2084
  alias: table.alias,
2085
+ targetJoinField: "",
2073
2086
  joinKeyField: "",
2074
2087
  targetFilter: null
2075
2088
  };
@@ -2088,6 +2101,7 @@ var Parser = class {
2088
2101
  }
2089
2102
  this.validateUpdateFromAssignments(assignments, from.alias, whereTok);
2090
2103
  const decomposed = this.decomposeUpdateFromWhere(where, appId, from.alias, whereTok);
2104
+ from.targetJoinField = decomposed.targetJoinField;
2091
2105
  from.joinKeyField = decomposed.joinKeyField;
2092
2106
  from.targetFilter = decomposed.targetFilter;
2093
2107
  } else if (assignments.some((a) => a.value.type === "SOURCE_FIELD")) {
@@ -2096,8 +2110,66 @@ var Parser = class {
2096
2110
  whereTok
2097
2111
  );
2098
2112
  }
2099
- if (from !== null) return { type: "UPDATE", appId, assignments, where, from };
2100
- return subtableCode ? { type: "UPDATE", appId, subtableCode, assignments, where } : { type: "UPDATE", appId, assignments, where };
2113
+ const validation = this.parseDmlControlSuffix();
2114
+ if (subtableCode && (validation.validateOnly || validation.onErrorSkip)) {
2115
+ 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());
2116
+ }
2117
+ if (from !== null) return { type: "UPDATE", appId, assignments, where, from, ...validation };
2118
+ return subtableCode ? { type: "UPDATE", appId, subtableCode, assignments, where, ...validation } : { type: "UPDATE", appId, assignments, where, ...validation };
2119
+ }
2120
+ /** DML末尾の VALIDATE ONLY または ON ERROR SKIP。各語はsoft keyword。 */
2121
+ parseDmlControlSuffix() {
2122
+ if (this.peek().kind === "ON" /* ON */) return this.parseOnErrorSkipSuffix();
2123
+ if (this.isSoftKeyword("REJECT")) {
2124
+ throw new ParseError("REJECT LIMIT \u306B\u306F ON ERROR SKIP INTO \u304C\u5FC5\u8981\u3067\u3059", this.peek());
2125
+ }
2126
+ if (!this.isSoftKeyword("VALIDATE")) return {};
2127
+ const validateTok = this.advance();
2128
+ if (!this.isSoftKeyword("ONLY")) {
2129
+ throw new ParseError("VALIDATE \u306E\u5F8C\u306B\u306F ONLY \u304C\u5FC5\u8981\u3067\u3059", this.peek());
2130
+ }
2131
+ this.advance();
2132
+ let validationErrorTable = null;
2133
+ if (this.consume("INTO" /* INTO */)) {
2134
+ const tok = this.peek();
2135
+ if (tok.kind !== "IDENT" /* IDENT */ || !tok.value.startsWith("#")) {
2136
+ 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);
2137
+ }
2138
+ validationErrorTable = this.parseTableName();
2139
+ }
2140
+ if (this.peek().kind === "ON" /* ON */ || this.isSoftKeyword("REJECT")) {
2141
+ throw new ParseError("VALIDATE ONLY \u3068 ON ERROR / REJECT LIMIT \u306F\u4F75\u8A18\u3067\u304D\u307E\u305B\u3093", validateTok);
2142
+ }
2143
+ return { validateOnly: true, validationErrorTable };
2144
+ }
2145
+ parseOnErrorSkipSuffix() {
2146
+ const onTok = this.advance();
2147
+ if (!this.isSoftKeyword("ERROR")) throw new ParseError("ON \u306E\u5F8C\u306B\u306F ERROR \u304C\u5FC5\u8981\u3067\u3059", this.peek());
2148
+ this.advance();
2149
+ if (!this.isSoftKeyword("SKIP")) throw new ParseError("ON ERROR \u306E\u5F8C\u306B\u306F SKIP \u304C\u5FC5\u8981\u3067\u3059", this.peek());
2150
+ this.advance();
2151
+ this.expect("INTO" /* INTO */, "ON ERROR SKIP \u306B\u306F INTO #\u4E00\u6642\u30C6\u30FC\u30D6\u30EB \u304C\u5FC5\u8981\u3067\u3059");
2152
+ const tableTok = this.peek();
2153
+ if (tableTok.kind !== "IDENT" /* IDENT */ || !tableTok.value.startsWith("#")) {
2154
+ 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);
2155
+ }
2156
+ const errorTable = this.parseTableName();
2157
+ let rejectLimit = null;
2158
+ if (this.isSoftKeyword("REJECT")) {
2159
+ this.advance();
2160
+ this.expect("LIMIT" /* LIMIT */, "REJECT \u306E\u5F8C\u306B\u306F LIMIT \u304C\u5FC5\u8981\u3067\u3059");
2161
+ const tok = this.expect("NUMBER" /* NUMBER */, "REJECT LIMIT \u306B\u306F 0 \u4EE5\u4E0A\u306E\u6574\u6570\u304C\u5FC5\u8981\u3067\u3059");
2162
+ 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);
2163
+ rejectLimit = Number(tok.value);
2164
+ 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);
2165
+ }
2166
+ if (this.isSoftKeyword("REJECT") || this.isSoftKeyword("VALIDATE") || this.peek().kind === "ON" /* ON */) {
2167
+ throw new ParseError("ON ERROR SKIP \u306E\u53E5\u304C\u91CD\u8907\u307E\u305F\u306F\u7AF6\u5408\u3057\u3066\u3044\u307E\u3059", onTok);
2168
+ }
2169
+ return { onErrorSkip: true, errorTable, rejectLimit };
2170
+ }
2171
+ isSoftKeyword(value) {
2172
+ return this.peek().kind === "IDENT" /* IDENT */ && this.peek().value.toUpperCase() === value;
2101
2173
  }
2102
2174
  validateUpdateFromAssignments(assignments, sourceAlias, tok) {
2103
2175
  for (const assignment of assignments) {
@@ -2122,11 +2194,11 @@ var Parser = class {
2122
2194
  const leaves = this.flattenTopLevelAnd(where);
2123
2195
  const joins = [];
2124
2196
  leaves.forEach((leaf, index) => {
2125
- const sourceField = this.matchUpdateFromJoin(leaf, targetAppId, sourceAlias);
2126
- if (sourceField !== null) joins.push({ index, sourceField });
2197
+ const matched = this.matchUpdateFromJoin(leaf, targetAppId, sourceAlias);
2198
+ if (matched !== null) joins.push({ index, ...matched });
2127
2199
  });
2128
2200
  if (joins.length !== 1) {
2129
- 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);
2201
+ 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);
2130
2202
  }
2131
2203
  const join2 = joins[0];
2132
2204
  for (let i = 0; i < leaves.length; i++) {
@@ -2142,7 +2214,7 @@ var Parser = class {
2142
2214
  (acc, expr) => acc === null ? expr : { type: "LOGICAL", op: "AND", left: acc, right: expr },
2143
2215
  null
2144
2216
  );
2145
- return { joinKeyField: join2.sourceField, targetFilter };
2217
+ return { targetJoinField: join2.targetField, joinKeyField: join2.sourceField, targetFilter };
2146
2218
  }
2147
2219
  flattenTopLevelAnd(expr) {
2148
2220
  if (expr.type === "GROUP") return this.flattenTopLevelAnd(expr.expr);
@@ -2156,16 +2228,20 @@ var Parser = class {
2156
2228
  const right = expr.right.type === "ARITH_VALUE" && expr.right.expr.type === "FIELD_REF" ? this.splitQualifiedField(expr.right.expr.field) : null;
2157
2229
  if (right === null) return null;
2158
2230
  const left = { alias: expr.left.tableAlias, field: expr.left.field };
2159
- if (this.isTargetIdRef(left, targetAppId) && this.isSourceRef(right, sourceAlias)) return right.field;
2160
- if (this.isSourceRef(left, sourceAlias) && this.isTargetIdRef(right, targetAppId)) return left.field;
2231
+ if (this.isTargetRef(left, targetAppId) && this.isSourceRef(right, sourceAlias)) {
2232
+ return { targetField: left.field, sourceField: right.field };
2233
+ }
2234
+ if (this.isSourceRef(left, sourceAlias) && this.isTargetRef(right, targetAppId)) {
2235
+ return { targetField: right.field, sourceField: left.field };
2236
+ }
2161
2237
  return null;
2162
2238
  }
2163
2239
  splitQualifiedField(field) {
2164
2240
  const dot = field.indexOf(".");
2165
2241
  return dot < 0 ? { alias: null, field } : { alias: field.slice(0, dot), field: field.slice(dot + 1) };
2166
2242
  }
2167
- isTargetIdRef(ref, appId) {
2168
- return ref.field === "$id" && (ref.alias === null || ref.alias.toLowerCase() === `app${appId}`.toLowerCase());
2243
+ isTargetRef(ref, appId) {
2244
+ return ref.alias === null || ref.alias.toLowerCase() === `app${appId}`.toLowerCase();
2169
2245
  }
2170
2246
  isSourceRef(ref, alias) {
2171
2247
  return ref.alias?.toLowerCase() === alias.toLowerCase();
@@ -2492,6 +2568,15 @@ function isDmlType(type) {
2492
2568
  function isReadOnlyType(type) {
2493
2569
  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";
2494
2570
  }
2571
+ function writesKintone(stmt) {
2572
+ return isDmlType(stmt.type) && !("validateOnly" in stmt && stmt.validateOnly === true);
2573
+ }
2574
+ function isReadOnlyStatement(stmt) {
2575
+ return !writesKintone(stmt) && (isReadOnlyType(stmt.type) || isDmlType(stmt.type));
2576
+ }
2577
+ function requiresCompleteInput(stmt) {
2578
+ return isDmlType(stmt.type);
2579
+ }
2495
2580
  function hasWhereClause(stmt) {
2496
2581
  if (!stmt || typeof stmt !== "object") return false;
2497
2582
  const obj = stmt;
@@ -3731,11 +3816,17 @@ function analyzeBatch(statements) {
3731
3816
  }
3732
3817
  }
3733
3818
  const defined = /* @__PURE__ */ new Map();
3819
+ const validationSchemas = /* @__PURE__ */ new Map();
3734
3820
  const createdOrder = [];
3735
3821
  const results = [];
3736
3822
  const variableDefs = /* @__PURE__ */ new Map();
3737
3823
  const variableOrder = [];
3738
3824
  statements.forEach((stmt, index) => {
3825
+ const validationTable = "validationErrorTable" in stmt && stmt.validationErrorTable ? stmt.validationErrorTable : "onErrorSkip" in stmt && stmt.onErrorSkip ? stmt.errorTable ?? null : null;
3826
+ if (statements.length === 1 && validationTable) {
3827
+ const message = "onErrorSkip" in stmt && stmt.onErrorSkip ? "ArgumentError: ON ERROR SKIP requires a batch." : "ArgumentError: VALIDATE ONLY INTO requires a batch.";
3828
+ throw new BatchAnalysisError(message, index);
3829
+ }
3739
3830
  const statementType = getStatementType(stmt);
3740
3831
  const created = [];
3741
3832
  const dropped = [];
@@ -3790,6 +3881,28 @@ function analyzeBatch(statements) {
3790
3881
  }
3791
3882
  dependsOn.add(at);
3792
3883
  }
3884
+ if (validationTable) {
3885
+ const payloadFields = stmt.type === "UPDATE" ? ["$id", ...stmt.assignments.map((a) => a.field)] : "fields" in stmt ? stmt.fields : [];
3886
+ const signature = JSON.stringify(payloadFields);
3887
+ const at = defined.get(validationTable);
3888
+ if (at === void 0) {
3889
+ defined.set(validationTable, index);
3890
+ validationSchemas.set(validationTable, signature);
3891
+ createdOrder.push(validationTable);
3892
+ created.push(validationTable);
3893
+ if (defined.size > MAX_TEMP_TABLES) {
3894
+ throw new BatchAnalysisError(`ParseError: batch exceeds ${MAX_TEMP_TABLES} temp tables.`, index);
3895
+ }
3896
+ } else {
3897
+ if (validationSchemas.get(validationTable) !== signature) {
3898
+ throw new BatchAnalysisError(
3899
+ `ParseError: validation error table ${validationTable} has a different payload schema.`,
3900
+ index
3901
+ );
3902
+ }
3903
+ dependsOn.add(at);
3904
+ }
3905
+ }
3793
3906
  if (stmt.type === "CREATE_TEMP_TABLE") {
3794
3907
  if (defined.has(stmt.name)) {
3795
3908
  throw new BatchAnalysisError(
@@ -3822,8 +3935,8 @@ function analyzeBatch(statements) {
3822
3935
  results.push({
3823
3936
  index,
3824
3937
  statementType,
3825
- isDml: isDmlType(statementType),
3826
- isReadOnly: isReadOnlyType(statementType),
3938
+ isDml: writesKintone(stmt),
3939
+ isReadOnly: isReadOnlyStatement(stmt),
3827
3940
  hasWhere: hasWhereClause(stmt),
3828
3941
  insertValuesCount: getInsertValuesCount(stmt),
3829
3942
  appIds: [...stmtAppIds].sort((a, b) => a - b),
@@ -3833,10 +3946,15 @@ function analyzeBatch(statements) {
3833
3946
  dependsOn: [...dependsOn].sort((a, b) => a - b),
3834
3947
  tempOnlySource,
3835
3948
  targetAppId: isDmlType(statementType) && typeof stmt.appId === "number" ? stmt.appId : null,
3836
- isUpdateFrom: stmt.type === "UPDATE" && stmt.from != null
3949
+ isUpdateFrom: stmt.type === "UPDATE" && stmt.from != null,
3950
+ isValidationOnly: "validateOnly" in stmt && stmt.validateOnly === true,
3951
+ isOnErrorSkip: "onErrorSkip" in stmt && stmt.onErrorSkip === true,
3952
+ requiresCompleteInput: requiresCompleteInput(stmt)
3837
3953
  });
3838
3954
  });
3839
3955
  const containsDml = results.some((r) => r.isDml);
3956
+ const containsValidationOnly = results.some((r) => r.isValidationOnly);
3957
+ const needsCompleteInput = results.some((r) => r.requiresCompleteInput);
3840
3958
  const variables = variableOrder.map((name) => ({
3841
3959
  name,
3842
3960
  referencedBy: [...variableDefs.get(name).referencedBy]
@@ -3845,6 +3963,8 @@ function analyzeBatch(statements) {
3845
3963
  statementCount: statements.length,
3846
3964
  isReadOnlyBatch: !containsDml && results.every((r) => r.isReadOnly),
3847
3965
  containsDml,
3966
+ containsValidationOnly,
3967
+ requiresCompleteInput: needsCompleteInput,
3848
3968
  tempTables: createdOrder,
3849
3969
  variables,
3850
3970
  warnings: variables.filter((v) => v.referencedBy.length === 0).map((v) => `variable @${v.name} is never used.`),
@@ -4654,6 +4774,18 @@ function evalCaseWhenValue(expr, row, fieldType) {
4654
4774
  return "";
4655
4775
  }
4656
4776
  function toKintoneValue(value, fieldType) {
4777
+ const result = normalizeDmlSqlValue(value, fieldType);
4778
+ if (!result.ok) throw new DmlConvertError(result.message);
4779
+ return result.value;
4780
+ }
4781
+ function normalizeDmlSqlValue(value, fieldType) {
4782
+ try {
4783
+ return { ok: true, value: convertDmlSqlValue(value, fieldType) };
4784
+ } catch (e) {
4785
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
4786
+ }
4787
+ }
4788
+ function convertDmlSqlValue(value, fieldType) {
4657
4789
  switch (value.type) {
4658
4790
  case "VARIABLE":
4659
4791
  throw new DmlConvertError(`\u672A\u89E3\u6C7A\u306E\u30D0\u30C3\u30C1\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
@@ -5494,6 +5626,228 @@ function toFlatString(value) {
5494
5626
  }
5495
5627
  }
5496
5628
 
5629
+ // src/core/dmlValidation.ts
5630
+ var ARRAY_TYPES2 = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
5631
+ var CHOICE_TYPES = /* @__PURE__ */ new Set(["DROP_DOWN", "RADIO_BUTTON", "CHECK_BOX", "MULTI_SELECT"]);
5632
+ function validateAndNormalizeDmlValue(raw, field) {
5633
+ if (field.fieldType === "DATE" || field.fieldType === "TIME" || field.fieldType === "DATETIME") {
5634
+ const original = rawScalarText(raw);
5635
+ if (original !== "" && !isValidTemporalInput(original, field.fieldType)) {
5636
+ return { ok: false, code: "ERR_TYPE_DATE", message: `${field.code} \u306E\u65E5\u4ED8\u30FB\u6642\u523B\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059` };
5637
+ }
5638
+ }
5639
+ let value;
5640
+ try {
5641
+ value = normalizeRaw(raw, field.fieldType);
5642
+ } catch (e) {
5643
+ const message = e instanceof Error ? e.message : String(e);
5644
+ return { ok: false, code: typeCode(field.fieldType), message };
5645
+ }
5646
+ if (field.required && isEmpty(value)) {
5647
+ return { ok: false, code: "ERR_REQUIRED", message: `${field.code} \u306F\u5FC5\u9808\u3067\u3059` };
5648
+ }
5649
+ if (!isEmpty(value) && field.fieldType === "NUMBER") {
5650
+ const text = String(value);
5651
+ if (!isFiniteDecimal(text)) {
5652
+ return { ok: false, code: "ERR_TYPE_NUMBER", message: `${field.code} \u306F\u6570\u5024\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044` };
5653
+ }
5654
+ if (field.minValue != null && compareDecimal(text, field.minValue) < 0) {
5655
+ return { ok: false, code: "ERR_RANGE_MIN", message: `${field.code} \u306F ${field.minValue} \u4EE5\u4E0A\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044` };
5656
+ }
5657
+ if (field.maxValue != null && compareDecimal(text, field.maxValue) > 0) {
5658
+ return { ok: false, code: "ERR_RANGE_MAX", message: `${field.code} \u306F ${field.maxValue} \u4EE5\u4E0B\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044` };
5659
+ }
5660
+ }
5661
+ if (!isEmpty(value) && (field.fieldType === "DATE" || field.fieldType === "TIME" || field.fieldType === "DATETIME")) {
5662
+ if (!isValidTemporal(String(value), field.fieldType)) {
5663
+ return { ok: false, code: "ERR_TYPE_DATE", message: `${field.code} \u306E\u65E5\u4ED8\u30FB\u6642\u523B\u5F62\u5F0F\u304C\u4E0D\u6B63\u3067\u3059` };
5664
+ }
5665
+ }
5666
+ if (typeof value === "string") {
5667
+ const length = value.length;
5668
+ const min = field.minLength == null ? null : Number(field.minLength);
5669
+ const max = field.maxLength == null ? null : Number(field.maxLength);
5670
+ if (Number.isFinite(min) && length < min) {
5671
+ 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` };
5672
+ }
5673
+ if (Number.isFinite(max) && length > max) {
5674
+ 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` };
5675
+ }
5676
+ }
5677
+ if (CHOICE_TYPES.has(field.fieldType) && field.optionOrder) {
5678
+ const selected = Array.isArray(value) ? value.map(String) : [String(value)];
5679
+ if (selected.some((choice) => !(choice in field.optionOrder))) {
5680
+ return { ok: false, code: "ERR_CHOICE_INVALID", message: `${field.code} \u306B\u5B9A\u7FA9\u5916\u306E\u9078\u629E\u80A2\u304C\u3042\u308A\u307E\u3059` };
5681
+ }
5682
+ }
5683
+ return { ok: true, value };
5684
+ }
5685
+ function rawScalarText(raw) {
5686
+ if (raw == null) return "";
5687
+ if (isSqlValue(raw) && (raw.type === "STRING" || raw.type === "NUMBER")) return String(raw.value);
5688
+ return typeof raw === "string" || typeof raw === "number" ? String(raw) : "";
5689
+ }
5690
+ function isValidTemporalInput(value, type) {
5691
+ if (type === "DATE") return isValidTemporal(value.replace(/\//g, "-"), "DATE");
5692
+ if (type === "TIME") return isValidTemporal(value, "TIME");
5693
+ let normalized = value.replace(/\//g, "-").replace(" ", "T");
5694
+ if (/T\d{2}:\d{2}$/.test(normalized)) normalized += ":00";
5695
+ if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(normalized)) {
5696
+ return isValidTemporal(normalized.slice(0, 10), "DATE") && isValidTemporal(normalized.slice(11), "TIME");
5697
+ }
5698
+ return isValidTemporal(normalized, "DATETIME");
5699
+ }
5700
+ function normalizeRaw(raw, fieldType) {
5701
+ if (isSqlValue(raw)) {
5702
+ const normalized = normalizeDmlSqlValue(raw, fieldType);
5703
+ if (!normalized.ok) throw new Error(normalized.message);
5704
+ return normalized.value;
5705
+ }
5706
+ if (Array.isArray(raw)) return raw.map((v) => typeof v === "object" && v !== null && "code" in v ? String(v.code) : String(v));
5707
+ const text = raw == null ? "" : String(raw);
5708
+ if (ARRAY_TYPES2.has(fieldType)) {
5709
+ if (text === "") return [];
5710
+ try {
5711
+ const parsed = JSON.parse(text);
5712
+ if (Array.isArray(parsed)) return parsed.map(String);
5713
+ } catch {
5714
+ }
5715
+ return text.split(",").map((v) => v.trim());
5716
+ }
5717
+ return text;
5718
+ }
5719
+ function isSqlValue(value) {
5720
+ return typeof value === "object" && value !== null && typeof value.type === "string";
5721
+ }
5722
+ function isEmptyDmlValue(value) {
5723
+ if (value == null || value === "") return true;
5724
+ if (Array.isArray(value)) return value.length === 0;
5725
+ if (isSqlValue(value)) {
5726
+ if (value.type === "STRING") return value.value === "";
5727
+ if (value.type === "ARRAY") return value.elements.length === 0;
5728
+ }
5729
+ return false;
5730
+ }
5731
+ function isEmpty(value) {
5732
+ return value === "" || Array.isArray(value) && value.length === 0;
5733
+ }
5734
+ function typeCode(type) {
5735
+ return type === "NUMBER" ? "ERR_TYPE_NUMBER" : "ERR_TYPE_DATE";
5736
+ }
5737
+ function isFiniteDecimal(value) {
5738
+ return /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(value.trim());
5739
+ }
5740
+ function compareDecimal(left, right) {
5741
+ const normalize = (input) => {
5742
+ let s = input.trim();
5743
+ let sign = 1;
5744
+ if (s.startsWith("-")) {
5745
+ sign = -1;
5746
+ s = s.slice(1);
5747
+ } else if (s.startsWith("+")) s = s.slice(1);
5748
+ let [whole, fraction = ""] = s.split(".");
5749
+ whole = (whole || "0").replace(/^0+(?=\d)/, "");
5750
+ fraction = fraction.replace(/0+$/, "");
5751
+ if (/^0*$/.test(whole) && fraction === "") sign = 1;
5752
+ return { sign, whole, fraction };
5753
+ };
5754
+ const a = normalize(left);
5755
+ const b = normalize(right);
5756
+ if (a.sign !== b.sign) return a.sign < b.sign ? -1 : 1;
5757
+ const direction = a.sign;
5758
+ if (a.whole.length !== b.whole.length) return a.whole.length < b.whole.length ? -direction : direction;
5759
+ if (a.whole !== b.whole) return a.whole < b.whole ? -direction : direction;
5760
+ const width = Math.max(a.fraction.length, b.fraction.length);
5761
+ const af = a.fraction.padEnd(width, "0");
5762
+ const bf = b.fraction.padEnd(width, "0");
5763
+ return af === bf ? 0 : af < bf ? -direction : direction;
5764
+ }
5765
+ function isValidTemporal(value, type) {
5766
+ if (type === "TIME") {
5767
+ const m2 = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(value);
5768
+ return m2 !== null && Number(m2[1]) <= 23 && Number(m2[2]) <= 59 && Number(m2[3] ?? 0) <= 59;
5769
+ }
5770
+ const datePart = type === "DATE" ? value : value.slice(0, 10);
5771
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(datePart);
5772
+ if (!m) return false;
5773
+ const year = Number(m[1]);
5774
+ const month = Number(m[2]);
5775
+ const day = Number(m[3]);
5776
+ const date = new Date(Date.UTC(year, month - 1, day));
5777
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return false;
5778
+ if (type === "DATE") return true;
5779
+ 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");
5780
+ }
5781
+
5782
+ // src/core/dmlValidationCandidates.ts
5783
+ var VALIDATION_META_COLUMNS = [
5784
+ "$err_statement",
5785
+ "$err_operation",
5786
+ "$err_row",
5787
+ "$err_field",
5788
+ "$err_code",
5789
+ "$err_message"
5790
+ ];
5791
+ function validateDmlCandidates(candidates, operation, payloadFields, targetFields, fieldInfos, statementNumber) {
5792
+ const infoByCode = new Map(fieldInfos.map((field) => [field.code, field]));
5793
+ const errors = [];
5794
+ const invalid = /* @__PURE__ */ new Set();
5795
+ for (const candidate of candidates) {
5796
+ candidate.record ??= {};
5797
+ const rowErrors = [...candidate.preErrors];
5798
+ for (const code of targetFields) {
5799
+ const result = validateAndNormalizeDmlValue(candidate.payload.get(code), infoByCode.get(code));
5800
+ if (!result.ok) rowErrors.push({ field: code, code: result.code, message: result.message });
5801
+ else candidate.record[code] = { value: result.value };
5802
+ }
5803
+ if (candidate.mode === "create") {
5804
+ for (const info of fieldInfos) {
5805
+ if (info.inSubtable) continue;
5806
+ if (candidate.payload.has(info.code)) continue;
5807
+ const emptyDefault = isEmptyDmlValue(info.defaultValue);
5808
+ if (!emptyDefault) {
5809
+ const defaultResult = validateAndNormalizeDmlValue(info.defaultValue, info);
5810
+ if (!defaultResult.ok) rowErrors.push({
5811
+ field: info.code,
5812
+ code: defaultResult.code,
5813
+ message: `\u65E2\u5B9A\u5024: ${defaultResult.message}`
5814
+ });
5815
+ } else {
5816
+ const emptyResult = validateAndNormalizeDmlValue("", info);
5817
+ if (!emptyResult.ok) {
5818
+ rowErrors.push({ field: info.code, code: emptyResult.code, message: emptyResult.message });
5819
+ } else if (info.required) {
5820
+ rowErrors.push({ field: info.code, code: "ERR_REQUIRED", message: `${info.code} \u306F\u5FC5\u9808\u3067\u3059` });
5821
+ }
5822
+ }
5823
+ }
5824
+ }
5825
+ if (rowErrors.length > 0) invalid.add(candidate.rowNumber);
5826
+ for (const error of rowErrors) {
5827
+ const row = {};
5828
+ for (const field of payloadFields) row[field] = renderValidationValue(candidate.payload.get(field));
5829
+ row["$err_statement"] = String(statementNumber);
5830
+ row["$err_operation"] = operation;
5831
+ row["$err_row"] = String(candidate.rowNumber);
5832
+ row["$err_field"] = error.field;
5833
+ row["$err_code"] = error.code;
5834
+ row["$err_message"] = error.message;
5835
+ errors.push(row);
5836
+ }
5837
+ }
5838
+ return { errors, invalidRows: invalid.size, invalidRowNumbers: invalid };
5839
+ }
5840
+ function renderValidationValue(value) {
5841
+ if (value == null) return "";
5842
+ if (typeof value === "object" && "type" in value) {
5843
+ const sql = value;
5844
+ if (sql.type === "STRING" || sql.type === "NUMBER") return String(sql.value ?? "");
5845
+ if (sql.type === "ARRAY") return JSON.stringify(sql.elements?.map((e) => e.value) ?? []);
5846
+ }
5847
+ if (Array.isArray(value)) return JSON.stringify(value);
5848
+ return String(value);
5849
+ }
5850
+
5497
5851
  // src/execute.ts
5498
5852
  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";
5499
5853
  var SearchAbortedError = class extends Error {
@@ -5597,6 +5951,15 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
5597
5951
  throw new Error(`ParseError: variable @${unresolved} is not defined in a batch.`);
5598
5952
  }
5599
5953
  validateKlikeStatement(stmt);
5954
+ if ("validateOnly" in stmt && stmt.validateOnly === true) {
5955
+ if (stmt.validationErrorTable) {
5956
+ throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
5957
+ }
5958
+ return executeDmlValidation(stmt, client, { ...options, onLimitReached: "error" }, cacheContext, void 0, 1);
5959
+ }
5960
+ if ("onErrorSkip" in stmt && stmt.onErrorSkip === true) {
5961
+ throw new Error("ArgumentError: ON ERROR SKIP requires a batch.");
5962
+ }
5600
5963
  switch (stmt.type) {
5601
5964
  case "SELECT":
5602
5965
  return executeSelect(stmt, client, options, cacheContext);
@@ -5638,6 +6001,17 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
5638
6001
  }
5639
6002
  }
5640
6003
  var TEMP_TABLE_MAX_ROWS = 1e4;
6004
+ function appendValidationErrors(tempTables, name, columns, rows, maxRows) {
6005
+ const current = tempTables.get(name);
6006
+ if (current && (current.columns.length !== columns.length || current.columns.some((c, i) => c !== columns[i]))) {
6007
+ throw new Error(`ArgumentError: validation error table ${name} has a different schema.`);
6008
+ }
6009
+ const existingRows = current?.rows ?? [];
6010
+ if (existingRows.length + rows.length > maxRows) {
6011
+ throw new Error(`ArgumentError: temp table ${name} exceeds max rows (${maxRows}).`);
6012
+ }
6013
+ tempTables.set(name, { columns: [...columns], rows: [...existingRows, ...rows] });
6014
+ }
5641
6015
  var BatchTimeoutError = class extends Error {
5642
6016
  constructor() {
5643
6017
  super("TimeoutError: batch timeout exceeded.");
@@ -5719,7 +6093,12 @@ async function executeBatch(sql, client, options = {}) {
5719
6093
  }
5720
6094
  results.push({ ...base, status: "success", ...outcome });
5721
6095
  } catch (e) {
5722
- results.push({ ...base, status: "error", error: toBatchStatementError(e) });
6096
+ results.push({
6097
+ ...base,
6098
+ status: "error",
6099
+ error: toBatchStatementError(e),
6100
+ ...e instanceof RejectLimitExceededError ? { result: e.diagnostic } : {}
6101
+ });
5723
6102
  failed.add(i);
5724
6103
  if (e instanceof BatchTimeoutError) {
5725
6104
  aborted = "timeout";
@@ -5778,6 +6157,38 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
5778
6157
  }
5779
6158
  const resolvedStmt = resolveVariableRefs(stmt, variables);
5780
6159
  validateKlikeStatement(resolvedStmt);
6160
+ if ("validateOnly" in resolvedStmt && resolvedStmt.validateOnly === true) {
6161
+ const result = await executeDmlValidation(
6162
+ resolvedStmt,
6163
+ client,
6164
+ { ...options, onLimitReached: "error" },
6165
+ cacheContext,
6166
+ tempTables,
6167
+ info.index + 1
6168
+ );
6169
+ if (resolvedStmt.validationErrorTable) {
6170
+ appendValidationErrors(
6171
+ tempTables,
6172
+ resolvedStmt.validationErrorTable,
6173
+ result.columns,
6174
+ result.errors,
6175
+ options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS
6176
+ );
6177
+ }
6178
+ return { result };
6179
+ }
6180
+ if ("onErrorSkip" in resolvedStmt && resolvedStmt.onErrorSkip === true) {
6181
+ return {
6182
+ result: await executeOnErrorSkip(
6183
+ resolvedStmt,
6184
+ client,
6185
+ { ...options, onLimitReached: "error" },
6186
+ cacheContext,
6187
+ tempTables,
6188
+ info.index + 1
6189
+ )
6190
+ };
6191
+ }
5781
6192
  if (resolvedStmt.type === "CREATE_TEMP_TABLE") {
5782
6193
  const materializeOptions = {
5783
6194
  ...options,
@@ -6926,7 +7337,7 @@ async function buildSortKindsForSelect(stmt, client, cacheContext) {
6926
7337
  }
6927
7338
  function convertProcessRowValue(raw, dstFieldType) {
6928
7339
  const USER_TYPES2 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
6929
- const ARRAY_TYPES2 = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
7340
+ const ARRAY_TYPES3 = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
6930
7341
  if (USER_TYPES2.has(dstFieldType ?? "")) {
6931
7342
  if (raw === "") return [];
6932
7343
  try {
@@ -6938,7 +7349,7 @@ function convertProcessRowValue(raw, dstFieldType) {
6938
7349
  }
6939
7350
  return raw.split(",").map((c) => ({ code: c.trim() }));
6940
7351
  }
6941
- if (ARRAY_TYPES2.has(dstFieldType ?? "")) {
7352
+ if (ARRAY_TYPES3.has(dstFieldType ?? "")) {
6942
7353
  if (raw === "") return [];
6943
7354
  try {
6944
7355
  const parsed = JSON.parse(raw);
@@ -6949,6 +7360,396 @@ function convertProcessRowValue(raw, dstFieldType) {
6949
7360
  }
6950
7361
  return raw;
6951
7362
  }
7363
+ var NON_WRITABLE_FIELD_TYPES = /* @__PURE__ */ new Set([
7364
+ "CALC",
7365
+ "RECORD_NUMBER",
7366
+ "CREATOR",
7367
+ "CREATED_TIME",
7368
+ "MODIFIER",
7369
+ "UPDATED_TIME",
7370
+ "STATUS",
7371
+ "STATUS_ASSIGNEE",
7372
+ "CATEGORY",
7373
+ "REFERENCE_TABLE"
7374
+ ]);
7375
+ async function executeDmlValidation(stmt, client, options, cacheContext, tempTables, statementNumber) {
7376
+ return (await prepareDmlValidation(stmt, client, options, cacheContext, tempTables, statementNumber)).result;
7377
+ }
7378
+ var RejectLimitExceededError = class extends Error {
7379
+ constructor(message, diagnostic) {
7380
+ super(`RejectLimitExceededError: ${message}`);
7381
+ this.diagnostic = diagnostic;
7382
+ this.name = "RejectLimitExceededError";
7383
+ }
7384
+ };
7385
+ async function prepareDmlValidation(stmt, client, options, cacheContext, tempTables, statementNumber) {
7386
+ const operation = stmt.type === "UPDATE" ? "UPDATE" : stmt.type.startsWith("UPSERT") ? "UPSERT" : "INSERT";
7387
+ const payloadFields = stmt.type === "UPDATE" ? ["$id", ...stmt.assignments.map((a) => a.field)] : [...stmt.fields];
7388
+ if (new Set(payloadFields).size !== payloadFields.length) {
7389
+ throw new Error("ArgumentError: DML target fields contain duplicates.");
7390
+ }
7391
+ const fieldInfos = await getFieldsCached(stmt.appId, client, cacheContext);
7392
+ const infoByCode = new Map(fieldInfos.map((field) => [field.code, field]));
7393
+ const targetFields = stmt.type === "UPDATE" ? stmt.assignments.map((a) => a.field) : stmt.fields;
7394
+ for (const code of targetFields) {
7395
+ const info = infoByCode.get(code);
7396
+ if (!info) throw new Error(`ArgumentError: DML target field ${code} does not exist.`);
7397
+ if (info.writable === false || NON_WRITABLE_FIELD_TYPES.has(info.fieldType)) {
7398
+ throw new Error(`ArgumentError: DML target field ${code} is not writable (${info.fieldType}).`);
7399
+ }
7400
+ }
7401
+ const candidates = await materializeValidationCandidates(stmt, operation, client, options, cacheContext, tempTables, infoByCode);
7402
+ const { errors, invalidRows, invalidRowNumbers } = validateDmlCandidates(
7403
+ candidates,
7404
+ operation,
7405
+ payloadFields,
7406
+ targetFields,
7407
+ fieldInfos,
7408
+ statementNumber
7409
+ );
7410
+ const columns = [...payloadFields, ...VALIDATION_META_COLUMNS];
7411
+ const result = {
7412
+ type: "VALIDATION",
7413
+ operation,
7414
+ validatedRows: candidates.length,
7415
+ validRows: candidates.length - invalidRows,
7416
+ invalidRows,
7417
+ errorCount: errors.length,
7418
+ columns,
7419
+ errors,
7420
+ ...stmt.validationErrorTable ? { errTable: stmt.validationErrorTable } : stmt.onErrorSkip && stmt.errorTable ? { errTable: stmt.errorTable } : {}
7421
+ };
7422
+ return { result, candidates, invalidRowNumbers };
7423
+ }
7424
+ async function executeOnErrorSkip(stmt, client, options, cacheContext, tempTables, statementNumber) {
7425
+ const prepared = await prepareDmlValidation(
7426
+ stmt,
7427
+ client,
7428
+ options,
7429
+ cacheContext,
7430
+ tempTables,
7431
+ statementNumber
7432
+ );
7433
+ const errTable = stmt.errorTable;
7434
+ if (!errTable) throw new Error("ArgumentError: ON ERROR SKIP requires INTO #error_table.");
7435
+ appendValidationErrors(
7436
+ tempTables,
7437
+ errTable,
7438
+ prepared.result.columns,
7439
+ prepared.result.errors,
7440
+ options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS
7441
+ );
7442
+ const rejectLimit = stmt.rejectLimit ?? null;
7443
+ if (rejectLimit !== null && prepared.result.invalidRows > rejectLimit) {
7444
+ throw new RejectLimitExceededError(
7445
+ `rejected rows (${prepared.result.invalidRows}) exceed REJECT LIMIT (${rejectLimit}).`,
7446
+ prepared.result
7447
+ );
7448
+ }
7449
+ const valid = prepared.candidates.filter((candidate) => !prepared.invalidRowNumbers.has(candidate.rowNumber));
7450
+ if (options.confirm) {
7451
+ const operation = stmt.type.startsWith("INSERT") ? "INSERT" : "UPDATE";
7452
+ const ok = await options.confirm(valid.length, operation);
7453
+ if (!ok) throw new OperationCancelledError(operation, valid.length);
7454
+ }
7455
+ const common = {
7456
+ affectedRows: valid.length,
7457
+ skippedRows: prepared.result.invalidRows,
7458
+ rejectLimit,
7459
+ errTable
7460
+ };
7461
+ if (stmt.type === "INSERT" || stmt.type === "INSERT_SELECT") {
7462
+ const createdIds = [];
7463
+ for (let i = 0; i < valid.length; i += 100) {
7464
+ const response = await client.postRecords({ app: stmt.appId, records: valid.slice(i, i + 100).map((c) => c.record) });
7465
+ createdIds.push(response.ids);
7466
+ }
7467
+ return { type: "INSERT", createdIds, insertedCount: createdIds.flat().length, ...common };
7468
+ }
7469
+ if (stmt.type === "UPDATE") {
7470
+ const updates2 = valid.map((candidate) => {
7471
+ if (candidate.targetId === void 0) throw new Error("InternalError: prepared UPDATE candidate has no targetId.");
7472
+ return { id: candidate.targetId, record: candidate.record };
7473
+ });
7474
+ for (let i = 0; i < updates2.length; i += 100) {
7475
+ await client.putRecords({ app: stmt.appId, records: updates2.slice(i, i + 100) });
7476
+ }
7477
+ return { type: "UPDATE", updatedCount: updates2.length, ...common };
7478
+ }
7479
+ const inserts = valid.filter((candidate) => candidate.mode === "create");
7480
+ const updates = valid.filter((candidate) => candidate.mode === "update").map((candidate) => {
7481
+ if (candidate.targetId === void 0) throw new Error("InternalError: prepared UPSERT candidate has no targetId.");
7482
+ return { id: candidate.targetId, record: candidate.record };
7483
+ });
7484
+ let insertedCount = 0;
7485
+ for (let i = 0; i < inserts.length; i += 100) {
7486
+ const response = await client.postRecords({ app: stmt.appId, records: inserts.slice(i, i + 100).map((c) => c.record) });
7487
+ insertedCount += response.ids.length;
7488
+ }
7489
+ for (let i = 0; i < updates.length; i += 100) {
7490
+ await client.putRecords({ app: stmt.appId, records: updates.slice(i, i + 100) });
7491
+ }
7492
+ return { type: "UPSERT", insertedCount, updatedCount: updates.length, ...common };
7493
+ }
7494
+ async function materializeValidationCandidates(stmt, operation, client, options, cacheContext, tempTables, infoByCode) {
7495
+ if (stmt.type === "UPDATE") return materializeUpdateValidationCandidates(stmt, client, options, cacheContext, tempTables);
7496
+ let rows;
7497
+ if (stmt.type === "INSERT" || stmt.type === "UPSERT") {
7498
+ rows = stmt.values.map((row) => row.map(
7499
+ (value, i) => value.type === "CASE_VALUE" ? evalCaseWhenValue(value.expr, {}, infoByCode.get(stmt.fields[i])?.fieldType) : value
7500
+ ));
7501
+ } else {
7502
+ 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);
7503
+ if (selectResult.columns.length !== stmt.fields.length) {
7504
+ 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`);
7505
+ }
7506
+ rows = selectResult.rows.map((row) => selectResult.columns.map((column) => row[column] ?? ""));
7507
+ }
7508
+ const candidates = rows.map((values, index) => ({
7509
+ rowNumber: index + 1,
7510
+ operation,
7511
+ mode: "create",
7512
+ payload: new Map(stmt.fields.map((field, i) => [field, values[i]])),
7513
+ preErrors: [],
7514
+ record: {}
7515
+ }));
7516
+ if (stmt.type !== "UPSERT" && stmt.type !== "UPSERT_SELECT") return candidates;
7517
+ for (const key of stmt.keyFields) {
7518
+ 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`);
7519
+ }
7520
+ const fieldTypes = new Map([...infoByCode].map(([code, info]) => [code, info.fieldType]));
7521
+ const rowKeys = candidates.map((candidate) => stmt.keyFields.map((key) => renderValidationValue(candidate.payload.get(key))));
7522
+ const targets = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeys, client, options, fieldTypes);
7523
+ const numeric = stmt.keyFields.map((key) => fieldTypes.get(key) === "NUMBER");
7524
+ const keyCounts = /* @__PURE__ */ new Map();
7525
+ for (const parts of rowKeys) {
7526
+ const key = upsertNormalizedKey(parts, numeric);
7527
+ keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
7528
+ }
7529
+ candidates.forEach((candidate, index) => {
7530
+ const parts = rowKeys[index];
7531
+ const targetId = lookupUpsertTarget(targets, parts);
7532
+ candidate.mode = targetId === void 0 ? "create" : "update";
7533
+ if (targetId !== void 0) candidate.targetId = targetId;
7534
+ stmt.keyFields.forEach((key, keyIndex) => {
7535
+ 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` });
7536
+ });
7537
+ if ((keyCounts.get(upsertNormalizedKey(parts, numeric)) ?? 0) > 1) {
7538
+ 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" });
7539
+ }
7540
+ });
7541
+ return candidates;
7542
+ }
7543
+ async function materializeUpdateValidationCandidates(stmt, client, options, cacheContext, tempTables) {
7544
+ if (stmt.from) return materializeUpdateFromValidationCandidates(stmt, stmt.from, client, options, cacheContext, tempTables);
7545
+ await resolveSetSubqueries(stmt.assignments, client, options, cacheContext);
7546
+ const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
7547
+ let records;
7548
+ if (hasArithAssignment(stmt)) {
7549
+ const getParams = updateToGetQueryForArith(stmt);
7550
+ const resolved = await fetchRecordsForSharedPlan(client.getRecords, getParams.app, getParams.query, [...getParams.fields], {
7551
+ maxRecords: options.maxRecords ?? 1e4,
7552
+ parallel: options.fetchParallel ?? 1,
7553
+ onLimit: "error"
7554
+ });
7555
+ records = updateToPutBatchesArith(stmt, resolved.records, fieldTypes).flatMap((batch) => batch.records);
7556
+ } else {
7557
+ const getParams = updateToGetQuery(stmt);
7558
+ const resolved = await resolveDmlTargetIds(client.getRecords, getParams.app, getParams.query, {
7559
+ maxRecords: options.maxRecords ?? 1e4,
7560
+ parallel: options.fetchParallel ?? 1
7561
+ });
7562
+ records = updateToPutBatches(stmt, resolved.ids, fieldTypes).flatMap((batch) => batch.records);
7563
+ }
7564
+ return records.sort((a, b) => a.id - b.id).map((entry, index) => ({
7565
+ rowNumber: index + 1,
7566
+ operation: "UPDATE",
7567
+ mode: "update",
7568
+ payload: new Map([["$id", String(entry.id)], ...stmt.assignments.map((a) => [a.field, entry.record[a.field]?.value ?? ""])]),
7569
+ preErrors: [],
7570
+ record: entry.record,
7571
+ targetId: entry.id
7572
+ }));
7573
+ }
7574
+ async function materializeUpdateFromValidationCandidates(stmt, from, client, options, cacheContext, tempTables) {
7575
+ const matched = await resolveUpdateFromMatchedRecords(stmt, from, client, options, cacheContext, tempTables);
7576
+ const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
7577
+ const records = updateFromToPutBatches(stmt, matched, fieldTypes).flatMap((batch) => batch.records);
7578
+ return records.sort((a, b) => a.id - b.id).map((entry, index) => ({
7579
+ rowNumber: index + 1,
7580
+ operation: "UPDATE",
7581
+ mode: "update",
7582
+ payload: new Map([["$id", String(entry.id)], ...stmt.assignments.map((a) => [a.field, entry.record[a.field]?.value ?? ""])]),
7583
+ preErrors: [],
7584
+ record: entry.record,
7585
+ targetId: entry.id
7586
+ }));
7587
+ }
7588
+ var UPDATE_FROM_KEY_CHUNK_SIZE = UPSERT_IN_CHUNK_SIZE;
7589
+ var UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES = /* @__PURE__ */ new Set([
7590
+ "CHECK_BOX",
7591
+ "MULTI_SELECT",
7592
+ "USER_SELECT",
7593
+ "ORGANIZATION_SELECT",
7594
+ "GROUP_SELECT",
7595
+ "FILE"
7596
+ ]);
7597
+ async function resolveUpdateFromMatchedRecords(stmt, from, client, options, cacheContext, tempTables) {
7598
+ const joinKind = await resolveUpdateFromTargetJoinKind(stmt, from, client, cacheContext);
7599
+ const sourceFields = [...new Set(stmt.assignments.filter((a) => a.value.type === "SOURCE_FIELD").map((a) => a.value.type === "SOURCE_FIELD" ? a.value.field : ""))];
7600
+ const requiredSourceFields = [.../* @__PURE__ */ new Set([from.joinKeyField, ...sourceFields])];
7601
+ const sourceRows = await loadUpdateFromSourceRows(
7602
+ from,
7603
+ requiredSourceFields,
7604
+ sourceFields,
7605
+ client,
7606
+ options,
7607
+ cacheContext,
7608
+ tempTables
7609
+ );
7610
+ const sourceByKey = /* @__PURE__ */ new Map();
7611
+ for (const row of sourceRows) {
7612
+ if (!Object.prototype.hasOwnProperty.call(row, from.joinKeyField)) {
7613
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${from.joinKeyField} does not exist.`);
7614
+ }
7615
+ const key = normalizeUpdateFromJoinKey(row[from.joinKeyField], joinKind, "source");
7616
+ if (sourceByKey.has(key)) {
7617
+ throw new Error(`ArgumentError: UPDATE ... FROM source has multiple rows for normalized key ${key}.`);
7618
+ }
7619
+ sourceByKey.set(key, row);
7620
+ }
7621
+ if (sourceByKey.size === 0) return [];
7622
+ const maxRecords = options.maxRecords ?? 1e4;
7623
+ const targetFields = collectUpdateFromTargetFields(stmt);
7624
+ const filterQuery = from.targetFilter === null ? "" : updateToGetQuery({ ...stmt, from: null, where: from.targetFilter }).query;
7625
+ const targetRecords = [];
7626
+ const seenTargetIds = /* @__PURE__ */ new Set();
7627
+ let fetchedTargetCount = 0;
7628
+ for (const keys of splitChunks([...sourceByKey.keys()], UPDATE_FROM_KEY_CHUNK_SIZE)) {
7629
+ const keyQuery = `${from.targetJoinField} in (${keys.map(sqlQuote).join(",")})`;
7630
+ const query = filterQuery ? `(${keyQuery}) and (${filterQuery})` : keyQuery;
7631
+ const resolved = await fetchRecordsForSharedPlan(
7632
+ client.getRecords,
7633
+ stmt.appId,
7634
+ query,
7635
+ targetFields,
7636
+ { maxRecords, parallel: options.fetchParallel ?? 1, onLimit: "error" }
7637
+ );
7638
+ fetchedTargetCount += resolved.records.length;
7639
+ if (fetchedTargetCount > maxRecords) {
7640
+ throw new FetchAllLimitError(
7641
+ `\u53D6\u5F97\u4EF6\u6570\u304C\u4E0A\u9650\uFF08${maxRecords} \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`
7642
+ );
7643
+ }
7644
+ for (const record of resolved.records) {
7645
+ const id = record["$id"]?.value;
7646
+ if (typeof id !== "string" || id === "") {
7647
+ throw new Error("ArgumentError: UPDATE ... FROM target record does not contain a valid $id.");
7648
+ }
7649
+ if (seenTargetIds.has(id)) continue;
7650
+ seenTargetIds.add(id);
7651
+ targetRecords.push(record);
7652
+ }
7653
+ }
7654
+ const matched = [];
7655
+ for (const target of targetRecords) {
7656
+ const raw = target[from.targetJoinField]?.value;
7657
+ const key = normalizeUpdateFromJoinKey(raw, joinKind, "target");
7658
+ if (key === null) continue;
7659
+ const source = sourceByKey.get(key);
7660
+ if (source !== void 0) matched.push({ target, source });
7661
+ }
7662
+ return matched;
7663
+ }
7664
+ async function resolveUpdateFromTargetJoinKind(stmt, from, client, cacheContext) {
7665
+ if (from.targetJoinField === "$id") return "id";
7666
+ const info = (await getFieldsCached(stmt.appId, client, cacheContext)).find((field) => field.code === from.targetJoinField);
7667
+ if (!info) {
7668
+ throw new Error(`ArgumentError: UPDATE ... FROM target column ${from.targetJoinField} does not exist.`);
7669
+ }
7670
+ if (info.inSubtable || info.writable === false || info.fieldType !== "SINGLE_LINE_TEXT" && info.fieldType !== "NUMBER") {
7671
+ throw new Error(
7672
+ `ArgumentError: UPDATE ... FROM does not support target join field type ${info.fieldType} (${from.targetJoinField}).`
7673
+ );
7674
+ }
7675
+ return info.fieldType === "NUMBER" ? "number" : "string";
7676
+ }
7677
+ async function loadUpdateFromSourceRows(from, requiredSourceFields, sourceValueFields, client, options, cacheContext, tempTables) {
7678
+ if (from.cteName !== null) {
7679
+ const table = tempTables?.get(from.cteName);
7680
+ if (!table) throw new Error(`ArgumentError: temp table ${from.cteName} is not available.`);
7681
+ for (const field of requiredSourceFields) {
7682
+ if (!table.columns.includes(field)) {
7683
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
7684
+ }
7685
+ }
7686
+ return table.rows;
7687
+ }
7688
+ const sourceTypes = await getFieldTypeMap(from.appId, client, cacheContext);
7689
+ const joinType = from.joinKeyField === "$id" ? "RECORD_NUMBER" : sourceTypes.get(from.joinKeyField);
7690
+ if (joinType === void 0) {
7691
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${from.joinKeyField} does not exist.`);
7692
+ }
7693
+ if (from.joinKeyField !== "$id" && joinType !== "SINGLE_LINE_TEXT" && joinType !== "NUMBER") {
7694
+ throw new Error(
7695
+ `ArgumentError: UPDATE ... FROM does not support source join field type ${joinType} (${from.joinKeyField}).`
7696
+ );
7697
+ }
7698
+ for (const field of sourceValueFields) {
7699
+ if (field !== "$id" && !sourceTypes.has(field)) {
7700
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
7701
+ }
7702
+ const type = field === "$id" ? "RECORD_NUMBER" : sourceTypes.get(field);
7703
+ if (UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES.has(type ?? "")) {
7704
+ throw new Error(`ArgumentError: UPDATE ... FROM does not support source field type ${type} (${field}).`);
7705
+ }
7706
+ }
7707
+ const resolved = await fetchRecordsForSharedPlan(
7708
+ client.getRecords,
7709
+ from.appId,
7710
+ "",
7711
+ requiredSourceFields,
7712
+ { maxRecords: options.maxRecords ?? 1e4, parallel: options.fetchParallel ?? 1, onLimit: "error" }
7713
+ );
7714
+ return resolved.records.map((record) => flatten(record, null));
7715
+ }
7716
+ function normalizeUpdateFromJoinKey(raw, kind, side) {
7717
+ if (typeof raw !== "string") {
7718
+ throw new Error(`ArgumentError: UPDATE ... FROM ${side} key must be a scalar string: ${String(raw)}`);
7719
+ }
7720
+ if (kind === "string") {
7721
+ if (raw === "") {
7722
+ if (side === "target") return null;
7723
+ throw new Error("ArgumentError: UPDATE ... FROM source key must not be empty.");
7724
+ }
7725
+ return raw;
7726
+ }
7727
+ if (kind === "number" && side === "target" && raw === "") return null;
7728
+ if (kind === "id") {
7729
+ const text2 = raw.trim();
7730
+ const id = Number(text2);
7731
+ if (text2 === "" || !Number.isSafeInteger(id) || id <= 0) {
7732
+ throw new Error(`ArgumentError: UPDATE ... FROM ${side} key must be a positive safe integer: ${raw}`);
7733
+ }
7734
+ return String(id);
7735
+ }
7736
+ const text = raw.trim();
7737
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(text)) {
7738
+ throw new Error(`ArgumentError: UPDATE ... FROM ${side} key must be a finite decimal: ${raw}`);
7739
+ }
7740
+ let unsigned = text;
7741
+ let negative = false;
7742
+ if (unsigned.startsWith("-") || unsigned.startsWith("+")) {
7743
+ negative = unsigned[0] === "-";
7744
+ unsigned = unsigned.slice(1);
7745
+ }
7746
+ let [whole, fraction = ""] = unsigned.split(".");
7747
+ whole = (whole || "0").replace(/^0+(?=\d)/, "");
7748
+ fraction = fraction.replace(/0+$/, "");
7749
+ const zero = /^0*$/.test(whole) && fraction === "";
7750
+ const canonical = fraction === "" ? whole : `${whole}.${fraction}`;
7751
+ return negative && !zero ? `-${canonical}` : canonical;
7752
+ }
6952
7753
  async function executeInsert(stmt, client, options, cacheContext) {
6953
7754
  if (stmt.subtableCode) {
6954
7755
  return executeInsertSubtable(stmt, client, options, cacheContext);
@@ -7048,98 +7849,20 @@ async function executeUpdate(stmt, client, options, cacheContext, tempTables) {
7048
7849
  }
7049
7850
  return { type: "UPDATE", updatedCount: ids.length };
7050
7851
  }
7051
- var UPDATE_FROM_ID_CHUNK_SIZE = 50;
7052
- var UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES = /* @__PURE__ */ new Set([
7053
- "CHECK_BOX",
7054
- "MULTI_SELECT",
7055
- "USER_SELECT",
7056
- "ORGANIZATION_SELECT",
7057
- "GROUP_SELECT",
7058
- "FILE"
7059
- ]);
7060
7852
  async function executeUpdateFrom(stmt, from, client, options, cacheContext, tempTables) {
7061
- const sourceFields = [...new Set(stmt.assignments.filter((a) => a.value.type === "SOURCE_FIELD").map((a) => a.value.type === "SOURCE_FIELD" ? a.value.field : ""))];
7062
- const requiredSourceFields = [.../* @__PURE__ */ new Set([from.joinKeyField, ...sourceFields])];
7063
- let sourceRows;
7064
- if (from.cteName !== null) {
7065
- const table = tempTables?.get(from.cteName);
7066
- if (!table) throw new Error(`ArgumentError: temp table ${from.cteName} is not available.`);
7067
- for (const field of requiredSourceFields) {
7068
- if (!table.columns.includes(field)) {
7069
- throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
7070
- }
7071
- }
7072
- sourceRows = table.rows;
7073
- } else {
7074
- const sourceTypes = await getFieldTypeMap(from.appId, client, cacheContext);
7075
- for (const field of requiredSourceFields) {
7076
- if (field !== "$id" && !sourceTypes.has(field)) {
7077
- throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
7078
- }
7079
- const type = sourceTypes.get(field);
7080
- if (UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES.has(type ?? "")) {
7081
- throw new Error(`ArgumentError: UPDATE ... FROM does not support source field type ${type} (${field}).`);
7082
- }
7083
- }
7084
- const maxRecords = options.maxRecords ?? 1e4;
7085
- const resolved = await fetchRecordsForSharedPlan(
7086
- client.getRecords,
7087
- from.appId,
7088
- "",
7089
- requiredSourceFields,
7090
- { maxRecords, parallel: options.fetchParallel ?? 1, onLimit: "error" }
7091
- );
7092
- sourceRows = resolved.records.map((record) => flatten(record, null));
7093
- }
7094
- const sourceById = /* @__PURE__ */ new Map();
7095
- for (const row of sourceRows) {
7096
- if (!Object.prototype.hasOwnProperty.call(row, from.joinKeyField)) {
7097
- throw new Error(`ArgumentError: UPDATE ... FROM source column ${from.joinKeyField} does not exist.`);
7098
- }
7099
- const raw = row[from.joinKeyField];
7100
- const text = typeof raw === "string" ? raw.trim() : "";
7101
- const id = Number(text);
7102
- if (text === "" || !Number.isSafeInteger(id) || id <= 0) {
7103
- throw new Error(`ArgumentError: UPDATE ... FROM source key must be a positive safe integer: ${String(raw)}`);
7104
- }
7105
- if (sourceById.has(id)) {
7106
- throw new Error(`ArgumentError: UPDATE ... FROM source has multiple rows for target $id ${id}.`);
7107
- }
7108
- sourceById.set(id, row);
7109
- }
7110
- const targetIds = [...sourceById.keys()];
7111
- const targetFields = collectUpdateFromTargetFields(stmt);
7112
- const filterQuery = from.targetFilter === null ? "" : updateToGetQuery({ ...stmt, from: null, where: from.targetFilter }).query;
7113
- const targetRecords = [];
7114
- for (const ids of splitChunks(targetIds, UPDATE_FROM_ID_CHUNK_SIZE)) {
7115
- const idQuery = `$id in (${ids.map((id) => sqlQuote(String(id))).join(",")})`;
7116
- const query = filterQuery ? `(${idQuery}) and (${filterQuery})` : idQuery;
7117
- const resolved = await fetchRecordsForSharedPlan(
7118
- client.getRecords,
7119
- stmt.appId,
7120
- query,
7121
- targetFields,
7122
- { maxRecords: Math.max(ids.length, 1), parallel: options.fetchParallel ?? 1, onLimit: "error" }
7123
- );
7124
- targetRecords.push(...resolved.records);
7125
- }
7853
+ const matched = await resolveUpdateFromMatchedRecords(stmt, from, client, options, cacheContext, tempTables);
7126
7854
  if (options.confirm) {
7127
- const ok = await options.confirm(targetRecords.length, "UPDATE");
7128
- if (!ok) throw new OperationCancelledError("UPDATE", targetRecords.length);
7855
+ const ok = await options.confirm(matched.length, "UPDATE");
7856
+ if (!ok) throw new OperationCancelledError("UPDATE", matched.length);
7129
7857
  }
7130
7858
  const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
7131
- const matched = targetRecords.map((target) => {
7132
- const id = Number(target["$id"]?.value);
7133
- const source = sourceById.get(id);
7134
- if (!source) throw new Error(`ArgumentError: UPDATE ... FROM could not resolve source row for target $id ${id}.`);
7135
- return { target, source };
7136
- });
7137
7859
  const batches = updateFromToPutBatches(stmt, matched, fieldTypes);
7138
7860
  for (const batch of batches) await client.putRecords(batch);
7139
- return { type: "UPDATE", updatedCount: targetRecords.length };
7861
+ return { type: "UPDATE", updatedCount: matched.length };
7140
7862
  }
7141
7863
  function collectUpdateFromTargetFields(stmt) {
7142
7864
  const fields = /* @__PURE__ */ new Set(["$id"]);
7865
+ if (stmt.from) fields.add(stmt.from.targetJoinField);
7143
7866
  const visit = (node) => {
7144
7867
  if (Array.isArray(node)) {
7145
7868
  node.forEach(visit);
@@ -8085,7 +8808,7 @@ function buildUpdatePlan(stmt, label) {
8085
8808
  if (stmt.from) {
8086
8809
  const source = stmt.from.cteName ?? `APP${stmt.from.appId}`;
8087
8810
  lines.push(` source: ${source} AS ${stmt.from.alias}`);
8088
- lines.push(` join: APP${stmt.appId}.$id = ${stmt.from.alias}.${stmt.from.joinKeyField}`);
8811
+ lines.push(` join: APP${stmt.appId}.${stmt.from.targetJoinField} = ${stmt.from.alias}.${stmt.from.joinKeyField}`);
8089
8812
  lines.push(` target filter: ${stmt.from.targetFilter ? safeWhereToKintone(stmt.from.targetFilter) : "(none)"}`);
8090
8813
  } else {
8091
8814
  lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
@@ -8326,12 +9049,32 @@ function isSubtableRow(v) {
8326
9049
  // src/output/batchEnvelope.ts
8327
9050
  function toMutationSummary(result) {
8328
9051
  if (result.type === "INSERT") {
8329
- return { insertedCount: result.insertedCount, createdIds: result.createdIds };
9052
+ return {
9053
+ insertedCount: result.insertedCount,
9054
+ createdIds: result.createdIds,
9055
+ ...result.affectedRows !== void 0 ? { affectedRows: result.affectedRows } : {},
9056
+ ...result.skippedRows !== void 0 ? { skippedRows: result.skippedRows } : {},
9057
+ ...result.rejectLimit !== void 0 ? { rejectLimit: result.rejectLimit } : {},
9058
+ ...result.errTable !== void 0 ? { errTable: result.errTable } : {}
9059
+ };
8330
9060
  }
8331
- if (result.type === "UPDATE") return { updatedCount: result.updatedCount };
9061
+ if (result.type === "UPDATE") return {
9062
+ updatedCount: result.updatedCount,
9063
+ ...result.affectedRows !== void 0 ? { affectedRows: result.affectedRows } : {},
9064
+ ...result.skippedRows !== void 0 ? { skippedRows: result.skippedRows } : {},
9065
+ ...result.rejectLimit !== void 0 ? { rejectLimit: result.rejectLimit } : {},
9066
+ ...result.errTable !== void 0 ? { errTable: result.errTable } : {}
9067
+ };
8332
9068
  if (result.type === "DELETE") return { deletedCount: result.deletedCount };
8333
9069
  if (result.type === "UPSERT") {
8334
- return { insertedCount: result.insertedCount, updatedCount: result.updatedCount };
9070
+ return {
9071
+ insertedCount: result.insertedCount,
9072
+ updatedCount: result.updatedCount,
9073
+ ...result.affectedRows !== void 0 ? { affectedRows: result.affectedRows } : {},
9074
+ ...result.skippedRows !== void 0 ? { skippedRows: result.skippedRows } : {},
9075
+ ...result.rejectLimit !== void 0 ? { rejectLimit: result.rejectLimit } : {},
9076
+ ...result.errTable !== void 0 ? { errTable: result.errTable } : {}
9077
+ };
8335
9078
  }
8336
9079
  return { reorderedParentCount: result.reorderedParentCount };
8337
9080
  }
@@ -8358,11 +9101,31 @@ function buildBatchEnvelope(batch, options = {}) {
8358
9101
  }
8359
9102
  entry.resultIndex = results.length;
8360
9103
  results.push({
9104
+ type: "SELECT",
8361
9105
  columns: s.result.columns,
8362
9106
  rows: s.result.rows,
8363
9107
  rowCount: s.result.rowCount,
8364
9108
  warnings: s.result.warnings ?? []
8365
9109
  });
9110
+ } else if (s.result?.type === "VALIDATION") {
9111
+ totalRows += s.result.errorCount;
9112
+ if (maxTotalRecords !== void 0 && totalRows > maxTotalRecords) {
9113
+ throw new Error(`ArgumentError: batch total rows (${totalRows}) exceed maxTotalRecords (${maxTotalRecords}).`);
9114
+ }
9115
+ entry.resultIndex = results.length;
9116
+ results.push({
9117
+ type: "VALIDATION",
9118
+ columns: s.result.columns,
9119
+ rows: s.result.errors,
9120
+ rowCount: s.result.errorCount,
9121
+ warnings: [],
9122
+ operation: s.result.operation,
9123
+ validatedRows: s.result.validatedRows,
9124
+ validRows: s.result.validRows,
9125
+ invalidRows: s.result.invalidRows,
9126
+ errorCount: s.result.errorCount,
9127
+ ...s.result.errTable ? { errTable: s.result.errTable } : {}
9128
+ });
8366
9129
  } else if (s.status === "success" && s.result && s.result.type !== "SELECT" && s.result.type !== "ASSERT") {
8367
9130
  Object.assign(entry, toMutationSummary(s.result));
8368
9131
  }
@@ -8643,6 +9406,9 @@ function clampInt(v, min, max) {
8643
9406
 
8644
9407
  // src/core/formFieldInfo.ts
8645
9408
  function flattenFormFieldProperties(properties) {
9409
+ return flattenFields(properties, collectLookupCopyFields(properties));
9410
+ }
9411
+ function flattenFields(properties, lookupCopyFields, inSubtable = false) {
8646
9412
  const out = [];
8647
9413
  for (const field of Object.values(properties)) {
8648
9414
  out.push({
@@ -8650,12 +9416,49 @@ function flattenFormFieldProperties(properties) {
8650
9416
  label: field.label,
8651
9417
  fieldType: field.type,
8652
9418
  optionOrder: toOptionOrderMap(field.options),
8653
- sortKind: detectSortKind(field.type, field.format)
9419
+ sortKind: detectSortKind(field.type, field.format),
9420
+ required: field.required,
9421
+ minValue: normalizeConstraintValue(field.minValue),
9422
+ maxValue: normalizeConstraintValue(field.maxValue),
9423
+ minLength: normalizeConstraintValue(field.minLength),
9424
+ maxLength: normalizeConstraintValue(field.maxLength),
9425
+ defaultValue: field.defaultValue,
9426
+ inSubtable,
9427
+ writable: !lookupCopyFields.has(field.code) && !NON_WRITABLE_FIELD_TYPES2.has(field.type)
8654
9428
  });
8655
- if (field.fields) out.push(...flattenFormFieldProperties(field.fields));
9429
+ if (field.fields) out.push(...flattenFields(field.fields, lookupCopyFields, true));
8656
9430
  }
8657
9431
  return out;
8658
9432
  }
9433
+ var NON_WRITABLE_FIELD_TYPES2 = /* @__PURE__ */ new Set([
9434
+ "CALC",
9435
+ "RECORD_NUMBER",
9436
+ "CREATOR",
9437
+ "CREATED_TIME",
9438
+ "MODIFIER",
9439
+ "UPDATED_TIME",
9440
+ "STATUS",
9441
+ "STATUS_ASSIGNEE",
9442
+ "CATEGORY",
9443
+ "REFERENCE_TABLE",
9444
+ "SUBTABLE"
9445
+ ]);
9446
+ function collectLookupCopyFields(properties) {
9447
+ const result = /* @__PURE__ */ new Set();
9448
+ const visit = (fields) => {
9449
+ for (const field of Object.values(fields)) {
9450
+ for (const mapping of field.lookup?.fieldMappings ?? []) {
9451
+ if (mapping.field) result.add(mapping.field);
9452
+ }
9453
+ if (field.fields) visit(field.fields);
9454
+ }
9455
+ };
9456
+ visit(properties);
9457
+ return result;
9458
+ }
9459
+ function normalizeConstraintValue(value) {
9460
+ return value == null || value === "" ? void 0 : value;
9461
+ }
8659
9462
  function toOptionOrderMap(options) {
8660
9463
  if (!options || typeof options !== "object") return void 0;
8661
9464
  const order = {};
@@ -9788,6 +10591,11 @@ function buildAssertOutput(result, format, pretty) {
9788
10591
  if (format === "jsonl") return JSON.stringify(payload);
9789
10592
  return `assertion ok: ${result.condition}`;
9790
10593
  }
10594
+ function buildValidationOutput(result, format, noHeader, pretty, displayOptions) {
10595
+ if (format === "json") return JSON.stringify({ ok: true, ...result, metrics: void 0 }, null, pretty ? 2 : 0);
10596
+ if (format === "jsonl") return result.errors.map((row) => JSON.stringify(row)).join("\n");
10597
+ return buildOutput({ type: "SELECT", columns: result.columns, rows: result.errors, rowCount: result.errorCount }, format, noHeader, pretty, displayOptions);
10598
+ }
9791
10599
  function buildMutationOutput(result, format, noHeader, pretty) {
9792
10600
  const row = { type: result.type };
9793
10601
  if (result.type === "INSERT") {
@@ -9802,6 +10610,12 @@ function buildMutationOutput(result, format, noHeader, pretty) {
9802
10610
  } else if (result.type === "REORDER") {
9803
10611
  row.reorderedParentCount = result.reorderedParentCount;
9804
10612
  }
10613
+ if (result.type === "INSERT" || result.type === "UPDATE" || result.type === "UPSERT") {
10614
+ if (result.affectedRows !== void 0) row.affectedRows = result.affectedRows;
10615
+ if (result.skippedRows !== void 0) row.skippedRows = result.skippedRows;
10616
+ if (result.rejectLimit !== void 0) row.rejectLimit = result.rejectLimit;
10617
+ if (result.errTable !== void 0) row.errTable = result.errTable;
10618
+ }
9805
10619
  if (format === "json") return JSON.stringify(row, null, pretty ? 2 : 0);
9806
10620
  if (format === "jsonl") return JSON.stringify(row);
9807
10621
  const cols = Object.keys(row);
@@ -9912,6 +10726,14 @@ function buildBatchStatementSummary(s) {
9912
10726
  else if (r.type === "DELETE") parts.push(`deleted=${r.deletedCount}`);
9913
10727
  else if (r.type === "UPSERT") parts.push(`inserted=${r.insertedCount} updated=${r.updatedCount}`);
9914
10728
  else if (r.type === "REORDER") parts.push(`reordered=${r.reorderedParentCount}`);
10729
+ else if (r.type === "VALIDATION") parts.push(`validated=${r.validatedRows} valid=${r.validRows} invalid=${r.invalidRows} errors=${r.errorCount}`);
10730
+ if ((r.type === "INSERT" || r.type === "UPDATE" || r.type === "UPSERT") && r.skippedRows !== void 0) {
10731
+ parts.push(`affected=${r.affectedRows} skipped=${r.skippedRows} errTable=${r.errTable}`);
10732
+ }
10733
+ }
10734
+ if (s.status === "error" && s.result?.type === "VALIDATION") {
10735
+ const r = s.result;
10736
+ parts.push(`validated=${r.validatedRows} valid=${r.validRows} invalid=${r.invalidRows} errors=${r.errorCount}`);
9915
10737
  }
9916
10738
  if (s.status === "error" && s.error) parts.push(s.error.message);
9917
10739
  if (s.status === "skipped" && s.skippedReason) parts.push(`reason=${s.skippedReason}`);
@@ -9945,6 +10767,8 @@ function buildBatchResultsOutput(batch, opts) {
9945
10767
  for (const s of batch.statements) {
9946
10768
  if (s.status === "success" && s.result?.type === "SELECT") {
9947
10769
  outputs.push(buildOutput(s.result, opts.format, opts.noHeader, opts.pretty, opts.displayOptions));
10770
+ } else if (s.result?.type === "VALIDATION") {
10771
+ outputs.push(buildValidationOutput(s.result, opts.format, opts.noHeader, opts.pretty, opts.displayOptions));
9948
10772
  }
9949
10773
  }
9950
10774
  return outputs.join("\n\n");
@@ -10607,6 +11431,7 @@ async function run() {
10607
11431
  let isBatchSql = false;
10608
11432
  let batchContainsDml = false;
10609
11433
  let batchAnalysis = null;
11434
+ let needsCompleteInput = false;
10610
11435
  if (args.diagRecordId === null) {
10611
11436
  sql = args.executeSql;
10612
11437
  if (!sql && args.filePath) sql = (0, import_fs2.readFileSync)(args.filePath, "utf-8");
@@ -10637,14 +11462,16 @@ async function run() {
10637
11462
  batchAnalysis = analyzeBatch(statements);
10638
11463
  isBatchSql = true;
10639
11464
  batchContainsDml = batchAnalysis.containsDml;
11465
+ needsCompleteInput = batchAnalysis.requiresCompleteInput;
10640
11466
  } else {
10641
11467
  const stmt = parseSqlStatement(sql);
10642
11468
  parsedStmt = stmt;
10643
11469
  stmtType = getStatementType(stmt);
10644
- isDmlStatement = isDmlType(stmtType);
11470
+ isDmlStatement = writesKintone(stmt);
11471
+ needsCompleteInput = requiresCompleteInput(stmt);
10645
11472
  hasWhere = hasWhereClause(stmt);
10646
11473
  insertValuesCount = getInsertValuesCount(stmt);
10647
- const supported = stmtType === "SELECT" || stmtType === "UNION" || stmtType === "WITH" || stmtType === "EXPLAIN" || stmtType === "SHOW_APPS" || stmtType === "DESCRIBE" || stmtType === "ASSERT" || isDmlStatement;
11474
+ const supported = stmtType === "SELECT" || stmtType === "UNION" || stmtType === "WITH" || stmtType === "EXPLAIN" || stmtType === "SHOW_APPS" || stmtType === "DESCRIBE" || stmtType === "ASSERT" || isDmlType(stmtType);
10648
11475
  if (!supported) {
10649
11476
  process.stderr.write(`ArgumentError: unsupported statement type in CLI: ${stmtType}
10650
11477
  `);
@@ -10688,10 +11515,10 @@ async function run() {
10688
11515
  const yes = args.yes || envBool("KSQL_YES") === true || Boolean(profile.dml?.yes);
10689
11516
  const allowWithoutWhere = args.allowWithoutWhere || envBool("KSQL_ALLOW_WITHOUT_WHERE") === true || Boolean(profile.dml?.allowWithoutWhere);
10690
11517
  const dmlMaxRows = args.dmlMaxRows ?? envInt2("KSQL_DML_MAX_ROWS") ?? profile.dml?.maxRows ?? 100;
10691
- const dmlForcesOnLimitError = isDmlStatement || batchContainsDml;
11518
+ const dmlForcesOnLimitError = needsCompleteInput;
10692
11519
  const effectiveOnLimit = dmlForcesOnLimitError ? "error" : onLimit;
10693
11520
  if (dmlForcesOnLimitError && onLimit === "truncate" && !quiet && !args.dryRun) {
10694
- process.stderr.write("note: onLimit=truncate is ignored for DML (forced to error)\n");
11521
+ process.stderr.write(isDmlStatement || batchContainsDml ? "note: onLimit=truncate is ignored for DML (forced to error)\n" : "note: onLimit=truncate is ignored for VALIDATE ONLY (forced to error)\n");
10695
11522
  }
10696
11523
  if (format === "markdown" && noHeader) {
10697
11524
  process.stderr.write("ArgumentError: --no-header cannot be used with --format markdown|md.\n");
@@ -11074,6 +11901,16 @@ query=${label}`);
11074
11901
  if (outputPath) (0, import_fs2.writeFileSync)(outputPath, `${output2}
11075
11902
  `, "utf-8");
11076
11903
  else if (output2) process.stdout.write(`${output2}
11904
+ `);
11905
+ return 0;
11906
+ }
11907
+ if (result.type === "VALIDATION") {
11908
+ const output2 = buildValidationOutput(result, format, noHeader, pretty, displayOptions);
11909
+ if (outputPath) (0, import_fs2.writeFileSync)(outputPath, `${output2}
11910
+ `, "utf-8");
11911
+ else if (output2) process.stdout.write(`${output2}
11912
+ `);
11913
+ if (!quiet) process.stderr.write(`validated=${result.validatedRows} valid=${result.validRows} invalid=${result.invalidRows} errors=${result.errorCount}
11077
11914
  `);
11078
11915
  return 0;
11079
11916
  }
@@ -11127,6 +11964,7 @@ if (isDirectCliRun()) {
11127
11964
  buildBatchStatementSummary,
11128
11965
  buildOutput,
11129
11966
  buildReplExecArgv,
11967
+ buildValidationOutput,
11130
11968
  extractAppIds,
11131
11969
  normalizeAppKey,
11132
11970
  normalizeSqlAppProfiles,