@rex0220/kintone-sql-tools 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ kintone アプリを SQL 風の構文で操作するツールセットです。
9
9
  ## 機能概要
10
10
 
11
11
  - `SELECT`(JOIN/GROUP BY/HAVING/CTE/UNION)
12
- - `INSERT` / `UPDATE` / `UPSERT` / `DELETE` / `REORDER`(`--allow-dml` 必須)
12
+ - `INSERT` / `UPDATE` / `UPDATE ... FROM` / `UPSERT` / `DELETE` / `REORDER`(`--allow-dml` 必須)
13
13
  - `EXPLAIN`
14
14
  - **バッチ実行(`;` 区切りの複文)と一時テーブル `CREATE TEMP TABLE #t AS SELECT ...`**(v1.4.0)
15
15
  - CLI / MCP: read-only バッチ + DML バッチ(一時テーブル経由の `INSERT ... SELECT` を含む)
package/dist-cli/ksql.js CHANGED
@@ -2054,6 +2054,26 @@ var Parser = class {
2054
2054
  const { appId, subtableCode } = extractTableRef(name, this.prev());
2055
2055
  this.expect("SET" /* SET */);
2056
2056
  const assignments = this.parseAssignments();
2057
+ let from = null;
2058
+ if (this.consume("FROM" /* FROM */)) {
2059
+ const table = this.parseTableRef();
2060
+ if (table.subtableCode) {
2061
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u306B\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u306F\u6307\u5B9A\u3067\u304D\u307E\u305B\u3093", this.prev());
2062
+ }
2063
+ if (table.cteName !== null && !table.cteName.startsWith("#")) {
2064
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u306F #temp \u307E\u305F\u306F APP<n> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08CTE \u306F\u975E\u5BFE\u5FDC\uFF09", this.prev());
2065
+ }
2066
+ if (!table.alias) {
2067
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u306B\u306F\u30A8\u30A4\u30EA\u30A2\u30B9\u304C\u5FC5\u8981\u3067\u3059", this.prev());
2068
+ }
2069
+ from = {
2070
+ appId: table.appId,
2071
+ cteName: table.cteName,
2072
+ alias: table.alias,
2073
+ joinKeyField: "",
2074
+ targetFilter: null
2075
+ };
2076
+ }
2057
2077
  const whereTok = this.peek();
2058
2078
  if (!this.consume("WHERE" /* WHERE */)) {
2059
2079
  throw new ParseError(
@@ -2062,8 +2082,129 @@ var Parser = class {
2062
2082
  );
2063
2083
  }
2064
2084
  const where = this.parseWhereExpr();
2085
+ if (from !== null) {
2086
+ if (subtableCode) {
2087
+ throw new ParseError("\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB UPDATE ... FROM \u306F\u30B5\u30DD\u30FC\u30C8\u3057\u3066\u3044\u307E\u305B\u3093", whereTok);
2088
+ }
2089
+ this.validateUpdateFromAssignments(assignments, from.alias, whereTok);
2090
+ const decomposed = this.decomposeUpdateFromWhere(where, appId, from.alias, whereTok);
2091
+ from.joinKeyField = decomposed.joinKeyField;
2092
+ from.targetFilter = decomposed.targetFilter;
2093
+ } else if (assignments.some((a) => a.value.type === "SOURCE_FIELD")) {
2094
+ throw new ParseError(
2095
+ "SET \u306E\u5024\u306B\u306F\u30EA\u30C6\u30E9\u30EB\u30FB\u7B97\u8853\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u306E\u307F\u306F\u4E0D\u53EF\uFF09",
2096
+ whereTok
2097
+ );
2098
+ }
2099
+ if (from !== null) return { type: "UPDATE", appId, assignments, where, from };
2065
2100
  return subtableCode ? { type: "UPDATE", appId, subtableCode, assignments, where } : { type: "UPDATE", appId, assignments, where };
2066
2101
  }
2102
+ validateUpdateFromAssignments(assignments, sourceAlias, tok) {
2103
+ for (const assignment of assignments) {
2104
+ if (assignment.value.type === "SOURCE_FIELD") {
2105
+ if (assignment.value.alias.toLowerCase() !== sourceAlias.toLowerCase()) {
2106
+ throw new ParseError(`UPDATE ... FROM \u306E SET \u53C2\u7167\u306F\u30BD\u30FC\u30B9 alias ${sourceAlias} \u3067\u4FEE\u98FE\u3057\u3066\u304F\u3060\u3055\u3044`, tok);
2107
+ }
2108
+ continue;
2109
+ }
2110
+ if (this.nodeContainsQualifiedField(assignment.value, sourceAlias)) {
2111
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u5217\u306F SET \u306E\u76F4\u63A5\u5024\u3068\u3057\u3066\u306E\u307F\u53C2\u7167\u3067\u304D\u307E\u3059", tok);
2112
+ }
2113
+ if (assignment.value.type === "SCALAR_SUBQUERY") {
2114
+ throw new ParseError("UPDATE ... FROM \u306E SET \u3067\u306F\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u3092\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
2115
+ }
2116
+ if (this.nodeContainsAnyQualifier(assignment.value)) {
2117
+ throw new ParseError("UPDATE ... FROM \u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u5F0F\u3067\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u3092\u4FEE\u98FE\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044", tok);
2118
+ }
2119
+ }
2120
+ }
2121
+ decomposeUpdateFromWhere(where, targetAppId, sourceAlias, tok) {
2122
+ const leaves = this.flattenTopLevelAnd(where);
2123
+ const joins = [];
2124
+ leaves.forEach((leaf, index) => {
2125
+ const sourceField = this.matchUpdateFromJoin(leaf, targetAppId, sourceAlias);
2126
+ if (sourceField !== null) joins.push({ index, sourceField });
2127
+ });
2128
+ if (joins.length !== 1) {
2129
+ throw new ParseError("UPDATE ... FROM \u306E WHERE \u306B\u306F target.$id = source.key \u306E\u7D50\u5408\u7B49\u5024\u304C\u3061\u3087\u3046\u30691\u3064\u5FC5\u8981\u3067\u3059", tok);
2130
+ }
2131
+ const join2 = joins[0];
2132
+ for (let i = 0; i < leaves.length; i++) {
2133
+ if (i !== join2.index && this.nodeContainsQualifiedField(leaves[i], sourceAlias)) {
2134
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9 alias \u306F\u7D50\u5408\u7B49\u5024\u4EE5\u5916\u306E WHERE \u6761\u4EF6\u3067\u306F\u53C2\u7167\u3067\u304D\u307E\u305B\u3093", tok);
2135
+ }
2136
+ if (i !== join2.index && this.nodeContainsForeignQualifier(leaves[i], targetAppId)) {
2137
+ throw new ParseError(`UPDATE ... FROM \u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u30D5\u30A3\u30EB\u30BF\u306F APP${targetAppId} \u306E\u30D5\u30A3\u30FC\u30EB\u30C9\u3060\u3051\u3092\u53C2\u7167\u3067\u304D\u307E\u3059`, tok);
2138
+ }
2139
+ }
2140
+ const filters = leaves.filter((_, index) => index !== join2.index);
2141
+ const targetFilter = filters.reduce(
2142
+ (acc, expr) => acc === null ? expr : { type: "LOGICAL", op: "AND", left: acc, right: expr },
2143
+ null
2144
+ );
2145
+ return { joinKeyField: join2.sourceField, targetFilter };
2146
+ }
2147
+ flattenTopLevelAnd(expr) {
2148
+ if (expr.type === "GROUP") return this.flattenTopLevelAnd(expr.expr);
2149
+ if (expr.type === "LOGICAL" && expr.op === "AND") {
2150
+ return [...this.flattenTopLevelAnd(expr.left), ...this.flattenTopLevelAnd(expr.right)];
2151
+ }
2152
+ return [expr];
2153
+ }
2154
+ matchUpdateFromJoin(expr, targetAppId, sourceAlias) {
2155
+ if (expr.type !== "BINARY" || expr.op !== "=" || expr.left.type !== "FIELD") return null;
2156
+ const right = expr.right.type === "ARITH_VALUE" && expr.right.expr.type === "FIELD_REF" ? this.splitQualifiedField(expr.right.expr.field) : null;
2157
+ if (right === null) return null;
2158
+ const left = { alias: expr.left.tableAlias, field: expr.left.field };
2159
+ if (this.isTargetIdRef(left, targetAppId) && this.isSourceRef(right, sourceAlias)) return right.field;
2160
+ if (this.isSourceRef(left, sourceAlias) && this.isTargetIdRef(right, targetAppId)) return left.field;
2161
+ return null;
2162
+ }
2163
+ splitQualifiedField(field) {
2164
+ const dot = field.indexOf(".");
2165
+ return dot < 0 ? { alias: null, field } : { alias: field.slice(0, dot), field: field.slice(dot + 1) };
2166
+ }
2167
+ isTargetIdRef(ref, appId) {
2168
+ return ref.field === "$id" && (ref.alias === null || ref.alias.toLowerCase() === `app${appId}`.toLowerCase());
2169
+ }
2170
+ isSourceRef(ref, alias) {
2171
+ return ref.alias?.toLowerCase() === alias.toLowerCase();
2172
+ }
2173
+ nodeContainsQualifiedField(node, alias) {
2174
+ if (Array.isArray(node)) return node.some((v) => this.nodeContainsQualifiedField(v, alias));
2175
+ if (node === null || typeof node !== "object") return false;
2176
+ const obj = node;
2177
+ if (obj["type"] === "FIELD" && typeof obj["tableAlias"] === "string" && obj["tableAlias"].toLowerCase() === alias.toLowerCase()) return true;
2178
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") {
2179
+ const ref = this.splitQualifiedField(obj["field"]);
2180
+ if (this.isSourceRef(ref, alias)) return true;
2181
+ }
2182
+ return Object.values(obj).some((v) => this.nodeContainsQualifiedField(v, alias));
2183
+ }
2184
+ nodeContainsForeignQualifier(node, targetAppId) {
2185
+ if (Array.isArray(node)) return node.some((v) => this.nodeContainsForeignQualifier(v, targetAppId));
2186
+ if (node === null || typeof node !== "object") return false;
2187
+ const obj = node;
2188
+ const expected = `app${targetAppId}`.toLowerCase();
2189
+ if (obj["type"] === "FIELD" && typeof obj["tableAlias"] === "string") {
2190
+ return obj["tableAlias"].toLowerCase() !== expected;
2191
+ }
2192
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") {
2193
+ const ref = this.splitQualifiedField(obj["field"]);
2194
+ if (ref.alias !== null) return ref.alias.toLowerCase() !== expected;
2195
+ }
2196
+ return Object.values(obj).some((v) => this.nodeContainsForeignQualifier(v, targetAppId));
2197
+ }
2198
+ nodeContainsAnyQualifier(node) {
2199
+ if (Array.isArray(node)) return node.some((v) => this.nodeContainsAnyQualifier(v));
2200
+ if (node === null || typeof node !== "object") return false;
2201
+ const obj = node;
2202
+ if (obj["type"] === "FIELD" && typeof obj["tableAlias"] === "string") return true;
2203
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") {
2204
+ if (this.splitQualifiedField(obj["field"]).alias !== null) return true;
2205
+ }
2206
+ return Object.values(obj).some((v) => this.nodeContainsAnyQualifier(v));
2207
+ }
2067
2208
  parseAssignments() {
2068
2209
  const assignments = [];
2069
2210
  do {
@@ -2098,6 +2239,12 @@ var Parser = class {
2098
2239
  const node = this.parseArithAddSub();
2099
2240
  if (node.type === "NUMBER") return node;
2100
2241
  if (node.type === "ARITH") return node;
2242
+ if (node.type === "FIELD_REF") {
2243
+ const dot = node.field.indexOf(".");
2244
+ if (dot > 0 && dot < node.field.length - 1) {
2245
+ return { type: "SOURCE_FIELD", alias: node.field.slice(0, dot), field: node.field.slice(dot + 1) };
2246
+ }
2247
+ }
2101
2248
  throw new ParseError(
2102
2249
  "SET \u306E\u5024\u306B\u306F\u30EA\u30C6\u30E9\u30EB\u30FB\u7B97\u8853\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u306E\u307F\u306F\u4E0D\u53EF\uFF09",
2103
2250
  tok
@@ -3685,7 +3832,8 @@ function analyzeBatch(statements) {
3685
3832
  tempTablesDropped: dropped,
3686
3833
  dependsOn: [...dependsOn].sort((a, b) => a - b),
3687
3834
  tempOnlySource,
3688
- targetAppId: isDmlType(statementType) && typeof stmt.appId === "number" ? stmt.appId : null
3835
+ targetAppId: isDmlType(statementType) && typeof stmt.appId === "number" ? stmt.appId : null,
3836
+ isUpdateFrom: stmt.type === "UPDATE" && stmt.from != null
3689
3837
  });
3690
3838
  });
3691
3839
  const containsDml = results.some((r) => r.isDml);
@@ -4230,7 +4378,7 @@ function updateToPutBatches(stmt, ids, fieldTypes = /* @__PURE__ */ new Map()) {
4230
4378
  function buildUpdateRecord(assignments, fieldTypes) {
4231
4379
  const record = {};
4232
4380
  for (const { field, value } of assignments) {
4233
- if (value.type === "ARITH" || value.type === "CASE_VALUE") continue;
4381
+ if (value.type === "ARITH" || value.type === "CASE_VALUE" || value.type === "SOURCE_FIELD") continue;
4234
4382
  record[field] = { value: toKintoneValue(value, fieldTypes.get(field)) };
4235
4383
  }
4236
4384
  return record;
@@ -4334,6 +4482,8 @@ function updateToPutBatchesArith(stmt, records, fieldTypes = /* @__PURE__ */ new
4334
4482
  record[field] = { value: String(evalArith(value, raw)) };
4335
4483
  } else if (value.type === "CASE_VALUE") {
4336
4484
  record[field] = { value: evalCaseWhenValue(value.expr, row, fieldTypes.get(field)) };
4485
+ } else if (value.type === "SOURCE_FIELD") {
4486
+ throw new DmlConvertError("SOURCE_FIELD \u306F UPDATE ... FROM \u5C02\u7528\u3067\u3059");
4337
4487
  } else {
4338
4488
  record[field] = { value: toKintoneValue(value, fieldTypes.get(field)) };
4339
4489
  }
@@ -4345,6 +4495,51 @@ function updateToPutBatchesArith(stmt, records, fieldTypes = /* @__PURE__ */ new
4345
4495
  records: batch
4346
4496
  }));
4347
4497
  }
