feat: create sqlx.toml format (#3383)

* feat: create `sqlx.toml` format

* feat: add support for ignored_chars config to sqlx_core::migrate

* chore: test ignored_chars with `U+FEFF` (ZWNBSP/BOM)

https://en.wikipedia.org/wiki/Byte_order_mark

* refactor: make `Config` always compiled

simplifies usage while still making parsing optional for less generated code

* refactor: add origin information to `Column`

* feat(macros): implement `type_override` and `column_override` from `sqlx.toml`

* refactor(sqlx.toml): make all keys kebab-case, create `macros.preferred-crates`

* feat: make macros aware of `macros.preferred-crates`

* feat: make `sqlx-cli` aware of `database-url-var`

* feat: teach macros about `migrate.table-name`, `migrations-dir`

* feat: teach macros about `migrate.ignored-chars`

* chore: delete unused source file `sqlx-cli/src/migration.rs`

* feat: teach `sqlx-cli` about `migrate.defaults`

* feat: teach `sqlx-cli` about `migrate.migrations-dir`

* feat: teach `sqlx-cli` about `migrate.table-name`

* feat: introduce `migrate.create-schemas`

* WIP feat: create multi-tenant database example

* fix(postgres): don't fetch `ColumnOrigin` for transparently-prepared statements

* feat: progress on axum-multi-tenant example

* feat(config): better errors for mislabeled fields

* WIP feat: filling out axum-multi-tenant example

* feat: multi-tenant example

No longer Axum-based because filling out the request routes would have distracted from the purpose of the example.

* chore(ci): test multi-tenant example

* fixup after merge

* fix(ci): enable `sqlx-toml` in CLI build for examples

* fix: CI, README for `multi-tenant`

* fix: clippy warnings

* fix: multi-tenant README

* fix: sequential versioning inference for migrations

* fix: migration versioning with explicit overrides

* fix: only warn on ambiguous crates if the invocation relies on it

* fix: remove unused imports

* fix: doctest

* fix: `sqlx mig add` behavior and tests

* fix: restore original type-checking order

* fix: deprecation warning in `tests/postgres/macros.rs`

* feat: create postgres/multi-database example

* fix: examples/postgres/multi-database

* fix: cargo fmt

* chore: add tests for config `migrate.defaults`

* fix: sqlx-cli/tests/add.rs

* feat(cli): add `--config` override to all relevant commands

* chore: run `sqlx mig add` test with `RUST_BACKTRACE=1`

* fix: properly canonicalize config path for `sqlx mig add` test

* fix: get `sqlx mig add` test passing

* fix(cli): test `migrate.ignored-chars`, fix bugs

* feat: create `macros.preferred-crates` example

* fix(examples): use workspace `sqlx`

* fix: examples

* fix(sqlite): unexpected feature flags in `type_checking.rs`

* fix: run `cargo fmt`

* fix: more example fixes

* fix(ci): preferred-crates setup

* fix(examples): enable default-features for workspace `sqlx`

* fix(examples): issues in `preferred-crates`

* chore: adjust error message for missing param type in `query!()`

* doc: mention new `sqlx.toml` configuration

* chore: add `CHANGELOG` entry

Normally I generate these when cutting the release, but I wanted to take time to editorialize this one.

* doc: fix new example titles

* refactor: make `sqlx-toml` feature non-default, improve errors

* refactor: eliminate panics in `Config` read path

* chore: remove unused `axum` dependency from new examples

* fix(config): restore fallback to default config for macros

* chore(config): remove use of `once_cell` (to match `main`)
This commit is contained in:
Austin Bonander
2025-06-30 16:34:46 -07:00
committed by GitHub
parent 764ae2f702
commit 25cbeedab4
127 changed files with 6443 additions and 1138 deletions

View File

@@ -10,6 +10,9 @@ pub struct MySqlColumn {
pub(crate) name: UStr,
pub(crate) type_info: MySqlTypeInfo,
#[cfg_attr(feature = "offline", serde(default))]
pub(crate) origin: ColumnOrigin,
#[cfg_attr(feature = "offline", serde(skip))]
pub(crate) flags: Option<ColumnFlags>,
}
@@ -28,4 +31,8 @@ impl Column for MySqlColumn {
fn type_info(&self) -> &MySqlTypeInfo {
&self.type_info
}
fn origin(&self) -> ColumnOrigin {
self.origin.clone()
}
}

View File

@@ -22,6 +22,7 @@ use futures_core::future::BoxFuture;
use futures_core::stream::BoxStream;
use futures_core::Stream;
use futures_util::TryStreamExt;
use sqlx_core::column::{ColumnOrigin, TableColumn};
use std::{borrow::Cow, pin::pin, sync::Arc};
impl MySqlConnection {
@@ -385,11 +386,30 @@ async fn recv_result_columns(
fn recv_next_result_column(def: &ColumnDefinition, ordinal: usize) -> Result<MySqlColumn, Error> {
// if the alias is empty, use the alias
// only then use the name
let column_name = def.name()?;
let name = match (def.name()?, def.alias()?) {
(_, alias) if !alias.is_empty() => UStr::new(alias),
(name, _) => UStr::new(name),
};
let table = def.table()?;
let origin = if table.is_empty() {
ColumnOrigin::Expression
} else {
let schema = def.schema()?;
ColumnOrigin::Table(TableColumn {
table: if !schema.is_empty() {
format!("{schema}.{table}").into()
} else {
table.into()
},
name: column_name.into(),
})
};
let type_info = MySqlTypeInfo::from_column(def);
Ok(MySqlColumn {
@@ -397,6 +417,7 @@ fn recv_next_result_column(def: &ColumnDefinition, ordinal: usize) -> Result<MyS
type_info,
ordinal,
flags: Some(def.flags),
origin,
})
}

View File

@@ -2,9 +2,6 @@ use std::str::FromStr;
use std::time::Duration;
use std::time::Instant;
use futures_core::future::BoxFuture;
pub(crate) use sqlx_core::migrate::*;
use crate::connection::{ConnectOptions, Connection};
use crate::error::Error;
use crate::executor::Executor;
@@ -12,6 +9,8 @@ use crate::query::query;
use crate::query_as::query_as;
use crate::query_scalar::query_scalar;
use crate::{MySql, MySqlConnectOptions, MySqlConnection};
use futures_core::future::BoxFuture;
pub(crate) use sqlx_core::migrate::*;
fn parse_for_maintenance(url: &str) -> Result<(MySqlConnectOptions, String), Error> {
let mut options = MySqlConnectOptions::from_str(url)?;
@@ -75,12 +74,28 @@ impl MigrateDatabase for MySql {
}
impl Migrate for MySqlConnection {
fn ensure_migrations_table(&mut self) -> BoxFuture<'_, Result<(), MigrateError>> {
fn create_schema_if_not_exists<'e>(
&'e mut self,
schema_name: &'e str,
) -> BoxFuture<'e, Result<(), MigrateError>> {
Box::pin(async move {
// language=SQL
self.execute(&*format!(r#"CREATE SCHEMA IF NOT EXISTS {schema_name};"#))
.await?;
Ok(())
})
}
fn ensure_migrations_table<'e>(
&'e mut self,
table_name: &'e str,
) -> BoxFuture<'e, Result<(), MigrateError>> {
Box::pin(async move {
// language=MySQL
self.execute(
self.execute(&*format!(
r#"
CREATE TABLE IF NOT EXISTS _sqlx_migrations (
CREATE TABLE IF NOT EXISTS {table_name} (
version BIGINT PRIMARY KEY,
description TEXT NOT NULL,
installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -88,20 +103,23 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations (
checksum BLOB NOT NULL,
execution_time BIGINT NOT NULL
);
"#,
)
"#
))
.await?;
Ok(())
})
}
fn dirty_version(&mut self) -> BoxFuture<'_, Result<Option<i64>, MigrateError>> {
fn dirty_version<'e>(
&'e mut self,
table_name: &'e str,
) -> BoxFuture<'e, Result<Option<i64>, MigrateError>> {
Box::pin(async move {
// language=SQL
let row: Option<(i64,)> = query_as(
"SELECT version FROM _sqlx_migrations WHERE success = false ORDER BY version LIMIT 1",
)
let row: Option<(i64,)> = query_as(&format!(
"SELECT version FROM {table_name} WHERE success = false ORDER BY version LIMIT 1"
))
.fetch_optional(self)
.await?;
@@ -109,15 +127,17 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations (
})
}
fn list_applied_migrations(
&mut self,
) -> BoxFuture<'_, Result<Vec<AppliedMigration>, MigrateError>> {
fn list_applied_migrations<'e>(
&'e mut self,
table_name: &'e str,
) -> BoxFuture<'e, Result<Vec<AppliedMigration>, MigrateError>> {
Box::pin(async move {
// language=SQL
let rows: Vec<(i64, Vec<u8>)> =
query_as("SELECT version, checksum FROM _sqlx_migrations ORDER BY version")
.fetch_all(self)
.await?;
let rows: Vec<(i64, Vec<u8>)> = query_as(&format!(
"SELECT version, checksum FROM {table_name} ORDER BY version"
))
.fetch_all(self)
.await?;
let migrations = rows
.into_iter()
@@ -167,10 +187,11 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations (
})
}
fn apply<'e: 'm, 'm>(
fn apply<'e>(
&'e mut self,
migration: &'m Migration,
) -> BoxFuture<'m, Result<Duration, MigrateError>> {
table_name: &'e str,
migration: &'e Migration,
) -> BoxFuture<'e, Result<Duration, MigrateError>> {
Box::pin(async move {
// Use a single transaction for the actual migration script and the essential bookeeping so we never
// execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966.
@@ -187,12 +208,12 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations (
// `success=FALSE` and later modify the flag.
//
// language=MySQL
let _ = query(
let _ = query(&format!(
r#"
INSERT INTO _sqlx_migrations ( version, description, success, checksum, execution_time )
INSERT INTO {table_name} ( version, description, success, checksum, execution_time )
VALUES ( ?, ?, FALSE, ?, -1 )
"#,
)
"#
))
.bind(migration.version)
.bind(&*migration.description)
.bind(&*migration.checksum)
@@ -205,13 +226,13 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations (
.map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?;
// language=MySQL
let _ = query(
let _ = query(&format!(
r#"
UPDATE _sqlx_migrations
UPDATE {table_name}
SET success = TRUE
WHERE version = ?
"#,
)
"#
))
.bind(migration.version)
.execute(&mut *tx)
.await?;
@@ -225,13 +246,13 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations (
let elapsed = start.elapsed();
#[allow(clippy::cast_possible_truncation)]
let _ = query(
let _ = query(&format!(
r#"
UPDATE _sqlx_migrations
UPDATE {table_name}
SET execution_time = ?
WHERE version = ?
"#,
)
"#
))
.bind(elapsed.as_nanos() as i64)
.bind(migration.version)
.execute(self)
@@ -241,10 +262,11 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations (
})
}
fn revert<'e: 'm, 'm>(
fn revert<'e>(
&'e mut self,
migration: &'m Migration,
) -> BoxFuture<'m, Result<Duration, MigrateError>> {
table_name: &'e str,
migration: &'e Migration,
) -> BoxFuture<'e, Result<Duration, MigrateError>> {
Box::pin(async move {
// Use a single transaction for the actual migration script and the essential bookeeping so we never
// execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966.
@@ -258,13 +280,13 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations (
// `success=FALSE` and later remove the migration altogether.
//
// language=MySQL
let _ = query(
let _ = query(&format!(
r#"
UPDATE _sqlx_migrations
UPDATE {table_name}
SET success = FALSE
WHERE version = ?
"#,
)
"#
))
.bind(migration.version)
.execute(&mut *tx)
.await?;
@@ -272,7 +294,7 @@ CREATE TABLE IF NOT EXISTS _sqlx_migrations (
tx.execute(&*migration.sql).await?;
// language=SQL
let _ = query(r#"DELETE FROM _sqlx_migrations WHERE version = ?"#)
let _ = query(&format!(r#"DELETE FROM {table_name} WHERE version = ?"#))
.bind(migration.version)
.execute(&mut *tx)
.await?;

View File

@@ -1,4 +1,4 @@
use std::str::from_utf8;
use std::str;
use bitflags::bitflags;
use bytes::{Buf, Bytes};
@@ -104,11 +104,9 @@ pub enum ColumnType {
pub(crate) struct ColumnDefinition {
#[allow(unused)]
catalog: Bytes,
#[allow(unused)]
schema: Bytes,
#[allow(unused)]
table_alias: Bytes,
#[allow(unused)]
table: Bytes,
alias: Bytes,
name: Bytes,
@@ -125,12 +123,20 @@ impl ColumnDefinition {
// NOTE: strings in-protocol are transmitted according to the client character set
// as this is UTF-8, all these strings should be UTF-8
pub(crate) fn schema(&self) -> Result<&str, Error> {
str::from_utf8(&self.schema).map_err(Error::protocol)
}
pub(crate) fn table(&self) -> Result<&str, Error> {
str::from_utf8(&self.table).map_err(Error::protocol)
}
pub(crate) fn name(&self) -> Result<&str, Error> {
from_utf8(&self.name).map_err(Error::protocol)
str::from_utf8(&self.name).map_err(Error::protocol)
}
pub(crate) fn alias(&self) -> Result<&str, Error> {
from_utf8(&self.alias).map_err(Error::protocol)
str::from_utf8(&self.alias).map_err(Error::protocol)
}
}

View File

@@ -25,41 +25,39 @@ impl_type_checking!(
// BINARY, VAR_BINARY, BLOB
Vec<u8>,
// Types from third-party crates need to be referenced at a known path
// for the macros to work, but we don't want to require the user to add extra dependencies.
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::NaiveTime,
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::NaiveDate,
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::NaiveDateTime,
#[cfg(all(feature = "chrono", not(feature = "time")))]
sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>,
#[cfg(feature = "time")]
sqlx::types::time::Time,
#[cfg(feature = "time")]
sqlx::types::time::Date,
#[cfg(feature = "time")]
sqlx::types::time::PrimitiveDateTime,
#[cfg(feature = "time")]
sqlx::types::time::OffsetDateTime,
#[cfg(feature = "bigdecimal")]
sqlx::types::BigDecimal,
#[cfg(feature = "rust_decimal")]
sqlx::types::Decimal,
#[cfg(feature = "json")]
sqlx::types::JsonValue,
},
ParamChecking::Weak,
feature-types: info => info.__type_feature_gate(),
// The expansion of the macro automatically applies the correct feature name
// and checks `[macros.preferred-crates]`
datetime-types: {
chrono: {
sqlx::types::chrono::NaiveTime,
sqlx::types::chrono::NaiveDate,
sqlx::types::chrono::NaiveDateTime,
sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>,
},
time: {
sqlx::types::time::Time,
sqlx::types::time::Date,
sqlx::types::time::PrimitiveDateTime,
sqlx::types::time::OffsetDateTime,
},
},
numeric-types: {
bigdecimal: {
sqlx::types::BigDecimal,
},
rust_decimal: {
sqlx::types::Decimal,
},
},
);