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