MODULE test_blob ; (* m2SQLITE test: incremental BLOB I/O and the online backup API. Covers open/read/write/reopen/bytes/close, the read-only write error, direct backup paging, and the BackupToFile helper with a temp file that is removed afterwards. *) FROM SYSTEM IMPORT ADDRESS, ADR; FROM SQLite IMPORT DbHandle, StmtHandle, BlobHandle, BackupHandle, SQLiteOk, SQLiteRow, SQLiteDone, SQLiteReadOnly, sqlite3_open, sqlite3_close, sqlite3_prepare_v2, sqlite3_step, sqlite3_finalize, sqlite3_last_insert_rowid, sqlite3_column_int, sqlite3_blob_open, sqlite3_blob_reopen, sqlite3_blob_close, sqlite3_blob_bytes, sqlite3_blob_read, sqlite3_blob_write, sqlite3_backup_init, sqlite3_backup_step, sqlite3_backup_finish, sqlite3_backup_remaining, sqlite3_backup_pagecount; FROM SQLiteUtils IMPORT ErrMsg, ExecSimple, BackupToFile; FROM libc IMPORT printf, unlink; VAR db, db2: DbHandle; stmt: StmtHandle; bh, bh2: BlobHandle; bk: BackupHandle; row1, row2: LONGINT; wbuf: ARRAY [0..15] OF CHAR; rbuf: ARRAY [0..255] OF CHAR; path: ARRAY [0..63] OF CHAR; i: INTEGER; PROCEDURE fail (what: ARRAY OF CHAR); VAR e: ARRAY [0..255] OF CHAR; BEGIN ErrMsg(db, e); printf("FAIL %s: %s\n", what, e); HALT(1) END fail; PROCEDURE check (rc: INTEGER; what: ARRAY OF CHAR); BEGIN IF rc # SQLiteOk THEN fail(what) END END check; PROCEDURE RemoveFile (name: ARRAY OF CHAR); VAR buf: ARRAY [0..255] OF CHAR; n: INTEGER; rc: INTEGER; BEGIN n := 0; WHILE (VAL(CARDINAL, n) <= HIGH(buf)) AND (VAL(CARDINAL, n) <= HIGH(name)) AND (name[n] # 0C) DO buf[n] := name[n]; INC(n) END; IF VAL(CARDINAL, n) <= HIGH(buf) THEN buf[n] := 0C END; rc := unlink(ADR(buf)) END RemoveFile; PROCEDURE countFiles (h: DbHandle; what: ARRAY OF CHAR) : INTEGER; VAR st: StmtHandle; n, rc: INTEGER; BEGIN rc := sqlite3_prepare_v2(h, "SELECT COUNT(*) FROM files;", -1, st, NIL); IF rc # SQLiteOk THEN fail(what) END; IF sqlite3_step(st) # SQLiteRow THEN fail(what) END; n := sqlite3_column_int(st, 0); IF sqlite3_finalize(st) # SQLiteOk THEN fail(what) END; RETURN n END countFiles; BEGIN FOR i := 0 TO 15 DO wbuf[i] := CHR(ORD('A') + VAL(CARDINAL, i)) END; check(sqlite3_open(":memory:", db), "open"); check(ExecSimple(db, "CREATE TABLE files(name TEXT, data BLOB);"), "create"); check(ExecSimple(db, "INSERT INTO files VALUES('a', zeroblob(256));"), "seed1"); row1 := sqlite3_last_insert_rowid(db); check(ExecSimple(db, "INSERT INTO files VALUES('b', zeroblob(16));"), "seed2"); row2 := sqlite3_last_insert_rowid(db); (* read-write handle, size check, write, read back *) check(sqlite3_blob_open(db, "main", "files", "data", row1, 1, bh), "blob open"); IF sqlite3_blob_bytes(bh) # 256 THEN fail("blob bytes") END; check(sqlite3_blob_write(bh, ADR(wbuf), 16, 10), "blob write"); check(sqlite3_blob_read(bh, ADR(rbuf), 16, 10), "blob read"); FOR i := 0 TO 15 DO IF rbuf[i] # wbuf[i] THEN fail("blob roundtrip") END END; printf("blob roundtrip ok\n"); (* move the handle to the second row *) check(sqlite3_blob_reopen(bh, row2), "blob reopen"); IF sqlite3_blob_bytes(bh) # 16 THEN fail("reopen bytes") END; check(sqlite3_blob_close(bh), "blob close"); (* read-only handle must refuse writes *) check(sqlite3_blob_open(db, "main", "files", "data", row1, 0, bh2), "ro open"); IF sqlite3_blob_write(bh2, ADR(wbuf), 1, 0) # SQLiteReadOnly THEN fail("ro write accepted") END; printf("readonly guard ok\n"); (* close relays the handle's pending write error, but the handle is released regardless *) i := sqlite3_blob_close(bh2); printf("ro close rc %d\n", i); (* direct backup API into a second memory database *) check(sqlite3_open(":memory:", db2), "open dst"); bk := sqlite3_backup_init(db2, "main", db, "main"); IF bk = NIL THEN fail("backup init") END; i := sqlite3_backup_step(bk, -1); printf("backup step %d remaining %d pages %d\n", i, sqlite3_backup_remaining(bk), sqlite3_backup_pagecount(bk)); IF i # SQLiteDone THEN fail("backup step") END; IF sqlite3_backup_remaining(bk) # 0 THEN fail("remaining") END; IF sqlite3_backup_pagecount(bk) <= 0 THEN fail("pagecount") END; check(sqlite3_backup_finish(bk), "backup finish"); IF countFiles(db2, "verify") # 2 THEN fail("backup content") END; printf("backup copy ok\n"); check(sqlite3_close(db2), "close dst"); (* helper to a temp file, verified and removed *) path[0] := '/'; path[1] := 't'; path[2] := 'm'; path[3] := 'p'; path[4] := '/'; path[5] := 'm'; path[6] := '2'; path[7] := 's'; path[8] := 'q'; path[9] := 'l'; path[10] := 'i'; path[11] := 't'; path[12] := 'e'; path[13] := '_'; path[14] := 'b'; path[15] := 'k'; path[16] := '.'; path[17] := 'd'; path[18] := 'b'; path[19] := 0C; check(BackupToFile(db, path), "backup file"); check(sqlite3_open(path, db2), "open file"); IF countFiles(db2, "file content") # 2 THEN fail("file rows") END; check(sqlite3_close(db2), "close file"); RemoveFile(path); printf("backup file ok\n"); check(sqlite3_close(db), "close"); printf("PASS test_blob\n") END test_blob.