3 Commits

Author SHA1 Message Date
29415da692 chore: Re-numbered todos. 2026-06-21 23:10:29 -04:00
f703a8df5d refactor(db.odin): Renamed fields for consistency. 2026-06-21 22:58:43 -04:00
2683e2a00f refactor(sqlite): Used distinct types for Db and Stmt pointers.
Also made some other improvements to it.
2026-06-21 16:52:21 -04:00
5 changed files with 198 additions and 203 deletions

View File

@@ -2,55 +2,55 @@
1. Commands are still leaking.
28. **db.odin** — Inconsistencies in how struct vs sqlite are named.
2. **db.odin** — Inconsistencies in how struct vs sqlite are named.
29. Add color flag and support non colored output.
3. Add color flag and support non colored output.
30. Use text/tables for command output
4. Use text/tables for command output
2. Generate md and man pages again.
5. Generate md and man pages again.
3. **db.odin:324-327** — Map iteration (`remote_set`) is non-deterministic. Same file can produce different JSON on each backup, causing spurious DB diffs. Sort remotes before storing.
6. **db.odin:324-327** — Map iteration (`remote_set`) is non-deterministic. Same file can produce different JSON on each backup, causing spurious DB diffs. Sort remotes before storing.
4. Make sure official path separators are used when appropriate, rather than '/'.
7. Make sure official path separators are used when appropriate, rather than '/'.
5. **cmd_restore.odin:20-30 & cmd_remove.odin:19-29** — Identical path-resolution block copy-pasted. `is_abs` guard is redundant since `filepath.abs` is a no-op on absolute paths. Extract a helper.
8. **cmd_restore.odin:20-30 & cmd_remove.odin:19-29** — Identical path-resolution block copy-pasted. `is_abs` guard is redundant since `filepath.abs` is a no-op on absolute paths. Extract a helper.
6. **cmd_restore.odin:44**`os.mkdir_all` error silently discarded. Subsequent write failure will be confusing.
9. **cmd_restore.odin:44**`os.mkdir_all` error silently discarded. Subsequent write failure will be confusing.
8. **config.odin:178**`search_paths` silently ignores `os.user_home_dir` error. If home is empty, `~` isn't expanded. Same class of bug as issue 3.
10. **config.odin:178**`search_paths` silently ignores `os.user_home_dir` error. If home is empty, `~` isn't expanded. Same class of bug as issue 3.
10. **db.odin:115**`json.unmarshal_string` error not checked. Malformed JSON silently produces empty/partial data.
11. **db.odin:115**`json.unmarshal_string` error not checked. Malformed JSON silently produces empty/partial data.
11. **db.odin:352-353**`hex.encode` error ignored. `string(hex_bytes)` aliases the byte slice.
12. **db.odin:352-353**`hex.encode` error ignored. `string(hex_bytes)` aliases the byte slice.
12. **cmd_sync.odin:80, cmd_list.odin:33**`make([]string, 2)` for table rows never freed. Leaks per row. Defer to memory pass.
13. **cmd_sync.odin:80, cmd_list.odin:33**`make([]string, 2)` for table rows never freed. Leaks per row. Defer to memory pass.
13. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
14. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
14. Add a text filter to the multi_select.
15. Add a text filter to the multi_select.
16. Add tests for untested commands.
17. 2 scan tests silently skip when fd isn't installed, tests pass without actually testing anything. These should use #assert to be sure that fd is in path.
19. add --format -f flag to commands that draw tables.
18. add --format -f flag to commands that draw tables.
20. Replace `testing.expect` calls with `testing.expect_value` calls where appropriate.
19. Replace `testing.expect` calls with `testing.expect_value` calls where appropriate.
21. Change struct field names from PascalCase to snake_case.
20. Change struct field names from PascalCase to snake_case.
23. procedures should be ordered by use, main at the top, then in the order they are called from main.
21. procedures should be ordered by use, main at the top, then in the order they are called from main.
24. Shell completion
22. Shell completion
25. Bring back windows support / cross-compilation.
23. Bring back windows support / cross-compilation.
26. Test all cmds / terminal branches.
24. Test all cmds / terminal branches.
27. Replace `fmt.tprintf("/tmp/envr-test-...-%d", os.get_pid())` + `os.mkdir_all` in test files with `os.mkdir_temp` (race-free, honors `$TMPDIR`, matches `findr/test_env.odin` pattern).
25. Replace `fmt.tprintf("/tmp/envr-test-...-%d", os.get_pid())` + `os.mkdir_all` in test files with `os.mkdir_temp` (race-free, honors `$TMPDIR`, matches `findr/test_env.odin` pattern).
28. Adopt `core:log` across `db.odin`, `crypto.odin`, `config.odin`, `ssh.odin` — replace ~30 scattered `fmt.printf("Error ...")` calls with leveled logging for consistent stderr routing and source locations.
26. Adopt `core:log` across `db.odin`, `crypto.odin`, `config.odin`, `ssh.odin` — replace ~30 scattered `fmt.printf("Error ...")` calls with leveled logging for consistent stderr routing and source locations.
## Double-check AI output
@@ -83,7 +83,7 @@
- [ ] scan.odin
- [ ] scan_test.odin
- [ ] sodium.odin
- [ ] sqlite/sqlite.odin
- [x] sqlite/sqlite.odin
- [ ] ssh.odin
- [ ] ssh_test.odin
- [ ] table.odin

