@rex0220/kintone-sql-tools 3.67.0 → 3.68.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/dist-cli/ksql.js CHANGED
@@ -475,7 +475,7 @@ var Lexer = class {
475
475
  // ヘルパー
476
476
  // ----------------------------------------------------------
477
477
  makeToken(kind, value, pos) {
478
- return { kind, value, pos };
478
+ return { kind, value, pos, end: this.pos };
479
479
  }
480
480
  };
481
481
  function isIdentStart(ch) {
@@ -1082,6 +1082,8 @@ var Parser = class {
1082
1082
  this.allowRelativeDateFunctions = false;
1083
1083
  /** WITH 句で定義された CTE 名のセット(parseTableRef で参照) */
1084
1084
  this.cteNames = /* @__PURE__ */ new Set();
1085
+ /** dialect 1 の裸名で宣言された一時テーブル名(参照時に # 付きへ正規化) */
1086
+ this.bareTempTableNames = /* @__PURE__ */ new Set();
1085
1087
  /** パース中に出現した一時テーブル参照(#name)のトークン。単文 API での拒否に使う */
1086
1088
  this.tempTableRefs = [];
1087
1089
  /** GROUP BY を読む前に作る B124 候補 leaf の診断位置。AST 公開型へ位置情報を足さない。 */
@@ -1120,7 +1122,12 @@ var Parser = class {
1120
1122
  }
1121
1123
  /** 複文(`;` 区切り)をパースする。空文はスキップする */
1122
1124
  parseStatements() {
1125
+ return this.parseStatementsWithRanges().statements;
1126
+ }
1127
+ /** 複文と、原文上の文ごとの文字範囲を同時に返す。 */
1128
+ parseStatementsWithRanges() {
1123
1129
  const stmts = [];
1130
+ const statementRanges = [];
1124
1131
  while (true) {
1125
1132
  while (this.peek().kind === ";" /* SEMICOLON */) this.advance();
1126
1133
  if (this.peek().kind === "EOF" /* EOF */) break;
@@ -1136,9 +1143,11 @@ var Parser = class {
1136
1143
  if (after.kind !== ";" /* SEMICOLON */ && after.kind !== "EOF" /* EOF */) {
1137
1144
  throw new ParseError("\u6587\u306E\u533A\u5207\u308A\u306B\u306F ; \u304C\u5FC5\u8981\u3067\u3059", after);
1138
1145
  }
1146
+ const lastTok = this.prev();
1147
+ statementRanges.push({ start: startTok.pos, end: lastTok.end ?? after.pos });
1139
1148
  }
1140
1149
  this.expect("EOF" /* EOF */);
1141
- return stmts;
1150
+ return { statements: stmts, statementRanges };
1142
1151
  }
1143
1152
  // ----------------------------------------------------------
1144
1153
  // Statement ディスパッチ
@@ -1177,6 +1186,13 @@ var Parser = class {
1177
1186
  if (upper === "DROP") return this.parseDropTempTable();
1178
1187
  if (upper === "DECLARE") return this.parseDeclareVariable();
1179
1188
  if (upper === "VALIDATE") return this.parseValidate();
1189
+ if (upper === "EXIT") return this.parseExit();
1190
+ if (upper === "MERGE") {
1191
+ if (!this.capabilities.dialect1) {
1192
+ throw new ParseError("MERGE \u306B\u306F -- @ksql dialect: 1 \u306E\u5BA3\u8A00\u304C\u5FC5\u8981\u3067\u3059", tok);
1193
+ }
1194
+ return this.parseMergeAsUpsert();
1195
+ }
1180
1196
  if (upper === "GENERATE_SERIES") {
1181
1197
  throw new ParseError(
1182
1198
  "GENERATE_SERIES \u306F WITH \u306E CTE \u672C\u4F53\u306B\u66F8\u3044\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B: WITH s AS (GENERATE_SERIES(1, 5)) SELECT generate_series FROM s",
@@ -1195,7 +1211,7 @@ var Parser = class {
1195
1211
  break;
1196
1212
  }
1197
1213
  throw new ParseError(
1198
- "SELECT / INSERT / UPDATE / DELETE / REORDER / VALIDATE / WITH / SHOW / DESCRIBE / EXPLAIN / CREATE TEMP TABLE / DROP TEMP TABLE / SET / DECLARE / ASSERT \u306E\u3044\u305A\u308C\u304B\u3067\u59CB\u307E\u308B SQL \u6587\u304C\u5FC5\u8981\u3067\u3059",
1214
+ "SELECT / INSERT / UPDATE / DELETE / REORDER / VALIDATE / WITH / SHOW / DESCRIBE / EXPLAIN / CREATE TEMP TABLE / DROP TEMP TABLE / SET / DECLARE / ASSERT / EXIT \u306E\u3044\u305A\u308C\u304B\u3067\u59CB\u307E\u308B SQL \u6587\u304C\u5FC5\u8981\u3067\u3059",
1199
1215
  tok
1200
1216
  );
1201
1217
  }
@@ -1323,6 +1339,7 @@ var Parser = class {
1323
1339
  this.advance();
1324
1340
  this.expectSoftKeyword("TEMP", "CREATE \u306E\u5F8C\u306B\u306F TEMP TABLE \u304C\u5FC5\u8981\u3067\u3059\uFF08\u4F8B: CREATE TEMP TABLE #temp AS SELECT ...\uFF09");
1325
1341
  this.expectSoftKeyword("TABLE", "CREATE TEMP \u306E\u5F8C\u306B\u306F TABLE \u304C\u5FC5\u8981\u3067\u3059");
1342
+ const bareName = this.isDialect1BareTempTableName();
1326
1343
  const name = this.parseTempTableName();
1327
1344
  this.expect("AS" /* AS */, "CREATE TEMP TABLE \u306B\u306F AS SELECT \u304C\u5FC5\u8981\u3067\u3059");
1328
1345
  const tok = this.peek();
@@ -1334,13 +1351,16 @@ var Parser = class {
1334
1351
  } else {
1335
1352
  throw new ParseError("CREATE TEMP TABLE ... AS \u306E\u5F8C\u306B\u306F SELECT / WITH \u304C\u5FC5\u8981\u3067\u3059", tok);
1336
1353
  }
1354
+ if (bareName !== null) this.bareTempTableNames.add(bareName);
1337
1355
  return { type: "CREATE_TEMP_TABLE", name, query };
1338
1356
  }
1339
1357
  parseDropTempTable() {
1340
1358
  this.advance();
1341
1359
  this.expectSoftKeyword("TEMP", "DROP \u306E\u5F8C\u306B\u306F TEMP TABLE \u304C\u5FC5\u8981\u3067\u3059\uFF08\u4F8B: DROP TEMP TABLE #temp\uFF09");
1342
1360
  this.expectSoftKeyword("TABLE", "DROP TEMP \u306E\u5F8C\u306B\u306F TABLE \u304C\u5FC5\u8981\u3067\u3059");
1361
+ const bareName = this.isDialect1BareTempTableName();
1343
1362
  const name = this.parseTempTableName();
1363
+ if (bareName !== null) this.bareTempTableNames.delete(bareName);
1344
1364
  return { type: "DROP_TEMP_TABLE", name };
1345
1365
  }
1346
1366
  expectSoftKeyword(word, msg) {
@@ -1362,8 +1382,16 @@ var Parser = class {
1362
1382
  this.advance();
1363
1383
  return tok.value;
1364
1384
  }
1385
+ if (this.capabilities.dialect1 && (tok.kind === "IDENT" /* IDENT */ || tok.kind === "BIDENT" /* BIDENT */)) {
1386
+ this.advance();
1387
+ return `#${tok.value}`;
1388
+ }
1365
1389
  throw new ParseError("\u4E00\u6642\u30C6\u30FC\u30D6\u30EB\u540D\u306F # \u3067\u59CB\u307E\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\uFF08\u4F8B: #temp\uFF09", tok);
1366
1390
  }
1391
+ isDialect1BareTempTableName() {
1392
+ const tok = this.peek();
1393
+ return this.capabilities.dialect1 && (tok.kind === "IDENT" /* IDENT */ || tok.kind === "BIDENT" /* BIDENT */) && !tok.value.startsWith("#") ? tok.value : null;
1394
+ }
1367
1395
  parseShow() {
1368
1396
  this.advance();
1369
1397
  if (!this.consume("APPS" /* APPS */)) {
@@ -1691,6 +1719,41 @@ var Parser = class {
1691
1719
  // ----------------------------------------------------------
1692
1720
  parseAssert() {
1693
1721
  this.expect("ASSERT" /* ASSERT */);
1722
+ const warnTok = this.peek();
1723
+ const warn = this.isSoftKeyword("WARN");
1724
+ if (warn) {
1725
+ this.requireDialect1(warnTok);
1726
+ this.advance();
1727
+ }
1728
+ const condition = this.parseAssertCondition();
1729
+ const message = this.parseFlowMessage();
1730
+ return {
1731
+ type: "ASSERT",
1732
+ ...condition,
1733
+ ...warn ? { warn: true } : {},
1734
+ ...message !== void 0 ? { message } : {}
1735
+ };
1736
+ }
1737
+ /** EXIT SUCCESS IF <ASSERT と同じ条件>, '<message>' */
1738
+ parseExit() {
1739
+ const exitTok = this.advance();
1740
+ this.requireDialect1(exitTok);
1741
+ if (!this.isSoftKeyword("SUCCESS")) {
1742
+ throw new ParseError("EXIT \u306E\u5F8C\u306B\u306F SUCCESS \u304C\u5FC5\u8981\u3067\u3059", this.peek());
1743
+ }
1744
+ this.advance();
1745
+ if (this.peek().kind !== "IF" /* IF */ && !this.isSoftKeyword("IF")) {
1746
+ throw new ParseError("EXIT SUCCESS \u306E\u5F8C\u306B\u306F IF \u304C\u5FC5\u8981\u3067\u3059", this.peek());
1747
+ }
1748
+ this.advance();
1749
+ const condition = this.parseAssertCondition();
1750
+ if (!this.consume("," /* COMMA */)) {
1751
+ throw new ParseError("EXIT SUCCESS IF \u306B\u306F\u672B\u5C3E\u306E\u30E1\u30C3\u30BB\u30FC\u30B8\u6587\u5B57\u5217\u304C\u5FC5\u8981\u3067\u3059", this.peek());
1752
+ }
1753
+ const message = this.expect("STRING" /* STRING */, "EXIT SUCCESS IF \u306E\u30E1\u30C3\u30BB\u30FC\u30B8\u306B\u306F\u6587\u5B57\u5217\u30EA\u30C6\u30E9\u30EB\u304C\u5FC5\u8981\u3067\u3059");
1754
+ return { type: "EXIT", ...condition, message: message.value };
1755
+ }
1756
+ parseAssertCondition() {
1694
1757
  const condStart = this.pos;
1695
1758
  const left = this.parseAssertOperand();
1696
1759
  const opTok = this.peek();
@@ -1703,7 +1766,6 @@ var Parser = class {
1703
1766
  const high = this.parseAssertOperand();
1704
1767
  this.rejectAssertCompound();
1705
1768
  return {
1706
- type: "ASSERT",
1707
1769
  left,
1708
1770
  op: "BETWEEN",
1709
1771
  right: null,
@@ -1722,7 +1784,6 @@ var Parser = class {
1722
1784
  const right = this.parseAssertOperand();
1723
1785
  this.rejectAssertCompound();
1724
1786
  return {
1725
- type: "ASSERT",
1726
1787
  left,
1727
1788
  op,
1728
1789
  right,
@@ -1731,6 +1792,17 @@ var Parser = class {
1731
1792
  text: this.renderTokenRange(condStart, this.pos)
1732
1793
  };
1733
1794
  }
1795
+ /** ASSERT の dialect 1 メッセージ。カンマが無ければ既存形式。 */
1796
+ parseFlowMessage() {
1797
+ if (!this.consume("," /* COMMA */)) return void 0;
1798
+ this.requireDialect1(this.prev());
1799
+ return this.expect("STRING" /* STRING */, "ASSERT \u306E\u30E1\u30C3\u30BB\u30FC\u30B8\u306B\u306F\u6587\u5B57\u5217\u30EA\u30C6\u30E9\u30EB\u304C\u5FC5\u8981\u3067\u3059").value;
1800
+ }
1801
+ requireDialect1(tok) {
1802
+ if (!this.capabilities.dialect1) {
1803
+ throw new ParseError("\u3053\u306E\u69CB\u6587\u306B\u306F -- @ksql dialect: 1 \u306E\u5BA3\u8A00\u304C\u5FC5\u8981\u3067\u3059", tok);
1804
+ }
1805
+ }
1734
1806
  /** ASSERT のオペランド: 文字列 / スカラーサブクエリ / 数値算術式 */
1735
1807
  parseAssertOperand() {
1736
1808
  const tok = this.peek();
@@ -3121,6 +3193,12 @@ var Parser = class {
3121
3193
  );
3122
3194
  }
3123
3195
  const name = this.parseTableName();
3196
+ if (this.capabilities.dialect1 && this.bareTempTableNames.has(name)) {
3197
+ const normalizedName = `#${name}`;
3198
+ this.tempTableRefs.push({ ...this.prev(), value: normalizedName });
3199
+ const alias2 = this.consume("AS" /* AS */) ? this.parseTableAliasName() : this.tryParseImplicitAlias();
3200
+ return { appId: 0, alias: alias2, cteName: normalizedName };
3201
+ }
3124
3202
  if (nameTok.kind === "IDENT" /* IDENT */ && name.startsWith("#")) {
3125
3203
  this.tempTableRefs.push(this.prev());
3126
3204
  const alias2 = this.consume("AS" /* AS */) ? this.parseTableAliasName() : this.tryParseImplicitAlias();
@@ -3161,6 +3239,7 @@ var Parser = class {
3161
3239
  if (k === "IDENT" /* IDENT */ || k === "BIDENT" /* BIDENT */) {
3162
3240
  if (k === "IDENT" /* IDENT */ && this.peek().value.toUpperCase() === "VALIDATE" && this.peekAt(1).kind === "IDENT" /* IDENT */ && this.peekAt(1).value.toUpperCase() === "ONLY") return null;
3163
3241
  if (k === "IDENT" /* IDENT */ && this.peek().value.toUpperCase() === "CHECK" && this.peekAt(1).kind === "WHEN" /* WHEN */) return null;
3242
+ if (k === "IDENT" /* IDENT */ && this.peek().value.toUpperCase() === "KEY" && this.peekAt(1).kind === "(" /* LPAREN */) return null;
3164
3243
  return this.parseTableAliasName();
3165
3244
  }
3166
3245
  return null;
@@ -4061,6 +4140,15 @@ var Parser = class {
4061
4140
  };
4062
4141
  }
4063
4142
  parseOnDuplicate() {
4143
+ if (this.capabilities.dialect1 && this.consumeSoftKeyword("KEY")) {
4144
+ this.expect("(" /* LPAREN */, "KEY \u306E\u5F8C\u306B\u306F (\u30AD\u30FC\u30D5\u30A3\u30FC\u30EB\u30C9) \u304C\u5FC5\u8981\u3067\u3059");
4145
+ const keyFields2 = this.parseIdentList();
4146
+ this.expect(")" /* RPAREN */);
4147
+ if (keyFields2.length === 0) {
4148
+ throw new ParseError("KEY \u306B\u306F\u30AD\u30FC\u30D5\u30A3\u30FC\u30EB\u30C9\u304C\u6700\u4F4E 1 \u3064\u5FC5\u8981\u3067\u3059", this.prev());
4149
+ }
4150
+ return keyFields2;
4151
+ }
4064
4152
  this.expectKeyword("ON" /* ON */, "UPSERT \u306B\u306F ON DUPLICATE (\u30AD\u30FC\u30D5\u30A3\u30FC\u30EB\u30C9) \u304C\u5FC5\u8981\u3067\u3059");
4065
4153
  if (!this.consume("DUPLICATE" /* DUPLICATE */)) {
4066
4154
  throw new ParseError("ON \u306E\u5F8C\u306B\u306F DUPLICATE \u304C\u5FC5\u8981\u3067\u3059", this.peek());
@@ -4073,6 +4161,238 @@ var Parser = class {
4073
4161
  }
4074
4162
  return keyFields;
4075
4163
  }
4164
+ parseMergeAsUpsert() {
4165
+ const mergeToken = this.advance();
4166
+ this.expect("INTO" /* INTO */, "MERGE \u306E\u5F8C\u306B\u306F INTO \u304C\u5FC5\u8981\u3067\u3059");
4167
+ this.rejectTempTableDml();
4168
+ const targetName = this.parseIdentifier();
4169
+ const { appId, subtableCode } = extractTableRef(targetName, this.prev());
4170
+ if (subtableCode) {
4171
+ throw new ParseError("MERGE \u306F\u307E\u3060\u30B5\u30D6\u30C6\u30FC\u30D6\u30EB\u4EEE\u60F3\u30C6\u30FC\u30D6\u30EB\u306B\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u305B\u3093", this.prev());
4172
+ }
4173
+ this.expect("AS" /* AS */, "MERGE \u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u306B\u306F AS alias \u304C\u5FC5\u8981\u3067\u3059");
4174
+ const targetAlias = this.parseTableAliasName();
4175
+ this.expectSoftKeyword("USING", "MERGE \u306B\u306F USING <source> AS alias \u304C\u5FC5\u8981\u3067\u3059");
4176
+ const sourceStart = this.pos;
4177
+ const source = this.parseTableRef();
4178
+ const explicitSourceAlias = this.tokens.slice(sourceStart, this.pos).some((token) => token.kind === "AS" /* AS */);
4179
+ if (!explicitSourceAlias || source.alias === null) {
4180
+ throw new ParseError("MERGE \u306E USING \u30BD\u30FC\u30B9\u306B\u306F AS alias \u304C\u5FC5\u8981\u3067\u3059", this.peek());
4181
+ }
4182
+ const sourceAlias = source.alias;
4183
+ this.expect("ON" /* ON */, "MERGE \u306B\u306F ON t.key = s.key \u306E\u5358\u4E00\u30AD\u30FC\u7B49\u5024\u304C\u5FC5\u8981\u3067\u3059");
4184
+ const left = this.parseMergeQualifiedField("MERGE \u306E ON \u5DE6\u8FBA\u306F targetAlias.key \u306E\u5F62\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
4185
+ if (!this.consume("=" /* EQ */)) {
4186
+ throw new ParseError(
4187
+ "MERGE \u306E ON \u306F\u5358\u4E00\u30AD\u30FC\u306E\u7B49\u5024\uFF08t.key = s.key\uFF09\u306E\u307F\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u3059\u3002\u8907\u6570\u6761\u4EF6\u3084\u975E\u7B49\u5024\u306F\u9023\u7D50\u30AD\u30FC\u30D5\u30A3\u30FC\u30EB\u30C9\u3067\u7F6E\u304D\u63DB\u3048\u3066\u304F\u3060\u3055\u3044",
4188
+ this.peek()
4189
+ );
4190
+ }
4191
+ const right = this.parseMergeQualifiedField("MERGE \u306E ON \u53F3\u8FBA\u306F sourceAlias.key \u306E\u5F62\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044");
4192
+ if (left.alias.toLowerCase() !== targetAlias.toLowerCase() || right.alias.toLowerCase() !== sourceAlias.toLowerCase()) {
4193
+ throw new ParseError(
4194
+ `MERGE \u306E ON \u306F ${targetAlias}.key = ${sourceAlias}.key \u306E\u5225\u540D\u4FEE\u98FE\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044`,
4195
+ mergeToken
4196
+ );
4197
+ }
4198
+ if (this.peek().kind === "AND" /* AND */ || this.peek().kind === "OR" /* OR */) {
4199
+ throw new ParseError(
4200
+ "MERGE \u306E ON \u306F\u5358\u4E00\u30AD\u30FC\u306E\u7B49\u5024\u306E\u307F\u5BFE\u5FDC\u3057\u3066\u3044\u307E\u3059\u3002\u8907\u6570\u6761\u4EF6\u306F\u9023\u7D50\u30AD\u30FC\u30D5\u30A3\u30FC\u30EB\u30C9\u3067\u7F6E\u304D\u63DB\u3048\u3066\u304F\u3060\u3055\u3044",
4201
+ this.peek()
4202
+ );
4203
+ }
4204
+ let matched = null;
4205
+ let insertFields = null;
4206
+ let insertValues = null;
4207
+ while (this.peek().kind === "WHEN" /* WHEN */) {
4208
+ const whenToken = this.advance();
4209
+ if (this.consume("NOT" /* NOT */)) {
4210
+ this.expectSoftKeyword("MATCHED", "WHEN NOT \u306E\u5F8C\u306B\u306F MATCHED \u304C\u5FC5\u8981\u3067\u3059");
4211
+ if (insertFields !== null) throw new ParseError("WHEN NOT MATCHED \u53E5\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059", whenToken);
4212
+ this.expect("THEN" /* THEN */, "WHEN NOT MATCHED \u306E\u5F8C\u306B\u306F THEN \u304C\u5FC5\u8981\u3067\u3059");
4213
+ this.expect("INSERT" /* INSERT */, "WHEN NOT MATCHED THEN \u306E\u5F8C\u306B\u306F INSERT \u304C\u5FC5\u8981\u3067\u3059");
4214
+ this.expect("(" /* LPAREN */, "MERGE INSERT \u306B\u306F\u5217\u30EA\u30B9\u30C8\u304C\u5FC5\u8981\u3067\u3059");
4215
+ insertFields = this.parseIdentList();
4216
+ this.expect(")" /* RPAREN */);
4217
+ this.expect("VALUES" /* VALUES */, "MERGE INSERT \u306E\u5217\u30EA\u30B9\u30C8\u306E\u5F8C\u306B\u306F VALUES \u304C\u5FC5\u8981\u3067\u3059");
4218
+ this.expect("(" /* LPAREN */, "MERGE INSERT VALUES \u306F ( \u3067\u59CB\u3081\u3066\u304F\u3060\u3055\u3044");
4219
+ insertValues = this.parseMergeValueList();
4220
+ this.expect(")" /* RPAREN */);
4221
+ if (insertFields.length !== insertValues.length) {
4222
+ throw new ParseError("MERGE INSERT \u306E\u5217\u6570\u3068 VALUES \u306E\u5024\u6570\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093", whenToken);
4223
+ }
4224
+ } else {
4225
+ this.expectSoftKeyword("MATCHED", "WHEN \u306E\u5F8C\u306B\u306F MATCHED \u307E\u305F\u306F NOT MATCHED \u304C\u5FC5\u8981\u3067\u3059");
4226
+ if (matched !== null) throw new ParseError("WHEN MATCHED \u53E5\u304C\u91CD\u8907\u3057\u3066\u3044\u307E\u3059", whenToken);
4227
+ this.expect("THEN" /* THEN */, "WHEN MATCHED \u306E\u5F8C\u306B\u306F THEN \u304C\u5FC5\u8981\u3067\u3059");
4228
+ this.expect("UPDATE" /* UPDATE */, "WHEN MATCHED THEN \u306E\u5F8C\u306B\u306F UPDATE \u304C\u5FC5\u8981\u3067\u3059");
4229
+ this.expect("SET" /* SET */, "WHEN MATCHED THEN UPDATE \u306E\u5F8C\u306B\u306F SET \u304C\u5FC5\u8981\u3067\u3059");
4230
+ matched = this.parseMergeAssignments(targetAlias);
4231
+ }
4232
+ }
4233
+ if (matched === null || insertFields === null || insertValues === null) {
4234
+ throw new ParseError(
4235
+ "WHEN MATCHED / WHEN NOT MATCHED \u306E\u4E21\u53E5\u304C\u5FC5\u8981\u3067\u3059\uFF08\u66F4\u65B0\u306E\u307F\u306F UPDATE ... FROM\u3001\u633F\u5165\u306E\u307F\u306F INSERT ... SELECT \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\uFF09",
4236
+ this.peek()
4237
+ );
4238
+ }
4239
+ if (!insertFields.some((field) => field.toLowerCase() === left.field.toLowerCase())) {
4240
+ throw new ParseError(
4241
+ `MERGE \u306E ON \u30AD\u30FC ${left.field} \u306F INSERT \u5217\u30EA\u30B9\u30C8\u306B\u542B\u3081\u3066\u304F\u3060\u3055\u3044`,
4242
+ mergeToken
4243
+ );
4244
+ }
4245
+ const expressions = /* @__PURE__ */ new Map();
4246
+ insertFields.forEach((field, index) => expressions.set(field.toLowerCase(), insertValues[index]));
4247
+ for (const assignment of matched) {
4248
+ const key = assignment.field.toLowerCase();
4249
+ const existing = expressions.get(key);
4250
+ if (existing !== void 0 && !this.mergeExpressionsEqual(existing, assignment.value, sourceAlias)) {
4251
+ throw new ParseError(
4252
+ `MERGE \u306E\u5217 ${assignment.field} \u306F\u4E21\u53E5\u306E\u5F0F\u304C\u4E00\u81F4\u3059\u308B\u5834\u5408\u306E\u307F MERGE \u3092 UPSERT \u3078\u6B63\u898F\u5316\u3067\u304D\u307E\u3059`,
4253
+ mergeToken
4254
+ );
4255
+ }
4256
+ if (existing === void 0) {
4257
+ insertFields.push(assignment.field);
4258
+ insertValues.push(assignment.value);
4259
+ expressions.set(key, assignment.value);
4260
+ }
4261
+ }
4262
+ const columns = insertValues.map((value) => this.mergeValueToSelectColumn(value, sourceAlias, mergeToken));
4263
+ const normalizedSource = source.cteName !== null ? { ...source, alias: null } : { ...source, alias: `APP${source.appId}${source.subtableCode ? `$${source.subtableCode}` : ""}` };
4264
+ const select = {
4265
+ type: "SELECT",
4266
+ distinct: false,
4267
+ columns,
4268
+ from: normalizedSource,
4269
+ joins: [],
4270
+ where: null,
4271
+ groupBy: [],
4272
+ having: null,
4273
+ orderMode: "CANONICAL",
4274
+ orderBy: [],
4275
+ limit: null,
4276
+ offset: null
4277
+ };
4278
+ const checkGroups = this.parseCheckGroups();
4279
+ const validation = this.parseDmlControlSuffix();
4280
+ return {
4281
+ type: "UPSERT_SELECT",
4282
+ appId,
4283
+ fields: insertFields,
4284
+ select,
4285
+ keyFields: [left.field],
4286
+ ...checkGroups,
4287
+ ...validation
4288
+ };
4289
+ }
4290
+ parseMergeQualifiedField(message) {
4291
+ const token = this.peek();
4292
+ const path = this.parseFieldPath();
4293
+ const ref = this.splitQualifiedField(path);
4294
+ if (ref.alias === null) throw new ParseError(message, token);
4295
+ return { alias: ref.alias, field: ref.field };
4296
+ }
4297
+ parseMergeAssignments(targetAlias) {
4298
+ const assignments = [];
4299
+ do {
4300
+ const token = this.peek();
4301
+ const path = this.parseFieldPath();
4302
+ const ref = this.splitQualifiedField(path);
4303
+ if (ref.alias !== null && ref.alias.toLowerCase() !== targetAlias.toLowerCase()) {
4304
+ throw new ParseError(`MERGE UPDATE SET \u306E\u5DE6\u8FBA\u306F target alias ${targetAlias} \u3067\u4FEE\u98FE\u3057\u3066\u304F\u3060\u3055\u3044`, token);
4305
+ }
4306
+ this.expect("=" /* EQ */);
4307
+ assignments.push({ field: ref.field, value: this.parseAssignmentValue() });
4308
+ } while (this.consume("," /* COMMA */));
4309
+ return assignments;
4310
+ }
4311
+ parseMergeValueList() {
4312
+ const values = [];
4313
+ if (this.peek().kind === ")" /* RPAREN */) return values;
4314
+ do
4315
+ values.push(this.parseAssignmentValue());
4316
+ while (this.consume("," /* COMMA */));
4317
+ return values;
4318
+ }
4319
+ mergeExpressionsEqual(left, right, sourceAlias) {
4320
+ return this.mergeNormalizedValueEqual(
4321
+ this.normalizeMergeValue(left, sourceAlias, true),
4322
+ this.normalizeMergeValue(right, sourceAlias, true)
4323
+ );
4324
+ }
4325
+ normalizeMergeValue(value, sourceAlias, compareLiteralValues = false) {
4326
+ if (Array.isArray(value)) {
4327
+ return value.map((item) => this.normalizeMergeValue(item, sourceAlias, compareLiteralValues));
4328
+ }
4329
+ if (value === null || typeof value !== "object") return value;
4330
+ const obj = value;
4331
+ if (compareLiteralValues && obj["type"] === "NUMBER") {
4332
+ return { type: "NUMBER", value: obj["value"] };
4333
+ }
4334
+ if (obj["type"] === "SOURCE_FIELD") {
4335
+ if (String(obj["alias"]).toLowerCase() !== sourceAlias.toLowerCase()) return { type: "INVALID_SOURCE" };
4336
+ return { type: "FIELD_REF", field: obj["field"] };
4337
+ }
4338
+ if (obj["type"] === "FIELD_REF" && typeof obj["field"] === "string") {
4339
+ const ref = this.splitQualifiedField(obj["field"]);
4340
+ if (ref.alias !== null && ref.alias.toLowerCase() !== sourceAlias.toLowerCase()) return { type: "INVALID_SOURCE" };
4341
+ return { ...obj, field: ref.field };
4342
+ }
4343
+ if (obj["type"] === "FIELD" && typeof obj["tableAlias"] === "string") {
4344
+ if (obj["tableAlias"].toLowerCase() !== sourceAlias.toLowerCase()) return { type: "INVALID_SOURCE" };
4345
+ return { ...obj, tableAlias: null };
4346
+ }
4347
+ return Object.fromEntries(Object.entries(obj).map(([key, child]) => [
4348
+ key,
4349
+ this.normalizeMergeValue(child, sourceAlias, compareLiteralValues)
4350
+ ]));
4351
+ }
4352
+ mergeNormalizedValueEqual(left, right) {
4353
+ if (left === right) return true;
4354
+ if (Array.isArray(left) || Array.isArray(right)) {
4355
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((item, index) => this.mergeNormalizedValueEqual(item, right[index]));
4356
+ }
4357
+ if (left === null || right === null || typeof left !== "object" || typeof right !== "object") return false;
4358
+ const leftObj = left;
4359
+ const rightObj = right;
4360
+ const leftKeys = Object.keys(leftObj);
4361
+ const rightKeys = Object.keys(rightObj);
4362
+ return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.prototype.hasOwnProperty.call(rightObj, key) && this.mergeNormalizedValueEqual(leftObj[key], rightObj[key]));
4363
+ }
4364
+ mergeValueToSelectColumn(value, sourceAlias, token) {
4365
+ const normalized = this.normalizeMergeValue(value, sourceAlias);
4366
+ if (this.mergeValueContainsInvalidSource(normalized)) {
4367
+ throw new ParseError(`MERGE \u306E\u5F0F\u306F source alias ${sourceAlias} \u306E\u30D5\u30A3\u30FC\u30EB\u30C9\u3060\u3051\u3092\u53C2\u7167\u3057\u3066\u304F\u3060\u3055\u3044`, token);
4368
+ }
4369
+ const expr = normalized;
4370
+ switch (expr["type"]) {
4371
+ case "FIELD_REF":
4372
+ return { type: "FIELD", field: String(expr["field"]), alias: null };
4373
+ case "STRING":
4374
+ return { type: "LITERAL_COL", value: String(expr["value"]), alias: null };
4375
+ case "NUMBER":
4376
+ return { type: "ARITH_COL", expr, alias: null };
4377
+ case "ARITH":
4378
+ return { type: "ARITH_COL", expr, alias: null };
4379
+ case "STRING_FUNC":
4380
+ return { type: "STRFUNC_COL", expr, alias: null };
4381
+ case "CASE_VALUE":
4382
+ return { type: "CASE_COL", expr: expr["expr"], alias: null };
4383
+ default:
4384
+ throw new ParseError(
4385
+ "MERGE \u306E\u4EE3\u5165\u5F0F\u306F\u30BD\u30FC\u30B9\u30D5\u30A3\u30FC\u30EB\u30C9\u30FB\u30EA\u30C6\u30E9\u30EB\u30FB\u7B97\u8853\u5F0F\u30FB\u6587\u5B57\u5217\u95A2\u6570\u30FBCASE \u306E\u3044\u305A\u308C\u304B\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
4386
+ token
4387
+ );
4388
+ }
4389
+ }
4390
+ mergeValueContainsInvalidSource(value) {
4391
+ if (Array.isArray(value)) return value.some((item) => this.mergeValueContainsInvalidSource(item));
4392
+ if (value === null || typeof value !== "object") return false;
4393
+ const obj = value;
4394
+ return obj["type"] === "INVALID_SOURCE" || Object.values(obj).some((item) => this.mergeValueContainsInvalidSource(item));
4395
+ }
4076
4396
  isUpsertApplyBranchStart() {
4077
4397
  return this.peek().kind === "ON" /* ON */ && (this.peekAt(1).kind === "INSERT" /* INSERT */ || this.peekAt(1).kind === "UPDATE" /* UPDATE */);
4078
4398
  }
@@ -4889,7 +5209,7 @@ function isDmlType(type) {
4889
5209
  return type === "INSERT" || type === "INSERT_SELECT" || type === "UPDATE" || type === "DELETE" || type === "UPSERT" || type === "UPSERT_SELECT" || type === "REORDER" || type === "IMPORT";
4890
5210
  }
4891
5211
  function isReadOnlyType(type) {
4892
- 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";
5212
+ 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" || type === "EXIT";
4893
5213
  }
4894
5214
  function writesKintone(stmt) {
4895
5215
  return isDmlType(stmt.type) && !("validateOnly" in stmt && stmt.validateOnly === true);
@@ -8259,6 +8579,7 @@ function validateStatement(stmt) {
8259
8579
  case "SET_VARIABLE":
8260
8580
  case "DECLARE_VARIABLE":
8261
8581
  case "ASSERT":
8582
+ case "EXIT":
8262
8583
  validateNestedSelects(stmt);
8263
8584
  return;
8264
8585
  case "UPDATE":
@@ -16134,6 +16455,147 @@ function toFlatString(value) {
16134
16455
  }
16135
16456
  }
16136
16457
 
16458
+ // src/core/diagnostics.ts
16459
+ var DiagnosticCodes = {
16460
+ HEADER_UNKNOWN_KEY: "KSQL1001",
16461
+ HEADER_DUPLICATE_KEY: "KSQL1002",
16462
+ HEADER_INVALID_NAME: "KSQL1003",
16463
+ HEADER_INVALID_DEPENDS_ON: "KSQL1004",
16464
+ HEADER_INVALID_TIMEOUT: "KSQL1005",
16465
+ HEADER_INVALID_DIALECT: "KSQL1006",
16466
+ LOGICAL_APP_UNRESOLVED: "KSQL1101",
16467
+ LEX_ERROR: "KSQL1201",
16468
+ PARSE_ERROR: "KSQL1202"
16469
+ };
16470
+ function sourceLocationAt(source, offset) {
16471
+ const target = Math.max(0, Math.min(offset, source.length));
16472
+ let line = 1;
16473
+ let column = 1;
16474
+ for (let i = 0; i < target; i++) {
16475
+ const ch = source[i];
16476
+ if (ch === "\r") {
16477
+ if (source[i + 1] === "\n" && i + 1 < target) i++;
16478
+ line++;
16479
+ column = 1;
16480
+ } else if (ch === "\n") {
16481
+ line++;
16482
+ column = 1;
16483
+ } else {
16484
+ column++;
16485
+ }
16486
+ }
16487
+ return { line, column };
16488
+ }
16489
+ function diagnosticAt(source, offset, diagnostic2) {
16490
+ return { ...diagnostic2, ...sourceLocationAt(source, offset) };
16491
+ }
16492
+
16493
+ // src/core/scriptHeader.ts
16494
+ var HEADER_LINE_RE = /^(\s*)--\s*@ksql\s+([^:\s]+)\s*:\s*(.*)$/i;
16495
+ function parseScriptHeader(source) {
16496
+ const meta = { name: null, dependsOn: [], timeout: null, dialect: 0 };
16497
+ const diagnostics = [];
16498
+ const seen = /* @__PURE__ */ new Set();
16499
+ let hasDirectives = false;
16500
+ let offset = source.charCodeAt(0) === 65279 ? 1 : 0;
16501
+ let headerEnd = offset;
16502
+ while (offset < source.length) {
16503
+ const lineEnd = findLineEnd(source, offset);
16504
+ const line = source.slice(offset, lineEnd.contentEnd);
16505
+ if (!/^\s*--/.test(line)) break;
16506
+ headerEnd = lineEnd.next;
16507
+ const match = HEADER_LINE_RE.exec(line);
16508
+ if (match) {
16509
+ hasDirectives = true;
16510
+ const rawKey = match[2];
16511
+ const key = rawKey.toLowerCase();
16512
+ const rawValue = match[3];
16513
+ const commentAt = rawValue.indexOf("#");
16514
+ const valuePart = commentAt < 0 ? rawValue : rawValue.slice(0, commentAt);
16515
+ const leading = valuePart.match(/^\s*/)?.[0].length ?? 0;
16516
+ const value = valuePart.trim();
16517
+ const valueOffset = offset + match.index + match[0].length - rawValue.length + leading;
16518
+ if (!isHeaderKey(key)) {
16519
+ diagnostics.push(diagnosticAt(source, valueOffset, {
16520
+ severity: "warning",
16521
+ code: DiagnosticCodes.HEADER_UNKNOWN_KEY,
16522
+ message: `Unknown @ksql header key "${rawKey}" was ignored.`
16523
+ }));
16524
+ } else if (seen.has(key)) {
16525
+ diagnostics.push(diagnosticAt(source, valueOffset, {
16526
+ severity: "warning",
16527
+ code: DiagnosticCodes.HEADER_DUPLICATE_KEY,
16528
+ message: `Duplicate @ksql header key "${key}" was ignored; the first value is retained.`
16529
+ }));
16530
+ } else {
16531
+ seen.add(key);
16532
+ applyHeaderValue(meta, key, value, source, valueOffset, diagnostics);
16533
+ }
16534
+ }
16535
+ offset = lineEnd.next;
16536
+ }
16537
+ return { meta, diagnostics, hasDirectives, headerEnd };
16538
+ }
16539
+ function isHeaderKey(value) {
16540
+ return value === "name" || value === "depends_on" || value === "timeout" || value === "dialect";
16541
+ }
16542
+ function applyHeaderValue(meta, key, value, source, valueOffset, diagnostics) {
16543
+ if (key === "name") {
16544
+ if (!value) {
16545
+ diagnostics.push(diagnosticAt(source, valueOffset, {
16546
+ severity: "error",
16547
+ code: DiagnosticCodes.HEADER_INVALID_NAME,
16548
+ message: "@ksql name must not be empty."
16549
+ }));
16550
+ } else {
16551
+ meta.name = value;
16552
+ }
16553
+ return;
16554
+ }
16555
+ if (key === "depends_on") {
16556
+ const dependencies = value.split(",").map((item) => item.trim());
16557
+ if (!value || dependencies.some((item) => !item)) {
16558
+ diagnostics.push(diagnosticAt(source, valueOffset, {
16559
+ severity: "error",
16560
+ code: DiagnosticCodes.HEADER_INVALID_DEPENDS_ON,
16561
+ message: "@ksql depends_on must be a comma-separated list without empty items."
16562
+ }));
16563
+ } else {
16564
+ meta.dependsOn = dependencies;
16565
+ }
16566
+ return;
16567
+ }
16568
+ if (key === "timeout") {
16569
+ if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value))) {
16570
+ diagnostics.push(diagnosticAt(source, valueOffset, {
16571
+ severity: "error",
16572
+ code: DiagnosticCodes.HEADER_INVALID_TIMEOUT,
16573
+ message: "@ksql timeout must be a positive integer."
16574
+ }));
16575
+ } else {
16576
+ meta.timeout = Number(value);
16577
+ }
16578
+ return;
16579
+ }
16580
+ if (value !== "0" && value !== "1") {
16581
+ diagnostics.push(diagnosticAt(source, valueOffset, {
16582
+ severity: "error",
16583
+ code: DiagnosticCodes.HEADER_INVALID_DIALECT,
16584
+ message: "@ksql dialect must be 0 or 1."
16585
+ }));
16586
+ } else {
16587
+ meta.dialect = Number(value);
16588
+ }
16589
+ }
16590
+ function findLineEnd(source, start) {
16591
+ let i = start;
16592
+ while (i < source.length && source[i] !== "\r" && source[i] !== "\n") i++;
16593
+ const contentEnd = i;
16594
+ if (source[i] === "\r" && source[i + 1] === "\n") i += 2;
16595
+ else if (i < source.length) i++;
16596
+ return { contentEnd, next: i };
16597
+ }
16598
+
16137
16599
  // src/core/dmlPrevalidation.ts
16138
16600
  function collectDmlPrevalidationSnapshotFields(fieldIndex) {
16139
16601
  return [
@@ -18155,6 +18617,8 @@ async function executeParsedStatement(stmt, client, options, cacheContext) {
18155
18617
  throw new Error("ArgumentError: DECLARE variable requires a batch.");
18156
18618
  case "ASSERT":
18157
18619
  return executeAssert(stmt, client, options, cacheContext);
18620
+ case "EXIT":
18621
+ throw new Error("ArgumentError: EXIT SUCCESS IF \u306F\u30D0\u30C3\u30C1\u5C02\u7528\u3067\u3059");
18158
18622
  }
18159
18623
  }
18160
18624
  var EXISTING_VALIDATION_COLUMNS = [
@@ -18422,7 +18886,16 @@ var BatchTimeoutError = class extends Error {
18422
18886
  };
18423
18887
  async function executeBatch(sql, client, options = {}) {
18424
18888
  resolveRecursiveCteLimits(options);
18425
- const statements = parseSqlBatch(sql, options.enableImport === true);
18889
+ const header = parseScriptHeader(sql);
18890
+ const headerError = header.diagnostics.find((diagnostic2) => diagnostic2.severity === "error");
18891
+ if (header.hasDirectives && headerError) {
18892
+ throw new Error(`${headerError.code}: ${headerError.message} (${headerError.line}:${headerError.column})`);
18893
+ }
18894
+ const statements = parseSqlBatch(
18895
+ header.hasDirectives ? sql.slice(header.headerEnd) : sql,
18896
+ options.enableImport === true,
18897
+ header.hasDirectives && header.meta.dialect === 1
18898
+ );
18426
18899
  const analysis = analyzeBatch(statements);
18427
18900
  statements.forEach((statement) => assertApplyExecutionScope("phase15b", statement));
18428
18901
  if (options.allowApplyMutation !== true && statements.some(
@@ -18464,7 +18937,7 @@ async function executeBatch(sql, client, options = {}) {
18464
18937
  const base = { index: i, type: info.statementType };
18465
18938
  if (aborted) {
18466
18939
  results.push({ ...base, status: "skipped", skippedReason: aborted });
18467
- failed.add(i);
18940
+ if (aborted !== "exit") failed.add(i);
18468
18941
  continue;
18469
18942
  }
18470
18943
  const brokenDep = info.dependsOn.find((d) => failed.has(d));
@@ -18502,24 +18975,29 @@ async function executeBatch(sql, client, options = {}) {
18502
18975
  info.statementType !== "SELECT" && info.statementType !== "UNION" && info.statementType !== "WITH" || statementContainsOuterJoin(statements[i])
18503
18976
  );
18504
18977
  const cursorScope = wrapClientWithCursorScope(statementClient);
18978
+ const boundOptions = bindStatementEvaluationContext(stmtOptions);
18979
+ const statementContext = {
18980
+ stmt: statements[i],
18981
+ info,
18982
+ client: cursorScope.client,
18983
+ options: boundOptions,
18984
+ cacheContext,
18985
+ tempTables,
18986
+ variables,
18987
+ relativeDateVariables,
18988
+ clock: statementEvaluationContext(boundOptions)
18989
+ };
18505
18990
  const outcome = await runWithDeadline(
18506
- executeBatchStatement(
18507
- statements[i],
18508
- info,
18509
- cursorScope.client,
18510
- stmtOptions,
18511
- cacheContext,
18512
- tempTables,
18513
- variables,
18514
- relativeDateVariables
18515
- ),
18991
+ executeBatchStatement(statementContext),
18516
18992
  remaining,
18517
18993
  cursorScope.closeActive
18518
18994
  );
18519
18995
  if (outcome.result) {
18520
18996
  outcome.result = attachSearchAbortWarning(outcome.result, searchAbortCollector);
18521
18997
  }
18522
- results.push({ ...base, status: "success", ...outcome });
18998
+ const { exitTriggered, ...statementOutcome } = outcome;
18999
+ results.push({ ...base, status: "success", ...statementOutcome });
19000
+ if (exitTriggered) aborted = "exit";
18523
19001
  } catch (e) {
18524
19002
  results.push({
18525
19003
  ...base,
@@ -18541,7 +19019,7 @@ async function executeBatch(sql, client, options = {}) {
18541
19019
  }
18542
19020
  metrics.elapsedMs = Date.now() - startedAt;
18543
19021
  return {
18544
- ok: results.every((r) => r.status === "success"),
19022
+ ok: results.every((r) => r.status === "success" || r.skippedReason === "exit"),
18545
19023
  statementCount: statements.length,
18546
19024
  statements: results,
18547
19025
  analysis,
@@ -18557,8 +19035,18 @@ function statementHasApplyMutation(statement) {
18557
19035
  }
18558
19036
  return statement.type === "UPSERT" && statement.validateOnly !== true && Boolean(statement.onInsertApplyBlocks?.length || statement.onUpdateApplyBlocks?.length);
18559
19037
  }
18560
- async function executeBatchStatement(stmt, info, client, options, cacheContext, tempTables, variables, relativeDateVariables) {
18561
- options = bindStatementEvaluationContext(options);
19038
+ async function executeBatchStatement(context) {
19039
+ const {
19040
+ stmt,
19041
+ info,
19042
+ client,
19043
+ options,
19044
+ cacheContext,
19045
+ tempTables,
19046
+ variables,
19047
+ relativeDateVariables,
19048
+ clock
19049
+ } = context;
18562
19050
  if (stmt.type === "SET_VARIABLE") {
18563
19051
  const resolvedStmt2 = resolveBatchVariableReferences(stmt, variables);
18564
19052
  validateStatementStatic(resolvedStmt2);
@@ -18598,7 +19086,7 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
18598
19086
  throw e;
18599
19087
  }
18600
19088
  } else {
18601
- variables.set(stmt.name, evaluateScalarExpr(resolvedStmt2.expr, statementEvaluationContext(options)));
19089
+ variables.set(stmt.name, evaluateScalarExpr(resolvedStmt2.expr, clock));
18602
19090
  }
18603
19091
  return {};
18604
19092
  }
@@ -18616,7 +19104,7 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
18616
19104
  } else {
18617
19105
  const value = evaluateScalarExpr(
18618
19106
  stmt.default,
18619
- statementEvaluationContext(options)
19107
+ clock
18620
19108
  );
18621
19109
  variables.set(stmt.name, {
18622
19110
  type: "string",
@@ -18713,8 +19201,12 @@ async function executeBatchStatement(stmt, info, client, options, cacheContext,
18713
19201
  }
18714
19202
  }
18715
19203
  if (resolvedStmt.type === "ASSERT") {
18716
- await executeAssert(resolvedStmt, client, options, cacheContext, tempTables);
18717
- return {};
19204
+ const result = await executeAssert(resolvedStmt, client, options, cacheContext, tempTables);
19205
+ return result.warning !== void 0 ? { result } : {};
19206
+ }
19207
+ if (resolvedStmt.type === "EXIT") {
19208
+ const result = await executeExit(resolvedStmt, client, options, cacheContext, tempTables);
19209
+ return { result, ...result.exited ? { exitTriggered: true } : {} };
18718
19210
  }
18719
19211
  if (info.tempTablesReferenced.length > 0) {
18720
19212
  if (resolvedStmt.type === "SELECT" || resolvedStmt.type === "UNION") {
@@ -18838,9 +19330,9 @@ function safeJsonStringify(v) {
18838
19330
  return String(v);
18839
19331
  }
18840
19332
  }
18841
- function parseSqlBatch(sql, enableImport = false) {
19333
+ function parseSqlBatch(sql, enableImport = false, dialect1 = false) {
18842
19334
  const tokens = new Lexer(sql).tokenize();
18843
- return new Parser(tokens, { import: enableImport }).parseStatements();
19335
+ return new Parser(tokens, { import: enableImport, dialect1 }).parseStatements();
18844
19336
  }
18845
19337
  function parseRelativeDateVariableValue(name, value) {
18846
19338
  try {
@@ -19026,6 +19518,31 @@ var ScalarSubqueryError = class extends Error {
19026
19518
  }
19027
19519
  };
19028
19520
  async function executeAssert(stmt, client, options, cacheContext, tempTables) {
19521
+ const evaluation = await evaluateAssertCondition(stmt, client, options, cacheContext, tempTables);
19522
+ if (!evaluation.passed) {
19523
+ if (stmt.warn === true) {
19524
+ return {
19525
+ type: "ASSERT",
19526
+ condition: stmt.text,
19527
+ passed: false,
19528
+ warning: stmt.message ?? `assertion failed: ${stmt.text} (actual: ${evaluation.actual}).`
19529
+ };
19530
+ }
19531
+ const suffix = stmt.message !== void 0 ? ` ${stmt.message}` : "";
19532
+ throw new AssertError(`assertion failed: ${stmt.text} (actual: ${evaluation.actual}).${suffix}`);
19533
+ }
19534
+ return { type: "ASSERT", condition: stmt.text };
19535
+ }
19536
+ async function executeExit(stmt, client, options, cacheContext, tempTables) {
19537
+ const evaluation = await evaluateAssertCondition(stmt, client, options, cacheContext, tempTables);
19538
+ return {
19539
+ type: "EXIT",
19540
+ condition: stmt.text,
19541
+ exited: evaluation.passed,
19542
+ message: stmt.message
19543
+ };
19544
+ }
19545
+ async function evaluateAssertCondition(stmt, client, options, cacheContext, tempTables) {
19029
19546
  const left = await evalAssertOperand(stmt.left, client, options, cacheContext, tempTables);
19030
19547
  const semantics = stmt.left.type === "NUMBER" || stmt.left.type === "ARITH" ? syntheticSemantics("number") : syntheticSemantics("string");
19031
19548
  if (stmt.op === "BETWEEN") {
@@ -19034,19 +19551,16 @@ async function executeAssert(stmt, client, options, cacheContext, tempTables) {
19034
19551
  }
19035
19552
  const low = await evalAssertOperand(stmt.low, client, options, cacheContext, tempTables);
19036
19553
  const high = await evalAssertOperand(stmt.high, client, options, cacheContext, tempTables);
19037
- if (!compareScalarValues(">=", left, low, semantics) || !compareScalarValues("<=", left, high, semantics)) {
19038
- throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
19039
- }
19040
- return { type: "ASSERT", condition: stmt.text };
19554
+ return {
19555
+ passed: compareScalarValues(">=", left, low, semantics) && compareScalarValues("<=", left, high, semantics),
19556
+ actual: left
19557
+ };
19041
19558
  }
19042
19559
  if (stmt.right === null) {
19043
19560
  throw new Error("ArgumentError: malformed ASSERT statement.");
19044
19561
  }
19045
19562
  const right = await evalAssertOperand(stmt.right, client, options, cacheContext, tempTables);
19046
- if (!compareScalarValues(stmt.op, left, right, semantics)) {
19047
- throw new AssertError(`assertion failed: ${stmt.text} (actual: ${left}).`);
19048
- }
19049
- return { type: "ASSERT", condition: stmt.text };
19563
+ return { passed: compareScalarValues(stmt.op, left, right, semantics), actual: left };
19050
19564
  }
19051
19565
  async function evalAssertOperand(operand, client, options, cacheContext, tempTables) {
19052
19566
  switch (operand.type) {
@@ -26380,7 +26894,16 @@ async function buildBatchExplainPlans(sql, client, injectedVariables, cacheConte
26380
26894
  });
26381
26895
  const invocationCacheContext = createInvocationCacheContext(cacheContext);
26382
26896
  try {
26383
- const statements = parseSqlBatch(sql, enableImport);
26897
+ const header = parseScriptHeader(sql);
26898
+ const headerError = header.diagnostics.find((diagnostic2) => diagnostic2.severity === "error");
26899
+ if (header.hasDirectives && headerError) {
26900
+ throw new Error(`${headerError.code}: ${headerError.message} (${headerError.line}:${headerError.column})`);
26901
+ }
26902
+ const statements = parseSqlBatch(
26903
+ header.hasDirectives ? sql.slice(header.headerEnd) : sql,
26904
+ enableImport,
26905
+ header.hasDirectives && header.meta.dialect === 1
26906
+ );
26384
26907
  const analysis = analyzeBatch(statements);
26385
26908
  const normalizedInjectedVariables = validateDeclaredBatchVariables(statements, injectedVariables);
26386
26909
  const relativeDateVariables = prepareRelativeDateVariables(statements, normalizedInjectedVariables);
@@ -26575,8 +27098,8 @@ function buildBatchStatementPlan(stmt, info, capabilities, orderPlans, plainGrou
26575
27098
  }
26576
27099
  if (stmt.type === "ASSERT") {
26577
27100
  const lines = [
26578
- `ASSERT ${stmt.text}`,
26579
- " check: \u5B9F\u884C\u6642\u306B\u6761\u4EF6\u8A55\u4FA1\uFF08\u4E0D\u6210\u7ACB\u306F AssertError \u3067\u30D0\u30C3\u30C1\u505C\u6B62\u3001\u4EE5\u964D\u306E\u6587\u306F skipped\uFF09"
27101
+ `ASSERT${stmt.warn === true ? " WARN" : ""} ${stmt.text}${stmt.message !== void 0 ? `, '${stmt.message.replace(/'/g, "''")}'` : ""}`,
27102
+ stmt.warn === true ? " check: \u5B9F\u884C\u6642\u306B\u6761\u4EF6\u8A55\u4FA1\uFF08\u4E0D\u6210\u7ACB\u306F\u8B66\u544A\u3068\u3057\u3066\u8A18\u9332\u3057\u3001\u5F8C\u7D9A\u6587\u3092\u7D9A\u884C\uFF09" : " check: \u5B9F\u884C\u6642\u306B\u6761\u4EF6\u8A55\u4FA1\uFF08\u4E0D\u6210\u7ACB\u306F AssertError \u3067\u30D0\u30C3\u30C1\u505C\u6B62\u3001\u4EE5\u964D\u306E\u6587\u306F skipped\uFF09"
26580
27103
  ];
26581
27104
  const subqueries = [stmt.left, stmt.right, stmt.low, stmt.high].filter(
26582
27105
  (o) => o !== null && o.type === "SCALAR_SUBQUERY"
@@ -26598,6 +27121,31 @@ function buildBatchStatementPlan(stmt, info, capabilities, orderPlans, plainGrou
26598
27121
  });
26599
27122
  return lines;
26600
27123
  }
27124
+ if (stmt.type === "EXIT") {
27125
+ const lines = [
27126
+ `EXIT SUCCESS IF ${stmt.text}, '${stmt.message.replace(/'/g, "''")}'`,
27127
+ " check: \u5B9F\u884C\u6642\u306B\u6761\u4EF6\u8A55\u4FA1\uFF08\u6210\u7ACB\u6642\u306F\u6B63\u5E38\u7D42\u4E86\u3057\u3001\u4EE5\u964D\u306E\u6587\u306F skippedReason: exit\uFF09"
27128
+ ];
27129
+ const subqueries = [stmt.left, stmt.right, stmt.low, stmt.high].filter(
27130
+ (o) => o !== null && o.type === "SCALAR_SUBQUERY"
27131
+ );
27132
+ subqueries.forEach((sq, i) => {
27133
+ lines.push(subqueries.length > 1 ? ` subquery[${i + 1}]:` : " subquery:");
27134
+ const subInfo = hasTempTableRef(sq.query) ? info : { ...info, tempTablesReferenced: [] };
27135
+ lines.push(...buildPlanForBatchQuery(
27136
+ sq.query,
27137
+ subInfo,
27138
+ capabilities,
27139
+ orderPlans,
27140
+ plainGroupByPlans,
27141
+ collector,
27142
+ "main",
27143
+ tempSchemaLedger,
27144
+ explainContext
27145
+ ).map((line) => ` ${line}`));
27146
+ });
27147
+ return lines;
27148
+ }
26601
27149
  if (stmt.type === "UPDATE" && (stmt.applyBlocks?.length ?? 0) > 0) {
26602
27150
  return buildExplainPlan(
26603
27151
  stmt,
@@ -28085,6 +28633,9 @@ function toMutationSummary(result) {
28085
28633
  ...result.errTable !== void 0 ? { errTable: result.errTable } : {}
28086
28634
  };
28087
28635
  }
28636
+ if (result.type === "EXIT") {
28637
+ return { condition: result.condition, exited: result.exited, message: result.message };
28638
+ }
28088
28639
  return { reorderedParentCount: result.reorderedParentCount };
28089
28640
  }
28090
28641
  function buildBatchEnvelope(batch, options = {}) {
@@ -28142,6 +28693,10 @@ function buildBatchEnvelope(batch, options = {}) {
28142
28693
  ...s.result.deletedRows ? { deletedRows: s.result.deletedRows } : {},
28143
28694
  ...s.result.diagnostic ? { diagnostic: s.result.diagnostic } : {}
28144
28695
  });
28696
+ } else if (s.status === "success" && s.result?.type === "ASSERT") {
28697
+ entry.condition = s.result.condition;
28698
+ if (s.result.passed !== void 0) entry.passed = s.result.passed;
28699
+ if (s.result.warning !== void 0) entry.warning = s.result.warning;
28145
28700
  } else if (s.status === "success" && s.result && s.result.type !== "SELECT" && s.result.type !== "ASSERT") {
28146
28701
  Object.assign(entry, toMutationSummary(s.result));
28147
28702
  }