1#![warn(missing_docs)]
2
3mod oid_cache;
15mod statement_cache;
16#[cfg(feature = "tls")]
17mod tls;
18mod r#type;
19mod value;
20
21pub(crate) use value::Value;
22
23use async_trait::async_trait;
24use percent_encoding::percent_decode_str;
25use std::{borrow::Cow, sync::Arc};
26use toasty_core::{
27 Result, Schema,
28 driver::{
29 Capability, ConnectContext, Driver, ExecResponse, Operation, QueryLogConfig,
30 log::QueryLog,
31 operation::{RawSqlRet, Transaction, TransactionMode, TypedValue},
32 },
33 schema::{
34 db::{self, Migration, Table},
35 diff,
36 },
37 stmt,
38 stmt::ValueRecord,
39};
40use toasty_sql::{self as sql};
41use tokio_postgres::{Client, Config, Socket, tls::MakeTlsConnect, types::ToSql};
42use url::Url;
43
44enum SqlReturn {
45 Count,
46 Infer,
47 Types(Vec<stmt::Type>),
48}
49
50use crate::{oid_cache::OidCache, statement_cache::StatementCache};
51
52fn classify_pg_error(e: tokio_postgres::Error) -> toasty_core::Error {
63 if let Some(db_err) = e.as_db_error() {
64 match db_err.code().code() {
65 "40001" => toasty_core::Error::serialization_failure(db_err.message()),
66 "25006" => toasty_core::Error::read_only_transaction(db_err.message()),
67 _ => toasty_core::Error::driver_operation_failed(e),
68 }
69 } else {
70 toasty_core::Error::connection_lost(e)
71 }
72}
73
74pub struct PostgreSQL {
84 url: String,
85 config: Config,
86 #[cfg(feature = "tls")]
87 tls: Option<tokio_postgres_rustls::MakeRustlsConnect>,
88}
89
90impl std::fmt::Debug for PostgreSQL {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 let Self {
93 url,
94 config,
95 #[cfg(feature = "tls")]
96 tls,
97 } = self;
98 let mut s = f.debug_struct("PostgreSQL");
99 s.field("url", url);
100 s.field("config", config);
101 #[cfg(feature = "tls")]
102 s.field("tls", &tls.as_ref().map(|_| "MakeRustlsConnect"));
103 s.finish()
104 }
105}
106
107impl PostgreSQL {
108 pub fn new(url: impl Into<String>) -> Result<Self> {
110 let url_str = url.into();
111 let url = Url::parse(&url_str).map_err(toasty_core::Error::driver_operation_failed)?;
112
113 if !matches!(url.scheme(), "postgresql" | "postgres") {
114 return Err(toasty_core::Error::invalid_connection_url(format!(
115 "connection URL does not have a `postgresql` scheme; url={}",
116 url
117 )));
118 }
119
120 if url.path().is_empty() {
121 return Err(toasty_core::Error::invalid_connection_url(format!(
122 "no database specified - missing path in connection URL; url={}",
123 url
124 )));
125 }
126
127 let mut config = Config::new();
128
129 let dbname = percent_decode_str(url.path().trim_start_matches('/'))
130 .decode_utf8()
131 .map_err(|_| {
132 toasty_core::Error::invalid_connection_url("database name is not valid UTF-8")
133 })?;
134 config.dbname(&*dbname);
135
136 if !url.username().is_empty() {
137 let user = percent_decode_str(url.username())
138 .decode_utf8()
139 .map_err(|_| {
140 toasty_core::Error::invalid_connection_url("username is not valid UTF-8")
141 })?;
142 config.user(&*user);
143 }
144
145 if let Some(password) = url.password() {
146 config.password(percent_decode_str(password).collect::<Vec<u8>>());
147 }
148
149 let mut host: Option<String> = None;
158 let mut port: Option<u16> = None;
159
160 for (key, value) in url.query_pairs() {
161 match key.as_ref() {
162 "host" => host = Some(value.into_owned()),
163 "port" => {
164 port = Some(value.parse::<u16>().map_err(|_| {
165 toasty_core::Error::invalid_connection_url(format!(
166 "invalid port in connection URL query parameter: {value}"
167 ))
168 })?);
169 }
170 "user" => {
171 config.user(&*value);
172 }
173 "password" => {
174 config.password(value.as_bytes());
175 }
176 "dbname" => {
177 config.dbname(&*value);
178 }
179 "application_name" => {
180 config.application_name(&*value);
181 }
182 _ => {}
183 }
184 }
185
186 let host = host
187 .or_else(|| url.host_str().filter(|h| !h.is_empty()).map(String::from))
188 .ok_or_else(|| {
189 toasty_core::Error::invalid_connection_url(format!(
190 "missing host in connection URL; url={}",
191 url
192 ))
193 })?;
194 config.host(&host);
195
196 if let Some(port) = port.or_else(|| url.port()) {
197 config.port(port);
198 }
199
200 #[cfg(feature = "tls")]
201 let tls = tls::configure_tls(&url, &mut config)?;
202
203 #[cfg(not(feature = "tls"))]
204 for (key, value) in url.query_pairs() {
205 if key == "sslmode" && value != "disable" {
206 return Err(toasty_core::Error::invalid_connection_url(
207 "TLS not available: compile with the `tls` feature",
208 ));
209 }
210 }
211
212 Ok(Self {
213 url: url_str,
214 config,
215 #[cfg(feature = "tls")]
216 tls,
217 })
218 }
219
220 async fn connect_with_config(&self, config: Config) -> Result<Connection> {
221 #[cfg(feature = "tls")]
222 if let Some(ref tls) = self.tls {
223 return Connection::connect(config, tls.clone()).await;
224 }
225 Connection::connect(config, tokio_postgres::NoTls).await
226 }
227}
228
229#[async_trait]
230impl Driver for PostgreSQL {
231 fn url(&self) -> Cow<'_, str> {
232 Cow::Borrowed(&self.url)
233 }
234
235 fn capability(&self) -> &'static Capability {
236 &Capability::POSTGRESQL
237 }
238
239 async fn connect(
240 &self,
241 cx: &ConnectContext,
242 ) -> toasty_core::Result<Box<dyn toasty_core::driver::Connection>> {
243 let mut connection = self.connect_with_config(self.config.clone()).await?;
244 connection.query_log = cx.query_log;
245 Ok(Box::new(connection))
246 }
247
248 fn generate_migration(&self, schema_diff: &diff::Schema<'_>) -> Migration {
249 let statements = sql::MigrationStatement::from_diff(schema_diff, &Capability::POSTGRESQL);
250
251 let sql_strings: Vec<String> = statements
252 .iter()
253 .map(|stmt| sql::Serializer::postgresql(stmt.schema()).serialize(stmt.statement()))
254 .collect();
255
256 Migration::new_sql(sql_strings.join("\n"))
257 }
258
259 async fn reset_db(&self) -> toasty_core::Result<()> {
260 let dbname = self
261 .config
262 .get_dbname()
263 .ok_or_else(|| {
264 toasty_core::Error::invalid_connection_url("no database name configured")
265 })?
266 .to_string();
267
268 let temp_dbname = "__toasty_reset_temp";
270
271 let connect = |dbname: &str| {
272 let mut config = self.config.clone();
273 config.dbname(dbname);
274 self.connect_with_config(config)
275 };
276
277 let conn = connect(&dbname).await?;
279 conn.client
280 .execute(&format!("DROP DATABASE IF EXISTS \"{}\"", temp_dbname), &[])
281 .await
282 .map_err(classify_pg_error)?;
283 conn.client
284 .execute(&format!("CREATE DATABASE \"{}\"", temp_dbname), &[])
285 .await
286 .map_err(classify_pg_error)?;
287 drop(conn);
288
289 let conn = connect(temp_dbname).await?;
291 conn.client
292 .execute(
293 "SELECT pg_terminate_backend(pid) \
294 FROM pg_stat_activity \
295 WHERE datname = $1 AND pid <> pg_backend_pid()",
296 &[&dbname],
297 )
298 .await
299 .map_err(classify_pg_error)?;
300 conn.client
301 .execute(&format!("DROP DATABASE IF EXISTS \"{}\"", dbname), &[])
302 .await
303 .map_err(classify_pg_error)?;
304 conn.client
305 .execute(&format!("CREATE DATABASE \"{}\"", dbname), &[])
306 .await
307 .map_err(classify_pg_error)?;
308 drop(conn);
309
310 let conn = connect(&dbname).await?;
312 conn.client
313 .execute(&format!("DROP DATABASE IF EXISTS \"{}\"", temp_dbname), &[])
314 .await
315 .map_err(classify_pg_error)?;
316
317 Ok(())
318 }
319}
320
321#[derive(Debug)]
323pub struct Connection {
324 client: Client,
325 statement_cache: StatementCache,
326 oid_cache: OidCache,
327 query_log: QueryLogConfig,
328}
329
330impl Connection {
331 pub fn new(client: Client) -> Self {
333 Self {
334 client,
335 statement_cache: StatementCache::new(100),
336 oid_cache: OidCache::new(),
337 query_log: QueryLogConfig::default(),
338 }
339 }
340
341 pub async fn connect<T>(config: Config, tls: T) -> Result<Self>
345 where
346 T: MakeTlsConnect<Socket> + 'static,
347 T::Stream: Send,
348 {
349 let (client, connection) = config.connect(tls).await.map_err(classify_pg_error)?;
350
351 tokio::spawn(async move {
352 if let Err(e) = connection.await {
353 eprintln!("connection error: {e}");
354 }
355 });
356
357 Ok(Self::new(client))
358 }
359
360 async fn exec_sql(
361 &mut self,
362 sql_as_str: &str,
363 typed_params: Vec<TypedValue>,
364 ret: SqlReturn,
365 ) -> Result<ExecResponse> {
366 let mut log = QueryLog::sql(
367 &self.query_log,
368 "postgresql",
369 sql_as_str,
370 typed_params.iter().map(|tv| &tv.value),
371 );
372 let result = self
373 .exec_sql_inner(sql_as_str, typed_params, ret, &mut log)
374 .await;
375 log.finish(&result);
376 result
377 }
378
379 async fn exec_sql_inner(
380 &mut self,
381 sql_as_str: &str,
382 typed_params: Vec<TypedValue>,
383 ret: SqlReturn,
384 log: &mut QueryLog<'_>,
385 ) -> Result<ExecResponse> {
386 self.oid_cache
387 .preload(&self.client, typed_params.iter().map(|tv| &tv.ty))
388 .await?;
389 let param_types: Vec<_> = typed_params
390 .iter()
391 .map(|tv| self.oid_cache.get(&tv.ty).clone())
392 .collect();
393
394 let values: Vec<_> = typed_params
395 .into_iter()
396 .map(|tv| Value::from(tv.value))
397 .collect();
398 let params = values
399 .iter()
400 .map(|param| param as &(dyn ToSql + Sync))
401 .collect::<Vec<_>>();
402
403 let statement = self
404 .statement_cache
405 .prepare_typed(&mut self.client, sql_as_str, ¶m_types)
406 .await
407 .map_err(classify_pg_error)?;
408
409 if matches!(ret, SqlReturn::Count) {
410 let count = self
411 .client
412 .execute(&statement, ¶ms)
413 .await
414 .map_err(classify_pg_error)?;
415 return Ok(ExecResponse::count(count));
416 }
417
418 let rows = self
419 .client
420 .query(&statement, ¶ms)
421 .await
422 .map_err(classify_pg_error)?;
423
424 log.rows(rows.len() as u64);
425
426 let results = rows.into_iter().map(move |row| {
427 let mut results = Vec::new();
428
429 match &ret {
430 SqlReturn::Count => unreachable!(),
431 SqlReturn::Infer => {
432 for (i, column) in row.columns().iter().enumerate() {
433 results.push(Value::from_sql_infer(i, &row, column).into_inner());
434 }
435 }
436 SqlReturn::Types(ret_tys) => {
437 for (i, column) in row.columns().iter().enumerate() {
438 results.push(Value::from_sql(i, &row, column, &ret_tys[i]).into_inner());
439 }
440 }
441 }
442
443 Ok(ValueRecord::from_vec(results))
444 });
445
446 Ok(ExecResponse::value_stream(stmt::ValueStream::from_iter(
447 results,
448 )))
449 }
450
451 pub async fn create_table(&mut self, schema: &db::Schema, table: &Table) -> Result<()> {
453 let serializer = sql::Serializer::postgresql(schema);
454
455 let sql = serializer.serialize(&sql::Statement::create_table(
456 table,
457 &Capability::POSTGRESQL,
458 ));
459
460 self.client
461 .execute(&sql, &[])
462 .await
463 .map_err(classify_pg_error)?;
464
465 for index in &table.indices {
466 if index.primary_key {
467 continue;
468 }
469
470 let sql = serializer.serialize(&sql::Statement::create_index(index));
471
472 self.client
473 .execute(&sql, &[])
474 .await
475 .map_err(classify_pg_error)?;
476 }
477
478 Ok(())
479 }
480}
481
482impl From<Client> for Connection {
483 fn from(client: Client) -> Self {
484 Self::new(client)
485 }
486}
487
488#[async_trait]
489impl toasty_core::driver::Connection for Connection {
490 async fn exec(&mut self, schema: &Arc<Schema>, op: Operation) -> Result<ExecResponse> {
491 tracing::trace!(driver = "postgresql", op = %op.name(), "driver exec");
492
493 if let Operation::Transaction(ref t) = op {
494 if let Transaction::Start {
498 mode: mode @ (TransactionMode::Immediate | TransactionMode::Exclusive),
499 ..
500 } = t
501 {
502 return Err(toasty_core::Error::unsupported_feature(format!(
503 "PostgreSQL does not support TransactionMode::{mode:?}"
504 )));
505 }
506 let sql = sql::Serializer::postgresql(&schema.db).serialize_transaction(t);
507 self.client
508 .batch_execute(&sql)
509 .await
510 .map_err(classify_pg_error)?;
511 return Ok(ExecResponse::count(0));
512 }
513
514 let (sql, typed_params, ret_tys) = match op {
515 Operation::Insert(op) => (sql::Statement::from(op.stmt), op.params, None),
516 Operation::QuerySql(query) => {
517 assert!(
518 query.last_insert_id_hack.is_none(),
519 "last_insert_id_hack is MySQL-specific and should not be set for PostgreSQL"
520 );
521 (sql::Statement::from(query.stmt), query.params, query.ret)
522 }
523 Operation::RawSql(op) => {
524 let ret = match op.ret {
525 RawSqlRet::None => SqlReturn::Count,
526 RawSqlRet::Infer => SqlReturn::Infer,
527 RawSqlRet::Types(types) => SqlReturn::Types(types),
528 };
529 return self.exec_sql(&op.sql, op.params, ret).await;
530 }
531 op => todo!("op={:#?}", op),
532 };
533
534 let sql_as_str = sql::Serializer::postgresql(&schema.db).serialize(&sql);
535
536 let ret = if sql.returning_len().is_some() {
537 SqlReturn::Types(ret_tys.unwrap())
538 } else {
539 SqlReturn::Count
540 };
541
542 self.exec_sql(&sql_as_str, typed_params, ret).await
543 }
544
545 async fn push_schema(&mut self, schema: &Schema) -> Result<()> {
546 let serializer = sql::Serializer::postgresql(&schema.db);
547
548 let mut created_enum_types = hashbrown::HashSet::new();
551 for table in &schema.db.tables {
552 for column in &table.columns {
553 if let toasty_core::schema::db::Type::Enum(type_enum) = &column.storage_ty
554 && created_enum_types.insert(type_enum.name.clone())
555 {
556 let sql = serializer.serialize(&sql::Statement::create_enum_type(type_enum));
557
558 tracing::debug!(enum_type = ?type_enum.name, "creating enum type");
559 self.client
560 .execute(&sql, &[])
561 .await
562 .map_err(classify_pg_error)?;
563 }
564 }
565 }
566
567 for table in &schema.db.tables {
568 tracing::debug!(table = %table.name, "creating table");
569 self.create_table(&schema.db, table).await?;
570 }
571 Ok(())
572 }
573
574 async fn applied_migrations(
575 &mut self,
576 ) -> Result<Vec<toasty_core::schema::db::AppliedMigration>> {
577 self.client
579 .execute(
580 "CREATE TABLE IF NOT EXISTS __toasty_migrations (
581 id BIGINT PRIMARY KEY,
582 name TEXT NOT NULL,
583 applied_at TIMESTAMP NOT NULL
584 )",
585 &[],
586 )
587 .await
588 .map_err(classify_pg_error)?;
589
590 let rows = self
592 .client
593 .query(
594 "SELECT id FROM __toasty_migrations ORDER BY applied_at",
595 &[],
596 )
597 .await
598 .map_err(classify_pg_error)?;
599
600 Ok(rows
601 .iter()
602 .map(|row| {
603 let id: i64 = row.get(0);
604 toasty_core::schema::db::AppliedMigration::new(id as u64)
605 })
606 .collect())
607 }
608
609 async fn apply_migration(
610 &mut self,
611 id: u64,
612 name: &str,
613 migration: &toasty_core::schema::db::Migration,
614 ) -> Result<()> {
615 tracing::info!(id = id, name = %name, "applying migration");
616 self.client
618 .execute(
619 "CREATE TABLE IF NOT EXISTS __toasty_migrations (
620 id BIGINT PRIMARY KEY,
621 name TEXT NOT NULL,
622 applied_at TIMESTAMP NOT NULL
623 )",
624 &[],
625 )
626 .await
627 .map_err(classify_pg_error)?;
628
629 let transaction = self.client.transaction().await.map_err(classify_pg_error)?;
631
632 for statement in migration.statements() {
634 if let Err(e) = transaction
635 .batch_execute(statement)
636 .await
637 .map_err(classify_pg_error)
638 {
639 transaction.rollback().await.map_err(classify_pg_error)?;
640 return Err(e);
641 }
642 }
643
644 if let Err(e) = transaction
646 .execute(
647 "INSERT INTO __toasty_migrations (id, name, applied_at) VALUES ($1, $2, NOW())",
648 &[&(id as i64), &name],
649 )
650 .await
651 .map_err(classify_pg_error)
652 {
653 transaction.rollback().await.map_err(classify_pg_error)?;
654 return Err(e);
655 }
656
657 transaction.commit().await.map_err(classify_pg_error)?;
659 Ok(())
660 }
661
662 fn is_valid(&self) -> bool {
663 !self.client.is_closed()
664 }
665
666 async fn ping(&mut self) -> Result<()> {
667 self.client
672 .simple_query("")
673 .await
674 .map(|_| ())
675 .map_err(toasty_core::Error::connection_lost)
676 }
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682 use tokio_postgres::config::Host;
683
684 fn cfg(url: &str) -> Config {
685 PostgreSQL::new(url).expect("valid URL").config
686 }
687
688 #[test]
689 fn host_in_url_authority() {
690 let c = cfg("postgresql://example.com/mydb");
691 assert_eq!(c.get_hosts(), &[Host::Tcp("example.com".into())]);
692 assert_eq!(c.get_dbname(), Some("mydb"));
693 }
694
695 #[cfg(unix)]
700 mod unix_socket {
701 use super::*;
702 use std::path::PathBuf;
703
704 #[test]
705 fn unix_socket_via_host_query_param() {
706 let c = cfg("postgresql:///mydb?host=/tmp&user=myuser");
710 assert_eq!(c.get_hosts(), &[Host::Unix(PathBuf::from("/tmp"))]);
711 assert_eq!(c.get_user(), Some("myuser"));
712 assert_eq!(c.get_dbname(), Some("mydb"));
713 }
714
715 #[test]
716 fn query_param_host_overrides_url_authority() {
717 let c = cfg("postgresql://example.com/mydb?host=/var/run/postgresql");
723 assert_eq!(
724 c.get_hosts(),
725 &[Host::Unix(PathBuf::from("/var/run/postgresql"))]
726 );
727 }
728
729 #[test]
730 fn query_param_port_user_password_dbname() {
731 let c = cfg(
732 "postgresql:///placeholder?host=/tmp&port=5433&user=alice&password=s3cret&dbname=real",
733 );
734 assert_eq!(c.get_hosts(), &[Host::Unix(PathBuf::from("/tmp"))]);
735 assert_eq!(c.get_ports(), &[5433]);
736 assert_eq!(c.get_user(), Some("alice"));
737 assert_eq!(c.get_password(), Some(&b"s3cret"[..]));
738 assert_eq!(c.get_dbname(), Some("real"));
739 }
740 }
741
742 #[test]
743 fn query_param_port_overrides_url_port() {
744 let c = cfg("postgresql://example.com:5432/mydb?port=5433");
747 assert_eq!(c.get_ports(), &[5433]);
748 }
749
750 #[test]
751 fn application_name_query_param() {
752 let c = cfg("postgresql://localhost/mydb?application_name=my_app");
753 assert_eq!(c.get_application_name(), Some("my_app"));
754 }
755
756 #[test]
757 fn missing_host_rejected() {
758 let err = PostgreSQL::new("postgresql:///mydb").unwrap_err();
759 assert!(
760 err.to_string().contains("missing host"),
761 "expected missing-host error, got: {err}"
762 );
763 }
764
765 #[test]
766 fn invalid_port_query_param_rejected() {
767 let err = PostgreSQL::new("postgresql:///mydb?host=/tmp&port=not-a-number").unwrap_err();
768 assert!(
769 err.to_string().contains("invalid port"),
770 "expected invalid-port error, got: {err}"
771 );
772 }
773}