@rex0220/kintone-sql-tools 3.5.0 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist-cli/ksql.js +2085 -133
- package/dist-mcp/ksql-mcp.js +2096 -163
- package/dist-mcpb/ksql-mcp.mcpb +0 -0
- package/package.json +1 -1
package/dist-cli/ksql.js
CHANGED
|
@@ -21,11 +21,13 @@ 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,
|
|
27
28
|
buildOutput: () => buildOutput,
|
|
28
29
|
buildReplExecArgv: () => buildReplExecArgv,
|
|
30
|
+
buildSelectSummary: () => buildSelectSummary,
|
|
29
31
|
buildValidationOutput: () => buildValidationOutput,
|
|
30
32
|
extractAppIds: () => extractAppIds,
|
|
31
33
|
normalizeAppKey: () => normalizeAppKey,
|
|
@@ -37,6 +39,7 @@ __export(index_exports, {
|
|
|
37
39
|
parseTokenMap: () => parseTokenMap,
|
|
38
40
|
runWithArgv: () => runWithArgv,
|
|
39
41
|
shouldExitOnEmpty: () => shouldExitOnEmpty,
|
|
42
|
+
toCliImportError: () => toCliImportError,
|
|
40
43
|
writeBatchOutput: () => writeBatchOutput
|
|
41
44
|
});
|
|
42
45
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -651,8 +654,9 @@ var ParseError = class extends Error {
|
|
|
651
654
|
}
|
|
652
655
|
};
|
|
653
656
|
var Parser = class {
|
|
654
|
-
constructor(tokens) {
|
|
657
|
+
constructor(tokens, capabilities = {}) {
|
|
655
658
|
this.tokens = tokens;
|
|
659
|
+
this.capabilities = capabilities;
|
|
656
660
|
this.allowUnaryPlusNumber = false;
|
|
657
661
|
this.scalarAllowsAggregateArgs = true;
|
|
658
662
|
this.scalarAllowsCase = true;
|
|
@@ -745,6 +749,12 @@ var Parser = class {
|
|
|
745
749
|
if (upper === "DROP") return this.parseDropTempTable();
|
|
746
750
|
if (upper === "DECLARE") return this.parseDeclareVariable();
|
|
747
751
|
if (upper === "VALIDATE") return this.parseValidate();
|
|
752
|
+
if (upper === "IMPORT") {
|
|
753
|
+
if (!this.capabilities.import) {
|
|
754
|
+
throw new ParseError("IMPORT is not supported (capability is disabled).", tok);
|
|
755
|
+
}
|
|
756
|
+
return this.parseImport();
|
|
757
|
+
}
|
|
748
758
|
break;
|
|
749
759
|
}
|
|
750
760
|
default:
|
|
@@ -925,24 +935,242 @@ var Parser = class {
|
|
|
925
935
|
query = this.parseReorder();
|
|
926
936
|
} else if (tok.kind === "IDENT" /* IDENT */ && tok.value.toUpperCase() === "VALIDATE") {
|
|
927
937
|
query = this.parseValidate();
|
|
938
|
+
} else if (tok.kind === "IDENT" /* IDENT */ && tok.value.toUpperCase() === "IMPORT") {
|
|
939
|
+
if (!this.capabilities.import) {
|
|
940
|
+
throw new ParseError("IMPORT is not supported (capability is disabled).", tok);
|
|
941
|
+
}
|
|
942
|
+
query = this.parseImport();
|
|
928
943
|
} else {
|
|
929
944
|
throw new ParseError("EXPLAIN \u306E\u5F8C\u306B\u306F SELECT / WITH / INSERT / UPSERT / UPDATE / DELETE / REORDER / VALIDATE \u304C\u5FC5\u8981\u3067\u3059", tok);
|
|
930
945
|
}
|
|
931
946
|
return { type: "EXPLAIN", query };
|
|
932
947
|
}
|
|
948
|
+
parseImport() {
|
|
949
|
+
this.advance();
|
|
950
|
+
let writeMode;
|
|
951
|
+
if (this.peek().kind === "UPDATE" /* UPDATE */) {
|
|
952
|
+
this.advance();
|
|
953
|
+
writeMode = "UPDATE_RECORD_NUMBER";
|
|
954
|
+
}
|
|
955
|
+
this.expect("INTO" /* INTO */);
|
|
956
|
+
this.rejectTempTableDml();
|
|
957
|
+
const target = this.parseIdentifier();
|
|
958
|
+
const { appId, subtableCode } = extractTableRef(target, this.prev());
|
|
959
|
+
if (subtableCode) throw new ParseError("IMPORT does not support subtables in Phase 1.", this.prev());
|
|
960
|
+
this.expect("(" /* LPAREN */);
|
|
961
|
+
const targets = [];
|
|
962
|
+
const fields = [];
|
|
963
|
+
const targetNames = /* @__PURE__ */ new Set();
|
|
964
|
+
while (true) {
|
|
965
|
+
const name = this.parseIdentifier();
|
|
966
|
+
if (targetNames.has(name)) throw new ParseError(`IMPORT target ${name} is declared more than once.`, this.prev());
|
|
967
|
+
targetNames.add(name);
|
|
968
|
+
if (this.peek().kind === "(" /* LPAREN */) {
|
|
969
|
+
this.advance();
|
|
970
|
+
const children = this.parseIdentList();
|
|
971
|
+
this.expect(")" /* RPAREN */);
|
|
972
|
+
if (new Set(children).size !== children.length) {
|
|
973
|
+
throw new ParseError(`IMPORT subtable ${name} contains duplicate child declarations.`, this.prev());
|
|
974
|
+
}
|
|
975
|
+
let rowIdSourceHeader;
|
|
976
|
+
if (this.isSoftKeyword("ROW")) {
|
|
977
|
+
this.advance();
|
|
978
|
+
for (const word of ["ID", "SOURCE"]) {
|
|
979
|
+
if (!this.isSoftKeyword(word)) throw new ParseError(`ROW must be followed by ID SOURCE <header>.`, this.peek());
|
|
980
|
+
this.advance();
|
|
981
|
+
}
|
|
982
|
+
rowIdSourceHeader = this.parseIdentifier();
|
|
983
|
+
}
|
|
984
|
+
targets.push({ kind: "SUBTABLE", subtableCode: name, children, ...rowIdSourceHeader ? { rowIdSourceHeader } : {} });
|
|
985
|
+
} else {
|
|
986
|
+
fields.push(name);
|
|
987
|
+
targets.push({ kind: "FIELD", field: name });
|
|
988
|
+
}
|
|
989
|
+
if (this.peek().kind !== "," /* COMMA */) break;
|
|
990
|
+
this.advance();
|
|
991
|
+
}
|
|
992
|
+
this.expect(")" /* RPAREN */);
|
|
993
|
+
this.expect("FROM" /* FROM */);
|
|
994
|
+
if (!this.isSoftKeyword("CSV") && !this.isSoftKeyword("JSON")) throw new ParseError("IMPORT FROM requires CSV or JSON.", this.peek());
|
|
995
|
+
const sourceKind = this.peek().value.toUpperCase();
|
|
996
|
+
this.advance();
|
|
997
|
+
const sourceName = this.parseIdentifier();
|
|
998
|
+
let encoding;
|
|
999
|
+
let hasHeader = true;
|
|
1000
|
+
let columns;
|
|
1001
|
+
if (this.isSoftKeyword("ENCODING")) {
|
|
1002
|
+
if (sourceKind === "JSON") throw new ParseError("JSON source is UTF-8 only; ENCODING is not allowed.", this.peek());
|
|
1003
|
+
this.advance();
|
|
1004
|
+
const value = this.parseIdentifier().toUpperCase();
|
|
1005
|
+
if (value !== "UTF8" && value !== "SJIS") throw new ParseError("ENCODING must be UTF8 or SJIS.", this.prev());
|
|
1006
|
+
encoding = value === "UTF8" ? "utf8" : "sjis";
|
|
1007
|
+
}
|
|
1008
|
+
if (this.peek().kind === "NOT" /* NOT */ && this.peekAt(1).kind === "IDENT" /* IDENT */ && this.peekAt(1).value.toUpperCase() === "HEADER") {
|
|
1009
|
+
if (sourceKind === "JSON") throw new ParseError("NO HEADER is CSV-only.", this.peek());
|
|
1010
|
+
this.advance();
|
|
1011
|
+
this.advance();
|
|
1012
|
+
hasHeader = false;
|
|
1013
|
+
} else if (this.isSoftKeyword("NO") && this.peekAt(1).kind === "IDENT" /* IDENT */ && this.peekAt(1).value.toUpperCase() === "HEADER") {
|
|
1014
|
+
if (sourceKind === "JSON") throw new ParseError("NO HEADER is CSV-only.", this.peek());
|
|
1015
|
+
this.advance();
|
|
1016
|
+
this.advance();
|
|
1017
|
+
hasHeader = false;
|
|
1018
|
+
}
|
|
1019
|
+
if (this.isSoftKeyword("COLUMNS")) {
|
|
1020
|
+
if (sourceKind === "JSON") throw new ParseError("COLUMNS is CSV-only.", this.peek());
|
|
1021
|
+
if (hasHeader) throw new ParseError("COLUMNS requires NO HEADER.", this.peek());
|
|
1022
|
+
this.advance();
|
|
1023
|
+
this.expect("(" /* LPAREN */);
|
|
1024
|
+
columns = this.parseIdentList();
|
|
1025
|
+
this.expect(")" /* RPAREN */);
|
|
1026
|
+
}
|
|
1027
|
+
let projection;
|
|
1028
|
+
if (this.peek().kind === "SELECT" /* SELECT */) {
|
|
1029
|
+
if (sourceKind === "JSON") throw new ParseError("SELECT projection is CSV-only.", this.peek());
|
|
1030
|
+
projection = this.parseSelect();
|
|
1031
|
+
if (projection.from.cteName !== NO_FROM_CTE_NAME || projection.joins.length > 0) {
|
|
1032
|
+
throw new ParseError("IMPORT projection cannot use FROM or JOIN.", this.prev());
|
|
1033
|
+
}
|
|
1034
|
+
if (projection.where || projection.groupBy.length || projection.having || projection.orderBy.length || projection.limit !== null || projection.offset !== null) {
|
|
1035
|
+
throw new ParseError("IMPORT projection supports SELECT expressions only.", this.prev());
|
|
1036
|
+
}
|
|
1037
|
+
this.validateImportProjectionScope(projection, this.prev());
|
|
1038
|
+
if (targets.some((item) => item.kind === "SUBTABLE")) {
|
|
1039
|
+
throw new ParseError("IMPORT subtable sources cannot use SELECT projection.", this.prev());
|
|
1040
|
+
}
|
|
1041
|
+
if (projection.columns.length !== fields.length) {
|
|
1042
|
+
throw new ParseError(`IMPORT projection has ${projection.columns.length} columns; target has ${fields.length}.`, this.prev());
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
let mappingMode = "POSITION";
|
|
1046
|
+
let ignoreUnknownColumns = false;
|
|
1047
|
+
if (this.peek().kind === "BY" /* BY */ || this.isSoftKeyword("BY")) {
|
|
1048
|
+
if (sourceKind === "JSON") throw new ParseError("BY NAME is CSV-only.", this.peek());
|
|
1049
|
+
this.advance();
|
|
1050
|
+
if (!this.isSoftKeyword("NAME")) throw new ParseError("BY must be followed by NAME in IMPORT.", this.peek());
|
|
1051
|
+
this.advance();
|
|
1052
|
+
if (!hasHeader) throw new ParseError("BY NAME requires HEADER.", this.prev());
|
|
1053
|
+
if (projection) throw new ParseError("BY NAME and SELECT projection are mutually exclusive.", this.prev());
|
|
1054
|
+
mappingMode = "BY_NAME";
|
|
1055
|
+
if (this.isSoftKeyword("IGNORE")) {
|
|
1056
|
+
this.advance();
|
|
1057
|
+
if (!this.isSoftKeyword("UNKNOWN")) throw new ParseError("IGNORE must be followed by UNKNOWN COLUMNS.", this.peek());
|
|
1058
|
+
this.advance();
|
|
1059
|
+
if (!this.isSoftKeyword("COLUMNS")) throw new ParseError("IGNORE UNKNOWN must be followed by COLUMNS.", this.peek());
|
|
1060
|
+
this.advance();
|
|
1061
|
+
ignoreUnknownColumns = true;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
let keyFields;
|
|
1065
|
+
let recordNumberSourceHeader;
|
|
1066
|
+
if (this.isSoftKeyword("MATCH")) {
|
|
1067
|
+
this.advance();
|
|
1068
|
+
for (const word of ["RECORD", "NUMBER", "SOURCE"]) {
|
|
1069
|
+
if (!this.isSoftKeyword(word)) throw new ParseError(`MATCH must be followed by RECORD NUMBER SOURCE <header>.`, this.peek());
|
|
1070
|
+
this.advance();
|
|
1071
|
+
}
|
|
1072
|
+
recordNumberSourceHeader = this.parseIdentifier();
|
|
1073
|
+
}
|
|
1074
|
+
if (this.peek().kind === "ON" /* ON */ && this.peekAt(1).kind === "DUPLICATE" /* DUPLICATE */) keyFields = this.parseOnDuplicate();
|
|
1075
|
+
let replaceSubtables;
|
|
1076
|
+
if (this.peek().kind === "REPLACE" /* REPLACE */ || this.isSoftKeyword("REPLACE")) {
|
|
1077
|
+
this.advance();
|
|
1078
|
+
if (!this.isSoftKeyword("SUBTABLES")) throw new ParseError("REPLACE must be followed by SUBTABLES (...).", this.peek());
|
|
1079
|
+
this.advance();
|
|
1080
|
+
this.expect("(" /* LPAREN */);
|
|
1081
|
+
replaceSubtables = this.parseIdentList();
|
|
1082
|
+
this.expect(")" /* RPAREN */);
|
|
1083
|
+
if (new Set(replaceSubtables).size !== replaceSubtables.length) throw new ParseError("REPLACE SUBTABLES contains duplicates.", this.prev());
|
|
1084
|
+
}
|
|
1085
|
+
const subtableTargets = targets.filter((item) => item.kind === "SUBTABLE");
|
|
1086
|
+
if (subtableTargets.length) {
|
|
1087
|
+
if (projection) throw new ParseError("IMPORT subtables cannot use SELECT projection.", this.prev());
|
|
1088
|
+
if (sourceKind === "JSON") {
|
|
1089
|
+
if (subtableTargets.some((item) => item.rowIdSourceHeader)) throw new ParseError("JSON subtable IMPORT does not accept ROW ID SOURCE.", this.prev());
|
|
1090
|
+
if (replaceSubtables) throw new ParseError("REPLACE SUBTABLES is CSV-only; JSON uses nested-array replacement semantics.", this.prev());
|
|
1091
|
+
} else {
|
|
1092
|
+
if (writeMode !== "UPDATE_RECORD_NUMBER" || !recordNumberSourceHeader) throw new ParseError("CSV subtable IMPORT requires IMPORT UPDATE and MATCH RECORD NUMBER SOURCE.", this.prev());
|
|
1093
|
+
if (mappingMode !== "BY_NAME") throw new ParseError("CSV subtable IMPORT requires BY NAME.", this.prev());
|
|
1094
|
+
if (!replaceSubtables) throw new ParseError("CSV subtable IMPORT requires REPLACE SUBTABLES (...).", this.prev());
|
|
1095
|
+
const replacement = new Set(replaceSubtables);
|
|
1096
|
+
for (const item of subtableTargets) {
|
|
1097
|
+
if (!item.rowIdSourceHeader) throw new ParseError(`CSV subtable ${item.subtableCode} requires ROW ID SOURCE <header>.`, this.prev());
|
|
1098
|
+
if (!replacement.has(item.subtableCode)) throw new ParseError(`IMPORT declares child columns for non-replaced subtable ${item.subtableCode}.`, this.prev());
|
|
1099
|
+
}
|
|
1100
|
+
for (const code of replacement) {
|
|
1101
|
+
if (!subtableTargets.some((item) => item.subtableCode === code)) throw new ParseError(`REPLACE SUBTABLES target ${code} is not declared in INTO.`, this.prev());
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
} else if (replaceSubtables) {
|
|
1105
|
+
throw new ParseError("REPLACE SUBTABLES requires subtable targets in INTO.", this.prev());
|
|
1106
|
+
}
|
|
1107
|
+
if (writeMode) {
|
|
1108
|
+
if (sourceKind !== "CSV") throw new ParseError("IMPORT UPDATE supports CSV only.", this.prev());
|
|
1109
|
+
if (mappingMode !== "BY_NAME") throw new ParseError("IMPORT UPDATE requires BY NAME.", this.prev());
|
|
1110
|
+
if (!recordNumberSourceHeader) throw new ParseError("IMPORT UPDATE requires MATCH RECORD NUMBER SOURCE <header>.", this.peek());
|
|
1111
|
+
if (keyFields) throw new ParseError("IMPORT UPDATE and ON DUPLICATE are mutually exclusive.", this.prev());
|
|
1112
|
+
} else if (recordNumberSourceHeader) {
|
|
1113
|
+
throw new ParseError("MATCH RECORD NUMBER SOURCE requires IMPORT UPDATE.", this.prev());
|
|
1114
|
+
}
|
|
1115
|
+
const checkGroups = this.parseCheckGroups();
|
|
1116
|
+
const control = this.parseDmlControlSuffix();
|
|
1117
|
+
return {
|
|
1118
|
+
type: "IMPORT",
|
|
1119
|
+
appId,
|
|
1120
|
+
fields,
|
|
1121
|
+
targets,
|
|
1122
|
+
source: sourceKind === "JSON" ? { kind: "JSON", sourceName } : { kind: "CSV", sourceName, encoding, hasHeader, mappingMode, ignoreUnknownColumns, ...columns ? { columns } : {}, ...projection ? { projection } : {} },
|
|
1123
|
+
...writeMode ? { writeMode, recordNumberSourceHeader } : {},
|
|
1124
|
+
...replaceSubtables ? { replaceSubtables } : {},
|
|
1125
|
+
...keyFields ? { keyFields } : {},
|
|
1126
|
+
...checkGroups,
|
|
1127
|
+
...control
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
validateImportProjectionScope(node, token) {
|
|
1131
|
+
if (Array.isArray(node)) {
|
|
1132
|
+
node.forEach((item) => this.validateImportProjectionScope(item, token));
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (node === null || typeof node !== "object") return;
|
|
1136
|
+
const value = node;
|
|
1137
|
+
if (value.type === "SCALAR_SUBQUERY" || value.type === "SCALAR_SUBQUERY_COL") {
|
|
1138
|
+
throw new ParseError("IMPORT projection cannot use subqueries.", token);
|
|
1139
|
+
}
|
|
1140
|
+
if (typeof value.tableAlias === "string") {
|
|
1141
|
+
throw new ParseError("IMPORT projection cannot use qualified column references.", token);
|
|
1142
|
+
}
|
|
1143
|
+
Object.values(value).forEach((item) => this.validateImportProjectionScope(item, token));
|
|
1144
|
+
}
|
|
933
1145
|
/** VALIDATE APP100 [(fields)] [WHERE ...] [CHECK ...] [INTO #err]. */
|
|
934
1146
|
parseValidate() {
|
|
935
1147
|
const validateTok = this.advance();
|
|
936
1148
|
const name = this.parseIdentifier();
|
|
937
1149
|
const { appId, subtableCode } = extractTableRef(name, this.prev());
|
|
938
|
-
if (subtableCode)
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1150
|
+
if (subtableCode) throw new ParseError(
|
|
1151
|
+
`VALIDATE APP${appId}$${subtableCode} \u306F\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002VALIDATE APP${appId} (${subtableCode}) \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044`,
|
|
1152
|
+
this.prev()
|
|
1153
|
+
);
|
|
1154
|
+
let targets;
|
|
942
1155
|
if (this.consume("(" /* LPAREN */)) {
|
|
943
|
-
|
|
1156
|
+
targets = [];
|
|
1157
|
+
do {
|
|
1158
|
+
const field = this.parseIdentifier();
|
|
1159
|
+
if (this.consume("(" /* LPAREN */)) {
|
|
1160
|
+
const children = this.peek().kind === ")" /* RPAREN */ ? [] : this.parseIdentList();
|
|
1161
|
+
this.expect(")" /* RPAREN */);
|
|
1162
|
+
targets.push({ kind: "SUBTABLE", subtableCode: field, children });
|
|
1163
|
+
} else {
|
|
1164
|
+
targets.push({ kind: "FIELD", field });
|
|
1165
|
+
}
|
|
1166
|
+
} while (this.consume("," /* COMMA */));
|
|
944
1167
|
this.expect(")" /* RPAREN */);
|
|
945
1168
|
}
|
|
1169
|
+
let summary;
|
|
1170
|
+
if (this.isSoftKeyword("SUMMARY")) {
|
|
1171
|
+
this.advance();
|
|
1172
|
+
summary = true;
|
|
1173
|
+
}
|
|
946
1174
|
const where = this.consume("WHERE" /* WHERE */) ? this.parseWhereExpr() : null;
|
|
947
1175
|
const checks = this.parseCheckGroups();
|
|
948
1176
|
let errorTable;
|
|
@@ -953,7 +1181,7 @@ var Parser = class {
|
|
|
953
1181
|
}
|
|
954
1182
|
errorTable = this.parseTableName();
|
|
955
1183
|
}
|
|
956
|
-
const stmt = { type: "VALIDATE", appId,
|
|
1184
|
+
const stmt = { type: "VALIDATE", appId, targets, ...summary ? { summary } : {}, where, ...checks, ...errorTable ? { errorTable } : {} };
|
|
957
1185
|
this.assertValidateExpressions(stmt, validateTok);
|
|
958
1186
|
return stmt;
|
|
959
1187
|
}
|
|
@@ -3094,7 +3322,7 @@ function getStatementType(stmt) {
|
|
|
3094
3322
|
return typeof obj.type === "string" ? obj.type : "UNKNOWN";
|
|
3095
3323
|
}
|
|
3096
3324
|
function isDmlType(type) {
|
|
3097
|
-
return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
|
|
3325
|
+
return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER" || type === "IMPORT";
|
|
3098
3326
|
}
|
|
3099
3327
|
function isReadOnlyType(type) {
|
|
3100
3328
|
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";
|
|
@@ -4591,7 +4819,7 @@ function analyzeBatch(statements) {
|
|
|
4591
4819
|
dependsOn.add(at);
|
|
4592
4820
|
}
|
|
4593
4821
|
if (validationTable) {
|
|
4594
|
-
const payloadFields = stmt.type === "VALIDATE" ? ["$id", "$err_field", "$err_code", "$err_message", "$err_value"] : stmt.type === "UPDATE" ? ["$id", ...stmt.assignments.map((a) => a.field)] : "fields" in stmt ? stmt.fields : [];
|
|
4822
|
+
const payloadFields = stmt.type === "VALIDATE" ? stmt.summary ? ["$id", "$err_subtable", "$err_field", "$err_code", "$err_count"] : ["$id", "$err_field", "$err_code", "$err_message", "$err_value", "$err_subtable", "$err_subrow", "$err_subrow_id", "$err_count"] : 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 : [];
|
|
4595
4823
|
const signature = JSON.stringify(payloadFields);
|
|
4596
4824
|
const at = defined.get(validationTable);
|
|
4597
4825
|
if (at === void 0) {
|
|
@@ -7456,7 +7684,7 @@ function validateAndNormalizeDmlValue(raw, field, numberPrecision) {
|
|
|
7456
7684
|
return { ok: false, code: "ERR_LENGTH_MAX", message: `${field.code} \u306F ${max} \u6587\u5B57\u4EE5\u4E0B\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044` };
|
|
7457
7685
|
}
|
|
7458
7686
|
}
|
|
7459
|
-
if (CHOICE_TYPES.has(field.fieldType) && field.optionOrder) {
|
|
7687
|
+
if (!isEmpty(value) && CHOICE_TYPES.has(field.fieldType) && field.optionOrder) {
|
|
7460
7688
|
const selected = Array.isArray(value) ? value.map(String) : [String(value)];
|
|
7461
7689
|
if (selected.some((choice) => !(choice in field.optionOrder))) {
|
|
7462
7690
|
return { ok: false, code: "ERR_CHOICE_INVALID", message: `${field.code} \u306B\u5B9A\u7FA9\u5916\u306E\u9078\u629E\u80A2\u304C\u3042\u308A\u307E\u3059` };
|
|
@@ -7553,9 +7781,15 @@ function validateDmlCandidates(candidates, operation, payloadFields, targetField
|
|
|
7553
7781
|
candidate.record ??= {};
|
|
7554
7782
|
const rowErrors = includePreErrors ? [...candidate.preErrors] : [];
|
|
7555
7783
|
for (const code of targetFields) {
|
|
7784
|
+
if (!candidate.payload.has(code)) continue;
|
|
7556
7785
|
const result = validateAndNormalizeDmlValue(candidate.payload.get(code), infoByCode.get(code), numberPrecision);
|
|
7557
7786
|
if (!result.ok) rowErrors.push({ field: code, code: result.code, message: result.message });
|
|
7558
|
-
else
|
|
7787
|
+
else {
|
|
7788
|
+
const original = candidate.payload.get(code);
|
|
7789
|
+
const type = infoByCode.get(code).fieldType;
|
|
7790
|
+
const preserveCodes = ["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"].includes(type) && Array.isArray(original) && original.every((item) => typeof item === "object" && item !== null && "code" in item);
|
|
7791
|
+
candidate.record[code] = { value: preserveCodes ? original : result.value };
|
|
7792
|
+
}
|
|
7559
7793
|
}
|
|
7560
7794
|
if (validateMissingCreateFields && candidate.mode === "create") {
|
|
7561
7795
|
for (const info of fieldInfos) {
|
|
@@ -7799,22 +8033,878 @@ function combineLogical(op, left, right) {
|
|
|
7799
8033
|
if (left.capability === "UNSUPPORTED" || right.capability === "UNSUPPORTED") {
|
|
7800
8034
|
return { capability: "UNSUPPORTED", reasons };
|
|
7801
8035
|
}
|
|
7802
|
-
if (left.capability === "EXACT_PUSHDOWN" && right.capability === "EXACT_PUSHDOWN") {
|
|
7803
|
-
return { capability: "EXACT_PUSHDOWN", reasons };
|
|
8036
|
+
if (left.capability === "EXACT_PUSHDOWN" && right.capability === "EXACT_PUSHDOWN") {
|
|
8037
|
+
return { capability: "EXACT_PUSHDOWN", reasons };
|
|
8038
|
+
}
|
|
8039
|
+
if (op === "AND" && (left.capability === "EXACT_PUSHDOWN" || right.capability === "EXACT_PUSHDOWN" || left.capability === "SUPERSET_PREFILTER" || right.capability === "SUPERSET_PREFILTER")) {
|
|
8040
|
+
return {
|
|
8041
|
+
capability: "SUPERSET_PREFILTER",
|
|
8042
|
+
reasons: [{ code: "WHERE_SUPERSET_PREFILTER" }, ...reasons]
|
|
8043
|
+
};
|
|
8044
|
+
}
|
|
8045
|
+
return { capability: "LOCAL_ONLY", reasons };
|
|
8046
|
+
}
|
|
8047
|
+
function localExpression() {
|
|
8048
|
+
return { capability: "LOCAL_ONLY", reasons: [{ code: "WHERE_EXPRESSION_LOCAL_ONLY" }] };
|
|
8049
|
+
}
|
|
8050
|
+
function unsupported(code, field, fieldType, operator) {
|
|
8051
|
+
return { capability: "UNSUPPORTED", reasons: [{ code, field, fieldType, operator }] };
|
|
8052
|
+
}
|
|
8053
|
+
|
|
8054
|
+
// src/import/sourceLoader.ts
|
|
8055
|
+
var IMPORT_MAX_BYTES = 10 * 1024 * 1024;
|
|
8056
|
+
var ImportSourceError = class extends Error {
|
|
8057
|
+
constructor(message) {
|
|
8058
|
+
super(`ImportSourceError: ${message}`);
|
|
8059
|
+
this.name = "ImportSourceError";
|
|
8060
|
+
}
|
|
8061
|
+
};
|
|
8062
|
+
function resolveImportSource(name, resolver) {
|
|
8063
|
+
if (!resolver) throw new ImportSourceError("IMPORT source capability is not available.");
|
|
8064
|
+
const handle = resolver(name);
|
|
8065
|
+
if (!handle) throw new ImportSourceError(`source "${name}" is not supplied.`);
|
|
8066
|
+
return handle;
|
|
8067
|
+
}
|
|
8068
|
+
async function loadImportSource(handle, cache) {
|
|
8069
|
+
let pending = cache.get(handle);
|
|
8070
|
+
if (!pending) {
|
|
8071
|
+
pending = handle.load().then((payload) => {
|
|
8072
|
+
if (!(payload.bytes instanceof Uint8Array)) throw new ImportSourceError("loader must return Uint8Array bytes.");
|
|
8073
|
+
if (payload.bytes.byteLength > IMPORT_MAX_BYTES) {
|
|
8074
|
+
throw new ImportSourceError(`source exceeds the ${IMPORT_MAX_BYTES} byte limit.`);
|
|
8075
|
+
}
|
|
8076
|
+
return payload;
|
|
8077
|
+
});
|
|
8078
|
+
cache.set(handle, pending);
|
|
8079
|
+
}
|
|
8080
|
+
return pending;
|
|
8081
|
+
}
|
|
8082
|
+
|
|
8083
|
+
// src/import/csvDecoder.ts
|
|
8084
|
+
function decodeImportText(bytes, encoding) {
|
|
8085
|
+
try {
|
|
8086
|
+
return new TextDecoder(encoding === "sjis" ? "shift_jis" : "utf-8", { fatal: true }).decode(bytes).replace(/^\uFEFF/, "");
|
|
8087
|
+
} catch {
|
|
8088
|
+
throw new ImportSourceError(`invalid ${encoding.toUpperCase()} byte sequence.`);
|
|
8089
|
+
}
|
|
8090
|
+
}
|
|
8091
|
+
function parseRfc4180(text) {
|
|
8092
|
+
const records = [];
|
|
8093
|
+
let record = [];
|
|
8094
|
+
let cell = "";
|
|
8095
|
+
let quoted = false;
|
|
8096
|
+
let afterQuote = false;
|
|
8097
|
+
let i = 0;
|
|
8098
|
+
const finishCell = () => {
|
|
8099
|
+
record.push(cell);
|
|
8100
|
+
cell = "";
|
|
8101
|
+
afterQuote = false;
|
|
8102
|
+
};
|
|
8103
|
+
const finishRecord = () => {
|
|
8104
|
+
finishCell();
|
|
8105
|
+
records.push(record);
|
|
8106
|
+
record = [];
|
|
8107
|
+
};
|
|
8108
|
+
while (i < text.length) {
|
|
8109
|
+
const ch = text[i];
|
|
8110
|
+
if (quoted) {
|
|
8111
|
+
if (ch === '"') {
|
|
8112
|
+
if (text[i + 1] === '"') {
|
|
8113
|
+
cell += '"';
|
|
8114
|
+
i += 2;
|
|
8115
|
+
continue;
|
|
8116
|
+
}
|
|
8117
|
+
quoted = false;
|
|
8118
|
+
afterQuote = true;
|
|
8119
|
+
i++;
|
|
8120
|
+
continue;
|
|
8121
|
+
}
|
|
8122
|
+
cell += ch;
|
|
8123
|
+
i++;
|
|
8124
|
+
continue;
|
|
8125
|
+
}
|
|
8126
|
+
if (afterQuote && ch !== "," && ch !== "\r" && ch !== "\n") {
|
|
8127
|
+
throw new ImportSourceError(`unexpected character after closing quote at offset ${i}.`);
|
|
8128
|
+
}
|
|
8129
|
+
if (ch === '"') {
|
|
8130
|
+
if (cell.length !== 0) throw new ImportSourceError(`quote in unquoted cell at offset ${i}.`);
|
|
8131
|
+
quoted = true;
|
|
8132
|
+
i++;
|
|
8133
|
+
continue;
|
|
8134
|
+
}
|
|
8135
|
+
if (ch === ",") {
|
|
8136
|
+
finishCell();
|
|
8137
|
+
i++;
|
|
8138
|
+
continue;
|
|
8139
|
+
}
|
|
8140
|
+
if (ch === "\r" || ch === "\n") {
|
|
8141
|
+
if (ch === "\r" && text[i + 1] === "\n") i++;
|
|
8142
|
+
finishRecord();
|
|
8143
|
+
i++;
|
|
8144
|
+
continue;
|
|
8145
|
+
}
|
|
8146
|
+
cell += ch;
|
|
8147
|
+
i++;
|
|
8148
|
+
}
|
|
8149
|
+
if (quoted) throw new ImportSourceError("unterminated quoted cell.");
|
|
8150
|
+
if (cell.length > 0 || record.length > 0 || afterQuote) finishRecord();
|
|
8151
|
+
return records;
|
|
8152
|
+
}
|
|
8153
|
+
function assertColumns(columns) {
|
|
8154
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8155
|
+
columns.forEach((column, index) => {
|
|
8156
|
+
if (column === "") throw new ImportSourceError(`CSV column ${index + 1} has an empty name.`);
|
|
8157
|
+
if (seen.has(column)) throw new ImportSourceError(`CSV column name "${column}" is duplicated.`);
|
|
8158
|
+
seen.add(column);
|
|
8159
|
+
});
|
|
8160
|
+
}
|
|
8161
|
+
function decodeCsv(bytes, options) {
|
|
8162
|
+
const records = parseRfc4180(decodeImportText(bytes, options.encoding));
|
|
8163
|
+
let columns;
|
|
8164
|
+
let rows;
|
|
8165
|
+
if (options.hasHeader) {
|
|
8166
|
+
columns = records[0] ?? [];
|
|
8167
|
+
rows = records.slice(1);
|
|
8168
|
+
} else {
|
|
8169
|
+
rows = records;
|
|
8170
|
+
columns = options.columns ? [...options.columns] : Array.from({ length: rows[0]?.length ?? 0 }, (_, i) => `c${i + 1}`);
|
|
8171
|
+
}
|
|
8172
|
+
assertColumns(columns);
|
|
8173
|
+
if (rows.length === 0) throw new ImportSourceError("CSV has no data rows.");
|
|
8174
|
+
rows.forEach((row, i) => {
|
|
8175
|
+
if (row.length !== columns.length) {
|
|
8176
|
+
throw new ImportSourceError(`CSV row ${i + (options.hasHeader ? 2 : 1)} has ${row.length} cells; expected ${columns.length}.`);
|
|
8177
|
+
}
|
|
8178
|
+
});
|
|
8179
|
+
return { columns, rows };
|
|
8180
|
+
}
|
|
8181
|
+
|
|
8182
|
+
// src/import/convertImportCsvValue.ts
|
|
8183
|
+
var LF_MULTI_TYPES = /* @__PURE__ */ new Set([
|
|
8184
|
+
"CHECK_BOX",
|
|
8185
|
+
"MULTI_SELECT",
|
|
8186
|
+
"USER_SELECT",
|
|
8187
|
+
"ORGANIZATION_SELECT",
|
|
8188
|
+
"GROUP_SELECT"
|
|
8189
|
+
]);
|
|
8190
|
+
var USER_TYPES2 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
8191
|
+
var ImportCsvValueError = class extends Error {
|
|
8192
|
+
constructor() {
|
|
8193
|
+
super("multiple-value CSV cell contains an empty LF-delimited item");
|
|
8194
|
+
this.code = "ERR_IMPORT_MULTI_EMPTY_ITEM";
|
|
8195
|
+
this.name = "ImportCsvValueError";
|
|
8196
|
+
}
|
|
8197
|
+
};
|
|
8198
|
+
function convertImportCsvValue(raw, type, options) {
|
|
8199
|
+
void options;
|
|
8200
|
+
if (!LF_MULTI_TYPES.has(type ?? "")) return raw;
|
|
8201
|
+
if (raw === "") return [];
|
|
8202
|
+
const items = raw.split(/\r\n|\n/);
|
|
8203
|
+
if (items.some((item) => item === "")) throw new ImportCsvValueError();
|
|
8204
|
+
return USER_TYPES2.has(type ?? "") ? items.map((code) => ({ code })) : items;
|
|
8205
|
+
}
|
|
8206
|
+
|
|
8207
|
+
// src/import/jsonTokenizer.ts
|
|
8208
|
+
function fail(message, offset, line, column) {
|
|
8209
|
+
throw new ImportSourceError(`JSON ${message} (offset=${offset}, line=${line}, column=${column}).`);
|
|
8210
|
+
}
|
|
8211
|
+
function decodeUtf8Json(bytes) {
|
|
8212
|
+
try {
|
|
8213
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
8214
|
+
} catch {
|
|
8215
|
+
throw new ImportSourceError("JSON source is not valid UTF-8.");
|
|
8216
|
+
}
|
|
8217
|
+
}
|
|
8218
|
+
function tokenizeJson(text) {
|
|
8219
|
+
const tokens = [];
|
|
8220
|
+
let i = 0, line = 1, column = 1;
|
|
8221
|
+
const advance = () => {
|
|
8222
|
+
const ch = text[i++];
|
|
8223
|
+
if (ch === "\n") {
|
|
8224
|
+
line++;
|
|
8225
|
+
column = 1;
|
|
8226
|
+
} else column++;
|
|
8227
|
+
return ch;
|
|
8228
|
+
};
|
|
8229
|
+
const position = () => ({ offset: i, line, column });
|
|
8230
|
+
while (i < text.length) {
|
|
8231
|
+
const ch = text[i];
|
|
8232
|
+
if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
|
|
8233
|
+
advance();
|
|
8234
|
+
continue;
|
|
8235
|
+
}
|
|
8236
|
+
const start = position();
|
|
8237
|
+
if ("{}[]:,".includes(ch)) {
|
|
8238
|
+
advance();
|
|
8239
|
+
tokens.push({ kind: "punct", value: ch, ...start });
|
|
8240
|
+
continue;
|
|
8241
|
+
}
|
|
8242
|
+
if (ch === '"') {
|
|
8243
|
+
advance();
|
|
8244
|
+
let value = "";
|
|
8245
|
+
let closed = false;
|
|
8246
|
+
while (i < text.length) {
|
|
8247
|
+
const c = advance();
|
|
8248
|
+
if (c === '"') {
|
|
8249
|
+
closed = true;
|
|
8250
|
+
break;
|
|
8251
|
+
}
|
|
8252
|
+
if (c.charCodeAt(0) < 32) fail("string contains an unescaped control character", start.offset, start.line, start.column);
|
|
8253
|
+
if (c !== "\\") {
|
|
8254
|
+
value += c;
|
|
8255
|
+
continue;
|
|
8256
|
+
}
|
|
8257
|
+
if (i >= text.length) fail("string has an unterminated escape", start.offset, start.line, start.column);
|
|
8258
|
+
const esc = advance();
|
|
8259
|
+
const simple = { '"': '"', "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: " " };
|
|
8260
|
+
if (esc in simple) {
|
|
8261
|
+
value += simple[esc];
|
|
8262
|
+
continue;
|
|
8263
|
+
}
|
|
8264
|
+
if (esc !== "u") fail(`has invalid escape \\${esc}`, i - 2, line, Math.max(1, column - 2));
|
|
8265
|
+
const hex = text.slice(i, i + 4);
|
|
8266
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) fail("has invalid unicode escape", i, line, column);
|
|
8267
|
+
for (let n = 0; n < 4; n++) advance();
|
|
8268
|
+
const code = Number.parseInt(hex, 16);
|
|
8269
|
+
if (code >= 55296 && code <= 56319) {
|
|
8270
|
+
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);
|
|
8271
|
+
advance();
|
|
8272
|
+
advance();
|
|
8273
|
+
const lowHex = text.slice(i, i + 4);
|
|
8274
|
+
for (let n = 0; n < 4; n++) advance();
|
|
8275
|
+
const low = Number.parseInt(lowHex, 16);
|
|
8276
|
+
if (low < 56320 || low > 57343) fail("has an invalid surrogate pair", i - 4, line, Math.max(1, column - 4));
|
|
8277
|
+
value += String.fromCodePoint(65536 + (code - 55296 << 10) + low - 56320);
|
|
8278
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
8279
|
+
fail("has an unpaired low surrogate", i - 4, line, Math.max(1, column - 4));
|
|
8280
|
+
} else value += String.fromCharCode(code);
|
|
8281
|
+
}
|
|
8282
|
+
if (!closed) fail("string is unterminated", start.offset, start.line, start.column);
|
|
8283
|
+
tokens.push({ kind: "string", value, ...start });
|
|
8284
|
+
continue;
|
|
8285
|
+
}
|
|
8286
|
+
const rest = text.slice(i);
|
|
8287
|
+
const number = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(rest)?.[0];
|
|
8288
|
+
if (number) {
|
|
8289
|
+
for (let n = 0; n < number.length; n++) advance();
|
|
8290
|
+
tokens.push({ kind: "number", lexeme: number, ...start });
|
|
8291
|
+
continue;
|
|
8292
|
+
}
|
|
8293
|
+
const literal = /^(true|false|null)/.exec(rest)?.[0];
|
|
8294
|
+
if (literal) {
|
|
8295
|
+
for (let n = 0; n < literal.length; n++) advance();
|
|
8296
|
+
tokens.push({ kind: "literal", value: literal === "true" ? true : literal === "false" ? false : null, ...start });
|
|
8297
|
+
continue;
|
|
8298
|
+
}
|
|
8299
|
+
fail(`has an unexpected token ${JSON.stringify(ch)}`, start.offset, start.line, start.column);
|
|
8300
|
+
}
|
|
8301
|
+
tokens.push({ kind: "eof", offset: i, line, column });
|
|
8302
|
+
return tokens;
|
|
8303
|
+
}
|
|
8304
|
+
|
|
8305
|
+
// src/import/jsonDecoder.ts
|
|
8306
|
+
function describe(token) {
|
|
8307
|
+
return token.kind === "eof" ? "end of input" : token.kind === "punct" ? token.value : token.kind;
|
|
8308
|
+
}
|
|
8309
|
+
function decodeJsonRecords(bytes) {
|
|
8310
|
+
if (bytes.byteLength === 0) throw new ImportSourceError("JSON source is empty.");
|
|
8311
|
+
const tokens = tokenizeJson(decodeUtf8Json(bytes));
|
|
8312
|
+
let index = 0;
|
|
8313
|
+
const fail3 = (message, token = tokens[index]) => {
|
|
8314
|
+
throw new ImportSourceError(`JSON ${message} (offset=${token.offset}, line=${token.line}, column=${token.column}).`);
|
|
8315
|
+
};
|
|
8316
|
+
const isPunct = (token, value) => token.kind === "punct" && token.value === value;
|
|
8317
|
+
const punct = (value) => {
|
|
8318
|
+
const token = tokens[index];
|
|
8319
|
+
if (token.kind !== "punct" || token.value !== value) fail3(`expected ${value}; found ${describe(token)}`, token);
|
|
8320
|
+
index++;
|
|
8321
|
+
};
|
|
8322
|
+
const parseValue = () => {
|
|
8323
|
+
const token = tokens[index++];
|
|
8324
|
+
if (token.kind === "string") return token.value;
|
|
8325
|
+
if (token.kind === "number") return { kind: "number", lexeme: token.lexeme };
|
|
8326
|
+
if (token.kind === "literal") return token.value;
|
|
8327
|
+
if (token.kind === "punct" && token.value === "{") {
|
|
8328
|
+
const object = /* @__PURE__ */ new Map();
|
|
8329
|
+
if (isPunct(tokens[index], "}")) {
|
|
8330
|
+
index++;
|
|
8331
|
+
return object;
|
|
8332
|
+
}
|
|
8333
|
+
while (true) {
|
|
8334
|
+
const key = tokens[index++];
|
|
8335
|
+
if (key.kind !== "string") return fail3(`object key must be a string; found ${describe(key)}`, key);
|
|
8336
|
+
const keyValue = key.value;
|
|
8337
|
+
if (object.has(keyValue)) fail3(`duplicate key ${JSON.stringify(keyValue)}`, key);
|
|
8338
|
+
punct(":");
|
|
8339
|
+
object.set(keyValue, parseValue());
|
|
8340
|
+
const separator = tokens[index];
|
|
8341
|
+
if (isPunct(separator, "}")) {
|
|
8342
|
+
index++;
|
|
8343
|
+
break;
|
|
8344
|
+
}
|
|
8345
|
+
punct(",");
|
|
8346
|
+
}
|
|
8347
|
+
return object;
|
|
8348
|
+
}
|
|
8349
|
+
if (token.kind === "punct" && token.value === "[") {
|
|
8350
|
+
const array = [];
|
|
8351
|
+
if (isPunct(tokens[index], "]")) {
|
|
8352
|
+
index++;
|
|
8353
|
+
return array;
|
|
8354
|
+
}
|
|
8355
|
+
while (true) {
|
|
8356
|
+
array.push(parseValue());
|
|
8357
|
+
const separator = tokens[index];
|
|
8358
|
+
if (isPunct(separator, "]")) {
|
|
8359
|
+
index++;
|
|
8360
|
+
break;
|
|
8361
|
+
}
|
|
8362
|
+
punct(",");
|
|
8363
|
+
}
|
|
8364
|
+
return array;
|
|
8365
|
+
}
|
|
8366
|
+
return fail3(`expected a value; found ${describe(token)}`, token);
|
|
8367
|
+
};
|
|
8368
|
+
const root = parseValue();
|
|
8369
|
+
if (tokens[index].kind !== "eof") fail3(`has trailing data; found ${describe(tokens[index])}`);
|
|
8370
|
+
const records = root instanceof Map ? [root] : Array.isArray(root) ? root : fail3("root must be an object or array.", tokens[0]);
|
|
8371
|
+
if (records.length === 0) throw new ImportSourceError("JSON source contains no records.");
|
|
8372
|
+
records.forEach((record, i) => {
|
|
8373
|
+
if (!(record instanceof Map)) throw new ImportSourceError(`JSON record ${i + 1} must be an object.`);
|
|
8374
|
+
});
|
|
8375
|
+
return records;
|
|
8376
|
+
}
|
|
8377
|
+
|
|
8378
|
+
// src/import/jsonMaterializer.ts
|
|
8379
|
+
var STRING_ARRAY_TYPES = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
|
|
8380
|
+
var CODE_ARRAY_TYPES = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
8381
|
+
function fail2(row, field, message) {
|
|
8382
|
+
throw new ImportSourceError(`JSON field validation failed (row=${row}, field=${field}): ${message}`);
|
|
8383
|
+
}
|
|
8384
|
+
function isNumber(value) {
|
|
8385
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Map) && value.kind === "number";
|
|
8386
|
+
}
|
|
8387
|
+
function materializeValue(value, target, row) {
|
|
8388
|
+
if (value === null) return "";
|
|
8389
|
+
if (typeof value === "string") return value;
|
|
8390
|
+
if (typeof value === "boolean") fail2(row, target.code, "boolean is not accepted.");
|
|
8391
|
+
if (isNumber(value)) {
|
|
8392
|
+
if (target.fieldType === "NUMBER") fail2(row, target.code, "precision target requires a JSON string.");
|
|
8393
|
+
if (!/^-?(?:0|[1-9]\d*)$/.test(value.lexeme) || value.lexeme === "-0") {
|
|
8394
|
+
fail2(row, target.code, `JSON number ${value.lexeme} must be a non-negative-zero safe integer lexeme.`);
|
|
8395
|
+
}
|
|
8396
|
+
const number = Number(value.lexeme);
|
|
8397
|
+
if (!Number.isSafeInteger(number)) fail2(row, target.code, `JSON number ${value.lexeme} is outside the safe integer range.`);
|
|
8398
|
+
return String(number);
|
|
8399
|
+
}
|
|
8400
|
+
if (value instanceof Map) fail2(row, target.code, "object is not accepted for a flat field.");
|
|
8401
|
+
if (!Array.isArray(value)) fail2(row, target.code, "unsupported value type.");
|
|
8402
|
+
if (!STRING_ARRAY_TYPES.has(target.fieldType) && !CODE_ARRAY_TYPES.has(target.fieldType)) {
|
|
8403
|
+
fail2(row, target.code, "array is accepted only for multi-value fields.");
|
|
8404
|
+
}
|
|
8405
|
+
const strings = value.map((entry) => {
|
|
8406
|
+
if (typeof entry !== "string") fail2(row, target.code, "array elements must be strings.");
|
|
8407
|
+
return entry;
|
|
8408
|
+
});
|
|
8409
|
+
if (new Set(strings).size !== strings.length) fail2(row, target.code, "array elements must not contain duplicates.");
|
|
8410
|
+
return CODE_ARRAY_TYPES.has(target.fieldType) ? JSON.stringify(strings.map((code) => ({ code }))) : JSON.stringify(strings);
|
|
8411
|
+
}
|
|
8412
|
+
function materializeJsonDmlSource(_source, payload, targets, maxRows) {
|
|
8413
|
+
if (payload.encoding && payload.encoding !== "utf8") throw new ImportSourceError("JSON source is UTF-8 only.");
|
|
8414
|
+
const records = decodeJsonRecords(payload.bytes);
|
|
8415
|
+
if (records.length > maxRows) throw new ImportSourceError(`source rows (${records.length}) exceed maxRecords (${maxRows}).`);
|
|
8416
|
+
const targetByCode = new Map(targets.map((target) => [target.code, target]));
|
|
8417
|
+
if (targetByCode.size !== targets.length) throw new ImportSourceError("JSON target fields contain duplicates.");
|
|
8418
|
+
const rows = [];
|
|
8419
|
+
const importPresence = [];
|
|
8420
|
+
records.forEach((record, index) => {
|
|
8421
|
+
for (const key of record.keys()) {
|
|
8422
|
+
if (!targetByCode.has(key)) fail2(index + 1, key, "unknown key (not declared in INTO).");
|
|
8423
|
+
}
|
|
8424
|
+
const row = {};
|
|
8425
|
+
const present = /* @__PURE__ */ new Set();
|
|
8426
|
+
for (const target of targets) {
|
|
8427
|
+
if (!record.has(target.code)) continue;
|
|
8428
|
+
present.add(target.code);
|
|
8429
|
+
row[target.code] = materializeValue(record.get(target.code), target, index + 1);
|
|
8430
|
+
}
|
|
8431
|
+
rows.push(row);
|
|
8432
|
+
importPresence.push(present);
|
|
8433
|
+
});
|
|
8434
|
+
return {
|
|
8435
|
+
rows,
|
|
8436
|
+
columns: targets.map((target) => target.code),
|
|
8437
|
+
columnMeta: new Map(targets.map((target) => [target.code, { fieldType: target.fieldType }])),
|
|
8438
|
+
importPresence
|
|
8439
|
+
};
|
|
8440
|
+
}
|
|
8441
|
+
|
|
8442
|
+
// src/import/materializeDmlSource.ts
|
|
8443
|
+
function materializeCsvDmlSource(source, payload, maxRows, targetCodes, fieldInfos, recordNumberSourceHeader) {
|
|
8444
|
+
const decoded = decodeCsv(payload.bytes, {
|
|
8445
|
+
encoding: source.encoding ?? payload.encoding ?? "utf8",
|
|
8446
|
+
hasHeader: source.hasHeader,
|
|
8447
|
+
columns: source.columns
|
|
8448
|
+
});
|
|
8449
|
+
if (decoded.rows.length > maxRows) {
|
|
8450
|
+
throw new ImportSourceError(`source rows (${decoded.rows.length}) exceed maxRecords (${maxRows}).`);
|
|
8451
|
+
}
|
|
8452
|
+
if (source.mappingMode === "BY_NAME") {
|
|
8453
|
+
if (!targetCodes || !fieldInfos) throw new Error("InternalError: BY NAME requires destination form metadata.");
|
|
8454
|
+
if (new Set(targetCodes).size !== targetCodes.length) {
|
|
8455
|
+
throw new ImportSourceError("ERR_IMPORT_HEADER_REUSED: a BY NAME header cannot be consumed more than once.");
|
|
8456
|
+
}
|
|
8457
|
+
const indexes = new Map(decoded.columns.map((column, index) => [column, index]));
|
|
8458
|
+
for (const code of targetCodes) {
|
|
8459
|
+
if (!indexes.has(code)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${code}" is missing.`);
|
|
8460
|
+
}
|
|
8461
|
+
const infoByCode = new Map(fieldInfos.map((info) => [info.code, info]));
|
|
8462
|
+
const targetSet = new Set(targetCodes);
|
|
8463
|
+
if (recordNumberSourceHeader && targetSet.has(recordNumberSourceHeader)) {
|
|
8464
|
+
throw new ImportSourceError("ERR_IMPORT_HEADER_REUSED: record-number source header is lookup-only and cannot be a write target.");
|
|
8465
|
+
}
|
|
8466
|
+
if (recordNumberSourceHeader && !indexes.has(recordNumberSourceHeader)) {
|
|
8467
|
+
throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${recordNumberSourceHeader}" is missing.`);
|
|
8468
|
+
}
|
|
8469
|
+
const ignoredKnownColumns = [];
|
|
8470
|
+
const ignoredUnknownColumns = [];
|
|
8471
|
+
const nonEmpty = (index) => decoded.rows.filter((row) => row[index] !== "").length;
|
|
8472
|
+
const reasonFor = (info) => {
|
|
8473
|
+
if (info.fieldType === "FILE") return "FILE attachment is outside flat IMPORT scope";
|
|
8474
|
+
if (info.inSubtable || info.fieldType === "SUBTABLE") return "subtable field is not writable in Phase 3";
|
|
8475
|
+
if (info.writable === false) return `non-writable ${info.fieldType} field`;
|
|
8476
|
+
return `known export-only ${info.fieldType} field`;
|
|
8477
|
+
};
|
|
8478
|
+
for (const [index, column] of decoded.columns.entries()) {
|
|
8479
|
+
if (targetSet.has(column) || column === recordNumberSourceHeader) continue;
|
|
8480
|
+
const info = infoByCode.get(column);
|
|
8481
|
+
if (info) ignoredKnownColumns.push({ column, reason: reasonFor(info), nonEmptyCells: nonEmpty(index) });
|
|
8482
|
+
else if (!source.ignoreUnknownColumns) throw new ImportSourceError(`ERR_IMPORT_UNKNOWN_COLUMN: unknown CSV header "${column}".`);
|
|
8483
|
+
else ignoredUnknownColumns.push({ column, reason: "unknown column ignored by explicit policy", nonEmptyCells: nonEmpty(index) });
|
|
8484
|
+
}
|
|
8485
|
+
for (const code of targetCodes) {
|
|
8486
|
+
const info = infoByCode.get(code);
|
|
8487
|
+
if (!info) throw new Error(`ArgumentError: DML target field ${code} does not exist.`);
|
|
8488
|
+
if (info.inSubtable || info.writable === false || info.fieldType === "FILE" || info.fieldType === "SUBTABLE") {
|
|
8489
|
+
throw new Error(`ArgumentError: DML target field ${code} is not writable (${info.fieldType}).`);
|
|
8490
|
+
}
|
|
8491
|
+
}
|
|
8492
|
+
const importRowErrors = [];
|
|
8493
|
+
const rows2 = decoded.rows.map((values) => {
|
|
8494
|
+
const errors = [];
|
|
8495
|
+
const row = {};
|
|
8496
|
+
for (const code of targetCodes) {
|
|
8497
|
+
const raw = values[indexes.get(code)];
|
|
8498
|
+
try {
|
|
8499
|
+
row[code] = convertImportCsvValue(raw, infoByCode.get(code)?.fieldType, { cliKintone: true });
|
|
8500
|
+
} catch (error) {
|
|
8501
|
+
if (!(error instanceof ImportCsvValueError)) throw error;
|
|
8502
|
+
row[code] = raw;
|
|
8503
|
+
errors.push({ field: code, code: error.code, message: error.message });
|
|
8504
|
+
}
|
|
8505
|
+
}
|
|
8506
|
+
importRowErrors.push(errors);
|
|
8507
|
+
return row;
|
|
8508
|
+
});
|
|
8509
|
+
return {
|
|
8510
|
+
rows: rows2,
|
|
8511
|
+
columns: [...targetCodes],
|
|
8512
|
+
columnMeta: new Map(targetCodes.map((code) => [code, { fieldType: infoByCode.get(code)?.fieldType ?? "SINGLE_LINE_TEXT" }])),
|
|
8513
|
+
importRowErrors,
|
|
8514
|
+
...recordNumberSourceHeader ? { recordNumberSourceValues: decoded.rows.map((row) => row[indexes.get(recordNumberSourceHeader)]) } : {},
|
|
8515
|
+
importAudit: { mapping: "BY_NAME", writtenColumns: [...targetCodes], ignoredKnownColumns, ignoredUnknownColumns }
|
|
8516
|
+
};
|
|
8517
|
+
}
|
|
8518
|
+
const rows = decoded.rows.map((values) => Object.fromEntries(decoded.columns.map((column, i) => [column, values[i]])));
|
|
8519
|
+
return {
|
|
8520
|
+
rows,
|
|
8521
|
+
columns: decoded.columns,
|
|
8522
|
+
// CSV cells are decoded as strings; projections such as CAST may replace this metadata downstream.
|
|
8523
|
+
columnMeta: new Map(decoded.columns.map((column) => [column, { fieldType: "SINGLE_LINE_TEXT" }]))
|
|
8524
|
+
};
|
|
8525
|
+
}
|
|
8526
|
+
|
|
8527
|
+
// src/import/importRecordsMaterializer.ts
|
|
8528
|
+
var sourceFail = (parentRow, code, message) => {
|
|
8529
|
+
throw new ImportSourceError(`JSON subtable validation failed (parentRow=${parentRow}, field=${code}): ${message}`);
|
|
8530
|
+
};
|
|
8531
|
+
function materializeJsonImportRecords(_source, payload, targets, maxParents, maxChildRows = maxParents) {
|
|
8532
|
+
if (payload.encoding && payload.encoding !== "utf8") throw new ImportSourceError("JSON source is UTF-8 only.");
|
|
8533
|
+
const decoded = decodeJsonRecords(payload.bytes);
|
|
8534
|
+
if (decoded.length > maxParents) throw new ImportSourceError(`source parent rows (${decoded.length}) exceed maxRecords (${maxParents}).`);
|
|
8535
|
+
const targetByCode = new Map(targets.map((target) => [target.kind === "FIELD" ? target.field : target.subtableCode, target]));
|
|
8536
|
+
if (targetByCode.size !== targets.length) throw new ImportSourceError("IMPORT targets contain duplicates.");
|
|
8537
|
+
let childTotal = 0;
|
|
8538
|
+
return {
|
|
8539
|
+
records: decoded.map((record, index) => {
|
|
8540
|
+
const parentRow = index + 1;
|
|
8541
|
+
for (const code of record.keys()) if (!targetByCode.has(code)) sourceFail(parentRow, code, "unknown key (not declared in INTO).");
|
|
8542
|
+
const top = /* @__PURE__ */ new Map();
|
|
8543
|
+
const subtables = /* @__PURE__ */ new Map();
|
|
8544
|
+
const replacementTables = /* @__PURE__ */ new Set();
|
|
8545
|
+
for (const target of targets) {
|
|
8546
|
+
const code = target.kind === "FIELD" ? target.field : target.subtableCode;
|
|
8547
|
+
if (!record.has(code)) continue;
|
|
8548
|
+
const value = record.get(code);
|
|
8549
|
+
if (target.kind === "FIELD") {
|
|
8550
|
+
if (value instanceof Map) sourceFail(parentRow, code, "object is not accepted for a top-level field.");
|
|
8551
|
+
top.set(code, value);
|
|
8552
|
+
continue;
|
|
8553
|
+
}
|
|
8554
|
+
if (!Array.isArray(value)) sourceFail(parentRow, code, "subtable value must be an array.");
|
|
8555
|
+
replacementTables.add(code);
|
|
8556
|
+
const children = new Set(target.children);
|
|
8557
|
+
const rows = value.map((entry, childIndex) => {
|
|
8558
|
+
if (!(entry instanceof Map)) sourceFail(parentRow, code, `childRow=${childIndex + 1} must be an object.`);
|
|
8559
|
+
const child = entry;
|
|
8560
|
+
for (const childCode of child.keys()) {
|
|
8561
|
+
if (!children.has(childCode)) sourceFail(parentRow, childCode, `unknown child key in subtable ${code} at childRow=${childIndex + 1}.`);
|
|
8562
|
+
}
|
|
8563
|
+
childTotal++;
|
|
8564
|
+
if (childTotal > maxChildRows) throw new ImportSourceError(`source child rows (${childTotal}) exceed limit (${maxChildRows}).`);
|
|
8565
|
+
return { childRowNumber: childIndex + 1, values: child };
|
|
8566
|
+
});
|
|
8567
|
+
subtables.set(code, rows);
|
|
8568
|
+
}
|
|
8569
|
+
return { rowNumber: parentRow, top, subtables, replacementTables };
|
|
8570
|
+
})
|
|
8571
|
+
};
|
|
8572
|
+
}
|
|
8573
|
+
function materializeCliKintoneCsvImportRecords(source, payload, targets, replacementTables, maxParents, recordNumberSourceHeader) {
|
|
8574
|
+
const decoded = decodeCsv(payload.bytes, {
|
|
8575
|
+
encoding: source.encoding ?? payload.encoding ?? "utf8",
|
|
8576
|
+
hasHeader: source.hasHeader,
|
|
8577
|
+
columns: source.columns
|
|
8578
|
+
});
|
|
8579
|
+
if (!source.hasHeader || decoded.columns[0] !== "*") throw new ImportSourceError('ERR_IMPORT_MARKER: first CSV header must be "*".');
|
|
8580
|
+
const indexes = new Map(decoded.columns.map((code, index) => [code, index]));
|
|
8581
|
+
if (recordNumberSourceHeader && !indexes.has(recordNumberSourceHeader)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${recordNumberSourceHeader}" is missing.`);
|
|
8582
|
+
const fields = targets.filter((target) => target.kind === "FIELD");
|
|
8583
|
+
const tables = targets.filter((target) => target.kind === "SUBTABLE");
|
|
8584
|
+
for (const field of fields) if (!indexes.has(field.field)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${field.field}" is missing.`);
|
|
8585
|
+
for (const table of tables) {
|
|
8586
|
+
if (!table.rowIdSourceHeader || !indexes.has(table.rowIdSourceHeader)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: row-ID header for ${table.subtableCode} is missing.`);
|
|
8587
|
+
for (const child of table.children) if (!indexes.has(child)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required child header "${child}" is missing.`);
|
|
8588
|
+
}
|
|
8589
|
+
const records = [];
|
|
8590
|
+
let current;
|
|
8591
|
+
decoded.rows.forEach((cells, physicalIndex) => {
|
|
8592
|
+
const sourceRowNumber = physicalIndex + 2;
|
|
8593
|
+
const marker = cells[0];
|
|
8594
|
+
if (marker !== "" && marker !== "*") throw new ImportSourceError(`ERR_IMPORT_MARKER: invalid marker ${JSON.stringify(marker)} at source row ${sourceRowNumber}.`);
|
|
8595
|
+
if (marker === "*") {
|
|
8596
|
+
if (records.length >= maxParents) throw new ImportSourceError(`source parent rows exceed maxRecords (${maxParents}).`);
|
|
8597
|
+
current = {
|
|
8598
|
+
rowNumber: records.length + 1,
|
|
8599
|
+
markerRowNumber: sourceRowNumber,
|
|
8600
|
+
top: new Map(fields.map((field) => [field.field, cells[indexes.get(field.field)]])),
|
|
8601
|
+
subtables: new Map(tables.map((table) => [table.subtableCode, []])),
|
|
8602
|
+
replacementTables: new Set(replacementTables),
|
|
8603
|
+
...recordNumberSourceHeader ? { recordNumberSourceValue: cells[indexes.get(recordNumberSourceHeader)] } : {}
|
|
8604
|
+
};
|
|
8605
|
+
records.push(current);
|
|
8606
|
+
} else if (!current) {
|
|
8607
|
+
throw new ImportSourceError(`ERR_IMPORT_MARKER: first data row must start a parent (source row ${sourceRowNumber}).`);
|
|
8608
|
+
} else {
|
|
8609
|
+
for (const field of fields) {
|
|
8610
|
+
const continuationValue = cells[indexes.get(field.field)];
|
|
8611
|
+
if (continuationValue !== "" && continuationValue !== current.top.get(field.field)) {
|
|
8612
|
+
throw new ImportSourceError(`ERR_IMPORT_PARENT_VALUE_ON_CONTINUATION: ${field.field} at source row ${sourceRowNumber}.`);
|
|
8613
|
+
}
|
|
8614
|
+
}
|
|
8615
|
+
if (recordNumberSourceHeader) {
|
|
8616
|
+
const continuationValue = cells[indexes.get(recordNumberSourceHeader)];
|
|
8617
|
+
if (continuationValue !== "" && continuationValue !== current.recordNumberSourceValue) {
|
|
8618
|
+
throw new ImportSourceError(`ERR_IMPORT_PARENT_VALUE_ON_CONTINUATION: ${recordNumberSourceHeader} at source row ${sourceRowNumber}.`);
|
|
8619
|
+
}
|
|
8620
|
+
}
|
|
8621
|
+
}
|
|
8622
|
+
for (const table of tables) {
|
|
8623
|
+
const rowId = cells[indexes.get(table.rowIdSourceHeader)];
|
|
8624
|
+
const values = new Map(table.children.map((child) => [child, cells[indexes.get(child)]]));
|
|
8625
|
+
if (rowId === "" && [...values.values()].every((value) => value === "")) continue;
|
|
8626
|
+
const rows = current.subtables.get(table.subtableCode);
|
|
8627
|
+
rows.push({ childRowNumber: rows.length + 1, sourceRowNumber, ...rowId ? { rowId } : {}, values });
|
|
8628
|
+
}
|
|
8629
|
+
});
|
|
8630
|
+
if (records.length === 0) throw new ImportSourceError("CSV has no parent rows.");
|
|
8631
|
+
return { records };
|
|
8632
|
+
}
|
|
8633
|
+
|
|
8634
|
+
// src/import/importRecordValidation.ts
|
|
8635
|
+
var USER_TYPES3 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
8636
|
+
var UNSUPPORTED_CHILD_TYPES = /* @__PURE__ */ new Set(["SUBTABLE", "FILE", "CALC", "RECORD_NUMBER", "CREATOR", "CREATED_TIME", "MODIFIER", "UPDATED_TIME", "STATUS", "STATUS_ASSIGNEE", "CATEGORY", "REFERENCE_TABLE"]);
|
|
8637
|
+
function assertImportRejectLimit(prepared, rejectLimit) {
|
|
8638
|
+
if (rejectLimit != null && prepared.invalidParentRows.size > rejectLimit) {
|
|
8639
|
+
throw new Error(`RejectLimitExceededError: rejected parents (${prepared.invalidParentRows.size}) exceed REJECT LIMIT (${rejectLimit}).`);
|
|
8640
|
+
}
|
|
8641
|
+
}
|
|
8642
|
+
function prepareImportRecords(materialized, targets, fieldInfos, numberPrecision, operation) {
|
|
8643
|
+
const topInfos = new Map(fieldInfos.filter((f) => !f.inSubtable).map((f) => [f.code, f]));
|
|
8644
|
+
const scoped = /* @__PURE__ */ new Map();
|
|
8645
|
+
for (const info of fieldInfos) if (info.inSubtable && info.subtableCode) {
|
|
8646
|
+
let children = scoped.get(info.subtableCode);
|
|
8647
|
+
if (!children) scoped.set(info.subtableCode, children = /* @__PURE__ */ new Map());
|
|
8648
|
+
children.set(info.code, info);
|
|
8649
|
+
}
|
|
8650
|
+
const targetTop = targets.filter((t) => t.kind === "FIELD");
|
|
8651
|
+
const targetTables = targets.filter((t) => t.kind === "SUBTABLE");
|
|
8652
|
+
for (const target of targetTop) assertWritable(target.field, topInfos.get(target.field), void 0);
|
|
8653
|
+
for (const target of targetTables) {
|
|
8654
|
+
const table = topInfos.get(target.subtableCode);
|
|
8655
|
+
if (!table || table.fieldType !== "SUBTABLE") throw new Error(`ArgumentError: IMPORT subtable ${target.subtableCode} does not exist.`);
|
|
8656
|
+
const children = scoped.get(target.subtableCode) ?? /* @__PURE__ */ new Map();
|
|
8657
|
+
for (const child of target.children) assertWritable(child, children.get(child), target.subtableCode);
|
|
8658
|
+
}
|
|
8659
|
+
const tableCounts = new Map(targetTables.map((t) => [t.subtableCode, { parentsPresent: 0, childRows: 0, validChildRows: 0, invalidChildRows: 0 }]));
|
|
8660
|
+
const parents = materialized.records.map((record) => validateParent(record, targetTop, targetTables, topInfos, scoped, numberPrecision, operation, tableCounts));
|
|
8661
|
+
const errors = parents.flatMap((parent) => [...parent.errors]);
|
|
8662
|
+
return { parents, errors, invalidParentRows: new Set(parents.filter((p) => !p.valid).map((p) => p.parentRow)), tableCounts };
|
|
8663
|
+
}
|
|
8664
|
+
function validateParent(source, topTargets, tableTargets, topInfos, scoped, precision, operation, tableCounts) {
|
|
8665
|
+
const errors = [];
|
|
8666
|
+
const top = {};
|
|
8667
|
+
for (const target of topTargets) {
|
|
8668
|
+
if (!source.top.has(target.field)) continue;
|
|
8669
|
+
validateValue(source.top.get(target.field), topInfos.get(target.field), precision, top, target.field, errors, location(source, operation, target.field));
|
|
8670
|
+
}
|
|
8671
|
+
const createValidationOnly = {};
|
|
8672
|
+
if (operation === "INSERT") for (const info of topInfos.values()) {
|
|
8673
|
+
if (info.fieldType === "SUBTABLE" || info.writable === false || source.top.has(info.code)) continue;
|
|
8674
|
+
validateMissing(info, precision, createValidationOnly, errors, location(source, operation, info.code));
|
|
8675
|
+
}
|
|
8676
|
+
const subtables = /* @__PURE__ */ new Map();
|
|
8677
|
+
for (const target of tableTargets) {
|
|
8678
|
+
if (!source.subtables.has(target.subtableCode)) continue;
|
|
8679
|
+
const count = tableCounts.get(target.subtableCode);
|
|
8680
|
+
count.parentsPresent++;
|
|
8681
|
+
const preparedRows = [];
|
|
8682
|
+
for (const child of source.subtables.get(target.subtableCode)) {
|
|
8683
|
+
count.childRows++;
|
|
8684
|
+
const before = errors.length;
|
|
8685
|
+
const record = {};
|
|
8686
|
+
const infos = scoped.get(target.subtableCode);
|
|
8687
|
+
for (const code of target.children) {
|
|
8688
|
+
const info = infos.get(code);
|
|
8689
|
+
const loc = location(source, operation, code, target.subtableCode, child.childRowNumber, child.sourceRowNumber ?? source.markerRowNumber);
|
|
8690
|
+
if (child.values.has(code)) validateValue(child.values.get(code), info, precision, record, code, errors, loc);
|
|
8691
|
+
else validateMissing(info, precision, record, errors, loc);
|
|
8692
|
+
}
|
|
8693
|
+
if (errors.length === before) {
|
|
8694
|
+
count.validChildRows++;
|
|
8695
|
+
preparedRows.push(record);
|
|
8696
|
+
} else count.invalidChildRows++;
|
|
8697
|
+
}
|
|
8698
|
+
subtables.set(target.subtableCode, preparedRows);
|
|
8699
|
+
}
|
|
8700
|
+
return { parentRow: source.rowNumber, valid: errors.length === 0, top, subtables, replacementTables: source.replacementTables, errors };
|
|
8701
|
+
}
|
|
8702
|
+
function assertWritable(code, info, table) {
|
|
8703
|
+
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.`);
|
|
8704
|
+
if (info.writable === false || table && UNSUPPORTED_CHILD_TYPES.has(info.fieldType)) {
|
|
8705
|
+
throw new Error(`ArgumentError: IMPORT ${table ? `child ${table}.${code}` : `field ${code}`} is not writable (${info.fieldType}).`);
|
|
8706
|
+
}
|
|
8707
|
+
}
|
|
8708
|
+
function validateMissing(info, precision, record, errors, loc) {
|
|
8709
|
+
const raw = isEmptyDmlValue(info.defaultValue) ? "" : info.defaultValue;
|
|
8710
|
+
validateValue(raw, info, precision, record, info.code, errors, loc, !isEmptyDmlValue(info.defaultValue));
|
|
8711
|
+
}
|
|
8712
|
+
function validateValue(raw, info, precision, record, code, errors, loc, isDefault = false) {
|
|
8713
|
+
const normalizedRaw = decodeRaw(raw);
|
|
8714
|
+
const result = validateAndNormalizeDmlValue(normalizedRaw, info, precision);
|
|
8715
|
+
if (!result.ok) errors.push({ ...loc, code: result.code, message: isDefault ? `\u65E2\u5B9A\u5024: ${result.message}` : result.message });
|
|
8716
|
+
else record[code] = { value: preserveUserCodes(normalizedRaw, info) ? normalizedRaw : result.value };
|
|
8717
|
+
}
|
|
8718
|
+
function decodeRaw(raw) {
|
|
8719
|
+
if (isJsonNumber(raw)) return raw.lexeme;
|
|
8720
|
+
if (Array.isArray(raw)) return raw.map((value) => value instanceof Map ? value : isJsonNumber(value) ? value.lexeme : value);
|
|
8721
|
+
return raw;
|
|
8722
|
+
}
|
|
8723
|
+
function isJsonNumber(raw) {
|
|
8724
|
+
return typeof raw === "object" && raw !== null && raw.kind === "number";
|
|
8725
|
+
}
|
|
8726
|
+
function preserveUserCodes(raw, info) {
|
|
8727
|
+
return USER_TYPES3.has(info.fieldType) && Array.isArray(raw) && raw.every((v) => typeof v === "object" && v !== null && "code" in v);
|
|
8728
|
+
}
|
|
8729
|
+
function location(source, operation, field, subtable, subrow, sourceRow) {
|
|
8730
|
+
const physicalRow = sourceRow ?? source.markerRowNumber;
|
|
8731
|
+
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 };
|
|
8732
|
+
}
|
|
8733
|
+
|
|
8734
|
+
// src/import/importErrors.ts
|
|
8735
|
+
var IMPORT_VALIDATION_META_COLUMNS = [
|
|
8736
|
+
"$err_statement",
|
|
8737
|
+
"$err_operation",
|
|
8738
|
+
"$err_row",
|
|
8739
|
+
"$err_field",
|
|
8740
|
+
"$err_subtable",
|
|
8741
|
+
"$err_subrow",
|
|
8742
|
+
"$err_source_row",
|
|
8743
|
+
"$err_code",
|
|
8744
|
+
"$err_message"
|
|
8745
|
+
];
|
|
8746
|
+
function materializeImportValidationErrors(errors, payloadFields, statementNumber = 1) {
|
|
8747
|
+
return errors.map((error) => {
|
|
8748
|
+
const row = {};
|
|
8749
|
+
for (const field of payloadFields) row[field] = error.sourceValues.get(field) == null ? "" : render(error.sourceValues.get(field));
|
|
8750
|
+
row["$err_statement"] = String(statementNumber);
|
|
8751
|
+
row["$err_operation"] = error.operation;
|
|
8752
|
+
row["$err_row"] = String(error.parentRow);
|
|
8753
|
+
row["$err_field"] = error.field;
|
|
8754
|
+
row["$err_subtable"] = error.subtable ?? "";
|
|
8755
|
+
row["$err_subrow"] = error.subrow == null ? "" : String(error.subrow);
|
|
8756
|
+
row["$err_source_row"] = error.sourceRow == null ? null : String(error.sourceRow);
|
|
8757
|
+
row["$err_code"] = error.code;
|
|
8758
|
+
row["$err_message"] = error.message;
|
|
8759
|
+
return row;
|
|
8760
|
+
});
|
|
8761
|
+
}
|
|
8762
|
+
function render(value) {
|
|
8763
|
+
if (value === null || value === void 0) return "";
|
|
8764
|
+
if (typeof value === "object" && value !== null && "kind" in value && "lexeme" in value && value.kind === "number") {
|
|
8765
|
+
return String(value.lexeme);
|
|
8766
|
+
}
|
|
8767
|
+
if (Array.isArray(value)) return JSON.stringify(value);
|
|
8768
|
+
return String(value);
|
|
8769
|
+
}
|
|
8770
|
+
|
|
8771
|
+
// src/import/subtablePayload.ts
|
|
8772
|
+
function buildImportRecordPayload(top, subtables, rowIdMode) {
|
|
8773
|
+
const record = {};
|
|
8774
|
+
for (const [code, value] of top) record[code] = { value };
|
|
8775
|
+
for (const [tableCode, sourceRows] of subtables) {
|
|
8776
|
+
record[tableCode] = {
|
|
8777
|
+
value: sourceRows.map((sourceRow) => ({
|
|
8778
|
+
...rowIdMode === "PRESERVE" && sourceRow.rowId ? { id: sourceRow.rowId } : {},
|
|
8779
|
+
value: Object.fromEntries([...sourceRow.values].map(([childCode, value]) => [childCode, { value }]))
|
|
8780
|
+
}))
|
|
8781
|
+
};
|
|
8782
|
+
}
|
|
8783
|
+
return record;
|
|
8784
|
+
}
|
|
8785
|
+
function buildJsonImportRecordPayload(top, subtables) {
|
|
8786
|
+
return buildImportRecordPayload(top, subtables, "DROP");
|
|
8787
|
+
}
|
|
8788
|
+
|
|
8789
|
+
// src/import/jsonSubtableWritePlan.ts
|
|
8790
|
+
function assertJsonImportHasNoRowIds(materialized) {
|
|
8791
|
+
for (const parent of materialized.records) for (const [table, rows] of parent.subtables) {
|
|
8792
|
+
for (const row of rows) {
|
|
8793
|
+
if (row.rowId !== void 0 || row.values.has("_rid") || row.values.has("id")) {
|
|
8794
|
+
throw new Error(`ArgumentError: JSON IMPORT subtable ${table} does not accept _rid/id; rows are always newly numbered.`);
|
|
8795
|
+
}
|
|
8796
|
+
}
|
|
7804
8797
|
}
|
|
7805
|
-
|
|
8798
|
+
}
|
|
8799
|
+
function buildJsonSubtableWritePlan(parents, targetIds, existingById) {
|
|
8800
|
+
return parents.map((parent, index) => {
|
|
8801
|
+
const targetId = targetIds[index];
|
|
8802
|
+
const existing = targetId === void 0 ? void 0 : existingById.get(targetId);
|
|
8803
|
+
if (targetId !== void 0 && !existing) throw new Error(`InternalError: IMPORT UPSERT target APP record ${targetId} was not loaded.`);
|
|
8804
|
+
const tables = [...parent.subtables].map(([table, input]) => {
|
|
8805
|
+
const raw = existing?.record[table]?.value;
|
|
8806
|
+
const existingRows = Array.isArray(raw) ? raw.length : 0;
|
|
8807
|
+
return { table, existingRows, inputRows: input.length, addRows: input.length, deleteRows: existingRows };
|
|
8808
|
+
});
|
|
7806
8809
|
return {
|
|
7807
|
-
|
|
7808
|
-
|
|
8810
|
+
parentRow: parent.parentRow,
|
|
8811
|
+
mode: targetId === void 0 ? "INSERT" : "UPDATE",
|
|
8812
|
+
...targetId === void 0 ? {} : { targetId, revision: existing?.revision },
|
|
8813
|
+
top: parent.top,
|
|
8814
|
+
subtables: parent.subtables,
|
|
8815
|
+
tables
|
|
7809
8816
|
};
|
|
7810
|
-
}
|
|
7811
|
-
return { capability: "LOCAL_ONLY", reasons };
|
|
8817
|
+
});
|
|
7812
8818
|
}
|
|
7813
|
-
|
|
7814
|
-
|
|
8819
|
+
|
|
8820
|
+
// src/import/subtableReplacementPlan.ts
|
|
8821
|
+
function tableRows(record, table) {
|
|
8822
|
+
const raw = record[table]?.value;
|
|
8823
|
+
return Array.isArray(raw) ? raw : [];
|
|
8824
|
+
}
|
|
8825
|
+
function assertNoDuplicateCsvSubtableRowIds(records) {
|
|
8826
|
+
const seen = /* @__PURE__ */ new Map();
|
|
8827
|
+
for (const parent of records) for (const [table, rows] of parent.subtables) for (const row of rows) {
|
|
8828
|
+
if (!row.rowId) continue;
|
|
8829
|
+
const key = `${table}\0${row.rowId}`;
|
|
8830
|
+
if (seen.has(key)) throw new Error(`ERR_SUBTABLE_ROW_ID_DUP_SOURCE: duplicate row ID ${row.rowId} in ${table}`);
|
|
8831
|
+
seen.set(key, parent.rowNumber);
|
|
8832
|
+
}
|
|
8833
|
+
}
|
|
8834
|
+
function buildCsvSubtableReplacementPlan(sources, prepared, targetIds, existingById, ownership) {
|
|
8835
|
+
return prepared.map((parent, index) => {
|
|
8836
|
+
const source = sources[index];
|
|
8837
|
+
const targetId = targetIds[index];
|
|
8838
|
+
const existing = targetId === void 0 ? void 0 : existingById.get(targetId);
|
|
8839
|
+
const errors = [...parent.errors];
|
|
8840
|
+
if (!existing || targetId === void 0) return { parentRow: parent.parentRow, targetId: targetId ?? 0, valid: false, top: parent.top, subtables: /* @__PURE__ */ new Map(), tables: [], errors };
|
|
8841
|
+
const subtables = /* @__PURE__ */ new Map();
|
|
8842
|
+
const tables = [];
|
|
8843
|
+
for (const table of parent.replacementTables) {
|
|
8844
|
+
const current = tableRows(existing.record, table);
|
|
8845
|
+
const currentIds = new Set(current.map((row) => row.id).filter((id) => !!id));
|
|
8846
|
+
const input = source.subtables.get(table) ?? [];
|
|
8847
|
+
const normalized = parent.subtables.get(table) ?? [];
|
|
8848
|
+
let updateRows = 0, addRows = 0, rowIdNotFound = 0;
|
|
8849
|
+
const payloadRows = input.map((row, rowIndex) => {
|
|
8850
|
+
const normalizedRecord = normalized[rowIndex] ?? {};
|
|
8851
|
+
if (row.rowId && currentIds.has(row.rowId)) {
|
|
8852
|
+
updateRows++;
|
|
8853
|
+
return { rowId: row.rowId, record: normalizedRecord };
|
|
8854
|
+
}
|
|
8855
|
+
if (row.rowId) {
|
|
8856
|
+
const owners = ownership.get(row.rowId) ?? [];
|
|
8857
|
+
if (owners.some((owner) => owner.parentId !== targetId || owner.table !== table)) errors.push({
|
|
8858
|
+
operation: "UPDATE",
|
|
8859
|
+
parentRow: parent.parentRow,
|
|
8860
|
+
field: row.rowId,
|
|
8861
|
+
subtable: table,
|
|
8862
|
+
subrow: row.childRowNumber,
|
|
8863
|
+
sourceRow: row.sourceRowNumber,
|
|
8864
|
+
code: "ERR_IMPORT_FIELD_OWNERSHIP",
|
|
8865
|
+
message: `rowIdOwnedElsewhere: ${row.rowId}`,
|
|
8866
|
+
sourceValues: row.values
|
|
8867
|
+
});
|
|
8868
|
+
rowIdNotFound++;
|
|
8869
|
+
}
|
|
8870
|
+
addRows++;
|
|
8871
|
+
return { record: normalizedRecord };
|
|
8872
|
+
});
|
|
8873
|
+
subtables.set(table, payloadRows);
|
|
8874
|
+
tables.push({ table, existingRows: current.length, inputRows: input.length, updateRows, addRows, deleteRows: current.length - updateRows, rowIdNotFound });
|
|
8875
|
+
}
|
|
8876
|
+
return { parentRow: parent.parentRow, targetId, ...existing.revision === void 0 ? {} : { revision: existing.revision }, valid: errors.length === 0, top: parent.top, subtables, tables, errors };
|
|
8877
|
+
});
|
|
7815
8878
|
}
|
|
7816
|
-
|
|
7817
|
-
|
|
8879
|
+
|
|
8880
|
+
// src/import/importProjection.ts
|
|
8881
|
+
var IMPORT_PROJECTION_SOURCE = "#__import_source";
|
|
8882
|
+
function bindImportProjection(projection) {
|
|
8883
|
+
return { ...projection, from: { appId: 0, alias: null, cteName: IMPORT_PROJECTION_SOURCE } };
|
|
8884
|
+
}
|
|
8885
|
+
|
|
8886
|
+
// src/import/recordNumberUpdate.ts
|
|
8887
|
+
function normalizeImportRecordNumber(raw) {
|
|
8888
|
+
return /^[0-9]+$/.test(raw) ? raw.replace(/^0+(?=\d)/, "") : null;
|
|
8889
|
+
}
|
|
8890
|
+
function preflightImportRecordNumbers(values, header) {
|
|
8891
|
+
const normalized = values.map(normalizeImportRecordNumber);
|
|
8892
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8893
|
+
for (const key of normalized) {
|
|
8894
|
+
if (key === null) continue;
|
|
8895
|
+
if (seen.has(key)) {
|
|
8896
|
+
throw new Error("ERR_RECORD_NUMBER_DUP_SOURCE: source contains a duplicate record number");
|
|
8897
|
+
}
|
|
8898
|
+
seen.add(key);
|
|
8899
|
+
}
|
|
8900
|
+
return {
|
|
8901
|
+
normalized,
|
|
8902
|
+
errors: normalized.map((key) => key === null ? [{
|
|
8903
|
+
field: header,
|
|
8904
|
+
code: "ERR_RECORD_NUMBER_INVALID",
|
|
8905
|
+
message: `${header} must be a non-empty ASCII decimal record number`
|
|
8906
|
+
}] : [])
|
|
8907
|
+
};
|
|
7818
8908
|
}
|
|
7819
8909
|
|
|
7820
8910
|
// src/execute.ts
|
|
@@ -7827,6 +8917,7 @@ var SearchAbortedError = class extends Error {
|
|
|
7827
8917
|
};
|
|
7828
8918
|
var materializedMetaBySelectResult = /* @__PURE__ */ new WeakMap();
|
|
7829
8919
|
var materializedMetaByValidationResult = /* @__PURE__ */ new WeakMap();
|
|
8920
|
+
var importSourceByDmlStatement = /* @__PURE__ */ new WeakMap();
|
|
7830
8921
|
var defaultCacheContextByClient = /* @__PURE__ */ new WeakMap();
|
|
7831
8922
|
var nextDefaultCacheContextId = 1;
|
|
7832
8923
|
function resolveCacheContext(client, explicit) {
|
|
@@ -7841,7 +8932,7 @@ function resolveCacheContext(client, explicit) {
|
|
|
7841
8932
|
async function execute(sql, client, options = {}) {
|
|
7842
8933
|
const startedAt = Date.now();
|
|
7843
8934
|
const cacheContext = resolveCacheContext(client, options.cacheContext);
|
|
7844
|
-
const stmt = parseSql(sql);
|
|
8935
|
+
const stmt = parseSql(sql, options.enableImport === true);
|
|
7845
8936
|
const metrics = createEmptyMetrics();
|
|
7846
8937
|
const countedClient = wrapClientWithMetrics(client, metrics);
|
|
7847
8938
|
const collector = { aborted: false };
|
|
@@ -8021,6 +9112,7 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
|
|
|
8021
9112
|
throw new Error(`ParseError: variable @${unresolved} is not defined in a batch.`);
|
|
8022
9113
|
}
|
|
8023
9114
|
validateKlikeStatement(stmt);
|
|
9115
|
+
if (stmt.type === "IMPORT") return executeImport(stmt, client, options, cacheContext);
|
|
8024
9116
|
if ("validateOnly" in stmt && stmt.validateOnly === true) {
|
|
8025
9117
|
if (stmt.validationErrorTable) {
|
|
8026
9118
|
throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
@@ -8078,25 +9170,84 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
|
|
|
8078
9170
|
return executeAssert(stmt, client, options, cacheContext);
|
|
8079
9171
|
}
|
|
8080
9172
|
}
|
|
8081
|
-
var EXISTING_VALIDATION_COLUMNS = [
|
|
9173
|
+
var EXISTING_VALIDATION_COLUMNS = [
|
|
9174
|
+
"$id",
|
|
9175
|
+
"$err_field",
|
|
9176
|
+
"$err_code",
|
|
9177
|
+
"$err_message",
|
|
9178
|
+
"$err_value",
|
|
9179
|
+
"$err_subtable",
|
|
9180
|
+
"$err_subrow",
|
|
9181
|
+
"$err_subrow_id",
|
|
9182
|
+
"$err_count"
|
|
9183
|
+
];
|
|
9184
|
+
var EXISTING_VALIDATION_SUMMARY_COLUMNS = [
|
|
9185
|
+
"$id",
|
|
9186
|
+
"$err_subtable",
|
|
9187
|
+
"$err_field",
|
|
9188
|
+
"$err_code",
|
|
9189
|
+
"$err_count"
|
|
9190
|
+
];
|
|
8082
9191
|
function hasAuditableConstraint(field) {
|
|
8083
9192
|
return field.required === true || field.minValue !== void 0 || field.maxValue !== void 0 || field.minLength !== void 0 || field.maxLength !== void 0 || field.optionOrder !== void 0;
|
|
8084
9193
|
}
|
|
8085
9194
|
function resolveExistingValidationTargets(stmt, fieldInfos) {
|
|
8086
|
-
const
|
|
8087
|
-
const
|
|
8088
|
-
|
|
9195
|
+
const topByCode = new Map(fieldInfos.filter((field) => !field.inSubtable).map((field) => [field.code, field]));
|
|
9196
|
+
const childrenByTable = /* @__PURE__ */ new Map();
|
|
9197
|
+
for (const field of fieldInfos) {
|
|
9198
|
+
if (!field.inSubtable || !field.subtableCode) continue;
|
|
9199
|
+
const children = childrenByTable.get(field.subtableCode) ?? [];
|
|
9200
|
+
children.push(field);
|
|
9201
|
+
childrenByTable.set(field.subtableCode, children);
|
|
9202
|
+
}
|
|
9203
|
+
const auditable = (field) => field.fieldType === "NUMBER" || hasAuditableConstraint(field);
|
|
9204
|
+
if (stmt.targets === void 0) return [
|
|
9205
|
+
...fieldInfos.filter((field) => !field.inSubtable && field.fieldType !== "SUBTABLE" && auditable(field)),
|
|
9206
|
+
...fieldInfos.filter((field) => field.inSubtable && !!field.subtableCode && auditable(field))
|
|
9207
|
+
].map((field) => ({ field, ...field.subtableCode ? { subtableCode: field.subtableCode } : {} }));
|
|
9208
|
+
const result = [];
|
|
8089
9209
|
const seen = /* @__PURE__ */ new Set();
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
seen.
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
9210
|
+
const add = (field, subtableCode) => {
|
|
9211
|
+
const key = subtableCode ? `${subtableCode}\0${field.code}` : field.code;
|
|
9212
|
+
if (seen.has(key)) throw new Error(`ArgumentError: VALIDATE \u306E\u30D5\u30A3\u30FC\u30EB\u30C9 ${field.code} \u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059\u3002`);
|
|
9213
|
+
seen.add(key);
|
|
9214
|
+
if (!auditable(field)) throw new Error(`ArgumentError: VALIDATE \u306E\u30D5\u30A3\u30FC\u30EB\u30C9 ${field.code} \u306B\u306F\u76E3\u67FB\u53EF\u80FD\u306A\u5236\u7D04\u304C\u3042\u308A\u307E\u305B\u3093\u3002`);
|
|
9215
|
+
result.push({ field, ...subtableCode ? { subtableCode } : {} });
|
|
9216
|
+
};
|
|
9217
|
+
for (const target of stmt.targets) {
|
|
9218
|
+
if (target.kind === "SUBTABLE") {
|
|
9219
|
+
const children = childrenByTable.get(target.subtableCode);
|
|
9220
|
+
if (!children) throw new Error(`ArgumentError: VALIDATE \u306E\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB ${target.subtableCode} \u306F\u5B58\u5728\u3057\u307E\u305B\u3093\u3002`);
|
|
9221
|
+
if (target.children.length === 0) throw new Error(`ArgumentError: VALIDATE \u306E\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB ${target.subtableCode} \u306B\u306F1\u3064\u4EE5\u4E0A\u306E\u5B50\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u5FC5\u8981\u3067\u3059\u3002`);
|
|
9222
|
+
for (const code2 of target.children) {
|
|
9223
|
+
const child = children.find((field) => field.code === code2);
|
|
9224
|
+
if (!child) {
|
|
9225
|
+
const belongsElsewhere = [...childrenByTable.entries()].some(([table, fields]) => table !== target.subtableCode && fields.some((field) => field.code === code2));
|
|
9226
|
+
throw new Error(belongsElsewhere ? `ArgumentError: VALIDATE \u306E\u5B50\u30D5\u30A3\u30FC\u30EB\u30C9 ${code2} \u306F\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB ${target.subtableCode} \u306B\u5C5E\u3057\u3066\u3044\u307E\u305B\u3093\u3002` : `ArgumentError: VALIDATE \u306E\u5B50\u30D5\u30A3\u30FC\u30EB\u30C9 ${code2} \u306F\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB ${target.subtableCode} \u306B\u5B58\u5728\u3057\u307E\u305B\u3093\u3002`);
|
|
9227
|
+
}
|
|
9228
|
+
add(child, target.subtableCode);
|
|
9229
|
+
}
|
|
9230
|
+
continue;
|
|
9231
|
+
}
|
|
9232
|
+
const code = target.field;
|
|
9233
|
+
if (code === "$id") throw new Error("ArgumentError: VALIDATE \u3067\u306F\u30B7\u30B9\u30C6\u30E0\u30D5\u30A3\u30FC\u30EB\u30C9 $id \u3092\u76E3\u67FB\u3067\u304D\u307E\u305B\u3093\u3002");
|
|
9234
|
+
const top = topByCode.get(code);
|
|
9235
|
+
if (top?.fieldType === "SUBTABLE") {
|
|
9236
|
+
const children = (childrenByTable.get(code) ?? []).filter(auditable);
|
|
9237
|
+
if (children.length === 0) throw new Error(`ArgumentError: VALIDATE \u306E\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB ${code} \u306B\u306F\u76E3\u67FB\u53EF\u80FD\u306A\u5B50\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u3042\u308A\u307E\u305B\u3093\u3002`);
|
|
9238
|
+
children.forEach((child) => add(child, code));
|
|
9239
|
+
continue;
|
|
9240
|
+
}
|
|
9241
|
+
if (top) {
|
|
9242
|
+
add(top);
|
|
9243
|
+
continue;
|
|
9244
|
+
}
|
|
9245
|
+
if ([...childrenByTable.values()].some((children) => children.some((field) => field.code === code))) {
|
|
9246
|
+
throw new Error(`ArgumentError: VALIDATE \u306E\u5B50\u30D5\u30A3\u30FC\u30EB\u30C9 ${code} \u306F\u6240\u6709\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u3092\u542B\u3080 T(${code}) \u5F62\u5F0F\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002`);
|
|
9247
|
+
}
|
|
9248
|
+
throw new Error(`ArgumentError: VALIDATE \u306E\u30D5\u30A3\u30FC\u30EB\u30C9 ${code} \u306F\u5B58\u5728\u3057\u307E\u305B\u3093\u3002`);
|
|
9249
|
+
}
|
|
9250
|
+
return result;
|
|
8100
9251
|
}
|
|
8101
9252
|
function collectValidateWhereFields(where) {
|
|
8102
9253
|
const fields = [];
|
|
@@ -8121,11 +9272,12 @@ function collectValidateWhereFields(where) {
|
|
|
8121
9272
|
visit(where);
|
|
8122
9273
|
return fields;
|
|
8123
9274
|
}
|
|
8124
|
-
function existingValidationColumnMeta() {
|
|
8125
|
-
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
|
|
9275
|
+
function existingValidationColumnMeta(summary = false) {
|
|
9276
|
+
const columns = summary ? EXISTING_VALIDATION_SUMMARY_COLUMNS : EXISTING_VALIDATION_COLUMNS;
|
|
9277
|
+
return new Map(columns.map((column) => [column, {
|
|
9278
|
+
fieldType: column === "$id" || column === "$err_count" ? "KSQL_NUMBER" : "KSQL_STRING",
|
|
9279
|
+
sortKind: column === "$id" || column === "$err_count" ? "number" : "string",
|
|
9280
|
+
semantics: syntheticSemantics(column === "$id" || column === "$err_count" ? "number" : "string")
|
|
8129
9281
|
}]));
|
|
8130
9282
|
}
|
|
8131
9283
|
async function executeExistingRecordValidation(stmt, client, options, cacheContext) {
|
|
@@ -8134,38 +9286,45 @@ async function executeExistingRecordValidation(stmt, client, options, cacheConte
|
|
|
8134
9286
|
}
|
|
8135
9287
|
async function executeExistingRecordValidationCore(stmt, client, options, cacheContext) {
|
|
8136
9288
|
const fieldInfos = await getFieldsCached(stmt.appId, client, cacheContext);
|
|
8137
|
-
const infoByCode = new Map(fieldInfos.map((field) => [field.code, field]));
|
|
9289
|
+
const infoByCode = new Map(fieldInfos.filter((field) => !field.inSubtable).map((field) => [field.code, field]));
|
|
9290
|
+
const childCodes = new Set(fieldInfos.filter((field) => field.inSubtable).map((field) => field.code));
|
|
8138
9291
|
const targets = resolveExistingValidationTargets(stmt, fieldInfos);
|
|
8139
9292
|
const checkGroups = stmt.checkGroups ?? [];
|
|
8140
9293
|
const checkRefs2 = collectCheckFieldRefs(checkGroups);
|
|
8141
9294
|
for (const ref of checkRefs2) {
|
|
8142
|
-
if (ref.field !== "$id" && !infoByCode.has(ref.field)) {
|
|
9295
|
+
if (ref.field !== "$id" && !infoByCode.has(ref.field) && !childCodes.has(ref.field)) {
|
|
8143
9296
|
throw customCheckParseError(`CHECK \u306E\u30D5\u30A3\u30FC\u30EB\u30C9 ${ref.field} \u306F APP${stmt.appId} \u306B\u5B58\u5728\u3057\u307E\u305B\u3093`);
|
|
8144
9297
|
}
|
|
9298
|
+
if (ref.field !== "$id" && !infoByCode.has(ref.field) && childCodes.has(ref.field)) {
|
|
9299
|
+
throw new Error(`ArgumentError: VALIDATE \u306E CHECK \u3067\u306F\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5B50\u30D5\u30A3\u30FC\u30EB\u30C9 ${ref.field} \u3092\u53C2\u7167\u3067\u304D\u307E\u305B\u3093\u3002`);
|
|
9300
|
+
}
|
|
8145
9301
|
}
|
|
8146
|
-
const evaluationTypes = new Map(
|
|
9302
|
+
const evaluationTypes = new Map([...infoByCode].map(([code, field]) => [code, field.fieldType]));
|
|
8147
9303
|
evaluationTypes.set("$id", "RECORD_NUMBER");
|
|
8148
9304
|
assertCheckComparisonTypes(stmt, evaluationTypes);
|
|
8149
9305
|
const whereFields = collectValidateWhereFields(stmt.where);
|
|
8150
9306
|
const requiredFields = [.../* @__PURE__ */ new Set([
|
|
8151
9307
|
"$id",
|
|
8152
|
-
...targets.map((
|
|
9308
|
+
...targets.map((target) => target.subtableCode ?? target.field.code),
|
|
8153
9309
|
...whereFields,
|
|
8154
9310
|
...checkRefs2.map((ref) => ref.field)
|
|
8155
9311
|
])];
|
|
8156
9312
|
for (const field of whereFields) {
|
|
8157
|
-
if (field !== "$id" && !infoByCode.has(field)) {
|
|
9313
|
+
if (field !== "$id" && !infoByCode.has(field) && !childCodes.has(field)) {
|
|
8158
9314
|
throw new Error(`ArgumentError: WHERE field ${field} does not exist in APP${stmt.appId}.`);
|
|
8159
9315
|
}
|
|
9316
|
+
if (field !== "$id" && !infoByCode.has(field) && childCodes.has(field)) {
|
|
9317
|
+
throw new Error(`ArgumentError: VALIDATE \u306E WHERE \u3067\u306F\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5B50\u30D5\u30A3\u30FC\u30EB\u30C9 ${field} \u3092\u53C2\u7167\u3067\u304D\u307E\u305B\u3093\u3002`);
|
|
9318
|
+
}
|
|
8160
9319
|
}
|
|
8161
|
-
const numberPrecision = targets.some((
|
|
9320
|
+
const numberPrecision = targets.some((target) => target.field.fieldType === "NUMBER") ? await getNumberPrecisionCached(stmt.appId, client, cacheContext) : void 0;
|
|
8162
9321
|
const semantics = (field) => field.field === "$id" ? resolveFieldSemantics({ fieldType: "__ID__" }) : infoByCode.get(field.field)?.semantics ?? (infoByCode.has(field.field) ? resolveFieldSemantics(infoByCode.get(field.field)) : void 0);
|
|
8163
9322
|
const capability = classifyWhereCapability(stmt.where, semantics);
|
|
8164
9323
|
if (capability.capability === "UNSUPPORTED") {
|
|
8165
9324
|
throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
|
|
8166
9325
|
}
|
|
8167
|
-
const fieldTypes = new Map(
|
|
8168
|
-
const fieldOptions = new Map(
|
|
9326
|
+
const fieldTypes = new Map([...infoByCode].map(([code, field]) => [code, field.fieldType]));
|
|
9327
|
+
const fieldOptions = new Map([...infoByCode.values()].flatMap((field) => field.optionOrder ? [[field.code, new Set(Object.keys(field.optionOrder))]] : []));
|
|
8169
9328
|
const prefilter = stmt.where === null ? null : capability.capability === "EXACT_PUSHDOWN" ? stmt.where : extractSafePushdownLeaves(stmt.where, {
|
|
8170
9329
|
allowUnqualifiedFields: true,
|
|
8171
9330
|
fieldTypes,
|
|
@@ -8184,36 +9343,110 @@ async function executeExistingRecordValidationCore(stmt, client, options, cacheC
|
|
|
8184
9343
|
flat: flatten(record, null)
|
|
8185
9344
|
})).filter((row) => stmt.where === null || evalWhere(stmt.where, row.flat, (field) => evaluationTypes.get(field.field)));
|
|
8186
9345
|
const rows = [];
|
|
9346
|
+
const detailRows = /* @__PURE__ */ new Map();
|
|
9347
|
+
const summaryRows = /* @__PURE__ */ new Map();
|
|
9348
|
+
const errorRecordIds = /* @__PURE__ */ new Set();
|
|
9349
|
+
let errorCount = 0;
|
|
9350
|
+
const appendError = (error) => {
|
|
9351
|
+
errorRecordIds.add(error.id);
|
|
9352
|
+
errorCount += 1;
|
|
9353
|
+
if (stmt.summary) {
|
|
9354
|
+
const key2 = JSON.stringify([error.id, error.subtable ?? "", error.field, error.code]);
|
|
9355
|
+
const current2 = summaryRows.get(key2);
|
|
9356
|
+
if (current2) current2["$err_count"] = String(Number(current2["$err_count"]) + 1);
|
|
9357
|
+
else summaryRows.set(key2, {
|
|
9358
|
+
"$id": error.id,
|
|
9359
|
+
"$err_subtable": error.subtable ?? "",
|
|
9360
|
+
"$err_field": error.field,
|
|
9361
|
+
"$err_code": error.code,
|
|
9362
|
+
"$err_count": "1"
|
|
9363
|
+
});
|
|
9364
|
+
return;
|
|
9365
|
+
}
|
|
9366
|
+
const key = JSON.stringify([error.id, error.subtable ?? "", error.field, error.code, error.message]);
|
|
9367
|
+
const current = detailRows.get(key);
|
|
9368
|
+
if (current) {
|
|
9369
|
+
current["$err_count"] = String(Number(current["$err_count"]) + 1);
|
|
9370
|
+
if (error.subrow !== void 0) {
|
|
9371
|
+
current["$err_subrow"] = `${current["$err_subrow"]},${error.subrow}`;
|
|
9372
|
+
current["$err_subrow_id"] = `${current["$err_subrow_id"]},${error.subrowId ?? ""}`;
|
|
9373
|
+
}
|
|
9374
|
+
} else detailRows.set(key, {
|
|
9375
|
+
"$id": error.id,
|
|
9376
|
+
"$err_field": error.field,
|
|
9377
|
+
"$err_code": error.code,
|
|
9378
|
+
"$err_message": error.message,
|
|
9379
|
+
"$err_value": error.value,
|
|
9380
|
+
"$err_subtable": error.subtable ?? "",
|
|
9381
|
+
"$err_subrow": error.subrow === void 0 ? "" : String(error.subrow),
|
|
9382
|
+
"$err_subrow_id": error.subrowId ?? "",
|
|
9383
|
+
"$err_count": "1"
|
|
9384
|
+
});
|
|
9385
|
+
};
|
|
9386
|
+
const topTargets = targets.filter((target) => !target.subtableCode);
|
|
9387
|
+
const subtableTargets = /* @__PURE__ */ new Map();
|
|
9388
|
+
for (const target of targets) {
|
|
9389
|
+
if (!target.subtableCode) continue;
|
|
9390
|
+
const children = subtableTargets.get(target.subtableCode) ?? [];
|
|
9391
|
+
children.push(target);
|
|
9392
|
+
subtableTargets.set(target.subtableCode, children);
|
|
9393
|
+
}
|
|
8187
9394
|
for (const row of validationRows) {
|
|
8188
|
-
for (const
|
|
8189
|
-
const raw = row.record[field.code]?.value;
|
|
8190
|
-
const validation = validateAndNormalizeDmlValue(raw, field, numberPrecision);
|
|
8191
|
-
if (validation.ok)
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
"$err_value": renderExistingValidationValue(raw, field.fieldType)
|
|
9395
|
+
for (const target of topTargets) {
|
|
9396
|
+
const raw = row.record[target.field.code]?.value;
|
|
9397
|
+
const validation = validateAndNormalizeDmlValue(raw, target.field, numberPrecision);
|
|
9398
|
+
if (!validation.ok) appendError({
|
|
9399
|
+
id: row.id,
|
|
9400
|
+
field: target.field.code,
|
|
9401
|
+
code: validation.code,
|
|
9402
|
+
message: validation.message,
|
|
9403
|
+
value: renderExistingValidationValue(raw, target.field.fieldType)
|
|
8198
9404
|
});
|
|
8199
9405
|
}
|
|
9406
|
+
for (const [tableCode, childTargets] of subtableTargets) {
|
|
9407
|
+
const tableRows2 = row.record[tableCode]?.value;
|
|
9408
|
+
if (!Array.isArray(tableRows2)) continue;
|
|
9409
|
+
for (let i = 0; i < tableRows2.length; i++) {
|
|
9410
|
+
const tableRow = tableRows2[i];
|
|
9411
|
+
for (const target of childTargets) {
|
|
9412
|
+
const raw = tableRow.value?.[target.field.code]?.value;
|
|
9413
|
+
const validation = validateAndNormalizeDmlValue(raw, target.field, numberPrecision);
|
|
9414
|
+
if (!validation.ok) appendError({
|
|
9415
|
+
id: row.id,
|
|
9416
|
+
field: target.field.code,
|
|
9417
|
+
code: validation.code,
|
|
9418
|
+
message: validation.message,
|
|
9419
|
+
value: renderExistingValidationValue(raw, target.field.fieldType),
|
|
9420
|
+
subtable: tableCode,
|
|
9421
|
+
subrow: i + 1,
|
|
9422
|
+
subrowId: String(tableRow.id ?? "")
|
|
9423
|
+
});
|
|
9424
|
+
}
|
|
9425
|
+
}
|
|
9426
|
+
}
|
|
8200
9427
|
for (const check of evaluateCustomChecks(checkGroups, row.flat, (field) => evaluationTypes.get(field.field))) {
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
9428
|
+
appendError({ id: row.id, field: "", code: "ERR_CHECK", message: check.message, value: "" });
|
|
9429
|
+
}
|
|
9430
|
+
}
|
|
9431
|
+
if (stmt.summary) rows.push(...summaryRows.values());
|
|
9432
|
+
else {
|
|
9433
|
+
for (const row of detailRows.values()) {
|
|
9434
|
+
const count = Number(row["$err_count"]);
|
|
9435
|
+
if (row["$err_subtable"] !== "" && count >= 2) {
|
|
9436
|
+
row["$err_message"] = `${row["$err_message"]}\uFF08${count}\u884C: ${row["$err_subrow"]}\uFF09`;
|
|
9437
|
+
}
|
|
9438
|
+
rows.push(row);
|
|
8208
9439
|
}
|
|
8209
9440
|
}
|
|
9441
|
+
const columns = stmt.summary ? EXISTING_VALIDATION_SUMMARY_COLUMNS : EXISTING_VALIDATION_COLUMNS;
|
|
8210
9442
|
const result = {
|
|
8211
9443
|
type: "SELECT",
|
|
8212
|
-
columns: [...
|
|
9444
|
+
columns: [...columns],
|
|
8213
9445
|
rows,
|
|
8214
|
-
rowCount: rows.length
|
|
9446
|
+
rowCount: rows.length,
|
|
9447
|
+
validateStats: { errorRecords: errorRecordIds.size, errorCount }
|
|
8215
9448
|
};
|
|
8216
|
-
materializedMetaBySelectResult.set(result, existingValidationColumnMeta());
|
|
9449
|
+
materializedMetaBySelectResult.set(result, existingValidationColumnMeta(stmt.summary === true));
|
|
8217
9450
|
return result;
|
|
8218
9451
|
}
|
|
8219
9452
|
var TEMP_TABLE_MAX_ROWS = 1e4;
|
|
@@ -8243,7 +9476,7 @@ var BatchTimeoutError = class extends Error {
|
|
|
8243
9476
|
}
|
|
8244
9477
|
};
|
|
8245
9478
|
async function executeBatch(sql, client, options = {}) {
|
|
8246
|
-
const statements = parseSqlBatch(sql);
|
|
9479
|
+
const statements = parseSqlBatch(sql, options.enableImport === true);
|
|
8247
9480
|
const analysis = analyzeBatch(statements);
|
|
8248
9481
|
const injectedVariables = validateDeclaredBatchVariables(statements, options.variables);
|
|
8249
9482
|
const batchOptions = { ...options, variables: injectedVariables };
|
|
@@ -8296,11 +9529,12 @@ async function executeBatch(sql, client, options = {}) {
|
|
|
8296
9529
|
const userConfirm = batchOptions.confirm;
|
|
8297
9530
|
const stmtOptions = userConfirm ? {
|
|
8298
9531
|
...batchOptions,
|
|
8299
|
-
confirm: (count, operation) => userConfirm(count, operation, {
|
|
9532
|
+
confirm: (count, operation, detailContext) => userConfirm(count, operation, {
|
|
8300
9533
|
statementIndex: i,
|
|
8301
9534
|
statementCount: statements.length,
|
|
8302
9535
|
statementType: info.statementType,
|
|
8303
|
-
targetAppId: info.targetAppId
|
|
9536
|
+
targetAppId: info.targetAppId,
|
|
9537
|
+
...detailContext?.importDetail ? { importDetail: detailContext.importDetail } : {}
|
|
8304
9538
|
})
|
|
8305
9539
|
} : batchOptions;
|
|
8306
9540
|
const searchAbortCollector = { aborted: false };
|
|
@@ -8409,11 +9643,14 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
|
|
|
8409
9643
|
result.columns,
|
|
8410
9644
|
result.rows,
|
|
8411
9645
|
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
8412
|
-
materializedMetaBySelectResult.get(result) ?? existingValidationColumnMeta()
|
|
9646
|
+
materializedMetaBySelectResult.get(result) ?? existingValidationColumnMeta(resolvedStmt.summary === true)
|
|
8413
9647
|
);
|
|
8414
9648
|
}
|
|
8415
9649
|
return { result };
|
|
8416
9650
|
}
|
|
9651
|
+
if (resolvedStmt.type === "IMPORT") {
|
|
9652
|
+
return { result: await executeImport(resolvedStmt, client, options, cacheContext, tempTables) };
|
|
9653
|
+
}
|
|
8417
9654
|
if ("validateOnly" in resolvedStmt && resolvedStmt.validateOnly === true) {
|
|
8418
9655
|
const result = await executeDmlValidation(
|
|
8419
9656
|
resolvedStmt,
|
|
@@ -8575,9 +9812,9 @@ function safeJsonStringify(v) {
|
|
|
8575
9812
|
return String(v);
|
|
8576
9813
|
}
|
|
8577
9814
|
}
|
|
8578
|
-
function parseSqlBatch(sql) {
|
|
9815
|
+
function parseSqlBatch(sql, enableImport = false) {
|
|
8579
9816
|
const tokens = new Lexer(sql).tokenize();
|
|
8580
|
-
return new Parser(tokens).parseStatements();
|
|
9817
|
+
return new Parser(tokens, { import: enableImport }).parseStatements();
|
|
8581
9818
|
}
|
|
8582
9819
|
function evaluateScalarExpr(expr) {
|
|
8583
9820
|
switch (expr.type) {
|
|
@@ -9646,8 +10883,14 @@ async function inferSelectColumnMeta(stmt, outputColumns, client, cacheContext,
|
|
|
9646
10883
|
}
|
|
9647
10884
|
} else if (column.type === "ARITH_AGG_COL" || column.type === "ARITH_COL") {
|
|
9648
10885
|
meta = syntheticColumnMeta("number");
|
|
9649
|
-
} else if (column.type === "LITERAL_COL"
|
|
10886
|
+
} else if (column.type === "LITERAL_COL") {
|
|
9650
10887
|
meta = syntheticColumnMeta("string");
|
|
10888
|
+
} else if (column.type === "SCALAR_VALUE_COL") {
|
|
10889
|
+
const expr = column.expr;
|
|
10890
|
+
if (expr.type === "STRING_FUNC") meta = stringFunctionColumnMeta(expr);
|
|
10891
|
+
else if (expr.type === "NUMBER" || expr.type === "SCALAR_ARITH") meta = syntheticColumnMeta("number");
|
|
10892
|
+
else if (expr.type === "FIELD") meta = resolveField2(expr);
|
|
10893
|
+
else meta = syntheticColumnMeta("string");
|
|
9651
10894
|
} else if (column.type === "STRFUNC_COL") {
|
|
9652
10895
|
meta = stringFunctionColumnMeta(column.expr);
|
|
9653
10896
|
} else if (column.type === "WINDOW_COL") {
|
|
@@ -10461,9 +11704,10 @@ async function buildSortKindsForSelect(stmt, client, cacheContext) {
|
|
|
10461
11704
|
return sortKinds;
|
|
10462
11705
|
}
|
|
10463
11706
|
function convertProcessRowValue(raw, dstFieldType) {
|
|
10464
|
-
|
|
11707
|
+
if (typeof raw !== "string") return raw;
|
|
11708
|
+
const USER_TYPES4 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
10465
11709
|
const ARRAY_TYPES3 = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
|
|
10466
|
-
if (
|
|
11710
|
+
if (USER_TYPES4.has(dstFieldType ?? "")) {
|
|
10467
11711
|
if (raw === "") return [];
|
|
10468
11712
|
try {
|
|
10469
11713
|
const parsed = JSON.parse(raw);
|
|
@@ -10528,11 +11772,13 @@ function assertValidDmlRecords(records, targetFields, fieldInfos, numberPrecisio
|
|
|
10528
11772
|
records.forEach((record, rowIndex) => {
|
|
10529
11773
|
for (const code of targetFields) {
|
|
10530
11774
|
const info = infoByCode.get(code);
|
|
10531
|
-
const
|
|
11775
|
+
const original = record[code]?.value ?? "";
|
|
11776
|
+
const result = validateAndNormalizeDmlValue(original, info, numberPrecision);
|
|
10532
11777
|
if (!result.ok) {
|
|
10533
11778
|
throw new Error(`DmlValidationError: ${result.code} ${result.message} (row=${rowIndex + 1}, field=${code})`);
|
|
10534
11779
|
}
|
|
10535
|
-
|
|
11780
|
+
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);
|
|
11781
|
+
record[code] = { value: preserveCodes ? original : result.value };
|
|
10536
11782
|
}
|
|
10537
11783
|
});
|
|
10538
11784
|
}
|
|
@@ -10692,6 +11938,8 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10692
11938
|
if (stmt.type === "UPDATE") return materializeUpdateValidationCandidates(stmt, client, options, cacheContext, tempTables);
|
|
10693
11939
|
let rows;
|
|
10694
11940
|
let sourceRows;
|
|
11941
|
+
let sourcePresence;
|
|
11942
|
+
let sourceRowErrors;
|
|
10695
11943
|
let evaluationTypes;
|
|
10696
11944
|
if (stmt.type === "INSERT" || stmt.type === "UPSERT") {
|
|
10697
11945
|
assertInsertCheckRefs(stmt, stmt.fields);
|
|
@@ -10701,7 +11949,7 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10701
11949
|
(value, i) => value.type === "CASE_VALUE" ? evalCaseWhenValue(value.expr, {}, infoByCode.get(stmt.fields[i])?.fieldType) : value
|
|
10702
11950
|
));
|
|
10703
11951
|
} else {
|
|
10704
|
-
const selectResult =
|
|
11952
|
+
const selectResult = await dmlSourceMaterializer.materialize(stmt, client, options, cacheContext, tempTables, [...infoByCode.values()]);
|
|
10705
11953
|
const hasChecks = (stmt.checkGroups?.length ?? 0) > 0;
|
|
10706
11954
|
if (selectResult.columns.length < stmt.fields.length || !hasChecks && selectResult.columns.length !== stmt.fields.length) {
|
|
10707
11955
|
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`);
|
|
@@ -10711,7 +11959,9 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10711
11959
|
}
|
|
10712
11960
|
assertInsertCheckRefs(stmt, selectResult.columns);
|
|
10713
11961
|
sourceRows = selectResult.rows;
|
|
10714
|
-
|
|
11962
|
+
sourcePresence = selectResult.importPresence;
|
|
11963
|
+
sourceRowErrors = selectResult.importRowErrors;
|
|
11964
|
+
const meta = selectResult.columnMeta;
|
|
10715
11965
|
evaluationTypes = new Map(selectResult.columns.map((column) => {
|
|
10716
11966
|
const columnMeta = meta?.get(column);
|
|
10717
11967
|
const type = columnMeta?.fieldType ?? (columnMeta?.semantics?.compareMode === "number" || columnMeta?.sortKind === "number" ? "NUMBER" : "SINGLE_LINE_TEXT");
|
|
@@ -10724,8 +11974,10 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10724
11974
|
rowNumber: index + 1,
|
|
10725
11975
|
operation,
|
|
10726
11976
|
mode: "create",
|
|
10727
|
-
payload: new Map(stmt.fields.
|
|
10728
|
-
|
|
11977
|
+
payload: new Map(stmt.fields.flatMap(
|
|
11978
|
+
(field, i) => sourcePresence && !sourcePresence[index]?.has(field) ? [] : [[field, values[i]]]
|
|
11979
|
+
)),
|
|
11980
|
+
preErrors: [...sourceRowErrors?.[index] ?? []],
|
|
10729
11981
|
record: {},
|
|
10730
11982
|
evaluationRow: sourceRows?.[index] ?? Object.fromEntries(
|
|
10731
11983
|
stmt.fields.map((field, i) => [field, renderValidationValue(values[i])])
|
|
@@ -10738,13 +11990,17 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10738
11990
|
}
|
|
10739
11991
|
const fieldTypes = new Map([...infoByCode].map(([code, info]) => [code, info.fieldType]));
|
|
10740
11992
|
const rowKeys = candidates.map((candidate) => stmt.keyFields.map((key) => renderValidationValue(candidate.payload.get(key))));
|
|
10741
|
-
const targets = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeys, client, options, fieldTypes);
|
|
10742
11993
|
const numeric = stmt.keyFields.map((key) => fieldTypes.get(key) === "NUMBER");
|
|
10743
11994
|
const keyCounts = /* @__PURE__ */ new Map();
|
|
10744
11995
|
for (const parts of rowKeys) {
|
|
10745
11996
|
const key = upsertNormalizedKey(parts, numeric);
|
|
10746
11997
|
keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
|
|
10747
11998
|
}
|
|
11999
|
+
const isImport = importSourceByDmlStatement.has(stmt);
|
|
12000
|
+
if (isImport && [...keyCounts.values()].some((count) => count > 1)) {
|
|
12001
|
+
throw new Error("ERR_KEY_DUP_SOURCE: UPSERT \u30BD\u30FC\u30B9\u5185\u3067\u30AD\u30FC\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059");
|
|
12002
|
+
}
|
|
12003
|
+
const targets = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeys, client, options, fieldTypes);
|
|
10748
12004
|
candidates.forEach((candidate, index) => {
|
|
10749
12005
|
const parts = rowKeys[index];
|
|
10750
12006
|
const targetId = lookupUpsertTarget(targets, parts);
|
|
@@ -10753,7 +12009,7 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10753
12009
|
stmt.keyFields.forEach((key, keyIndex) => {
|
|
10754
12010
|
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` });
|
|
10755
12011
|
});
|
|
10756
|
-
if ((keyCounts.get(upsertNormalizedKey(parts, numeric)) ?? 0) > 1) {
|
|
12012
|
+
if (!isImport && (keyCounts.get(upsertNormalizedKey(parts, numeric)) ?? 0) > 1) {
|
|
10757
12013
|
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" });
|
|
10758
12014
|
}
|
|
10759
12015
|
});
|
|
@@ -11113,12 +12369,508 @@ async function executeInsert(stmt, client, options, cacheContext) {
|
|
|
11113
12369
|
insertedCount: createdIds.flat().length
|
|
11114
12370
|
};
|
|
11115
12371
|
}
|
|
12372
|
+
function importPlaceholderSelect() {
|
|
12373
|
+
return {
|
|
12374
|
+
type: "SELECT",
|
|
12375
|
+
distinct: false,
|
|
12376
|
+
columns: [],
|
|
12377
|
+
from: { appId: 0, alias: null, cteName: NO_FROM_CTE_NAME },
|
|
12378
|
+
joins: [],
|
|
12379
|
+
where: null,
|
|
12380
|
+
groupBy: [],
|
|
12381
|
+
having: null,
|
|
12382
|
+
orderMode: "CANONICAL",
|
|
12383
|
+
orderBy: [],
|
|
12384
|
+
limit: null,
|
|
12385
|
+
offset: null
|
|
12386
|
+
};
|
|
12387
|
+
}
|
|
12388
|
+
async function executeImport(stmt, client, options, cacheContext, tempTables) {
|
|
12389
|
+
if (!options.enableImport) throw new Error("UnsupportedError: IMPORT capability is disabled.");
|
|
12390
|
+
const handle = resolveImportSource(stmt.source.sourceName, options.importSource);
|
|
12391
|
+
if (stmt.targets?.some((target) => target.kind === "SUBTABLE")) {
|
|
12392
|
+
if (!stmt.validateOnly && !options.supportsImportConfirmDetail) {
|
|
12393
|
+
throw new Error("UnsupportedError: IMPORT subtable mutation requires a surface that displays parent/table replacement and deletion detail; use VALIDATE ONLY/EXPLAIN.");
|
|
12394
|
+
}
|
|
12395
|
+
if (stmt.source.kind === "CSV") {
|
|
12396
|
+
if (stmt.writeMode !== "UPDATE_RECORD_NUMBER" || !stmt.recordNumberSourceHeader) throw new Error("ArgumentError: CSV subtable replacement requires IMPORT UPDATE and MATCH RECORD NUMBER SOURCE.");
|
|
12397
|
+
if (!stmt.replaceSubtables?.length) throw new Error("ArgumentError: CSV subtable replacement requires REPLACE SUBTABLES (...).");
|
|
12398
|
+
const declared = new Set(stmt.targets.filter((target) => target.kind === "SUBTABLE").map((target) => target.subtableCode));
|
|
12399
|
+
if (stmt.replaceSubtables.some((table) => !declared.has(table))) throw new Error("ArgumentError: REPLACE SUBTABLES contains a table not declared in INTO.");
|
|
12400
|
+
for (const target of stmt.targets.filter((target2) => target2.kind === "SUBTABLE")) {
|
|
12401
|
+
if (!target.rowIdSourceHeader || !stmt.replaceSubtables.includes(target.subtableCode)) throw new Error(`ArgumentError: CSV subtable ${target.subtableCode} requires ROW ID SOURCE and REPLACE SUBTABLES declaration.`);
|
|
12402
|
+
}
|
|
12403
|
+
}
|
|
12404
|
+
if (stmt.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12405
|
+
const targets = stmt.targets;
|
|
12406
|
+
const fieldInfos = await getFieldsCached(stmt.appId, client, cacheContext);
|
|
12407
|
+
const targetCodes = targets.flatMap((target) => target.kind === "FIELD" ? [target.field] : target.children);
|
|
12408
|
+
const numberPrecision = fieldInfos.some((info) => targetCodes.includes(info.code) && info.fieldType === "NUMBER") ? await getNumberPrecisionCached(stmt.appId, client, cacheContext) : void 0;
|
|
12409
|
+
const payload = await loadImportSource(handle, /* @__PURE__ */ new Map());
|
|
12410
|
+
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);
|
|
12411
|
+
const operation = stmt.source.kind === "CSV" ? "UPDATE" : stmt.keyFields ? "UPSERT" : "INSERT";
|
|
12412
|
+
const prepared = prepareImportRecords(materialized, targets, fieldInfos, numberPrecision, operation);
|
|
12413
|
+
if (stmt.source.kind === "CSV") return executeCsvSubtableReplacement(stmt, materialized, prepared, fieldInfos, client, options, tempTables);
|
|
12414
|
+
assertImportRejectLimit(prepared, stmt.rejectLimit);
|
|
12415
|
+
const payloadFields = [...new Set(targets.flatMap((target) => target.kind === "FIELD" ? [target.field] : target.children))];
|
|
12416
|
+
const errors = materializeImportValidationErrors(prepared.errors, payloadFields);
|
|
12417
|
+
const columns = [...payloadFields, ...IMPORT_VALIDATION_META_COLUMNS];
|
|
12418
|
+
const invalidRows = prepared.invalidParentRows.size;
|
|
12419
|
+
const detail = {
|
|
12420
|
+
preflight: "ACTUAL_DATA",
|
|
12421
|
+
parents: { total: prepared.parents.length, valid: prepared.parents.length - invalidRows, invalid: invalidRows, mutationCandidates: prepared.parents.filter((parent) => parent.valid).length },
|
|
12422
|
+
tables: Object.fromEntries(prepared.tableCounts),
|
|
12423
|
+
writesKintone: false
|
|
12424
|
+
};
|
|
12425
|
+
const result2 = {
|
|
12426
|
+
type: "VALIDATION",
|
|
12427
|
+
operation,
|
|
12428
|
+
validatedRows: prepared.parents.length,
|
|
12429
|
+
validRows: prepared.parents.length - invalidRows,
|
|
12430
|
+
invalidRows,
|
|
12431
|
+
errorCount: errors.length,
|
|
12432
|
+
columns,
|
|
12433
|
+
errors,
|
|
12434
|
+
importDetail: detail,
|
|
12435
|
+
...stmt.validationErrorTable ? { errTable: stmt.validationErrorTable } : {}
|
|
12436
|
+
};
|
|
12437
|
+
if (stmt.validationErrorTable && tempTables) appendValidationErrors(
|
|
12438
|
+
tempTables,
|
|
12439
|
+
stmt.validationErrorTable,
|
|
12440
|
+
columns,
|
|
12441
|
+
errors,
|
|
12442
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12443
|
+
/* @__PURE__ */ new Map()
|
|
12444
|
+
);
|
|
12445
|
+
if (stmt.validateOnly) return result2;
|
|
12446
|
+
assertJsonImportHasNoRowIds(materialized);
|
|
12447
|
+
if (prepared.errors.length > 0 && !stmt.onErrorSkip) {
|
|
12448
|
+
const first = prepared.errors[0];
|
|
12449
|
+
throw new Error(`DmlValidationError: ${first.code} ${first.message} (row=${first.parentRow}, field=${first.field})`);
|
|
12450
|
+
}
|
|
12451
|
+
if (stmt.onErrorSkip) {
|
|
12452
|
+
if (!tempTables || !stmt.errorTable) throw new Error("ArgumentError: ON ERROR SKIP requires a batch and INTO error table.");
|
|
12453
|
+
appendValidationErrors(
|
|
12454
|
+
tempTables,
|
|
12455
|
+
stmt.errorTable,
|
|
12456
|
+
columns,
|
|
12457
|
+
errors,
|
|
12458
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12459
|
+
/* @__PURE__ */ new Map()
|
|
12460
|
+
);
|
|
12461
|
+
}
|
|
12462
|
+
const validParents = prepared.parents.filter((parent) => parent.valid);
|
|
12463
|
+
const fieldTypes = new Map(fieldInfos.map((info) => [info.code, info.fieldType]));
|
|
12464
|
+
const targetIds = validParents.map(() => void 0);
|
|
12465
|
+
if (stmt.keyFields) {
|
|
12466
|
+
for (const key of stmt.keyFields) if (!stmt.fields.includes(key)) {
|
|
12467
|
+
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`);
|
|
12468
|
+
}
|
|
12469
|
+
const numeric = stmt.keyFields.map((key) => fieldTypes.get(key) === "NUMBER");
|
|
12470
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
12471
|
+
const rowKeys = validParents.map((parent) => stmt.keyFields.map((key) => String(parent.top[key]?.value ?? "")));
|
|
12472
|
+
for (const parts of rowKeys) {
|
|
12473
|
+
const normalized = upsertNormalizedKey(parts, numeric);
|
|
12474
|
+
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");
|
|
12475
|
+
sourceKeys.add(normalized);
|
|
12476
|
+
}
|
|
12477
|
+
const targetsIndex = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeys, client, options, fieldTypes);
|
|
12478
|
+
rowKeys.forEach((parts, index) => {
|
|
12479
|
+
targetIds[index] = lookupUpsertTarget(targetsIndex, parts);
|
|
12480
|
+
});
|
|
12481
|
+
}
|
|
12482
|
+
const tableCodes = targets.filter((target) => target.kind === "SUBTABLE").map((target) => target.subtableCode);
|
|
12483
|
+
const existingById = /* @__PURE__ */ new Map();
|
|
12484
|
+
const updateIds = targetIds.filter((id) => id !== void 0);
|
|
12485
|
+
for (const chunk2 of splitChunks([...new Set(updateIds)], 100)) {
|
|
12486
|
+
const response = await client.getRecords({ app: stmt.appId, query: `$id in (${chunk2.join(",")}) limit 500`, fields: ["$id", "$revision", ...tableCodes] });
|
|
12487
|
+
for (const record of response.records) {
|
|
12488
|
+
const id = Number(record["$id"]?.value);
|
|
12489
|
+
const revision = Number(record["$revision"]?.value);
|
|
12490
|
+
if (Number.isFinite(id)) existingById.set(id, { id, ...Number.isFinite(revision) ? { revision } : {}, record });
|
|
12491
|
+
}
|
|
12492
|
+
}
|
|
12493
|
+
const writePlan = buildJsonSubtableWritePlan(validParents, targetIds, existingById);
|
|
12494
|
+
const importDetail = {
|
|
12495
|
+
kind: "IMPORT_JSON_SUBTABLE",
|
|
12496
|
+
rowIdPolicy: "DROP_AND_RENUMBER_ALL",
|
|
12497
|
+
parentsToWrite: writePlan.length,
|
|
12498
|
+
insertedParents: writePlan.filter((parent) => parent.mode === "INSERT").length,
|
|
12499
|
+
updatedParents: writePlan.filter((parent) => parent.mode === "UPDATE").length,
|
|
12500
|
+
hasDeletes: writePlan.some((parent) => parent.tables.some((table) => table.deleteRows > 0)),
|
|
12501
|
+
parents: writePlan.map((parent) => ({ parentRow: parent.parentRow, mode: parent.mode, ...parent.targetId === void 0 ? {} : { targetId: parent.targetId }, tables: parent.tables }))
|
|
12502
|
+
};
|
|
12503
|
+
if (writePlan.length > 0) {
|
|
12504
|
+
if (!options.confirm) throw new Error("UnsupportedError: JSON IMPORT subtable mutation requires explicit confirmation detail approval.");
|
|
12505
|
+
const ok = await options.confirm(writePlan.length, stmt.keyFields ? "UPDATE" : "INSERT", { statementIndex: 0, statementCount: 1, statementType: "IMPORT", targetAppId: stmt.appId, importDetail });
|
|
12506
|
+
if (!ok) throw new OperationCancelledError(stmt.keyFields ? "UPDATE" : "INSERT", writePlan.length);
|
|
12507
|
+
}
|
|
12508
|
+
const toScalarMap = (record) => new Map(
|
|
12509
|
+
Object.entries(record).map(([code, field]) => [code, field.value])
|
|
12510
|
+
);
|
|
12511
|
+
const payloadFor = (parent) => buildJsonImportRecordPayload(
|
|
12512
|
+
toScalarMap(parent.top),
|
|
12513
|
+
new Map([...parent.subtables].map(([table, rows]) => [table, rows.map((row) => ({ values: toScalarMap(row) }))]))
|
|
12514
|
+
);
|
|
12515
|
+
const inserts = writePlan.filter((parent) => parent.mode === "INSERT");
|
|
12516
|
+
const updates = writePlan.filter((parent) => parent.mode === "UPDATE");
|
|
12517
|
+
const createdIds = [];
|
|
12518
|
+
for (let i = 0; i < inserts.length; i += 100) {
|
|
12519
|
+
const response = await client.postRecords({ app: stmt.appId, records: inserts.slice(i, i + 100).map(payloadFor) });
|
|
12520
|
+
createdIds.push(response.ids);
|
|
12521
|
+
}
|
|
12522
|
+
for (let i = 0; i < updates.length; i += 100) await client.putRecords({
|
|
12523
|
+
app: stmt.appId,
|
|
12524
|
+
records: updates.slice(i, i + 100).map((parent) => ({ id: parent.targetId, ...parent.revision === void 0 ? {} : { revision: parent.revision }, record: payloadFor(parent) }))
|
|
12525
|
+
});
|
|
12526
|
+
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 };
|
|
12527
|
+
}
|
|
12528
|
+
if (stmt.writeMode === "UPDATE_RECORD_NUMBER") {
|
|
12529
|
+
return executeImportRecordNumberUpdate(
|
|
12530
|
+
stmt,
|
|
12531
|
+
handle,
|
|
12532
|
+
client,
|
|
12533
|
+
options,
|
|
12534
|
+
cacheContext,
|
|
12535
|
+
tempTables
|
|
12536
|
+
);
|
|
12537
|
+
}
|
|
12538
|
+
const common = {
|
|
12539
|
+
appId: stmt.appId,
|
|
12540
|
+
fields: stmt.fields,
|
|
12541
|
+
select: importPlaceholderSelect(),
|
|
12542
|
+
validateOnly: stmt.validateOnly,
|
|
12543
|
+
validationErrorTable: stmt.validationErrorTable,
|
|
12544
|
+
onErrorSkip: stmt.onErrorSkip,
|
|
12545
|
+
errorTable: stmt.errorTable,
|
|
12546
|
+
rejectLimit: stmt.rejectLimit,
|
|
12547
|
+
checkGroups: stmt.checkGroups
|
|
12548
|
+
};
|
|
12549
|
+
const generated = stmt.keyFields ? { type: "UPSERT_SELECT", ...common, keyFields: stmt.keyFields } : { type: "INSERT_SELECT", ...common };
|
|
12550
|
+
const executionSource = { source: stmt.source, handle, cache: /* @__PURE__ */ new Map() };
|
|
12551
|
+
importSourceByDmlStatement.set(generated, executionSource);
|
|
12552
|
+
const withAudit = (result2) => {
|
|
12553
|
+
if (executionSource.audit) Object.assign(result2, { importAudit: executionSource.audit });
|
|
12554
|
+
return result2;
|
|
12555
|
+
};
|
|
12556
|
+
if (generated.validateOnly) {
|
|
12557
|
+
if (generated.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12558
|
+
const result2 = await executeDmlValidation(generated, client, { ...options, onLimitReached: "error" }, cacheContext, tempTables, 1);
|
|
12559
|
+
if (generated.validationErrorTable && tempTables) {
|
|
12560
|
+
appendValidationErrors(
|
|
12561
|
+
tempTables,
|
|
12562
|
+
generated.validationErrorTable,
|
|
12563
|
+
result2.columns,
|
|
12564
|
+
result2.errors,
|
|
12565
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12566
|
+
materializedMetaByValidationResult.get(result2) ?? /* @__PURE__ */ new Map()
|
|
12567
|
+
);
|
|
12568
|
+
}
|
|
12569
|
+
return withAudit(result2);
|
|
12570
|
+
}
|
|
12571
|
+
if (generated.onErrorSkip) {
|
|
12572
|
+
if (!tempTables) throw new Error("ArgumentError: ON ERROR SKIP requires a batch.");
|
|
12573
|
+
const result2 = await (generated.type === "UPSERT_SELECT" ? executeOnErrorSkip(generated, client, options, cacheContext, tempTables, 1) : executeOnErrorSkip(generated, client, options, cacheContext, tempTables, 1));
|
|
12574
|
+
return withAudit(result2);
|
|
12575
|
+
}
|
|
12576
|
+
const result = await (generated.type === "UPSERT_SELECT" ? executeUpsertSelect(generated, client, options, cacheContext, tempTables) : executeInsertSelect(generated, client, options, cacheContext, tempTables));
|
|
12577
|
+
return withAudit(result);
|
|
12578
|
+
}
|
|
12579
|
+
async function executeCsvSubtableReplacement(stmt, materialized, preparedBase, fieldInfos, client, options, tempTables) {
|
|
12580
|
+
if (!stmt.recordNumberSourceHeader || !stmt.replaceSubtables?.length) throw new Error("InternalError: incomplete CSV subtable replacement AST.");
|
|
12581
|
+
assertNoDuplicateCsvSubtableRowIds(materialized.records);
|
|
12582
|
+
const rawKeys = materialized.records.map((record) => record.recordNumberSourceValue ?? "");
|
|
12583
|
+
const keyPlan = preflightImportRecordNumbers(rawKeys, stmt.recordNumberSourceHeader);
|
|
12584
|
+
const tableCodes = [...stmt.replaceSubtables];
|
|
12585
|
+
const ownershipTableCodes = [...new Set(fieldInfos.filter((info) => !info.inSubtable && info.fieldType === "SUBTABLE").map((info) => info.code))];
|
|
12586
|
+
const allRecords = await fetchAll(client.getRecords, stmt.appId, "", ["$id", "$revision", ...ownershipTableCodes], {
|
|
12587
|
+
maxRecords: options.maxRecords ?? 1e4,
|
|
12588
|
+
parallel: options.fetchParallel ?? 1,
|
|
12589
|
+
onLimit: "error"
|
|
12590
|
+
});
|
|
12591
|
+
const existingById = /* @__PURE__ */ new Map();
|
|
12592
|
+
const ownership = /* @__PURE__ */ new Map();
|
|
12593
|
+
for (const record of allRecords) {
|
|
12594
|
+
const id = Number(record["$id"]?.value);
|
|
12595
|
+
const revision = Number(record["$revision"]?.value);
|
|
12596
|
+
if (!Number.isFinite(id)) continue;
|
|
12597
|
+
existingById.set(id, { id, ...Number.isFinite(revision) ? { revision } : {}, record });
|
|
12598
|
+
for (const table of ownershipTableCodes) {
|
|
12599
|
+
const rows = record[table]?.value;
|
|
12600
|
+
if (!Array.isArray(rows)) continue;
|
|
12601
|
+
for (const row of rows) if (row.id) {
|
|
12602
|
+
const owners = ownership.get(row.id) ?? [];
|
|
12603
|
+
owners.push({ parentId: id, table });
|
|
12604
|
+
ownership.set(row.id, owners);
|
|
12605
|
+
}
|
|
12606
|
+
}
|
|
12607
|
+
}
|
|
12608
|
+
const targetIds = keyPlan.normalized.map((key) => key === null ? void 0 : Number(key));
|
|
12609
|
+
const parents = preparedBase.parents.map((parent, index) => {
|
|
12610
|
+
const errors2 = [...parent.errors];
|
|
12611
|
+
for (const error of keyPlan.errors[index]) errors2.push({
|
|
12612
|
+
operation: "UPDATE",
|
|
12613
|
+
parentRow: parent.parentRow,
|
|
12614
|
+
field: error.field,
|
|
12615
|
+
code: error.code,
|
|
12616
|
+
message: error.message,
|
|
12617
|
+
sourceValues: materialized.records[index].top
|
|
12618
|
+
});
|
|
12619
|
+
const targetId = targetIds[index];
|
|
12620
|
+
if (targetId !== void 0 && !existingById.has(targetId)) errors2.push({
|
|
12621
|
+
operation: "UPDATE",
|
|
12622
|
+
parentRow: parent.parentRow,
|
|
12623
|
+
field: stmt.recordNumberSourceHeader,
|
|
12624
|
+
code: "ERR_RECORD_NUMBER_NOT_FOUND",
|
|
12625
|
+
message: `record number ${targetId} does not exist in APP${stmt.appId}`,
|
|
12626
|
+
sourceValues: materialized.records[index].top
|
|
12627
|
+
});
|
|
12628
|
+
return { ...parent, valid: errors2.length === 0, errors: errors2 };
|
|
12629
|
+
});
|
|
12630
|
+
const initialPlan = buildCsvSubtableReplacementPlan(materialized.records, parents, targetIds, existingById, ownership);
|
|
12631
|
+
const planErrors = initialPlan.flatMap((parent) => [...parent.errors]);
|
|
12632
|
+
const invalidParentRows = new Set(initialPlan.filter((parent) => !parent.valid).map((parent) => parent.parentRow));
|
|
12633
|
+
const prepared = { ...preparedBase, parents, errors: planErrors, invalidParentRows };
|
|
12634
|
+
assertImportRejectLimit(prepared, stmt.rejectLimit);
|
|
12635
|
+
const validPlan = initialPlan.filter((parent) => parent.valid);
|
|
12636
|
+
const allTables = initialPlan.flatMap((parent) => parent.tables);
|
|
12637
|
+
const sum = (table, key) => allTables.filter((item) => item.table === table).reduce((n, item) => n + Number(item[key]), 0);
|
|
12638
|
+
const tableDetail = Object.fromEntries(tableCodes.map((table) => [table, {
|
|
12639
|
+
existingRows: sum(table, "existingRows"),
|
|
12640
|
+
inputRows: sum(table, "inputRows"),
|
|
12641
|
+
updateRows: sum(table, "updateRows"),
|
|
12642
|
+
addRows: sum(table, "addRows"),
|
|
12643
|
+
deleteRows: sum(table, "deleteRows"),
|
|
12644
|
+
rowIdNotFound: sum(table, "rowIdNotFound")
|
|
12645
|
+
}]));
|
|
12646
|
+
const importDetail = {
|
|
12647
|
+
kind: "IMPORT_CSV_SUBTABLE_REPLACE",
|
|
12648
|
+
rowIdPolicy: "PRESERVE_EXISTING",
|
|
12649
|
+
parentsToWrite: validPlan.length,
|
|
12650
|
+
insertedParents: 0,
|
|
12651
|
+
updatedParents: validPlan.length,
|
|
12652
|
+
hasDeletes: validPlan.some((parent) => parent.tables.some((table) => table.deleteRows > 0)),
|
|
12653
|
+
totalDeleteRows: validPlan.flatMap((parent) => parent.tables).reduce((n, table) => n + table.deleteRows, 0),
|
|
12654
|
+
rowIdNotFound: validPlan.flatMap((parent) => parent.tables).reduce((n, table) => n + table.rowIdNotFound, 0),
|
|
12655
|
+
invalidParents: invalidParentRows.size,
|
|
12656
|
+
parents: validPlan.map((parent) => ({ parentRow: parent.parentRow, mode: "UPDATE", targetId: parent.targetId, tables: parent.tables }))
|
|
12657
|
+
};
|
|
12658
|
+
const payloadFields = [...new Set(stmt.targets.flatMap((target) => target.kind === "FIELD" ? [target.field] : target.children))];
|
|
12659
|
+
const errors = materializeImportValidationErrors(planErrors, payloadFields);
|
|
12660
|
+
const columns = [...payloadFields, ...IMPORT_VALIDATION_META_COLUMNS];
|
|
12661
|
+
if (stmt.validateOnly) {
|
|
12662
|
+
if (stmt.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12663
|
+
if (stmt.validationErrorTable && tempTables) appendValidationErrors(tempTables, stmt.validationErrorTable, columns, errors, options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS, /* @__PURE__ */ new Map());
|
|
12664
|
+
return {
|
|
12665
|
+
type: "VALIDATION",
|
|
12666
|
+
operation: "UPDATE",
|
|
12667
|
+
validatedRows: parents.length,
|
|
12668
|
+
validRows: parents.length - invalidParentRows.size,
|
|
12669
|
+
invalidRows: invalidParentRows.size,
|
|
12670
|
+
errorCount: errors.length,
|
|
12671
|
+
columns,
|
|
12672
|
+
errors,
|
|
12673
|
+
...stmt.validationErrorTable ? { errTable: stmt.validationErrorTable } : {},
|
|
12674
|
+
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 }
|
|
12675
|
+
};
|
|
12676
|
+
}
|
|
12677
|
+
if (planErrors.length && !stmt.onErrorSkip) {
|
|
12678
|
+
const first = planErrors[0];
|
|
12679
|
+
throw new Error(`DmlValidationError: ${first.code} ${first.message} (row=${first.parentRow}, field=${first.field})`);
|
|
12680
|
+
}
|
|
12681
|
+
if (stmt.onErrorSkip) {
|
|
12682
|
+
if (!tempTables || !stmt.errorTable) throw new Error("ArgumentError: ON ERROR SKIP requires a batch and INTO error table.");
|
|
12683
|
+
appendValidationErrors(tempTables, stmt.errorTable, columns, errors, options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS, /* @__PURE__ */ new Map());
|
|
12684
|
+
}
|
|
12685
|
+
if (validPlan.length) {
|
|
12686
|
+
if (!options.supportsImportConfirmDetail || !options.confirm) throw new Error("UnsupportedError: CSV subtable replacement requires explicit rendered detail approval.");
|
|
12687
|
+
const ok = await options.confirm(validPlan.length, "UPDATE", { statementIndex: 0, statementCount: 1, statementType: "IMPORT", targetAppId: stmt.appId, importDetail });
|
|
12688
|
+
if (!ok) throw new OperationCancelledError("UPDATE", validPlan.length);
|
|
12689
|
+
}
|
|
12690
|
+
const scalarMap = (record) => new Map(Object.entries(record).map(([code, field]) => [code, field.value]));
|
|
12691
|
+
for (const parent of validPlan) {
|
|
12692
|
+
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");
|
|
12693
|
+
await client.putRecords({ app: stmt.appId, records: [{ id: parent.targetId, ...parent.revision === void 0 ? {} : { revision: parent.revision }, record }] });
|
|
12694
|
+
}
|
|
12695
|
+
return { type: "UPDATE", updatedCount: validPlan.length, affectedRows: validPlan.length, skippedRows: invalidParentRows.size, rejectLimit: stmt.rejectLimit, ...stmt.errorTable ? { errTable: stmt.errorTable } : {}, importDetail };
|
|
12696
|
+
}
|
|
12697
|
+
async function executeImportRecordNumberUpdate(stmt, handle, client, options, cacheContext, tempTables) {
|
|
12698
|
+
if (stmt.source.kind !== "CSV" || stmt.source.mappingMode !== "BY_NAME" || !stmt.recordNumberSourceHeader) {
|
|
12699
|
+
throw new Error("InternalError: invalid IMPORT UPDATE AST.");
|
|
12700
|
+
}
|
|
12701
|
+
if (new Set(stmt.fields).size !== stmt.fields.length) throw new Error("ArgumentError: DML target fields contain duplicates.");
|
|
12702
|
+
const fieldInfos = await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
|
|
12703
|
+
const numberPrecision = await loadNumberPrecisionForTargets(stmt.appId, stmt.fields, fieldInfos, client, cacheContext);
|
|
12704
|
+
const payload = await loadImportSource(handle, /* @__PURE__ */ new Map());
|
|
12705
|
+
const sourceTable = materializeCsvDmlSource(
|
|
12706
|
+
stmt.source,
|
|
12707
|
+
payload,
|
|
12708
|
+
options.maxRecords ?? 1e4,
|
|
12709
|
+
stmt.fields,
|
|
12710
|
+
fieldInfos,
|
|
12711
|
+
stmt.recordNumberSourceHeader
|
|
12712
|
+
);
|
|
12713
|
+
const keyValues = sourceTable.recordNumberSourceValues;
|
|
12714
|
+
if (!keyValues) throw new Error("InternalError: record-number source values were not materialized.");
|
|
12715
|
+
const keyPlan = preflightImportRecordNumbers(keyValues, stmt.recordNumberSourceHeader);
|
|
12716
|
+
const matchedIds = /* @__PURE__ */ new Set();
|
|
12717
|
+
const lookupKeys = [...new Set(keyPlan.normalized.filter((key) => key !== null))];
|
|
12718
|
+
for (let i = 0; i < lookupKeys.length; i += 100) {
|
|
12719
|
+
const chunk2 = lookupKeys.slice(i, i + 100);
|
|
12720
|
+
const response = await client.getRecords({
|
|
12721
|
+
app: stmt.appId,
|
|
12722
|
+
query: `$id in (${chunk2.join(",")}) limit 500`,
|
|
12723
|
+
fields: ["$id"]
|
|
12724
|
+
});
|
|
12725
|
+
for (const record of response.records) {
|
|
12726
|
+
const id = record["$id"]?.value;
|
|
12727
|
+
if (typeof id === "string" && id !== "") matchedIds.add(id.replace(/^0+(?=\d)/, ""));
|
|
12728
|
+
}
|
|
12729
|
+
}
|
|
12730
|
+
const infoByCode = new Map(fieldInfos.map((info) => [info.code, info]));
|
|
12731
|
+
const evaluationTypes = new Map(stmt.fields.map((field) => [field, infoByCode.get(field)?.fieldType ?? "SINGLE_LINE_TEXT"]));
|
|
12732
|
+
const candidates = sourceTable.rows.map((row, index) => {
|
|
12733
|
+
const key = keyPlan.normalized[index];
|
|
12734
|
+
const preErrors = [
|
|
12735
|
+
...sourceTable.importRowErrors?.[index] ?? [],
|
|
12736
|
+
...keyPlan.errors[index]
|
|
12737
|
+
];
|
|
12738
|
+
if (key !== null && !matchedIds.has(key)) preErrors.push({
|
|
12739
|
+
field: stmt.recordNumberSourceHeader,
|
|
12740
|
+
code: "ERR_RECORD_NUMBER_NOT_FOUND",
|
|
12741
|
+
message: `record number ${key} does not exist in APP${stmt.appId}`
|
|
12742
|
+
});
|
|
12743
|
+
return {
|
|
12744
|
+
rowNumber: index + 1,
|
|
12745
|
+
operation: "UPDATE",
|
|
12746
|
+
mode: "update",
|
|
12747
|
+
...key !== null && matchedIds.has(key) ? { targetId: Number(key) } : {},
|
|
12748
|
+
payload: new Map([
|
|
12749
|
+
[stmt.recordNumberSourceHeader, keyValues[index]],
|
|
12750
|
+
...stmt.fields.map((field) => [field, row[field] ?? ""])
|
|
12751
|
+
]),
|
|
12752
|
+
preErrors,
|
|
12753
|
+
record: {},
|
|
12754
|
+
evaluationRow: row,
|
|
12755
|
+
evaluationFieldTypes: evaluationTypes
|
|
12756
|
+
};
|
|
12757
|
+
});
|
|
12758
|
+
const diagnosticFields = [stmt.recordNumberSourceHeader, ...stmt.fields];
|
|
12759
|
+
const validation = validateDmlCandidates(
|
|
12760
|
+
candidates,
|
|
12761
|
+
"UPDATE",
|
|
12762
|
+
diagnosticFields,
|
|
12763
|
+
stmt.fields,
|
|
12764
|
+
fieldInfos,
|
|
12765
|
+
1,
|
|
12766
|
+
numberPrecision,
|
|
12767
|
+
stmt.checkGroups ?? [],
|
|
12768
|
+
false
|
|
12769
|
+
);
|
|
12770
|
+
const columns = [...diagnosticFields, ...VALIDATION_META_COLUMNS];
|
|
12771
|
+
const validationResult = {
|
|
12772
|
+
type: "VALIDATION",
|
|
12773
|
+
operation: "UPDATE",
|
|
12774
|
+
validatedRows: candidates.length,
|
|
12775
|
+
validRows: candidates.length - validation.invalidRows,
|
|
12776
|
+
invalidRows: validation.invalidRows,
|
|
12777
|
+
errorCount: validation.errors.length,
|
|
12778
|
+
columns,
|
|
12779
|
+
errors: validation.errors,
|
|
12780
|
+
...stmt.validationErrorTable ?? stmt.errorTable ? { errTable: stmt.validationErrorTable ?? stmt.errorTable } : {}
|
|
12781
|
+
};
|
|
12782
|
+
Object.assign(validationResult, { importAudit: sourceTable.importAudit });
|
|
12783
|
+
if (stmt.validateOnly) {
|
|
12784
|
+
if (stmt.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12785
|
+
if (stmt.validationErrorTable && tempTables) appendValidationErrors(
|
|
12786
|
+
tempTables,
|
|
12787
|
+
stmt.validationErrorTable,
|
|
12788
|
+
columns,
|
|
12789
|
+
validation.errors,
|
|
12790
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12791
|
+
/* @__PURE__ */ new Map()
|
|
12792
|
+
);
|
|
12793
|
+
return validationResult;
|
|
12794
|
+
}
|
|
12795
|
+
if (!stmt.onErrorSkip && validation.invalidRows > 0) {
|
|
12796
|
+
const first = validation.errors[0];
|
|
12797
|
+
throw new Error(`DmlValidationError: ${first.$err_code} ${first.$err_message} (row=${first.$err_row}, field=${first.$err_field})`);
|
|
12798
|
+
}
|
|
12799
|
+
if (stmt.onErrorSkip) {
|
|
12800
|
+
if (!tempTables || !stmt.errorTable) throw new Error("ArgumentError: ON ERROR SKIP requires a batch.");
|
|
12801
|
+
appendValidationErrors(
|
|
12802
|
+
tempTables,
|
|
12803
|
+
stmt.errorTable,
|
|
12804
|
+
columns,
|
|
12805
|
+
validation.errors,
|
|
12806
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12807
|
+
/* @__PURE__ */ new Map()
|
|
12808
|
+
);
|
|
12809
|
+
if (stmt.rejectLimit != null && validation.invalidRows > stmt.rejectLimit) {
|
|
12810
|
+
throw new RejectLimitExceededError(
|
|
12811
|
+
`rejected rows (${validation.invalidRows}) exceed REJECT LIMIT (${stmt.rejectLimit}).`,
|
|
12812
|
+
validationResult
|
|
12813
|
+
);
|
|
12814
|
+
}
|
|
12815
|
+
}
|
|
12816
|
+
const valid = candidates.filter((candidate) => !validation.invalidRowNumbers.has(candidate.rowNumber));
|
|
12817
|
+
if (options.confirm) {
|
|
12818
|
+
const ok = await options.confirm(valid.length, "UPDATE");
|
|
12819
|
+
if (!ok) throw new OperationCancelledError("UPDATE", valid.length);
|
|
12820
|
+
}
|
|
12821
|
+
const updates = valid.map((candidate) => ({ id: candidate.targetId, record: candidate.record }));
|
|
12822
|
+
for (let i = 0; i < updates.length; i += 100) {
|
|
12823
|
+
await client.putRecords({ app: stmt.appId, records: updates.slice(i, i + 100) });
|
|
12824
|
+
}
|
|
12825
|
+
const result = {
|
|
12826
|
+
type: "UPDATE",
|
|
12827
|
+
updatedCount: updates.length,
|
|
12828
|
+
...stmt.onErrorSkip ? {
|
|
12829
|
+
affectedRows: updates.length,
|
|
12830
|
+
skippedRows: validation.invalidRows,
|
|
12831
|
+
rejectLimit: stmt.rejectLimit ?? null,
|
|
12832
|
+
errTable: stmt.errorTable
|
|
12833
|
+
} : {}
|
|
12834
|
+
};
|
|
12835
|
+
Object.assign(result, { insertedCount: 0, importAudit: sourceTable.importAudit });
|
|
12836
|
+
return result;
|
|
12837
|
+
}
|
|
12838
|
+
async function materializeDmlSource(stmt, client, options, cacheContext, tempTables, targetFields) {
|
|
12839
|
+
const imported = importSourceByDmlStatement.get(stmt);
|
|
12840
|
+
if (!imported) {
|
|
12841
|
+
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);
|
|
12842
|
+
return { rows: selected2.rows, columns: selected2.columns, columnMeta: materializedMetaBySelectResult.get(selected2) };
|
|
12843
|
+
}
|
|
12844
|
+
const payload = await loadImportSource(imported.handle, imported.cache);
|
|
12845
|
+
const rowLimit = options.maxRecords ?? 1e4;
|
|
12846
|
+
const targetByCode = new Map((targetFields ?? []).map((info) => [info.code, info]));
|
|
12847
|
+
const jsonTargets = stmt.fields.map((code) => ({ code, fieldType: targetByCode.get(code)?.fieldType ?? "SINGLE_LINE_TEXT" }));
|
|
12848
|
+
const raw = imported.source.kind === "JSON" ? materializeJsonDmlSource(imported.source, payload, jsonTargets, rowLimit) : materializeCsvDmlSource(imported.source, payload, rowLimit, stmt.fields, targetFields);
|
|
12849
|
+
imported.audit = raw.importAudit;
|
|
12850
|
+
if (imported.source.kind === "JSON") return raw;
|
|
12851
|
+
if (!imported.source.projection) return raw;
|
|
12852
|
+
const projection = bindImportProjection(imported.source.projection);
|
|
12853
|
+
const tables = new Map(tempTables ?? []);
|
|
12854
|
+
tables.set(IMPORT_PROJECTION_SOURCE, raw);
|
|
12855
|
+
const selected = await executeQueryWithCte(projection, client, { ...options, onLimitReached: "error" }, tables, cacheContext, true);
|
|
12856
|
+
return { rows: selected.rows, columns: selected.columns, columnMeta: materializedMetaBySelectResult.get(selected) };
|
|
12857
|
+
}
|
|
12858
|
+
var dmlSourceMaterializer = { materialize: materializeDmlSource };
|
|
12859
|
+
function assertNoImportRowErrors(table) {
|
|
12860
|
+
for (let rowIndex = 0; rowIndex < (table.importRowErrors?.length ?? 0); rowIndex++) {
|
|
12861
|
+
const first = table.importRowErrors?.[rowIndex]?.[0];
|
|
12862
|
+
if (first) {
|
|
12863
|
+
throw new Error(`DmlValidationError: ${first.code} ${first.message} (row=${rowIndex + 1}, field=${first.field})`);
|
|
12864
|
+
}
|
|
12865
|
+
}
|
|
12866
|
+
}
|
|
11116
12867
|
async function executeInsertSelect(stmt, client, options, cacheContext, cteCache) {
|
|
11117
12868
|
if (stmt.checkGroups?.length) return executeCheckedPlainDml(stmt, client, options, cacheContext, cteCache);
|
|
11118
12869
|
const fieldInfos = await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
|
|
11119
12870
|
const numberPrecision = await loadNumberPrecisionForTargets(stmt.appId, stmt.fields, fieldInfos, client, cacheContext);
|
|
11120
|
-
const
|
|
11121
|
-
const { rows, columns } =
|
|
12871
|
+
const sourceTable = await dmlSourceMaterializer.materialize(stmt, client, options, cacheContext, cteCache, fieldInfos);
|
|
12872
|
+
const { rows, columns } = sourceTable;
|
|
12873
|
+
assertNoImportRowErrors(sourceTable);
|
|
11122
12874
|
if (columns.length !== stmt.fields.length) {
|
|
11123
12875
|
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" : "";
|
|
11124
12876
|
throw new Error(
|
|
@@ -11130,15 +12882,16 @@ async function executeInsertSelect(stmt, client, options, cacheContext, cteCache
|
|
|
11130
12882
|
if (!ok) throw new OperationCancelledError("INSERT", rows.length);
|
|
11131
12883
|
}
|
|
11132
12884
|
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
11133
|
-
const allRecords = rows.map((row) => {
|
|
12885
|
+
const allRecords = rows.map((row, rowIndex) => {
|
|
11134
12886
|
const record = {};
|
|
11135
12887
|
stmt.fields.forEach((field, i) => {
|
|
12888
|
+
if (sourceTable.importPresence && !sourceTable.importPresence[rowIndex]?.has(field)) return;
|
|
11136
12889
|
const raw = row[columns[i]] ?? "";
|
|
11137
12890
|
record[field] = { value: convertProcessRowValue(raw, fieldTypes.get(field)) };
|
|
11138
12891
|
});
|
|
11139
12892
|
return record;
|
|
11140
12893
|
});
|
|
11141
|
-
assertValidDmlRecords(
|
|
12894
|
+
allRecords.forEach((record) => assertValidDmlRecords([record], stmt.fields.filter((field) => field in record), fieldInfos, numberPrecision));
|
|
11142
12895
|
const createdIds = [];
|
|
11143
12896
|
for (let i = 0; i < allRecords.length; i += 100) {
|
|
11144
12897
|
const batch = allRecords.slice(i, i + 100);
|
|
@@ -11531,9 +13284,9 @@ function expandRowsForSubtableDml(parents, subtableCode) {
|
|
|
11531
13284
|
for (const parent of parents) {
|
|
11532
13285
|
const parentId = String(parent["$id"]?.value ?? "");
|
|
11533
13286
|
const parentRevision = getRevision(parent);
|
|
11534
|
-
const
|
|
11535
|
-
for (let i = 0; i <
|
|
11536
|
-
const row =
|
|
13287
|
+
const tableRows2 = getMutableTableRows(parent, subtableCode);
|
|
13288
|
+
for (let i = 0; i < tableRows2.length; i++) {
|
|
13289
|
+
const row = tableRows2[i];
|
|
11537
13290
|
const flat = {
|
|
11538
13291
|
_pid: parentId,
|
|
11539
13292
|
_rid: row.id ?? "",
|
|
@@ -11739,8 +13492,9 @@ async function executeUpsertSelect(stmt, client, options, cacheContext, cteCache
|
|
|
11739
13492
|
if (stmt.checkGroups?.length) return executeCheckedPlainDml(stmt, client, options, cacheContext, cteCache);
|
|
11740
13493
|
const fieldInfos = await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
|
|
11741
13494
|
const numberPrecision = await loadNumberPrecisionForTargets(stmt.appId, stmt.fields, fieldInfos, client, cacheContext);
|
|
11742
|
-
const
|
|
11743
|
-
const { rows, columns } =
|
|
13495
|
+
const sourceTable = await dmlSourceMaterializer.materialize(stmt, client, options, cacheContext, cteCache, fieldInfos);
|
|
13496
|
+
const { rows, columns } = sourceTable;
|
|
13497
|
+
assertNoImportRowErrors(sourceTable);
|
|
11744
13498
|
if (columns.length !== stmt.fields.length) {
|
|
11745
13499
|
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" : "";
|
|
11746
13500
|
throw new Error(
|
|
@@ -11754,18 +13508,29 @@ async function executeUpsertSelect(stmt, client, options, cacheContext, cteCache
|
|
|
11754
13508
|
}
|
|
11755
13509
|
const toInsert = [];
|
|
11756
13510
|
const toUpdate = [];
|
|
11757
|
-
const
|
|
13511
|
+
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
13512
|
+
const records = rows.map((row, rowIndex) => {
|
|
11758
13513
|
const record = {};
|
|
11759
13514
|
stmt.fields.forEach((field, i) => {
|
|
11760
|
-
|
|
13515
|
+
if (sourceTable.importPresence && !sourceTable.importPresence[rowIndex]?.has(field)) return;
|
|
13516
|
+
const raw = row[columns[i]] ?? "";
|
|
13517
|
+
record[field] = { value: convertProcessRowValue(raw, fieldTypes.get(field)) };
|
|
11761
13518
|
});
|
|
11762
13519
|
return record;
|
|
11763
13520
|
});
|
|
11764
|
-
assertValidDmlRecords(
|
|
11765
|
-
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
13521
|
+
records.forEach((record) => assertValidDmlRecords([record], stmt.fields.filter((field) => field in record), fieldInfos, numberPrecision));
|
|
11766
13522
|
const rowKeyValues = records.map(
|
|
11767
13523
|
(record) => stmt.keyFields.map((key) => String(record[key]?.value ?? ""))
|
|
11768
13524
|
);
|
|
13525
|
+
if (importSourceByDmlStatement.has(stmt)) {
|
|
13526
|
+
const numericKey = stmt.keyFields.map((key) => fieldTypes.get(key) === "NUMBER");
|
|
13527
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
13528
|
+
for (const parts of rowKeyValues) {
|
|
13529
|
+
const normalized = upsertNormalizedKey(parts, numericKey);
|
|
13530
|
+
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");
|
|
13531
|
+
sourceKeys.add(normalized);
|
|
13532
|
+
}
|
|
13533
|
+
}
|
|
11769
13534
|
const targetIndex = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeyValues, client, options, fieldTypes);
|
|
11770
13535
|
records.forEach((record, rowIdx) => {
|
|
11771
13536
|
const id = lookupUpsertTarget(targetIndex, rowKeyValues[rowIdx]);
|
|
@@ -11814,10 +13579,10 @@ async function executeDescribe(stmt, client, cacheContext) {
|
|
|
11814
13579
|
}));
|
|
11815
13580
|
return { type: "SELECT", rows, columns, rowCount: rows.length };
|
|
11816
13581
|
}
|
|
11817
|
-
function parseSql(sql) {
|
|
13582
|
+
function parseSql(sql, enableImport = false) {
|
|
11818
13583
|
try {
|
|
11819
13584
|
const tokens = new Lexer(sql).tokenize();
|
|
11820
|
-
const stmt = new Parser(tokens).parse();
|
|
13585
|
+
const stmt = new Parser(tokens, { import: enableImport }).parse();
|
|
11821
13586
|
validateKlikeStatement(stmt);
|
|
11822
13587
|
return stmt;
|
|
11823
13588
|
} catch (e) {
|
|
@@ -11993,43 +13758,51 @@ async function buildExplainWhereAnalysis(query, client, cacheContext, maxRecords
|
|
|
11993
13758
|
const validate = node;
|
|
11994
13759
|
fieldApps.add(validate.appId);
|
|
11995
13760
|
const fields = await getFieldsCached(validate.appId, tracedClient, cacheContext);
|
|
11996
|
-
const infoByCode = new Map(fields.map((field) => [field.code, field]));
|
|
13761
|
+
const infoByCode = new Map(fields.filter((field) => !field.inSubtable).map((field) => [field.code, field]));
|
|
13762
|
+
const childCodes = new Set(fields.filter((field) => field.inSubtable).map((field) => field.code));
|
|
11997
13763
|
const targets = resolveExistingValidationTargets(validate, fields);
|
|
11998
13764
|
const checks = collectCheckFieldRefs(validate.checkGroups ?? []);
|
|
11999
13765
|
const whereFields = collectValidateWhereFields(validate.where);
|
|
12000
13766
|
for (const ref of checks) {
|
|
12001
|
-
if (ref.field !== "$id" && !infoByCode.has(ref.field)) {
|
|
13767
|
+
if (ref.field !== "$id" && !infoByCode.has(ref.field) && !childCodes.has(ref.field)) {
|
|
12002
13768
|
throw customCheckParseError(`CHECK \u306E\u30D5\u30A3\u30FC\u30EB\u30C9 ${ref.field} \u306F APP${validate.appId} \u306B\u5B58\u5728\u3057\u307E\u305B\u3093`);
|
|
12003
13769
|
}
|
|
13770
|
+
if (ref.field !== "$id" && !infoByCode.has(ref.field) && childCodes.has(ref.field)) {
|
|
13771
|
+
throw new Error(`ArgumentError: VALIDATE \u306E CHECK \u3067\u306F\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5B50\u30D5\u30A3\u30FC\u30EB\u30C9 ${ref.field} \u3092\u53C2\u7167\u3067\u304D\u307E\u305B\u3093\u3002`);
|
|
13772
|
+
}
|
|
12004
13773
|
}
|
|
12005
13774
|
for (const field of whereFields) {
|
|
12006
|
-
if (field !== "$id" && !infoByCode.has(field)) {
|
|
13775
|
+
if (field !== "$id" && !infoByCode.has(field) && !childCodes.has(field)) {
|
|
12007
13776
|
throw new Error(`ArgumentError: WHERE field ${field} does not exist in APP${validate.appId}.`);
|
|
12008
13777
|
}
|
|
13778
|
+
if (field !== "$id" && !infoByCode.has(field) && childCodes.has(field)) {
|
|
13779
|
+
throw new Error(`ArgumentError: VALIDATE \u306E WHERE \u3067\u306F\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5B50\u30D5\u30A3\u30FC\u30EB\u30C9 ${field} \u3092\u53C2\u7167\u3067\u304D\u307E\u305B\u3093\u3002`);
|
|
13780
|
+
}
|
|
12009
13781
|
}
|
|
12010
|
-
const types = new Map(
|
|
13782
|
+
const types = new Map([...infoByCode].map(([code, field]) => [code, field.fieldType]));
|
|
12011
13783
|
types.set("$id", "RECORD_NUMBER");
|
|
12012
13784
|
assertCheckComparisonTypes(validate, types);
|
|
12013
13785
|
const capability = classifyWhereCapability(validate.where, (field) => field.field === "$id" ? resolveFieldSemantics({ fieldType: "__ID__" }) : infoByCode.get(field.field)?.semantics ?? (infoByCode.has(field.field) ? resolveFieldSemantics(infoByCode.get(field.field)) : void 0));
|
|
12014
13786
|
if (capability.capability === "UNSUPPORTED") {
|
|
12015
13787
|
throw new Error(`ArgumentError: WHERE predicate is unsupported (${formatWhereCapabilityFailure(capability)}).`);
|
|
12016
13788
|
}
|
|
12017
|
-
const fieldTypes = new Map(
|
|
12018
|
-
const fieldOptions = new Map(
|
|
13789
|
+
const fieldTypes = new Map([...infoByCode].map(([code, field]) => [code, field.fieldType]));
|
|
13790
|
+
const fieldOptions = new Map([...infoByCode.values()].flatMap((field) => field.optionOrder ? [[field.code, new Set(Object.keys(field.optionOrder))]] : []));
|
|
12019
13791
|
const prefilter = validate.where === null ? null : capability.capability === "EXACT_PUSHDOWN" ? validate.where : extractSafePushdownLeaves(validate.where, {
|
|
12020
13792
|
allowUnqualifiedFields: true,
|
|
12021
13793
|
fieldTypes,
|
|
12022
13794
|
fieldOptions,
|
|
12023
13795
|
allowKlike: false
|
|
12024
13796
|
});
|
|
12025
|
-
const needsPrecision = targets.some((
|
|
13797
|
+
const needsPrecision = targets.some((target) => target.field.fieldType === "NUMBER");
|
|
12026
13798
|
if (needsPrecision) {
|
|
12027
13799
|
numberPrecisionApps.add(validate.appId);
|
|
12028
13800
|
await getNumberPrecisionCached(validate.appId, tracedClient, cacheContext);
|
|
12029
13801
|
}
|
|
12030
13802
|
validateExplainInfo.set(validate, {
|
|
12031
|
-
targetFields: targets.map((
|
|
12032
|
-
fetchFields: [.../* @__PURE__ */ new Set(["$id", ...targets.map((
|
|
13803
|
+
targetFields: targets.map((target) => target.subtableCode ? `${target.subtableCode}(${target.field.code})` : target.field.code),
|
|
13804
|
+
fetchFields: [.../* @__PURE__ */ new Set(["$id", ...targets.map((target) => target.subtableCode ?? target.field.code), ...whereFields, ...checks.map((ref) => ref.field)])],
|
|
13805
|
+
subtables: new Map([...new Set(targets.flatMap((target) => target.subtableCode ? [target.subtableCode] : []))].map((table) => [table, targets.filter((target) => target.subtableCode === table).length])),
|
|
12033
13806
|
capability,
|
|
12034
13807
|
prefilter,
|
|
12035
13808
|
numberPrecision: needsPrecision
|
|
@@ -12073,8 +13846,8 @@ function explainMetadataLines(analysis) {
|
|
|
12073
13846
|
...[...analysis.numberPrecisionApps].sort((a, b) => a - b).map((appId) => ` metadata API: number precision APP${appId}`)
|
|
12074
13847
|
];
|
|
12075
13848
|
}
|
|
12076
|
-
async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4, cursorMaxActive = 2) {
|
|
12077
|
-
const statements = parseSqlBatch(sql);
|
|
13849
|
+
async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4, cursorMaxActive = 2, enableImport = false) {
|
|
13850
|
+
const statements = parseSqlBatch(sql, enableImport);
|
|
12078
13851
|
const analysis = analyzeBatch(statements);
|
|
12079
13852
|
validateDeclaredBatchVariables(statements, injectedVariables);
|
|
12080
13853
|
const variables = /* @__PURE__ */ new Map();
|
|
@@ -12233,6 +14006,79 @@ function buildExplainPlan(query, label, capabilities, orderPlans) {
|
|
|
12233
14006
|
if (query.type === "DELETE") return buildDeletePlan(query, label);
|
|
12234
14007
|
if (query.type === "REORDER") return buildReorderPlan(query, label);
|
|
12235
14008
|
if (query.type === "VALIDATE") return buildValidatePlan(query, label);
|
|
14009
|
+
if (query.type === "IMPORT") {
|
|
14010
|
+
if (query.writeMode === "UPDATE_RECORD_NUMBER") {
|
|
14011
|
+
const csvTables = query.targets?.filter((target) => target.kind === "SUBTABLE") ?? [];
|
|
14012
|
+
return [
|
|
14013
|
+
...label ? [label] : [],
|
|
14014
|
+
`IMPORT UPDATE INTO APP${query.appId}`,
|
|
14015
|
+
` writeMode: UPDATE_RECORD_NUMBER`,
|
|
14016
|
+
` source: CSV ${query.source.sourceName}`,
|
|
14017
|
+
` keyHeader: ${query.recordNumberSourceHeader}`,
|
|
14018
|
+
` mapping: BY_NAME`,
|
|
14019
|
+
` parentRows: requires source load`,
|
|
14020
|
+
` duplicate: preflight before lookup/write`,
|
|
14021
|
+
` matched: requires lookup`,
|
|
14022
|
+
` unmatched: requires lookup`,
|
|
14023
|
+
` invalid: requires source load`,
|
|
14024
|
+
` requiresLookup:true`,
|
|
14025
|
+
` inserted: 0`,
|
|
14026
|
+
` keyInPayload: false`,
|
|
14027
|
+
...csvTables.length ? [
|
|
14028
|
+
` replaceSubtables: ${query.replaceSubtables?.join(", ") ?? "ERROR: required"}`,
|
|
14029
|
+
` subtableRowIdPolicy: PRESERVE existing; empty/unknown add without id`,
|
|
14030
|
+
` rowIdOwnership: owned elsewhere invalidates the parent`,
|
|
14031
|
+
` replacementDiff: existing/input/update/add/delete/rowIdNotFound requires actual-data preflight`,
|
|
14032
|
+
` confirmPolicy: highest warning "\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5168\u7F6E\u63DB\u30FBN\u884C\u524A\u9664" plus per-table detail (including delete=0)`
|
|
14033
|
+
] : [],
|
|
14034
|
+
` disposition: ${query.validateOnly ? "VALIDATE ONLY" : query.onErrorSkip ? `ON ERROR SKIP INTO ${query.errorTable}` : "fail-fast"}`,
|
|
14035
|
+
` gate: enabled for this parse`,
|
|
14036
|
+
` writesKintone: ${query.validateOnly ? "false" : "true"}`
|
|
14037
|
+
];
|
|
14038
|
+
}
|
|
14039
|
+
const mode = query.keyFields ? "UPSERT" : "INSERT";
|
|
14040
|
+
const hasSubtables = query.targets?.some((target) => target.kind === "SUBTABLE") === true;
|
|
14041
|
+
return [
|
|
14042
|
+
...label ? [label] : [],
|
|
14043
|
+
`IMPORT ${mode} INTO APP${query.appId}`,
|
|
14044
|
+
` source: ${query.source.kind} ${query.source.sourceName}`,
|
|
14045
|
+
` sourceFormat: ${query.source.kind}`,
|
|
14046
|
+
` encoding: ${query.source.kind === "JSON" ? "UTF8 only" : query.source.encoding ?? "UTF8 (or loader metadata)"}`,
|
|
14047
|
+
` mapping: ${query.source.kind === "JSON" ? "BY NAME (INTO order)" : query.source.projection ? "SELECT expressions" : query.source.mappingMode}`,
|
|
14048
|
+
...query.source.kind === "JSON" ? [
|
|
14049
|
+
` duplicateKeyPolicy: reject`,
|
|
14050
|
+
` numberLexemePolicy: preserve; JSON number accepts safe integer only`,
|
|
14051
|
+
` precisionTargetsRequireString: true`,
|
|
14052
|
+
` unknownKeyPolicy: reject`,
|
|
14053
|
+
` presenceAware: true`,
|
|
14054
|
+
...hasSubtables ? [
|
|
14055
|
+
` subtableRowIdPolicy: reject _rid/id; DROP IDs and renumber every input row`,
|
|
14056
|
+
` subtableUpdatePolicy: present table replaces all rows; missing table is preserved; [] deletes all rows`,
|
|
14057
|
+
` confirmPolicy: parent/table existing/input/add/delete detail required; delete is highest warning`
|
|
14058
|
+
] : []
|
|
14059
|
+
] : [
|
|
14060
|
+
` header: ${query.source.hasHeader ? "HEADER" : "NO HEADER"}`,
|
|
14061
|
+
...query.source.mappingMode === "BY_NAME" ? [
|
|
14062
|
+
` writtenColumns: ${query.fields.join(", ")}`,
|
|
14063
|
+
` knownExportColumns: audit and ignore with reason/non-empty count`,
|
|
14064
|
+
` unknownColumnPolicy: ${query.source.ignoreUnknownColumns ? "ignore with audit/non-empty count" : "ERR_IMPORT_UNKNOWN_COLUMN"}`,
|
|
14065
|
+
` multipleValueDelimiter: LF (CRLF or LF)`,
|
|
14066
|
+
` sourceValueMode: string-preserving`,
|
|
14067
|
+
` roundTripNumericGuarantee: exact CSV lexeme passes strict decimal validation`,
|
|
14068
|
+
` FILE: audit-ignore unless named in INTO (analyze error)`
|
|
14069
|
+
] : []
|
|
14070
|
+
],
|
|
14071
|
+
` sourceLimit: 10485760 bytes / ${query.fields.length} target columns`,
|
|
14072
|
+
` key: ${query.keyFields?.join(", ") ?? "none"}`,
|
|
14073
|
+
` checks: ${query.checkGroups?.length ?? 0}`,
|
|
14074
|
+
` disposition: ${query.validateOnly ? "VALIDATE ONLY" : query.onErrorSkip ? `ON ERROR SKIP INTO ${query.errorTable}` : "fail-fast"}`,
|
|
14075
|
+
` gate: enabled for this parse`,
|
|
14076
|
+
` preflight: ${query.validateOnly && hasSubtables ? "requires actual source load at execution; this EXPLAIN is static" : "requires load"}`,
|
|
14077
|
+
...hasSubtables ? [query.source.kind === "JSON" ? ` Phase5C: JSON mutation requires detail-capable confirmation surface` : ` Phase5D: CSV mutation requires detail-capable confirmation surface`] : [],
|
|
14078
|
+
` writesKintone: ${query.validateOnly ? "false" : "true"}`,
|
|
14079
|
+
` duplicateKey: preflight before lookup/write (requires load)`
|
|
14080
|
+
];
|
|
14081
|
+
}
|
|
12236
14082
|
return buildSelectPlan(query, label, capabilities, orderPlans);
|
|
12237
14083
|
}
|
|
12238
14084
|
function buildValidatePlan(stmt, label) {
|
|
@@ -12251,6 +14097,10 @@ function buildValidatePlan(stmt, label) {
|
|
|
12251
14097
|
lines.push(` kintone query: ${info.prefilter === null ? "(\u5168\u4EF6\u53D6\u5F97)" : whereToKintone(info.prefilter)}`);
|
|
12252
14098
|
lines.push(` audit fields: ${info.targetFields.length === 0 ? "(\u306A\u3057)" : info.targetFields.join(", ")}`);
|
|
12253
14099
|
lines.push(` fetch fields: ${info.fetchFields.join(", ")}`);
|
|
14100
|
+
lines.push(` mode: ${stmt.summary ? "SUMMARY" : "DETAIL"}`);
|
|
14101
|
+
if (info.subtables.size > 0) lines.push(` subtable audit: ${[...info.subtables].map(([table, count]) => `${table}(${count} fields)`).join(", ")}`);
|
|
14102
|
+
lines.push(` output schema: ${(stmt.summary ? EXISTING_VALIDATION_SUMMARY_COLUMNS : EXISTING_VALIDATION_COLUMNS).join(", ")}`);
|
|
14103
|
+
lines.push(stmt.summary ? " aggregation: record/subtable/field/code; row locator=none" : " row locator: grouped by message; $err_subrow / $err_subrow_id list all matching rows (first-occurrence order)");
|
|
12254
14104
|
lines.push(` number precision: ${info.numberPrecision ? "required" : "not required"}`);
|
|
12255
14105
|
lines.push(" local checks: original WHERE re-evaluation + built-in constraints + CHECK groups");
|
|
12256
14106
|
lines.push(" records/mutation API during EXPLAIN: none; violation count unavailable");
|
|
@@ -12686,15 +14536,15 @@ var OperationCancelledError = class extends Error {
|
|
|
12686
14536
|
};
|
|
12687
14537
|
|
|
12688
14538
|
// src/core/sql.ts
|
|
12689
|
-
function parseSqlStatement(sql) {
|
|
14539
|
+
function parseSqlStatement(sql, capabilities = {}) {
|
|
12690
14540
|
const tokens = new Lexer(sql).tokenize();
|
|
12691
|
-
const stmt = new Parser(tokens).parse();
|
|
14541
|
+
const stmt = new Parser(tokens, capabilities).parse();
|
|
12692
14542
|
validateKlikeStatement(stmt);
|
|
12693
14543
|
return stmt;
|
|
12694
14544
|
}
|
|
12695
|
-
function parseSqlStatements(sql) {
|
|
14545
|
+
function parseSqlStatements(sql, capabilities = {}) {
|
|
12696
14546
|
const tokens = new Lexer(sql).tokenize();
|
|
12697
|
-
const statements = new Parser(tokens).parseStatements();
|
|
14547
|
+
const statements = new Parser(tokens, capabilities).parseStatements();
|
|
12698
14548
|
statements.forEach(validateKlikeStatement);
|
|
12699
14549
|
return statements;
|
|
12700
14550
|
}
|
|
@@ -12842,7 +14692,8 @@ function buildBatchEnvelope(batch, options = {}) {
|
|
|
12842
14692
|
columns: s.result.columns,
|
|
12843
14693
|
rows: s.result.rows,
|
|
12844
14694
|
rowCount: s.result.rowCount,
|
|
12845
|
-
warnings: s.result.warnings ?? []
|
|
14695
|
+
warnings: s.result.warnings ?? [],
|
|
14696
|
+
...s.result.validateStats ? { validateStats: s.result.validateStats } : {}
|
|
12846
14697
|
});
|
|
12847
14698
|
} else if (s.result?.type === "VALIDATION") {
|
|
12848
14699
|
totalRows += s.result.errorCount;
|
|
@@ -12861,7 +14712,8 @@ function buildBatchEnvelope(batch, options = {}) {
|
|
|
12861
14712
|
validRows: s.result.validRows,
|
|
12862
14713
|
invalidRows: s.result.invalidRows,
|
|
12863
14714
|
errorCount: s.result.errorCount,
|
|
12864
|
-
...s.result.errTable ? { errTable: s.result.errTable } : {}
|
|
14715
|
+
...s.result.errTable ? { errTable: s.result.errTable } : {},
|
|
14716
|
+
...s.result.importDetail ? { importDetail: s.result.importDetail } : {}
|
|
12865
14717
|
});
|
|
12866
14718
|
} else if (s.status === "success" && s.result && s.result.type !== "SELECT" && s.result.type !== "ASSERT") {
|
|
12867
14719
|
Object.assign(entry, toMutationSummary(s.result));
|
|
@@ -13164,7 +15016,7 @@ function clampInt(v, min, max) {
|
|
|
13164
15016
|
function flattenFormFieldProperties(properties) {
|
|
13165
15017
|
return flattenFields(properties, collectLookupCopyFields(properties));
|
|
13166
15018
|
}
|
|
13167
|
-
function flattenFields(properties, lookupCopyFields, inSubtable = false) {
|
|
15019
|
+
function flattenFields(properties, lookupCopyFields, inSubtable = false, subtableCode) {
|
|
13168
15020
|
const out = [];
|
|
13169
15021
|
for (const field of Object.values(properties)) {
|
|
13170
15022
|
const optionOrder = toOptionOrderMap(field.options);
|
|
@@ -13182,11 +15034,12 @@ function flattenFields(properties, lookupCopyFields, inSubtable = false) {
|
|
|
13182
15034
|
maxLength: normalizeConstraintValue(field.maxLength),
|
|
13183
15035
|
defaultValue: field.defaultValue,
|
|
13184
15036
|
inSubtable,
|
|
15037
|
+
...subtableCode ? { subtableCode } : {},
|
|
13185
15038
|
writable: !lookupCopyFields.has(field.code) && !NON_WRITABLE_FIELD_TYPES2.has(field.type)
|
|
13186
15039
|
};
|
|
13187
15040
|
info.semantics = resolveFieldSemantics(info);
|
|
13188
15041
|
out.push(info);
|
|
13189
|
-
if (field.fields) out.push(...flattenFields(field.fields, lookupCopyFields, true));
|
|
15042
|
+
if (field.fields) out.push(...flattenFields(field.fields, lookupCopyFields, true, field.type === "SUBTABLE" ? field.code : subtableCode));
|
|
13190
15043
|
}
|
|
13191
15044
|
return out;
|
|
13192
15045
|
}
|
|
@@ -14170,6 +16023,17 @@ function restoreSqlContextError(err, sourceSql, context) {
|
|
|
14170
16023
|
return err;
|
|
14171
16024
|
}
|
|
14172
16025
|
|
|
16026
|
+
// src/import/importGateError.ts
|
|
16027
|
+
var IMPORT_CAPABILITY_GATE_MARKER = "capability is disabled";
|
|
16028
|
+
function errorMessage(error) {
|
|
16029
|
+
if (error instanceof Error) return error.message;
|
|
16030
|
+
if (typeof error === "string") return error;
|
|
16031
|
+
return null;
|
|
16032
|
+
}
|
|
16033
|
+
function isImportCapabilityGateError(error) {
|
|
16034
|
+
return errorMessage(error)?.includes(IMPORT_CAPABILITY_GATE_MARKER) === true;
|
|
16035
|
+
}
|
|
16036
|
+
|
|
14173
16037
|
// src/cli/index.ts
|
|
14174
16038
|
var HELP_TEXT = `ksql - Execute SQL against kintone apps
|
|
14175
16039
|
|
|
@@ -14184,6 +16048,8 @@ Options:
|
|
|
14184
16048
|
--console Start interactive console mode
|
|
14185
16049
|
--dry-run Parse and show execution plan only
|
|
14186
16050
|
--var <name=value> Override a DECLARE variable (repeatable; not for secrets)
|
|
16051
|
+
--import-csv <name=path> Supply named CSV and enable IMPORT (repeatable)
|
|
16052
|
+
--import-json <name=path> Supply named JSON and enable IMPORT (repeatable)
|
|
14187
16053
|
--format <type> Output format: table | json | jsonl | csv | markdown | md
|
|
14188
16054
|
(batch + json: prints one JSON envelope for the whole batch)
|
|
14189
16055
|
--max-records <n> Max records to fetch (default: 500)
|
|
@@ -14231,6 +16097,15 @@ Options:
|
|
|
14231
16097
|
-h, --help Show help
|
|
14232
16098
|
-v, --version Show version
|
|
14233
16099
|
`;
|
|
16100
|
+
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";
|
|
16101
|
+
function toCliImportError(error, importEnabled) {
|
|
16102
|
+
if (importEnabled || !isImportCapabilityGateError(error)) return error;
|
|
16103
|
+
if (error instanceof Error) {
|
|
16104
|
+
error.message = CLI_IMPORT_SOURCE_REQUIRED_MESSAGE;
|
|
16105
|
+
return error;
|
|
16106
|
+
}
|
|
16107
|
+
return CLI_IMPORT_SOURCE_REQUIRED_MESSAGE;
|
|
16108
|
+
}
|
|
14234
16109
|
function parseArgs(argv) {
|
|
14235
16110
|
const out = {
|
|
14236
16111
|
help: false,
|
|
@@ -14282,7 +16157,9 @@ function parseArgs(argv) {
|
|
|
14282
16157
|
arrayFormat: null,
|
|
14283
16158
|
tableFormat: null,
|
|
14284
16159
|
dateFormat: null,
|
|
14285
|
-
attachmentFormat: null
|
|
16160
|
+
attachmentFormat: null,
|
|
16161
|
+
importCsv: /* @__PURE__ */ Object.create(null),
|
|
16162
|
+
importJson: /* @__PURE__ */ Object.create(null)
|
|
14286
16163
|
};
|
|
14287
16164
|
for (let i = 0; i < argv.length; i++) {
|
|
14288
16165
|
const a = argv[i];
|
|
@@ -14364,6 +16241,28 @@ function parseArgs(argv) {
|
|
|
14364
16241
|
i++;
|
|
14365
16242
|
continue;
|
|
14366
16243
|
}
|
|
16244
|
+
if (a === "--import-csv") {
|
|
16245
|
+
const raw = v ?? "";
|
|
16246
|
+
const eq = raw.indexOf("=");
|
|
16247
|
+
if (eq <= 0 || eq === raw.length - 1) throw new Error("ArgumentError: --import-csv must use name=path.");
|
|
16248
|
+
const name = raw.slice(0, eq);
|
|
16249
|
+
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.`);
|
|
16250
|
+
out.importCsv[name] = raw.slice(eq + 1);
|
|
16251
|
+
i++;
|
|
16252
|
+
continue;
|
|
16253
|
+
}
|
|
16254
|
+
if (a === "--import-json") {
|
|
16255
|
+
const raw = v ?? "";
|
|
16256
|
+
const eq = raw.indexOf("=");
|
|
16257
|
+
if (eq <= 0 || eq === raw.length - 1) throw new Error("ArgumentError: --import-json must use name=path.");
|
|
16258
|
+
const name = raw.slice(0, eq);
|
|
16259
|
+
if (Object.prototype.hasOwnProperty.call(out.importJson, name) || Object.prototype.hasOwnProperty.call(out.importCsv, name)) {
|
|
16260
|
+
throw new Error(`ArgumentError: import source "${name}" is specified more than once.`);
|
|
16261
|
+
}
|
|
16262
|
+
out.importJson[name] = raw.slice(eq + 1);
|
|
16263
|
+
i++;
|
|
16264
|
+
continue;
|
|
16265
|
+
}
|
|
14367
16266
|
if (a === "-e" || a === "--execute") {
|
|
14368
16267
|
out.executeSql = v ?? "";
|
|
14369
16268
|
i++;
|
|
@@ -14713,7 +16612,8 @@ function buildOutput(result, format, noHeader, pretty, display) {
|
|
|
14713
16612
|
columns: result.columns,
|
|
14714
16613
|
rowCount: result.rowCount,
|
|
14715
16614
|
warnings: result.warnings ?? [],
|
|
14716
|
-
rows: result.rows
|
|
16615
|
+
rows: result.rows,
|
|
16616
|
+
...result.validateStats ? { validateStats: result.validateStats } : {}
|
|
14717
16617
|
};
|
|
14718
16618
|
return JSON.stringify(obj, null, pretty ? 2 : 0);
|
|
14719
16619
|
}
|
|
@@ -14768,7 +16668,12 @@ function buildBatchStatementSummary(s) {
|
|
|
14768
16668
|
const parts = [`[${s.index + 1}] ${s.type}`, s.status];
|
|
14769
16669
|
if (s.tempTable) parts.push(`temp=${s.tempTable}`);
|
|
14770
16670
|
if (s.rowCount !== void 0) parts.push(`rows=${s.rowCount}`);
|
|
14771
|
-
if (s.status === "success" && s.result?.type === "SELECT")
|
|
16671
|
+
if (s.status === "success" && s.result?.type === "SELECT") {
|
|
16672
|
+
parts.push(`rowCount=${s.result.rowCount}`);
|
|
16673
|
+
if (s.result.validateStats) {
|
|
16674
|
+
parts.push(`errorRecords=${s.result.validateStats.errorRecords} errorCount=${s.result.validateStats.errorCount}`);
|
|
16675
|
+
}
|
|
16676
|
+
}
|
|
14772
16677
|
if (s.status === "success" && s.result && s.result.type !== "SELECT") {
|
|
14773
16678
|
const r = s.result;
|
|
14774
16679
|
if (r.type === "INSERT") parts.push(`inserted=${r.insertedCount}`);
|
|
@@ -14789,6 +16694,10 @@ function buildBatchStatementSummary(s) {
|
|
|
14789
16694
|
if (s.status === "skipped" && s.skippedReason) parts.push(`reason=${s.skippedReason}`);
|
|
14790
16695
|
return parts.join(" ");
|
|
14791
16696
|
}
|
|
16697
|
+
function buildSelectSummary(result) {
|
|
16698
|
+
const validateSummary = result.validateStats ? ` errorRecords=${result.validateStats.errorRecords} errorCount=${result.validateStats.errorCount}` : "";
|
|
16699
|
+
return `rowCount=${result.rowCount}${validateSummary}`;
|
|
16700
|
+
}
|
|
14792
16701
|
function buildBatchDmlConfirmMessage(analysis) {
|
|
14793
16702
|
const lines = ["[DML Confirm] batch"];
|
|
14794
16703
|
for (const s of analysis.statements) {
|
|
@@ -15511,15 +17420,16 @@ async function run() {
|
|
|
15511
17420
|
`);
|
|
15512
17421
|
return 2;
|
|
15513
17422
|
}
|
|
17423
|
+
const importEnabled = Object.keys(args.importCsv).length > 0 || Object.keys(args.importJson).length > 0;
|
|
15514
17424
|
try {
|
|
15515
|
-
const statements = parseSqlStatements(sql);
|
|
17425
|
+
const statements = parseSqlStatements(sql, { import: importEnabled });
|
|
15516
17426
|
dryRunNeedsMetadata = statements.some(explainNeedsAppMetadata);
|
|
15517
17427
|
if (statements.length > 1) {
|
|
15518
17428
|
batchAnalysis = analyzeBatch(statements);
|
|
15519
17429
|
isBatchSql = true;
|
|
15520
17430
|
batchContainsDml = batchAnalysis.containsDml;
|
|
15521
17431
|
} else {
|
|
15522
|
-
const stmt = parseSqlStatement(sql);
|
|
17432
|
+
const stmt = parseSqlStatement(sql, { import: importEnabled });
|
|
15523
17433
|
parsedStmt = stmt;
|
|
15524
17434
|
stmtType = getStatementType(stmt);
|
|
15525
17435
|
isDmlStatement = writesKintone(stmt);
|
|
@@ -15537,7 +17447,8 @@ async function run() {
|
|
|
15537
17447
|
bindings: sqlDiagnosticContext.appBindingByMappedApp,
|
|
15538
17448
|
rewriteSegments: sqlDiagnosticContext.rewriteSegments
|
|
15539
17449
|
}) : err;
|
|
15540
|
-
|
|
17450
|
+
const surfaced = toCliImportError(restored, importEnabled);
|
|
17451
|
+
process.stderr.write(`${surfaced instanceof Error ? surfaced.message : String(surfaced)}
|
|
15541
17452
|
`);
|
|
15542
17453
|
return 1;
|
|
15543
17454
|
}
|
|
@@ -15894,7 +17805,8 @@ async function run() {
|
|
|
15894
17805
|
args.variables,
|
|
15895
17806
|
cacheContext,
|
|
15896
17807
|
maxRecords,
|
|
15897
|
-
cursorMaxActive
|
|
17808
|
+
cursorMaxActive,
|
|
17809
|
+
Object.keys(args.importCsv).length > 0 || Object.keys(args.importJson).length > 0
|
|
15898
17810
|
);
|
|
15899
17811
|
const out = [];
|
|
15900
17812
|
const restoredStatements = sqlDiagnosticContext ? restoreSqlDiagnosticValue(plans.statements, sqlDiagnosticContext.appBindingByMappedApp) : plans.statements;
|
|
@@ -15933,10 +17845,30 @@ async function run() {
|
|
|
15933
17845
|
}
|
|
15934
17846
|
}
|
|
15935
17847
|
}
|
|
15936
|
-
const
|
|
17848
|
+
const importEnabled = Object.keys(args.importCsv).length > 0 || Object.keys(args.importJson).length > 0;
|
|
17849
|
+
const importSource = importEnabled ? (name) => {
|
|
17850
|
+
const sourcePath = args.importCsv[name] ?? args.importJson[name];
|
|
17851
|
+
return sourcePath === void 0 ? void 0 : { load: async () => ({ bytes: new Uint8Array((0, import_fs2.readFileSync)(sourcePath)) }) };
|
|
17852
|
+
} : void 0;
|
|
17853
|
+
const confirm = async (count, operation, context) => {
|
|
15937
17854
|
if (count > dmlMaxRows) {
|
|
15938
17855
|
throw new Error(`ArgumentError: ${operation} affected rows (${count}) exceed --dml-max-rows (${dmlMaxRows}).`);
|
|
15939
17856
|
}
|
|
17857
|
+
if (context?.importDetail) {
|
|
17858
|
+
const detail = context.importDetail;
|
|
17859
|
+
const csv = detail.kind === "IMPORT_CSV_SUBTABLE_REPLACE";
|
|
17860
|
+
const lines = [
|
|
17861
|
+
...csv ? [`\u3010\u6700\u91CD\u8981\u8B66\u544A\u3011\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5168\u7F6E\u63DB\u30FB${detail.totalDeleteRows}\u884C\u524A\u9664`] : [],
|
|
17862
|
+
`[IMPORT ${csv ? "CSV" : "JSON"} Confirm] parentsToWrite=${detail.parentsToWrite} insert=${detail.insertedParents} update=${detail.updatedParents}`,
|
|
17863
|
+
csv ? `rowIdPolicy=PRESERVE_EXISTING rowIdNotFound=${detail.rowIdNotFound} invalidParents=${detail.invalidParents}` : `rowIdPolicy=DROP_AND_RENUMBER_ALL (JSON child rows are all newly numbered)`,
|
|
17864
|
+
...!csv && detail.hasDeletes ? ["WARNING: existing subtable rows will be deleted/replaced."] : [],
|
|
17865
|
+
...detail.parents.flatMap((parent) => parent.tables.map(
|
|
17866
|
+
(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}` : ""}`
|
|
17867
|
+
))
|
|
17868
|
+
];
|
|
17869
|
+
process.stderr.write(`${lines.join("\n")}
|
|
17870
|
+
`);
|
|
17871
|
+
}
|
|
15940
17872
|
if (yes) return true;
|
|
15941
17873
|
if (args.console) return true;
|
|
15942
17874
|
const label = sql?.replace(/\s+/g, " ").trim() ?? operation;
|
|
@@ -15961,10 +17893,20 @@ query=${label}`);
|
|
|
15961
17893
|
timeoutMs: timeout,
|
|
15962
17894
|
cursorMaxActive,
|
|
15963
17895
|
variables: args.variables,
|
|
15964
|
-
|
|
17896
|
+
enableImport: importEnabled,
|
|
17897
|
+
importSource,
|
|
17898
|
+
supportsImportConfirmDetail: true,
|
|
17899
|
+
confirm: batchContainsDml ? async (count, operation, context) => {
|
|
15965
17900
|
if (count > dmlMaxRows) {
|
|
15966
17901
|
throw new Error(`ArgumentError: ${operation} affected rows (${count}) exceed --dml-max-rows (${dmlMaxRows}).`);
|
|
15967
17902
|
}
|
|
17903
|
+
if (context?.importDetail) {
|
|
17904
|
+
const importDetail = context.importDetail;
|
|
17905
|
+
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
|
|
17906
|
+
`);
|
|
17907
|
+
process.stderr.write(`[IMPORT ${importDetail.kind === "IMPORT_CSV_SUBTABLE_REPLACE" ? "CSV" : "JSON"} Confirm] ${JSON.stringify(importDetail)}
|
|
17908
|
+
`);
|
|
17909
|
+
}
|
|
15968
17910
|
return true;
|
|
15969
17911
|
} : void 0
|
|
15970
17912
|
});
|
|
@@ -15974,14 +17916,19 @@ query=${label}`);
|
|
|
15974
17916
|
maxRecords,
|
|
15975
17917
|
onLimitReached: onLimit,
|
|
15976
17918
|
cacheContext,
|
|
15977
|
-
cursorMaxActive
|
|
17919
|
+
cursorMaxActive,
|
|
17920
|
+
enableImport: importEnabled,
|
|
17921
|
+
importSource
|
|
15978
17922
|
}) : await execute(sql, client, {
|
|
15979
17923
|
maxRecords,
|
|
15980
17924
|
fetchParallel,
|
|
15981
17925
|
onLimitReached: effectiveOnLimit,
|
|
15982
17926
|
confirm: isDmlStatement ? confirm : void 0,
|
|
15983
17927
|
cacheContext,
|
|
15984
|
-
cursorMaxActive
|
|
17928
|
+
cursorMaxActive,
|
|
17929
|
+
enableImport: importEnabled,
|
|
17930
|
+
importSource,
|
|
17931
|
+
supportsImportConfirmDetail: true
|
|
15985
17932
|
});
|
|
15986
17933
|
if (args.dryRun && sqlDiagnosticContext) {
|
|
15987
17934
|
result = restoreSqlDiagnosticValue(result, sqlDiagnosticContext.appBindingByMappedApp);
|
|
@@ -16019,8 +17966,10 @@ query=${label}`);
|
|
|
16019
17966
|
`, "utf-8");
|
|
16020
17967
|
else if (output) process.stdout.write(`${output}
|
|
16021
17968
|
`);
|
|
16022
|
-
if (!quiet)
|
|
17969
|
+
if (!quiet) {
|
|
17970
|
+
process.stderr.write(`${buildSelectSummary(result)}
|
|
16023
17971
|
`);
|
|
17972
|
+
}
|
|
16024
17973
|
if (shouldExitOnEmpty(args.dryRun, exitOnEmpty, result.rowCount)) return 1;
|
|
16025
17974
|
return 0;
|
|
16026
17975
|
} catch (err) {
|
|
@@ -16049,11 +17998,13 @@ if (isDirectCliRun()) {
|
|
|
16049
17998
|
}
|
|
16050
17999
|
// Annotate the CommonJS export names for ESM import in node:
|
|
16051
18000
|
0 && (module.exports = {
|
|
18001
|
+
CLI_IMPORT_SOURCE_REQUIRED_MESSAGE,
|
|
16052
18002
|
HELP_TEXT,
|
|
16053
18003
|
buildBatchDmlConfirmMessage,
|
|
16054
18004
|
buildBatchStatementSummary,
|
|
16055
18005
|
buildOutput,
|
|
16056
18006
|
buildReplExecArgv,
|
|
18007
|
+
buildSelectSummary,
|
|
16057
18008
|
buildValidationOutput,
|
|
16058
18009
|
extractAppIds,
|
|
16059
18010
|
normalizeAppKey,
|
|
@@ -16065,5 +18016,6 @@ if (isDirectCliRun()) {
|
|
|
16065
18016
|
parseTokenMap,
|
|
16066
18017
|
runWithArgv,
|
|
16067
18018
|
shouldExitOnEmpty,
|
|
18019
|
+
toCliImportError,
|
|
16068
18020
|
writeBatchOutput
|
|
16069
18021
|
});
|