@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/README.md +1 -1
- package/dist-cli/ksql.js +528 -40
- package/dist-mcp/ksql-mcp.js +529 -42
- package/dist-mcpb/ksql-mcp.mcpb +0 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
-
|
|
4371
|
-
|
|
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 =
|
|
4525
|
+
const variableRefs = [];
|
|
4424
4526
|
collectVariableRefs(stmt, variableRefs);
|
|
4425
|
-
|
|
4426
|
-
|
|
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.
|
|
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, {
|
|
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 =
|
|
8352
|
+
const resolvedStmt2 = resolveBatchVariableReferences(stmt, variables);
|
|
8068
8353
|
validateKlikeStatement(resolvedStmt2);
|
|
8069
|
-
if (resolvedStmt2.expr.type === "
|
|
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 =
|
|
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
|
|
8601
|
+
function resolveBatchVariableReferences(node, variables) {
|
|
8293
8602
|
if (Array.isArray(node)) {
|
|
8294
|
-
return node.map((v) =>
|
|
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
|
-
|
|
8306
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
}
|