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