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