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