@rex0220/kintone-sql-tools 3.4.0 → 3.6.1
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 +5 -3
- package/dist-cli/ksql.js +2349 -93
- package/dist-mcp/ksql-mcp.js +2386 -137
- package/dist-mcpb/ksql-mcp.mcpb +0 -0
- package/package.json +1 -1
package/dist-cli/ksql.js
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
// src/cli/index.ts
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
|
+
CLI_IMPORT_SOURCE_REQUIRED_MESSAGE: () => CLI_IMPORT_SOURCE_REQUIRED_MESSAGE,
|
|
24
25
|
HELP_TEXT: () => HELP_TEXT,
|
|
25
26
|
buildBatchDmlConfirmMessage: () => buildBatchDmlConfirmMessage,
|
|
26
27
|
buildBatchStatementSummary: () => buildBatchStatementSummary,
|
|
@@ -37,6 +38,7 @@ __export(index_exports, {
|
|
|
37
38
|
parseTokenMap: () => parseTokenMap,
|
|
38
39
|
runWithArgv: () => runWithArgv,
|
|
39
40
|
shouldExitOnEmpty: () => shouldExitOnEmpty,
|
|
41
|
+
toCliImportError: () => toCliImportError,
|
|
40
42
|
writeBatchOutput: () => writeBatchOutput
|
|
41
43
|
});
|
|
42
44
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -651,8 +653,9 @@ var ParseError = class extends Error {
|
|
|
651
653
|
}
|
|
652
654
|
};
|
|
653
655
|
var Parser = class {
|
|
654
|
-
constructor(tokens) {
|
|
656
|
+
constructor(tokens, capabilities = {}) {
|
|
655
657
|
this.tokens = tokens;
|
|
658
|
+
this.capabilities = capabilities;
|
|
656
659
|
this.allowUnaryPlusNumber = false;
|
|
657
660
|
this.scalarAllowsAggregateArgs = true;
|
|
658
661
|
this.scalarAllowsCase = true;
|
|
@@ -744,13 +747,20 @@ var Parser = class {
|
|
|
744
747
|
if (upper === "CREATE") return this.parseCreateTempTable();
|
|
745
748
|
if (upper === "DROP") return this.parseDropTempTable();
|
|
746
749
|
if (upper === "DECLARE") return this.parseDeclareVariable();
|
|
750
|
+
if (upper === "VALIDATE") return this.parseValidate();
|
|
751
|
+
if (upper === "IMPORT") {
|
|
752
|
+
if (!this.capabilities.import) {
|
|
753
|
+
throw new ParseError("IMPORT is not supported (capability is disabled).", tok);
|
|
754
|
+
}
|
|
755
|
+
return this.parseImport();
|
|
756
|
+
}
|
|
747
757
|
break;
|
|
748
758
|
}
|
|
749
759
|
default:
|
|
750
760
|
break;
|
|
751
761
|
}
|
|
752
762
|
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",
|
|
763
|
+
"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
764
|
tok
|
|
755
765
|
);
|
|
756
766
|
}
|
|
@@ -761,7 +771,7 @@ var Parser = class {
|
|
|
761
771
|
this.expect("SET" /* SET */);
|
|
762
772
|
const variable = this.expect("VARIABLE" /* VARIABLE */, "SET \u306E\u5F8C\u306B\u306F\u5909\u6570\u540D\uFF08\u4F8B: @name\uFF09\u304C\u5FC5\u8981\u3067\u3059");
|
|
763
773
|
this.expect("=" /* EQ */);
|
|
764
|
-
const expr = this.parseScalarExpr("SET", true);
|
|
774
|
+
const expr = this.peek().kind === "[" /* LBRACKET */ ? this.parseArrayLiteral() : this.parseScalarExpr("SET", true);
|
|
765
775
|
return { type: "SET_VARIABLE", name: variable.value.slice(1).toLowerCase(), expr };
|
|
766
776
|
}
|
|
767
777
|
parseDeclareVariable() {
|
|
@@ -922,11 +932,265 @@ var Parser = class {
|
|
|
922
932
|
query = this.parseDelete();
|
|
923
933
|
} else if (tok.kind === "REORDER" /* REORDER */) {
|
|
924
934
|
query = this.parseReorder();
|
|
935
|
+
} else if (tok.kind === "IDENT" /* IDENT */ && tok.value.toUpperCase() === "VALIDATE") {
|
|
936
|
+
query = this.parseValidate();
|
|
937
|
+
} else if (tok.kind === "IDENT" /* IDENT */ && tok.value.toUpperCase() === "IMPORT") {
|
|
938
|
+
if (!this.capabilities.import) {
|
|
939
|
+
throw new ParseError("IMPORT is not supported (capability is disabled).", tok);
|
|
940
|
+
}
|
|
941
|
+
query = this.parseImport();
|
|
925
942
|
} else {
|
|
926
|
-
throw new ParseError("EXPLAIN \u306E\u5F8C\u306B\u306F SELECT / WITH / INSERT / UPSERT / UPDATE / DELETE / REORDER \u304C\u5FC5\u8981\u3067\u3059", tok);
|
|
943
|
+
throw new ParseError("EXPLAIN \u306E\u5F8C\u306B\u306F SELECT / WITH / INSERT / UPSERT / UPDATE / DELETE / REORDER / VALIDATE \u304C\u5FC5\u8981\u3067\u3059", tok);
|
|
927
944
|
}
|
|
928
945
|
return { type: "EXPLAIN", query };
|
|
929
946
|
}
|
|
947
|
+
parseImport() {
|
|
948
|
+
this.advance();
|
|
949
|
+
let writeMode;
|
|
950
|
+
if (this.peek().kind === "UPDATE" /* UPDATE */) {
|
|
951
|
+
this.advance();
|
|
952
|
+
writeMode = "UPDATE_RECORD_NUMBER";
|
|
953
|
+
}
|
|
954
|
+
this.expect("INTO" /* INTO */);
|
|
955
|
+
this.rejectTempTableDml();
|
|
956
|
+
const target = this.parseIdentifier();
|
|
957
|
+
const { appId, subtableCode } = extractTableRef(target, this.prev());
|
|
958
|
+
if (subtableCode) throw new ParseError("IMPORT does not support subtables in Phase 1.", this.prev());
|
|
959
|
+
this.expect("(" /* LPAREN */);
|
|
960
|
+
const targets = [];
|
|
961
|
+
const fields = [];
|
|
962
|
+
const targetNames = /* @__PURE__ */ new Set();
|
|
963
|
+
while (true) {
|
|
964
|
+
const name = this.parseIdentifier();
|
|
965
|
+
if (targetNames.has(name)) throw new ParseError(`IMPORT target ${name} is declared more than once.`, this.prev());
|
|
966
|
+
targetNames.add(name);
|
|
967
|
+
if (this.peek().kind === "(" /* LPAREN */) {
|
|
968
|
+
this.advance();
|
|
969
|
+
const children = this.parseIdentList();
|
|
970
|
+
this.expect(")" /* RPAREN */);
|
|
971
|
+
if (new Set(children).size !== children.length) {
|
|
972
|
+
throw new ParseError(`IMPORT subtable ${name} contains duplicate child declarations.`, this.prev());
|
|
973
|
+
}
|
|
974
|
+
let rowIdSourceHeader;
|
|
975
|
+
if (this.isSoftKeyword("ROW")) {
|
|
976
|
+
this.advance();
|
|
977
|
+
for (const word of ["ID", "SOURCE"]) {
|
|
978
|
+
if (!this.isSoftKeyword(word)) throw new ParseError(`ROW must be followed by ID SOURCE <header>.`, this.peek());
|
|
979
|
+
this.advance();
|
|
980
|
+
}
|
|
981
|
+
rowIdSourceHeader = this.parseIdentifier();
|
|
982
|
+
}
|
|
983
|
+
targets.push({ kind: "SUBTABLE", subtableCode: name, children, ...rowIdSourceHeader ? { rowIdSourceHeader } : {} });
|
|
984
|
+
} else {
|
|
985
|
+
fields.push(name);
|
|
986
|
+
targets.push({ kind: "FIELD", field: name });
|
|
987
|
+
}
|
|
988
|
+
if (this.peek().kind !== "," /* COMMA */) break;
|
|
989
|
+
this.advance();
|
|
990
|
+
}
|
|
991
|
+
this.expect(")" /* RPAREN */);
|
|
992
|
+
this.expect("FROM" /* FROM */);
|
|
993
|
+
if (!this.isSoftKeyword("CSV") && !this.isSoftKeyword("JSON")) throw new ParseError("IMPORT FROM requires CSV or JSON.", this.peek());
|
|
994
|
+
const sourceKind = this.peek().value.toUpperCase();
|
|
995
|
+
this.advance();
|
|
996
|
+
const sourceName = this.parseIdentifier();
|
|
997
|
+
let encoding;
|
|
998
|
+
let hasHeader = true;
|
|
999
|
+
let columns;
|
|
1000
|
+
if (this.isSoftKeyword("ENCODING")) {
|
|
1001
|
+
if (sourceKind === "JSON") throw new ParseError("JSON source is UTF-8 only; ENCODING is not allowed.", this.peek());
|
|
1002
|
+
this.advance();
|
|
1003
|
+
const value = this.parseIdentifier().toUpperCase();
|
|
1004
|
+
if (value !== "UTF8" && value !== "SJIS") throw new ParseError("ENCODING must be UTF8 or SJIS.", this.prev());
|
|
1005
|
+
encoding = value === "UTF8" ? "utf8" : "sjis";
|
|
1006
|
+
}
|
|
1007
|
+
if (this.peek().kind === "NOT" /* NOT */ && this.peekAt(1).kind === "IDENT" /* IDENT */ && this.peekAt(1).value.toUpperCase() === "HEADER") {
|
|
1008
|
+
if (sourceKind === "JSON") throw new ParseError("NO HEADER is CSV-only.", this.peek());
|
|
1009
|
+
this.advance();
|
|
1010
|
+
this.advance();
|
|
1011
|
+
hasHeader = false;
|
|
1012
|
+
} else if (this.isSoftKeyword("NO") && this.peekAt(1).kind === "IDENT" /* IDENT */ && this.peekAt(1).value.toUpperCase() === "HEADER") {
|
|
1013
|
+
if (sourceKind === "JSON") throw new ParseError("NO HEADER is CSV-only.", this.peek());
|
|
1014
|
+
this.advance();
|
|
1015
|
+
this.advance();
|
|
1016
|
+
hasHeader = false;
|
|
1017
|
+
}
|
|
1018
|
+
if (this.isSoftKeyword("COLUMNS")) {
|
|
1019
|
+
if (sourceKind === "JSON") throw new ParseError("COLUMNS is CSV-only.", this.peek());
|
|
1020
|
+
if (hasHeader) throw new ParseError("COLUMNS requires NO HEADER.", this.peek());
|
|
1021
|
+
this.advance();
|
|
1022
|
+
this.expect("(" /* LPAREN */);
|
|
1023
|
+
columns = this.parseIdentList();
|
|
1024
|
+
this.expect(")" /* RPAREN */);
|
|
1025
|
+
}
|
|
1026
|
+
let projection;
|
|
1027
|
+
if (this.peek().kind === "SELECT" /* SELECT */) {
|
|
1028
|
+
if (sourceKind === "JSON") throw new ParseError("SELECT projection is CSV-only.", this.peek());
|
|
1029
|
+
projection = this.parseSelect();
|
|
1030
|
+
if (projection.from.cteName !== NO_FROM_CTE_NAME || projection.joins.length > 0) {
|
|
1031
|
+
throw new ParseError("IMPORT projection cannot use FROM or JOIN.", this.prev());
|
|
1032
|
+
}
|
|
1033
|
+
if (projection.where || projection.groupBy.length || projection.having || projection.orderBy.length || projection.limit !== null || projection.offset !== null) {
|
|
1034
|
+
throw new ParseError("IMPORT projection supports SELECT expressions only.", this.prev());
|
|
1035
|
+
}
|
|
1036
|
+
this.validateImportProjectionScope(projection, this.prev());
|
|
1037
|
+
if (targets.some((item) => item.kind === "SUBTABLE")) {
|
|
1038
|
+
throw new ParseError("IMPORT subtable sources cannot use SELECT projection.", this.prev());
|
|
1039
|
+
}
|
|
1040
|
+
if (projection.columns.length !== fields.length) {
|
|
1041
|
+
throw new ParseError(`IMPORT projection has ${projection.columns.length} columns; target has ${fields.length}.`, this.prev());
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
let mappingMode = "POSITION";
|
|
1045
|
+
let ignoreUnknownColumns = false;
|
|
1046
|
+
if (this.peek().kind === "BY" /* BY */ || this.isSoftKeyword("BY")) {
|
|
1047
|
+
if (sourceKind === "JSON") throw new ParseError("BY NAME is CSV-only.", this.peek());
|
|
1048
|
+
this.advance();
|
|
1049
|
+
if (!this.isSoftKeyword("NAME")) throw new ParseError("BY must be followed by NAME in IMPORT.", this.peek());
|
|
1050
|
+
this.advance();
|
|
1051
|
+
if (!hasHeader) throw new ParseError("BY NAME requires HEADER.", this.prev());
|
|
1052
|
+
if (projection) throw new ParseError("BY NAME and SELECT projection are mutually exclusive.", this.prev());
|
|
1053
|
+
mappingMode = "BY_NAME";
|
|
1054
|
+
if (this.isSoftKeyword("IGNORE")) {
|
|
1055
|
+
this.advance();
|
|
1056
|
+
if (!this.isSoftKeyword("UNKNOWN")) throw new ParseError("IGNORE must be followed by UNKNOWN COLUMNS.", this.peek());
|
|
1057
|
+
this.advance();
|
|
1058
|
+
if (!this.isSoftKeyword("COLUMNS")) throw new ParseError("IGNORE UNKNOWN must be followed by COLUMNS.", this.peek());
|
|
1059
|
+
this.advance();
|
|
1060
|
+
ignoreUnknownColumns = true;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
let keyFields;
|
|
1064
|
+
let recordNumberSourceHeader;
|
|
1065
|
+
if (this.isSoftKeyword("MATCH")) {
|
|
1066
|
+
this.advance();
|
|
1067
|
+
for (const word of ["RECORD", "NUMBER", "SOURCE"]) {
|
|
1068
|
+
if (!this.isSoftKeyword(word)) throw new ParseError(`MATCH must be followed by RECORD NUMBER SOURCE <header>.`, this.peek());
|
|
1069
|
+
this.advance();
|
|
1070
|
+
}
|
|
1071
|
+
recordNumberSourceHeader = this.parseIdentifier();
|
|
1072
|
+
}
|
|
1073
|
+
if (this.peek().kind === "ON" /* ON */ && this.peekAt(1).kind === "DUPLICATE" /* DUPLICATE */) keyFields = this.parseOnDuplicate();
|
|
1074
|
+
let replaceSubtables;
|
|
1075
|
+
if (this.peek().kind === "REPLACE" /* REPLACE */ || this.isSoftKeyword("REPLACE")) {
|
|
1076
|
+
this.advance();
|
|
1077
|
+
if (!this.isSoftKeyword("SUBTABLES")) throw new ParseError("REPLACE must be followed by SUBTABLES (...).", this.peek());
|
|
1078
|
+
this.advance();
|
|
1079
|
+
this.expect("(" /* LPAREN */);
|
|
1080
|
+
replaceSubtables = this.parseIdentList();
|
|
1081
|
+
this.expect(")" /* RPAREN */);
|
|
1082
|
+
if (new Set(replaceSubtables).size !== replaceSubtables.length) throw new ParseError("REPLACE SUBTABLES contains duplicates.", this.prev());
|
|
1083
|
+
}
|
|
1084
|
+
const subtableTargets = targets.filter((item) => item.kind === "SUBTABLE");
|
|
1085
|
+
if (subtableTargets.length) {
|
|
1086
|
+
if (projection) throw new ParseError("IMPORT subtables cannot use SELECT projection.", this.prev());
|
|
1087
|
+
if (sourceKind === "JSON") {
|
|
1088
|
+
if (subtableTargets.some((item) => item.rowIdSourceHeader)) throw new ParseError("JSON subtable IMPORT does not accept ROW ID SOURCE.", this.prev());
|
|
1089
|
+
if (replaceSubtables) throw new ParseError("REPLACE SUBTABLES is CSV-only; JSON uses nested-array replacement semantics.", this.prev());
|
|
1090
|
+
} else {
|
|
1091
|
+
if (writeMode !== "UPDATE_RECORD_NUMBER" || !recordNumberSourceHeader) throw new ParseError("CSV subtable IMPORT requires IMPORT UPDATE and MATCH RECORD NUMBER SOURCE.", this.prev());
|
|
1092
|
+
if (mappingMode !== "BY_NAME") throw new ParseError("CSV subtable IMPORT requires BY NAME.", this.prev());
|
|
1093
|
+
if (!replaceSubtables) throw new ParseError("CSV subtable IMPORT requires REPLACE SUBTABLES (...).", this.prev());
|
|
1094
|
+
const replacement = new Set(replaceSubtables);
|
|
1095
|
+
for (const item of subtableTargets) {
|
|
1096
|
+
if (!item.rowIdSourceHeader) throw new ParseError(`CSV subtable ${item.subtableCode} requires ROW ID SOURCE <header>.`, this.prev());
|
|
1097
|
+
if (!replacement.has(item.subtableCode)) throw new ParseError(`IMPORT declares child columns for non-replaced subtable ${item.subtableCode}.`, this.prev());
|
|
1098
|
+
}
|
|
1099
|
+
for (const code of replacement) {
|
|
1100
|
+
if (!subtableTargets.some((item) => item.subtableCode === code)) throw new ParseError(`REPLACE SUBTABLES target ${code} is not declared in INTO.`, this.prev());
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
} else if (replaceSubtables) {
|
|
1104
|
+
throw new ParseError("REPLACE SUBTABLES requires subtable targets in INTO.", this.prev());
|
|
1105
|
+
}
|
|
1106
|
+
if (writeMode) {
|
|
1107
|
+
if (sourceKind !== "CSV") throw new ParseError("IMPORT UPDATE supports CSV only.", this.prev());
|
|
1108
|
+
if (mappingMode !== "BY_NAME") throw new ParseError("IMPORT UPDATE requires BY NAME.", this.prev());
|
|
1109
|
+
if (!recordNumberSourceHeader) throw new ParseError("IMPORT UPDATE requires MATCH RECORD NUMBER SOURCE <header>.", this.peek());
|
|
1110
|
+
if (keyFields) throw new ParseError("IMPORT UPDATE and ON DUPLICATE are mutually exclusive.", this.prev());
|
|
1111
|
+
} else if (recordNumberSourceHeader) {
|
|
1112
|
+
throw new ParseError("MATCH RECORD NUMBER SOURCE requires IMPORT UPDATE.", this.prev());
|
|
1113
|
+
}
|
|
1114
|
+
const checkGroups = this.parseCheckGroups();
|
|
1115
|
+
const control = this.parseDmlControlSuffix();
|
|
1116
|
+
return {
|
|
1117
|
+
type: "IMPORT",
|
|
1118
|
+
appId,
|
|
1119
|
+
fields,
|
|
1120
|
+
targets,
|
|
1121
|
+
source: sourceKind === "JSON" ? { kind: "JSON", sourceName } : { kind: "CSV", sourceName, encoding, hasHeader, mappingMode, ignoreUnknownColumns, ...columns ? { columns } : {}, ...projection ? { projection } : {} },
|
|
1122
|
+
...writeMode ? { writeMode, recordNumberSourceHeader } : {},
|
|
1123
|
+
...replaceSubtables ? { replaceSubtables } : {},
|
|
1124
|
+
...keyFields ? { keyFields } : {},
|
|
1125
|
+
...checkGroups,
|
|
1126
|
+
...control
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
validateImportProjectionScope(node, token) {
|
|
1130
|
+
if (Array.isArray(node)) {
|
|
1131
|
+
node.forEach((item) => this.validateImportProjectionScope(item, token));
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (node === null || typeof node !== "object") return;
|
|
1135
|
+
const value = node;
|
|
1136
|
+
if (value.type === "SCALAR_SUBQUERY" || value.type === "SCALAR_SUBQUERY_COL") {
|
|
1137
|
+
throw new ParseError("IMPORT projection cannot use subqueries.", token);
|
|
1138
|
+
}
|
|
1139
|
+
if (typeof value.tableAlias === "string") {
|
|
1140
|
+
throw new ParseError("IMPORT projection cannot use qualified column references.", token);
|
|
1141
|
+
}
|
|
1142
|
+
Object.values(value).forEach((item) => this.validateImportProjectionScope(item, token));
|
|
1143
|
+
}
|
|
1144
|
+
/** VALIDATE APP100 [(fields)] [WHERE ...] [CHECK ...] [INTO #err]. */
|
|
1145
|
+
parseValidate() {
|
|
1146
|
+
const validateTok = this.advance();
|
|
1147
|
+
const name = this.parseIdentifier();
|
|
1148
|
+
const { appId, subtableCode } = extractTableRef(name, this.prev());
|
|
1149
|
+
if (subtableCode) {
|
|
1150
|
+
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());
|
|
1151
|
+
}
|
|
1152
|
+
let fields;
|
|
1153
|
+
if (this.consume("(" /* LPAREN */)) {
|
|
1154
|
+
fields = this.parseIdentList();
|
|
1155
|
+
this.expect(")" /* RPAREN */);
|
|
1156
|
+
}
|
|
1157
|
+
const where = this.consume("WHERE" /* WHERE */) ? this.parseWhereExpr() : null;
|
|
1158
|
+
const checks = this.parseCheckGroups();
|
|
1159
|
+
let errorTable;
|
|
1160
|
+
if (this.consume("INTO" /* INTO */)) {
|
|
1161
|
+
const tableTok = this.peek();
|
|
1162
|
+
if (tableTok.kind !== "IDENT" /* IDENT */ || !tableTok.value.startsWith("#")) {
|
|
1163
|
+
throw new ParseError("VALIDATE INTO \u306B\u306F # \u3067\u59CB\u307E\u308B\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u540D\u304C\u5FC5\u8981\u3067\u3059", tableTok);
|
|
1164
|
+
}
|
|
1165
|
+
errorTable = this.parseTableName();
|
|
1166
|
+
}
|
|
1167
|
+
const stmt = { type: "VALIDATE", appId, fields, where, ...checks, ...errorTable ? { errorTable } : {} };
|
|
1168
|
+
this.assertValidateExpressions(stmt, validateTok);
|
|
1169
|
+
return stmt;
|
|
1170
|
+
}
|
|
1171
|
+
/** v1 VALIDATE is single-app/local: subqueries and qualified references are rejected. */
|
|
1172
|
+
assertValidateExpressions(stmt, tok) {
|
|
1173
|
+
const visit = (node) => {
|
|
1174
|
+
if (Array.isArray(node)) {
|
|
1175
|
+
node.forEach(visit);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
if (node === null || typeof node !== "object") return;
|
|
1179
|
+
const obj = node;
|
|
1180
|
+
if (obj.type === "EXISTS" || obj.type === "SUBQUERY_IN_LIST" || obj.type === "SCALAR_SUBQUERY") {
|
|
1181
|
+
throw new ParseError("VALIDATE \u306E WHERE / CHECK \u306B\u30B5\u30D6\u30AF\u30A8\u30EA\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
|
|
1182
|
+
}
|
|
1183
|
+
if (obj.type === "FIELD" && obj.tableAlias !== null && obj.tableAlias !== void 0) {
|
|
1184
|
+
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);
|
|
1185
|
+
}
|
|
1186
|
+
if (obj.type === "FIELD_REF" && typeof obj.field === "string" && obj.field.includes(".")) {
|
|
1187
|
+
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);
|
|
1188
|
+
}
|
|
1189
|
+
Object.values(obj).forEach(visit);
|
|
1190
|
+
};
|
|
1191
|
+
visit(stmt.where);
|
|
1192
|
+
visit(stmt.checkGroups);
|
|
1193
|
+
}
|
|
930
1194
|
// ----------------------------------------------------------
|
|
931
1195
|
// ASSERT
|
|
932
1196
|
//
|
|
@@ -1212,6 +1476,17 @@ var Parser = class {
|
|
|
1212
1476
|
const alias2 = this.consume("AS" /* AS */) ? this.parseAliasName() : null;
|
|
1213
1477
|
return { type: "SCALAR_VALUE_COL", expr, alias: alias2 };
|
|
1214
1478
|
}
|
|
1479
|
+
if (this.peek().kind === "VARIABLE" /* VARIABLE */) {
|
|
1480
|
+
const variable = this.advance();
|
|
1481
|
+
if (!this.consume("AS" /* AS */)) {
|
|
1482
|
+
throw new ParseError("SELECT \u5217\u306E\u30D0\u30C3\u30C1\u5909\u6570\u306B\u306F AS alias \u304C\u5FC5\u8981\u3067\u3059", this.peek());
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1485
|
+
type: "VARIABLE_COL",
|
|
1486
|
+
name: variable.value.slice(1).toLowerCase(),
|
|
1487
|
+
alias: this.parseAliasName()
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1215
1490
|
const windowFunc = this.tryWindowFunc();
|
|
1216
1491
|
if (windowFunc !== null) {
|
|
1217
1492
|
return this.parseWindowColumn(windowFunc);
|
|
@@ -2027,9 +2302,7 @@ var Parser = class {
|
|
|
2027
2302
|
}
|
|
2028
2303
|
if (this.consume("NOT" /* NOT */)) {
|
|
2029
2304
|
if (this.consume("IN" /* IN */)) {
|
|
2030
|
-
this.
|
|
2031
|
-
const right2 = this.parseInListOrSubquery();
|
|
2032
|
-
this.expect(")" /* RPAREN */);
|
|
2305
|
+
const right2 = this.parseInRight();
|
|
2033
2306
|
return { type: "BINARY", op: "NOT_IN", left: field, right: right2 };
|
|
2034
2307
|
}
|
|
2035
2308
|
if (this.consume("LIKE" /* LIKE */)) {
|
|
@@ -2046,9 +2319,7 @@ var Parser = class {
|
|
|
2046
2319
|
);
|
|
2047
2320
|
}
|
|
2048
2321
|
if (this.consume("IN" /* IN */)) {
|
|
2049
|
-
this.
|
|
2050
|
-
const right2 = this.parseInListOrSubquery();
|
|
2051
|
-
this.expect(")" /* RPAREN */);
|
|
2322
|
+
const right2 = this.parseInRight();
|
|
2052
2323
|
return { type: "BINARY", op: "IN", left: field, right: right2 };
|
|
2053
2324
|
}
|
|
2054
2325
|
if (this.consume("KLIKE" /* KLIKE */)) {
|
|
@@ -2215,6 +2486,18 @@ var Parser = class {
|
|
|
2215
2486
|
);
|
|
2216
2487
|
}
|
|
2217
2488
|
// IN (...) — 値リストまたはサブクエリ
|
|
2489
|
+
parseInRight() {
|
|
2490
|
+
if (this.consume("(" /* LPAREN */)) {
|
|
2491
|
+
const right = this.parseInListOrSubquery();
|
|
2492
|
+
this.expect(")" /* RPAREN */);
|
|
2493
|
+
return right;
|
|
2494
|
+
}
|
|
2495
|
+
const variable = this.expect(
|
|
2496
|
+
"VARIABLE" /* VARIABLE */,
|
|
2497
|
+
"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"
|
|
2498
|
+
);
|
|
2499
|
+
return { type: "VARIABLE_IN_LIST", name: variable.value.slice(1).toLowerCase() };
|
|
2500
|
+
}
|
|
2218
2501
|
parseInListOrSubquery() {
|
|
2219
2502
|
if (this.peek().kind === "SELECT" /* SELECT */) {
|
|
2220
2503
|
const query = this.parseSelect();
|
|
@@ -3022,10 +3305,10 @@ function getStatementType(stmt) {
|
|
|
3022
3305
|
return typeof obj.type === "string" ? obj.type : "UNKNOWN";
|
|
3023
3306
|
}
|
|
3024
3307
|
function isDmlType(type) {
|
|
3025
|
-
return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
|
|
3308
|
+
return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER" || type === "IMPORT";
|
|
3026
3309
|
}
|
|
3027
3310
|
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";
|
|
3311
|
+
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
3312
|
}
|
|
3030
3313
|
function writesKintone(stmt) {
|
|
3031
3314
|
return isDmlType(stmt.type) && !("validateOnly" in stmt && stmt.validateOnly === true);
|
|
@@ -3036,6 +3319,8 @@ function isReadOnlyStatement(stmt) {
|
|
|
3036
3319
|
function requiresCompleteInput(stmt) {
|
|
3037
3320
|
if (isDmlType(stmt.type)) return true;
|
|
3038
3321
|
switch (stmt.type) {
|
|
3322
|
+
case "VALIDATE":
|
|
3323
|
+
return true;
|
|
3039
3324
|
case "SELECT":
|
|
3040
3325
|
return selectRequiresCompleteInput(stmt);
|
|
3041
3326
|
case "UNION":
|
|
@@ -3076,6 +3361,7 @@ function whereRequiresCompleteInput(where) {
|
|
|
3076
3361
|
case "EXISTS":
|
|
3077
3362
|
return selectRequiresCompleteInput(where.query);
|
|
3078
3363
|
case "NULL_CHECK":
|
|
3364
|
+
case "BOOLEAN":
|
|
3079
3365
|
return false;
|
|
3080
3366
|
}
|
|
3081
3367
|
}
|
|
@@ -3111,6 +3397,8 @@ function collectDmlTargetFields(stmt) {
|
|
|
3111
3397
|
// src/engine/pushDownNot.ts
|
|
3112
3398
|
function pushDownNot(expr) {
|
|
3113
3399
|
switch (expr.type) {
|
|
3400
|
+
case "BOOLEAN":
|
|
3401
|
+
return { type: "BOOLEAN", value: !expr.value };
|
|
3114
3402
|
case "BINARY": {
|
|
3115
3403
|
const negated = negateOp(expr.op);
|
|
3116
3404
|
if (negated === null) {
|
|
@@ -3190,6 +3478,7 @@ function whereHasLike(where) {
|
|
|
3190
3478
|
case "BINARY":
|
|
3191
3479
|
case "NULL_CHECK":
|
|
3192
3480
|
case "EXISTS":
|
|
3481
|
+
case "BOOLEAN":
|
|
3193
3482
|
return false;
|
|
3194
3483
|
}
|
|
3195
3484
|
}
|
|
@@ -3205,6 +3494,7 @@ function whereHasKlike(where) {
|
|
|
3205
3494
|
case "BINARY":
|
|
3206
3495
|
case "NULL_CHECK":
|
|
3207
3496
|
case "EXISTS":
|
|
3497
|
+
case "BOOLEAN":
|
|
3208
3498
|
return false;
|
|
3209
3499
|
}
|
|
3210
3500
|
}
|
|
@@ -3224,6 +3514,8 @@ function whereToKintone(expr) {
|
|
|
3224
3514
|
return convertGroup(expr);
|
|
3225
3515
|
case "EXISTS":
|
|
3226
3516
|
throw new KintoneQueryError("EXISTS \u306F kintone \u30AF\u30A8\u30EA\u306B\u5909\u63DB\u3067\u304D\u307E\u305B\u3093");
|
|
3517
|
+
case "BOOLEAN":
|
|
3518
|
+
throw new KintoneQueryError("internal error: BOOLEAN predicate reached kintone query conversion");
|
|
3227
3519
|
}
|
|
3228
3520
|
}
|
|
3229
3521
|
function convertBinary(expr) {
|
|
@@ -3302,6 +3594,8 @@ function convertValue(value, op) {
|
|
|
3302
3594
|
switch (value.type) {
|
|
3303
3595
|
case "VARIABLE":
|
|
3304
3596
|
throw new KintoneQueryError(`\u672A\u89E3\u6C7A\u306E\u30D0\u30C3\u30C1\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
|
|
3597
|
+
case "VARIABLE_IN_LIST":
|
|
3598
|
+
throw new KintoneQueryError(`\u672A\u89E3\u6C7A\u306E\u914D\u5217\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
|
|
3305
3599
|
case "STRING":
|
|
3306
3600
|
return convertString(value);
|
|
3307
3601
|
case "NUMBER":
|
|
@@ -3381,6 +3675,8 @@ function resolveSelectMode(stmt) {
|
|
|
3381
3675
|
function whereRequiresJsEval(where) {
|
|
3382
3676
|
if (where === null) return false;
|
|
3383
3677
|
switch (where.type) {
|
|
3678
|
+
case "BOOLEAN":
|
|
3679
|
+
return true;
|
|
3384
3680
|
case "BINARY":
|
|
3385
3681
|
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
3682
|
case "NULL_CHECK":
|
|
@@ -3775,6 +4071,7 @@ function collectRequiredFieldsByTable(stmt) {
|
|
|
3775
4071
|
walkWhere(where.expr, phase);
|
|
3776
4072
|
return;
|
|
3777
4073
|
case "EXISTS":
|
|
4074
|
+
case "BOOLEAN":
|
|
3778
4075
|
return;
|
|
3779
4076
|
}
|
|
3780
4077
|
};
|
|
@@ -3813,6 +4110,8 @@ function collectRequiredFieldsByTable(stmt) {
|
|
|
3813
4110
|
break;
|
|
3814
4111
|
case "LITERAL_COL":
|
|
3815
4112
|
break;
|
|
4113
|
+
case "VARIABLE_COL":
|
|
4114
|
+
throw new Error(`internal error: unresolved SELECT variable @${col.name}`);
|
|
3816
4115
|
case "AGGREGATE":
|
|
3817
4116
|
if (col.arg.type !== "WILDCARD") walkArith(col.arg, "select");
|
|
3818
4117
|
break;
|
|
@@ -3988,6 +4287,7 @@ function stripCteAlias(where, alias) {
|
|
|
3988
4287
|
case "GROUP":
|
|
3989
4288
|
return { ...where, expr: stripCteAlias(where.expr, alias) };
|
|
3990
4289
|
case "EXISTS":
|
|
4290
|
+
case "BOOLEAN":
|
|
3991
4291
|
return where;
|
|
3992
4292
|
}
|
|
3993
4293
|
}
|
|
@@ -4025,6 +4325,7 @@ function extractAndLeaves(where, accept) {
|
|
|
4025
4325
|
case "NULL_CHECK":
|
|
4026
4326
|
case "NOT":
|
|
4027
4327
|
case "EXISTS":
|
|
4328
|
+
case "BOOLEAN":
|
|
4028
4329
|
return null;
|
|
4029
4330
|
}
|
|
4030
4331
|
}
|
|
@@ -4163,6 +4464,7 @@ function collectKlikes(where, out) {
|
|
|
4163
4464
|
case "BINARY":
|
|
4164
4465
|
case "NULL_CHECK":
|
|
4165
4466
|
case "EXISTS":
|
|
4467
|
+
case "BOOLEAN":
|
|
4166
4468
|
return;
|
|
4167
4469
|
}
|
|
4168
4470
|
}
|
|
@@ -4219,6 +4521,11 @@ function validateStatement(stmt) {
|
|
|
4219
4521
|
);
|
|
4220
4522
|
}
|
|
4221
4523
|
return;
|
|
4524
|
+
case "VALIDATE":
|
|
4525
|
+
if (containsKlike(stmt)) {
|
|
4526
|
+
throw new KlikeValidationError("KLIKE / NOT KLIKE \u306F VALIDATE \u306E WHERE / CHECK \u3067\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093");
|
|
4527
|
+
}
|
|
4528
|
+
return;
|
|
4222
4529
|
case "SHOW_APPS":
|
|
4223
4530
|
case "DESCRIBE":
|
|
4224
4531
|
case "DROP_TEMP_TABLE":
|
|
@@ -4306,6 +4613,8 @@ function isDescendantOf(root, target) {
|
|
|
4306
4613
|
case "NULL_CHECK":
|
|
4307
4614
|
case "EXISTS":
|
|
4308
4615
|
return false;
|
|
4616
|
+
case "BOOLEAN":
|
|
4617
|
+
return false;
|
|
4309
4618
|
}
|
|
4310
4619
|
}
|
|
4311
4620
|
function walkWithoutNestedSelects(node, visitWhere) {
|
|
@@ -4367,8 +4676,12 @@ function collectVariableRefs(node, refs) {
|
|
|
4367
4676
|
}
|
|
4368
4677
|
if (node !== null && typeof node === "object") {
|
|
4369
4678
|
const obj = node;
|
|
4370
|
-
|
|
4371
|
-
|
|
4679
|
+
const type = obj["type"];
|
|
4680
|
+
if ((type === "VARIABLE" || type === "VARIABLE_COL" || type === "VARIABLE_IN_LIST") && typeof obj["name"] === "string") {
|
|
4681
|
+
refs.push({
|
|
4682
|
+
name: obj["name"],
|
|
4683
|
+
kind: type === "VARIABLE" ? "scalar" : type === "VARIABLE_COL" ? "select-column" : "array-in-list"
|
|
4684
|
+
});
|
|
4372
4685
|
return;
|
|
4373
4686
|
}
|
|
4374
4687
|
for (const v of Object.values(obj)) collectVariableRefs(v, refs);
|
|
@@ -4409,9 +4722,9 @@ function analyzeBatch(statements) {
|
|
|
4409
4722
|
const variableDefs = /* @__PURE__ */ new Map();
|
|
4410
4723
|
const variableOrder = [];
|
|
4411
4724
|
statements.forEach((stmt, index) => {
|
|
4412
|
-
const validationTable = "validationErrorTable" in stmt && stmt.validationErrorTable ? stmt.validationErrorTable : "onErrorSkip" in stmt && stmt.onErrorSkip ? stmt.errorTable ?? null : null;
|
|
4725
|
+
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
4726
|
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.";
|
|
4727
|
+
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
4728
|
throw new BatchAnalysisError(message, index);
|
|
4416
4729
|
}
|
|
4417
4730
|
const statementType = getStatementType(stmt);
|
|
@@ -4420,23 +4733,43 @@ function analyzeBatch(statements) {
|
|
|
4420
4733
|
const refs = /* @__PURE__ */ new Set();
|
|
4421
4734
|
const stmtAppIds = /* @__PURE__ */ new Set();
|
|
4422
4735
|
const dependsOn = /* @__PURE__ */ new Set();
|
|
4423
|
-
const variableRefs =
|
|
4736
|
+
const variableRefs = [];
|
|
4424
4737
|
collectVariableRefs(stmt, variableRefs);
|
|
4425
|
-
|
|
4426
|
-
|
|
4738
|
+
const referencedThisStatement = /* @__PURE__ */ new Set();
|
|
4739
|
+
for (const use of variableRefs) {
|
|
4740
|
+
const def = variableDefs.get(use.name);
|
|
4427
4741
|
if (def === void 0) {
|
|
4428
4742
|
throw new BatchAnalysisError(
|
|
4429
|
-
`ParseError: variable @${name} is not defined before statement ${index + 1}.`,
|
|
4743
|
+
`ParseError: variable @${use.name} is not defined before statement ${index + 1}.`,
|
|
4430
4744
|
index
|
|
4431
4745
|
);
|
|
4432
4746
|
}
|
|
4433
|
-
def.
|
|
4747
|
+
if (def.kind === "scalar" && use.kind === "array-in-list") {
|
|
4748
|
+
throw new BatchAnalysisError(
|
|
4749
|
+
`ParseError: scalar variable @${use.name} cannot be used as IN @${use.name}; use IN (@${use.name}) instead.`,
|
|
4750
|
+
index
|
|
4751
|
+
);
|
|
4752
|
+
}
|
|
4753
|
+
if (def.kind === "array" && use.kind !== "array-in-list") {
|
|
4754
|
+
throw new BatchAnalysisError(
|
|
4755
|
+
`ParseError: array variable @${use.name} can only be used as IN @${use.name}.`,
|
|
4756
|
+
index
|
|
4757
|
+
);
|
|
4758
|
+
}
|
|
4759
|
+
if (!referencedThisStatement.has(use.name)) {
|
|
4760
|
+
def.referencedBy.push(index);
|
|
4761
|
+
referencedThisStatement.add(use.name);
|
|
4762
|
+
}
|
|
4434
4763
|
}
|
|
4435
4764
|
if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
|
|
4436
4765
|
if (variableDefs.has(stmt.name)) {
|
|
4437
4766
|
throw new BatchAnalysisError(`ParseError: variable @${stmt.name} is already defined.`, index);
|
|
4438
4767
|
}
|
|
4439
|
-
variableDefs.set(stmt.name, {
|
|
4768
|
+
variableDefs.set(stmt.name, {
|
|
4769
|
+
index,
|
|
4770
|
+
kind: stmt.type === "SET_VARIABLE" && stmt.expr.type === "ARRAY" ? "array" : "scalar",
|
|
4771
|
+
referencedBy: []
|
|
4772
|
+
});
|
|
4440
4773
|
variableOrder.push(stmt.name);
|
|
4441
4774
|
if (variableOrder.length > MAX_BATCH_VARIABLES) {
|
|
4442
4775
|
throw new BatchAnalysisError(
|
|
@@ -4469,7 +4802,7 @@ function analyzeBatch(statements) {
|
|
|
4469
4802
|
dependsOn.add(at);
|
|
4470
4803
|
}
|
|
4471
4804
|
if (validationTable) {
|
|
4472
|
-
const payloadFields = stmt.type === "UPDATE" ? ["$id", ...stmt.assignments.map((a) => a.field)] : "fields" in stmt ? stmt.fields : [];
|
|
4805
|
+
const payloadFields = stmt.type === "VALIDATE" ? ["$id", "$err_field", "$err_code", "$err_message", "$err_value"] : stmt.type === "IMPORT" && stmt.targets?.some((target) => target.kind === "SUBTABLE") ? [...new Set(stmt.targets.flatMap((target) => target.kind === "FIELD" ? [target.field] : target.children)), "$err_subtable", "$err_subrow", "$err_source_row"] : stmt.type === "UPDATE" ? ["$id", ...stmt.assignments.map((a) => a.field)] : "fields" in stmt ? stmt.fields : [];
|
|
4473
4806
|
const signature = JSON.stringify(payloadFields);
|
|
4474
4807
|
const at = defined.get(validationTable);
|
|
4475
4808
|
if (at === void 0) {
|
|
@@ -4544,6 +4877,7 @@ function analyzeBatch(statements) {
|
|
|
4544
4877
|
const needsCompleteInput = results.some((r) => r.requiresCompleteInput);
|
|
4545
4878
|
const variables = variableOrder.map((name) => ({
|
|
4546
4879
|
name,
|
|
4880
|
+
kind: variableDefs.get(name).kind,
|
|
4547
4881
|
referencedBy: [...variableDefs.get(name).referencedBy]
|
|
4548
4882
|
}));
|
|
4549
4883
|
return {
|
|
@@ -4820,6 +5154,7 @@ function whereNeedsFieldMetadata(where) {
|
|
|
4820
5154
|
case "GROUP":
|
|
4821
5155
|
return whereNeedsFieldMetadata(where.expr);
|
|
4822
5156
|
case "EXISTS":
|
|
5157
|
+
case "BOOLEAN":
|
|
4823
5158
|
return false;
|
|
4824
5159
|
}
|
|
4825
5160
|
}
|
|
@@ -4844,6 +5179,7 @@ function explainNeedsAppMetadata(statement) {
|
|
|
4844
5179
|
seen.add(node);
|
|
4845
5180
|
if (Array.isArray(node)) return node.some(visit);
|
|
4846
5181
|
const item = node;
|
|
5182
|
+
if (item["type"] === "VALIDATE") return true;
|
|
4847
5183
|
if (item["type"] === "SELECT" && selectNeedsOwnMetadata(node)) {
|
|
4848
5184
|
return true;
|
|
4849
5185
|
}
|
|
@@ -5334,6 +5670,8 @@ function resolveFieldRef(row, field) {
|
|
|
5334
5670
|
// src/engine/evalWhere.ts
|
|
5335
5671
|
function evalWhere(expr, row, resolveFieldType, appliedKlikes, resolveFieldSemantics2) {
|
|
5336
5672
|
switch (expr.type) {
|
|
5673
|
+
case "BOOLEAN":
|
|
5674
|
+
return expr.value;
|
|
5337
5675
|
case "BINARY":
|
|
5338
5676
|
return evalBinary(expr, row, resolveFieldType, appliedKlikes, resolveFieldSemantics2);
|
|
5339
5677
|
case "NULL_CHECK":
|
|
@@ -5504,6 +5842,8 @@ function resolveValue(value, row, resolveFieldType, resolveFieldSemantics2) {
|
|
|
5504
5842
|
switch (value.type) {
|
|
5505
5843
|
case "VARIABLE":
|
|
5506
5844
|
throw new Error(`ParseError: unresolved batch variable @${value.name}.`);
|
|
5845
|
+
case "VARIABLE_IN_LIST":
|
|
5846
|
+
throw new Error(`ParseError: unresolved batch array variable @${value.name}.`);
|
|
5507
5847
|
case "STRING":
|
|
5508
5848
|
return value.value;
|
|
5509
5849
|
case "NUMBER":
|
|
@@ -5819,6 +6159,9 @@ function collectConditionFields(expr, out) {
|
|
|
5819
6159
|
case "GROUP":
|
|
5820
6160
|
collectConditionFields(expr.expr, out);
|
|
5821
6161
|
break;
|
|
6162
|
+
case "EXISTS":
|
|
6163
|
+
case "BOOLEAN":
|
|
6164
|
+
break;
|
|
5822
6165
|
}
|
|
5823
6166
|
}
|
|
5824
6167
|
function updateToPutBatchesArith(stmt, records, fieldTypes = /* @__PURE__ */ new Map()) {
|
|
@@ -6034,6 +6377,8 @@ function convertDmlSqlValue(value, fieldType) {
|
|
|
6034
6377
|
switch (value.type) {
|
|
6035
6378
|
case "VARIABLE":
|
|
6036
6379
|
throw new DmlConvertError(`\u672A\u89E3\u6C7A\u306E\u30D0\u30C3\u30C1\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
|
|
6380
|
+
case "VARIABLE_IN_LIST":
|
|
6381
|
+
throw new DmlConvertError(`\u672A\u89E3\u6C7A\u306E\u914D\u5217\u5909\u6570 @${value.name} \u304C\u3042\u308A\u307E\u3059`);
|
|
6037
6382
|
case "STRING":
|
|
6038
6383
|
return convertString2(value.value, fieldType);
|
|
6039
6384
|
case "NUMBER":
|
|
@@ -6869,6 +7214,8 @@ function project(rows, columns, scalarCache, resolveFieldType, sourceColumns, re
|
|
|
6869
7214
|
const out = {};
|
|
6870
7215
|
for (const [colIdx, col] of columns.entries()) {
|
|
6871
7216
|
switch (col.type) {
|
|
7217
|
+
case "VARIABLE_COL":
|
|
7218
|
+
throw new Error(`internal error: unresolved SELECT variable @${col.name}`);
|
|
6872
7219
|
case "WILDCARD":
|
|
6873
7220
|
Object.assign(out, stripParentShortcutColumns(row));
|
|
6874
7221
|
break;
|
|
@@ -6972,6 +7319,8 @@ function computeExplicitOutputKeys(columns, defaultFieldKeys) {
|
|
|
6972
7319
|
}
|
|
6973
7320
|
function computeOutputKey(col, colIdx, defaultFieldKeys) {
|
|
6974
7321
|
switch (col.type) {
|
|
7322
|
+
case "VARIABLE_COL":
|
|
7323
|
+
throw new Error(`internal error: unresolved SELECT variable @${col.name}`);
|
|
6975
7324
|
case "FIELD":
|
|
6976
7325
|
return col.alias ?? defaultFieldKeys.get(colIdx) ?? col.field;
|
|
6977
7326
|
case "LITERAL_COL":
|
|
@@ -7415,9 +7764,15 @@ function validateDmlCandidates(candidates, operation, payloadFields, targetField
|
|
|
7415
7764
|
candidate.record ??= {};
|
|
7416
7765
|
const rowErrors = includePreErrors ? [...candidate.preErrors] : [];
|
|
7417
7766
|
for (const code of targetFields) {
|
|
7767
|
+
if (!candidate.payload.has(code)) continue;
|
|
7418
7768
|
const result = validateAndNormalizeDmlValue(candidate.payload.get(code), infoByCode.get(code), numberPrecision);
|
|
7419
7769
|
if (!result.ok) rowErrors.push({ field: code, code: result.code, message: result.message });
|
|
7420
|
-
else
|
|
7770
|
+
else {
|
|
7771
|
+
const original = candidate.payload.get(code);
|
|
7772
|
+
const type = infoByCode.get(code).fieldType;
|
|
7773
|
+
const preserveCodes = ["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"].includes(type) && Array.isArray(original) && original.every((item) => typeof item === "object" && item !== null && "code" in item);
|
|
7774
|
+
candidate.record[code] = { value: preserveCodes ? original : result.value };
|
|
7775
|
+
}
|
|
7421
7776
|
}
|
|
7422
7777
|
if (validateMissingCreateFields && candidate.mode === "create") {
|
|
7423
7778
|
for (const info of fieldInfos) {
|
|
@@ -7486,6 +7841,11 @@ function renderValidationValue(value) {
|
|
|
7486
7841
|
return String(value);
|
|
7487
7842
|
}
|
|
7488
7843
|
|
|
7844
|
+
// src/core/existingRecordValidation.ts
|
|
7845
|
+
function renderExistingValidationValue(raw, fieldType) {
|
|
7846
|
+
return isEmptyDmlValue(raw) ? "" : renderValidationValue(normalizeRaw(raw, fieldType));
|
|
7847
|
+
}
|
|
7848
|
+
|
|
7489
7849
|
// src/core/optimization/whereCapability.ts
|
|
7490
7850
|
var RANGE_AND_EQUALITY = ["=", "!=", ">", "<", ">=", "<="];
|
|
7491
7851
|
var EQUALITY_IN = ["=", "!=", "in", "not in"];
|
|
@@ -7560,6 +7920,8 @@ function classifyWhereCapability(where, resolveField2) {
|
|
|
7560
7920
|
}
|
|
7561
7921
|
function classifyNode(where, resolveField2) {
|
|
7562
7922
|
switch (where.type) {
|
|
7923
|
+
case "BOOLEAN":
|
|
7924
|
+
return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
|
|
7563
7925
|
case "BINARY":
|
|
7564
7926
|
return classifyBinary(where.op, where.left, where.right.type, resolveField2);
|
|
7565
7927
|
case "NULL_CHECK":
|
|
@@ -7672,6 +8034,862 @@ function unsupported(code, field, fieldType, operator) {
|
|
|
7672
8034
|
return { capability: "UNSUPPORTED", reasons: [{ code, field, fieldType, operator }] };
|
|
7673
8035
|
}
|
|
7674
8036
|
|
|
8037
|
+
// src/import/sourceLoader.ts
|
|
8038
|
+
var IMPORT_MAX_BYTES = 10 * 1024 * 1024;
|
|
8039
|
+
var ImportSourceError = class extends Error {
|
|
8040
|
+
constructor(message) {
|
|
8041
|
+
super(`ImportSourceError: ${message}`);
|
|
8042
|
+
this.name = "ImportSourceError";
|
|
8043
|
+
}
|
|
8044
|
+
};
|
|
8045
|
+
function resolveImportSource(name, resolver) {
|
|
8046
|
+
if (!resolver) throw new ImportSourceError("IMPORT source capability is not available.");
|
|
8047
|
+
const handle = resolver(name);
|
|
8048
|
+
if (!handle) throw new ImportSourceError(`source "${name}" is not supplied.`);
|
|
8049
|
+
return handle;
|
|
8050
|
+
}
|
|
8051
|
+
async function loadImportSource(handle, cache) {
|
|
8052
|
+
let pending = cache.get(handle);
|
|
8053
|
+
if (!pending) {
|
|
8054
|
+
pending = handle.load().then((payload) => {
|
|
8055
|
+
if (!(payload.bytes instanceof Uint8Array)) throw new ImportSourceError("loader must return Uint8Array bytes.");
|
|
8056
|
+
if (payload.bytes.byteLength > IMPORT_MAX_BYTES) {
|
|
8057
|
+
throw new ImportSourceError(`source exceeds the ${IMPORT_MAX_BYTES} byte limit.`);
|
|
8058
|
+
}
|
|
8059
|
+
return payload;
|
|
8060
|
+
});
|
|
8061
|
+
cache.set(handle, pending);
|
|
8062
|
+
}
|
|
8063
|
+
return pending;
|
|
8064
|
+
}
|
|
8065
|
+
|
|
8066
|
+
// src/import/csvDecoder.ts
|
|
8067
|
+
function decodeImportText(bytes, encoding) {
|
|
8068
|
+
try {
|
|
8069
|
+
return new TextDecoder(encoding === "sjis" ? "shift_jis" : "utf-8", { fatal: true }).decode(bytes).replace(/^\uFEFF/, "");
|
|
8070
|
+
} catch {
|
|
8071
|
+
throw new ImportSourceError(`invalid ${encoding.toUpperCase()} byte sequence.`);
|
|
8072
|
+
}
|
|
8073
|
+
}
|
|
8074
|
+
function parseRfc4180(text) {
|
|
8075
|
+
const records = [];
|
|
8076
|
+
let record = [];
|
|
8077
|
+
let cell = "";
|
|
8078
|
+
let quoted = false;
|
|
8079
|
+
let afterQuote = false;
|
|
8080
|
+
let i = 0;
|
|
8081
|
+
const finishCell = () => {
|
|
8082
|
+
record.push(cell);
|
|
8083
|
+
cell = "";
|
|
8084
|
+
afterQuote = false;
|
|
8085
|
+
};
|
|
8086
|
+
const finishRecord = () => {
|
|
8087
|
+
finishCell();
|
|
8088
|
+
records.push(record);
|
|
8089
|
+
record = [];
|
|
8090
|
+
};
|
|
8091
|
+
while (i < text.length) {
|
|
8092
|
+
const ch = text[i];
|
|
8093
|
+
if (quoted) {
|
|
8094
|
+
if (ch === '"') {
|
|
8095
|
+
if (text[i + 1] === '"') {
|
|
8096
|
+
cell += '"';
|
|
8097
|
+
i += 2;
|
|
8098
|
+
continue;
|
|
8099
|
+
}
|
|
8100
|
+
quoted = false;
|
|
8101
|
+
afterQuote = true;
|
|
8102
|
+
i++;
|
|
8103
|
+
continue;
|
|
8104
|
+
}
|
|
8105
|
+
cell += ch;
|
|
8106
|
+
i++;
|
|
8107
|
+
continue;
|
|
8108
|
+
}
|
|
8109
|
+
if (afterQuote && ch !== "," && ch !== "\r" && ch !== "\n") {
|
|
8110
|
+
throw new ImportSourceError(`unexpected character after closing quote at offset ${i}.`);
|
|
8111
|
+
}
|
|
8112
|
+
if (ch === '"') {
|
|
8113
|
+
if (cell.length !== 0) throw new ImportSourceError(`quote in unquoted cell at offset ${i}.`);
|
|
8114
|
+
quoted = true;
|
|
8115
|
+
i++;
|
|
8116
|
+
continue;
|
|
8117
|
+
}
|
|
8118
|
+
if (ch === ",") {
|
|
8119
|
+
finishCell();
|
|
8120
|
+
i++;
|
|
8121
|
+
continue;
|
|
8122
|
+
}
|
|
8123
|
+
if (ch === "\r" || ch === "\n") {
|
|
8124
|
+
if (ch === "\r" && text[i + 1] === "\n") i++;
|
|
8125
|
+
finishRecord();
|
|
8126
|
+
i++;
|
|
8127
|
+
continue;
|
|
8128
|
+
}
|
|
8129
|
+
cell += ch;
|
|
8130
|
+
i++;
|
|
8131
|
+
}
|
|
8132
|
+
if (quoted) throw new ImportSourceError("unterminated quoted cell.");
|
|
8133
|
+
if (cell.length > 0 || record.length > 0 || afterQuote) finishRecord();
|
|
8134
|
+
return records;
|
|
8135
|
+
}
|
|
8136
|
+
function assertColumns(columns) {
|
|
8137
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8138
|
+
columns.forEach((column, index) => {
|
|
8139
|
+
if (column === "") throw new ImportSourceError(`CSV column ${index + 1} has an empty name.`);
|
|
8140
|
+
if (seen.has(column)) throw new ImportSourceError(`CSV column name "${column}" is duplicated.`);
|
|
8141
|
+
seen.add(column);
|
|
8142
|
+
});
|
|
8143
|
+
}
|
|
8144
|
+
function decodeCsv(bytes, options) {
|
|
8145
|
+
const records = parseRfc4180(decodeImportText(bytes, options.encoding));
|
|
8146
|
+
let columns;
|
|
8147
|
+
let rows;
|
|
8148
|
+
if (options.hasHeader) {
|
|
8149
|
+
columns = records[0] ?? [];
|
|
8150
|
+
rows = records.slice(1);
|
|
8151
|
+
} else {
|
|
8152
|
+
rows = records;
|
|
8153
|
+
columns = options.columns ? [...options.columns] : Array.from({ length: rows[0]?.length ?? 0 }, (_, i) => `c${i + 1}`);
|
|
8154
|
+
}
|
|
8155
|
+
assertColumns(columns);
|
|
8156
|
+
if (rows.length === 0) throw new ImportSourceError("CSV has no data rows.");
|
|
8157
|
+
rows.forEach((row, i) => {
|
|
8158
|
+
if (row.length !== columns.length) {
|
|
8159
|
+
throw new ImportSourceError(`CSV row ${i + (options.hasHeader ? 2 : 1)} has ${row.length} cells; expected ${columns.length}.`);
|
|
8160
|
+
}
|
|
8161
|
+
});
|
|
8162
|
+
return { columns, rows };
|
|
8163
|
+
}
|
|
8164
|
+
|
|
8165
|
+
// src/import/convertImportCsvValue.ts
|
|
8166
|
+
var LF_MULTI_TYPES = /* @__PURE__ */ new Set([
|
|
8167
|
+
"CHECK_BOX",
|
|
8168
|
+
"MULTI_SELECT",
|
|
8169
|
+
"USER_SELECT",
|
|
8170
|
+
"ORGANIZATION_SELECT",
|
|
8171
|
+
"GROUP_SELECT"
|
|
8172
|
+
]);
|
|
8173
|
+
var USER_TYPES2 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
8174
|
+
var ImportCsvValueError = class extends Error {
|
|
8175
|
+
constructor() {
|
|
8176
|
+
super("multiple-value CSV cell contains an empty LF-delimited item");
|
|
8177
|
+
this.code = "ERR_IMPORT_MULTI_EMPTY_ITEM";
|
|
8178
|
+
this.name = "ImportCsvValueError";
|
|
8179
|
+
}
|
|
8180
|
+
};
|
|
8181
|
+
function convertImportCsvValue(raw, type, options) {
|
|
8182
|
+
void options;
|
|
8183
|
+
if (!LF_MULTI_TYPES.has(type ?? "")) return raw;
|
|
8184
|
+
if (raw === "") return [];
|
|
8185
|
+
const items = raw.split(/\r\n|\n/);
|
|
8186
|
+
if (items.some((item) => item === "")) throw new ImportCsvValueError();
|
|
8187
|
+
return USER_TYPES2.has(type ?? "") ? items.map((code) => ({ code })) : items;
|
|
8188
|
+
}
|
|
8189
|
+
|
|
8190
|
+
// src/import/jsonTokenizer.ts
|
|
8191
|
+
function fail(message, offset, line, column) {
|
|
8192
|
+
throw new ImportSourceError(`JSON ${message} (offset=${offset}, line=${line}, column=${column}).`);
|
|
8193
|
+
}
|
|
8194
|
+
function decodeUtf8Json(bytes) {
|
|
8195
|
+
try {
|
|
8196
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
8197
|
+
} catch {
|
|
8198
|
+
throw new ImportSourceError("JSON source is not valid UTF-8.");
|
|
8199
|
+
}
|
|
8200
|
+
}
|
|
8201
|
+
function tokenizeJson(text) {
|
|
8202
|
+
const tokens = [];
|
|
8203
|
+
let i = 0, line = 1, column = 1;
|
|
8204
|
+
const advance = () => {
|
|
8205
|
+
const ch = text[i++];
|
|
8206
|
+
if (ch === "\n") {
|
|
8207
|
+
line++;
|
|
8208
|
+
column = 1;
|
|
8209
|
+
} else column++;
|
|
8210
|
+
return ch;
|
|
8211
|
+
};
|
|
8212
|
+
const position = () => ({ offset: i, line, column });
|
|
8213
|
+
while (i < text.length) {
|
|
8214
|
+
const ch = text[i];
|
|
8215
|
+
if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
|
|
8216
|
+
advance();
|
|
8217
|
+
continue;
|
|
8218
|
+
}
|
|
8219
|
+
const start = position();
|
|
8220
|
+
if ("{}[]:,".includes(ch)) {
|
|
8221
|
+
advance();
|
|
8222
|
+
tokens.push({ kind: "punct", value: ch, ...start });
|
|
8223
|
+
continue;
|
|
8224
|
+
}
|
|
8225
|
+
if (ch === '"') {
|
|
8226
|
+
advance();
|
|
8227
|
+
let value = "";
|
|
8228
|
+
let closed = false;
|
|
8229
|
+
while (i < text.length) {
|
|
8230
|
+
const c = advance();
|
|
8231
|
+
if (c === '"') {
|
|
8232
|
+
closed = true;
|
|
8233
|
+
break;
|
|
8234
|
+
}
|
|
8235
|
+
if (c.charCodeAt(0) < 32) fail("string contains an unescaped control character", start.offset, start.line, start.column);
|
|
8236
|
+
if (c !== "\\") {
|
|
8237
|
+
value += c;
|
|
8238
|
+
continue;
|
|
8239
|
+
}
|
|
8240
|
+
if (i >= text.length) fail("string has an unterminated escape", start.offset, start.line, start.column);
|
|
8241
|
+
const esc = advance();
|
|
8242
|
+
const simple = { '"': '"', "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: " " };
|
|
8243
|
+
if (esc in simple) {
|
|
8244
|
+
value += simple[esc];
|
|
8245
|
+
continue;
|
|
8246
|
+
}
|
|
8247
|
+
if (esc !== "u") fail(`has invalid escape \\${esc}`, i - 2, line, Math.max(1, column - 2));
|
|
8248
|
+
const hex = text.slice(i, i + 4);
|
|
8249
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) fail("has invalid unicode escape", i, line, column);
|
|
8250
|
+
for (let n = 0; n < 4; n++) advance();
|
|
8251
|
+
const code = Number.parseInt(hex, 16);
|
|
8252
|
+
if (code >= 55296 && code <= 56319) {
|
|
8253
|
+
if (text.slice(i, i + 2) !== "\\u" || !/^[0-9a-fA-F]{4}$/.test(text.slice(i + 2, i + 6))) fail("has an unpaired high surrogate", i, line, column);
|
|
8254
|
+
advance();
|
|
8255
|
+
advance();
|
|
8256
|
+
const lowHex = text.slice(i, i + 4);
|
|
8257
|
+
for (let n = 0; n < 4; n++) advance();
|
|
8258
|
+
const low = Number.parseInt(lowHex, 16);
|
|
8259
|
+
if (low < 56320 || low > 57343) fail("has an invalid surrogate pair", i - 4, line, Math.max(1, column - 4));
|
|
8260
|
+
value += String.fromCodePoint(65536 + (code - 55296 << 10) + low - 56320);
|
|
8261
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
8262
|
+
fail("has an unpaired low surrogate", i - 4, line, Math.max(1, column - 4));
|
|
8263
|
+
} else value += String.fromCharCode(code);
|
|
8264
|
+
}
|
|
8265
|
+
if (!closed) fail("string is unterminated", start.offset, start.line, start.column);
|
|
8266
|
+
tokens.push({ kind: "string", value, ...start });
|
|
8267
|
+
continue;
|
|
8268
|
+
}
|
|
8269
|
+
const rest = text.slice(i);
|
|
8270
|
+
const number = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(rest)?.[0];
|
|
8271
|
+
if (number) {
|
|
8272
|
+
for (let n = 0; n < number.length; n++) advance();
|
|
8273
|
+
tokens.push({ kind: "number", lexeme: number, ...start });
|
|
8274
|
+
continue;
|
|
8275
|
+
}
|
|
8276
|
+
const literal = /^(true|false|null)/.exec(rest)?.[0];
|
|
8277
|
+
if (literal) {
|
|
8278
|
+
for (let n = 0; n < literal.length; n++) advance();
|
|
8279
|
+
tokens.push({ kind: "literal", value: literal === "true" ? true : literal === "false" ? false : null, ...start });
|
|
8280
|
+
continue;
|
|
8281
|
+
}
|
|
8282
|
+
fail(`has an unexpected token ${JSON.stringify(ch)}`, start.offset, start.line, start.column);
|
|
8283
|
+
}
|
|
8284
|
+
tokens.push({ kind: "eof", offset: i, line, column });
|
|
8285
|
+
return tokens;
|
|
8286
|
+
}
|
|
8287
|
+
|
|
8288
|
+
// src/import/jsonDecoder.ts
|
|
8289
|
+
function describe(token) {
|
|
8290
|
+
return token.kind === "eof" ? "end of input" : token.kind === "punct" ? token.value : token.kind;
|
|
8291
|
+
}
|
|
8292
|
+
function decodeJsonRecords(bytes) {
|
|
8293
|
+
if (bytes.byteLength === 0) throw new ImportSourceError("JSON source is empty.");
|
|
8294
|
+
const tokens = tokenizeJson(decodeUtf8Json(bytes));
|
|
8295
|
+
let index = 0;
|
|
8296
|
+
const fail3 = (message, token = tokens[index]) => {
|
|
8297
|
+
throw new ImportSourceError(`JSON ${message} (offset=${token.offset}, line=${token.line}, column=${token.column}).`);
|
|
8298
|
+
};
|
|
8299
|
+
const isPunct = (token, value) => token.kind === "punct" && token.value === value;
|
|
8300
|
+
const punct = (value) => {
|
|
8301
|
+
const token = tokens[index];
|
|
8302
|
+
if (token.kind !== "punct" || token.value !== value) fail3(`expected ${value}; found ${describe(token)}`, token);
|
|
8303
|
+
index++;
|
|
8304
|
+
};
|
|
8305
|
+
const parseValue = () => {
|
|
8306
|
+
const token = tokens[index++];
|
|
8307
|
+
if (token.kind === "string") return token.value;
|
|
8308
|
+
if (token.kind === "number") return { kind: "number", lexeme: token.lexeme };
|
|
8309
|
+
if (token.kind === "literal") return token.value;
|
|
8310
|
+
if (token.kind === "punct" && token.value === "{") {
|
|
8311
|
+
const object = /* @__PURE__ */ new Map();
|
|
8312
|
+
if (isPunct(tokens[index], "}")) {
|
|
8313
|
+
index++;
|
|
8314
|
+
return object;
|
|
8315
|
+
}
|
|
8316
|
+
while (true) {
|
|
8317
|
+
const key = tokens[index++];
|
|
8318
|
+
if (key.kind !== "string") return fail3(`object key must be a string; found ${describe(key)}`, key);
|
|
8319
|
+
const keyValue = key.value;
|
|
8320
|
+
if (object.has(keyValue)) fail3(`duplicate key ${JSON.stringify(keyValue)}`, key);
|
|
8321
|
+
punct(":");
|
|
8322
|
+
object.set(keyValue, parseValue());
|
|
8323
|
+
const separator = tokens[index];
|
|
8324
|
+
if (isPunct(separator, "}")) {
|
|
8325
|
+
index++;
|
|
8326
|
+
break;
|
|
8327
|
+
}
|
|
8328
|
+
punct(",");
|
|
8329
|
+
}
|
|
8330
|
+
return object;
|
|
8331
|
+
}
|
|
8332
|
+
if (token.kind === "punct" && token.value === "[") {
|
|
8333
|
+
const array = [];
|
|
8334
|
+
if (isPunct(tokens[index], "]")) {
|
|
8335
|
+
index++;
|
|
8336
|
+
return array;
|
|
8337
|
+
}
|
|
8338
|
+
while (true) {
|
|
8339
|
+
array.push(parseValue());
|
|
8340
|
+
const separator = tokens[index];
|
|
8341
|
+
if (isPunct(separator, "]")) {
|
|
8342
|
+
index++;
|
|
8343
|
+
break;
|
|
8344
|
+
}
|
|
8345
|
+
punct(",");
|
|
8346
|
+
}
|
|
8347
|
+
return array;
|
|
8348
|
+
}
|
|
8349
|
+
return fail3(`expected a value; found ${describe(token)}`, token);
|
|
8350
|
+
};
|
|
8351
|
+
const root = parseValue();
|
|
8352
|
+
if (tokens[index].kind !== "eof") fail3(`has trailing data; found ${describe(tokens[index])}`);
|
|
8353
|
+
const records = root instanceof Map ? [root] : Array.isArray(root) ? root : fail3("root must be an object or array.", tokens[0]);
|
|
8354
|
+
if (records.length === 0) throw new ImportSourceError("JSON source contains no records.");
|
|
8355
|
+
records.forEach((record, i) => {
|
|
8356
|
+
if (!(record instanceof Map)) throw new ImportSourceError(`JSON record ${i + 1} must be an object.`);
|
|
8357
|
+
});
|
|
8358
|
+
return records;
|
|
8359
|
+
}
|
|
8360
|
+
|
|
8361
|
+
// src/import/jsonMaterializer.ts
|
|
8362
|
+
var STRING_ARRAY_TYPES = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
|
|
8363
|
+
var CODE_ARRAY_TYPES = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
8364
|
+
function fail2(row, field, message) {
|
|
8365
|
+
throw new ImportSourceError(`JSON field validation failed (row=${row}, field=${field}): ${message}`);
|
|
8366
|
+
}
|
|
8367
|
+
function isNumber(value) {
|
|
8368
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Map) && value.kind === "number";
|
|
8369
|
+
}
|
|
8370
|
+
function materializeValue(value, target, row) {
|
|
8371
|
+
if (value === null) return "";
|
|
8372
|
+
if (typeof value === "string") return value;
|
|
8373
|
+
if (typeof value === "boolean") fail2(row, target.code, "boolean is not accepted.");
|
|
8374
|
+
if (isNumber(value)) {
|
|
8375
|
+
if (target.fieldType === "NUMBER") fail2(row, target.code, "precision target requires a JSON string.");
|
|
8376
|
+
if (!/^-?(?:0|[1-9]\d*)$/.test(value.lexeme) || value.lexeme === "-0") {
|
|
8377
|
+
fail2(row, target.code, `JSON number ${value.lexeme} must be a non-negative-zero safe integer lexeme.`);
|
|
8378
|
+
}
|
|
8379
|
+
const number = Number(value.lexeme);
|
|
8380
|
+
if (!Number.isSafeInteger(number)) fail2(row, target.code, `JSON number ${value.lexeme} is outside the safe integer range.`);
|
|
8381
|
+
return String(number);
|
|
8382
|
+
}
|
|
8383
|
+
if (value instanceof Map) fail2(row, target.code, "object is not accepted for a flat field.");
|
|
8384
|
+
if (!Array.isArray(value)) fail2(row, target.code, "unsupported value type.");
|
|
8385
|
+
if (!STRING_ARRAY_TYPES.has(target.fieldType) && !CODE_ARRAY_TYPES.has(target.fieldType)) {
|
|
8386
|
+
fail2(row, target.code, "array is accepted only for multi-value fields.");
|
|
8387
|
+
}
|
|
8388
|
+
const strings = value.map((entry) => {
|
|
8389
|
+
if (typeof entry !== "string") fail2(row, target.code, "array elements must be strings.");
|
|
8390
|
+
return entry;
|
|
8391
|
+
});
|
|
8392
|
+
if (new Set(strings).size !== strings.length) fail2(row, target.code, "array elements must not contain duplicates.");
|
|
8393
|
+
return CODE_ARRAY_TYPES.has(target.fieldType) ? JSON.stringify(strings.map((code) => ({ code }))) : JSON.stringify(strings);
|
|
8394
|
+
}
|
|
8395
|
+
function materializeJsonDmlSource(_source, payload, targets, maxRows) {
|
|
8396
|
+
if (payload.encoding && payload.encoding !== "utf8") throw new ImportSourceError("JSON source is UTF-8 only.");
|
|
8397
|
+
const records = decodeJsonRecords(payload.bytes);
|
|
8398
|
+
if (records.length > maxRows) throw new ImportSourceError(`source rows (${records.length}) exceed maxRecords (${maxRows}).`);
|
|
8399
|
+
const targetByCode = new Map(targets.map((target) => [target.code, target]));
|
|
8400
|
+
if (targetByCode.size !== targets.length) throw new ImportSourceError("JSON target fields contain duplicates.");
|
|
8401
|
+
const rows = [];
|
|
8402
|
+
const importPresence = [];
|
|
8403
|
+
records.forEach((record, index) => {
|
|
8404
|
+
for (const key of record.keys()) {
|
|
8405
|
+
if (!targetByCode.has(key)) fail2(index + 1, key, "unknown key (not declared in INTO).");
|
|
8406
|
+
}
|
|
8407
|
+
const row = {};
|
|
8408
|
+
const present = /* @__PURE__ */ new Set();
|
|
8409
|
+
for (const target of targets) {
|
|
8410
|
+
if (!record.has(target.code)) continue;
|
|
8411
|
+
present.add(target.code);
|
|
8412
|
+
row[target.code] = materializeValue(record.get(target.code), target, index + 1);
|
|
8413
|
+
}
|
|
8414
|
+
rows.push(row);
|
|
8415
|
+
importPresence.push(present);
|
|
8416
|
+
});
|
|
8417
|
+
return {
|
|
8418
|
+
rows,
|
|
8419
|
+
columns: targets.map((target) => target.code),
|
|
8420
|
+
columnMeta: new Map(targets.map((target) => [target.code, { fieldType: target.fieldType }])),
|
|
8421
|
+
importPresence
|
|
8422
|
+
};
|
|
8423
|
+
}
|
|
8424
|
+
|
|
8425
|
+
// src/import/materializeDmlSource.ts
|
|
8426
|
+
function materializeCsvDmlSource(source, payload, maxRows, targetCodes, fieldInfos, recordNumberSourceHeader) {
|
|
8427
|
+
const decoded = decodeCsv(payload.bytes, {
|
|
8428
|
+
encoding: source.encoding ?? payload.encoding ?? "utf8",
|
|
8429
|
+
hasHeader: source.hasHeader,
|
|
8430
|
+
columns: source.columns
|
|
8431
|
+
});
|
|
8432
|
+
if (decoded.rows.length > maxRows) {
|
|
8433
|
+
throw new ImportSourceError(`source rows (${decoded.rows.length}) exceed maxRecords (${maxRows}).`);
|
|
8434
|
+
}
|
|
8435
|
+
if (source.mappingMode === "BY_NAME") {
|
|
8436
|
+
if (!targetCodes || !fieldInfos) throw new Error("InternalError: BY NAME requires destination form metadata.");
|
|
8437
|
+
if (new Set(targetCodes).size !== targetCodes.length) {
|
|
8438
|
+
throw new ImportSourceError("ERR_IMPORT_HEADER_REUSED: a BY NAME header cannot be consumed more than once.");
|
|
8439
|
+
}
|
|
8440
|
+
const indexes = new Map(decoded.columns.map((column, index) => [column, index]));
|
|
8441
|
+
for (const code of targetCodes) {
|
|
8442
|
+
if (!indexes.has(code)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${code}" is missing.`);
|
|
8443
|
+
}
|
|
8444
|
+
const infoByCode = new Map(fieldInfos.map((info) => [info.code, info]));
|
|
8445
|
+
const targetSet = new Set(targetCodes);
|
|
8446
|
+
if (recordNumberSourceHeader && targetSet.has(recordNumberSourceHeader)) {
|
|
8447
|
+
throw new ImportSourceError("ERR_IMPORT_HEADER_REUSED: record-number source header is lookup-only and cannot be a write target.");
|
|
8448
|
+
}
|
|
8449
|
+
if (recordNumberSourceHeader && !indexes.has(recordNumberSourceHeader)) {
|
|
8450
|
+
throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${recordNumberSourceHeader}" is missing.`);
|
|
8451
|
+
}
|
|
8452
|
+
const ignoredKnownColumns = [];
|
|
8453
|
+
const ignoredUnknownColumns = [];
|
|
8454
|
+
const nonEmpty = (index) => decoded.rows.filter((row) => row[index] !== "").length;
|
|
8455
|
+
const reasonFor = (info) => {
|
|
8456
|
+
if (info.fieldType === "FILE") return "FILE attachment is outside flat IMPORT scope";
|
|
8457
|
+
if (info.inSubtable || info.fieldType === "SUBTABLE") return "subtable field is not writable in Phase 3";
|
|
8458
|
+
if (info.writable === false) return `non-writable ${info.fieldType} field`;
|
|
8459
|
+
return `known export-only ${info.fieldType} field`;
|
|
8460
|
+
};
|
|
8461
|
+
for (const [index, column] of decoded.columns.entries()) {
|
|
8462
|
+
if (targetSet.has(column) || column === recordNumberSourceHeader) continue;
|
|
8463
|
+
const info = infoByCode.get(column);
|
|
8464
|
+
if (info) ignoredKnownColumns.push({ column, reason: reasonFor(info), nonEmptyCells: nonEmpty(index) });
|
|
8465
|
+
else if (!source.ignoreUnknownColumns) throw new ImportSourceError(`ERR_IMPORT_UNKNOWN_COLUMN: unknown CSV header "${column}".`);
|
|
8466
|
+
else ignoredUnknownColumns.push({ column, reason: "unknown column ignored by explicit policy", nonEmptyCells: nonEmpty(index) });
|
|
8467
|
+
}
|
|
8468
|
+
for (const code of targetCodes) {
|
|
8469
|
+
const info = infoByCode.get(code);
|
|
8470
|
+
if (!info) throw new Error(`ArgumentError: DML target field ${code} does not exist.`);
|
|
8471
|
+
if (info.inSubtable || info.writable === false || info.fieldType === "FILE" || info.fieldType === "SUBTABLE") {
|
|
8472
|
+
throw new Error(`ArgumentError: DML target field ${code} is not writable (${info.fieldType}).`);
|
|
8473
|
+
}
|
|
8474
|
+
}
|
|
8475
|
+
const importRowErrors = [];
|
|
8476
|
+
const rows2 = decoded.rows.map((values) => {
|
|
8477
|
+
const errors = [];
|
|
8478
|
+
const row = {};
|
|
8479
|
+
for (const code of targetCodes) {
|
|
8480
|
+
const raw = values[indexes.get(code)];
|
|
8481
|
+
try {
|
|
8482
|
+
row[code] = convertImportCsvValue(raw, infoByCode.get(code)?.fieldType, { cliKintone: true });
|
|
8483
|
+
} catch (error) {
|
|
8484
|
+
if (!(error instanceof ImportCsvValueError)) throw error;
|
|
8485
|
+
row[code] = raw;
|
|
8486
|
+
errors.push({ field: code, code: error.code, message: error.message });
|
|
8487
|
+
}
|
|
8488
|
+
}
|
|
8489
|
+
importRowErrors.push(errors);
|
|
8490
|
+
return row;
|
|
8491
|
+
});
|
|
8492
|
+
return {
|
|
8493
|
+
rows: rows2,
|
|
8494
|
+
columns: [...targetCodes],
|
|
8495
|
+
columnMeta: new Map(targetCodes.map((code) => [code, { fieldType: infoByCode.get(code)?.fieldType ?? "SINGLE_LINE_TEXT" }])),
|
|
8496
|
+
importRowErrors,
|
|
8497
|
+
...recordNumberSourceHeader ? { recordNumberSourceValues: decoded.rows.map((row) => row[indexes.get(recordNumberSourceHeader)]) } : {},
|
|
8498
|
+
importAudit: { mapping: "BY_NAME", writtenColumns: [...targetCodes], ignoredKnownColumns, ignoredUnknownColumns }
|
|
8499
|
+
};
|
|
8500
|
+
}
|
|
8501
|
+
const rows = decoded.rows.map((values) => Object.fromEntries(decoded.columns.map((column, i) => [column, values[i]])));
|
|
8502
|
+
return {
|
|
8503
|
+
rows,
|
|
8504
|
+
columns: decoded.columns,
|
|
8505
|
+
// CSV cells are decoded as strings; projections such as CAST may replace this metadata downstream.
|
|
8506
|
+
columnMeta: new Map(decoded.columns.map((column) => [column, { fieldType: "SINGLE_LINE_TEXT" }]))
|
|
8507
|
+
};
|
|
8508
|
+
}
|
|
8509
|
+
|
|
8510
|
+
// src/import/importRecordsMaterializer.ts
|
|
8511
|
+
var sourceFail = (parentRow, code, message) => {
|
|
8512
|
+
throw new ImportSourceError(`JSON subtable validation failed (parentRow=${parentRow}, field=${code}): ${message}`);
|
|
8513
|
+
};
|
|
8514
|
+
function materializeJsonImportRecords(_source, payload, targets, maxParents, maxChildRows = maxParents) {
|
|
8515
|
+
if (payload.encoding && payload.encoding !== "utf8") throw new ImportSourceError("JSON source is UTF-8 only.");
|
|
8516
|
+
const decoded = decodeJsonRecords(payload.bytes);
|
|
8517
|
+
if (decoded.length > maxParents) throw new ImportSourceError(`source parent rows (${decoded.length}) exceed maxRecords (${maxParents}).`);
|
|
8518
|
+
const targetByCode = new Map(targets.map((target) => [target.kind === "FIELD" ? target.field : target.subtableCode, target]));
|
|
8519
|
+
if (targetByCode.size !== targets.length) throw new ImportSourceError("IMPORT targets contain duplicates.");
|
|
8520
|
+
let childTotal = 0;
|
|
8521
|
+
return {
|
|
8522
|
+
records: decoded.map((record, index) => {
|
|
8523
|
+
const parentRow = index + 1;
|
|
8524
|
+
for (const code of record.keys()) if (!targetByCode.has(code)) sourceFail(parentRow, code, "unknown key (not declared in INTO).");
|
|
8525
|
+
const top = /* @__PURE__ */ new Map();
|
|
8526
|
+
const subtables = /* @__PURE__ */ new Map();
|
|
8527
|
+
const replacementTables = /* @__PURE__ */ new Set();
|
|
8528
|
+
for (const target of targets) {
|
|
8529
|
+
const code = target.kind === "FIELD" ? target.field : target.subtableCode;
|
|
8530
|
+
if (!record.has(code)) continue;
|
|
8531
|
+
const value = record.get(code);
|
|
8532
|
+
if (target.kind === "FIELD") {
|
|
8533
|
+
if (value instanceof Map) sourceFail(parentRow, code, "object is not accepted for a top-level field.");
|
|
8534
|
+
top.set(code, value);
|
|
8535
|
+
continue;
|
|
8536
|
+
}
|
|
8537
|
+
if (!Array.isArray(value)) sourceFail(parentRow, code, "subtable value must be an array.");
|
|
8538
|
+
replacementTables.add(code);
|
|
8539
|
+
const children = new Set(target.children);
|
|
8540
|
+
const rows = value.map((entry, childIndex) => {
|
|
8541
|
+
if (!(entry instanceof Map)) sourceFail(parentRow, code, `childRow=${childIndex + 1} must be an object.`);
|
|
8542
|
+
const child = entry;
|
|
8543
|
+
for (const childCode of child.keys()) {
|
|
8544
|
+
if (!children.has(childCode)) sourceFail(parentRow, childCode, `unknown child key in subtable ${code} at childRow=${childIndex + 1}.`);
|
|
8545
|
+
}
|
|
8546
|
+
childTotal++;
|
|
8547
|
+
if (childTotal > maxChildRows) throw new ImportSourceError(`source child rows (${childTotal}) exceed limit (${maxChildRows}).`);
|
|
8548
|
+
return { childRowNumber: childIndex + 1, values: child };
|
|
8549
|
+
});
|
|
8550
|
+
subtables.set(code, rows);
|
|
8551
|
+
}
|
|
8552
|
+
return { rowNumber: parentRow, top, subtables, replacementTables };
|
|
8553
|
+
})
|
|
8554
|
+
};
|
|
8555
|
+
}
|
|
8556
|
+
function materializeCliKintoneCsvImportRecords(source, payload, targets, replacementTables, maxParents, recordNumberSourceHeader) {
|
|
8557
|
+
const decoded = decodeCsv(payload.bytes, {
|
|
8558
|
+
encoding: source.encoding ?? payload.encoding ?? "utf8",
|
|
8559
|
+
hasHeader: source.hasHeader,
|
|
8560
|
+
columns: source.columns
|
|
8561
|
+
});
|
|
8562
|
+
if (!source.hasHeader || decoded.columns[0] !== "*") throw new ImportSourceError('ERR_IMPORT_MARKER: first CSV header must be "*".');
|
|
8563
|
+
const indexes = new Map(decoded.columns.map((code, index) => [code, index]));
|
|
8564
|
+
if (recordNumberSourceHeader && !indexes.has(recordNumberSourceHeader)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${recordNumberSourceHeader}" is missing.`);
|
|
8565
|
+
const fields = targets.filter((target) => target.kind === "FIELD");
|
|
8566
|
+
const tables = targets.filter((target) => target.kind === "SUBTABLE");
|
|
8567
|
+
for (const field of fields) if (!indexes.has(field.field)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${field.field}" is missing.`);
|
|
8568
|
+
for (const table of tables) {
|
|
8569
|
+
if (!table.rowIdSourceHeader || !indexes.has(table.rowIdSourceHeader)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: row-ID header for ${table.subtableCode} is missing.`);
|
|
8570
|
+
for (const child of table.children) if (!indexes.has(child)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required child header "${child}" is missing.`);
|
|
8571
|
+
}
|
|
8572
|
+
const records = [];
|
|
8573
|
+
let current;
|
|
8574
|
+
decoded.rows.forEach((cells, physicalIndex) => {
|
|
8575
|
+
const sourceRowNumber = physicalIndex + 2;
|
|
8576
|
+
const marker = cells[0];
|
|
8577
|
+
if (marker !== "" && marker !== "*") throw new ImportSourceError(`ERR_IMPORT_MARKER: invalid marker ${JSON.stringify(marker)} at source row ${sourceRowNumber}.`);
|
|
8578
|
+
if (marker === "*") {
|
|
8579
|
+
if (records.length >= maxParents) throw new ImportSourceError(`source parent rows exceed maxRecords (${maxParents}).`);
|
|
8580
|
+
current = {
|
|
8581
|
+
rowNumber: records.length + 1,
|
|
8582
|
+
markerRowNumber: sourceRowNumber,
|
|
8583
|
+
top: new Map(fields.map((field) => [field.field, cells[indexes.get(field.field)]])),
|
|
8584
|
+
subtables: new Map(tables.map((table) => [table.subtableCode, []])),
|
|
8585
|
+
replacementTables: new Set(replacementTables),
|
|
8586
|
+
...recordNumberSourceHeader ? { recordNumberSourceValue: cells[indexes.get(recordNumberSourceHeader)] } : {}
|
|
8587
|
+
};
|
|
8588
|
+
records.push(current);
|
|
8589
|
+
} else if (!current) {
|
|
8590
|
+
throw new ImportSourceError(`ERR_IMPORT_MARKER: first data row must start a parent (source row ${sourceRowNumber}).`);
|
|
8591
|
+
} else {
|
|
8592
|
+
for (const field of fields) {
|
|
8593
|
+
const continuationValue = cells[indexes.get(field.field)];
|
|
8594
|
+
if (continuationValue !== "" && continuationValue !== current.top.get(field.field)) {
|
|
8595
|
+
throw new ImportSourceError(`ERR_IMPORT_PARENT_VALUE_ON_CONTINUATION: ${field.field} at source row ${sourceRowNumber}.`);
|
|
8596
|
+
}
|
|
8597
|
+
}
|
|
8598
|
+
if (recordNumberSourceHeader) {
|
|
8599
|
+
const continuationValue = cells[indexes.get(recordNumberSourceHeader)];
|
|
8600
|
+
if (continuationValue !== "" && continuationValue !== current.recordNumberSourceValue) {
|
|
8601
|
+
throw new ImportSourceError(`ERR_IMPORT_PARENT_VALUE_ON_CONTINUATION: ${recordNumberSourceHeader} at source row ${sourceRowNumber}.`);
|
|
8602
|
+
}
|
|
8603
|
+
}
|
|
8604
|
+
}
|
|
8605
|
+
for (const table of tables) {
|
|
8606
|
+
const rowId = cells[indexes.get(table.rowIdSourceHeader)];
|
|
8607
|
+
const values = new Map(table.children.map((child) => [child, cells[indexes.get(child)]]));
|
|
8608
|
+
if (rowId === "" && [...values.values()].every((value) => value === "")) continue;
|
|
8609
|
+
const rows = current.subtables.get(table.subtableCode);
|
|
8610
|
+
rows.push({ childRowNumber: rows.length + 1, sourceRowNumber, ...rowId ? { rowId } : {}, values });
|
|
8611
|
+
}
|
|
8612
|
+
});
|
|
8613
|
+
if (records.length === 0) throw new ImportSourceError("CSV has no parent rows.");
|
|
8614
|
+
return { records };
|
|
8615
|
+
}
|
|
8616
|
+
|
|
8617
|
+
// src/import/importRecordValidation.ts
|
|
8618
|
+
var USER_TYPES3 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
8619
|
+
var UNSUPPORTED_CHILD_TYPES = /* @__PURE__ */ new Set(["SUBTABLE", "FILE", "CALC", "RECORD_NUMBER", "CREATOR", "CREATED_TIME", "MODIFIER", "UPDATED_TIME", "STATUS", "STATUS_ASSIGNEE", "CATEGORY", "REFERENCE_TABLE"]);
|
|
8620
|
+
function assertImportRejectLimit(prepared, rejectLimit) {
|
|
8621
|
+
if (rejectLimit != null && prepared.invalidParentRows.size > rejectLimit) {
|
|
8622
|
+
throw new Error(`RejectLimitExceededError: rejected parents (${prepared.invalidParentRows.size}) exceed REJECT LIMIT (${rejectLimit}).`);
|
|
8623
|
+
}
|
|
8624
|
+
}
|
|
8625
|
+
function prepareImportRecords(materialized, targets, fieldInfos, numberPrecision, operation) {
|
|
8626
|
+
const topInfos = new Map(fieldInfos.filter((f) => !f.inSubtable).map((f) => [f.code, f]));
|
|
8627
|
+
const scoped = /* @__PURE__ */ new Map();
|
|
8628
|
+
for (const info of fieldInfos) if (info.inSubtable && info.subtableCode) {
|
|
8629
|
+
let children = scoped.get(info.subtableCode);
|
|
8630
|
+
if (!children) scoped.set(info.subtableCode, children = /* @__PURE__ */ new Map());
|
|
8631
|
+
children.set(info.code, info);
|
|
8632
|
+
}
|
|
8633
|
+
const targetTop = targets.filter((t) => t.kind === "FIELD");
|
|
8634
|
+
const targetTables = targets.filter((t) => t.kind === "SUBTABLE");
|
|
8635
|
+
for (const target of targetTop) assertWritable(target.field, topInfos.get(target.field), void 0);
|
|
8636
|
+
for (const target of targetTables) {
|
|
8637
|
+
const table = topInfos.get(target.subtableCode);
|
|
8638
|
+
if (!table || table.fieldType !== "SUBTABLE") throw new Error(`ArgumentError: IMPORT subtable ${target.subtableCode} does not exist.`);
|
|
8639
|
+
const children = scoped.get(target.subtableCode) ?? /* @__PURE__ */ new Map();
|
|
8640
|
+
for (const child of target.children) assertWritable(child, children.get(child), target.subtableCode);
|
|
8641
|
+
}
|
|
8642
|
+
const tableCounts = new Map(targetTables.map((t) => [t.subtableCode, { parentsPresent: 0, childRows: 0, validChildRows: 0, invalidChildRows: 0 }]));
|
|
8643
|
+
const parents = materialized.records.map((record) => validateParent(record, targetTop, targetTables, topInfos, scoped, numberPrecision, operation, tableCounts));
|
|
8644
|
+
const errors = parents.flatMap((parent) => [...parent.errors]);
|
|
8645
|
+
return { parents, errors, invalidParentRows: new Set(parents.filter((p) => !p.valid).map((p) => p.parentRow)), tableCounts };
|
|
8646
|
+
}
|
|
8647
|
+
function validateParent(source, topTargets, tableTargets, topInfos, scoped, precision, operation, tableCounts) {
|
|
8648
|
+
const errors = [];
|
|
8649
|
+
const top = {};
|
|
8650
|
+
for (const target of topTargets) {
|
|
8651
|
+
if (!source.top.has(target.field)) continue;
|
|
8652
|
+
validateValue(source.top.get(target.field), topInfos.get(target.field), precision, top, target.field, errors, location(source, operation, target.field));
|
|
8653
|
+
}
|
|
8654
|
+
const createValidationOnly = {};
|
|
8655
|
+
if (operation === "INSERT") for (const info of topInfos.values()) {
|
|
8656
|
+
if (info.fieldType === "SUBTABLE" || info.writable === false || source.top.has(info.code)) continue;
|
|
8657
|
+
validateMissing(info, precision, createValidationOnly, errors, location(source, operation, info.code));
|
|
8658
|
+
}
|
|
8659
|
+
const subtables = /* @__PURE__ */ new Map();
|
|
8660
|
+
for (const target of tableTargets) {
|
|
8661
|
+
if (!source.subtables.has(target.subtableCode)) continue;
|
|
8662
|
+
const count = tableCounts.get(target.subtableCode);
|
|
8663
|
+
count.parentsPresent++;
|
|
8664
|
+
const preparedRows = [];
|
|
8665
|
+
for (const child of source.subtables.get(target.subtableCode)) {
|
|
8666
|
+
count.childRows++;
|
|
8667
|
+
const before = errors.length;
|
|
8668
|
+
const record = {};
|
|
8669
|
+
const infos = scoped.get(target.subtableCode);
|
|
8670
|
+
for (const code of target.children) {
|
|
8671
|
+
const info = infos.get(code);
|
|
8672
|
+
const loc = location(source, operation, code, target.subtableCode, child.childRowNumber, child.sourceRowNumber ?? source.markerRowNumber);
|
|
8673
|
+
if (child.values.has(code)) validateValue(child.values.get(code), info, precision, record, code, errors, loc);
|
|
8674
|
+
else validateMissing(info, precision, record, errors, loc);
|
|
8675
|
+
}
|
|
8676
|
+
if (errors.length === before) {
|
|
8677
|
+
count.validChildRows++;
|
|
8678
|
+
preparedRows.push(record);
|
|
8679
|
+
} else count.invalidChildRows++;
|
|
8680
|
+
}
|
|
8681
|
+
subtables.set(target.subtableCode, preparedRows);
|
|
8682
|
+
}
|
|
8683
|
+
return { parentRow: source.rowNumber, valid: errors.length === 0, top, subtables, replacementTables: source.replacementTables, errors };
|
|
8684
|
+
}
|
|
8685
|
+
function assertWritable(code, info, table) {
|
|
8686
|
+
if (!info) throw new Error(table ? `ArgumentError: IMPORT child ${code} does not belong to subtable ${table}.` : `ArgumentError: IMPORT top-level field ${code} does not exist.`);
|
|
8687
|
+
if (info.writable === false || table && UNSUPPORTED_CHILD_TYPES.has(info.fieldType)) {
|
|
8688
|
+
throw new Error(`ArgumentError: IMPORT ${table ? `child ${table}.${code}` : `field ${code}`} is not writable (${info.fieldType}).`);
|
|
8689
|
+
}
|
|
8690
|
+
}
|
|
8691
|
+
function validateMissing(info, precision, record, errors, loc) {
|
|
8692
|
+
const raw = isEmptyDmlValue(info.defaultValue) ? "" : info.defaultValue;
|
|
8693
|
+
validateValue(raw, info, precision, record, info.code, errors, loc, !isEmptyDmlValue(info.defaultValue));
|
|
8694
|
+
}
|
|
8695
|
+
function validateValue(raw, info, precision, record, code, errors, loc, isDefault = false) {
|
|
8696
|
+
const normalizedRaw = decodeRaw(raw);
|
|
8697
|
+
const result = validateAndNormalizeDmlValue(normalizedRaw, info, precision);
|
|
8698
|
+
if (!result.ok) errors.push({ ...loc, code: result.code, message: isDefault ? `\u65E2\u5B9A\u5024: ${result.message}` : result.message });
|
|
8699
|
+
else record[code] = { value: preserveUserCodes(normalizedRaw, info) ? normalizedRaw : result.value };
|
|
8700
|
+
}
|
|
8701
|
+
function decodeRaw(raw) {
|
|
8702
|
+
if (isJsonNumber(raw)) return raw.lexeme;
|
|
8703
|
+
if (Array.isArray(raw)) return raw.map((value) => value instanceof Map ? value : isJsonNumber(value) ? value.lexeme : value);
|
|
8704
|
+
return raw;
|
|
8705
|
+
}
|
|
8706
|
+
function isJsonNumber(raw) {
|
|
8707
|
+
return typeof raw === "object" && raw !== null && raw.kind === "number";
|
|
8708
|
+
}
|
|
8709
|
+
function preserveUserCodes(raw, info) {
|
|
8710
|
+
return USER_TYPES3.has(info.fieldType) && Array.isArray(raw) && raw.every((v) => typeof v === "object" && v !== null && "code" in v);
|
|
8711
|
+
}
|
|
8712
|
+
function location(source, operation, field, subtable, subrow, sourceRow) {
|
|
8713
|
+
const physicalRow = sourceRow ?? source.markerRowNumber;
|
|
8714
|
+
return { operation, parentRow: source.rowNumber, field, ...subtable ? { subtable } : {}, ...subrow == null ? {} : { subrow }, ...physicalRow == null ? {} : { sourceRow: physicalRow }, sourceValues: subtable ? source.subtables.get(subtable)?.[subrow - 1]?.values ?? /* @__PURE__ */ new Map() : source.top };
|
|
8715
|
+
}
|
|
8716
|
+
|
|
8717
|
+
// src/import/importErrors.ts
|
|
8718
|
+
var IMPORT_VALIDATION_META_COLUMNS = [
|
|
8719
|
+
"$err_statement",
|
|
8720
|
+
"$err_operation",
|
|
8721
|
+
"$err_row",
|
|
8722
|
+
"$err_field",
|
|
8723
|
+
"$err_subtable",
|
|
8724
|
+
"$err_subrow",
|
|
8725
|
+
"$err_source_row",
|
|
8726
|
+
"$err_code",
|
|
8727
|
+
"$err_message"
|
|
8728
|
+
];
|
|
8729
|
+
function materializeImportValidationErrors(errors, payloadFields, statementNumber = 1) {
|
|
8730
|
+
return errors.map((error) => {
|
|
8731
|
+
const row = {};
|
|
8732
|
+
for (const field of payloadFields) row[field] = error.sourceValues.get(field) == null ? "" : render(error.sourceValues.get(field));
|
|
8733
|
+
row["$err_statement"] = String(statementNumber);
|
|
8734
|
+
row["$err_operation"] = error.operation;
|
|
8735
|
+
row["$err_row"] = String(error.parentRow);
|
|
8736
|
+
row["$err_field"] = error.field;
|
|
8737
|
+
row["$err_subtable"] = error.subtable ?? "";
|
|
8738
|
+
row["$err_subrow"] = error.subrow == null ? "" : String(error.subrow);
|
|
8739
|
+
row["$err_source_row"] = error.sourceRow == null ? null : String(error.sourceRow);
|
|
8740
|
+
row["$err_code"] = error.code;
|
|
8741
|
+
row["$err_message"] = error.message;
|
|
8742
|
+
return row;
|
|
8743
|
+
});
|
|
8744
|
+
}
|
|
8745
|
+
function render(value) {
|
|
8746
|
+
if (value === null || value === void 0) return "";
|
|
8747
|
+
if (typeof value === "object" && value !== null && "kind" in value && "lexeme" in value && value.kind === "number") {
|
|
8748
|
+
return String(value.lexeme);
|
|
8749
|
+
}
|
|
8750
|
+
if (Array.isArray(value)) return JSON.stringify(value);
|
|
8751
|
+
return String(value);
|
|
8752
|
+
}
|
|
8753
|
+
|
|
8754
|
+
// src/import/subtablePayload.ts
|
|
8755
|
+
function buildImportRecordPayload(top, subtables, rowIdMode) {
|
|
8756
|
+
const record = {};
|
|
8757
|
+
for (const [code, value] of top) record[code] = { value };
|
|
8758
|
+
for (const [tableCode, sourceRows] of subtables) {
|
|
8759
|
+
record[tableCode] = {
|
|
8760
|
+
value: sourceRows.map((sourceRow) => ({
|
|
8761
|
+
...rowIdMode === "PRESERVE" && sourceRow.rowId ? { id: sourceRow.rowId } : {},
|
|
8762
|
+
value: Object.fromEntries([...sourceRow.values].map(([childCode, value]) => [childCode, { value }]))
|
|
8763
|
+
}))
|
|
8764
|
+
};
|
|
8765
|
+
}
|
|
8766
|
+
return record;
|
|
8767
|
+
}
|
|
8768
|
+
function buildJsonImportRecordPayload(top, subtables) {
|
|
8769
|
+
return buildImportRecordPayload(top, subtables, "DROP");
|
|
8770
|
+
}
|
|
8771
|
+
|
|
8772
|
+
// src/import/jsonSubtableWritePlan.ts
|
|
8773
|
+
function assertJsonImportHasNoRowIds(materialized) {
|
|
8774
|
+
for (const parent of materialized.records) for (const [table, rows] of parent.subtables) {
|
|
8775
|
+
for (const row of rows) {
|
|
8776
|
+
if (row.rowId !== void 0 || row.values.has("_rid") || row.values.has("id")) {
|
|
8777
|
+
throw new Error(`ArgumentError: JSON IMPORT subtable ${table} does not accept _rid/id; rows are always newly numbered.`);
|
|
8778
|
+
}
|
|
8779
|
+
}
|
|
8780
|
+
}
|
|
8781
|
+
}
|
|
8782
|
+
function buildJsonSubtableWritePlan(parents, targetIds, existingById) {
|
|
8783
|
+
return parents.map((parent, index) => {
|
|
8784
|
+
const targetId = targetIds[index];
|
|
8785
|
+
const existing = targetId === void 0 ? void 0 : existingById.get(targetId);
|
|
8786
|
+
if (targetId !== void 0 && !existing) throw new Error(`InternalError: IMPORT UPSERT target APP record ${targetId} was not loaded.`);
|
|
8787
|
+
const tables = [...parent.subtables].map(([table, input]) => {
|
|
8788
|
+
const raw = existing?.record[table]?.value;
|
|
8789
|
+
const existingRows = Array.isArray(raw) ? raw.length : 0;
|
|
8790
|
+
return { table, existingRows, inputRows: input.length, addRows: input.length, deleteRows: existingRows };
|
|
8791
|
+
});
|
|
8792
|
+
return {
|
|
8793
|
+
parentRow: parent.parentRow,
|
|
8794
|
+
mode: targetId === void 0 ? "INSERT" : "UPDATE",
|
|
8795
|
+
...targetId === void 0 ? {} : { targetId, revision: existing?.revision },
|
|
8796
|
+
top: parent.top,
|
|
8797
|
+
subtables: parent.subtables,
|
|
8798
|
+
tables
|
|
8799
|
+
};
|
|
8800
|
+
});
|
|
8801
|
+
}
|
|
8802
|
+
|
|
8803
|
+
// src/import/subtableReplacementPlan.ts
|
|
8804
|
+
function tableRows(record, table) {
|
|
8805
|
+
const raw = record[table]?.value;
|
|
8806
|
+
return Array.isArray(raw) ? raw : [];
|
|
8807
|
+
}
|
|
8808
|
+
function assertNoDuplicateCsvSubtableRowIds(records) {
|
|
8809
|
+
const seen = /* @__PURE__ */ new Map();
|
|
8810
|
+
for (const parent of records) for (const [table, rows] of parent.subtables) for (const row of rows) {
|
|
8811
|
+
if (!row.rowId) continue;
|
|
8812
|
+
const key = `${table}\0${row.rowId}`;
|
|
8813
|
+
if (seen.has(key)) throw new Error(`ERR_SUBTABLE_ROW_ID_DUP_SOURCE: duplicate row ID ${row.rowId} in ${table}`);
|
|
8814
|
+
seen.set(key, parent.rowNumber);
|
|
8815
|
+
}
|
|
8816
|
+
}
|
|
8817
|
+
function buildCsvSubtableReplacementPlan(sources, prepared, targetIds, existingById, ownership) {
|
|
8818
|
+
return prepared.map((parent, index) => {
|
|
8819
|
+
const source = sources[index];
|
|
8820
|
+
const targetId = targetIds[index];
|
|
8821
|
+
const existing = targetId === void 0 ? void 0 : existingById.get(targetId);
|
|
8822
|
+
const errors = [...parent.errors];
|
|
8823
|
+
if (!existing || targetId === void 0) return { parentRow: parent.parentRow, targetId: targetId ?? 0, valid: false, top: parent.top, subtables: /* @__PURE__ */ new Map(), tables: [], errors };
|
|
8824
|
+
const subtables = /* @__PURE__ */ new Map();
|
|
8825
|
+
const tables = [];
|
|
8826
|
+
for (const table of parent.replacementTables) {
|
|
8827
|
+
const current = tableRows(existing.record, table);
|
|
8828
|
+
const currentIds = new Set(current.map((row) => row.id).filter((id) => !!id));
|
|
8829
|
+
const input = source.subtables.get(table) ?? [];
|
|
8830
|
+
const normalized = parent.subtables.get(table) ?? [];
|
|
8831
|
+
let updateRows = 0, addRows = 0, rowIdNotFound = 0;
|
|
8832
|
+
const payloadRows = input.map((row, rowIndex) => {
|
|
8833
|
+
const normalizedRecord = normalized[rowIndex] ?? {};
|
|
8834
|
+
if (row.rowId && currentIds.has(row.rowId)) {
|
|
8835
|
+
updateRows++;
|
|
8836
|
+
return { rowId: row.rowId, record: normalizedRecord };
|
|
8837
|
+
}
|
|
8838
|
+
if (row.rowId) {
|
|
8839
|
+
const owners = ownership.get(row.rowId) ?? [];
|
|
8840
|
+
if (owners.some((owner) => owner.parentId !== targetId || owner.table !== table)) errors.push({
|
|
8841
|
+
operation: "UPDATE",
|
|
8842
|
+
parentRow: parent.parentRow,
|
|
8843
|
+
field: row.rowId,
|
|
8844
|
+
subtable: table,
|
|
8845
|
+
subrow: row.childRowNumber,
|
|
8846
|
+
sourceRow: row.sourceRowNumber,
|
|
8847
|
+
code: "ERR_IMPORT_FIELD_OWNERSHIP",
|
|
8848
|
+
message: `rowIdOwnedElsewhere: ${row.rowId}`,
|
|
8849
|
+
sourceValues: row.values
|
|
8850
|
+
});
|
|
8851
|
+
rowIdNotFound++;
|
|
8852
|
+
}
|
|
8853
|
+
addRows++;
|
|
8854
|
+
return { record: normalizedRecord };
|
|
8855
|
+
});
|
|
8856
|
+
subtables.set(table, payloadRows);
|
|
8857
|
+
tables.push({ table, existingRows: current.length, inputRows: input.length, updateRows, addRows, deleteRows: current.length - updateRows, rowIdNotFound });
|
|
8858
|
+
}
|
|
8859
|
+
return { parentRow: parent.parentRow, targetId, ...existing.revision === void 0 ? {} : { revision: existing.revision }, valid: errors.length === 0, top: parent.top, subtables, tables, errors };
|
|
8860
|
+
});
|
|
8861
|
+
}
|
|
8862
|
+
|
|
8863
|
+
// src/import/importProjection.ts
|
|
8864
|
+
var IMPORT_PROJECTION_SOURCE = "#__import_source";
|
|
8865
|
+
function bindImportProjection(projection) {
|
|
8866
|
+
return { ...projection, from: { appId: 0, alias: null, cteName: IMPORT_PROJECTION_SOURCE } };
|
|
8867
|
+
}
|
|
8868
|
+
|
|
8869
|
+
// src/import/recordNumberUpdate.ts
|
|
8870
|
+
function normalizeImportRecordNumber(raw) {
|
|
8871
|
+
return /^[0-9]+$/.test(raw) ? raw.replace(/^0+(?=\d)/, "") : null;
|
|
8872
|
+
}
|
|
8873
|
+
function preflightImportRecordNumbers(values, header) {
|
|
8874
|
+
const normalized = values.map(normalizeImportRecordNumber);
|
|
8875
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8876
|
+
for (const key of normalized) {
|
|
8877
|
+
if (key === null) continue;
|
|
8878
|
+
if (seen.has(key)) {
|
|
8879
|
+
throw new Error("ERR_RECORD_NUMBER_DUP_SOURCE: source contains a duplicate record number");
|
|
8880
|
+
}
|
|
8881
|
+
seen.add(key);
|
|
8882
|
+
}
|
|
8883
|
+
return {
|
|
8884
|
+
normalized,
|
|
8885
|
+
errors: normalized.map((key) => key === null ? [{
|
|
8886
|
+
field: header,
|
|
8887
|
+
code: "ERR_RECORD_NUMBER_INVALID",
|
|
8888
|
+
message: `${header} must be a non-empty ASCII decimal record number`
|
|
8889
|
+
}] : [])
|
|
8890
|
+
};
|
|
8891
|
+
}
|
|
8892
|
+
|
|
7675
8893
|
// src/execute.ts
|
|
7676
8894
|
var SEARCH_ABORTED_WARNING = "\u691C\u7D22\u304C 10 \u4E07\u4EF6\u3067\u6253\u3061\u5207\u3089\u308C\u3001\u7D50\u679C\u304C\u6B20\u843D\u3057\u305F\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002";
|
|
7677
8895
|
var SearchAbortedError = class extends Error {
|
|
@@ -7682,6 +8900,7 @@ var SearchAbortedError = class extends Error {
|
|
|
7682
8900
|
};
|
|
7683
8901
|
var materializedMetaBySelectResult = /* @__PURE__ */ new WeakMap();
|
|
7684
8902
|
var materializedMetaByValidationResult = /* @__PURE__ */ new WeakMap();
|
|
8903
|
+
var importSourceByDmlStatement = /* @__PURE__ */ new WeakMap();
|
|
7685
8904
|
var defaultCacheContextByClient = /* @__PURE__ */ new WeakMap();
|
|
7686
8905
|
var nextDefaultCacheContextId = 1;
|
|
7687
8906
|
function resolveCacheContext(client, explicit) {
|
|
@@ -7696,7 +8915,7 @@ function resolveCacheContext(client, explicit) {
|
|
|
7696
8915
|
async function execute(sql, client, options = {}) {
|
|
7697
8916
|
const startedAt = Date.now();
|
|
7698
8917
|
const cacheContext = resolveCacheContext(client, options.cacheContext);
|
|
7699
|
-
const stmt = parseSql(sql);
|
|
8918
|
+
const stmt = parseSql(sql, options.enableImport === true);
|
|
7700
8919
|
const metrics = createEmptyMetrics();
|
|
7701
8920
|
const countedClient = wrapClientWithMetrics(client, metrics);
|
|
7702
8921
|
const collector = { aborted: false };
|
|
@@ -7876,6 +9095,7 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
|
|
|
7876
9095
|
throw new Error(`ParseError: variable @${unresolved} is not defined in a batch.`);
|
|
7877
9096
|
}
|
|
7878
9097
|
validateKlikeStatement(stmt);
|
|
9098
|
+
if (stmt.type === "IMPORT") return executeImport(stmt, client, options, cacheContext);
|
|
7879
9099
|
if ("validateOnly" in stmt && stmt.validateOnly === true) {
|
|
7880
9100
|
if (stmt.validationErrorTable) {
|
|
7881
9101
|
throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
@@ -7886,6 +9106,8 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
|
|
|
7886
9106
|
throw new Error("ArgumentError: ON ERROR SKIP requires a batch.");
|
|
7887
9107
|
}
|
|
7888
9108
|
switch (stmt.type) {
|
|
9109
|
+
case "VALIDATE":
|
|
9110
|
+
return executeExistingRecordValidation(stmt, client, options, cacheContext);
|
|
7889
9111
|
case "SELECT":
|
|
7890
9112
|
return executeSelect(stmt, client, options, cacheContext);
|
|
7891
9113
|
case "UNION":
|
|
@@ -7931,6 +9153,144 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
|
|
|
7931
9153
|
return executeAssert(stmt, client, options, cacheContext);
|
|
7932
9154
|
}
|
|
7933
9155
|
}
|
|
9156
|
+
var EXISTING_VALIDATION_COLUMNS = ["$id", "$err_field", "$err_code", "$err_message", "$err_value"];
|
|
9157
|
+
function hasAuditableConstraint(field) {
|
|
9158
|
+
return field.required === true || field.minValue !== void 0 || field.maxValue !== void 0 || field.minLength !== void 0 || field.maxLength !== void 0 || field.optionOrder !== void 0;
|
|
9159
|
+
}
|
|
9160
|
+
function resolveExistingValidationTargets(stmt, fieldInfos) {
|
|
9161
|
+
const byCode = new Map(fieldInfos.map((field) => [field.code, field]));
|
|
9162
|
+
const auditable = (field) => !field.inSubtable && (field.fieldType === "NUMBER" || hasAuditableConstraint(field));
|
|
9163
|
+
if (stmt.fields === void 0) return fieldInfos.filter(auditable);
|
|
9164
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9165
|
+
return stmt.fields.map((code) => {
|
|
9166
|
+
if (seen.has(code)) throw new Error(`ArgumentError: VALIDATE field ${code} is duplicated.`);
|
|
9167
|
+
seen.add(code);
|
|
9168
|
+
if (code === "$id") throw new Error("ArgumentError: VALIDATE cannot audit system field $id.");
|
|
9169
|
+
const info = byCode.get(code);
|
|
9170
|
+
if (!info) throw new Error(`ArgumentError: VALIDATE field ${code} does not exist.`);
|
|
9171
|
+
if (info.inSubtable) throw new Error(`ArgumentError: VALIDATE field ${code} is a subtable child field.`);
|
|
9172
|
+
if (!auditable(info)) throw new Error(`ArgumentError: VALIDATE field ${code} has no auditable constraint.`);
|
|
9173
|
+
return info;
|
|
9174
|
+
});
|
|
9175
|
+
}
|
|
9176
|
+
function collectValidateWhereFields(where) {
|
|
9177
|
+
const fields = [];
|
|
9178
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9179
|
+
const add = (field) => {
|
|
9180
|
+
if (!seen.has(field)) {
|
|
9181
|
+
seen.add(field);
|
|
9182
|
+
fields.push(field);
|
|
9183
|
+
}
|
|
9184
|
+
};
|
|
9185
|
+
const visit = (node) => {
|
|
9186
|
+
if (Array.isArray(node)) {
|
|
9187
|
+
node.forEach(visit);
|
|
9188
|
+
return;
|
|
9189
|
+
}
|
|
9190
|
+
if (node === null || typeof node !== "object") return;
|
|
9191
|
+
const obj = node;
|
|
9192
|
+
if (obj.type === "FIELD" && typeof obj.field === "string") add(obj.field);
|
|
9193
|
+
if (obj.type === "FIELD_REF" && typeof obj.field === "string") add(obj.field);
|
|
9194
|
+
Object.values(obj).forEach(visit);
|
|
9195
|
+
};
|
|
9196
|
+
visit(where);
|
|
9197
|
+
return fields;
|
|
9198
|
+
}
|
|
9199
|
+
function existingValidationColumnMeta() {
|
|
9200
|
+
return new Map(EXISTING_VALIDATION_COLUMNS.map((column) => [column, {
|
|
9201
|
+
fieldType: column === "$id" ? "KSQL_NUMBER" : "KSQL_STRING",
|
|
9202
|
+
sortKind: column === "$id" ? "number" : "string",
|
|
9203
|
+
semantics: syntheticSemantics(column === "$id" ? "number" : "string")
|
|
9204
|
+
}]));
|
|
9205
|
+
}
|
|
9206
|
+
async function executeExistingRecordValidation(stmt, client, options, cacheContext) {
|
|
9207
|
+
if (stmt.errorTable) throw new Error("ArgumentError: VALIDATE INTO requires a batch.");
|
|
9208
|
+
return executeExistingRecordValidationCore(stmt, client, options, cacheContext);
|
|
9209
|
+
}
|
|
9210
|
+
async function executeExistingRecordValidationCore(stmt, client, options, cacheContext) {
|
|
9211
|
+
const fieldInfos = await getFieldsCached(stmt.appId, client, cacheContext);
|
|
9212
|
+
const infoByCode = new Map(fieldInfos.map((field) => [field.code, field]));
|
|
9213
|
+
const targets = resolveExistingValidationTargets(stmt, fieldInfos);
|
|
9214
|
+
const checkGroups = stmt.checkGroups ?? [];
|
|
9215
|
+
const checkRefs2 = collectCheckFieldRefs(checkGroups);
|
|
9216
|
+
for (const ref of checkRefs2) {
|
|
9217
|
+
if (ref.field !== "$id" && !infoByCode.has(ref.field)) {
|
|
9218
|
+
throw customCheckParseError(`CHECK \u306E\u30D5\u30A3\u30FC\u30EB\u30C9 ${ref.field} \u306F APP${stmt.appId} \u306B\u5B58\u5728\u3057\u307E\u305B\u3093`);
|
|
9219
|
+
}
|
|
9220
|
+
}
|
|
9221
|
+
const evaluationTypes = new Map(fieldInfos.map((field) => [field.code, field.fieldType]));
|
|
9222
|
+
evaluationTypes.set("$id", "RECORD_NUMBER");
|
|
9223
|
+
assertCheckComparisonTypes(stmt, evaluationTypes);
|
|
9224
|
+
const whereFields = collectValidateWhereFields(stmt.where);
|
|
9225
|
+
const requiredFields = [.../* @__PURE__ */ new Set([
|
|
9226
|
+
"$id",
|
|
9227
|
+
...targets.map((field) => field.code),
|
|
9228
|
+
...whereFields,
|
|
9229
|
+
...checkRefs2.map((ref) => ref.field)
|
|
9230
|
+
])];
|
|
9231
|
+
for (const field of whereFields) {
|
|
9232
|
+
if (field !== "$id" && !infoByCode.has(field)) {
|
|
9233
|
+
throw new Error(`ArgumentError: WHERE field ${field} does not exist in APP${stmt.appId}.`);
|
|
9234
|
+
}
|
|
9235
|
+
}
|
|
9236
|
+
const numberPrecision = targets.some((field) => field.fieldType === "NUMBER") ? await getNumberPrecisionCached(stmt.appId, client, cacheContext) : void 0;
|
|
9237
|
+
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);
|
|
9238
|
+
const capability = classifyWhereCapability(stmt.where, semantics);
|
|
9239
|
+
if (capability.capability === "UNSUPPORTED") {
|
|
9240
|
+
throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
|
|
9241
|
+
}
|
|
9242
|
+
const fieldTypes = new Map(fieldInfos.map((field) => [field.code, field.fieldType]));
|
|
9243
|
+
const fieldOptions = new Map(fieldInfos.flatMap((field) => field.optionOrder ? [[field.code, new Set(Object.keys(field.optionOrder))]] : []));
|
|
9244
|
+
const prefilter = stmt.where === null ? null : capability.capability === "EXACT_PUSHDOWN" ? stmt.where : extractSafePushdownLeaves(stmt.where, {
|
|
9245
|
+
allowUnqualifiedFields: true,
|
|
9246
|
+
fieldTypes,
|
|
9247
|
+
fieldOptions,
|
|
9248
|
+
allowKlike: false
|
|
9249
|
+
});
|
|
9250
|
+
const query = prefilter === null ? "" : whereToKintone(prefilter);
|
|
9251
|
+
const records = await fetchAll(client.getRecords, stmt.appId, query, requiredFields, {
|
|
9252
|
+
maxRecords: options.maxRecords ?? 1e4,
|
|
9253
|
+
parallel: options.fetchParallel ?? 1,
|
|
9254
|
+
onLimit: "error"
|
|
9255
|
+
});
|
|
9256
|
+
const validationRows = records.map((record) => ({
|
|
9257
|
+
id: String(record["$id"]?.value ?? ""),
|
|
9258
|
+
record,
|
|
9259
|
+
flat: flatten(record, null)
|
|
9260
|
+
})).filter((row) => stmt.where === null || evalWhere(stmt.where, row.flat, (field) => evaluationTypes.get(field.field)));
|
|
9261
|
+
const rows = [];
|
|
9262
|
+
for (const row of validationRows) {
|
|
9263
|
+
for (const field of targets) {
|
|
9264
|
+
const raw = row.record[field.code]?.value;
|
|
9265
|
+
const validation = validateAndNormalizeDmlValue(raw, field, numberPrecision);
|
|
9266
|
+
if (validation.ok) continue;
|
|
9267
|
+
rows.push({
|
|
9268
|
+
"$id": row.id,
|
|
9269
|
+
"$err_field": field.code,
|
|
9270
|
+
"$err_code": validation.code,
|
|
9271
|
+
"$err_message": validation.message,
|
|
9272
|
+
"$err_value": renderExistingValidationValue(raw, field.fieldType)
|
|
9273
|
+
});
|
|
9274
|
+
}
|
|
9275
|
+
for (const check of evaluateCustomChecks(checkGroups, row.flat, (field) => evaluationTypes.get(field.field))) {
|
|
9276
|
+
rows.push({
|
|
9277
|
+
"$id": row.id,
|
|
9278
|
+
"$err_field": "",
|
|
9279
|
+
"$err_code": "ERR_CHECK",
|
|
9280
|
+
"$err_message": check.message,
|
|
9281
|
+
"$err_value": ""
|
|
9282
|
+
});
|
|
9283
|
+
}
|
|
9284
|
+
}
|
|
9285
|
+
const result = {
|
|
9286
|
+
type: "SELECT",
|
|
9287
|
+
columns: [...EXISTING_VALIDATION_COLUMNS],
|
|
9288
|
+
rows,
|
|
9289
|
+
rowCount: rows.length
|
|
9290
|
+
};
|
|
9291
|
+
materializedMetaBySelectResult.set(result, existingValidationColumnMeta());
|
|
9292
|
+
return result;
|
|
9293
|
+
}
|
|
7934
9294
|
var TEMP_TABLE_MAX_ROWS = 1e4;
|
|
7935
9295
|
function appendValidationErrors(tempTables, name, columns, rows, maxRows, columnMeta) {
|
|
7936
9296
|
const current = tempTables.get(name);
|
|
@@ -7958,7 +9318,7 @@ var BatchTimeoutError = class extends Error {
|
|
|
7958
9318
|
}
|
|
7959
9319
|
};
|
|
7960
9320
|
async function executeBatch(sql, client, options = {}) {
|
|
7961
|
-
const statements = parseSqlBatch(sql);
|
|
9321
|
+
const statements = parseSqlBatch(sql, options.enableImport === true);
|
|
7962
9322
|
const analysis = analyzeBatch(statements);
|
|
7963
9323
|
const injectedVariables = validateDeclaredBatchVariables(statements, options.variables);
|
|
7964
9324
|
const batchOptions = { ...options, variables: injectedVariables };
|
|
@@ -8011,11 +9371,12 @@ async function executeBatch(sql, client, options = {}) {
|
|
|
8011
9371
|
const userConfirm = batchOptions.confirm;
|
|
8012
9372
|
const stmtOptions = userConfirm ? {
|
|
8013
9373
|
...batchOptions,
|
|
8014
|
-
confirm: (count, operation) => userConfirm(count, operation, {
|
|
9374
|
+
confirm: (count, operation, detailContext) => userConfirm(count, operation, {
|
|
8015
9375
|
statementIndex: i,
|
|
8016
9376
|
statementCount: statements.length,
|
|
8017
9377
|
statementType: info.statementType,
|
|
8018
|
-
targetAppId: info.targetAppId
|
|
9378
|
+
targetAppId: info.targetAppId,
|
|
9379
|
+
...detailContext?.importDetail ? { importDetail: detailContext.importDetail } : {}
|
|
8019
9380
|
})
|
|
8020
9381
|
} : batchOptions;
|
|
8021
9382
|
const searchAbortCollector = { aborted: false };
|
|
@@ -8064,9 +9425,14 @@ async function executeBatch(sql, client, options = {}) {
|
|
|
8064
9425
|
}
|
|
8065
9426
|
async function executeBatchStatement(stmt, info, client, options, cacheContext, tempTables, variables) {
|
|
8066
9427
|
if (stmt.type === "SET_VARIABLE") {
|
|
8067
|
-
const resolvedStmt2 =
|
|
9428
|
+
const resolvedStmt2 = resolveBatchVariableReferences(stmt, variables);
|
|
8068
9429
|
validateKlikeStatement(resolvedStmt2);
|
|
8069
|
-
if (resolvedStmt2.expr.type === "
|
|
9430
|
+
if (resolvedStmt2.expr.type === "ARRAY") {
|
|
9431
|
+
variables.set(stmt.name, {
|
|
9432
|
+
type: "array",
|
|
9433
|
+
elements: resolvedStmt2.expr.elements.map((element) => ({ type: "string", value: element.value }))
|
|
9434
|
+
});
|
|
9435
|
+
} else if (resolvedStmt2.expr.type === "SCALAR_SUBQUERY") {
|
|
8070
9436
|
try {
|
|
8071
9437
|
const value = await evaluateScalarSubquery(
|
|
8072
9438
|
resolvedStmt2.expr.query,
|
|
@@ -8103,8 +9469,30 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
|
|
|
8103
9469
|
}
|
|
8104
9470
|
return {};
|
|
8105
9471
|
}
|
|
8106
|
-
const resolvedStmt =
|
|
9472
|
+
const resolvedStmt = resolveBatchVariableReferences(stmt, variables);
|
|
8107
9473
|
validateKlikeStatement(resolvedStmt);
|
|
9474
|
+
if (resolvedStmt.type === "VALIDATE") {
|
|
9475
|
+
const result = await executeExistingRecordValidationCore(
|
|
9476
|
+
resolvedStmt,
|
|
9477
|
+
client,
|
|
9478
|
+
{ ...options, onLimitReached: "error" },
|
|
9479
|
+
cacheContext
|
|
9480
|
+
);
|
|
9481
|
+
if (resolvedStmt.errorTable) {
|
|
9482
|
+
appendValidationErrors(
|
|
9483
|
+
tempTables,
|
|
9484
|
+
resolvedStmt.errorTable,
|
|
9485
|
+
result.columns,
|
|
9486
|
+
result.rows,
|
|
9487
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
9488
|
+
materializedMetaBySelectResult.get(result) ?? existingValidationColumnMeta()
|
|
9489
|
+
);
|
|
9490
|
+
}
|
|
9491
|
+
return { result };
|
|
9492
|
+
}
|
|
9493
|
+
if (resolvedStmt.type === "IMPORT") {
|
|
9494
|
+
return { result: await executeImport(resolvedStmt, client, options, cacheContext, tempTables) };
|
|
9495
|
+
}
|
|
8108
9496
|
if ("validateOnly" in resolvedStmt && resolvedStmt.validateOnly === true) {
|
|
8109
9497
|
const result = await executeDmlValidation(
|
|
8110
9498
|
resolvedStmt,
|
|
@@ -8266,9 +9654,9 @@ function safeJsonStringify(v) {
|
|
|
8266
9654
|
return String(v);
|
|
8267
9655
|
}
|
|
8268
9656
|
}
|
|
8269
|
-
function parseSqlBatch(sql) {
|
|
9657
|
+
function parseSqlBatch(sql, enableImport = false) {
|
|
8270
9658
|
const tokens = new Lexer(sql).tokenize();
|
|
8271
|
-
return new Parser(tokens).parseStatements();
|
|
9659
|
+
return new Parser(tokens, { import: enableImport }).parseStatements();
|
|
8272
9660
|
}
|
|
8273
9661
|
function evaluateScalarExpr(expr) {
|
|
8274
9662
|
switch (expr.type) {
|
|
@@ -8289,9 +9677,9 @@ function evaluateScalarExpr(expr) {
|
|
|
8289
9677
|
}
|
|
8290
9678
|
}
|
|
8291
9679
|
}
|
|
8292
|
-
function
|
|
9680
|
+
function resolveBatchVariableReferences(node, variables) {
|
|
8293
9681
|
if (Array.isArray(node)) {
|
|
8294
|
-
return node.map((v) =>
|
|
9682
|
+
return node.map((v) => resolveBatchVariableReferences(v, variables));
|
|
8295
9683
|
}
|
|
8296
9684
|
if (node !== null && typeof node === "object") {
|
|
8297
9685
|
const obj = node;
|
|
@@ -8300,14 +9688,72 @@ function resolveVariableRefs(node, variables) {
|
|
|
8300
9688
|
if (value === void 0) {
|
|
8301
9689
|
throw new Error(`ParseError: variable @${obj["name"]} is not defined in this batch.`);
|
|
8302
9690
|
}
|
|
9691
|
+
if (value.type === "array") {
|
|
9692
|
+
throw new Error(`ParseError: array variable @${obj["name"]} can only be used as IN @${obj["name"]}.`);
|
|
9693
|
+
}
|
|
8303
9694
|
return value.type === "number" ? { type: "NUMBER", value: value.value, raw: value.raw ?? String(value.value) } : { type: "STRING", value: value.value };
|
|
8304
9695
|
}
|
|
8305
|
-
|
|
8306
|
-
|
|
9696
|
+
if (obj["type"] === "VARIABLE_COL" && typeof obj["name"] === "string" && typeof obj["alias"] === "string") {
|
|
9697
|
+
const value = variables.get(obj["name"]);
|
|
9698
|
+
if (value === void 0) throw new Error(`ParseError: variable @${obj["name"]} is not defined in this batch.`);
|
|
9699
|
+
if (value.type === "array") throw new Error(`ParseError: array variable @${obj["name"]} cannot be used as a SELECT column.`);
|
|
9700
|
+
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"] };
|
|
9701
|
+
}
|
|
9702
|
+
if (obj["type"] === "VARIABLE_IN_LIST") return obj;
|
|
9703
|
+
const resolved = Object.fromEntries(
|
|
9704
|
+
Object.entries(obj).map(([key, value]) => [key, resolveBatchVariableReferences(value, variables)])
|
|
8307
9705
|
);
|
|
9706
|
+
if (resolved["type"] === "BINARY") {
|
|
9707
|
+
const right = resolved["right"];
|
|
9708
|
+
if (right?.["type"] === "VARIABLE_IN_LIST" && typeof right["name"] === "string") {
|
|
9709
|
+
const value = variables.get(right["name"]);
|
|
9710
|
+
if (value === void 0) throw new Error(`ParseError: variable @${right["name"]} is not defined in this batch.`);
|
|
9711
|
+
if (value.type !== "array") {
|
|
9712
|
+
throw new Error(`ParseError: scalar variable @${right["name"]} cannot be used as IN @${right["name"]}; use IN (@${right["name"]}) instead.`);
|
|
9713
|
+
}
|
|
9714
|
+
if (value.elements.length === 0) {
|
|
9715
|
+
return { type: "BOOLEAN", value: resolved["op"] === "NOT_IN" };
|
|
9716
|
+
}
|
|
9717
|
+
resolved["right"] = {
|
|
9718
|
+
type: "IN_LIST",
|
|
9719
|
+
values: value.elements.map((element) => ({ type: "STRING", value: element.value }))
|
|
9720
|
+
};
|
|
9721
|
+
}
|
|
9722
|
+
}
|
|
9723
|
+
const simplified = simplifyBooleanWhere(resolved);
|
|
9724
|
+
if (simplified["type"] === "SELECT" && isBooleanNode(simplified["where"], true)) {
|
|
9725
|
+
simplified["where"] = null;
|
|
9726
|
+
}
|
|
9727
|
+
if ((simplified["type"] === "UPDATE" || simplified["type"] === "DELETE" || simplified["type"] === "REORDER") && isBooleanNode(simplified["where"], true)) {
|
|
9728
|
+
throw new Error("ArgumentError: empty-array simplification makes the target WHERE always true; use an explicit safe target condition.");
|
|
9729
|
+
}
|
|
9730
|
+
return simplified;
|
|
8308
9731
|
}
|
|
8309
9732
|
return node;
|
|
8310
9733
|
}
|
|
9734
|
+
function isBooleanNode(value, expected) {
|
|
9735
|
+
return value !== null && typeof value === "object" && value.type === "BOOLEAN" && (expected === void 0 || value.value === expected);
|
|
9736
|
+
}
|
|
9737
|
+
function simplifyBooleanWhere(obj) {
|
|
9738
|
+
if (obj["type"] === "NOT" && isBooleanNode(obj["expr"])) {
|
|
9739
|
+
return { type: "BOOLEAN", value: !obj["expr"].value };
|
|
9740
|
+
}
|
|
9741
|
+
if (obj["type"] === "GROUP" && isBooleanNode(obj["expr"])) return obj["expr"];
|
|
9742
|
+
if (obj["type"] === "LOGICAL") {
|
|
9743
|
+
const left = obj["left"];
|
|
9744
|
+
const right = obj["right"];
|
|
9745
|
+
if (obj["op"] === "AND") {
|
|
9746
|
+
if (isBooleanNode(left, false) || isBooleanNode(right, false)) return { type: "BOOLEAN", value: false };
|
|
9747
|
+
if (isBooleanNode(left, true)) return right;
|
|
9748
|
+
if (isBooleanNode(right, true)) return left;
|
|
9749
|
+
} else if (obj["op"] === "OR") {
|
|
9750
|
+
if (isBooleanNode(left, true) || isBooleanNode(right, true)) return { type: "BOOLEAN", value: true };
|
|
9751
|
+
if (isBooleanNode(left, false)) return right;
|
|
9752
|
+
if (isBooleanNode(right, false)) return left;
|
|
9753
|
+
}
|
|
9754
|
+
}
|
|
9755
|
+
return obj;
|
|
9756
|
+
}
|
|
8311
9757
|
function findVariableRef(node) {
|
|
8312
9758
|
if (Array.isArray(node)) {
|
|
8313
9759
|
for (const value of node) {
|
|
@@ -8318,7 +9764,7 @@ function findVariableRef(node) {
|
|
|
8318
9764
|
}
|
|
8319
9765
|
if (node !== null && typeof node === "object") {
|
|
8320
9766
|
const obj = node;
|
|
8321
|
-
if (obj["type"] === "VARIABLE" && typeof obj["name"] === "string") return obj["name"];
|
|
9767
|
+
if ((obj["type"] === "VARIABLE" || obj["type"] === "VARIABLE_COL" || obj["type"] === "VARIABLE_IN_LIST") && typeof obj["name"] === "string") return obj["name"];
|
|
8322
9768
|
for (const value of Object.values(obj)) {
|
|
8323
9769
|
const found = findVariableRef(value);
|
|
8324
9770
|
if (found !== null) return found;
|
|
@@ -8561,6 +10007,7 @@ async function assertDmlWhereCapability(stmt, client, cacheContext) {
|
|
|
8561
10007
|
if (stmt.subtableCode || stmt.type === "UPDATE" && stmt.from != null) return;
|
|
8562
10008
|
const fields = whereNeedsFieldMetadata(stmt.where) ? await getFieldsCached(stmt.appId, client, cacheContext) : [];
|
|
8563
10009
|
const byCode = new Map(fields.map((field) => [field.code, field]));
|
|
10010
|
+
if (stmt.where.type === "BOOLEAN" && stmt.where.value === false) return;
|
|
8564
10011
|
const result = classifyWhereCapability(stmt.where, (field) => {
|
|
8565
10012
|
if (field.field === "$id") return resolveFieldSemantics({ fieldType: "__ID__" });
|
|
8566
10013
|
const info = byCode.get(field.field);
|
|
@@ -8633,6 +10080,9 @@ async function executeSelect(stmt, client, options, cacheContext, cteCache, capt
|
|
|
8633
10080
|
}
|
|
8634
10081
|
return result;
|
|
8635
10082
|
}
|
|
10083
|
+
function isConstantFalseWhere(where) {
|
|
10084
|
+
return where?.type === "BOOLEAN" && where.value === false;
|
|
10085
|
+
}
|
|
8636
10086
|
function isNoFromSelect(stmt) {
|
|
8637
10087
|
return stmt.from.appId === 0 && stmt.from.cteName === NO_FROM_CTE_NAME;
|
|
8638
10088
|
}
|
|
@@ -8664,6 +10114,8 @@ function stringFuncHasFieldRef(expr) {
|
|
|
8664
10114
|
function validateNoFromColumns(stmt) {
|
|
8665
10115
|
for (const col of stmt.columns) {
|
|
8666
10116
|
switch (col.type) {
|
|
10117
|
+
case "VARIABLE_COL":
|
|
10118
|
+
throw new Error(`internal error: unresolved SELECT variable @${col.name}`);
|
|
8667
10119
|
case "LITERAL_COL":
|
|
8668
10120
|
break;
|
|
8669
10121
|
case "ARITH_COL":
|
|
@@ -8899,6 +10351,7 @@ function collectTypedInFieldRefs(expr, out) {
|
|
|
8899
10351
|
return;
|
|
8900
10352
|
case "NULL_CHECK":
|
|
8901
10353
|
case "EXISTS":
|
|
10354
|
+
case "BOOLEAN":
|
|
8902
10355
|
return;
|
|
8903
10356
|
}
|
|
8904
10357
|
}
|
|
@@ -9272,8 +10725,14 @@ async function inferSelectColumnMeta(stmt, outputColumns, client, cacheContext,
|
|
|
9272
10725
|
}
|
|
9273
10726
|
} else if (column.type === "ARITH_AGG_COL" || column.type === "ARITH_COL") {
|
|
9274
10727
|
meta = syntheticColumnMeta("number");
|
|
9275
|
-
} else if (column.type === "LITERAL_COL"
|
|
10728
|
+
} else if (column.type === "LITERAL_COL") {
|
|
9276
10729
|
meta = syntheticColumnMeta("string");
|
|
10730
|
+
} else if (column.type === "SCALAR_VALUE_COL") {
|
|
10731
|
+
const expr = column.expr;
|
|
10732
|
+
if (expr.type === "STRING_FUNC") meta = stringFunctionColumnMeta(expr);
|
|
10733
|
+
else if (expr.type === "NUMBER" || expr.type === "SCALAR_ARITH") meta = syntheticColumnMeta("number");
|
|
10734
|
+
else if (expr.type === "FIELD") meta = resolveField2(expr);
|
|
10735
|
+
else meta = syntheticColumnMeta("string");
|
|
9277
10736
|
} else if (column.type === "STRFUNC_COL") {
|
|
9278
10737
|
meta = stringFunctionColumnMeta(column.expr);
|
|
9279
10738
|
} else if (column.type === "WINDOW_COL") {
|
|
@@ -9361,7 +10820,8 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
|
|
|
9361
10820
|
validateKlikePushdownPlan(pushdownPlan);
|
|
9362
10821
|
const mainPushDown = pushdownPlan.mainCondition;
|
|
9363
10822
|
const tableConditions = pushdownPlan.joinConditions;
|
|
9364
|
-
const
|
|
10823
|
+
const constantFalse = isConstantFalseWhere(stmt.where);
|
|
10824
|
+
const mainFetch = constantFalse ? Promise.resolve([]) : fetchTableRecordsForFullScan(
|
|
9365
10825
|
stmt,
|
|
9366
10826
|
stmt.from,
|
|
9367
10827
|
client,
|
|
@@ -9376,6 +10836,10 @@ async function executeFullScanSelect(stmt, client, options, cacheContext, cteCac
|
|
|
9376
10836
|
const parallelJoins = [];
|
|
9377
10837
|
const onOptJoins = [];
|
|
9378
10838
|
for (const join2 of stmt.joins) {
|
|
10839
|
+
if (constantFalse) {
|
|
10840
|
+
parallelJoins.push({ join: join2, promise: Promise.resolve([]) });
|
|
10841
|
+
continue;
|
|
10842
|
+
}
|
|
9379
10843
|
const jCond = join2.table.alias ? tableConditions.get(join2.table.alias) ?? null : null;
|
|
9380
10844
|
if (jCond !== null) {
|
|
9381
10845
|
parallelJoins.push({
|
|
@@ -10082,9 +11546,10 @@ async function buildSortKindsForSelect(stmt, client, cacheContext) {
|
|
|
10082
11546
|
return sortKinds;
|
|
10083
11547
|
}
|
|
10084
11548
|
function convertProcessRowValue(raw, dstFieldType) {
|
|
10085
|
-
|
|
11549
|
+
if (typeof raw !== "string") return raw;
|
|
11550
|
+
const USER_TYPES4 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
10086
11551
|
const ARRAY_TYPES3 = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
|
|
10087
|
-
if (
|
|
11552
|
+
if (USER_TYPES4.has(dstFieldType ?? "")) {
|
|
10088
11553
|
if (raw === "") return [];
|
|
10089
11554
|
try {
|
|
10090
11555
|
const parsed = JSON.parse(raw);
|
|
@@ -10149,11 +11614,13 @@ function assertValidDmlRecords(records, targetFields, fieldInfos, numberPrecisio
|
|
|
10149
11614
|
records.forEach((record, rowIndex) => {
|
|
10150
11615
|
for (const code of targetFields) {
|
|
10151
11616
|
const info = infoByCode.get(code);
|
|
10152
|
-
const
|
|
11617
|
+
const original = record[code]?.value ?? "";
|
|
11618
|
+
const result = validateAndNormalizeDmlValue(original, info, numberPrecision);
|
|
10153
11619
|
if (!result.ok) {
|
|
10154
11620
|
throw new Error(`DmlValidationError: ${result.code} ${result.message} (row=${rowIndex + 1}, field=${code})`);
|
|
10155
11621
|
}
|
|
10156
|
-
|
|
11622
|
+
const preserveCodes = ["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"].includes(info.fieldType) && Array.isArray(original) && original.every((item) => typeof item === "object" && item !== null && "code" in item);
|
|
11623
|
+
record[code] = { value: preserveCodes ? original : result.value };
|
|
10157
11624
|
}
|
|
10158
11625
|
});
|
|
10159
11626
|
}
|
|
@@ -10313,6 +11780,8 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10313
11780
|
if (stmt.type === "UPDATE") return materializeUpdateValidationCandidates(stmt, client, options, cacheContext, tempTables);
|
|
10314
11781
|
let rows;
|
|
10315
11782
|
let sourceRows;
|
|
11783
|
+
let sourcePresence;
|
|
11784
|
+
let sourceRowErrors;
|
|
10316
11785
|
let evaluationTypes;
|
|
10317
11786
|
if (stmt.type === "INSERT" || stmt.type === "UPSERT") {
|
|
10318
11787
|
assertInsertCheckRefs(stmt, stmt.fields);
|
|
@@ -10322,7 +11791,7 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10322
11791
|
(value, i) => value.type === "CASE_VALUE" ? evalCaseWhenValue(value.expr, {}, infoByCode.get(stmt.fields[i])?.fieldType) : value
|
|
10323
11792
|
));
|
|
10324
11793
|
} else {
|
|
10325
|
-
const selectResult =
|
|
11794
|
+
const selectResult = await dmlSourceMaterializer.materialize(stmt, client, options, cacheContext, tempTables, [...infoByCode.values()]);
|
|
10326
11795
|
const hasChecks = (stmt.checkGroups?.length ?? 0) > 0;
|
|
10327
11796
|
if (selectResult.columns.length < stmt.fields.length || !hasChecks && selectResult.columns.length !== stmt.fields.length) {
|
|
10328
11797
|
throw new Error(`SELECT \u306E\u5217\u6570\uFF08${selectResult.columns.length}\uFF09\u3068 DML \u306E\u30D5\u30A3\u30FC\u30EB\u30C9\u6570\uFF08${stmt.fields.length}\uFF09\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093`);
|
|
@@ -10332,7 +11801,9 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10332
11801
|
}
|
|
10333
11802
|
assertInsertCheckRefs(stmt, selectResult.columns);
|
|
10334
11803
|
sourceRows = selectResult.rows;
|
|
10335
|
-
|
|
11804
|
+
sourcePresence = selectResult.importPresence;
|
|
11805
|
+
sourceRowErrors = selectResult.importRowErrors;
|
|
11806
|
+
const meta = selectResult.columnMeta;
|
|
10336
11807
|
evaluationTypes = new Map(selectResult.columns.map((column) => {
|
|
10337
11808
|
const columnMeta = meta?.get(column);
|
|
10338
11809
|
const type = columnMeta?.fieldType ?? (columnMeta?.semantics?.compareMode === "number" || columnMeta?.sortKind === "number" ? "NUMBER" : "SINGLE_LINE_TEXT");
|
|
@@ -10345,8 +11816,10 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10345
11816
|
rowNumber: index + 1,
|
|
10346
11817
|
operation,
|
|
10347
11818
|
mode: "create",
|
|
10348
|
-
payload: new Map(stmt.fields.
|
|
10349
|
-
|
|
11819
|
+
payload: new Map(stmt.fields.flatMap(
|
|
11820
|
+
(field, i) => sourcePresence && !sourcePresence[index]?.has(field) ? [] : [[field, values[i]]]
|
|
11821
|
+
)),
|
|
11822
|
+
preErrors: [...sourceRowErrors?.[index] ?? []],
|
|
10350
11823
|
record: {},
|
|
10351
11824
|
evaluationRow: sourceRows?.[index] ?? Object.fromEntries(
|
|
10352
11825
|
stmt.fields.map((field, i) => [field, renderValidationValue(values[i])])
|
|
@@ -10359,13 +11832,17 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10359
11832
|
}
|
|
10360
11833
|
const fieldTypes = new Map([...infoByCode].map(([code, info]) => [code, info.fieldType]));
|
|
10361
11834
|
const rowKeys = candidates.map((candidate) => stmt.keyFields.map((key) => renderValidationValue(candidate.payload.get(key))));
|
|
10362
|
-
const targets = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeys, client, options, fieldTypes);
|
|
10363
11835
|
const numeric = stmt.keyFields.map((key) => fieldTypes.get(key) === "NUMBER");
|
|
10364
11836
|
const keyCounts = /* @__PURE__ */ new Map();
|
|
10365
11837
|
for (const parts of rowKeys) {
|
|
10366
11838
|
const key = upsertNormalizedKey(parts, numeric);
|
|
10367
11839
|
keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
|
|
10368
11840
|
}
|
|
11841
|
+
const isImport = importSourceByDmlStatement.has(stmt);
|
|
11842
|
+
if (isImport && [...keyCounts.values()].some((count) => count > 1)) {
|
|
11843
|
+
throw new Error("ERR_KEY_DUP_SOURCE: UPSERT \u30BD\u30FC\u30B9\u5185\u3067\u30AD\u30FC\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059");
|
|
11844
|
+
}
|
|
11845
|
+
const targets = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeys, client, options, fieldTypes);
|
|
10369
11846
|
candidates.forEach((candidate, index) => {
|
|
10370
11847
|
const parts = rowKeys[index];
|
|
10371
11848
|
const targetId = lookupUpsertTarget(targets, parts);
|
|
@@ -10374,7 +11851,7 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10374
11851
|
stmt.keyFields.forEach((key, keyIndex) => {
|
|
10375
11852
|
if (parts[keyIndex] === "") candidate.preErrors.push({ field: key, code: "ERR_KEY_EMPTY", message: `UPSERT \u30AD\u30FC ${key} \u306F\u7A7A\u306B\u3067\u304D\u307E\u305B\u3093` });
|
|
10376
11853
|
});
|
|
10377
|
-
if ((keyCounts.get(upsertNormalizedKey(parts, numeric)) ?? 0) > 1) {
|
|
11854
|
+
if (!isImport && (keyCounts.get(upsertNormalizedKey(parts, numeric)) ?? 0) > 1) {
|
|
10378
11855
|
candidate.preErrors.push({ field: stmt.keyFields[0], code: "ERR_KEY_DUP_SOURCE", message: "UPSERT \u30BD\u30FC\u30B9\u5185\u3067\u30AD\u30FC\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059" });
|
|
10379
11856
|
}
|
|
10380
11857
|
});
|
|
@@ -10734,12 +12211,508 @@ async function executeInsert(stmt, client, options, cacheContext) {
|
|
|
10734
12211
|
insertedCount: createdIds.flat().length
|
|
10735
12212
|
};
|
|
10736
12213
|
}
|
|
12214
|
+
function importPlaceholderSelect() {
|
|
12215
|
+
return {
|
|
12216
|
+
type: "SELECT",
|
|
12217
|
+
distinct: false,
|
|
12218
|
+
columns: [],
|
|
12219
|
+
from: { appId: 0, alias: null, cteName: NO_FROM_CTE_NAME },
|
|
12220
|
+
joins: [],
|
|
12221
|
+
where: null,
|
|
12222
|
+
groupBy: [],
|
|
12223
|
+
having: null,
|
|
12224
|
+
orderMode: "CANONICAL",
|
|
12225
|
+
orderBy: [],
|
|
12226
|
+
limit: null,
|
|
12227
|
+
offset: null
|
|
12228
|
+
};
|
|
12229
|
+
}
|
|
12230
|
+
async function executeImport(stmt, client, options, cacheContext, tempTables) {
|
|
12231
|
+
if (!options.enableImport) throw new Error("UnsupportedError: IMPORT capability is disabled.");
|
|
12232
|
+
const handle = resolveImportSource(stmt.source.sourceName, options.importSource);
|
|
12233
|
+
if (stmt.targets?.some((target) => target.kind === "SUBTABLE")) {
|
|
12234
|
+
if (!stmt.validateOnly && !options.supportsImportConfirmDetail) {
|
|
12235
|
+
throw new Error("UnsupportedError: IMPORT subtable mutation requires a surface that displays parent/table replacement and deletion detail; use VALIDATE ONLY/EXPLAIN.");
|
|
12236
|
+
}
|
|
12237
|
+
if (stmt.source.kind === "CSV") {
|
|
12238
|
+
if (stmt.writeMode !== "UPDATE_RECORD_NUMBER" || !stmt.recordNumberSourceHeader) throw new Error("ArgumentError: CSV subtable replacement requires IMPORT UPDATE and MATCH RECORD NUMBER SOURCE.");
|
|
12239
|
+
if (!stmt.replaceSubtables?.length) throw new Error("ArgumentError: CSV subtable replacement requires REPLACE SUBTABLES (...).");
|
|
12240
|
+
const declared = new Set(stmt.targets.filter((target) => target.kind === "SUBTABLE").map((target) => target.subtableCode));
|
|
12241
|
+
if (stmt.replaceSubtables.some((table) => !declared.has(table))) throw new Error("ArgumentError: REPLACE SUBTABLES contains a table not declared in INTO.");
|
|
12242
|
+
for (const target of stmt.targets.filter((target2) => target2.kind === "SUBTABLE")) {
|
|
12243
|
+
if (!target.rowIdSourceHeader || !stmt.replaceSubtables.includes(target.subtableCode)) throw new Error(`ArgumentError: CSV subtable ${target.subtableCode} requires ROW ID SOURCE and REPLACE SUBTABLES declaration.`);
|
|
12244
|
+
}
|
|
12245
|
+
}
|
|
12246
|
+
if (stmt.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12247
|
+
const targets = stmt.targets;
|
|
12248
|
+
const fieldInfos = await getFieldsCached(stmt.appId, client, cacheContext);
|
|
12249
|
+
const targetCodes = targets.flatMap((target) => target.kind === "FIELD" ? [target.field] : target.children);
|
|
12250
|
+
const numberPrecision = fieldInfos.some((info) => targetCodes.includes(info.code) && info.fieldType === "NUMBER") ? await getNumberPrecisionCached(stmt.appId, client, cacheContext) : void 0;
|
|
12251
|
+
const payload = await loadImportSource(handle, /* @__PURE__ */ new Map());
|
|
12252
|
+
const materialized = stmt.source.kind === "JSON" ? materializeJsonImportRecords(stmt.source, payload, targets, options.maxRecords ?? 1e4) : materializeCliKintoneCsvImportRecords(stmt.source, payload, targets, stmt.replaceSubtables ?? [], options.maxRecords ?? 1e4, stmt.recordNumberSourceHeader);
|
|
12253
|
+
const operation = stmt.source.kind === "CSV" ? "UPDATE" : stmt.keyFields ? "UPSERT" : "INSERT";
|
|
12254
|
+
const prepared = prepareImportRecords(materialized, targets, fieldInfos, numberPrecision, operation);
|
|
12255
|
+
if (stmt.source.kind === "CSV") return executeCsvSubtableReplacement(stmt, materialized, prepared, fieldInfos, client, options, tempTables);
|
|
12256
|
+
assertImportRejectLimit(prepared, stmt.rejectLimit);
|
|
12257
|
+
const payloadFields = [...new Set(targets.flatMap((target) => target.kind === "FIELD" ? [target.field] : target.children))];
|
|
12258
|
+
const errors = materializeImportValidationErrors(prepared.errors, payloadFields);
|
|
12259
|
+
const columns = [...payloadFields, ...IMPORT_VALIDATION_META_COLUMNS];
|
|
12260
|
+
const invalidRows = prepared.invalidParentRows.size;
|
|
12261
|
+
const detail = {
|
|
12262
|
+
preflight: "ACTUAL_DATA",
|
|
12263
|
+
parents: { total: prepared.parents.length, valid: prepared.parents.length - invalidRows, invalid: invalidRows, mutationCandidates: prepared.parents.filter((parent) => parent.valid).length },
|
|
12264
|
+
tables: Object.fromEntries(prepared.tableCounts),
|
|
12265
|
+
writesKintone: false
|
|
12266
|
+
};
|
|
12267
|
+
const result2 = {
|
|
12268
|
+
type: "VALIDATION",
|
|
12269
|
+
operation,
|
|
12270
|
+
validatedRows: prepared.parents.length,
|
|
12271
|
+
validRows: prepared.parents.length - invalidRows,
|
|
12272
|
+
invalidRows,
|
|
12273
|
+
errorCount: errors.length,
|
|
12274
|
+
columns,
|
|
12275
|
+
errors,
|
|
12276
|
+
importDetail: detail,
|
|
12277
|
+
...stmt.validationErrorTable ? { errTable: stmt.validationErrorTable } : {}
|
|
12278
|
+
};
|
|
12279
|
+
if (stmt.validationErrorTable && tempTables) appendValidationErrors(
|
|
12280
|
+
tempTables,
|
|
12281
|
+
stmt.validationErrorTable,
|
|
12282
|
+
columns,
|
|
12283
|
+
errors,
|
|
12284
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12285
|
+
/* @__PURE__ */ new Map()
|
|
12286
|
+
);
|
|
12287
|
+
if (stmt.validateOnly) return result2;
|
|
12288
|
+
assertJsonImportHasNoRowIds(materialized);
|
|
12289
|
+
if (prepared.errors.length > 0 && !stmt.onErrorSkip) {
|
|
12290
|
+
const first = prepared.errors[0];
|
|
12291
|
+
throw new Error(`DmlValidationError: ${first.code} ${first.message} (row=${first.parentRow}, field=${first.field})`);
|
|
12292
|
+
}
|
|
12293
|
+
if (stmt.onErrorSkip) {
|
|
12294
|
+
if (!tempTables || !stmt.errorTable) throw new Error("ArgumentError: ON ERROR SKIP requires a batch and INTO error table.");
|
|
12295
|
+
appendValidationErrors(
|
|
12296
|
+
tempTables,
|
|
12297
|
+
stmt.errorTable,
|
|
12298
|
+
columns,
|
|
12299
|
+
errors,
|
|
12300
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12301
|
+
/* @__PURE__ */ new Map()
|
|
12302
|
+
);
|
|
12303
|
+
}
|
|
12304
|
+
const validParents = prepared.parents.filter((parent) => parent.valid);
|
|
12305
|
+
const fieldTypes = new Map(fieldInfos.map((info) => [info.code, info.fieldType]));
|
|
12306
|
+
const targetIds = validParents.map(() => void 0);
|
|
12307
|
+
if (stmt.keyFields) {
|
|
12308
|
+
for (const key of stmt.keyFields) if (!stmt.fields.includes(key)) {
|
|
12309
|
+
throw new Error(`ON DUPLICATE \u306E\u30AD\u30FC\u300C${key}\u300D\u304C UPSERT \u30D5\u30A3\u30FC\u30EB\u30C9\u306B\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093`);
|
|
12310
|
+
}
|
|
12311
|
+
const numeric = stmt.keyFields.map((key) => fieldTypes.get(key) === "NUMBER");
|
|
12312
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
12313
|
+
const rowKeys = validParents.map((parent) => stmt.keyFields.map((key) => String(parent.top[key]?.value ?? "")));
|
|
12314
|
+
for (const parts of rowKeys) {
|
|
12315
|
+
const normalized = upsertNormalizedKey(parts, numeric);
|
|
12316
|
+
if (sourceKeys.has(normalized)) throw new Error("ERR_KEY_DUP_SOURCE: UPSERT \u30BD\u30FC\u30B9\u5185\u3067\u30AD\u30FC\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059");
|
|
12317
|
+
sourceKeys.add(normalized);
|
|
12318
|
+
}
|
|
12319
|
+
const targetsIndex = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeys, client, options, fieldTypes);
|
|
12320
|
+
rowKeys.forEach((parts, index) => {
|
|
12321
|
+
targetIds[index] = lookupUpsertTarget(targetsIndex, parts);
|
|
12322
|
+
});
|
|
12323
|
+
}
|
|
12324
|
+
const tableCodes = targets.filter((target) => target.kind === "SUBTABLE").map((target) => target.subtableCode);
|
|
12325
|
+
const existingById = /* @__PURE__ */ new Map();
|
|
12326
|
+
const updateIds = targetIds.filter((id) => id !== void 0);
|
|
12327
|
+
for (const chunk2 of splitChunks([...new Set(updateIds)], 100)) {
|
|
12328
|
+
const response = await client.getRecords({ app: stmt.appId, query: `$id in (${chunk2.join(",")}) limit 500`, fields: ["$id", "$revision", ...tableCodes] });
|
|
12329
|
+
for (const record of response.records) {
|
|
12330
|
+
const id = Number(record["$id"]?.value);
|
|
12331
|
+
const revision = Number(record["$revision"]?.value);
|
|
12332
|
+
if (Number.isFinite(id)) existingById.set(id, { id, ...Number.isFinite(revision) ? { revision } : {}, record });
|
|
12333
|
+
}
|
|
12334
|
+
}
|
|
12335
|
+
const writePlan = buildJsonSubtableWritePlan(validParents, targetIds, existingById);
|
|
12336
|
+
const importDetail = {
|
|
12337
|
+
kind: "IMPORT_JSON_SUBTABLE",
|
|
12338
|
+
rowIdPolicy: "DROP_AND_RENUMBER_ALL",
|
|
12339
|
+
parentsToWrite: writePlan.length,
|
|
12340
|
+
insertedParents: writePlan.filter((parent) => parent.mode === "INSERT").length,
|
|
12341
|
+
updatedParents: writePlan.filter((parent) => parent.mode === "UPDATE").length,
|
|
12342
|
+
hasDeletes: writePlan.some((parent) => parent.tables.some((table) => table.deleteRows > 0)),
|
|
12343
|
+
parents: writePlan.map((parent) => ({ parentRow: parent.parentRow, mode: parent.mode, ...parent.targetId === void 0 ? {} : { targetId: parent.targetId }, tables: parent.tables }))
|
|
12344
|
+
};
|
|
12345
|
+
if (writePlan.length > 0) {
|
|
12346
|
+
if (!options.confirm) throw new Error("UnsupportedError: JSON IMPORT subtable mutation requires explicit confirmation detail approval.");
|
|
12347
|
+
const ok = await options.confirm(writePlan.length, stmt.keyFields ? "UPDATE" : "INSERT", { statementIndex: 0, statementCount: 1, statementType: "IMPORT", targetAppId: stmt.appId, importDetail });
|
|
12348
|
+
if (!ok) throw new OperationCancelledError(stmt.keyFields ? "UPDATE" : "INSERT", writePlan.length);
|
|
12349
|
+
}
|
|
12350
|
+
const toScalarMap = (record) => new Map(
|
|
12351
|
+
Object.entries(record).map(([code, field]) => [code, field.value])
|
|
12352
|
+
);
|
|
12353
|
+
const payloadFor = (parent) => buildJsonImportRecordPayload(
|
|
12354
|
+
toScalarMap(parent.top),
|
|
12355
|
+
new Map([...parent.subtables].map(([table, rows]) => [table, rows.map((row) => ({ values: toScalarMap(row) }))]))
|
|
12356
|
+
);
|
|
12357
|
+
const inserts = writePlan.filter((parent) => parent.mode === "INSERT");
|
|
12358
|
+
const updates = writePlan.filter((parent) => parent.mode === "UPDATE");
|
|
12359
|
+
const createdIds = [];
|
|
12360
|
+
for (let i = 0; i < inserts.length; i += 100) {
|
|
12361
|
+
const response = await client.postRecords({ app: stmt.appId, records: inserts.slice(i, i + 100).map(payloadFor) });
|
|
12362
|
+
createdIds.push(response.ids);
|
|
12363
|
+
}
|
|
12364
|
+
for (let i = 0; i < updates.length; i += 100) await client.putRecords({
|
|
12365
|
+
app: stmt.appId,
|
|
12366
|
+
records: updates.slice(i, i + 100).map((parent) => ({ id: parent.targetId, ...parent.revision === void 0 ? {} : { revision: parent.revision }, record: payloadFor(parent) }))
|
|
12367
|
+
});
|
|
12368
|
+
return stmt.keyFields ? { type: "UPSERT", insertedCount: createdIds.flat().length, updatedCount: updates.length, affectedRows: writePlan.length, skippedRows: prepared.invalidParentRows.size, rejectLimit: stmt.rejectLimit, ...stmt.errorTable ? { errTable: stmt.errorTable } : {}, importDetail } : { type: "INSERT", createdIds, insertedCount: createdIds.flat().length, affectedRows: writePlan.length, skippedRows: prepared.invalidParentRows.size, rejectLimit: stmt.rejectLimit, ...stmt.errorTable ? { errTable: stmt.errorTable } : {}, importDetail };
|
|
12369
|
+
}
|
|
12370
|
+
if (stmt.writeMode === "UPDATE_RECORD_NUMBER") {
|
|
12371
|
+
return executeImportRecordNumberUpdate(
|
|
12372
|
+
stmt,
|
|
12373
|
+
handle,
|
|
12374
|
+
client,
|
|
12375
|
+
options,
|
|
12376
|
+
cacheContext,
|
|
12377
|
+
tempTables
|
|
12378
|
+
);
|
|
12379
|
+
}
|
|
12380
|
+
const common = {
|
|
12381
|
+
appId: stmt.appId,
|
|
12382
|
+
fields: stmt.fields,
|
|
12383
|
+
select: importPlaceholderSelect(),
|
|
12384
|
+
validateOnly: stmt.validateOnly,
|
|
12385
|
+
validationErrorTable: stmt.validationErrorTable,
|
|
12386
|
+
onErrorSkip: stmt.onErrorSkip,
|
|
12387
|
+
errorTable: stmt.errorTable,
|
|
12388
|
+
rejectLimit: stmt.rejectLimit,
|
|
12389
|
+
checkGroups: stmt.checkGroups
|
|
12390
|
+
};
|
|
12391
|
+
const generated = stmt.keyFields ? { type: "UPSERT_SELECT", ...common, keyFields: stmt.keyFields } : { type: "INSERT_SELECT", ...common };
|
|
12392
|
+
const executionSource = { source: stmt.source, handle, cache: /* @__PURE__ */ new Map() };
|
|
12393
|
+
importSourceByDmlStatement.set(generated, executionSource);
|
|
12394
|
+
const withAudit = (result2) => {
|
|
12395
|
+
if (executionSource.audit) Object.assign(result2, { importAudit: executionSource.audit });
|
|
12396
|
+
return result2;
|
|
12397
|
+
};
|
|
12398
|
+
if (generated.validateOnly) {
|
|
12399
|
+
if (generated.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12400
|
+
const result2 = await executeDmlValidation(generated, client, { ...options, onLimitReached: "error" }, cacheContext, tempTables, 1);
|
|
12401
|
+
if (generated.validationErrorTable && tempTables) {
|
|
12402
|
+
appendValidationErrors(
|
|
12403
|
+
tempTables,
|
|
12404
|
+
generated.validationErrorTable,
|
|
12405
|
+
result2.columns,
|
|
12406
|
+
result2.errors,
|
|
12407
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12408
|
+
materializedMetaByValidationResult.get(result2) ?? /* @__PURE__ */ new Map()
|
|
12409
|
+
);
|
|
12410
|
+
}
|
|
12411
|
+
return withAudit(result2);
|
|
12412
|
+
}
|
|
12413
|
+
if (generated.onErrorSkip) {
|
|
12414
|
+
if (!tempTables) throw new Error("ArgumentError: ON ERROR SKIP requires a batch.");
|
|
12415
|
+
const result2 = await (generated.type === "UPSERT_SELECT" ? executeOnErrorSkip(generated, client, options, cacheContext, tempTables, 1) : executeOnErrorSkip(generated, client, options, cacheContext, tempTables, 1));
|
|
12416
|
+
return withAudit(result2);
|
|
12417
|
+
}
|
|
12418
|
+
const result = await (generated.type === "UPSERT_SELECT" ? executeUpsertSelect(generated, client, options, cacheContext, tempTables) : executeInsertSelect(generated, client, options, cacheContext, tempTables));
|
|
12419
|
+
return withAudit(result);
|
|
12420
|
+
}
|
|
12421
|
+
async function executeCsvSubtableReplacement(stmt, materialized, preparedBase, fieldInfos, client, options, tempTables) {
|
|
12422
|
+
if (!stmt.recordNumberSourceHeader || !stmt.replaceSubtables?.length) throw new Error("InternalError: incomplete CSV subtable replacement AST.");
|
|
12423
|
+
assertNoDuplicateCsvSubtableRowIds(materialized.records);
|
|
12424
|
+
const rawKeys = materialized.records.map((record) => record.recordNumberSourceValue ?? "");
|
|
12425
|
+
const keyPlan = preflightImportRecordNumbers(rawKeys, stmt.recordNumberSourceHeader);
|
|
12426
|
+
const tableCodes = [...stmt.replaceSubtables];
|
|
12427
|
+
const ownershipTableCodes = [...new Set(fieldInfos.filter((info) => !info.inSubtable && info.fieldType === "SUBTABLE").map((info) => info.code))];
|
|
12428
|
+
const allRecords = await fetchAll(client.getRecords, stmt.appId, "", ["$id", "$revision", ...ownershipTableCodes], {
|
|
12429
|
+
maxRecords: options.maxRecords ?? 1e4,
|
|
12430
|
+
parallel: options.fetchParallel ?? 1,
|
|
12431
|
+
onLimit: "error"
|
|
12432
|
+
});
|
|
12433
|
+
const existingById = /* @__PURE__ */ new Map();
|
|
12434
|
+
const ownership = /* @__PURE__ */ new Map();
|
|
12435
|
+
for (const record of allRecords) {
|
|
12436
|
+
const id = Number(record["$id"]?.value);
|
|
12437
|
+
const revision = Number(record["$revision"]?.value);
|
|
12438
|
+
if (!Number.isFinite(id)) continue;
|
|
12439
|
+
existingById.set(id, { id, ...Number.isFinite(revision) ? { revision } : {}, record });
|
|
12440
|
+
for (const table of ownershipTableCodes) {
|
|
12441
|
+
const rows = record[table]?.value;
|
|
12442
|
+
if (!Array.isArray(rows)) continue;
|
|
12443
|
+
for (const row of rows) if (row.id) {
|
|
12444
|
+
const owners = ownership.get(row.id) ?? [];
|
|
12445
|
+
owners.push({ parentId: id, table });
|
|
12446
|
+
ownership.set(row.id, owners);
|
|
12447
|
+
}
|
|
12448
|
+
}
|
|
12449
|
+
}
|
|
12450
|
+
const targetIds = keyPlan.normalized.map((key) => key === null ? void 0 : Number(key));
|
|
12451
|
+
const parents = preparedBase.parents.map((parent, index) => {
|
|
12452
|
+
const errors2 = [...parent.errors];
|
|
12453
|
+
for (const error of keyPlan.errors[index]) errors2.push({
|
|
12454
|
+
operation: "UPDATE",
|
|
12455
|
+
parentRow: parent.parentRow,
|
|
12456
|
+
field: error.field,
|
|
12457
|
+
code: error.code,
|
|
12458
|
+
message: error.message,
|
|
12459
|
+
sourceValues: materialized.records[index].top
|
|
12460
|
+
});
|
|
12461
|
+
const targetId = targetIds[index];
|
|
12462
|
+
if (targetId !== void 0 && !existingById.has(targetId)) errors2.push({
|
|
12463
|
+
operation: "UPDATE",
|
|
12464
|
+
parentRow: parent.parentRow,
|
|
12465
|
+
field: stmt.recordNumberSourceHeader,
|
|
12466
|
+
code: "ERR_RECORD_NUMBER_NOT_FOUND",
|
|
12467
|
+
message: `record number ${targetId} does not exist in APP${stmt.appId}`,
|
|
12468
|
+
sourceValues: materialized.records[index].top
|
|
12469
|
+
});
|
|
12470
|
+
return { ...parent, valid: errors2.length === 0, errors: errors2 };
|
|
12471
|
+
});
|
|
12472
|
+
const initialPlan = buildCsvSubtableReplacementPlan(materialized.records, parents, targetIds, existingById, ownership);
|
|
12473
|
+
const planErrors = initialPlan.flatMap((parent) => [...parent.errors]);
|
|
12474
|
+
const invalidParentRows = new Set(initialPlan.filter((parent) => !parent.valid).map((parent) => parent.parentRow));
|
|
12475
|
+
const prepared = { ...preparedBase, parents, errors: planErrors, invalidParentRows };
|
|
12476
|
+
assertImportRejectLimit(prepared, stmt.rejectLimit);
|
|
12477
|
+
const validPlan = initialPlan.filter((parent) => parent.valid);
|
|
12478
|
+
const allTables = initialPlan.flatMap((parent) => parent.tables);
|
|
12479
|
+
const sum = (table, key) => allTables.filter((item) => item.table === table).reduce((n, item) => n + Number(item[key]), 0);
|
|
12480
|
+
const tableDetail = Object.fromEntries(tableCodes.map((table) => [table, {
|
|
12481
|
+
existingRows: sum(table, "existingRows"),
|
|
12482
|
+
inputRows: sum(table, "inputRows"),
|
|
12483
|
+
updateRows: sum(table, "updateRows"),
|
|
12484
|
+
addRows: sum(table, "addRows"),
|
|
12485
|
+
deleteRows: sum(table, "deleteRows"),
|
|
12486
|
+
rowIdNotFound: sum(table, "rowIdNotFound")
|
|
12487
|
+
}]));
|
|
12488
|
+
const importDetail = {
|
|
12489
|
+
kind: "IMPORT_CSV_SUBTABLE_REPLACE",
|
|
12490
|
+
rowIdPolicy: "PRESERVE_EXISTING",
|
|
12491
|
+
parentsToWrite: validPlan.length,
|
|
12492
|
+
insertedParents: 0,
|
|
12493
|
+
updatedParents: validPlan.length,
|
|
12494
|
+
hasDeletes: validPlan.some((parent) => parent.tables.some((table) => table.deleteRows > 0)),
|
|
12495
|
+
totalDeleteRows: validPlan.flatMap((parent) => parent.tables).reduce((n, table) => n + table.deleteRows, 0),
|
|
12496
|
+
rowIdNotFound: validPlan.flatMap((parent) => parent.tables).reduce((n, table) => n + table.rowIdNotFound, 0),
|
|
12497
|
+
invalidParents: invalidParentRows.size,
|
|
12498
|
+
parents: validPlan.map((parent) => ({ parentRow: parent.parentRow, mode: "UPDATE", targetId: parent.targetId, tables: parent.tables }))
|
|
12499
|
+
};
|
|
12500
|
+
const payloadFields = [...new Set(stmt.targets.flatMap((target) => target.kind === "FIELD" ? [target.field] : target.children))];
|
|
12501
|
+
const errors = materializeImportValidationErrors(planErrors, payloadFields);
|
|
12502
|
+
const columns = [...payloadFields, ...IMPORT_VALIDATION_META_COLUMNS];
|
|
12503
|
+
if (stmt.validateOnly) {
|
|
12504
|
+
if (stmt.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12505
|
+
if (stmt.validationErrorTable && tempTables) appendValidationErrors(tempTables, stmt.validationErrorTable, columns, errors, options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS, /* @__PURE__ */ new Map());
|
|
12506
|
+
return {
|
|
12507
|
+
type: "VALIDATION",
|
|
12508
|
+
operation: "UPDATE",
|
|
12509
|
+
validatedRows: parents.length,
|
|
12510
|
+
validRows: parents.length - invalidParentRows.size,
|
|
12511
|
+
invalidRows: invalidParentRows.size,
|
|
12512
|
+
errorCount: errors.length,
|
|
12513
|
+
columns,
|
|
12514
|
+
errors,
|
|
12515
|
+
...stmt.validationErrorTable ? { errTable: stmt.validationErrorTable } : {},
|
|
12516
|
+
importDetail: { preflight: "ACTUAL_DATA", parents: { total: parents.length, valid: validPlan.length, invalid: invalidParentRows.size, mutationCandidates: validPlan.length }, tables: tableDetail, rowIdPolicy: "PRESERVE_EXISTING", rowIdNotFound: importDetail.rowIdNotFound, writesKintone: false }
|
|
12517
|
+
};
|
|
12518
|
+
}
|
|
12519
|
+
if (planErrors.length && !stmt.onErrorSkip) {
|
|
12520
|
+
const first = planErrors[0];
|
|
12521
|
+
throw new Error(`DmlValidationError: ${first.code} ${first.message} (row=${first.parentRow}, field=${first.field})`);
|
|
12522
|
+
}
|
|
12523
|
+
if (stmt.onErrorSkip) {
|
|
12524
|
+
if (!tempTables || !stmt.errorTable) throw new Error("ArgumentError: ON ERROR SKIP requires a batch and INTO error table.");
|
|
12525
|
+
appendValidationErrors(tempTables, stmt.errorTable, columns, errors, options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS, /* @__PURE__ */ new Map());
|
|
12526
|
+
}
|
|
12527
|
+
if (validPlan.length) {
|
|
12528
|
+
if (!options.supportsImportConfirmDetail || !options.confirm) throw new Error("UnsupportedError: CSV subtable replacement requires explicit rendered detail approval.");
|
|
12529
|
+
const ok = await options.confirm(validPlan.length, "UPDATE", { statementIndex: 0, statementCount: 1, statementType: "IMPORT", targetAppId: stmt.appId, importDetail });
|
|
12530
|
+
if (!ok) throw new OperationCancelledError("UPDATE", validPlan.length);
|
|
12531
|
+
}
|
|
12532
|
+
const scalarMap = (record) => new Map(Object.entries(record).map(([code, field]) => [code, field.value]));
|
|
12533
|
+
for (const parent of validPlan) {
|
|
12534
|
+
const record = buildImportRecordPayload(scalarMap(parent.top), new Map([...parent.subtables].map(([table, rows]) => [table, rows.map((row) => ({ ...row.rowId ? { rowId: row.rowId } : {}, values: scalarMap(row.record) }))])), "PRESERVE");
|
|
12535
|
+
await client.putRecords({ app: stmt.appId, records: [{ id: parent.targetId, ...parent.revision === void 0 ? {} : { revision: parent.revision }, record }] });
|
|
12536
|
+
}
|
|
12537
|
+
return { type: "UPDATE", updatedCount: validPlan.length, affectedRows: validPlan.length, skippedRows: invalidParentRows.size, rejectLimit: stmt.rejectLimit, ...stmt.errorTable ? { errTable: stmt.errorTable } : {}, importDetail };
|
|
12538
|
+
}
|
|
12539
|
+
async function executeImportRecordNumberUpdate(stmt, handle, client, options, cacheContext, tempTables) {
|
|
12540
|
+
if (stmt.source.kind !== "CSV" || stmt.source.mappingMode !== "BY_NAME" || !stmt.recordNumberSourceHeader) {
|
|
12541
|
+
throw new Error("InternalError: invalid IMPORT UPDATE AST.");
|
|
12542
|
+
}
|
|
12543
|
+
if (new Set(stmt.fields).size !== stmt.fields.length) throw new Error("ArgumentError: DML target fields contain duplicates.");
|
|
12544
|
+
const fieldInfos = await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
|
|
12545
|
+
const numberPrecision = await loadNumberPrecisionForTargets(stmt.appId, stmt.fields, fieldInfos, client, cacheContext);
|
|
12546
|
+
const payload = await loadImportSource(handle, /* @__PURE__ */ new Map());
|
|
12547
|
+
const sourceTable = materializeCsvDmlSource(
|
|
12548
|
+
stmt.source,
|
|
12549
|
+
payload,
|
|
12550
|
+
options.maxRecords ?? 1e4,
|
|
12551
|
+
stmt.fields,
|
|
12552
|
+
fieldInfos,
|
|
12553
|
+
stmt.recordNumberSourceHeader
|
|
12554
|
+
);
|
|
12555
|
+
const keyValues = sourceTable.recordNumberSourceValues;
|
|
12556
|
+
if (!keyValues) throw new Error("InternalError: record-number source values were not materialized.");
|
|
12557
|
+
const keyPlan = preflightImportRecordNumbers(keyValues, stmt.recordNumberSourceHeader);
|
|
12558
|
+
const matchedIds = /* @__PURE__ */ new Set();
|
|
12559
|
+
const lookupKeys = [...new Set(keyPlan.normalized.filter((key) => key !== null))];
|
|
12560
|
+
for (let i = 0; i < lookupKeys.length; i += 100) {
|
|
12561
|
+
const chunk2 = lookupKeys.slice(i, i + 100);
|
|
12562
|
+
const response = await client.getRecords({
|
|
12563
|
+
app: stmt.appId,
|
|
12564
|
+
query: `$id in (${chunk2.join(",")}) limit 500`,
|
|
12565
|
+
fields: ["$id"]
|
|
12566
|
+
});
|
|
12567
|
+
for (const record of response.records) {
|
|
12568
|
+
const id = record["$id"]?.value;
|
|
12569
|
+
if (typeof id === "string" && id !== "") matchedIds.add(id.replace(/^0+(?=\d)/, ""));
|
|
12570
|
+
}
|
|
12571
|
+
}
|
|
12572
|
+
const infoByCode = new Map(fieldInfos.map((info) => [info.code, info]));
|
|
12573
|
+
const evaluationTypes = new Map(stmt.fields.map((field) => [field, infoByCode.get(field)?.fieldType ?? "SINGLE_LINE_TEXT"]));
|
|
12574
|
+
const candidates = sourceTable.rows.map((row, index) => {
|
|
12575
|
+
const key = keyPlan.normalized[index];
|
|
12576
|
+
const preErrors = [
|
|
12577
|
+
...sourceTable.importRowErrors?.[index] ?? [],
|
|
12578
|
+
...keyPlan.errors[index]
|
|
12579
|
+
];
|
|
12580
|
+
if (key !== null && !matchedIds.has(key)) preErrors.push({
|
|
12581
|
+
field: stmt.recordNumberSourceHeader,
|
|
12582
|
+
code: "ERR_RECORD_NUMBER_NOT_FOUND",
|
|
12583
|
+
message: `record number ${key} does not exist in APP${stmt.appId}`
|
|
12584
|
+
});
|
|
12585
|
+
return {
|
|
12586
|
+
rowNumber: index + 1,
|
|
12587
|
+
operation: "UPDATE",
|
|
12588
|
+
mode: "update",
|
|
12589
|
+
...key !== null && matchedIds.has(key) ? { targetId: Number(key) } : {},
|
|
12590
|
+
payload: new Map([
|
|
12591
|
+
[stmt.recordNumberSourceHeader, keyValues[index]],
|
|
12592
|
+
...stmt.fields.map((field) => [field, row[field] ?? ""])
|
|
12593
|
+
]),
|
|
12594
|
+
preErrors,
|
|
12595
|
+
record: {},
|
|
12596
|
+
evaluationRow: row,
|
|
12597
|
+
evaluationFieldTypes: evaluationTypes
|
|
12598
|
+
};
|
|
12599
|
+
});
|
|
12600
|
+
const diagnosticFields = [stmt.recordNumberSourceHeader, ...stmt.fields];
|
|
12601
|
+
const validation = validateDmlCandidates(
|
|
12602
|
+
candidates,
|
|
12603
|
+
"UPDATE",
|
|
12604
|
+
diagnosticFields,
|
|
12605
|
+
stmt.fields,
|
|
12606
|
+
fieldInfos,
|
|
12607
|
+
1,
|
|
12608
|
+
numberPrecision,
|
|
12609
|
+
stmt.checkGroups ?? [],
|
|
12610
|
+
false
|
|
12611
|
+
);
|
|
12612
|
+
const columns = [...diagnosticFields, ...VALIDATION_META_COLUMNS];
|
|
12613
|
+
const validationResult = {
|
|
12614
|
+
type: "VALIDATION",
|
|
12615
|
+
operation: "UPDATE",
|
|
12616
|
+
validatedRows: candidates.length,
|
|
12617
|
+
validRows: candidates.length - validation.invalidRows,
|
|
12618
|
+
invalidRows: validation.invalidRows,
|
|
12619
|
+
errorCount: validation.errors.length,
|
|
12620
|
+
columns,
|
|
12621
|
+
errors: validation.errors,
|
|
12622
|
+
...stmt.validationErrorTable ?? stmt.errorTable ? { errTable: stmt.validationErrorTable ?? stmt.errorTable } : {}
|
|
12623
|
+
};
|
|
12624
|
+
Object.assign(validationResult, { importAudit: sourceTable.importAudit });
|
|
12625
|
+
if (stmt.validateOnly) {
|
|
12626
|
+
if (stmt.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12627
|
+
if (stmt.validationErrorTable && tempTables) appendValidationErrors(
|
|
12628
|
+
tempTables,
|
|
12629
|
+
stmt.validationErrorTable,
|
|
12630
|
+
columns,
|
|
12631
|
+
validation.errors,
|
|
12632
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12633
|
+
/* @__PURE__ */ new Map()
|
|
12634
|
+
);
|
|
12635
|
+
return validationResult;
|
|
12636
|
+
}
|
|
12637
|
+
if (!stmt.onErrorSkip && validation.invalidRows > 0) {
|
|
12638
|
+
const first = validation.errors[0];
|
|
12639
|
+
throw new Error(`DmlValidationError: ${first.$err_code} ${first.$err_message} (row=${first.$err_row}, field=${first.$err_field})`);
|
|
12640
|
+
}
|
|
12641
|
+
if (stmt.onErrorSkip) {
|
|
12642
|
+
if (!tempTables || !stmt.errorTable) throw new Error("ArgumentError: ON ERROR SKIP requires a batch.");
|
|
12643
|
+
appendValidationErrors(
|
|
12644
|
+
tempTables,
|
|
12645
|
+
stmt.errorTable,
|
|
12646
|
+
columns,
|
|
12647
|
+
validation.errors,
|
|
12648
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12649
|
+
/* @__PURE__ */ new Map()
|
|
12650
|
+
);
|
|
12651
|
+
if (stmt.rejectLimit != null && validation.invalidRows > stmt.rejectLimit) {
|
|
12652
|
+
throw new RejectLimitExceededError(
|
|
12653
|
+
`rejected rows (${validation.invalidRows}) exceed REJECT LIMIT (${stmt.rejectLimit}).`,
|
|
12654
|
+
validationResult
|
|
12655
|
+
);
|
|
12656
|
+
}
|
|
12657
|
+
}
|
|
12658
|
+
const valid = candidates.filter((candidate) => !validation.invalidRowNumbers.has(candidate.rowNumber));
|
|
12659
|
+
if (options.confirm) {
|
|
12660
|
+
const ok = await options.confirm(valid.length, "UPDATE");
|
|
12661
|
+
if (!ok) throw new OperationCancelledError("UPDATE", valid.length);
|
|
12662
|
+
}
|
|
12663
|
+
const updates = valid.map((candidate) => ({ id: candidate.targetId, record: candidate.record }));
|
|
12664
|
+
for (let i = 0; i < updates.length; i += 100) {
|
|
12665
|
+
await client.putRecords({ app: stmt.appId, records: updates.slice(i, i + 100) });
|
|
12666
|
+
}
|
|
12667
|
+
const result = {
|
|
12668
|
+
type: "UPDATE",
|
|
12669
|
+
updatedCount: updates.length,
|
|
12670
|
+
...stmt.onErrorSkip ? {
|
|
12671
|
+
affectedRows: updates.length,
|
|
12672
|
+
skippedRows: validation.invalidRows,
|
|
12673
|
+
rejectLimit: stmt.rejectLimit ?? null,
|
|
12674
|
+
errTable: stmt.errorTable
|
|
12675
|
+
} : {}
|
|
12676
|
+
};
|
|
12677
|
+
Object.assign(result, { insertedCount: 0, importAudit: sourceTable.importAudit });
|
|
12678
|
+
return result;
|
|
12679
|
+
}
|
|
12680
|
+
async function materializeDmlSource(stmt, client, options, cacheContext, tempTables, targetFields) {
|
|
12681
|
+
const imported = importSourceByDmlStatement.get(stmt);
|
|
12682
|
+
if (!imported) {
|
|
12683
|
+
const selected2 = tempTables && tempTables.size > 0 ? await executeQueryWithCte(stmt.select, client, options, tempTables, cacheContext, true) : await executeSelect(stmt.select, client, options, cacheContext, void 0, true);
|
|
12684
|
+
return { rows: selected2.rows, columns: selected2.columns, columnMeta: materializedMetaBySelectResult.get(selected2) };
|
|
12685
|
+
}
|
|
12686
|
+
const payload = await loadImportSource(imported.handle, imported.cache);
|
|
12687
|
+
const rowLimit = options.maxRecords ?? 1e4;
|
|
12688
|
+
const targetByCode = new Map((targetFields ?? []).map((info) => [info.code, info]));
|
|
12689
|
+
const jsonTargets = stmt.fields.map((code) => ({ code, fieldType: targetByCode.get(code)?.fieldType ?? "SINGLE_LINE_TEXT" }));
|
|
12690
|
+
const raw = imported.source.kind === "JSON" ? materializeJsonDmlSource(imported.source, payload, jsonTargets, rowLimit) : materializeCsvDmlSource(imported.source, payload, rowLimit, stmt.fields, targetFields);
|
|
12691
|
+
imported.audit = raw.importAudit;
|
|
12692
|
+
if (imported.source.kind === "JSON") return raw;
|
|
12693
|
+
if (!imported.source.projection) return raw;
|
|
12694
|
+
const projection = bindImportProjection(imported.source.projection);
|
|
12695
|
+
const tables = new Map(tempTables ?? []);
|
|
12696
|
+
tables.set(IMPORT_PROJECTION_SOURCE, raw);
|
|
12697
|
+
const selected = await executeQueryWithCte(projection, client, { ...options, onLimitReached: "error" }, tables, cacheContext, true);
|
|
12698
|
+
return { rows: selected.rows, columns: selected.columns, columnMeta: materializedMetaBySelectResult.get(selected) };
|
|
12699
|
+
}
|
|
12700
|
+
var dmlSourceMaterializer = { materialize: materializeDmlSource };
|
|
12701
|
+
function assertNoImportRowErrors(table) {
|
|
12702
|
+
for (let rowIndex = 0; rowIndex < (table.importRowErrors?.length ?? 0); rowIndex++) {
|
|
12703
|
+
const first = table.importRowErrors?.[rowIndex]?.[0];
|
|
12704
|
+
if (first) {
|
|
12705
|
+
throw new Error(`DmlValidationError: ${first.code} ${first.message} (row=${rowIndex + 1}, field=${first.field})`);
|
|
12706
|
+
}
|
|
12707
|
+
}
|
|
12708
|
+
}
|
|
10737
12709
|
async function executeInsertSelect(stmt, client, options, cacheContext, cteCache) {
|
|
10738
12710
|
if (stmt.checkGroups?.length) return executeCheckedPlainDml(stmt, client, options, cacheContext, cteCache);
|
|
10739
12711
|
const fieldInfos = await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
|
|
10740
12712
|
const numberPrecision = await loadNumberPrecisionForTargets(stmt.appId, stmt.fields, fieldInfos, client, cacheContext);
|
|
10741
|
-
const
|
|
10742
|
-
const { rows, columns } =
|
|
12713
|
+
const sourceTable = await dmlSourceMaterializer.materialize(stmt, client, options, cacheContext, cteCache, fieldInfos);
|
|
12714
|
+
const { rows, columns } = sourceTable;
|
|
12715
|
+
assertNoImportRowErrors(sourceTable);
|
|
10743
12716
|
if (columns.length !== stmt.fields.length) {
|
|
10744
12717
|
const emptySourceHint = columns.length === 0 && rows.length === 0 ? "\u3002\u7D50\u679C\u304C 0 \u884C\u306E\u305F\u3081\u5217\u3092\u7279\u5B9A\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08SELECT * \u3092\u7A7A\u30BD\u30FC\u30B9\u306B\u4F7F\u3046\u3068\u5217\u3092\u6C7A\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002\u660E\u793A\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF09" : "";
|
|
10745
12718
|
throw new Error(
|
|
@@ -10751,15 +12724,16 @@ async function executeInsertSelect(stmt, client, options, cacheContext, cteCache
|
|
|
10751
12724
|
if (!ok) throw new OperationCancelledError("INSERT", rows.length);
|
|
10752
12725
|
}
|
|
10753
12726
|
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
10754
|
-
const allRecords = rows.map((row) => {
|
|
12727
|
+
const allRecords = rows.map((row, rowIndex) => {
|
|
10755
12728
|
const record = {};
|
|
10756
12729
|
stmt.fields.forEach((field, i) => {
|
|
12730
|
+
if (sourceTable.importPresence && !sourceTable.importPresence[rowIndex]?.has(field)) return;
|
|
10757
12731
|
const raw = row[columns[i]] ?? "";
|
|
10758
12732
|
record[field] = { value: convertProcessRowValue(raw, fieldTypes.get(field)) };
|
|
10759
12733
|
});
|
|
10760
12734
|
return record;
|
|
10761
12735
|
});
|
|
10762
|
-
assertValidDmlRecords(
|
|
12736
|
+
allRecords.forEach((record) => assertValidDmlRecords([record], stmt.fields.filter((field) => field in record), fieldInfos, numberPrecision));
|
|
10763
12737
|
const createdIds = [];
|
|
10764
12738
|
for (let i = 0; i < allRecords.length; i += 100) {
|
|
10765
12739
|
const batch = allRecords.slice(i, i + 100);
|
|
@@ -10773,6 +12747,26 @@ async function executeInsertSelect(stmt, client, options, cacheContext, cteCache
|
|
|
10773
12747
|
};
|
|
10774
12748
|
}
|
|
10775
12749
|
async function executeUpdate(stmt, client, options, cacheContext, tempTables) {
|
|
12750
|
+
if (stmt.checkGroups?.length && isConstantFalseWhere(stmt.where)) {
|
|
12751
|
+
const fieldInfos2 = await loadWritableTopLevelDmlFields(
|
|
12752
|
+
stmt.appId,
|
|
12753
|
+
stmt.assignments.map((assignment) => assignment.field),
|
|
12754
|
+
client,
|
|
12755
|
+
cacheContext
|
|
12756
|
+
);
|
|
12757
|
+
await loadNumberPrecisionForTargets(
|
|
12758
|
+
stmt.appId,
|
|
12759
|
+
stmt.assignments.map((assignment) => assignment.field),
|
|
12760
|
+
fieldInfos2,
|
|
12761
|
+
client,
|
|
12762
|
+
cacheContext
|
|
12763
|
+
);
|
|
12764
|
+
const fieldTypes2 = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
12765
|
+
assertUpdateCheckRefs(stmt, fieldTypes2);
|
|
12766
|
+
assertCheckComparisonTypes(stmt, updateEvaluationTypes(fieldTypes2, stmt.appId));
|
|
12767
|
+
await assertDmlWhereCapability(stmt, client, cacheContext);
|
|
12768
|
+
return { type: "UPDATE", updatedCount: 0 };
|
|
12769
|
+
}
|
|
10776
12770
|
if (stmt.checkGroups?.length) return executeCheckedPlainDml(stmt, client, options, cacheContext, tempTables);
|
|
10777
12771
|
if (stmt.subtableCode) {
|
|
10778
12772
|
await assertDmlWhereCapability(stmt, client, cacheContext);
|
|
@@ -10793,6 +12787,7 @@ async function executeUpdate(stmt, client, options, cacheContext, tempTables) {
|
|
|
10793
12787
|
cacheContext
|
|
10794
12788
|
);
|
|
10795
12789
|
await assertDmlWhereCapability(stmt, client, cacheContext);
|
|
12790
|
+
if (isConstantFalseWhere(stmt.where)) return { type: "UPDATE", updatedCount: 0 };
|
|
10796
12791
|
if (stmt.from != null) {
|
|
10797
12792
|
return executeUpdateFrom(stmt, stmt.from, client, options, cacheContext, tempTables);
|
|
10798
12793
|
}
|
|
@@ -10874,6 +12869,7 @@ function collectUpdateFromTargetFields(stmt) {
|
|
|
10874
12869
|
}
|
|
10875
12870
|
async function executeDelete(stmt, client, options, cacheContext) {
|
|
10876
12871
|
await assertDmlWhereCapability(stmt, client, cacheContext);
|
|
12872
|
+
if (isConstantFalseWhere(stmt.where)) return { type: "DELETE", deletedCount: 0 };
|
|
10877
12873
|
if (stmt.subtableCode) {
|
|
10878
12874
|
return executeDeleteSubtable(stmt, client, options, cacheContext);
|
|
10879
12875
|
}
|
|
@@ -11130,9 +13126,9 @@ function expandRowsForSubtableDml(parents, subtableCode) {
|
|
|
11130
13126
|
for (const parent of parents) {
|
|
11131
13127
|
const parentId = String(parent["$id"]?.value ?? "");
|
|
11132
13128
|
const parentRevision = getRevision(parent);
|
|
11133
|
-
const
|
|
11134
|
-
for (let i = 0; i <
|
|
11135
|
-
const row =
|
|
13129
|
+
const tableRows2 = getMutableTableRows(parent, subtableCode);
|
|
13130
|
+
for (let i = 0; i < tableRows2.length; i++) {
|
|
13131
|
+
const row = tableRows2[i];
|
|
11136
13132
|
const flat = {
|
|
11137
13133
|
_pid: parentId,
|
|
11138
13134
|
_rid: row.id ?? "",
|
|
@@ -11256,6 +13252,7 @@ async function executeReorder(stmt, client, options, cacheContext) {
|
|
|
11256
13252
|
cacheContext
|
|
11257
13253
|
);
|
|
11258
13254
|
const reorderFields = await getFieldsCached(stmt.appId, client, cacheContext);
|
|
13255
|
+
if (isConstantFalseWhere(stmt.where)) return { type: "REORDER", reorderedParentCount: 0 };
|
|
11259
13256
|
const reorderSemanticsByCode = new Map(reorderFields.map((field) => [
|
|
11260
13257
|
field.code,
|
|
11261
13258
|
field.semantics ?? resolveFieldSemantics(field)
|
|
@@ -11337,8 +13334,9 @@ async function executeUpsertSelect(stmt, client, options, cacheContext, cteCache
|
|
|
11337
13334
|
if (stmt.checkGroups?.length) return executeCheckedPlainDml(stmt, client, options, cacheContext, cteCache);
|
|
11338
13335
|
const fieldInfos = await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
|
|
11339
13336
|
const numberPrecision = await loadNumberPrecisionForTargets(stmt.appId, stmt.fields, fieldInfos, client, cacheContext);
|
|
11340
|
-
const
|
|
11341
|
-
const { rows, columns } =
|
|
13337
|
+
const sourceTable = await dmlSourceMaterializer.materialize(stmt, client, options, cacheContext, cteCache, fieldInfos);
|
|
13338
|
+
const { rows, columns } = sourceTable;
|
|
13339
|
+
assertNoImportRowErrors(sourceTable);
|
|
11342
13340
|
if (columns.length !== stmt.fields.length) {
|
|
11343
13341
|
const emptySourceHint = columns.length === 0 && rows.length === 0 ? "\u3002\u7D50\u679C\u304C 0 \u884C\u306E\u305F\u3081\u5217\u3092\u7279\u5B9A\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08SELECT * \u3092\u7A7A\u30BD\u30FC\u30B9\u306B\u4F7F\u3046\u3068\u5217\u3092\u6C7A\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002\u660E\u793A\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF09" : "";
|
|
11344
13342
|
throw new Error(
|
|
@@ -11352,18 +13350,29 @@ async function executeUpsertSelect(stmt, client, options, cacheContext, cteCache
|
|
|
11352
13350
|
}
|
|
11353
13351
|
const toInsert = [];
|
|
11354
13352
|
const toUpdate = [];
|
|
11355
|
-
const
|
|
13353
|
+
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
13354
|
+
const records = rows.map((row, rowIndex) => {
|
|
11356
13355
|
const record = {};
|
|
11357
13356
|
stmt.fields.forEach((field, i) => {
|
|
11358
|
-
|
|
13357
|
+
if (sourceTable.importPresence && !sourceTable.importPresence[rowIndex]?.has(field)) return;
|
|
13358
|
+
const raw = row[columns[i]] ?? "";
|
|
13359
|
+
record[field] = { value: convertProcessRowValue(raw, fieldTypes.get(field)) };
|
|
11359
13360
|
});
|
|
11360
13361
|
return record;
|
|
11361
13362
|
});
|
|
11362
|
-
assertValidDmlRecords(
|
|
11363
|
-
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
13363
|
+
records.forEach((record) => assertValidDmlRecords([record], stmt.fields.filter((field) => field in record), fieldInfos, numberPrecision));
|
|
11364
13364
|
const rowKeyValues = records.map(
|
|
11365
13365
|
(record) => stmt.keyFields.map((key) => String(record[key]?.value ?? ""))
|
|
11366
13366
|
);
|
|
13367
|
+
if (importSourceByDmlStatement.has(stmt)) {
|
|
13368
|
+
const numericKey = stmt.keyFields.map((key) => fieldTypes.get(key) === "NUMBER");
|
|
13369
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
13370
|
+
for (const parts of rowKeyValues) {
|
|
13371
|
+
const normalized = upsertNormalizedKey(parts, numericKey);
|
|
13372
|
+
if (sourceKeys.has(normalized)) throw new Error("ERR_KEY_DUP_SOURCE: UPSERT \u30BD\u30FC\u30B9\u5185\u3067\u30AD\u30FC\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059");
|
|
13373
|
+
sourceKeys.add(normalized);
|
|
13374
|
+
}
|
|
13375
|
+
}
|
|
11367
13376
|
const targetIndex = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeyValues, client, options, fieldTypes);
|
|
11368
13377
|
records.forEach((record, rowIdx) => {
|
|
11369
13378
|
const id = lookupUpsertTarget(targetIndex, rowKeyValues[rowIdx]);
|
|
@@ -11412,10 +13421,10 @@ async function executeDescribe(stmt, client, cacheContext) {
|
|
|
11412
13421
|
}));
|
|
11413
13422
|
return { type: "SELECT", rows, columns, rowCount: rows.length };
|
|
11414
13423
|
}
|
|
11415
|
-
function parseSql(sql) {
|
|
13424
|
+
function parseSql(sql, enableImport = false) {
|
|
11416
13425
|
try {
|
|
11417
13426
|
const tokens = new Lexer(sql).tokenize();
|
|
11418
|
-
const stmt = new Parser(tokens).parse();
|
|
13427
|
+
const stmt = new Parser(tokens, { import: enableImport }).parse();
|
|
11419
13428
|
validateKlikeStatement(stmt);
|
|
11420
13429
|
return stmt;
|
|
11421
13430
|
} catch (e) {
|
|
@@ -11482,6 +13491,8 @@ function collectSubqueryTasks(where, client, options, cacheContext, tasks, cteCa
|
|
|
11482
13491
|
}));
|
|
11483
13492
|
break;
|
|
11484
13493
|
}
|
|
13494
|
+
case "BOOLEAN":
|
|
13495
|
+
break;
|
|
11485
13496
|
}
|
|
11486
13497
|
}
|
|
11487
13498
|
async function resolveSetSubqueries(assignments, client, options, cacheContext) {
|
|
@@ -11519,9 +13530,11 @@ async function resolveScalarColumns(columns, client, options, cacheContext, cteC
|
|
|
11519
13530
|
pending.forEach(([i], idx) => cache.set(i, values[idx]));
|
|
11520
13531
|
return cache;
|
|
11521
13532
|
}
|
|
13533
|
+
var validateExplainInfo = /* @__PURE__ */ new WeakMap();
|
|
11522
13534
|
async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords = 1e4) {
|
|
11523
13535
|
const fieldApps = /* @__PURE__ */ new Set();
|
|
11524
13536
|
const processStatusApps = /* @__PURE__ */ new Set();
|
|
13537
|
+
const numberPrecisionApps = /* @__PURE__ */ new Set();
|
|
11525
13538
|
const tracedClient = {
|
|
11526
13539
|
...client,
|
|
11527
13540
|
getFields: async (appId) => {
|
|
@@ -11531,6 +13544,10 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
|
|
|
11531
13544
|
getProcessStatuses: async (appId) => {
|
|
11532
13545
|
processStatusApps.add(appId);
|
|
11533
13546
|
return client.getProcessStatuses(appId);
|
|
13547
|
+
},
|
|
13548
|
+
getNumberPrecision: async (appId) => {
|
|
13549
|
+
numberPrecisionApps.add(appId);
|
|
13550
|
+
return client.getNumberPrecision(appId);
|
|
11534
13551
|
}
|
|
11535
13552
|
};
|
|
11536
13553
|
const capabilities = /* @__PURE__ */ new Map();
|
|
@@ -11579,6 +13596,51 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
|
|
|
11579
13596
|
}));
|
|
11580
13597
|
}
|
|
11581
13598
|
}
|
|
13599
|
+
} else if (typed["type"] === "VALIDATE") {
|
|
13600
|
+
const validate = node;
|
|
13601
|
+
fieldApps.add(validate.appId);
|
|
13602
|
+
const fields = await getFieldsCached(validate.appId, tracedClient, cacheContext);
|
|
13603
|
+
const infoByCode = new Map(fields.map((field) => [field.code, field]));
|
|
13604
|
+
const targets = resolveExistingValidationTargets(validate, fields);
|
|
13605
|
+
const checks = collectCheckFieldRefs(validate.checkGroups ?? []);
|
|
13606
|
+
const whereFields = collectValidateWhereFields(validate.where);
|
|
13607
|
+
for (const ref of checks) {
|
|
13608
|
+
if (ref.field !== "$id" && !infoByCode.has(ref.field)) {
|
|
13609
|
+
throw customCheckParseError(`CHECK \u306E\u30D5\u30A3\u30FC\u30EB\u30C9 ${ref.field} \u306F APP${validate.appId} \u306B\u5B58\u5728\u3057\u307E\u305B\u3093`);
|
|
13610
|
+
}
|
|
13611
|
+
}
|
|
13612
|
+
for (const field of whereFields) {
|
|
13613
|
+
if (field !== "$id" && !infoByCode.has(field)) {
|
|
13614
|
+
throw new Error(`ArgumentError: WHERE field ${field} does not exist in APP${validate.appId}.`);
|
|
13615
|
+
}
|
|
13616
|
+
}
|
|
13617
|
+
const types = new Map(fields.map((field) => [field.code, field.fieldType]));
|
|
13618
|
+
types.set("$id", "RECORD_NUMBER");
|
|
13619
|
+
assertCheckComparisonTypes(validate, types);
|
|
13620
|
+
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));
|
|
13621
|
+
if (capability.capability === "UNSUPPORTED") {
|
|
13622
|
+
throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
|
|
13623
|
+
}
|
|
13624
|
+
const fieldTypes = new Map(fields.map((field) => [field.code, field.fieldType]));
|
|
13625
|
+
const fieldOptions = new Map(fields.flatMap((field) => field.optionOrder ? [[field.code, new Set(Object.keys(field.optionOrder))]] : []));
|
|
13626
|
+
const prefilter = validate.where === null ? null : capability.capability === "EXACT_PUSHDOWN" ? validate.where : extractSafePushdownLeaves(validate.where, {
|
|
13627
|
+
allowUnqualifiedFields: true,
|
|
13628
|
+
fieldTypes,
|
|
13629
|
+
fieldOptions,
|
|
13630
|
+
allowKlike: false
|
|
13631
|
+
});
|
|
13632
|
+
const needsPrecision = targets.some((field) => field.fieldType === "NUMBER");
|
|
13633
|
+
if (needsPrecision) {
|
|
13634
|
+
numberPrecisionApps.add(validate.appId);
|
|
13635
|
+
await getNumberPrecisionCached(validate.appId, tracedClient, cacheContext);
|
|
13636
|
+
}
|
|
13637
|
+
validateExplainInfo.set(validate, {
|
|
13638
|
+
targetFields: targets.map((field) => field.code),
|
|
13639
|
+
fetchFields: [.../* @__PURE__ */ new Set(["$id", ...targets.map((field) => field.code), ...whereFields, ...checks.map((ref) => ref.field)])],
|
|
13640
|
+
capability,
|
|
13641
|
+
prefilter,
|
|
13642
|
+
numberPrecision: needsPrecision
|
|
13643
|
+
});
|
|
11582
13644
|
} else if (typed["type"] === "UPDATE" || typed["type"] === "DELETE") {
|
|
11583
13645
|
fieldApps.add(node.appId);
|
|
11584
13646
|
await assertDmlWhereCapability(
|
|
@@ -11609,23 +13671,24 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
|
|
|
11609
13671
|
}));
|
|
11610
13672
|
}
|
|
11611
13673
|
}
|
|
11612
|
-
return { capabilities, orderPlans, fieldApps, processStatusApps };
|
|
13674
|
+
return { capabilities, orderPlans, fieldApps, processStatusApps, numberPrecisionApps };
|
|
11613
13675
|
}
|
|
11614
13676
|
function explainMetadataLines(analysis) {
|
|
11615
13677
|
return [
|
|
11616
13678
|
...[...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}`)
|
|
13679
|
+
...[...analysis.processStatusApps].sort((a, b) => a - b).map((appId) => ` metadata API: process status APP${appId}`),
|
|
13680
|
+
...[...analysis.numberPrecisionApps].sort((a, b) => a - b).map((appId) => ` metadata API: number precision APP${appId}`)
|
|
11618
13681
|
];
|
|
11619
13682
|
}
|
|
11620
|
-
async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4, cursorMaxActive = 2) {
|
|
11621
|
-
const statements = parseSqlBatch(sql);
|
|
13683
|
+
async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4, cursorMaxActive = 2, enableImport = false) {
|
|
13684
|
+
const statements = parseSqlBatch(sql, enableImport);
|
|
11622
13685
|
const analysis = analyzeBatch(statements);
|
|
11623
13686
|
validateDeclaredBatchVariables(statements, injectedVariables);
|
|
11624
13687
|
const variables = /* @__PURE__ */ new Map();
|
|
11625
13688
|
const plans = [];
|
|
11626
13689
|
for (let i = 0; i < statements.length; i++) {
|
|
11627
13690
|
const stmt = statements[i];
|
|
11628
|
-
const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr:
|
|
13691
|
+
const planStmt = stmt.type === "SET_VARIABLE" ? stmt.expr.type === "SCALAR_SUBQUERY" ? { ...stmt, expr: resolveBatchVariableReferences(stmt.expr, variables) } : stmt : resolveBatchVariableReferences(stmt, variables);
|
|
11629
13692
|
validateKlikeStatement(planStmt);
|
|
11630
13693
|
const whereAnalysis = await buildExplainWhereAnalysis(planStmt, client, cacheContext, maxRecords);
|
|
11631
13694
|
const statementPlan = addCursorConcurrency(buildBatchStatementPlan(
|
|
@@ -11641,7 +13704,7 @@ async function buildBatchExplainPlans(sql, client, injectedVariables, cacheConte
|
|
|
11641
13704
|
plan: statementPlan.length === 0 ? metadataPlan : [statementPlan[0], ...metadataPlan, ...statementPlan.slice(1)]
|
|
11642
13705
|
});
|
|
11643
13706
|
if (stmt.type === "SET_VARIABLE" || stmt.type === "DECLARE_VARIABLE") {
|
|
11644
|
-
variables.set(stmt.name, { type: "string", value: `@${stmt.name}` });
|
|
13707
|
+
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
13708
|
}
|
|
11646
13709
|
}
|
|
11647
13710
|
return { statementCount: statements.length, statements: plans };
|
|
@@ -11776,8 +13839,103 @@ function buildExplainPlan(query, label, capabilities, orderPlans) {
|
|
|
11776
13839
|
if (query.type === "UPDATE") return buildUpdatePlan(query, label, capabilities, orderPlans);
|
|
11777
13840
|
if (query.type === "DELETE") return buildDeletePlan(query, label);
|
|
11778
13841
|
if (query.type === "REORDER") return buildReorderPlan(query, label);
|
|
13842
|
+
if (query.type === "VALIDATE") return buildValidatePlan(query, label);
|
|
13843
|
+
if (query.type === "IMPORT") {
|
|
13844
|
+
if (query.writeMode === "UPDATE_RECORD_NUMBER") {
|
|
13845
|
+
const csvTables = query.targets?.filter((target) => target.kind === "SUBTABLE") ?? [];
|
|
13846
|
+
return [
|
|
13847
|
+
...label ? [label] : [],
|
|
13848
|
+
`IMPORT UPDATE INTO APP${query.appId}`,
|
|
13849
|
+
` writeMode: UPDATE_RECORD_NUMBER`,
|
|
13850
|
+
` source: CSV ${query.source.sourceName}`,
|
|
13851
|
+
` keyHeader: ${query.recordNumberSourceHeader}`,
|
|
13852
|
+
` mapping: BY_NAME`,
|
|
13853
|
+
` parentRows: requires source load`,
|
|
13854
|
+
` duplicate: preflight before lookup/write`,
|
|
13855
|
+
` matched: requires lookup`,
|
|
13856
|
+
` unmatched: requires lookup`,
|
|
13857
|
+
` invalid: requires source load`,
|
|
13858
|
+
` requiresLookup:true`,
|
|
13859
|
+
` inserted: 0`,
|
|
13860
|
+
` keyInPayload: false`,
|
|
13861
|
+
...csvTables.length ? [
|
|
13862
|
+
` replaceSubtables: ${query.replaceSubtables?.join(", ") ?? "ERROR: required"}`,
|
|
13863
|
+
` subtableRowIdPolicy: PRESERVE existing; empty/unknown add without id`,
|
|
13864
|
+
` rowIdOwnership: owned elsewhere invalidates the parent`,
|
|
13865
|
+
` replacementDiff: existing/input/update/add/delete/rowIdNotFound requires actual-data preflight`,
|
|
13866
|
+
` confirmPolicy: highest warning "\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5168\u7F6E\u63DB\u30FBN\u884C\u524A\u9664" plus per-table detail (including delete=0)`
|
|
13867
|
+
] : [],
|
|
13868
|
+
` disposition: ${query.validateOnly ? "VALIDATE ONLY" : query.onErrorSkip ? `ON ERROR SKIP INTO ${query.errorTable}` : "fail-fast"}`,
|
|
13869
|
+
` gate: enabled for this parse`,
|
|
13870
|
+
` writesKintone: ${query.validateOnly ? "false" : "true"}`
|
|
13871
|
+
];
|
|
13872
|
+
}
|
|
13873
|
+
const mode = query.keyFields ? "UPSERT" : "INSERT";
|
|
13874
|
+
const hasSubtables = query.targets?.some((target) => target.kind === "SUBTABLE") === true;
|
|
13875
|
+
return [
|
|
13876
|
+
...label ? [label] : [],
|
|
13877
|
+
`IMPORT ${mode} INTO APP${query.appId}`,
|
|
13878
|
+
` source: ${query.source.kind} ${query.source.sourceName}`,
|
|
13879
|
+
` sourceFormat: ${query.source.kind}`,
|
|
13880
|
+
` encoding: ${query.source.kind === "JSON" ? "UTF8 only" : query.source.encoding ?? "UTF8 (or loader metadata)"}`,
|
|
13881
|
+
` mapping: ${query.source.kind === "JSON" ? "BY NAME (INTO order)" : query.source.projection ? "SELECT expressions" : query.source.mappingMode}`,
|
|
13882
|
+
...query.source.kind === "JSON" ? [
|
|
13883
|
+
` duplicateKeyPolicy: reject`,
|
|
13884
|
+
` numberLexemePolicy: preserve; JSON number accepts safe integer only`,
|
|
13885
|
+
` precisionTargetsRequireString: true`,
|
|
13886
|
+
` unknownKeyPolicy: reject`,
|
|
13887
|
+
` presenceAware: true`,
|
|
13888
|
+
...hasSubtables ? [
|
|
13889
|
+
` subtableRowIdPolicy: reject _rid/id; DROP IDs and renumber every input row`,
|
|
13890
|
+
` subtableUpdatePolicy: present table replaces all rows; missing table is preserved; [] deletes all rows`,
|
|
13891
|
+
` confirmPolicy: parent/table existing/input/add/delete detail required; delete is highest warning`
|
|
13892
|
+
] : []
|
|
13893
|
+
] : [
|
|
13894
|
+
` header: ${query.source.hasHeader ? "HEADER" : "NO HEADER"}`,
|
|
13895
|
+
...query.source.mappingMode === "BY_NAME" ? [
|
|
13896
|
+
` writtenColumns: ${query.fields.join(", ")}`,
|
|
13897
|
+
` knownExportColumns: audit and ignore with reason/non-empty count`,
|
|
13898
|
+
` unknownColumnPolicy: ${query.source.ignoreUnknownColumns ? "ignore with audit/non-empty count" : "ERR_IMPORT_UNKNOWN_COLUMN"}`,
|
|
13899
|
+
` multipleValueDelimiter: LF (CRLF or LF)`,
|
|
13900
|
+
` sourceValueMode: string-preserving`,
|
|
13901
|
+
` roundTripNumericGuarantee: exact CSV lexeme passes strict decimal validation`,
|
|
13902
|
+
` FILE: audit-ignore unless named in INTO (analyze error)`
|
|
13903
|
+
] : []
|
|
13904
|
+
],
|
|
13905
|
+
` sourceLimit: 10485760 bytes / ${query.fields.length} target columns`,
|
|
13906
|
+
` key: ${query.keyFields?.join(", ") ?? "none"}`,
|
|
13907
|
+
` checks: ${query.checkGroups?.length ?? 0}`,
|
|
13908
|
+
` disposition: ${query.validateOnly ? "VALIDATE ONLY" : query.onErrorSkip ? `ON ERROR SKIP INTO ${query.errorTable}` : "fail-fast"}`,
|
|
13909
|
+
` gate: enabled for this parse`,
|
|
13910
|
+
` preflight: ${query.validateOnly && hasSubtables ? "requires actual source load at execution; this EXPLAIN is static" : "requires load"}`,
|
|
13911
|
+
...hasSubtables ? [query.source.kind === "JSON" ? ` Phase5C: JSON mutation requires detail-capable confirmation surface` : ` Phase5D: CSV mutation requires detail-capable confirmation surface`] : [],
|
|
13912
|
+
` writesKintone: ${query.validateOnly ? "false" : "true"}`,
|
|
13913
|
+
` duplicateKey: preflight before lookup/write (requires load)`
|
|
13914
|
+
];
|
|
13915
|
+
}
|
|
11779
13916
|
return buildSelectPlan(query, label, capabilities, orderPlans);
|
|
11780
13917
|
}
|
|
13918
|
+
function buildValidatePlan(stmt, label) {
|
|
13919
|
+
const info = validateExplainInfo.get(stmt);
|
|
13920
|
+
const lines = [];
|
|
13921
|
+
if (label) lines.push(label);
|
|
13922
|
+
lines.push(`VALIDATE APP${stmt.appId}`);
|
|
13923
|
+
lines.push(" operation: read-only existing-record constraint audit (writesKintone=false)");
|
|
13924
|
+
lines.push(" fetch API: GET records via offset + $id keyset paging (Cursor API unused)");
|
|
13925
|
+
lines.push(" complete input: required (onLimit=truncate disabled)");
|
|
13926
|
+
if (!info) {
|
|
13927
|
+
lines.push(" metadata: form definition required; number precision required for NUMBER targets");
|
|
13928
|
+
return lines;
|
|
13929
|
+
}
|
|
13930
|
+
lines.push(` WHERE capability: ${info.capability.capability}`);
|
|
13931
|
+
lines.push(` kintone query: ${info.prefilter === null ? "(\u5168\u4EF6\u53D6\u5F97)" : whereToKintone(info.prefilter)}`);
|
|
13932
|
+
lines.push(` audit fields: ${info.targetFields.length === 0 ? "(\u306A\u3057)" : info.targetFields.join(", ")}`);
|
|
13933
|
+
lines.push(` fetch fields: ${info.fetchFields.join(", ")}`);
|
|
13934
|
+
lines.push(` number precision: ${info.numberPrecision ? "required" : "not required"}`);
|
|
13935
|
+
lines.push(" local checks: original WHERE re-evaluation + built-in constraints + CHECK groups");
|
|
13936
|
+
lines.push(" records/mutation API during EXPLAIN: none; violation count unavailable");
|
|
13937
|
+
return lines;
|
|
13938
|
+
}
|
|
11781
13939
|
function buildSelectPlan(stmt, label, capabilities, orderPlans) {
|
|
11782
13940
|
const whereCapability = capabilities?.get(stmt) ?? (capabilities ? [...capabilities].find(([candidate]) => JSON.stringify(candidate) === JSON.stringify(stmt))?.[1] : void 0);
|
|
11783
13941
|
const orderPlan = orderPlans?.get(stmt) ?? (orderPlans ? [...orderPlans].find(([candidate]) => JSON.stringify(candidate) === JSON.stringify(stmt))?.[1] : void 0);
|
|
@@ -11789,6 +13947,12 @@ function buildSelectPlan(stmt, label, capabilities, orderPlans) {
|
|
|
11789
13947
|
const lines = [];
|
|
11790
13948
|
if (label) lines.push(label);
|
|
11791
13949
|
lines.push(` mode: ${mode}`);
|
|
13950
|
+
if (isConstantFalseWhere(stmt.where)) {
|
|
13951
|
+
lines.push(" predicate: constant false");
|
|
13952
|
+
lines.push(" records API access: none");
|
|
13953
|
+
lines.push(` app: APP${stmt.from.appId} (${stmt.from.appId})`);
|
|
13954
|
+
return lines;
|
|
13955
|
+
}
|
|
11792
13956
|
if (orderPlan) {
|
|
11793
13957
|
lines.push(` order plan: ${orderPlan.kind}`);
|
|
11794
13958
|
if (orderPlan.reasonCodes.length > 0) lines.push(` order reason: ${orderPlan.reasonCodes.join(", ")}`);
|
|
@@ -11936,6 +14100,8 @@ function collectSubqueryPlans(stmt, capabilities, orderPlans) {
|
|
|
11936
14100
|
break;
|
|
11937
14101
|
case "NULL_CHECK":
|
|
11938
14102
|
break;
|
|
14103
|
+
case "BOOLEAN":
|
|
14104
|
+
break;
|
|
11939
14105
|
}
|
|
11940
14106
|
};
|
|
11941
14107
|
visitWhere(stmt.where);
|
|
@@ -11988,7 +14154,7 @@ function buildUpdatePlan(stmt, label, capabilities, orderPlans) {
|
|
|
11988
14154
|
} else {
|
|
11989
14155
|
lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
|
|
11990
14156
|
}
|
|
11991
|
-
lines.push(` api: GET /k/v1/records.json \u2192 PUT /k/v1/records.json`);
|
|
14157
|
+
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
14158
|
const setTypes = [];
|
|
11993
14159
|
if (isArith) setTypes.push("\u7B97\u8853 SET\uFF08\u73FE\u5728\u5024\u3092\u53D6\u5F97\u3057\u3066\u8A08\u7B97\uFF09");
|
|
11994
14160
|
if (isStringFunc) setTypes.push("\u6587\u5B57\u5217\u95A2\u6570 SET\uFF08\u73FE\u5728\u5024\u3092\u53D6\u5F97\u3057\u3066\u8A55\u4FA1\uFF09");
|
|
@@ -12019,7 +14185,7 @@ function buildDeletePlan(stmt, label) {
|
|
|
12019
14185
|
lines.push(` [DELETE]`);
|
|
12020
14186
|
lines.push(` target: APP${stmt.appId} (${stmt.appId})`);
|
|
12021
14187
|
lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
|
|
12022
|
-
lines.push(` api: GET /k/v1/records.json \u2192 DELETE /k/v1/records.json`);
|
|
14188
|
+
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
14189
|
return lines;
|
|
12024
14190
|
}
|
|
12025
14191
|
function buildUpsertPlan(stmt, label) {
|
|
@@ -12059,7 +14225,7 @@ function buildReorderPlan(stmt, label) {
|
|
|
12059
14225
|
` table: ${target}`,
|
|
12060
14226
|
` scope: ${scope}`,
|
|
12061
14227
|
` 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`
|
|
14228
|
+
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
14229
|
];
|
|
12064
14230
|
if (!stmt.all && stmt.where) {
|
|
12065
14231
|
lines.splice(5, 0, ` where: ${safeWhereToKintone(stmt.where)}`);
|
|
@@ -12071,6 +14237,7 @@ function formatOrderByItem(item) {
|
|
|
12071
14237
|
return `${key} ${item.direction}`;
|
|
12072
14238
|
}
|
|
12073
14239
|
function safeWhereToKintone(where) {
|
|
14240
|
+
if (where.type === "BOOLEAN") return where.value ? "TRUE" : "FALSE (constant)";
|
|
12074
14241
|
try {
|
|
12075
14242
|
return whereToKintone(where);
|
|
12076
14243
|
} catch {
|
|
@@ -12199,15 +14366,15 @@ var OperationCancelledError = class extends Error {
|
|
|
12199
14366
|
};
|
|
12200
14367
|
|
|
12201
14368
|
// src/core/sql.ts
|
|
12202
|
-
function parseSqlStatement(sql) {
|
|
14369
|
+
function parseSqlStatement(sql, capabilities = {}) {
|
|
12203
14370
|
const tokens = new Lexer(sql).tokenize();
|
|
12204
|
-
const stmt = new Parser(tokens).parse();
|
|
14371
|
+
const stmt = new Parser(tokens, capabilities).parse();
|
|
12205
14372
|
validateKlikeStatement(stmt);
|
|
12206
14373
|
return stmt;
|
|
12207
14374
|
}
|
|
12208
|
-
function parseSqlStatements(sql) {
|
|
14375
|
+
function parseSqlStatements(sql, capabilities = {}) {
|
|
12209
14376
|
const tokens = new Lexer(sql).tokenize();
|
|
12210
|
-
const statements = new Parser(tokens).parseStatements();
|
|
14377
|
+
const statements = new Parser(tokens, capabilities).parseStatements();
|
|
12211
14378
|
statements.forEach(validateKlikeStatement);
|
|
12212
14379
|
return statements;
|
|
12213
14380
|
}
|
|
@@ -12374,7 +14541,8 @@ function buildBatchEnvelope(batch, options = {}) {
|
|
|
12374
14541
|
validRows: s.result.validRows,
|
|
12375
14542
|
invalidRows: s.result.invalidRows,
|
|
12376
14543
|
errorCount: s.result.errorCount,
|
|
12377
|
-
...s.result.errTable ? { errTable: s.result.errTable } : {}
|
|
14544
|
+
...s.result.errTable ? { errTable: s.result.errTable } : {},
|
|
14545
|
+
...s.result.importDetail ? { importDetail: s.result.importDetail } : {}
|
|
12378
14546
|
});
|
|
12379
14547
|
} else if (s.status === "success" && s.result && s.result.type !== "SELECT" && s.result.type !== "ASSERT") {
|
|
12380
14548
|
Object.assign(entry, toMutationSummary(s.result));
|
|
@@ -12677,7 +14845,7 @@ function clampInt(v, min, max) {
|
|
|
12677
14845
|
function flattenFormFieldProperties(properties) {
|
|
12678
14846
|
return flattenFields(properties, collectLookupCopyFields(properties));
|
|
12679
14847
|
}
|
|
12680
|
-
function flattenFields(properties, lookupCopyFields, inSubtable = false) {
|
|
14848
|
+
function flattenFields(properties, lookupCopyFields, inSubtable = false, subtableCode) {
|
|
12681
14849
|
const out = [];
|
|
12682
14850
|
for (const field of Object.values(properties)) {
|
|
12683
14851
|
const optionOrder = toOptionOrderMap(field.options);
|
|
@@ -12695,11 +14863,12 @@ function flattenFields(properties, lookupCopyFields, inSubtable = false) {
|
|
|
12695
14863
|
maxLength: normalizeConstraintValue(field.maxLength),
|
|
12696
14864
|
defaultValue: field.defaultValue,
|
|
12697
14865
|
inSubtable,
|
|
14866
|
+
...subtableCode ? { subtableCode } : {},
|
|
12698
14867
|
writable: !lookupCopyFields.has(field.code) && !NON_WRITABLE_FIELD_TYPES2.has(field.type)
|
|
12699
14868
|
};
|
|
12700
14869
|
info.semantics = resolveFieldSemantics(info);
|
|
12701
14870
|
out.push(info);
|
|
12702
|
-
if (field.fields) out.push(...flattenFields(field.fields, lookupCopyFields, true));
|
|
14871
|
+
if (field.fields) out.push(...flattenFields(field.fields, lookupCopyFields, true, field.type === "SUBTABLE" ? field.code : subtableCode));
|
|
12703
14872
|
}
|
|
12704
14873
|
return out;
|
|
12705
14874
|
}
|
|
@@ -13683,6 +15852,17 @@ function restoreSqlContextError(err, sourceSql, context) {
|
|
|
13683
15852
|
return err;
|
|
13684
15853
|
}
|
|
13685
15854
|
|
|
15855
|
+
// src/import/importGateError.ts
|
|
15856
|
+
var IMPORT_CAPABILITY_GATE_MARKER = "capability is disabled";
|
|
15857
|
+
function errorMessage(error) {
|
|
15858
|
+
if (error instanceof Error) return error.message;
|
|
15859
|
+
if (typeof error === "string") return error;
|
|
15860
|
+
return null;
|
|
15861
|
+
}
|
|
15862
|
+
function isImportCapabilityGateError(error) {
|
|
15863
|
+
return errorMessage(error)?.includes(IMPORT_CAPABILITY_GATE_MARKER) === true;
|
|
15864
|
+
}
|
|
15865
|
+
|
|
13686
15866
|
// src/cli/index.ts
|
|
13687
15867
|
var HELP_TEXT = `ksql - Execute SQL against kintone apps
|
|
13688
15868
|
|
|
@@ -13697,6 +15877,8 @@ Options:
|
|
|
13697
15877
|
--console Start interactive console mode
|
|
13698
15878
|
--dry-run Parse and show execution plan only
|
|
13699
15879
|
--var <name=value> Override a DECLARE variable (repeatable; not for secrets)
|
|
15880
|
+
--import-csv <name=path> Supply named CSV and enable IMPORT (repeatable)
|
|
15881
|
+
--import-json <name=path> Supply named JSON and enable IMPORT (repeatable)
|
|
13700
15882
|
--format <type> Output format: table | json | jsonl | csv | markdown | md
|
|
13701
15883
|
(batch + json: prints one JSON envelope for the whole batch)
|
|
13702
15884
|
--max-records <n> Max records to fetch (default: 500)
|
|
@@ -13744,6 +15926,15 @@ Options:
|
|
|
13744
15926
|
-h, --help Show help
|
|
13745
15927
|
-v, --version Show version
|
|
13746
15928
|
`;
|
|
15929
|
+
var CLI_IMPORT_SOURCE_REQUIRED_MESSAGE = "IMPORT \u306B\u306F\u30BD\u30FC\u30B9\u304C\u5FC5\u8981\u3067\u3059\u3002--import-csv <name=path> \u307E\u305F\u306F --import-json <name=path> \u3067\u30D5\u30A1\u30A4\u30EB\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002";
|
|
15930
|
+
function toCliImportError(error, importEnabled) {
|
|
15931
|
+
if (importEnabled || !isImportCapabilityGateError(error)) return error;
|
|
15932
|
+
if (error instanceof Error) {
|
|
15933
|
+
error.message = CLI_IMPORT_SOURCE_REQUIRED_MESSAGE;
|
|
15934
|
+
return error;
|
|
15935
|
+
}
|
|
15936
|
+
return CLI_IMPORT_SOURCE_REQUIRED_MESSAGE;
|
|
15937
|
+
}
|
|
13747
15938
|
function parseArgs(argv) {
|
|
13748
15939
|
const out = {
|
|
13749
15940
|
help: false,
|
|
@@ -13795,7 +15986,9 @@ function parseArgs(argv) {
|
|
|
13795
15986
|
arrayFormat: null,
|
|
13796
15987
|
tableFormat: null,
|
|
13797
15988
|
dateFormat: null,
|
|
13798
|
-
attachmentFormat: null
|
|
15989
|
+
attachmentFormat: null,
|
|
15990
|
+
importCsv: /* @__PURE__ */ Object.create(null),
|
|
15991
|
+
importJson: /* @__PURE__ */ Object.create(null)
|
|
13799
15992
|
};
|
|
13800
15993
|
for (let i = 0; i < argv.length; i++) {
|
|
13801
15994
|
const a = argv[i];
|
|
@@ -13877,6 +16070,28 @@ function parseArgs(argv) {
|
|
|
13877
16070
|
i++;
|
|
13878
16071
|
continue;
|
|
13879
16072
|
}
|
|
16073
|
+
if (a === "--import-csv") {
|
|
16074
|
+
const raw = v ?? "";
|
|
16075
|
+
const eq = raw.indexOf("=");
|
|
16076
|
+
if (eq <= 0 || eq === raw.length - 1) throw new Error("ArgumentError: --import-csv must use name=path.");
|
|
16077
|
+
const name = raw.slice(0, eq);
|
|
16078
|
+
if (Object.prototype.hasOwnProperty.call(out.importCsv, name) || Object.prototype.hasOwnProperty.call(out.importJson, name)) throw new Error(`ArgumentError: import source "${name}" is specified more than once.`);
|
|
16079
|
+
out.importCsv[name] = raw.slice(eq + 1);
|
|
16080
|
+
i++;
|
|
16081
|
+
continue;
|
|
16082
|
+
}
|
|
16083
|
+
if (a === "--import-json") {
|
|
16084
|
+
const raw = v ?? "";
|
|
16085
|
+
const eq = raw.indexOf("=");
|
|
16086
|
+
if (eq <= 0 || eq === raw.length - 1) throw new Error("ArgumentError: --import-json must use name=path.");
|
|
16087
|
+
const name = raw.slice(0, eq);
|
|
16088
|
+
if (Object.prototype.hasOwnProperty.call(out.importJson, name) || Object.prototype.hasOwnProperty.call(out.importCsv, name)) {
|
|
16089
|
+
throw new Error(`ArgumentError: import source "${name}" is specified more than once.`);
|
|
16090
|
+
}
|
|
16091
|
+
out.importJson[name] = raw.slice(eq + 1);
|
|
16092
|
+
i++;
|
|
16093
|
+
continue;
|
|
16094
|
+
}
|
|
13880
16095
|
if (a === "-e" || a === "--execute") {
|
|
13881
16096
|
out.executeSql = v ?? "";
|
|
13882
16097
|
i++;
|
|
@@ -15024,21 +17239,22 @@ async function run() {
|
|
|
15024
17239
|
`);
|
|
15025
17240
|
return 2;
|
|
15026
17241
|
}
|
|
17242
|
+
const importEnabled = Object.keys(args.importCsv).length > 0 || Object.keys(args.importJson).length > 0;
|
|
15027
17243
|
try {
|
|
15028
|
-
const statements = parseSqlStatements(sql);
|
|
17244
|
+
const statements = parseSqlStatements(sql, { import: importEnabled });
|
|
15029
17245
|
dryRunNeedsMetadata = statements.some(explainNeedsAppMetadata);
|
|
15030
17246
|
if (statements.length > 1) {
|
|
15031
17247
|
batchAnalysis = analyzeBatch(statements);
|
|
15032
17248
|
isBatchSql = true;
|
|
15033
17249
|
batchContainsDml = batchAnalysis.containsDml;
|
|
15034
17250
|
} else {
|
|
15035
|
-
const stmt = parseSqlStatement(sql);
|
|
17251
|
+
const stmt = parseSqlStatement(sql, { import: importEnabled });
|
|
15036
17252
|
parsedStmt = stmt;
|
|
15037
17253
|
stmtType = getStatementType(stmt);
|
|
15038
17254
|
isDmlStatement = writesKintone(stmt);
|
|
15039
17255
|
hasWhere = hasWhereClause(stmt);
|
|
15040
17256
|
insertValuesCount = getInsertValuesCount(stmt);
|
|
15041
|
-
const supported = stmtType === "SELECT" || stmtType === "UNION" || stmtType === "WITH" || stmtType === "EXPLAIN" || stmtType === "SHOW_APPS" || stmtType === "DESCRIBE" || stmtType === "ASSERT" || isDmlType(stmtType);
|
|
17257
|
+
const supported = stmtType === "SELECT" || stmtType === "UNION" || stmtType === "WITH" || stmtType === "EXPLAIN" || stmtType === "SHOW_APPS" || stmtType === "DESCRIBE" || stmtType === "ASSERT" || stmtType === "VALIDATE" || isDmlType(stmtType);
|
|
15042
17258
|
if (!supported) {
|
|
15043
17259
|
process.stderr.write(`ArgumentError: unsupported statement type in CLI: ${stmtType}
|
|
15044
17260
|
`);
|
|
@@ -15050,7 +17266,8 @@ async function run() {
|
|
|
15050
17266
|
bindings: sqlDiagnosticContext.appBindingByMappedApp,
|
|
15051
17267
|
rewriteSegments: sqlDiagnosticContext.rewriteSegments
|
|
15052
17268
|
}) : err;
|
|
15053
|
-
|
|
17269
|
+
const surfaced = toCliImportError(restored, importEnabled);
|
|
17270
|
+
process.stderr.write(`${surfaced instanceof Error ? surfaced.message : String(surfaced)}
|
|
15054
17271
|
`);
|
|
15055
17272
|
return 1;
|
|
15056
17273
|
}
|
|
@@ -15088,10 +17305,11 @@ async function run() {
|
|
|
15088
17305
|
const allowWithoutWhere = args.allowWithoutWhere || envBool("KSQL_ALLOW_WITHOUT_WHERE") === true || Boolean(profile.dml?.allowWithoutWhere);
|
|
15089
17306
|
const dmlMaxRows = args.dmlMaxRows ?? envInt2("KSQL_DML_MAX_ROWS") ?? profile.dml?.maxRows ?? 100;
|
|
15090
17307
|
const isValidationOnly = batchAnalysis?.containsValidationOnly === true || parsedStmt !== null && typeof parsedStmt === "object" && "validateOnly" in parsedStmt && parsedStmt.validateOnly === true;
|
|
15091
|
-
const
|
|
17308
|
+
const isExistingRecordValidation = batchAnalysis?.statements.some((s) => s.statementType === "VALIDATE") === true || getStatementType(parsedStmt) === "VALIDATE";
|
|
17309
|
+
const surfaceForcesOnLimitError = isDmlStatement || batchContainsDml || isValidationOnly || isExistingRecordValidation;
|
|
15092
17310
|
const effectiveOnLimit = surfaceForcesOnLimitError ? "error" : onLimit;
|
|
15093
17311
|
if (surfaceForcesOnLimitError && onLimit === "truncate" && !quiet && !args.dryRun) {
|
|
15094
|
-
const reason = isDmlStatement || batchContainsDml ? "DML" : "VALIDATE ONLY";
|
|
17312
|
+
const reason = isDmlStatement || batchContainsDml ? "DML" : isExistingRecordValidation ? "VALIDATE" : "VALIDATE ONLY";
|
|
15095
17313
|
process.stderr.write(`note: onLimit=truncate is ignored for ${reason} (forced to error)
|
|
15096
17314
|
`);
|
|
15097
17315
|
}
|
|
@@ -15406,7 +17624,8 @@ async function run() {
|
|
|
15406
17624
|
args.variables,
|
|
15407
17625
|
cacheContext,
|
|
15408
17626
|
maxRecords,
|
|
15409
|
-
cursorMaxActive
|
|
17627
|
+
cursorMaxActive,
|
|
17628
|
+
Object.keys(args.importCsv).length > 0 || Object.keys(args.importJson).length > 0
|
|
15410
17629
|
);
|
|
15411
17630
|
const out = [];
|
|
15412
17631
|
const restoredStatements = sqlDiagnosticContext ? restoreSqlDiagnosticValue(plans.statements, sqlDiagnosticContext.appBindingByMappedApp) : plans.statements;
|
|
@@ -15445,10 +17664,30 @@ async function run() {
|
|
|
15445
17664
|
}
|
|
15446
17665
|
}
|
|
15447
17666
|
}
|
|
15448
|
-
const
|
|
17667
|
+
const importEnabled = Object.keys(args.importCsv).length > 0 || Object.keys(args.importJson).length > 0;
|
|
17668
|
+
const importSource = importEnabled ? (name) => {
|
|
17669
|
+
const sourcePath = args.importCsv[name] ?? args.importJson[name];
|
|
17670
|
+
return sourcePath === void 0 ? void 0 : { load: async () => ({ bytes: new Uint8Array((0, import_fs2.readFileSync)(sourcePath)) }) };
|
|
17671
|
+
} : void 0;
|
|
17672
|
+
const confirm = async (count, operation, context) => {
|
|
15449
17673
|
if (count > dmlMaxRows) {
|
|
15450
17674
|
throw new Error(`ArgumentError: ${operation} affected rows (${count}) exceed --dml-max-rows (${dmlMaxRows}).`);
|
|
15451
17675
|
}
|
|
17676
|
+
if (context?.importDetail) {
|
|
17677
|
+
const detail = context.importDetail;
|
|
17678
|
+
const csv = detail.kind === "IMPORT_CSV_SUBTABLE_REPLACE";
|
|
17679
|
+
const lines = [
|
|
17680
|
+
...csv ? [`\u3010\u6700\u91CD\u8981\u8B66\u544A\u3011\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5168\u7F6E\u63DB\u30FB${detail.totalDeleteRows}\u884C\u524A\u9664`] : [],
|
|
17681
|
+
`[IMPORT ${csv ? "CSV" : "JSON"} Confirm] parentsToWrite=${detail.parentsToWrite} insert=${detail.insertedParents} update=${detail.updatedParents}`,
|
|
17682
|
+
csv ? `rowIdPolicy=PRESERVE_EXISTING rowIdNotFound=${detail.rowIdNotFound} invalidParents=${detail.invalidParents}` : `rowIdPolicy=DROP_AND_RENUMBER_ALL (JSON child rows are all newly numbered)`,
|
|
17683
|
+
...!csv && detail.hasDeletes ? ["WARNING: existing subtable rows will be deleted/replaced."] : [],
|
|
17684
|
+
...detail.parents.flatMap((parent) => parent.tables.map(
|
|
17685
|
+
(table) => `parentRow=${parent.parentRow} mode=${parent.mode} table=${table.table} existing=${table.existingRows} input=${table.inputRows} update=${"updateRows" in table ? table.updateRows : 0} add=${table.addRows} delete=${table.deleteRows}${"rowIdNotFound" in table ? ` rowIdNotFound=${table.rowIdNotFound}` : ""}`
|
|
17686
|
+
))
|
|
17687
|
+
];
|
|
17688
|
+
process.stderr.write(`${lines.join("\n")}
|
|
17689
|
+
`);
|
|
17690
|
+
}
|
|
15452
17691
|
if (yes) return true;
|
|
15453
17692
|
if (args.console) return true;
|
|
15454
17693
|
const label = sql?.replace(/\s+/g, " ").trim() ?? operation;
|
|
@@ -15473,10 +17712,20 @@ query=${label}`);
|
|
|
15473
17712
|
timeoutMs: timeout,
|
|
15474
17713
|
cursorMaxActive,
|
|
15475
17714
|
variables: args.variables,
|
|
15476
|
-
|
|
17715
|
+
enableImport: importEnabled,
|
|
17716
|
+
importSource,
|
|
17717
|
+
supportsImportConfirmDetail: true,
|
|
17718
|
+
confirm: batchContainsDml ? async (count, operation, context) => {
|
|
15477
17719
|
if (count > dmlMaxRows) {
|
|
15478
17720
|
throw new Error(`ArgumentError: ${operation} affected rows (${count}) exceed --dml-max-rows (${dmlMaxRows}).`);
|
|
15479
17721
|
}
|
|
17722
|
+
if (context?.importDetail) {
|
|
17723
|
+
const importDetail = context.importDetail;
|
|
17724
|
+
if (importDetail.kind === "IMPORT_CSV_SUBTABLE_REPLACE") process.stderr.write(`\u3010\u6700\u91CD\u8981\u8B66\u544A\u3011\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5168\u7F6E\u63DB\u30FB${importDetail.totalDeleteRows}\u884C\u524A\u9664
|
|
17725
|
+
`);
|
|
17726
|
+
process.stderr.write(`[IMPORT ${importDetail.kind === "IMPORT_CSV_SUBTABLE_REPLACE" ? "CSV" : "JSON"} Confirm] ${JSON.stringify(importDetail)}
|
|
17727
|
+
`);
|
|
17728
|
+
}
|
|
15480
17729
|
return true;
|
|
15481
17730
|
} : void 0
|
|
15482
17731
|
});
|
|
@@ -15486,14 +17735,19 @@ query=${label}`);
|
|
|
15486
17735
|
maxRecords,
|
|
15487
17736
|
onLimitReached: onLimit,
|
|
15488
17737
|
cacheContext,
|
|
15489
|
-
cursorMaxActive
|
|
17738
|
+
cursorMaxActive,
|
|
17739
|
+
enableImport: importEnabled,
|
|
17740
|
+
importSource
|
|
15490
17741
|
}) : await execute(sql, client, {
|
|
15491
17742
|
maxRecords,
|
|
15492
17743
|
fetchParallel,
|
|
15493
17744
|
onLimitReached: effectiveOnLimit,
|
|
15494
17745
|
confirm: isDmlStatement ? confirm : void 0,
|
|
15495
17746
|
cacheContext,
|
|
15496
|
-
cursorMaxActive
|
|
17747
|
+
cursorMaxActive,
|
|
17748
|
+
enableImport: importEnabled,
|
|
17749
|
+
importSource,
|
|
17750
|
+
supportsImportConfirmDetail: true
|
|
15497
17751
|
});
|
|
15498
17752
|
if (args.dryRun && sqlDiagnosticContext) {
|
|
15499
17753
|
result = restoreSqlDiagnosticValue(result, sqlDiagnosticContext.appBindingByMappedApp);
|
|
@@ -15561,6 +17815,7 @@ if (isDirectCliRun()) {
|
|
|
15561
17815
|
}
|
|
15562
17816
|
// Annotate the CommonJS export names for ESM import in node:
|
|
15563
17817
|
0 && (module.exports = {
|
|
17818
|
+
CLI_IMPORT_SOURCE_REQUIRED_MESSAGE,
|
|
15564
17819
|
HELP_TEXT,
|
|
15565
17820
|
buildBatchDmlConfirmMessage,
|
|
15566
17821
|
buildBatchStatementSummary,
|
|
@@ -15577,5 +17832,6 @@ if (isDirectCliRun()) {
|
|
|
15577
17832
|
parseTokenMap,
|
|
15578
17833
|
runWithArgv,
|
|
15579
17834
|
shouldExitOnEmpty,
|
|
17835
|
+
toCliImportError,
|
|
15580
17836
|
writeBatchOutput
|
|
15581
17837
|
});
|