@rex0220/kintone-sql-tools 3.4.0 → 3.5.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
@@ -744,13 +744,14 @@ var Parser = class {
744
744
  if (upper === "CREATE") return this.parseCreateTempTable();
745
745
  if (upper === "DROP") return this.parseDropTempTable();
746
746
  if (upper === "DECLARE") return this.parseDeclareVariable();
747
+ if (upper === "VALIDATE") return this.parseValidate();
747
748
  break;
748
749
  }
749
750
  default:
750
751
  break;
751
752
  }
752
753
  throw new ParseError(
753
- "SELECT / INSERT / UPDATE / DELETE / REORDER / WITH / SHOW / DESCRIBE / EXPLAIN / CREATE TEMP TABLE / DROP TEMP TABLE / SET / DECLARE / ASSERT \u306E\u3044\u305A\u308C\u304B\u3067\u59CB\u307E\u308B SQL \u6587\u304C\u5FC5\u8981\u3067\u3059",
754
+ "SELECT / INSERT / UPDATE / DELETE / REORDER / VALIDATE / WITH / SHOW / DESCRIBE / EXPLAIN / CREATE TEMP TABLE / DROP TEMP TABLE / SET / DECLARE / ASSERT \u306E\u3044\u305A\u308C\u304B\u3067\u59CB\u307E\u308B SQL \u6587\u304C\u5FC5\u8981\u3067\u3059",
754
755
  tok
755
756
  );
756
757
  }
@@ -761,7 +762,7 @@ var Parser = class {
761
762
  this.expect("SET" /* SET */);
762
763
  const variable = this.expect("VARIABLE" /* VARIABLE */, "SET \u306E\u5F8C\u306B\u306F\u5909\u6570\u540D\uFF08\u4F8B: @name\uFF09\u304C\u5FC5\u8981\u3067\u3059");
763
764
  this.expect("=" /* EQ */);
764
- const expr = this.parseScalarExpr("SET", true);
765
+ const expr = this.peek().kind === "[" /* LBRACKET */ ? this.parseArrayLiteral() : this.parseScalarExpr("SET", true);
765
766
  return { type: "SET_VARIABLE", name: variable.value.slice(1).toLowerCase(), expr };
766
767
  }
767
768
  parseDeclareVariable() {
@@ -922,11 +923,63 @@ var Parser = class {
922
923
  query = this.parseDelete();
923
924
  } else if (tok.kind === "REORDER" /* REORDER */) {
924
925
  query = this.parseReorder();
926
+ } else if (tok.kind === "IDENT" /* IDENT */ && tok.value.toUpperCase() === "VALIDATE") {
927
+ query = this.parseValidate();
925
928
  } else {
926
- throw new ParseError("EXPLAIN \u306E\u5F8C\u306B\u306F SELECT / WITH / INSERT / UPSERT / UPDATE / DELETE / REORDER \u304C\u5FC5\u8981\u3067\u3059", tok);
929
+ throw new ParseError("EXPLAIN \u306E\u5F8C\u306B\u306F SELECT / WITH / INSERT / UPSERT / UPDATE / DELETE / REORDER / VALIDATE \u304C\u5FC5\u8981\u3067\u3059", tok);
927
930
  }
928
931
  return { type: "EXPLAIN", query };
929
932
  }
