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. 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. 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. 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 ## Double-check AI output
@@ -83,7 +83,7 @@
- [ ] scan.odin - [ ] scan.odin
- [ ] scan_test.odin - [ ] scan_test.odin
- [ ] sodium.odin - [ ] sodium.odin
- [ ] sqlite/sqlite.odin - [x] sqlite/sqlite.odin
- [ ] ssh.odin - [ ] ssh.odin
- [ ] ssh_test.odin - [ ] ssh_test.odin
- [ ] table.odin - [ ] table.odin

164
db.odin
View File

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

View File

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

View File

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

View File

@@ -4,40 +4,48 @@ import "core:c"
foreign import lib "system:sqlite3" foreign import lib "system:sqlite3"
Db :: distinct rawptr
Stmt :: distinct rawptr
// TODO: Use an enum?
OK :: 0 OK :: 0
ROW :: 100 ROW :: 100
DONE :: 101 DONE :: 101
DESERIALIZE_FREEONCLOSE :: 1
DESERIALIZE_RESIZEABLE :: 2 DESERIALIZE_FLAGS :: bit_set[DESERIALIZE_FLAG]
DESERIALIZE_FLAG :: enum u32 {
FREEONCLOSE = 1,
RESIZEABLE = 2,
}
foreign lib { foreign lib {
@(link_name = "sqlite3_open") @(link_name = "sqlite3_open")
open :: proc(filename: cstring, ppDb: ^^rawptr) -> c.int --- open :: proc(filename: cstring, ppDb: ^Db) -> c.int ---
@(link_name = "sqlite3_close") @(link_name = "sqlite3_close")
close :: proc(db: ^rawptr) -> c.int --- close :: proc(db: Db) -> c.int ---
@(link_name = "sqlite3_errmsg") @(link_name = "sqlite3_errmsg")
db_errmsg :: proc(db: ^rawptr) -> cstring --- db_errmsg :: proc(db: Db) -> cstring ---
@(link_name = "sqlite3_exec") @(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") @(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") @(link_name = "sqlite3_step")
step :: proc(stmt: ^rawptr) -> c.int --- step :: proc(stmt: Stmt) -> c.int ---
@(link_name = "sqlite3_finalize") @(link_name = "sqlite3_finalize")
finalize :: proc(stmt: ^rawptr) -> c.int --- finalize :: proc(stmt: Stmt) -> c.int ---
@(link_name = "sqlite3_column_text") @(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") @(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") @(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") @(link_name = "sqlite3_changes")
changes :: proc(db: ^rawptr) -> c.int --- changes :: proc(db: Db) -> c.int ---
@(link_name = "sqlite3_serialize") @(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") @(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") @(link_name = "sqlite3_malloc64")
malloc64 :: proc(n: i64) -> [^]u8 --- malloc64 :: proc(n: i64) -> [^]u8 ---
@(link_name = "sqlite3_free") @(link_name = "sqlite3_free")