toasty_driver_postgresql/
lib.rs1#![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::{Capability, Driver, ExecResponse, Operation},
29 schema::db::{self, Migration, SchemaDiff, Table},
30 stmt,
31 stmt::ValueRecord,
32};
33use toasty_sql::{self as sql};
34use tokio_postgres::{Client, Config, Socket, tls::MakeTlsConnect, types::ToSql};
35use url::Url;
36
37use crate::{oid_cache::OidCache, statement_cache::StatementCache};
38
39fn classify_pg_error(e: tokio_postgres::Error) -> toasty_core::Error {
50 if let Some(db_err) = e.as_db_error() {
51 match db_err.code().code() {
52 "40001" => toasty_core::Error::serialization_failure(db_err.message()),
53 "25006" => toasty_core::Error::read_only_transaction(db_err.message()),
54 _ => toasty_core::Error::driver_operation_failed(e),
55 }
56 } else {
57 toasty_core::Error::connection_lost(e)
58 }
59}
60
61#[derive(Debug)]
71pub struct PostgreSQL {
72 url: String,
73 config: Config,
74 #[cfg(feature = "tls")]
75 tls: Option<tls::MakeRustlsConnect>,
76}
77
78impl PostgreSQL {
79 pub fn new(url: impl Into<String>) -> Result<Self> {
81 let url_str = url.into();
82 let url = Url::parse(&url_str).map_err(toasty_core::Error::driver_operation_failed)?;
83
84 if !matches!(url.scheme(), "postgresql" | "postgres") {
85 return Err(toasty_core::Error::invalid_connection_url(format!(
86 "connection URL does not have a `postgresql` scheme; url={}",
87 url
88 )));
89 }
90
91 let host = url.host_str().ok_or_else(|| {
92 toasty_core::Error::invalid_connection_url(format!(
93 "missing host in connection URL; url={}",
94 url
95 ))
96 })?;
97
98 if url.path().is_empty() {
99 return Err(toasty_core::Error::invalid_connection_url(format!(
100 "no database specified - missing path in connection URL; url={}",
101 url
102 )));
103 }
104
105 let mut config = Config::new();
106 config.host(host);
107
108 let dbname = percent_decode_str(url.path().trim_start_matches('/'))
109 .decode_utf8()
110 .map_err(|_| {
111 toasty_core::Error::invalid_connection_url("database name is not valid UTF-8")
112 })?;
113 config.dbname(&*dbname);
114
115 if let Some(port) = url.port() {
116 config.port(port);
117 }
118
119 if !url.username().is_empty() {
120 let user = percent_decode_str(url.username())
121 .decode_utf8()
122 .map_err(|_| {
123 toasty_core::Error::invalid_connection_url("username is not valid UTF-8")
124 })?;
125 config.user(&*user);
126 }
127
128 if let Some(password) = url.password() {
129 config.password(percent_decode_str(password).collect::<Vec<u8>>());
130 }
131
132 for (key, value) in url.query_pairs() {
133 if key == "application_name" {
134 config.application_name(&*value);
135 }
136 }
137
138 #[cfg(feature = "tls")]
139 let tls = tls::configure_tls(&url, &mut config)?;
140
141 #[cfg(not(feature = "tls"))]
142 for (key, value) in url.query_pairs() {
143 if key == "sslmode" && value != "disable" {
144 return Err(toasty_core::Error::invalid_connection_url(
145 "TLS not available: compile with the `tls` feature",
146 ));
147 }
148 }
149
150 Ok(Self {
151 url: url_str,
152 config,
153 #[cfg(feature = "tls")]
154 tls,
155 })
156 }
157
158 async fn connect_with_config(&self, config: Config) -> Result<Connection> {
159 #[cfg(feature = "tls")]
160 if let Some(ref tls) = self.tls {
161 return Connection::connect(config, tls.clone()).await;
162 }
163 Connection::connect(config, tokio_postgres::NoTls).await
164 }
165}
166
167#[async_trait]
168impl Driver for PostgreSQL {
169 fn url(&self) -> Cow<'_, str> {
170 Cow::Borrowed(&self.url)
171 }
172
173 fn capability(&self) -> &'static Capability {
174 &Capability::POSTGRESQL
175 }
176
177 async fn connect(&self) -> toasty_core::Result<Box<dyn toasty_core::driver::Connection>> {
178 Ok(Box::new(
179 self.connect_with_config(self.config.clone()).await?,
180 ))
181 }
182
183 fn generate_migration(&self, schema_diff: &SchemaDiff<'_>) -> Migration {
184 let statements = sql::MigrationStatement::from_diff(schema_diff, &Capability::POSTGRESQL);
185
186 let sql_strings: Vec<String> = statements
187 .iter()
188 .map(|stmt| sql::Serializer::postgresql(stmt.schema()).serialize(stmt.statement()))
189 .collect();
190
191 Migration::new_sql(sql_strings.join("\n"))
192 }
193
194 async fn reset_db(&self) -> toasty_core::Result<()> {
195 let dbname = self
196 .config
197 .get_dbname()
198 .ok_or_else(|| {
199 toasty_core::Error::invalid_connection_url("no database name configured")
200 })?
201 .to_string();
202
203 let temp_dbname = "__toasty_reset_temp";
205
206 let connect = |dbname: &str| {
207 let mut config = self.config.clone();
208 config.dbname(dbname);
209 self.connect_with_config(config)
210 };
211
212 let conn = connect(&dbname).await?;
214 conn.client
215 .execute(&format!("DROP DATABASE IF EXISTS \"{}\"", temp_dbname), &[])
216 .await
217 .map_err(classify_pg_error)?;
218 conn.client
219 .execute(&format!("CREATE DATABASE \"{}\"", temp_dbname), &[])
220 .await
221 .map_err(classify_pg_error)?;
222 drop(conn);
223
224 let conn = connect(temp_dbname).await?;
226 conn.client
227 .execute(
228 "SELECT pg_terminate_backend(pid) \
229 FROM pg_stat_activity \
230 WHERE datname = $1 AND pid <> pg_backend_pid()",
231 &[&dbname],
232 )
233 .await
234 .map_err(classify_pg_error)?;
235 conn.client
236 .execute(&format!("DROP DATABASE IF EXISTS \"{}\"", dbname), &[])
237 .await
238 .map_err(classify_pg_error)?;
239 conn.client
240 .execute(&format!("CREATE DATABASE \"{}\"", dbname), &[])
241 .await
242 .map_err(classify_pg_error)?;
243 drop(conn);
244
245 let conn = connect(&dbname).await?;
247 conn.client
248 .execute(&format!("DROP DATABASE IF EXISTS \"{}\"", temp_dbname), &[])
249 .await
250 .map_err(classify_pg_error)?;
251
252 Ok(())
253 }
254}
255
256#[derive(Debug)]
258pub struct Connection {
259 client: Client,
260 statement_cache: StatementCache,
261 oid_cache: OidCache,
262}
263
264impl Connection {
265 pub fn new(client: Client) -> Self {
267 Self {
268 client,
269 statement_cache: StatementCache::new(100),
270 oid_cache: OidCache::new(),
271 }
272 }
273
274 pub async fn connect<T>(config: Config, tls: T) -> Result<Self>
278 where
279 T: MakeTlsConnect<Socket> + 'static,
280 T::Stream: Send,
281 {
282 let (client, connection) = config.connect(tls).await.map_err(classify_pg_error)?;
283
284 tokio::spawn(async move {
285 if let Err(e) = connection.await {
286 eprintln!("connection error: {e}");
287 }
288 });
289
290 Ok(Self::new(client))
291 }
292
293 pub async fn create_table(&mut self, schema: &db::Schema, table: &Table) -> Result<()> {
295 let serializer = sql::Serializer::postgresql(schema);
296
297 let sql = serializer.serialize(&sql::Statement::create_table(
298 table,
299 &Capability::POSTGRESQL,
300 ));
301
302 self.client
303 .execute(&sql, &[])
304 .await
305 .map_err(classify_pg_error)?;
306
307 for index in &table.indices {
308 if index.primary_key {
309 continue;
310 }
311
312 let sql = serializer.serialize(&sql::Statement::create_index(index));
313
314 self.client
315 .execute(&sql, &[])
316 .await
317 .map_err(classify_pg_error)?;
318 }
319
320 Ok(())
321 }
322}
323
324impl From<Client> for Connection {
325 fn from(client: Client) -> Self {
326 Self::new(client)
327 }
328}
329
330#[async_trait]
331impl toasty_core::driver::Connection for Connection {
332 async fn exec(&mut self, schema: &Arc<Schema>, op: Operation) -> Result<ExecResponse> {
333 tracing::trace!(driver = "postgresql", op = %op.name(), "driver exec");
334
335 if let Operation::Transaction(ref t) = op {
336 let sql = sql::Serializer::postgresql(&schema.db).serialize_transaction(t);
337 self.client
338 .batch_execute(&sql)
339 .await
340 .map_err(classify_pg_error)?;
341 return Ok(ExecResponse::count(0));
342 }
343
344 let (sql, typed_params, ret_tys) = match op {
345 Operation::Insert(op) => (sql::Statement::from(op.stmt), op.params, None),
346 Operation::QuerySql(query) => {
347 assert!(
348 query.last_insert_id_hack.is_none(),
349 "last_insert_id_hack is MySQL-specific and should not be set for PostgreSQL"
350 );
351 (sql::Statement::from(query.stmt), query.params, query.ret)
352 }
353 op => todo!("op={:#?}", op),
354 };
355
356 let width = sql.returning_len();
357
358 let sql_as_str = sql::Serializer::postgresql(&schema.db).serialize(&sql);
359
360 tracing::debug!(db.system = "postgresql", db.statement = %sql_as_str, params = typed_params.len(), "executing SQL");
361
362 self.oid_cache
363 .preload(&self.client, typed_params.iter().map(|tv| &tv.ty))
364 .await?;
365 let param_types: Vec<_> = typed_params
366 .iter()
367 .map(|tv| self.oid_cache.get(&tv.ty).clone())
368 .collect();
369
370 let values: Vec<_> = typed_params
371 .into_iter()
372 .map(|tv| Value::from(tv.value))
373 .collect();
374 let params = values
375 .iter()
376 .map(|param| param as &(dyn ToSql + Sync))
377 .collect::<Vec<_>>();
378
379 let statement = self
380 .statement_cache
381 .prepare_typed(&mut self.client, &sql_as_str, ¶m_types)
382 .await
383 .map_err(classify_pg_error)?;
384
385 if width.is_none() {
386 let count = self
387 .client
388 .execute(&statement, ¶ms)
389 .await
390 .map_err(classify_pg_error)?;
391 return Ok(ExecResponse::count(count));
392 }
393
394 let rows = self
395 .client
396 .query(&statement, ¶ms)
397 .await
398 .map_err(classify_pg_error)?;
399
400 if width.is_none() {
401 let [row] = &rows[..] else { todo!() };
402 let total = row.get::<usize, i64>(0);
403 let condition_matched = row.get::<usize, i64>(1);
404
405 if total == condition_matched {
406 Ok(ExecResponse::count(total as _))
407 } else {
408 Err(toasty_core::Error::condition_failed(
409 "update condition did not match",
410 ))
411 }
412 } else {
413 let ret_tys = ret_tys.as_ref().unwrap().clone();
414 let results = rows.into_iter().map(move |row| {
415 let mut results = Vec::new();
416 for (i, column) in row.columns().iter().enumerate() {
417 results.push(Value::from_sql(i, &row, column, &ret_tys[i]).into_inner());
418 }
419
420 Ok(ValueRecord::from_vec(results))
421 });
422
423 Ok(ExecResponse::value_stream(stmt::ValueStream::from_iter(
424 results,
425 )))
426 }
427 }
428
429 async fn push_schema(&mut self, schema: &Schema) -> Result<()> {
430 let serializer = sql::Serializer::postgresql(&schema.db);
431
432 let mut created_enum_types = hashbrown::HashSet::new();
435 for table in &schema.db.tables {
436 for column in &table.columns {
437 if let toasty_core::schema::db::Type::Enum(type_enum) = &column.storage_ty
438 && created_enum_types.insert(type_enum.name.clone())
439 {
440 let sql = serializer.serialize(&sql::Statement::create_enum_type(type_enum));
441
442 tracing::debug!(enum_type = ?type_enum.name, "creating enum type");
443 self.client
444 .execute(&sql, &[])
445 .await
446 .map_err(classify_pg_error)?;
447 }
448 }
449 }
450
451 for table in &schema.db.tables {
452 tracing::debug!(table = %table.name, "creating table");
453 self.create_table(&schema.db, table).await?;
454 }
455 Ok(())
456 }
457
458 async fn applied_migrations(
459 &mut self,
460 ) -> Result<Vec<toasty_core::schema::db::AppliedMigration>> {
461 self.client
463 .execute(
464 "CREATE TABLE IF NOT EXISTS __toasty_migrations (
465 id BIGINT PRIMARY KEY,
466 name TEXT NOT NULL,
467 applied_at TIMESTAMP NOT NULL
468 )",
469 &[],
470 )
471 .await
472 .map_err(classify_pg_error)?;
473
474 let rows = self
476 .client
477 .query(
478 "SELECT id FROM __toasty_migrations ORDER BY applied_at",
479 &[],
480 )
481 .await
482 .map_err(classify_pg_error)?;
483
484 Ok(rows
485 .iter()
486 .map(|row| {
487 let id: i64 = row.get(0);
488 toasty_core::schema::db::AppliedMigration::new(id as u64)
489 })
490 .collect())
491 }
492
493 async fn apply_migration(
494 &mut self,
495 id: u64,
496 name: &str,
497 migration: &toasty_core::schema::db::Migration,
498 ) -> Result<()> {
499 tracing::info!(id = id, name = %name, "applying migration");
500 self.client
502 .execute(
503 "CREATE TABLE IF NOT EXISTS __toasty_migrations (
504 id BIGINT PRIMARY KEY,
505 name TEXT NOT NULL,
506 applied_at TIMESTAMP NOT NULL
507 )",
508 &[],
509 )
510 .await
511 .map_err(classify_pg_error)?;
512
513 let transaction = self.client.transaction().await.map_err(classify_pg_error)?;
515
516 for statement in migration.statements() {
518 if let Err(e) = transaction
519 .batch_execute(statement)
520 .await
521 .map_err(classify_pg_error)
522 {
523 transaction.rollback().await.map_err(classify_pg_error)?;
524 return Err(e);
525 }
526 }
527
528 if let Err(e) = transaction
530 .execute(
531 "INSERT INTO __toasty_migrations (id, name, applied_at) VALUES ($1, $2, NOW())",
532 &[&(id as i64), &name],
533 )
534 .await
535 .map_err(classify_pg_error)
536 {
537 transaction.rollback().await.map_err(classify_pg_error)?;
538 return Err(e);
539 }
540
541 transaction.commit().await.map_err(classify_pg_error)?;
543 Ok(())
544 }
545
546 fn is_valid(&self) -> bool {
547 !self.client.is_closed()
548 }
549
550 async fn ping(&mut self) -> Result<()> {
551 self.client
556 .simple_query("")
557 .await
558 .map(|_| ())
559 .map_err(toasty_core::Error::connection_lost)
560 }
561}