| 1 | // vtest build: present_sqlite3? && !sanitize-memory-clang |
| 2 | import db.sqlite |
| 3 | import orm |
| 4 | |
| 5 | struct TxUser { |
| 6 | id int @[primary; sql: serial] |
| 7 | name string |
| 8 | } |
| 9 | |
| 10 | fn setup_tx_db() !sqlite.DB { |
| 11 | mut db := sqlite.connect(':memory:')! |
| 12 | sql db { |
| 13 | create table TxUser |
| 14 | }! |
| 15 | return db |
| 16 | } |
| 17 | |
| 18 | fn insert_callback_user(mut tx orm.Tx) !int { |
| 19 | user := TxUser{ |
| 20 | name: 'callback_commit' |
| 21 | } |
| 22 | sql tx { |
| 23 | insert user into TxUser |
| 24 | }! |
| 25 | return tx.last_id() |
| 26 | } |
| 27 | |
| 28 | fn insert_and_fail(mut tx orm.Tx) !int { |
| 29 | user := TxUser{ |
| 30 | name: 'callback_rollback' |
| 31 | } |
| 32 | sql tx { |
| 33 | insert user into TxUser |
| 34 | }! |
| 35 | return error('force rollback') |
| 36 | } |
| 37 | |
| 38 | fn nested_success_inner(mut tx orm.Tx) !int { |
| 39 | user := TxUser{ |
| 40 | name: 'nested_success' |
| 41 | } |
| 42 | sql tx { |
| 43 | insert user into TxUser |
| 44 | }! |
| 45 | return tx.last_id() |
| 46 | } |
| 47 | |
| 48 | fn nested_failure_inner(mut tx orm.Tx) !int { |
| 49 | user := TxUser{ |
| 50 | name: 'nested_failure' |
| 51 | } |
| 52 | sql tx { |
| 53 | insert user into TxUser |
| 54 | }! |
| 55 | return error('nested failure') |
| 56 | } |
| 57 | |
| 58 | fn count_tx_users(db sqlite.DB) !int { |
| 59 | return db.q_int('select count(*) from TxUser')! |
| 60 | } |
| 61 | |
| 62 | fn tx_user_names(db sqlite.DB) ![]string { |
| 63 | rows := sql db { |
| 64 | select from TxUser order by id |
| 65 | }! |
| 66 | return rows.map(it.name) |
| 67 | } |
| 68 | |
| 69 | fn test_transaction_callback_commits_on_success() { |
| 70 | mut db := setup_tx_db()! |
| 71 | defer { |
| 72 | db.close() or {} |
| 73 | } |
| 74 | |
| 75 | id := orm.transaction[int](mut db, insert_callback_user)! |
| 76 | assert id == 1 |
| 77 | assert count_tx_users(db)! == 1 |
| 78 | assert tx_user_names(db)! == ['callback_commit'] |
| 79 | } |
| 80 | |
| 81 | fn test_transaction_callback_rolls_back_on_error() { |
| 82 | mut db := setup_tx_db()! |
| 83 | defer { |
| 84 | db.close() or {} |
| 85 | } |
| 86 | |
| 87 | orm.transaction[int](mut db, insert_and_fail) or { |
| 88 | assert err.msg() == 'force rollback' |
| 89 | assert count_tx_users(db)! == 0 |
| 90 | return |
| 91 | } |
| 92 | assert false |
| 93 | } |
| 94 | |
| 95 | fn test_manual_begin_with_sql_tx_queries() { |
| 96 | mut db := setup_tx_db()! |
| 97 | defer { |
| 98 | db.close() or {} |
| 99 | } |
| 100 | |
| 101 | mut tx := orm.begin(mut db)! |
| 102 | user := TxUser{ |
| 103 | name: 'manual' |
| 104 | } |
| 105 | sql tx { |
| 106 | insert user into TxUser |
| 107 | }! |
| 108 | inserted := sql tx { |
| 109 | select from TxUser where name == 'manual' |
| 110 | }! |
| 111 | assert inserted.len == 1 |
| 112 | assert inserted[0].name == 'manual' |
| 113 | |
| 114 | sql tx { |
| 115 | update TxUser set name = 'manual_updated' where name == 'manual' |
| 116 | }! |
| 117 | updated := sql tx { |
| 118 | select from TxUser where name == 'manual_updated' |
| 119 | }! |
| 120 | assert updated.len == 1 |
| 121 | |
| 122 | sql tx { |
| 123 | delete from TxUser where name == 'manual_updated' |
| 124 | }! |
| 125 | assert count_tx_users(db)! == 0 |
| 126 | tx.commit()! |
| 127 | } |
| 128 | |
| 129 | fn test_query_builder_works_inside_transaction() { |
| 130 | mut db := setup_tx_db()! |
| 131 | defer { |
| 132 | db.close() or {} |
| 133 | } |
| 134 | |
| 135 | mut tx := orm.begin(mut db)! |
| 136 | mut qb := orm.new_query[TxUser](tx) |
| 137 | qb.insert(TxUser{ |
| 138 | name: 'qb_user' |
| 139 | })! |
| 140 | |
| 141 | selected := qb.where('name = ?', 'qb_user')!.query()! |
| 142 | assert selected.len == 1 |
| 143 | assert selected[0].name == 'qb_user' |
| 144 | |
| 145 | tx.commit()! |
| 146 | assert tx_user_names(db)! == ['qb_user'] |
| 147 | } |
| 148 | |
| 149 | fn test_nested_transaction_success_releases_savepoint() { |
| 150 | mut db := setup_tx_db()! |
| 151 | defer { |
| 152 | db.close() or {} |
| 153 | } |
| 154 | |
| 155 | orm.transaction[int](mut db, fn (mut tx orm.Tx) !int { |
| 156 | before := TxUser{ |
| 157 | name: 'outer_before' |
| 158 | } |
| 159 | sql tx { |
| 160 | insert before into TxUser |
| 161 | }! |
| 162 | |
| 163 | nested_id := tx.transaction[int](nested_success_inner)! |
| 164 | after := TxUser{ |
| 165 | name: 'outer_after' |
| 166 | } |
| 167 | sql tx { |
| 168 | insert after into TxUser |
| 169 | }! |
| 170 | return nested_id |
| 171 | })! |
| 172 | |
| 173 | assert tx_user_names(db)! == ['outer_before', 'nested_success', 'outer_after'] |
| 174 | } |
| 175 | |
| 176 | fn test_nested_transaction_failure_rolls_back_inner_work_only() { |
| 177 | mut db := setup_tx_db()! |
| 178 | defer { |
| 179 | db.close() or {} |
| 180 | } |
| 181 | |
| 182 | orm.transaction[int](mut db, fn (mut tx orm.Tx) !int { |
| 183 | before := TxUser{ |
| 184 | name: 'outer_before' |
| 185 | } |
| 186 | sql tx { |
| 187 | insert before into TxUser |
| 188 | }! |
| 189 | |
| 190 | tx.transaction[int](nested_failure_inner) or {} |
| 191 | |
| 192 | after := TxUser{ |
| 193 | name: 'outer_after' |
| 194 | } |
| 195 | sql tx { |
| 196 | insert after into TxUser |
| 197 | }! |
| 198 | return 0 |
| 199 | })! |
| 200 | |
| 201 | assert tx_user_names(db)! == ['outer_before', 'outer_after'] |
| 202 | } |
| 203 | |
| 204 | fn test_manual_savepoint_rollback_and_release() { |
| 205 | mut db := setup_tx_db()! |
| 206 | defer { |
| 207 | db.close() or {} |
| 208 | } |
| 209 | |
| 210 | mut tx := orm.begin(mut db)! |
| 211 | before := TxUser{ |
| 212 | name: 'before_savepoint' |
| 213 | } |
| 214 | sql tx { |
| 215 | insert before into TxUser |
| 216 | }! |
| 217 | |
| 218 | mut sp := tx.savepoint()! |
| 219 | rollback_user := TxUser{ |
| 220 | name: 'rollback_me' |
| 221 | } |
| 222 | sql tx { |
| 223 | insert rollback_user into TxUser |
| 224 | }! |
| 225 | sp.rollback()! |
| 226 | |
| 227 | after := TxUser{ |
| 228 | name: 'after_rollback' |
| 229 | } |
| 230 | sql tx { |
| 231 | insert after into TxUser |
| 232 | }! |
| 233 | |
| 234 | mut sp2 := tx.savepoint()! |
| 235 | released := TxUser{ |
| 236 | name: 'keep_me' |
| 237 | } |
| 238 | sql tx { |
| 239 | insert released into TxUser |
| 240 | }! |
| 241 | sp2.release()! |
| 242 | |
| 243 | tx.commit()! |
| 244 | assert tx_user_names(db)! == ['before_savepoint', 'after_rollback', 'keep_me'] |
| 245 | } |
| 246 | |
| 247 | fn test_inactive_transaction_errors() { |
| 248 | mut db := setup_tx_db()! |
| 249 | defer { |
| 250 | db.close() or {} |
| 251 | } |
| 252 | |
| 253 | mut tx := orm.begin(mut db)! |
| 254 | tx.commit()! |
| 255 | |
| 256 | tx.commit() or { assert err.msg().contains('inactive') } |
| 257 | |
| 258 | user := TxUser{ |
| 259 | name: 'after_close' |
| 260 | } |
| 261 | sql tx { |
| 262 | insert user into TxUser |
| 263 | } or { |
| 264 | assert err.msg().contains('inactive') |
| 265 | return |
| 266 | } |
| 267 | assert false |
| 268 | } |
| 269 | |
| 270 | fn test_inactive_savepoint_errors() { |
| 271 | mut db := setup_tx_db()! |
| 272 | defer { |
| 273 | db.close() or {} |
| 274 | } |
| 275 | |
| 276 | mut tx := orm.begin(mut db)! |
| 277 | mut sp := tx.savepoint()! |
| 278 | sp.release()! |
| 279 | sp.rollback() or { |
| 280 | assert err.msg().contains('inactive') |
| 281 | tx.rollback()! |
| 282 | return |
| 283 | } |
| 284 | assert false |
| 285 | } |
| 286 | |
| 287 | fn test_savepoint_becomes_inactive_when_parent_transaction_closes() { |
| 288 | mut db := setup_tx_db()! |
| 289 | defer { |
| 290 | db.close() or {} |
| 291 | } |
| 292 | |
| 293 | mut tx := orm.begin(mut db)! |
| 294 | mut sp := tx.savepoint()! |
| 295 | tx.commit()! |
| 296 | |
| 297 | sp.release() or { |
| 298 | assert err.msg().contains('inactive') |
| 299 | return |
| 300 | } |
| 301 | assert false |
| 302 | } |
| 303 | |