mirror of
https://github.com/launchbadge/sqlx.git
synced 2026-03-19 08:39:44 +00:00
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:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user