@@ -64,7 +64,7 @@ ParseResult Parser<D>::classify_and_dispatch() {
6464 case TokenType::TK_DELETE : return parse_delete ();
6565 case TokenType::TK_REPLACE : return parse_insert (true );
6666 case TokenType::TK_BEGIN :
67- case TokenType::TK_START :
67+ case TokenType::TK_START : return parse_transaction (first);
6868 case TokenType::TK_COMMIT :
6969 case TokenType::TK_ROLLBACK :
7070 case TokenType::TK_SAVEPOINT :return extract_transaction (first);
@@ -910,6 +910,40 @@ ParseResult Parser<D>::parse_load_data() {
910910 return r;
911911}
912912
913+ // ---- TRANSACTION ----
914+
915+ template <Dialect D>
916+ ParseResult Parser<D>::parse_transaction(const Token& first) {
917+ ParseResult r;
918+ bool is_begin = (first.type == TokenType::TK_BEGIN );
919+ r.stmt_type = is_begin ? StmtType::BEGIN : StmtType::START_TRANSACTION ;
920+
921+ StringRef introducer = is_begin ? StringRef{" BEGIN" , 5 }
922+ : StringRef{" START TRANSACTION" , 17 };
923+ Token next = tokenizer_.peek ();
924+ if (next.type == TokenType::TK_TRANSACTION ) {
925+ if (!is_begin) {
926+ tokenizer_.skip ();
927+ } else if constexpr (D == Dialect::PostgreSQL) {
928+ // MySQL's BEGIN takes WORK but not TRANSACTION.
929+ tokenizer_.skip ();
930+ introducer = StringRef{" BEGIN TRANSACTION" , 17 };
931+ }
932+ } else if (is_begin && next.type == TokenType::TK_IDENTIFIER &&
933+ next.text .equals_ci (" WORK" , 4 )) {
934+ // WORK is a noise word after BEGIN in both dialects; no form of its own.
935+ tokenizer_.skip ();
936+ }
937+
938+ // MySQL carries modes on START TRANSACTION only; its BEGIN takes none.
939+ bool allow_modes = (D == Dialect::PostgreSQL) || !is_begin;
940+
941+ r.status = ParseResult::OK ;
942+ parse_transaction_modes (r, introducer, allow_modes);
943+ scan_to_end (r);
944+ return r;
945+ }
946+
913947// ---- Helpers ----
914948
915949template <Dialect D>
@@ -972,6 +1006,122 @@ void Parser<D>::scan_to_end(ParseResult& result) {
9721006 }
9731007}
9741008
1009+ template <Dialect D>
1010+ void Parser<D>::parse_transaction_modes(ParseResult& result, StringRef introducer,
1011+ bool allow_modes) {
1012+ AstNode* root = make_node (arena_, NodeType::NODE_TRANSACTION_STMT , introducer);
1013+ if (!root) { result.status = ParseResult::ERROR ; return ; }
1014+
1015+ while (allow_modes) {
1016+ Token t = tokenizer_.peek ();
1017+
1018+ if (t.type == TokenType::TK_ISOLATION ) {
1019+ // MySQL sets the isolation level with SET TRANSACTION, not here.
1020+ if constexpr (D == Dialect::MySQL) break ;
1021+ tokenizer_.skip ();
1022+ if (tokenizer_.peek ().type == TokenType::TK_LEVEL ) tokenizer_.skip ();
1023+
1024+ Token level = tokenizer_.next_token ();
1025+ if (level.type == TokenType::TK_EOF ) {
1026+ result.status = ParseResult::PARTIAL ;
1027+ break ;
1028+ }
1029+ StringRef value = level.text ;
1030+ if (level.type == TokenType::TK_SERIALIZABLE ) {
1031+ value = StringRef{" SERIALIZABLE" , 12 };
1032+ } else if (level.type == TokenType::TK_READ ||
1033+ level.type == TokenType::TK_REPEATABLE ) {
1034+ // READ COMMITTED / READ UNCOMMITTED / REPEATABLE READ
1035+ Token second = tokenizer_.next_token ();
1036+ if (second.type == TokenType::TK_EOF ) {
1037+ result.status = ParseResult::PARTIAL ;
1038+ break ;
1039+ }
1040+ if (second.type == TokenType::TK_COMMITTED ) {
1041+ value = StringRef{" READ COMMITTED" , 14 };
1042+ } else if (second.type == TokenType::TK_UNCOMMITTED ) {
1043+ value = StringRef{" READ UNCOMMITTED" , 16 };
1044+ } else if (second.type == TokenType::TK_READ ) {
1045+ value = StringRef{" REPEATABLE READ" , 15 };
1046+ } else {
1047+ value = StringRef{level.text .ptr ,
1048+ static_cast <uint32_t >((second.text .ptr + second.text .len ) - level.text .ptr )};
1049+ }
1050+ }
1051+ AstNode* mode = make_node (arena_, NodeType::NODE_IDENTIFIER , value);
1052+ if (mode) mode->flags = FLAG_TXN_MODE_ISOLATION ;
1053+ root->add_child (mode);
1054+ } else if (t.type == TokenType::TK_READ ) {
1055+ tokenizer_.skip ();
1056+ Token rw = tokenizer_.next_token (); // ONLY or WRITE
1057+ if (rw.type == TokenType::TK_EOF ) {
1058+ result.status = ParseResult::PARTIAL ;
1059+ break ;
1060+ }
1061+ StringRef value;
1062+ if (rw.type == TokenType::TK_ONLY ) {
1063+ value = StringRef{" READ ONLY" , 9 };
1064+ } else if (rw.type == TokenType::TK_WRITE ) {
1065+ value = StringRef{" READ WRITE" , 10 };
1066+ } else {
1067+ value = StringRef{t.text .ptr ,
1068+ static_cast <uint32_t >((rw.text .ptr + rw.text .len ) - t.text .ptr )};
1069+ }
1070+ root->add_child (make_node (arena_, NodeType::NODE_IDENTIFIER , value));
1071+ } else if (t.type == TokenType::TK_NOT ||
1072+ (t.type == TokenType::TK_IDENTIFIER &&
1073+ t.text .equals_ci (" DEFERRABLE" , 10 ))) {
1074+ // PostgreSQL: [ NOT ] DEFERRABLE. Parsed so it cannot hide a later mode.
1075+ if constexpr (D == Dialect::PostgreSQL) {
1076+ bool is_not = (t.type == TokenType::TK_NOT );
1077+ tokenizer_.skip ();
1078+ if (is_not) {
1079+ Token d = tokenizer_.next_token ();
1080+ if (d.type != TokenType::TK_IDENTIFIER ||
1081+ !d.text .equals_ci (" DEFERRABLE" , 10 )) {
1082+ result.status = ParseResult::PARTIAL ;
1083+ break ;
1084+ }
1085+ }
1086+ root->add_child (make_node (arena_, NodeType::NODE_IDENTIFIER ,
1087+ is_not ? StringRef{" NOT DEFERRABLE" , 14 }
1088+ : StringRef{" DEFERRABLE" , 10 }));
1089+ } else {
1090+ break ;
1091+ }
1092+ } else if (t.type == TokenType::TK_WITH ) {
1093+ // MySQL: WITH CONSISTENT SNAPSHOT, same reason.
1094+ if constexpr (D == Dialect::MySQL) {
1095+ tokenizer_.skip ();
1096+ Token c = tokenizer_.next_token ();
1097+ Token sn = tokenizer_.next_token ();
1098+ if (c.type != TokenType::TK_IDENTIFIER ||
1099+ !c.text .equals_ci (" CONSISTENT" , 10 ) ||
1100+ sn.type != TokenType::TK_IDENTIFIER ||
1101+ !sn.text .equals_ci (" SNAPSHOT" , 8 )) {
1102+ result.status = ParseResult::PARTIAL ;
1103+ break ;
1104+ }
1105+ root->add_child (make_node (arena_, NodeType::NODE_IDENTIFIER ,
1106+ StringRef{" WITH CONSISTENT SNAPSHOT" , 24 }));
1107+ } else {
1108+ break ;
1109+ }
1110+ } else {
1111+ break ;
1112+ }
1113+
1114+ // PostgreSQL allows the commas to be omitted; MySQL requires them.
1115+ if (tokenizer_.peek ().type == TokenType::TK_COMMA ) {
1116+ tokenizer_.skip ();
1117+ } else if constexpr (D == Dialect::MySQL) {
1118+ break ;
1119+ }
1120+ }
1121+
1122+ result.ast = root;
1123+ }
1124+
9751125// ---- Tier 2 Extractors ----
9761126
9771127template <Dialect D>
@@ -1060,15 +1210,6 @@ ParseResult Parser<D>::extract_transaction(const Token& first) {
10601210 r.status = ParseResult::OK ;
10611211
10621212 switch (first.type ) {
1063- case TokenType::TK_BEGIN :
1064- r.stmt_type = StmtType::BEGIN ;
1065- break ;
1066- case TokenType::TK_START :
1067- r.stmt_type = StmtType::START_TRANSACTION ;
1068- // consume TRANSACTION if present
1069- if (tokenizer_.peek ().type == TokenType::TK_TRANSACTION )
1070- tokenizer_.skip ();
1071- break ;
10721213 case TokenType::TK_COMMIT :
10731214 r.stmt_type = StmtType::COMMIT ;
10741215 break ;
0 commit comments