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(crate) async fn ensure_postgres_schema_for(
755    context: &UserContext,
756) -> Result<(), MutationExecutorError> {
757    let dialect = context.get_resource::<PostgresDialect>().ok_or_else(|| {
758        MutationExecutorError::Bind("missing typed resource: PostgresDialect".to_owned())
759    })?;
760    let executor = context
761        .get_resource::<PgMutationExecutor>()
762        .ok_or_else(|| {
763            MutationExecutorError::Bind("missing typed resource: PgMutationExecutor".to_owned())
764        })?;
765
766    let entities = context.all_entities();
767
768    executor.ensure_schema(dialect, &entities).await?;
769    ensure_initial_graphs_postgres(executor, dialect, context).await
770}
771
772#[cfg(test)]
773mod streaming_tests {
774    use super::*;
775    use futures_util::StreamExt;
776    use teaql_core::RelationDescriptor;
777    use teaql_sql::{SqlTransport, StreamingSqlTransport};
778
779    fn configured_pool(url: String) -> Pool {
780        let mut config = deadpool_postgres::Config::new();
781        config.url = Some(url);
782        config
783            .create_pool(
784                Some(deadpool_postgres::Runtime::Tokio1),
785                tokio_postgres::NoTls,
786            )
787            .unwrap()
788    }
789
790    #[tokio::test]
791    async fn streams_from_real_postgres_when_configured() {
792        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
793            return;
794        };
795        let pool = configured_pool(url);
796        let executor = PgMutationExecutor::new(pool);
797        let query = CompiledQuery {
798            sql: "SELECT id FROM (VALUES (1), (2), (3), (4), (5)) AS fixture(id) ORDER BY id"
799                .to_owned(),
800            params: vec![],
801            comment: None,
802        };
803        let mut stream = executor.stream_sql(query, 2);
804        let mut sizes = Vec::new();
805        while let Some(chunk) = stream.next().await {
806            sizes.push(chunk.unwrap().rows.len());
807        }
808        assert_eq!(sizes, vec![2, 2, 1]);
809    }
810
811    #[tokio::test]
812    async fn boolean_roundtrips_real_postgres_when_configured() {
813        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
814            return;
815        };
816        let mut config = deadpool_postgres::Config::new();
817        config.url = Some(url);
818        let pool = config
819            .create_pool(
820                Some(deadpool_postgres::Runtime::Tokio1),
821                tokio_postgres::NoTls,
822            )
823            .unwrap();
824        let executor = PgMutationExecutor::new(pool);
825        executor
826            .execute_sql(&CompiledQuery {
827                sql: "DROP TABLE IF EXISTS teaql_boolean_runtime_fixture".to_owned(),
828                params: vec![],
829                comment: None,
830            })
831            .await
832            .unwrap();
833        executor
834            .execute_sql(&CompiledQuery {
835                sql: "CREATE TABLE teaql_boolean_runtime_fixture(id BIGINT, required_flag BOOLEAN NOT NULL, optional_flag BOOLEAN)".to_owned(),
836                params: vec![],
837                comment: None,
838            })
839            .await
840            .unwrap();
841        for (id, required_flag, optional_flag) in [
842            (1_i64, Value::Bool(false), Value::Bool(true)),
843            (2_i64, Value::Bool(true), Value::Bool(false)),
844            (3_i64, Value::Bool(true), Value::Null),
845        ] {
846            executor
847                .execute_sql(&CompiledQuery {
848                    sql: "INSERT INTO teaql_boolean_runtime_fixture VALUES ($1, $2, $3)".to_owned(),
849                    params: vec![Value::I64(id), required_flag, optional_flag],
850                    comment: None,
851                })
852                .await
853                .unwrap();
854        }
855        let rows = executor
856            .fetch_all_compact_sql(&CompiledQuery {
857                sql: "SELECT required_flag, optional_flag FROM teaql_boolean_runtime_fixture ORDER BY id".to_owned(),
858                params: vec![],
859                comment: None,
860            })
861            .await
862            .unwrap();
863        assert_eq!(rows[0].get("required_flag"), Some(&Value::Bool(false)));
864        assert_eq!(rows[0].get("optional_flag"), Some(&Value::Bool(true)));
865        assert_eq!(rows[1].get("required_flag"), Some(&Value::Bool(true)));
866        assert_eq!(rows[1].get("optional_flag"), Some(&Value::Bool(false)));
867        assert_eq!(rows[2].get("optional_flag"), Some(&Value::Null));
868        executor
869            .execute_sql(&CompiledQuery {
870                sql: "DROP TABLE teaql_boolean_runtime_fixture".to_owned(),
871                params: vec![],
872                comment: None,
873            })
874            .await
875            .unwrap();
876    }
877
878    #[tokio::test]
879    async fn teaql_long_binds_to_legacy_postgres_int4_scalars_and_arrays() {
880        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
881            return;
882        };
883        let pool = configured_pool(url);
884        let executor = PgMutationExecutor::new(pool);
885        for sql in [
886            "DROP TABLE IF EXISTS teaql_int4_binding_fixture",
887            "CREATE TABLE teaql_int4_binding_fixture(id INTEGER PRIMARY KEY)",
888        ] {
889            executor
890                .execute_sql(&CompiledQuery {
891                    sql: sql.to_owned(),
892                    params: vec![],
893                    comment: None,
894                })
895                .await
896                .unwrap();
897        }
898        for id in [1_i64, i64::from(i32::MAX)] {
899            executor
900                .execute_sql(&CompiledQuery {
901                    sql: "INSERT INTO teaql_int4_binding_fixture(id) VALUES ($1)".to_owned(),
902                    params: vec![Value::I64(id)],
903                    comment: None,
904                })
905                .await
906                .unwrap();
907        }
908        let rows = executor
909            .fetch_all_compact_sql(&CompiledQuery {
910                sql: "SELECT id FROM teaql_int4_binding_fixture WHERE id = ANY($1) ORDER BY id"
911                    .to_owned(),
912                params: vec![Value::List(vec![
913                    Value::U64(1),
914                    Value::U64(i32::MAX as u64),
915                ])],
916                comment: None,
917            })
918            .await
919            .unwrap();
920        assert_eq!(rows.len(), 2);
921        assert_eq!(rows[0].get("id"), Some(&Value::I64(1)));
922
923        let overflow = executor
924            .fetch_all_compact_sql(&CompiledQuery {
925                sql: "SELECT id FROM teaql_int4_binding_fixture WHERE id = $1".to_owned(),
926                params: vec![Value::I64(i64::from(i32::MAX) + 1)],
927                comment: None,
928            })
929            .await;
930        assert!(overflow.is_err());
931        executor
932            .execute_sql(&CompiledQuery {
933                sql: "DROP TABLE teaql_int4_binding_fixture".to_owned(),
934                params: vec![],
935                comment: None,
936            })
937            .await
938            .unwrap();
939    }
940
941    #[tokio::test]
942    async fn topn_012_ensure_schema_creates_relation_index_idempotently() {
943        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
944            return;
945        };
946        let pool = configured_pool(url);
947        let client = pool.get().await.unwrap();
948        client
949            .batch_execute("DROP TABLE IF EXISTS teaql_relation_index_fixture")
950            .await
951            .unwrap();
952
953        let entity = EntityDescriptor::new("RelationIndexFixture")
954            .table_name("teaql_relation_index_fixture")
955            .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
956            .property(PropertyDescriptor::new("version", DataType::I64).version())
957            .property(PropertyDescriptor::new("vendor_id", DataType::U64).not_null())
958            .relation(
959                RelationDescriptor::new("vendor", "Vendor")
960                    .local_key("vendor_id")
961                    .foreign_key("id"),
962            );
963        let executor = PgMutationExecutor::new(pool.clone());
964
965        executor
966            .ensure_schema(&PostgresDialect, &[&entity])
967            .await
968            .unwrap();
969        executor
970            .ensure_schema(&PostgresDialect, &[&entity])
971            .await
972            .unwrap();
973
974        let rows = client
975            .query(
976                "SELECT indexdef FROM pg_indexes WHERE schemaname = current_schema() AND tablename = 'teaql_relation_index_fixture' AND indexdef LIKE '%(vendor_id, id DESC)%'",
977                &[],
978            )
979            .await
980            .unwrap();
981        assert_eq!(rows.len(), 1);
982
983        client
984            .batch_execute("DROP TABLE teaql_relation_index_fixture")
985            .await
986            .unwrap();
987    }
988
989    #[tokio::test]
990    async fn ensure_schema_creates_foreign_key_once_by_semantics() {
991        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
992            return;
993        };
994        let pool = configured_pool(url);
995        let client = pool.get().await.unwrap();
996        client
997            .batch_execute(
998                "DROP TABLE IF EXISTS teaql_fk_child_fixture;
999                 DROP TABLE IF EXISTS teaql_fk_parent_fixture;",
1000            )
1001            .await
1002            .unwrap();
1003
1004        let parent = EntityDescriptor::new("FkParentFixture")
1005            .table_name("teaql_fk_parent_fixture")
1006            .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1007            .relation(
1008                RelationDescriptor::new("children", "FkChildFixture")
1009                    .local_key("id")
1010                    .foreign_key("parent_id")
1011                    .many(),
1012            );
1013        let child = EntityDescriptor::new("FkChildFixture")
1014            .table_name("teaql_fk_child_fixture")
1015            .property(PropertyDescriptor::new("id", DataType::U64).id().not_null())
1016            .property(PropertyDescriptor::new("parent_id", DataType::U64).not_null())
1017            .relation(
1018                RelationDescriptor::new("parent", "FkParentFixture")
1019                    .local_key("parent_id")
1020                    .foreign_key("id"),
1021            );
1022        let executor = PgMutationExecutor::new(pool.clone());
1023
1024        executor
1025            .ensure_schema(&PostgresDialect, &[&child, &parent])
1026            .await
1027            .unwrap();
1028        executor
1029            .ensure_schema(&PostgresDialect, &[&parent, &child])
1030            .await
1031            .unwrap();
1032
1033        let count: i64 = client
1034            .query_one(
1035                "SELECT COUNT(*)
1036                   FROM pg_constraint c
1037                   JOIN pg_class t ON t.oid = c.conrelid
1038                  WHERE c.contype = 'f'
1039                    AND t.relname = 'teaql_fk_child_fixture'",
1040                &[],
1041            )
1042            .await
1043            .unwrap()
1044            .try_get(0)
1045            .unwrap();
1046        assert_eq!(count, 1);
1047
1048        let violation = client
1049            .execute(
1050                "INSERT INTO teaql_fk_child_fixture(id, parent_id) VALUES (1, 999)",
1051                &[],
1052            )
1053            .await;
1054        assert!(violation.is_err());
1055
1056        client
1057            .batch_execute(
1058                "DROP TABLE teaql_fk_child_fixture;
1059                 DROP TABLE teaql_fk_parent_fixture;",
1060            )
1061            .await
1062            .unwrap();
1063    }
1064
1065    #[tokio::test]
1066    async fn temporal_debug_sql_matches_real_postgres_when_configured() {
1067        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
1068            return;
1069        };
1070        let mut config = deadpool_postgres::Config::new();
1071        config.url = Some(url);
1072        let pool = config
1073            .create_pool(
1074                Some(deadpool_postgres::Runtime::Tokio1),
1075                tokio_postgres::NoTls,
1076            )
1077            .unwrap();
1078        let executor = PgMutationExecutor::new(pool);
1079        executor
1080            .execute_sql(&CompiledQuery {
1081                sql: "DROP TABLE IF EXISTS teaql_temporal_runtime_fixture".to_owned(),
1082                params: vec![],
1083                comment: None,
1084            })
1085            .await
1086            .unwrap();
1087        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();
1088        let prepared = CompiledQuery {
1089            sql: "INSERT INTO teaql_temporal_runtime_fixture VALUES ($1, $2, $3, TIMESTAMP '1960-01-02 03:04:05.678')".to_owned(),
1090            params: vec![
1091                Value::I64(1),
1092                Value::Date("2024-02-29".parse().unwrap()),
1093                Value::Timestamp(teaql_core::time::Timestamp(-315_521_754_322)),
1094            ],
1095            comment: Some("teaql source=temporal.verify $1".to_owned()),
1096        };
1097        executor.execute_sql(&prepared).await.unwrap();
1098        executor
1099            .execute_sql(&CompiledQuery {
1100                sql: prepared
1101                    .debug_sql(DatabaseKind::PostgreSql)
1102                    .replace("VALUES (1,", "VALUES (2,"),
1103                params: vec![],
1104                comment: None,
1105            })
1106            .await
1107            .unwrap();
1108        let rows = executor
1109            .fetch_all_compact_sql(&CompiledQuery {
1110                sql: "SELECT d, t, t_local FROM teaql_temporal_runtime_fixture ORDER BY id"
1111                    .to_owned(),
1112                params: vec![],
1113                comment: None,
1114            })
1115            .await
1116            .unwrap();
1117        assert_eq!(rows[0], rows[1]);
1118        executor
1119            .execute_sql(&CompiledQuery {
1120                sql: "DROP TABLE teaql_temporal_runtime_fixture".to_owned(),
1121                params: vec![],
1122                comment: None,
1123            })
1124            .await
1125            .unwrap();
1126    }
1127}
1128
1129#[derive(Debug, Default, Clone, Copy)]
1130pub struct PostgresSchemaProvider;
1131
1132impl SchemaProvider for PostgresSchemaProvider {
1133    fn ensure_schema<'a>(
1134        &'a self,
1135        context: &'a UserContext,
1136        _invocation: &'a teaql_runtime::SchemaInvocation,
1137    ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
1138        Box::pin(async move {
1139            ensure_postgres_schema_for(context)
1140                .await
1141                .map_err(|err| RuntimeError::Schema(err.to_string()))
1142        })
1143    }
1144}
1145
1146pub trait PostgresProviderExt {
1147    fn use_postgres_provider(&mut self, executor: PgMutationExecutor) -> &mut Self;
1148}
1149
1150impl PostgresProviderExt for UserContext {
1151    fn use_postgres_provider(&mut self, executor: PgMutationExecutor) -> &mut Self {
1152        self.insert_resource(PostgresDialect);
1153        self.insert_resource(executor);
1154        self.set_schema_provider(PostgresSchemaProvider);
1155        self
1156    }
1157}
1158
1159#[derive(Clone)]
1160pub struct PgIdSpaceGenerator {
1161    pool: Pool,
1162    table_name: String,
1163}
1164
1165impl PgIdSpaceGenerator {
1166    pub fn new(pool: Pool) -> Self {
1167        Self {
1168            pool,
1169            table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
1170        }
1171    }
1172
1173    pub fn from_executor(executor: PgMutationExecutor) -> Self {
1174        Self::new(executor.pool())
1175    }
1176
1177    pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
1178        self.table_name = table_name.into();
1179        self
1180    }
1181
1182    pub async fn ensure_table(&self) -> Result<(), MutationExecutorError> {
1183        PgMutationExecutor::new(self.pool.clone())
1184            .ensure_id_space_table(&self.table_name)
1185            .await
1186    }
1187
1188    pub async fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
1189        let entity = canonical_id_space_entity(entity);
1190        let entity = entity.as_str();
1191        self.ensure_table().await?;
1192        let table = quote_ident(&self.table_name);
1193        let client = self
1194            .pool
1195            .get()
1196            .await
1197            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
1198        let select_sql = format!("SELECT current_level FROM {table} WHERE type_name = $1");
1199        let insert_sql = format!("INSERT INTO {table}(type_name, current_level) VALUES ($1, 1)");
1200        let update_sql = format!(
1201            "UPDATE {table} SET current_level = $1 WHERE type_name = $2 AND current_level = $3"
1202        );
1203        for _ in 1..=100 {
1204            let current = client
1205                .query_opt(&select_sql, &[&entity])
1206                .await?
1207                .map(|row| row.try_get::<_, i64>(0))
1208                .transpose()?;
1209            if let Some(current) = current {
1210                let next = current.checked_add(1).ok_or_else(|| {
1211                    MutationExecutorError::Bind(format!("ID space overflow for {entity}"))
1212                })?;
1213                if client
1214                    .execute(&update_sql, &[&next, &entity, &current])
1215                    .await?
1216                    == 1
1217                {
1218                    return u64::try_from(next).map_err(|_| {
1219                        MutationExecutorError::Bind(format!(
1220                            "generated id {next} cannot be represented as u64"
1221                        ))
1222                    });
1223                }
1224            } else {
1225                match client.execute(&insert_sql, &[&entity]).await {
1226                    Ok(1) => return Ok(1),
1227                    Ok(changed) => {
1228                        return Err(MutationExecutorError::Bind(format!(
1229                            "ID space insert for {entity} changed {changed} rows"
1230                        )));
1231                    }
1232                    Err(error) => {
1233                        if client.query_opt(&select_sql, &[&entity]).await?.is_none() {
1234                            return Err(error.into());
1235                        }
1236                    }
1237                }
1238            }
1239        }
1240        Err(MutationExecutorError::Bind(format!(
1241            "Unable to allocate ID for {entity} after 100 optimistic-lock attempts"
1242        )))
1243    }
1244
1245    pub async fn ensure_floor(
1246        &self,
1247        entity: &str,
1248        floor: u64,
1249    ) -> Result<(), MutationExecutorError> {
1250        let entity = canonical_id_space_entity(entity);
1251        let entity = entity.as_str();
1252        self.ensure_table().await?;
1253        let floor = i64::try_from(floor).map_err(|_| {
1254            MutationExecutorError::Bind(format!(
1255                "ID space floor {floor} for {entity} exceeds BIGINT"
1256            ))
1257        })?;
1258        let table = quote_ident(&self.table_name);
1259        let client = self
1260            .pool
1261            .get()
1262            .await
1263            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
1264        let select = format!("SELECT current_level FROM {table} WHERE type_name = $1");
1265        let insert = format!("INSERT INTO {table}(type_name, current_level) VALUES ($1, $2)");
1266        let update = format!(
1267            "UPDATE {table} SET current_level = $1 WHERE type_name = $2 AND current_level = $3"
1268        );
1269        for _ in 1..=100 {
1270            let current = client
1271                .query_opt(&select, &[&entity])
1272                .await?
1273                .map(|row| row.try_get::<_, i64>(0))
1274                .transpose()?;
1275            match current {
1276                Some(current) if current >= floor => return Ok(()),
1277                Some(current) => {
1278                    if client
1279                        .execute(&update, &[&floor, &entity, &current])
1280                        .await?
1281                        == 1
1282                    {
1283                        return Ok(());
1284                    }
1285                }
1286                None => match client.execute(&insert, &[&entity, &floor]).await {
1287                    Ok(1) => return Ok(()),
1288                    Ok(_) => {}
1289                    Err(error) => {
1290                        if client.query_opt(&select, &[&entity]).await?.is_none() {
1291                            return Err(error.into());
1292                        }
1293                    }
1294                },
1295            }
1296        }
1297        Err(MutationExecutorError::Bind(format!(
1298            "Unable to synchronize ID space floor for {entity} after 100 optimistic-lock attempts"
1299        )))
1300    }
1301}
1302
1303impl InternalIdGenerator for PgIdSpaceGenerator {
1304    fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
1305        let generator = self.clone();
1306        let entity = entity.to_owned();
1307        block_on_id_generation(async move { generator.next_id(&entity).await })
1308    }
1309}
1310
1311fn block_on_id_generation<F>(future: F) -> Result<u64, RuntimeError>
1312where
1313    F: Future<Output = Result<u64, MutationExecutorError>> + Send + 'static,
1314{
1315    let result = match tokio::runtime::Handle::try_current() {
1316        Ok(handle) => tokio::task::block_in_place(|| handle.block_on(future)),
1317        Err(_) => tokio::runtime::Builder::new_current_thread()
1318            .enable_all()
1319            .build()
1320            .map_err(|err| RuntimeError::IdGeneration(err.to_string()))?
1321            .block_on(future),
1322    };
1323    result.map_err(|err| RuntimeError::IdGeneration(err.to_string()))
1324}
1325
1326fn quote_ident(ident: &str) -> String {
1327    quote_identifier_if_needed(ident, '"')
1328}
1329
1330/// Strip wrapping identifier quotes from a SQL identifier so that bare column
1331/// names returned by `information_schema.columns` can be compared with
1332/// potentially-quoted `PropertyDescriptor::column_name` values.
1333fn strip_identifier_quotes(ident: &str) -> &str {
1334    let bytes = ident.as_bytes();
1335    if bytes.len() >= 2 {
1336        let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
1337        if (first == b'"' && last == b'"')
1338            || (first == b'`' && last == b'`')
1339            || (first == b'[' && last == b']')
1340        {
1341            return &ident[1..ident.len() - 1];
1342        }
1343    }
1344    ident
1345}
1346
1347fn try_parse_datetime_from_str(s: &str) -> Option<chrono::DateTime<chrono::Utc>> {
1348    if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
1349        return Some(dt.with_timezone(&chrono::Utc));
1350    }
1351    if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
1352        return Some(chrono::DateTime::from_naive_utc_and_offset(
1353            ndt,
1354            chrono::Utc,
1355        ));
1356    }
1357    if let Ok(nd) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
1358        let ndt = nd.and_hms_opt(0, 0, 0)?;
1359        return Some(chrono::DateTime::from_naive_utc_and_offset(
1360            ndt,
1361            chrono::Utc,
1362        ));
1363    }
1364    None
1365}
1366
1367#[derive(Debug, Clone, Copy)]
1368struct PgNull;
1369
1370impl tokio_postgres::types::ToSql for PgNull {
1371    fn to_sql(
1372        &self,
1373        ty: &tokio_postgres::types::Type,
1374        out: &mut bytes::BytesMut,
1375    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1376        Ok(tokio_postgres::types::IsNull::Yes)
1377    }
1378
1379    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1380        true
1381    }
1382
1383    fn to_sql_checked(
1384        &self,
1385        ty: &tokio_postgres::types::Type,
1386        out: &mut bytes::BytesMut,
1387    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1388        Ok(tokio_postgres::types::IsNull::Yes)
1389    }
1390}
1391
1392#[derive(Debug, Clone, Copy)]
1393struct PgTimestamp(DateTime<Utc>);
1394
1395impl tokio_postgres::types::ToSql for PgTimestamp {
1396    fn to_sql(
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        if *ty == tokio_postgres::types::Type::TIMESTAMP {
1402            self.0.naive_utc().to_sql(ty, out)
1403        } else {
1404            self.0.to_sql(ty, out)
1405        }
1406    }
1407
1408    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1409        *ty == tokio_postgres::types::Type::TIMESTAMP
1410            || *ty == tokio_postgres::types::Type::TIMESTAMPTZ
1411    }
1412
1413    tokio_postgres::types::to_sql_checked!();
1414}
1415
1416#[derive(Debug, Clone, Copy)]
1417struct PgInteger(i64);
1418
1419impl tokio_postgres::types::ToSql for PgInteger {
1420    fn to_sql(
1421        &self,
1422        ty: &tokio_postgres::types::Type,
1423        out: &mut bytes::BytesMut,
1424    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1425        match *ty {
1426            tokio_postgres::types::Type::INT2 => i16::try_from(self.0)?.to_sql(ty, out),
1427            tokio_postgres::types::Type::INT4 => i32::try_from(self.0)?.to_sql(ty, out),
1428            tokio_postgres::types::Type::INT8 => self.0.to_sql(ty, out),
1429            _ => Err(format!("integer cannot be encoded as PostgreSQL type {ty}").into()),
1430        }
1431    }
1432
1433    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1434        matches!(
1435            *ty,
1436            tokio_postgres::types::Type::INT2
1437                | tokio_postgres::types::Type::INT4
1438                | tokio_postgres::types::Type::INT8
1439        )
1440    }
1441
1442    tokio_postgres::types::to_sql_checked!();
1443}
1444
1445#[derive(Debug, Clone)]
1446struct PgIntegerList(Vec<i64>);
1447
1448impl tokio_postgres::types::ToSql for PgIntegerList {
1449    fn to_sql(
1450        &self,
1451        ty: &tokio_postgres::types::Type,
1452        out: &mut bytes::BytesMut,
1453    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1454        match *ty {
1455            tokio_postgres::types::Type::INT2_ARRAY => self
1456                .0
1457                .iter()
1458                .copied()
1459                .map(i16::try_from)
1460                .collect::<Result<Vec<_>, _>>()?
1461                .to_sql(ty, out),
1462            tokio_postgres::types::Type::INT4_ARRAY => self
1463                .0
1464                .iter()
1465                .copied()
1466                .map(i32::try_from)
1467                .collect::<Result<Vec<_>, _>>()?
1468                .to_sql(ty, out),
1469            tokio_postgres::types::Type::INT8_ARRAY => self.0.to_sql(ty, out),
1470            _ => Err(format!("integer list cannot be encoded as PostgreSQL type {ty}").into()),
1471        }
1472    }
1473
1474    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1475        matches!(
1476            *ty,
1477            tokio_postgres::types::Type::INT2_ARRAY
1478                | tokio_postgres::types::Type::INT4_ARRAY
1479                | tokio_postgres::types::Type::INT8_ARRAY
1480        )
1481    }
1482
1483    tokio_postgres::types::to_sql_checked!();
1484}
1485
1486struct PgArgs {
1487    values: Vec<Box<dyn tokio_postgres::types::ToSql + Sync + Send>>,
1488}
1489impl PgArgs {
1490    fn add<T: tokio_postgres::types::ToSql + Sync + Send + 'static>(&mut self, v: T) {
1491        self.values.push(Box::new(v));
1492    }
1493    fn as_refs(&self) -> Vec<&(dyn tokio_postgres::types::ToSql + Sync)> {
1494        self.values.iter().map(|b| b.as_ref() as _).collect()
1495    }
1496}
1497
1498fn bind_pg(args: &mut PgArgs, value: &Value) -> Result<(), MutationExecutorError> {
1499    match value {
1500        Value::Null => {
1501            args.add(PgNull);
1502        }
1503        Value::Bool(v) => args.add(*v),
1504        Value::I64(v) => args.add(PgInteger(*v)),
1505        Value::U64(v) => {
1506            let v = i64::try_from(*v).map_err(|_| {
1507                MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))
1508            })?;
1509            args.add(PgInteger(v));
1510        }
1511        Value::F64(v) => args.add(*v),
1512        Value::Decimal(v) => args.add(*v),
1513        Value::Text(v) => match try_parse_datetime_from_str(v) {
1514            Some(dt) => args.add(dt),
1515            None => args.add(v.clone()),
1516        },
1517        Value::Json(v) => {
1518            let j_val: serde_json::Value =
1519                serde_json::to_value(v).map_err(|e| MutationExecutorError::Bind(e.to_string()))?;
1520            args.add(j_val);
1521        }
1522        Value::Date(v) => args.add(*v),
1523        Value::Timestamp(v) => args.add(PgTimestamp(v.to_datetime())),
1524        Value::Object(_) => return Err(MutationExecutorError::UnsupportedValue("object")),
1525        Value::List(values) => bind_pg_list(args, values)?,
1526        Value::TypedNull(dt) => match dt {
1527            DataType::Bool => args.add(Option::<bool>::None),
1528            DataType::I64 | DataType::U64 => args.add(Option::<i64>::None),
1529            DataType::F64 => args.add(Option::<f64>::None),
1530            DataType::Decimal => args.add(Option::<Decimal>::None),
1531            DataType::Text | DataType::LargeText => args.add(Option::<String>::None),
1532            DataType::Json => args.add(Option::<serde_json::Value>::None),
1533            DataType::Date => args.add(Option::<NaiveDate>::None),
1534            DataType::Timestamp => args.add(PgNull),
1535        },
1536    }
1537    Ok(())
1538}
1539
1540fn bind_pg_list(args: &mut PgArgs, values: &[Value]) -> Result<(), MutationExecutorError> {
1541    let Some(first) = values.first() else {
1542        return Err(MutationExecutorError::UnsupportedValue("empty list"));
1543    };
1544    match first {
1545        Value::Bool(_) => {
1546            let values = values
1547                .iter()
1548                .map(|value| match value {
1549                    Value::Bool(value) => Ok(*value),
1550                    _ => Err(MutationExecutorError::UnsupportedValue("mixed bool list")),
1551                })
1552                .collect::<Result<Vec<_>, _>>()?;
1553            args.add(values);
1554        }
1555        Value::I64(_) => {
1556            let values = values
1557                .iter()
1558                .map(|value| match value {
1559                    Value::I64(value) => Ok(*value),
1560                    _ => Err(MutationExecutorError::UnsupportedValue("mixed i64 list")),
1561                })
1562                .collect::<Result<Vec<_>, _>>()?;
1563            args.add(PgIntegerList(values));
1564        }
1565        Value::U64(_) => {
1566            let values = values
1567                .iter()
1568                .map(|value| match value {
1569                    Value::U64(value) => i64::try_from(*value).map_err(|_| {
1570                        MutationExecutorError::Bind(format!("u64 value {value} exceeds i64 range"))
1571                    }),
1572                    _ => Err(MutationExecutorError::UnsupportedValue("mixed u64 list")),
1573                })
1574                .collect::<Result<Vec<_>, _>>()?;
1575            args.add(PgIntegerList(values));
1576        }
1577        Value::F64(_) => {
1578            let values = values
1579                .iter()
1580                .map(|value| match value {
1581                    Value::F64(value) => Ok(*value),
1582                    _ => Err(MutationExecutorError::UnsupportedValue("mixed f64 list")),
1583                })
1584                .collect::<Result<Vec<_>, _>>()?;
1585            args.add(values);
1586        }
1587        Value::Decimal(_) => {
1588            let values = values
1589                .iter()
1590                .map(|value| match value {
1591                    Value::Decimal(value) => Ok(*value),
1592                    _ => Err(MutationExecutorError::UnsupportedValue(
1593                        "mixed decimal list",
1594                    )),
1595                })
1596                .collect::<Result<Vec<_>, _>>()?;
1597            args.add(values);
1598        }
1599        Value::Text(_) => {
1600            let values = values
1601                .iter()
1602                .map(|value| match value {
1603                    Value::Text(value) => Ok(value.clone()),
1604                    _ => Err(MutationExecutorError::UnsupportedValue("mixed text list")),
1605                })
1606                .collect::<Result<Vec<_>, _>>()?;
1607            args.add(values);
1608        }
1609        Value::Date(_) => {
1610            let values = values
1611                .iter()
1612                .map(|value| match value {
1613                    Value::Date(value) => Ok(*value),
1614                    _ => Err(MutationExecutorError::UnsupportedValue("mixed date list")),
1615                })
1616                .collect::<Result<Vec<_>, _>>()?;
1617            args.add(values);
1618        }
1619        Value::Timestamp(_) => {
1620            let values = values
1621                .iter()
1622                .map(|value| match value {
1623                    Value::Timestamp(value) => Ok(value.to_datetime()),
1624                    _ => Err(MutationExecutorError::UnsupportedValue(
1625                        "mixed timestamp list",
1626                    )),
1627                })
1628                .collect::<Result<Vec<_>, _>>()?;
1629            args.add(values);
1630        }
1631        Value::Null => return Err(MutationExecutorError::UnsupportedValue("null list")),
1632        Value::Json(_) => return Err(MutationExecutorError::UnsupportedValue("json list")),
1633        Value::Object(_) => return Err(MutationExecutorError::UnsupportedValue("object list")),
1634        Value::List(_) => return Err(MutationExecutorError::UnsupportedValue("nested list")),
1635        Value::TypedNull(_) => return Err(MutationExecutorError::UnsupportedValue("null list")),
1636    }
1637    Ok(())
1638}
1639
1640fn decode_pg_values(row: &tokio_postgres::Row) -> Result<Vec<Value>, MutationExecutorError> {
1641    let mut values = Vec::with_capacity(row.len());
1642    for (index, column) in row.columns().iter().enumerate() {
1643        let type_name = column.type_().name();
1644
1645        let value = match type_name {
1646            "bool" | "boolean" => {
1647                let v: Option<bool> = row.try_get(index)?;
1648                match v {
1649                    Some(v) => Value::Bool(v),
1650                    None => Value::Null,
1651                }
1652            }
1653            "int2" => {
1654                let v: Option<i16> = row.try_get(index)?;
1655                match v {
1656                    Some(v) => Value::I64(v as i64),
1657                    None => Value::Null,
1658                }
1659            }
1660            "int4" => {
1661                let v: Option<i32> = row.try_get(index)?;
1662                match v {
1663                    Some(v) => Value::I64(v as i64),
1664                    None => Value::Null,
1665                }
1666            }
1667            "int8" => {
1668                let v: Option<i64> = row.try_get(index)?;
1669                match v {
1670                    Some(v) => Value::I64(v),
1671                    None => Value::Null,
1672                }
1673            }
1674            "float4" => {
1675                let v: Option<f32> = row.try_get(index)?;
1676                match v {
1677                    Some(v) => Value::F64(v as f64),
1678                    None => Value::Null,
1679                }
1680            }
1681            "float8" => {
1682                let v: Option<f64> = row.try_get(index)?;
1683                match v {
1684                    Some(v) => Value::F64(v),
1685                    None => Value::Null,
1686                }
1687            }
1688            "numeric" => {
1689                let v: Option<Decimal> = row.try_get(index)?;
1690                match v {
1691                    Some(v) => Value::Decimal(v),
1692                    None => Value::Null,
1693                }
1694            }
1695            "json" | "jsonb" => {
1696                let v: Option<serde_json::Value> = row.try_get(index)?;
1697                match v {
1698                    Some(j) => Value::Json(j.into()),
1699                    None => Value::Null,
1700                }
1701            }
1702            "date" => {
1703                let v: Option<NaiveDate> = row.try_get(index)?;
1704                match v {
1705                    Some(v) => Value::Date(v),
1706                    None => Value::Null,
1707                }
1708            }
1709            "timestamp" => {
1710                let v: Option<NaiveDateTime> = row.try_get(index)?;
1711                match v {
1712                    Some(v) => Value::Timestamp(teaql_core::time::Timestamp(
1713                        v.and_utc().timestamp_millis(),
1714                    )),
1715                    None => Value::Null,
1716                }
1717            }
1718            "timestamptz" => {
1719                let v: Option<DateTime<Utc>> = row.try_get(index)?;
1720                match v {
1721                    Some(v) => Value::Timestamp(teaql_core::time::Timestamp(v.timestamp_millis())),
1722                    None => Value::Null,
1723                }
1724            }
1725            "text" | "varchar" | "bpchar" | "name" | "uuid" => {
1726                let v: Option<String> = row.try_get(index)?;
1727                match v {
1728                    Some(v) => Value::Text(v),
1729                    None => Value::Null,
1730                }
1731            }
1732            other => {
1733                return Err(MutationExecutorError::UnsupportedColumnType(
1734                    other.to_owned(),
1735                ));
1736            }
1737        };
1738        values.push(value);
1739    }
1740    Ok(values)
1741}
1742
1743#[cfg(test)]
1744mod tests {
1745    use super::*;
1746    use teaql_core::{DeleteCommand, RecoverCommand, RelationDescriptor};
1747
1748    fn entity() -> EntityDescriptor {
1749        EntityDescriptor::new("Order")
1750            .table_name("orders")
1751            .property(
1752                PropertyDescriptor::new("id", DataType::U64)
1753                    .column_name("id")
1754                    .id()
1755                    .not_null(),
1756            )
1757            .property(
1758                PropertyDescriptor::new("version", DataType::I64)
1759                    .column_name("version")
1760                    .version()
1761                    .not_null(),
1762            )
1763            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1764    }
1765
1766    #[test]
1767    fn postgres_dialect_compiles_mutations_with_numbered_placeholders() {
1768        let insert = PostgresDialect
1769            .compile_insert(
1770                &entity(),
1771                &InsertCommand::new("Order")
1772                    .value("id", 1_u64)
1773                    .value("name", "A"),
1774            )
1775            .unwrap();
1776        assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES ($1, $2)");
1777
1778        let update = PostgresDialect
1779            .compile_update(
1780                &entity(),
1781                &UpdateCommand::new("Order", 1_u64)
1782                    .expected_version(3)
1783                    .value("name", "B"),
1784            )
1785            .unwrap();
1786        assert_eq!(
1787            update.sql,
1788            "UPDATE orders SET name = $1, version = $2 WHERE id = $3 AND version = $4"
1789        );
1790
1791        let delete = PostgresDialect
1792            .compile_delete(
1793                &entity(),
1794                &DeleteCommand::new("Order", 1_u64).expected_version(3),
1795            )
1796            .unwrap();
1797        let recover = PostgresDialect
1798            .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1799            .unwrap();
1800        assert_eq!(
1801            delete.sql,
1802            "UPDATE orders SET version = $1 WHERE id = $2 AND version = $3"
1803        );
1804        assert_eq!(
1805            recover.sql,
1806            "UPDATE orders SET version = $1 WHERE id = $2 AND version = $3"
1807        );
1808    }
1809
1810    #[test]
1811    fn postgres_dialect_compiles_schema_and_large_in_array_binds() {
1812        let create = PostgresDialect.compile_create_table(&entity()).unwrap();
1813        assert_eq!(
1814            create,
1815            "CREATE TABLE IF NOT EXISTS orders (id BIGINT PRIMARY KEY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255))"
1816        );
1817        assert!(
1818            PostgresDialect
1819                .schema_setup_sqls()
1820                .iter()
1821                .any(|sql| sql.contains("CREATE OR REPLACE FUNCTION soundex"))
1822        );
1823
1824        let values = (1_u64..=21).map(Value::from).collect::<Vec<_>>();
1825        let query = PostgresDialect
1826            .compile_select(
1827                &entity(),
1828                &SelectQuery::new("Order")
1829                    .filter(Expr::in_list("id", values.clone()))
1830                    .order_asc("id"),
1831            )
1832            .unwrap();
1833        assert_eq!(
1834            query.sql,
1835            "SELECT id, version, name FROM orders WHERE (id = ANY($1)) ORDER BY id ASC"
1836        );
1837        assert_eq!(query.params, vec![Value::List(values)]);
1838    }
1839
1840    #[test]
1841    fn topn_012_postgres_schema_adds_full_foreign_key_id_desc_index() {
1842        let trip = EntityDescriptor::new("Trip")
1843            .table_name("trip_data")
1844            .property(
1845                PropertyDescriptor::new("id", DataType::U64)
1846                    .column_name("id")
1847                    .id()
1848                    .not_null(),
1849            )
1850            .property(
1851                PropertyDescriptor::new("vendor_id", DataType::U64)
1852                    .column_name("vendor")
1853                    .not_null(),
1854            )
1855            .relation(
1856                RelationDescriptor::new("vendor", "Vendor")
1857                    .local_key("vendor_id")
1858                    .foreign_key("id"),
1859            )
1860            // A second relation through the same key must not duplicate DDL.
1861            .relation(
1862                RelationDescriptor::new("billing_vendor", "Vendor")
1863                    .local_key("vendor_id")
1864                    .foreign_key("id"),
1865            )
1866            // Reverse relations belong to the target table and are ignored here.
1867            .relation(
1868                RelationDescriptor::new("items", "TripItem")
1869                    .local_key("id")
1870                    .foreign_key("trip_id")
1871                    .many(),
1872            );
1873
1874        assert_eq!(
1875            PostgresDialect.relation_indexes_sqls(&trip),
1876            vec![
1877                "CREATE INDEX IF NOT EXISTS IDX_TRIP_DATA_VENDOR_ID_DESC ON trip_data (vendor, id DESC)"
1878            ]
1879        );
1880    }
1881
1882    #[test]
1883    fn postgres_relation_index_name_is_stable_and_within_identifier_limit() {
1884        let name = postgres_index_name(
1885            "an_extremely_long_generated_transaction_history_table_name",
1886            "an_equally_long_business_owner_reference_identifier",
1887            "id",
1888        );
1889        assert!(name.len() <= 63);
1890        assert_eq!(
1891            name,
1892            postgres_index_name(
1893                "an_extremely_long_generated_transaction_history_table_name",
1894                "an_equally_long_business_owner_reference_identifier",
1895                "id",
1896            )
1897        );
1898        assert!(name.ends_with("_889B21BBED38CC82"));
1899    }
1900}