@rex0220/kintone-sql-tools 3.5.0 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist-cli/ksql.js +1822 -54
- package/dist-mcp/ksql-mcp.js +1859 -97
- package/dist-mcpb/ksql-mcp.mcpb +0 -0
- package/package.json +1 -1
package/dist-cli/ksql.js
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
// src/cli/index.ts
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
|
+
CLI_IMPORT_SOURCE_REQUIRED_MESSAGE: () => CLI_IMPORT_SOURCE_REQUIRED_MESSAGE,
|
|
24
25
|
HELP_TEXT: () => HELP_TEXT,
|
|
25
26
|
buildBatchDmlConfirmMessage: () => buildBatchDmlConfirmMessage,
|
|
26
27
|
buildBatchStatementSummary: () => buildBatchStatementSummary,
|
|
@@ -37,6 +38,7 @@ __export(index_exports, {
|
|
|
37
38
|
parseTokenMap: () => parseTokenMap,
|
|
38
39
|
runWithArgv: () => runWithArgv,
|
|
39
40
|
shouldExitOnEmpty: () => shouldExitOnEmpty,
|
|
41
|
+
toCliImportError: () => toCliImportError,
|
|
40
42
|
writeBatchOutput: () => writeBatchOutput
|
|
41
43
|
});
|
|
42
44
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -651,8 +653,9 @@ var ParseError = class extends Error {
|
|
|
651
653
|
}
|
|
652
654
|
};
|
|
653
655
|
var Parser = class {
|
|
654
|
-
constructor(tokens) {
|
|
656
|
+
constructor(tokens, capabilities = {}) {
|
|
655
657
|
this.tokens = tokens;
|
|
658
|
+
this.capabilities = capabilities;
|
|
656
659
|
this.allowUnaryPlusNumber = false;
|
|
657
660
|
this.scalarAllowsAggregateArgs = true;
|
|
658
661
|
this.scalarAllowsCase = true;
|
|
@@ -745,6 +748,12 @@ var Parser = class {
|
|
|
745
748
|
if (upper === "DROP") return this.parseDropTempTable();
|
|
746
749
|
if (upper === "DECLARE") return this.parseDeclareVariable();
|
|
747
750
|
if (upper === "VALIDATE") return this.parseValidate();
|
|
751
|
+
if (upper === "IMPORT") {
|
|
752
|
+
if (!this.capabilities.import) {
|
|
753
|
+
throw new ParseError("IMPORT is not supported (capability is disabled).", tok);
|
|
754
|
+
}
|
|
755
|
+
return this.parseImport();
|
|
756
|
+
}
|
|
748
757
|
break;
|
|
749
758
|
}
|
|
750
759
|
default:
|
|
@@ -925,11 +934,213 @@ var Parser = class {
|
|
|
925
934
|
query = this.parseReorder();
|
|
926
935
|
} else if (tok.kind === "IDENT" /* IDENT */ && tok.value.toUpperCase() === "VALIDATE") {
|
|
927
936
|
query = this.parseValidate();
|
|
937
|
+
} else if (tok.kind === "IDENT" /* IDENT */ && tok.value.toUpperCase() === "IMPORT") {
|
|
938
|
+
if (!this.capabilities.import) {
|
|
939
|
+
throw new ParseError("IMPORT is not supported (capability is disabled).", tok);
|
|
940
|
+
}
|
|
941
|
+
query = this.parseImport();
|
|
928
942
|
} else {
|
|
929
943
|
throw new ParseError("EXPLAIN \u306E\u5F8C\u306B\u306F SELECT / WITH / INSERT / UPSERT / UPDATE / DELETE / REORDER / VALIDATE \u304C\u5FC5\u8981\u3067\u3059", tok);
|
|
930
944
|
}
|
|
931
945
|
return { type: "EXPLAIN", query };
|
|
932
946
|
}
|
|
947
|
+
parseImport() {
|
|
948
|
+
this.advance();
|
|
949
|
+
let writeMode;
|
|
950
|
+
if (this.peek().kind === "UPDATE" /* UPDATE */) {
|
|
951
|
+
this.advance();
|
|
952
|
+
writeMode = "UPDATE_RECORD_NUMBER";
|
|
953
|
+
}
|
|
954
|
+
this.expect("INTO" /* INTO */);
|
|
955
|
+
this.rejectTempTableDml();
|
|
956
|
+
const target = this.parseIdentifier();
|
|
957
|
+
const { appId, subtableCode } = extractTableRef(target, this.prev());
|
|
958
|
+
if (subtableCode) throw new ParseError("IMPORT does not support subtables in Phase 1.", this.prev());
|
|
959
|
+
this.expect("(" /* LPAREN */);
|
|
960
|
+
const targets = [];
|
|
961
|
+
const fields = [];
|
|
962
|
+
const targetNames = /* @__PURE__ */ new Set();
|
|
963
|
+
while (true) {
|
|
964
|
+
const name = this.parseIdentifier();
|
|
965
|
+
if (targetNames.has(name)) throw new ParseError(`IMPORT target ${name} is declared more than once.`, this.prev());
|
|
966
|
+
targetNames.add(name);
|
|
967
|
+
if (this.peek().kind === "(" /* LPAREN */) {
|
|
968
|
+
this.advance();
|
|
969
|
+
const children = this.parseIdentList();
|
|
970
|
+
this.expect(")" /* RPAREN */);
|
|
971
|
+
if (new Set(children).size !== children.length) {
|
|
972
|
+
throw new ParseError(`IMPORT subtable ${name} contains duplicate child declarations.`, this.prev());
|
|
973
|
+
}
|
|
974
|
+
let rowIdSourceHeader;
|
|
975
|
+
if (this.isSoftKeyword("ROW")) {
|
|
976
|
+
this.advance();
|
|
977
|
+
for (const word of ["ID", "SOURCE"]) {
|
|
978
|
+
if (!this.isSoftKeyword(word)) throw new ParseError(`ROW must be followed by ID SOURCE <header>.`, this.peek());
|
|
979
|
+
this.advance();
|
|
980
|
+
}
|
|
981
|
+
rowIdSourceHeader = this.parseIdentifier();
|
|
982
|
+
}
|
|
983
|
+
targets.push({ kind: "SUBTABLE", subtableCode: name, children, ...rowIdSourceHeader ? { rowIdSourceHeader } : {} });
|
|
984
|
+
} else {
|
|
985
|
+
fields.push(name);
|
|
986
|
+
targets.push({ kind: "FIELD", field: name });
|
|
987
|
+
}
|
|
988
|
+
if (this.peek().kind !== "," /* COMMA */) break;
|
|
989
|
+
this.advance();
|
|
990
|
+
}
|
|
991
|
+
this.expect(")" /* RPAREN */);
|
|
992
|
+
this.expect("FROM" /* FROM */);
|
|
993
|
+
if (!this.isSoftKeyword("CSV") && !this.isSoftKeyword("JSON")) throw new ParseError("IMPORT FROM requires CSV or JSON.", this.peek());
|
|
994
|
+
const sourceKind = this.peek().value.toUpperCase();
|
|
995
|
+
this.advance();
|
|
996
|
+
const sourceName = this.parseIdentifier();
|
|
997
|
+
let encoding;
|
|
998
|
+
let hasHeader = true;
|
|
999
|
+
let columns;
|
|
1000
|
+
if (this.isSoftKeyword("ENCODING")) {
|
|
1001
|
+
if (sourceKind === "JSON") throw new ParseError("JSON source is UTF-8 only; ENCODING is not allowed.", this.peek());
|
|
1002
|
+
this.advance();
|
|
1003
|
+
const value = this.parseIdentifier().toUpperCase();
|
|
1004
|
+
if (value !== "UTF8" && value !== "SJIS") throw new ParseError("ENCODING must be UTF8 or SJIS.", this.prev());
|
|
1005
|
+
encoding = value === "UTF8" ? "utf8" : "sjis";
|
|
1006
|
+
}
|
|
1007
|
+
if (this.peek().kind === "NOT" /* NOT */ && this.peekAt(1).kind === "IDENT" /* IDENT */ && this.peekAt(1).value.toUpperCase() === "HEADER") {
|
|
1008
|
+
if (sourceKind === "JSON") throw new ParseError("NO HEADER is CSV-only.", this.peek());
|
|
1009
|
+
this.advance();
|
|
1010
|
+
this.advance();
|
|
1011
|
+
hasHeader = false;
|
|
1012
|
+
} else if (this.isSoftKeyword("NO") && this.peekAt(1).kind === "IDENT" /* IDENT */ && this.peekAt(1).value.toUpperCase() === "HEADER") {
|
|
1013
|
+
if (sourceKind === "JSON") throw new ParseError("NO HEADER is CSV-only.", this.peek());
|
|
1014
|
+
this.advance();
|
|
1015
|
+
this.advance();
|
|
1016
|
+
hasHeader = false;
|
|
1017
|
+
}
|
|
1018
|
+
if (this.isSoftKeyword("COLUMNS")) {
|
|
1019
|
+
if (sourceKind === "JSON") throw new ParseError("COLUMNS is CSV-only.", this.peek());
|
|
1020
|
+
if (hasHeader) throw new ParseError("COLUMNS requires NO HEADER.", this.peek());
|
|
1021
|
+
this.advance();
|
|
1022
|
+
this.expect("(" /* LPAREN */);
|
|
1023
|
+
columns = this.parseIdentList();
|
|
1024
|
+
this.expect(")" /* RPAREN */);
|
|
1025
|
+
}
|
|
1026
|
+
let projection;
|
|
1027
|
+
if (this.peek().kind === "SELECT" /* SELECT */) {
|
|
1028
|
+
if (sourceKind === "JSON") throw new ParseError("SELECT projection is CSV-only.", this.peek());
|
|
1029
|
+
projection = this.parseSelect();
|
|
1030
|
+
if (projection.from.cteName !== NO_FROM_CTE_NAME || projection.joins.length > 0) {
|
|
1031
|
+
throw new ParseError("IMPORT projection cannot use FROM or JOIN.", this.prev());
|
|
1032
|
+
}
|
|
1033
|
+
if (projection.where || projection.groupBy.length || projection.having || projection.orderBy.length || projection.limit !== null || projection.offset !== null) {
|
|
1034
|
+
throw new ParseError("IMPORT projection supports SELECT expressions only.", this.prev());
|
|
1035
|
+
}
|
|
1036
|
+
this.validateImportProjectionScope(projection, this.prev());
|
|
1037
|
+
if (targets.some((item) => item.kind === "SUBTABLE")) {
|
|
1038
|
+
throw new ParseError("IMPORT subtable sources cannot use SELECT projection.", this.prev());
|
|
1039
|
+
}
|
|
1040
|
+
if (projection.columns.length !== fields.length) {
|
|
1041
|
+
throw new ParseError(`IMPORT projection has ${projection.columns.length} columns; target has ${fields.length}.`, this.prev());
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
let mappingMode = "POSITION";
|
|
1045
|
+
let ignoreUnknownColumns = false;
|
|
1046
|
+
if (this.peek().kind === "BY" /* BY */ || this.isSoftKeyword("BY")) {
|
|
1047
|
+
if (sourceKind === "JSON") throw new ParseError("BY NAME is CSV-only.", this.peek());
|
|
1048
|
+
this.advance();
|
|
1049
|
+
if (!this.isSoftKeyword("NAME")) throw new ParseError("BY must be followed by NAME in IMPORT.", this.peek());
|
|
1050
|
+
this.advance();
|
|
1051
|
+
if (!hasHeader) throw new ParseError("BY NAME requires HEADER.", this.prev());
|
|
1052
|
+
if (projection) throw new ParseError("BY NAME and SELECT projection are mutually exclusive.", this.prev());
|
|
1053
|
+
mappingMode = "BY_NAME";
|
|
1054
|
+
if (this.isSoftKeyword("IGNORE")) {
|
|
1055
|
+
this.advance();
|
|
1056
|
+
if (!this.isSoftKeyword("UNKNOWN")) throw new ParseError("IGNORE must be followed by UNKNOWN COLUMNS.", this.peek());
|
|
1057
|
+
this.advance();
|
|
1058
|
+
if (!this.isSoftKeyword("COLUMNS")) throw new ParseError("IGNORE UNKNOWN must be followed by COLUMNS.", this.peek());
|
|
1059
|
+
this.advance();
|
|
1060
|
+
ignoreUnknownColumns = true;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
let keyFields;
|
|
1064
|
+
let recordNumberSourceHeader;
|
|
1065
|
+
if (this.isSoftKeyword("MATCH")) {
|
|
1066
|
+
this.advance();
|
|
1067
|
+
for (const word of ["RECORD", "NUMBER", "SOURCE"]) {
|
|
1068
|
+
if (!this.isSoftKeyword(word)) throw new ParseError(`MATCH must be followed by RECORD NUMBER SOURCE <header>.`, this.peek());
|
|
1069
|
+
this.advance();
|
|
1070
|
+
}
|
|
1071
|
+
recordNumberSourceHeader = this.parseIdentifier();
|
|
1072
|
+
}
|
|
1073
|
+
if (this.peek().kind === "ON" /* ON */ && this.peekAt(1).kind === "DUPLICATE" /* DUPLICATE */) keyFields = this.parseOnDuplicate();
|
|
1074
|
+
let replaceSubtables;
|
|
1075
|
+
if (this.peek().kind === "REPLACE" /* REPLACE */ || this.isSoftKeyword("REPLACE")) {
|
|
1076
|
+
this.advance();
|
|
1077
|
+
if (!this.isSoftKeyword("SUBTABLES")) throw new ParseError("REPLACE must be followed by SUBTABLES (...).", this.peek());
|
|
1078
|
+
this.advance();
|
|
1079
|
+
this.expect("(" /* LPAREN */);
|
|
1080
|
+
replaceSubtables = this.parseIdentList();
|
|
1081
|
+
this.expect(")" /* RPAREN */);
|
|
1082
|
+
if (new Set(replaceSubtables).size !== replaceSubtables.length) throw new ParseError("REPLACE SUBTABLES contains duplicates.", this.prev());
|
|
1083
|
+
}
|
|
1084
|
+
const subtableTargets = targets.filter((item) => item.kind === "SUBTABLE");
|
|
1085
|
+
if (subtableTargets.length) {
|
|
1086
|
+
if (projection) throw new ParseError("IMPORT subtables cannot use SELECT projection.", this.prev());
|
|
1087
|
+
if (sourceKind === "JSON") {
|
|
1088
|
+
if (subtableTargets.some((item) => item.rowIdSourceHeader)) throw new ParseError("JSON subtable IMPORT does not accept ROW ID SOURCE.", this.prev());
|
|
1089
|
+
if (replaceSubtables) throw new ParseError("REPLACE SUBTABLES is CSV-only; JSON uses nested-array replacement semantics.", this.prev());
|
|
1090
|
+
} else {
|
|
1091
|
+
if (writeMode !== "UPDATE_RECORD_NUMBER" || !recordNumberSourceHeader) throw new ParseError("CSV subtable IMPORT requires IMPORT UPDATE and MATCH RECORD NUMBER SOURCE.", this.prev());
|
|
1092
|
+
if (mappingMode !== "BY_NAME") throw new ParseError("CSV subtable IMPORT requires BY NAME.", this.prev());
|
|
1093
|
+
if (!replaceSubtables) throw new ParseError("CSV subtable IMPORT requires REPLACE SUBTABLES (...).", this.prev());
|
|
1094
|
+
const replacement = new Set(replaceSubtables);
|
|
1095
|
+
for (const item of subtableTargets) {
|
|
1096
|
+
if (!item.rowIdSourceHeader) throw new ParseError(`CSV subtable ${item.subtableCode} requires ROW ID SOURCE <header>.`, this.prev());
|
|
1097
|
+
if (!replacement.has(item.subtableCode)) throw new ParseError(`IMPORT declares child columns for non-replaced subtable ${item.subtableCode}.`, this.prev());
|
|
1098
|
+
}
|
|
1099
|
+
for (const code of replacement) {
|
|
1100
|
+
if (!subtableTargets.some((item) => item.subtableCode === code)) throw new ParseError(`REPLACE SUBTABLES target ${code} is not declared in INTO.`, this.prev());
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
} else if (replaceSubtables) {
|
|
1104
|
+
throw new ParseError("REPLACE SUBTABLES requires subtable targets in INTO.", this.prev());
|
|
1105
|
+
}
|
|
1106
|
+
if (writeMode) {
|
|
1107
|
+
if (sourceKind !== "CSV") throw new ParseError("IMPORT UPDATE supports CSV only.", this.prev());
|
|
1108
|
+
if (mappingMode !== "BY_NAME") throw new ParseError("IMPORT UPDATE requires BY NAME.", this.prev());
|
|
1109
|
+
if (!recordNumberSourceHeader) throw new ParseError("IMPORT UPDATE requires MATCH RECORD NUMBER SOURCE <header>.", this.peek());
|
|
1110
|
+
if (keyFields) throw new ParseError("IMPORT UPDATE and ON DUPLICATE are mutually exclusive.", this.prev());
|
|
1111
|
+
} else if (recordNumberSourceHeader) {
|
|
1112
|
+
throw new ParseError("MATCH RECORD NUMBER SOURCE requires IMPORT UPDATE.", this.prev());
|
|
1113
|
+
}
|
|
1114
|
+
const checkGroups = this.parseCheckGroups();
|
|
1115
|
+
const control = this.parseDmlControlSuffix();
|
|
1116
|
+
return {
|
|
1117
|
+
type: "IMPORT",
|
|
1118
|
+
appId,
|
|
1119
|
+
fields,
|
|
1120
|
+
targets,
|
|
1121
|
+
source: sourceKind === "JSON" ? { kind: "JSON", sourceName } : { kind: "CSV", sourceName, encoding, hasHeader, mappingMode, ignoreUnknownColumns, ...columns ? { columns } : {}, ...projection ? { projection } : {} },
|
|
1122
|
+
...writeMode ? { writeMode, recordNumberSourceHeader } : {},
|
|
1123
|
+
...replaceSubtables ? { replaceSubtables } : {},
|
|
1124
|
+
...keyFields ? { keyFields } : {},
|
|
1125
|
+
...checkGroups,
|
|
1126
|
+
...control
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
validateImportProjectionScope(node, token) {
|
|
1130
|
+
if (Array.isArray(node)) {
|
|
1131
|
+
node.forEach((item) => this.validateImportProjectionScope(item, token));
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
if (node === null || typeof node !== "object") return;
|
|
1135
|
+
const value = node;
|
|
1136
|
+
if (value.type === "SCALAR_SUBQUERY" || value.type === "SCALAR_SUBQUERY_COL") {
|
|
1137
|
+
throw new ParseError("IMPORT projection cannot use subqueries.", token);
|
|
1138
|
+
}
|
|
1139
|
+
if (typeof value.tableAlias === "string") {
|
|
1140
|
+
throw new ParseError("IMPORT projection cannot use qualified column references.", token);
|
|
1141
|
+
}
|
|
1142
|
+
Object.values(value).forEach((item) => this.validateImportProjectionScope(item, token));
|
|
1143
|
+
}
|
|
933
1144
|
/** VALIDATE APP100 [(fields)] [WHERE ...] [CHECK ...] [INTO #err]. */
|
|
934
1145
|
parseValidate() {
|
|
935
1146
|
const validateTok = this.advance();
|
|
@@ -3094,7 +3305,7 @@ function getStatementType(stmt) {
|
|
|
3094
3305
|
return typeof obj.type === "string" ? obj.type : "UNKNOWN";
|
|
3095
3306
|
}
|
|
3096
3307
|
function isDmlType(type) {
|
|
3097
|
-
return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER";
|
|
3308
|
+
return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER" || type === "IMPORT";
|
|
3098
3309
|
}
|
|
3099
3310
|
function isReadOnlyType(type) {
|
|
3100
3311
|
return type === "SELECT" || type === "VALIDATE" || type === "UNION" || type === "WITH" || type === "EXPLAIN" || type === "SHOW_APPS" || type === "DESCRIBE" || type === "CREATE_TEMP_TABLE" || type === "DROP_TEMP_TABLE" || type === "SET_VARIABLE" || type === "DECLARE_VARIABLE" || type === "ASSERT";
|
|
@@ -4591,7 +4802,7 @@ function analyzeBatch(statements) {
|
|
|
4591
4802
|
dependsOn.add(at);
|
|
4592
4803
|
}
|
|
4593
4804
|
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 : [];
|
|
4805
|
+
const payloadFields = stmt.type === "VALIDATE" ? ["$id", "$err_field", "$err_code", "$err_message", "$err_value"] : stmt.type === "IMPORT" && stmt.targets?.some((target) => target.kind === "SUBTABLE") ? [...new Set(stmt.targets.flatMap((target) => target.kind === "FIELD" ? [target.field] : target.children)), "$err_subtable", "$err_subrow", "$err_source_row"] : stmt.type === "UPDATE" ? ["$id", ...stmt.assignments.map((a) => a.field)] : "fields" in stmt ? stmt.fields : [];
|
|
4595
4806
|
const signature = JSON.stringify(payloadFields);
|
|
4596
4807
|
const at = defined.get(validationTable);
|
|
4597
4808
|
if (at === void 0) {
|
|
@@ -7553,9 +7764,15 @@ function validateDmlCandidates(candidates, operation, payloadFields, targetField
|
|
|
7553
7764
|
candidate.record ??= {};
|
|
7554
7765
|
const rowErrors = includePreErrors ? [...candidate.preErrors] : [];
|
|
7555
7766
|
for (const code of targetFields) {
|
|
7767
|
+
if (!candidate.payload.has(code)) continue;
|
|
7556
7768
|
const result = validateAndNormalizeDmlValue(candidate.payload.get(code), infoByCode.get(code), numberPrecision);
|
|
7557
7769
|
if (!result.ok) rowErrors.push({ field: code, code: result.code, message: result.message });
|
|
7558
|
-
else
|
|
7770
|
+
else {
|
|
7771
|
+
const original = candidate.payload.get(code);
|
|
7772
|
+
const type = infoByCode.get(code).fieldType;
|
|
7773
|
+
const preserveCodes = ["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"].includes(type) && Array.isArray(original) && original.every((item) => typeof item === "object" && item !== null && "code" in item);
|
|
7774
|
+
candidate.record[code] = { value: preserveCodes ? original : result.value };
|
|
7775
|
+
}
|
|
7559
7776
|
}
|
|
7560
7777
|
if (validateMissingCreateFields && candidate.mode === "create") {
|
|
7561
7778
|
for (const info of fieldInfos) {
|
|
@@ -7817,6 +8034,862 @@ function unsupported(code, field, fieldType, operator) {
|
|
|
7817
8034
|
return { capability: "UNSUPPORTED", reasons: [{ code, field, fieldType, operator }] };
|
|
7818
8035
|
}
|
|
7819
8036
|
|
|
8037
|
+
// src/import/sourceLoader.ts
|
|
8038
|
+
var IMPORT_MAX_BYTES = 10 * 1024 * 1024;
|
|
8039
|
+
var ImportSourceError = class extends Error {
|
|
8040
|
+
constructor(message) {
|
|
8041
|
+
super(`ImportSourceError: ${message}`);
|
|
8042
|
+
this.name = "ImportSourceError";
|
|
8043
|
+
}
|
|
8044
|
+
};
|
|
8045
|
+
function resolveImportSource(name, resolver) {
|
|
8046
|
+
if (!resolver) throw new ImportSourceError("IMPORT source capability is not available.");
|
|
8047
|
+
const handle = resolver(name);
|
|
8048
|
+
if (!handle) throw new ImportSourceError(`source "${name}" is not supplied.`);
|
|
8049
|
+
return handle;
|
|
8050
|
+
}
|
|
8051
|
+
async function loadImportSource(handle, cache) {
|
|
8052
|
+
let pending = cache.get(handle);
|
|
8053
|
+
if (!pending) {
|
|
8054
|
+
pending = handle.load().then((payload) => {
|
|
8055
|
+
if (!(payload.bytes instanceof Uint8Array)) throw new ImportSourceError("loader must return Uint8Array bytes.");
|
|
8056
|
+
if (payload.bytes.byteLength > IMPORT_MAX_BYTES) {
|
|
8057
|
+
throw new ImportSourceError(`source exceeds the ${IMPORT_MAX_BYTES} byte limit.`);
|
|
8058
|
+
}
|
|
8059
|
+
return payload;
|
|
8060
|
+
});
|
|
8061
|
+
cache.set(handle, pending);
|
|
8062
|
+
}
|
|
8063
|
+
return pending;
|
|
8064
|
+
}
|
|
8065
|
+
|
|
8066
|
+
// src/import/csvDecoder.ts
|
|
8067
|
+
function decodeImportText(bytes, encoding) {
|
|
8068
|
+
try {
|
|
8069
|
+
return new TextDecoder(encoding === "sjis" ? "shift_jis" : "utf-8", { fatal: true }).decode(bytes).replace(/^\uFEFF/, "");
|
|
8070
|
+
} catch {
|
|
8071
|
+
throw new ImportSourceError(`invalid ${encoding.toUpperCase()} byte sequence.`);
|
|
8072
|
+
}
|
|
8073
|
+
}
|
|
8074
|
+
function parseRfc4180(text) {
|
|
8075
|
+
const records = [];
|
|
8076
|
+
let record = [];
|
|
8077
|
+
let cell = "";
|
|
8078
|
+
let quoted = false;
|
|
8079
|
+
let afterQuote = false;
|
|
8080
|
+
let i = 0;
|
|
8081
|
+
const finishCell = () => {
|
|
8082
|
+
record.push(cell);
|
|
8083
|
+
cell = "";
|
|
8084
|
+
afterQuote = false;
|
|
8085
|
+
};
|
|
8086
|
+
const finishRecord = () => {
|
|
8087
|
+
finishCell();
|
|
8088
|
+
records.push(record);
|
|
8089
|
+
record = [];
|
|
8090
|
+
};
|
|
8091
|
+
while (i < text.length) {
|
|
8092
|
+
const ch = text[i];
|
|
8093
|
+
if (quoted) {
|
|
8094
|
+
if (ch === '"') {
|
|
8095
|
+
if (text[i + 1] === '"') {
|
|
8096
|
+
cell += '"';
|
|
8097
|
+
i += 2;
|
|
8098
|
+
continue;
|
|
8099
|
+
}
|
|
8100
|
+
quoted = false;
|
|
8101
|
+
afterQuote = true;
|
|
8102
|
+
i++;
|
|
8103
|
+
continue;
|
|
8104
|
+
}
|
|
8105
|
+
cell += ch;
|
|
8106
|
+
i++;
|
|
8107
|
+
continue;
|
|
8108
|
+
}
|
|
8109
|
+
if (afterQuote && ch !== "," && ch !== "\r" && ch !== "\n") {
|
|
8110
|
+
throw new ImportSourceError(`unexpected character after closing quote at offset ${i}.`);
|
|
8111
|
+
}
|
|
8112
|
+
if (ch === '"') {
|
|
8113
|
+
if (cell.length !== 0) throw new ImportSourceError(`quote in unquoted cell at offset ${i}.`);
|
|
8114
|
+
quoted = true;
|
|
8115
|
+
i++;
|
|
8116
|
+
continue;
|
|
8117
|
+
}
|
|
8118
|
+
if (ch === ",") {
|
|
8119
|
+
finishCell();
|
|
8120
|
+
i++;
|
|
8121
|
+
continue;
|
|
8122
|
+
}
|
|
8123
|
+
if (ch === "\r" || ch === "\n") {
|
|
8124
|
+
if (ch === "\r" && text[i + 1] === "\n") i++;
|
|
8125
|
+
finishRecord();
|
|
8126
|
+
i++;
|
|
8127
|
+
continue;
|
|
8128
|
+
}
|
|
8129
|
+
cell += ch;
|
|
8130
|
+
i++;
|
|
8131
|
+
}
|
|
8132
|
+
if (quoted) throw new ImportSourceError("unterminated quoted cell.");
|
|
8133
|
+
if (cell.length > 0 || record.length > 0 || afterQuote) finishRecord();
|
|
8134
|
+
return records;
|
|
8135
|
+
}
|
|
8136
|
+
function assertColumns(columns) {
|
|
8137
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8138
|
+
columns.forEach((column, index) => {
|
|
8139
|
+
if (column === "") throw new ImportSourceError(`CSV column ${index + 1} has an empty name.`);
|
|
8140
|
+
if (seen.has(column)) throw new ImportSourceError(`CSV column name "${column}" is duplicated.`);
|
|
8141
|
+
seen.add(column);
|
|
8142
|
+
});
|
|
8143
|
+
}
|
|
8144
|
+
function decodeCsv(bytes, options) {
|
|
8145
|
+
const records = parseRfc4180(decodeImportText(bytes, options.encoding));
|
|
8146
|
+
let columns;
|
|
8147
|
+
let rows;
|
|
8148
|
+
if (options.hasHeader) {
|
|
8149
|
+
columns = records[0] ?? [];
|
|
8150
|
+
rows = records.slice(1);
|
|
8151
|
+
} else {
|
|
8152
|
+
rows = records;
|
|
8153
|
+
columns = options.columns ? [...options.columns] : Array.from({ length: rows[0]?.length ?? 0 }, (_, i) => `c${i + 1}`);
|
|
8154
|
+
}
|
|
8155
|
+
assertColumns(columns);
|
|
8156
|
+
if (rows.length === 0) throw new ImportSourceError("CSV has no data rows.");
|
|
8157
|
+
rows.forEach((row, i) => {
|
|
8158
|
+
if (row.length !== columns.length) {
|
|
8159
|
+
throw new ImportSourceError(`CSV row ${i + (options.hasHeader ? 2 : 1)} has ${row.length} cells; expected ${columns.length}.`);
|
|
8160
|
+
}
|
|
8161
|
+
});
|
|
8162
|
+
return { columns, rows };
|
|
8163
|
+
}
|
|
8164
|
+
|
|
8165
|
+
// src/import/convertImportCsvValue.ts
|
|
8166
|
+
var LF_MULTI_TYPES = /* @__PURE__ */ new Set([
|
|
8167
|
+
"CHECK_BOX",
|
|
8168
|
+
"MULTI_SELECT",
|
|
8169
|
+
"USER_SELECT",
|
|
8170
|
+
"ORGANIZATION_SELECT",
|
|
8171
|
+
"GROUP_SELECT"
|
|
8172
|
+
]);
|
|
8173
|
+
var USER_TYPES2 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
8174
|
+
var ImportCsvValueError = class extends Error {
|
|
8175
|
+
constructor() {
|
|
8176
|
+
super("multiple-value CSV cell contains an empty LF-delimited item");
|
|
8177
|
+
this.code = "ERR_IMPORT_MULTI_EMPTY_ITEM";
|
|
8178
|
+
this.name = "ImportCsvValueError";
|
|
8179
|
+
}
|
|
8180
|
+
};
|
|
8181
|
+
function convertImportCsvValue(raw, type, options) {
|
|
8182
|
+
void options;
|
|
8183
|
+
if (!LF_MULTI_TYPES.has(type ?? "")) return raw;
|
|
8184
|
+
if (raw === "") return [];
|
|
8185
|
+
const items = raw.split(/\r\n|\n/);
|
|
8186
|
+
if (items.some((item) => item === "")) throw new ImportCsvValueError();
|
|
8187
|
+
return USER_TYPES2.has(type ?? "") ? items.map((code) => ({ code })) : items;
|
|
8188
|
+
}
|
|
8189
|
+
|
|
8190
|
+
// src/import/jsonTokenizer.ts
|
|
8191
|
+
function fail(message, offset, line, column) {
|
|
8192
|
+
throw new ImportSourceError(`JSON ${message} (offset=${offset}, line=${line}, column=${column}).`);
|
|
8193
|
+
}
|
|
8194
|
+
function decodeUtf8Json(bytes) {
|
|
8195
|
+
try {
|
|
8196
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
8197
|
+
} catch {
|
|
8198
|
+
throw new ImportSourceError("JSON source is not valid UTF-8.");
|
|
8199
|
+
}
|
|
8200
|
+
}
|
|
8201
|
+
function tokenizeJson(text) {
|
|
8202
|
+
const tokens = [];
|
|
8203
|
+
let i = 0, line = 1, column = 1;
|
|
8204
|
+
const advance = () => {
|
|
8205
|
+
const ch = text[i++];
|
|
8206
|
+
if (ch === "\n") {
|
|
8207
|
+
line++;
|
|
8208
|
+
column = 1;
|
|
8209
|
+
} else column++;
|
|
8210
|
+
return ch;
|
|
8211
|
+
};
|
|
8212
|
+
const position = () => ({ offset: i, line, column });
|
|
8213
|
+
while (i < text.length) {
|
|
8214
|
+
const ch = text[i];
|
|
8215
|
+
if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
|
|
8216
|
+
advance();
|
|
8217
|
+
continue;
|
|
8218
|
+
}
|
|
8219
|
+
const start = position();
|
|
8220
|
+
if ("{}[]:,".includes(ch)) {
|
|
8221
|
+
advance();
|
|
8222
|
+
tokens.push({ kind: "punct", value: ch, ...start });
|
|
8223
|
+
continue;
|
|
8224
|
+
}
|
|
8225
|
+
if (ch === '"') {
|
|
8226
|
+
advance();
|
|
8227
|
+
let value = "";
|
|
8228
|
+
let closed = false;
|
|
8229
|
+
while (i < text.length) {
|
|
8230
|
+
const c = advance();
|
|
8231
|
+
if (c === '"') {
|
|
8232
|
+
closed = true;
|
|
8233
|
+
break;
|
|
8234
|
+
}
|
|
8235
|
+
if (c.charCodeAt(0) < 32) fail("string contains an unescaped control character", start.offset, start.line, start.column);
|
|
8236
|
+
if (c !== "\\") {
|
|
8237
|
+
value += c;
|
|
8238
|
+
continue;
|
|
8239
|
+
}
|
|
8240
|
+
if (i >= text.length) fail("string has an unterminated escape", start.offset, start.line, start.column);
|
|
8241
|
+
const esc = advance();
|
|
8242
|
+
const simple = { '"': '"', "\\": "\\", "/": "/", b: "\b", f: "\f", n: "\n", r: "\r", t: " " };
|
|
8243
|
+
if (esc in simple) {
|
|
8244
|
+
value += simple[esc];
|
|
8245
|
+
continue;
|
|
8246
|
+
}
|
|
8247
|
+
if (esc !== "u") fail(`has invalid escape \\${esc}`, i - 2, line, Math.max(1, column - 2));
|
|
8248
|
+
const hex = text.slice(i, i + 4);
|
|
8249
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) fail("has invalid unicode escape", i, line, column);
|
|
8250
|
+
for (let n = 0; n < 4; n++) advance();
|
|
8251
|
+
const code = Number.parseInt(hex, 16);
|
|
8252
|
+
if (code >= 55296 && code <= 56319) {
|
|
8253
|
+
if (text.slice(i, i + 2) !== "\\u" || !/^[0-9a-fA-F]{4}$/.test(text.slice(i + 2, i + 6))) fail("has an unpaired high surrogate", i, line, column);
|
|
8254
|
+
advance();
|
|
8255
|
+
advance();
|
|
8256
|
+
const lowHex = text.slice(i, i + 4);
|
|
8257
|
+
for (let n = 0; n < 4; n++) advance();
|
|
8258
|
+
const low = Number.parseInt(lowHex, 16);
|
|
8259
|
+
if (low < 56320 || low > 57343) fail("has an invalid surrogate pair", i - 4, line, Math.max(1, column - 4));
|
|
8260
|
+
value += String.fromCodePoint(65536 + (code - 55296 << 10) + low - 56320);
|
|
8261
|
+
} else if (code >= 56320 && code <= 57343) {
|
|
8262
|
+
fail("has an unpaired low surrogate", i - 4, line, Math.max(1, column - 4));
|
|
8263
|
+
} else value += String.fromCharCode(code);
|
|
8264
|
+
}
|
|
8265
|
+
if (!closed) fail("string is unterminated", start.offset, start.line, start.column);
|
|
8266
|
+
tokens.push({ kind: "string", value, ...start });
|
|
8267
|
+
continue;
|
|
8268
|
+
}
|
|
8269
|
+
const rest = text.slice(i);
|
|
8270
|
+
const number = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(rest)?.[0];
|
|
8271
|
+
if (number) {
|
|
8272
|
+
for (let n = 0; n < number.length; n++) advance();
|
|
8273
|
+
tokens.push({ kind: "number", lexeme: number, ...start });
|
|
8274
|
+
continue;
|
|
8275
|
+
}
|
|
8276
|
+
const literal = /^(true|false|null)/.exec(rest)?.[0];
|
|
8277
|
+
if (literal) {
|
|
8278
|
+
for (let n = 0; n < literal.length; n++) advance();
|
|
8279
|
+
tokens.push({ kind: "literal", value: literal === "true" ? true : literal === "false" ? false : null, ...start });
|
|
8280
|
+
continue;
|
|
8281
|
+
}
|
|
8282
|
+
fail(`has an unexpected token ${JSON.stringify(ch)}`, start.offset, start.line, start.column);
|
|
8283
|
+
}
|
|
8284
|
+
tokens.push({ kind: "eof", offset: i, line, column });
|
|
8285
|
+
return tokens;
|
|
8286
|
+
}
|
|
8287
|
+
|
|
8288
|
+
// src/import/jsonDecoder.ts
|
|
8289
|
+
function describe(token) {
|
|
8290
|
+
return token.kind === "eof" ? "end of input" : token.kind === "punct" ? token.value : token.kind;
|
|
8291
|
+
}
|
|
8292
|
+
function decodeJsonRecords(bytes) {
|
|
8293
|
+
if (bytes.byteLength === 0) throw new ImportSourceError("JSON source is empty.");
|
|
8294
|
+
const tokens = tokenizeJson(decodeUtf8Json(bytes));
|
|
8295
|
+
let index = 0;
|
|
8296
|
+
const fail3 = (message, token = tokens[index]) => {
|
|
8297
|
+
throw new ImportSourceError(`JSON ${message} (offset=${token.offset}, line=${token.line}, column=${token.column}).`);
|
|
8298
|
+
};
|
|
8299
|
+
const isPunct = (token, value) => token.kind === "punct" && token.value === value;
|
|
8300
|
+
const punct = (value) => {
|
|
8301
|
+
const token = tokens[index];
|
|
8302
|
+
if (token.kind !== "punct" || token.value !== value) fail3(`expected ${value}; found ${describe(token)}`, token);
|
|
8303
|
+
index++;
|
|
8304
|
+
};
|
|
8305
|
+
const parseValue = () => {
|
|
8306
|
+
const token = tokens[index++];
|
|
8307
|
+
if (token.kind === "string") return token.value;
|
|
8308
|
+
if (token.kind === "number") return { kind: "number", lexeme: token.lexeme };
|
|
8309
|
+
if (token.kind === "literal") return token.value;
|
|
8310
|
+
if (token.kind === "punct" && token.value === "{") {
|
|
8311
|
+
const object = /* @__PURE__ */ new Map();
|
|
8312
|
+
if (isPunct(tokens[index], "}")) {
|
|
8313
|
+
index++;
|
|
8314
|
+
return object;
|
|
8315
|
+
}
|
|
8316
|
+
while (true) {
|
|
8317
|
+
const key = tokens[index++];
|
|
8318
|
+
if (key.kind !== "string") return fail3(`object key must be a string; found ${describe(key)}`, key);
|
|
8319
|
+
const keyValue = key.value;
|
|
8320
|
+
if (object.has(keyValue)) fail3(`duplicate key ${JSON.stringify(keyValue)}`, key);
|
|
8321
|
+
punct(":");
|
|
8322
|
+
object.set(keyValue, parseValue());
|
|
8323
|
+
const separator = tokens[index];
|
|
8324
|
+
if (isPunct(separator, "}")) {
|
|
8325
|
+
index++;
|
|
8326
|
+
break;
|
|
8327
|
+
}
|
|
8328
|
+
punct(",");
|
|
8329
|
+
}
|
|
8330
|
+
return object;
|
|
8331
|
+
}
|
|
8332
|
+
if (token.kind === "punct" && token.value === "[") {
|
|
8333
|
+
const array = [];
|
|
8334
|
+
if (isPunct(tokens[index], "]")) {
|
|
8335
|
+
index++;
|
|
8336
|
+
return array;
|
|
8337
|
+
}
|
|
8338
|
+
while (true) {
|
|
8339
|
+
array.push(parseValue());
|
|
8340
|
+
const separator = tokens[index];
|
|
8341
|
+
if (isPunct(separator, "]")) {
|
|
8342
|
+
index++;
|
|
8343
|
+
break;
|
|
8344
|
+
}
|
|
8345
|
+
punct(",");
|
|
8346
|
+
}
|
|
8347
|
+
return array;
|
|
8348
|
+
}
|
|
8349
|
+
return fail3(`expected a value; found ${describe(token)}`, token);
|
|
8350
|
+
};
|
|
8351
|
+
const root = parseValue();
|
|
8352
|
+
if (tokens[index].kind !== "eof") fail3(`has trailing data; found ${describe(tokens[index])}`);
|
|
8353
|
+
const records = root instanceof Map ? [root] : Array.isArray(root) ? root : fail3("root must be an object or array.", tokens[0]);
|
|
8354
|
+
if (records.length === 0) throw new ImportSourceError("JSON source contains no records.");
|
|
8355
|
+
records.forEach((record, i) => {
|
|
8356
|
+
if (!(record instanceof Map)) throw new ImportSourceError(`JSON record ${i + 1} must be an object.`);
|
|
8357
|
+
});
|
|
8358
|
+
return records;
|
|
8359
|
+
}
|
|
8360
|
+
|
|
8361
|
+
// src/import/jsonMaterializer.ts
|
|
8362
|
+
var STRING_ARRAY_TYPES = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
|
|
8363
|
+
var CODE_ARRAY_TYPES = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
8364
|
+
function fail2(row, field, message) {
|
|
8365
|
+
throw new ImportSourceError(`JSON field validation failed (row=${row}, field=${field}): ${message}`);
|
|
8366
|
+
}
|
|
8367
|
+
function isNumber(value) {
|
|
8368
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Map) && value.kind === "number";
|
|
8369
|
+
}
|
|
8370
|
+
function materializeValue(value, target, row) {
|
|
8371
|
+
if (value === null) return "";
|
|
8372
|
+
if (typeof value === "string") return value;
|
|
8373
|
+
if (typeof value === "boolean") fail2(row, target.code, "boolean is not accepted.");
|
|
8374
|
+
if (isNumber(value)) {
|
|
8375
|
+
if (target.fieldType === "NUMBER") fail2(row, target.code, "precision target requires a JSON string.");
|
|
8376
|
+
if (!/^-?(?:0|[1-9]\d*)$/.test(value.lexeme) || value.lexeme === "-0") {
|
|
8377
|
+
fail2(row, target.code, `JSON number ${value.lexeme} must be a non-negative-zero safe integer lexeme.`);
|
|
8378
|
+
}
|
|
8379
|
+
const number = Number(value.lexeme);
|
|
8380
|
+
if (!Number.isSafeInteger(number)) fail2(row, target.code, `JSON number ${value.lexeme} is outside the safe integer range.`);
|
|
8381
|
+
return String(number);
|
|
8382
|
+
}
|
|
8383
|
+
if (value instanceof Map) fail2(row, target.code, "object is not accepted for a flat field.");
|
|
8384
|
+
if (!Array.isArray(value)) fail2(row, target.code, "unsupported value type.");
|
|
8385
|
+
if (!STRING_ARRAY_TYPES.has(target.fieldType) && !CODE_ARRAY_TYPES.has(target.fieldType)) {
|
|
8386
|
+
fail2(row, target.code, "array is accepted only for multi-value fields.");
|
|
8387
|
+
}
|
|
8388
|
+
const strings = value.map((entry) => {
|
|
8389
|
+
if (typeof entry !== "string") fail2(row, target.code, "array elements must be strings.");
|
|
8390
|
+
return entry;
|
|
8391
|
+
});
|
|
8392
|
+
if (new Set(strings).size !== strings.length) fail2(row, target.code, "array elements must not contain duplicates.");
|
|
8393
|
+
return CODE_ARRAY_TYPES.has(target.fieldType) ? JSON.stringify(strings.map((code) => ({ code }))) : JSON.stringify(strings);
|
|
8394
|
+
}
|
|
8395
|
+
function materializeJsonDmlSource(_source, payload, targets, maxRows) {
|
|
8396
|
+
if (payload.encoding && payload.encoding !== "utf8") throw new ImportSourceError("JSON source is UTF-8 only.");
|
|
8397
|
+
const records = decodeJsonRecords(payload.bytes);
|
|
8398
|
+
if (records.length > maxRows) throw new ImportSourceError(`source rows (${records.length}) exceed maxRecords (${maxRows}).`);
|
|
8399
|
+
const targetByCode = new Map(targets.map((target) => [target.code, target]));
|
|
8400
|
+
if (targetByCode.size !== targets.length) throw new ImportSourceError("JSON target fields contain duplicates.");
|
|
8401
|
+
const rows = [];
|
|
8402
|
+
const importPresence = [];
|
|
8403
|
+
records.forEach((record, index) => {
|
|
8404
|
+
for (const key of record.keys()) {
|
|
8405
|
+
if (!targetByCode.has(key)) fail2(index + 1, key, "unknown key (not declared in INTO).");
|
|
8406
|
+
}
|
|
8407
|
+
const row = {};
|
|
8408
|
+
const present = /* @__PURE__ */ new Set();
|
|
8409
|
+
for (const target of targets) {
|
|
8410
|
+
if (!record.has(target.code)) continue;
|
|
8411
|
+
present.add(target.code);
|
|
8412
|
+
row[target.code] = materializeValue(record.get(target.code), target, index + 1);
|
|
8413
|
+
}
|
|
8414
|
+
rows.push(row);
|
|
8415
|
+
importPresence.push(present);
|
|
8416
|
+
});
|
|
8417
|
+
return {
|
|
8418
|
+
rows,
|
|
8419
|
+
columns: targets.map((target) => target.code),
|
|
8420
|
+
columnMeta: new Map(targets.map((target) => [target.code, { fieldType: target.fieldType }])),
|
|
8421
|
+
importPresence
|
|
8422
|
+
};
|
|
8423
|
+
}
|
|
8424
|
+
|
|
8425
|
+
// src/import/materializeDmlSource.ts
|
|
8426
|
+
function materializeCsvDmlSource(source, payload, maxRows, targetCodes, fieldInfos, recordNumberSourceHeader) {
|
|
8427
|
+
const decoded = decodeCsv(payload.bytes, {
|
|
8428
|
+
encoding: source.encoding ?? payload.encoding ?? "utf8",
|
|
8429
|
+
hasHeader: source.hasHeader,
|
|
8430
|
+
columns: source.columns
|
|
8431
|
+
});
|
|
8432
|
+
if (decoded.rows.length > maxRows) {
|
|
8433
|
+
throw new ImportSourceError(`source rows (${decoded.rows.length}) exceed maxRecords (${maxRows}).`);
|
|
8434
|
+
}
|
|
8435
|
+
if (source.mappingMode === "BY_NAME") {
|
|
8436
|
+
if (!targetCodes || !fieldInfos) throw new Error("InternalError: BY NAME requires destination form metadata.");
|
|
8437
|
+
if (new Set(targetCodes).size !== targetCodes.length) {
|
|
8438
|
+
throw new ImportSourceError("ERR_IMPORT_HEADER_REUSED: a BY NAME header cannot be consumed more than once.");
|
|
8439
|
+
}
|
|
8440
|
+
const indexes = new Map(decoded.columns.map((column, index) => [column, index]));
|
|
8441
|
+
for (const code of targetCodes) {
|
|
8442
|
+
if (!indexes.has(code)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${code}" is missing.`);
|
|
8443
|
+
}
|
|
8444
|
+
const infoByCode = new Map(fieldInfos.map((info) => [info.code, info]));
|
|
8445
|
+
const targetSet = new Set(targetCodes);
|
|
8446
|
+
if (recordNumberSourceHeader && targetSet.has(recordNumberSourceHeader)) {
|
|
8447
|
+
throw new ImportSourceError("ERR_IMPORT_HEADER_REUSED: record-number source header is lookup-only and cannot be a write target.");
|
|
8448
|
+
}
|
|
8449
|
+
if (recordNumberSourceHeader && !indexes.has(recordNumberSourceHeader)) {
|
|
8450
|
+
throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${recordNumberSourceHeader}" is missing.`);
|
|
8451
|
+
}
|
|
8452
|
+
const ignoredKnownColumns = [];
|
|
8453
|
+
const ignoredUnknownColumns = [];
|
|
8454
|
+
const nonEmpty = (index) => decoded.rows.filter((row) => row[index] !== "").length;
|
|
8455
|
+
const reasonFor = (info) => {
|
|
8456
|
+
if (info.fieldType === "FILE") return "FILE attachment is outside flat IMPORT scope";
|
|
8457
|
+
if (info.inSubtable || info.fieldType === "SUBTABLE") return "subtable field is not writable in Phase 3";
|
|
8458
|
+
if (info.writable === false) return `non-writable ${info.fieldType} field`;
|
|
8459
|
+
return `known export-only ${info.fieldType} field`;
|
|
8460
|
+
};
|
|
8461
|
+
for (const [index, column] of decoded.columns.entries()) {
|
|
8462
|
+
if (targetSet.has(column) || column === recordNumberSourceHeader) continue;
|
|
8463
|
+
const info = infoByCode.get(column);
|
|
8464
|
+
if (info) ignoredKnownColumns.push({ column, reason: reasonFor(info), nonEmptyCells: nonEmpty(index) });
|
|
8465
|
+
else if (!source.ignoreUnknownColumns) throw new ImportSourceError(`ERR_IMPORT_UNKNOWN_COLUMN: unknown CSV header "${column}".`);
|
|
8466
|
+
else ignoredUnknownColumns.push({ column, reason: "unknown column ignored by explicit policy", nonEmptyCells: nonEmpty(index) });
|
|
8467
|
+
}
|
|
8468
|
+
for (const code of targetCodes) {
|
|
8469
|
+
const info = infoByCode.get(code);
|
|
8470
|
+
if (!info) throw new Error(`ArgumentError: DML target field ${code} does not exist.`);
|
|
8471
|
+
if (info.inSubtable || info.writable === false || info.fieldType === "FILE" || info.fieldType === "SUBTABLE") {
|
|
8472
|
+
throw new Error(`ArgumentError: DML target field ${code} is not writable (${info.fieldType}).`);
|
|
8473
|
+
}
|
|
8474
|
+
}
|
|
8475
|
+
const importRowErrors = [];
|
|
8476
|
+
const rows2 = decoded.rows.map((values) => {
|
|
8477
|
+
const errors = [];
|
|
8478
|
+
const row = {};
|
|
8479
|
+
for (const code of targetCodes) {
|
|
8480
|
+
const raw = values[indexes.get(code)];
|
|
8481
|
+
try {
|
|
8482
|
+
row[code] = convertImportCsvValue(raw, infoByCode.get(code)?.fieldType, { cliKintone: true });
|
|
8483
|
+
} catch (error) {
|
|
8484
|
+
if (!(error instanceof ImportCsvValueError)) throw error;
|
|
8485
|
+
row[code] = raw;
|
|
8486
|
+
errors.push({ field: code, code: error.code, message: error.message });
|
|
8487
|
+
}
|
|
8488
|
+
}
|
|
8489
|
+
importRowErrors.push(errors);
|
|
8490
|
+
return row;
|
|
8491
|
+
});
|
|
8492
|
+
return {
|
|
8493
|
+
rows: rows2,
|
|
8494
|
+
columns: [...targetCodes],
|
|
8495
|
+
columnMeta: new Map(targetCodes.map((code) => [code, { fieldType: infoByCode.get(code)?.fieldType ?? "SINGLE_LINE_TEXT" }])),
|
|
8496
|
+
importRowErrors,
|
|
8497
|
+
...recordNumberSourceHeader ? { recordNumberSourceValues: decoded.rows.map((row) => row[indexes.get(recordNumberSourceHeader)]) } : {},
|
|
8498
|
+
importAudit: { mapping: "BY_NAME", writtenColumns: [...targetCodes], ignoredKnownColumns, ignoredUnknownColumns }
|
|
8499
|
+
};
|
|
8500
|
+
}
|
|
8501
|
+
const rows = decoded.rows.map((values) => Object.fromEntries(decoded.columns.map((column, i) => [column, values[i]])));
|
|
8502
|
+
return {
|
|
8503
|
+
rows,
|
|
8504
|
+
columns: decoded.columns,
|
|
8505
|
+
// CSV cells are decoded as strings; projections such as CAST may replace this metadata downstream.
|
|
8506
|
+
columnMeta: new Map(decoded.columns.map((column) => [column, { fieldType: "SINGLE_LINE_TEXT" }]))
|
|
8507
|
+
};
|
|
8508
|
+
}
|
|
8509
|
+
|
|
8510
|
+
// src/import/importRecordsMaterializer.ts
|
|
8511
|
+
var sourceFail = (parentRow, code, message) => {
|
|
8512
|
+
throw new ImportSourceError(`JSON subtable validation failed (parentRow=${parentRow}, field=${code}): ${message}`);
|
|
8513
|
+
};
|
|
8514
|
+
function materializeJsonImportRecords(_source, payload, targets, maxParents, maxChildRows = maxParents) {
|
|
8515
|
+
if (payload.encoding && payload.encoding !== "utf8") throw new ImportSourceError("JSON source is UTF-8 only.");
|
|
8516
|
+
const decoded = decodeJsonRecords(payload.bytes);
|
|
8517
|
+
if (decoded.length > maxParents) throw new ImportSourceError(`source parent rows (${decoded.length}) exceed maxRecords (${maxParents}).`);
|
|
8518
|
+
const targetByCode = new Map(targets.map((target) => [target.kind === "FIELD" ? target.field : target.subtableCode, target]));
|
|
8519
|
+
if (targetByCode.size !== targets.length) throw new ImportSourceError("IMPORT targets contain duplicates.");
|
|
8520
|
+
let childTotal = 0;
|
|
8521
|
+
return {
|
|
8522
|
+
records: decoded.map((record, index) => {
|
|
8523
|
+
const parentRow = index + 1;
|
|
8524
|
+
for (const code of record.keys()) if (!targetByCode.has(code)) sourceFail(parentRow, code, "unknown key (not declared in INTO).");
|
|
8525
|
+
const top = /* @__PURE__ */ new Map();
|
|
8526
|
+
const subtables = /* @__PURE__ */ new Map();
|
|
8527
|
+
const replacementTables = /* @__PURE__ */ new Set();
|
|
8528
|
+
for (const target of targets) {
|
|
8529
|
+
const code = target.kind === "FIELD" ? target.field : target.subtableCode;
|
|
8530
|
+
if (!record.has(code)) continue;
|
|
8531
|
+
const value = record.get(code);
|
|
8532
|
+
if (target.kind === "FIELD") {
|
|
8533
|
+
if (value instanceof Map) sourceFail(parentRow, code, "object is not accepted for a top-level field.");
|
|
8534
|
+
top.set(code, value);
|
|
8535
|
+
continue;
|
|
8536
|
+
}
|
|
8537
|
+
if (!Array.isArray(value)) sourceFail(parentRow, code, "subtable value must be an array.");
|
|
8538
|
+
replacementTables.add(code);
|
|
8539
|
+
const children = new Set(target.children);
|
|
8540
|
+
const rows = value.map((entry, childIndex) => {
|
|
8541
|
+
if (!(entry instanceof Map)) sourceFail(parentRow, code, `childRow=${childIndex + 1} must be an object.`);
|
|
8542
|
+
const child = entry;
|
|
8543
|
+
for (const childCode of child.keys()) {
|
|
8544
|
+
if (!children.has(childCode)) sourceFail(parentRow, childCode, `unknown child key in subtable ${code} at childRow=${childIndex + 1}.`);
|
|
8545
|
+
}
|
|
8546
|
+
childTotal++;
|
|
8547
|
+
if (childTotal > maxChildRows) throw new ImportSourceError(`source child rows (${childTotal}) exceed limit (${maxChildRows}).`);
|
|
8548
|
+
return { childRowNumber: childIndex + 1, values: child };
|
|
8549
|
+
});
|
|
8550
|
+
subtables.set(code, rows);
|
|
8551
|
+
}
|
|
8552
|
+
return { rowNumber: parentRow, top, subtables, replacementTables };
|
|
8553
|
+
})
|
|
8554
|
+
};
|
|
8555
|
+
}
|
|
8556
|
+
function materializeCliKintoneCsvImportRecords(source, payload, targets, replacementTables, maxParents, recordNumberSourceHeader) {
|
|
8557
|
+
const decoded = decodeCsv(payload.bytes, {
|
|
8558
|
+
encoding: source.encoding ?? payload.encoding ?? "utf8",
|
|
8559
|
+
hasHeader: source.hasHeader,
|
|
8560
|
+
columns: source.columns
|
|
8561
|
+
});
|
|
8562
|
+
if (!source.hasHeader || decoded.columns[0] !== "*") throw new ImportSourceError('ERR_IMPORT_MARKER: first CSV header must be "*".');
|
|
8563
|
+
const indexes = new Map(decoded.columns.map((code, index) => [code, index]));
|
|
8564
|
+
if (recordNumberSourceHeader && !indexes.has(recordNumberSourceHeader)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${recordNumberSourceHeader}" is missing.`);
|
|
8565
|
+
const fields = targets.filter((target) => target.kind === "FIELD");
|
|
8566
|
+
const tables = targets.filter((target) => target.kind === "SUBTABLE");
|
|
8567
|
+
for (const field of fields) if (!indexes.has(field.field)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required header "${field.field}" is missing.`);
|
|
8568
|
+
for (const table of tables) {
|
|
8569
|
+
if (!table.rowIdSourceHeader || !indexes.has(table.rowIdSourceHeader)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: row-ID header for ${table.subtableCode} is missing.`);
|
|
8570
|
+
for (const child of table.children) if (!indexes.has(child)) throw new ImportSourceError(`ERR_IMPORT_MISSING_COLUMN: required child header "${child}" is missing.`);
|
|
8571
|
+
}
|
|
8572
|
+
const records = [];
|
|
8573
|
+
let current;
|
|
8574
|
+
decoded.rows.forEach((cells, physicalIndex) => {
|
|
8575
|
+
const sourceRowNumber = physicalIndex + 2;
|
|
8576
|
+
const marker = cells[0];
|
|
8577
|
+
if (marker !== "" && marker !== "*") throw new ImportSourceError(`ERR_IMPORT_MARKER: invalid marker ${JSON.stringify(marker)} at source row ${sourceRowNumber}.`);
|
|
8578
|
+
if (marker === "*") {
|
|
8579
|
+
if (records.length >= maxParents) throw new ImportSourceError(`source parent rows exceed maxRecords (${maxParents}).`);
|
|
8580
|
+
current = {
|
|
8581
|
+
rowNumber: records.length + 1,
|
|
8582
|
+
markerRowNumber: sourceRowNumber,
|
|
8583
|
+
top: new Map(fields.map((field) => [field.field, cells[indexes.get(field.field)]])),
|
|
8584
|
+
subtables: new Map(tables.map((table) => [table.subtableCode, []])),
|
|
8585
|
+
replacementTables: new Set(replacementTables),
|
|
8586
|
+
...recordNumberSourceHeader ? { recordNumberSourceValue: cells[indexes.get(recordNumberSourceHeader)] } : {}
|
|
8587
|
+
};
|
|
8588
|
+
records.push(current);
|
|
8589
|
+
} else if (!current) {
|
|
8590
|
+
throw new ImportSourceError(`ERR_IMPORT_MARKER: first data row must start a parent (source row ${sourceRowNumber}).`);
|
|
8591
|
+
} else {
|
|
8592
|
+
for (const field of fields) {
|
|
8593
|
+
const continuationValue = cells[indexes.get(field.field)];
|
|
8594
|
+
if (continuationValue !== "" && continuationValue !== current.top.get(field.field)) {
|
|
8595
|
+
throw new ImportSourceError(`ERR_IMPORT_PARENT_VALUE_ON_CONTINUATION: ${field.field} at source row ${sourceRowNumber}.`);
|
|
8596
|
+
}
|
|
8597
|
+
}
|
|
8598
|
+
if (recordNumberSourceHeader) {
|
|
8599
|
+
const continuationValue = cells[indexes.get(recordNumberSourceHeader)];
|
|
8600
|
+
if (continuationValue !== "" && continuationValue !== current.recordNumberSourceValue) {
|
|
8601
|
+
throw new ImportSourceError(`ERR_IMPORT_PARENT_VALUE_ON_CONTINUATION: ${recordNumberSourceHeader} at source row ${sourceRowNumber}.`);
|
|
8602
|
+
}
|
|
8603
|
+
}
|
|
8604
|
+
}
|
|
8605
|
+
for (const table of tables) {
|
|
8606
|
+
const rowId = cells[indexes.get(table.rowIdSourceHeader)];
|
|
8607
|
+
const values = new Map(table.children.map((child) => [child, cells[indexes.get(child)]]));
|
|
8608
|
+
if (rowId === "" && [...values.values()].every((value) => value === "")) continue;
|
|
8609
|
+
const rows = current.subtables.get(table.subtableCode);
|
|
8610
|
+
rows.push({ childRowNumber: rows.length + 1, sourceRowNumber, ...rowId ? { rowId } : {}, values });
|
|
8611
|
+
}
|
|
8612
|
+
});
|
|
8613
|
+
if (records.length === 0) throw new ImportSourceError("CSV has no parent rows.");
|
|
8614
|
+
return { records };
|
|
8615
|
+
}
|
|
8616
|
+
|
|
8617
|
+
// src/import/importRecordValidation.ts
|
|
8618
|
+
var USER_TYPES3 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
8619
|
+
var UNSUPPORTED_CHILD_TYPES = /* @__PURE__ */ new Set(["SUBTABLE", "FILE", "CALC", "RECORD_NUMBER", "CREATOR", "CREATED_TIME", "MODIFIER", "UPDATED_TIME", "STATUS", "STATUS_ASSIGNEE", "CATEGORY", "REFERENCE_TABLE"]);
|
|
8620
|
+
function assertImportRejectLimit(prepared, rejectLimit) {
|
|
8621
|
+
if (rejectLimit != null && prepared.invalidParentRows.size > rejectLimit) {
|
|
8622
|
+
throw new Error(`RejectLimitExceededError: rejected parents (${prepared.invalidParentRows.size}) exceed REJECT LIMIT (${rejectLimit}).`);
|
|
8623
|
+
}
|
|
8624
|
+
}
|
|
8625
|
+
function prepareImportRecords(materialized, targets, fieldInfos, numberPrecision, operation) {
|
|
8626
|
+
const topInfos = new Map(fieldInfos.filter((f) => !f.inSubtable).map((f) => [f.code, f]));
|
|
8627
|
+
const scoped = /* @__PURE__ */ new Map();
|
|
8628
|
+
for (const info of fieldInfos) if (info.inSubtable && info.subtableCode) {
|
|
8629
|
+
let children = scoped.get(info.subtableCode);
|
|
8630
|
+
if (!children) scoped.set(info.subtableCode, children = /* @__PURE__ */ new Map());
|
|
8631
|
+
children.set(info.code, info);
|
|
8632
|
+
}
|
|
8633
|
+
const targetTop = targets.filter((t) => t.kind === "FIELD");
|
|
8634
|
+
const targetTables = targets.filter((t) => t.kind === "SUBTABLE");
|
|
8635
|
+
for (const target of targetTop) assertWritable(target.field, topInfos.get(target.field), void 0);
|
|
8636
|
+
for (const target of targetTables) {
|
|
8637
|
+
const table = topInfos.get(target.subtableCode);
|
|
8638
|
+
if (!table || table.fieldType !== "SUBTABLE") throw new Error(`ArgumentError: IMPORT subtable ${target.subtableCode} does not exist.`);
|
|
8639
|
+
const children = scoped.get(target.subtableCode) ?? /* @__PURE__ */ new Map();
|
|
8640
|
+
for (const child of target.children) assertWritable(child, children.get(child), target.subtableCode);
|
|
8641
|
+
}
|
|
8642
|
+
const tableCounts = new Map(targetTables.map((t) => [t.subtableCode, { parentsPresent: 0, childRows: 0, validChildRows: 0, invalidChildRows: 0 }]));
|
|
8643
|
+
const parents = materialized.records.map((record) => validateParent(record, targetTop, targetTables, topInfos, scoped, numberPrecision, operation, tableCounts));
|
|
8644
|
+
const errors = parents.flatMap((parent) => [...parent.errors]);
|
|
8645
|
+
return { parents, errors, invalidParentRows: new Set(parents.filter((p) => !p.valid).map((p) => p.parentRow)), tableCounts };
|
|
8646
|
+
}
|
|
8647
|
+
function validateParent(source, topTargets, tableTargets, topInfos, scoped, precision, operation, tableCounts) {
|
|
8648
|
+
const errors = [];
|
|
8649
|
+
const top = {};
|
|
8650
|
+
for (const target of topTargets) {
|
|
8651
|
+
if (!source.top.has(target.field)) continue;
|
|
8652
|
+
validateValue(source.top.get(target.field), topInfos.get(target.field), precision, top, target.field, errors, location(source, operation, target.field));
|
|
8653
|
+
}
|
|
8654
|
+
const createValidationOnly = {};
|
|
8655
|
+
if (operation === "INSERT") for (const info of topInfos.values()) {
|
|
8656
|
+
if (info.fieldType === "SUBTABLE" || info.writable === false || source.top.has(info.code)) continue;
|
|
8657
|
+
validateMissing(info, precision, createValidationOnly, errors, location(source, operation, info.code));
|
|
8658
|
+
}
|
|
8659
|
+
const subtables = /* @__PURE__ */ new Map();
|
|
8660
|
+
for (const target of tableTargets) {
|
|
8661
|
+
if (!source.subtables.has(target.subtableCode)) continue;
|
|
8662
|
+
const count = tableCounts.get(target.subtableCode);
|
|
8663
|
+
count.parentsPresent++;
|
|
8664
|
+
const preparedRows = [];
|
|
8665
|
+
for (const child of source.subtables.get(target.subtableCode)) {
|
|
8666
|
+
count.childRows++;
|
|
8667
|
+
const before = errors.length;
|
|
8668
|
+
const record = {};
|
|
8669
|
+
const infos = scoped.get(target.subtableCode);
|
|
8670
|
+
for (const code of target.children) {
|
|
8671
|
+
const info = infos.get(code);
|
|
8672
|
+
const loc = location(source, operation, code, target.subtableCode, child.childRowNumber, child.sourceRowNumber ?? source.markerRowNumber);
|
|
8673
|
+
if (child.values.has(code)) validateValue(child.values.get(code), info, precision, record, code, errors, loc);
|
|
8674
|
+
else validateMissing(info, precision, record, errors, loc);
|
|
8675
|
+
}
|
|
8676
|
+
if (errors.length === before) {
|
|
8677
|
+
count.validChildRows++;
|
|
8678
|
+
preparedRows.push(record);
|
|
8679
|
+
} else count.invalidChildRows++;
|
|
8680
|
+
}
|
|
8681
|
+
subtables.set(target.subtableCode, preparedRows);
|
|
8682
|
+
}
|
|
8683
|
+
return { parentRow: source.rowNumber, valid: errors.length === 0, top, subtables, replacementTables: source.replacementTables, errors };
|
|
8684
|
+
}
|
|
8685
|
+
function assertWritable(code, info, table) {
|
|
8686
|
+
if (!info) throw new Error(table ? `ArgumentError: IMPORT child ${code} does not belong to subtable ${table}.` : `ArgumentError: IMPORT top-level field ${code} does not exist.`);
|
|
8687
|
+
if (info.writable === false || table && UNSUPPORTED_CHILD_TYPES.has(info.fieldType)) {
|
|
8688
|
+
throw new Error(`ArgumentError: IMPORT ${table ? `child ${table}.${code}` : `field ${code}`} is not writable (${info.fieldType}).`);
|
|
8689
|
+
}
|
|
8690
|
+
}
|
|
8691
|
+
function validateMissing(info, precision, record, errors, loc) {
|
|
8692
|
+
const raw = isEmptyDmlValue(info.defaultValue) ? "" : info.defaultValue;
|
|
8693
|
+
validateValue(raw, info, precision, record, info.code, errors, loc, !isEmptyDmlValue(info.defaultValue));
|
|
8694
|
+
}
|
|
8695
|
+
function validateValue(raw, info, precision, record, code, errors, loc, isDefault = false) {
|
|
8696
|
+
const normalizedRaw = decodeRaw(raw);
|
|
8697
|
+
const result = validateAndNormalizeDmlValue(normalizedRaw, info, precision);
|
|
8698
|
+
if (!result.ok) errors.push({ ...loc, code: result.code, message: isDefault ? `\u65E2\u5B9A\u5024: ${result.message}` : result.message });
|
|
8699
|
+
else record[code] = { value: preserveUserCodes(normalizedRaw, info) ? normalizedRaw : result.value };
|
|
8700
|
+
}
|
|
8701
|
+
function decodeRaw(raw) {
|
|
8702
|
+
if (isJsonNumber(raw)) return raw.lexeme;
|
|
8703
|
+
if (Array.isArray(raw)) return raw.map((value) => value instanceof Map ? value : isJsonNumber(value) ? value.lexeme : value);
|
|
8704
|
+
return raw;
|
|
8705
|
+
}
|
|
8706
|
+
function isJsonNumber(raw) {
|
|
8707
|
+
return typeof raw === "object" && raw !== null && raw.kind === "number";
|
|
8708
|
+
}
|
|
8709
|
+
function preserveUserCodes(raw, info) {
|
|
8710
|
+
return USER_TYPES3.has(info.fieldType) && Array.isArray(raw) && raw.every((v) => typeof v === "object" && v !== null && "code" in v);
|
|
8711
|
+
}
|
|
8712
|
+
function location(source, operation, field, subtable, subrow, sourceRow) {
|
|
8713
|
+
const physicalRow = sourceRow ?? source.markerRowNumber;
|
|
8714
|
+
return { operation, parentRow: source.rowNumber, field, ...subtable ? { subtable } : {}, ...subrow == null ? {} : { subrow }, ...physicalRow == null ? {} : { sourceRow: physicalRow }, sourceValues: subtable ? source.subtables.get(subtable)?.[subrow - 1]?.values ?? /* @__PURE__ */ new Map() : source.top };
|
|
8715
|
+
}
|
|
8716
|
+
|
|
8717
|
+
// src/import/importErrors.ts
|
|
8718
|
+
var IMPORT_VALIDATION_META_COLUMNS = [
|
|
8719
|
+
"$err_statement",
|
|
8720
|
+
"$err_operation",
|
|
8721
|
+
"$err_row",
|
|
8722
|
+
"$err_field",
|
|
8723
|
+
"$err_subtable",
|
|
8724
|
+
"$err_subrow",
|
|
8725
|
+
"$err_source_row",
|
|
8726
|
+
"$err_code",
|
|
8727
|
+
"$err_message"
|
|
8728
|
+
];
|
|
8729
|
+
function materializeImportValidationErrors(errors, payloadFields, statementNumber = 1) {
|
|
8730
|
+
return errors.map((error) => {
|
|
8731
|
+
const row = {};
|
|
8732
|
+
for (const field of payloadFields) row[field] = error.sourceValues.get(field) == null ? "" : render(error.sourceValues.get(field));
|
|
8733
|
+
row["$err_statement"] = String(statementNumber);
|
|
8734
|
+
row["$err_operation"] = error.operation;
|
|
8735
|
+
row["$err_row"] = String(error.parentRow);
|
|
8736
|
+
row["$err_field"] = error.field;
|
|
8737
|
+
row["$err_subtable"] = error.subtable ?? "";
|
|
8738
|
+
row["$err_subrow"] = error.subrow == null ? "" : String(error.subrow);
|
|
8739
|
+
row["$err_source_row"] = error.sourceRow == null ? null : String(error.sourceRow);
|
|
8740
|
+
row["$err_code"] = error.code;
|
|
8741
|
+
row["$err_message"] = error.message;
|
|
8742
|
+
return row;
|
|
8743
|
+
});
|
|
8744
|
+
}
|
|
8745
|
+
function render(value) {
|
|
8746
|
+
if (value === null || value === void 0) return "";
|
|
8747
|
+
if (typeof value === "object" && value !== null && "kind" in value && "lexeme" in value && value.kind === "number") {
|
|
8748
|
+
return String(value.lexeme);
|
|
8749
|
+
}
|
|
8750
|
+
if (Array.isArray(value)) return JSON.stringify(value);
|
|
8751
|
+
return String(value);
|
|
8752
|
+
}
|
|
8753
|
+
|
|
8754
|
+
// src/import/subtablePayload.ts
|
|
8755
|
+
function buildImportRecordPayload(top, subtables, rowIdMode) {
|
|
8756
|
+
const record = {};
|
|
8757
|
+
for (const [code, value] of top) record[code] = { value };
|
|
8758
|
+
for (const [tableCode, sourceRows] of subtables) {
|
|
8759
|
+
record[tableCode] = {
|
|
8760
|
+
value: sourceRows.map((sourceRow) => ({
|
|
8761
|
+
...rowIdMode === "PRESERVE" && sourceRow.rowId ? { id: sourceRow.rowId } : {},
|
|
8762
|
+
value: Object.fromEntries([...sourceRow.values].map(([childCode, value]) => [childCode, { value }]))
|
|
8763
|
+
}))
|
|
8764
|
+
};
|
|
8765
|
+
}
|
|
8766
|
+
return record;
|
|
8767
|
+
}
|
|
8768
|
+
function buildJsonImportRecordPayload(top, subtables) {
|
|
8769
|
+
return buildImportRecordPayload(top, subtables, "DROP");
|
|
8770
|
+
}
|
|
8771
|
+
|
|
8772
|
+
// src/import/jsonSubtableWritePlan.ts
|
|
8773
|
+
function assertJsonImportHasNoRowIds(materialized) {
|
|
8774
|
+
for (const parent of materialized.records) for (const [table, rows] of parent.subtables) {
|
|
8775
|
+
for (const row of rows) {
|
|
8776
|
+
if (row.rowId !== void 0 || row.values.has("_rid") || row.values.has("id")) {
|
|
8777
|
+
throw new Error(`ArgumentError: JSON IMPORT subtable ${table} does not accept _rid/id; rows are always newly numbered.`);
|
|
8778
|
+
}
|
|
8779
|
+
}
|
|
8780
|
+
}
|
|
8781
|
+
}
|
|
8782
|
+
function buildJsonSubtableWritePlan(parents, targetIds, existingById) {
|
|
8783
|
+
return parents.map((parent, index) => {
|
|
8784
|
+
const targetId = targetIds[index];
|
|
8785
|
+
const existing = targetId === void 0 ? void 0 : existingById.get(targetId);
|
|
8786
|
+
if (targetId !== void 0 && !existing) throw new Error(`InternalError: IMPORT UPSERT target APP record ${targetId} was not loaded.`);
|
|
8787
|
+
const tables = [...parent.subtables].map(([table, input]) => {
|
|
8788
|
+
const raw = existing?.record[table]?.value;
|
|
8789
|
+
const existingRows = Array.isArray(raw) ? raw.length : 0;
|
|
8790
|
+
return { table, existingRows, inputRows: input.length, addRows: input.length, deleteRows: existingRows };
|
|
8791
|
+
});
|
|
8792
|
+
return {
|
|
8793
|
+
parentRow: parent.parentRow,
|
|
8794
|
+
mode: targetId === void 0 ? "INSERT" : "UPDATE",
|
|
8795
|
+
...targetId === void 0 ? {} : { targetId, revision: existing?.revision },
|
|
8796
|
+
top: parent.top,
|
|
8797
|
+
subtables: parent.subtables,
|
|
8798
|
+
tables
|
|
8799
|
+
};
|
|
8800
|
+
});
|
|
8801
|
+
}
|
|
8802
|
+
|
|
8803
|
+
// src/import/subtableReplacementPlan.ts
|
|
8804
|
+
function tableRows(record, table) {
|
|
8805
|
+
const raw = record[table]?.value;
|
|
8806
|
+
return Array.isArray(raw) ? raw : [];
|
|
8807
|
+
}
|
|
8808
|
+
function assertNoDuplicateCsvSubtableRowIds(records) {
|
|
8809
|
+
const seen = /* @__PURE__ */ new Map();
|
|
8810
|
+
for (const parent of records) for (const [table, rows] of parent.subtables) for (const row of rows) {
|
|
8811
|
+
if (!row.rowId) continue;
|
|
8812
|
+
const key = `${table}\0${row.rowId}`;
|
|
8813
|
+
if (seen.has(key)) throw new Error(`ERR_SUBTABLE_ROW_ID_DUP_SOURCE: duplicate row ID ${row.rowId} in ${table}`);
|
|
8814
|
+
seen.set(key, parent.rowNumber);
|
|
8815
|
+
}
|
|
8816
|
+
}
|
|
8817
|
+
function buildCsvSubtableReplacementPlan(sources, prepared, targetIds, existingById, ownership) {
|
|
8818
|
+
return prepared.map((parent, index) => {
|
|
8819
|
+
const source = sources[index];
|
|
8820
|
+
const targetId = targetIds[index];
|
|
8821
|
+
const existing = targetId === void 0 ? void 0 : existingById.get(targetId);
|
|
8822
|
+
const errors = [...parent.errors];
|
|
8823
|
+
if (!existing || targetId === void 0) return { parentRow: parent.parentRow, targetId: targetId ?? 0, valid: false, top: parent.top, subtables: /* @__PURE__ */ new Map(), tables: [], errors };
|
|
8824
|
+
const subtables = /* @__PURE__ */ new Map();
|
|
8825
|
+
const tables = [];
|
|
8826
|
+
for (const table of parent.replacementTables) {
|
|
8827
|
+
const current = tableRows(existing.record, table);
|
|
8828
|
+
const currentIds = new Set(current.map((row) => row.id).filter((id) => !!id));
|
|
8829
|
+
const input = source.subtables.get(table) ?? [];
|
|
8830
|
+
const normalized = parent.subtables.get(table) ?? [];
|
|
8831
|
+
let updateRows = 0, addRows = 0, rowIdNotFound = 0;
|
|
8832
|
+
const payloadRows = input.map((row, rowIndex) => {
|
|
8833
|
+
const normalizedRecord = normalized[rowIndex] ?? {};
|
|
8834
|
+
if (row.rowId && currentIds.has(row.rowId)) {
|
|
8835
|
+
updateRows++;
|
|
8836
|
+
return { rowId: row.rowId, record: normalizedRecord };
|
|
8837
|
+
}
|
|
8838
|
+
if (row.rowId) {
|
|
8839
|
+
const owners = ownership.get(row.rowId) ?? [];
|
|
8840
|
+
if (owners.some((owner) => owner.parentId !== targetId || owner.table !== table)) errors.push({
|
|
8841
|
+
operation: "UPDATE",
|
|
8842
|
+
parentRow: parent.parentRow,
|
|
8843
|
+
field: row.rowId,
|
|
8844
|
+
subtable: table,
|
|
8845
|
+
subrow: row.childRowNumber,
|
|
8846
|
+
sourceRow: row.sourceRowNumber,
|
|
8847
|
+
code: "ERR_IMPORT_FIELD_OWNERSHIP",
|
|
8848
|
+
message: `rowIdOwnedElsewhere: ${row.rowId}`,
|
|
8849
|
+
sourceValues: row.values
|
|
8850
|
+
});
|
|
8851
|
+
rowIdNotFound++;
|
|
8852
|
+
}
|
|
8853
|
+
addRows++;
|
|
8854
|
+
return { record: normalizedRecord };
|
|
8855
|
+
});
|
|
8856
|
+
subtables.set(table, payloadRows);
|
|
8857
|
+
tables.push({ table, existingRows: current.length, inputRows: input.length, updateRows, addRows, deleteRows: current.length - updateRows, rowIdNotFound });
|
|
8858
|
+
}
|
|
8859
|
+
return { parentRow: parent.parentRow, targetId, ...existing.revision === void 0 ? {} : { revision: existing.revision }, valid: errors.length === 0, top: parent.top, subtables, tables, errors };
|
|
8860
|
+
});
|
|
8861
|
+
}
|
|
8862
|
+
|
|
8863
|
+
// src/import/importProjection.ts
|
|
8864
|
+
var IMPORT_PROJECTION_SOURCE = "#__import_source";
|
|
8865
|
+
function bindImportProjection(projection) {
|
|
8866
|
+
return { ...projection, from: { appId: 0, alias: null, cteName: IMPORT_PROJECTION_SOURCE } };
|
|
8867
|
+
}
|
|
8868
|
+
|
|
8869
|
+
// src/import/recordNumberUpdate.ts
|
|
8870
|
+
function normalizeImportRecordNumber(raw) {
|
|
8871
|
+
return /^[0-9]+$/.test(raw) ? raw.replace(/^0+(?=\d)/, "") : null;
|
|
8872
|
+
}
|
|
8873
|
+
function preflightImportRecordNumbers(values, header) {
|
|
8874
|
+
const normalized = values.map(normalizeImportRecordNumber);
|
|
8875
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8876
|
+
for (const key of normalized) {
|
|
8877
|
+
if (key === null) continue;
|
|
8878
|
+
if (seen.has(key)) {
|
|
8879
|
+
throw new Error("ERR_RECORD_NUMBER_DUP_SOURCE: source contains a duplicate record number");
|
|
8880
|
+
}
|
|
8881
|
+
seen.add(key);
|
|
8882
|
+
}
|
|
8883
|
+
return {
|
|
8884
|
+
normalized,
|
|
8885
|
+
errors: normalized.map((key) => key === null ? [{
|
|
8886
|
+
field: header,
|
|
8887
|
+
code: "ERR_RECORD_NUMBER_INVALID",
|
|
8888
|
+
message: `${header} must be a non-empty ASCII decimal record number`
|
|
8889
|
+
}] : [])
|
|
8890
|
+
};
|
|
8891
|
+
}
|
|
8892
|
+
|
|
7820
8893
|
// src/execute.ts
|
|
7821
8894
|
var SEARCH_ABORTED_WARNING = "\u691C\u7D22\u304C 10 \u4E07\u4EF6\u3067\u6253\u3061\u5207\u3089\u308C\u3001\u7D50\u679C\u304C\u6B20\u843D\u3057\u305F\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002";
|
|
7822
8895
|
var SearchAbortedError = class extends Error {
|
|
@@ -7827,6 +8900,7 @@ var SearchAbortedError = class extends Error {
|
|
|
7827
8900
|
};
|
|
7828
8901
|
var materializedMetaBySelectResult = /* @__PURE__ */ new WeakMap();
|
|
7829
8902
|
var materializedMetaByValidationResult = /* @__PURE__ */ new WeakMap();
|
|
8903
|
+
var importSourceByDmlStatement = /* @__PURE__ */ new WeakMap();
|
|
7830
8904
|
var defaultCacheContextByClient = /* @__PURE__ */ new WeakMap();
|
|
7831
8905
|
var nextDefaultCacheContextId = 1;
|
|
7832
8906
|
function resolveCacheContext(client, explicit) {
|
|
@@ -7841,7 +8915,7 @@ function resolveCacheContext(client, explicit) {
|
|
|
7841
8915
|
async function execute(sql, client, options = {}) {
|
|
7842
8916
|
const startedAt = Date.now();
|
|
7843
8917
|
const cacheContext = resolveCacheContext(client, options.cacheContext);
|
|
7844
|
-
const stmt = parseSql(sql);
|
|
8918
|
+
const stmt = parseSql(sql, options.enableImport === true);
|
|
7845
8919
|
const metrics = createEmptyMetrics();
|
|
7846
8920
|
const countedClient = wrapClientWithMetrics(client, metrics);
|
|
7847
8921
|
const collector = { aborted: false };
|
|
@@ -8021,6 +9095,7 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
|
|
|
8021
9095
|
throw new Error(`ParseError: variable @${unresolved} is not defined in a batch.`);
|
|
8022
9096
|
}
|
|
8023
9097
|
validateKlikeStatement(stmt);
|
|
9098
|
+
if (stmt.type === "IMPORT") return executeImport(stmt, client, options, cacheContext);
|
|
8024
9099
|
if ("validateOnly" in stmt && stmt.validateOnly === true) {
|
|
8025
9100
|
if (stmt.validationErrorTable) {
|
|
8026
9101
|
throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
@@ -8243,7 +9318,7 @@ var BatchTimeoutError = class extends Error {
|
|
|
8243
9318
|
}
|
|
8244
9319
|
};
|
|
8245
9320
|
async function executeBatch(sql, client, options = {}) {
|
|
8246
|
-
const statements = parseSqlBatch(sql);
|
|
9321
|
+
const statements = parseSqlBatch(sql, options.enableImport === true);
|
|
8247
9322
|
const analysis = analyzeBatch(statements);
|
|
8248
9323
|
const injectedVariables = validateDeclaredBatchVariables(statements, options.variables);
|
|
8249
9324
|
const batchOptions = { ...options, variables: injectedVariables };
|
|
@@ -8296,11 +9371,12 @@ async function executeBatch(sql, client, options = {}) {
|
|
|
8296
9371
|
const userConfirm = batchOptions.confirm;
|
|
8297
9372
|
const stmtOptions = userConfirm ? {
|
|
8298
9373
|
...batchOptions,
|
|
8299
|
-
confirm: (count, operation) => userConfirm(count, operation, {
|
|
9374
|
+
confirm: (count, operation, detailContext) => userConfirm(count, operation, {
|
|
8300
9375
|
statementIndex: i,
|
|
8301
9376
|
statementCount: statements.length,
|
|
8302
9377
|
statementType: info.statementType,
|
|
8303
|
-
targetAppId: info.targetAppId
|
|
9378
|
+
targetAppId: info.targetAppId,
|
|
9379
|
+
...detailContext?.importDetail ? { importDetail: detailContext.importDetail } : {}
|
|
8304
9380
|
})
|
|
8305
9381
|
} : batchOptions;
|
|
8306
9382
|
const searchAbortCollector = { aborted: false };
|
|
@@ -8414,6 +9490,9 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
|
|
|
8414
9490
|
}
|
|
8415
9491
|
return { result };
|
|
8416
9492
|
}
|
|
9493
|
+
if (resolvedStmt.type === "IMPORT") {
|
|
9494
|
+
return { result: await executeImport(resolvedStmt, client, options, cacheContext, tempTables) };
|
|
9495
|
+
}
|
|
8417
9496
|
if ("validateOnly" in resolvedStmt && resolvedStmt.validateOnly === true) {
|
|
8418
9497
|
const result = await executeDmlValidation(
|
|
8419
9498
|
resolvedStmt,
|
|
@@ -8575,9 +9654,9 @@ function safeJsonStringify(v) {
|
|
|
8575
9654
|
return String(v);
|
|
8576
9655
|
}
|
|
8577
9656
|
}
|
|
8578
|
-
function parseSqlBatch(sql) {
|
|
9657
|
+
function parseSqlBatch(sql, enableImport = false) {
|
|
8579
9658
|
const tokens = new Lexer(sql).tokenize();
|
|
8580
|
-
return new Parser(tokens).parseStatements();
|
|
9659
|
+
return new Parser(tokens, { import: enableImport }).parseStatements();
|
|
8581
9660
|
}
|
|
8582
9661
|
function evaluateScalarExpr(expr) {
|
|
8583
9662
|
switch (expr.type) {
|
|
@@ -9646,8 +10725,14 @@ async function inferSelectColumnMeta(stmt, outputColumns, client, cacheContext,
|
|
|
9646
10725
|
}
|
|
9647
10726
|
} else if (column.type === "ARITH_AGG_COL" || column.type === "ARITH_COL") {
|
|
9648
10727
|
meta = syntheticColumnMeta("number");
|
|
9649
|
-
} else if (column.type === "LITERAL_COL"
|
|
10728
|
+
} else if (column.type === "LITERAL_COL") {
|
|
9650
10729
|
meta = syntheticColumnMeta("string");
|
|
10730
|
+
} else if (column.type === "SCALAR_VALUE_COL") {
|
|
10731
|
+
const expr = column.expr;
|
|
10732
|
+
if (expr.type === "STRING_FUNC") meta = stringFunctionColumnMeta(expr);
|
|
10733
|
+
else if (expr.type === "NUMBER" || expr.type === "SCALAR_ARITH") meta = syntheticColumnMeta("number");
|
|
10734
|
+
else if (expr.type === "FIELD") meta = resolveField2(expr);
|
|
10735
|
+
else meta = syntheticColumnMeta("string");
|
|
9651
10736
|
} else if (column.type === "STRFUNC_COL") {
|
|
9652
10737
|
meta = stringFunctionColumnMeta(column.expr);
|
|
9653
10738
|
} else if (column.type === "WINDOW_COL") {
|
|
@@ -10461,9 +11546,10 @@ async function buildSortKindsForSelect(stmt, client, cacheContext) {
|
|
|
10461
11546
|
return sortKinds;
|
|
10462
11547
|
}
|
|
10463
11548
|
function convertProcessRowValue(raw, dstFieldType) {
|
|
10464
|
-
|
|
11549
|
+
if (typeof raw !== "string") return raw;
|
|
11550
|
+
const USER_TYPES4 = /* @__PURE__ */ new Set(["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"]);
|
|
10465
11551
|
const ARRAY_TYPES3 = /* @__PURE__ */ new Set(["CHECK_BOX", "MULTI_SELECT"]);
|
|
10466
|
-
if (
|
|
11552
|
+
if (USER_TYPES4.has(dstFieldType ?? "")) {
|
|
10467
11553
|
if (raw === "") return [];
|
|
10468
11554
|
try {
|
|
10469
11555
|
const parsed = JSON.parse(raw);
|
|
@@ -10528,11 +11614,13 @@ function assertValidDmlRecords(records, targetFields, fieldInfos, numberPrecisio
|
|
|
10528
11614
|
records.forEach((record, rowIndex) => {
|
|
10529
11615
|
for (const code of targetFields) {
|
|
10530
11616
|
const info = infoByCode.get(code);
|
|
10531
|
-
const
|
|
11617
|
+
const original = record[code]?.value ?? "";
|
|
11618
|
+
const result = validateAndNormalizeDmlValue(original, info, numberPrecision);
|
|
10532
11619
|
if (!result.ok) {
|
|
10533
11620
|
throw new Error(`DmlValidationError: ${result.code} ${result.message} (row=${rowIndex + 1}, field=${code})`);
|
|
10534
11621
|
}
|
|
10535
|
-
|
|
11622
|
+
const preserveCodes = ["USER_SELECT", "ORGANIZATION_SELECT", "GROUP_SELECT"].includes(info.fieldType) && Array.isArray(original) && original.every((item) => typeof item === "object" && item !== null && "code" in item);
|
|
11623
|
+
record[code] = { value: preserveCodes ? original : result.value };
|
|
10536
11624
|
}
|
|
10537
11625
|
});
|
|
10538
11626
|
}
|
|
@@ -10692,6 +11780,8 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10692
11780
|
if (stmt.type === "UPDATE") return materializeUpdateValidationCandidates(stmt, client, options, cacheContext, tempTables);
|
|
10693
11781
|
let rows;
|
|
10694
11782
|
let sourceRows;
|
|
11783
|
+
let sourcePresence;
|
|
11784
|
+
let sourceRowErrors;
|
|
10695
11785
|
let evaluationTypes;
|
|
10696
11786
|
if (stmt.type === "INSERT" || stmt.type === "UPSERT") {
|
|
10697
11787
|
assertInsertCheckRefs(stmt, stmt.fields);
|
|
@@ -10701,7 +11791,7 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10701
11791
|
(value, i) => value.type === "CASE_VALUE" ? evalCaseWhenValue(value.expr, {}, infoByCode.get(stmt.fields[i])?.fieldType) : value
|
|
10702
11792
|
));
|
|
10703
11793
|
} else {
|
|
10704
|
-
const selectResult =
|
|
11794
|
+
const selectResult = await dmlSourceMaterializer.materialize(stmt, client, options, cacheContext, tempTables, [...infoByCode.values()]);
|
|
10705
11795
|
const hasChecks = (stmt.checkGroups?.length ?? 0) > 0;
|
|
10706
11796
|
if (selectResult.columns.length < stmt.fields.length || !hasChecks && selectResult.columns.length !== stmt.fields.length) {
|
|
10707
11797
|
throw new Error(`SELECT \u306E\u5217\u6570\uFF08${selectResult.columns.length}\uFF09\u3068 DML \u306E\u30D5\u30A3\u30FC\u30EB\u30C9\u6570\uFF08${stmt.fields.length}\uFF09\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093`);
|
|
@@ -10711,7 +11801,9 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10711
11801
|
}
|
|
10712
11802
|
assertInsertCheckRefs(stmt, selectResult.columns);
|
|
10713
11803
|
sourceRows = selectResult.rows;
|
|
10714
|
-
|
|
11804
|
+
sourcePresence = selectResult.importPresence;
|
|
11805
|
+
sourceRowErrors = selectResult.importRowErrors;
|
|
11806
|
+
const meta = selectResult.columnMeta;
|
|
10715
11807
|
evaluationTypes = new Map(selectResult.columns.map((column) => {
|
|
10716
11808
|
const columnMeta = meta?.get(column);
|
|
10717
11809
|
const type = columnMeta?.fieldType ?? (columnMeta?.semantics?.compareMode === "number" || columnMeta?.sortKind === "number" ? "NUMBER" : "SINGLE_LINE_TEXT");
|
|
@@ -10724,8 +11816,10 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10724
11816
|
rowNumber: index + 1,
|
|
10725
11817
|
operation,
|
|
10726
11818
|
mode: "create",
|
|
10727
|
-
payload: new Map(stmt.fields.
|
|
10728
|
-
|
|
11819
|
+
payload: new Map(stmt.fields.flatMap(
|
|
11820
|
+
(field, i) => sourcePresence && !sourcePresence[index]?.has(field) ? [] : [[field, values[i]]]
|
|
11821
|
+
)),
|
|
11822
|
+
preErrors: [...sourceRowErrors?.[index] ?? []],
|
|
10729
11823
|
record: {},
|
|
10730
11824
|
evaluationRow: sourceRows?.[index] ?? Object.fromEntries(
|
|
10731
11825
|
stmt.fields.map((field, i) => [field, renderValidationValue(values[i])])
|
|
@@ -10738,13 +11832,17 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10738
11832
|
}
|
|
10739
11833
|
const fieldTypes = new Map([...infoByCode].map(([code, info]) => [code, info.fieldType]));
|
|
10740
11834
|
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
11835
|
const numeric = stmt.keyFields.map((key) => fieldTypes.get(key) === "NUMBER");
|
|
10743
11836
|
const keyCounts = /* @__PURE__ */ new Map();
|
|
10744
11837
|
for (const parts of rowKeys) {
|
|
10745
11838
|
const key = upsertNormalizedKey(parts, numeric);
|
|
10746
11839
|
keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
|
|
10747
11840
|
}
|
|
11841
|
+
const isImport = importSourceByDmlStatement.has(stmt);
|
|
11842
|
+
if (isImport && [...keyCounts.values()].some((count) => count > 1)) {
|
|
11843
|
+
throw new Error("ERR_KEY_DUP_SOURCE: UPSERT \u30BD\u30FC\u30B9\u5185\u3067\u30AD\u30FC\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059");
|
|
11844
|
+
}
|
|
11845
|
+
const targets = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeys, client, options, fieldTypes);
|
|
10748
11846
|
candidates.forEach((candidate, index) => {
|
|
10749
11847
|
const parts = rowKeys[index];
|
|
10750
11848
|
const targetId = lookupUpsertTarget(targets, parts);
|
|
@@ -10753,7 +11851,7 @@ async function materializeValidationCandidates(stmt, operation, client, options,
|
|
|
10753
11851
|
stmt.keyFields.forEach((key, keyIndex) => {
|
|
10754
11852
|
if (parts[keyIndex] === "") candidate.preErrors.push({ field: key, code: "ERR_KEY_EMPTY", message: `UPSERT \u30AD\u30FC ${key} \u306F\u7A7A\u306B\u3067\u304D\u307E\u305B\u3093` });
|
|
10755
11853
|
});
|
|
10756
|
-
if ((keyCounts.get(upsertNormalizedKey(parts, numeric)) ?? 0) > 1) {
|
|
11854
|
+
if (!isImport && (keyCounts.get(upsertNormalizedKey(parts, numeric)) ?? 0) > 1) {
|
|
10757
11855
|
candidate.preErrors.push({ field: stmt.keyFields[0], code: "ERR_KEY_DUP_SOURCE", message: "UPSERT \u30BD\u30FC\u30B9\u5185\u3067\u30AD\u30FC\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059" });
|
|
10758
11856
|
}
|
|
10759
11857
|
});
|
|
@@ -11113,12 +12211,508 @@ async function executeInsert(stmt, client, options, cacheContext) {
|
|
|
11113
12211
|
insertedCount: createdIds.flat().length
|
|
11114
12212
|
};
|
|
11115
12213
|
}
|
|
12214
|
+
function importPlaceholderSelect() {
|
|
12215
|
+
return {
|
|
12216
|
+
type: "SELECT",
|
|
12217
|
+
distinct: false,
|
|
12218
|
+
columns: [],
|
|
12219
|
+
from: { appId: 0, alias: null, cteName: NO_FROM_CTE_NAME },
|
|
12220
|
+
joins: [],
|
|
12221
|
+
where: null,
|
|
12222
|
+
groupBy: [],
|
|
12223
|
+
having: null,
|
|
12224
|
+
orderMode: "CANONICAL",
|
|
12225
|
+
orderBy: [],
|
|
12226
|
+
limit: null,
|
|
12227
|
+
offset: null
|
|
12228
|
+
};
|
|
12229
|
+
}
|
|
12230
|
+
async function executeImport(stmt, client, options, cacheContext, tempTables) {
|
|
12231
|
+
if (!options.enableImport) throw new Error("UnsupportedError: IMPORT capability is disabled.");
|
|
12232
|
+
const handle = resolveImportSource(stmt.source.sourceName, options.importSource);
|
|
12233
|
+
if (stmt.targets?.some((target) => target.kind === "SUBTABLE")) {
|
|
12234
|
+
if (!stmt.validateOnly && !options.supportsImportConfirmDetail) {
|
|
12235
|
+
throw new Error("UnsupportedError: IMPORT subtable mutation requires a surface that displays parent/table replacement and deletion detail; use VALIDATE ONLY/EXPLAIN.");
|
|
12236
|
+
}
|
|
12237
|
+
if (stmt.source.kind === "CSV") {
|
|
12238
|
+
if (stmt.writeMode !== "UPDATE_RECORD_NUMBER" || !stmt.recordNumberSourceHeader) throw new Error("ArgumentError: CSV subtable replacement requires IMPORT UPDATE and MATCH RECORD NUMBER SOURCE.");
|
|
12239
|
+
if (!stmt.replaceSubtables?.length) throw new Error("ArgumentError: CSV subtable replacement requires REPLACE SUBTABLES (...).");
|
|
12240
|
+
const declared = new Set(stmt.targets.filter((target) => target.kind === "SUBTABLE").map((target) => target.subtableCode));
|
|
12241
|
+
if (stmt.replaceSubtables.some((table) => !declared.has(table))) throw new Error("ArgumentError: REPLACE SUBTABLES contains a table not declared in INTO.");
|
|
12242
|
+
for (const target of stmt.targets.filter((target2) => target2.kind === "SUBTABLE")) {
|
|
12243
|
+
if (!target.rowIdSourceHeader || !stmt.replaceSubtables.includes(target.subtableCode)) throw new Error(`ArgumentError: CSV subtable ${target.subtableCode} requires ROW ID SOURCE and REPLACE SUBTABLES declaration.`);
|
|
12244
|
+
}
|
|
12245
|
+
}
|
|
12246
|
+
if (stmt.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12247
|
+
const targets = stmt.targets;
|
|
12248
|
+
const fieldInfos = await getFieldsCached(stmt.appId, client, cacheContext);
|
|
12249
|
+
const targetCodes = targets.flatMap((target) => target.kind === "FIELD" ? [target.field] : target.children);
|
|
12250
|
+
const numberPrecision = fieldInfos.some((info) => targetCodes.includes(info.code) && info.fieldType === "NUMBER") ? await getNumberPrecisionCached(stmt.appId, client, cacheContext) : void 0;
|
|
12251
|
+
const payload = await loadImportSource(handle, /* @__PURE__ */ new Map());
|
|
12252
|
+
const materialized = stmt.source.kind === "JSON" ? materializeJsonImportRecords(stmt.source, payload, targets, options.maxRecords ?? 1e4) : materializeCliKintoneCsvImportRecords(stmt.source, payload, targets, stmt.replaceSubtables ?? [], options.maxRecords ?? 1e4, stmt.recordNumberSourceHeader);
|
|
12253
|
+
const operation = stmt.source.kind === "CSV" ? "UPDATE" : stmt.keyFields ? "UPSERT" : "INSERT";
|
|
12254
|
+
const prepared = prepareImportRecords(materialized, targets, fieldInfos, numberPrecision, operation);
|
|
12255
|
+
if (stmt.source.kind === "CSV") return executeCsvSubtableReplacement(stmt, materialized, prepared, fieldInfos, client, options, tempTables);
|
|
12256
|
+
assertImportRejectLimit(prepared, stmt.rejectLimit);
|
|
12257
|
+
const payloadFields = [...new Set(targets.flatMap((target) => target.kind === "FIELD" ? [target.field] : target.children))];
|
|
12258
|
+
const errors = materializeImportValidationErrors(prepared.errors, payloadFields);
|
|
12259
|
+
const columns = [...payloadFields, ...IMPORT_VALIDATION_META_COLUMNS];
|
|
12260
|
+
const invalidRows = prepared.invalidParentRows.size;
|
|
12261
|
+
const detail = {
|
|
12262
|
+
preflight: "ACTUAL_DATA",
|
|
12263
|
+
parents: { total: prepared.parents.length, valid: prepared.parents.length - invalidRows, invalid: invalidRows, mutationCandidates: prepared.parents.filter((parent) => parent.valid).length },
|
|
12264
|
+
tables: Object.fromEntries(prepared.tableCounts),
|
|
12265
|
+
writesKintone: false
|
|
12266
|
+
};
|
|
12267
|
+
const result2 = {
|
|
12268
|
+
type: "VALIDATION",
|
|
12269
|
+
operation,
|
|
12270
|
+
validatedRows: prepared.parents.length,
|
|
12271
|
+
validRows: prepared.parents.length - invalidRows,
|
|
12272
|
+
invalidRows,
|
|
12273
|
+
errorCount: errors.length,
|
|
12274
|
+
columns,
|
|
12275
|
+
errors,
|
|
12276
|
+
importDetail: detail,
|
|
12277
|
+
...stmt.validationErrorTable ? { errTable: stmt.validationErrorTable } : {}
|
|
12278
|
+
};
|
|
12279
|
+
if (stmt.validationErrorTable && tempTables) appendValidationErrors(
|
|
12280
|
+
tempTables,
|
|
12281
|
+
stmt.validationErrorTable,
|
|
12282
|
+
columns,
|
|
12283
|
+
errors,
|
|
12284
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12285
|
+
/* @__PURE__ */ new Map()
|
|
12286
|
+
);
|
|
12287
|
+
if (stmt.validateOnly) return result2;
|
|
12288
|
+
assertJsonImportHasNoRowIds(materialized);
|
|
12289
|
+
if (prepared.errors.length > 0 && !stmt.onErrorSkip) {
|
|
12290
|
+
const first = prepared.errors[0];
|
|
12291
|
+
throw new Error(`DmlValidationError: ${first.code} ${first.message} (row=${first.parentRow}, field=${first.field})`);
|
|
12292
|
+
}
|
|
12293
|
+
if (stmt.onErrorSkip) {
|
|
12294
|
+
if (!tempTables || !stmt.errorTable) throw new Error("ArgumentError: ON ERROR SKIP requires a batch and INTO error table.");
|
|
12295
|
+
appendValidationErrors(
|
|
12296
|
+
tempTables,
|
|
12297
|
+
stmt.errorTable,
|
|
12298
|
+
columns,
|
|
12299
|
+
errors,
|
|
12300
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12301
|
+
/* @__PURE__ */ new Map()
|
|
12302
|
+
);
|
|
12303
|
+
}
|
|
12304
|
+
const validParents = prepared.parents.filter((parent) => parent.valid);
|
|
12305
|
+
const fieldTypes = new Map(fieldInfos.map((info) => [info.code, info.fieldType]));
|
|
12306
|
+
const targetIds = validParents.map(() => void 0);
|
|
12307
|
+
if (stmt.keyFields) {
|
|
12308
|
+
for (const key of stmt.keyFields) if (!stmt.fields.includes(key)) {
|
|
12309
|
+
throw new Error(`ON DUPLICATE \u306E\u30AD\u30FC\u300C${key}\u300D\u304C UPSERT \u30D5\u30A3\u30FC\u30EB\u30C9\u306B\u542B\u307E\u308C\u3066\u3044\u307E\u305B\u3093`);
|
|
12310
|
+
}
|
|
12311
|
+
const numeric = stmt.keyFields.map((key) => fieldTypes.get(key) === "NUMBER");
|
|
12312
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
12313
|
+
const rowKeys = validParents.map((parent) => stmt.keyFields.map((key) => String(parent.top[key]?.value ?? "")));
|
|
12314
|
+
for (const parts of rowKeys) {
|
|
12315
|
+
const normalized = upsertNormalizedKey(parts, numeric);
|
|
12316
|
+
if (sourceKeys.has(normalized)) throw new Error("ERR_KEY_DUP_SOURCE: UPSERT \u30BD\u30FC\u30B9\u5185\u3067\u30AD\u30FC\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059");
|
|
12317
|
+
sourceKeys.add(normalized);
|
|
12318
|
+
}
|
|
12319
|
+
const targetsIndex = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeys, client, options, fieldTypes);
|
|
12320
|
+
rowKeys.forEach((parts, index) => {
|
|
12321
|
+
targetIds[index] = lookupUpsertTarget(targetsIndex, parts);
|
|
12322
|
+
});
|
|
12323
|
+
}
|
|
12324
|
+
const tableCodes = targets.filter((target) => target.kind === "SUBTABLE").map((target) => target.subtableCode);
|
|
12325
|
+
const existingById = /* @__PURE__ */ new Map();
|
|
12326
|
+
const updateIds = targetIds.filter((id) => id !== void 0);
|
|
12327
|
+
for (const chunk2 of splitChunks([...new Set(updateIds)], 100)) {
|
|
12328
|
+
const response = await client.getRecords({ app: stmt.appId, query: `$id in (${chunk2.join(",")}) limit 500`, fields: ["$id", "$revision", ...tableCodes] });
|
|
12329
|
+
for (const record of response.records) {
|
|
12330
|
+
const id = Number(record["$id"]?.value);
|
|
12331
|
+
const revision = Number(record["$revision"]?.value);
|
|
12332
|
+
if (Number.isFinite(id)) existingById.set(id, { id, ...Number.isFinite(revision) ? { revision } : {}, record });
|
|
12333
|
+
}
|
|
12334
|
+
}
|
|
12335
|
+
const writePlan = buildJsonSubtableWritePlan(validParents, targetIds, existingById);
|
|
12336
|
+
const importDetail = {
|
|
12337
|
+
kind: "IMPORT_JSON_SUBTABLE",
|
|
12338
|
+
rowIdPolicy: "DROP_AND_RENUMBER_ALL",
|
|
12339
|
+
parentsToWrite: writePlan.length,
|
|
12340
|
+
insertedParents: writePlan.filter((parent) => parent.mode === "INSERT").length,
|
|
12341
|
+
updatedParents: writePlan.filter((parent) => parent.mode === "UPDATE").length,
|
|
12342
|
+
hasDeletes: writePlan.some((parent) => parent.tables.some((table) => table.deleteRows > 0)),
|
|
12343
|
+
parents: writePlan.map((parent) => ({ parentRow: parent.parentRow, mode: parent.mode, ...parent.targetId === void 0 ? {} : { targetId: parent.targetId }, tables: parent.tables }))
|
|
12344
|
+
};
|
|
12345
|
+
if (writePlan.length > 0) {
|
|
12346
|
+
if (!options.confirm) throw new Error("UnsupportedError: JSON IMPORT subtable mutation requires explicit confirmation detail approval.");
|
|
12347
|
+
const ok = await options.confirm(writePlan.length, stmt.keyFields ? "UPDATE" : "INSERT", { statementIndex: 0, statementCount: 1, statementType: "IMPORT", targetAppId: stmt.appId, importDetail });
|
|
12348
|
+
if (!ok) throw new OperationCancelledError(stmt.keyFields ? "UPDATE" : "INSERT", writePlan.length);
|
|
12349
|
+
}
|
|
12350
|
+
const toScalarMap = (record) => new Map(
|
|
12351
|
+
Object.entries(record).map(([code, field]) => [code, field.value])
|
|
12352
|
+
);
|
|
12353
|
+
const payloadFor = (parent) => buildJsonImportRecordPayload(
|
|
12354
|
+
toScalarMap(parent.top),
|
|
12355
|
+
new Map([...parent.subtables].map(([table, rows]) => [table, rows.map((row) => ({ values: toScalarMap(row) }))]))
|
|
12356
|
+
);
|
|
12357
|
+
const inserts = writePlan.filter((parent) => parent.mode === "INSERT");
|
|
12358
|
+
const updates = writePlan.filter((parent) => parent.mode === "UPDATE");
|
|
12359
|
+
const createdIds = [];
|
|
12360
|
+
for (let i = 0; i < inserts.length; i += 100) {
|
|
12361
|
+
const response = await client.postRecords({ app: stmt.appId, records: inserts.slice(i, i + 100).map(payloadFor) });
|
|
12362
|
+
createdIds.push(response.ids);
|
|
12363
|
+
}
|
|
12364
|
+
for (let i = 0; i < updates.length; i += 100) await client.putRecords({
|
|
12365
|
+
app: stmt.appId,
|
|
12366
|
+
records: updates.slice(i, i + 100).map((parent) => ({ id: parent.targetId, ...parent.revision === void 0 ? {} : { revision: parent.revision }, record: payloadFor(parent) }))
|
|
12367
|
+
});
|
|
12368
|
+
return stmt.keyFields ? { type: "UPSERT", insertedCount: createdIds.flat().length, updatedCount: updates.length, affectedRows: writePlan.length, skippedRows: prepared.invalidParentRows.size, rejectLimit: stmt.rejectLimit, ...stmt.errorTable ? { errTable: stmt.errorTable } : {}, importDetail } : { type: "INSERT", createdIds, insertedCount: createdIds.flat().length, affectedRows: writePlan.length, skippedRows: prepared.invalidParentRows.size, rejectLimit: stmt.rejectLimit, ...stmt.errorTable ? { errTable: stmt.errorTable } : {}, importDetail };
|
|
12369
|
+
}
|
|
12370
|
+
if (stmt.writeMode === "UPDATE_RECORD_NUMBER") {
|
|
12371
|
+
return executeImportRecordNumberUpdate(
|
|
12372
|
+
stmt,
|
|
12373
|
+
handle,
|
|
12374
|
+
client,
|
|
12375
|
+
options,
|
|
12376
|
+
cacheContext,
|
|
12377
|
+
tempTables
|
|
12378
|
+
);
|
|
12379
|
+
}
|
|
12380
|
+
const common = {
|
|
12381
|
+
appId: stmt.appId,
|
|
12382
|
+
fields: stmt.fields,
|
|
12383
|
+
select: importPlaceholderSelect(),
|
|
12384
|
+
validateOnly: stmt.validateOnly,
|
|
12385
|
+
validationErrorTable: stmt.validationErrorTable,
|
|
12386
|
+
onErrorSkip: stmt.onErrorSkip,
|
|
12387
|
+
errorTable: stmt.errorTable,
|
|
12388
|
+
rejectLimit: stmt.rejectLimit,
|
|
12389
|
+
checkGroups: stmt.checkGroups
|
|
12390
|
+
};
|
|
12391
|
+
const generated = stmt.keyFields ? { type: "UPSERT_SELECT", ...common, keyFields: stmt.keyFields } : { type: "INSERT_SELECT", ...common };
|
|
12392
|
+
const executionSource = { source: stmt.source, handle, cache: /* @__PURE__ */ new Map() };
|
|
12393
|
+
importSourceByDmlStatement.set(generated, executionSource);
|
|
12394
|
+
const withAudit = (result2) => {
|
|
12395
|
+
if (executionSource.audit) Object.assign(result2, { importAudit: executionSource.audit });
|
|
12396
|
+
return result2;
|
|
12397
|
+
};
|
|
12398
|
+
if (generated.validateOnly) {
|
|
12399
|
+
if (generated.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12400
|
+
const result2 = await executeDmlValidation(generated, client, { ...options, onLimitReached: "error" }, cacheContext, tempTables, 1);
|
|
12401
|
+
if (generated.validationErrorTable && tempTables) {
|
|
12402
|
+
appendValidationErrors(
|
|
12403
|
+
tempTables,
|
|
12404
|
+
generated.validationErrorTable,
|
|
12405
|
+
result2.columns,
|
|
12406
|
+
result2.errors,
|
|
12407
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12408
|
+
materializedMetaByValidationResult.get(result2) ?? /* @__PURE__ */ new Map()
|
|
12409
|
+
);
|
|
12410
|
+
}
|
|
12411
|
+
return withAudit(result2);
|
|
12412
|
+
}
|
|
12413
|
+
if (generated.onErrorSkip) {
|
|
12414
|
+
if (!tempTables) throw new Error("ArgumentError: ON ERROR SKIP requires a batch.");
|
|
12415
|
+
const result2 = await (generated.type === "UPSERT_SELECT" ? executeOnErrorSkip(generated, client, options, cacheContext, tempTables, 1) : executeOnErrorSkip(generated, client, options, cacheContext, tempTables, 1));
|
|
12416
|
+
return withAudit(result2);
|
|
12417
|
+
}
|
|
12418
|
+
const result = await (generated.type === "UPSERT_SELECT" ? executeUpsertSelect(generated, client, options, cacheContext, tempTables) : executeInsertSelect(generated, client, options, cacheContext, tempTables));
|
|
12419
|
+
return withAudit(result);
|
|
12420
|
+
}
|
|
12421
|
+
async function executeCsvSubtableReplacement(stmt, materialized, preparedBase, fieldInfos, client, options, tempTables) {
|
|
12422
|
+
if (!stmt.recordNumberSourceHeader || !stmt.replaceSubtables?.length) throw new Error("InternalError: incomplete CSV subtable replacement AST.");
|
|
12423
|
+
assertNoDuplicateCsvSubtableRowIds(materialized.records);
|
|
12424
|
+
const rawKeys = materialized.records.map((record) => record.recordNumberSourceValue ?? "");
|
|
12425
|
+
const keyPlan = preflightImportRecordNumbers(rawKeys, stmt.recordNumberSourceHeader);
|
|
12426
|
+
const tableCodes = [...stmt.replaceSubtables];
|
|
12427
|
+
const ownershipTableCodes = [...new Set(fieldInfos.filter((info) => !info.inSubtable && info.fieldType === "SUBTABLE").map((info) => info.code))];
|
|
12428
|
+
const allRecords = await fetchAll(client.getRecords, stmt.appId, "", ["$id", "$revision", ...ownershipTableCodes], {
|
|
12429
|
+
maxRecords: options.maxRecords ?? 1e4,
|
|
12430
|
+
parallel: options.fetchParallel ?? 1,
|
|
12431
|
+
onLimit: "error"
|
|
12432
|
+
});
|
|
12433
|
+
const existingById = /* @__PURE__ */ new Map();
|
|
12434
|
+
const ownership = /* @__PURE__ */ new Map();
|
|
12435
|
+
for (const record of allRecords) {
|
|
12436
|
+
const id = Number(record["$id"]?.value);
|
|
12437
|
+
const revision = Number(record["$revision"]?.value);
|
|
12438
|
+
if (!Number.isFinite(id)) continue;
|
|
12439
|
+
existingById.set(id, { id, ...Number.isFinite(revision) ? { revision } : {}, record });
|
|
12440
|
+
for (const table of ownershipTableCodes) {
|
|
12441
|
+
const rows = record[table]?.value;
|
|
12442
|
+
if (!Array.isArray(rows)) continue;
|
|
12443
|
+
for (const row of rows) if (row.id) {
|
|
12444
|
+
const owners = ownership.get(row.id) ?? [];
|
|
12445
|
+
owners.push({ parentId: id, table });
|
|
12446
|
+
ownership.set(row.id, owners);
|
|
12447
|
+
}
|
|
12448
|
+
}
|
|
12449
|
+
}
|
|
12450
|
+
const targetIds = keyPlan.normalized.map((key) => key === null ? void 0 : Number(key));
|
|
12451
|
+
const parents = preparedBase.parents.map((parent, index) => {
|
|
12452
|
+
const errors2 = [...parent.errors];
|
|
12453
|
+
for (const error of keyPlan.errors[index]) errors2.push({
|
|
12454
|
+
operation: "UPDATE",
|
|
12455
|
+
parentRow: parent.parentRow,
|
|
12456
|
+
field: error.field,
|
|
12457
|
+
code: error.code,
|
|
12458
|
+
message: error.message,
|
|
12459
|
+
sourceValues: materialized.records[index].top
|
|
12460
|
+
});
|
|
12461
|
+
const targetId = targetIds[index];
|
|
12462
|
+
if (targetId !== void 0 && !existingById.has(targetId)) errors2.push({
|
|
12463
|
+
operation: "UPDATE",
|
|
12464
|
+
parentRow: parent.parentRow,
|
|
12465
|
+
field: stmt.recordNumberSourceHeader,
|
|
12466
|
+
code: "ERR_RECORD_NUMBER_NOT_FOUND",
|
|
12467
|
+
message: `record number ${targetId} does not exist in APP${stmt.appId}`,
|
|
12468
|
+
sourceValues: materialized.records[index].top
|
|
12469
|
+
});
|
|
12470
|
+
return { ...parent, valid: errors2.length === 0, errors: errors2 };
|
|
12471
|
+
});
|
|
12472
|
+
const initialPlan = buildCsvSubtableReplacementPlan(materialized.records, parents, targetIds, existingById, ownership);
|
|
12473
|
+
const planErrors = initialPlan.flatMap((parent) => [...parent.errors]);
|
|
12474
|
+
const invalidParentRows = new Set(initialPlan.filter((parent) => !parent.valid).map((parent) => parent.parentRow));
|
|
12475
|
+
const prepared = { ...preparedBase, parents, errors: planErrors, invalidParentRows };
|
|
12476
|
+
assertImportRejectLimit(prepared, stmt.rejectLimit);
|
|
12477
|
+
const validPlan = initialPlan.filter((parent) => parent.valid);
|
|
12478
|
+
const allTables = initialPlan.flatMap((parent) => parent.tables);
|
|
12479
|
+
const sum = (table, key) => allTables.filter((item) => item.table === table).reduce((n, item) => n + Number(item[key]), 0);
|
|
12480
|
+
const tableDetail = Object.fromEntries(tableCodes.map((table) => [table, {
|
|
12481
|
+
existingRows: sum(table, "existingRows"),
|
|
12482
|
+
inputRows: sum(table, "inputRows"),
|
|
12483
|
+
updateRows: sum(table, "updateRows"),
|
|
12484
|
+
addRows: sum(table, "addRows"),
|
|
12485
|
+
deleteRows: sum(table, "deleteRows"),
|
|
12486
|
+
rowIdNotFound: sum(table, "rowIdNotFound")
|
|
12487
|
+
}]));
|
|
12488
|
+
const importDetail = {
|
|
12489
|
+
kind: "IMPORT_CSV_SUBTABLE_REPLACE",
|
|
12490
|
+
rowIdPolicy: "PRESERVE_EXISTING",
|
|
12491
|
+
parentsToWrite: validPlan.length,
|
|
12492
|
+
insertedParents: 0,
|
|
12493
|
+
updatedParents: validPlan.length,
|
|
12494
|
+
hasDeletes: validPlan.some((parent) => parent.tables.some((table) => table.deleteRows > 0)),
|
|
12495
|
+
totalDeleteRows: validPlan.flatMap((parent) => parent.tables).reduce((n, table) => n + table.deleteRows, 0),
|
|
12496
|
+
rowIdNotFound: validPlan.flatMap((parent) => parent.tables).reduce((n, table) => n + table.rowIdNotFound, 0),
|
|
12497
|
+
invalidParents: invalidParentRows.size,
|
|
12498
|
+
parents: validPlan.map((parent) => ({ parentRow: parent.parentRow, mode: "UPDATE", targetId: parent.targetId, tables: parent.tables }))
|
|
12499
|
+
};
|
|
12500
|
+
const payloadFields = [...new Set(stmt.targets.flatMap((target) => target.kind === "FIELD" ? [target.field] : target.children))];
|
|
12501
|
+
const errors = materializeImportValidationErrors(planErrors, payloadFields);
|
|
12502
|
+
const columns = [...payloadFields, ...IMPORT_VALIDATION_META_COLUMNS];
|
|
12503
|
+
if (stmt.validateOnly) {
|
|
12504
|
+
if (stmt.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12505
|
+
if (stmt.validationErrorTable && tempTables) appendValidationErrors(tempTables, stmt.validationErrorTable, columns, errors, options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS, /* @__PURE__ */ new Map());
|
|
12506
|
+
return {
|
|
12507
|
+
type: "VALIDATION",
|
|
12508
|
+
operation: "UPDATE",
|
|
12509
|
+
validatedRows: parents.length,
|
|
12510
|
+
validRows: parents.length - invalidParentRows.size,
|
|
12511
|
+
invalidRows: invalidParentRows.size,
|
|
12512
|
+
errorCount: errors.length,
|
|
12513
|
+
columns,
|
|
12514
|
+
errors,
|
|
12515
|
+
...stmt.validationErrorTable ? { errTable: stmt.validationErrorTable } : {},
|
|
12516
|
+
importDetail: { preflight: "ACTUAL_DATA", parents: { total: parents.length, valid: validPlan.length, invalid: invalidParentRows.size, mutationCandidates: validPlan.length }, tables: tableDetail, rowIdPolicy: "PRESERVE_EXISTING", rowIdNotFound: importDetail.rowIdNotFound, writesKintone: false }
|
|
12517
|
+
};
|
|
12518
|
+
}
|
|
12519
|
+
if (planErrors.length && !stmt.onErrorSkip) {
|
|
12520
|
+
const first = planErrors[0];
|
|
12521
|
+
throw new Error(`DmlValidationError: ${first.code} ${first.message} (row=${first.parentRow}, field=${first.field})`);
|
|
12522
|
+
}
|
|
12523
|
+
if (stmt.onErrorSkip) {
|
|
12524
|
+
if (!tempTables || !stmt.errorTable) throw new Error("ArgumentError: ON ERROR SKIP requires a batch and INTO error table.");
|
|
12525
|
+
appendValidationErrors(tempTables, stmt.errorTable, columns, errors, options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS, /* @__PURE__ */ new Map());
|
|
12526
|
+
}
|
|
12527
|
+
if (validPlan.length) {
|
|
12528
|
+
if (!options.supportsImportConfirmDetail || !options.confirm) throw new Error("UnsupportedError: CSV subtable replacement requires explicit rendered detail approval.");
|
|
12529
|
+
const ok = await options.confirm(validPlan.length, "UPDATE", { statementIndex: 0, statementCount: 1, statementType: "IMPORT", targetAppId: stmt.appId, importDetail });
|
|
12530
|
+
if (!ok) throw new OperationCancelledError("UPDATE", validPlan.length);
|
|
12531
|
+
}
|
|
12532
|
+
const scalarMap = (record) => new Map(Object.entries(record).map(([code, field]) => [code, field.value]));
|
|
12533
|
+
for (const parent of validPlan) {
|
|
12534
|
+
const record = buildImportRecordPayload(scalarMap(parent.top), new Map([...parent.subtables].map(([table, rows]) => [table, rows.map((row) => ({ ...row.rowId ? { rowId: row.rowId } : {}, values: scalarMap(row.record) }))])), "PRESERVE");
|
|
12535
|
+
await client.putRecords({ app: stmt.appId, records: [{ id: parent.targetId, ...parent.revision === void 0 ? {} : { revision: parent.revision }, record }] });
|
|
12536
|
+
}
|
|
12537
|
+
return { type: "UPDATE", updatedCount: validPlan.length, affectedRows: validPlan.length, skippedRows: invalidParentRows.size, rejectLimit: stmt.rejectLimit, ...stmt.errorTable ? { errTable: stmt.errorTable } : {}, importDetail };
|
|
12538
|
+
}
|
|
12539
|
+
async function executeImportRecordNumberUpdate(stmt, handle, client, options, cacheContext, tempTables) {
|
|
12540
|
+
if (stmt.source.kind !== "CSV" || stmt.source.mappingMode !== "BY_NAME" || !stmt.recordNumberSourceHeader) {
|
|
12541
|
+
throw new Error("InternalError: invalid IMPORT UPDATE AST.");
|
|
12542
|
+
}
|
|
12543
|
+
if (new Set(stmt.fields).size !== stmt.fields.length) throw new Error("ArgumentError: DML target fields contain duplicates.");
|
|
12544
|
+
const fieldInfos = await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
|
|
12545
|
+
const numberPrecision = await loadNumberPrecisionForTargets(stmt.appId, stmt.fields, fieldInfos, client, cacheContext);
|
|
12546
|
+
const payload = await loadImportSource(handle, /* @__PURE__ */ new Map());
|
|
12547
|
+
const sourceTable = materializeCsvDmlSource(
|
|
12548
|
+
stmt.source,
|
|
12549
|
+
payload,
|
|
12550
|
+
options.maxRecords ?? 1e4,
|
|
12551
|
+
stmt.fields,
|
|
12552
|
+
fieldInfos,
|
|
12553
|
+
stmt.recordNumberSourceHeader
|
|
12554
|
+
);
|
|
12555
|
+
const keyValues = sourceTable.recordNumberSourceValues;
|
|
12556
|
+
if (!keyValues) throw new Error("InternalError: record-number source values were not materialized.");
|
|
12557
|
+
const keyPlan = preflightImportRecordNumbers(keyValues, stmt.recordNumberSourceHeader);
|
|
12558
|
+
const matchedIds = /* @__PURE__ */ new Set();
|
|
12559
|
+
const lookupKeys = [...new Set(keyPlan.normalized.filter((key) => key !== null))];
|
|
12560
|
+
for (let i = 0; i < lookupKeys.length; i += 100) {
|
|
12561
|
+
const chunk2 = lookupKeys.slice(i, i + 100);
|
|
12562
|
+
const response = await client.getRecords({
|
|
12563
|
+
app: stmt.appId,
|
|
12564
|
+
query: `$id in (${chunk2.join(",")}) limit 500`,
|
|
12565
|
+
fields: ["$id"]
|
|
12566
|
+
});
|
|
12567
|
+
for (const record of response.records) {
|
|
12568
|
+
const id = record["$id"]?.value;
|
|
12569
|
+
if (typeof id === "string" && id !== "") matchedIds.add(id.replace(/^0+(?=\d)/, ""));
|
|
12570
|
+
}
|
|
12571
|
+
}
|
|
12572
|
+
const infoByCode = new Map(fieldInfos.map((info) => [info.code, info]));
|
|
12573
|
+
const evaluationTypes = new Map(stmt.fields.map((field) => [field, infoByCode.get(field)?.fieldType ?? "SINGLE_LINE_TEXT"]));
|
|
12574
|
+
const candidates = sourceTable.rows.map((row, index) => {
|
|
12575
|
+
const key = keyPlan.normalized[index];
|
|
12576
|
+
const preErrors = [
|
|
12577
|
+
...sourceTable.importRowErrors?.[index] ?? [],
|
|
12578
|
+
...keyPlan.errors[index]
|
|
12579
|
+
];
|
|
12580
|
+
if (key !== null && !matchedIds.has(key)) preErrors.push({
|
|
12581
|
+
field: stmt.recordNumberSourceHeader,
|
|
12582
|
+
code: "ERR_RECORD_NUMBER_NOT_FOUND",
|
|
12583
|
+
message: `record number ${key} does not exist in APP${stmt.appId}`
|
|
12584
|
+
});
|
|
12585
|
+
return {
|
|
12586
|
+
rowNumber: index + 1,
|
|
12587
|
+
operation: "UPDATE",
|
|
12588
|
+
mode: "update",
|
|
12589
|
+
...key !== null && matchedIds.has(key) ? { targetId: Number(key) } : {},
|
|
12590
|
+
payload: new Map([
|
|
12591
|
+
[stmt.recordNumberSourceHeader, keyValues[index]],
|
|
12592
|
+
...stmt.fields.map((field) => [field, row[field] ?? ""])
|
|
12593
|
+
]),
|
|
12594
|
+
preErrors,
|
|
12595
|
+
record: {},
|
|
12596
|
+
evaluationRow: row,
|
|
12597
|
+
evaluationFieldTypes: evaluationTypes
|
|
12598
|
+
};
|
|
12599
|
+
});
|
|
12600
|
+
const diagnosticFields = [stmt.recordNumberSourceHeader, ...stmt.fields];
|
|
12601
|
+
const validation = validateDmlCandidates(
|
|
12602
|
+
candidates,
|
|
12603
|
+
"UPDATE",
|
|
12604
|
+
diagnosticFields,
|
|
12605
|
+
stmt.fields,
|
|
12606
|
+
fieldInfos,
|
|
12607
|
+
1,
|
|
12608
|
+
numberPrecision,
|
|
12609
|
+
stmt.checkGroups ?? [],
|
|
12610
|
+
false
|
|
12611
|
+
);
|
|
12612
|
+
const columns = [...diagnosticFields, ...VALIDATION_META_COLUMNS];
|
|
12613
|
+
const validationResult = {
|
|
12614
|
+
type: "VALIDATION",
|
|
12615
|
+
operation: "UPDATE",
|
|
12616
|
+
validatedRows: candidates.length,
|
|
12617
|
+
validRows: candidates.length - validation.invalidRows,
|
|
12618
|
+
invalidRows: validation.invalidRows,
|
|
12619
|
+
errorCount: validation.errors.length,
|
|
12620
|
+
columns,
|
|
12621
|
+
errors: validation.errors,
|
|
12622
|
+
...stmt.validationErrorTable ?? stmt.errorTable ? { errTable: stmt.validationErrorTable ?? stmt.errorTable } : {}
|
|
12623
|
+
};
|
|
12624
|
+
Object.assign(validationResult, { importAudit: sourceTable.importAudit });
|
|
12625
|
+
if (stmt.validateOnly) {
|
|
12626
|
+
if (stmt.validationErrorTable && !tempTables) throw new Error("ArgumentError: VALIDATE ONLY INTO requires a batch.");
|
|
12627
|
+
if (stmt.validationErrorTable && tempTables) appendValidationErrors(
|
|
12628
|
+
tempTables,
|
|
12629
|
+
stmt.validationErrorTable,
|
|
12630
|
+
columns,
|
|
12631
|
+
validation.errors,
|
|
12632
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12633
|
+
/* @__PURE__ */ new Map()
|
|
12634
|
+
);
|
|
12635
|
+
return validationResult;
|
|
12636
|
+
}
|
|
12637
|
+
if (!stmt.onErrorSkip && validation.invalidRows > 0) {
|
|
12638
|
+
const first = validation.errors[0];
|
|
12639
|
+
throw new Error(`DmlValidationError: ${first.$err_code} ${first.$err_message} (row=${first.$err_row}, field=${first.$err_field})`);
|
|
12640
|
+
}
|
|
12641
|
+
if (stmt.onErrorSkip) {
|
|
12642
|
+
if (!tempTables || !stmt.errorTable) throw new Error("ArgumentError: ON ERROR SKIP requires a batch.");
|
|
12643
|
+
appendValidationErrors(
|
|
12644
|
+
tempTables,
|
|
12645
|
+
stmt.errorTable,
|
|
12646
|
+
columns,
|
|
12647
|
+
validation.errors,
|
|
12648
|
+
options.tempTableMaxRows ?? TEMP_TABLE_MAX_ROWS,
|
|
12649
|
+
/* @__PURE__ */ new Map()
|
|
12650
|
+
);
|
|
12651
|
+
if (stmt.rejectLimit != null && validation.invalidRows > stmt.rejectLimit) {
|
|
12652
|
+
throw new RejectLimitExceededError(
|
|
12653
|
+
`rejected rows (${validation.invalidRows}) exceed REJECT LIMIT (${stmt.rejectLimit}).`,
|
|
12654
|
+
validationResult
|
|
12655
|
+
);
|
|
12656
|
+
}
|
|
12657
|
+
}
|
|
12658
|
+
const valid = candidates.filter((candidate) => !validation.invalidRowNumbers.has(candidate.rowNumber));
|
|
12659
|
+
if (options.confirm) {
|
|
12660
|
+
const ok = await options.confirm(valid.length, "UPDATE");
|
|
12661
|
+
if (!ok) throw new OperationCancelledError("UPDATE", valid.length);
|
|
12662
|
+
}
|
|
12663
|
+
const updates = valid.map((candidate) => ({ id: candidate.targetId, record: candidate.record }));
|
|
12664
|
+
for (let i = 0; i < updates.length; i += 100) {
|
|
12665
|
+
await client.putRecords({ app: stmt.appId, records: updates.slice(i, i + 100) });
|
|
12666
|
+
}
|
|
12667
|
+
const result = {
|
|
12668
|
+
type: "UPDATE",
|
|
12669
|
+
updatedCount: updates.length,
|
|
12670
|
+
...stmt.onErrorSkip ? {
|
|
12671
|
+
affectedRows: updates.length,
|
|
12672
|
+
skippedRows: validation.invalidRows,
|
|
12673
|
+
rejectLimit: stmt.rejectLimit ?? null,
|
|
12674
|
+
errTable: stmt.errorTable
|
|
12675
|
+
} : {}
|
|
12676
|
+
};
|
|
12677
|
+
Object.assign(result, { insertedCount: 0, importAudit: sourceTable.importAudit });
|
|
12678
|
+
return result;
|
|
12679
|
+
}
|
|
12680
|
+
async function materializeDmlSource(stmt, client, options, cacheContext, tempTables, targetFields) {
|
|
12681
|
+
const imported = importSourceByDmlStatement.get(stmt);
|
|
12682
|
+
if (!imported) {
|
|
12683
|
+
const selected2 = tempTables && tempTables.size > 0 ? await executeQueryWithCte(stmt.select, client, options, tempTables, cacheContext, true) : await executeSelect(stmt.select, client, options, cacheContext, void 0, true);
|
|
12684
|
+
return { rows: selected2.rows, columns: selected2.columns, columnMeta: materializedMetaBySelectResult.get(selected2) };
|
|
12685
|
+
}
|
|
12686
|
+
const payload = await loadImportSource(imported.handle, imported.cache);
|
|
12687
|
+
const rowLimit = options.maxRecords ?? 1e4;
|
|
12688
|
+
const targetByCode = new Map((targetFields ?? []).map((info) => [info.code, info]));
|
|
12689
|
+
const jsonTargets = stmt.fields.map((code) => ({ code, fieldType: targetByCode.get(code)?.fieldType ?? "SINGLE_LINE_TEXT" }));
|
|
12690
|
+
const raw = imported.source.kind === "JSON" ? materializeJsonDmlSource(imported.source, payload, jsonTargets, rowLimit) : materializeCsvDmlSource(imported.source, payload, rowLimit, stmt.fields, targetFields);
|
|
12691
|
+
imported.audit = raw.importAudit;
|
|
12692
|
+
if (imported.source.kind === "JSON") return raw;
|
|
12693
|
+
if (!imported.source.projection) return raw;
|
|
12694
|
+
const projection = bindImportProjection(imported.source.projection);
|
|
12695
|
+
const tables = new Map(tempTables ?? []);
|
|
12696
|
+
tables.set(IMPORT_PROJECTION_SOURCE, raw);
|
|
12697
|
+
const selected = await executeQueryWithCte(projection, client, { ...options, onLimitReached: "error" }, tables, cacheContext, true);
|
|
12698
|
+
return { rows: selected.rows, columns: selected.columns, columnMeta: materializedMetaBySelectResult.get(selected) };
|
|
12699
|
+
}
|
|
12700
|
+
var dmlSourceMaterializer = { materialize: materializeDmlSource };
|
|
12701
|
+
function assertNoImportRowErrors(table) {
|
|
12702
|
+
for (let rowIndex = 0; rowIndex < (table.importRowErrors?.length ?? 0); rowIndex++) {
|
|
12703
|
+
const first = table.importRowErrors?.[rowIndex]?.[0];
|
|
12704
|
+
if (first) {
|
|
12705
|
+
throw new Error(`DmlValidationError: ${first.code} ${first.message} (row=${rowIndex + 1}, field=${first.field})`);
|
|
12706
|
+
}
|
|
12707
|
+
}
|
|
12708
|
+
}
|
|
11116
12709
|
async function executeInsertSelect(stmt, client, options, cacheContext, cteCache) {
|
|
11117
12710
|
if (stmt.checkGroups?.length) return executeCheckedPlainDml(stmt, client, options, cacheContext, cteCache);
|
|
11118
12711
|
const fieldInfos = await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
|
|
11119
12712
|
const numberPrecision = await loadNumberPrecisionForTargets(stmt.appId, stmt.fields, fieldInfos, client, cacheContext);
|
|
11120
|
-
const
|
|
11121
|
-
const { rows, columns } =
|
|
12713
|
+
const sourceTable = await dmlSourceMaterializer.materialize(stmt, client, options, cacheContext, cteCache, fieldInfos);
|
|
12714
|
+
const { rows, columns } = sourceTable;
|
|
12715
|
+
assertNoImportRowErrors(sourceTable);
|
|
11122
12716
|
if (columns.length !== stmt.fields.length) {
|
|
11123
12717
|
const emptySourceHint = columns.length === 0 && rows.length === 0 ? "\u3002\u7D50\u679C\u304C 0 \u884C\u306E\u305F\u3081\u5217\u3092\u7279\u5B9A\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08SELECT * \u3092\u7A7A\u30BD\u30FC\u30B9\u306B\u4F7F\u3046\u3068\u5217\u3092\u6C7A\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002\u660E\u793A\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF09" : "";
|
|
11124
12718
|
throw new Error(
|
|
@@ -11130,15 +12724,16 @@ async function executeInsertSelect(stmt, client, options, cacheContext, cteCache
|
|
|
11130
12724
|
if (!ok) throw new OperationCancelledError("INSERT", rows.length);
|
|
11131
12725
|
}
|
|
11132
12726
|
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
11133
|
-
const allRecords = rows.map((row) => {
|
|
12727
|
+
const allRecords = rows.map((row, rowIndex) => {
|
|
11134
12728
|
const record = {};
|
|
11135
12729
|
stmt.fields.forEach((field, i) => {
|
|
12730
|
+
if (sourceTable.importPresence && !sourceTable.importPresence[rowIndex]?.has(field)) return;
|
|
11136
12731
|
const raw = row[columns[i]] ?? "";
|
|
11137
12732
|
record[field] = { value: convertProcessRowValue(raw, fieldTypes.get(field)) };
|
|
11138
12733
|
});
|
|
11139
12734
|
return record;
|
|
11140
12735
|
});
|
|
11141
|
-
assertValidDmlRecords(
|
|
12736
|
+
allRecords.forEach((record) => assertValidDmlRecords([record], stmt.fields.filter((field) => field in record), fieldInfos, numberPrecision));
|
|
11142
12737
|
const createdIds = [];
|
|
11143
12738
|
for (let i = 0; i < allRecords.length; i += 100) {
|
|
11144
12739
|
const batch = allRecords.slice(i, i + 100);
|
|
@@ -11531,9 +13126,9 @@ function expandRowsForSubtableDml(parents, subtableCode) {
|
|
|
11531
13126
|
for (const parent of parents) {
|
|
11532
13127
|
const parentId = String(parent["$id"]?.value ?? "");
|
|
11533
13128
|
const parentRevision = getRevision(parent);
|
|
11534
|
-
const
|
|
11535
|
-
for (let i = 0; i <
|
|
11536
|
-
const row =
|
|
13129
|
+
const tableRows2 = getMutableTableRows(parent, subtableCode);
|
|
13130
|
+
for (let i = 0; i < tableRows2.length; i++) {
|
|
13131
|
+
const row = tableRows2[i];
|
|
11537
13132
|
const flat = {
|
|
11538
13133
|
_pid: parentId,
|
|
11539
13134
|
_rid: row.id ?? "",
|
|
@@ -11739,8 +13334,9 @@ async function executeUpsertSelect(stmt, client, options, cacheContext, cteCache
|
|
|
11739
13334
|
if (stmt.checkGroups?.length) return executeCheckedPlainDml(stmt, client, options, cacheContext, cteCache);
|
|
11740
13335
|
const fieldInfos = await loadWritableTopLevelDmlFields(stmt.appId, stmt.fields, client, cacheContext);
|
|
11741
13336
|
const numberPrecision = await loadNumberPrecisionForTargets(stmt.appId, stmt.fields, fieldInfos, client, cacheContext);
|
|
11742
|
-
const
|
|
11743
|
-
const { rows, columns } =
|
|
13337
|
+
const sourceTable = await dmlSourceMaterializer.materialize(stmt, client, options, cacheContext, cteCache, fieldInfos);
|
|
13338
|
+
const { rows, columns } = sourceTable;
|
|
13339
|
+
assertNoImportRowErrors(sourceTable);
|
|
11744
13340
|
if (columns.length !== stmt.fields.length) {
|
|
11745
13341
|
const emptySourceHint = columns.length === 0 && rows.length === 0 ? "\u3002\u7D50\u679C\u304C 0 \u884C\u306E\u305F\u3081\u5217\u3092\u7279\u5B9A\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\uFF08SELECT * \u3092\u7A7A\u30BD\u30FC\u30B9\u306B\u4F7F\u3046\u3068\u5217\u3092\u6C7A\u5B9A\u3067\u304D\u307E\u305B\u3093\u3002\u660E\u793A\u5217\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF09" : "";
|
|
11746
13342
|
throw new Error(
|
|
@@ -11754,18 +13350,29 @@ async function executeUpsertSelect(stmt, client, options, cacheContext, cteCache
|
|
|
11754
13350
|
}
|
|
11755
13351
|
const toInsert = [];
|
|
11756
13352
|
const toUpdate = [];
|
|
11757
|
-
const
|
|
13353
|
+
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
13354
|
+
const records = rows.map((row, rowIndex) => {
|
|
11758
13355
|
const record = {};
|
|
11759
13356
|
stmt.fields.forEach((field, i) => {
|
|
11760
|
-
|
|
13357
|
+
if (sourceTable.importPresence && !sourceTable.importPresence[rowIndex]?.has(field)) return;
|
|
13358
|
+
const raw = row[columns[i]] ?? "";
|
|
13359
|
+
record[field] = { value: convertProcessRowValue(raw, fieldTypes.get(field)) };
|
|
11761
13360
|
});
|
|
11762
13361
|
return record;
|
|
11763
13362
|
});
|
|
11764
|
-
assertValidDmlRecords(
|
|
11765
|
-
const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
|
|
13363
|
+
records.forEach((record) => assertValidDmlRecords([record], stmt.fields.filter((field) => field in record), fieldInfos, numberPrecision));
|
|
11766
13364
|
const rowKeyValues = records.map(
|
|
11767
13365
|
(record) => stmt.keyFields.map((key) => String(record[key]?.value ?? ""))
|
|
11768
13366
|
);
|
|
13367
|
+
if (importSourceByDmlStatement.has(stmt)) {
|
|
13368
|
+
const numericKey = stmt.keyFields.map((key) => fieldTypes.get(key) === "NUMBER");
|
|
13369
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
13370
|
+
for (const parts of rowKeyValues) {
|
|
13371
|
+
const normalized = upsertNormalizedKey(parts, numericKey);
|
|
13372
|
+
if (sourceKeys.has(normalized)) throw new Error("ERR_KEY_DUP_SOURCE: UPSERT \u30BD\u30FC\u30B9\u5185\u3067\u30AD\u30FC\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059");
|
|
13373
|
+
sourceKeys.add(normalized);
|
|
13374
|
+
}
|
|
13375
|
+
}
|
|
11769
13376
|
const targetIndex = await resolveUpsertTargets(stmt.appId, stmt.keyFields, rowKeyValues, client, options, fieldTypes);
|
|
11770
13377
|
records.forEach((record, rowIdx) => {
|
|
11771
13378
|
const id = lookupUpsertTarget(targetIndex, rowKeyValues[rowIdx]);
|
|
@@ -11814,10 +13421,10 @@ async function executeDescribe(stmt, client, cacheContext) {
|
|
|
11814
13421
|
}));
|
|
11815
13422
|
return { type: "SELECT", rows, columns, rowCount: rows.length };
|
|
11816
13423
|
}
|
|
11817
|
-
function parseSql(sql) {
|
|
13424
|
+
function parseSql(sql, enableImport = false) {
|
|
11818
13425
|
try {
|
|
11819
13426
|
const tokens = new Lexer(sql).tokenize();
|
|
11820
|
-
const stmt = new Parser(tokens).parse();
|
|
13427
|
+
const stmt = new Parser(tokens, { import: enableImport }).parse();
|
|
11821
13428
|
validateKlikeStatement(stmt);
|
|
11822
13429
|
return stmt;
|
|
11823
13430
|
} catch (e) {
|
|
@@ -12073,8 +13680,8 @@ function explainMetadataLines(analysis) {
|
|
|
12073
13680
|
...[...analysis.numberPrecisionApps].sort((a, b) => a - b).map((appId) => ` metadata API: number precision APP${appId}`)
|
|
12074
13681
|
];
|
|
12075
13682
|
}
|
|
12076
|
-
async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4, cursorMaxActive = 2) {
|
|
12077
|
-
const statements = parseSqlBatch(sql);
|
|
13683
|
+
async function buildBatchExplainPlans(sql, client, injectedVariables, cacheContext = "batch-explain", maxRecords = 1e4, cursorMaxActive = 2, enableImport = false) {
|
|
13684
|
+
const statements = parseSqlBatch(sql, enableImport);
|
|
12078
13685
|
const analysis = analyzeBatch(statements);
|
|
12079
13686
|
validateDeclaredBatchVariables(statements, injectedVariables);
|
|
12080
13687
|
const variables = /* @__PURE__ */ new Map();
|
|
@@ -12233,6 +13840,79 @@ function buildExplainPlan(query, label, capabilities, orderPlans) {
|
|
|
12233
13840
|
if (query.type === "DELETE") return buildDeletePlan(query, label);
|
|
12234
13841
|
if (query.type === "REORDER") return buildReorderPlan(query, label);
|
|
12235
13842
|
if (query.type === "VALIDATE") return buildValidatePlan(query, label);
|
|
13843
|
+
if (query.type === "IMPORT") {
|
|
13844
|
+
if (query.writeMode === "UPDATE_RECORD_NUMBER") {
|
|
13845
|
+
const csvTables = query.targets?.filter((target) => target.kind === "SUBTABLE") ?? [];
|
|
13846
|
+
return [
|
|
13847
|
+
...label ? [label] : [],
|
|
13848
|
+
`IMPORT UPDATE INTO APP${query.appId}`,
|
|
13849
|
+
` writeMode: UPDATE_RECORD_NUMBER`,
|
|
13850
|
+
` source: CSV ${query.source.sourceName}`,
|
|
13851
|
+
` keyHeader: ${query.recordNumberSourceHeader}`,
|
|
13852
|
+
` mapping: BY_NAME`,
|
|
13853
|
+
` parentRows: requires source load`,
|
|
13854
|
+
` duplicate: preflight before lookup/write`,
|
|
13855
|
+
` matched: requires lookup`,
|
|
13856
|
+
` unmatched: requires lookup`,
|
|
13857
|
+
` invalid: requires source load`,
|
|
13858
|
+
` requiresLookup:true`,
|
|
13859
|
+
` inserted: 0`,
|
|
13860
|
+
` keyInPayload: false`,
|
|
13861
|
+
...csvTables.length ? [
|
|
13862
|
+
` replaceSubtables: ${query.replaceSubtables?.join(", ") ?? "ERROR: required"}`,
|
|
13863
|
+
` subtableRowIdPolicy: PRESERVE existing; empty/unknown add without id`,
|
|
13864
|
+
` rowIdOwnership: owned elsewhere invalidates the parent`,
|
|
13865
|
+
` replacementDiff: existing/input/update/add/delete/rowIdNotFound requires actual-data preflight`,
|
|
13866
|
+
` confirmPolicy: highest warning "\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5168\u7F6E\u63DB\u30FBN\u884C\u524A\u9664" plus per-table detail (including delete=0)`
|
|
13867
|
+
] : [],
|
|
13868
|
+
` disposition: ${query.validateOnly ? "VALIDATE ONLY" : query.onErrorSkip ? `ON ERROR SKIP INTO ${query.errorTable}` : "fail-fast"}`,
|
|
13869
|
+
` gate: enabled for this parse`,
|
|
13870
|
+
` writesKintone: ${query.validateOnly ? "false" : "true"}`
|
|
13871
|
+
];
|
|
13872
|
+
}
|
|
13873
|
+
const mode = query.keyFields ? "UPSERT" : "INSERT";
|
|
13874
|
+
const hasSubtables = query.targets?.some((target) => target.kind === "SUBTABLE") === true;
|
|
13875
|
+
return [
|
|
13876
|
+
...label ? [label] : [],
|
|
13877
|
+
`IMPORT ${mode} INTO APP${query.appId}`,
|
|
13878
|
+
` source: ${query.source.kind} ${query.source.sourceName}`,
|
|
13879
|
+
` sourceFormat: ${query.source.kind}`,
|
|
13880
|
+
` encoding: ${query.source.kind === "JSON" ? "UTF8 only" : query.source.encoding ?? "UTF8 (or loader metadata)"}`,
|
|
13881
|
+
` mapping: ${query.source.kind === "JSON" ? "BY NAME (INTO order)" : query.source.projection ? "SELECT expressions" : query.source.mappingMode}`,
|
|
13882
|
+
...query.source.kind === "JSON" ? [
|
|
13883
|
+
` duplicateKeyPolicy: reject`,
|
|
13884
|
+
` numberLexemePolicy: preserve; JSON number accepts safe integer only`,
|
|
13885
|
+
` precisionTargetsRequireString: true`,
|
|
13886
|
+
` unknownKeyPolicy: reject`,
|
|
13887
|
+
` presenceAware: true`,
|
|
13888
|
+
...hasSubtables ? [
|
|
13889
|
+
` subtableRowIdPolicy: reject _rid/id; DROP IDs and renumber every input row`,
|
|
13890
|
+
` subtableUpdatePolicy: present table replaces all rows; missing table is preserved; [] deletes all rows`,
|
|
13891
|
+
` confirmPolicy: parent/table existing/input/add/delete detail required; delete is highest warning`
|
|
13892
|
+
] : []
|
|
13893
|
+
] : [
|
|
13894
|
+
` header: ${query.source.hasHeader ? "HEADER" : "NO HEADER"}`,
|
|
13895
|
+
...query.source.mappingMode === "BY_NAME" ? [
|
|
13896
|
+
` writtenColumns: ${query.fields.join(", ")}`,
|
|
13897
|
+
` knownExportColumns: audit and ignore with reason/non-empty count`,
|
|
13898
|
+
` unknownColumnPolicy: ${query.source.ignoreUnknownColumns ? "ignore with audit/non-empty count" : "ERR_IMPORT_UNKNOWN_COLUMN"}`,
|
|
13899
|
+
` multipleValueDelimiter: LF (CRLF or LF)`,
|
|
13900
|
+
` sourceValueMode: string-preserving`,
|
|
13901
|
+
` roundTripNumericGuarantee: exact CSV lexeme passes strict decimal validation`,
|
|
13902
|
+
` FILE: audit-ignore unless named in INTO (analyze error)`
|
|
13903
|
+
] : []
|
|
13904
|
+
],
|
|
13905
|
+
` sourceLimit: 10485760 bytes / ${query.fields.length} target columns`,
|
|
13906
|
+
` key: ${query.keyFields?.join(", ") ?? "none"}`,
|
|
13907
|
+
` checks: ${query.checkGroups?.length ?? 0}`,
|
|
13908
|
+
` disposition: ${query.validateOnly ? "VALIDATE ONLY" : query.onErrorSkip ? `ON ERROR SKIP INTO ${query.errorTable}` : "fail-fast"}`,
|
|
13909
|
+
` gate: enabled for this parse`,
|
|
13910
|
+
` preflight: ${query.validateOnly && hasSubtables ? "requires actual source load at execution; this EXPLAIN is static" : "requires load"}`,
|
|
13911
|
+
...hasSubtables ? [query.source.kind === "JSON" ? ` Phase5C: JSON mutation requires detail-capable confirmation surface` : ` Phase5D: CSV mutation requires detail-capable confirmation surface`] : [],
|
|
13912
|
+
` writesKintone: ${query.validateOnly ? "false" : "true"}`,
|
|
13913
|
+
` duplicateKey: preflight before lookup/write (requires load)`
|
|
13914
|
+
];
|
|
13915
|
+
}
|
|
12236
13916
|
return buildSelectPlan(query, label, capabilities, orderPlans);
|
|
12237
13917
|
}
|
|
12238
13918
|
function buildValidatePlan(stmt, label) {
|
|
@@ -12686,15 +14366,15 @@ var OperationCancelledError = class extends Error {
|
|
|
12686
14366
|
};
|
|
12687
14367
|
|
|
12688
14368
|
// src/core/sql.ts
|
|
12689
|
-
function parseSqlStatement(sql) {
|
|
14369
|
+
function parseSqlStatement(sql, capabilities = {}) {
|
|
12690
14370
|
const tokens = new Lexer(sql).tokenize();
|
|
12691
|
-
const stmt = new Parser(tokens).parse();
|
|
14371
|
+
const stmt = new Parser(tokens, capabilities).parse();
|
|
12692
14372
|
validateKlikeStatement(stmt);
|
|
12693
14373
|
return stmt;
|
|
12694
14374
|
}
|
|
12695
|
-
function parseSqlStatements(sql) {
|
|
14375
|
+
function parseSqlStatements(sql, capabilities = {}) {
|
|
12696
14376
|
const tokens = new Lexer(sql).tokenize();
|
|
12697
|
-
const statements = new Parser(tokens).parseStatements();
|
|
14377
|
+
const statements = new Parser(tokens, capabilities).parseStatements();
|
|
12698
14378
|
statements.forEach(validateKlikeStatement);
|
|
12699
14379
|
return statements;
|
|
12700
14380
|
}
|
|
@@ -12861,7 +14541,8 @@ function buildBatchEnvelope(batch, options = {}) {
|
|
|
12861
14541
|
validRows: s.result.validRows,
|
|
12862
14542
|
invalidRows: s.result.invalidRows,
|
|
12863
14543
|
errorCount: s.result.errorCount,
|
|
12864
|
-
...s.result.errTable ? { errTable: s.result.errTable } : {}
|
|
14544
|
+
...s.result.errTable ? { errTable: s.result.errTable } : {},
|
|
14545
|
+
...s.result.importDetail ? { importDetail: s.result.importDetail } : {}
|
|
12865
14546
|
});
|
|
12866
14547
|
} else if (s.status === "success" && s.result && s.result.type !== "SELECT" && s.result.type !== "ASSERT") {
|
|
12867
14548
|
Object.assign(entry, toMutationSummary(s.result));
|
|
@@ -13164,7 +14845,7 @@ function clampInt(v, min, max) {
|
|
|
13164
14845
|
function flattenFormFieldProperties(properties) {
|
|
13165
14846
|
return flattenFields(properties, collectLookupCopyFields(properties));
|
|
13166
14847
|
}
|
|
13167
|
-
function flattenFields(properties, lookupCopyFields, inSubtable = false) {
|
|
14848
|
+
function flattenFields(properties, lookupCopyFields, inSubtable = false, subtableCode) {
|
|
13168
14849
|
const out = [];
|
|
13169
14850
|
for (const field of Object.values(properties)) {
|
|
13170
14851
|
const optionOrder = toOptionOrderMap(field.options);
|
|
@@ -13182,11 +14863,12 @@ function flattenFields(properties, lookupCopyFields, inSubtable = false) {
|
|
|
13182
14863
|
maxLength: normalizeConstraintValue(field.maxLength),
|
|
13183
14864
|
defaultValue: field.defaultValue,
|
|
13184
14865
|
inSubtable,
|
|
14866
|
+
...subtableCode ? { subtableCode } : {},
|
|
13185
14867
|
writable: !lookupCopyFields.has(field.code) && !NON_WRITABLE_FIELD_TYPES2.has(field.type)
|
|
13186
14868
|
};
|
|
13187
14869
|
info.semantics = resolveFieldSemantics(info);
|
|
13188
14870
|
out.push(info);
|
|
13189
|
-
if (field.fields) out.push(...flattenFields(field.fields, lookupCopyFields, true));
|
|
14871
|
+
if (field.fields) out.push(...flattenFields(field.fields, lookupCopyFields, true, field.type === "SUBTABLE" ? field.code : subtableCode));
|
|
13190
14872
|
}
|
|
13191
14873
|
return out;
|
|
13192
14874
|
}
|
|
@@ -14170,6 +15852,17 @@ function restoreSqlContextError(err, sourceSql, context) {
|
|
|
14170
15852
|
return err;
|
|
14171
15853
|
}
|
|
14172
15854
|
|
|
15855
|
+
// src/import/importGateError.ts
|
|
15856
|
+
var IMPORT_CAPABILITY_GATE_MARKER = "capability is disabled";
|
|
15857
|
+
function errorMessage(error) {
|
|
15858
|
+
if (error instanceof Error) return error.message;
|
|
15859
|
+
if (typeof error === "string") return error;
|
|
15860
|
+
return null;
|
|
15861
|
+
}
|
|
15862
|
+
function isImportCapabilityGateError(error) {
|
|
15863
|
+
return errorMessage(error)?.includes(IMPORT_CAPABILITY_GATE_MARKER) === true;
|
|
15864
|
+
}
|
|
15865
|
+
|
|
14173
15866
|
// src/cli/index.ts
|
|
14174
15867
|
var HELP_TEXT = `ksql - Execute SQL against kintone apps
|
|
14175
15868
|
|
|
@@ -14184,6 +15877,8 @@ Options:
|
|
|
14184
15877
|
--console Start interactive console mode
|
|
14185
15878
|
--dry-run Parse and show execution plan only
|
|
14186
15879
|
--var <name=value> Override a DECLARE variable (repeatable; not for secrets)
|
|
15880
|
+
--import-csv <name=path> Supply named CSV and enable IMPORT (repeatable)
|
|
15881
|
+
--import-json <name=path> Supply named JSON and enable IMPORT (repeatable)
|
|
14187
15882
|
--format <type> Output format: table | json | jsonl | csv | markdown | md
|
|
14188
15883
|
(batch + json: prints one JSON envelope for the whole batch)
|
|
14189
15884
|
--max-records <n> Max records to fetch (default: 500)
|
|
@@ -14231,6 +15926,15 @@ Options:
|
|
|
14231
15926
|
-h, --help Show help
|
|
14232
15927
|
-v, --version Show version
|
|
14233
15928
|
`;
|
|
15929
|
+
var CLI_IMPORT_SOURCE_REQUIRED_MESSAGE = "IMPORT \u306B\u306F\u30BD\u30FC\u30B9\u304C\u5FC5\u8981\u3067\u3059\u3002--import-csv <name=path> \u307E\u305F\u306F --import-json <name=path> \u3067\u30D5\u30A1\u30A4\u30EB\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002";
|
|
15930
|
+
function toCliImportError(error, importEnabled) {
|
|
15931
|
+
if (importEnabled || !isImportCapabilityGateError(error)) return error;
|
|
15932
|
+
if (error instanceof Error) {
|
|
15933
|
+
error.message = CLI_IMPORT_SOURCE_REQUIRED_MESSAGE;
|
|
15934
|
+
return error;
|
|
15935
|
+
}
|
|
15936
|
+
return CLI_IMPORT_SOURCE_REQUIRED_MESSAGE;
|
|
15937
|
+
}
|
|
14234
15938
|
function parseArgs(argv) {
|
|
14235
15939
|
const out = {
|
|
14236
15940
|
help: false,
|
|
@@ -14282,7 +15986,9 @@ function parseArgs(argv) {
|
|
|
14282
15986
|
arrayFormat: null,
|
|
14283
15987
|
tableFormat: null,
|
|
14284
15988
|
dateFormat: null,
|
|
14285
|
-
attachmentFormat: null
|
|
15989
|
+
attachmentFormat: null,
|
|
15990
|
+
importCsv: /* @__PURE__ */ Object.create(null),
|
|
15991
|
+
importJson: /* @__PURE__ */ Object.create(null)
|
|
14286
15992
|
};
|
|
14287
15993
|
for (let i = 0; i < argv.length; i++) {
|
|
14288
15994
|
const a = argv[i];
|
|
@@ -14364,6 +16070,28 @@ function parseArgs(argv) {
|
|
|
14364
16070
|
i++;
|
|
14365
16071
|
continue;
|
|
14366
16072
|
}
|
|
16073
|
+
if (a === "--import-csv") {
|
|
16074
|
+
const raw = v ?? "";
|
|
16075
|
+
const eq = raw.indexOf("=");
|
|
16076
|
+
if (eq <= 0 || eq === raw.length - 1) throw new Error("ArgumentError: --import-csv must use name=path.");
|
|
16077
|
+
const name = raw.slice(0, eq);
|
|
16078
|
+
if (Object.prototype.hasOwnProperty.call(out.importCsv, name) || Object.prototype.hasOwnProperty.call(out.importJson, name)) throw new Error(`ArgumentError: import source "${name}" is specified more than once.`);
|
|
16079
|
+
out.importCsv[name] = raw.slice(eq + 1);
|
|
16080
|
+
i++;
|
|
16081
|
+
continue;
|
|
16082
|
+
}
|
|
16083
|
+
if (a === "--import-json") {
|
|
16084
|
+
const raw = v ?? "";
|
|
16085
|
+
const eq = raw.indexOf("=");
|
|
16086
|
+
if (eq <= 0 || eq === raw.length - 1) throw new Error("ArgumentError: --import-json must use name=path.");
|
|
16087
|
+
const name = raw.slice(0, eq);
|
|
16088
|
+
if (Object.prototype.hasOwnProperty.call(out.importJson, name) || Object.prototype.hasOwnProperty.call(out.importCsv, name)) {
|
|
16089
|
+
throw new Error(`ArgumentError: import source "${name}" is specified more than once.`);
|
|
16090
|
+
}
|
|
16091
|
+
out.importJson[name] = raw.slice(eq + 1);
|
|
16092
|
+
i++;
|
|
16093
|
+
continue;
|
|
16094
|
+
}
|
|
14367
16095
|
if (a === "-e" || a === "--execute") {
|
|
14368
16096
|
out.executeSql = v ?? "";
|
|
14369
16097
|
i++;
|
|
@@ -15511,15 +17239,16 @@ async function run() {
|
|
|
15511
17239
|
`);
|
|
15512
17240
|
return 2;
|
|
15513
17241
|
}
|
|
17242
|
+
const importEnabled = Object.keys(args.importCsv).length > 0 || Object.keys(args.importJson).length > 0;
|
|
15514
17243
|
try {
|
|
15515
|
-
const statements = parseSqlStatements(sql);
|
|
17244
|
+
const statements = parseSqlStatements(sql, { import: importEnabled });
|
|
15516
17245
|
dryRunNeedsMetadata = statements.some(explainNeedsAppMetadata);
|
|
15517
17246
|
if (statements.length > 1) {
|
|
15518
17247
|
batchAnalysis = analyzeBatch(statements);
|
|
15519
17248
|
isBatchSql = true;
|
|
15520
17249
|
batchContainsDml = batchAnalysis.containsDml;
|
|
15521
17250
|
} else {
|
|
15522
|
-
const stmt = parseSqlStatement(sql);
|
|
17251
|
+
const stmt = parseSqlStatement(sql, { import: importEnabled });
|
|
15523
17252
|
parsedStmt = stmt;
|
|
15524
17253
|
stmtType = getStatementType(stmt);
|
|
15525
17254
|
isDmlStatement = writesKintone(stmt);
|
|
@@ -15537,7 +17266,8 @@ async function run() {
|
|
|
15537
17266
|
bindings: sqlDiagnosticContext.appBindingByMappedApp,
|
|
15538
17267
|
rewriteSegments: sqlDiagnosticContext.rewriteSegments
|
|
15539
17268
|
}) : err;
|
|
15540
|
-
|
|
17269
|
+
const surfaced = toCliImportError(restored, importEnabled);
|
|
17270
|
+
process.stderr.write(`${surfaced instanceof Error ? surfaced.message : String(surfaced)}
|
|
15541
17271
|
`);
|
|
15542
17272
|
return 1;
|
|
15543
17273
|
}
|
|
@@ -15894,7 +17624,8 @@ async function run() {
|
|
|
15894
17624
|
args.variables,
|
|
15895
17625
|
cacheContext,
|
|
15896
17626
|
maxRecords,
|
|
15897
|
-
cursorMaxActive
|
|
17627
|
+
cursorMaxActive,
|
|
17628
|
+
Object.keys(args.importCsv).length > 0 || Object.keys(args.importJson).length > 0
|
|
15898
17629
|
);
|
|
15899
17630
|
const out = [];
|
|
15900
17631
|
const restoredStatements = sqlDiagnosticContext ? restoreSqlDiagnosticValue(plans.statements, sqlDiagnosticContext.appBindingByMappedApp) : plans.statements;
|
|
@@ -15933,10 +17664,30 @@ async function run() {
|
|
|
15933
17664
|
}
|
|
15934
17665
|
}
|
|
15935
17666
|
}
|
|
15936
|
-
const
|
|
17667
|
+
const importEnabled = Object.keys(args.importCsv).length > 0 || Object.keys(args.importJson).length > 0;
|
|
17668
|
+
const importSource = importEnabled ? (name) => {
|
|
17669
|
+
const sourcePath = args.importCsv[name] ?? args.importJson[name];
|
|
17670
|
+
return sourcePath === void 0 ? void 0 : { load: async () => ({ bytes: new Uint8Array((0, import_fs2.readFileSync)(sourcePath)) }) };
|
|
17671
|
+
} : void 0;
|
|
17672
|
+
const confirm = async (count, operation, context) => {
|
|
15937
17673
|
if (count > dmlMaxRows) {
|
|
15938
17674
|
throw new Error(`ArgumentError: ${operation} affected rows (${count}) exceed --dml-max-rows (${dmlMaxRows}).`);
|
|
15939
17675
|
}
|
|
17676
|
+
if (context?.importDetail) {
|
|
17677
|
+
const detail = context.importDetail;
|
|
17678
|
+
const csv = detail.kind === "IMPORT_CSV_SUBTABLE_REPLACE";
|
|
17679
|
+
const lines = [
|
|
17680
|
+
...csv ? [`\u3010\u6700\u91CD\u8981\u8B66\u544A\u3011\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5168\u7F6E\u63DB\u30FB${detail.totalDeleteRows}\u884C\u524A\u9664`] : [],
|
|
17681
|
+
`[IMPORT ${csv ? "CSV" : "JSON"} Confirm] parentsToWrite=${detail.parentsToWrite} insert=${detail.insertedParents} update=${detail.updatedParents}`,
|
|
17682
|
+
csv ? `rowIdPolicy=PRESERVE_EXISTING rowIdNotFound=${detail.rowIdNotFound} invalidParents=${detail.invalidParents}` : `rowIdPolicy=DROP_AND_RENUMBER_ALL (JSON child rows are all newly numbered)`,
|
|
17683
|
+
...!csv && detail.hasDeletes ? ["WARNING: existing subtable rows will be deleted/replaced."] : [],
|
|
17684
|
+
...detail.parents.flatMap((parent) => parent.tables.map(
|
|
17685
|
+
(table) => `parentRow=${parent.parentRow} mode=${parent.mode} table=${table.table} existing=${table.existingRows} input=${table.inputRows} update=${"updateRows" in table ? table.updateRows : 0} add=${table.addRows} delete=${table.deleteRows}${"rowIdNotFound" in table ? ` rowIdNotFound=${table.rowIdNotFound}` : ""}`
|
|
17686
|
+
))
|
|
17687
|
+
];
|
|
17688
|
+
process.stderr.write(`${lines.join("\n")}
|
|
17689
|
+
`);
|
|
17690
|
+
}
|
|
15940
17691
|
if (yes) return true;
|
|
15941
17692
|
if (args.console) return true;
|
|
15942
17693
|
const label = sql?.replace(/\s+/g, " ").trim() ?? operation;
|
|
@@ -15961,10 +17712,20 @@ query=${label}`);
|
|
|
15961
17712
|
timeoutMs: timeout,
|
|
15962
17713
|
cursorMaxActive,
|
|
15963
17714
|
variables: args.variables,
|
|
15964
|
-
|
|
17715
|
+
enableImport: importEnabled,
|
|
17716
|
+
importSource,
|
|
17717
|
+
supportsImportConfirmDetail: true,
|
|
17718
|
+
confirm: batchContainsDml ? async (count, operation, context) => {
|
|
15965
17719
|
if (count > dmlMaxRows) {
|
|
15966
17720
|
throw new Error(`ArgumentError: ${operation} affected rows (${count}) exceed --dml-max-rows (${dmlMaxRows}).`);
|
|
15967
17721
|
}
|
|
17722
|
+
if (context?.importDetail) {
|
|
17723
|
+
const importDetail = context.importDetail;
|
|
17724
|
+
if (importDetail.kind === "IMPORT_CSV_SUBTABLE_REPLACE") process.stderr.write(`\u3010\u6700\u91CD\u8981\u8B66\u544A\u3011\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u5168\u7F6E\u63DB\u30FB${importDetail.totalDeleteRows}\u884C\u524A\u9664
|
|
17725
|
+
`);
|
|
17726
|
+
process.stderr.write(`[IMPORT ${importDetail.kind === "IMPORT_CSV_SUBTABLE_REPLACE" ? "CSV" : "JSON"} Confirm] ${JSON.stringify(importDetail)}
|
|
17727
|
+
`);
|
|
17728
|
+
}
|
|
15968
17729
|
return true;
|
|
15969
17730
|
} : void 0
|
|
15970
17731
|
});
|
|
@@ -15974,14 +17735,19 @@ query=${label}`);
|
|
|
15974
17735
|
maxRecords,
|
|
15975
17736
|
onLimitReached: onLimit,
|
|
15976
17737
|
cacheContext,
|
|
15977
|
-
cursorMaxActive
|
|
17738
|
+
cursorMaxActive,
|
|
17739
|
+
enableImport: importEnabled,
|
|
17740
|
+
importSource
|
|
15978
17741
|
}) : await execute(sql, client, {
|
|
15979
17742
|
maxRecords,
|
|
15980
17743
|
fetchParallel,
|
|
15981
17744
|
onLimitReached: effectiveOnLimit,
|
|
15982
17745
|
confirm: isDmlStatement ? confirm : void 0,
|
|
15983
17746
|
cacheContext,
|
|
15984
|
-
cursorMaxActive
|
|
17747
|
+
cursorMaxActive,
|
|
17748
|
+
enableImport: importEnabled,
|
|
17749
|
+
importSource,
|
|
17750
|
+
supportsImportConfirmDetail: true
|
|
15985
17751
|
});
|
|
15986
17752
|
if (args.dryRun && sqlDiagnosticContext) {
|
|
15987
17753
|
result = restoreSqlDiagnosticValue(result, sqlDiagnosticContext.appBindingByMappedApp);
|
|
@@ -16049,6 +17815,7 @@ if (isDirectCliRun()) {
|
|
|
16049
17815
|
}
|
|
16050
17816
|
// Annotate the CommonJS export names for ESM import in node:
|
|
16051
17817
|
0 && (module.exports = {
|
|
17818
|
+
CLI_IMPORT_SOURCE_REQUIRED_MESSAGE,
|
|
16052
17819
|
HELP_TEXT,
|
|
16053
17820
|
buildBatchDmlConfirmMessage,
|
|
16054
17821
|
buildBatchStatementSummary,
|
|
@@ -16065,5 +17832,6 @@ if (isDirectCliRun()) {
|
|
|
16065
17832
|
parseTokenMap,
|
|
16066
17833
|
runWithArgv,
|
|
16067
17834
|
shouldExitOnEmpty,
|
|
17835
|
+
toCliImportError,
|
|
16068
17836
|
writeBatchOutput
|
|
16069
17837
|
});
|