diff --git a/Cargo.toml b/Cargo.toml index 316dc471..72a9d01c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,16 +50,21 @@ authors.workspace = true repository.workspace = true [package.metadata.docs.rs] -features = ["all-databases", "_unstable-all-types"] +features = ["all-databases", "_unstable-all-types", "_unstable-doc"] rustdoc-args = ["--cfg", "docsrs"] [features] -default = ["any", "macros", "migrate", "json"] +default = ["any", "macros", "migrate", "json", "config-all"] derive = ["sqlx-macros/derive"] macros = ["derive", "sqlx-macros/macros"] migrate = ["sqlx-core/migrate", "sqlx-macros?/migrate", "sqlx-mysql?/migrate", "sqlx-postgres?/migrate", "sqlx-sqlite?/migrate"] +# Enable parsing of `sqlx.toml` for configuring macros, migrations, or both. +config-macros = ["sqlx-macros?/config-macros"] +config-migrate = ["sqlx-macros?/config-migrate"] +config-all = ["config-macros", "config-migrate"] + # intended mainly for CI and docs all-databases = ["mysql", "sqlite", "postgres", "any"] _unstable-all-types = [ @@ -73,6 +78,8 @@ _unstable-all-types = [ "uuid", "bit-vec", ] +# Render documentation that wouldn't otherwise be shown (e.g. `sqlx_core::config`). +_unstable-doc = ["config-all", "sqlx-core/_unstable-doc"] # Base runtime features without TLS runtime-async-std = ["_rt-async-std", "sqlx-core/_rt-async-std", "sqlx-macros?/_rt-async-std"] diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index d6628614..f70adde5 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -12,7 +12,7 @@ features = ["offline"] [features] default = [] -migrate = ["sha2", "crc"] +migrate = ["sha2", "crc", "config-migrate"] any = [] @@ -31,6 +31,12 @@ _tls-none = [] # support offline/decoupled building (enables serialization of `Describe`) offline = ["serde", "either/serde"] +config = ["serde", "toml/parse"] +config-macros = ["config"] +config-migrate = ["config"] + +_unstable-doc = ["config-macros", "config-migrate"] + [dependencies] # Runtimes async-std = { workspace = true, optional = true } @@ -70,6 +76,7 @@ percent-encoding = "2.1.0" regex = { version = "1.5.5", optional = true } serde = { version = "1.0.132", features = ["derive", "rc"], optional = true } serde_json = { version = "1.0.73", features = ["raw_value"], optional = true } +toml = { version = "0.8.16", optional = true } sha2 = { version = "0.10.0", default-features = false, optional = true } #sqlformat = "0.2.0" thiserror = "2.0.0" diff --git a/sqlx-core/src/config/common.rs b/sqlx-core/src/config/common.rs new file mode 100644 index 00000000..8c774fc6 --- /dev/null +++ b/sqlx-core/src/config/common.rs @@ -0,0 +1,38 @@ +/// Configuration shared by multiple components. +#[derive(Debug, Default, serde::Deserialize)] +pub struct Config { + /// Override the database URL environment variable. + /// + /// This is used by both the macros and `sqlx-cli`. + /// + /// Case-sensitive. Defaults to `DATABASE_URL`. + /// + /// Example: Multi-Database Project + /// ------- + /// You can use multiple databases in the same project by breaking it up into multiple crates, + /// then using a different environment variable for each. + /// + /// For example, with two crates in the workspace named `foo` and `bar`: + /// + /// #### `foo/sqlx.toml` + /// ```toml + /// [macros] + /// database_url_var = "FOO_DATABASE_URL" + /// ``` + /// + /// #### `bar/sqlx.toml` + /// ```toml + /// [macros] + /// database_url_var = "BAR_DATABASE_URL" + /// ``` + /// + /// #### `.env` + /// ```text + /// FOO_DATABASE_URL=postgres://postgres@localhost:5432/foo + /// BAR_DATABASE_URL=postgres://postgres@localhost:5432/bar + /// ``` + /// + /// The query macros used in `foo` will use `FOO_DATABASE_URL`, + /// and the ones used in `bar` will use `BAR_DATABASE_URL`. + pub database_url_var: Option, +} diff --git a/sqlx-core/src/config/macros.rs b/sqlx-core/src/config/macros.rs new file mode 100644 index 00000000..5edd30dc --- /dev/null +++ b/sqlx-core/src/config/macros.rs @@ -0,0 +1,296 @@ +use std::collections::BTreeMap; + +/// Configuration for the `query!()` family of macros. +#[derive(Debug, Default, serde::Deserialize)] +#[serde(default)] +pub struct Config { + /// Specify the crate to use for mapping date/time types to Rust. + /// + /// The default behavior is to use whatever crate is enabled, + /// [`chrono`] or [`time`] (the latter takes precedent). + /// + /// [`chrono`]: crate::types::chrono + /// [`time`]: crate::types::time + /// + /// Example: Always Use Chrono + /// ------- + /// Thanks to Cargo's [feature unification], a crate in the dependency graph may enable + /// the `time` feature of SQLx which will force it on for all crates using SQLx, + /// which will result in problems if your crate wants to use types from [`chrono`]. + /// + /// You can use the type override syntax (see `sqlx::query!` for details), + /// or you can force an override globally by setting this option. + /// + /// #### `sqlx.toml` + /// ```toml + /// [macros] + /// datetime_crate = "chrono" + /// ``` + /// + /// [feature unification]: https://doc.rust-lang.org/cargo/reference/features.html#feature-unification + pub datetime_crate: DateTimeCrate, + + /// Specify global overrides for mapping SQL type names to Rust type names. + /// + /// Default type mappings are defined by the database driver. + /// Refer to the `sqlx::types` module for details. + /// + /// ## Note: Orthogonal to Nullability + /// These overrides do not affect whether `query!()` decides to wrap a column in `Option<_>` + /// or not. They only override the inner type used. + /// + /// ## Note: Schema Qualification (Postgres) + /// Type names may be schema-qualified in Postgres. If so, the schema should be part + /// of the type string, e.g. `'foo.bar'` to reference type `bar` in schema `foo`. + /// + /// The schema and/or type name may additionally be quoted in the string + /// for a quoted identifier (see next section). + /// + /// Schema qualification should not be used for types in the search path. + /// + /// ## Note: Quoted Identifiers (Postgres) + /// Type names using [quoted identifiers in Postgres] must also be specified with quotes here. + /// + /// Note, however, that the TOML format parses way the outer pair of quotes, + /// so for quoted names in Postgres, double-quoting is necessary, + /// e.g. `'"Foo"'` for SQL type `"Foo"`. + /// + /// To reference a schema-qualified type with a quoted name, use double-quotes after the + /// dot, e.g. `'foo."Bar"'` to reference type `"Bar"` of schema `foo`, and vice versa for + /// quoted schema names. + /// + /// We recommend wrapping all type names in single quotes, as shown below, + /// to avoid confusion. + /// + /// MySQL/MariaDB and SQLite do not support custom types, so quoting type names should + /// never be necessary. + /// + /// [quoted identifiers in Postgres]: https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS + // Note: we wanted to be able to handle this intelligently, + // but the `toml` crate authors weren't interested: https://github.com/toml-rs/toml/issues/761 + // + // We decided to just encourage always quoting type names instead. + /// Example: Custom Wrapper Types + /// ------- + /// Does SQLx not support a type that you need? Do you want additional semantics not + /// implemented on the built-in types? You can create a custom wrapper, + /// or use an external crate. + /// + /// #### `sqlx.toml` + /// ```toml + /// [macros.type_overrides] + /// # Override a built-in type + /// 'uuid' = "crate::types::MyUuid" + /// + /// # Support an external or custom wrapper type (e.g. from the `isn` Postgres extension) + /// # (NOTE: FOR DOCUMENTATION PURPOSES ONLY; THIS CRATE/TYPE DOES NOT EXIST AS OF WRITING) + /// 'isbn13' = "isn_rs::sqlx::ISBN13" + /// ``` + /// + /// Example: Custom Types in Postgres + /// ------- + /// If you have a custom type in Postgres that you want to map without needing to use + /// the type override syntax in `sqlx::query!()` every time, you can specify a global + /// override here. + /// + /// For example, a custom enum type `foo`: + /// + /// #### Migration or Setup SQL (e.g. `migrations/0_setup.sql`) + /// ```sql + /// CREATE TYPE foo AS ENUM ('Bar', 'Baz'); + /// ``` + /// + /// #### `src/types.rs` + /// ```rust,no_run + /// #[derive(sqlx::Type)] + /// pub enum Foo { + /// Bar, + /// Baz + /// } + /// ``` + /// + /// If you're not using `PascalCase` in your enum variants then you'll want to use + /// `#[sqlx(rename_all = "")]` on your enum. + /// See [`Type`][crate::type::Type] for details. + /// + /// #### `sqlx.toml` + /// ```toml + /// [macros.type_overrides] + /// # Map SQL type `foo` to `crate::types::Foo` + /// 'foo' = "crate::types::Foo" + /// ``` + /// + /// Example: Schema-Qualified Types + /// ------- + /// (See `Note` section above for details.) + /// + /// ```toml + /// [macros.type_overrides] + /// # Map SQL type `foo.foo` to `crate::types::Foo` + /// 'foo.foo' = "crate::types::Foo" + /// ``` + /// + /// Example: Quoted Identifiers + /// ------- + /// If a type or schema uses quoted identifiers, + /// it must be wrapped in quotes _twice_ for SQLx to know the difference: + /// + /// ```toml + /// [macros.type_overrides] + /// # `"Foo"` in SQLx + /// '"Foo"' = "crate::types::Foo" + /// # **NOT** `"Foo"` in SQLx (parses as just `Foo`) + /// "Foo" = "crate::types::Foo" + /// + /// # Schema-qualified + /// '"foo".foo' = "crate::types::Foo" + /// 'foo."Foo"' = "crate::types::Foo" + /// '"foo"."Foo"' = "crate::types::Foo" + /// ``` + /// + /// (See `Note` section above for details.) + pub type_overrides: BTreeMap, + + /// Specify per-column overrides for mapping SQL types to Rust types. + /// + /// Default type mappings are defined by the database driver. + /// Refer to the `sqlx::types` module for details. + /// + /// The supported syntax is similar to [`type_overrides`][Self::type_overrides], + /// (with the same caveat for quoted names!) but column names must be qualified + /// by a separately quoted table name, which may optionally be schema-qualified. + /// + /// Multiple columns for the same SQL table may be written in the same table in TOML + /// (see examples below). + /// + /// ## Note: Orthogonal to Nullability + /// These overrides do not affect whether `query!()` decides to wrap a column in `Option<_>` + /// or not. They only override the inner type used. + /// + /// ## Note: Schema Qualification + /// Table names may be schema-qualified. If so, the schema should be part + /// of the table name string, e.g. `'foo.bar'` to reference table `bar` in schema `foo`. + /// + /// The schema and/or type name may additionally be quoted in the string + /// for a quoted identifier (see next section). + /// + /// Postgres users: schema qualification should not be used for tables in the search path. + /// + /// ## Note: Quoted Identifiers + /// Schema, table, or column names using quoted identifiers ([MySQL], [Postgres], [SQLite]) + /// in SQL must also be specified with quotes here. + /// + /// Postgres and SQLite use double-quotes (`"Foo"`) while MySQL uses backticks (`\`Foo\`). + /// + /// Note, however, that the TOML format parses way the outer pair of quotes, + /// so for quoted names in Postgres, double-quoting is necessary, + /// e.g. `'"Foo"'` for SQL name `"Foo"`. + /// + /// To reference a schema-qualified table with a quoted name, use the appropriate quotation + /// characters after the dot, e.g. `'foo."Bar"'` to reference table `"Bar"` of schema `foo`, + /// and vice versa for quoted schema names. + /// + /// We recommend wrapping all table and column names in single quotes, as shown below, + /// to avoid confusion. + /// + /// [MySQL]: https://dev.mysql.com/doc/refman/8.4/en/identifiers.html + /// [Postgres]: https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS + /// [SQLite]: https://sqlite.org/lang_keywords.html + // Note: we wanted to be able to handle this intelligently, + // but the `toml` crate authors weren't interested: https://github.com/toml-rs/toml/issues/761 + // + // We decided to just encourage always quoting type names instead. + /// + /// Example + /// ------- + /// + /// #### `sqlx.toml` + /// ```toml + /// [macros.column_overrides.'foo'] + /// # Map column `bar` of table `foo` to Rust type `crate::types::Foo`: + /// 'bar' = "crate::types::Bar" + /// + /// # Quoted column name + /// # Note: same quoting requirements as `macros.type_overrides` + /// '"Bar"' = "crate::types::Bar" + /// + /// # Note: will NOT work (parses as `Bar`) + /// # "Bar" = "crate::types::Bar" + /// + /// # Table name may be quoted (note the wrapping single-quotes) + /// [macros.column_overrides.'"Foo"'] + /// 'bar' = "crate::types::Bar" + /// '"Bar"' = "crate::types::Bar" + /// + /// # Table name may also be schema-qualified. + /// # Note how the dot is inside the quotes. + /// [macros.column_overrides.'my_schema.my_table'] + /// 'my_column' = "crate::types::MyType" + /// + /// # Quoted schema, table, and column names + /// [macros.column_overrides.'"My Schema"."My Table"'] + /// '"My Column"' = "crate::types::MyType" + /// ``` + pub column_overrides: BTreeMap>, +} + +/// The crate to use for mapping date/time types to Rust. +#[derive(Debug, Default, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DateTimeCrate { + /// Use whichever crate is enabled (`time` then `chrono`). + #[default] + Inferred, + + /// Always use types from [`chrono`][crate::types::chrono]. + /// + /// ```toml + /// [macros] + /// datetime_crate = "chrono" + /// ``` + Chrono, + + /// Always use types from [`time`][crate::types::time]. + /// + /// ```toml + /// [macros] + /// datetime_crate = "time" + /// ``` + Time, +} + +/// A SQL type name; may optionally be schema-qualified. +/// +/// See [`macros.type_overrides`][Config::type_overrides] for usages. +pub type SqlType = Box; + +/// A SQL table name; may optionally be schema-qualified. +/// +/// See [`macros.column_overrides`][Config::column_overrides] for usages. +pub type TableName = Box; + +/// A column in a SQL table. +/// +/// See [`macros.column_overrides`][Config::column_overrides] for usages. +pub type ColumnName = Box; + +/// A Rust type name or path. +/// +/// Should be a global path (not relative). +pub type RustType = Box; + +/// Internal getter methods. +impl Config { + /// Get the override for a given type name (optionally schema-qualified). + pub fn type_override(&self, type_name: &str) -> Option<&str> { + self.type_overrides.get(type_name).map(|s| &**s) + } + + /// Get the override for a given column and table name (optionally schema-qualified). + pub fn column_override(&self, table: &str, column: &str) -> Option<&str> { + self.column_overrides + .get(table) + .and_then(|by_column| by_column.get(column)) + .map(|s| &**s) + } +} diff --git a/sqlx-core/src/config/migrate.rs b/sqlx-core/src/config/migrate.rs new file mode 100644 index 00000000..5878f9a2 --- /dev/null +++ b/sqlx-core/src/config/migrate.rs @@ -0,0 +1,158 @@ +use std::collections::BTreeSet; + +/// Configuration for migrations when executed using `sqlx::migrate!()` or through `sqlx-cli`. +/// +/// ### Note +/// A manually constructed [`Migrator`][crate::migrate::Migrator] will not be aware of these +/// configuration options. We recommend using `sqlx::migrate!()` instead. +/// +/// ### Warning: Potential Data Loss or Corruption! +/// Many of these options, if changed after migrations are set up, +/// can result in data loss or corruption of a production database +/// if the proper precautions are not taken. +/// +/// Be sure you know what you are doing and that you read all relevant documentation _thoroughly_. +#[derive(Debug, Default, serde::Deserialize)] +#[serde(default)] +pub struct Config { + /// Override the name of the table used to track executed migrations. + /// + /// May be schema-qualified and/or contain quotes. Defaults to `_sqlx_migrations`. + /// + /// Potentially useful for multi-tenant databases. + /// + /// ### Warning: Potential Data Loss or Corruption! + /// Changing this option for a production database will likely result in data loss or corruption + /// as the migration machinery will no longer be aware of what migrations have been applied + /// and will attempt to re-run them. + /// + /// You should create the new table as a copy of the existing migrations table (with contents!), + /// and be sure all instances of your application have been migrated to the new + /// table before deleting the old one. + /// + /// ### Example + /// `sqlx.toml`: + /// ```toml + /// [migrate] + /// # Put `_sqlx_migrations` in schema `foo` + /// table_name = "foo._sqlx_migrations" + /// ``` + pub table_name: Option>, + + /// Override the directory used for migrations files. + /// + /// Relative to the crate root for `sqlx::migrate!()`, or the current directory for `sqlx-cli`. + pub migrations_dir: Option>, + + /// Specify characters that should be ignored when hashing migrations. + /// + /// Any characters contained in the given array will be dropped when a migration is hashed. + /// + /// ### Warning: May Change Hashes for Existing Migrations + /// Changing the characters considered in hashing migrations will likely + /// change the output of the hash. + /// + /// This may require manual rectification for deployed databases. + /// + /// ### Example: Ignore Carriage Return (`` | `\r`) + /// Line ending differences between platforms can result in migrations having non-repeatable + /// hashes. The most common culprit is the carriage return (`` | `\r`), which Windows + /// uses in its line endings alongside line feed (`` | `\n`), often written `CRLF` or `\r\n`, + /// whereas Linux and macOS use only line feeds. + /// + /// `sqlx.toml`: + /// ```toml + /// [migrate] + /// ignored_chars = ["\r"] + /// ``` + /// + /// For projects using Git, this can also be addressed using [`.gitattributes`]: + /// + /// ```text + /// # Force newlines in migrations to be line feeds on all platforms + /// migrations/*.sql text eol=lf + /// ``` + /// + /// This may require resetting or re-checking out the migrations files to take effect. + /// + /// [`.gitattributes`]: https://git-scm.com/docs/gitattributes + /// + /// ### Example: Ignore all Whitespace Characters + /// To make your migrations amenable to reformatting, you may wish to tell SQLx to ignore + /// _all_ whitespace characters in migrations. + /// + /// ##### Warning: Beware Syntatically Significant Whitespace! + /// If your migrations use string literals or quoted identifiers which contain whitespace, + /// this configuration will cause the migration machinery to ignore some changes to these. + /// This may result in a mismatch between the development and production versions of + /// your database. + /// + /// `sqlx.toml`: + /// ```toml + /// [migrate] + /// # Ignore common whitespace characters when hashing + /// ignored_chars = [" ", "\t", "\r", "\n"] # Space, tab, CR, LF + /// ``` + // Likely lower overhead for small sets than `HashSet`. + pub ignored_chars: BTreeSet, + + /// Specify the default type of migration that `sqlx migrate create` should create by default. + /// + /// ### Example: Use Reversible Migrations by Default + /// `sqlx.toml`: + /// ```toml + /// [migrate] + /// default_type = "reversible" + /// ``` + pub default_type: DefaultMigrationType, + + /// Specify the default scheme that `sqlx migrate create` should use for version integers. + /// + /// ### Example: Use Sequential Versioning by Default + /// `sqlx.toml`: + /// ```toml + /// [migrate] + /// default_versioning = "sequential" + /// ``` + pub default_versioning: DefaultVersioning, +} + +/// The default type of migration that `sqlx migrate create` should create by default. +#[derive(Debug, Default, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DefaultMigrationType { + /// Create the same migration type as that of the latest existing migration, + /// or `Simple` otherwise. + #[default] + Inferred, + + /// Create a non-reversible migration (`_.sql`). + Simple, + + /// Create a reversible migration (`_.up.sql` and `[...].down.sql`). + Reversible, +} + +/// The default scheme that `sqlx migrate create` should use for version integers. +#[derive(Debug, Default, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DefaultVersioning { + /// Infer the versioning scheme from existing migrations: + /// + /// * If the versions of the last two migrations differ by `1`, infer `Sequential`. + /// * If only one migration exists and has version `1`, infer `Sequential`. + /// * Otherwise, infer `Timestamp`. + #[default] + Inferred, + + /// Use UTC timestamps for migration versions. + /// + /// This is the recommended versioning format as it's less likely to collide when multiple + /// developers are creating migrations on different branches. + /// + /// The exact timestamp format is unspecified. + Timestamp, + + /// Use sequential integers for migration versions. + Sequential, +} diff --git a/sqlx-core/src/config/mod.rs b/sqlx-core/src/config/mod.rs new file mode 100644 index 00000000..97947724 --- /dev/null +++ b/sqlx-core/src/config/mod.rs @@ -0,0 +1,206 @@ +//! (Exported for documentation only) Guide and reference for `sqlx.toml` files. +//! +//! To use, create a `sqlx.toml` file in your crate root (the same directory as your `Cargo.toml`). +//! The configuration in a `sqlx.toml` configures SQLx *only* for the current crate. +//! +//! See the [`Config`] type and its fields for individual configuration options. +//! +//! See the [reference][`_reference`] for the full `sqlx.toml` file. + +use std::fmt::Debug; +use std::io; +use std::path::{Path, PathBuf}; + +// `std::sync::OnceLock` doesn't have a stable `.get_or_try_init()` +// because it's blocked on a stable `Try` trait. +use once_cell::sync::OnceCell; + +/// Configuration shared by multiple components. +/// +/// See [`common::Config`] for details. +pub mod common; + +/// Configuration for the `query!()` family of macros. +/// +/// See [`macros::Config`] for details. +#[cfg(feature = "config-macros")] +pub mod macros; + +/// Configuration for migrations when executed using `sqlx::migrate!()` or through `sqlx-cli`. +/// +/// See [`migrate::Config`] for details. +#[cfg(feature = "config-migrate")] +pub mod migrate; + +/// Reference for `sqlx.toml` files +/// +/// Source: `sqlx-core/src/config/reference.toml` +/// +/// ```toml +#[doc = include_str!("reference.toml")] +/// ``` +pub mod _reference {} + +#[cfg(test)] +mod tests; + +/// The parsed structure of a `sqlx.toml` file. +#[derive(Debug, Default, serde::Deserialize)] +pub struct Config { + /// Configuration shared by multiple components. + /// + /// See [`common::Config`] for details. + pub common: common::Config, + + /// Configuration for the `query!()` family of macros. + /// + /// See [`macros::Config`] for details. + #[cfg_attr( + docsrs, + doc(cfg(any(feature = "config-all", feature = "config-macros"))) + )] + #[cfg(feature = "config-macros")] + pub macros: macros::Config, + + /// Configuration for migrations when executed using `sqlx::migrate!()` or through `sqlx-cli`. + /// + /// See [`migrate::Config`] for details. + #[cfg_attr( + docsrs, + doc(cfg(any(feature = "config-all", feature = "config-migrate"))) + )] + #[cfg(feature = "config-migrate")] + pub migrate: migrate::Config, +} + +/// Error returned from various methods of [`Config`]. +#[derive(thiserror::Error, Debug)] +pub enum ConfigError { + /// The loading method expected `CARGO_MANIFEST_DIR` to be set and it wasn't. + /// + /// This is necessary to locate the root of the crate currently being compiled. + /// + /// See [the "Environment Variables" page of the Cargo Book][cargo-env] for details. + /// + /// [cargo-env]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates + #[error("environment variable `CARGO_MANIFEST_DIR` must be set and valid")] + Env( + #[from] + #[source] + std::env::VarError, + ), + + /// An I/O error occurred while attempting to read the config file at `path`. + /// + /// This includes [`io::ErrorKind::NotFound`]. + /// + /// [`Self::not_found_path()`] will return the path if the file was not found. + #[error("error reading config file {path:?}")] + Read { + path: PathBuf, + #[source] + error: io::Error, + }, + + /// An error in the TOML was encountered while parsing the config file at `path`. + /// + /// The error gives line numbers and context when printed with `Display`/`ToString`. + #[error("error parsing config file {path:?}")] + Parse { + path: PathBuf, + #[source] + error: toml::de::Error, + }, +} + +impl ConfigError { + /// If this error means the file was not found, return the path that was attempted. + pub fn not_found_path(&self) -> Option<&Path> { + match self { + ConfigError::Read { path, error } if error.kind() == io::ErrorKind::NotFound => { + Some(path) + } + _ => None, + } + } +} + +static CACHE: OnceCell = OnceCell::new(); + +/// Internal methods for loading a `Config`. +#[allow(clippy::result_large_err)] +impl Config { + /// Get the cached config, or attempt to read `$CARGO_MANIFEST_DIR/sqlx.toml`. + /// + /// On success, the config is cached in a `static` and returned by future calls. + /// + /// Returns `Config::default()` if the file does not exist. + /// + /// ### Panics + /// If the file exists but an unrecoverable error was encountered while parsing it. + pub fn from_crate() -> &'static Self { + Self::try_from_crate().unwrap_or_else(|e| { + if let Some(path) = e.not_found_path() { + // Non-fatal + tracing::debug!("Not reading config, file {path:?} not found (error: {e})"); + CACHE.get_or_init(Config::default) + } else { + // In the case of migrations, + // we can't proceed with defaults as they may be completely wrong. + panic!("failed to read sqlx config: {e}") + } + }) + } + + /// Get the cached config, or to read `$CARGO_MANIFEST_DIR/sqlx.toml`. + /// + /// On success, the config is cached in a `static` and returned by future calls. + /// + /// Errors if `CARGO_MANIFEST_DIR` is not set, or if the config file could not be read. + pub fn try_from_crate() -> Result<&'static Self, ConfigError> { + Self::try_get_with(|| { + let mut path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?); + path.push("sqlx.toml"); + Ok(path) + }) + } + + /// Get the cached config, or attempt to read `sqlx.toml` from the current working directory. + /// + /// On success, the config is cached in a `static` and returned by future calls. + /// + /// Errors if the config file does not exist, or could not be read. + pub fn try_from_current_dir() -> Result<&'static Self, ConfigError> { + Self::try_get_with(|| Ok("sqlx.toml".into())) + } + + /// Get the cached config, or attempt to read it from the path returned by the closure. + /// + /// On success, the config is cached in a `static` and returned by future calls. + /// + /// Errors if the config file does not exist, or could not be read. + pub fn try_get_with( + make_path: impl FnOnce() -> Result, + ) -> Result<&'static Self, ConfigError> { + CACHE.get_or_try_init(|| { + let path = make_path()?; + Self::read_from(path) + }) + } + + fn read_from(path: PathBuf) -> Result { + // The `toml` crate doesn't provide an incremental reader. + let toml_s = match std::fs::read_to_string(&path) { + Ok(toml) => toml, + Err(error) => { + return Err(ConfigError::Read { path, error }); + } + }; + + // TODO: parse and lint TOML structure before deserializing + // Motivation: https://github.com/toml-rs/toml/issues/761 + tracing::debug!("read config TOML from {path:?}:\n{toml_s}"); + + toml::from_str(&toml_s).map_err(|error| ConfigError::Parse { path, error }) + } +} diff --git a/sqlx-core/src/config/reference.toml b/sqlx-core/src/config/reference.toml new file mode 100644 index 00000000..fae92f34 --- /dev/null +++ b/sqlx-core/src/config/reference.toml @@ -0,0 +1,175 @@ +# `sqlx.toml` reference. +# +# Note: shown values are *not* defaults. +# They are explicitly set to non-default values to test parsing. +# Refer to the comment for a given option for its default value. + +############################################################################################### + +# Configuration shared by multiple components. +[common] +# Change the environment variable to get the database URL. +# +# This is used by both the macros and `sqlx-cli`. +# +# If not specified, defaults to `DATABASE_URL` +database_url_var = "FOO_DATABASE_URL" + +############################################################################################### + +# Configuration for the `query!()` family of macros. +[macros] +# Force the macros to use the `chrono` crate for date/time types, even if `time` is enabled. +# +# Defaults to "inferred": use whichever crate is enabled (`time` takes precedence over `chrono`). +datetime_crate = "chrono" + +# Or, ensure the macros always prefer `time` +# in case new date/time crates are added in the future: +# datetime_crate = "time" + +# Set global overrides for mapping SQL types to Rust types. +# +# Default type mappings are defined by the database driver. +# Refer to the `sqlx::types` module for details. +# +# Postgres users: schema qualification should not be used for types in the search path. +# +# ### Note: Orthogonal to Nullability +# These overrides do not affect whether `query!()` decides to wrap a column in `Option<_>` +# or not. They only override the inner type used. +[macros.type_overrides] +# Override a built-in type (map all `UUID` columns to `crate::types::MyUuid`) +'uuid' = "crate::types::MyUuid" + +# Support an external or custom wrapper type (e.g. from the `isn` Postgres extension) +# (NOTE: FOR DOCUMENTATION PURPOSES ONLY; THIS CRATE/TYPE DOES NOT EXIST AS OF WRITING) +'isbn13' = "isn_rs::isbn::ISBN13" + +# SQL type `foo` to Rust type `crate::types::Foo`: +'foo' = "crate::types::Foo" + +# SQL type `"Bar"` to Rust type `crate::types::Bar`; notice the extra pair of quotes: +'"Bar"' = "crate::types::Bar" + +# Will NOT work (the first pair of quotes are parsed by TOML) +# "Bar" = "crate::types::Bar" + +# Schema qualified +'foo.bar' = "crate::types::Bar" + +# Schema qualified and quoted +'foo."Bar"' = "crate::schema::foo::Bar" + +# Quoted schema name +'"Foo".bar' = "crate::schema::foo::Bar" + +# Quoted schema and type name +'"Foo"."Bar"' = "crate::schema::foo::Bar" + +# Set per-column overrides for mapping SQL types to Rust types. +# +# Note: table name is required in the header. +# +# Postgres users: schema qualification should not be used for types in the search path. +# +# ### Note: Orthogonal to Nullability +# These overrides do not affect whether `query!()` decides to wrap a column in `Option<_>` +# or not. They only override the inner type used. +[macros.column_overrides.'foo'] +# Map column `bar` of table `foo` to Rust type `crate::types::Foo`: +'bar' = "crate::types::Bar" + +# Quoted column name +# Note: same quoting requirements as `macros.type_overrides` +'"Bar"' = "crate::types::Bar" + +# Note: will NOT work (parses as `Bar`) +# "Bar" = "crate::types::Bar" + +# Table name may be quoted (note the wrapping single-quotes) +[macros.column_overrides.'"Foo"'] +'bar' = "crate::types::Bar" +'"Bar"' = "crate::types::Bar" + +# Table name may also be schema-qualified. +# Note how the dot is inside the quotes. +[macros.column_overrides.'my_schema.my_table'] +'my_column' = "crate::types::MyType" + +# Quoted schema, table, and column names +[macros.column_overrides.'"My Schema"."My Table"'] +'"My Column"' = "crate::types::MyType" + +############################################################################################### + +# Configuration for migrations when executed using `sqlx::migrate!()` or through `sqlx-cli`. +# +# ### Note +# A manually constructed [`Migrator`][crate::migrate::Migrator] will not be aware of these +# configuration options. We recommend using `sqlx::migrate!()` instead. +# +# ### Warning: Potential Data Loss or Corruption! +# Many of these options, if changed after migrations are set up, +# can result in data loss or corruption of a production database +# if the proper precautions are not taken. +# +# Be sure you know what you are doing and that you read all relevant documentation _thoroughly_. +[migrate] +# Override the name of the table used to track executed migrations. +# +# May be schema-qualified and/or contain quotes. Defaults to `_sqlx_migrations`. +# +# Potentially useful for multi-tenant databases. +# +# ### Warning: Potential Data Loss or Corruption! +# Changing this option for a production database will likely result in data loss or corruption +# as the migration machinery will no longer be aware of what migrations have been applied +# and will attempt to re-run them. +# +# You should create the new table as a copy of the existing migrations table (with contents!), +# and be sure all instances of your application have been migrated to the new +# table before deleting the old one. +table_name = "foo._sqlx_migrations" + +# Override the directory used for migrations files. +# +# Relative to the crate root for `sqlx::migrate!()`, or the current directory for `sqlx-cli`. +migrations_dir = "foo/migrations" + +# Specify characters that should be ignored when hashing migrations. +# +# Any characters contained in the given set will be dropped when a migration is hashed. +# +# Defaults to an empty array (don't drop any characters). +# +# ### Warning: May Change Hashes for Existing Migrations +# Changing the characters considered in hashing migrations will likely +# change the output of the hash. +# +# This may require manual rectification for deployed databases. +# ignored_chars = [] + +# Ignore Carriage Returns (`` | `\r`) +# Note that the TOML format requires double-quoted strings to process escapes. +# ignored_chars = ["\r"] + +# Ignore common whitespace characters (beware syntatically significant whitespace!) +ignored_chars = [" ", "\t", "\r", "\n"] # Space, tab, CR, LF + +# Specify reversible migrations by default (for `sqlx migrate create`). +# +# Defaults to "inferred": uses the type of the last migration, or "simple" otherwise. +default_type = "reversible" + +# Specify simple (non-reversible) migrations by default. +# default_type = "simple" + +# Specify sequential versioning by default (for `sqlx migrate create`). +# +# Defaults to "inferred": guesses the versioning scheme from the latest migrations, +# or "timestamp" otherwise. +default_versioning = "sequential" + +# Specify timestamp versioning by default. +# default_versioning = "timestamp" diff --git a/sqlx-core/src/config/tests.rs b/sqlx-core/src/config/tests.rs new file mode 100644 index 00000000..bf042069 --- /dev/null +++ b/sqlx-core/src/config/tests.rs @@ -0,0 +1,90 @@ +use crate::config::{self, Config}; +use std::collections::BTreeSet; + +#[test] +fn reference_parses_as_config() { + let config: Config = toml::from_str(include_str!("reference.toml")) + // The `Display` impl of `toml::Error` is *actually* more useful than `Debug` + .unwrap_or_else(|e| panic!("expected reference.toml to parse as Config: {e}")); + + assert_common_config(&config.common); + + #[cfg(feature = "config-macros")] + assert_macros_config(&config.macros); + + #[cfg(feature = "config-migrate")] + assert_migrate_config(&config.migrate); +} + +fn assert_common_config(config: &config::common::Config) { + assert_eq!(config.database_url_var.as_deref(), Some("FOO_DATABASE_URL")); +} + +#[cfg(feature = "config-macros")] +fn assert_macros_config(config: &config::macros::Config) { + use config::macros::*; + + assert_eq!(config.datetime_crate, DateTimeCrate::Chrono); + + // Type overrides + // Don't need to cover everything, just some important canaries. + assert_eq!(config.type_override("foo"), Some("crate::types::Foo")); + + assert_eq!(config.type_override(r#""Bar""#), Some("crate::types::Bar"),); + + assert_eq!( + config.type_override(r#""Foo".bar"#), + Some("crate::schema::foo::Bar"), + ); + + assert_eq!( + config.type_override(r#""Foo"."Bar""#), + Some("crate::schema::foo::Bar"), + ); + + // Column overrides + assert_eq!( + config.column_override("foo", "bar"), + Some("crate::types::Bar"), + ); + + assert_eq!( + config.column_override("foo", r#""Bar""#), + Some("crate::types::Bar"), + ); + + assert_eq!( + config.column_override(r#""Foo""#, "bar"), + Some("crate::types::Bar"), + ); + + assert_eq!( + config.column_override(r#""Foo""#, r#""Bar""#), + Some("crate::types::Bar"), + ); + + assert_eq!( + config.column_override("my_schema.my_table", "my_column"), + Some("crate::types::MyType"), + ); + + assert_eq!( + config.column_override(r#""My Schema"."My Table""#, r#""My Column""#), + Some("crate::types::MyType"), + ); +} + +#[cfg(feature = "config-migrate")] +fn assert_migrate_config(config: &config::migrate::Config) { + use config::migrate::*; + + assert_eq!(config.table_name.as_deref(), Some("foo._sqlx_migrations")); + assert_eq!(config.migrations_dir.as_deref(), Some("foo/migrations")); + + let ignored_chars = BTreeSet::from([' ', '\t', '\r', '\n']); + + assert_eq!(config.ignored_chars, ignored_chars); + + assert_eq!(config.default_type, DefaultMigrationType::Reversible); + assert_eq!(config.default_versioning, DefaultVersioning::Sequential); +} diff --git a/sqlx-core/src/lib.rs b/sqlx-core/src/lib.rs index df4b2cc2..8b831eca 100644 --- a/sqlx-core/src/lib.rs +++ b/sqlx-core/src/lib.rs @@ -91,6 +91,9 @@ pub mod any; #[cfg(feature = "migrate")] pub mod testing; +#[cfg(feature = "config")] +pub mod config; + pub use error::{Error, Result}; pub use either::Either; diff --git a/sqlx-macros-core/Cargo.toml b/sqlx-macros-core/Cargo.toml index 46786b7d..46b7dfbf 100644 --- a/sqlx-macros-core/Cargo.toml +++ b/sqlx-macros-core/Cargo.toml @@ -26,6 +26,10 @@ derive = [] macros = [] migrate = ["sqlx-core/migrate"] +config = ["sqlx-core/config"] +config-macros = ["config", "sqlx-core/config-macros"] +config-migrate = ["config", "sqlx-core/config-migrate"] + # database mysql = ["sqlx-mysql"] postgres = ["sqlx-postgres"] diff --git a/sqlx-macros/Cargo.toml b/sqlx-macros/Cargo.toml index 5617d3f2..1d1b0bcd 100644 --- a/sqlx-macros/Cargo.toml +++ b/sqlx-macros/Cargo.toml @@ -27,6 +27,9 @@ derive = ["sqlx-macros-core/derive"] macros = ["sqlx-macros-core/macros"] migrate = ["sqlx-macros-core/migrate"] +config-macros = ["sqlx-macros-core/config-macros"] +config-migrate = ["sqlx-macros-core/config-migrate"] + # database mysql = ["sqlx-macros-core/mysql"] postgres = ["sqlx-macros-core/postgres"] diff --git a/src/lib.rs b/src/lib.rs index 870fa703..19142f66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,3 +164,6 @@ pub mod prelude { pub use super::Statement; pub use super::Type; } + +#[cfg(feature = "_unstable-doc")] +pub use sqlx_core::config;