diff --git a/Cargo.toml b/Cargo.toml index 886451a73a..17c7772110 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ 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] @@ -58,9 +58,9 @@ 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-core/config-macros"] -config-migrate = ["sqlx-core/config-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 @@ -76,6 +76,8 @@ _unstable-all-types = [ "uuid", "bit-vec", ] +# Render documentation that wouldn't otherwise be shown (e.g. `sqlx_core::config`). +_unstable-doc = ["config-all"] # 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 286f2aee7e..1ef5f6a3d3 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 = [] @@ -28,9 +28,9 @@ _tls-none = [] # support offline/decoupled building (enables serialization of `Describe`) offline = ["serde", "either/serde"] -config-toml = ["serde", "toml/parse"] -config-macros = ["config-toml"] -config-migrate = ["config-toml"] +config = ["serde", "toml/parse"] +config-macros = ["config"] +config-migrate = ["config"] [dependencies] # Runtimes diff --git a/sqlx-core/src/config/macros.rs b/sqlx-core/src/config/macros.rs index dadbfcf0a4..1283ba56a6 100644 --- a/sqlx-core/src/config/macros.rs +++ b/sqlx-core/src/config/macros.rs @@ -1,6 +1,399 @@ +use std::borrow::Borrow; +use std::cmp::Ordering; +use std::collections::BTreeMap; +use std::fmt::Formatter; +use serde::{Deserialize, Deserializer}; +use serde::de::{DeserializeSeed, Error, MapAccess, Visitor}; + /// Configuration for the [`sqlx::query!()`] family of macros. -#[derive(Debug, serde::Deserialize)] +#[derive(Debug, Default, serde::Deserialize)] +#[serde(default)] pub struct Config { - /// Override the environment variable + /// Override the database URL environment variable used by the macros. + /// + /// 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, -} \ No newline at end of file + + /// 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. + /// + /// ### Example: Custom Wrapper Types + /// Does SQLx not support a type that you need? 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, for 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 + /// } + /// ``` + /// + /// ##### `sqlx.toml` + /// ```toml + /// [macros.type_overrides] + /// foo = "crate::types::Foo" + /// ``` + /// + /// #### Note: Schema Qualification + /// Types may be schema-qualified without quotation marks: + /// + /// ```toml + /// [macros.type_overrides] + /// foo.foo = "crate::types::Foo" + /// ``` + /// + /// Multiple types in the same schema can be listed in a common table: + /// ```toml + /// [macros.type_overrides.foo] + /// # foo.foo + /// foo = "crate::types::Foo" + /// # foo.bar + /// bar = "crate::types::Bar" + /// ``` + /// + /// Schema qualification is not necessary for types in the search path. + /// + /// #### Note: 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."foo"` in SQL + /// foo.'"foo"' = "crate::types::Foo" + /// # *NOT* `foo."foo"` in SQL (parses as `foo.foo`) + /// foo."foo" = "crate::types::Foo" + /// ``` + /// + /// This is because the `toml` crate doesn't (as of writing) provide any way to get the + /// raw representation of the key when deserializing through `serde`; + /// `foo.foo` and `foo."foo"` parse to the same key. + /// + /// See for the proposed solution. + // FIXME(abonander, 2024/07/26): my gut tells me this is going to surprise people + pub type_overrides: TypeOverrides, + + /// Specify per-column overrides for mapping SQL types to Rust types. + /// + /// The supported syntax is identical to [`type_overrides`][Self::type_overrides], + /// (with the same caveat for quoted names!) but column names must _at minimum_ be qualified + /// by their table name, and can additionally be qualified by schema. + /// + /// ### Example + /// + /// ##### `sqlx.toml` + /// ```toml + /// [macros.column_overrides] + /// # Map column `bar` of table `foo` to Rust type `crate::types::Foo`: + /// foo.bar = "crate::types::Bar" + /// + /// # Quoted column name + /// # Note: same quoting requirements as `macros.type_overrides` + /// foo.'"Bar"' = "crate::types::Bar" + /// + /// # Schema-qualified + /// # Map column `baz` of table `foo.bar` to Rust type `crate::types::Baz`: + /// foo.bar.baz = "crate::types::Baz" + /// + /// # Schema, table and/or column names may be quoted + /// # Don't forget the wrapping single quotes! + /// '"Foo"'.'"Bar"'.'"Baz"' = "crate::types::Baz" + /// + /// # Multiple columns in the same table (`bar`) + /// [macros.column_overrides.bar] + /// # bar.foo + /// foo = "crate::schema::bar::Foo" + /// # bar."Bar" + /// '"Bar"' = "crate::schema::bar::Bar" + /// + /// # May also be schema-qualified + /// [macros.column_overrides.my_schema.my_table] + /// my_column = "crate::types::MyType" + /// + /// # And quoted + /// [macros.column_overrides.'"My Schema"'.'"My Table"'] + /// '"My Column"' = "crate::types::MyType" + /// ``` + pub column_overrides: TypeOverrides, +} + +/// 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` takes precedent). + #[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 map of SQL type names or column names to type overrides. +/// +/// If `require_qualified` is `true`, paths are required to have at least one level of qualification +/// (e.g. table names for [`column_overrides`][Config::column_overrides]). +#[derive(Debug, Default)] +pub struct TypeOverrides(pub BTreeMap>); + +#[doc(hidden)] +impl TypeOverrides { + pub fn get(&self, path: &str) -> Option<&str> { + self.0.get(path).map(|s| &**s) + } +} + +/// A type or column path in SQL. +#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)] +pub struct Path(pub Box); + +impl Borrow for Path { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl PartialOrd for Path { + fn partial_cmp(&self, other: &str) -> Option { + str::partial_cmp(&self.0, other) + } +} + +impl PartialEq for Path { + fn eq(&self, other: &str) -> bool { + *self.0 == *other + } +} + +impl<'de, const REQUIRE_QUALIFIED: bool> Deserialize<'de> for TypeOverrides { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de> + { + let mut path = String::new(); + let mut table = TypeOverrides(BTreeMap::new()); + + deserializer.deserialize_map(MapVisitor { + path: &mut path, + path_len: 0, + table: &mut table, + })?; + + Ok(table) + } +} + +struct MapVisitor<'a, const REQUIRE_QUALIFIED: bool> { + path: &'a mut String, + path_len: usize, + table: &'a mut TypeOverrides, +} + +struct PathVisitor<'a>(&'a mut String); + +impl<'map, const REQUIRE_QUALIFIED: bool> Drop for MapVisitor<'map, REQUIRE_QUALIFIED> { + fn drop(&mut self) { + // Always truncate in case of weird control flow (error recovery?) + self.path.truncate(self.path_len); + } +} + +impl<'map, const REQUIRE_QUALIFIED: bool> MapVisitor<'map, REQUIRE_QUALIFIED> { + fn finish_entry(self, value: impl Into>) -> Result<(), E> { + // This will give a false positive for quoted names containing a dot, + // but if someone's using quoted names they should know what they are doing. + if REQUIRE_QUALIFIED && !self.path.contains('.') { + return Err(E::custom(format_args!("path incomplete: {:?}, expected table name", self.path))); + } + + self.table.0.insert(Path(self.path[..].into()), value.into()); + + Ok(()) + } +} + +impl<'de, 'map, const REQUIRE_QUALIFIED: bool> Visitor<'de> for MapVisitor<'map, REQUIRE_QUALIFIED> { + type Value = (); + + fn expecting(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.path.is_empty() { + write!(f, "expected map") + } else { + write!(f, "expected string or map at column path {:?}", self.path) + } + } + + fn visit_str(self, v: &str) -> Result + where + E: Error + { + self.finish_entry(v) + } + + fn visit_string(self, v: String) -> Result + where + E: Error + { + self.finish_entry(v) + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de> + { + let path_len = self.path.len(); + + // `PathVisitor` will push to `self.path` without allocating + loop { + if map.next_key_seed(PathVisitor(self.path))?.is_none() { + break; + } + + // Note: `Self` is `MapVisitor<'map>`, causes unnecessary lifetime extension here + map.next_value_seed(MapVisitor { + path_len, + // FRU syntax doesn't reborrow + // ..self + path: self.path, + table: self.table, + })?; + + // `path` is truncated on-drop of the inner `MapVisitor` + } + + Ok(()) + } +} + +impl<'de, 'a, const REQUIRE_QUALIFIED: bool> DeserializeSeed<'de> for MapVisitor<'a, REQUIRE_QUALIFIED> { + type Value = (); + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de> + { + deserializer.deserialize_any(self) + } +} + +impl<'de, 'a> Visitor<'de> for PathVisitor<'a> { + type Value = (); + + fn expecting(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "expected string") + } + + fn visit_str(self, key: &str) -> Result + where + E: Error + { + use std::fmt::Write; + + if !self.0.is_empty() { + write!(self.0, ".{key}").ok(); + } else { + self.0.push_str(key); + } + + Ok(()) + } +} + +impl<'de, 'a> DeserializeSeed<'de> for PathVisitor<'a> { + type Value = (); + + fn deserialize(self, deserializer: D) -> Result + where + D: Deserializer<'de> + { + deserializer.deserialize_str(self) + } +} diff --git a/sqlx-core/src/config/migrate.rs b/sqlx-core/src/config/migrate.rs index 234eb24127..f9a6b9d760 100644 --- a/sqlx-core/src/config/migrate.rs +++ b/sqlx-core/src/config/migrate.rs @@ -2,15 +2,16 @@ /// /// ### Note /// A manually constructed [`Migrator`][crate::migrate::Migrator] will not be aware of these -/// configuration options. We recommend using [`sqlx::migrate!()`] instead. +/// 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 to read all relevant documentation _thoroughly_. -#[derive(Debug, serde::Deserialize)] +/// 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. /// @@ -33,12 +34,23 @@ pub struct Config { /// [migrate] /// table_name = "foo._sqlx_migrations" /// ``` - pub table_name: Option, + 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 string 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 @@ -76,9 +88,8 @@ pub struct Config { /// ```toml /// [migrate] /// # Ignore common whitespace characters when hashing - /// ignored_chars = " \t\r\n" + /// ignored_chars = " \t\r\n" # Space, tab, CR, LF /// ``` - #[serde(default)] pub ignored_chars: Box, /// Specify the default type of migration that `sqlx migrate create` should create by default. diff --git a/sqlx-core/src/config/mod.rs b/sqlx-core/src/config/mod.rs index f75be068ea..4763be71b7 100644 --- a/sqlx-core/src/config/mod.rs +++ b/sqlx-core/src/config/mod.rs @@ -1,32 +1,63 @@ -//! Support for reading and caching configuration data from `sqlx.toml` +//! (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::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 for the [`sqlx::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, serde::Deserialize)] +#[derive(Debug, Default, serde::Deserialize)] pub struct Config { /// Configuration for the [`sqlx::query!()`] family of macros. - /// - /// See type documentation for details. + /// + /// 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 type documentation for details. + /// 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, } +#[doc(hidden)] #[derive(thiserror::Error, Debug)] pub enum ConfigError { #[error("CARGO_MANIFEST_DIR must be set and valid")] @@ -35,7 +66,7 @@ pub enum ConfigError { #[error("error reading config file {path:?}")] Read { path: PathBuf, - #[source] error: std::io::Error, + #[source] error: io::Error, }, #[error("error parsing config file {path:?}")] @@ -45,11 +76,30 @@ pub enum ConfigError { } } +static CACHE: OnceCell = OnceCell::new(); + #[doc(hidden)] #[allow(clippy::result_large_err)] impl Config { + pub fn get() -> &'static Self { - Self::try_get().unwrap() + Self::try_get() + .unwrap_or_else(|e| { + match e { + ConfigError::Read { path, error } + if error.kind() == io::ErrorKind::NotFound => + { + // Non-fatal + tracing::debug!("Not reading config, file {path:?} not found"); + CACHE.get_or_init(|| Config::default()) + } + e => { + // In the case of migrations, + // we can't proceed with defaults as they may be completely wrong. + panic!("failed to read sqlx config: {e}") + } + } + }) } pub fn try_get() -> Result<&'static Self, ConfigError> { @@ -61,12 +111,6 @@ impl Config { } pub fn try_get_with(make_path: impl FnOnce() -> Result) -> Result<&'static Self, ConfigError> { - // `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; - - static CACHE: OnceCell = OnceCell::new(); - CACHE.get_or_try_init(|| { let path = make_path()?; Self::read_from(path) diff --git a/sqlx-core/src/config/reference.toml b/sqlx-core/src/config/reference.toml new file mode 100644 index 0000000000..8ce54c1670 --- /dev/null +++ b/sqlx-core/src/config/reference.toml @@ -0,0 +1,150 @@ +# Configuration for the `query!()` family of macros. +[macros] +# Change the environment variable to get the database URL for macro invocations in the current crate. +# If not specified, defaults to `DATABASE_URL` +database_url_var = "FOO_DATABASE_URL" + +# Force the macros to use the `chrono` crate for date/time types, even if `time` is enabled. +datetime_crate = "chrono" + +# Or, ensure the macros always prefer `time` (in case new time crates are added in the future) +# datetime_crate = "time" + +# Set global overrides for mapping SQL types to Rust types +[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 +foobar.foo = "crate::types::Foo" + +# Schema qualified and quoted +foobar.'"Bar"' = "crate::types::Bar" + +# Multiple types in the same schema (`bar`) +[macros.type_overrides.bar] +# bar.foo +foo = "crate::schema::bar::Foo" +# bar."Bar" +'"Bar"' = "crate::schema::bar::Bar" + +# Quoted schema name +[macros.type_overrides.'"Foo"'] +# foo.bar +bar = "crate::schema::foo::Bar" +# foo."Bar" +'"Bar"' = "crate::schema::foo::Bar" + +# Set per-column overrides for mapping SQL types to Rust types +# +# Note: table name is required. +[macros.column_overrides] +# Map column `bar` of table `foo` to Rust type `crate::types::Foo`: +foo.bar = "crate::types::Bar" + +# Quoted column name +# Note: same quoting requirements as `macros.type_overrides` +foo.'"Bar"' = "crate::types::Bar" + +# Schema-qualified +# Map column `bar` of table `foobar.foo` to Rust type `crate::types::Baz`: +foobar.foo.bar = "crate::types::Bar" + +# schema, table and/or column names may be quoted +# Don't forget the wrapping single quotes! +'"Foo"'.'"Bar"'.'"Baz"' = "crate::types::Baz" + +# Multiple columns in the same table (`bar`) +[macros.column_overrides.bar] +# bar.foo +foo = "crate::schema::bar::Foo" +# bar."Bar" +'"Bar"' = "crate::schema::bar::Bar" + +# May also be schema-qualified +[macros.column_overrides.my_schema.my_table] +my_column = "crate::types::MyType" + +# And quoted +[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. 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 string 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. + +# Ignore Carriage Returns (`` | `\r`) +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" \ No newline at end of file diff --git a/sqlx-core/src/config/tests.rs b/sqlx-core/src/config/tests.rs new file mode 100644 index 0000000000..969d9a09df --- /dev/null +++ b/sqlx-core/src/config/tests.rs @@ -0,0 +1,81 @@ +use crate::config::{self, Config}; + +#[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}")); + + #[cfg(feature = "config-macros")] + assert_macros_config(&config.macros); + + #[cfg(feature = "config-migrate")] + assert_migrate_config(&config.migrate); +} + +#[cfg(feature = "config-macros")] +fn assert_macros_config(config: &config::macros::Config) { + use config::macros::*; + + assert_eq!(config.database_url_var.as_deref(), Some("FOO_DATABASE_URL")); + + assert_eq!(config.datetime_crate, DateTimeCrate::Chrono); + + // Type overrides + // Don't need to cover everything, just some important canaries. + assert_eq!(config.type_overrides.get("foo"), Some("crate::types::Foo"),); + + assert_eq!( + config.type_overrides.get(r#""Bar""#), + Some("crate::types::Bar"), + ); + + assert_eq!( + config.type_overrides.get(r#""Foo".bar"#), + Some("crate::schema::foo::Bar"), + ); + + // Column overrides + assert_eq!( + config.column_overrides.get("foo.bar"), + Some("crate::types::Bar"), + ); + + assert_eq!( + config.column_overrides.get(r#"foobar.foo.bar"#), + Some("crate::types::Bar"), + ); + + assert_eq!( + config.column_overrides.get(r#""Foo"."Bar"."Baz""#), + Some("crate::types::Baz") + ); + + assert_eq!( + config.column_overrides.get(r#"bar."Bar""#), + Some("crate::schema::bar::Bar"), + ); + + assert_eq!( + config.column_overrides.get("my_schema.my_table.my_column"), + Some("crate::types::MyType"), + ); + + assert_eq!( + config.column_overrides.get(r#""My Schema"."My Table"."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")); + + assert_eq!(&*config.ignored_chars, "\r"); + + 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 029a631007..7b4c270d59 100644 --- a/sqlx-core/src/lib.rs +++ b/sqlx-core/src/lib.rs @@ -93,7 +93,7 @@ pub mod any; #[cfg(feature = "migrate")] pub mod testing; -#[cfg(feature = "config-toml")] +#[cfg(feature = "config")] pub mod config; pub use error::{Error, Result}; diff --git a/sqlx-macros-core/Cargo.toml b/sqlx-macros-core/Cargo.toml index fec34b6528..5033851947 100644 --- a/sqlx-macros-core/Cargo.toml +++ b/sqlx-macros-core/Cargo.toml @@ -22,6 +22,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 cb4d1b91c3..dd0fa138c0 100644 --- a/sqlx-macros/Cargo.toml +++ b/sqlx-macros/Cargo.toml @@ -25,6 +25,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 2a393387fa..e2fd0b1567 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,8 +159,5 @@ pub mod prelude { pub use super::Type; } -/// (Documentation only) Guide and reference for `sqlx.toml` files. -#[cfg(doc)] -pub mod config { - pub use sqlx_core::config::*; -} +#[cfg(feature = "_unstable-doc")] +pub use sqlx_core::config;