Skip to main content

ankurah_storage_postgres/
lib.rs

1use std::{
2    collections::{hash_map::DefaultHasher, BTreeMap},
3    hash::{Hash, Hasher},
4    sync::{Arc, RwLock},
5    time::Duration,
6};
7
8use ankurah_core::{
9    error::{MutationError, RetrievalError, StateError},
10    property::backend::backend_from_string,
11    storage::{StorageCollection, StorageEngine},
12};
13use ankurah_proto::{Attestation, AttestationSet, Attested, EntityState, EventId, OperationSet, State, StateBuffers};
14
15use futures_util::{pin_mut, TryStreamExt};
16
17mod dump;
18pub mod sql_builder;
19pub mod value;
20
21use value::PGValue;
22
23use ankurah_proto::{Clock, CollectionId, EntityId, Event};
24use async_trait::async_trait;
25use bb8_postgres::{tokio_postgres::NoTls, PostgresConnectionManager};
26use tokio_postgres::{error::SqlState, types::ToSql};
27use tracing::{debug, error, info, warn};
28
29/// Default connection pool size for `Postgres::open()`.
30/// Production applications should configure their own pool via `Postgres::new()`.
31pub const DEFAULT_POOL_SIZE: u32 = 15;
32
33/// Default connection timeout in seconds
34pub const DEFAULT_CONNECTION_TIMEOUT_SECS: u64 = 30;
35
36pub struct Postgres {
37    pool: bb8::Pool<PostgresConnectionManager<NoTls>>,
38}
39
40impl Postgres {
41    pub fn new(pool: bb8::Pool<PostgresConnectionManager<NoTls>>) -> anyhow::Result<Self> { Ok(Self { pool }) }
42
43    pub async fn open(uri: &str) -> anyhow::Result<Self> {
44        let manager = PostgresConnectionManager::new_from_stringlike(uri, NoTls)?;
45        let pool = bb8::Pool::builder()
46            .max_size(DEFAULT_POOL_SIZE)
47            .connection_timeout(Duration::from_secs(DEFAULT_CONNECTION_TIMEOUT_SECS))
48            .build(manager)
49            .await?;
50        Self::new(pool)
51    }
52
53    // TODO: newtype this to `BucketName(&str)` with a constructor that
54    // only accepts a subset of characters.
55    pub fn sane_name(collection: &str) -> bool {
56        for char in collection.chars() {
57            match char {
58                char if char.is_alphanumeric() => {}
59                char if char.is_numeric() => {}
60                '_' | '.' | ':' => {}
61                _ => return false,
62            }
63        }
64
65        true
66    }
67}
68
69/// Compute advisory lock key from a string identifier
70fn advisory_lock_key(identifier: &str) -> i64 {
71    let mut hasher = DefaultHasher::new();
72    identifier.hash(&mut hasher);
73    hasher.finish() as i64
74}
75
76/// Acquire a PostgreSQL advisory lock for DDL operations on a collection
77async fn acquire_ddl_lock(client: &tokio_postgres::Client, collection_id: &str) -> Result<i64, StateError> {
78    let lock_key = advisory_lock_key(&format!("ankurah_ddl:{}", collection_id));
79    debug!("Acquiring advisory lock {} for collection {}", lock_key, collection_id);
80    client.execute("SELECT pg_advisory_lock($1)", &[&lock_key]).await.map_err(|err| {
81        error!("Failed to acquire advisory lock for {}: {:?}", collection_id, err);
82        StateError::DDLError(Box::new(err))
83    })?;
84    Ok(lock_key)
85}
86
87/// Release a PostgreSQL advisory lock
88async fn release_ddl_lock(client: &tokio_postgres::Client, lock_key: i64) -> Result<(), StateError> {
89    debug!("Releasing advisory lock {}", lock_key);
90    client.execute("SELECT pg_advisory_unlock($1)", &[&lock_key]).await.map_err(|err| {
91        error!("Failed to release advisory lock {}: {:?}", lock_key, err);
92        StateError::DDLError(Box::new(err))
93    })?;
94    Ok(())
95}
96
97#[async_trait]
98impl StorageEngine for Postgres {
99    type Value = PGValue;
100
101    async fn collection(&self, collection_id: &CollectionId) -> Result<std::sync::Arc<dyn StorageCollection>, RetrievalError> {
102        if !Postgres::sane_name(collection_id.as_str()) {
103            return Err(RetrievalError::InvalidBucketName);
104        }
105
106        let mut client = self.pool.get().await.map_err(RetrievalError::storage)?;
107
108        // get the current schema from the database
109        let schema = client.query_one("SELECT current_database()", &[]).await.map_err(RetrievalError::storage)?;
110        let schema = schema.get("current_database");
111
112        let bucket = PostgresBucket {
113            pool: self.pool.clone(),
114            schema,
115            collection_id: collection_id.clone(),
116            columns: Arc::new(RwLock::new(Vec::new())),
117            #[cfg(debug_assertions)]
118            last_spilled_predicate: Arc::new(RwLock::new(None)),
119        };
120
121        // Acquire advisory lock to serialize DDL operations for this collection
122        let lock_key = acquire_ddl_lock(&client, collection_id.as_str()).await?;
123
124        // Create tables if they don't exist (protected by advisory lock)
125        let result = async {
126            bucket.create_state_table(&mut client).await?;
127            bucket.create_event_table(&mut client).await?;
128            bucket.rebuild_columns_cache(&mut client).await?;
129            Ok::<_, StateError>(())
130        }
131        .await;
132
133        // Always release the lock, even if DDL failed
134        release_ddl_lock(&client, lock_key).await?;
135
136        result?;
137        Ok(Arc::new(bucket))
138    }
139
140    async fn delete_all_collections(&self) -> Result<bool, MutationError> {
141        let mut client = self.pool.get().await.map_err(|err| MutationError::General(Box::new(err)))?;
142
143        // Get all tables in the public schema
144        let query = r#"
145            SELECT table_name 
146            FROM information_schema.tables 
147            WHERE table_schema = 'public'
148        "#;
149
150        let rows = client.query(query, &[]).await.map_err(|err| MutationError::General(Box::new(err)))?;
151        if rows.is_empty() {
152            return Ok(false);
153        }
154
155        // Start a transaction to drop all tables atomically
156        let transaction = client.transaction().await.map_err(|err| MutationError::General(Box::new(err)))?;
157
158        // Drop each table
159        for row in rows {
160            let table_name: String = row.get("table_name");
161            let drop_query = format!(r#"DROP TABLE IF EXISTS "{}""#, table_name);
162            transaction.execute(&drop_query, &[]).await.map_err(|err| MutationError::General(Box::new(err)))?;
163        }
164
165        // Commit the transaction
166        transaction.commit().await.map_err(|err| MutationError::General(Box::new(err)))?;
167
168        Ok(true)
169    }
170}
171
172#[derive(Clone, Debug)]
173pub struct PostgresColumn {
174    pub name: String,
175    pub is_nullable: bool,
176    pub data_type: String,
177}
178
179pub struct PostgresBucket {
180    pool: bb8::Pool<PostgresConnectionManager<NoTls>>,
181    collection_id: CollectionId,
182    schema: String,
183    columns: Arc<RwLock<Vec<PostgresColumn>>>,
184    /// Tracks the last predicate that spilled to post-filtering (debug builds only)
185    #[cfg(debug_assertions)]
186    last_spilled_predicate: Arc<RwLock<Option<ankql::ast::Predicate>>>,
187}
188
189impl PostgresBucket {
190    fn state_table(&self) -> String { self.collection_id.as_str().to_string() }
191
192    pub fn event_table(&self) -> String { format!("{}_event", self.collection_id.as_str()) }
193
194    /// Returns the last predicate that spilled to post-filtering (debug builds only).
195    ///
196    /// Use this in tests to verify queries are fully pushed down to PostgreSQL:
197    /// ```rust,ignore
198    /// let spilled = bucket.last_spilled_predicate();
199    /// assert!(spilled.is_none(), "Expected full pushdown, but got spill: {:?}", spilled);
200    /// ```
201    #[cfg(debug_assertions)]
202    pub fn last_spilled_predicate(&self) -> Option<ankql::ast::Predicate> { self.last_spilled_predicate.read().unwrap().clone() }
203
204    /// Rebuild the cache of columns in the table.
205    pub async fn rebuild_columns_cache(&self, client: &mut tokio_postgres::Client) -> Result<(), StateError> {
206        debug!("PostgresBucket({}).rebuild_columns_cache", self.collection_id);
207        let column_query =
208            r#"SELECT column_name, is_nullable, data_type FROM information_schema.columns WHERE table_catalog = $1 AND table_name = $2;"#
209                .to_string();
210        let mut new_columns = Vec::new();
211        debug!("Querying existing columns: {:?}, [{:?}, {:?}]", column_query, &self.schema, &self.collection_id.as_str());
212        let rows = client
213            .query(&column_query, &[&self.schema, &self.collection_id.as_str()])
214            .await
215            .map_err(|err| StateError::DDLError(Box::new(err)))?;
216        for row in rows {
217            let is_nullable: String = row.get("is_nullable");
218            new_columns.push(PostgresColumn {
219                name: row.get("column_name"),
220                is_nullable: is_nullable.eq("YES"),
221                data_type: row.get("data_type"),
222            })
223        }
224
225        let mut columns = self.columns.write().unwrap();
226        *columns = new_columns;
227        drop(columns);
228
229        Ok(())
230    }
231
232    pub fn existing_columns(&self) -> Vec<String> {
233        let columns = self.columns.read().unwrap();
234        columns.iter().map(|column| column.name.clone()).collect()
235    }
236
237    pub fn column(&self, column_name: &String) -> Option<PostgresColumn> {
238        let columns = self.columns.read().unwrap();
239        columns.iter().find(|column| column.name == *column_name).cloned()
240    }
241
242    pub fn has_column(&self, column_name: &String) -> bool { self.column(column_name).is_some() }
243
244    pub async fn create_event_table(&self, client: &mut tokio_postgres::Client) -> Result<(), StateError> {
245        let create_query = format!(
246            r#"CREATE TABLE IF NOT EXISTS "{}"(
247                "id" character(43) PRIMARY KEY,
248                "entity_id" character(22),
249                "operations" bytea,
250                "parent" character(43)[],
251                "attestations" bytea
252            )"#,
253            self.event_table()
254        );
255
256        debug!("{create_query}");
257        client.execute(&create_query, &[]).await.map_err(|e| StateError::DDLError(Box::new(e)))?;
258        Ok(())
259    }
260
261    pub async fn create_state_table(&self, client: &mut tokio_postgres::Client) -> Result<(), StateError> {
262        let create_query = format!(
263            r#"CREATE TABLE IF NOT EXISTS "{}"(
264                "id" character(22) PRIMARY KEY,
265                "state_buffer" BYTEA,
266                "head" character(43)[],
267                "attestations" BYTEA[]
268            )"#,
269            self.state_table()
270        );
271
272        debug!("{create_query}");
273        match client.execute(&create_query, &[]).await {
274            Ok(_) => Ok(()),
275            Err(err) => {
276                // Log full error details for debugging
277                if let Some(db_err) = err.as_db_error() {
278                    error!("PostgresBucket({}).create_state_table error: {} (code: {:?})", self.collection_id, db_err, db_err.code());
279                } else {
280                    error!("PostgresBucket({}).create_state_table error: {:?}", self.collection_id, err);
281                }
282                Err(StateError::DDLError(Box::new(err)))
283            }
284        }
285    }
286
287    pub async fn add_missing_columns(
288        &self,
289        client: &mut tokio_postgres::Client,
290        missing: Vec<(String, &'static str)>, // column name, datatype
291    ) -> Result<(), StateError> {
292        if missing.is_empty() {
293            return Ok(());
294        }
295
296        // Acquire advisory lock to serialize DDL operations for this collection
297        let lock_key = acquire_ddl_lock(client, self.collection_id.as_str()).await?;
298
299        let result = async {
300            // Re-check columns after acquiring lock (another session may have added them)
301            self.rebuild_columns_cache(client).await?;
302
303            for (column, datatype) in missing {
304                if Postgres::sane_name(&column) && !self.has_column(&column) {
305                    let alter_query = format!(r#"ALTER TABLE "{}" ADD COLUMN "{}" {}"#, self.state_table(), column, datatype);
306                    info!("PostgresBucket({}).add_missing_columns: {}", self.collection_id, alter_query);
307                    match client.execute(&alter_query, &[]).await {
308                        Ok(_) => {}
309                        Err(err) => {
310                            // Log full error details for debugging
311                            if let Some(db_err) = err.as_db_error() {
312                                warn!(
313                                    "Error adding column {} to table {}: {} (code: {:?})",
314                                    column,
315                                    self.state_table(),
316                                    db_err,
317                                    db_err.code()
318                                );
319                            } else {
320                                warn!("Error adding column {} to table {}: {:?}", column, self.state_table(), err);
321                            }
322                            self.rebuild_columns_cache(client).await?;
323                            return Err(StateError::DDLError(Box::new(err)));
324                        }
325                    }
326                }
327            }
328
329            self.rebuild_columns_cache(client).await?;
330            Ok(())
331        }
332        .await;
333
334        // Always release the lock
335        release_ddl_lock(client, lock_key).await?;
336
337        result
338    }
339}
340
341#[async_trait]
342impl StorageCollection for PostgresBucket {
343    async fn set_state(&self, state: Attested<EntityState>) -> Result<bool, MutationError> {
344        let state_buffers = bincode::serialize(&state.payload.state.state_buffers)?;
345        let attestations: Vec<Vec<u8>> = state.attestations.iter().map(bincode::serialize).collect::<Result<Vec<_>, _>>()?;
346        let id = state.payload.entity_id;
347
348        // Ensure head is not empty for new records
349        if state.payload.state.head.is_empty() {
350            warn!("Warning: Empty head detected for entity {}", id);
351        }
352
353        let mut client = self.pool.get().await.map_err(|err| MutationError::General(err.into()))?;
354
355        let mut columns: Vec<String> = vec!["id".to_owned(), "state_buffer".to_owned(), "head".to_owned(), "attestations".to_owned()];
356        let mut params: Vec<&(dyn ToSql + Sync)> = Vec::new();
357        params.push(&id);
358        params.push(&state_buffers);
359        params.push(&state.payload.state.head);
360        params.push(&attestations);
361
362        let mut materialized: Vec<(String, Option<PGValue>)> = Vec::new();
363        let mut seen_properties = std::collections::HashSet::new();
364
365        // Process property values directly from state buffers
366        for (name, state_buffer) in state.payload.state.state_buffers.iter() {
367            let backend = backend_from_string(name, Some(state_buffer))?;
368            for (column, value) in backend.property_values() {
369                if !seen_properties.insert(column.clone()) {
370                    // Skip if property already seen in another backend
371                    // TODO: this should cause all (or subsequent?) fields with the same name
372                    // to be suffixed with the property id when we have property ids
373                    // requires some thought (and field metadata) on how to do this right
374                    continue;
375                }
376
377                let pg_value: Option<PGValue> = value.map(|value| value.into());
378                if !self.has_column(&column) {
379                    // We don't have the column yet and we know the type.
380                    if let Some(ref pg_value) = pg_value {
381                        self.add_missing_columns(&mut client, vec![(column.clone(), pg_value.postgres_type())]).await?;
382                    } else {
383                        // The column doesn't exist yet and we don't have a value.
384                        // This means the entire column is already null/none so we
385                        // don't need to set anything.
386                        continue;
387                    }
388                }
389
390                materialized.push((column.clone(), pg_value));
391            }
392        }
393
394        for (name, parameter) in &materialized {
395            columns.push(name.clone());
396
397            match &parameter {
398                Some(value) => match value {
399                    PGValue::CharacterVarying(string) => params.push(string),
400                    PGValue::SmallInt(number) => params.push(number),
401                    PGValue::Integer(number) => params.push(number),
402                    PGValue::BigInt(number) => params.push(number),
403                    PGValue::DoublePrecision(float) => params.push(float),
404                    PGValue::Bytea(bytes) => params.push(bytes),
405                    PGValue::Boolean(bool) => params.push(bool),
406                    PGValue::Jsonb(json_val) => params.push(json_val),
407                },
408                None => params.push(&UntypedNull),
409            }
410        }
411        let columns_str = columns.iter().map(|name| format!("\"{}\"", name)).collect::<Vec<String>>().join(", ");
412        let values_str = params.iter().enumerate().map(|(index, _)| format!("${}", index + 1)).collect::<Vec<String>>().join(", ");
413        let columns_update_str = columns
414            .iter()
415            .enumerate()
416            .skip(1) // Skip "id"
417            .map(|(index, name)| format!("\"{}\" = ${}", name, index + 1))
418            .collect::<Vec<String>>()
419            .join(", ");
420
421        // be careful with sql injection via bucket name
422        let query = format!(
423            r#"WITH old_state AS (
424                SELECT "head" FROM "{0}" WHERE "id" = $1
425            )
426            INSERT INTO "{0}"({1}) VALUES({2})
427            ON CONFLICT("id") DO UPDATE SET {3}
428            RETURNING (SELECT "head" FROM old_state) as old_head"#,
429            self.state_table(),
430            columns_str,
431            values_str,
432            columns_update_str
433        );
434
435        debug!("PostgresBucket({}).set_state: {}", self.collection_id, query);
436        let mut created_table = false;
437        let row = loop {
438            match client.query_one(&query, params.as_slice()).await {
439                Ok(row) => break row,
440                Err(err) => {
441                    let kind = error_kind(&err);
442                    if let ErrorKind::UndefinedTable { table } = kind {
443                        if table == self.state_table() && !created_table {
444                            self.create_state_table(&mut client).await?;
445                            created_table = true;
446                            continue; // retry exactly once
447                        }
448                    }
449                    return Err(StateError::DDLError(Box::new(err)).into());
450                }
451            }
452        };
453
454        // If this is a new entity (no old_head), or if the heads are different, return true
455        let old_head: Option<Clock> = row.get("old_head");
456        let changed = match old_head {
457            None => true, // New entity
458            Some(old_head) => old_head != state.payload.state.head,
459        };
460
461        debug!("PostgresBucket({}).set_state: Changed: {}", self.collection_id, changed);
462        Ok(changed)
463    }
464
465    async fn get_state(&self, id: EntityId) -> Result<Attested<EntityState>, RetrievalError> {
466        // be careful with sql injection via bucket name
467        let query = format!(r#"SELECT "id", "state_buffer", "head", "attestations" FROM "{}" WHERE "id" = $1"#, self.state_table());
468
469        let mut client = match self.pool.get().await {
470            Ok(client) => client,
471            Err(err) => {
472                return Err(RetrievalError::StorageError(err.into()));
473            }
474        };
475
476        debug!("PostgresBucket({}).get_state: {}", self.collection_id, query);
477        let rows = match client.query(&query, &[&id]).await {
478            Ok(rows) => rows,
479            Err(err) => {
480                let kind = error_kind(&err);
481                if let ErrorKind::UndefinedTable { table } = kind {
482                    if table == self.state_table() {
483                        self.create_state_table(&mut client).await.map_err(|e| RetrievalError::StorageError(e.into()))?;
484                        return Err(RetrievalError::EntityNotFound(id));
485                    }
486                }
487                return Err(RetrievalError::StorageError(err.into()));
488            }
489        };
490
491        let row = match rows.into_iter().next() {
492            Some(row) => row,
493            None => return Err(RetrievalError::EntityNotFound(id)),
494        };
495
496        debug!("PostgresBucket({}).get_state: Row: {:?}", self.collection_id, row);
497        let row_id: EntityId = row.try_get("id").map_err(RetrievalError::storage)?;
498        assert_eq!(row_id, id);
499
500        let serialized_buffers: Vec<u8> = row.try_get("state_buffer").map_err(RetrievalError::storage)?;
501        let state_buffers: BTreeMap<String, Vec<u8>> = bincode::deserialize(&serialized_buffers).map_err(RetrievalError::storage)?;
502        let head: Clock = row.try_get("head").map_err(RetrievalError::storage)?;
503        let attestation_bytes: Vec<Vec<u8>> = row.try_get("attestations").map_err(RetrievalError::storage)?;
504        let attestations = attestation_bytes
505            .into_iter()
506            .map(|bytes| bincode::deserialize(&bytes))
507            .collect::<Result<Vec<Attestation>, _>>()
508            .map_err(RetrievalError::storage)?;
509
510        Ok(Attested {
511            payload: EntityState {
512                entity_id: id,
513                collection: self.collection_id.clone(),
514                state: State { state_buffers: StateBuffers(state_buffers), head },
515            },
516            attestations: AttestationSet(attestations),
517        })
518    }
519
520    async fn fetch_states(&self, selection: &ankql::ast::Selection) -> Result<Vec<Attested<EntityState>>, RetrievalError> {
521        debug!("fetch_states: {:?}", selection);
522        let mut client = self.pool.get().await.map_err(|err| RetrievalError::StorageError(Box::new(err)))?;
523
524        // Pre-filter selection based on cached schema to avoid undefined column errors.
525        // If we see columns not in our cache, refresh it first (they might have been added).
526        // TODO: Once property metadata is in the system catalog, we can create missing columns
527        // on-demand here instead of refreshing the cache each time we see unknown columns.
528        let referenced = selection.referenced_columns();
529        let cached = self.existing_columns();
530        let unknown_to_cache: Vec<&String> = referenced.iter().filter(|col| !cached.contains(col)).collect();
531
532        // Refresh cache if we see columns we haven't seen before
533        if !unknown_to_cache.is_empty() {
534            debug!("PostgresBucket({}).fetch_states: Unknown columns {:?}, refreshing schema cache", self.collection_id, unknown_to_cache);
535            self.rebuild_columns_cache(&mut client).await.map_err(|e| RetrievalError::StorageError(e.into()))?;
536        }
537
538        // Now check with (possibly refreshed) cache - columns still missing truly don't exist
539        let existing = self.existing_columns();
540        let missing: Vec<String> = referenced.into_iter().filter(|col| !existing.contains(col)).collect();
541
542        let effective_selection = if missing.is_empty() {
543            selection.clone()
544        } else {
545            debug!("PostgresBucket({}).fetch_states: Columns {:?} don't exist, treating as NULL", self.collection_id, missing);
546            selection.assume_null(&missing)
547        };
548
549        // Split predicate into parts we can pushdown to PostgreSQL vs post-filter in Rust
550        let split = sql_builder::split_predicate_for_postgres(&effective_selection.predicate);
551        let needs_post_filter = split.needs_post_filter();
552        let remaining_predicate = split.remaining_predicate; // Cache before moving sql_predicate
553        debug!(
554            "PostgresBucket({}).fetch_states: SQL predicate: {:?}, remaining: {:?}, needs_post_filter: {}",
555            self.collection_id, split.sql_predicate, remaining_predicate, needs_post_filter
556        );
557
558        // Track spilled predicate for test assertions (debug builds only)
559        #[cfg(debug_assertions)]
560        {
561            let mut spilled = self.last_spilled_predicate.write().unwrap();
562            *spilled = if needs_post_filter { Some(remaining_predicate.clone()) } else { None };
563        }
564
565        // Track spilled predicate for test assertions (debug builds only)
566        #[cfg(debug_assertions)]
567        {
568            let spilled = if needs_post_filter { Some(remaining_predicate.clone()) } else { None };
569            *self.last_spilled_predicate.write().unwrap() = spilled;
570        }
571
572        // Build SQL with only the pushdown-capable predicate
573        let sql_selection = ankql::ast::Selection {
574            predicate: split.sql_predicate,
575            order_by: effective_selection.order_by.clone(),
576            limit: if needs_post_filter {
577                None // Can't limit in SQL if we need to post-filter (would drop valid results)
578            } else {
579                effective_selection.limit
580            },
581        };
582
583        let mut results = Vec::new();
584        let mut builder = SqlBuilder::with_fields(vec!["id", "state_buffer", "head", "attestations"]);
585        builder.table_name(self.state_table());
586        builder.selection(&sql_selection)?;
587
588        let (sql, args) = builder.build()?;
589        debug!("PostgresBucket({}).fetch_states: SQL: {} with args: {:?}", self.collection_id, sql, args);
590
591        let stream = match client.query_raw(&sql, args).await {
592            Ok(stream) => stream,
593            Err(err) => {
594                let kind = error_kind(&err);
595                if let ErrorKind::UndefinedTable { table } = kind {
596                    if table == self.state_table() {
597                        // Table doesn't exist yet, return empty results
598                        return Ok(Vec::new());
599                    }
600                }
601                return Err(RetrievalError::StorageError(err.into()));
602            }
603        };
604        pin_mut!(stream);
605
606        while let Some(row) = stream.try_next().await.map_err(RetrievalError::storage)? {
607            let id: EntityId = row.try_get(0).map_err(RetrievalError::storage)?;
608            let state_buffer: Vec<u8> = row.try_get(1).map_err(RetrievalError::storage)?;
609            let state_buffers: BTreeMap<String, Vec<u8>> = bincode::deserialize(&state_buffer).map_err(RetrievalError::storage)?;
610            let head: Clock = row.try_get("head").map_err(RetrievalError::storage)?;
611            let attestation_bytes: Vec<Vec<u8>> = row.try_get("attestations").map_err(RetrievalError::storage)?;
612            let attestations = attestation_bytes
613                .into_iter()
614                .map(|bytes| bincode::deserialize(&bytes))
615                .collect::<Result<Vec<Attestation>, _>>()
616                .map_err(RetrievalError::storage)?;
617
618            results.push(Attested {
619                payload: EntityState {
620                    entity_id: id,
621                    collection: self.collection_id.clone(),
622                    state: State { state_buffers: StateBuffers(state_buffers), head },
623                },
624                attestations: AttestationSet(attestations),
625            });
626        }
627
628        // Post-filter results if we have remaining predicate that couldn't be pushed down
629        let results = if needs_post_filter {
630            debug!(
631                "PostgresBucket({}).fetch_states: Post-filtering {} results with remaining predicate",
632                self.collection_id,
633                results.len()
634            );
635            let filtered = post_filter_states(&results, &remaining_predicate, &self.collection_id);
636
637            // Apply limit after post-filter if needed
638            if let Some(limit) = effective_selection.limit {
639                filtered.into_iter().take(limit as usize).collect()
640            } else {
641                filtered
642            }
643        } else {
644            results
645        };
646
647        Ok(results)
648    }
649
650    async fn add_event(&self, entity_event: &Attested<Event>) -> Result<bool, MutationError> {
651        let operations = bincode::serialize(&entity_event.payload.operations)?;
652        let attestations = bincode::serialize(&entity_event.attestations)?;
653
654        let query = format!(
655            r#"INSERT INTO "{0}"("id", "entity_id", "operations", "parent", "attestations") VALUES($1, $2, $3, $4, $5)
656               ON CONFLICT ("id") DO NOTHING"#,
657            self.event_table(),
658        );
659
660        let mut client = self.pool.get().await.map_err(|err| MutationError::General(err.into()))?;
661        debug!("PostgresBucket({}).add_event: {}", self.collection_id, query);
662        let mut created_table = false;
663        let affected = loop {
664            match client
665                .execute(
666                    &query,
667                    &[
668                        &entity_event.payload.id(),
669                        &entity_event.payload.entity_id,
670                        &operations,
671                        &entity_event.payload.parent,
672                        &attestations,
673                    ],
674                )
675                .await
676            {
677                Ok(affected) => break affected,
678                Err(err) => {
679                    let kind = error_kind(&err);
680                    if let ErrorKind::UndefinedTable { table } = kind {
681                        if table == self.event_table() && !created_table {
682                            self.create_event_table(&mut client).await?;
683                            created_table = true;
684                            continue; // retry exactly once
685                        }
686                    }
687                    error!("PostgresBucket({}).add_event: Error: {:?}", self.collection_id, err);
688                    return Err(StateError::DMLError(Box::new(err)).into());
689                }
690            }
691        };
692
693        Ok(affected > 0)
694    }
695
696    async fn get_events(&self, event_ids: Vec<EventId>) -> Result<Vec<Attested<Event>>, RetrievalError> {
697        if event_ids.is_empty() {
698            return Ok(Vec::new());
699        }
700
701        let query = format!(
702            r#"SELECT "id", "entity_id", "operations", "parent", "attestations" FROM "{0}" WHERE "id" = ANY($1)"#,
703            self.event_table(),
704        );
705
706        let client = self.pool.get().await.map_err(RetrievalError::storage)?;
707        let rows = match client.query(&query, &[&event_ids]).await {
708            Ok(rows) => rows,
709            Err(err) => {
710                let kind = error_kind(&err);
711                match kind {
712                    ErrorKind::UndefinedTable { table } if table == self.event_table() => return Ok(Vec::new()),
713                    _ => return Err(RetrievalError::storage(err)),
714                }
715            }
716        };
717
718        let mut events = Vec::new();
719        for row in rows {
720            let entity_id: EntityId = row.try_get("entity_id").map_err(RetrievalError::storage)?;
721            let operations: OperationSet = row.try_get("operations").map_err(RetrievalError::storage)?;
722            let parent: Clock = row.try_get("parent").map_err(RetrievalError::storage)?;
723            let attestations_binary: Vec<u8> = row.try_get("attestations").map_err(RetrievalError::storage)?;
724            let attestations: Vec<Attestation> = bincode::deserialize(&attestations_binary).map_err(RetrievalError::storage)?;
725
726            let event = Attested {
727                payload: Event { collection: self.collection_id.clone(), entity_id, operations, parent },
728                attestations: AttestationSet(attestations),
729            };
730            events.push(event);
731        }
732        Ok(events)
733    }
734
735    async fn dump_entity_events(&self, entity_id: EntityId) -> Result<Vec<Attested<Event>>, ankurah_core::error::RetrievalError> {
736        let query =
737            format!(r#"SELECT "id", "operations", "parent", "attestations" FROM "{0}" WHERE "entity_id" = $1"#, self.event_table(),);
738
739        let client = self.pool.get().await.map_err(RetrievalError::storage)?;
740        debug!("PostgresBucket({}).get_events: {}", self.collection_id, query);
741        let rows = match client.query(&query, &[&entity_id]).await {
742            Ok(rows) => rows,
743            Err(err) => {
744                let kind = error_kind(&err);
745                if let ErrorKind::UndefinedTable { table } = kind {
746                    if table == self.event_table() {
747                        return Ok(Vec::new());
748                    }
749                }
750
751                return Err(RetrievalError::storage(err));
752            }
753        };
754
755        let mut events = Vec::new();
756        for row in rows {
757            // let event_id: EventId = row.try_get("id").map_err(|err| RetrievalError::storage(err))?;
758            let operations_binary: Vec<u8> = row.try_get("operations").map_err(RetrievalError::storage)?;
759            let operations = bincode::deserialize(&operations_binary).map_err(RetrievalError::storage)?;
760            let parent: Clock = row.try_get("parent").map_err(RetrievalError::storage)?;
761            let attestations_binary: Vec<u8> = row.try_get("attestations").map_err(RetrievalError::storage)?;
762            let attestations: Vec<Attestation> = bincode::deserialize(&attestations_binary).map_err(RetrievalError::storage)?;
763
764            events.push(Attested {
765                payload: Event { collection: self.collection_id.clone(), entity_id, operations, parent },
766                attestations: AttestationSet(attestations),
767            });
768        }
769
770        Ok(events)
771    }
772}
773
774// Some hacky shit because rust-postgres doesn't let us ask for the error kind
775// TODO: remove this when https://github.com/sfackler/rust-postgres/pull/1185
776//       gets merged
777#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
778pub enum ErrorKind {
779    RowCount,
780    UndefinedTable { table: String },
781    UndefinedColumn { table: Option<String>, column: String },
782    Unknown,
783    PostgresError(String),
784}
785
786pub fn error_kind(err: &tokio_postgres::Error) -> ErrorKind {
787    let string = err.as_db_error().map(|e| e.message()).unwrap_or_default().trim().to_owned();
788    let _db_error = err.as_db_error();
789    let sql_code = err.code().cloned();
790
791    // Check the error's Display string for RowCount errors (client-side, not db error)
792    let err_string = err.to_string();
793    if err_string.contains("query returned an unexpected number of rows") || string == "query returned an unexpected number of rows" {
794        return ErrorKind::RowCount;
795    }
796
797    // Useful for adding new errors
798    // error!("postgres error: {:?}", err);
799    // error!("db_err: {:?}", err.as_db_error());
800    // error!("sql_code: {:?}", err.code());
801    // error!("err: {:?}", err);
802    // error!("err: {:?}", err.to_string());
803    debug!("postgres error: {:?}", err);
804
805    let quote_indices = |s: &str| {
806        let mut quotes = Vec::new();
807        for (index, char) in s.char_indices() {
808            if char == '"' {
809                quotes.push(index)
810            }
811        }
812        quotes
813    };
814
815    match sql_code {
816        Some(SqlState::UNDEFINED_TABLE) => {
817            // relation "album" does not exist
818            let quotes = quote_indices(&string);
819            if quotes.len() >= 2 {
820                let table = &string[quotes[0] + 1..quotes[1]];
821                ErrorKind::UndefinedTable { table: table.to_owned() }
822            } else {
823                ErrorKind::PostgresError(string.clone())
824            }
825        }
826        Some(SqlState::UNDEFINED_COLUMN) => {
827            // Handle both formats:
828            // "column "name" of relation "album" does not exist"
829            // "column "status" does not exist"
830            let quotes = quote_indices(&string);
831            if quotes.len() >= 2 {
832                let column = string[quotes[0] + 1..quotes[1]].to_owned();
833
834                let table = if quotes.len() >= 4 {
835                    // Full format with table name
836                    Some(string[quotes[2] + 1..quotes[3]].to_owned())
837                } else {
838                    // Short format without table name
839                    None
840                };
841
842                ErrorKind::UndefinedColumn { table, column }
843            } else {
844                ErrorKind::PostgresError(string.clone())
845            }
846        }
847        _ => ErrorKind::Unknown,
848    }
849}
850
851#[allow(unused)]
852pub struct MissingMaterialized {
853    pub name: String,
854}
855
856use bytes::BytesMut;
857use tokio_postgres::types::{to_sql_checked, IsNull, Type};
858
859use crate::sql_builder::SqlBuilder;
860
861/// Post-filter EntityStates using a predicate that couldn't be pushed to SQL.
862///
863/// This is the escape hatch for predicates that PostgreSQL can't handle natively,
864/// such as complex JSON traversals or future features like Ref traversal.
865fn post_filter_states(
866    states: &[Attested<EntityState>],
867    predicate: &ankql::ast::Predicate,
868    collection_id: &CollectionId,
869) -> Vec<Attested<EntityState>> {
870    use ankurah_core::entity::TemporaryEntity;
871    use ankurah_core::selection::filter::evaluate_predicate;
872
873    states
874        .iter()
875        .filter(|attested| {
876            // Create a TemporaryEntity for filtering (implements Filterable)
877            match TemporaryEntity::new(attested.payload.entity_id, collection_id.clone(), &attested.payload.state) {
878                Ok(temp_entity) => {
879                    // Evaluate the predicate
880                    match evaluate_predicate(&temp_entity, predicate) {
881                        Ok(true) => true,
882                        Ok(false) => false,
883                        Err(e) => {
884                            warn!("Post-filter evaluation error for entity {}: {}", attested.payload.entity_id, e);
885                            false // Exclude entities that fail evaluation
886                        }
887                    }
888                }
889                Err(e) => {
890                    warn!("Failed to create TemporaryEntity for post-filtering {}: {}", attested.payload.entity_id, e);
891                    false // Exclude entities we can't evaluate
892                }
893            }
894        })
895        .cloned()
896        .collect()
897}
898
899#[derive(Debug)]
900struct UntypedNull;
901
902impl ToSql for UntypedNull {
903    fn to_sql(&self, _ty: &Type, _out: &mut BytesMut) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> { Ok(IsNull::Yes) }
904
905    fn accepts(_ty: &Type) -> bool {
906        true // Accept all types
907    }
908
909    to_sql_checked!();
910}