Skip to main content

teaql_provider_postgres/
lib.rs

1#![allow(warnings)]
2use std::future::Future;
3use std::pin::Pin;
4
5use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
6use deadpool_postgres::Pool;
7use rust_decimal::Decimal;
8use std::collections::HashSet;
9use std::sync::Arc;
10use teaql_core::{
11    BinaryOp, DataType, EntityDescriptor, Expr, InsertCommand, PropertyDescriptor, SelectQuery,
12    UpdateCommand, Value,
13};
14use teaql_runtime::{
15    GraphNode, InternalIdGenerator, RuntimeError, SchemaProvider, UserContext,
16    canonical_id_space_entity,
17};
18use teaql_sql::{
19    CompiledQuery, DatabaseKind, SqlCompileError, SqlDialect, SqlTransport,
20    quote_identifier_if_needed,
21};
22use tokio::sync::Mutex;
23
24pub const DEFAULT_ID_SPACE_TABLE: &str = "teaql_id_space";
25
26#[derive(Debug, Default, Clone, Copy)]
27pub struct PostgresDialect;
28
29impl PostgresDialect {
30    /// Indexes supporting the common "recent children for each parent" access
31    /// pattern.  This deliberately uses a full index: PostgreSQL cannot use a
32    /// partial `WHERE version > 0` index for a generic prepared plan whose
33    /// version predicate is parameterized.
34    fn relation_indexes_sqls(&self, entity: &EntityDescriptor) -> Vec<String> {
35        let Some(id_property) = entity.id_property() else {
36            return Vec::new();
37        };
38        let mut indexed_columns = HashSet::new();
39        let mut sqls = Vec::new();
40
41        for relation in &entity.relations {
42            // A to-one relation whose local key is not the entity ID represents
43            // a foreign-key property on this table.  `(foreign_key, id DESC)`
44            // serves both equality lookup and top-N/recent-child queries.
45            if relation.many || relation.local_key == id_property.name {
46                continue;
47            }
48            let Some(foreign_key_property) = entity.property_by_name(&relation.local_key) else {
49                continue;
50            };
51            if !indexed_columns.insert(foreign_key_property.column_name.as_str()) {
52                continue;
53            }
54
55            let index_name = postgres_index_name(
56                &entity.table_name,
57                &foreign_key_property.column_name,
58                &id_property.column_name,
59            );
60            sqls.push(format!(
61                "CREATE INDEX IF NOT EXISTS {} ON {} ({}, {} DESC)",
62                self.quote_ident(&index_name),
63                self.quote_ident(&entity.table_name),
64                self.quote_ident(&foreign_key_property.column_name),
65                self.quote_ident(&id_property.column_name),
66            ));
67        }
68        sqls
69    }
70}
71
72fn postgres_index_name(table: &str, foreign_key: &str, id: &str) -> String {
73    let full = format!("IDX_{table}_{foreign_key}_{id}_DESC").to_uppercase();
74    if full.len() <= 63 {
75        return full;
76    }
77
78    // PostgreSQL silently truncates identifiers to 63 bytes. Add a stable hash
79    // ourselves so two long generated names cannot collapse to the same index.
80    let hash = full.bytes().fold(0xcbf29ce484222325_u64, |hash, byte| {
81        (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)
82    });
83    let suffix = format!("_{hash:016X}");
84    let prefix_bytes = 63 - suffix.len();
85    let mut end = prefix_bytes.min(full.len());
86    while !full.is_char_boundary(end) {
87        end -= 1;
88    }
89    format!("{}{}", &full[..end], suffix)
90}
91
92fn postgres_foreign_key_name(
93    source_table: &str,
94    source_column: &str,
95    referenced_table: &str,
96    referenced_column: &str,
97) -> String {
98    let full = format!("FK_{source_table}_{source_column}_{referenced_table}_{referenced_column}")
99        .to_uppercase();
100    if full.len() <= 63 {
101        return full;
102    }
103    let hash = full.bytes().fold(0xcbf29ce484222325_u64, |hash, byte| {
104        (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3)
105    });
106    let suffix = format!("_{hash:016X}");
107    let mut end = (63 - suffix.len()).min(full.len());
108    while !full.is_char_boundary(end) {
109        end -= 1;
110    }
111    format!("{}{}", &full[..end], suffix)
112}
113
114impl SqlDialect for PostgresDialect {
115    fn kind(&self) -> DatabaseKind {
116        DatabaseKind::PostgreSql
117    }
118
119    fn large_in_uses_array_param(&self) -> bool {
120        true
121    }
122
123    fn quote_ident(&self, ident: &str) -> String {
124        quote_ident(ident)
125    }
126
127    fn placeholder(&self, index: usize) -> String {
128        format!("${index}")
129    }
130
131    fn schema_setup_sqls(&self) -> &'static [&'static str] {
132        &[CREATE_SOUNDEX_FUNCTION]
133    }
134
135    fn schema_type_sql(
136        &self,
137        data_type: DataType,
138        _property: &PropertyDescriptor,
139    ) -> Result<&'static str, SqlCompileError> {
140        match data_type {
141            DataType::Bool => Ok("BOOLEAN"),
142            DataType::I64 | DataType::U64 => Ok("BIGINT"),
143            DataType::F64 => Ok("DOUBLE PRECISION"),
144            DataType::Decimal => Ok("NUMERIC"),
145            DataType::Text => Ok("VARCHAR(255)"),
146            DataType::LargeText => Ok("TEXT"),
147            DataType::Json => Ok("JSONB"),
148            DataType::Date => Ok("DATE"),
149            DataType::Timestamp => Ok("TIMESTAMPTZ"),
150        }
151    }
152
153    fn compile_in(
154        &self,
155        entity: &EntityDescriptor,
156        left: &Expr,
157        op: BinaryOp,
158        right: &Expr,
159        params: &mut Vec<Value>,
160    ) -> Result<String, SqlCompileError> {
161        match op {
162            BinaryOp::InLarge | BinaryOp::NotInLarge => {
163                let Expr::Value(Value::List(values)) = right else {
164                    let lhs = self.compile_expr(entity, left, params)?;
165                    let rhs = self.compile_expr(entity, right, params)?;
166                    let operator = match op {
167                        BinaryOp::InLarge => "= ANY",
168                        BinaryOp::NotInLarge => "<> ALL",
169                        _ => unreachable!(),
170                    };
171                    return Ok(format!("({lhs} {operator} ({rhs}))"));
172                };
173                if values.is_empty() {
174                    return Err(SqlCompileError::EmptyInList);
175                }
176                let lhs = self.compile_expr(entity, left, params)?;
177                params.push(Value::List(values.clone()));
178                let placeholder = self.placeholder(params.len());
179                let operator = match op {
180                    BinaryOp::InLarge => "= ANY",
181                    BinaryOp::NotInLarge => "<> ALL",
182                    _ => unreachable!(),
183                };
184                Ok(format!("({lhs} {operator}({placeholder}))"))
185            }
186            _ => {
187                let lhs = self.compile_expr(entity, left, params)?;
188                let operator = match op {
189                    BinaryOp::In => "IN",
190                    BinaryOp::NotIn => "NOT IN",
191                    _ => unreachable!(),
192                };
193                match right {
194                    Expr::Value(Value::List(values)) => {
195                        if values.is_empty() {
196                            return Err(SqlCompileError::EmptyInList);
197                        }
198                        let mut placeholders = Vec::with_capacity(values.len());
199                        for value in values {
200                            params.push(value.clone());
201                            placeholders.push(self.placeholder(params.len()));
202                        }
203                        Ok(format!("({lhs} {operator} ({}))", placeholders.join(", ")))
204                    }
205                    _ => {
206                        let rhs = self.compile_expr(entity, right, params)?;
207                        Ok(format!("({lhs} {operator} ({rhs}))"))
208                    }
209                }
210            }
211        }
212    }
213}
214
215const CREATE_SOUNDEX_FUNCTION: &str = r#"
216CREATE OR REPLACE FUNCTION soundex(input text)
217RETURNS text
218LANGUAGE plpgsql
219IMMUTABLE
220STRICT
221AS $$
222DECLARE
223    normalized text := upper(regexp_replace(input, '[^A-Za-z]', '', 'g'));
224    first_char text;
225    output text;
226    previous_code text;
227    code text;
228    ch text;
229    i integer;
230BEGIN
231    IF normalized = '' THEN
232        RETURN '0000';
233    END IF;
234
235    first_char := substr(normalized, 1, 1);
236    output := first_char;
237    previous_code := CASE
238        WHEN first_char IN ('B', 'F', 'P', 'V') THEN '1'
239        WHEN first_char IN ('C', 'G', 'J', 'K', 'Q', 'S', 'X', 'Z') THEN '2'
240        WHEN first_char IN ('D', 'T') THEN '3'
241        WHEN first_char = 'L' THEN '4'
242        WHEN first_char IN ('M', 'N') THEN '5'
243        WHEN first_char = 'R' THEN '6'
244        ELSE '0'
245    END;
246
247    FOR i IN 2..char_length(normalized) LOOP
248        ch := substr(normalized, i, 1);
249        code := CASE
250            WHEN ch IN ('B', 'F', 'P', 'V') THEN '1'
251            WHEN ch IN ('C', 'G', 'J', 'K', 'Q', 'S', 'X', 'Z') THEN '2'
252            WHEN ch IN ('D', 'T') THEN '3'
253            WHEN ch = 'L' THEN '4'
254            WHEN ch IN ('M', 'N') THEN '5'
255            WHEN ch = 'R' THEN '6'
256            ELSE '0'
257        END;
258
259        IF code <> '0' AND code <> previous_code THEN
260            output := output || code;
261            IF char_length(output) = 4 THEN
262                RETURN output;
263            END IF;
264        END IF;
265        previous_code := code;
266    END LOOP;
267
268    RETURN rpad(output, 4, '0');
269END;
270$$
271"#;
272
273#[derive(Debug)]
274pub enum MutationExecutorError {
275    Driver(tokio_postgres::Error),
276    Pool(String),
277    SqlCompile(SqlCompileError),
278    UnsupportedValue(&'static str),
279    UnsupportedColumnType(String),
280    Bind(String),
281}
282
283impl std::fmt::Display for MutationExecutorError {
284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285        match self {
286            Self::Driver(err) => err.fmt(f),
287            Self::Pool(err) => write!(f, "postgres pool error: {err}"),
288            Self::SqlCompile(err) => err.fmt(f),
289            Self::UnsupportedValue(kind) => {
290                write!(f, "unsupported bind value for mutation executor: {kind}")
291            }
292            Self::UnsupportedColumnType(kind) => {
293                write!(f, "unsupported column type for record decoding: {kind}")
294            }
295            Self::Bind(message) => write!(f, "bind error: {message}"),
296        }
297    }
298}
299
300impl std::error::Error for MutationExecutorError {}
301
302impl From<tokio_postgres::Error> for MutationExecutorError {
303    fn from(value: tokio_postgres::Error) -> Self {
304        Self::Driver(value)
305    }
306}
307
308impl From<SqlCompileError> for MutationExecutorError {
309    fn from(value: SqlCompileError) -> Self {
310        Self::SqlCompile(value)
311    }
312}
313
314#[derive(Clone)]
315pub struct PgMutationExecutor {
316    pool: Pool,
317}
318
319/// A transaction owns one checked-out pooled connection for its complete
320/// lifetime. Using SQL BEGIN/COMMIT avoids a self-referential wrapper around
321/// `tokio_postgres::Transaction` while preserving connection affinity.
322pub struct PgTransactionExecutor {
323    client: deadpool_postgres::Object,
324}
325
326impl SqlTransport for PgMutationExecutor {
327    type Error = MutationExecutorError;
328
329    async fn fetch_all_compact_sql(
330        &self,
331        query: &CompiledQuery,
332    ) -> Result<Vec<teaql_core::CompactRow>, Self::Error> {
333        let mut args = PgArgs { values: Vec::new() };
334        for value in &query.params {
335            bind_pg(&mut args, value)?;
336        }
337        let client = self
338            .pool
339            .get()
340            .await
341            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
342        let statement = client.prepare_cached(&query.sql).await?;
343        let rows = client.query(&statement, &args.as_refs()).await?;
344        let columns: std::sync::Arc<[String]> = statement
345            .columns()
346            .iter()
347            .map(|column| column.name().to_owned())
348            .collect::<Vec<_>>()
349            .into();
350        rows.iter()
351            .map(|row| {
352                Ok(teaql_core::CompactRow::new(
353                    columns.clone(),
354                    decode_pg_values(row)?,
355                ))
356            })
357            .collect()
358    }
359
360    async fn execute_sql(&self, query: &CompiledQuery) -> Result<u64, Self::Error> {
361        self.execute(query).await
362    }
363}
364
365impl teaql_sql::StreamingSqlTransport for PgMutationExecutor {
366    fn stream_sql(
367        &self,
368        query: CompiledQuery,
369        chunk_size: usize,
370    ) -> teaql_data_service::QueryStream<'_, Self::Error> {
371        let pool = self.pool.clone();
372        Box::pin(async_stream::try_stream! {
373            use futures_util::TryStreamExt;
374            let mut args = PgArgs { values: Vec::new() }; for value in &query.params { bind_pg(&mut args, value)?; }
375            let client = pool.get().await.map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
376            let params = args.as_refs();
377            let statement = client.prepare_cached(&query.sql).await?;
378            let columns: std::sync::Arc<[String]> = statement.columns().iter().map(|column| column.name().to_owned()).collect::<Vec<_>>().into();
379            let rows = client.query_raw(&statement, params).await?;
380            futures_util::pin_mut!(rows);
381            let mut chunk = Vec::with_capacity(chunk_size); let mut index = 0;
382            while let Some(row) = rows.try_next().await? { chunk.push(teaql_core::CompactRow::new(columns.clone(), decode_pg_values(&row)?)); if chunk.len()==chunk_size { yield teaql_data_service::StreamChunk { rows: std::mem::take(&mut chunk), chunk_index:index, is_last:false }; index+=1; } }
383            if !chunk.is_empty() { yield teaql_data_service::StreamChunk { rows:chunk, chunk_index:index, is_last:true }; }
384        })
385    }
386}
387
388impl SqlTransport for PgTransactionExecutor {
389    type Error = MutationExecutorError;
390
391    async fn fetch_all_compact_sql(
392        &self,
393        query: &CompiledQuery,
394    ) -> Result<Vec<teaql_core::CompactRow>, Self::Error> {
395        let mut args = PgArgs { values: Vec::new() };
396        for value in &query.params {
397            bind_pg(&mut args, value)?;
398        }
399        let statement = self.client.prepare_cached(&query.sql).await?;
400        let rows = self.client.query(&statement, &args.as_refs()).await?;
401        let columns: Arc<[String]> = statement
402            .columns()
403            .iter()
404            .map(|column| column.name().to_owned())
405            .collect::<Vec<_>>()
406            .into();
407        rows.iter()
408            .map(|row| {
409                Ok(teaql_core::CompactRow::new(
410                    columns.clone(),
411                    decode_pg_values(row)?,
412                ))
413            })
414            .collect()
415    }
416
417    async fn execute_sql(&self, query: &CompiledQuery) -> Result<u64, Self::Error> {
418        let mut args = PgArgs { values: Vec::new() };
419        for value in &query.params {
420            bind_pg(&mut args, value)?;
421        }
422        let statement = self.client.prepare_cached(&query.sql).await?;
423        Ok(self.client.execute(&statement, &args.as_refs()).await?)
424    }
425}
426
427impl teaql_sql::SqlTransaction for PgTransactionExecutor {
428    type Error = MutationExecutorError;
429
430    async fn commit_sql(self) -> Result<(), Self::Error> {
431        self.client.batch_execute("COMMIT").await?;
432        Ok(())
433    }
434
435    async fn rollback_sql(self) -> Result<(), Self::Error> {
436        self.client.batch_execute("ROLLBACK").await?;
437        Ok(())
438    }
439}
440
441impl teaql_sql::SqlTransactionTransport for PgMutationExecutor {
442    type Tx<'a>
443        = PgTransactionExecutor
444    where
445        Self: 'a;
446
447    async fn begin_sql(&self) -> Result<Self::Tx<'_>, Self::Error> {
448        let client = self
449            .pool
450            .get()
451            .await
452            .map_err(|error| MutationExecutorError::Pool(error.to_string()))?;
453        client.batch_execute("BEGIN").await?;
454        Ok(PgTransactionExecutor { client })
455    }
456}
457
458impl PgMutationExecutor {
459    pub fn new(pool: Pool) -> Self {
460        Self { pool }
461    }
462
463    pub fn pool(&self) -> Pool {
464        self.pool.clone()
465    }
466
467    pub async fn ensure_schema(
468        &self,
469        dialect: &PostgresDialect,
470        entities: &[&EntityDescriptor],
471    ) -> Result<(), MutationExecutorError> {
472        let mut client = self
473            .pool
474            .get()
475            .await
476            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
477        {
478            let transaction = client.transaction().await?;
479            transaction
480                .query_one(
481                    "SELECT pg_advisory_xact_lock(hashtextextended('teaql-schema-setup', 0))",
482                    &[],
483                )
484                .await?;
485            for sql in dialect.schema_setup_sqls() {
486                transaction.execute(*sql, &[]).await?;
487            }
488            transaction.commit().await?;
489        }
490        self.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE).await?;
491
492        for entity in entities {
493            if !self.table_exists(&entity.table_name).await? {
494                let sql = dialect.compile_create_table(entity)?;
495                client.execute(&sql, &[]).await?;
496            } else {
497                let existing_columns = self.table_columns(&entity.table_name).await?;
498                for property in &entity.properties {
499                    let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
500                    if existing_columns.contains(&bare_column) {
501                        continue;
502                    }
503                    let sql = dialect.compile_add_column(entity, property)?;
504                    client.execute(&sql, &[]).await?;
505                }
506            }
507
508            for sql in dialect.schema_indexes_sqls(entity)? {
509                client.execute(&sql, &[]).await?;
510            }
511            for sql in dialect.relation_indexes_sqls(entity) {
512                client.execute(&sql, &[]).await?;
513            }
514        }
515
516        // Install constraints only after every table and column exists, so
517        // descriptor registration order does not affect schema creation.
518        for entity in entities {
519            for relation in &entity.relations {
520                let Some(target) = entities
521                    .iter()
522                    .copied()
523                    .find(|candidate| candidate.name == relation.target_entity)
524                else {
525                    // A module may intentionally reference an entity supplied by
526                    // another module or service. That remains a logical relation.
527                    continue;
528                };
529                if entity.data_service != target.data_service {
530                    // Cross-data-source relations cannot be represented by a
531                    // database-local foreign-key constraint.
532                    continue;
533                }
534                let (source, source_key, referenced, referenced_key) = if relation.many {
535                    (target, &relation.foreign_key, *entity, &relation.local_key)
536                } else {
537                    (*entity, &relation.local_key, target, &relation.foreign_key)
538                };
539                let source_property = source.property_by_name(source_key).ok_or_else(|| {
540                    MutationExecutorError::Bind(format!(
541                        "cannot ensure relation {}.{}: source key {}.{} does not exist",
542                        entity.name, relation.name, source.name, source_key
543                    ))
544                })?;
545                let referenced_property =
546                    referenced.property_by_name(referenced_key).ok_or_else(|| {
547                        MutationExecutorError::Bind(format!(
548                            "cannot ensure relation {}.{}: referenced key {}.{} does not exist",
549                            entity.name, relation.name, referenced.name, referenced_key
550                        ))
551                    })?;
552                self.ensure_foreign_key(
553                    &source.table_name,
554                    &source_property.column_name,
555                    &referenced.table_name,
556                    &referenced_property.column_name,
557                )
558                .await?;
559            }
560        }
561        Ok(())
562    }
563
564    async fn ensure_foreign_key(
565        &self,
566        source_table: &str,
567        source_column: &str,
568        referenced_table: &str,
569        referenced_column: &str,
570    ) -> Result<(), MutationExecutorError> {
571        let semantic_key = format!(
572            "teaql-fk:{source_table}:{source_column}:{referenced_table}:{referenced_column}:a:a"
573        );
574        let mut client = self
575            .pool
576            .get()
577            .await
578            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
579        let transaction = client.transaction().await?;
580        transaction
581            .query_one(
582                "SELECT pg_advisory_xact_lock(hashtextextended($1, 0))",
583                &[&semantic_key],
584            )
585            .await?;
586        let exists: bool = transaction
587            .query_one(
588                "SELECT EXISTS (
589                    SELECT 1
590                      FROM pg_constraint c
591                      JOIN pg_class st ON st.oid = c.conrelid
592                      JOIN pg_namespace sn ON sn.oid = st.relnamespace
593                      JOIN pg_class rt ON rt.oid = c.confrelid
594                      JOIN pg_namespace rn ON rn.oid = rt.relnamespace
595                      JOIN pg_attribute sc ON sc.attrelid = c.conrelid AND sc.attnum = c.conkey[1]
596                      JOIN pg_attribute rc ON rc.attrelid = c.confrelid AND rc.attnum = c.confkey[1]
597                     WHERE c.contype = 'f'
598                       AND sn.nspname = current_schema()
599                       AND rn.nspname = current_schema()
600                       AND st.relname = $1 AND sc.attname = $2
601                       AND rt.relname = $3 AND rc.attname = $4
602                       AND cardinality(c.conkey) = 1 AND cardinality(c.confkey) = 1
603                       AND c.confupdtype = 'a' AND c.confdeltype = 'a'
604                )",
605                &[
606                    &strip_identifier_quotes(source_table),
607                    &strip_identifier_quotes(source_column),
608                    &strip_identifier_quotes(referenced_table),
609                    &strip_identifier_quotes(referenced_column),
610                ],
611            )
612            .await?
613            .try_get(0)?;
614        if !exists {
615            let constraint_name = postgres_foreign_key_name(
616                source_table,
617                source_column,
618                referenced_table,
619                referenced_column,
620            );
621            let sql = format!(
622                "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({})",
623                quote_ident(source_table),
624                quote_ident(&constraint_name),
625                quote_ident(source_column),
626                quote_ident(referenced_table),
627                quote_ident(referenced_column),
628            );
629            transaction.execute(&sql, &[]).await?;
630        }
631        transaction.commit().await?;
632        Ok(())
633    }
634
635    pub async fn ensure_id_space_table(
636        &self,
637        table_name: &str,
638    ) -> Result<(), MutationExecutorError> {
639        let sql = format!(
640            "CREATE TABLE IF NOT EXISTS {} (type_name VARCHAR(100) PRIMARY KEY, current_level BIGINT NOT NULL)",
641            quote_ident(table_name)
642        );
643        let client = self
644            .pool
645            .get()
646            .await
647            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
648        client.execute(&sql, &[]).await?;
649        Ok(())
650    }
651
652    pub async fn execute(&self, query: &CompiledQuery) -> Result<u64, MutationExecutorError> {
653        let mut args = PgArgs { values: Vec::new() };
654        for value in &query.params {
655            bind_pg(&mut args, value)?;
656        }
657        let client = self
658            .pool
659            .get()
660            .await
661            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
662        let statement = client.prepare_cached(&query.sql).await?;
663        let result = client.execute(&statement, &args.as_refs()).await?;
664        Ok(result)
665    }
666
667    async fn table_exists(&self, table_name: &str) -> Result<bool, MutationExecutorError> {
668        let client = self
669            .pool
670            .get()
671            .await
672            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
673        let row = client
674            .query_one(
675                "SELECT COUNT(1)
676             FROM information_schema.tables
677             WHERE table_schema = current_schema()
678               AND table_name = $1",
679                &[&table_name],
680            )
681            .await?;
682        let exists: i64 = row.try_get(0)?;
683        Ok(exists > 0)
684    }
685
686    async fn table_columns(
687        &self,
688        table_name: &str,
689    ) -> Result<std::collections::BTreeSet<String>, MutationExecutorError> {
690        let client = self
691            .pool
692            .get()
693            .await
694            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
695        let rows = client
696            .query(
697                "SELECT column_name
698             FROM information_schema.columns
699             WHERE table_schema = current_schema()
700               AND table_name = $1",
701                &[&table_name],
702            )
703            .await?;
704        let mut columns = std::collections::BTreeSet::new();
705        for row in rows {
706            let name: String = row.try_get("column_name")?;
707            columns.insert(name.to_lowercase());
708        }
709        Ok(columns)
710    }
711}
712
713async fn ensure_initial_graphs_postgres(
714    executor: &PgMutationExecutor,
715    dialect: &PostgresDialect,
716    context: &UserContext,
717) -> Result<(), MutationExecutorError> {
718    for graph in context.initial_graphs() {
719        let entity = context.entity(&graph.entity).ok_or_else(|| {
720            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
721        })?;
722        if initial_graph_exists_postgres(executor, dialect, entity, graph).await? {
723            if let Some(query) = compile_initial_graph_update(dialect, entity, graph)? {
724                executor.execute(&query).await?;
725            }
726            continue;
727        }
728        let query = compile_initial_graph_insert(dialect, entity, graph)?;
729        executor.execute(&query).await?;
730    }
731    for graph in context.root_graphs() {
732        let entity = context.entity(&graph.entity).ok_or_else(|| {
733            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
734        })?;
735        if initial_graph_exists_postgres(executor, dialect, entity, graph).await? {
736            continue;
737        }
738        let query = compile_initial_graph_insert(dialect, entity, graph)?;
739        executor.execute(&query).await?;
740    }
741    let generator = PgIdSpaceGenerator::from_executor(executor.clone());
742    for graph in context.initial_graphs().iter().chain(context.root_graphs()) {
743        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
744            generator.ensure_floor(&graph.entity, id).await?;
745        }
746    }
747    Ok(())
748}
749
750async fn initial_graph_exists_postgres(
751    executor: &PgMutationExecutor,
752    dialect: &PostgresDialect,
753    entity: &EntityDescriptor,
754    graph: &GraphNode,
755) -> Result<bool, MutationExecutorError> {
756    let Some(id) = graph.values.get("id") else {
757        return Ok(false);
758    };
759    let query = dialect.compile_select(
760        entity,
761        &SelectQuery::new(&graph.entity)
762            .project("id")
763            .filter(Expr::eq("id", id.clone()))
764            .limit(1),
765    )?;
766    Ok(!executor.fetch_all_compact_sql(&query).await?.is_empty())
767}
768
769fn compile_initial_graph_insert(
770    dialect: &impl SqlDialect,
771    entity: &EntityDescriptor,
772    graph: &GraphNode,
773) -> Result<CompiledQuery, MutationExecutorError> {
774    let mut command = InsertCommand::new(&graph.entity);
775    for (field, value) in &graph.values {
776        command = command.value(field.clone(), value.clone());
777    }
778    dialect.compile_insert(entity, &command).map_err(Into::into)
779}
780
781fn compile_initial_graph_update(
782    dialect: &impl SqlDialect,
783    entity: &EntityDescriptor,
784    graph: &crate::GraphNode,
785) -> Result<Option<CompiledQuery>, MutationExecutorError> {
786    let Some(id) = graph.values.get("id") else {
787        return Ok(None);
788    };
789    let mut command = UpdateCommand::new(&graph.entity, id.clone());
790    for (field, value) in &graph.values {
791        if field != "id" {
792            command = command.value(field.clone(), value.clone());
793        }
794    }
795    match dialect.compile_update(entity, &command) {
796        Ok(query) => Ok(Some(query)),
797        Err(SqlCompileError::EmptyMutation(_)) => Ok(None),
798        Err(err) => Err(err.into()),
799    }
800}
801
802pub(crate) async fn ensure_postgres_schema_for(
803    context: &UserContext,
804) -> Result<(), MutationExecutorError> {
805    let dialect = context.get_resource::<PostgresDialect>().ok_or_else(|| {
806        MutationExecutorError::Bind("missing typed resource: PostgresDialect".to_owned())
807    })?;
808    let executor = context
809        .get_resource::<PgMutationExecutor>()
810        .ok_or_else(|| {
811            MutationExecutorError::Bind("missing typed resource: PgMutationExecutor".to_owned())
812        })?;
813
814    let entities = context.all_entities();
815
816    executor.ensure_schema(dialect, &entities).await?;
817    ensure_initial_graphs_postgres(executor, dialect, context).await
818}
819
820#[cfg(test)]
821mod streaming_tests {
822    use super::*;
823    use futures_util::StreamExt;
824    use teaql_core::RelationDescriptor;
825    use teaql_sql::{SqlTransaction, SqlTransactionTransport, SqlTransport, StreamingSqlTransport};
826
827    fn configured_pool(url: String) -> Pool {
828        let mut config = deadpool_postgres::Config::new();
829        config.url = Some(url);
830        config
831            .create_pool(
832                Some(deadpool_postgres::Runtime::Tokio1),
833                tokio_postgres::NoTls,
834            )
835            .unwrap()
836    }
837
838    #[tokio::test]
839    async fn streams_from_real_postgres_when_configured() {
840        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
841            return;
842        };
843        let pool = configured_pool(url);
844        let executor = PgMutationExecutor::new(pool);
845        let query = CompiledQuery {
846            sql: "SELECT id FROM (VALUES (1), (2), (3), (4), (5)) AS fixture(id) ORDER BY id"
847                .to_owned(),
848            params: vec![],
849            comment: None,
850        };
851        let mut stream = executor.stream_sql(query, 2);
852        let mut sizes = Vec::new();
853        while let Some(chunk) = stream.next().await {
854            sizes.push(chunk.unwrap().rows.len());
855        }
856        assert_eq!(sizes, vec![2, 2, 1]);
857    }
858
859    #[tokio::test]
860    async fn transaction_commit_and_rollback_use_one_connection_when_configured() {
861        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
862            return;
863        };
864        let executor = PgMutationExecutor::new(configured_pool(url));
865        for sql in [
866            "DROP TABLE IF EXISTS teaql_transaction_runtime_fixture",
867            "CREATE TABLE teaql_transaction_runtime_fixture(id BIGINT PRIMARY KEY)",
868        ] {
869            executor
870                .execute_sql(&CompiledQuery {
871                    sql: sql.to_owned(),
872                    params: vec![],
873                    comment: None,
874                })
875                .await
876                .unwrap();
877        }
878
879        let rolled_back = executor.begin_sql().await.unwrap();
880        rolled_back
881            .execute_sql(&CompiledQuery {
882                sql: "INSERT INTO teaql_transaction_runtime_fixture(id) VALUES ($1)".to_owned(),
883                params: vec![Value::I64(1)],
884                comment: None,
885            })
886            .await
887            .unwrap();
888        rolled_back.rollback_sql().await.unwrap();
889
890        let committed = executor.begin_sql().await.unwrap();
891        committed
892            .execute_sql(&CompiledQuery {
893                sql: "INSERT INTO teaql_transaction_runtime_fixture(id) VALUES ($1)".to_owned(),
894                params: vec![Value::I64(2)],
895                comment: None,
896            })
897            .await
898            .unwrap();
899        committed.commit_sql().await.unwrap();
900
901        let rows = executor
902            .fetch_all_compact_sql(&CompiledQuery {
903                sql: "SELECT id FROM teaql_transaction_runtime_fixture ORDER BY id".to_owned(),
904                params: vec![],
905                comment: None,
906            })
907            .await
908            .unwrap();
909        assert_eq!(rows.len(), 1);
910        assert_eq!(rows[0].get("id"), Some(&Value::I64(2)));
911    }
912
913    #[tokio::test]
914    async fn boolean_roundtrips_real_postgres_when_configured() {
915        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
916            return;
917        };
918        let mut config = deadpool_postgres::Config::new();
919        config.url = Some(url);
920        let pool = config
921            .create_pool(
922                Some(deadpool_postgres::Runtime::Tokio1),
923                tokio_postgres::NoTls,
924            )
925            .unwrap();
926        let executor = PgMutationExecutor::new(pool);
927        executor
928            .execute_sql(&CompiledQuery {
929                sql: "DROP TABLE IF EXISTS teaql_boolean_runtime_fixture".to_owned(),
930                params: vec![],
931                comment: None,
932            })
933            .await
934            .unwrap();
935        executor
936            .execute_sql(&CompiledQuery {
937                sql: "CREATE TABLE teaql_boolean_runtime_fixture(id BIGINT, required_flag BOOLEAN NOT NULL, optional_flag BOOLEAN)".to_owned(),
938                params: vec![],
939                comment: None,
940            })
941            .await
942            .unwrap();
943        for (id, required_flag, optional_flag) in [
944            (1_i64, Value::Bool(false), Value::Bool(true)),
945            (2_i64, Value::Bool(true), Value::Bool(false)),
946            (3_i64, Value::Bool(true), Value::Null),
947        ] {
948            executor
949                .execute_sql(&CompiledQuery {
950                    sql: "INSERT INTO teaql_boolean_runtime_fixture VALUES ($1, $2, $3)".to_owned(),
951                    params: vec![Value::I64(id), required_flag, optional_flag],
952                    comment: None,
953                })
954                .await
955                .unwrap();
956        }
957        let rows = executor
958            .fetch_all_compact_sql(&CompiledQuery {
959                sql: "SELECT required_flag, optional_flag FROM teaql_boolean_runtime_fixture ORDER BY id".to_owned(),
960                params: vec![],
961                comment: None,
962            })
963            .await
964            .unwrap();
965        assert_eq!(rows[0].get("required_flag"), Some(&Value::Bool(false)));
966        assert_eq!(rows[0].get("optional_flag"), Some(&Value::Bool(true)));
967        assert_eq!(rows[1].get("required_flag"), Some(&Value::Bool(true)));
968        assert_eq!(rows[1].get("optional_flag"), Some(&Value::Bool(false)));
969        assert_eq!(rows[2].get("optional_flag"), Some(&Value::Null));
970        executor
971            .execute_sql(&CompiledQuery {
972                sql: "DROP TABLE teaql_boolean_runtime_fixture".to_owned(),
973                params: vec![],
974                comment: None,
975            })
976            .await
977            .unwrap();
978    }
979
980    #[tokio::test]
981    async fn teaql_long_binds_to_legacy_postgres_int4_scalars_and_arrays() {
982        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
983            return;
984        };
985        let pool = configured_pool(url);
986        let executor = PgMutationExecutor::new(pool);
987        for sql in [
988            "DROP TABLE IF EXISTS teaql_int4_binding_fixture",
989            "CREATE TABLE teaql_int4_binding_fixture(id INTEGER PRIMARY KEY)",
990        ] {
991            executor
992                .execute_sql(&CompiledQuery {
993                    sql: sql.to_owned(),
994                    params: vec![],
995                    comment: None,
996                })
997                .await
998                .unwrap();
999        }
1000        for id in [1_i64, i64::from(i32::MAX)] {
1001            executor
1002                .execute_sql(&CompiledQuery {
1003                    sql: "INSERT INTO teaql_int4_binding_fixture(id) VALUES ($1)".to_owned(),
1004                    params: vec![Value::I64(id)],
1005                    comment: None,
1006                })
1007                .await
1008                .unwrap();
1009        }
1010        let rows = executor
1011            .fetch_all_compact_sql(&CompiledQuery {
1012                sql: "SELECT id FROM teaql_int4_binding_fixture WHERE id = ANY($1) ORDER BY id"
1013                    .to_owned(),
1014                params: vec![Value::List(vec![
1015                    Value::U64(1),
1016                    Value::U64(i32::MAX as u64),
1017                ])],
1018                comment: None,
1019            })
1020            .await
1021            .unwrap();
1022        assert_eq!(rows.len(), 2);
1023        assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
1024
1025        let overflow = executor
1026            .fetch_all_compact_sql(&CompiledQuery {
1027                sql: "SELECT id FROM teaql_int4_binding_fixture WHERE id = $1".to_owned(),
1028                params: vec![Value::I64(i64::from(i32::MAX) + 1)],
1029                comment: None,
1030            })
1031            .await;
1032        assert!(overflow.is_err());
1033        executor
1034            .execute_sql(&CompiledQuery {
1035                sql: "DROP TABLE teaql_int4_binding_fixture".to_owned(),
1036                params: vec![],
1037                comment: None,
1038            })
1039            .await
1040            .unwrap();
1041    }
1042
1043    #[tokio::test]
1044    async fn topn_012_ensure_schema_creates_relation_index_idempotently() {
1045        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
1046            return;
1047        };
1048        let pool = configured_pool(url);
1049        let client = pool.get().await.unwrap();
1050        client
1051            .batch_execute("DROP TABLE IF EXISTS teaql_relation_index_fixture")
1052            .await
1053            .unwrap();
1054
1055        let entity = EntityDescriptor::new("RelationIndexFixture")
1056            .table_name("teaql_relation_index_fixture")
1057            .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1058            .property(PropertyDescriptor::new("version", DataType::I64).version())
1059            .property(PropertyDescriptor::new("vendor_id", DataType::U64).not_null())
1060            .relation(
1061                RelationDescriptor::new("vendor", "Vendor")
1062                    .local_key("vendor_id")
1063                    .foreign_key("id"),
1064            );
1065        let executor = PgMutationExecutor::new(pool.clone());
1066
1067        executor
1068            .ensure_schema(&PostgresDialect, &[&entity])
1069            .await
1070            .unwrap();
1071        executor
1072            .ensure_schema(&PostgresDialect, &[&entity])
1073            .await
1074            .unwrap();
1075
1076        let rows = client
1077            .query(
1078                "SELECT indexdef FROM pg_indexes WHERE schemaname = current_schema() AND tablename = 'teaql_relation_index_fixture' AND indexdef LIKE '%(vendor_id, id DESC)%'",
1079                &[],
1080            )
1081            .await
1082            .unwrap();
1083        assert_eq!(rows.len(), 1);
1084
1085        client
1086            .batch_execute("DROP TABLE teaql_relation_index_fixture")
1087            .await
1088            .unwrap();
1089    }
1090
1091    #[tokio::test]
1092    async fn ensure_schema_creates_foreign_key_once_by_semantics() {
1093        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
1094            return;
1095        };
1096        let pool = configured_pool(url);
1097        let client = pool.get().await.unwrap();
1098        client
1099            .batch_execute(
1100                "DROP TABLE IF EXISTS teaql_fk_child_fixture;
1101                 DROP TABLE IF EXISTS teaql_fk_parent_fixture;",
1102            )
1103            .await
1104            .unwrap();
1105
1106        let parent = EntityDescriptor::new("FkParentFixture")
1107            .table_name("teaql_fk_parent_fixture")
1108            .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1109            .relation(
1110                RelationDescriptor::new("children", "FkChildFixture")
1111                    .local_key("id")
1112                    .foreign_key("parent_id")
1113                    .many(),
1114            );
1115        let child = EntityDescriptor::new("FkChildFixture")
1116            .table_name("teaql_fk_child_fixture")
1117            .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1118            .property(PropertyDescriptor::new("parent_id", DataType::U64).not_null())
1119            .relation(
1120                RelationDescriptor::new("parent", "FkParentFixture")
1121                    .local_key("parent_id")
1122                    .foreign_key("id"),
1123            );
1124        let executor = PgMutationExecutor::new(pool.clone());
1125
1126        executor
1127            .ensure_schema(&PostgresDialect, &[&child, &parent])
1128            .await
1129            .unwrap();
1130        executor
1131            .ensure_schema(&PostgresDialect, &[&parent, &child])
1132            .await
1133            .unwrap();
1134
1135        let count: i64 = client
1136            .query_one(
1137                "SELECT COUNT(*)
1138                   FROM pg_constraint c
1139                   JOIN pg_class t ON t.oid = c.conrelid
1140                  WHERE c.contype = 'f'
1141                    AND t.relname = 'teaql_fk_child_fixture'",
1142                &[],
1143            )
1144            .await
1145            .unwrap()
1146            .try_get(0)
1147            .unwrap();
1148        assert_eq!(count, 1);
1149
1150        let violation = client
1151            .execute(
1152                "INSERT INTO teaql_fk_child_fixture(id, parent_id) VALUES (1, 999)",
1153                &[],
1154            )
1155            .await;
1156        assert!(violation.is_err());
1157
1158        client
1159            .batch_execute(
1160                "DROP TABLE teaql_fk_child_fixture;
1161                 DROP TABLE teaql_fk_parent_fixture;",
1162            )
1163            .await
1164            .unwrap();
1165    }
1166
1167    #[tokio::test]
1168    async fn temporal_debug_sql_matches_real_postgres_when_configured() {
1169        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
1170            return;
1171        };
1172        let mut config = deadpool_postgres::Config::new();
1173        config.url = Some(url);
1174        let pool = config
1175            .create_pool(
1176                Some(deadpool_postgres::Runtime::Tokio1),
1177                tokio_postgres::NoTls,
1178            )
1179            .unwrap();
1180        let executor = PgMutationExecutor::new(pool);
1181        executor
1182            .execute_sql(&CompiledQuery {
1183                sql: "DROP TABLE IF EXISTS teaql_temporal_runtime_fixture".to_owned(),
1184                params: vec![],
1185                comment: None,
1186            })
1187            .await
1188            .unwrap();
1189        executor.execute_sql(&CompiledQuery { sql: "CREATE TABLE teaql_temporal_runtime_fixture(id BIGINT, d DATE, t TIMESTAMPTZ(3), t_local TIMESTAMP(3))".to_owned(), params: vec![], comment: None }).await.unwrap();
1190        let prepared = CompiledQuery {
1191            sql: "INSERT INTO teaql_temporal_runtime_fixture VALUES ($1, $2, $3, TIMESTAMP '1960-01-02 03:04:05.678')".to_owned(),
1192            params: vec![
1193                Value::I64(1),
1194                Value::Date("2024-02-29".parse().unwrap()),
1195                Value::Timestamp(teaql_core::time::Timestamp(-315_521_754_322)),
1196            ],
1197            comment: Some("teaql source=temporal.verify $1".to_owned()),
1198        };
1199        executor.execute_sql(&prepared).await.unwrap();
1200        executor
1201            .execute_sql(&CompiledQuery {
1202                sql: prepared
1203                    .debug_sql(DatabaseKind::PostgreSql)
1204                    .replace("VALUES (1,", "VALUES (2,"),
1205                params: vec![],
1206                comment: None,
1207            })
1208            .await
1209            .unwrap();
1210        let rows = executor
1211            .fetch_all_compact_sql(&CompiledQuery {
1212                sql: "SELECT d, t, t_local FROM teaql_temporal_runtime_fixture ORDER BY id"
1213                    .to_owned(),
1214                params: vec![],
1215                comment: None,
1216            })
1217            .await
1218            .unwrap();
1219        assert_eq!(rows[0], rows[1]);
1220        executor
1221            .execute_sql(&CompiledQuery {
1222                sql: "DROP TABLE teaql_temporal_runtime_fixture".to_owned(),
1223                params: vec![],
1224                comment: None,
1225            })
1226            .await
1227            .unwrap();
1228    }
1229}
1230
1231#[derive(Debug, Default, Clone, Copy)]
1232pub struct PostgresSchemaProvider;
1233
1234impl SchemaProvider for PostgresSchemaProvider {
1235    fn ensure_schema<'a>(
1236        &'a self,
1237        context: &'a UserContext,
1238        _invocation: &'a teaql_runtime::SchemaInvocation,
1239    ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
1240        Box::pin(async move {
1241            ensure_postgres_schema_for(context)
1242                .await
1243                .map_err(|err| RuntimeError::Schema(err.to_string()))
1244        })
1245    }
1246}
1247
1248pub trait PostgresProviderExt {
1249    fn use_postgres_provider(&mut self, executor: PgMutationExecutor) -> &mut Self;
1250}
1251
1252impl PostgresProviderExt for UserContext {
1253    fn use_postgres_provider(&mut self, executor: PgMutationExecutor) -> &mut Self {
1254        self.insert_resource(PostgresDialect);
1255        self.insert_resource(executor);
1256        self.set_schema_provider(PostgresSchemaProvider);
1257        self
1258    }
1259}
1260
1261#[derive(Clone)]
1262pub struct PgIdSpaceGenerator {
1263    pool: Pool,
1264    table_name: String,
1265}
1266
1267impl PgIdSpaceGenerator {
1268    pub fn new(pool: Pool) -> Self {
1269        Self {
1270            pool,
1271            table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
1272        }
1273    }
1274
1275    pub fn from_executor(executor: PgMutationExecutor) -> Self {
1276        Self::new(executor.pool())
1277    }
1278
1279    pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
1280        self.table_name = table_name.into();
1281        self
1282    }
1283
1284    pub async fn ensure_table(&self) -> Result<(), MutationExecutorError> {
1285        PgMutationExecutor::new(self.pool.clone())
1286            .ensure_id_space_table(&self.table_name)
1287            .await
1288    }
1289
1290    pub async fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
1291        let entity = canonical_id_space_entity(entity);
1292        let entity = entity.as_str();
1293        self.ensure_table().await?;
1294        let table = quote_ident(&self.table_name);
1295        let client = self
1296            .pool
1297            .get()
1298            .await
1299            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
1300        let select_sql = format!("SELECT current_level FROM {table} WHERE type_name = $1");
1301        let insert_sql = format!("INSERT INTO {table}(type_name, current_level) VALUES ($1, 1)");
1302        let update_sql = format!(
1303            "UPDATE {table} SET current_level = $1 WHERE type_name = $2 AND current_level = $3"
1304        );
1305        for _ in 1..=100 {
1306            let current = client
1307                .query_opt(&select_sql, &[&entity])
1308                .await?
1309                .map(|row| row.try_get::<_, i64>(0))
1310                .transpose()?;
1311            if let Some(current) = current {
1312                let next = current.checked_add(1).ok_or_else(|| {
1313                    MutationExecutorError::Bind(format!("ID space overflow for {entity}"))
1314                })?;
1315                if client
1316                    .execute(&update_sql, &[&next, &entity, &current])
1317                    .await?
1318                    == 1
1319                {
1320                    return u64::try_from(next).map_err(|_| {
1321                        MutationExecutorError::Bind(format!(
1322                            "generated id {next} cannot be represented as u64"
1323                        ))
1324                    });
1325                }
1326            } else {
1327                match client.execute(&insert_sql, &[&entity]).await {
1328                    Ok(1) => return Ok(1),
1329                    Ok(changed) => {
1330                        return Err(MutationExecutorError::Bind(format!(
1331                            "ID space insert for {entity} changed {changed} rows"
1332                        )));
1333                    }
1334                    Err(error) => {
1335                        if client.query_opt(&select_sql, &[&entity]).await?.is_none() {
1336                            return Err(error.into());
1337                        }
1338                    }
1339                }
1340            }
1341        }
1342        Err(MutationExecutorError::Bind(format!(
1343            "Unable to allocate ID for {entity} after 100 optimistic-lock attempts"
1344        )))
1345    }
1346
1347    pub async fn ensure_floor(
1348        &self,
1349        entity: &str,
1350        floor: u64,
1351    ) -> Result<(), MutationExecutorError> {
1352        let entity = canonical_id_space_entity(entity);
1353        let entity = entity.as_str();
1354        self.ensure_table().await?;
1355        let floor = i64::try_from(floor).map_err(|_| {
1356            MutationExecutorError::Bind(format!(
1357                "ID space floor {floor} for {entity} exceeds BIGINT"
1358            ))
1359        })?;
1360        let table = quote_ident(&self.table_name);
1361        let client = self
1362            .pool
1363            .get()
1364            .await
1365            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
1366        let select = format!("SELECT current_level FROM {table} WHERE type_name = $1");
1367        let insert = format!("INSERT INTO {table}(type_name, current_level) VALUES ($1, $2)");
1368        let update = format!(
1369            "UPDATE {table} SET current_level = $1 WHERE type_name = $2 AND current_level = $3"
1370        );
1371        for _ in 1..=100 {
1372            let current = client
1373                .query_opt(&select, &[&entity])
1374                .await?
1375                .map(|row| row.try_get::<_, i64>(0))
1376                .transpose()?;
1377            match current {
1378                Some(current) if current >= floor => return Ok(()),
1379                Some(current) => {
1380                    if client
1381                        .execute(&update, &[&floor, &entity, &current])
1382                        .await?
1383                        == 1
1384                    {
1385                        return Ok(());
1386                    }
1387                }
1388                None => match client.execute(&insert, &[&entity, &floor]).await {
1389                    Ok(1) => return Ok(()),
1390                    Ok(_) => {}
1391                    Err(error) => {
1392                        if client.query_opt(&select, &[&entity]).await?.is_none() {
1393                            return Err(error.into());
1394                        }
1395                    }
1396                },
1397            }
1398        }
1399        Err(MutationExecutorError::Bind(format!(
1400            "Unable to synchronize ID space floor for {entity} after 100 optimistic-lock attempts"
1401        )))
1402    }
1403}
1404
1405impl InternalIdGenerator for PgIdSpaceGenerator {
1406    fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
1407        let generator = self.clone();
1408        let entity = entity.to_owned();
1409        block_on_id_generation(async move { generator.next_id(&entity).await })
1410    }
1411
1412    fn ensure_floor(&self, entity: &str, floor: u64) -> Result<(), RuntimeError> {
1413        let generator = self.clone();
1414        let entity = entity.to_owned();
1415        block_on_id_generation(async move { generator.ensure_floor(&entity, floor).await })
1416    }
1417}
1418
1419fn block_on_id_generation<T, F>(future: F) -> Result<T, RuntimeError>
1420where
1421    T: Send + 'static,
1422    F: Future<Output = Result<T, MutationExecutorError>> + Send + 'static,
1423{
1424    let result = match tokio::runtime::Handle::try_current() {
1425        Ok(handle) => tokio::task::block_in_place(|| handle.block_on(future)),
1426        Err(_) => tokio::runtime::Builder::new_current_thread()
1427            .enable_all()
1428            .build()
1429            .map_err(|err| RuntimeError::IdGeneration(err.to_string()))?
1430            .block_on(future),
1431    };
1432    result.map_err(|err| RuntimeError::IdGeneration(err.to_string()))
1433}
1434
1435fn quote_ident(ident: &str) -> String {
1436    quote_identifier_if_needed(ident, '"')
1437}
1438
1439/// Strip wrapping identifier quotes from a SQL identifier so that bare column
1440/// names returned by `information_schema.columns` can be compared with
1441/// potentially-quoted `PropertyDescriptor::column_name` values.
1442fn strip_identifier_quotes(ident: &str) -> &str {
1443    let bytes = ident.as_bytes();
1444    if bytes.len() >= 2 {
1445        let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
1446        if (first == b'"' && last == b'"')
1447            || (first == b'`' && last == b'`')
1448            || (first == b'[' && last == b']')
1449        {
1450            return &ident[1..ident.len() - 1];
1451        }
1452    }
1453    ident
1454}
1455
1456fn try_parse_datetime_from_str(s: &str) -> Option<chrono::DateTime<chrono::Utc>> {
1457    if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
1458        return Some(dt.with_timezone(&chrono::Utc));
1459    }
1460    if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
1461        return Some(chrono::DateTime::from_naive_utc_and_offset(
1462            ndt,
1463            chrono::Utc,
1464        ));
1465    }
1466    if let Ok(nd) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
1467        let ndt = nd.and_hms_opt(0, 0, 0)?;
1468        return Some(chrono::DateTime::from_naive_utc_and_offset(
1469            ndt,
1470            chrono::Utc,
1471        ));
1472    }
1473    None
1474}
1475
1476#[derive(Debug, Clone, Copy)]
1477struct PgNull;
1478
1479impl tokio_postgres::types::ToSql for PgNull {
1480    fn to_sql(
1481        &self,
1482        ty: &tokio_postgres::types::Type,
1483        out: &mut bytes::BytesMut,
1484    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1485        Ok(tokio_postgres::types::IsNull::Yes)
1486    }
1487
1488    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1489        true
1490    }
1491
1492    fn to_sql_checked(
1493        &self,
1494        ty: &tokio_postgres::types::Type,
1495        out: &mut bytes::BytesMut,
1496    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1497        Ok(tokio_postgres::types::IsNull::Yes)
1498    }
1499}
1500
1501#[derive(Debug, Clone, Copy)]
1502struct PgTimestamp(DateTime<Utc>);
1503
1504impl tokio_postgres::types::ToSql for PgTimestamp {
1505    fn to_sql(
1506        &self,
1507        ty: &tokio_postgres::types::Type,
1508        out: &mut bytes::BytesMut,
1509    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1510        if *ty == tokio_postgres::types::Type::TIMESTAMP {
1511            self.0.naive_utc().to_sql(ty, out)
1512        } else {
1513            self.0.to_sql(ty, out)
1514        }
1515    }
1516
1517    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1518        *ty == tokio_postgres::types::Type::TIMESTAMP
1519            || *ty == tokio_postgres::types::Type::TIMESTAMPTZ
1520    }
1521
1522    tokio_postgres::types::to_sql_checked!();
1523}
1524
1525#[derive(Debug, Clone, Copy)]
1526struct PgInteger(i64);
1527
1528impl tokio_postgres::types::ToSql for PgInteger {
1529    fn to_sql(
1530        &self,
1531        ty: &tokio_postgres::types::Type,
1532        out: &mut bytes::BytesMut,
1533    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1534        match *ty {
1535            tokio_postgres::types::Type::INT2 => i16::try_from(self.0)?.to_sql(ty, out),
1536            tokio_postgres::types::Type::INT4 => i32::try_from(self.0)?.to_sql(ty, out),
1537            tokio_postgres::types::Type::INT8 => self.0.to_sql(ty, out),
1538            _ => Err(format!("integer cannot be encoded as PostgreSQL type {ty}").into()),
1539        }
1540    }
1541
1542    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1543        matches!(
1544            *ty,
1545            tokio_postgres::types::Type::INT2
1546                | tokio_postgres::types::Type::INT4
1547                | tokio_postgres::types::Type::INT8
1548        )
1549    }
1550
1551    tokio_postgres::types::to_sql_checked!();
1552}
1553
1554#[derive(Debug, Clone)]
1555struct PgIntegerList(Vec<i64>);
1556
1557impl tokio_postgres::types::ToSql for PgIntegerList {
1558    fn to_sql(
1559        &self,
1560        ty: &tokio_postgres::types::Type,
1561        out: &mut bytes::BytesMut,
1562    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1563        match *ty {
1564            tokio_postgres::types::Type::INT2_ARRAY => self
1565                .0
1566                .iter()
1567                .copied()
1568                .map(i16::try_from)
1569                .collect::<Result<Vec<_>, _>>()?
1570                .to_sql(ty, out),
1571            tokio_postgres::types::Type::INT4_ARRAY => self
1572                .0
1573                .iter()
1574                .copied()
1575                .map(i32::try_from)
1576                .collect::<Result<Vec<_>, _>>()?
1577                .to_sql(ty, out),
1578            tokio_postgres::types::Type::INT8_ARRAY => self.0.to_sql(ty, out),
1579            _ => Err(format!("integer list cannot be encoded as PostgreSQL type {ty}").into()),
1580        }
1581    }
1582
1583    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1584        matches!(
1585            *ty,
1586            tokio_postgres::types::Type::INT2_ARRAY
1587                | tokio_postgres::types::Type::INT4_ARRAY
1588                | tokio_postgres::types::Type::INT8_ARRAY
1589        )
1590    }
1591
1592    tokio_postgres::types::to_sql_checked!();
1593}
1594
1595struct PgArgs {
1596    values: Vec<Box<dyn tokio_postgres::types::ToSql + Sync + Send>>,
1597}
1598impl PgArgs {
1599    fn add<T: tokio_postgres::types::ToSql + Sync + Send + 'static>(&mut self, v: T) {
1600        self.values.push(Box::new(v));
1601    }
1602    fn as_refs(&self) -> Vec<&(dyn tokio_postgres::types::ToSql + Sync)> {
1603        self.values.iter().map(|b| b.as_ref() as _).collect()
1604    }
1605}
1606
1607fn bind_pg(args: &mut PgArgs, value: &Value) -> Result<(), MutationExecutorError> {
1608    match value {
1609        Value::Null => {
1610            args.add(PgNull);
1611        }
1612        Value::Bool(v) => args.add(*v),
1613        Value::I64(v) => args.add(PgInteger(*v)),
1614        Value::U64(v) => {
1615            let v = i64::try_from(*v).map_err(|_| {
1616                MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))
1617            })?;
1618            args.add(PgInteger(v));
1619        }
1620        Value::F64(v) => args.add(*v),
1621        Value::Decimal(v) => args.add(*v),
1622        Value::Text(v) => match try_parse_datetime_from_str(v) {
1623            Some(dt) => args.add(dt),
1624            None => args.add(v.clone()),
1625        },
1626        Value::Json(v) => {
1627            let j_val: serde_json::Value =
1628                serde_json::to_value(v).map_err(|e| MutationExecutorError::Bind(e.to_string()))?;
1629            args.add(j_val);
1630        }
1631        Value::Date(v) => args.add(*v),
1632        Value::Timestamp(v) => args.add(PgTimestamp(v.to_datetime())),
1633        Value::Object(_) => return Err(MutationExecutorError::UnsupportedValue("object")),
1634        Value::List(values) => bind_pg_list(args, values)?,
1635        Value::TypedNull(dt) => match dt {
1636            DataType::Bool => args.add(Option::<bool>::None),
1637            DataType::I64 | DataType::U64 => args.add(Option::<i64>::None),
1638            DataType::F64 => args.add(Option::<f64>::None),
1639            DataType::Decimal => args.add(Option::<Decimal>::None),
1640            DataType::Text | DataType::LargeText => args.add(Option::<String>::None),
1641            DataType::Json => args.add(Option::<serde_json::Value>::None),
1642            DataType::Date => args.add(Option::<NaiveDate>::None),
1643            DataType::Timestamp => args.add(PgNull),
1644        },
1645    }
1646    Ok(())
1647}
1648
1649fn bind_pg_list(args: &mut PgArgs, values: &[Value]) -> Result<(), MutationExecutorError> {
1650    let Some(first) = values.first() else {
1651        return Err(MutationExecutorError::UnsupportedValue("empty list"));
1652    };
1653    match first {
1654        Value::Bool(_) => {
1655            let values = values
1656                .iter()
1657                .map(|value| match value {
1658                    Value::Bool(value) => Ok(*value),
1659                    _ => Err(MutationExecutorError::UnsupportedValue("mixed bool list")),
1660                })
1661                .collect::<Result<Vec<_>, _>>()?;
1662            args.add(values);
1663        }
1664        Value::I64(_) => {
1665            let values = values
1666                .iter()
1667                .map(|value| match value {
1668                    Value::I64(value) => Ok(*value),
1669                    _ => Err(MutationExecutorError::UnsupportedValue("mixed i64 list")),
1670                })
1671                .collect::<Result<Vec<_>, _>>()?;
1672            args.add(PgIntegerList(values));
1673        }
1674        Value::U64(_) => {
1675            let values = values
1676                .iter()
1677                .map(|value| match value {
1678                    Value::U64(value) => i64::try_from(*value).map_err(|_| {
1679                        MutationExecutorError::Bind(format!("u64 value {value} exceeds i64 range"))
1680                    }),
1681                    _ => Err(MutationExecutorError::UnsupportedValue("mixed u64 list")),
1682                })
1683                .collect::<Result<Vec<_>, _>>()?;
1684            args.add(PgIntegerList(values));
1685        }
1686        Value::F64(_) => {
1687            let values = values
1688                .iter()
1689                .map(|value| match value {
1690                    Value::F64(value) => Ok(*value),
1691                    _ => Err(MutationExecutorError::UnsupportedValue("mixed f64 list")),
1692                })
1693                .collect::<Result<Vec<_>, _>>()?;
1694            args.add(values);
1695        }
1696        Value::Decimal(_) => {
1697            let values = values
1698                .iter()
1699                .map(|value| match value {
1700                    Value::Decimal(value) => Ok(*value),
1701                    _ => Err(MutationExecutorError::UnsupportedValue(
1702                        "mixed decimal list",
1703                    )),
1704                })
1705                .collect::<Result<Vec<_>, _>>()?;
1706            args.add(values);
1707        }
1708        Value::Text(_) => {
1709            let values = values
1710                .iter()
1711                .map(|value| match value {
1712                    Value::Text(value) => Ok(value.clone()),
1713                    _ => Err(MutationExecutorError::UnsupportedValue("mixed text list")),
1714                })
1715                .collect::<Result<Vec<_>, _>>()?;
1716            args.add(values);
1717        }
1718        Value::Date(_) => {
1719            let values = values
1720                .iter()
1721                .map(|value| match value {
1722                    Value::Date(value) => Ok(*value),
1723                    _ => Err(MutationExecutorError::UnsupportedValue("mixed date list")),
1724                })
1725                .collect::<Result<Vec<_>, _>>()?;
1726            args.add(values);
1727        }
1728        Value::Timestamp(_) => {
1729            let values = values
1730                .iter()
1731                .map(|value| match value {
1732                    Value::Timestamp(value) => Ok(value.to_datetime()),
1733                    _ => Err(MutationExecutorError::UnsupportedValue(
1734                        "mixed timestamp list",
1735                    )),
1736                })
1737                .collect::<Result<Vec<_>, _>>()?;
1738            args.add(values);
1739        }
1740        Value::Null => return Err(MutationExecutorError::UnsupportedValue("null list")),
1741        Value::Json(_) => return Err(MutationExecutorError::UnsupportedValue("json list")),
1742        Value::Object(_) => return Err(MutationExecutorError::UnsupportedValue("object list")),
1743        Value::List(_) => return Err(MutationExecutorError::UnsupportedValue("nested list")),
1744        Value::TypedNull(_) => return Err(MutationExecutorError::UnsupportedValue("null list")),
1745    }
1746    Ok(())
1747}
1748
1749fn decode_pg_values(row: &tokio_postgres::Row) -> Result<Vec<Value>, MutationExecutorError> {
1750    let mut values = Vec::with_capacity(row.len());
1751    for (index, column) in row.columns().iter().enumerate() {
1752        let type_name = column.type_().name();
1753
1754        let value = match type_name {
1755            "bool" | "boolean" => {
1756                let v: Option<bool> = row.try_get(index)?;
1757                match v {
1758                    Some(v) => Value::Bool(v),
1759                    None => Value::Null,
1760                }
1761            }
1762            "int2" => {
1763                let v: Option<i16> = row.try_get(index)?;
1764                match v {
1765                    Some(v) => Value::I64(v as i64),
1766                    None => Value::Null,
1767                }
1768            }
1769            "int4" => {
1770                let v: Option<i32> = row.try_get(index)?;
1771                match v {
1772                    Some(v) => Value::I64(v as i64),
1773                    None => Value::Null,
1774                }
1775            }
1776            "int8" => {
1777                let v: Option<i64> = row.try_get(index)?;
1778                match v {
1779                    Some(v) => Value::I64(v),
1780                    None => Value::Null,
1781                }
1782            }
1783            "float4" => {
1784                let v: Option<f32> = row.try_get(index)?;
1785                match v {
1786                    Some(v) => Value::F64(v as f64),
1787                    None => Value::Null,
1788                }
1789            }
1790            "float8" => {
1791                let v: Option<f64> = row.try_get(index)?;
1792                match v {
1793                    Some(v) => Value::F64(v),
1794                    None => Value::Null,
1795                }
1796            }
1797            "numeric" => {
1798                let v: Option<Decimal> = row.try_get(index)?;
1799                match v {
1800                    Some(v) => Value::Decimal(v),
1801                    None => Value::Null,
1802                }
1803            }
1804            "json" | "jsonb" => {
1805                let v: Option<serde_json::Value> = row.try_get(index)?;
1806                match v {
1807                    Some(j) => Value::Json(j.into()),
1808                    None => Value::Null,
1809                }
1810            }
1811            "date" => {
1812                let v: Option<NaiveDate> = row.try_get(index)?;
1813                match v {
1814                    Some(v) => Value::Date(v),
1815                    None => Value::Null,
1816                }
1817            }
1818            "timestamp" => {
1819                let v: Option<NaiveDateTime> = row.try_get(index)?;
1820                match v {
1821                    Some(v) => Value::Timestamp(teaql_core::time::Timestamp(
1822                        v.and_utc().timestamp_millis(),
1823                    )),
1824                    None => Value::Null,
1825                }
1826            }
1827            "timestamptz" => {
1828                let v: Option<DateTime<Utc>> = row.try_get(index)?;
1829                match v {
1830                    Some(v) => Value::Timestamp(teaql_core::time::Timestamp(v.timestamp_millis())),
1831                    None => Value::Null,
1832                }
1833            }
1834            "text" | "varchar" | "bpchar" | "name" | "uuid" => {
1835                let v: Option<String> = row.try_get(index)?;
1836                match v {
1837                    Some(v) => Value::Text(v),
1838                    None => Value::Null,
1839                }
1840            }
1841            other => {
1842                return Err(MutationExecutorError::UnsupportedColumnType(
1843                    other.to_owned(),
1844                ));
1845            }
1846        };
1847        values.push(value);
1848    }
1849    Ok(values)
1850}
1851
1852#[cfg(test)]
1853mod tests {
1854    use super::*;
1855    use teaql_core::{DeleteCommand, RecoverCommand, RelationDescriptor};
1856
1857    fn entity() -> EntityDescriptor {
1858        EntityDescriptor::new("Order")
1859            .table_name("orders")
1860            .property(
1861                PropertyDescriptor::new("id", DataType::U64)
1862                    .column_name("id")
1863                    .id()
1864                    .not_null(),
1865            )
1866            .property(
1867                PropertyDescriptor::new("version", DataType::I64)
1868                    .column_name("version")
1869                    .version()
1870                    .not_null(),
1871            )
1872            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1873    }
1874
1875    #[test]
1876    fn postgres_dialect_compiles_mutations_with_numbered_placeholders() {
1877        let insert = PostgresDialect
1878            .compile_insert(
1879                &entity(),
1880                &InsertCommand::new("Order")
1881                    .value("id", 1_u64)
1882                    .value("name", "A"),
1883            )
1884            .unwrap();
1885        assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES ($1, $2)");
1886
1887        let update = PostgresDialect
1888            .compile_update(
1889                &entity(),
1890                &UpdateCommand::new("Order", 1_u64)
1891                    .expected_version(3)
1892                    .value("name", "B"),
1893            )
1894            .unwrap();
1895        assert_eq!(
1896            update.sql,
1897            "UPDATE orders SET name = $1, version = $2 WHERE id = $3 AND version = $4"
1898        );
1899
1900        let delete = PostgresDialect
1901            .compile_delete(
1902                &entity(),
1903                &DeleteCommand::new("Order", 1_u64).expected_version(3),
1904            )
1905            .unwrap();
1906        let recover = PostgresDialect
1907            .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1908            .unwrap();
1909        assert_eq!(
1910            delete.sql,
1911            "UPDATE orders SET version = $1 WHERE id = $2 AND version = $3"
1912        );
1913        assert_eq!(
1914            recover.sql,
1915            "UPDATE orders SET version = $1 WHERE id = $2 AND version = $3"
1916        );
1917    }
1918
1919    #[test]
1920    fn postgres_dialect_compiles_schema_and_large_in_array_binds() {
1921        let create = PostgresDialect.compile_create_table(&entity()).unwrap();
1922        assert_eq!(
1923            create,
1924            "CREATE TABLE IF NOT EXISTS orders (id BIGINT PRIMARY KEY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255))"
1925        );
1926        assert!(
1927            PostgresDialect
1928                .schema_setup_sqls()
1929                .iter()
1930                .any(|sql| sql.contains("CREATE OR REPLACE FUNCTION soundex"))
1931        );
1932
1933        let values = (1_u64..=21).map(Value::from).collect::<Vec<_>>();
1934        let query = PostgresDialect
1935            .compile_select(
1936                &entity(),
1937                &SelectQuery::new("Order")
1938                    .filter(Expr::in_list("id", values.clone()))
1939                    .order_asc("id"),
1940            )
1941            .unwrap();
1942        assert_eq!(
1943            query.sql,
1944            "SELECT id, version, name FROM orders WHERE (id = ANY($1)) ORDER BY id ASC"
1945        );
1946        assert_eq!(query.params, vec![Value::List(values)]);
1947    }
1948
1949    #[test]
1950    fn topn_012_postgres_schema_adds_full_foreign_key_id_desc_index() {
1951        let trip = EntityDescriptor::new("Trip")
1952            .table_name("trip_data")
1953            .property(
1954                PropertyDescriptor::new("id", DataType::U64)
1955                    .column_name("id")
1956                    .id()
1957                    .not_null(),
1958            )
1959            .property(
1960                PropertyDescriptor::new("vendor_id", DataType::U64)
1961                    .column_name("vendor")
1962                    .not_null(),
1963            )
1964            .relation(
1965                RelationDescriptor::new("vendor", "Vendor")
1966                    .local_key("vendor_id")
1967                    .foreign_key("id"),
1968            )
1969            // A second relation through the same key must not duplicate DDL.
1970            .relation(
1971                RelationDescriptor::new("billing_vendor", "Vendor")
1972                    .local_key("vendor_id")
1973                    .foreign_key("id"),
1974            )
1975            // Reverse relations belong to the target table and are ignored here.
1976            .relation(
1977                RelationDescriptor::new("items", "TripItem")
1978                    .local_key("id")
1979                    .foreign_key("trip_id")
1980                    .many(),
1981            );
1982
1983        assert_eq!(
1984            PostgresDialect.relation_indexes_sqls(&trip),
1985            vec![
1986                "CREATE INDEX IF NOT EXISTS IDX_TRIP_DATA_VENDOR_ID_DESC ON trip_data (vendor, id DESC)"
1987            ]
1988        );
1989    }
1990
1991    #[test]
1992    fn postgres_relation_index_name_is_stable_and_within_identifier_limit() {
1993        let name = postgres_index_name(
1994            "an_extremely_long_generated_transaction_history_table_name",
1995            "an_equally_long_business_owner_reference_identifier",
1996            "id",
1997        );
1998        assert!(name.len() <= 63);
1999        assert_eq!(
2000            name,
2001            postgres_index_name(
2002                "an_extremely_long_generated_transaction_history_table_name",
2003                "an_equally_long_business_owner_reference_identifier",
2004                "id",
2005            )
2006        );
2007        assert!(name.ends_with("_889B21BBED38CC82"));
2008    }
2009}