@rex0220/kintone-sql-tools 2.11.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");
@@ -2054,6 +2063,30 @@ var Parser = class {
2054
2063
  const { appId, subtableCode } = extractTableRef(name, this.prev());
2055
2064
  this.expect("SET" /* SET */);
2056
2065
  const assignments = this.parseAssignments();
2066
+ let from = null;
2067
+ if (this.consume("FROM" /* FROM */)) {
2068
+ const table = this.parseTableRef();
2069
+ if (table.subtableCode) {
2070
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u306B\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u306F\u6307\u5B9A\u3067\u304D\u307E\u305B\u3093", this.prev());
2071
+ }
2072
+ if (table.cteName !== null && !table.cteName.startsWith("#")) {
2073
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u306F #temp \u307E\u305F\u306F APP<n> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08CTE \u306F\u975E\u5BFE\u5FDC\uFF09", this.prev());
2074
+ }
2075
+ if (!table.alias) {
2076
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u306B\u306F\u30A8\u30A4\u30EA\u30A2\u30B9\u304C\u5FC5\u8981\u3067\u3059", this.prev());
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
+ }
2081
+ from = {
2082
+ appId: table.appId,
2083
+ cteName: table.cteName,
2084
+ alias: table.alias,
2085
+ targetJoinField: "",
2086
+ joinKeyField: "",
2087
+ targetFilter: null
2088
+ };
2089
+ }
2057
2090
  const whereTok = this.peek();
2058
2091
  if (!this.consume("WHERE" /* WHERE */)) {
2059
2092
  throw new ParseError(
@@ -2062,7 +2095,191 @@ var Parser = class {
2062
2095
  );
2063
2096
  }
2064
2097
  const where = this.parseWhereExpr();
2065
- return subtableCode ? { type: "UPDATE", appId, subtableCode, assignments, where } : { type: "UPDATE", appId, assignments, where };
2098
+ if (from !== null) {
2099
+ if (subtableCode) {
2100
+ throw new ParseError("\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB UPDATE ... FROM \u306F\u30B5\u30DD\u30FC\u30C8\u3057\u3066\u3044\u307E\u305B\u3093", whereTok);
2101
+ }
2102
+ this.validateUpdateFromAssignments(assignments, from.alias, whereTok);
2103
+ const decomposed = this.decomposeUpdateFromWhere(where, appId, from.alias, whereTok);
2104
+ from.targetJoinField = decomposed.targetJoinField;
2105
+ from.joinKeyField = decomposed.joinKeyField;
2106
+ from.targetFilter = decomposed.targetFilter;
2107
+ } else if (assignments.some((a) => a.value.type === "SOURCE_FIELD")) {
2108
+ throw new ParseError(
2109
+ "SET \u306E\u5024\u306B\u306F\u30EA\u30C6\u30E9\u30EB\u30FB\u7B97\u8853\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u306E\u307F\u306F\u4E0D\u53EF\uFF09",
2110
+ whereTok
2111
+ );
2112
+ }
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;
2173
+ }
2174
+ validateUpdateFromAssignments(assignments, sourceAlias, tok) {
2175
+ for (const assignment of assignments) {
2176
+ if (assignment.value.type === "SOURCE_FIELD") {
2177
+ if (assignment.value.alias.toLowerCase() !== sourceAlias.toLowerCase()) {
2178
+ throw new ParseError(`UPDATE ... FROM \u306E SET \u53C2\u7167\u306F\u30BD\u30FC\u30B9 alias ${sourceAlias} \u3067\u4FEE\u98FE\u3057\u3066\u304F\u3060\u3055\u3044`, tok);
2179
+ }
2180
+ continue;
2181
+ }
2182
+ if (this.nodeContainsQualifiedField(assignment.value, sourceAlias)) {
2183
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u5217\u306F SET \u306E\u76F4\u63A5\u5024\u3068\u3057\u3066\u306E\u307F\u53C2\u7167\u3067\u304D\u307E\u3059", tok);
2184
+ }
2185
+ if (assignment.value.type === "SCALAR_SUBQUERY") {
2186
+ throw new ParseError("UPDATE ... FROM \u306E SET \u3067\u306F\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u3092\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
2187
+ }
2188
+ if (this.nodeContainsAnyQualifier(assignment.value)) {
2189
+ throw new ParseError("UPDATE ... FROM \u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u5F0F\u3067\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u3092\u4FEE\u98FE\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044", tok);
2190
+ }
2191
+ }
2192
+ }
2193
+ decomposeUpdateFromWhere(where, targetAppId, sourceAlias, tok) {
2194
+ const leaves = this.flattenTopLevelAnd(where);
2195
+ const joins = [];
2196
+ leaves.forEach((leaf, index) => {
2197
+ const matched = this.matchUpdateFromJoin(leaf, targetAppId, sourceAlias);
2198
+ if (matched !== null) joins.push({ index, ...matched });
2199
+ });
2200
+ if (joins.length !== 1) {
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);
2202
+ }
2203
+ const join2 = joins[0];
2204
+ for (let i = 0; i < leaves.length; i++) {
2205
+ if (i !== join2.index && this.nodeContainsQualifiedField(leaves[i], sourceAlias)) {
2206
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9 alias \u306F\u7D50\u5408\u7B49\u5024\u4EE5\u5916\u306E WHERE \u6761\u4EF6\u3067\u306F\u53C2\u7167\u3067\u304D\u307E\u305B\u3093", tok);
2207
+ }
2208
+ if (i !== join2.index && this.nodeContainsForeignQualifier(leaves[i], targetAppId)) {
2209
+ throw new ParseError(`UPDATE ... FROM \u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u30D5\u30A3\u30EB\u30BF\u306F APP${targetAppId} \u306E\u30D5\u30A3\u30FC\u30EB\u30C9\u3060\u3051\u3092\u53C2\u7167\u3067\u304D\u307E\u3059`, tok);
2210
+ }
2211
+ }
2212
+ const filters = leaves.filter((_, index) => index !== join2.index);
2213
+ const targetFilter = filters.reduce(
2214
+ (acc, expr) => acc === null ? expr : { type: "LOGICAL", op: "AND", left: acc, right: expr },
2215
+ null
2216
+ );
2217
+ return { targetJoinField: join2.targetField, joinKeyField: join2.sourceField, targetFilter };
2218
+ }
2219
+ flattenTopLevelAnd(expr) {
2220
+ if (expr.type === "GROUP") return this.flattenTopLevelAnd(expr.expr);
2221
+ if (expr.type === "LOGICAL" && expr.op === "AND") {
2222
+ return [...this.flattenTopLevelAnd(expr.left), ...this.flattenTopLevelAnd(expr.right)];
2223
+ }
2224
+ return [expr];
2225
+ }
2226
+ matchUpdateFromJoin(expr, targetAppId, sourceAlias) {
2227
+ if (expr.type !== "BINARY" || expr.op !== "=" || expr.left.type !== "FIELD") return null;
2228
+ const right = expr.right.type === "ARITH_VALUE" && expr.right.expr.type === "FIELD_REF" ? this.splitQualifiedField(expr.right.expr.field) : null;
2229
+ if (right === null) return null;
2230
+ const left = { alias: expr.left.tableAlias, field: expr.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
+ }
2237
+ return null;
2238
+ }
2239
+ splitQualifiedField(field) {
2240
+ const dot = field.indexOf(".");
2241
+ return dot < 0 ? { alias: null, field } : { alias: field.slice(0, dot), field: field.slice(dot + 1) };
2242
+ }
2243
+ isTargetRef(ref, appId) {
2244
+ return ref.alias === null || ref.alias.toLowerCase() === `app${appId}`.toLowerCase();
2245
+ }
2246
+ isSourceRef(ref, alias) {
2247
+ return ref.alias?.toLowerCase() === alias.toLowerCase();
2248
+ }
2249
+ nodeContainsQualifiedField(node, alias) {
2250
+ if (Array.isArray(node)) return node.some((v) => this.nodeContainsQualifiedField(v, alias));
2251
+ if (node === null || typeof node !== "object") return false;
2252
+ const obj = node;
2253
+ if (obj["type"] === "FIELD" && typeof obj["tableAlias"] === "string" && obj["tableAlias"].toLowerCase() === alias.toLowerCase()) return true;
2254
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") {
2255
+ const ref = this.splitQualifiedField(obj["field"]);
2256
+ if (this.isSourceRef(ref, alias)) return true;
2257
+ }
2258
+ return Object.values(obj).some((v) => this.nodeContainsQualifiedField(v, alias));
2259
+ }
2260
+ nodeContainsForeignQualifier(node, targetAppId) {
2261
+ if (Array.isArray(node)) return node.some((v) => this.nodeContainsForeignQualifier(v, targetAppId));
2262
+ if (node === null || typeof node !== "object") return false;
2263
+ const obj = node;
2264
+ const expected = `app${targetAppId}`.toLowerCase();
2265
+ if (obj["type"] === "FIELD" && typeof obj["tableAlias"] === "string") {
2266
+ return obj["tableAlias"].toLowerCase() !== expected;
2267
+ }
2268
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") {
2269
+ const ref = this.splitQualifiedField(obj["field"]);
2270
+ if (ref.alias !== null) return ref.alias.toLowerCase() !== expected;
2271
+ }
2272
+ return Object.values(obj).some((v) => this.nodeContainsForeignQualifier(v, targetAppId));
2273
+ }
2274
+ nodeContainsAnyQualifier(node) {
2275
+ if (Array.isArray(node)) return node.some((v) => this.nodeContainsAnyQualifier(v));
2276
+ if (node === null || typeof node !== "object") return false;
2277
+ const obj = node;
2278
+ if (obj["type"] === "FIELD" && typeof obj["tableAlias"] === "string") return true;
2279
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") {
2280
+ if (this.splitQualifiedField(obj["field"]).alias !== null) return true;
2281
+ }
2282
+ return Object.values(obj).some((v) => this.nodeContainsAnyQualifier(v));
2066
2283
  }
