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
319impl SqlTransport for PgMutationExecutor {
320    type Error = MutationExecutorError;
321
322    async fn fetch_all_compact_sql(
323        &self,
324        query: &CompiledQuery,
325    ) -> Result<Vec<teaql_core::CompactRow>, Self::Error> {
326        let mut args = PgArgs { values: Vec::new() };
327        for value in &query.params {
328            bind_pg(&mut args, value)?;
329        }
330        let client = self
331            .pool
332            .get()
333            .await
334            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
335        let statement = client.prepare_cached(&query.sql).await?;
336        let rows = client.query(&statement, &args.as_refs()).await?;
337        let columns: std::sync::Arc<[String]> = statement
338            .columns()
339            .iter()
340            .map(|column| column.name().to_owned())
341            .collect::<Vec<_>>()
342            .into();
343        rows.iter()
344            .map(|row| {
345                Ok(teaql_core::CompactRow::new(
346                    columns.clone(),
347                    decode_pg_values(row)?,
348                ))
349            })
350            .collect()
351    }
352
353    async fn execute_sql(&self, query: &CompiledQuery) -> Result<u64, Self::Error> {
354        self.execute(query).await
355    }
356}
357
358impl teaql_sql::StreamingSqlTransport for PgMutationExecutor {
359    fn stream_sql(
360        &self,
361        query: CompiledQuery,
362        chunk_size: usize,
363    ) -> teaql_data_service::QueryStream<'_, Self::Error> {
364        let pool = self.pool.clone();
365        Box::pin(async_stream::try_stream! {
366            use futures_util::TryStreamExt;
367            let mut args = PgArgs { values: Vec::new() }; for value in &query.params { bind_pg(&mut args, value)?; }
368            let client = pool.get().await.map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
369            let params = args.as_refs();
370            let statement = client.prepare_cached(&query.sql).await?;
371            let columns: std::sync::Arc<[String]> = statement.columns().iter().map(|column| column.name().to_owned()).collect::<Vec<_>>().into();
372            let rows = client.query_raw(&statement, params).await?;
373            futures_util::pin_mut!(rows);
374            let mut chunk = Vec::with_capacity(chunk_size); let mut index = 0;
375            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; } }
376            if !chunk.is_empty() { yield teaql_data_service::StreamChunk { rows:chunk, chunk_index:index, is_last:true }; }
377        })
378    }
379}
380
381impl teaql_sql::SqlTransaction for PgMutationExecutor {
382    type Error = MutationExecutorError;
383
384    async fn commit_sql(self) -> Result<(), Self::Error> {
385        Err(MutationExecutorError::Bind(
386            "Transactions not supported yet".to_string(),
387        ))
388    }
389
390    async fn rollback_sql(self) -> Result<(), Self::Error> {
391        Err(MutationExecutorError::Bind(
392            "Transactions not supported yet".to_string(),
393        ))
394    }
395}
396
397impl teaql_sql::SqlTransactionTransport for PgMutationExecutor {
398    type Tx<'a>
399        = Self
400    where
401        Self: 'a;
402
403    async fn begin_sql(&self) -> Result<Self::Tx<'_>, Self::Error> {
404        Err(MutationExecutorError::Bind(
405            "Transactions not supported yet".to_string(),
406        ))
407    }
408}
409
410impl PgMutationExecutor {
411    pub fn new(pool: Pool) -> Self {
412        Self { pool }
413    }
414
415    pub fn pool(&self) -> Pool {
416        self.pool.clone()
417    }
418
419    pub async fn ensure_schema(
420        &self,
421        dialect: &PostgresDialect,
422        entities: &[&EntityDescriptor],
423    ) -> Result<(), MutationExecutorError> {
424        let mut client = self
425            .pool
426            .get()
427            .await
428            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
429        {
430            let transaction = client.transaction().await?;
431            transaction
432                .query_one(
433                    "SELECT pg_advisory_xact_lock(hashtextextended('teaql-schema-setup', 0))",
434                    &[],
435                )
436                .await?;
437            for sql in dialect.schema_setup_sqls() {
438                transaction.execute(*sql, &[]).await?;
439            }
440            transaction.commit().await?;
441        }
442        self.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE).await?;
443
444        for entity in entities {
445            if !self.table_exists(&entity.table_name).await? {
446                let sql = dialect.compile_create_table(entity)?;
447                client.execute(&sql, &[]).await?;
448            } else {
449                let existing_columns = self.table_columns(&entity.table_name).await?;
450                for property in &entity.properties {
451                    let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
452                    if existing_columns.contains(&bare_column) {
453                        continue;
454                    }
455                    let sql = dialect.compile_add_column(entity, property)?;
456                    client.execute(&sql, &[]).await?;
457                }
458            }
459
460            for sql in dialect.schema_indexes_sqls(entity)? {
461                client.execute(&sql, &[]).await?;
462            }
463            for sql in dialect.relation_indexes_sqls(entity) {
464                client.execute(&sql, &[]).await?;
465            }
466        }
467
468        // Install constraints only after every table and column exists, so
469        // descriptor registration order does not affect schema creation.
470        for entity in entities {
471            for relation in &entity.relations {
472                let Some(target) = entities
473                    .iter()
474                    .copied()
475                    .find(|candidate| candidate.name == relation.target_entity)
476                else {
477                    // A module may intentionally reference an entity supplied by
478                    // another module or service. That remains a logical relation.
479                    continue;
480                };
481                if entity.data_service != target.data_service {
482                    // Cross-data-source relations cannot be represented by a
483                    // database-local foreign-key constraint.
484                    continue;
485                }
486                let (source, source_key, referenced, referenced_key) = if relation.many {
487                    (target, &relation.foreign_key, *entity, &relation.local_key)
488                } else {
489                    (*entity, &relation.local_key, target, &relation.foreign_key)
490                };
491                let source_property = source.property_by_name(source_key).ok_or_else(|| {
492                    MutationExecutorError::Bind(format!(
493                        "cannot ensure relation {}.{}: source key {}.{} does not exist",
494                        entity.name, relation.name, source.name, source_key
495                    ))
496                })?;
497                let referenced_property =
498                    referenced.property_by_name(referenced_key).ok_or_else(|| {
499                        MutationExecutorError::Bind(format!(
500                            "cannot ensure relation {}.{}: referenced key {}.{} does not exist",
501                            entity.name, relation.name, referenced.name, referenced_key
502                        ))
503                    })?;
504                self.ensure_foreign_key(
505                    &source.table_name,
506                    &source_property.column_name,
507                    &referenced.table_name,
508                    &referenced_property.column_name,
509                )
510                .await?;
511            }
512        }
513        Ok(())
514    }
515
516    async fn ensure_foreign_key(
517        &self,
518        source_table: &str,
519        source_column: &str,
520        referenced_table: &str,
521        referenced_column: &str,
522    ) -> Result<(), MutationExecutorError> {
523        let semantic_key = format!(
524            "teaql-fk:{source_table}:{source_column}:{referenced_table}:{referenced_column}:a:a"
525        );
526        let mut client = self
527            .pool
528            .get()
529            .await
530            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
531        let transaction = client.transaction().await?;
532        transaction
533            .query_one(
534                "SELECT pg_advisory_xact_lock(hashtextextended($1, 0))",
535                &[&semantic_key],
536            )
537            .await?;
538        let exists: bool = transaction
539            .query_one(
540                "SELECT EXISTS (
541                    SELECT 1
542                      FROM pg_constraint c
543                      JOIN pg_class st ON st.oid = c.conrelid
544                      JOIN pg_namespace sn ON sn.oid = st.relnamespace
545                      JOIN pg_class rt ON rt.oid = c.confrelid
546                      JOIN pg_namespace rn ON rn.oid = rt.relnamespace
547                      JOIN pg_attribute sc ON sc.attrelid = c.conrelid AND sc.attnum = c.conkey[1]
548                      JOIN pg_attribute rc ON rc.attrelid = c.confrelid AND rc.attnum = c.confkey[1]
549                     WHERE c.contype = 'f'
550                       AND sn.nspname = current_schema()
551                       AND rn.nspname = current_schema()
552                       AND st.relname = $1 AND sc.attname = $2
553                       AND rt.relname = $3 AND rc.attname = $4
554                       AND cardinality(c.conkey) = 1 AND cardinality(c.confkey) = 1
555                       AND c.confupdtype = 'a' AND c.confdeltype = 'a'
556                )",
557                &[
558                    &strip_identifier_quotes(source_table),
559                    &strip_identifier_quotes(source_column),
560                    &strip_identifier_quotes(referenced_table),
561                    &strip_identifier_quotes(referenced_column),
562                ],
563            )
564            .await?
565            .try_get(0)?;
566        if !exists {
567            let constraint_name = postgres_foreign_key_name(
568                source_table,
569                source_column,
570                referenced_table,
571                referenced_column,
572            );
573            let sql = format!(
574                "ALTER TABLE {} ADD CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({})",
575                quote_ident(source_table),
576                quote_ident(&constraint_name),
577                quote_ident(source_column),
578                quote_ident(referenced_table),
579                quote_ident(referenced_column),
580            );
581            transaction.execute(&sql, &[]).await?;
582        }
583        transaction.commit().await?;
584        Ok(())
585    }
586
587    pub async fn ensure_id_space_table(
588        &self,
589        table_name: &str,
590    ) -> Result<(), MutationExecutorError> {
591        let sql = format!(
592            "CREATE TABLE IF NOT EXISTS {} (type_name VARCHAR(100) PRIMARY KEY, current_level BIGINT NOT NULL)",
593            quote_ident(table_name)
594        );
595        let client = self
596            .pool
597            .get()
598            .await
599            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
600        client.execute(&sql, &[]).await?;
601        Ok(())
602    }
603
604    pub async fn execute(&self, query: &CompiledQuery) -> Result<u64, MutationExecutorError> {
605        let mut args = PgArgs { values: Vec::new() };
606        for value in &query.params {
607            bind_pg(&mut args, value)?;
608        }
609        let client = self
610            .pool
611            .get()
612            .await
613            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
614        let statement = client.prepare_cached(&query.sql).await?;
615        let result = client.execute(&statement, &args.as_refs()).await?;
616        Ok(result)
617    }
618
619    async fn table_exists(&self, table_name: &str) -> Result<bool, MutationExecutorError> {
620        let client = self
621            .pool
622            .get()
623            .await
624            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
625        let row = client
626            .query_one(
627                "SELECT COUNT(1)
628             FROM information_schema.tables
629             WHERE table_schema = current_schema()
630               AND table_name = $1",
631                &[&table_name],
632            )
633            .await?;
634        let exists: i64 = row.try_get(0)?;
635        Ok(exists > 0)
636    }
637
638    async fn table_columns(
639        &self,
640        table_name: &str,
641    ) -> Result<std::collections::BTreeSet<String>, MutationExecutorError> {
642        let client = self
643            .pool
644            .get()
645            .await
646            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
647        let rows = client
648            .query(
649                "SELECT column_name
650             FROM information_schema.columns
651             WHERE table_schema = current_schema()
652               AND table_name = $1",
653                &[&table_name],
654            )
655            .await?;
656        let mut columns = std::collections::BTreeSet::new();
657        for row in rows {
658            let name: String = row.try_get("column_name")?;
659            columns.insert(name.to_lowercase());
660        }
661        Ok(columns)
662    }
663}
664
665async fn ensure_initial_graphs_postgres(
666    executor: &PgMutationExecutor,
667    dialect: &PostgresDialect,
668    context: &UserContext,
669) -> Result<(), MutationExecutorError> {
670    for graph in context.initial_graphs() {
671        let entity = context.entity(&graph.entity).ok_or_else(|| {
672            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
673        })?;
674        if initial_graph_exists_postgres(executor, dialect, entity, graph).await? {
675            if let Some(query) = compile_initial_graph_update(dialect, entity, graph)? {
676                executor.execute(&query).await?;
677            }
678            continue;
679        }
680        let query = compile_initial_graph_insert(dialect, entity, graph)?;
681        executor.execute(&query).await?;
682    }
683    for graph in context.root_graphs() {
684        let entity = context.entity(&graph.entity).ok_or_else(|| {
685            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
686        })?;
687        if initial_graph_exists_postgres(executor, dialect, entity, graph).await? {
688            continue;
689        }
690        let query = compile_initial_graph_insert(dialect, entity, graph)?;
691        executor.execute(&query).await?;
692    }
693    let generator = PgIdSpaceGenerator::from_executor(executor.clone());
694    for graph in context.initial_graphs().iter().chain(context.root_graphs()) {
695        if let Some(id) = graph.values.get("id").and_then(Value::try_u64) {
696            generator.ensure_floor(&graph.entity, id).await?;
697        }
698    }
699    Ok(())
700}
701
702async fn initial_graph_exists_postgres(
703    executor: &PgMutationExecutor,
704    dialect: &PostgresDialect,
705    entity: &EntityDescriptor,
706    graph: &GraphNode,
707) -> Result<bool, MutationExecutorError> {
708    let Some(id) = graph.values.get("id") else {
709        return Ok(false);
710    };
711    let query = dialect.compile_select(
712        entity,
713        &SelectQuery::new(&graph.entity)
714            .project("id")
715            .filter(Expr::eq("id", id.clone()))
716            .limit(1),
717    )?;
718    Ok(!executor.fetch_all_compact_sql(&query).await?.is_empty())
719}
720
721fn compile_initial_graph_insert(
722    dialect: &impl SqlDialect,
723    entity: &EntityDescriptor,
724    graph: &GraphNode,
725) -> Result<CompiledQuery, MutationExecutorError> {
726    let mut command = InsertCommand::new(&graph.entity);
727    for (field, value) in &graph.values {
728        command = command.value(field.clone(), value.clone());
729    }
730    dialect.compile_insert(entity, &command).map_err(Into::into)
731}
732
733fn compile_initial_graph_update(
734    dialect: &impl SqlDialect,
735    entity: &EntityDescriptor,
736    graph: &crate::GraphNode,
737) -> Result<Option<CompiledQuery>, MutationExecutorError> {
738    let Some(id) = graph.values.get("id") else {
739        return Ok(None);
740    };
741    let mut command = UpdateCommand::new(&graph.entity, id.clone());
742    for (field, value) in &graph.values {
743        if field != "id" {
744            command = command.value(field.clone(), value.clone());
745        }
746    }
747    match dialect.compile_update(entity, &command) {
748        Ok(query) => Ok(Some(query)),
749        Err(SqlCompileError::EmptyMutation(_)) => Ok(None),
750        Err(err) => Err(err.into()),
751    }
752}
753
754pub trait PostgresSchemaExt {
755    fn ensure_postgres_schema(
756        &self,
757    ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + '_>>;
758}
759
760pub async fn ensure_postgres_schema_for(
761    context: &UserContext,
762) -> Result<(), MutationExecutorError> {
763    let dialect = context.get_resource::<PostgresDialect>().ok_or_else(|| {
764        MutationExecutorError::Bind("missing typed resource: PostgresDialect".to_owned())
765    })?;
766    let executor = context
767        .get_resource::<PgMutationExecutor>()
768        .ok_or_else(|| {
769            MutationExecutorError::Bind("missing typed resource: PgMutationExecutor".to_owned())
770        })?;
771
772    let entities = context.all_entities();
773
774    executor.ensure_schema(dialect, &entities).await?;
775    ensure_initial_graphs_postgres(executor, dialect, context).await
776}
777
778#[cfg(test)]
779mod streaming_tests {
780    use super::*;
781    use futures_util::StreamExt;
782    use teaql_core::RelationDescriptor;
783    use teaql_sql::{SqlTransport, StreamingSqlTransport};
784
785    fn configured_pool(url: String) -> Pool {
786        let mut config = deadpool_postgres::Config::new();
787        config.url = Some(url);
788        config
789            .create_pool(
790                Some(deadpool_postgres::Runtime::Tokio1),
791                tokio_postgres::NoTls,
792            )
793            .unwrap()
794    }
795
796    #[tokio::test]
797    async fn streams_from_real_postgres_when_configured() {
798        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
799            return;
800        };
801        let pool = configured_pool(url);
802        let executor = PgMutationExecutor::new(pool);
803        let query = CompiledQuery {
804            sql: "SELECT id FROM (VALUES (1), (2), (3), (4), (5)) AS fixture(id) ORDER BY id"
805                .to_owned(),
806            params: vec![],
807            comment: None,
808        };
809        let mut stream = executor.stream_sql(query, 2);
810        let mut sizes = Vec::new();
811        while let Some(chunk) = stream.next().await {
812            sizes.push(chunk.unwrap().rows.len());
813        }
814        assert_eq!(sizes, vec![2, 2, 1]);
815    }
816
817    #[tokio::test]
818    async fn boolean_roundtrips_real_postgres_when_configured() {
819        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
820            return;
821        };
822        let mut config = deadpool_postgres::Config::new();
823        config.url = Some(url);
824        let pool = config
825            .create_pool(
826                Some(deadpool_postgres::Runtime::Tokio1),
827                tokio_postgres::NoTls,
828            )
829            .unwrap();
830        let executor = PgMutationExecutor::new(pool);
831        executor
832            .execute_sql(&CompiledQuery {
833                sql: "DROP TABLE IF EXISTS teaql_boolean_runtime_fixture".to_owned(),
834                params: vec![],
835                comment: None,
836            })
837            .await
838            .unwrap();
839        executor
840            .execute_sql(&CompiledQuery {
841                sql: "CREATE TABLE teaql_boolean_runtime_fixture(id BIGINT, required_flag BOOLEAN NOT NULL, optional_flag BOOLEAN)".to_owned(),
842                params: vec![],
843                comment: None,
844            })
845            .await
846            .unwrap();
847        for (id, required_flag, optional_flag) in [
848            (1_i64, Value::Bool(false), Value::Bool(true)),
849            (2_i64, Value::Bool(true), Value::Bool(false)),
850            (3_i64, Value::Bool(true), Value::Null),
851        ] {
852            executor
853                .execute_sql(&CompiledQuery {
854                    sql: "INSERT INTO teaql_boolean_runtime_fixture VALUES ($1, $2, $3)".to_owned(),
855                    params: vec![Value::I64(id), required_flag, optional_flag],
856                    comment: None,
857                })
858                .await
859                .unwrap();
860        }
861        let rows = executor
862            .fetch_all_compact_sql(&CompiledQuery {
863                sql: "SELECT required_flag, optional_flag FROM teaql_boolean_runtime_fixture ORDER BY id".to_owned(),
864                params: vec![],
865                comment: None,
866            })
867            .await
868            .unwrap();
869        assert_eq!(rows[0].get("required_flag"), Some(&Value::Bool(false)));
870        assert_eq!(rows[0].get("optional_flag"), Some(&Value::Bool(true)));
871        assert_eq!(rows[1].get("required_flag"), Some(&Value::Bool(true)));
872        assert_eq!(rows[1].get("optional_flag"), Some(&Value::Bool(false)));
873        assert_eq!(rows[2].get("optional_flag"), Some(&Value::Null));
874        executor
875            .execute_sql(&CompiledQuery {
876                sql: "DROP TABLE teaql_boolean_runtime_fixture".to_owned(),
877                params: vec![],
878                comment: None,
879            })
880            .await
881            .unwrap();
882    }
883
884    #[tokio::test]
885    async fn teaql_long_binds_to_legacy_postgres_int4_scalars_and_arrays() {
886        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
887            return;
888        };
889        let pool = configured_pool(url);
890        let executor = PgMutationExecutor::new(pool);
891        for sql in [
892            "DROP TABLE IF EXISTS teaql_int4_binding_fixture",
893            "CREATE TABLE teaql_int4_binding_fixture(id INTEGER PRIMARY KEY)",
894        ] {
895            executor
896                .execute_sql(&CompiledQuery {
897                    sql: sql.to_owned(),
898                    params: vec![],
899                    comment: None,
900                })
901                .await
902                .unwrap();
903        }
904        for id in [1_i64, i64::from(i32::MAX)] {
905            executor
906                .execute_sql(&CompiledQuery {
907                    sql: "INSERT INTO teaql_int4_binding_fixture(id) VALUES ($1)".to_owned(),
908                    params: vec![Value::I64(id)],
909                    comment: None,
910                })
911                .await
912                .unwrap();
913        }
914        let rows = executor
915            .fetch_all_compact_sql(&CompiledQuery {
916                sql: "SELECT id FROM teaql_int4_binding_fixture WHERE id = ANY($1) ORDER BY id"
917                    .to_owned(),
918                params: vec![Value::List(vec![
919                    Value::U64(1),
920                    Value::U64(i32::MAX as u64),
921                ])],
922                comment: None,
923            })
924            .await
925            .unwrap();
926        assert_eq!(rows.len(), 2);
927        assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
928
929        let overflow = executor
930            .fetch_all_compact_sql(&CompiledQuery {
931                sql: "SELECT id FROM teaql_int4_binding_fixture WHERE id = $1".to_owned(),
932                params: vec![Value::I64(i64::from(i32::MAX) + 1)],
933                comment: None,
934            })
935            .await;
936        assert!(overflow.is_err());
937        executor
938            .execute_sql(&CompiledQuery {
939                sql: "DROP TABLE teaql_int4_binding_fixture".to_owned(),
940                params: vec![],
941                comment: None,
942            })
943            .await
944            .unwrap();
945    }
946
947    #[tokio::test]
948    async fn ensure_schema_creates_relation_index_on_first_run_and_is_idempotent() {
949        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
950            return;
951        };
952        let pool = configured_pool(url);
953        let client = pool.get().await.unwrap();
954        client
955            .batch_execute("DROP TABLE IF EXISTS teaql_relation_index_fixture")
956            .await
957            .unwrap();
958
959        let entity = EntityDescriptor::new("RelationIndexFixture")
960            .table_name("teaql_relation_index_fixture")
961            .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
962            .property(PropertyDescriptor::new("version", DataType::I64).version())
963            .property(PropertyDescriptor::new("vendor_id", DataType::U64).not_null())
964            .relation(
965                RelationDescriptor::new("vendor", "Vendor")
966                    .local_key("vendor_id")
967                    .foreign_key("id"),
968            );
969        let executor = PgMutationExecutor::new(pool.clone());
970
971        executor
972            .ensure_schema(&PostgresDialect, &[&entity])
973            .await
974            .unwrap();
975        executor
976            .ensure_schema(&PostgresDialect, &[&entity])
977            .await
978            .unwrap();
979
980        let rows = client
981            .query(
982                "SELECT indexdef FROM pg_indexes WHERE schemaname = current_schema() AND tablename = 'teaql_relation_index_fixture' AND indexdef LIKE '%(vendor_id, id DESC)%'",
983                &[],
984            )
985            .await
986            .unwrap();
987        assert_eq!(rows.len(), 1);
988
989        client
990            .batch_execute("DROP TABLE teaql_relation_index_fixture")
991            .await
992            .unwrap();
993    }
994
995    #[tokio::test]
996    async fn ensure_schema_creates_foreign_key_once_by_semantics() {
997        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
998            return;
999        };
1000        let pool = configured_pool(url);
1001        let client = pool.get().await.unwrap();
1002        client
1003            .batch_execute(
1004                "DROP TABLE IF EXISTS teaql_fk_child_fixture;
1005                 DROP TABLE IF EXISTS teaql_fk_parent_fixture;",
1006            )
1007            .await
1008            .unwrap();
1009
1010        let parent = EntityDescriptor::new("FkParentFixture")
1011            .table_name("teaql_fk_parent_fixture")
1012            .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1013            .relation(
1014                RelationDescriptor::new("children", "FkChildFixture")
1015                    .local_key("id")
1016                    .foreign_key("parent_id")
1017                    .many(),
1018            );
1019        let child = EntityDescriptor::new("FkChildFixture")
1020            .table_name("teaql_fk_child_fixture")
1021            .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1022            .property(PropertyDescriptor::new("parent_id", DataType::U64).not_null())
1023            .relation(
1024                RelationDescriptor::new("parent", "FkParentFixture")
1025                    .local_key("parent_id")
1026                    .foreign_key("id"),
1027            );
1028        let executor = PgMutationExecutor::new(pool.clone());
1029
1030        executor
1031            .ensure_schema(&PostgresDialect, &[&child, &parent])
1032            .await
1033            .unwrap();
1034        executor
1035            .ensure_schema(&PostgresDialect, &[&parent, &child])
1036            .await
1037            .unwrap();
1038
1039        let count: i64 = client
1040            .query_one(
1041                "SELECT COUNT(*)
1042                   FROM pg_constraint c
1043                   JOIN pg_class t ON t.oid = c.conrelid
1044                  WHERE c.contype = 'f'
1045                    AND t.relname = 'teaql_fk_child_fixture'",
1046                &[],
1047            )
1048            .await
1049            .unwrap()
1050            .try_get(0)
1051            .unwrap();
1052        assert_eq!(count, 1);
1053
1054        let violation = client
1055            .execute(
1056                "INSERT INTO teaql_fk_child_fixture(id, parent_id) VALUES (1, 999)",
1057                &[],
1058            )
1059            .await;
1060        assert!(violation.is_err());
1061
1062        client
1063            .batch_execute(
1064                "DROP TABLE teaql_fk_child_fixture;
1065                 DROP TABLE teaql_fk_parent_fixture;",
1066            )
1067            .await
1068            .unwrap();
1069    }
1070
1071    #[tokio::test]
1072    async fn temporal_debug_sql_matches_real_postgres_when_configured() {
1073        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
1074            return;
1075        };
1076        let mut config = deadpool_postgres::Config::new();
1077        config.url = Some(url);
1078        let pool = config
1079            .create_pool(
1080                Some(deadpool_postgres::Runtime::Tokio1),
1081                tokio_postgres::NoTls,
1082            )
1083            .unwrap();
1084        let executor = PgMutationExecutor::new(pool);
1085        executor
1086            .execute_sql(&CompiledQuery {
1087                sql: "DROP TABLE IF EXISTS teaql_temporal_runtime_fixture".to_owned(),
1088                params: vec![],
1089                comment: None,
1090            })
1091            .await
1092            .unwrap();
1093        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();
1094        let prepared = CompiledQuery {
1095            sql: "INSERT INTO teaql_temporal_runtime_fixture VALUES ($1, $2, $3, TIMESTAMP '1960-01-02 03:04:05.678')".to_owned(),
1096            params: vec![
1097                Value::I64(1),
1098                Value::Date("2024-02-29".parse().unwrap()),
1099                Value::Timestamp(teaql_core::time::Timestamp(-315_521_754_322)),
1100            ],
1101            comment: Some("teaql source=temporal.verify $1".to_owned()),
1102        };
1103        executor.execute_sql(&prepared).await.unwrap();
1104        executor
1105            .execute_sql(&CompiledQuery {
1106                sql: prepared
1107                    .debug_sql(DatabaseKind::PostgreSql)
1108                    .replace("VALUES (1,", "VALUES (2,"),
1109                params: vec![],
1110                comment: None,
1111            })
1112            .await
1113            .unwrap();
1114        let rows = executor
1115            .fetch_all_compact_sql(&CompiledQuery {
1116                sql: "SELECT d, t, t_local FROM teaql_temporal_runtime_fixture ORDER BY id"
1117                    .to_owned(),
1118                params: vec![],
1119                comment: None,
1120            })
1121            .await
1122            .unwrap();
1123        assert_eq!(rows[0], rows[1]);
1124        executor
1125            .execute_sql(&CompiledQuery {
1126                sql: "DROP TABLE teaql_temporal_runtime_fixture".to_owned(),
1127                params: vec![],
1128                comment: None,
1129            })
1130            .await
1131            .unwrap();
1132    }
1133}
1134
1135impl PostgresSchemaExt for UserContext {
1136    fn ensure_postgres_schema(
1137        &self,
1138    ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + '_>> {
1139        Box::pin(ensure_postgres_schema_for(self))
1140    }
1141}
1142
1143#[derive(Debug, Default, Clone, Copy)]
1144pub struct PostgresSchemaProvider;
1145
1146impl SchemaProvider for PostgresSchemaProvider {
1147    fn ensure_schema<'a>(
1148        &'a self,
1149        context: &'a UserContext,
1150    ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
1151        Box::pin(async move {
1152            ensure_postgres_schema_for(context)
1153                .await
1154                .map_err(|err| RuntimeError::Schema(err.to_string()))
1155        })
1156    }
1157}
1158
1159pub trait PostgresProviderExt {
1160    fn use_postgres_provider(&mut self, executor: PgMutationExecutor) -> &mut Self;
1161}
1162
1163impl PostgresProviderExt for UserContext {
1164    fn use_postgres_provider(&mut self, executor: PgMutationExecutor) -> &mut Self {
1165        self.insert_resource(PostgresDialect);
1166        self.insert_resource(executor);
1167        self.set_schema_provider(PostgresSchemaProvider);
1168        self
1169    }
1170}
1171
1172#[derive(Clone)]
1173pub struct PgIdSpaceGenerator {
1174    pool: Pool,
1175    table_name: String,
1176}
1177
1178impl PgIdSpaceGenerator {
1179    pub fn new(pool: Pool) -> Self {
1180        Self {
1181            pool,
1182            table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
1183        }
1184    }
1185
1186    pub fn from_executor(executor: PgMutationExecutor) -> Self {
1187        Self::new(executor.pool())
1188    }
1189
1190    pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
1191        self.table_name = table_name.into();
1192        self
1193    }
1194
1195    pub async fn ensure_table(&self) -> Result<(), MutationExecutorError> {
1196        PgMutationExecutor::new(self.pool.clone())
1197            .ensure_id_space_table(&self.table_name)
1198            .await
1199    }
1200
1201    pub async fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
1202        let entity = canonical_id_space_entity(entity);
1203        let entity = entity.as_str();
1204        self.ensure_table().await?;
1205        let table = quote_ident(&self.table_name);
1206        let client = self
1207            .pool
1208            .get()
1209            .await
1210            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
1211        let select_sql = format!("SELECT current_level FROM {table} WHERE type_name = $1");
1212        let insert_sql = format!("INSERT INTO {table}(type_name, current_level) VALUES ($1, 1)");
1213        let update_sql = format!(
1214            "UPDATE {table} SET current_level = $1 WHERE type_name = $2 AND current_level = $3"
1215        );
1216        for _ in 1..=100 {
1217            let current = client
1218                .query_opt(&select_sql, &[&entity])
1219                .await?
1220                .map(|row| row.try_get::<_, i64>(0))
1221                .transpose()?;
1222            if let Some(current) = current {
1223                let next = current.checked_add(1).ok_or_else(|| {
1224                    MutationExecutorError::Bind(format!("ID space overflow for {entity}"))
1225                })?;
1226                if client
1227                    .execute(&update_sql, &[&next, &entity, &current])
1228                    .await?
1229                    == 1
1230                {
1231                    return u64::try_from(next).map_err(|_| {
1232                        MutationExecutorError::Bind(format!(
1233                            "generated id {next} cannot be represented as u64"
1234                        ))
1235                    });
1236                }
1237            } else {
1238                match client.execute(&insert_sql, &[&entity]).await {
1239                    Ok(1) => return Ok(1),
1240                    Ok(changed) => {
1241                        return Err(MutationExecutorError::Bind(format!(
1242                            "ID space insert for {entity} changed {changed} rows"
1243                        )));
1244                    }
1245                    Err(error) => {
1246                        if client.query_opt(&select_sql, &[&entity]).await?.is_none() {
1247                            return Err(error.into());
1248                        }
1249                    }
1250                }
1251            }
1252        }
1253        Err(MutationExecutorError::Bind(format!(
1254            "Unable to allocate ID for {entity} after 100 optimistic-lock attempts"
1255        )))
1256    }
1257
1258    pub async fn ensure_floor(
1259        &self,
1260        entity: &str,
1261        floor: u64,
1262    ) -> Result<(), MutationExecutorError> {
1263        let entity = canonical_id_space_entity(entity);
1264        let entity = entity.as_str();
1265        self.ensure_table().await?;
1266        let floor = i64::try_from(floor).map_err(|_| {
1267            MutationExecutorError::Bind(format!(
1268                "ID space floor {floor} for {entity} exceeds BIGINT"
1269            ))
1270        })?;
1271        let table = quote_ident(&self.table_name);
1272        let client = self
1273            .pool
1274            .get()
1275            .await
1276            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
1277        let select = format!("SELECT current_level FROM {table} WHERE type_name = $1");
1278        let insert = format!("INSERT INTO {table}(type_name, current_level) VALUES ($1, $2)");
1279        let update = format!(
1280            "UPDATE {table} SET current_level = $1 WHERE type_name = $2 AND current_level = $3"
1281        );
1282        for _ in 1..=100 {
1283            let current = client
1284                .query_opt(&select, &[&entity])
1285                .await?
1286                .map(|row| row.try_get::<_, i64>(0))
1287                .transpose()?;
1288            match current {
1289                Some(current) if current >= floor => return Ok(()),
1290                Some(current) => {
1291                    if client
1292                        .execute(&update, &[&floor, &entity, &current])
1293                        .await?
1294                        == 1
1295                    {
1296                        return Ok(());
1297                    }
1298                }
1299                None => match client.execute(&insert, &[&entity, &floor]).await {
1300                    Ok(1) => return Ok(()),
1301                    Ok(_) => {}
1302                    Err(error) => {
1303                        if client.query_opt(&select, &[&entity]).await?.is_none() {
1304                            return Err(error.into());
1305                        }
1306                    }
1307                },
1308            }
1309        }
1310        Err(MutationExecutorError::Bind(format!(
1311            "Unable to synchronize ID space floor for {entity} after 100 optimistic-lock attempts"
1312        )))
1313    }
1314}
1315
1316impl InternalIdGenerator for PgIdSpaceGenerator {
1317    fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
1318        let generator = self.clone();
1319        let entity = entity.to_owned();
1320        block_on_id_generation(async move { generator.next_id(&entity).await })
1321    }
1322}
1323
1324fn block_on_id_generation<F>(future: F) -> Result<u64, RuntimeError>
1325where
1326    F: Future<Output = Result<u64, MutationExecutorError>> + Send + 'static,
1327{
1328    let result = match tokio::runtime::Handle::try_current() {
1329        Ok(handle) => tokio::task::block_in_place(|| handle.block_on(future)),
1330        Err(_) => tokio::runtime::Builder::new_current_thread()
1331            .enable_all()
1332            .build()
1333            .map_err(|err| RuntimeError::IdGeneration(err.to_string()))?
1334            .block_on(future),
1335    };
1336    result.map_err(|err| RuntimeError::IdGeneration(err.to_string()))
1337}
1338
1339fn quote_ident(ident: &str) -> String {
1340    quote_identifier_if_needed(ident, '"')
1341}
1342
1343/// Strip wrapping identifier quotes from a SQL identifier so that bare column
1344/// names returned by `information_schema.columns` can be compared with
1345/// potentially-quoted `PropertyDescriptor::column_name` values.
1346fn strip_identifier_quotes(ident: &str) -> &str {
1347    let bytes = ident.as_bytes();
1348    if bytes.len() >= 2 {
1349        let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
1350        if (first == b'"' && last == b'"')
1351            || (first == b'`' && last == b'`')
1352            || (first == b'[' && last == b']')
1353        {
1354            return &ident[1..ident.len() - 1];
1355        }
1356    }
1357    ident
1358}
1359
1360fn try_parse_datetime_from_str(s: &str) -> Option<chrono::DateTime<chrono::Utc>> {
1361    if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
1362        return Some(dt.with_timezone(&chrono::Utc));
1363    }
1364    if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
1365        return Some(chrono::DateTime::from_naive_utc_and_offset(
1366            ndt,
1367            chrono::Utc,
1368        ));
1369    }
1370    if let Ok(nd) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
1371        let ndt = nd.and_hms_opt(0, 0, 0)?;
1372        return Some(chrono::DateTime::from_naive_utc_and_offset(
1373            ndt,
1374            chrono::Utc,
1375        ));
1376    }
1377    None
1378}
1379
1380#[derive(Debug, Clone, Copy)]
1381struct PgNull;
1382
1383impl tokio_postgres::types::ToSql for PgNull {
1384    fn to_sql(
1385        &self,
1386        ty: &tokio_postgres::types::Type,
1387        out: &mut bytes::BytesMut,
1388    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1389        Ok(tokio_postgres::types::IsNull::Yes)
1390    }
1391
1392    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1393        true
1394    }
1395
1396    fn to_sql_checked(
1397        &self,
1398        ty: &tokio_postgres::types::Type,
1399        out: &mut bytes::BytesMut,
1400    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1401        Ok(tokio_postgres::types::IsNull::Yes)
1402    }
1403}
1404
1405#[derive(Debug, Clone, Copy)]
1406struct PgTimestamp(DateTime<Utc>);
1407
1408impl tokio_postgres::types::ToSql for PgTimestamp {
1409    fn to_sql(
1410        &self,
1411        ty: &tokio_postgres::types::Type,
1412        out: &mut bytes::BytesMut,
1413    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1414        if *ty == tokio_postgres::types::Type::TIMESTAMP {
1415            self.0.naive_utc().to_sql(ty, out)
1416        } else {
1417            self.0.to_sql(ty, out)
1418        }
1419    }
1420
1421    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1422        *ty == tokio_postgres::types::Type::TIMESTAMP
1423            || *ty == tokio_postgres::types::Type::TIMESTAMPTZ
1424    }
1425
1426    tokio_postgres::types::to_sql_checked!();
1427}
1428
1429#[derive(Debug, Clone, Copy)]
1430struct PgInteger(i64);
1431
1432impl tokio_postgres::types::ToSql for PgInteger {
1433    fn to_sql(
1434        &self,
1435        ty: &tokio_postgres::types::Type,
1436        out: &mut bytes::BytesMut,
1437    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1438        match *ty {
1439            tokio_postgres::types::Type::INT2 => i16::try_from(self.0)?.to_sql(ty, out),
1440            tokio_postgres::types::Type::INT4 => i32::try_from(self.0)?.to_sql(ty, out),
1441            tokio_postgres::types::Type::INT8 => self.0.to_sql(ty, out),
1442            _ => Err(format!("integer cannot be encoded as PostgreSQL type {ty}").into()),
1443        }
1444    }
1445
1446    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1447        matches!(
1448            *ty,
1449            tokio_postgres::types::Type::INT2
1450                | tokio_postgres::types::Type::INT4
1451                | tokio_postgres::types::Type::INT8
1452        )
1453    }
1454
1455    tokio_postgres::types::to_sql_checked!();
1456}
1457
1458#[derive(Debug, Clone)]
1459struct PgIntegerList(Vec<i64>);
1460
1461impl tokio_postgres::types::ToSql for PgIntegerList {
1462    fn to_sql(
1463        &self,
1464        ty: &tokio_postgres::types::Type,
1465        out: &mut bytes::BytesMut,
1466    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1467        match *ty {
1468            tokio_postgres::types::Type::INT2_ARRAY => self
1469                .0
1470                .iter()
1471                .copied()
1472                .map(i16::try_from)
1473                .collect::<Result<Vec<_>, _>>()?
1474                .to_sql(ty, out),
1475            tokio_postgres::types::Type::INT4_ARRAY => self
1476                .0
1477                .iter()
1478                .copied()
1479                .map(i32::try_from)
1480                .collect::<Result<Vec<_>, _>>()?
1481                .to_sql(ty, out),
1482            tokio_postgres::types::Type::INT8_ARRAY => self.0.to_sql(ty, out),
1483            _ => Err(format!("integer list cannot be encoded as PostgreSQL type {ty}").into()),
1484        }
1485    }
1486
1487    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1488        matches!(
1489            *ty,
1490            tokio_postgres::types::Type::INT2_ARRAY
1491                | tokio_postgres::types::Type::INT4_ARRAY
1492                | tokio_postgres::types::Type::INT8_ARRAY
1493        )
1494    }
1495
1496    tokio_postgres::types::to_sql_checked!();
1497}
1498
1499struct PgArgs {
1500    values: Vec<Box<dyn tokio_postgres::types::ToSql + Sync + Send>>,
1501}
1502impl PgArgs {
1503    fn add<T: tokio_postgres::types::ToSql + Sync + Send + 'static>(&mut self, v: T) {
1504        self.values.push(Box::new(v));
1505    }
1506    fn as_refs(&self) -> Vec<&(dyn tokio_postgres::types::ToSql + Sync)> {
1507        self.values.iter().map(|b| b.as_ref() as _).collect()
1508    }
1509}
1510
1511fn bind_pg(args: &mut PgArgs, value: &Value) -> Result<(), MutationExecutorError> {
1512    match value {
1513        Value::Null => {
1514            args.add(PgNull);
1515        }
1516        Value::Bool(v) => args.add(*v),
1517        Value::I64(v) => args.add(PgInteger(*v)),
1518        Value::U64(v) => {
1519            let v = i64::try_from(*v).map_err(|_| {
1520                MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))
1521            })?;
1522            args.add(PgInteger(v));
1523        }
1524        Value::F64(v) => args.add(*v),
1525        Value::Decimal(v) => args.add(*v),
1526        Value::Text(v) => match try_parse_datetime_from_str(v) {
1527            Some(dt) => args.add(dt),
1528            None => args.add(v.clone()),
1529        },
1530        Value::Json(v) => {
1531            let j_val: serde_json::Value =
1532                serde_json::to_value(v).map_err(|e| MutationExecutorError::Bind(e.to_string()))?;
1533            args.add(j_val);
1534        }
1535        Value::Date(v) => args.add(*v),
1536        Value::Timestamp(v) => args.add(PgTimestamp(v.to_datetime())),
1537        Value::Object(_) => return Err(MutationExecutorError::UnsupportedValue("object")),
1538        Value::List(values) => bind_pg_list(args, values)?,
1539        Value::TypedNull(dt) => match dt {
1540            DataType::Bool => args.add(Option::<bool>::None),
1541            DataType::I64 | DataType::U64 => args.add(Option::<i64>::None),
1542            DataType::F64 => args.add(Option::<f64>::None),
1543            DataType::Decimal => args.add(Option::<Decimal>::None),
1544            DataType::Text | DataType::LargeText => args.add(Option::<String>::None),
1545            DataType::Json => args.add(Option::<serde_json::Value>::None),
1546            DataType::Date => args.add(Option::<NaiveDate>::None),
1547            DataType::Timestamp => args.add(PgNull),
1548        },
1549    }
1550    Ok(())
1551}
1552
1553fn bind_pg_list(args: &mut PgArgs, values: &[Value]) -> Result<(), MutationExecutorError> {
1554    let Some(first) = values.first() else {
1555        return Err(MutationExecutorError::UnsupportedValue("empty list"));
1556    };
1557    match first {
1558        Value::Bool(_) => {
1559            let values = values
1560                .iter()
1561                .map(|value| match value {
1562                    Value::Bool(value) => Ok(*value),
1563                    _ => Err(MutationExecutorError::UnsupportedValue("mixed bool list")),
1564                })
1565                .collect::<Result<Vec<_>, _>>()?;
1566            args.add(values);
1567        }
1568        Value::I64(_) => {
1569            let values = values
1570                .iter()
1571                .map(|value| match value {
1572                    Value::I64(value) => Ok(*value),
1573                    _ => Err(MutationExecutorError::UnsupportedValue("mixed i64 list")),
1574                })
1575                .collect::<Result<Vec<_>, _>>()?;
1576            args.add(PgIntegerList(values));
1577        }
1578        Value::U64(_) => {
1579            let values = values
1580                .iter()
1581                .map(|value| match value {
1582                    Value::U64(value) => i64::try_from(*value).map_err(|_| {
1583                        MutationExecutorError::Bind(format!("u64 value {value} exceeds i64 range"))
1584                    }),
1585                    _ => Err(MutationExecutorError::UnsupportedValue("mixed u64 list")),
1586                })
1587                .collect::<Result<Vec<_>, _>>()?;
1588            args.add(PgIntegerList(values));
1589        }
1590        Value::F64(_) => {
1591            let values = values
1592                .iter()
1593                .map(|value| match value {
1594                    Value::F64(value) => Ok(*value),
1595                    _ => Err(MutationExecutorError::UnsupportedValue("mixed f64 list")),
1596                })
1597                .collect::<Result<Vec<_>, _>>()?;
1598            args.add(values);
1599        }
1600        Value::Decimal(_) => {
1601            let values = values
1602                .iter()
1603                .map(|value| match value {
1604                    Value::Decimal(value) => Ok(*value),
1605                    _ => Err(MutationExecutorError::UnsupportedValue(
1606                        "mixed decimal list",
1607                    )),
1608                })
1609                .collect::<Result<Vec<_>, _>>()?;
1610            args.add(values);
1611        }
1612        Value::Text(_) => {
1613            let values = values
1614                .iter()
1615                .map(|value| match value {
1616                    Value::Text(value) => Ok(value.clone()),
1617                    _ => Err(MutationExecutorError::UnsupportedValue("mixed text list")),
1618                })
1619                .collect::<Result<Vec<_>, _>>()?;
1620            args.add(values);
1621        }
1622        Value::Date(_) => {
1623            let values = values
1624                .iter()
1625                .map(|value| match value {
1626                    Value::Date(value) => Ok(*value),
1627                    _ => Err(MutationExecutorError::UnsupportedValue("mixed date list")),
1628                })
1629                .collect::<Result<Vec<_>, _>>()?;
1630            args.add(values);
1631        }
1632        Value::Timestamp(_) => {
1633            let values = values
1634                .iter()
1635                .map(|value| match value {
1636                    Value::Timestamp(value) => Ok(value.to_datetime()),
1637                    _ => Err(MutationExecutorError::UnsupportedValue(
1638                        "mixed timestamp list",
1639                    )),
1640                })
1641                .collect::<Result<Vec<_>, _>>()?;
1642            args.add(values);
1643        }
1644        Value::Null => return Err(MutationExecutorError::UnsupportedValue("null list")),
1645        Value::Json(_) => return Err(MutationExecutorError::UnsupportedValue("json list")),
1646        Value::Object(_) => return Err(MutationExecutorError::UnsupportedValue("object list")),
1647        Value::List(_) => return Err(MutationExecutorError::UnsupportedValue("nested list")),
1648        Value::TypedNull(_) => return Err(MutationExecutorError::UnsupportedValue("null list")),
1649    }
1650    Ok(())
1651}
1652
1653fn decode_pg_values(row: &tokio_postgres::Row) -> Result<Vec<Value>, MutationExecutorError> {
1654    let mut values = Vec::with_capacity(row.len());
1655    for (index, column) in row.columns().iter().enumerate() {
1656        let type_name = column.type_().name();
1657
1658        let value = match type_name {
1659            "bool" | "boolean" => {
1660                let v: Option<bool> = row.try_get(index)?;
1661                match v {
1662                    Some(v) => Value::Bool(v),
1663                    None => Value::Null,
1664                }
1665            }
1666            "int2" => {
1667                let v: Option<i16> = row.try_get(index)?;
1668                match v {
1669                    Some(v) => Value::I64(v as i64),
1670                    None => Value::Null,
1671                }
1672            }
1673            "int4" => {
1674                let v: Option<i32> = row.try_get(index)?;
1675                match v {
1676                    Some(v) => Value::I64(v as i64),
1677                    None => Value::Null,
1678                }
1679            }
1680            "int8" => {
1681                let v: Option<i64> = row.try_get(index)?;
1682                match v {
1683                    Some(v) => Value::I64(v),
1684                    None => Value::Null,
1685                }
1686            }
1687            "float4" => {
1688                let v: Option<f32> = row.try_get(index)?;
1689                match v {
1690                    Some(v) => Value::F64(v as f64),
1691                    None => Value::Null,
1692                }
1693            }
1694            "float8" => {
1695                let v: Option<f64> = row.try_get(index)?;
1696                match v {
1697                    Some(v) => Value::F64(v),
1698                    None => Value::Null,
1699                }
1700            }
1701            "numeric" => {
1702                let v: Option<Decimal> = row.try_get(index)?;
1703                match v {
1704                    Some(v) => Value::Decimal(v),
1705                    None => Value::Null,
1706                }
1707            }
1708            "json" | "jsonb" => {
1709                let v: Option<serde_json::Value> = row.try_get(index)?;
1710                match v {
1711                    Some(j) => Value::Json(j.into()),
1712                    None => Value::Null,
1713                }
1714            }
1715            "date" => {
1716                let v: Option<NaiveDate> = row.try_get(index)?;
1717                match v {
1718                    Some(v) => Value::Date(v),
1719                    None => Value::Null,
1720                }
1721            }
1722            "timestamp" => {
1723                let v: Option<NaiveDateTime> = row.try_get(index)?;
1724                match v {
1725                    Some(v) => Value::Timestamp(teaql_core::time::Timestamp(
1726                        v.and_utc().timestamp_millis(),
1727                    )),
1728                    None => Value::Null,
1729                }
1730            }
1731            "timestamptz" => {
1732                let v: Option<DateTime<Utc>> = row.try_get(index)?;
1733                match v {
1734                    Some(v) => Value::Timestamp(teaql_core::time::Timestamp(v.timestamp_millis())),
1735                    None => Value::Null,
1736                }
1737            }
1738            "text" | "varchar" | "bpchar" | "name" | "uuid" => {
1739                let v: Option<String> = row.try_get(index)?;
1740                match v {
1741                    Some(v) => Value::Text(v),
1742                    None => Value::Null,
1743                }
1744            }
1745            other => {
1746                return Err(MutationExecutorError::UnsupportedColumnType(
1747                    other.to_owned(),
1748                ));
1749            }
1750        };
1751        values.push(value);
1752    }
1753    Ok(values)
1754}
1755
1756#[cfg(test)]
1757mod tests {
1758    use super::*;
1759    use teaql_core::{DeleteCommand, RecoverCommand, RelationDescriptor};
1760
1761    fn entity() -> EntityDescriptor {
1762        EntityDescriptor::new("Order")
1763            .table_name("orders")
1764            .property(
1765                PropertyDescriptor::new("id", DataType::U64)
1766                    .column_name("id")
1767                    .id()
1768                    .not_null(),
1769            )
1770            .property(
1771                PropertyDescriptor::new("version", DataType::I64)
1772                    .column_name("version")
1773                    .version()
1774                    .not_null(),
1775            )
1776            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1777    }
1778
1779    #[test]
1780    fn postgres_dialect_compiles_mutations_with_numbered_placeholders() {
1781        let insert = PostgresDialect
1782            .compile_insert(
1783                &entity(),
1784                &InsertCommand::new("Order")
1785                    .value("id", 1_u64)
1786                    .value("name", "A"),
1787            )
1788            .unwrap();
1789        assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES ($1, $2)");
1790
1791        let update = PostgresDialect
1792            .compile_update(
1793                &entity(),
1794                &UpdateCommand::new("Order", 1_u64)
1795                    .expected_version(3)
1796                    .value("name", "B"),
1797            )
1798            .unwrap();
1799        assert_eq!(
1800            update.sql,
1801            "UPDATE orders SET name = $1, version = $2 WHERE id = $3 AND version = $4"
1802        );
1803
1804        let delete = PostgresDialect
1805            .compile_delete(
1806                &entity(),
1807                &DeleteCommand::new("Order", 1_u64).expected_version(3),
1808            )
1809            .unwrap();
1810        let recover = PostgresDialect
1811            .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1812            .unwrap();
1813        assert_eq!(
1814            delete.sql,
1815            "UPDATE orders SET version = $1 WHERE id = $2 AND version = $3"
1816        );
1817        assert_eq!(
1818            recover.sql,
1819            "UPDATE orders SET version = $1 WHERE id = $2 AND version = $3"
1820        );
1821    }
1822
1823    #[test]
1824    fn postgres_dialect_compiles_schema_and_large_in_array_binds() {
1825        let create = PostgresDialect.compile_create_table(&entity()).unwrap();
1826        assert_eq!(
1827            create,
1828            "CREATE TABLE IF NOT EXISTS orders (id BIGINT PRIMARY KEY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255))"
1829        );
1830        assert!(
1831            PostgresDialect
1832                .schema_setup_sqls()
1833                .iter()
1834                .any(|sql| sql.contains("CREATE OR REPLACE FUNCTION soundex"))
1835        );
1836
1837        let values = (1_u64..=21).map(Value::from).collect::<Vec<_>>();
1838        let query = PostgresDialect
1839            .compile_select(
1840                &entity(),
1841                &SelectQuery::new("Order")
1842                    .filter(Expr::in_list("id", values.clone()))
1843                    .order_asc("id"),
1844            )
1845            .unwrap();
1846        assert_eq!(
1847            query.sql,
1848            "SELECT id, version, name FROM orders WHERE (id = ANY($1)) ORDER BY id ASC"
1849        );
1850        assert_eq!(query.params, vec![Value::List(values)]);
1851    }
1852
1853    #[test]
1854    fn postgres_schema_adds_full_foreign_key_id_desc_index() {
1855        let trip = EntityDescriptor::new("Trip")
1856            .table_name("trip_data")
1857            .property(
1858                PropertyDescriptor::new("id", DataType::U64)
1859                    .column_name("id")
1860                    .id()
1861                    .not_null(),
1862            )
1863            .property(
1864                PropertyDescriptor::new("vendor_id", DataType::U64)
1865                    .column_name("vendor")
1866                    .not_null(),
1867            )
1868            .relation(
1869                RelationDescriptor::new("vendor", "Vendor")
1870                    .local_key("vendor_id")
1871                    .foreign_key("id"),
1872            )
1873            // A second relation through the same key must not duplicate DDL.
1874            .relation(
1875                RelationDescriptor::new("billing_vendor", "Vendor")
1876                    .local_key("vendor_id")
1877                    .foreign_key("id"),
1878            )
1879            // Reverse relations belong to the target table and are ignored here.
1880            .relation(
1881                RelationDescriptor::new("items", "TripItem")
1882                    .local_key("id")
1883                    .foreign_key("trip_id")
1884                    .many(),
1885            );
1886
1887        assert_eq!(
1888            PostgresDialect.relation_indexes_sqls(&trip),
1889            vec![
1890                "CREATE INDEX IF NOT EXISTS IDX_TRIP_DATA_VENDOR_ID_DESC ON trip_data (vendor, id DESC)"
1891            ]
1892        );
1893    }
1894
1895    #[test]
1896    fn postgres_relation_index_name_is_stable_and_within_identifier_limit() {
1897        let name = postgres_index_name(
1898            "an_extremely_long_generated_transaction_history_table_name",
1899            "an_equally_long_business_owner_reference_identifier",
1900            "id",
1901        );
1902        assert!(name.len() <= 63);
1903        assert_eq!(
1904            name,
1905            postgres_index_name(
1906                "an_extremely_long_generated_transaction_history_table_name",
1907                "an_equally_long_business_owner_reference_identifier",
1908                "id",
1909            )
1910        );
1911        assert!(name.ends_with("_889B21BBED38CC82"));
1912    }
1913}