ousia 1.3.1

Postgres ORM with native double-entry ledger, graph relations, and atomic money operations for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
#[cfg(feature = "postgres")]
pub mod postgres;

pub mod query;
pub mod record;

#[cfg(feature = "ledger")]
use std::sync::Arc;

use async_trait::async_trait;
pub use query::*;
pub use record::*;
use uuid::Uuid;

use crate::{
    Object,
    edge::query::EdgeQuery,
    error::Error,
    query::{GeoPoint, QueryFilter},
};

/// -----------------------------
/// Adapter contract
/// -----------------------------

#[async_trait]
pub trait UniqueAdapter {
    async fn insert_unique_hashes(
        &self,
        type_name: &str,
        object_id: Uuid,
        hashes: Vec<(String, &str)>,
    ) -> Result<(), Error>;

    async fn delete_unique(&self, hash: &str) -> Result<(), Error>;
    async fn delete_unique_hashes(&self, hashes: Vec<String>) -> Result<(), Error>;

    async fn get_hashes_for_object(&self, object_id: Uuid) -> Result<Vec<String>, Error>;
}

/// Side-table operations for geo-indexed fields. All methods have default
/// "not supported" implementations so any `Adapter` impl gets `GeoAdapter` for
/// free — only adapters that actually want spatial queries (currently only
/// postgres) need to override.
#[async_trait]
pub trait GeoAdapter {
    /// Insert or update `object_geo` rows for the given points. Existing rows
    /// for the same (object_id, field) are overwritten. Rows for fields not
    /// listed here are NOT touched — callers handle removal via
    /// `delete_geo_fields` / `delete_geo_for_object`.
    async fn upsert_geo_points(
        &self,
        _type_name: &str,
        _object_id: Uuid,
        points: Vec<GeoPoint>,
    ) -> Result<(), Error> {
        if points.is_empty() {
            return Ok(());
        }
        Err(Error::Unsupported(
            "geo indexes are not supported by this adapter".to_string(),
        ))
    }

    /// Return `(field, hash)` for every `object_geo` row belonging to the object.
    async fn get_geo_hashes(&self, _object_id: Uuid) -> Result<Vec<(String, String)>, Error> {
        Ok(Vec::new())
    }

    /// Delete specific fields for an object (used when geo fields disappear on update).
    async fn delete_geo_fields(
        &self,
        _object_id: Uuid,
        _fields: Vec<String>,
    ) -> Result<(), Error> {
        Ok(())
    }

    /// Delete every `object_geo` row for the object (used on object delete).
    async fn delete_geo_for_object(&self, _object_id: Uuid) -> Result<(), Error> {
        Ok(())
    }
}

#[async_trait]
pub trait EdgeTraversal {
    async fn fetch_object_from_edge_traversal_internal(
        &self,
        edge_type_name: &str,
        type_name: &str,
        owner: Uuid,
        filters: &[QueryFilter],
        plan: EdgeQuery,
    ) -> Result<Vec<ObjectRecord>, Error>;

    async fn fetch_object_from_edge_reverse_traversal_internal(
        &self,
        edge_type_name: &str,
        type_name: &str,
        owner: Uuid,
        filters: &[QueryFilter],
        plan: EdgeQuery,
    ) -> Result<Vec<ObjectRecord>, Error>;

    // ── Batch traversal (multiple pivots, single query each) ─────────────────

    /// Forward JOIN for multiple source IDs — edges WHERE "from" = ANY(ids) + target objects.
    async fn query_edges_with_targets_batch(
        &self,
        edge_type: &'static str,
        obj_type: &'static str,
        from_ids: &[Uuid],
        obj_filters: &[QueryFilter],
        plan: EdgeQuery,
    ) -> Result<Vec<(EdgeRecord, ObjectRecord)>, Error>;

    /// Reverse JOIN for multiple target IDs — edges WHERE "to" = ANY(ids) + source objects.
    async fn query_reverse_edges_with_sources_batch(
        &self,
        edge_type: &'static str,
        obj_type: &'static str,
        to_ids: &[Uuid],
        obj_filters: &[QueryFilter],
        plan: EdgeQuery,
    ) -> Result<Vec<(EdgeRecord, ObjectRecord)>, Error>;

    /// Forward edge records only for multiple sources — no object JOIN.
    async fn query_edges_batch(
        &self,
        edge_type: &'static str,
        from_ids: &[Uuid],
        plan: EdgeQuery,
    ) -> Result<Vec<EdgeRecord>, Error>;