933
+ /** VALIDATE APP100 [(fields)] [WHERE ...] [CHECK ...] [INTO #err]. */
934
+ parseValidate() {
935
+ const validateTok = this.advance();
936
+ const name = this.parseIdentifier();
937
+ const { appId, subtableCode } = extractTableRef(name, this.prev());
938
+ if (subtableCode) {
939
+ throw new ParseError("VALIDATE \u306F\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u4EEE\u60F3\u30C6\u30FC\u30D6\u30EB\u3092\u5BFE\u8C61\u306B\u3067\u304D\u307E\u305B\u3093", this.prev());
940
+ }
941
+ let fields;
942
+ if (this.consume("(" /* LPAREN */)) {
943
+ fields = this.parseIdentList();
944
+ this.expect(")" /* RPAREN */);
945
+ }
946
+ const where = this.consume("WHERE" /* WHERE */) ? this.parseWhereExpr() : null;
947
+ const checks = this.parseCheckGroups();
948
+ let errorTable;
949
+ if (this.consume("INTO" /* INTO */)) {
950
+ const tableTok = this.peek();
951
+ if (tableTok.kind !== "IDENT" /* IDENT */ || !tableTok.value.startsWith("#")) {
952
+ throw new ParseError("VALIDATE INTO \u306B\u306F # \u3067\u59CB\u307E\u308B\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u540D\u304C\u5FC5\u8981\u3067\u3059", tableTok);
953
+ }
954
+ errorTable = this.parseTableName();
955
+ }
956
+ const stmt = { type: "VALIDATE", appId, fields, where, ...checks, ...errorTable ? { errorTable } : {} };
957
+ this.assertValidateExpressions(stmt, validateTok);
958
+ return stmt;
959
+ }
960
+ /** v1 VALIDATE is single-app/local: subqueries and qualified references are rejected. */
961
+ assertValidateExpressions(stmt, tok) {
962
+ const visit = (node) => {
963
+ if (Array.isArray(node)) {
964
+ node.forEach(visit);
965
+ return;
966
+ }
967
+ if (node === null || typeof node !== "object") return;
968
+ const obj = node;
969
+ if (obj.type === "EXISTS" || obj.type === "SUBQUERY_IN_LIST" || obj.type === "SCALAR_SUBQUERY") {
970
+ throw new ParseError("VALIDATE \u306E WHERE / CHECK \u306B\u30B5\u30D6\u30AF\u30A8\u30EA\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
971
+ }
972
+ if (obj.type === "FIELD" && obj.tableAlias !== null && obj.tableAlias !== void 0) {
973
+ throw new ParseError("VALIDATE \u306E WHERE / CHECK \u3067\u306F\u4FEE\u98FE\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u3092\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
974
+ }
975
+ if (obj.type === "FIELD_REF" && typeof obj.field === "string" && obj.field.includes(".")) {
976
+ throw new ParseError("VALIDATE \u306E WHERE / CHECK \u3067\u306F\u4FEE\u98FE\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u3092\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
977
+ }
978
+ Object.values(obj).forEach(visit);
979
+ };
980
+ visit(stmt.where);
981
+ visit(stmt.checkGroups);
982
+ }
930
983
  // ----------------------------------------------------------
931
984
  // ASSERT
932
985
  //
@@ -1212,6 +1265,17 @@ var Parser = class {
1212
1265
  const alias2 = this.consume("AS" /* AS */) ? this.parseAliasName() : null;
1213
1266
  return { type: "SCALAR_VALUE_COL", expr, alias: alias2 };
1214
1267
  }
1268
+ if (this.peek().kind === "VARIABLE" /* VARIABLE */) {
1269
+ const variable = this.advance();
1270
+ if (!this.consume("AS" /* AS */)) {
1271
+ throw new ParseError("SELECT \u5217\u306E\u30D0\u30C3\u30C1\u5909\u6570\u306B\u306F AS alias \u304C\u5FC5\u8981\u3067\u3059", this.peek());
1272
+ }
1273
+ return {
1274
+ type: "VARIABLE_COL",
1275
+ name: variable.value.slice(1).toLowerCase(),
1276
+ alias: this.parseAliasName()
1277
+ };
1278
+ }
1215
1279
  const windowFunc = this.tryWindowFunc();
1216
1280
  if (windowFunc !== null) {
1217
1281
  return this.parseWindowColumn(windowFunc);
@@ -2027,9 +2091,7 @@ var Parser = class {
2027
2091
  }
2028
2092
  if (this.consume("NOT" /* NOT */)) {
2029
2093
  if (this.consume("IN" /* IN */)) {
2030
- this.expect("(" /* LPAREN */);
2031
- const right2 = this.parseInListOrSubquery();
2032
- this.expect(")" /* RPAREN */);
2094
+ const right2 = this.parseInRight();
2033
2095
  return { type: "BINARY", op: "NOT_IN", left: field, right: right2 };
2034
2096
  }
2035
2097
  if (this.consume("LIKE" /* LIKE */)) {
@@ -2046,9 +2108,7 @@ var Parser = class {
2046
2108
  );
2047
2109
  }
2048
2110
  if (this.consume("IN" /* IN */)) {
2049
- this.expect("(" /* LPAREN */);
2050
- const right2 = this.parseInListOrSubquery();
2051
- this.expect(")" /* RPAREN */);
2111
+ const right2 = this.parseInRight();
2052
2112
  return { type: "BINARY", op: "IN", left: field, right: right2 };
2053
2113
  }
2054
2114
  if (this.consume("KLIKE" /* KLIKE */)) {
@@ -2215,6 +2275,18 @@ var Parser = class {
2215
2275
  );
2216
2276
  }
2217
2277
  // IN (...) — 値リストまたはサブクエリ
2278
+ parseInRight() {
2279
+ if (this.consume("(" /* LPAREN */)) {
2280
+ const right = this.parseInListOrSubquery();
2281
+ this.expect(")" /* RPAREN */);
2282
+ return right;
2283
+ }
2284
+ const variable = this.expect(
2285
+ "VARIABLE" /* VARIABLE */,
2286
+ "IN / NOT IN \u306E\u5F8C\u306B\u306F (\u5024\u30EA\u30B9\u30C8\u307E\u305F\u306F SELECT) \u304B\u914D\u5217\u5909\u6570\u304C\u5FC5\u8981\u3067\u3059"
2287
+ );
2288
+ return { type: "VARIABLE_IN_LIST", name: variable.value.slice(1).toLowerCase() };
2289
+ }
2218
2290
  parseInListOrSubquery() {
2219
2291
  if (this.peek().kind === "SELECT" /* SELECT */) {
2220
2292
  const query = this.parseSelect();
@@ -3025,7 +3097,7 @@ function isDmlType(type) {
3025
3097
  return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
3026
3098
  }
3027
3099
  function isReadOnlyType(type) {
3028
- 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";
3100
+ return type === "SELECT" || type === "VALIDATE" || 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";
3029
3101
  }
3030
3102
  function writesKintone(stmt) {
3031
3103
  return isDmlType(stmt.type) && !("validateOnly" in stmt && stmt.validateOnly === true);
@@ -3036,6 +3108,8 @@ function isReadOnlyStatement(stmt) {
3036
3108
  function requiresCompleteInput(stmt) {
3037
3109
  if (isDmlType(stmt.type)) return true;
3038
3110
  switch (stmt.type) {
3111
+ case "VALIDATE":
3112
+ return true;
3039
3113
  case "SELECT":
3040
3114
  return selectRequiresCompleteInput(stmt);
3041
3115
  case "UNION":
@@ -3076,6 +3150,7 @@ function whereRequiresCompleteInput(where) {
3076
3150
  case "EXISTS":
3077
3151
  return selectRequiresCompleteInput(where.query);
3078
3152
  case "NULL_CHECK":
3153
+ case "BOOLEAN":
3079
3154
  return false;
3080
3155
  }
3081
3156
  }
@@ -3111,6 +3186,8 @@ function collectDmlTargetFields(stmt) {
3111
3186
  // src/engine/pushDownNot.ts
3112
3187
  function pushDownNot(expr) {
3113
3188
  switch (expr.type) {
3189
+ case "BOOLEAN":
3190
+ return { type: "BOOLEAN", value: !expr.value };
3114
3191
  case "BINARY": {
3115
3192
  const negated = negateOp(expr.op);
3116
3193
  if (negated === null) {
@@ -3190,6 +3267,7 @@ function whereHasLike(where) {
3190
3267
  case "BINARY":
3191
3268
  case "NULL_CHECK":
3192
3269
  case "EXISTS":
3270
+ case "BOOLEAN":
3193
3271
  return false;
3194
3272
  }
3195
3273
  }
@@ -3205,6 +3283,7 @@ function whereHasKlike(where) {
3205
3283
  case "BINARY":
3206
3284
  case "NULL_CHECK":
3207
3285
  case "EXISTS":
3286
+ case "BOOLEAN":
3208
3287
  return false;
3209
3288
  }
3210
3289
  }
@@ -3224,6 +3303,8 @@ function whereToKintone(expr) {
3224
3303
  return convertGroup(expr);
3225
3304
  case "EXISTS":
3226
3305
  throw new KintoneQueryError("EXISTS \u306F kintone \u30AF\u30A8\u30EA\u306B\u5909\u63DB\u3067\u304D\u307E\u305B\u3093");
3306
+ case "BOOLEAN":
3307
+ throw new KintoneQueryError("internal error: BOOLEAN predicate reached kintone query conversion");
3227
3308
  }
3228
3309
  }
3229
3310
  function convertBinary(expr) {
@@ -3302,6 +3383,8 @@ function convertValue(value, op) {
3302
3383
  switch (value.type) {
3303
3384
  case "VARIABLE":
3304
3385
  throw new KintoneQueryError(`\u672A\u89E3\u6C7A\u306E\u30D0\u30C3\u30C1\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
3386
+ case "VARIABLE_IN_LIST":
3387
+ throw new KintoneQueryError(`\u672A\u89E3\u6C7A\u306E\u914D\u5217\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
3305
3388
  case "STRING":
3306
3389
  return convertString(value);
3307
3390
  case "NUMBER":
@@ -3381,6 +3464,8 @@ function resolveSelectMode(stmt) {
3381
3464
  function whereRequiresJsEval(where) {
3382
3465
  if (where === null) return false;
3383
3466
  switch (where.type) {
3467
+ case "BOOLEAN":
3468
+ return true;
3384
3469
  case "BINARY":
3385
3470
  return isFunc(where.left) || where.right.type === "ARITH_VALUE" || where.right.type === "CASE_VALUE" || where.right.type === "SUBQUERY_IN_LIST" || where.right.type === "SCALAR_SUBQUERY" || isLike(where);
3386
3471
  case "NULL_CHECK":
@@ -3775,6 +3860,7 @@ function collectRequiredFieldsByTable(stmt) {
3775
3860
  walkWhere(where.expr, phase);
3776
3861
  return;
3777
3862
  case "EXISTS":
3863
+ case "BOOLEAN":
3778
3864
  return;
3779
3865
  }
3780
3866
  };
@@ -3813,6 +3899,8 @@ function collectRequiredFieldsByTable(stmt) {
3813
3899
  break;
3814
3900
  case "LITERAL_COL":
3815
3901
  break;
3902
+ case "VARIABLE_COL":
3903
+ throw new Error(`internal error: unresolved SELECT variable @${col.name}`);
3816
3904
  case "AGGREGATE":
3817
3905
  if (col.arg.type !== "WILDCARD") walkArith(col.arg, "select");
3818
3906
  break;
@@ -3988,6 +4076,7 @@ function stripCteAlias(where, alias) {
3988
4076
  case "GROUP":
3989
4077
  return { ...where, expr: stripCteAlias(where.expr, alias) };
3990
4078
  case "EXISTS":
4079
+ case "BOOLEAN":
3991
4080
  return where;
3992
4081
  }
3993
4082
  }
@@ -4025,6 +4114,7 @@ function extractAndLeaves(where, accept) {
4025
4114
  case "NULL_CHECK":
4026
4115
  case "NOT":
4027
4116
  case "EXISTS":
4117
+ case "BOOLEAN":
4028
4118
  return null;
4029
4119
  }
4030
4120
  }
@@ -4163,6 +4253,7 @@ function collectKlikes(where, out) {
4163
4253
  case "BINARY":
4164
4254
  case "NULL_CHECK":
4165
4255
  case "EXISTS":
4256
+ case "BOOLEAN":
4166
4257
  return;
4167
4258
  }
4168
4259
  }
@@ -4219,6 +4310,11 @@ function validateStatement(stmt) {
4219
4310
  );
4220
4311
  }
4221
4312
  return;
4313
+ case "VALIDATE":
4314
+ if (containsKlike(stmt)) {
4315
+ throw new KlikeValidationError("KLIKE / NOT KLIKE \u306F VALIDATE \u306E WHERE / CHECK \u3067\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093");
4316
+ }
4317
+ return;
4222
4318
  case "SHOW_APPS":
4223
4319
  case "DESCRIBE":
4224
4320
  case "DROP_TEMP_TABLE":
@@ -4306,6 +4402,8 @@ function isDescendantOf(root, target) {
4306
4402
  case "NULL_CHECK":
4307
4403
  case "EXISTS":
4308
4404
  return false;
4405
+ case "BOOLEAN":
4406
+ return false;
4309
4407
  }
4310
4408
  }
4311
4409
  function walkWithoutNestedSelects(node, visitWhere) {
@@ -4367,8 +4465,12 @@ function collectVariableRefs(node, refs) {
4367
4465
  }
4368
4466
  if (node !== null && typeof node === "object") {
4369
4467
  const obj = node;
4370
- if (obj["type"] === "VARIABLE" && typeof obj["name"] === "string") {
4371
- refs.add(obj["name"]);
4468
+ const type = obj["type"];
4469
+ if ((type === "VARIABLE" || type === "VARIABLE_COL" || type === "VARIABLE_IN_LIST") && typeof obj["name"] === "string") {
4470
+ refs.push({
4471
+ name: obj["name"],
4472
+ kind: type === "VARIABLE" ? "scalar" : type === "VARIABLE_COL" ? "select-column" : "array-in-list"
4473
+ });
4372
4474
  return;
4373
4475
  }
4374
4476
  for (const v of Object.values(obj)) collectVariableRefs(v, refs);
@@ -4409,9 +4511,9 @@ function analyzeBatch(statements) {
4409
4511
  const variableDefs = /* @__PURE__ */ new Map();
4410
4512
  const variableOrder = [];
4411
4513
  statements.forEach((stmt, index) => {
4412
- const validationTable = "validationErrorTable" in stmt && stmt.validationErrorTable ? stmt.validationErrorTable : "onErrorSkip" in stmt && stmt.onErrorSkip ? stmt.errorTable ?? null : null;
4514
+ const validationTable = stmt.type === "VALIDATE" && stmt.errorTable ? stmt.errorTable : "validationErrorTable" in stmt && stmt.validationErrorTable ? stmt.validationErrorTable : "onErrorSkip" in stmt && stmt.onErrorSkip ? stmt.errorTable ?? null : null;
4413
4515
  if (statements.length === 1 && validationTable) {
4414
- const message = "onErrorSkip" in stmt && stmt.onErrorSkip ? "ArgumentError: ON ERROR SKIP requires a batch." : "ArgumentError: VALIDATE ONLY INTO requires a batch.";
4516
+ const message = stmt.type === "VALIDATE" ? "ArgumentError: VALIDATE INTO requires a batch." : "onErrorSkip" in stmt && stmt.onErrorSkip ? "ArgumentError: ON ERROR SKIP requires a batch." : "ArgumentError: VALIDATE ONLY INTO requires a batch.";
4415
4517
  throw new BatchAnalysisError(message, index);
4416
4518
  }
4417
4519
  const statementType = getStatementType(stmt);
@@ -4420,23 +4522,43 @@ function analyzeBatch(statements) {
4420
4522
  const refs = /* @__PURE__ */ new Set();
4421
4523
  const stmtAppIds = /* @__PURE__ */ new Set();
4422
4524
  const dependsOn = /* @__PURE__ */ new Set();
4423
- const variableRefs = /* @__PURE__ */ new Set();
4525
+ const variableRefs = [];
4424
4526
  collectVariableRefs(stmt, variableRefs);
4425
- for (const name of variableRefs) {
4426
- const def = variableDefs.get(name);
4527
+ const referencedThisStatement = /* @__PURE__ */ new Set();
4528
+ for (const use of variableRefs) {
4529
+ const def = variableDefs.get(use.name);
4427
4530
  if (def === void 0) {
4428
4531
  throw new BatchAnalysisError(
4429
- `ParseError: variable @${name} is not defined before statement ${index + 1}.`,
4532
+ `ParseError: variable @${use.name} is not defined before statement ${index + 1}.`,
4533
+ index
4534
+ );
4535
+ }
4536
+ if (def.kind === "scalar" && use.kind === "array-in-list") {
4537
+ throw new BatchAnalysisError(
4538
+ `ParseError: scalar variable @${use.name} cannot be used as IN @${use.name}; use IN (@${use.name}) instead.`,
4430
4539
  index
4431
4540
  );
4432
4541
  }
4433
- def.referencedBy.push(index);
4542
+ if (def.kind === "array" && use.kind !== "array-in-list") {
4543
+ throw new BatchAnalysisError(
4544
+ `ParseError: array variable @${use.name} can only be used as IN @${use.name}.`,
4545
+ index
4546
+ );
4547
+ }
4548
+ if (!referencedThisStatement.has(use.name)) {
4549
+ def.referencedBy.push(index);
4550
+ referencedThisStatement.add(use.name);
4551
+ }
4434
4552
  }
4435
4553
  if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
4436
4554
  if (variableDefs.has(stmt.name)) {
4437
4555
  throw new BatchAnalysisError(`ParseError: variable @${stmt.name} is already defined.`, index);
4438
4556
  }
4439
- variableDefs.set(stmt.name, { index, referencedBy: [] });
4557
+ variableDefs.set(stmt.name, {
4558
+ index,
4559
+ kind: stmt.type === "SET_VARIABLE" && stmt.expr.type === "ARRAY" ? "array" : "scalar",
4560
+ referencedBy: []
4561
+ });
4440
4562
  variableOrder.push(stmt.name);
4441
4563
  if (variableOrder.length > MAX_BATCH_VARIABLES) {
4442
4564
  throw new BatchAnalysisError(
@@ -4469,7 +4591,7 @@ function analyzeBatch(statements) {
4469
4591
  dependsOn.add(at);
4470
4592
  }
4471
4593
  if (validationTable) {
4472
- const payloadFields = stmt.type === "UPDATE" ? ["$id", ...stmt.assignments.map((a) => a.field)] : "fields" in stmt ? stmt.fields : [];
4594
+ const payloadFields = stmt.type === "VALIDATE" ? ["$id", "$err_field", "$err_code", "$err_message", "$err_value"] : stmt.type === "UPDATE" ? ["$id", ...stmt.assignments.map((a) => a.field)] : "fields" in stmt ? stmt.fields : [];
4473
4595
  const signature = JSON.stringify(payloadFields);
4474
4596
  const at = defined.get(validationTable);
4475
4597
  if (at === void 0) {
@@ -4544,6 +4666,7 @@ function analyzeBatch(statements) {
4544
4666
  const needsCompleteInput = results.some((r) => r.requiresCompleteInput);
4545
4667
  const variables = variableOrder.map((name) => ({
4546
4668
  name,
4669
+ kind: variableDefs.get(name).kind,
4547
4670
  referencedBy: [...variableDefs.get(name).referencedBy]
4548
4671
  }));
4549
4672
  return {
@@ -4820,6 +4943,7 @@ function whereNeedsFieldMetadata(where) {
4820
4943
  case "GROUP":
4821
4944
  return whereNeedsFieldMetadata(where.expr);
4822
4945
  case "EXISTS":
4946
+ case "BOOLEAN":
4823
4947
  return false;
4824
4948
  }
4825
4949
  }
@@ -4844,6 +4968,7 @@ function explainNeedsAppMetadata(statement) {
4844
4968
  seen.add(node);
4845
4969
  if (Array.isArray(node)) return node.some(visit);
4846
4970
  const item = node;
4971
+ if (item["type"] === "VALIDATE") return true;
4847
4972
  if (item["type"] === "SELECT" && selectNeedsOwnMetadata(node)) {
4848
4973
  return true;
4849
4974
  }
@@ -5334,6 +5459,8 @@ function resolveFieldRef(row, field) {
5334
5459
  // src/engine/evalWhere.ts
5335
5460
  function evalWhere(expr, row, resolveFieldType, appliedKlikes, resolveFieldSemantics2) {
5336
5461
  switch (expr.type) {
5462
+ case "BOOLEAN":
5463
+ return expr.value;
5337
5464
  case "BINARY":
5338
5465
  return evalBinary(expr, row, resolveFieldType, appliedKlikes, resolveFieldSemantics2);
5339
5466
  case "NULL_CHECK":
@@ -5504,6 +5631,8 @@ function resolveValue(value, row, resolveFieldType, resolveFieldSemantics2) {
5504
5631
  switch (value.type) {
5505
5632
  case "VARIABLE":
5506
5633
  throw new Error(`ParseError: unresolved batch variable @${value.name}.`);
5634
+ case "VARIABLE_IN_LIST":
5635
+ throw new Error(`ParseError: unresolved batch array variable @${value.name}.`);
5507
5636
  case "STRING":
5508
5637
  return value.value;
5509
5638
  case "NUMBER":
@@ -5819,6 +5948,9 @@ function collectConditionFields(expr, out) {
5819
5948
  case "GROUP":
5820
5949
  collectConditionFields(expr.expr, out);
5821
5950
  break;
5951
+ case "EXISTS":
5952
+ case "BOOLEAN":
5953
+ break;
5822
5954
  }
5823
5955
  }
5824
5956
  function updateToPutBatchesArith(stmt, records, fieldTypes = /* @__PURE__ */ new Map()) {
@@ -6034,6 +6166,8 @@ function convertDmlSqlValue(value, fieldType) {
6034
6166
  switch (value.type) {
6035
6167
  case "VARIABLE":
6036
6168
  throw new DmlConvertError(`\u672A\u89E3\u6C7A\u306E\u30D0\u30C3\u30C1\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
6169
+ case "VARIABLE_IN_LIST":
6170
+ throw new DmlConvertError(`\u672A\u89E3\u6C7A\u306E\u914D\u5217\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
6037
6171
  case "STRING":
6038
6172
  return convertString2(value.value, fieldType);
6039
6173
  case "NUMBER":
@@ -6869,6 +7003,8 @@ function project(rows, columns, scalarCache, resolveFieldType, sourceColumns, re
6869
7003
  const out = {};
6870
7004
  for (const [colIdx, col] of columns.entries()) {
6871
7005
  switch (col.type) {
7006
+ case "VARIABLE_COL":
7007
+ throw new Error(`internal error: unresolved SELECT variable @${col.name}`);
6872
7008
  case "WILDCARD":
6873
7009
  Object.assign(out, stripParentShortcutColumns(row));
6874
7010
  break;
@@ -6972,6 +7108,8 @@ function computeExplicitOutputKeys(columns, defaultFieldKeys) {
6972
7108
  }
6973
7109
  function computeOutputKey(col, colIdx, defaultFieldKeys) {
6974
7110
  switch (col.type) {
7111
+ case "VARIABLE_COL":
7112
+ throw new Error(`internal error: unresolved SELECT variable @${col.name}`);
6975
7113
  case "FIELD":
6976
7114
  return col.alias ?? defaultFieldKeys.get(colIdx) ?? col.field;
6977
7115
  case "LITERAL_COL":
@@ -7486,6 +7624,11 @@ function renderValidationValue(value) {
7486
7624
  return String(value);
7487
7625
  }
7488
7626
 
7627
+ // src/core/existingRecordValidation.ts
7628
+ function renderExistingValidationValue(raw, fieldType) {
7629
+ return isEmptyDmlValue(raw) ? "" : renderValidationValue(normalizeRaw(raw, fieldType));
7630
+ }
7631
+
7489
7632
  // src/core/optimization/whereCapability.ts
7490
7633
  var RANGE_AND_EQUALITY = ["=", "!=", ">", "<", ">=", "<="];
7491
7634
  var EQUALITY_IN = ["=", "!=", "in", "not in"];
@@ -7560,6 +7703,8 @@ function classifyWhereCapability(where, resolveField2) {
7560
7703
  }
7561
7704
  function classifyNode(where, resolveField2) {
7562
7705
  switch (where.type) {
7706
+ case "BOOLEAN":
7707
+ return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
7563
7708
  case "BINARY":
7564
7709
  return classifyBinary(where.op, where.left, where.right.type, resolveField2);
7565
7710
  case "NULL_CHECK":
@@ -7886,6 +8031,8 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
7886
8031
  throw new Error("ArgumentError: ON ERROR SKIP requires a batch.");
7887
8032
  }
7888
8033
  switch (stmt.type) {
8034
+ case "VALIDATE":
8035
+ return executeExistingRecordValidation(stmt, client, options, cacheContext);
7889
8036
  case "SELECT":
7890
8037
  return executeSelect(stmt, client, options, cacheContext);
7891
8038
  case "UNION":
@@ -7931,6 +8078,144 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
7931
8078
  return executeAssert(stmt, client, options, cacheContext);
7932
8079
  }
7933
8080
  }
8081
+ var EXISTING_VALIDATION_COLUMNS = ["$id", "$err_field", "$err_code", "$err_message", "$err_value"];
8082
+ function hasAuditableConstraint(field) {
8083
+ return field.required === true || field.minValue !== void 0 || field.maxValue !== void 0 || field.minLength !== void 0 || field.maxLength !== void 0 || field.optionOrder !== void 0;
8084
+ }
8085
+ function resolveExistingValidationTargets(stmt, fieldInfos) {
8086
+ const byCode = new Map(fieldInfos.map((field) => [field.code, field]));
8087
+ const auditable = (field) => !field.inSubtable && (field.fieldType === "NUMBER" || hasAuditableConstraint(field));
8088
+ if (stmt.fields === void 0) return fieldInfos.filter(auditable);
8089
+ const seen = /* @__PURE__ */ new Set();
8090
+ return stmt.fields.map((code) => {
8091
+ if (seen.has(code)) throw new Error(`ArgumentError: VALIDATE field ${code} is duplicated.`);
8092
+ seen.add(code);
8093
+ if (code === "$id") throw new Error("ArgumentError: VALIDATE cannot audit system field $id.");
8094
+ const info = byCode.get(code);
8095
+ if (!info) throw new Error(`ArgumentError: VALIDATE field ${code} does not exist.`);
8096
+ if (info.inSubtable) throw new Error(`ArgumentError: VALIDATE field ${code} is a subtable child field.`);
8097
+ if (!auditable(info)) throw new Error(`ArgumentError: VALIDATE field ${code} has no auditable constraint.`);
8098
+ return info;
8099
+ });
8100
+ }
8101
+ function collectValidateWhereFields(where) {
8102
+ const fields = [];
8103
+ const seen = /* @__PURE__ */ new Set();
8104
+ const add = (field) => {
8105
+ if (!seen.has(field)) {
8106
+ seen.add(field);
8107
+ fields.push(field);
8108
+ }
8109
+ };
8110
+ const visit = (node) => {
8111
+ if (Array.isArray(node)) {
8112
+ node.forEach(visit);
8113
+ return;
8114
+ }
8115
+ if (node === null || typeof node !== "object") return;
8116
+ const obj = node;
8117
+ if (obj.type === "FIELD" && typeof obj.field === "string") add(obj.field);
8118
+ if (obj.type === "FIELD_REF" && typeof obj.field === "string") add(obj.field);
8119
+ Object.values(obj).forEach(visit);
8120
+ };
8121
+ visit(where);
8122
+ return fields;
8123
+ }
8124
+ function existingValidationColumnMeta() {
8125
+ return new Map(EXISTING_VALIDATION_COLUMNS.map((column) => [column, {
8126
+ fieldType: column === "$id" ? "KSQL_NUMBER" : "KSQL_STRING",
8127
+ sortKind: column === "$id" ? "number" : "string",
8128
+ semantics: syntheticSemantics(column === "$id" ? "number" : "string")
8129
+ }]));
8130
+ }
8131
+ async function executeExistingRecordValidation(stmt, client, options, cacheContext) {
8132
+ if (stmt.errorTable) throw new Error("ArgumentError: VALIDATE INTO requires a batch.");
8133
+ return executeExistingRecordValidationCore(stmt, client, options, cacheContext);
8134
+ }
8135
+ async function executeExistingRecordValidationCore(stmt, client, options, cacheContext) {
8136
+ const fieldInfos = await getFieldsCached(stmt.appId, client, cacheContext);
8137
+ const infoByCode = new Map(fieldInfos.map((field) => [field.code, field]));
8138
+ const targets = resolveExistingValidationTargets(stmt, fieldInfos);
8139
+ const checkGroups = stmt.checkGroups ?? [];
8140
+ const checkRefs2 = collectCheckFieldRefs(checkGroups);
8141
+ for (const ref of checkRefs2) {
8142
+ if (ref.field !== "$id" && !infoByCode.has(ref.field)) {
8143
+ throw customCheckParseError(`CHECK \u306E\u30D5\u30A3\u30FC\u30EB\u30C9 ${ref.field} \u306F APP${stmt.appId} \u306B\u5B58\u5728\u3057\u307E\u305B\u3093`);
8144
+ }
8145
+ }
8146
+ const evaluationTypes = new Map(fieldInfos.map((field) => [field.code, field.fieldType]));
8147
+ evaluationTypes.set("$id", "RECORD_NUMBER");
8148
+ assertCheckComparisonTypes(stmt, evaluationTypes);
8149
+ const whereFields = collectValidateWhereFields(stmt.where);
8150
+ const requiredFields = [.../* @__PURE__ */ new Set([
8151
+ "$id",
8152
+ ...targets.map((field) => field.code),
8153
+ ...whereFields,
8154
+ ...checkRefs2.map((ref) => ref.field)
8155
+ ])];
8156
+ for (const field of whereFields) {
8157
+ if (field !== "$id" && !infoByCode.has(field)) {
8158
+ throw new Error(`ArgumentError: WHERE field ${field} does not exist in APP${stmt.appId}.`);
8159
+ }
8160
+ }
8161
+ const numberPrecision = targets.some((field) => field.fieldType === "NUMBER") ? await getNumberPrecisionCached(stmt.appId, client, cacheContext) : void 0;
8162
+ const semantics = (field) => field.field === "$id" ? resolveFieldSemantics({ fieldType: "__ID__" }) : infoByCode.get(field.field)?.semantics ?? (infoByCode.has(field.field) ? resolveFieldSemantics(infoByCode.get(field.field)) : void 0);
8163
+ const capability = classifyWhereCapability(stmt.where, semantics);
8164
+ if (capability.capability === "UNSUPPORTED") {
8165
+ throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
8166
+ }
8167
+ const fieldTypes = new Map(fieldInfos.map((field) => [field.code, field.fieldType]));
8168
+ const fieldOptions = new Map(fieldInfos.flatMap((field) => field.optionOrder ? [[field.code, new Set(Object.keys(field.optionOrder))]] : []));
8169
+ const prefilter = stmt.where === null ? null : capability.capability === "EXACT_PUSHDOWN" ? stmt.where : extractSafePushdownLeaves(stmt.where, {
8170
+ allowUnqualifiedFields: true,
8171
+ fieldTypes,
8172
+ fieldOptions,
8173
+ allowKlike: false
8174
+ });
8175
+ const query = prefilter === null ? "" : whereToKintone(prefilter);
8176
+ const records = await fetchAll(client.getRecords, stmt.appId, query, requiredFields, {
8177
+ maxRecords: options.maxRecords ?? 1e4,
8178
+ parallel: options.fetchParallel ?? 1,
8179
+ onLimit: "error"
8180
+ });
8181
+ const validationRows = records.map((record) => ({
8182
+ id: String(record["$id"]?.value ?? ""),
8183
+ record,
8184
+ flat: flatten(record, null)
8185
+ })).filter((row) => stmt.where === null || evalWhere(stmt.where, row.flat, (field) => evaluationTypes.get(field.field)));
8186
+ const rows = [];
8187
+ for (const row of validationRows) {
8188
+ for (const field of targets) {
8189
+ const raw = row.record[field.code]?.value;
8190
+ const validation = validateAndNormalizeDmlValue(raw, field, numberPrecision);
8191
+ if (validation.ok) continue;
8192
+ rows.push({
8193
+ "$id": row.id,
8194
+ "$err_field": field.code,
8195
+ "$err_code": validation.code,
8196
+ "$err_message": validation.message,
8197
+ "$err_value": renderExistingValidationValue(raw, field.fieldType)
8198
+ });
8199
+ }
8200
+ for (const check of evaluateCustomChecks(checkGroups, row.flat, (field) => evaluationTypes.get(field.field))) {
8201
+ rows.push({
8202
+ "$id": row.id,
8203
+ "$err_field": "",
8204
+ "$err_code": "ERR_CHECK",
8205
+ "$err_message": check.message,
8206
+ "$err_value": ""
8207
+ });
8208
+ }
8209
+ }
8210
+ const result = {
8211
+ type: "SELECT",
8212
+ columns: [...EXISTING_VALIDATION_COLUMNS],
8213
+ rows,
8214
+ rowCount: rows.length
8215
+ };
8216
+ materializedMetaBySelectResult.set(result, existingValidationColumnMeta());
8217
+ return result;
8218
+ }
7934
8219
  var TEMP_TABLE_MAX_ROWS = 1e4;
7935
8220
  function appendValidationErrors(tempTables, name, columns, rows, maxRows, columnMeta) {
7936
8221
  const current = tempTables.get(name);
@@ -8064,9 +8349,14 @@ async function executeBatch(sql, client, options = {}) {
8064
8349
  }
8065
8350
  async function executeBatchStatement(stmt, info, client, options, cacheContext, tempTables, variables) {
8066
8351
  if (stmt.type === "SET_VARIABLE") {
8067
- const resolvedStmt2 = resolveVariableRefs(stmt, variables);
8352
+ const resolvedStmt2 = resolveBatchVariableReferences(stmt, variables);
8068
8353
  validateKlikeStatement(resolvedStmt2);
8069
- if (resolvedStmt2.expr.type === "SCALAR_SUBQUERY") {
8354
+ if (resolvedStmt2.expr.type === "ARRAY") {
8355
+ variables.set(stmt.name, {
8356
+ type: "array",
8357
+ elements: resolvedStmt2.expr.elements.map((element) => ({ type: "string", value: element.value }))
8358
+ });
8359
+ } else if (resolvedStmt2.expr.type === "SCALAR_SUBQUERY") {
8070
8360
  try {
8071
8361
  const value = await evaluateScalarSubquery(
8072
8362
  resolvedStmt2.expr.query,
@@ -8103,8 +8393,27 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
8103
8393
  }
8104
8394
  return {};
8105
8395
  }
8106
- const resolvedStmt = resolveVariableRefs(stmt, variables);
8396
+ const resolvedStmt = resolveBatchVariableReferences(stmt, variables);
8107
8397
  validateKlikeStatement(resolvedStmt);
8398
+ if (resolvedStmt.type === "VALIDATE") {
8399
+ const result = await executeExistingRecordValidationCore(
8400
+ resolvedStmt,
8401
+ client,
8402
+ { ...options, onLimitReached: "error" },
8403
+ cacheContext
8404
+ );
8405
+ if (resolvedStmt.errorTable) {
8406
+ appendValidationErrors(
8407
+ tempTables,
8408
+ resolvedStmt.errorTable,
8409
+ result.columns,
8410
+ result.rows,
8411
+ options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
8412
+ materializedMetaBySelectResult.get(result) ?? existingValidationColumnMeta()
8413
+ );
8414
+ }
8415
+ return { result };
8416
+ }
8108
8417
  if ("validateOnly" in resolvedStmt && resolvedStmt.validateOnly === true) {
8109
8418
  const result = await executeDmlValidation(
8110
8419
  resolvedStmt,
@@ -8289,9 +8598,9 @@ function evaluateScalarExpr(expr) {
8289
8598
  }
8290
8599
  }
8291
8600
  }
8292
- function resolveVariableRefs(node, variables) {
8601
+ function resolveBatchVariableReferences(node, variables) {
8293
8602
  if (Array.isArray(node)) {
8294
- return node.map((v) => resolveVariableRefs(v, variables));
8603
+ return node.map((v) => resolveBatchVariableReferences(v, variables));
8295
8604
  }
8296
8605
  if (node !== null && typeof node === "object") {
8297
8606
  const obj = node;
@@ -8300,14 +8609,72 @@ function resolveVariableRefs(node, variables) {
8300
8609
  if (value === void 0) {
8301
8610
  throw new Error(`ParseError: variable @${obj["name"]} is not defined in this batch.`);
8302
8611
  }
8612
+ if (value.type === "array") {
8613
+ throw new Error(`ParseError: array variable @${obj["name"]} can only be used as IN @${obj["name"]}.`);
8614
+ }
8303
8615
  return value.type === "number" ? { type: "NUMBER", value: value.value, raw: value.raw ?? String(value.value) } : { type: "STRING", value: value.value };
8304
8616
  }
8305
- return Object.fromEntries(
8306
- Object.entries(obj).map(([key, value]) => [key, resolveVariableRefs(value, variables)])
8617
+ if (obj["type"] === "VARIABLE_COL" && typeof obj["name"] === "string" && typeof obj["alias"] === "string") {
8618
+ const value = variables.get(obj["name"]);
8619
+ if (value === void 0) throw new Error(`ParseError: variable @${obj["name"]} is not defined in this batch.`);
8620
+ if (value.type === "array") throw new Error(`ParseError: array variable @${obj["name"]} cannot be used as a SELECT column.`);
8621
+ return value.type === "number" ? { type: "ARITH_COL", expr: { type: "NUMBER", value: value.value, raw: value.raw ?? String(value.value) }, alias: obj["alias"] } : { type: "LITERAL_COL", value: value.value, alias: obj["alias"] };
8622
+ }
8623
+ if (obj["type"] === "VARIABLE_IN_LIST") return obj;
8624
+ const resolved = Object.fromEntries(
8625
+ Object.entries(obj).map(([key, value]) => [key, resolveBatchVariableReferences(value, variables)])
8307
8626
  );
8627
+ if (resolved["type"] === "BINARY") {
8628
+ const right = resolved["right"];
8629
+ if (right?.["type"] === "VARIABLE_IN_LIST" && typeof right["name"] === "string") {
8630
+ const value = variables.get(right["name"]);
8631
+ if (value === void 0) throw new Error(`ParseError: variable @${right["name"]} is not defined in this batch.`);
8632
+ if (value.type !== "array") {
8633
+ throw new Error(`ParseError: scalar variable @${right["name"]} cannot be used as IN @${right["name"]}; use IN (@${right["name"]}) instead.`);
8634
+ }
8635
+ if (value.elements.length === 0) {
8636
+ return { type: "BOOLEAN", value: resolved["op"] === "NOT_IN" };
8637
+ }
8638
+ resolved["right"] = {
8639
+ type: "IN_LIST",
8640
+ values: value.elements.map((element) => ({ type: "STRING", value: element.value }))
8641
+ };
8642
+ }
8643
+ }
8644
+ const simplified = simplifyBooleanWhere(resolved);
8645
+ if (simplified["type"] === "SELECT" && isBooleanNode(simplified["where"], true)) {
8646
+ simplified["where"] = null;
8647
+ }
8648
+ if ((simplified["type"] === "UPDATE" || simplified["type"] === "DELETE" || simplified["type"] === "REORDER") && isBooleanNode(simplified["where"], true)) {
8649
+ throw new Error("ArgumentError: empty-array simplification makes the target WHERE always true; use an explicit safe target condition.");
8650
+ }
8651
+ return simplified;
8308
8652
  }
8309
8653
  return node;
8310
8654
  }
8655
+ function isBooleanNode(value, expected) {
8656
+ return value !== null && typeof value === "object" && value.type === "BOOLEAN" && (expected === void 0 || value.value === expected);
8657
+ }
8658
+ function simplifyBooleanWhere(obj) {
8659
+ if (obj["type"] === "NOT" && isBooleanNode(obj["expr"])) {
8660
+ return { type: "BOOLEAN", value: !obj["expr"].value };
8661
+ }
8662
+ if (obj["type"] === "GROUP" && isBooleanNode(obj["expr"])) return obj["expr"];
8663
+ if (obj["type"] === "LOGICAL") {
8664
+ const left = obj["left"];
8665
+ const right = obj["right"];
8666
+ if (obj["op"] === "AND") {
8667
+ if (isBooleanNode(left, false) || isBooleanNode(right, false)) return { type: "BOOLEAN", value: false };
8668
+ if (isBooleanNode(left, true)) return right;
8669
+ if (isBooleanNode(right, true)) return left;
8670
+ } else if (obj["op"] === "OR") {
8671
+ if (isBooleanNode(left, true) || isBooleanNode(right, true)) return { type: "BOOLEAN", value: true };
8672
+ if (isBooleanNode(left, false)) return right;
8673
+ if (isBooleanNode(right, false)) return left;
8674
+ }
8675
+ }
8676
+ return obj;
8677
+ }
8311
8678
  function findVariableRef(node) {
8312
8679
  if (Array.isArray(node)) {
8313
8680
  for (const value of node) {
@@ -8318,7 +8685,7 @@ function findVariableRef(node) {
8318
8685
  }
8319
8686
  if (node !== null && typeof node === "object") {
8320
8687
  const obj = node;
8321
- if (obj["type"] === "VARIABLE" && typeof obj["name"] === "string") return obj["name"];
8688
+ if ((obj["type"] === "VARIABLE" || obj["type"] === "VARIABLE_COL" || obj["type"] === "VARIABLE_IN_LIST") && typeof obj["name"] === "string") return obj["name"];
8322
8689
  for (const value of Object.values(obj)) {
8323
8690
  const found = findVariableRef(value);
8324
8691
  if (found !== null) return found;
@@ -8561,6 +8928,7 @@ async function assertDmlWhereCapability(stmt, client, cacheContext) {
8561
8928
  if (stmt.subtableCode || stmt.type === "UPDATE" && stmt.from != null) return;
8562
8929
  const fields = whereNeedsFieldMetadata(stmt.where) ? await getFieldsCached(stmt.appId, client, cacheContext) : [];
8563
8930
  const byCode = new Map(fields.map((field) => [field.code, field]));
8931
+ if (stmt.where.type === "BOOLEAN" && stmt.where.value === false) return;
8564
8932
  const result = classifyWhereCapability(stmt.where, (field) => {
8565
8933
  if (field.field === "$id") return resolveFieldSemantics({ fieldType: "__ID__" });
8566
8934
  const info = byCode.get(field.field);
@@ -8633,6 +9001,9 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
8633
9001
  }
8634
9002
  return result;
8635
9003
  }
9004
+ function isConstantFalseWhere(where) {
9005
+ return where?.type === "BOOLEAN" && where.value === false;
9006
+ }
8636
9007
  function isNoFromSelect(stmt) {
8637
9008
  return stmt.from.appId === 0 && stmt.from.cteName === NO_FROM_CTE_NAME;
8638
9009
  }
@@ -8664,6 +9035,8 @@ function stringFuncHasFieldRef(expr) {
8664
9035
  function validateNoFromColumns(stmt) {
8665
9036
  for (const col of stmt.columns) {
8666
9037
  switch (col.type) {
9038
+ case "VARIABLE_COL":
9039
+ throw new Error(`internal error: unresolved SELECT variable @${col.name}`);
8667
9040
  case "LITERAL_COL":
8668
9041
  break;
8669
9042
  case "ARITH_COL":
@@ -8899,6 +9272,7 @@ function collectTypedInFieldRefs(expr, out) {
8899
9272
  return;
8900
9273
  case "NULL_CHECK":
8901
9274
  case "EXISTS":
9275
+ case "BOOLEAN":
8902
9276
  return;
8903
9277
  }
8904
9278
  }
@@ -9361,7 +9735,8 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
9361
9735
  validateKlikePushdownPlan(pushdownPlan);
9362
9736
  const mainPushDown = pushdownPlan.mainCondition;
9363
9737
  const tableConditions = pushdownPlan.joinConditions;
9364
- const mainFetch = fetchTableRecordsForFullScan(
9738
+ const constantFalse = isConstantFalseWhere(stmt.where);
9739
+ const mainFetch = constantFalse ? Promise.resolve([]) : fetchTableRecordsForFullScan(
9365
9740
  stmt,
9366
9741
  stmt.from,
9367
9742
  client,
@@ -9376,6 +9751,10 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
9376
9751
  const parallelJoins = [];
9377
9752
  const onOptJoins = [];
9378
9753
  for (const join2 of stmt.joins) {
9754
+ if (constantFalse) {
9755
+ parallelJoins.push({ join: join2, promise: Promise.resolve([]) });
9756
+ continue;
9757
+ }
9379
9758
  const jCond = join2.table.alias ? tableConditions.get(join2.table.alias) ?? null : null;
9380
9759
  if (jCond !== null) {
9381
9760
  parallelJoins.push({
@@ -10773,6 +11152,26 @@ async function executeInsertSelect(stmt, client, options, cacheContext, cteCache
10773
11152
  };
10774
11153
  }
10775
11154
  async function executeUpdate(stmt, client, options, cacheContext, tempTables) {
11155
+ if (stmt.checkGroups?.length && isConstantFalseWhere(stmt.where)) {
11156
+ const fieldInfos2 = await loadWritableTopLevelDmlFields(
11157
+ stmt.appId,
11158
+ stmt.assignments.map((assignment) => assignment.field),
11159
+ client,
11160
+ cacheContext
11161
+ );
11162
+ await loadNumberPrecisionForTargets(
11163
+ stmt.appId,
11164
+ stmt.assignments.map((assignment) => assignment.field),
11165
+ fieldInfos2,
11166
+ client,
11167
+ cacheContext
11168
+ );
11169
+ const fieldTypes2 = await getFieldTypeMap(stmt.appId, client, cacheContext);
11170
+ assertUpdateCheckRefs(stmt, fieldTypes2);
11171
+ assertCheckComparisonTypes(stmt, updateEvaluationTypes(fieldTypes2, stmt.appId));
11172
+ await assertDmlWhereCapability(stmt, client, cacheContext);
11173
+ return { type: "UPDATE", updatedCount: 0 };
11174
+ }
10776
11175
  if (stmt.checkGroups?.length) return executeCheckedPlainDml(stmt, client, options, cacheContext, tempTables);
10777
11176
  if (stmt.subtableCode) {
10778
11177
  await assertDmlWhereCapability(stmt, client, cacheContext);
@@ -10793,6 +11192,7 @@ async function executeUpdate(stmt, client, options, cacheContext, tempTables) {
10793
11192
  cacheContext
10794
11193
  );
10795
11194
  await assertDmlWhereCapability(stmt, client, cacheContext);
11195
+ if (isConstantFalseWhere(stmt.where)) return { type: "UPDATE", updatedCount: 0 };
10796
11196
  if (stmt.from != null) {
10797
11197
  return executeUpdateFrom(stmt, stmt.from, client, options, cacheContext, tempTables);
10798
11198
  }
@@ -10874,6 +11274,7 @@ function collectUpdateFromTargetFields(stmt) {
10874
11274
  }
10875
11275
  async function executeDelete(stmt, client, options, cacheContext) {
10876
11276
  await assertDmlWhereCapability(stmt, client, cacheContext);
11277
+ if (isConstantFalseWhere(stmt.where)) return { type: "DELETE", deletedCount: 0 };
10877
11278
  if (stmt.subtableCode) {
10878
11279
  return executeDeleteSubtable(stmt, client, options, cacheContext);
10879
11280
  }
@@ -11256,6 +11657,7 @@ async function executeReorder(stmt, client, options, cacheContext) {
11256
11657
  cacheContext
11257
11658
  );
11258
11659
  const reorderFields = await getFieldsCached(stmt.appId, client, cacheContext);
11660
+ if (isConstantFalseWhere(stmt.where)) return { type: "REORDER", reorderedParentCount: 0 };
11259
11661
  const reorderSemanticsByCode = new Map(reorderFields.map((field) => [
11260
11662
  field.code,
11261
11663
  field.semantics ?? resolveFieldSemantics(field)
@@ -11482,6 +11884,8 @@ function collectSubqueryTasks(where, client, options, cacheContext, tasks, cteCa
11482
11884
  }));
11483
11885
  break;
11484
11886
  }
11887
+ case "BOOLEAN":
11888
+ break;
11485
11889
  }
11486
11890
  }
11487
11891
  async function resolveSetSubqueries(assignments, client, options, cacheContext) {
@@ -11519,9 +11923,11 @@ async function resolveScalarColumns(columns, client, options, cacheContext, cteC
11519
11923
  pending.forEach(([i], idx) => cache.set(i, values[idx]));
11520
11924
  return cache;
11521
11925
  }
11926
+ var validateExplainInfo = /* @__PURE__ */ new WeakMap();
11522
11927
  async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords = 1e4) {
11523
11928
  const fieldApps = /* @__PURE__ */ new Set();
11524
11929
  const processStatusApps = /* @__PURE__ */ new Set();
11930
+ const numberPrecisionApps = /* @__PURE__ */ new Set();
11525
11931
  const tracedClient = {
11526
11932
  ...client,
11527
11933
  getFields: async (appId) => {
@@ -11531,6 +11937,10 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
11531
11937
  getProcessStatuses: async (appId) => {
11532
11938
  processStatusApps.add(appId);
11533
11939
  return client.getProcessStatuses(appId);
11940
+ },
11941
+ getNumberPrecision: async (appId) => {
11942
+ numberPrecisionApps.add(appId);
11943
+ return client.getNumberPrecision(appId);
11534
11944
  }
11535
11945
  };
11536
11946
  const capabilities = /* @__PURE__ */ new Map();
@@ -11579,6 +11989,51 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
11579
11989
  }));
11580
11990
  }
11581
11991
  }
11992
+ } else if (typed["type"] === "VALIDATE") {
11993
+ const validate = node;
11994
+ fieldApps.add(validate.appId);
11995
+ const fields = await getFieldsCached(validate.appId, tracedClient, cacheContext);
11996
+ const infoByCode = new Map(fields.map((field) => [field.code, field]));
11997
+ const targets = resolveExistingValidationTargets(validate, fields);
11998
+ const checks = collectCheckFieldRefs(validate.checkGroups ?? []);
11999
+ const whereFields = collectValidateWhereFields(validate.where);
12000
+ for (const ref of checks) {
12001
+ if (ref.field !== "$id" && !infoByCode.has(ref.field)) {
12002
+ throw customCheckParseError(`CHECK \u306E\u30D5\u30A3\u30FC\u30EB\u30C9 ${ref.field} \u306F APP${validate.appId} \u306B\u5B58\u5728\u3057\u307E\u305B\u3093`);
12003
+ }
12004
+ }
12005
+ for (const field of whereFields) {
12006
+ if (field !== "$id" && !infoByCode.has(field)) {
12007
+ throw new Error(`ArgumentError: WHERE field ${field} does not exist in APP${validate.appId}.`);
12008
+ }
12009
+ }
12010
+ const types = new Map(fields.map((field) => [field.code, field.fieldType]));
12011
+ types.set("$id", "RECORD_NUMBER");
12012
+ assertCheckComparisonTypes(validate, types);
12013
+ const capability = classifyWhereCapability(validate.where, (field) => field.field === "$id" ? resolveFieldSemantics({ fieldType: "__ID__" }) : infoByCode.get(field.field)?.semantics ?? (infoByCode.has(field.field) ? resolveFieldSemantics(infoByCode.get(field.field)) : void 0));
12014
+ if (capability.capability === "UNSUPPORTED") {
12015
+ throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
12016
+ }
12017
+ const fieldTypes = new Map(fields.map((field) => [field.code, field.fieldType]));
12018
+ const fieldOptions = new Map(fields.flatMap((field) => field.optionOrder ? [[field.code, new Set(Object.keys(field.optionOrder))]] : []));
12019
+ const prefilter = validate.where === null ? null : capability.capability === "EXACT_PUSHDOWN" ? validate.where : extractSafePushdownLeaves(validate.where, {
12020
+ allowUnqualifiedFields: true,
12021
+ fieldTypes,
12022
+ fieldOptions,
12023
+ allowKlike: false
12024
+ });
12025
+ const needsPrecision = targets.some((field) => field.fieldType === "NUMBER");
12026
+ if (needsPrecision) {
12027
+ numberPrecisionApps.add(validate.appId);
12028
+ await getNumberPrecisionCached(validate.appId, tracedClient, cacheContext);
12029
+ }
12030
+ validateExplainInfo.set(validate, {
12031
+ targetFields: targets.map((field) => field.code),
12032
+ fetchFields: [.../* @__PURE__ */ new Set(["$id", ...targets.map((field) => field.code), ...whereFields, ...checks.map((ref) => ref.field)])],
12033
+ capability,
12034
+ prefilter,
12035
+ numberPrecision: needsPrecision
12036
+ });
11582
12037
  } else if (typed["type"] === "UPDATE" || typed["type"] === "DELETE") {
11583
12038
  fieldApps.add(node.appId);
11584
12039
  await assertDmlWhereCapability(
@@ -11609,12 +12064,13 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
11609
12064
  }));
11610
12065
  }
11611
12066
  }
11612
- return { capabilities, orderPlans, fieldApps, processStatusApps };
12067
+ return { capabilities, orderPlans, fieldApps, processStatusApps, numberPrecisionApps };
11613
12068
  }
11614
12069
  function explainMetadataLines(analysis) {
11615
12070
  return [
11616
12071
  ...[...analysis.fieldApps].sort((a, b) => a - b).map((appId) => ` metadata API: form definition APP${appId}`),
11617
- ...[...analysis.processStatusApps].sort((a, b) => a - b).map((appId) => ` metadata API: process status APP${appId}`)
12072
+ ...[...analysis.processStatusApps].sort((a, b) => a - b).map((appId) => ` metadata API: process status APP${appId}`),
12073
+ ...[...analysis.numberPrecisionApps].sort((a, b) => a - b).map((appId) => ` metadata API: number precision APP${appId}`)
11618
12074
  ];
11619
12075
  }
11620
12076
  async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4, cursorMaxActive = 2) {
@@ -11625,7 +12081,7 @@ async function buildBatchExplainPlans(sql, client, injectedVariables, cacheConte
11625
12081
  const plans = [];
11626
12082
  for (let i = 0; i < statements.length; i++) {
11627
12083
  const stmt = statements[i];
11628
- const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr: resolveVariableRefs(stmt.expr, variables) } : stmt : resolveVariableRefs(stmt, variables);
12084
+ const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr: resolveBatchVariableReferences(stmt.expr, variables) } : stmt : resolveBatchVariableReferences(stmt, variables);
11629
12085
  validateKlikeStatement(planStmt);
11630
12086
  const whereAnalysis = await buildExplainWhereAnalysis(planStmt, client, cacheContext, maxRecords);
11631
12087
  const statementPlan = addCursorConcurrency(buildBatchStatementPlan(
@@ -11641,7 +12097,7 @@ async function buildBatchExplainPlans(sql, client, injectedVariables, cacheConte
11641
12097
  plan: statementPlan.length === 0 ? metadataPlan : [statementPlan[0], ...metadataPlan, ...statementPlan.slice(1)]
11642
12098
  });
11643
12099
  if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
11644
- variables.set(stmt.name, { type: "string", value: `@${stmt.name}` });
12100
+ variables.set(stmt.name, stmt.type === "SET_VARIABLE" && stmt.expr.type === "ARRAY" ? { type: "array", elements: stmt.expr.elements.map((element) => ({ type: "string", value: element.value })) } : { type: "string", value: `@${stmt.name}` });
11645
12101
  }
11646
12102
  }
11647
12103
  return { statementCount: statements.length, statements: plans };
@@ -11776,8 +12232,30 @@ function buildExplainPlan(query, label, capabilities, orderPlans) {
11776
12232
  if (query.type === "UPDATE") return buildUpdatePlan(query, label, capabilities, orderPlans);
11777
12233
  if (query.type === "DELETE") return buildDeletePlan(query, label);
11778
12234
  if (query.type === "REORDER") return buildReorderPlan(query, label);
12235
+ if (query.type === "VALIDATE") return buildValidatePlan(query, label);
11779
12236
  return buildSelectPlan(query, label, capabilities, orderPlans);
11780
12237
  }
12238
+ function buildValidatePlan(stmt, label) {
12239
+ const info = validateExplainInfo.get(stmt);
12240
+ const lines = [];
12241
+ if (label) lines.push(label);
12242
+ lines.push(`VALIDATE APP${stmt.appId}`);
12243
+ lines.push(" operation: read-only existing-record constraint audit (writesKintone=false)");
12244
+ lines.push(" fetch API: GET records via offset + $id keyset paging (Cursor API unused)");
12245
+ lines.push(" complete input: required (onLimit=truncate disabled)");
12246
+ if (!info) {
12247
+ lines.push(" metadata: form definition required; number precision required for NUMBER targets");
12248
+ return lines;
12249
+ }
12250
+ lines.push(` WHERE capability: ${info.capability.capability}`);
12251
+ lines.push(` kintone query: ${info.prefilter === null ? "(\u5168\u4EF6\u53D6\u5F97)" : whereToKintone(info.prefilter)}`);
12252
+ lines.push(` audit fields: ${info.targetFields.length === 0 ? "(\u306A\u3057)" : info.targetFields.join(", ")}`);
12253
+ lines.push(` fetch fields: ${info.fetchFields.join(", ")}`);
12254
+ lines.push(` number precision: ${info.numberPrecision ? "required" : "not required"}`);
12255
+ lines.push(" local checks: original WHERE re-evaluation + built-in constraints + CHECK groups");
12256
+ lines.push(" records/mutation API during EXPLAIN: none; violation count unavailable");
12257
+ return lines;
12258
+ }
11781
12259
  function buildSelectPlan(stmt, label, capabilities, orderPlans) {
11782
12260
  const whereCapability = capabilities?.get(stmt) ?? (capabilities ? [...capabilities].find(([candidate]) => JSON.stringify(candidate) === JSON.stringify(stmt))?.[1] : void 0);
11783
12261
  const orderPlan = orderPlans?.get(stmt) ?? (orderPlans ? [...orderPlans].find(([candidate]) => JSON.stringify(candidate) === JSON.stringify(stmt))?.[1] : void 0);
@@ -11789,6 +12267,12 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans) {
11789
12267
  const lines = [];
11790
12268
  if (label) lines.push(label);
11791
12269
  lines.push(` mode: ${mode}`);
12270
+ if (isConstantFalseWhere(stmt.where)) {
12271
+ lines.push(" predicate: constant false");
12272
+ lines.push(" records API access: none");
12273
+ lines.push(` app: APP${stmt.from.appId} (${stmt.from.appId})`);
12274
+ return lines;
12275
+ }
11792
12276
  if (orderPlan) {
11793
12277
  lines.push(` order plan: ${orderPlan.kind}`);
11794
12278
  if (orderPlan.reasonCodes.length > 0) lines.push(` order reason: ${orderPlan.reasonCodes.join(", ")}`);
@@ -11936,6 +12420,8 @@ function collectSubqueryPlans(stmt, capabilities, orderPlans) {
11936
12420
  break;
11937
12421
  case "NULL_CHECK":
11938
12422
  break;
12423
+ case "BOOLEAN":
12424
+ break;
11939
12425
  }
11940
12426
  };
11941
12427
  visitWhere(stmt.where);
@@ -11988,7 +12474,7 @@ function buildUpdatePlan(stmt, label, capabilities, orderPlans) {
11988
12474
  } else {
11989
12475
  lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
11990
12476
  }
11991
- lines.push(` api: GET /k/v1/records.json \u2192 PUT /k/v1/records.json`);
12477
+ lines.push(isConstantFalseWhere(stmt.where) ? " api: metadata validation only (records API access: none)" : ` api: GET /k/v1/records.json \u2192 PUT /k/v1/records.json`);
11992
12478
  const setTypes = [];
11993
12479
  if (isArith) setTypes.push("\u7B97\u8853 SET\uFF08\u73FE\u5728\u5024\u3092\u53D6\u5F97\u3057\u3066\u8A08\u7B97\uFF09");
11994
12480
  if (isStringFunc) setTypes.push("\u6587\u5B57\u5217\u95A2\u6570 SET\uFF08\u73FE\u5728\u5024\u3092\u53D6\u5F97\u3057\u3066\u8A55\u4FA1\uFF09");
@@ -12019,7 +12505,7 @@ function buildDeletePlan(stmt, label) {
12019
12505
  lines.push(` [DELETE]`);
12020
12506
  lines.push(` target: APP${stmt.appId} (${stmt.appId})`);
12021
12507
  lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
12022
- lines.push(` api: GET /k/v1/records.json \u2192 DELETE /k/v1/records.json`);
12508
+ lines.push(isConstantFalseWhere(stmt.where) ? " api: metadata validation only (records API access: none)" : ` api: GET /k/v1/records.json \u2192 DELETE /k/v1/records.json`);
12023
12509
  return lines;
12024
12510
  }
12025
12511
  function buildUpsertPlan(stmt, label) {
@@ -12059,7 +12545,7 @@ function buildReorderPlan(stmt, label) {
12059
12545
  ` table: ${target}`,
12060
12546
  ` scope: ${scope}`,
12061
12547
  ` by: ${byStr}`,
12062
- ` api: GET /k/v1/records.json\uFF08\u884C ID \u53D6\u5F97\uFF09\u2192 PUT /k/v1/records.json\uFF08id \u914D\u5217\u306E\u307F\u9001\u4FE1\uFF09`
12548
+ isConstantFalseWhere(stmt.where) ? ` api: metadata validation only (records API access: none)` : ` api: GET /k/v1/records.json\uFF08\u884C ID \u53D6\u5F97\uFF09\u2192 PUT /k/v1/records.json\uFF08id \u914D\u5217\u306E\u307F\u9001\u4FE1\uFF09`
12063
12549
  ];
12064
12550
  if (!stmt.all && stmt.where) {
12065
12551
  lines.splice(5, 0, ` where: ${safeWhereToKintone(stmt.where)}`);
@@ -12071,6 +12557,7 @@ function formatOrderByItem(item) {
12071
12557
  return `${key} ${item.direction}`;
12072
12558
  }
12073
12559
  function safeWhereToKintone(where) {
12560
+ if (where.type === "BOOLEAN") return where.value ? "TRUE" : "FALSE (constant)";
12074
12561
  try {
12075
12562
  return whereToKintone(where);
12076
12563
  } catch {
@@ -15038,7 +15525,7 @@ async function run() {
15038
15525
  isDmlStatement = writesKintone(stmt);
15039
15526
  hasWhere = hasWhereClause(stmt);
15040
15527
  insertValuesCount = getInsertValuesCount(stmt);
15041
- const supported = stmtType === "SELECT" || stmtType === "UNION" || stmtType === "WITH" || stmtType === "EXPLAIN" || stmtType === "SHOW_APPS" || stmtType === "DESCRIBE" || stmtType === "ASSERT" || isDmlType(stmtType);
15528
+ const supported = stmtType === "SELECT" || stmtType === "UNION" || stmtType === "WITH" || stmtType === "EXPLAIN" || stmtType === "SHOW_APPS" || stmtType === "DESCRIBE" || stmtType === "ASSERT" || stmtType === "VALIDATE" || isDmlType(stmtType);
15042
15529
  if (!supported) {
15043
15530
  process.stderr.write(`ArgumentError: unsupported statement type in CLI: ${stmtType}
15044
15531
  `);
@@ -15088,10 +15575,11 @@ async function run() {
15088
15575
  const allowWithoutWhere = args.allowWithoutWhere || envBool("KSQL_ALLOW_WITHOUT_WHERE") === true || Boolean(profile.dml?.allowWithoutWhere);
15089
15576
  const dmlMaxRows = args.dmlMaxRows ?? envInt2("KSQL_DML_MAX_ROWS") ?? profile.dml?.maxRows ?? 100;
15090
15577
  const isValidationOnly = batchAnalysis?.containsValidationOnly === true || parsedStmt !== null && typeof parsedStmt === "object" && "validateOnly" in parsedStmt && parsedStmt.validateOnly === true;
15091
- const surfaceForcesOnLimitError = isDmlStatement || batchContainsDml || isValidationOnly;
15578
+ const isExistingRecordValidation = batchAnalysis?.statements.some((s) => s.statementType === "VALIDATE") === true || getStatementType(parsedStmt) === "VALIDATE";
15579
+ const surfaceForcesOnLimitError = isDmlStatement || batchContainsDml || isValidationOnly || isExistingRecordValidation;
15092
15580
  const effectiveOnLimit = surfaceForcesOnLimitError ? "error" : onLimit;
15093
15581
  if (surfaceForcesOnLimitError && onLimit === "truncate" && !quiet && !args.dryRun) {
15094
- const reason = isDmlStatement || batchContainsDml ? "DML" : "VALIDATE ONLY";
15582
+ const reason = isDmlStatement || batchContainsDml ? "DML" : isExistingRecordValidation ? "VALIDATE" : "VALIDATE ONLY";
15095
15583
  process.stderr.write(`note: onLimit=truncate is ignored for ${reason} (forced to error)
15096
15584
  `);
15097
15585
  }