commit c04c5401c1bdc916afc4eda1b106d003ea64163f
parent e0ac766b313379aa8d7160d517643d1207b9646b
Author: triesap <tyson@radroots.org>
Date: Sun, 2 Aug 2026 18:24:15 +0000
core(storage): add SQLite migration foundation
- open bundled SQLite with safe connection policy
- apply an embedded idempotent Refinery schema migration
- attempt owner-only permissions for file-backed databases
- document the compatible Rusqlite 0.39 dependency pin
Diffstat:
9 files changed, 705 insertions(+), 11 deletions(-)
diff --git a/core/Cargo.lock b/core/Cargo.lock
@@ -3,6 +3,60 @@
version = 4
[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "cc"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+
+[[package]]
name = "displaydoc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -14,6 +68,46 @@ dependencies = [
]
[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+[[package]]
+name = "fastrand"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -23,6 +117,36 @@ dependencies = [
]
[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
name = "icu_collections"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -126,18 +250,81 @@ dependencies = [
]
[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
name = "litemap"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
name = "potential_utf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -147,6 +334,12 @@ dependencies = [
]
[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -165,6 +358,12 @@ dependencies = [
]
[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
name = "radroots-studio-application"
version = "0.1.0-alpha.0"
dependencies = [
@@ -190,12 +389,141 @@ version = "0.1.0-alpha.0"
[[package]]
name = "radroots-studio-storage"
version = "0.1.0-alpha.0"
+dependencies = [
+ "radroots-studio-domain",
+ "refinery",
+ "rusqlite",
+ "tempfile",
+]
[[package]]
name = "radroots-studio-uniffi-bindgen"
version = "0.1.0-alpha.0"
[[package]]
+name = "refinery"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2a344cdb48871e27addeafbbaffab8828cc12cec2b9041119e9bea0c0f551a"
+dependencies = [
+ "refinery-core",
+ "refinery-macros",
+]
+
+[[package]]
+name = "refinery-core"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24eeafd893124f29183dd6afa9137a27e7bef59250223b8b660005279c60aea4"
+dependencies = [
+ "async-trait",
+ "cfg-if",
+ "log",
+ "regex",
+ "rusqlite",
+ "siphasher",
+ "thiserror",
+ "time",
+ "url",
+ "walkdir",
+]
+
+[[package]]
+name = "refinery-macros"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a90cea6d11a9a4e8a85a884b6305461004101b28ca65dd35ec028e009a898e16"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "refinery-core",
+ "regex",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "rsqlite-vfs"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
+dependencies = [
+ "hashbrown",
+ "thiserror",
+]
+
+[[package]]
+name = "rusqlite"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e"
+dependencies = [
+ "bitflags",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "smallvec",
+ "sqlite-wasm-rs",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
name = "secrecy"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -234,12 +562,36 @@ dependencies = [
]
[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
+[[package]]
name = "smallvec"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
[[package]]
+name = "sqlite-wasm-rs"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75"
+dependencies = [
+ "cc",
+ "js-sys",
+ "rsqlite-vfs",
+ "wasm-bindgen",
+]
+
+[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -279,6 +631,69 @@ dependencies = [
]
[[package]]
+name = "tempfile"
+version = "3.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
name = "tinystr"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -313,6 +728,106 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasip2"
+version = "1.0.4+wasi-0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
name = "writeable"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/core/Cargo.toml b/core/Cargo.toml
@@ -24,6 +24,8 @@ all = "deny"
pedantic = "deny"
[workspace.dependencies]
+refinery = { version = "=0.9.2", default-features = false, features = ["rusqlite"] }
+rusqlite = { version = "=0.39.0", features = ["bundled"] }
secrecy = "=0.10.3"
url = "=2.5.8"
zeroize = "=1.9.0"
diff --git a/core/crates/storage/Cargo.toml b/core/crates/storage/Cargo.toml
@@ -6,5 +6,13 @@ rust-version.workspace = true
license.workspace = true
repository.workspace = true
+[dependencies]
+radroots-studio-domain = { path = "../domain" }
+refinery.workspace = true
+rusqlite.workspace = true
+
+[dev-dependencies]
+tempfile = "=3.23.0"
+
[lints]
workspace = true
diff --git a/core/crates/storage/migrations/V1__initialize.sql b/core/crates/storage/migrations/V1__initialize.sql
@@ -0,0 +1,6 @@
+CREATE TABLE application_schema (
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
+ schema_version INTEGER NOT NULL CHECK (schema_version >= 1)
+);
+
+INSERT INTO application_schema (singleton, schema_version) VALUES (1, 1);
diff --git a/core/crates/storage/src/db.rs b/core/crates/storage/src/db.rs
@@ -0,0 +1,161 @@
+use std::path::Path;
+use std::sync::{Mutex, MutexGuard};
+use std::time::Duration;
+
+use radroots_studio_domain::{SafeError, SafeErrorCode, SafeMessage};
+use refinery::embed_migrations;
+use rusqlite::{Connection, OpenFlags};
+
+mod migrations {
+ use super::embed_migrations;
+
+ embed_migrations!("migrations");
+}
+
+pub struct Database {
+ connection: Mutex<Connection>,
+}
+
+impl Database {
+ /// Opens, configures, and migrates a file-backed `SQLite` database.
+ ///
+ /// # Errors
+ ///
+ /// Returns a safe storage error when the file, connection configuration,
+ /// permission update, or migration cannot complete.
+ pub fn open(path: &Path) -> Result<Self, SafeError> {
+ let flags = OpenFlags::SQLITE_OPEN_READ_WRITE
+ | OpenFlags::SQLITE_OPEN_CREATE
+ | OpenFlags::SQLITE_OPEN_NO_MUTEX;
+ let mut connection =
+ Connection::open_with_flags(path, flags).map_err(|_| storage_error())?;
+ configure(&connection)?;
+ migrations::migrations::runner()
+ .run(&mut connection)
+ .map_err(|_| corrupt_storage_error())?;
+ restrict_file_permissions(path)?;
+ Ok(Self {
+ connection: Mutex::new(connection),
+ })
+ }
+
+ /// Opens and migrates an isolated in-memory `SQLite` database.
+ ///
+ /// # Errors
+ ///
+ /// Returns a safe storage error when configuration or migration fails.
+ pub fn in_memory() -> Result<Self, SafeError> {
+ let mut connection = Connection::open_in_memory().map_err(|_| storage_error())?;
+ configure(&connection)?;
+ migrations::migrations::runner()
+ .run(&mut connection)
+ .map_err(|_| corrupt_storage_error())?;
+ Ok(Self {
+ connection: Mutex::new(connection),
+ })
+ }
+
+ /// Returns the highest successfully applied migration version.
+ ///
+ /// # Errors
+ ///
+ /// Returns a safe storage error when migration history cannot be read.
+ pub fn schema_version(&self) -> Result<u32, SafeError> {
+ self.connection()
+ .query_row(
+ "SELECT COALESCE(MAX(version), 0) FROM refinery_schema_history",
+ [],
+ |row| row.get(0),
+ )
+ .map_err(|_| corrupt_storage_error())
+ }
+
+ pub(crate) fn connection(&self) -> MutexGuard<'_, Connection> {
+ self.connection
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner)
+ }
+}
+
+fn configure(connection: &Connection) -> Result<(), SafeError> {
+ connection
+ .pragma_update(None, "foreign_keys", "ON")
+ .and_then(|()| connection.pragma_update(None, "journal_mode", "WAL"))
+ .and_then(|()| connection.busy_timeout(Duration::from_secs(5)))
+ .map_err(|_| storage_error())
+}
+
+#[cfg(unix)]
+fn restrict_file_permissions(path: &Path) -> Result<(), SafeError> {
+ use std::fs;
+ use std::os::unix::fs::PermissionsExt;
+
+ fs::set_permissions(path, fs::Permissions::from_mode(0o600)).map_err(|_| storage_error())
+}
+
+#[cfg(not(unix))]
+fn restrict_file_permissions(_path: &Path) -> Result<(), SafeError> {
+ Ok(())
+}
+
+const fn storage_error() -> SafeError {
+ SafeError::new(
+ SafeErrorCode::StorageUnavailable,
+ SafeMessage::new("The application database is unavailable."),
+ )
+}
+
+const fn corrupt_storage_error() -> SafeError {
+ SafeError::new(
+ SafeErrorCode::StorageCorrupt,
+ SafeMessage::new("The application database could not be read."),
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use std::fs;
+
+ use tempfile::tempdir;
+
+ use super::Database;
+
+ #[test]
+ fn migration_opens_fresh_memory_database_once() {
+ let database = Database::in_memory().expect("open memory database");
+
+ assert_eq!(database.schema_version().expect("schema version"), 1);
+ assert_eq!(database.schema_version().expect("repeat schema version"), 1);
+ }
+
+ #[test]
+ fn migration_persists_schema_version_across_file_reopen() {
+ let directory = tempdir().expect("temporary directory");
+ let path = directory.path().join("studio.sqlite3");
+
+ {
+ let database = Database::open(&path).expect("open file database");
+ assert_eq!(database.schema_version().expect("schema version"), 1);
+ }
+ let reopened = Database::open(&path).expect("reopen file database");
+ assert_eq!(reopened.schema_version().expect("schema version"), 1);
+ assert!(fs::metadata(path).expect("database metadata").len() > 0);
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn migration_attempts_owner_only_database_permissions() {
+ use std::os::unix::fs::PermissionsExt;
+
+ let directory = tempdir().expect("temporary directory");
+ let path = directory.path().join("studio.sqlite3");
+ let _database = Database::open(&path).expect("open file database");
+ let mode = fs::metadata(path)
+ .expect("database metadata")
+ .permissions()
+ .mode()
+ & 0o777;
+
+ assert_eq!(mode, 0o600);
+ }
+}
diff --git a/core/crates/storage/src/lib.rs b/core/crates/storage/src/lib.rs
@@ -1,9 +1,5 @@
#![doc = "Radroots Studio persistence adapters."]
-#[cfg(test)]
-mod tests {
- #[test]
- fn crate_is_available_to_the_workspace() {
- assert_eq!(env!("CARGO_PKG_NAME"), "radroots-studio-storage");
- }
-}
+pub mod db;
+
+pub use db::Database;
diff --git a/docs/adr/0003-account-metadata-and-secret-storage.md b/docs/adr/0003-account-metadata-and-secret-storage.md
@@ -16,6 +16,10 @@ Use a non-secret operation journal because SQLite and the credential store
cannot share an atomic transaction. Publish snapshots only after durable
success. There is no ordinary-file secret fallback.
+Use `refinery 0.9.2` with bundled `rusqlite 0.39.0`. Refinery's supported
+Rusqlite range ends at 0.39, so this compatibility pin replaces the initially
+reviewed 0.40.1 candidate.
+
## Consequences
Startup recovery is mandatory. Add/import and removal need failure injection at
diff --git a/docs/architecture/reference-research.md b/docs/architecture/reference-research.md
@@ -103,7 +103,9 @@ and source audit will be reconciled again before final acceptance.
- `nostr` and `nostr-sdk` `0.44.1`.
- `uniffi` `0.32.0`.
- `keyring` `4.1.6` behind the Radroots `SecretStore`.
-- `rusqlite` `0.40.1` with bundled SQLite.
+- `rusqlite` `0.39.0` with bundled SQLite. Refinery `0.9.2` constrains its
+ supported `rusqlite` range to `<=0.39`, so the originally reviewed `0.40.1`
+ cannot be used in the same migration adapter.
- `refinery` `0.9.2`.
- `secrecy` `0.10.3`.
- `zeroize` `1.9.0`.
diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md
@@ -210,7 +210,7 @@ licenses and primary-source provenance:
- stable `nostr` and `nostr-sdk` `0.44.1`;
- UniFFI `0.32.0`;
- `keyring` `4.1.6`, behind the Radroots `SecretStore`;
-- `rusqlite` `0.40.1` with bundled SQLite;
+- `rusqlite` `0.39.0` with bundled SQLite, constrained by Refinery `0.9.2`;
- `refinery` `0.9.2`;
- `secrecy` `0.10.3`;
- `zeroize` `1.9.0`;
@@ -293,7 +293,7 @@ re-entry tests, and workspace regression tests.
### RCLD-04: SQLite persistence
-Status: pending.
+Status: in progress.
Scope: checkpoints 15 through 20. Add bundled SQLite and migrations, account and
selection persistence, profile cache, typed account-scoped partitioning,
@@ -474,7 +474,7 @@ handoff commit sequence.
### RCLD-04
-- [ ] 15. Create SQLite storage crate and migration runner.
+- [x] 15. Create SQLite storage crate and migration runner.
- [ ] 16. Implement account and selected-account persistence.
- [ ] 17. Implement profile cache persistence.
- [ ] 18. Implement typed account-scoped namespace persistence.