OLD | NEW |
(Empty) | |
| 1 # 2009 January 8 |
| 2 # |
| 3 # The author disclaims copyright to this source code. In place of |
| 4 # a legal notice, here is a blessing: |
| 5 # |
| 6 # May you do good and not evil. |
| 7 # May you find forgiveness for yourself and forgive others. |
| 8 # May you share freely, never taking more than you give. |
| 9 # |
| 10 #*********************************************************************** |
| 11 # |
| 12 # This test verifies a couple of specific potential data corruption |
| 13 # scenarios involving crashes or power failures. |
| 14 # |
| 15 # Later: Also, some other specific scenarios required for coverage |
| 16 # testing that do not lead to corruption. |
| 17 # |
| 18 # $Id: crash8.test,v 1.4 2009/01/11 00:44:48 drh Exp $ |
| 19 |
| 20 |
| 21 set testdir [file dirname $argv0] |
| 22 source $testdir/tester.tcl |
| 23 |
| 24 ifcapable !crashtest { |
| 25 finish_test |
| 26 return |
| 27 } |
| 28 |
| 29 do_test crash8-1.1 { |
| 30 execsql { |
| 31 PRAGMA auto_vacuum=OFF; |
| 32 CREATE TABLE t1(a, b); |
| 33 CREATE INDEX i1 ON t1(a, b); |
| 34 INSERT INTO t1 VALUES(1, randstr(1000,1000)); |
| 35 INSERT INTO t1 VALUES(2, randstr(1000,1000)); |
| 36 INSERT INTO t1 VALUES(3, randstr(1000,1000)); |
| 37 INSERT INTO t1 VALUES(4, randstr(1000,1000)); |
| 38 INSERT INTO t1 VALUES(5, randstr(1000,1000)); |
| 39 INSERT INTO t1 VALUES(6, randstr(1000,1000)); |
| 40 CREATE TABLE t2(a, b); |
| 41 CREATE TABLE t3(a, b); |
| 42 CREATE TABLE t4(a, b); |
| 43 CREATE TABLE t5(a, b); |
| 44 CREATE TABLE t6(a, b); |
| 45 CREATE TABLE t7(a, b); |
| 46 CREATE TABLE t8(a, b); |
| 47 CREATE TABLE t9(a, b); |
| 48 CREATE TABLE t10(a, b); |
| 49 PRAGMA integrity_check |
| 50 } |
| 51 } {ok} |
| 52 |
| 53 |
| 54 # Potential corruption scenario 1. A second process opens the database |
| 55 # and modifies a large portion of it. It then opens a second transaction |
| 56 # and modifies a small part of the database, but crashes before it commits |
| 57 # the transaction. |
| 58 # |
| 59 # When the first process accessed the database again, it was rolling back |
| 60 # the aborted transaction, but was not purging its in-memory cache (which |
| 61 # was loaded before the second process made its first, successful, |
| 62 # modification). Producing an inconsistent cache. |
| 63 # |
| 64 do_test crash8-1.2 { |
| 65 crashsql -delay 2 -file test.db { |
| 66 PRAGMA cache_size = 10; |
| 67 UPDATE t1 SET b = randstr(1000,1000); |
| 68 INSERT INTO t9 VALUES(1, 2); |
| 69 } |
| 70 } {1 {child process exited abnormally}} |
| 71 do_test crash8-1.3 { |
| 72 execsql {PRAGMA integrity_check} |
| 73 } {ok} |
| 74 |
| 75 # Potential corruption scenario 2. The second process, operating in |
| 76 # persistent-journal mode, makes a large change to the database file |
| 77 # with a small in-memory cache. Such that more than one journal-header |
| 78 # was written to the file. It then opens a second transaction and makes |
| 79 # a smaller change that requires only a single journal-header to be |
| 80 # written to the journal file. The second change is such that the |
| 81 # journal content written to the persistent journal file exactly overwrites |
| 82 # the first journal-header and set of subsequent records written by the |
| 83 # first, successful, change. The second process crashes before it can |
| 84 # commit its second change. |
| 85 # |
| 86 # When the first process accessed the database again, it was rolling back |
| 87 # the second aborted transaction, then continuing to rollback the second |
| 88 # and subsequent journal-headers written by the first, successful, change. |
| 89 # Database corruption. |
| 90 # |
| 91 do_test crash8.2.1 { |
| 92 crashsql -delay 2 -file test.db { |
| 93 PRAGMA journal_mode = persist; |
| 94 PRAGMA cache_size = 10; |
| 95 UPDATE t1 SET b = randstr(1000,1000); |
| 96 PRAGMA cache_size = 100; |
| 97 BEGIN; |
| 98 INSERT INTO t2 VALUES('a', 'b'); |
| 99 INSERT INTO t3 VALUES('a', 'b'); |
| 100 INSERT INTO t4 VALUES('a', 'b'); |
| 101 INSERT INTO t5 VALUES('a', 'b'); |
| 102 INSERT INTO t6 VALUES('a', 'b'); |
| 103 INSERT INTO t7 VALUES('a', 'b'); |
| 104 INSERT INTO t8 VALUES('a', 'b'); |
| 105 INSERT INTO t9 VALUES('a', 'b'); |
| 106 INSERT INTO t10 VALUES('a', 'b'); |
| 107 COMMIT; |
| 108 } |
| 109 } {1 {child process exited abnormally}} |
| 110 |
| 111 do_test crash8-2.3 { |
| 112 execsql {PRAGMA integrity_check} |
| 113 } {ok} |
| 114 |
| 115 proc read_file {zFile} { |
| 116 set fd [open $zFile] |
| 117 fconfigure $fd -translation binary |
| 118 set zData [read $fd] |
| 119 close $fd |
| 120 return $zData |
| 121 } |
| 122 proc write_file {zFile zData} { |
| 123 set fd [open $zFile w] |
| 124 fconfigure $fd -translation binary |
| 125 puts -nonewline $fd $zData |
| 126 close $fd |
| 127 } |
| 128 |
| 129 # The following tests check that SQLite will not roll back a hot-journal |
| 130 # file if the sector-size field in the first journal file header is |
| 131 # suspect. Definition of suspect: |
| 132 # |
| 133 # a) Not a power of 2, or (crash8-3.5) |
| 134 # b) Greater than 0x01000000 (16MB), or (crash8-3.6) |
| 135 # c) Less than 512. (crash8-3.7) |
| 136 # |
| 137 # Also test that SQLite will not rollback a hot-journal file with a |
| 138 # suspect page-size. In this case "suspect" means: |
| 139 # |
| 140 # a) Not a power of 2, or |
| 141 # b) Less than 512, or |
| 142 # c) Greater than SQLITE_MAX_PAGE_SIZE |
| 143 # |
| 144 do_test crash8-3.1 { |
| 145 list [file exists test.db-joural] [file exists test.db] |
| 146 } {0 1} |
| 147 do_test crash8-3.2 { |
| 148 execsql { |
| 149 PRAGMA synchronous = off; |
| 150 BEGIN; |
| 151 DELETE FROM t1; |
| 152 SELECT count(*) FROM t1; |
| 153 } |
| 154 } {0} |
| 155 do_test crash8-3.3 { |
| 156 set zJournal [read_file test.db-journal] |
| 157 execsql { |
| 158 COMMIT; |
| 159 SELECT count(*) FROM t1; |
| 160 } |
| 161 } {0} |
| 162 do_test crash8-3.4 { |
| 163 binary scan [string range $zJournal 20 23] I nSector |
| 164 set nSector |
| 165 } {512} |
| 166 |
| 167 do_test crash8-3.5 { |
| 168 set zJournal2 [string replace $zJournal 20 23 [binary format I 513]] |
| 169 write_file test.db-journal $zJournal2 |
| 170 |
| 171 execsql { |
| 172 SELECT count(*) FROM t1; |
| 173 PRAGMA integrity_check |
| 174 } |
| 175 } {0 ok} |
| 176 do_test crash8-3.6 { |
| 177 set zJournal2 [string replace $zJournal 20 23 [binary format I 0x2000000]] |
| 178 write_file test.db-journal $zJournal2 |
| 179 execsql { |
| 180 SELECT count(*) FROM t1; |
| 181 PRAGMA integrity_check |
| 182 } |
| 183 } {0 ok} |
| 184 do_test crash8-3.7 { |
| 185 set zJournal2 [string replace $zJournal 20 23 [binary format I 256]] |
| 186 write_file test.db-journal $zJournal2 |
| 187 execsql { |
| 188 SELECT count(*) FROM t1; |
| 189 PRAGMA integrity_check |
| 190 } |
| 191 } {0 ok} |
| 192 |
| 193 do_test crash8-3.8 { |
| 194 set zJournal2 [string replace $zJournal 24 27 [binary format I 513]] |
| 195 write_file test.db-journal $zJournal2 |
| 196 |
| 197 execsql { |
| 198 SELECT count(*) FROM t1; |
| 199 PRAGMA integrity_check |
| 200 } |
| 201 } {0 ok} |
| 202 do_test crash8-3.9 { |
| 203 set big [expr $SQLITE_MAX_PAGE_SIZE * 2] |
| 204 set zJournal2 [string replace $zJournal 24 27 [binary format I $big]] |
| 205 write_file test.db-journal $zJournal2 |
| 206 execsql { |
| 207 SELECT count(*) FROM t1; |
| 208 PRAGMA integrity_check |
| 209 } |
| 210 } {0 ok} |
| 211 do_test crash8-3.10 { |
| 212 set zJournal2 [string replace $zJournal 24 27 [binary format I 256]] |
| 213 write_file test.db-journal $zJournal2 |
| 214 execsql { |
| 215 SELECT count(*) FROM t1; |
| 216 PRAGMA integrity_check |
| 217 } |
| 218 } {0 ok} |
| 219 |
| 220 do_test crash8-3.11 { |
| 221 set fd [open test.db-journal w] |
| 222 fconfigure $fd -translation binary |
| 223 puts -nonewline $fd $zJournal |
| 224 close $fd |
| 225 execsql { |
| 226 SELECT count(*) FROM t1; |
| 227 PRAGMA integrity_check |
| 228 } |
| 229 } {6 ok} |
| 230 |
| 231 |
| 232 # If a connection running in persistent-journal mode is part of a |
| 233 # multi-file transaction, it must ensure that the master-journal name |
| 234 # appended to the journal file contents during the commit is located |
| 235 # at the end of the physical journal file. If there was already a |
| 236 # large journal file allocated at the start of the transaction, this |
| 237 # may mean truncating the file so that the master journal name really |
| 238 # is at the physical end of the file. |
| 239 # |
| 240 # This block of tests test that SQLite correctly truncates such |
| 241 # journal files, and that the results behave correctly if a hot-journal |
| 242 # rollback occurs. |
| 243 # |
| 244 ifcapable pragma { |
| 245 reset_db |
| 246 forcedelete test2.db |
| 247 |
| 248 do_test crash8-4.1 { |
| 249 execsql { |
| 250 PRAGMA journal_mode = persist; |
| 251 CREATE TABLE ab(a, b); |
| 252 INSERT INTO ab VALUES(0, 'abc'); |
| 253 INSERT INTO ab VALUES(1, NULL); |
| 254 INSERT INTO ab VALUES(2, NULL); |
| 255 INSERT INTO ab VALUES(3, NULL); |
| 256 INSERT INTO ab VALUES(4, NULL); |
| 257 INSERT INTO ab VALUES(5, NULL); |
| 258 INSERT INTO ab VALUES(6, NULL); |
| 259 UPDATE ab SET b = randstr(1000,1000); |
| 260 ATTACH 'test2.db' AS aux; |
| 261 PRAGMA aux.journal_mode = persist; |
| 262 CREATE TABLE aux.ab(a, b); |
| 263 INSERT INTO aux.ab SELECT * FROM main.ab; |
| 264 |
| 265 UPDATE aux.ab SET b = randstr(1000,1000) WHERE a>=1; |
| 266 UPDATE ab SET b = randstr(1000,1000) WHERE a>=1; |
| 267 } |
| 268 list [file exists test.db-journal] [file exists test2.db-journal] |
| 269 } {1 1} |
| 270 |
| 271 do_test crash8-4.2 { |
| 272 execsql { |
| 273 BEGIN; |
| 274 UPDATE aux.ab SET b = 'def' WHERE a = 0; |
| 275 UPDATE main.ab SET b = 'def' WHERE a = 0; |
| 276 COMMIT; |
| 277 } |
| 278 } {} |
| 279 |
| 280 do_test crash8-4.3 { |
| 281 execsql { |
| 282 UPDATE aux.ab SET b = randstr(1000,1000) WHERE a>=1; |
| 283 UPDATE ab SET b = randstr(1000,1000) WHERE a>=1; |
| 284 } |
| 285 } {} |
| 286 |
| 287 set contents_main [db eval {SELECT b FROM main.ab WHERE a = 1}] |
| 288 set contents_aux [db eval {SELECT b FROM aux.ab WHERE a = 1}] |
| 289 |
| 290 do_test crash8-4.4 { |
| 291 crashsql -file test2.db -delay 1 { |
| 292 ATTACH 'test2.db' AS aux; |
| 293 BEGIN; |
| 294 UPDATE aux.ab SET b = 'ghi' WHERE a = 0; |
| 295 UPDATE main.ab SET b = 'ghi' WHERE a = 0; |
| 296 COMMIT; |
| 297 } |
| 298 } {1 {child process exited abnormally}} |
| 299 |
| 300 do_test crash8-4.5 { |
| 301 list [file exists test.db-journal] [file exists test2.db-journal] |
| 302 } {1 1} |
| 303 |
| 304 do_test crash8-4.6 { |
| 305 execsql { |
| 306 SELECT b FROM main.ab WHERE a = 0; |
| 307 SELECT b FROM aux.ab WHERE a = 0; |
| 308 } |
| 309 } {def def} |
| 310 |
| 311 do_test crash8-4.7 { |
| 312 crashsql -file test2.db -delay 1 { |
| 313 ATTACH 'test2.db' AS aux; |
| 314 BEGIN; |
| 315 UPDATE aux.ab SET b = 'jkl' WHERE a = 0; |
| 316 UPDATE main.ab SET b = 'jkl' WHERE a = 0; |
| 317 COMMIT; |
| 318 } |
| 319 } {1 {child process exited abnormally}} |
| 320 |
| 321 do_test crash8-4.8 { |
| 322 set fd [open test.db-journal] |
| 323 fconfigure $fd -translation binary |
| 324 seek $fd -16 end |
| 325 binary scan [read $fd 4] I len |
| 326 |
| 327 seek $fd [expr {-1 * ($len + 16)}] end |
| 328 set zMasterJournal [read $fd $len] |
| 329 close $fd |
| 330 |
| 331 file exists $zMasterJournal |
| 332 } {1} |
| 333 |
| 334 do_test crash8-4.9 { |
| 335 execsql { SELECT b FROM aux.ab WHERE a = 0 } |
| 336 } {def} |
| 337 |
| 338 do_test crash8-4.10 { |
| 339 delete_file $zMasterJournal |
| 340 execsql { SELECT b FROM main.ab WHERE a = 0 } |
| 341 } {jkl} |
| 342 } |
| 343 |
| 344 # |
| 345 # Since the following tests (crash8-5.*) rely upon being able |
| 346 # to copy a file while open, they will not work on Windows. |
| 347 # |
| 348 if {$::tcl_platform(platform)=="unix"} { |
| 349 for {set i 1} {$i < 10} {incr i} { |
| 350 catch { db close } |
| 351 forcedelete test.db test.db-journal |
| 352 sqlite3 db test.db |
| 353 do_test crash8-5.$i.1 { |
| 354 execsql { |
| 355 CREATE TABLE t1(x PRIMARY KEY); |
| 356 INSERT INTO t1 VALUES(randomblob(900)); |
| 357 INSERT INTO t1 SELECT randomblob(900) FROM t1; |
| 358 INSERT INTO t1 SELECT randomblob(900) FROM t1; |
| 359 INSERT INTO t1 SELECT randomblob(900) FROM t1; |
| 360 INSERT INTO t1 SELECT randomblob(900) FROM t1; |
| 361 INSERT INTO t1 SELECT randomblob(900) FROM t1; |
| 362 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 64 rows */ |
| 363 } |
| 364 crashsql -file test.db -delay [expr ($::i%2) + 1] { |
| 365 PRAGMA cache_size = 10; |
| 366 BEGIN; |
| 367 UPDATE t1 SET x = randomblob(900); |
| 368 ROLLBACK; |
| 369 INSERT INTO t1 VALUES(randomblob(900)); |
| 370 } |
| 371 execsql { PRAGMA integrity_check } |
| 372 } {ok} |
| 373 |
| 374 catch { db close } |
| 375 forcedelete test.db test.db-journal |
| 376 sqlite3 db test.db |
| 377 do_test crash8-5.$i.2 { |
| 378 execsql { |
| 379 PRAGMA cache_size = 10; |
| 380 CREATE TABLE t1(x PRIMARY KEY); |
| 381 INSERT INTO t1 VALUES(randomblob(900)); |
| 382 INSERT INTO t1 SELECT randomblob(900) FROM t1; |
| 383 INSERT INTO t1 SELECT randomblob(900) FROM t1; |
| 384 INSERT INTO t1 SELECT randomblob(900) FROM t1; |
| 385 INSERT INTO t1 SELECT randomblob(900) FROM t1; |
| 386 INSERT INTO t1 SELECT randomblob(900) FROM t1; |
| 387 INSERT INTO t1 SELECT randomblob(900) FROM t1; /* 64 rows */ |
| 388 BEGIN; |
| 389 UPDATE t1 SET x = randomblob(900); |
| 390 } |
| 391 forcedelete testX.db testX.db-journal testX.db-wal |
| 392 forcecopy test.db testX.db |
| 393 forcecopy test.db-journal testX.db-journal |
| 394 db close |
| 395 |
| 396 crashsql -file test.db -delay [expr ($::i%2) + 1] { |
| 397 SELECT * FROM sqlite_master; |
| 398 INSERT INTO t1 VALUES(randomblob(900)); |
| 399 } |
| 400 |
| 401 sqlite3 db2 testX.db |
| 402 execsql { PRAGMA integrity_check } db2 |
| 403 } {ok} |
| 404 } |
| 405 catch {db2 close} |
| 406 } |
| 407 |
| 408 finish_test |
OLD | NEW |