    /// Reverse edge records only for multiple targets — no object JOIN.
    async fn query_reverse_edges_batch(
        &self,
        edge_type: &'static str,
        to_ids: &[Uuid],
        plan: EdgeQuery,
    ) -> Result<Vec<EdgeRecord>, Error>;

    /// Single pivot, both directions in one UNION query, with objects.
    /// Returns (forward_results, reverse_results).
    async fn query_edges_both_directions_with_objects(
        &self,
        edge_type: &'static str,
        obj_type: &'static str,
        pivot: Uuid,
        obj_filters: &[QueryFilter],
        plan: EdgeQuery,
    ) -> Result<
        (
            Vec<(EdgeRecord, ObjectRecord)>,
            Vec<(EdgeRecord, ObjectRecord)>,
        ),
        Error,
    >;

    /// Single pivot, both directions in one UNION query, edges only.
    /// Returns (forward_edges, reverse_edges).
    async fn query_edges_both_directions(
        &self,
        edge_type: &'static str,
        pivot: Uuid,
        plan: EdgeQuery,
    ) -> Result<(Vec<EdgeRecord>, Vec<EdgeRecord>), Error>;

    /// Count forward edges per source in one GROUP BY query — Vec<(from_id, count)>.
    async fn count_edges_batch(
        &self,
        edge_type: &'static str,
        from_ids: &[Uuid],
        plan: EdgeQuery,
    ) -> Result<Vec<(Uuid, u64)>, Error>;

    /// Count reverse edges per target in one GROUP BY query — Vec<(to_id, count)>.
    async fn count_reverse_edges_batch(
        &self,
        edge_type: &'static str,
        to_ids: &[Uuid],
        plan: EdgeQuery,
    ) -> Result<Vec<(Uuid, u64)>, Error>;
}