4498
+ var UPDATE_FROM_UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
4499
+ "CHECK_BOX",
4500
+ "MULTI_SELECT",
4501
+ "USER_SELECT",
4502
+ "ORGANIZATION_SELECT",
4503
+ "GROUP_SELECT",
4504
+ "FILE"
4505
+ ]);
4506
+ function updateFromToPutBatches(stmt, matched, fieldTypes = /* @__PURE__ */ new Map()) {
4507
+ const updateRecords = matched.map(({ target, source }) => {
4508
+ const id = Number(target["$id"]?.value);
4509
+ if (!Number.isSafeInteger(id) || id <= 0) {
4510
+ throw new DmlConvertError("UPDATE ... FROM \u306E\u5BFE\u8C61\u30EC\u30B3\u30FC\u30C9\u756A\u53F7\u304C\u4E0D\u6B63\u3067\u3059");
4511
+ }
4512
+ const targetRow = kintoneRecordToProcessRow(target);
4513
+ const record = {};
4514
+ for (const { field, value } of stmt.assignments) {
4515
+ const fieldType = fieldTypes.get(field);
4516
+ if (value.type === "SOURCE_FIELD") {
4517
+ if (UPDATE_FROM_UNSUPPORTED_TYPES.has(fieldType ?? "")) {
4518
+ throw new DmlConvertError(`UPDATE ... FROM \u306E SOURCE_FIELD \u306F ${fieldType} \u30D5\u30A3\u30FC\u30EB\u30C9\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u305B\u3093\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9: ${field}\uFF09`);
4519
+ }
4520
+ if (!Object.prototype.hasOwnProperty.call(source, value.field)) {
4521
+ throw new DmlConvertError(`UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u5217 ${value.field} \u304C\u5B58\u5728\u3057\u307E\u305B\u3093`);
4522
+ }
4523
+ const raw = source[value.field];
4524
+ if (typeof raw !== "string") {
4525
+ throw new DmlConvertError(`UPDATE ... FROM \u306E SOURCE_FIELD \u306F\u30B9\u30AB\u30E9\u30FC\u5024\u306E\u307F\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u3059\uFF08\u5217: ${value.field}\uFF09`);
4526
+ }
4527
+ if ((fieldType === "NUMBER" || fieldType === "CALC") && raw !== "" && !Number.isFinite(Number(raw))) {
4528
+ throw new DmlConvertError(`\u6570\u5024\u30D5\u30A3\u30FC\u30EB\u30C9 ${field} \u306B\u5909\u63DB\u3067\u304D\u306A\u3044\u5024\u3067\u3059: ${raw}`);
4529
+ }
4530
+ record[field] = { value: toKintoneValue({ type: "STRING", value: raw }, fieldType) };
4531
+ } else if (value.type === "ARITH") {
4532
+ record[field] = { value: String(evalArith(value, target)) };
4533
+ } else if (value.type === "CASE_VALUE") {
4534
+ record[field] = { value: evalCaseWhenValue(value.expr, targetRow, fieldType) };
4535
+ } else {
4536
+ record[field] = { value: toKintoneValue(value, fieldType) };
4537
+ }
4538
+ }
4539
+ return { id, record };
4540
+ });
4541
+ return chunk(updateRecords, 100).map((records) => ({ app: stmt.appId, records }));
4542
+ }
4348
4543
  function kintoneRecordToProcessRow(raw) {
4349
4544
  return Object.fromEntries(
4350
4545
  Object.entries(raw).map(([k, v]) => [
@@ -5459,6 +5654,8 @@ async function executeBatch(sql, client, options = {}) {
5459
5654
  for (const s of analysis.statements) {
5460
5655
  if (!s.isDml || s.tempTablesReferenced.length === 0) continue;
5461
5656
  if (s.statementType === "INSERT_SELECT" || s.statementType === "UPSERT_SELECT") continue;
5657
+ const parsed = statements[s.index];
5658
+ if (parsed?.type === "UPDATE" && parsed.from?.cteName != null) continue;
5462
5659
  throw new BatchAnalysisError(
5463
5660
  `ArgumentError: temp table references in ${s.statementType} are not supported yet.`,
5464
5661
  s.index
@@ -5615,6 +5812,9 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
5615
5812
  if (resolvedStmt.type === "UPSERT_SELECT") {
5616
5813
  return { result: await executeUpsertSelect(resolvedStmt, client, options, cacheContext, tempTables) };
5617
5814
  }
5815
+ if (resolvedStmt.type === "UPDATE" && resolvedStmt.from?.cteName != null) {
5816
+ return { result: await executeUpdate(resolvedStmt, client, options, cacheContext, tempTables) };
5817
+ }
5618
5818
  throw new Error(`ArgumentError: temp table references in ${stmt.type} are not supported yet.`);
5619
5819
  }
5620
5820
  return { result: await executeParsedStatement(resolvedStmt, client, options, cacheContext) };
@@ -6800,10 +7000,13 @@ async function executeInsertSelect(stmt, client, options, cacheContext, cteCache
6800
7000
  insertedCount: createdIds.flat().length
6801
7001
  };
6802
7002
  }
6803
- async function executeUpdate(stmt, client, options, cacheContext) {
7003
+ async function executeUpdate(stmt, client, options, cacheContext, tempTables) {
6804
7004
  if (stmt.subtableCode) {
6805
7005
  return executeUpdateSubtable(stmt, client, options, cacheContext);
6806
7006
  }
7007
+ if (stmt.from != null) {
7008
+ return executeUpdateFrom(stmt, stmt.from, client, options, cacheContext, tempTables);
7009
+ }
6807
7010
  const maxRecords = options.maxRecords ?? 1e4;
6808
7011
  await resolveSetSubqueries(stmt.assignments, client, options, cacheContext);
6809
7012
  const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
@@ -6845,6 +7048,113 @@ async function executeUpdate(stmt, client, options, cacheContext) {
6845
7048
  }
6846
7049
  return { type: "UPDATE", updatedCount: ids.length };
6847
7050
  }
7051
+ var UPDATE_FROM_ID_CHUNK_SIZE = 50;
7052
+ var UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES = /* @__PURE__ */ new Set([
7053
+ "CHECK_BOX",
7054
+ "MULTI_SELECT",
7055
+ "USER_SELECT",
7056
+ "ORGANIZATION_SELECT",
7057
+ "GROUP_SELECT",
7058
+ "FILE"
7059
+ ]);
7060
+ async function executeUpdateFrom(stmt, from, client, options, cacheContext, tempTables) {
7061
+ const sourceFields = [...new Set(stmt.assignments.filter((a) => a.value.type === "SOURCE_FIELD").map((a) => a.value.type === "SOURCE_FIELD" ? a.value.field : ""))];
7062
+ const requiredSourceFields = [.../* @__PURE__ */ new Set([from.joinKeyField, ...sourceFields])];
7063
+ let sourceRows;
7064
+ if (from.cteName !== null) {
7065
+ const table = tempTables?.get(from.cteName);
7066
+ if (!table) throw new Error(`ArgumentError: temp table ${from.cteName} is not available.`);
7067
+ for (const field of requiredSourceFields) {
7068
+ if (!table.columns.includes(field)) {
7069
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
7070
+ }
7071
+ }
7072
+ sourceRows = table.rows;
7073
+ } else {
7074
+ const sourceTypes = await getFieldTypeMap(from.appId, client, cacheContext);
7075
+ for (const field of requiredSourceFields) {
7076
+ if (field !== "$id" && !sourceTypes.has(field)) {
7077
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
7078
+ }
7079
+ const type = sourceTypes.get(field);
7080
+ if (UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES.has(type ?? "")) {
7081
+ throw new Error(`ArgumentError: UPDATE ... FROM does not support source field type ${type} (${field}).`);
7082
+ }
7083
+ }
7084
+ const maxRecords = options.maxRecords ?? 1e4;
7085
+ const resolved = await fetchRecordsForSharedPlan(
7086
+ client.getRecords,
7087
+ from.appId,
7088
+ "",
7089
+ requiredSourceFields,
7090
+ { maxRecords, parallel: options.fetchParallel ?? 1, onLimit: "error" }
7091
+ );
7092
+ sourceRows = resolved.records.map((record) => flatten(record, null));
7093
+ }
7094
+ const sourceById = /* @__PURE__ */ new Map();
7095
+ for (const row of sourceRows) {
7096
+ if (!Object.prototype.hasOwnProperty.call(row, from.joinKeyField)) {
7097
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${from.joinKeyField} does not exist.`);
7098
+ }
7099
+ const raw = row[from.joinKeyField];
7100
+ const text = typeof raw === "string" ? raw.trim() : "";
7101
+ const id = Number(text);
7102
+ if (text === "" || !Number.isSafeInteger(id) || id <= 0) {
7103
+ throw new Error(`ArgumentError: UPDATE ... FROM source key must be a positive safe integer: ${String(raw)}`);
7104
+ }
7105
+ if (sourceById.has(id)) {
7106
+ throw new Error(`ArgumentError: UPDATE ... FROM source has multiple rows for target $id ${id}.`);
7107
+ }
7108
+ sourceById.set(id, row);
7109
+ }
7110
+ const targetIds = [...sourceById.keys()];
7111
+ const targetFields = collectUpdateFromTargetFields(stmt);
7112
+ const filterQuery = from.targetFilter === null ? "" : updateToGetQuery({ ...stmt, from: null, where: from.targetFilter }).query;
7113
+ const targetRecords = [];
7114
+ for (const ids of splitChunks(targetIds, UPDATE_FROM_ID_CHUNK_SIZE)) {
7115
+ const idQuery = `$id in (${ids.map((id) => sqlQuote(String(id))).join(",")})`;
7116
+ const query = filterQuery ? `(${idQuery}) and (${filterQuery})` : idQuery;
7117
+ const resolved = await fetchRecordsForSharedPlan(
7118
+ client.getRecords,
7119
+ stmt.appId,
7120
+ query,
7121
+ targetFields,
7122
+ { maxRecords: Math.max(ids.length, 1), parallel: options.fetchParallel ?? 1, onLimit: "error" }
7123
+ );
7124
+ targetRecords.push(...resolved.records);
7125
+ }
7126
+ if (options.confirm) {
7127
+ const ok = await options.confirm(targetRecords.length, "UPDATE");
7128
+ if (!ok) throw new OperationCancelledError("UPDATE", targetRecords.length);
7129
+ }
7130
+ const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
7131
+ const matched = targetRecords.map((target) => {
7132
+ const id = Number(target["$id"]?.value);
7133
+ const source = sourceById.get(id);
7134
+ if (!source) throw new Error(`ArgumentError: UPDATE ... FROM could not resolve source row for target $id ${id}.`);
7135
+ return { target, source };
7136
+ });
7137
+ const batches = updateFromToPutBatches(stmt, matched, fieldTypes);
7138
+ for (const batch of batches) await client.putRecords(batch);
7139
+ return { type: "UPDATE", updatedCount: targetRecords.length };
7140
+ }
7141
+ function collectUpdateFromTargetFields(stmt) {
7142
+ const fields = /* @__PURE__ */ new Set(["$id"]);
7143
+ const visit = (node) => {
7144
+ if (Array.isArray(node)) {
7145
+ node.forEach(visit);
7146
+ return;
7147
+ }
7148
+ if (node === null || typeof node !== "object") return;
7149
+ const obj = node;
7150
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") fields.add(obj["field"]);
7151
+ for (const value of Object.values(obj)) visit(value);
7152
+ };
7153
+ for (const assignment of stmt.assignments) {
7154
+ if (assignment.value.type !== "SOURCE_FIELD") visit(assignment.value);
7155
+ }
7156
+ return [...fields];
7157
+ }
6848
7158
  async function executeDelete(stmt, client, options, cacheContext) {
6849
7159
  if (stmt.subtableCode) {
6850
7160
  return executeDeleteSubtable(stmt, client, options, cacheContext);
@@ -7770,9 +8080,16 @@ function buildUpdatePlan(stmt, label) {
7770
8080
  const isSubq = stmt.assignments.some((a) => a.value.type === "SCALAR_SUBQUERY");
7771
8081
  const lines = [];
7772
8082
  if (label) lines.push(label);
7773
- lines.push(` [UPDATE]`);
8083
+ lines.push(stmt.from ? ` [UPDATE FROM]` : ` [UPDATE]`);
7774
8084
  lines.push(` target: APP${stmt.appId} (${stmt.appId})`);
7775
- lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
8085
+ if (stmt.from) {
8086
+ const source = stmt.from.cteName ?? `APP${stmt.from.appId}`;
8087
+ lines.push(` source: ${source} AS ${stmt.from.alias}`);
8088
+ lines.push(` join: APP${stmt.appId}.$id = ${stmt.from.alias}.${stmt.from.joinKeyField}`);
8089
+ lines.push(` target filter: ${stmt.from.targetFilter ? safeWhereToKintone(stmt.from.targetFilter) : "(none)"}`);
8090
+ } else {
8091
+ lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
8092
+ }
7776
8093
  lines.push(` api: GET /k/v1/records.json \u2192 PUT /k/v1/records.json`);
7777
8094
  const setTypes = [];
7778
8095
  if (isArith) setTypes.push("\u7B97\u8853 SET\uFF08\u73FE\u5728\u5024\u3092\u53D6\u5F97\u3057\u3066\u8A08\u7B97\uFF09");
@@ -7885,6 +8202,7 @@ function formatAssignment(a) {
7885
8202
  if (v.type === "ARITH") return `${a.field} = ${formatArithExprStr(v)}`;
7886
8203
  if (v.type === "CASE_VALUE") return `${a.field} = CASE WHEN ...`;
7887
8204
  if (v.type === "SCALAR_SUBQUERY") return `${a.field} = (SELECT ...)`;
8205
+ if (v.type === "SOURCE_FIELD") return `${a.field} = ${v.alias}.${v.field}`;
7888
8206
  return `${a.field} = (${v.type})`;
7889
8207
  }
7890
8208
  function formatArithExprStr(expr) {
@@ -32967,6 +32967,26 @@ var Parser = class {
32967
32967
  const { appId, subtableCode } = extractTableRef(name, this.prev());
32968
32968
  this.expect("SET" /* SET */);
32969
32969
  const assignments = this.parseAssignments();
32970
+ let from = null;
32971
+ if (this.consume("FROM" /* FROM */)) {
32972
+ const table = this.parseTableRef();
32973
+ if (table.subtableCode) {
32974
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u306B\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u306F\u6307\u5B9A\u3067\u304D\u307E\u305B\u3093", this.prev());
32975
+ }
32976
+ if (table.cteName !== null && !table.cteName.startsWith("#")) {
32977
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u306F #temp \u307E\u305F\u306F APP<n> \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08CTE \u306F\u975E\u5BFE\u5FDC\uFF09", this.prev());
32978
+ }
32979
+ if (!table.alias) {
32980
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u306B\u306F\u30A8\u30A4\u30EA\u30A2\u30B9\u304C\u5FC5\u8981\u3067\u3059", this.prev());
32981
+ }
32982
+ from = {
32983
+ appId: table.appId,
32984
+ cteName: table.cteName,
32985
+ alias: table.alias,
32986
+ joinKeyField: "",
32987
+ targetFilter: null
32988
+ };
32989
+ }
32970
32990
  const whereTok = this.peek();
32971
32991
  if (!this.consume("WHERE" /* WHERE */)) {
32972
32992
  throw new ParseError(
@@ -32975,8 +32995,129 @@ var Parser = class {
32975
32995
  );
32976
32996
  }
32977
32997
  const where = this.parseWhereExpr();
32998
+ if (from !== null) {
32999
+ if (subtableCode) {
33000
+ throw new ParseError("\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB UPDATE ... FROM \u306F\u30B5\u30DD\u30FC\u30C8\u3057\u3066\u3044\u307E\u305B\u3093", whereTok);
33001
+ }
33002
+ this.validateUpdateFromAssignments(assignments, from.alias, whereTok);
33003
+ const decomposed = this.decomposeUpdateFromWhere(where, appId, from.alias, whereTok);
33004
+ from.joinKeyField = decomposed.joinKeyField;
33005
+ from.targetFilter = decomposed.targetFilter;
33006
+ } else if (assignments.some((a) => a.value.type === "SOURCE_FIELD")) {
33007
+ throw new ParseError(
33008
+ "SET \u306E\u5024\u306B\u306F\u30EA\u30C6\u30E9\u30EB\u30FB\u7B97\u8853\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u306E\u307F\u306F\u4E0D\u53EF\uFF09",
33009
+ whereTok
33010
+ );
33011
+ }
33012
+ if (from !== null) return { type: "UPDATE", appId, assignments, where, from };
32978
33013
  return subtableCode ? { type: "UPDATE", appId, subtableCode, assignments, where } : { type: "UPDATE", appId, assignments, where };
32979
33014
  }
33015
+ validateUpdateFromAssignments(assignments, sourceAlias, tok) {
33016
+ for (const assignment of assignments) {
33017
+ if (assignment.value.type === "SOURCE_FIELD") {
33018
+ if (assignment.value.alias.toLowerCase() !== sourceAlias.toLowerCase()) {
33019
+ throw new ParseError(`UPDATE ... FROM \u306E SET \u53C2\u7167\u306F\u30BD\u30FC\u30B9 alias ${sourceAlias} \u3067\u4FEE\u98FE\u3057\u3066\u304F\u3060\u3055\u3044`, tok);
33020
+ }
33021
+ continue;
33022
+ }
33023
+ if (this.nodeContainsQualifiedField(assignment.value, sourceAlias)) {
33024
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u5217\u306F SET \u306E\u76F4\u63A5\u5024\u3068\u3057\u3066\u306E\u307F\u53C2\u7167\u3067\u304D\u307E\u3059", tok);
33025
+ }
33026
+ if (assignment.value.type === "SCALAR_SUBQUERY") {
33027
+ throw new ParseError("UPDATE ... FROM \u306E SET \u3067\u306F\u30B9\u30AB\u30E9\u30FC\u30B5\u30D6\u30AF\u30A8\u30EA\u3092\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093", tok);
33028
+ }
33029
+ if (this.nodeContainsAnyQualifier(assignment.value)) {
33030
+ throw new ParseError("UPDATE ... FROM \u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u5F0F\u3067\u306F\u30D5\u30A3\u30FC\u30EB\u30C9\u3092\u4FEE\u98FE\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044", tok);
33031
+ }
33032
+ }
33033
+ }
33034
+ decomposeUpdateFromWhere(where, targetAppId, sourceAlias, tok) {
33035
+ const leaves = this.flattenTopLevelAnd(where);
33036
+ const joins = [];
33037
+ leaves.forEach((leaf, index) => {
33038
+ const sourceField = this.matchUpdateFromJoin(leaf, targetAppId, sourceAlias);
33039
+ if (sourceField !== null) joins.push({ index, sourceField });
33040
+ });
33041
+ if (joins.length !== 1) {
33042
+ throw new ParseError("UPDATE ... FROM \u306E WHERE \u306B\u306F target.$id = source.key \u306E\u7D50\u5408\u7B49\u5024\u304C\u3061\u3087\u3046\u30691\u3064\u5FC5\u8981\u3067\u3059", tok);
33043
+ }
33044
+ const join = joins[0];
33045
+ for (let i = 0; i < leaves.length; i++) {
33046
+ if (i !== join.index && this.nodeContainsQualifiedField(leaves[i], sourceAlias)) {
33047
+ throw new ParseError("UPDATE ... FROM \u306E\u30BD\u30FC\u30B9 alias \u306F\u7D50\u5408\u7B49\u5024\u4EE5\u5916\u306E WHERE \u6761\u4EF6\u3067\u306F\u53C2\u7167\u3067\u304D\u307E\u305B\u3093", tok);
33048
+ }
33049
+ if (i !== join.index && this.nodeContainsForeignQualifier(leaves[i], targetAppId)) {
33050
+ throw new ParseError(`UPDATE ... FROM \u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u30D5\u30A3\u30EB\u30BF\u306F APP${targetAppId} \u306E\u30D5\u30A3\u30FC\u30EB\u30C9\u3060\u3051\u3092\u53C2\u7167\u3067\u304D\u307E\u3059`, tok);
33051
+ }
33052
+ }
33053
+ const filters = leaves.filter((_, index) => index !== join.index);
33054
+ const targetFilter = filters.reduce(
33055
+ (acc, expr) => acc === null ? expr : { type: "LOGICAL", op: "AND", left: acc, right: expr },
33056
+ null
33057
+ );
33058
+ return { joinKeyField: join.sourceField, targetFilter };
33059
+ }
33060
+ flattenTopLevelAnd(expr) {
33061
+ if (expr.type === "GROUP") return this.flattenTopLevelAnd(expr.expr);
33062
+ if (expr.type === "LOGICAL" && expr.op === "AND") {
33063
+ return [...this.flattenTopLevelAnd(expr.left), ...this.flattenTopLevelAnd(expr.right)];
33064
+ }
33065
+ return [expr];
33066
+ }
33067
+ matchUpdateFromJoin(expr, targetAppId, sourceAlias) {
33068
+ if (expr.type !== "BINARY" || expr.op !== "=" || expr.left.type !== "FIELD") return null;
33069
+ const right = expr.right.type === "ARITH_VALUE" && expr.right.expr.type === "FIELD_REF" ? this.splitQualifiedField(expr.right.expr.field) : null;
33070
+ if (right === null) return null;
33071
+ const left = { alias: expr.left.tableAlias, field: expr.left.field };
33072
+ if (this.isTargetIdRef(left, targetAppId) && this.isSourceRef(right, sourceAlias)) return right.field;
33073
+ if (this.isSourceRef(left, sourceAlias) && this.isTargetIdRef(right, targetAppId)) return left.field;
33074
+ return null;
33075
+ }
33076
+ splitQualifiedField(field) {
33077
+ const dot = field.indexOf(".");
33078
+ return dot < 0 ? { alias: null, field } : { alias: field.slice(0, dot), field: field.slice(dot + 1) };
33079
+ }
33080
+ isTargetIdRef(ref, appId) {
33081
+ return ref.field === "$id" && (ref.alias === null || ref.alias.toLowerCase() === `app${appId}`.toLowerCase());
33082
+ }
33083
+ isSourceRef(ref, alias) {
33084
+ return ref.alias?.toLowerCase() === alias.toLowerCase();
33085
+ }
33086
+ nodeContainsQualifiedField(node, alias) {
33087
+ if (Array.isArray(node)) return node.some((v) => this.nodeContainsQualifiedField(v, alias));
33088
+ if (node === null || typeof node !== "object") return false;
33089
+ const obj = node;
33090
+ if (obj["type"] === "FIELD" && typeof obj["tableAlias"] === "string" && obj["tableAlias"].toLowerCase() === alias.toLowerCase()) return true;
33091
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") {
33092
+ const ref = this.splitQualifiedField(obj["field"]);
33093
+ if (this.isSourceRef(ref, alias)) return true;
33094
+ }
33095
+ return Object.values(obj).some((v) => this.nodeContainsQualifiedField(v, alias));
33096
+ }
33097
+ nodeContainsForeignQualifier(node, targetAppId) {
33098
+ if (Array.isArray(node)) return node.some((v) => this.nodeContainsForeignQualifier(v, targetAppId));
33099
+ if (node === null || typeof node !== "object") return false;
33100
+ const obj = node;
33101
+ const expected = `app${targetAppId}`.toLowerCase();
33102
+ if (obj["type"] === "FIELD" && typeof obj["tableAlias"] === "string") {
33103
+ return obj["tableAlias"].toLowerCase() !== expected;
33104
+ }
33105
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") {
33106
+ const ref = this.splitQualifiedField(obj["field"]);
33107
+ if (ref.alias !== null) return ref.alias.toLowerCase() !== expected;
33108
+ }
33109
+ return Object.values(obj).some((v) => this.nodeContainsForeignQualifier(v, targetAppId));
33110
+ }
33111
+ nodeContainsAnyQualifier(node) {
33112
+ if (Array.isArray(node)) return node.some((v) => this.nodeContainsAnyQualifier(v));
33113
+ if (node === null || typeof node !== "object") return false;
33114
+ const obj = node;
33115
+ if (obj["type"] === "FIELD" && typeof obj["tableAlias"] === "string") return true;
33116
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") {
33117
+ if (this.splitQualifiedField(obj["field"]).alias !== null) return true;
33118
+ }
33119
+ return Object.values(obj).some((v) => this.nodeContainsAnyQualifier(v));
33120
+ }
32980
33121
  parseAssignments() {
32981
33122
  const assignments = [];
32982
33123
  do {
@@ -33011,6 +33152,12 @@ var Parser = class {
33011
33152
  const node = this.parseArithAddSub();
33012
33153
  if (node.type === "NUMBER") return node;
33013
33154
  if (node.type === "ARITH") return node;
33155
+ if (node.type === "FIELD_REF") {
33156
+ const dot = node.field.indexOf(".");
33157
+ if (dot > 0 && dot < node.field.length - 1) {
33158
+ return { type: "SOURCE_FIELD", alias: node.field.slice(0, dot), field: node.field.slice(dot + 1) };
33159
+ }
33160
+ }
33014
33161
  throw new ParseError(
33015
33162
  "SET \u306E\u5024\u306B\u306F\u30EA\u30C6\u30E9\u30EB\u30FB\u7B97\u8853\u5F0F\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9\u53C2\u7167\u306E\u307F\u306F\u4E0D\u53EF\uFF09",
33016
33163
  tok
@@ -34586,7 +34733,8 @@ function analyzeBatch(statements) {
34586
34733
  tempTablesDropped: dropped,
34587
34734
  dependsOn: [...dependsOn].sort((a, b) => a - b),
34588
34735
  tempOnlySource,
34589
- targetAppId: isDmlType(statementType) && typeof stmt.appId === "number" ? stmt.appId : null
34736
+ targetAppId: isDmlType(statementType) && typeof stmt.appId === "number" ? stmt.appId : null,
34737
+ isUpdateFrom: stmt.type === "UPDATE" && stmt.from != null
34590
34738
  });
34591
34739
  });
34592
34740
  const containsDml = results.some((r) => r.isDml);
@@ -35131,7 +35279,7 @@ function updateToPutBatches(stmt, ids, fieldTypes = /* @__PURE__ */ new Map()) {
35131
35279
  function buildUpdateRecord(assignments, fieldTypes) {
35132
35280
  const record2 = {};
35133
35281
  for (const { field, value } of assignments) {
35134
- if (value.type === "ARITH" || value.type === "CASE_VALUE") continue;
35282
+ if (value.type === "ARITH" || value.type === "CASE_VALUE" || value.type === "SOURCE_FIELD") continue;
35135
35283
  record2[field] = { value: toKintoneValue(value, fieldTypes.get(field)) };
35136
35284
  }
35137
35285
  return record2;
@@ -35235,6 +35383,8 @@ function updateToPutBatchesArith(stmt, records, fieldTypes = /* @__PURE__ */ new
35235
35383
  record2[field] = { value: String(evalArith(value, raw)) };
35236
35384
  } else if (value.type === "CASE_VALUE") {
35237
35385
  record2[field] = { value: evalCaseWhenValue(value.expr, row, fieldTypes.get(field)) };
35386
+ } else if (value.type === "SOURCE_FIELD") {
35387
+ throw new DmlConvertError("SOURCE_FIELD \u306F UPDATE ... FROM \u5C02\u7528\u3067\u3059");
35238
35388
  } else {
35239
35389
  record2[field] = { value: toKintoneValue(value, fieldTypes.get(field)) };
35240
35390
  }
@@ -35246,6 +35396,51 @@ function updateToPutBatchesArith(stmt, records, fieldTypes = /* @__PURE__ */ new
35246
35396
  records: batch
35247
35397
  }));
35248
35398
  }
35399
+ var UPDATE_FROM_UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
35400
+ "CHECK_BOX",
35401
+ "MULTI_SELECT",
35402
+ "USER_SELECT",
35403
+ "ORGANIZATION_SELECT",
35404
+ "GROUP_SELECT",
35405
+ "FILE"
35406
+ ]);
35407
+ function updateFromToPutBatches(stmt, matched, fieldTypes = /* @__PURE__ */ new Map()) {
35408
+ const updateRecords = matched.map(({ target, source }) => {
35409
+ const id = Number(target["$id"]?.value);
35410
+ if (!Number.isSafeInteger(id) || id <= 0) {
35411
+ throw new DmlConvertError("UPDATE ... FROM \u306E\u5BFE\u8C61\u30EC\u30B3\u30FC\u30C9\u756A\u53F7\u304C\u4E0D\u6B63\u3067\u3059");
35412
+ }
35413
+ const targetRow = kintoneRecordToProcessRow(target);
35414
+ const record2 = {};
35415
+ for (const { field, value } of stmt.assignments) {
35416
+ const fieldType = fieldTypes.get(field);
35417
+ if (value.type === "SOURCE_FIELD") {
35418
+ if (UPDATE_FROM_UNSUPPORTED_TYPES.has(fieldType ?? "")) {
35419
+ throw new DmlConvertError(`UPDATE ... FROM \u306E SOURCE_FIELD \u306F ${fieldType} \u30D5\u30A3\u30FC\u30EB\u30C9\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u305B\u3093\uFF08\u30D5\u30A3\u30FC\u30EB\u30C9: ${field}\uFF09`);
35420
+ }
35421
+ if (!Object.prototype.hasOwnProperty.call(source, value.field)) {
35422
+ throw new DmlConvertError(`UPDATE ... FROM \u306E\u30BD\u30FC\u30B9\u5217 ${value.field} \u304C\u5B58\u5728\u3057\u307E\u305B\u3093`);
35423
+ }
35424
+ const raw = source[value.field];
35425
+ if (typeof raw !== "string") {
35426
+ throw new DmlConvertError(`UPDATE ... FROM \u306E SOURCE_FIELD \u306F\u30B9\u30AB\u30E9\u30FC\u5024\u306E\u307F\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u3059\uFF08\u5217: ${value.field}\uFF09`);
35427
+ }
35428
+ if ((fieldType === "NUMBER" || fieldType === "CALC") && raw !== "" && !Number.isFinite(Number(raw))) {
35429
+ throw new DmlConvertError(`\u6570\u5024\u30D5\u30A3\u30FC\u30EB\u30C9 ${field} \u306B\u5909\u63DB\u3067\u304D\u306A\u3044\u5024\u3067\u3059: ${raw}`);
35430
+ }
35431
+ record2[field] = { value: toKintoneValue({ type: "STRING", value: raw }, fieldType) };
35432
+ } else if (value.type === "ARITH") {
35433
+ record2[field] = { value: String(evalArith(value, target)) };
35434
+ } else if (value.type === "CASE_VALUE") {
35435
+ record2[field] = { value: evalCaseWhenValue(value.expr, targetRow, fieldType) };
35436
+ } else {
35437
+ record2[field] = { value: toKintoneValue(value, fieldType) };
35438
+ }
35439
+ }
35440
+ return { id, record: record2 };
35441
+ });
35442
+ return chunk(updateRecords, 100).map((records) => ({ app: stmt.appId, records }));
35443
+ }
35249
35444
  function kintoneRecordToProcessRow(raw) {
35250
35445
  return Object.fromEntries(
35251
35446
  Object.entries(raw).map(([k, v]) => [
@@ -36360,6 +36555,8 @@ async function executeBatch(sql, client, options = {}) {
36360
36555
  for (const s of analysis.statements) {
36361
36556
  if (!s.isDml || s.tempTablesReferenced.length === 0) continue;
36362
36557
  if (s.statementType === "INSERT_SELECT" || s.statementType === "UPSERT_SELECT") continue;
36558
+ const parsed = statements[s.index];
36559
+ if (parsed?.type === "UPDATE" && parsed.from?.cteName != null) continue;
36363
36560
  throw new BatchAnalysisError(
36364
36561
  `ArgumentError: temp table references in ${s.statementType} are not supported yet.`,
36365
36562
  s.index
@@ -36516,6 +36713,9 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
36516
36713
  if (resolvedStmt.type === "UPSERT_SELECT") {
36517
36714
  return { result: await executeUpsertSelect(resolvedStmt, client, options, cacheContext, tempTables) };
36518
36715
  }
36716
+ if (resolvedStmt.type === "UPDATE" && resolvedStmt.from?.cteName != null) {
36717
+ return { result: await executeUpdate(resolvedStmt, client, options, cacheContext, tempTables) };
36718
+ }
36519
36719
  throw new Error(`ArgumentError: temp table references in ${stmt.type} are not supported yet.`);
36520
36720
  }
36521
36721
  return { result: await executeParsedStatement(resolvedStmt, client, options, cacheContext) };
@@ -37701,10 +37901,13 @@ async function executeInsertSelect(stmt, client, options, cacheContext, cteCache
37701
37901
  insertedCount: createdIds.flat().length
37702
37902
  };
37703
37903
  }
37704
- async function executeUpdate(stmt, client, options, cacheContext) {
37904
+ async function executeUpdate(stmt, client, options, cacheContext, tempTables) {
37705
37905
  if (stmt.subtableCode) {
37706
37906
  return executeUpdateSubtable(stmt, client, options, cacheContext);
37707
37907
  }
37908
+ if (stmt.from != null) {
37909
+ return executeUpdateFrom(stmt, stmt.from, client, options, cacheContext, tempTables);
37910
+ }
37708
37911
  const maxRecords2 = options.maxRecords ?? 1e4;
37709
37912
  await resolveSetSubqueries(stmt.assignments, client, options, cacheContext);
37710
37913
  const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
@@ -37746,6 +37949,113 @@ async function executeUpdate(stmt, client, options, cacheContext) {
37746
37949
  }
37747
37950
  return { type: "UPDATE", updatedCount: ids.length };
37748
37951
  }
37952
+ var UPDATE_FROM_ID_CHUNK_SIZE = 50;
37953
+ var UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES = /* @__PURE__ */ new Set([
37954
+ "CHECK_BOX",
37955
+ "MULTI_SELECT",
37956
+ "USER_SELECT",
37957
+ "ORGANIZATION_SELECT",
37958
+ "GROUP_SELECT",
37959
+ "FILE"
37960
+ ]);
37961
+ async function executeUpdateFrom(stmt, from, client, options, cacheContext, tempTables) {
37962
+ const sourceFields = [...new Set(stmt.assignments.filter((a) => a.value.type === "SOURCE_FIELD").map((a) => a.value.type === "SOURCE_FIELD" ? a.value.field : ""))];
37963
+ const requiredSourceFields = [.../* @__PURE__ */ new Set([from.joinKeyField, ...sourceFields])];
37964
+ let sourceRows;
37965
+ if (from.cteName !== null) {
37966
+ const table = tempTables?.get(from.cteName);
37967
+ if (!table) throw new Error(`ArgumentError: temp table ${from.cteName} is not available.`);
37968
+ for (const field of requiredSourceFields) {
37969
+ if (!table.columns.includes(field)) {
37970
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
37971
+ }
37972
+ }
37973
+ sourceRows = table.rows;
37974
+ } else {
37975
+ const sourceTypes = await getFieldTypeMap(from.appId, client, cacheContext);
37976
+ for (const field of requiredSourceFields) {
37977
+ if (field !== "$id" && !sourceTypes.has(field)) {
37978
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${field} does not exist.`);
37979
+ }
37980
+ const type = sourceTypes.get(field);
37981
+ if (UPDATE_FROM_UNSUPPORTED_SOURCE_TYPES.has(type ?? "")) {
37982
+ throw new Error(`ArgumentError: UPDATE ... FROM does not support source field type ${type} (${field}).`);
37983
+ }
37984
+ }
37985
+ const maxRecords2 = options.maxRecords ?? 1e4;
37986
+ const resolved = await fetchRecordsForSharedPlan(
37987
+ client.getRecords,
37988
+ from.appId,
37989
+ "",
37990
+ requiredSourceFields,
37991
+ { maxRecords: maxRecords2, parallel: options.fetchParallel ?? 1, onLimit: "error" }
37992
+ );
37993
+ sourceRows = resolved.records.map((record2) => flatten(record2, null));
37994
+ }
37995
+ const sourceById = /* @__PURE__ */ new Map();
37996
+ for (const row of sourceRows) {
37997
+ if (!Object.prototype.hasOwnProperty.call(row, from.joinKeyField)) {
37998
+ throw new Error(`ArgumentError: UPDATE ... FROM source column ${from.joinKeyField} does not exist.`);
37999
+ }
38000
+ const raw = row[from.joinKeyField];
38001
+ const text = typeof raw === "string" ? raw.trim() : "";
38002
+ const id = Number(text);
38003
+ if (text === "" || !Number.isSafeInteger(id) || id <= 0) {
38004
+ throw new Error(`ArgumentError: UPDATE ... FROM source key must be a positive safe integer: ${String(raw)}`);
38005
+ }
38006
+ if (sourceById.has(id)) {
38007
+ throw new Error(`ArgumentError: UPDATE ... FROM source has multiple rows for target $id ${id}.`);
38008
+ }
38009
+ sourceById.set(id, row);
38010
+ }
38011
+ const targetIds = [...sourceById.keys()];
38012
+ const targetFields = collectUpdateFromTargetFields(stmt);
38013
+ const filterQuery = from.targetFilter === null ? "" : updateToGetQuery({ ...stmt, from: null, where: from.targetFilter }).query;
38014
+ const targetRecords = [];
38015
+ for (const ids of splitChunks(targetIds, UPDATE_FROM_ID_CHUNK_SIZE)) {
38016
+ const idQuery = `$id in (${ids.map((id) => sqlQuote(String(id))).join(",")})`;
38017
+ const query = filterQuery ? `(${idQuery}) and (${filterQuery})` : idQuery;
38018
+ const resolved = await fetchRecordsForSharedPlan(
38019
+ client.getRecords,
38020
+ stmt.appId,
38021
+ query,
38022
+ targetFields,
38023
+ { maxRecords: Math.max(ids.length, 1), parallel: options.fetchParallel ?? 1, onLimit: "error" }
38024
+ );
38025
+ targetRecords.push(...resolved.records);
38026
+ }
38027
+ if (options.confirm) {
38028
+ const ok = await options.confirm(targetRecords.length, "UPDATE");
38029
+ if (!ok) throw new OperationCancelledError("UPDATE", targetRecords.length);
38030
+ }
38031
+ const fieldTypes = await getFieldTypeMap(stmt.appId, client, cacheContext);
38032
+ const matched = targetRecords.map((target) => {
38033
+ const id = Number(target["$id"]?.value);
38034
+ const source = sourceById.get(id);
38035
+ if (!source) throw new Error(`ArgumentError: UPDATE ... FROM could not resolve source row for target $id ${id}.`);
38036
+ return { target, source };
38037
+ });
38038
+ const batches = updateFromToPutBatches(stmt, matched, fieldTypes);
38039
+ for (const batch of batches) await client.putRecords(batch);
38040
+ return { type: "UPDATE", updatedCount: targetRecords.length };
38041
+ }
38042
+ function collectUpdateFromTargetFields(stmt) {
38043
+ const fields = /* @__PURE__ */ new Set(["$id"]);
38044
+ const visit = (node) => {
38045
+ if (Array.isArray(node)) {
38046
+ node.forEach(visit);
38047
+ return;
38048
+ }
38049
+ if (node === null || typeof node !== "object") return;
38050
+ const obj = node;
38051
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") fields.add(obj["field"]);
38052
+ for (const value of Object.values(obj)) visit(value);
38053
+ };
38054
+ for (const assignment of stmt.assignments) {
38055
+ if (assignment.value.type !== "SOURCE_FIELD") visit(assignment.value);
38056
+ }
38057
+ return [...fields];
38058
+ }
37749
38059
  async function executeDelete(stmt, client, options, cacheContext) {
37750
38060
  if (stmt.subtableCode) {
37751
38061
  return executeDeleteSubtable(stmt, client, options, cacheContext);
@@ -38671,9 +38981,16 @@ function buildUpdatePlan(stmt, label) {
38671
38981
  const isSubq = stmt.assignments.some((a) => a.value.type === "SCALAR_SUBQUERY");
38672
38982
  const lines = [];
38673
38983
  if (label) lines.push(label);
38674
- lines.push(` [UPDATE]`);
38984
+ lines.push(stmt.from ? ` [UPDATE FROM]` : ` [UPDATE]`);
38675
38985
  lines.push(` target: APP${stmt.appId} (${stmt.appId})`);
38676
- lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
38986
+ if (stmt.from) {
38987
+ const source = stmt.from.cteName ?? `APP${stmt.from.appId}`;
38988
+ lines.push(` source: ${source} AS ${stmt.from.alias}`);
38989
+ lines.push(` join: APP${stmt.appId}.$id = ${stmt.from.alias}.${stmt.from.joinKeyField}`);
38990
+ lines.push(` target filter: ${stmt.from.targetFilter ? safeWhereToKintone(stmt.from.targetFilter) : "(none)"}`);
38991
+ } else {
38992
+ lines.push(` kintone query: ${safeWhereToKintone(stmt.where)}`);
38993
+ }
38677
38994
  lines.push(` api: GET /k/v1/records.json \u2192 PUT /k/v1/records.json`);
38678
38995
  const setTypes = [];
38679
38996
  if (isArith) setTypes.push("\u7B97\u8853 SET\uFF08\u73FE\u5728\u5024\u3092\u53D6\u5F97\u3057\u3066\u8A08\u7B97\uFF09");
@@ -38786,6 +39103,7 @@ function formatAssignment(a) {
38786
39103
  if (v.type === "ARITH") return `${a.field} = ${formatArithExprStr(v)}`;
38787
39104
  if (v.type === "CASE_VALUE") return `${a.field} = CASE WHEN ...`;
38788
39105
  if (v.type === "SCALAR_SUBQUERY") return `${a.field} = (SELECT ...)`;
39106
+ if (v.type === "SOURCE_FIELD") return `${a.field} = ${v.alias}.${v.field}`;
38789
39107
  return `${a.field} = (${v.type})`;
38790
39108
  }
38791
39109
  function formatArithExprStr(expr) {
@@ -40319,14 +40637,14 @@ function requireDmlApproval(input, toolName, suffix = "") {
40319
40637
  }
40320
40638
  function containsSelectBasedDml(statements) {
40321
40639
  return statements.some(
40322
- (s) => s.statementType === "INSERT_SELECT" || s.statementType === "UPSERT_SELECT"
40640
+ (s) => s.statementType === "INSERT_SELECT" || s.statementType === "UPSERT_SELECT" || s.isUpdateFrom === true
40323
40641
  );
40324
40642
  }
40325
40643
  function resolveMutateRuntimeMaxRecords(statements, dmlMaxRows) {
40326
40644
  return containsSelectBasedDml(statements) ? void 0 : dmlMaxRows + 1;
40327
40645
  }
40328
40646
  var READ_LIMIT_MESSAGE_FRAGMENT = "\u53D6\u5F97\u4EF6\u6570\u304C\u4E0A\u9650";
40329
- var SELECT_BASED_DML_READ_LIMIT_HINT = "SELECT-based DML \u306E\u30BD\u30FC\u30B9\u8AAD\u307F\u53D6\u308A\u4E0A\u9650\u306F dmlMaxRows \u3067\u306F\u306A\u304F maxRecords \u89E3\u6C7A\u5024(KSQL_MAX_RECORDS / profile \u306E query.maxRecords\u3001\u65E2\u5B9A 500)\u3067\u5236\u5FA1\u3055\u308C\u307E\u3059\u3002dmlMaxRows \u306F\u5F71\u97FF\u884C\u6570\u30AC\u30FC\u30C9\u3067\u3059\u3002";
40647
+ var SELECT_BASED_DML_READ_LIMIT_HINT = "SELECT-based DML\uFF08UPDATE \u2026 FROM \u3092\u542B\u3080\uFF09\u306E\u30BD\u30FC\u30B9\u8AAD\u307F\u53D6\u308A\u4E0A\u9650\u306F dmlMaxRows \u3067\u306F\u306A\u304F maxRecords \u89E3\u6C7A\u5024(KSQL_MAX_RECORDS / profile \u306E query.maxRecords\u3001\u65E2\u5B9A 500)\u3067\u5236\u5FA1\u3055\u308C\u307E\u3059\u3002dmlMaxRows \u306F\u5F71\u97FF\u884C\u6570\u30AC\u30FC\u30C9\u3067\u3059\u3002";
40330
40648
  function appendSelectBasedDmlReadLimitHint(err) {
40331
40649
  if (err instanceof Error && err.message.includes(READ_LIMIT_MESSAGE_FRAGMENT)) {
40332
40650
  const hinted = new Error(`${err.message} ${SELECT_BASED_DML_READ_LIMIT_HINT}`);
@@ -40381,7 +40699,8 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
40381
40699
  tempTablesReferenced: s2.tempTablesReferenced,
40382
40700
  tempTablesDropped: s2.tempTablesDropped,
40383
40701
  tempOnlySource: s2.tempOnlySource,
40384
- targetAppId: s2.targetAppId
40702
+ targetAppId: s2.targetAppId,
40703
+ isUpdateFrom: s2.isUpdateFrom
40385
40704
  }));
40386
40705
  const common = {
40387
40706
  ok: true,
@@ -40584,7 +40903,8 @@ function createKsqlMcpTools(serverOptions, deps = {}) {
40584
40903
  const payload = buildBatchEnvelope(batchResult);
40585
40904
  if (selectBasedDml) {
40586
40905
  for (const entry of payload.statements) {
40587
- if (entry.type !== "INSERT_SELECT" && entry.type !== "UPSERT_SELECT") continue;
40906
+ const statement = validation.statements.find((s) => s.index === entry.index);
40907
+ if (entry.type !== "INSERT_SELECT" && entry.type !== "UPSERT_SELECT" && statement?.isUpdateFrom !== true) continue;
40588
40908
  const error51 = entry.error;
40589
40909
  if (typeof error51?.message !== "string") continue;
40590
40910
  if (!error51.message.includes(READ_LIMIT_MESSAGE_FRAGMENT)) continue;
@@ -40923,7 +41243,7 @@ Options:
40923
41243
  -h, --help Show help
40924
41244
  `);
40925
41245
  }
40926
- var SERVER_VERSION = true ? "2.11.0" : "0.0.0-dev";
41246
+ var SERVER_VERSION = true ? "2.12.0" : "0.0.0-dev";
40927
41247
  function createServer(args) {
40928
41248
  const server = new McpServer({
40929
41249
  name: "ksql-mcp",
@@ -40950,7 +41270,7 @@ function createServer(args) {
40950
41270
  }, tools.queryTool);
40951
41271
  server.registerTool("ksql_mutate", {
40952
41272
  title: "Run mutating kSQL",
40953
- description: "Execute DML kSQL with explicit allowDml, confirmText, and dmlMaxRows safety controls. Supports multi-statement DML batches with temp tables. INSERT/UPSERT INTO app ... SELECT supports app sources, temp tables, or joins of both. For UPSERT, dmlMaxRows counts inserts + updates. dmlMaxRows caps affected rows only, not source reads: the source SELECT reads up to the runtime maxRecords (KSQL_MAX_RECORDS / profile query.maxRecords, default 500); temp tables hold at most 10000 rows by default (adjustable via tempTableMaxRows).",
41273
+ description: "Execute DML kSQL with explicit allowDml, confirmText, and dmlMaxRows safety controls. Supports multi-statement DML batches with temp tables. INSERT/UPSERT INTO app ... SELECT supports app sources, temp tables, or joins of both. UPDATE ... FROM supports copying scalar fields from an app or temp table by matching target $id to one source key. For UPSERT, dmlMaxRows counts inserts + updates. dmlMaxRows caps affected rows only, not source reads: source SELECT and UPDATE ... FROM app reads use the runtime maxRecords (KSQL_MAX_RECORDS / profile query.maxRecords, default 500); temp tables hold at most 10000 rows by default (adjustable via tempTableMaxRows).",
40954
41274
  inputSchema: mutateInputShape
40955
41275
  }, tools.mutateTool);
40956
41276
  server.registerTool("ksql_describe_app", {
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rex0220/kintone-sql-tools",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "kintone SQL plugin, CLI, and MCP tools (ksql)",
5
5
  "publishConfig": {
6
6
  "access": "public"