refactor: Gave db its own allocator.

This commit is contained in:
2026-06-18 11:58:33 -04:00
parent 8d5e50566b
commit b66e905482
5 changed files with 126 additions and 112 deletions

110
db.odin
View File

@@ -5,6 +5,7 @@ import "core:encoding/hex"
import "core:encoding/ini"
import "core:encoding/json"
import "core:fmt"
import "core:mem"
import "core:os"
import "core:path/filepath"
import "core:strings"
@@ -31,6 +32,7 @@ Db :: struct {
db: ^rawptr,
cfg: Config,
changed: bool,
arena: mem.Dynamic_Arena,
}
EnvFile :: struct {
@@ -51,25 +53,15 @@ delete_envfile :: proc(f: ^EnvFile) {
delete(f.contents)
}
db_open :: proc(cfg_path: string) -> (database: Db, ok: bool) {
database.cfg = load_config(cfg_path) or_return
db_open :: proc(cfg_path: string) -> (Db, bool) {
database, ok := db_init()
if !ok {
return database, ok
}
{
db: ^rawptr
rc := sqlite.db_open(":memory:", &db)
if rc != sqlite.OK {
fmt.printf("Error opening in-memory database: %s\n", sqlite.db_errmsg(db))
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)
if rc != sqlite.OK {
fmt.printf("Error creating table: %s\n", sqlite.db_errmsg(db))
sqlite.db_close(db)
return
}
database.db = db
database.cfg, ok = load_config(cfg_path, db_allocator(&database))
if !ok {
return database, ok
}
// TODO: Use different allocators?
@@ -77,7 +69,7 @@ db_open :: proc(cfg_path: string) -> (database: Db, ok: bool) {
if os.exists(data_path) {
if ok = db_restore_from_encrypted(&database, data_path); !ok {
sqlite.db_close(database.db)
return
return database, ok
}
} else {
// DB was created
@@ -87,6 +79,36 @@ db_open :: proc(cfg_path: string) -> (database: Db, ok: bool) {
return database, 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.db_open(":memory:", &db)
if rc != sqlite.OK {
fmt.printf("Error opening in-memory database: %s\n", sqlite.db_errmsg(db))
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)
if rc != sqlite.OK {
fmt.printf("Error creating table: %s\n", sqlite.db_errmsg(db))
sqlite.db_close(db)
return
}
database.db = db
mem.dynamic_arena_init(&database.arena)
return database, true
}
// TODO: Should allocator live on the db?
db_allocator :: proc(db: ^Db) -> mem.Allocator {
return mem.dynamic_arena_allocator(&db.arena)
}
// TODO: use db allocator?
db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
encrypted_data, read_err := os.read_entire_file_from_path(data_path, context.allocator)
defer delete(encrypted_data)
@@ -95,6 +117,7 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
return false
}
// TODO: allow allocator selection?
plaintext, dec_ok := decrypt(encrypted_data, db.cfg.Keys[:])
if !dec_ok {
fmt.println("Error: decryption failed")
@@ -127,9 +150,15 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
return true
}
// TODO: Return error enum?
db_close :: proc(d: ^Db) {
defer sqlite.db_close(d.db)
defer delete_config(&d.cfg)
allocator := db_allocator(d)
defer {
sqlite.db_close(d.db)
delete_config(&d.cfg, allocator)
mem.dynamic_arena_destroy(&d.arena)
}
if d.changed {
rc := sqlite.db_exec(d.db, "VACUUM", nil, nil, nil)
@@ -147,13 +176,15 @@ db_close :: proc(d: ^Db) {
defer sqlite.free(data)
sqlite_data := data[:sz]
// TODO: PAss allocator chain
// FIXME Warning gets printed in tests.
encrypted, enc_ok := encrypt(sqlite_data, d.cfg.Keys[:])
if !enc_ok {
fmt.println("Error: encryption failed")
return
}
data_path := data_path(d.cfg.config_path)
data_path := data_path(d.cfg.config_path, allocator)
envr_d := envr_dir(d.cfg.config_path)
os.mkdir_all(envr_d)
@@ -168,7 +199,8 @@ db_close :: proc(d: ^Db) {
}
}
db_list :: proc(d: ^Db, allocator := context.allocator) -> (results: [dynamic]EnvFile, ok: bool) {
// Results will be freed when `db_close` is called.
db_list :: proc(d: ^Db) -> ([]EnvFile, bool) {
stmt: ^rawptr
rc := sqlite.prepare_v2(
d.db,
@@ -179,18 +211,22 @@ db_list :: proc(d: ^Db, allocator := context.allocator) -> (results: [dynamic]En
)
if rc != sqlite.OK {
fmt.printf("Error preparing query: %s\n", sqlite.db_errmsg(d.db))
return
return []EnvFile{}, false
}
defer sqlite.finalize(stmt)
allocator := db_allocator(d)
// TODO: Is 10 a good size?
results := make([dynamic]EnvFile, 0, 10, allocator)
for {
rc = sqlite.step(stmt)
if rc == sqlite.DONE {
break
}
if rc != sqlite.ROW {
#no_bounds_check if rc != sqlite.ROW {
fmt.printf("Error stepping query: %s\n", sqlite.db_errmsg(d.db))
return
return results[:], false
}
remotes_json := string(sqlite.column_text(stmt, 1))
@@ -212,10 +248,10 @@ db_list :: proc(d: ^Db, allocator := context.allocator) -> (results: [dynamic]En
)
}
ok = true
return
#no_bounds_check return results[:], true
}
// FIXME: Needs to use the allocator
db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
remotes_json, marshal_err := json.marshal(file.Remotes)
if marshal_err != nil {
@@ -278,7 +314,8 @@ db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
return true
}
db_fetch :: proc(d: ^Db, path: string, allocator := context.allocator) -> (EnvFile, bool) {
// Result will be freed when `db_close` is called.
db_fetch :: proc(d: ^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)
@@ -288,8 +325,8 @@ db_fetch :: proc(d: ^Db, path: string, allocator := context.allocator) -> (EnvFi
}
defer sqlite.finalize(stmt)
cpath := to_cstring(path, allocator)
defer delete(cpath, allocator)
// TODO: Use standard allocator + delete here?
cpath := to_cstring(path, context.temp_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))
@@ -305,13 +342,15 @@ db_fetch :: proc(d: ^Db, path: string, allocator := context.allocator) -> (EnvFi
return EnvFile{}, false
}
allocator := db_allocator(d)
remotes_json := string(sqlite.column_text(stmt, 1))
remotes: [dynamic]string = ---
if len(remotes_json) > 0 {
json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
}
file_path := clone_cstring(sqlite.column_text(stmt, 0))
file_path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
return EnvFile {
Path = file_path,
@@ -333,6 +372,7 @@ db_delete :: proc(d: ^Db, path: string) -> bool {
}
defer sqlite.finalize(stmt)
// TODO: Use db allocator here?
cpath := to_cstring(path)
defer delete(cpath)
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
@@ -392,6 +432,7 @@ db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, string) {
}
// If SyncFlag is .BackedUp, Caller is responsible for calling delete on f.contents and f.Sha256
// TODO: Should this use the db allocator?
env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncFlag, string) {
result: SyncFlag = {}
@@ -463,6 +504,7 @@ env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncFlag, str
return result, ""
}
// TODO: Should this use the db allocator?
find_moved_dirs :: proc(d: ^Db, f: ^EnvFile) -> ([dynamic]string, bool) {
roots, roots_ok := find_git_roots(d.cfg)
if !roots_ok {
@@ -480,6 +522,7 @@ find_moved_dirs :: proc(d: ^Db, f: ^EnvFile) -> ([dynamic]string, bool) {
return moved, true
}
// TODO: Should this use the db allocator?
update_dir :: proc(f: ^EnvFile, new_dir: string) {
f.Dir = new_dir
base := filepath.base(f.Path)
@@ -491,6 +534,8 @@ update_dir :: proc(f: ^EnvFile, new_dir: string) {
// Loads the contents of the the file at f.Path into f.contents
//
// Caller is responsible for calling delete on f.contents and f.Sha256
//
// TODO: Should this use the db allocator?
env_file_backup :: proc(f: ^EnvFile) -> bool {
data, read_err := os.read_entire_file_from_path(f.Path, context.allocator)
if read_err != nil {
@@ -520,6 +565,7 @@ shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
return false
}
// TODO: Should this use the db allocator?
get_git_remotes :: proc(dir: string) -> [dynamic]string {
remotes: [dynamic]string
remote_set: map[string]bool