2067
2284
  parseAssignments() {
2068
2285
  const assignments = [];
@@ -2098,6 +2315,12 @@ var Parser = class {
2098
2315
  const node = this.parseArithAddSub();
2099
2316
  if (node.type === "NUMBER") return node;
2100
2317
  if (node.type === "ARITH") return node;
2318
+ if (node.type === "FIELD_REF") {
2319
+ const dot = node.field.indexOf(".");
2320
+ if (dot > 0 && dot < node.field.length - 1) {
2321
+ return { type: "SOURCE_FIELD", alias: node.field.slice(0, dot), field: node.field.slice(dot + 1) };
2322
+ }
2323
+ }
2101
2324
  throw new ParseError(
2102
2325
  "SET \u306E\u5024\u306B\u306F\u30EA\u30C6\u30E9\u30EB\u30FB\u7B97\u8853\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u306E\u307F\u306F\u4E0D\u53EF\uFF09",
2103
2326
  tok
@@ -2345,6 +2568,15 @@ function isDmlType(type) {
2345
2568
  function isReadOnlyType(type) {
2346
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";
2347
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
+ }
2348
2580
  function hasWhereClause(stmt) {
2349
2581
  if (!stmt || typeof stmt !== "object") return false;
2350
2582
  const obj = stmt;
@@ -3584,11 +3816,17 @@ function analyzeBatch(statements) {
3584
3816
  }
3585
3817
  }
3586
3818
  const defined = /* @__PURE__ */ new Map();
3819
+ const validationSchemas = /* @__PURE__ */ new Map();
3587
3820
  const createdOrder = [];
3588
3821
  const results = [];
3589
3822
  const variableDefs = /* @__PURE__ */ new Map();
3590
3823
  const variableOrder = [];
3591
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
+ }
3592
3830
  const statementType = getStatementType(stmt);
3593
3831
  const created = [];
3594
3832
  const dropped = [];
@@ -3643,6 +3881,28 @@ function analyzeBatch(statements) {
3643
3881
  }
3644
3882
  dependsOn.add(at);
3645
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
+ }
3646
3906
  if (stmt.type === "CREATE_TEMP_TABLE") {
3647
3907
  if (defined.has(stmt.name)) {
3648
3908
  throw new BatchAnalysisError(
@@ -3675,8 +3935,8 @@ function analyzeBatch(statements) {
3675
3935
  results.push({
3676
3936
  index,
3677
3937
  statementType,
3678
- isDml: isDmlType(statementType),
3679
- isReadOnly: isReadOnlyType(statementType),
3938
+ isDml: writesKintone(stmt),
3939
+ isReadOnly: isReadOnlyStatement(stmt),
3680
3940
  hasWhere: hasWhereClause(stmt),
3681
3941
  insertValuesCount: getInsertValuesCount(stmt),
3682
3942
  appIds: [...stmtAppIds].sort((a, b) => a - b),
@@ -3685,10 +3945,16 @@ function analyzeBatch(statements) {
3685
3945
  tempTablesDropped: dropped,
3686
3946
  dependsOn: [...dependsOn].sort((a, b) => a - b),
3687
3947
  tempOnlySource,
3688
- targetAppId: isDmlType(statementType) && typeof stmt.appId === "number" ? stmt.appId : null
3948
+ targetAppId: isDmlType(statementType) && typeof stmt.appId === "number" ? stmt.appId : 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)
3689
3953
  });
3690
3954
  });
3691
3955
  const containsDml = results.some((r) => r.isDml);
3956
+ const containsValidationOnly = results.some((r) => r.isValidationOnly);
3957
+ const needsCompleteInput = results.some((r) => r.requiresCompleteInput);
3692
3958
  const variables = variableOrder.map((name) => ({
3693
3959
  name,
3694
3960
  referencedBy: [...variableDefs.get(name).referencedBy]
@@ -3697,6 +3963,8 @@ function analyzeBatch(statements) {
3697
3963
  statementCount: statements.length,
3698
3964
  isReadOnlyBatch: !containsDml && results.every((r) => r.isReadOnly),
3699
3965
  containsDml,
3966
+ containsValidationOnly,
3967
+ requiresCompleteInput: needsCompleteInput,
3700
3968
  tempTables: createdOrder,
3701
3969
  variables,
3702
3970
  warnings: variables.filter((v) => v.referencedBy.length === 0).map((v) => `variable @${v.name} is never used.`),
@@ -4230,7 +4498,7 @@ function updateToPutBatches(stmt, ids, fieldTypes = /* @__PURE__ */ new Map()) {
4230
4498
  function buildUpdateRecord(assignments, fieldTypes) {
4231
4499
  const record = {};
4232
4500
  for (const { field, value } of assignments) {
4233
- if (value.type === "ARITH" || value.type === "CASE_VALUE") continue;
4501
+ if (value.type === "ARITH" || value.type === "CASE_VALUE" || value.type === "SOURCE_FIELD") continue;
4234
4502
  record[field] = { value: toKintoneValue(value, fieldTypes.get(field)) };
4235
4503
  }
4236
4504
  return record;
@@ -4334,6 +4602,8 @@ function updateToPutBatchesArith(stmt, records, fieldTypes = /* @__PURE__ */ new
4334
4602
  record[field] = { value: String(evalArith(value, raw)) };
4335
4603
  } else if (value.type === "CASE_VALUE") {
4336
4604
  record[field] = { value: evalCaseWhenValue(value.expr, row, fieldTypes.get(field)) };
4605
+ } else if (value.type === "SOURCE_FIELD") {
4606
+ throw new DmlConvertError("SOURCE_FIELD \u306F UPDATE ... FROM \u5C02\u7528\u3067\u3059");
4337
4607
  } else {
4338
4608
  record[field] = { value: toKintoneValue(value, fieldTypes.get(field)) };
4339
4609
  }
@@ -4345,6 +4615,51 @@ function updateToPutBatchesArith(stmt, records, fieldTypes = /* @__PURE__ */ new
4345
4615
  records: batch
4346
4616
  }));
4347
4617
  }
4618
+ var UPDATE_FROM_UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
4619
+ "CHECK_BOX",
4620
+ "MULTI_SELECT",
4621
+ "USER_SELECT",
4622
+ "ORGANIZATION_SELECT",
4623
+ "GROUP_SELECT",
4624
+ "FILE"
4625
+ ]);
4626
+ function updateFromToPutBatches(stmt, matched, fieldTypes = /* @__PURE__ */ new Map()) {
4627
+ const updateRecords = matched.map(({ target, source }) => {
4628
+ const id = Number(target["$id"]?.value);
4629
+ if (!Number.isSafeInteger(id) || id <= 0) {
4630
+ throw new DmlConvertError("UPDATE ... FROM \u306E\u5BFE\u8C61\u30EC\u30B3\u30FC\u30C9\u756A\u53F7\u304C\u4E0D\u6B63\u3067\u3059");
4631
+ }
4632
+ const targetRow = kintoneRecordToProcessRow(target);
4633
+ const record = {};
4634
+ for (const { field, value } of stmt.assignments) {
4635
+ const fieldType = fieldTypes.get(field);
4636
+ if (value.type === "SOURCE_FIELD") {
4637
+ if (UPDATE_FROM_UNSUPPORTED_TYPES.has(fieldType ?? "")) {
4638
+ throw new DmlConvertError(`UPDATE ... FROM \u306E SOURCE_FIELD \u306F ${fieldType} \u30D5\u30A3\u30FC\u30EB\u30C9\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u305B\u3093\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9: ${field}\uFF09`);
4639
+ }
4640
+ if (!Object.prototype.hasOwnProperty.call(source, value.field)) {
4641
+ throw new DmlConvertError(`UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u5217 ${value.field} \u304C\u5B58\u5728\u3057\u307E\u305B\u3093`);
4642
+ }
4643
+ const raw = source[value.field];
4644
+ if (typeof raw !== "string") {
4645
+ throw new DmlConvertError(`UPDATE ... FROM \u306E SOURCE_FIELD \u306F\u30B9\u30AB\u30E9\u30FC\u5024\u306E\u307F\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u3059\uFF08\u5217: ${value.field}\uFF09`);
4646
+ }
4647
+ if ((fieldType === "NUMBER" || fieldType === "CALC") && raw !== "" && !Number.isFinite(Number(raw))) {
4648
+ throw new DmlConvertError(`\u6570\u5024\u30D5\u30A3\u30FC\u30EB\u30C9 ${field} \u306B\u5909\u63DB\u3067\u304D\u306A\u3044\u5024\u3067\u3059: ${raw}`);
4649
+ }
4650
+ record[field] = { value: toKintoneValue({ type: "STRING", value: raw }, fieldType) };
4651
+ } else if (value.type === "ARITH") {
4652
+ record[field] = { value: String(evalArith(value, target)) };
4653
+ } else if (value.type === "CASE_VALUE") {
4654
+ record[field] = { value: evalCaseWhenValue(value.expr, targetRow, fieldType) };
4655
+ } else {
4656
+ record[field] = { value: toKintoneValue(value, fieldType) };
4657
+ }
4658
+ }
4659
+ return { id, record };
4660
+ });
4661
+ return chunk(updateRecords, 100).map((records) => ({ app: stmt.appId, records }));
4662
+ }
4348
4663
  function kintoneRecordToProcessRow(raw) {
4349
4664
  return Object.fromEntries(
4350
4665
  Object.entries(raw).map(([k, v]) => [
@@ -4459,6 +4774,18 @@ function evalCaseWhenValue(expr, row, fieldType) {
4459
4774
  return "";
4460
4775
  }
4461
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) {
4462
4789
  switch (value.type) {
4463
4790
  case "VARIABLE":
4464
4791
  throw new DmlConvertError(`\u672A\u89E3\u6C7A\u306E\u30D0\u30C3\u30C1\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
@@ -5299,6 +5626,228 @@ function toFlatString(value) {
5299
5626
  }
5300
5627
  }
5301
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
+
5302
5851
  // src/execute.ts
5303
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";
5304
5853
  var SearchAbortedError = class extends Error {
@@ -5402,6 +5951,15 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
5402
5951
  throw new Error(`ParseError: variable @${unresolved} is not defined in a batch.`);
5403
5952
  }
5404
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
+ }
5405
5963
  switch (stmt.type) {
5406
5964
  case "SELECT":
5407
5965
  return executeSelect(stmt, client, options, cacheContext);
@@ -5443,6 +6001,17 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
5443
6001
  }
5444
6002
  }
5445
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
+ }
5446
6015
  var BatchTimeoutError = class extends Error {
5447
6016
  constructor() {
5448
6017
  super("TimeoutError: batch timeout exceeded.");
@@ -5459,6 +6028,8 @@ async function executeBatch(sql, client, options = {}) {
5459
6028
  for (const s of analysis.statements) {
5460
6029
  if (!s.isDml || s.tempTablesReferenced.length === 0) continue;
5461
6030
  if (s.statementType === "INSERT_SELECT" || s.statementType === "UPSERT_SELECT") continue;
6031
+ const parsed = statements[s.index];
6032
+ if (parsed?.type === "UPDATE" && parsed.from?.cteName != null) continue;
5462
6033
  throw new BatchAnalysisError(
5463
6034
  `ArgumentError: temp table references in ${s.statementType} are not supported yet.`,
5464
6035
  s.index
@@ -5522,7 +6093,12 @@ async function executeBatch(sql, client, options = {}) {
5522
6093
  }
5523
6094
  results.push({ ...base, status: "success", ...outcome });
5524
6095
  } catch (e) {
5525
- 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
+ });
5526
6102
  failed.add(i);
5527
6103
  if (e instanceof BatchTimeoutError) {
5528
6104
  aborted = "timeout";
@@ -5581,6 +6157,38 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
5581
6157
  }
5582
6158
  const resolvedStmt = resolveVariableRefs(stmt, variables);
5583
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
+ }
5584
6192
  if (resolvedStmt.type === "CREATE_TEMP_TABLE") {
5585
6193
  const materializeOptions = {
5586
6194
  ...options,
@@ -5615,6 +6223,9 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
5615
6223
  if (resolvedStmt.type === "UPSERT_SELECT") {
5616
6224
  return { result: await executeUpsertSelect(resolvedStmt, client, options, cacheContext, tempTables) };
5617
6225
  }
6226
+ if (resolvedStmt.type === "UPDATE" && resolvedStmt.from?.cteName != null) {
6227
+ return { result: await executeUpdate(resolvedStmt, client, options, cacheContext, tempTables) };
6228
+ }
5618
6229
  throw new Error(`ArgumentError: temp table references in ${stmt.type} are not supported yet.`);
5619
6230
  }
5620
6231
  return { result: await executeParsedStatement(resolvedStmt, client, options, cacheContext) };
@@ -6726,7 +7337,7 @@ async function buildSortKindsForSelect(stmt, client, cacheContext) {
6726
7337
  }
6727
7338
  function convertProcessRowValue(raw, dstFieldType) {
6728
7339
  const USER_TYPES2 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
6729
- const ARRAY_TYPES2 = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
7340
+ const ARRAY_TYPES3 = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
6730
7341
  if (USER_TYPES2.has(dstFieldType ?? "")) {
6731
7342
  if (raw === "") return [];
6732
7343
  try {
@@ -6738,7 +7349,7 @@ function convertProcessRowValue(raw, dstFieldType) {
6738
7349
  }
6739
7350
  return raw.split(",").map((c) => ({ code: c.trim() }));
6740
7351
  }
6741
- if (ARRAY_TYPES2.has(dstFieldType ?? "")) {
7352
+ if (ARRAY_TYPES3.has(dstFieldType ?? "")) {
6742
7353
  if (raw === "") return [];
6743
7354
  try {
6744
7355
  const parsed = JSON.parse(raw);
@@ -6749,6 +7360,396 @@ function convertProcessRowValue(raw, dstFieldType) {
6749
7360
  }
6750
7361
  return raw;
6751
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
+ }
6752
7753
  async function executeInsert(stmt, client, options, cacheContext) {
6753
7754
  if (stmt.subtableCode) {
6754
7755
  return executeInsertSubtable(stmt, client, options, cacheContext);
@@ -6800,10 +7801,13 @@ async function executeInsertSelect(stmt, client, options, cacheContext, cteCache
6800
7801
  insertedCount: createdIds.flat().length
6801
7802
  };
6802
7803
  }
6803
- async function executeUpdate(stmt, client, options, cacheContext) {
7804
+ async function executeUpdate(stmt, client, options, cacheContext, tempTables) {
6804
7805
  if (stmt.subtableCode) {
6805
7806
  return executeUpdateSubtable(stmt, client, options, cacheContext);
6806
7807
  }
7808
+ if (stmt.from != null) {
7809
+ return executeUpdateFrom(stmt, stmt.from, client, options, cacheContext, tempTables);
7810
+ }
6807
7811
  const maxRecords = options.maxRecords ?? 1e4;
6808
7812
  await resolveSetSubqueries(stmt.assignments, client, options, cacheContext);
6809
7813
  const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
@@ -6845,6 +7849,35 @@ async function executeUpdate(stmt, client, options, cacheContext) {
6845
7849
  }
6846
7850
  return { type: "UPDATE", updatedCount: ids.length };
6847
7851
  }
7852
+ async function executeUpdateFrom(stmt, from, client, options, cacheContext, tempTables) {
7853
+ const matched = await resolveUpdateFromMatchedRecords(stmt, from, client, options, cacheContext, tempTables);
7854
+ if (options.confirm) {
7855
+ const ok = await options.confirm(matched.length, "UPDATE");
7856
+ if (!ok) throw new OperationCancelledError("UPDATE", matched.length);
7857
+ }
7858
+ const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
7859
+ const batches = updateFromToPutBatches(stmt, matched, fieldTypes);
7860
+ for (const batch of batches) await client.putRecords(batch);
7861
+ return { type: "UPDATE", updatedCount: matched.length };
7862
+ }
7863
+ function collectUpdateFromTargetFields(stmt) {
7864
+ const fields = /* @__PURE__ */ new Set(["$id"]);
7865
+ if (stmt.from) fields.add(stmt.from.targetJoinField);
7866
+ const visit = (node) => {
7867
+ if (Array.isArray(node)) {
7868
+ node.forEach(visit);
7869
+ return;
7870
+ }
7871
+ if (node === null || typeof node !== "object") return;
7872
+ const obj = node;
7873
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") fields.add(obj["field"]);
7874
+ for (const value of Object.values(obj)) visit(value);
7875
+ };
7876
+ for (const assignment of stmt.assignments) {
7877
+ if (assignment.value.type !== "SOURCE_FIELD") visit(assignment.value);
7878
+ }
7879
+ return [...fields];
7880
+ }
6848
7881
  async function executeDelete(stmt, client, options, cacheContext) {
6849
7882
  if (stmt.subtableCode) {
6850
7883
  return executeDeleteSubtable(stmt, client, options, cacheContext);
@@ -7770,9 +8803,16 @@ function buildUpdatePlan(stmt, label) {
7770
8803
  const isSubq = stmt.assignments.some((a) => a.value.type === "SCALAR_SUBQUERY");
7771
8804
  const lines = [];
7772
8805
  if (label) lines.push(label);
7773
- lines.push(` [UPDATE]`);
8806
+ lines.push(stmt.from ? ` [UPDATE FROM]` : ` [UPDATE]`);
7774
8807
  lines.push(` target: APP${stmt.appId} (${stmt.appId})`);
7775
- lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
8808
+ if (stmt.from) {
8809
+ const source = stmt.from.cteName ?? `APP${stmt.from.appId}`;
8810
+ lines.push(` source: ${source} AS ${stmt.from.alias}`);
8811
+ lines.push(` join: APP${stmt.appId}.${stmt.from.targetJoinField} = ${stmt.from.alias}.${stmt.from.joinKeyField}`);
8812
+ lines.push(` target filter: ${stmt.from.targetFilter ? safeWhereToKintone(stmt.from.targetFilter) : "(none)"}`);
8813
+ } else {
8814
+ lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
8815
+ }
7776
8816
  lines.push(` api: GET /k/v1/records.json \u2192 PUT /k/v1/records.json`);
7777
8817
  const setTypes = [];
7778
8818
  if (isArith) setTypes.push("\u7B97\u8853 SET\uFF08\u73FE\u5728\u5024\u3092\u53D6\u5F97\u3057\u3066\u8A08\u7B97\uFF09");
@@ -7885,6 +8925,7 @@ function formatAssignment(a) {
7885
8925
  if (v.type === "ARITH") return `${a.field} = ${formatArithExprStr(v)}`;
7886
8926
  if (v.type === "CASE_VALUE") return `${a.field} = CASE WHEN ...`;
7887
8927
  if (v.type === "SCALAR_SUBQUERY") return `${a.field} = (SELECT ...)`;
8928
+ if (v.type === "SOURCE_FIELD") return `${a.field} = ${v.alias}.${v.field}`;
7888
8929
  return `${a.field} = (${v.type})`;
7889
8930
  }
7890
8931
  function formatArithExprStr(expr) {
@@ -8008,12 +9049,32 @@ function isSubtableRow(v) {
8008
9049
  // src/output/batchEnvelope.ts
8009
9050
  function toMutationSummary(result) {
8010
9051
  if (result.type === "INSERT") {
8011
- 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
+ };
8012
9060
  }
8013
- 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
+ };
8014
9068
  if (result.type === "DELETE") return { deletedCount: result.deletedCount };
8015
9069
  if (result.type === "UPSERT") {
8016
- 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
+ };
8017
9078
  }
8018
9079
  return { reorderedParentCount: result.reorderedParentCount };
8019
9080
  }
@@ -8040,11 +9101,31 @@ function buildBatchEnvelope(batch, options = {}) {
8040
9101
  }
8041
9102
  entry.resultIndex = results.length;
8042
9103
  results.push({
9104
+ type: "SELECT",
8043
9105
  columns: s.result.columns,
8044
9106
  rows: s.result.rows,
8045
9107
  rowCount: s.result.rowCount,
8046
9108
  warnings: s.result.warnings ?? []
8047
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
+ });
8048
9129
  } else if (s.status === "success" && s.result && s.result.type !== "SELECT" && s.result.type !== "ASSERT") {
8049
9130
  Object.assign(entry, toMutationSummary(s.result));
8050
9131
  }
@@ -8325,6 +9406,9 @@ function clampInt(v, min, max) {
8325
9406
 
8326
9407
  // src/core/formFieldInfo.ts
8327
9408
  function flattenFormFieldProperties(properties) {
9409
+ return flattenFields(properties, collectLookupCopyFields(properties));
9410
+ }
9411
+ function flattenFields(properties, lookupCopyFields, inSubtable = false) {
8328
9412
  const out = [];
8329
9413
  for (const field of Object.values(properties)) {
8330
9414
  out.push({
@@ -8332,12 +9416,49 @@ function flattenFormFieldProperties(properties) {
8332
9416
  label: field.label,
8333
9417
  fieldType: field.type,
8334
9418
  optionOrder: toOptionOrderMap(field.options),
8335
- 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)
8336
9428
  });
8337
- if (field.fields) out.push(...flattenFormFieldProperties(field.fields));
9429
+ if (field.fields) out.push(...flattenFields(field.fields, lookupCopyFields, true));
8338
9430
  }
8339
9431
  return out;
8340
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
+ }
8341
9462
  function toOptionOrderMap(options) {
8342
9463
  if (!options || typeof options !== "object") return void 0;
8343
9464
  const order = {};
@@ -9470,6 +10591,11 @@ function buildAssertOutput(result, format, pretty) {
9470
10591
  if (format === "jsonl") return JSON.stringify(payload);
9471
10592
  return `assertion ok: ${result.condition}`;
9472
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
+ }
9473
10599
  function buildMutationOutput(result, format, noHeader, pretty) {
9474
10600
  const row = { type: result.type };
9475
10601
  if (result.type === "INSERT") {
@@ -9484,6 +10610,12 @@ function buildMutationOutput(result, format, noHeader, pretty) {
9484
10610
  } else if (result.type === "REORDER") {
9485
10611
  row.reorderedParentCount = result.reorderedParentCount;
9486
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
+ }
9487
10619
  if (format === "json") return JSON.stringify(row, null, pretty ? 2 : 0);
9488
10620
  if (format === "jsonl") return JSON.stringify(row);
9489
10621
  const cols = Object.keys(row);
@@ -9594,6 +10726,14 @@ function buildBatchStatementSummary(s) {
9594
10726
  else if (r.type === "DELETE") parts.push(`deleted=${r.deletedCount}`);
9595
10727
  else if (r.type === "UPSERT") parts.push(`inserted=${r.insertedCount} updated=${r.updatedCount}`);
9596
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}`);
9597
10737
  }
9598
10738
  if (s.status === "error" && s.error) parts.push(s.error.message);
9599
10739
  if (s.status === "skipped" && s.skippedReason) parts.push(`reason=${s.skippedReason}`);
@@ -9627,6 +10767,8 @@ function buildBatchResultsOutput(batch, opts) {
9627
10767
  for (const s of batch.statements) {
9628
10768
  if (s.status === "success" && s.result?.type === "SELECT") {
9629
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));
9630
10772
  }
9631
10773
  }
9632
10774
  return outputs.join("\n\n");
@@ -10289,6 +11431,7 @@ async function run() {
10289
11431
  let isBatchSql = false;
10290
11432
  let batchContainsDml = false;
10291
11433
  let batchAnalysis = null;
11434
+ let needsCompleteInput = false;
10292
11435
  if (args.diagRecordId === null) {
10293
11436
  sql = args.executeSql;
10294
11437
  if (!sql && args.filePath) sql = (0, import_fs2.readFileSync)(args.filePath, "utf-8");
@@ -10319,14 +11462,16 @@ async function run() {
10319
11462
  batchAnalysis = analyzeBatch(statements);
10320
11463
  isBatchSql = true;
10321
11464
  batchContainsDml = batchAnalysis.containsDml;
11465
+ needsCompleteInput = batchAnalysis.requiresCompleteInput;
10322
11466
  } else {
10323
11467
  const stmt = parseSqlStatement(sql);
10324
11468
  parsedStmt = stmt;
10325
11469
  stmtType = getStatementType(stmt);
10326
- isDmlStatement = isDmlType(stmtType);
11470
+ isDmlStatement = writesKintone(stmt);
11471
+ needsCompleteInput = requiresCompleteInput(stmt);
10327
11472
  hasWhere = hasWhereClause(stmt);
10328
11473
  insertValuesCount = getInsertValuesCount(stmt);
10329
- 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);
10330
11475
  if (!supported) {
10331
11476
  process.stderr.write(`ArgumentError: unsupported statement type in CLI: ${stmtType}
10332
11477
  `);
@@ -10370,10 +11515,10 @@ async function run() {
10370
11515
  const yes = args.yes || envBool("KSQL_YES") === true || Boolean(profile.dml?.yes);
10371
11516
  const allowWithoutWhere = args.allowWithoutWhere || envBool("KSQL_ALLOW_WITHOUT_WHERE") === true || Boolean(profile.dml?.allowWithoutWhere);
10372
11517
  const dmlMaxRows = args.dmlMaxRows ?? envInt2("KSQL_DML_MAX_ROWS") ?? profile.dml?.maxRows ?? 100;
10373
- const dmlForcesOnLimitError = isDmlStatement || batchContainsDml;
11518
+ const dmlForcesOnLimitError = needsCompleteInput;
10374
11519
  const effectiveOnLimit = dmlForcesOnLimitError ? "error" : onLimit;
10375
11520
  if (dmlForcesOnLimitError && onLimit === "truncate" && !quiet && !args.dryRun) {
10376
- 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");
10377
11522
  }
10378
11523
  if (format === "markdown" && noHeader) {
10379
11524
  process.stderr.write("ArgumentError: --no-header cannot be used with --format markdown|md.\n");
@@ -10756,6 +11901,16 @@ query=${label}`);
10756
11901
  if (outputPath) (0, import_fs2.writeFileSync)(outputPath, `${output2}
10757
11902
  `, "utf-8");
10758
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}
10759
11914
  `);
10760
11915
  return 0;
10761
11916
  }
@@ -10809,6 +11964,7 @@ if (isDirectCliRun()) {
10809
11964
  buildBatchStatementSummary,
10810
11965
  buildOutput,
10811
11966
  buildReplExecArgv,
11967
+ buildValidationOutput,
10812
11968
  extractAppIds,
10813
11969
  normalizeAppKey,
10814
11970
  normalizeSqlAppProfiles,