164
db.odin
View File

@@ -31,8 +31,7 @@ SyncError :: enum {
}
Db :: struct {
// Pointer to the sqlite db
db: ^rawptr,
conn: sqlite.Db,
cfg: Config,
changed: bool,
arena: mem.Dynamic_Arena,
@@ -57,47 +56,47 @@ delete_envfile :: proc(f: ^EnvFile) {
delete(f.contents)
}
db_open :: proc(cfg_path: string) -> (database: Db, ok: bool) {
database = db_init() or_return
database.cfg = load_config(cfg_path, db_allocator(&database)) or_return
db_open :: proc(cfg_path: string) -> (db: Db, ok: bool) {
db = db_init() or_return
db.cfg = load_config(cfg_path, db_allocator(&db)) or_return
// TODO: Use different allocators?
data_path := data_path(database.cfg.config_path, context.temp_allocator)
data_path := data_path(db.cfg.config_path, context.temp_allocator)
if os.exists(data_path) {
if ok = db_restore_from_encrypted(&database, data_path); !ok {
sqlite.close(database.db)
return database, false
if ok = db_restore_from_encrypted(&db, data_path); !ok {
sqlite.close(db.conn)
return db, false
}
} else {
// DB was created
database.changed = true
db.changed = true
}
return database, true
return db, true
}
// Creates a database an allocator and fresh, empty table, with zero encryption.
// In production, you most likely want to use `db_open`.
db_init :: proc() -> (database: Db, ok: bool) {
db: ^rawptr
rc := sqlite.open(":memory:", &db)
db_init :: proc() -> (db: Db, ok: bool) {
conn: sqlite.Db
rc := sqlite.open(":memory:", &conn)
if rc != sqlite.OK {
fmt.printf("Error opening in-memory database: %s\n", sqlite.db_errmsg(db))
fmt.printf("Error opening in-memory database: %s\n", sqlite.db_errmsg(conn))
return
}
create_sql: cstring = "CREATE TABLE IF NOT EXISTS envr_env_files (path TEXT PRIMARY KEY NOT NULL, remotes TEXT, sha256 TEXT NOT NULL, contents TEXT NOT NULL)"
rc = sqlite.db_exec(db, create_sql, nil, nil, nil)
rc = sqlite.db_exec(conn, create_sql, nil, nil, nil)
if rc != sqlite.OK {
fmt.printf("Error creating table: %s\n", sqlite.db_errmsg(db))
sqlite.close(db)
fmt.printf("Error creating table: %s\n", sqlite.db_errmsg(conn))
sqlite.close(conn)
return
}
database.db = db
db.conn = conn
mem.dynamic_arena_init(&database.arena)
mem.dynamic_arena_init(&db.arena)
return database, true
return db, true
}
db_allocator :: proc(db: ^Db) -> mem.Allocator {
@@ -127,43 +126,38 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
}
copy(buf[:len(plaintext)], plaintext)
rc := sqlite.deserialize(
db.db,
"main",
buf,
n,
n,
sqlite.DESERIALIZE_FREEONCLOSE | sqlite.DESERIALIZE_RESIZEABLE,
)
flags: sqlite.DESERIALIZE_FLAGS = {.FREEONCLOSE, .RESIZEABLE}
rc := sqlite.deserialize(db.conn, "main", buf, n, n, flags)
if rc != sqlite.OK {
sqlite.free(buf)
fmt.printf("Error deserializing database: %s\n", sqlite.db_errmsg(db.db))
fmt.printf("Error deserializing database: %s\n", sqlite.db_errmsg(db.conn))
return false
}
return true
}
db_close :: proc(d: ^Db) {
allocator := db_allocator(d)
db_close :: proc(db: ^Db) {
allocator := db_allocator(db)
defer {
sqlite.close(d.db)
sqlite.close(db.conn)
delete_config(&d.cfg, allocator)
delete_config(&db.cfg, allocator)
mem.dynamic_arena_destroy(&d.arena)
mem.dynamic_arena_destroy(&db.arena)
}
if d.changed {
rc := sqlite.db_exec(d.db, "VACUUM", nil, nil, nil)
if db.changed {
rc := sqlite.db_exec(db.conn, "VACUUM", nil, nil, nil)
if rc != sqlite.OK {
fmt.printf("Error vacuuming database: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error vacuuming database: %s\n", sqlite.db_errmsg(db.conn))
return
}
sz: i64
data := sqlite.serialize(d.db, "main", &sz, 0)
data := sqlite.serialize(db.conn, "main", &sz, 0)
if data == nil {
fmt.println("Error: failed to serialize database")
return
@@ -172,14 +166,14 @@ db_close :: proc(d: ^Db) {
sqlite_data := data[:sz]
// TODO: PAss allocator chain
encrypted, enc_ok := encrypt(sqlite_data, d.cfg.Keys[:])
encrypted, enc_ok := encrypt(sqlite_data, db.cfg.Keys[:])
if !enc_ok {
fmt.println("Error: encryption failed")
return
}
data_path := data_path(d.cfg.config_path, allocator)
envr_d := envr_dir(d.cfg.config_path)
data_path := data_path(db.cfg.config_path, allocator)
envr_d := envr_dir(db.cfg.config_path)
os.mkdir_all(envr_d)
write_err := os.write_entire_file(data_path, encrypted)
@@ -189,27 +183,27 @@ db_close :: proc(d: ^Db) {
return
}
d.changed = false
db.changed = false
}
}
// Results will be freed when `db_close` is called.
db_list :: proc(d: ^Db) -> ([]EnvFile, bool) {
stmt: ^rawptr
db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
stmt: sqlite.Stmt
rc := sqlite.prepare_v2(
d.db,
db.conn,
"SELECT path, remotes, sha256, contents FROM envr_env_files",
-1,
&stmt,
nil,
)
if rc != sqlite.OK {
fmt.printf("Error preparing query: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error preparing query: %s\n", sqlite.db_errmsg(db.conn))
return []EnvFile{}, false
}
defer sqlite.finalize(stmt)
allocator := db_allocator(d)
allocator := db_allocator(db)
results := make([dynamic]EnvFile, 0, 10, allocator)
for {
@@ -218,7 +212,7 @@ db_list :: proc(d: ^Db) -> ([]EnvFile, bool) {
break
}
if rc != sqlite.ROW {
fmt.printf("Error stepping query: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error stepping query: %s\n", sqlite.db_errmsg(db.conn))
#no_bounds_check return results[:], false
}
@@ -245,7 +239,7 @@ db_list :: proc(d: ^Db) -> ([]EnvFile, bool) {
}
// TODO: Should we use context.temp_allocator for proc scoped lifetimes?
db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
remotes_json, marshal_err := json.marshal(file.Remotes, allocator = context.temp_allocator)
if marshal_err != nil {
fmt.printf("Error marshaling remotes: %v\n", marshal_err)
@@ -255,10 +249,10 @@ db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
sql: cstring =
"INSERT OR REPLACE INTO " +
"envr_env_files (path, remotes, sha256, contents) VALUES (?, ?, ?, ?)"
stmt: ^rawptr
rc := sqlite.prepare_v2(d.db, sql, -1, &stmt, nil)
stmt: sqlite.Stmt
rc := sqlite.prepare_v2(db.conn, sql, -1, &stmt, nil)
if rc != sqlite.OK {
fmt.printf("Error preparing insert: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error preparing insert: %s\n", sqlite.db_errmsg(db.conn))
return false
}
defer sqlite.finalize(stmt)
@@ -268,7 +262,7 @@ db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
defer delete(cpath)
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
if rc != sqlite.OK {
fmt.printf("Error binding path: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error binding path: %s\n", sqlite.db_errmsg(db.conn))
return false
}
@@ -276,7 +270,7 @@ db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
defer delete(cremotes)
rc = sqlite.bind_text(stmt, 2, cremotes, -1, nil)
if rc != sqlite.OK {
fmt.printf("Error binding remotes: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error binding remotes: %s\n", sqlite.db_errmsg(db.conn))
return false
}
@@ -284,7 +278,7 @@ db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
defer delete(csha)
rc = sqlite.bind_text(stmt, 3, csha, -1, nil)
if rc != sqlite.OK {
fmt.printf("Error binding sha256: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error binding sha256: %s\n", sqlite.db_errmsg(db.conn))
return false
}
@@ -292,38 +286,38 @@ db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
defer delete(ccontents)
rc = sqlite.bind_text(stmt, 4, ccontents, -1, nil)
if rc != sqlite.OK {
fmt.printf("Error binding contents: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error binding contents: %s\n", sqlite.db_errmsg(db.conn))
return false
}
rc = sqlite.step(stmt)
if rc != sqlite.DONE {
fmt.printf("Error inserting: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error inserting: %s\n", sqlite.db_errmsg(db.conn))
return false
}
d.changed = true
db.changed = true
return true
}
// Result will be freed when `db_close` is called.
db_fetch :: proc(d: ^Db, path: string) -> (EnvFile, bool) {
db_fetch :: proc(db: ^Db, path: string) -> (EnvFile, bool) {
sql: cstring = "SELECT path, remotes, sha256, contents FROM envr_env_files WHERE path = ?"
stmt: ^rawptr
rc := sqlite.prepare_v2(d.db, sql, -1, &stmt, nil)
stmt: sqlite.Stmt
rc := sqlite.prepare_v2(db.conn, sql, -1, &stmt, nil)
if rc != sqlite.OK {
fmt.printf("Error preparing fetch: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error preparing fetch: %s\n", sqlite.db_errmsg(db.conn))
return EnvFile{}, false
}
defer sqlite.finalize(stmt)
allocator := db_allocator(d)
allocator := db_allocator(db)
cpath := to_cstring(path, allocator)
defer delete(cpath, allocator)
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
if rc != sqlite.OK {
fmt.printf("Error binding path: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error binding path: %s\n", sqlite.db_errmsg(db.conn))
return EnvFile{}, false
}
rc = sqlite.step(stmt)
@@ -332,7 +326,7 @@ db_fetch :: proc(d: ^Db, path: string) -> (EnvFile, bool) {
return EnvFile{}, false
}
if rc != sqlite.ROW {
fmt.printf("Error fetching: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error fetching: %s\n", sqlite.db_errmsg(db.conn))
return EnvFile{}, false
}
@@ -354,12 +348,12 @@ db_fetch :: proc(d: ^Db, path: string) -> (EnvFile, bool) {
true
}
db_delete :: proc(d: ^Db, path: string) -> bool {
db_delete :: proc(db: ^Db, path: string) -> bool {
sql: cstring = "DELETE FROM envr_env_files WHERE path = ?"
stmt: ^rawptr
rc := sqlite.prepare_v2(d.db, sql, -1, &stmt, nil)
stmt: sqlite.Stmt
rc := sqlite.prepare_v2(db.conn, sql, -1, &stmt, nil)
if rc != sqlite.OK {
fmt.printf("Error preparing delete: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error preparing delete: %s\n", sqlite.db_errmsg(db.conn))
return false
}
defer sqlite.finalize(stmt)
@@ -368,21 +362,21 @@ db_delete :: proc(d: ^Db, path: string) -> bool {
defer delete(cpath)
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
if rc != sqlite.OK {
fmt.printf("Error binding path: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error binding path: %s\n", sqlite.db_errmsg(db.conn))
return false
}
rc = sqlite.step(stmt)
if rc != sqlite.DONE {
fmt.printf("Error deleting: %s\n", sqlite.db_errmsg(d.db))
fmt.printf("Error deleting: %s\n", sqlite.db_errmsg(db.conn))
return false
}
if sqlite.changes(d.db) == 0 {
if sqlite.changes(db.conn) == 0 {
fmt.printf("No file found with path: %s\n", path)
return false
}
d.changed = true
db.changed = true
return true
}
@@ -420,13 +414,13 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
}
// Reconciles `f` with the filesystem and persists changes to the database.
db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
allocator := db_allocator(d)
db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
allocator := db_allocator(db)
result: SyncFlag = {}
old_path := f.Path
if !os.exists(f.Dir) {
moved, err := try_move_dir(d, f, allocator)
moved, err := try_move_dir(db, f, allocator)
if !moved {
return {}, err
}
@@ -440,7 +434,7 @@ db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
return result, .WriteFailed
}
if !db_persist(d, f, old_path) {
if !db_persist(db, f, old_path) {
return result, .DbFailed
}
return result + {.Restored}, .None
@@ -461,7 +455,7 @@ db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
current_sha := string(hex_bytes)
if current_sha == f.Sha256 {
if !db_persist(d, f, old_path) {
if !db_persist(db, f, old_path) {
return result, .DbFailed
}
return result, .None
@@ -469,23 +463,23 @@ db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
f.contents = string(data)
f.Sha256 = current_sha
if !db_persist(d, f, old_path) {
if !db_persist(db, f, old_path) {
return result, .DbFailed
}
return result + {.BackedUp}, .None
}
db_persist :: proc(d: ^Db, f: ^EnvFile, old_path: string) -> bool {
db_persist :: proc(db: ^Db, f: ^EnvFile, old_path: string) -> bool {
if f.Path != old_path {
if !db_delete(d, old_path) {
if !db_delete(db, old_path) {
return false
}
}
return db_insert(d, f^)
return db_insert(db, f^)
}
try_move_dir :: proc(d: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, SyncError) {
roots, ok := find_git_roots(d.cfg)
try_move_dir :: proc(db: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, SyncError) {
roots, ok := find_git_roots(db.cfg)
if !ok {
return false, .GitRootFailed
}

View File

@@ -165,7 +165,7 @@ test_decrypt_then_deserialize_sqlite :: proc(t: ^testing.T) {
}
defer delete(plaintext)
mem_db: ^rawptr
mem_db: sqlite.Db
rc := sqlite.open(":memory:", &mem_db)
testing.expectf(t, rc == sqlite.OK, "failed to open in-memory db")
if rc != sqlite.OK {
@@ -179,14 +179,7 @@ test_decrypt_then_deserialize_sqlite :: proc(t: ^testing.T) {
if buf == nil do return
copy(buf[:len(plaintext)], plaintext)
rc = sqlite.deserialize(
mem_db,
"main",
buf,
n,
n,
sqlite.DESERIALIZE_FREEONCLOSE | sqlite.DESERIALIZE_RESIZEABLE,
)
rc = sqlite.deserialize(mem_db, "main", buf, n, n, {.FREEONCLOSE, .RESIZEABLE})
testing.expect(t, rc == sqlite.OK, "deserialize should succeed")
if rc != sqlite.OK {
sqlite.free(buf)
@@ -194,7 +187,7 @@ test_decrypt_then_deserialize_sqlite :: proc(t: ^testing.T) {
}
sql: cstring = "SELECT path FROM envr_env_files"
stmt: ^rawptr
stmt: sqlite.Stmt
rc = sqlite.prepare_v2(mem_db, sql, -1, &stmt, nil)
testing.expect(t, rc == sqlite.OK, "prepare failed")
if rc != sqlite.OK {

View File

@@ -27,10 +27,10 @@ make_test_env_file :: proc(path, sha, contents: string, remotes: []string = {})
@(test)
test_db_insert_and_fetch :: proc(t: ^testing.T) {
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
if !ok do return
defer db_close(&d)
defer db_close(&db)
path := "/project/.env"
sha := "abc123"
@@ -39,9 +39,9 @@ test_db_insert_and_fetch :: proc(t: ^testing.T) {
f := make_test_env_file(path, sha, contents, []string{"git@github.com:user/repo.git"})
defer delete(f.Remotes)
testing.expect(t, db_insert(&d, f), "insert should succeed")
testing.expect(t, db_insert(&db, f), "insert should succeed")
fetched, fetch_ok := db_fetch(&d, "/project/.env")
fetched, fetch_ok := db_fetch(&db, "/project/.env")
// defer delete_envfile(&fetched)
testing.expect(t, fetch_ok, "fetch should succeed")
if !fetch_ok do return
@@ -55,35 +55,35 @@ test_db_insert_and_fetch :: proc(t: ^testing.T) {
@(test)
test_db_fetch_missing :: proc(t: ^testing.T) {
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
if !ok do return
defer db_close(&d)
defer db_close(&db)
_, fetch_ok := db_fetch(&d, "/nonexistent/.env")
_, fetch_ok := db_fetch(&db, "/nonexistent/.env")
testing.expect(t, !fetch_ok, "fetch missing should return false")
}
@(test)
test_db_insert_or_replace :: proc(t: ^testing.T) {
d, ok := db_init()
defer db_close(&d)
db, ok := db_init()
defer db_close(&db)
testing.expect(t, ok, "failed to create test db")
f1 := make_test_env_file("/project/.env", "sha1", "KEY=old")
defer delete(f1.Remotes)
testing.expect(t, db_insert(&d, f1), "first insert should succeed")
testing.expect(t, db_insert(&db, f1), "first insert should succeed")
f2 := make_test_env_file("/project/.env", "sha2", "KEY=new")
defer delete(f2.Remotes)
testing.expect(t, db_insert(&d, f2), "second insert should succeed")
testing.expect(t, db_insert(&db, f2), "second insert should succeed")
results, list_ok := db_list(&d)
results, list_ok := db_list(&db)
testing.expect(t, list_ok, "list should succeed")
testing.expect(t, len(results) == 1, "should have 1 row, not 2")
fetched, fetch_ok := db_fetch(&d, "/project/.env")
fetched, fetch_ok := db_fetch(&db, "/project/.env")
testing.expect(t, fetch_ok, "fetch should succeed")
if !fetch_ok do return
// defer delete_envfile(&fetched)
@@ -94,36 +94,36 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
@(test)
test_db_delete_existing :: proc(t: ^testing.T) {
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
if !ok do return
defer db_close(&d)
defer db_close(&db)
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.Remotes)
db_insert(&d, f)
db_insert(&db, f)
testing.expect(t, db_delete(&d, "/project/.env"), "delete should return true")
testing.expect(t, db_delete(&db, "/project/.env"), "delete should return true")
_, fetch_ok := db_fetch(&d, "/project/.env")
_, fetch_ok := db_fetch(&db, "/project/.env")
testing.expect(t, !fetch_ok, "row should be gone after delete")
}
@(test)
test_db_delete_missing :: proc(t: ^testing.T) {
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
if !ok do return
defer db_close(&d)
defer db_close(&db)
testing.expect(t, !db_delete(&d, "/nonexistent/.env"), "delete missing should return false")
testing.expect(t, !db_delete(&db, "/nonexistent/.env"), "delete missing should return false")
}
@(test)
test_db_list_multiple :: proc(t: ^testing.T) {
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
defer db_close(&d)
defer db_close(&db)
f1 := make_test_env_file("/proj1/.env", "sha1", "A=1", []string{"git@github.com:a/repo.git"})
defer delete(f1.Remotes)
@@ -131,11 +131,11 @@ test_db_list_multiple :: proc(t: ^testing.T) {
defer delete(f2.Remotes)
f3 := make_test_env_file("/proj3/.env", "sha3", "C=3")
db_insert(&d, f1)
db_insert(&d, f2)
db_insert(&d, f3)
db_insert(&db, f1)
db_insert(&db, f2)
db_insert(&db, f3)
results, list_ok := db_list(&d)
results, list_ok := db_list(&db)
testing.expect(t, list_ok, "list should succeed")
testing.expect_value(t, len(results), 3)
@@ -143,60 +143,60 @@ test_db_list_multiple :: proc(t: ^testing.T) {
@(test)
test_db_list_empty :: proc(t: ^testing.T) {
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
defer db_close(&d)
defer db_close(&db)
results, list_ok := db_list(&d)
results, list_ok := db_list(&db)
testing.expect(t, list_ok, "list should succeed on empty db")
testing.expect(t, len(results) == 0, "should have 0 rows")
}
@(test)
test_db_insert_sets_changed :: proc(t: ^testing.T) {
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
if !ok do return
defer db_close(&d)
defer db_close(&db)
testing.expect(t, !d.changed, "changed should start false")
testing.expect(t, !db.changed, "changed should start false")
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.Remotes)
db_insert(&d, f)
db_insert(&db, f)
testing.expect(t, d.changed, "changed should be true after insert")
testing.expect(t, db.changed, "changed should be true after insert")
}
@(test)
test_db_delete_sets_changed :: proc(t: ^testing.T) {
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
if !ok do return
defer db_close(&d)
defer db_close(&db)
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.Remotes)
db_insert(&d, f)
d.changed = false
db_insert(&db, f)
db.changed = false
db_delete(&d, "/project/.env")
testing.expect(t, d.changed, "changed should be true after delete")
db_delete(&db, "/project/.env")
testing.expect(t, db.changed, "changed should be true after delete")
}
@(test)
test_db_serialize :: proc(t: ^testing.T) {
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
if !ok do return
defer db_close(&d)
defer db_close(&db)
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.Remotes)
db_insert(&d, f)
db_insert(&db, f)
sz: i64
data := sqlite.serialize(d.db, "main", &sz, 0)
data := sqlite.serialize(db.conn, "main", &sz, 0)
testing.expect(t, data != nil, "serialize should return non-nil")
if data == nil do return
defer sqlite.free(data)
@@ -439,15 +439,15 @@ test_db_sync_noop :: proc(t: ^testing.T) {
hex_bytes, _ := hex.encode(digest, context.temp_allocator)
sha := string(hex_bytes)
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
defer db_close(&d)
defer db_close(&db)
f := make_test_env_file(env_path, sha, content)
f.Dir = base
db_insert(&d, f)
db_insert(&db, f)
result, sync_err := db_sync(&d, &f)
result, sync_err := db_sync(&db, &f)
testing.expect(t, sync_err == .None, "sync should not error")
testing.expect(t, result == {}, "should be noop")
}
@@ -463,15 +463,15 @@ test_db_sync_backed_up :: proc(t: ^testing.T) {
write_err := os.write_entire_file(env_path, transmute([]u8)changed_content)
testing.expect(t, write_err == nil, "should write .env file")
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
defer db_close(&d)
defer db_close(&db)
f := make_test_env_file(env_path, "old_sha", "KEY=original")
f.Dir = base
db_insert(&d, f)
db_insert(&db, f)
result, sync_err := db_sync(&d, &f)
result, sync_err := db_sync(&db, &f)
testing.expect(t, sync_err == .None, "sync should not error")
testing.expect(t, .BackedUp in result, "should be backed up")
}
@@ -484,16 +484,16 @@ test_db_sync_restored :: proc(t: ^testing.T) {
env_path := fmt.tprintf("%s/.env", base)
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
defer db_close(&d)
defer db_close(&db)
f := make_test_env_file(env_path, "some_sha", "SECRET=value")
f.Dir = base
defer delete(f.Remotes)
db_insert(&d, f)
db_insert(&db, f)
result, err := db_sync(&d, &f)
result, err := db_sync(&db, &f)
testing.expect(t, err == .None, "sync should not error")
testing.expect(t, .Restored in result, "should be restored")
@@ -506,14 +506,14 @@ test_db_sync_restored :: proc(t: ^testing.T) {
@(test)
test_db_sync_dir_missing :: proc(t: ^testing.T) {
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
defer db_close(&d)
defer db_close(&db)
f := make_test_env_file("/nonexistent/path/.env", "sha", "KEY=val")
db_insert(&d, f)
db_insert(&db, f)
result, err := db_sync(&d, &f)
result, err := db_sync(&db, &f)
testing.expect_value(t, err, SyncError.DirMissing)
testing.expect_value(t, result, nil)
}
@@ -533,12 +533,12 @@ test_db_sync_moved :: proc(t: ^testing.T) {
write_err := os.write_entire_file(config_path, transmute([]u8)config_content)
testing.expect(t, write_err == nil, "should write .git/config")
d, ok := db_init()
db, ok := db_init()
testing.expect(t, ok, "failed to create test db")
defer db_close(&d)
defer db_close(&db)
d.cfg.ScanConfig.Include = make([dynamic]string, 0, 1, context.temp_allocator)
append(&d.cfg.ScanConfig.Include, search_root)
db.cfg.ScanConfig.Include = make([dynamic]string, 0, 1, context.temp_allocator)
append(&db.cfg.ScanConfig.Include, search_root)
f := make_test_env_file(
"/old/nonexistent/path/.env",
@@ -546,9 +546,9 @@ test_db_sync_moved :: proc(t: ^testing.T) {
"SECRET=value",
[]string{"git@github.com:user/repo.git"},
)
testing.expect(t, db_insert(&d, f), "insert should succeed")
testing.expect(t, db_insert(&db, f), "insert should succeed")
result, err := db_sync(&d, &f)
result, err := db_sync(&db, &f)
testing.expect(t, err == .None, "sync should not error")
if err != .None do return
testing.expect(t, .DirUpdated in result, "should have DirUpdated flag")
@@ -558,10 +558,10 @@ test_db_sync_moved :: proc(t: ^testing.T) {
testing.expect_value(t, f.Path, expected_path)
testing.expect_value(t, f.Dir, repo_dir)
_, old_exists := db_fetch(&d, "/old/nonexistent/path/.env")
_, old_exists := db_fetch(&db, "/old/nonexistent/path/.env")
testing.expect(t, !old_exists, "old path should be deleted from db")
new_fetched, new_ok := db_fetch(&d, expected_path)
new_fetched, new_ok := db_fetch(&db, expected_path)
testing.expect(t, new_ok, "new path should exist in db")
if new_ok {
testing.expect_value(t, new_fetched.contents, "SECRET=value")

View File

@@ -4,40 +4,48 @@ import "core:c"
foreign import lib "system:sqlite3"
Db :: distinct rawptr
Stmt :: distinct rawptr
// TODO: Use an enum?
OK :: 0
ROW :: 100
DONE :: 101
DESERIALIZE_FREEONCLOSE :: 1
DESERIALIZE_RESIZEABLE :: 2
DESERIALIZE_FLAGS :: bit_set[DESERIALIZE_FLAG]
DESERIALIZE_FLAG :: enum u32 {
FREEONCLOSE = 1,
RESIZEABLE = 2,
}
foreign lib {
@(link_name = "sqlite3_open")
open :: proc(filename: cstring, ppDb: ^^rawptr) -> c.int ---
open :: proc(filename: cstring, ppDb: ^Db) -> c.int ---
@(link_name = "sqlite3_close")
close :: proc(db: ^rawptr) -> c.int ---
close :: proc(db: Db) -> c.int ---
@(link_name = "sqlite3_errmsg")
db_errmsg :: proc(db: ^rawptr) -> cstring ---
db_errmsg :: proc(db: Db) -> cstring ---
@(link_name = "sqlite3_exec")
db_exec :: proc(db: ^rawptr, sql: cstring, callback: rawptr, callback_arg: rawptr, errmsg: ^cstring) -> c.int ---
db_exec :: proc(db: Db, sql: cstring, callback: rawptr, callback_arg: rawptr, errmsg: ^cstring) -> c.int ---
@(link_name = "sqlite3_prepare_v2")
prepare_v2 :: proc(db: ^rawptr, sql: cstring, nByte: c.int, ppStmt: ^^rawptr, pzTail: ^cstring) -> c.int ---
prepare_v2 :: proc(db: Db, sql: cstring, nByte: c.int, ppStmt: ^Stmt, pzTail: ^cstring) -> c.int ---
@(link_name = "sqlite3_step")
step :: proc(stmt: ^rawptr) -> c.int ---
step :: proc(stmt: Stmt) -> c.int ---
@(link_name = "sqlite3_finalize")
finalize :: proc(stmt: ^rawptr) -> c.int ---
finalize :: proc(stmt: Stmt) -> c.int ---
@(link_name = "sqlite3_column_text")
column_text :: proc(stmt: ^rawptr, iCol: c.int) -> cstring ---
column_text :: proc(stmt: Stmt, iCol: c.int) -> cstring ---
@(link_name = "sqlite3_column_bytes")
column_bytes :: proc(stmt: ^rawptr, iCol: c.int) -> c.int ---
column_bytes :: proc(stmt: Stmt, iCol: c.int) -> c.int ---
@(link_name = "sqlite3_bind_text")
bind_text :: proc(stmt: ^rawptr, idx: c.int, val: cstring, n: c.int, destructor: rawptr) -> c.int ---
bind_text :: proc(stmt: Stmt, idx: c.int, val: cstring, n: c.int, destructor: rawptr) -> c.int ---
@(link_name = "sqlite3_changes")
changes :: proc(db: ^rawptr) -> c.int ---
changes :: proc(db: Db) -> c.int ---
@(link_name = "sqlite3_serialize")
serialize :: proc(db: ^rawptr, zSchema: cstring, piSize: ^i64, mFlags: u32) -> [^]u8 ---
serialize :: proc(db: Db, zSchema: cstring, piSize: ^i64, mFlags: u32) -> [^]u8 ---
@(link_name = "sqlite3_deserialize")
deserialize :: proc(db: ^rawptr, zSchema: cstring, pData: [^]u8, szDb: i64, szBuf: i64, mFlags: u32) -> c.int ---
deserialize :: proc(db: Db, zSchema: cstring, pData: [^]u8, szDb: i64, szBuf: i64, mFlags: DESERIALIZE_FLAGS) -> c.int ---
@(link_name = "sqlite3_malloc64")
malloc64 :: proc(n: i64) -> [^]u8 ---
@(link_name = "sqlite3_free")