#[async_trait]
pub trait Adapter: UniqueAdapter + GeoAdapter + EdgeTraversal + Send + Sync + 'static {
    /* ---------------- OBJECTS ---------------- */
    async fn insert_object(&self, record: ObjectRecord) -> Result<(), Error>;
    async fn fetch_object(
        &self,
        type_name: &'static str,
        id: Uuid,
    ) -> Result<Option<ObjectRecord>, Error>;
    async fn fetch_bulk_objects(
        &self,
        type_name: &'static str,
        ids: Vec<Uuid>,
    ) -> Result<Vec<ObjectRecord>, Error>;
    async fn update_object(&self, record: ObjectRecord) -> Result<(), Error>;

    /// Explicit ownership transfer
    async fn transfer_object(
        &self,
        type_name: &'static str,
        id: Uuid,
        from_owner: Uuid,
        to_owner: Uuid,
    ) -> Result<ObjectRecord, Error>;

    async fn delete_object(
        &self,
        type_name: &'static str,
        id: Uuid,
        owner: Uuid,
    ) -> Result<Option<ObjectRecord>, Error>;

    async fn delete_bulk_objects(
        &self,
        type_name: &'static str,
        ids: Vec<Uuid>,
        owner: Uuid,
    ) -> Result<u64, Error>;

    async fn delete_owned_objects(
        &self,
        type_name: &'static str,
        owner: Uuid,
    ) -> Result<u64, Error>;

    /* ---------------- QUERIES ---------------- */
    /// Fetch ALL objects matching `plan`. Filters by owner.
    async fn find_object(
        &self,
        type_name: &'static str,
        owner: Uuid,
        filters: &[QueryFilter],
    ) -> Result<Option<ObjectRecord>, Error>;

    async fn query_objects(
        &self,
        type_name: &'static str,
        plan: Query,
    ) -> Result<Vec<ObjectRecord>, Error>;

    /// Query objects and return the distance (meters) from each result to
    /// the reference point set by `plan.geo_order`. Requires `geo_order` to
    /// be `Some` — otherwise returns `Error::InvalidQuery`.
    ///
    /// Default impl returns `Error::Unsupported` — only Postgres (with PostGIS)
    /// overrides this.
    async fn query_objects_with_distance(
        &self,
        _type_name: &'static str,
        _plan: Query,
    ) -> Result<Vec<(ObjectRecord, f64)>, Error> {
        Err(Error::Unsupported(
            "query_objects_with_distance requires postgres".to_string(),
        ))
    }

    async fn count_objects(
        &self,
        type_name: &'static str,
        plan: Option<Query>,
    ) -> Result<u64, Error>;

    /// Fetch ALL objects owned by `owner`
    async fn fetch_owned_objects(
        &self,
        type_name: &'static str,
        owner: Uuid,
    ) -> Result<Vec<ObjectRecord>, Error>;

    /// Batch-fetch owned children for multiple parents in one query.
    /// Each returned ObjectRecord's `.owner` field is the parent ID — use it to group.
    async fn fetch_owned_objects_batch(
        &self,
        type_name: &'static str,
        owner_ids: &[Uuid],
    ) -> Result<Vec<ObjectRecord>, Error>;

    /// Fetch a SINGLE owned object (O2O)
    async fn fetch_owned_object(
        &self,
        type_name: &'static str,
        owner: Uuid,
    ) -> Result<Option<ObjectRecord>, Error>;

    // ==================== Union Operations ====================
    async fn fetch_union_object(
        &self,
        a_type_name: &'static str,
        b_type_name: &'static str,
        id: Uuid,
    ) -> Result<Option<ObjectRecord>, Error>;

    async fn fetch_union_objects(
        &self,
        a_type_name: &'static str,
        b_type_name: &'static str,
        id: Vec<Uuid>,
    ) -> Result<Vec<ObjectRecord>, Error>;

    async fn fetch_owned_union_object(
        &self,
        a_type_name: &'static str,
        b_type_name: &'static str,
        owner: Uuid,
    ) -> Result<Option<ObjectRecord>, Error>;

    async fn fetch_owned_union_objects(
        &self,
        a_type_name: &'static str,
        b_type_name: &'static str,
        owner: Uuid,
    ) -> Result<Vec<ObjectRecord>, Error>;

    /* ---------------- EDGES ---------------- */
    async fn insert_edge(&self, record: EdgeRecord) -> Result<(), Error>;
    async fn update_edge(
        &self,
        record: EdgeRecord,
        old_to: Uuid,
        to: Option<Uuid>,
    ) -> Result<(), Error>;
    async fn delete_edge(&self, type_name: &'static str, from: Uuid, to: Uuid)
    -> Result<(), Error>;

    async fn delete_object_edge(&self, type_name: &'static str, from: Uuid) -> Result<(), Error>;

    async fn fetch_edge(
        &self,
        type_name: &'static str,
        from: Uuid,
        to: Uuid,
    ) -> Result<Option<EdgeRecord>, Error>;

    async fn query_edges(
        &self,
        type_name: &'static str,
        owner: Uuid,
        plan: EdgeQuery,
    ) -> Result<Vec<EdgeRecord>, Error>;

    async fn query_reverse_edges(
        &self,
        type_name: &'static str,
        owner_reverse: Uuid,
        plan: EdgeQuery,
    ) -> Result<Vec<EdgeRecord>, Error>;

    /// Single JOIN query: edges WHERE "from" = owner + their target objects.
    async fn query_edges_with_targets(
        &self,
        edge_type: &'static str,
        obj_type: &'static str,
        owner: Uuid,
        obj_filters: &[QueryFilter],
        plan: EdgeQuery,
    ) -> Result<Vec<(EdgeRecord, ObjectRecord)>, Error>;

    /// Single JOIN query: edges WHERE "to" = owner + their source objects.
    async fn query_reverse_edges_with_sources(
        &self,
        edge_type: &'static str,
        obj_type: &'static str,
        owner: Uuid,
        obj_filters: &[QueryFilter],
        plan: EdgeQuery,
    ) -> Result<Vec<(EdgeRecord, ObjectRecord)>, Error>;

    async fn count_edges(
        &self,
        type_name: &'static str,
        owner: Uuid,
        plan: Option<EdgeQuery>,
    ) -> Result<u64, Error>;

    async fn count_reverse_edges(
        &self,
        type_name: &'static str,
        to: Uuid,
        plan: Option<EdgeQuery>,
    ) -> Result<u64, Error>;

    /* ---------------- SEQUENCE ---------------- */
    async fn sequence_value(&self, sq: String) -> u64;
    async fn sequence_next_value(&self, sq: String) -> u64;

    /* ---------------- LEDGER ---------------- */
    #[cfg(feature = "ledger")]
    fn ledger_adapter(&self) -> Option<Arc<dyn ledger::LedgerAdapter>> {
        None // default — adapters opt in
    }
}

impl dyn Adapter {
    pub fn preload_object<'a, T: Object>(&'a self, id: Uuid) -> QueryContext<'a, T> {
        QueryContext::new(self, id)
    }

    pub fn preload_objects<'a, P: Object>(&'a self, query: Query) -> MultiPreloadContext<'a, P> {
        MultiPreloadContext::new(self, query)
    }
}