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 fn ensure_floor(&self, entity: &str, floor: u64) -> Result<(), RuntimeError> {
1311 let generator = self.clone();
1312 let entity = entity.to_owned();
1313 block_on_id_generation(async move { generator.ensure_floor(&entity, floor).await })
1314 }
1315}
1316
1317fn block_on_id_generation<T, F>(future: F) -> Result<T, RuntimeError>
1318where
1319 T: Send + 'static,
1320 F: Future<Output = Result<T, MutationExecutorError>> + Send + 'static,
1321{
1322 let result = match tokio::runtime::Handle::try_current() {
1323 Ok(handle) => tokio::task::block_in_place(|| handle.block_on(future)),
1324 Err(_) => tokio::runtime::Builder::new_current_thread()
1325 .enable_all()
1326 .build()
1327 .map_err(|err| RuntimeError::IdGeneration(err.to_string()))?
1328 .block_on(future),
1329 };
1330 result.map_err(|err| RuntimeError::IdGeneration(err.to_string()))
1331}
1332
1333fn quote_ident(ident: &str) -> String {
1334 quote_identifier_if_needed(ident, '"')
1335}
1336
1337fn strip_identifier_quotes(ident: &str) -> &str {
1341 let bytes = ident.as_bytes();
1342 if bytes.len() >= 2 {
1343 let (first, last) = (bytes[0], bytes[bytes.len() - 1]);
1344 if (first == b'"' && last == b'"')
1345 || (first == b'`' && last == b'`')
1346 || (first == b'[' && last == b']')
1347 {
1348 return &ident[1..ident.len() - 1];
1349 }
1350 }
1351 ident
1352}
1353
1354fn try_parse_datetime_from_str(s: &str) -> Option<chrono::DateTime<chrono::Utc>> {
1355 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
1356 return Some(dt.with_timezone(&chrono::Utc));
1357 }
1358 if let Ok(ndt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
1359 return Some(chrono::DateTime::from_naive_utc_and_offset(
1360 ndt,
1361 chrono::Utc,
1362 ));
1363 }
1364 if let Ok(nd) = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d") {
1365 let ndt = nd.and_hms_opt(0, 0, 0)?;
1366 return Some(chrono::DateTime::from_naive_utc_and_offset(
1367 ndt,
1368 chrono::Utc,
1369 ));
1370 }
1371 None
1372}
1373
1374#[derive(Debug, Clone, Copy)]
1375struct PgNull;
1376
1377impl tokio_postgres::types::ToSql for PgNull {
1378 fn to_sql(
1379 &self,
1380 ty: &tokio_postgres::types::Type,
1381 out: &mut bytes::BytesMut,
1382 ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1383 Ok(tokio_postgres::types::IsNull::Yes)
1384 }
1385
1386 fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1387 true
1388 }
1389
1390 fn to_sql_checked(
1391 &self,
1392 ty: &tokio_postgres::types::Type,
1393 out: &mut bytes::BytesMut,
1394 ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1395 Ok(tokio_postgres::types::IsNull::Yes)
1396 }
1397}
1398
1399#[derive(Debug, Clone, Copy)]
1400struct PgTimestamp(DateTime<Utc>);
1401
1402impl tokio_postgres::types::ToSql for PgTimestamp {
1403 fn to_sql(
1404 &self,
1405 ty: &tokio_postgres::types::Type,
1406 out: &mut bytes::BytesMut,
1407 ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1408 if *ty == tokio_postgres::types::Type::TIMESTAMP {
1409 self.0.naive_utc().to_sql(ty, out)
1410 } else {
1411 self.0.to_sql(ty, out)
1412 }
1413 }
1414
1415 fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1416 *ty == tokio_postgres::types::Type::TIMESTAMP
1417 || *ty == tokio_postgres::types::Type::TIMESTAMPTZ
1418 }
1419
1420 tokio_postgres::types::to_sql_checked!();
1421}
1422
1423#[derive(Debug, Clone, Copy)]
1424struct PgInteger(i64);
1425
1426impl tokio_postgres::types::ToSql for PgInteger {
1427 fn to_sql(
1428 &self,
1429 ty: &tokio_postgres::types::Type,
1430 out: &mut bytes::BytesMut,
1431 ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1432 match *ty {
1433 tokio_postgres::types::Type::INT2 => i16::try_from(self.0)?.to_sql(ty, out),
1434 tokio_postgres::types::Type::INT4 => i32::try_from(self.0)?.to_sql(ty, out),
1435 tokio_postgres::types::Type::INT8 => self.0.to_sql(ty, out),
1436 _ => Err(format!("integer cannot be encoded as PostgreSQL type {ty}").into()),
1437 }
1438 }
1439
1440 fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1441 matches!(
1442 *ty,
1443 tokio_postgres::types::Type::INT2
1444 | tokio_postgres::types::Type::INT4
1445 | tokio_postgres::types::Type::INT8
1446 )
1447 }
1448
1449 tokio_postgres::types::to_sql_checked!();
1450}
1451
1452#[derive(Debug, Clone)]
1453struct PgIntegerList(Vec<i64>);
1454
1455impl tokio_postgres::types::ToSql for PgIntegerList {
1456 fn to_sql(
1457 &self,
1458 ty: &tokio_postgres::types::Type,
1459 out: &mut bytes::BytesMut,
1460 ) -> Result<tokio_postgres::types::IsNull, Box<dyn std::error::Error + Sync + Send>> {
1461 match *ty {
1462 tokio_postgres::types::Type::INT2_ARRAY => self
1463 .0
1464 .iter()
1465 .copied()
1466 .map(i16::try_from)
1467 .collect::<Result<Vec<_>, _>>()?
1468 .to_sql(ty, out),
1469 tokio_postgres::types::Type::INT4_ARRAY => self
1470 .0
1471 .iter()
1472 .copied()
1473 .map(i32::try_from)
1474 .collect::<Result<Vec<_>, _>>()?
1475 .to_sql(ty, out),
1476 tokio_postgres::types::Type::INT8_ARRAY => self.0.to_sql(ty, out),
1477 _ => Err(format!("integer list cannot be encoded as PostgreSQL type {ty}").into()),
1478 }
1479 }
1480
1481 fn accepts(ty: &tokio_postgres::types::Type) -> bool {
1482 matches!(
1483 *ty,
1484 tokio_postgres::types::Type::INT2_ARRAY
1485 | tokio_postgres::types::Type::INT4_ARRAY
1486 | tokio_postgres::types::Type::INT8_ARRAY
1487 )
1488 }
1489
1490 tokio_postgres::types::to_sql_checked!();
1491}
1492
1493struct PgArgs {
1494 values: Vec<Box<dyn tokio_postgres::types::ToSql + Sync + Send>>,
1495}
1496impl PgArgs {
1497 fn add<T: tokio_postgres::types::ToSql + Sync + Send + 'static>(&mut self, v: T) {
1498 self.values.push(Box::new(v));
1499 }
1500 fn as_refs(&self) -> Vec<&(dyn tokio_postgres::types::ToSql + Sync)> {
1501 self.values.iter().map(|b| b.as_ref() as _).collect()
1502 }
1503}
1504
1505fn bind_pg(args: &mut PgArgs, value: &Value) -> Result<(), MutationExecutorError> {
1506 match value {
1507 Value::Null => {
1508 args.add(PgNull);
1509 }
1510 Value::Bool(v) => args.add(*v),
1511 Value::I64(v) => args.add(PgInteger(*v)),
1512 Value::U64(v) => {
1513 let v = i64::try_from(*v).map_err(|_| {
1514 MutationExecutorError::Bind(format!("u64 value {v} exceeds i64 range"))
1515 })?;
1516 args.add(PgInteger(v));
1517 }
1518 Value::F64(v) => args.add(*v),
1519 Value::Decimal(v) => args.add(*v),
1520 Value::Text(v) => match try_parse_datetime_from_str(v) {
1521 Some(dt) => args.add(dt),
1522 None => args.add(v.clone()),
1523 },
1524 Value::Json(v) => {
1525 let j_val: serde_json::Value =
1526 serde_json::to_value(v).map_err(|e| MutationExecutorError::Bind(e.to_string()))?;
1527 args.add(j_val);
1528 }
1529 Value::Date(v) => args.add(*v),
1530 Value::Timestamp(v) => args.add(PgTimestamp(v.to_datetime())),
1531 Value::Object(_) => return Err(MutationExecutorError::UnsupportedValue("object")),
1532 Value::List(values) => bind_pg_list(args, values)?,
1533 Value::TypedNull(dt) => match dt {
1534 DataType::Bool => args.add(Option::<bool>::None),
1535 DataType::I64 | DataType::U64 => args.add(Option::<i64>::None),
1536 DataType::F64 => args.add(Option::<f64>::None),
1537 DataType::Decimal => args.add(Option::<Decimal>::None),
1538 DataType::Text | DataType::LargeText => args.add(Option::<String>::None),
1539 DataType::Json => args.add(Option::<serde_json::Value>::None),
1540 DataType::Date => args.add(Option::<NaiveDate>::None),
1541 DataType::Timestamp => args.add(PgNull),
1542 },
1543 }
1544 Ok(())
1545}
1546
1547fn bind_pg_list(args: &mut PgArgs, values: &[Value]) -> Result<(), MutationExecutorError> {
1548 let Some(first) = values.first() else {
1549 return Err(MutationExecutorError::UnsupportedValue("empty list"));
1550 };
1551 match first {
1552 Value::Bool(_) => {
1553 let values = values
1554 .iter()
1555 .map(|value| match value {
1556 Value::Bool(value) => Ok(*value),
1557 _ => Err(MutationExecutorError::UnsupportedValue("mixed bool list")),
1558 })
1559 .collect::<Result<Vec<_>, _>>()?;
1560 args.add(values);
1561 }
1562 Value::I64(_) => {
1563 let values = values
1564 .iter()
1565 .map(|value| match value {
1566 Value::I64(value) => Ok(*value),
1567 _ => Err(MutationExecutorError::UnsupportedValue("mixed i64 list")),
1568 })
1569 .collect::<Result<Vec<_>, _>>()?;
1570 args.add(PgIntegerList(values));
1571 }
1572 Value::U64(_) => {
1573 let values = values
1574 .iter()
1575 .map(|value| match value {
1576 Value::U64(value) => i64::try_from(*value).map_err(|_| {
1577 MutationExecutorError::Bind(format!("u64 value {value} exceeds i64 range"))
1578 }),
1579 _ => Err(MutationExecutorError::UnsupportedValue("mixed u64 list")),
1580 })
1581 .collect::<Result<Vec<_>, _>>()?;
1582 args.add(PgIntegerList(values));
1583 }
1584 Value::F64(_) => {
1585 let values = values
1586 .iter()
1587 .map(|value| match value {
1588 Value::F64(value) => Ok(*value),
1589 _ => Err(MutationExecutorError::UnsupportedValue("mixed f64 list")),
1590 })
1591 .collect::<Result<Vec<_>, _>>()?;
1592 args.add(values);
1593 }
1594 Value::Decimal(_) => {
1595 let values = values
1596 .iter()
1597 .map(|value| match value {
1598 Value::Decimal(value) => Ok(*value),
1599 _ => Err(MutationExecutorError::UnsupportedValue(
1600 "mixed decimal list",
1601 )),
1602 })
1603 .collect::<Result<Vec<_>, _>>()?;
1604 args.add(values);
1605 }
1606 Value::Text(_) => {
1607 let values = values
1608 .iter()
1609 .map(|value| match value {
1610 Value::Text(value) => Ok(value.clone()),
1611 _ => Err(MutationExecutorError::UnsupportedValue("mixed text list")),
1612 })
1613 .collect::<Result<Vec<_>, _>>()?;
1614 args.add(values);
1615 }
1616 Value::Date(_) => {
1617 let values = values
1618 .iter()
1619 .map(|value| match value {
1620 Value::Date(value) => Ok(*value),
1621 _ => Err(MutationExecutorError::UnsupportedValue("mixed date list")),
1622 })
1623 .collect::<Result<Vec<_>, _>>()?;
1624 args.add(values);
1625 }
1626 Value::Timestamp(_) => {
1627 let values = values
1628 .iter()
1629 .map(|value| match value {
1630 Value::Timestamp(value) => Ok(value.to_datetime()),
1631 _ => Err(MutationExecutorError::UnsupportedValue(
1632 "mixed timestamp list",
1633 )),
1634 })
1635 .collect::<Result<Vec<_>, _>>()?;
1636 args.add(values);
1637 }
1638 Value::Null => return Err(MutationExecutorError::UnsupportedValue("null list")),
1639 Value::Json(_) => return Err(MutationExecutorError::UnsupportedValue("json list")),
1640 Value::Object(_) => return Err(MutationExecutorError::UnsupportedValue("object list")),
1641 Value::List(_) => return Err(MutationExecutorError::UnsupportedValue("nested list")),
1642 Value::TypedNull(_) => return Err(MutationExecutorError::UnsupportedValue("null list")),
1643 }
1644 Ok(())
1645}
1646
1647fn decode_pg_values(row: &tokio_postgres::Row) -> Result<Vec<Value>, MutationExecutorError> {
1648 let mut values = Vec::with_capacity(row.len());
1649 for (index, column) in row.columns().iter().enumerate() {
1650 let type_name = column.type_().name();
1651
1652 let value = match type_name {
1653 "bool" | "boolean" => {
1654 let v: Option<bool> = row.try_get(index)?;
1655 match v {
1656 Some(v) => Value::Bool(v),
1657 None => Value::Null,
1658 }
1659 }
1660 "int2" => {
1661 let v: Option<i16> = row.try_get(index)?;
1662 match v {
1663 Some(v) => Value::I64(v as i64),
1664 None => Value::Null,
1665 }
1666 }
1667 "int4" => {
1668 let v: Option<i32> = row.try_get(index)?;
1669 match v {
1670 Some(v) => Value::I64(v as i64),
1671 None => Value::Null,
1672 }
1673 }
1674 "int8" => {
1675 let v: Option<i64> = row.try_get(index)?;
1676 match v {
1677 Some(v) => Value::I64(v),
1678 None => Value::Null,
1679 }
1680 }
1681 "float4" => {
1682 let v: Option<f32> = row.try_get(index)?;
1683 match v {
1684 Some(v) => Value::F64(v as f64),
1685 None => Value::Null,
1686 }
1687 }
1688 "float8" => {
1689 let v: Option<f64> = row.try_get(index)?;
1690 match v {
1691 Some(v) => Value::F64(v),
1692 None => Value::Null,
1693 }
1694 }
1695 "numeric" => {
1696 let v: Option<Decimal> = row.try_get(index)?;
1697 match v {
1698 Some(v) => Value::Decimal(v),
1699 None => Value::Null,
1700 }
1701 }
1702 "json" | "jsonb" => {
1703 let v: Option<serde_json::Value> = row.try_get(index)?;
1704 match v {
1705 Some(j) => Value::Json(j.into()),
1706 None => Value::Null,
1707 }
1708 }
1709 "date" => {
1710 let v: Option<NaiveDate> = row.try_get(index)?;
1711 match v {
1712 Some(v) => Value::Date(v),
1713 None => Value::Null,
1714 }
1715 }
1716 "timestamp" => {
1717 let v: Option<NaiveDateTime> = row.try_get(index)?;
1718 match v {
1719 Some(v) => Value::Timestamp(teaql_core::time::Timestamp(
1720 v.and_utc().timestamp_millis(),
1721 )),
1722 None => Value::Null,
1723 }
1724 }
1725 "timestamptz" => {
1726 let v: Option<DateTime<Utc>> = row.try_get(index)?;
1727 match v {
1728 Some(v) => Value::Timestamp(teaql_core::time::Timestamp(v.timestamp_millis())),
1729 None => Value::Null,
1730 }
1731 }
1732 "text" | "varchar" | "bpchar" | "name" | "uuid" => {
1733 let v: Option<String> = row.try_get(index)?;
1734 match v {
1735 Some(v) => Value::Text(v),
1736 None => Value::Null,
1737 }
1738 }
1739 other => {
1740 return Err(MutationExecutorError::UnsupportedColumnType(
1741 other.to_owned(),
1742 ));
1743 }
1744 };
1745 values.push(value);
1746 }
1747 Ok(values)
1748}
1749
1750#[cfg(test)]
1751mod tests {
1752 use super::*;
1753 use teaql_core::{DeleteCommand, RecoverCommand, RelationDescriptor};
1754
1755 fn entity() -> EntityDescriptor {
1756 EntityDescriptor::new("Order")
1757 .table_name("orders")
1758 .property(
1759 PropertyDescriptor::new("id", DataType::U64)
1760 .column_name("id")
1761 .id()
1762 .not_null(),
1763 )
1764 .property(
1765 PropertyDescriptor::new("version", DataType::I64)
1766 .column_name("version")
1767 .version()
1768 .not_null(),
1769 )
1770 .property(PropertyDescriptor::new("name", DataType::Text).column_name("name"))
1771 }
1772
1773 #[test]
1774 fn postgres_dialect_compiles_mutations_with_numbered_placeholders() {
1775 let insert = PostgresDialect
1776 .compile_insert(
1777 &entity(),
1778 &InsertCommand::new("Order")
1779 .value("id", 1_u64)
1780 .value("name", "A"),
1781 )
1782 .unwrap();
1783 assert_eq!(insert.sql, "INSERT INTO orders (id, name) VALUES ($1, $2)");
1784
1785 let update = PostgresDialect
1786 .compile_update(
1787 &entity(),
1788 &UpdateCommand::new("Order", 1_u64)
1789 .expected_version(3)
1790 .value("name", "B"),
1791 )
1792 .unwrap();
1793 assert_eq!(
1794 update.sql,
1795 "UPDATE orders SET name = $1, version = $2 WHERE id = $3 AND version = $4"
1796 );
1797
1798 let delete = PostgresDialect
1799 .compile_delete(
1800 &entity(),
1801 &DeleteCommand::new("Order", 1_u64).expected_version(3),
1802 )
1803 .unwrap();
1804 let recover = PostgresDialect
1805 .compile_recover(&entity(), &RecoverCommand::new("Order", 1_u64, -4))
1806 .unwrap();
1807 assert_eq!(
1808 delete.sql,
1809 "UPDATE orders SET version = $1 WHERE id = $2 AND version = $3"
1810 );
1811 assert_eq!(
1812 recover.sql,
1813 "UPDATE orders SET version = $1 WHERE id = $2 AND version = $3"
1814 );
1815 }
1816
1817 #[test]
1818 fn postgres_dialect_compiles_schema_and_large_in_array_binds() {
1819 let create = PostgresDialect.compile_create_table(&entity()).unwrap();
1820 assert_eq!(
1821 create,
1822 "CREATE TABLE IF NOT EXISTS orders (id BIGINT PRIMARY KEY NOT NULL, version BIGINT NOT NULL, name VARCHAR(255))"
1823 );
1824 assert!(
1825 PostgresDialect
1826 .schema_setup_sqls()
1827 .iter()
1828 .any(|sql| sql.contains("CREATE OR REPLACE FUNCTION soundex"))
1829 );
1830
1831 let values = (1_u64..=21).map(Value::from).collect::<Vec<_>>();
1832 let query = PostgresDialect
1833 .compile_select(
1834 &entity(),
1835 &SelectQuery::new("Order")
1836 .filter(Expr::in_list("id", values.clone()))
1837 .order_asc("id"),
1838 )
1839 .unwrap();
1840 assert_eq!(
1841 query.sql,
1842 "SELECT id, version, name FROM orders WHERE (id = ANY($1)) ORDER BY id ASC"
1843 );
1844 assert_eq!(query.params, vec![Value::List(values)]);
1845 }
1846
1847 #[test]
1848 fn topn_012_postgres_schema_adds_full_foreign_key_id_desc_index() {
1849 let trip = EntityDescriptor::new("Trip")
1850 .table_name("trip_data")
1851 .property(
1852 PropertyDescriptor::new("id", DataType::U64)
1853 .column_name("id")
1854 .id()
1855 .not_null(),
1856 )
1857 .property(
1858 PropertyDescriptor::new("vendor_id", DataType::U64)
1859 .column_name("vendor")
1860 .not_null(),
1861 )
1862 .relation(
1863 RelationDescriptor::new("vendor", "Vendor")
1864 .local_key("vendor_id")
1865 .foreign_key("id"),
1866 )
1867 .relation(
1869 RelationDescriptor::new("billing_vendor", "Vendor")
1870 .local_key("vendor_id")
1871 .foreign_key("id"),
1872 )
1873 .relation(
1875 RelationDescriptor::new("items", "TripItem")
1876 .local_key("id")
1877 .foreign_key("trip_id")
1878 .many(),
1879 );
1880
1881 assert_eq!(
1882 PostgresDialect.relation_indexes_sqls(&trip),
1883 vec![
1884 "CREATE INDEX IF NOT EXISTS IDX_TRIP_DATA_VENDOR_ID_DESC ON trip_data (vendor, id DESC)"
1885 ]
1886 );
1887 }
1888
1889 #[test]
1890 fn postgres_relation_index_name_is_stable_and_within_identifier_limit() {
1891 let name = postgres_index_name(
1892 "an_extremely_long_generated_transaction_history_table_name",
1893 "an_equally_long_business_owner_reference_identifier",
1894 "id",
1895 );
1896 assert!(name.len() <= 63);
1897 assert_eq!(
1898 name,
1899 postgres_index_name(
1900 "an_extremely_long_generated_transaction_history_table_name",
1901 "an_equally_long_business_owner_reference_identifier",
1902 "id",
1903 )
1904 );
1905 assert!(name.ends_with("_889B21BBED38CC82"));
1906 }
1907}