snm_fdbcli/
lib.rs

1use foundationdb::{Database, FdbResult, RangeOption, Transaction};
2use foundationdb::directory::{DirectoryLayer, Directory, DirectoryOutput, DirectoryError};
3use foundationdb::tuple::{pack, unpack};
4use std::env;
5
6pub const ENV_DB_PATH: &str = "SNM_FDBCLI_DB_PATH";
7
8/// Connect to FDB using env var or default cluster file.
9///
10/// - If `SNM_FDBCLI_DB_PATH` is set, it's treated as a *cluster file path*.
11///   Example:
12///     export SNM_FDBCLI_DB_PATH=/usr/local/etc/foundationdb/fdb.cluster
13///
14/// - Otherwise `Database::default()` is used.
15pub fn connect_db() -> FdbResult<Database> {
16    if let Ok(path) = env::var(ENV_DB_PATH) {
17        Database::from_path(&path)
18    } else {
19        Database::default()
20    }
21}
22
23/// Create or open the four core spaces under `srotas/*`.
24///
25/// Returns (users_dir, logins_dir, orders_dir, wallets_dir)
26pub async fn create_spaces(
27    trx: &Transaction,
28) -> (
29    DirectoryOutput,
30    DirectoryOutput,
31    DirectoryOutput,
32    DirectoryOutput,
33) {
34    let dir_layer = DirectoryLayer::default();
35
36    let users_path   = vec!["srotas".to_string(), "users".to_string()];
37    let logins_path  = vec!["srotas".to_string(), "logins".to_string()];
38    let orders_path  = vec!["srotas".to_string(), "orders".to_string()];
39    let wallets_path = vec!["srotas".to_string(), "wallets".to_string()];
40
41    let users_dir = dir_layer
42        .create_or_open(trx, &users_path, None, None)
43        .await
44        .expect("create/open srotas/users failed");
45
46    let logins_dir = dir_layer
47        .create_or_open(trx, &logins_path, None, None)
48        .await
49        .expect("create/open srotas/logins failed");
50
51    let orders_dir = dir_layer
52        .create_or_open(trx, &orders_path, None, None)
53        .await
54        .expect("create/open srotas/orders failed");
55
56    let wallets_dir = dir_layer
57        .create_or_open(trx, &wallets_path, None, None)
58        .await
59        .expect("create/open srotas/wallets failed");
60
61    (users_dir, logins_dir, orders_dir, wallets_dir)
62}
63
64/// Open existing spaces (fails if they don't exist).
65pub async fn open_spaces(
66    trx: &Transaction,
67) -> (
68    DirectoryOutput,
69    DirectoryOutput,
70    DirectoryOutput,
71    DirectoryOutput,
72) {
73    let dir_layer = DirectoryLayer::default();
74
75    let users_path   = vec!["srotas".to_string(), "users".to_string()];
76    let logins_path  = vec!["srotas".to_string(), "logins".to_string()];
77    let orders_path  = vec!["srotas".to_string(), "orders".to_string()];
78    let wallets_path = vec!["srotas".to_string(), "wallets".to_string()];
79
80    let users_dir = dir_layer
81        .open(trx, &users_path, None)
82        .await
83        .expect("open srotas/users failed");
84    let logins_dir = dir_layer
85        .open(trx, &logins_path, None)
86        .await
87        .expect("open srotas/logins failed");
88    let orders_dir = dir_layer
89        .open(trx, &orders_path, None)
90        .await
91        .expect("open srotas/orders failed");
92    let wallets_dir = dir_layer
93        .open(trx, &wallets_path, None)
94        .await
95        .expect("open srotas/wallets failed");
96
97    (users_dir, logins_dir, orders_dir, wallets_dir)
98}
99
100/// Build a prefix range for "all keys starting with this user's id" in a subspace.
101pub fn prefix_range_for_user(
102    dir: &DirectoryOutput,
103    user_id: &str,
104) -> (Vec<u8>, Vec<u8>) {
105    let prefix = dir
106        .pack(&(user_id,))
107        .expect("pack user prefix");
108    let mut end = prefix.clone();
109    end.push(0xFF); // simple prefix upper-bound
110
111    (prefix, end)
112}
113
114/// Dump an entire directory (bounded by `limit`).
115pub async fn dump_dir(
116    trx: &Transaction,
117    dir: &DirectoryOutput,
118    limit: i32,
119) -> FdbResult<()> {
120    let (begin, end) = dir.range().expect("dir.range()");
121    let range = RangeOption::from((begin.as_slice(), end.as_slice()));
122    let kvs = trx
123        .get_range(&range, limit.try_into().unwrap(), false)
124        .await?;
125
126    if kvs.is_empty() {
127        println!("  (empty)");
128    } else {
129        for kv in kvs.iter() {
130            println!(
131                "  key = {:?}, value = {}",
132                kv.key(),
133                String::from_utf8_lossy(kv.value())
134            );
135        }
136    }
137
138    Ok(())
139}
140
141// =====================================================================
142// Generic directory + tuple helpers (for dynamic REPL commands)
143// =====================================================================
144
145/// Generic: create or open a directory like ["srotas"], ["srotas","users"]
146pub async fn dir_create(
147    trx: &Transaction,
148    path: &[&str],
149) -> Result<DirectoryOutput, DirectoryError> {
150    let layer = DirectoryLayer::default();
151    let vec_path: Vec<String> = path.iter().map(|s| s.to_string()).collect();
152    let out = layer.create_or_open(trx, &vec_path, None, None).await?;
153    Ok(out)
154}
155
156/// Generic: open existing directory; `path` may be empty for root dir layer.
157pub async fn dir_open(
158    trx: &Transaction,
159    path: &[&str],
160) -> Result<DirectoryOutput, DirectoryError> {
161    let layer = DirectoryLayer::default();
162    let vec_path: Vec<String> = path.iter().map(|s| s.to_string()).collect();
163    let out = layer.open(trx, &vec_path, None).await?;
164    Ok(out)
165}
166
167/// List child directories under `path` (or root if `path` is empty).
168pub async fn dir_list(
169    trx: &Transaction,
170    path: &[&str],
171) -> Result<Vec<String>, DirectoryError> {
172    let layer = DirectoryLayer::default();
173    let vec_path: Vec<String> = path.iter().map(|s| s.to_string()).collect();
174    let children = layer.list(trx, &vec_path).await?;
175    Ok(children)
176}
177
178// ---- simple tuple support for REPL prefix / pack / unpack ----
179
180/// Very simple tuple parser: expects "(value)" and uses it as a single-string tuple.
181/// You can extend this later to handle multiple elements, ints, etc.
182fn parse_simple_tuple_str(tuple_str: &str) -> Result<String, String> {
183    let s = tuple_str.trim();
184    if !s.starts_with('(') || !s.ends_with(')') {
185        return Err(format!("Expected (value), got: {}", s));
186    }
187    let inner = &s[1..s.len() - 1]; // strip '(' and ')'
188    Ok(inner.trim().to_string())
189}
190
191/// Generic prefix range: given a directory and a tuple string like "(user-1)",
192/// build [begin, end) so you can scan by prefix.
193pub fn tuple_prefix_range(
194    dir: &DirectoryOutput,
195    tuple_str: &str,
196) -> Result<(Vec<u8>, Vec<u8>), String> {
197    let v = parse_simple_tuple_str(tuple_str)?;
198
199    let prefix = dir
200        .pack(&(v.as_str(),))
201        .map_err(|e| format!("pack error: {:?}", e))?;
202    let mut end = prefix.clone();
203    end.push(0xFF);
204
205    Ok((prefix, end))
206}
207
208/// Build a *single key* from a tuple string, e.g. "(user-1)".
209pub fn tuple_key_from_string(
210    dir: &DirectoryOutput,
211    tuple_str: &str,
212) -> Result<Vec<u8>, String> {
213    let v = parse_simple_tuple_str(tuple_str)?;
214    let key = dir
215        .pack(&(v.as_str(),))
216        .map_err(|e| format!("pack error: {:?}", e))?;
217    Ok(key)
218}
219
220/// Pack a tuple string into bytes *without* a directory (global tuple).
221/// Currently supports "(value)" as single string.
222pub fn tuple_pack_from_string(tuple_str: &str) -> Result<Vec<u8>, String> {
223    let v = parse_simple_tuple_str(tuple_str)?;
224    let bytes = pack(&(v.as_str(),));
225    Ok(bytes)
226}
227
228/// Unpack raw bytes into a simple tuple string like "(value)".
229pub fn tuple_unpack_to_string(bytes: &[u8]) -> Result<String, String> {
230    let t: (String,) =
231        unpack(bytes).map_err(|e| format!("unpack error: {:?}", e))?;
232    Ok(format!("({})", t.0))
233}
234
235
236
237
238
239
240
241
242// =====================================================================
243// Tests
244// =====================================================================
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use foundationdb::api::FdbApiBuilder;
250
251    // -----------------------
252    // Pure tuple tests (no FDB server required)
253    // -----------------------
254
255    #[test]
256    fn tuple_pack_and_unpack_roundtrip() {
257        let input = "(alice)";
258        let bytes = tuple_pack_from_string(input).expect("pack should succeed");
259        let out = tuple_unpack_to_string(&bytes).expect("unpack should succeed");
260        assert_eq!(out, "(alice)");
261    }
262
263    #[test]
264    fn tuple_pack_rejects_invalid_format() {
265        // Missing parentheses
266        assert!(tuple_pack_from_string("alice").is_err());
267        // Bad parentheses
268        assert!(tuple_pack_from_string("(alice").is_err());
269        assert!(tuple_pack_from_string("alice)").is_err());
270    }
271
272    // -----------------------
273    // Single FDB-backed test (requires FDB server)
274    //
275    // Marked #[ignore] so it does NOT run by default.
276    // Run it manually with:
277    //   cargo test -- --ignored
278    // -----------------------
279
280    #[tokio::test]
281    #[ignore]
282    async fn fdb_end_to_end_directory_and_prefix() {
283        // Boot FDB network once for this process
284        let _network = unsafe {
285            FdbApiBuilder::default()
286                .build()
287                .expect("build FDB API")
288                .boot()
289                .expect("boot network")
290        };
291
292        let db = connect_db().expect("connect_db()");
293
294        // ---------- 1) directory create + list ----------
295        {
296            let trx = db.create_trx().expect("create_trx");
297            let path = ["test-root", "sub"];
298            let _dir = dir_create(&trx, &path).await.expect("dir_create");
299            trx.commit().await.expect("commit create");
300        }
301
302        {
303            let trx2 = db.create_trx().expect("create_trx 2");
304            let children = dir_list(&trx2, &["test-root"])
305                .await
306                .expect("dir_list");
307            assert!(
308                children.contains(&"sub".to_string()),
309                "expected 'sub' in children: {:?}",
310                children
311            );
312            // no mutation, but ok to commit
313            trx2.commit().await.expect("commit list");
314        }
315
316        // ---------- 2) tuple_prefix_range sanity on real DirectoryOutput ----------
317        {
318            let trx3 = db.create_trx().expect("create_trx 3");
319            // open the same directory we created above
320            let dir = dir_open(&trx3, &["test-root", "sub"])
321                .await
322                .expect("dir_open for prefix test");
323
324            let (begin, end) = tuple_prefix_range(&dir, "(alice)")
325                .expect("tuple_prefix_range");
326            assert!(!begin.is_empty(), "begin should not be empty");
327            assert!(begin.len() < end.len(), "end should be longer than begin");
328            assert_eq!(end.last().copied(), Some(0xFF), "end should end with 0xFF");
329
330            trx3.commit().await.expect("commit prefix test");
331        }
332    }
333}