Skip to main content

teaql_provider_postgres/
lib.rs

1#![allow(warnings)]
2use std::collections::BTreeMap;
3use std::future::Future;
4use std::pin::Pin;
5
6use chrono::{DateTime, NaiveDate, Utc};
7use deadpool_postgres::Pool;
8use rust_decimal::Decimal;
9use std::sync::Arc;
10use teaql_core::{
11    BinaryOp, DataType, EntityDescriptor, Expr, InsertCommand, PropertyDescriptor, Record,
12    SelectQuery, UpdateCommand, Value,
13};
14use teaql_runtime::{GraphNode, InternalIdGenerator, RuntimeError, SchemaProvider, UserContext};
15use teaql_sql::{
16    CompiledQuery, DatabaseKind, SqlCompileError, SqlDialect, SqlTransport,
17    quote_identifier_if_needed,
18};
19use tokio::sync::Mutex;
20
21pub const DEFAULT_ID_SPACE_TABLE: &str = "teaql_id_space";
22
23#[derive(Debug, Default, Clone, Copy)]
24pub struct PostgresDialect;
25
26impl SqlDialect for PostgresDialect {
27    fn kind(&self) -> DatabaseKind {
28        DatabaseKind::PostgreSql
29    }
30
31    fn quote_ident(&self, ident: &str) -> String {
32        quote_ident(ident)
33    }
34
35    fn placeholder(&self, index: usize) -> String {
36        format!("${index}")
37    }
38
39    fn schema_setup_sqls(&self) -> &'static [&'static str] {
40        &[CREATE_SOUNDEX_FUNCTION]
41    }
42
43    fn schema_type_sql(
44        &self,
45        data_type: DataType,
46        _property: &PropertyDescriptor,
47    ) -> Result<&'static str, SqlCompileError> {
48        match data_type {
49            DataType::Bool => Ok("BOOLEAN"),
50            DataType::I64 | DataType::U64 => Ok("BIGINT"),
51            DataType::F64 => Ok("DOUBLE PRECISION"),
52            DataType::Decimal => Ok("NUMERIC"),
53            DataType::Text => Ok("VARCHAR(255)"),
54            DataType::LargeText => Ok("TEXT"),
55            DataType::Json => Ok("JSONB"),
56            DataType::Date => Ok("DATE"),
57            DataType::Timestamp => Ok("TIMESTAMPTZ"),
58        }
59    }
60
61    fn compile_in(
62        &self,
63        entity: &EntityDescriptor,
64        left: &Expr,
65        op: BinaryOp,
66        right: &Expr,
67        params: &mut Vec<Value>,
68    ) -> Result<String, SqlCompileError> {
69        match op {
70            BinaryOp::InLarge | BinaryOp::NotInLarge => {
71                let Expr::Value(Value::List(values)) = right else {
72                    let lhs = self.compile_expr(entity, left, params)?;
73                    let rhs = self.compile_expr(entity, right, params)?;
74                    let operator = match op {
75                        BinaryOp::InLarge => "= ANY",
76                        BinaryOp::NotInLarge => "<> ALL",
77                        _ => unreachable!(),
78                    };
79                    return Ok(format!("({lhs} {operator} ({rhs}))"));
80                };
81                if values.is_empty() {
82                    return Err(SqlCompileError::EmptyInList);
83                }
84                let lhs = self.compile_expr(entity, left, params)?;
85                params.push(Value::List(values.clone()));
86                let placeholder = self.placeholder(params.len());
87                let operator = match op {
88                    BinaryOp::InLarge => "= ANY",
89                    BinaryOp::NotInLarge => "<> ALL",
90                    _ => unreachable!(),
91                };
92                Ok(format!("({lhs} {operator}({placeholder}))"))
93            }
94            _ => {
95                let lhs = self.compile_expr(entity, left, params)?;
96                let operator = match op {
97                    BinaryOp::In => "IN",
98                    BinaryOp::NotIn => "NOT IN",
99                    _ => unreachable!(),
100                };
101                match right {
102                    Expr::Value(Value::List(values)) => {
103                        if values.is_empty() {
104                            return Err(SqlCompileError::EmptyInList);
105                        }
106                        let mut placeholders = Vec::with_capacity(values.len());
107                        for value in values {
108                            params.push(value.clone());
109                            placeholders.push(self.placeholder(params.len()));
110                        }
111                        Ok(format!("({lhs} {operator} ({}))", placeholders.join(", ")))
112                    }
113                    _ => {
114                        let rhs = self.compile_expr(entity, right, params)?;
115                        Ok(format!("({lhs} {operator} ({rhs}))"))
116                    }
117                }
118            }
119        }
120    }
121}
122
123const CREATE_SOUNDEX_FUNCTION: &str = r#"
124CREATE OR REPLACE FUNCTION soundex(input text)
125RETURNS text
126LANGUAGE plpgsql
127IMMUTABLE
128STRICT
129AS $$
130DECLARE
131    normalized text := upper(regexp_replace(input, '[^A-Za-z]', '', 'g'));
132    first_char text;
133    output text;
134    previous_code text;
135    code text;
136    ch text;
137    i integer;
138BEGIN
139    IF normalized = '' THEN
140        RETURN '0000';
141    END IF;
142
143    first_char := substr(normalized, 1, 1);
144    output := first_char;
145    previous_code := CASE
146        WHEN first_char IN ('B', 'F', 'P', 'V') THEN '1'
147        WHEN first_char IN ('C', 'G', 'J', 'K', 'Q', 'S', 'X', 'Z') THEN '2'
148        WHEN first_char IN ('D', 'T') THEN '3'
149        WHEN first_char = 'L' THEN '4'
150        WHEN first_char IN ('M', 'N') THEN '5'
151        WHEN first_char = 'R' THEN '6'
152        ELSE '0'
153    END;
154
155    FOR i IN 2..char_length(normalized) LOOP
156        ch := substr(normalized, i, 1);
157        code := CASE
158            WHEN ch IN ('B', 'F', 'P', 'V') THEN '1'
159            WHEN ch IN ('C', 'G', 'J', 'K', 'Q', 'S', 'X', 'Z') THEN '2'
160            WHEN ch IN ('D', 'T') THEN '3'
161            WHEN ch = 'L' THEN '4'
162            WHEN ch IN ('M', 'N') THEN '5'
163            WHEN ch = 'R' THEN '6'
164            ELSE '0'
165        END;
166
167        IF code <> '0' AND code <> previous_code THEN
168            output := output || code;
169            IF char_length(output) = 4 THEN
170                RETURN output;
171            END IF;
172        END IF;
173        previous_code := code;
174    END LOOP;
175
176    RETURN rpad(output, 4, '0');
177END;
178$$
179"#;
180
181#[derive(Debug)]
182pub enum MutationExecutorError {
183    Driver(tokio_postgres::Error),
184    Pool(String),
185    SqlCompile(SqlCompileError),
186    UnsupportedValue(&'static str),
187    UnsupportedColumnType(String),
188    Bind(String),
189}
190
191impl std::fmt::Display for MutationExecutorError {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        match self {
194            Self::Driver(err) => err.fmt(f),
195            Self::Pool(err) => write!(f, "postgres pool error: {err}"),
196            Self::SqlCompile(err) => err.fmt(f),
197            Self::UnsupportedValue(kind) => {
198                write!(f, "unsupported bind value for mutation executor: {kind}")
199            }
200            Self::UnsupportedColumnType(kind) => {
201                write!(f, "unsupported column type for record decoding: {kind}")
202            }
203            Self::Bind(message) => write!(f, "bind error: {message}"),
204        }
205    }
206}
207
208impl std::error::Error for MutationExecutorError {}
209
210impl From<tokio_postgres::Error> for MutationExecutorError {
211    fn from(value: tokio_postgres::Error) -> Self {
212        Self::Driver(value)
213    }
214}
215
216impl From<SqlCompileError> for MutationExecutorError {
217    fn from(value: SqlCompileError) -> Self {
218        Self::SqlCompile(value)
219    }
220}
221
222#[derive(Clone)]
223pub struct PgMutationExecutor {
224    pool: Pool,
225}
226
227impl SqlTransport for PgMutationExecutor {
228    type Error = MutationExecutorError;
229
230    async fn fetch_all_sql(&self, query: &CompiledQuery) -> Result<Vec<Record>, Self::Error> {
231        self.fetch_all(query).await
232    }
233
234    async fn execute_sql(&self, query: &CompiledQuery) -> Result<u64, Self::Error> {
235        self.execute(query).await
236    }
237}
238
239impl teaql_sql::StreamingSqlTransport for PgMutationExecutor {
240    fn stream_sql(
241        &self,
242        query: CompiledQuery,
243        chunk_size: usize,
244    ) -> teaql_data_service::QueryStream<'_, Self::Error> {
245        let pool = self.pool.clone();
246        Box::pin(async_stream::try_stream! {
247            use futures_util::TryStreamExt;
248            let mut args = PgArgs { values: Vec::new() }; for value in &query.params { bind_pg(&mut args, value)?; }
249            let client = pool.get().await.map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
250            let params = args.as_refs();
251            let rows = client.query_raw(&query.sql, params).await?;
252            futures_util::pin_mut!(rows);
253            let mut chunk = Vec::with_capacity(chunk_size); let mut index = 0;
254            while let Some(row) = rows.try_next().await? { chunk.push(decode_pg_row(&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; } }
255            if !chunk.is_empty() { yield teaql_data_service::StreamChunk { rows:chunk, chunk_index:index, is_last:true }; }
256        })
257    }
258}
259
260impl teaql_sql::SqlTransaction for PgMutationExecutor {
261    type Error = MutationExecutorError;
262
263    async fn commit_sql(self) -> Result<(), Self::Error> {
264        Err(MutationExecutorError::Bind(
265            "Transactions not supported yet".to_string(),
266        ))
267    }
268
269    async fn rollback_sql(self) -> Result<(), Self::Error> {
270        Err(MutationExecutorError::Bind(
271            "Transactions not supported yet".to_string(),
272        ))
273    }
274}
275
276impl teaql_sql::SqlTransactionTransport for PgMutationExecutor {
277    type Tx<'a>
278        = Self
279    where
280        Self: 'a;
281
282    async fn begin_sql(&self) -> Result<Self::Tx<'_>, Self::Error> {
283        Err(MutationExecutorError::Bind(
284            "Transactions not supported yet".to_string(),
285        ))
286    }
287}
288
289impl PgMutationExecutor {
290    pub fn new(pool: Pool) -> Self {
291        Self { pool }
292    }
293
294    pub fn pool(&self) -> Pool {
295        self.pool.clone()
296    }
297
298    pub async fn ensure_schema(
299        &self,
300        dialect: &PostgresDialect,
301        entities: &[&EntityDescriptor],
302    ) -> Result<(), MutationExecutorError> {
303        let client = self
304            .pool
305            .get()
306            .await
307            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
308        for sql in dialect.schema_setup_sqls() {
309            client.execute(*sql, &[]).await?;
310        }
311        self.ensure_id_space_table(DEFAULT_ID_SPACE_TABLE).await?;
312
313        for entity in entities {
314            if !self.table_exists(&entity.table_name).await? {
315                let sql = dialect.compile_create_table(entity)?;
316                client.execute(&sql, &[]).await?;
317                continue;
318            }
319
320            let existing_columns = self.table_columns(&entity.table_name).await?;
321            for property in &entity.properties {
322                let bare_column = strip_identifier_quotes(&property.column_name).to_lowercase();
323                if existing_columns.contains(&bare_column) {
324                    continue;
325                }
326                let sql = dialect.compile_add_column(entity, property)?;
327                client.execute(&sql, &[]).await?;
328            }
329
330            for sql in dialect.schema_indexes_sqls(entity)? {
331                client.execute(&sql, &[]).await?;
332            }
333        }
334        Ok(())
335    }
336
337    pub async fn ensure_id_space_table(
338        &self,
339        table_name: &str,
340    ) -> Result<(), MutationExecutorError> {
341        let sql = format!(
342            "CREATE TABLE IF NOT EXISTS {} (type_name VARCHAR(100) PRIMARY KEY, current_level BIGINT NOT NULL)",
343            quote_ident(table_name)
344        );
345        let client = self
346            .pool
347            .get()
348            .await
349            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
350        client.execute(&sql, &[]).await?;
351        Ok(())
352    }
353
354    pub async fn execute(&self, query: &CompiledQuery) -> Result<u64, MutationExecutorError> {
355        let mut args = PgArgs { values: Vec::new() };
356        for value in &query.params {
357            bind_pg(&mut args, value)?;
358        }
359        let client = self
360            .pool
361            .get()
362            .await
363            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
364        let result = client.execute(&query.sql, &args.as_refs()).await?;
365        Ok(result)
366    }
367
368    pub async fn fetch_all(
369        &self,
370        query: &CompiledQuery,
371    ) -> Result<Vec<Record>, MutationExecutorError> {
372        let mut args = PgArgs { values: Vec::new() };
373        for value in &query.params {
374            bind_pg(&mut args, value)?;
375        }
376        let client = self
377            .pool
378            .get()
379            .await
380            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
381        let rows = client.query(&query.sql, &args.as_refs()).await?;
382        rows.iter().map(decode_pg_row).collect()
383    }
384
385    async fn table_exists(&self, table_name: &str) -> Result<bool, MutationExecutorError> {
386        let client = self
387            .pool
388            .get()
389            .await
390            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
391        let row = client
392            .query_one(
393                "SELECT COUNT(1)
394             FROM information_schema.tables
395             WHERE table_schema = current_schema()
396               AND table_name = $1",
397                &[&table_name],
398            )
399            .await?;
400        let exists: i64 = row.try_get(0)?;
401        Ok(exists > 0)
402    }
403
404    async fn table_columns(
405        &self,
406        table_name: &str,
407    ) -> Result<std::collections::BTreeSet<String>, MutationExecutorError> {
408        let client = self
409            .pool
410            .get()
411            .await
412            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
413        let rows = client
414            .query(
415                "SELECT column_name
416             FROM information_schema.columns
417             WHERE table_schema = current_schema()
418               AND table_name = $1",
419                &[&table_name],
420            )
421            .await?;
422        let mut columns = std::collections::BTreeSet::new();
423        for row in rows {
424            let name: String = row.try_get("column_name")?;
425            columns.insert(name.to_lowercase());
426        }
427        Ok(columns)
428    }
429}
430
431async fn ensure_initial_graphs_postgres(
432    executor: &PgMutationExecutor,
433    dialect: &PostgresDialect,
434    ctx: &UserContext,
435) -> Result<(), MutationExecutorError> {
436    for graph in ctx.initial_graphs() {
437        let entity = ctx.entity(&graph.entity).ok_or_else(|| {
438            MutationExecutorError::Bind(format!("missing entity: {}", graph.entity))
439        })?;
440        if initial_graph_exists_postgres(executor, dialect, entity, graph).await? {
441            if let Some(query) = compile_initial_graph_update(dialect, entity, graph)? {
442                executor.execute(&query).await?;
443            }
444            continue;
445        }
446        let query = compile_initial_graph_insert(dialect, entity, graph)?;
447        executor.execute(&query).await?;
448    }
449    Ok(())
450}
451
452async fn initial_graph_exists_postgres(
453    executor: &PgMutationExecutor,
454    dialect: &PostgresDialect,
455    entity: &EntityDescriptor,
456    graph: &GraphNode,
457) -> Result<bool, MutationExecutorError> {
458    let Some(id) = graph.values.get("id") else {
459        return Ok(false);
460    };
461    let query = dialect.compile_select(
462        entity,
463        &SelectQuery::new(&graph.entity)
464            .project("id")
465            .filter(Expr::eq("id", id.clone()))
466            .limit(1),
467    )?;
468    Ok(!executor.fetch_all(&query).await?.is_empty())
469}
470
471fn compile_initial_graph_insert(
472    dialect: &impl SqlDialect,
473    entity: &EntityDescriptor,
474    graph: &GraphNode,
475) -> Result<CompiledQuery, MutationExecutorError> {
476    let mut command = InsertCommand::new(&graph.entity);
477    for (field, value) in &graph.values {
478        command = command.value(field.clone(), value.clone());
479    }
480    dialect.compile_insert(entity, &command).map_err(Into::into)
481}
482
483fn compile_initial_graph_update(
484    dialect: &impl SqlDialect,
485    entity: &EntityDescriptor,
486    graph: &crate::GraphNode,
487) -> Result<Option<CompiledQuery>, MutationExecutorError> {
488    let Some(id) = graph.values.get("id") else {
489        return Ok(None);
490    };
491    let mut command = UpdateCommand::new(&graph.entity, id.clone());
492    for (field, value) in &graph.values {
493        if field == "id" {
494            continue;
495        }
496        command = command.value(field.clone(), value.clone());
497    }
498    match dialect.compile_update(entity, &command) {
499        Ok(query) => Ok(Some(query)),
500        Err(SqlCompileError::EmptyMutation(_)) => Ok(None),
501        Err(err) => Err(err.into()),
502    }
503}
504
505pub trait PostgresSchemaExt {
506    fn ensure_postgres_schema(
507        &self,
508    ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + '_>>;
509}
510
511pub async fn ensure_postgres_schema_for(ctx: &UserContext) -> Result<(), MutationExecutorError> {
512    let dialect = ctx.get_resource::<PostgresDialect>().ok_or_else(|| {
513        MutationExecutorError::Bind("missing typed resource: PostgresDialect".to_owned())
514    })?;
515    let executor = ctx.get_resource::<PgMutationExecutor>().ok_or_else(|| {
516        MutationExecutorError::Bind("missing typed resource: PgMutationExecutor".to_owned())
517    })?;
518
519    let entities = ctx.all_entities();
520
521    executor.ensure_schema(dialect, &entities).await?;
522    ensure_initial_graphs_postgres(executor, dialect, ctx).await
523}
524
525#[cfg(test)]
526mod streaming_tests {
527    use super::*;
528    use futures_util::StreamExt;
529    use teaql_sql::StreamingSqlTransport;
530
531    #[tokio::test]
532    async fn streams_from_real_postgres_when_configured() {
533        let Ok(url) = std::env::var("TEAQL_TEST_POSTGRES_URL") else {
534            return;
535        };
536        let mut config = deadpool_postgres::Config::new();
537        config.url = Some(url);
538        let pool = config
539            .create_pool(
540                Some(deadpool_postgres::Runtime::Tokio1),
541                tokio_postgres::NoTls,
542            )
543            .unwrap();
544        let executor = PgMutationExecutor::new(pool);
545        let query = CompiledQuery {
546            sql: "SELECT id FROM (VALUES (1), (2), (3), (4), (5)) AS fixture(id) ORDER BY id"
547                .to_owned(),
548            params: vec![],
549            comment: None,
550        };
551        let mut stream = executor.stream_sql(query, 2);
552        let mut sizes = Vec::new();
553        while let Some(chunk) = stream.next().await {
554            sizes.push(chunk.unwrap().rows.len());
555        }
556        assert_eq!(sizes, vec![2, 2, 1]);
557    }
558}
559
560impl PostgresSchemaExt for UserContext {
561    fn ensure_postgres_schema(
562        &self,
563    ) -> Pin<Box<dyn Future<Output = Result<(), MutationExecutorError>> + '_>> {
564        Box::pin(ensure_postgres_schema_for(self))
565    }
566}
567
568#[derive(Debug, Default, Clone, Copy)]
569pub struct PostgresSchemaProvider;
570
571impl SchemaProvider for PostgresSchemaProvider {
572    fn ensure_schema<'a>(
573        &'a self,
574        ctx: &'a UserContext,
575    ) -> Pin<Box<dyn Future<Output = Result<(), RuntimeError>> + Send + 'a>> {
576        Box::pin(async move {
577            ensure_postgres_schema_for(ctx)
578                .await
579                .map_err(|err| RuntimeError::Schema(err.to_string()))
580        })
581    }
582}
583
584pub trait PostgresProviderExt {
585    fn use_postgres_provider(&mut self, executor: PgMutationExecutor) -> &mut Self;
586}
587
588impl PostgresProviderExt for UserContext {
589    fn use_postgres_provider(&mut self, executor: PgMutationExecutor) -> &mut Self {
590        self.insert_resource(PostgresDialect);
591        self.insert_resource(executor);
592        self.set_schema_provider(PostgresSchemaProvider);
593        self
594    }
595}
596
597#[derive(Clone)]
598pub struct PgIdSpaceGenerator {
599    pool: Pool,
600    table_name: String,
601}
602
603impl PgIdSpaceGenerator {
604    pub fn new(pool: Pool) -> Self {
605        Self {
606            pool,
607            table_name: DEFAULT_ID_SPACE_TABLE.to_owned(),
608        }
609    }
610
611    pub fn from_executor(executor: PgMutationExecutor) -> Self {
612        Self::new(executor.pool())
613    }
614
615    pub fn with_table_name(mut self, table_name: impl Into<String>) -> Self {
616        self.table_name = table_name.into();
617        self
618    }
619
620    pub async fn ensure_table(&self) -> Result<(), MutationExecutorError> {
621        PgMutationExecutor::new(self.pool.clone())
622            .ensure_id_space_table(&self.table_name)
623            .await
624    }
625
626    pub async fn next_id(&self, entity: &str) -> Result<u64, MutationExecutorError> {
627        self.ensure_table().await?;
628        let update_sql = format!(
629            "UPDATE {} SET current_level = current_level + 1 WHERE type_name = $1 RETURNING current_level",
630            quote_ident(&self.table_name)
631        );
632        let client = self
633            .pool
634            .get()
635            .await
636            .map_err(|e| MutationExecutorError::Pool(e.to_string()))?;
637        let row = client.query_opt(&update_sql, &[&entity]).await?;
638
639        let id = match row {
640            Some(r) => {
641                let level: i64 = r.try_get(0)?;
642                level
643            }
644            None => {
645                let insert_sql = format!(
646                    "INSERT INTO {} (type_name, current_level) VALUES ($1, 1) RETURNING current_level",
647                    quote_ident(&self.table_name)
648                );
649                let insert_res = client.query_one(&insert_sql, &[&entity]).await;
650                match insert_res {
651                    Ok(r) => {
652                        let level: i64 = r.try_get(0)?;
653                        level
654                    }
655                    Err(_) => {
656                        let row = client.query_one(&update_sql, &[&entity]).await?;
657                        let level: i64 = row.try_get(0)?;
658                        level
659                    }
660                }
661            }
662        };
663
664        u64::try_from(id).map_err(|_| {
665            MutationExecutorError::Bind(format!("generated id {id} cannot be represented as u64"))
666        })
667    }
668}
669
670impl InternalIdGenerator for PgIdSpaceGenerator {
671    fn generate_id(&self, entity: &str) -> Result<u64, RuntimeError> {
672        let generator = self.clone();
673        let entity = entity.to_owned();
674        block_on_id_generation(async move { generator.next_id(&entity).await })
675    }
676}
677
678fn block_on_id_generation<F>(future: F) -> Result<u64, RuntimeError>
679where
680    F: Future<Output = Result<u64, MutationExecutorError>> + Send + 'static,
681{
682    let result = match tokio::runtime::Handle::try_current() {
683        Ok(handle) => tokio::task::block_in_place(|| handle.block_on(future)),
684        Err(_) => tokio::runtime::Builder::new_current_thread()
685            .enable_all()
686            .build()
687            .map_err(|err| RuntimeError::IdGeneration(err.to_string()))?
688            .block_on(future),
689    };
690    result.map_err(|err| RuntimeError::IdGeneration(err.to_string()))
691}
692
693fn quote_ident(ident: &str) -> String {
694    quote_identifier_if_needed(ident, '"')
695}
696
697/// Strip wrapping identifier quotes from a SQL identifier so that bare column
698/// names returned by `information_schema.columns` can be compared with
699/// potentially-quoted `PropertyDescriptor::column_name` values.
700fn strip_identifier_quotes(ident: &str) -> &str {
701    let bytes = ident.as_bytes();
702    if bytes.len() >= 2 {
703        let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
704        if (first == b'"' && last == b'"')
705            || (first == b'`' && last == b'`')
706            || (first == b'[' && last == b']')
707        {
708            return &ident[1..ident.len() - 1];
709        }
710    }
711    ident
712}
713
714fn try_parse_datetime_from_str(s: &str) -> Option<chrono::DateTime<chrono::Utc>> {
715    if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
716        return Some(dt.with_timezone(&chrono::Utc));
717    }
718    if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
719        return Some(chrono::DateTime::from_naive_utc_and_offset(
720            ndt,
721            chrono::Utc,
722        ));
723    }
724    if let Ok(nd) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
725        let ndt = nd.and_hms_opt(0, 0, 0)?;
726        return Some(chrono::DateTime::from_naive_utc_and_offset(
727            ndt,
728            chrono::Utc,
729        ));
730    }
731    None
732}
733
734#[derive(Debug, Clone, Copy)]
735struct PgNull;
736
737impl tokio_postgres::types::ToSql for PgNull {
738    fn to_sql(
739        &self,
740        ty: &tokio_postgres::types::Type,
741        out: &mut bytes::BytesMut,
742    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
743        Ok(tokio_postgres::types::IsNull::Yes)
744    }
745
746    fn accepts(ty: &tokio_postgres::types::Type) -> bool {
747        true
748    }
749
750    fn to_sql_checked(
751        &self,
752        ty: &tokio_postgres::types::Type,
753        out: &mut bytes::BytesMut,
754    ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
755        Ok(tokio_postgres::types::IsNull::Yes)
756    }
757}
758
759struct PgArgs {
760    values: Vec<Box<dyn tokio_postgres::types::ToSql + Sync + Send>>,
761}
762impl PgArgs {
763    fn add<T: tokio_postgres::types::ToSql + Sync + Send + 'static>(&mut self, v: T) {
764        self.values.push(Box::new(v));
765    }
766    fn as_refs(&self) -> Vec<&(dyn tokio_postgres::types::ToSql + Sync)> {
767        self.values.iter().map(|b| b.as_ref() as _).collect()
768    }
769}
770
771fn bind_pg(args: &mut PgArgs, value: &Value) -> Result<(), MutationExecutorError> {
772    match value {
773        Value::Null => {
774            args.add(PgNull);
775        }
776        Value::Bool(v) => args.add(*v),
777        Value::I64(v) => args.add(*v),
778        Value::U64(v) => {
779            let v = i64::try_from(*v).map_err(|_| {
780                MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))
781            })?;
782            args.add(v);
783        }
784        Value::F64(v) => args.add(*v),
785        Value::Decimal(v) => args.add(*v),
786        Value::Text(v) => match try_parse_datetime_from_str(v) {
787            Some(dt) => args.add(dt),
788            None => args.add(v.clone()),
789        },
790        Value::Json(v) => {
791            let j_val: serde_json::Value =
792                serde_json::to_value(v).map_err(|e| MutationExecutorError::Bind(e.to_string()))?;
793            args.add(j_val);
794        }
795        Value::Date(v) => args.add(*v),
796        Value::Timestamp(v) => args.add(v.to_datetime()),
797        Value::Object(_) => return Err(MutationExecutorError::UnsupportedValue("object")),
798        Value::List(values) => bind_pg_list(args, values)?,
799        Value::TypedNull(dt) => match dt {
800            DataType::Bool => args.add(Option::<bool>::None),
801            DataType::I64 | DataType::U64 => args.add(Option::<i64>::None),
802            DataType::F64 => args.add(Option::<f64>::None),
803            DataType::Decimal => args.add(Option::<Decimal>::None),
804            DataType::Text | DataType::LargeText => args.add(Option::<String>::None),
805            DataType::Json => args.add(Option::<serde_json::Value>::None),
806            DataType::Date => args.add(Option::<NaiveDate>::None),
807            DataType::Timestamp => args.add(Option::<DateTime<Utc>>::None),
808        },
809    }
810    Ok(())
811}
812
813fn bind_pg_list(args: &mut PgArgs, values: &[Value]) -> Result<(), MutationExecutorError> {
814    let Some(first) = values.first() else {
815        return Err(MutationExecutorError::UnsupportedValue("empty list"));
816    };
817    match first {
818        Value::Bool(_) => {
819            let values = values
820                .iter()
821                .map(|value| match value {
822                    Value::Bool(value) => Ok(*value),
823                    _ => Err(MutationExecutorError::UnsupportedValue("mixed bool list")),
824                })
825                .collect::<Result<Vec<_>, _>>()?;
826            args.add(values);
827        }
828        Value::I64(_) => {
829            let values = values
830                .iter()
831                .map(|value| match value {
832                    Value::I64(value) => Ok(*value),
833                    _ => Err(MutationExecutorError::UnsupportedValue("mixed i64 list")),
834                })
835                .collect::<Result<Vec<_>, _>>()?;
836            args.add(values);
837        }
838        Value::U64(_) => {
839            let values = values
840                .iter()
841                .map(|value| match value {
842                    Value::U64(value) => i64::try_from(*value).map_err(|_| {
843                        MutationExecutorError::Bind(format!("u64 value {value} exceeds i64 range"))
844                    }),
845                    _ => Err(MutationExecutorError::UnsupportedValue("mixed u64 list")),
846                })
847                .collect::<Result<Vec<_>, _>>()?;
848            args.add(values);
849        }
850        Value::F64(_) => {
851            let values = values
852                .iter()
853                .map(|value| match value {
854                    Value::F64(value) => Ok(*value),
855                    _ => Err(MutationExecutorError::UnsupportedValue("mixed f64 list")),
856                })
857                .collect::<Result<Vec<_>, _>>()?;
858            args.add(values);
859        }
860        Value::Decimal(_) => {
861            let values = values
862                .iter()
863                .map(|value| match value {
864                    Value::Decimal(value) => Ok(*value),
865                    _ => Err(MutationExecutorError::UnsupportedValue(
866                        "mixed decimal list",
867                    )),
868                })
869                .collect::<Result<Vec<_>, _>>()?;
870            args.add(values);
871        }
872        Value::Text(_) => {
873            let values = values
874                .iter()
875                .map(|value| match value {
876                    Value::Text(value) => Ok(value.clone()),
877                    _ => Err(MutationExecutorError::UnsupportedValue("mixed text list")),
878                })
879                .collect::<Result<Vec<_>, _>>()?;
880            args.add(values);
881        }
882        Value::Date(_) => {
883            let values = values
884                .iter()
885                .map(|value| match value {
886                    Value::Date(value) => Ok(*value),
887                    _ => Err(MutationExecutorError::UnsupportedValue("mixed date list")),
888                })
889                .collect::<Result<Vec<_>, _>>()?;
890            args.add(values);
891        }
892        Value::Timestamp(_) => {
893            let values = values
894                .iter()
895                .map(|value| match value {
896                    Value::Timestamp(value) => Ok(value.to_datetime()),
897                    _ => Err(MutationExecutorError::UnsupportedValue(
898                        "mixed timestamp list",
899                    )),
900                })
901                .collect::<Result<Vec<_>, _>>()?;
902            args.add(values);
903        }
904        Value::Null => return Err(MutationExecutorError::UnsupportedValue("null list")),
905        Value::Json(_) => return Err(MutationExecutorError::UnsupportedValue("json list")),
906        Value::Object(_) => return Err(MutationExecutorError::UnsupportedValue("object list")),
907        Value::List(_) => return Err(MutationExecutorError::UnsupportedValue("nested list")),
908        Value::TypedNull(_) => return Err(MutationExecutorError::UnsupportedValue("null list")),
909    }
910    Ok(())
911}
912
913fn decode_pg_row(row: &tokio_postgres::Row) -> Result<Record, MutationExecutorError> {
914    let mut record = BTreeMap::new();
915    for (index, column) in row.columns().iter().enumerate() {
916        let name = column.name().to_owned();
917        let type_name = column.type_().name().to_ascii_uppercase();
918
919        let value = match type_name.as_str() {
920            "BOOL" | "BOOLEAN" => {
921                let v: Option<bool> = row.try_get(index)?;
922                match v {
923                    Some(v) => Value::Bool(v),
924                    None => Value::Null,
925                }
926            }
927            "INT2" => {
928                let v: Option<i16> = row.try_get(index)?;
929                match v {
930                    Some(v) => Value::I64(v as i64),
931                    None => Value::Null,
932                }
933            }
934            "INT4" => {
935                let v: Option<i32> = row.try_get(index)?;
936                match v {
937                    Some(v) => Value::I64(v as i64),
938                    None => Value::Null,
939                }
940            }
941            "INT8" => {
942                let v: Option<i64> = row.try_get(index)?;
943                match v {
944                    Some(v) => Value::I64(v),
945                    None => Value::Null,
946                }
947            }
948            "FLOAT4" => {
949                let v: Option<f32> = row.try_get(index)?;
950                match v {
951                    Some(v) => Value::F64(v as f64),
952                    None => Value::Null,
953                }
954            }
955            "FLOAT8" => {
956                let v: Option<f64> = row.try_get(index)?;
957                match v {
958                    Some(v) => Value::F64(v),
959                    None => Value::Null,
960                }
961            }
962            "NUMERIC" => {
963                let v: Option<Decimal> = row.try_get(index)?;
964                match v {
965                    Some(v) => Value::Decimal(v),
966                    None => Value::Null,
967                }
968            }
969            "JSON" | "JSONB" => {
970                let v: Option<serde_json::Value> = row.try_get(index)?;
971                match v {
972                    Some(j) => Value::Json(j.into()),
973                    None => Value::Null,
974                }
975            }
976            "DATE" => {
977                let v: Option<NaiveDate> = row.try_get(index)?;
978                match v {
979                    Some(v) => Value::Date(v),
980                    None => Value::Null,
981                }
982            }
983            "TIMESTAMP" | "TIMESTAMPTZ" => {
984                let v: Option<DateTime<Utc>> = row.try_get(index)?;
985                match v {
986                    Some(v) => Value::Timestamp(teaql_core::time::Timestamp(v.timestamp_millis())),
987                    None => Value::Null,
988                }
989            }
990            "TEXT" | "VARCHAR" | "BPCHAR" | "NAME" | "UUID" => {
991                let v: Option<String> = row.try_get(index)?;
992                match v {
993                    Some(v) => Value::Text(v),
994                    None => Value::Null,
995                }
996            }
997            other => {
998                return Err(MutationExecutorError::UnsupportedColumnType(
999                    other.to_owned(),
1000                ));
1001            }
1002        };
1003        record.insert(name, value);
1004    }
1005    Ok(record)
1006}
1007
1008#[cfg(test)]
1009mod tests {
1010    use super::*;
1011    use teaql_core::{DeleteCommand, RecoverCommand};
1012
1013    fn entity() -> EntityDescriptor {
1014        EntityDescriptor::new("Order")
1015            .table_name("orders")
1016            .property(
1017                PropertyDescriptor::new("id", DataType::U64)
1018                    .column_name("id")
1019                    .id()
1020                    .not_null(),
1021            )
1022            .property(
1023                PropertyDescriptor::new("version", DataType::I64)
1024                    .column_name("version")
1025                    .version()
1026                    .not_null(),
1027            )
1028            .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1029    }
1030
1031    #[test]
1032    fn postgres_dialect_compiles_mutations_with_numbered_placeholders() {
1033        let insert = PostgresDialect
1034            .compile_insert(
1035                &entity(),
1036                &InsertCommand::new("Order")
1037                    .value("id", 1_u64)
1038                    .value("name", "A"),
1039            )
1040            .unwrap();
1041        assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES ($1, $2)");
1042
1043        let update = PostgresDialect
1044            .compile_update(
1045                &entity(),
1046                &UpdateCommand::new("Order", 1_u64)
1047                    .expected_version(3)
1048                    .value("name", "B"),
1049            )
1050            .unwrap();
1051        assert_eq!(
1052            update.sql,
1053            "UPDATE orders SET name = $1, version = $2 WHERE id = $3 AND version = $4"
1054        );
1055
1056        let delete = PostgresDialect
1057            .compile_delete(
1058                &entity(),
1059                &DeleteCommand::new("Order", 1_u64).expected_version(3),
1060            )
1061            .unwrap();
1062        let recover = PostgresDialect
1063            .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1064            .unwrap();
1065        assert_eq!(
1066            delete.sql,
1067            "UPDATE orders SET version = $1 WHERE id = $2 AND version = $3"
1068        );
1069        assert_eq!(
1070            recover.sql,
1071            "UPDATE orders SET version = $1 WHERE id = $2 AND version = $3"
1072        );
1073    }
1074
1075    #[test]
1076    fn postgres_dialect_compiles_schema_and_large_in_array_binds() {
1077        let create = PostgresDialect.compile_create_table(&entity()).unwrap();
1078        assert_eq!(
1079            create,
1080            "CREATE TABLE IF NOT EXISTS orders (id BIGINT PRIMARY KEY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255))"
1081        );
1082        assert!(
1083            PostgresDialect
1084                .schema_setup_sqls()
1085                .iter()
1086                .any(|sql| sql.contains("CREATE OR REPLACE FUNCTION soundex"))
1087        );
1088
1089        let query = PostgresDialect
1090            .compile_select(
1091                &entity(),
1092                &SelectQuery::new("Order")
1093                    .filter(Expr::in_large(
1094                        "id",
1095                        vec![Value::from(1_u64), Value::from(2_u64)],
1096                    ))
1097                    .order_asc("id"),
1098            )
1099            .unwrap();
1100        assert_eq!(
1101            query.sql,
1102            "SELECT id, version, name FROM orders WHERE (id = ANY($1)) ORDER BY id ASC"
1103        );
1104        assert_eq!(
1105            query.params,
1106            vec![Value::List(vec![Value::U64(1), Value::U64(2)])]
1107        );
1108    }
1109}