From 9d74aeae5253a5bcdf07c5e3f581a6282e609eac Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 4 Jan 2025 06:24:04 +0100 Subject: [PATCH 01/66] chore: issue forms with reason-listings (#3476) * feat: added a draft of an issue form * fixed a validation issue * finished up the templates * used austins phrasing * Applied comments from the code review Co-authored-by: Austin Bonander * Applyed more suggestions from code review Co-authored-by: Austin Bonander * Broke the environment up into multiple fields * boiled the FR-fields down to a description and an breaking change field * fixed an unique id issue --------- Co-authored-by: Austin Bonander --- .github/ISSUE_TEMPLATE/bug_report.md | 19 ------- .github/ISSUE_TEMPLATE/bug_report.yml | 61 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ------- .github/ISSUE_TEMPLATE/feature_request.yml | 37 +++++++++++++ 4 files changed, 98 insertions(+), 39 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 12a71c650b..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: I think I found a bug in SQLx -about: Create a bug-report issue. -title: '' -labels: 'bug' -assignees: '' ---- -### Bug Description -A clear and concise description of what the bug is. - -### Minimal Reproduction -A small code snippet or a link to a Github repo or Gist, with instructions on reproducing the bug. - -### Info -* SQLx version: [REQUIRED] -* SQLx features enabled: [REQUIRED] -* Database server and version: [REQUIRED] (MySQL / Postgres / SQLite ) -* Operating system: [REQUIRED] -* `rustc --version`: [REQUIRED] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..317e4a87ce --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,61 @@ +name: I think I found a bug in SQLx +description: Create a bug-report issue +labels: [bug] +body: + - type: textarea + id: related-issues + validations: + required: true + attributes: + label: I have found these related issues/pull requests + description: | + I have searched by clicking [HERE](https://github.com/launchbadge/sqlx/issues?q=) + for existing issues, these are the ones I've found, + and this is why I think this deserves a new issue. + placeholder: "Related to ..." + - type: textarea + id: description + validations: + required: true + attributes: + label: Description + description: Clear and concise description of what the bug is + - type: textarea + id: steps-to-reproduce + validations: + required: true + attributes: + label: Reproduction steps + description: A small code snippet or a link to a Github repo or Gist, with instructions on reproducing the bug. + - type: input + id: sqlx-version + attributes: + label: SQLx version + validations: + required: true + - type: input + id: sqlx-features + attributes: + label: Enabled SQLx features + validations: + required: true + - type: input + id: db-server-and-version + attributes: + label: Database server and version + placeholder: MySQL / Postgres / SQLite + validations: + required: true + - type: input + id: os-type + attributes: + label: Operating system + validations: + required: true + - type: input + id: rust-version + attributes: + label: Rust version + description: You can get this via running `rustc --version` + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 178ddfda5f..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: I have a feature request for SQLx -about: Create a feature-request issue. -title: '' -labels: 'enhancement' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..0f39efc46d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,37 @@ +name: I have a feature request for SQLx +description: Create a feature-request issue +labels: [enhancement] +body: + - type: textarea + id: related-issues + validations: + required: true + attributes: + label: I have found these related issues/pull requests + description: "Provide context for your pull request." + placeholder: | + Closes \#... + Relates to \#... + - type: textarea + id: feature-description + validations: + required: true + attributes: + label: Description + description: A clear and concise description of what the problem is + placeholder: You should add ... + - type: textarea + id: solution + validations: + required: true + attributes: + label: Prefered solution + description: A clear and concise description of what you want to happen. + placeholder: In my use-case, ... + - type: textarea + id: breaking-change + validations: + required: true + attributes: + label: Is this a breaking change? Why or why not? + From 6b337668dedfed5a40f08df75e45121ec3a947e2 Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Tue, 7 Jan 2025 01:00:18 +0100 Subject: [PATCH 02/66] fix(cli): running tests on 32bit platforms (#3666) --- sqlx-cli/tests/add.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlx-cli/tests/add.rs b/sqlx-cli/tests/add.rs index 523ce4767e..1d5ed7c7dd 100644 --- a/sqlx-cli/tests/add.rs +++ b/sqlx-cli/tests/add.rs @@ -17,7 +17,7 @@ fn add_migration_ambiguous() -> anyhow::Result<()> { #[derive(Debug, PartialEq, Eq)] struct FileName { - id: usize, + id: u64, description: String, suffix: String, } @@ -50,7 +50,7 @@ impl From for FileName { fn from(path: PathBuf) -> Self { let filename = path.file_name().unwrap().to_string_lossy(); let (id, rest) = filename.split_once("_").unwrap(); - let id: usize = id.parse().unwrap(); + let id: u64 = id.parse().unwrap(); let (description, suffix) = rest.split_once(".").unwrap(); Self { id, From b386862ed516ab15881929ac1824881c9e42b556 Mon Sep 17 00:00:00 2001 From: Karam Barakat <32816424+karambarakat@users.noreply.github.com> Date: Wed, 8 Jan 2025 20:17:59 -0800 Subject: [PATCH 03/66] Update row.rs Row::get should track caller for a better development experience. --- sqlx-core/src/row.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/sqlx-core/src/row.rs b/sqlx-core/src/row.rs index 4c96ced048..fe0b75ebb0 100644 --- a/sqlx-core/src/row.rs +++ b/sqlx-core/src/row.rs @@ -64,6 +64,7 @@ pub trait Row: Unpin + Send + Sync + 'static { /// See [`try_get`](Self::try_get) for a non-panicking version. /// #[inline] + #[track_caller] fn get<'r, T, I>(&'r self, index: I) -> T where I: ColumnIndex, From 8b5277462d6e5f80432ab85c2f7ad29a9e30d7de Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Sat, 4 Jan 2025 17:38:57 +0100 Subject: [PATCH 04/66] build(deps): bump semver compatible dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the following cargo-audit feedback: ``` Crate: openssl Version: 0.10.64 Title: `MemBio::get_buf` has undefined behavior with empty buffers Date: 2024-07-21 ID: RUSTSEC-2024-0357 URL: https://rustsec.org/advisories/RUSTSEC-2024-0357 Solution: Upgrade to >=0.10.66 Dependency tree: openssl 0.10.64 ├── sqlx-cli 0.8.3 └── native-tls 0.2.11 └── sqlx-core 0.8.3 ├── sqlx-sqlite 0.8.3 │ ├── sqlx-macros-core 0.8.3 │ │ └── sqlx-macros 0.8.3 │ │ └── sqlx 0.8.3 │ │ ├── sqlx-test 0.1.0 │ │ │ └── sqlx 0.8.3 │ │ ├── sqlx-sqlite 0.8.3 │ │ ├── sqlx-postgres 0.8.3 │ │ │ ├── sqlx-macros-core 0.8.3 │ │ │ └── sqlx 0.8.3 │ │ ├── sqlx-mysql 0.8.3 │ │ │ ├── sqlx-macros-core 0.8.3 │ │ │ └── sqlx 0.8.3 │ │ ├── sqlx-example-sqlite-todos 0.1.0 │ │ ├── sqlx-example-postgres-transaction 0.1.0 │ │ ├── sqlx-example-postgres-todos 0.1.0 │ │ ├── sqlx-example-postgres-mockable-todos 0.1.0 │ │ ├── sqlx-example-postgres-listen 0.1.0 │ │ ├── sqlx-example-postgres-json 0.1.0 │ │ ├── sqlx-example-postgres-files 0.1.0 │ │ ├── sqlx-example-postgres-chat 0.1.0 │ │ ├── sqlx-example-postgres-axum-social 0.1.0 │ │ ├── sqlx-example-mysql-todos 0.1.0 │ │ ├── sqlx-core 0.8.3 │ │ └── sqlx-cli 0.8.3 │ └── sqlx 0.8.3 ├── sqlx-postgres 0.8.3 ├── sqlx-mysql 0.8.3 ├── sqlx-macros-core 0.8.3 ├── sqlx-macros 0.8.3 └── sqlx 0.8.3 Crate: futures-util Version: 0.3.30 Warning: yanked Dependency tree: futures-util 0.3.30 ├── tower-http 0.3.5 │ └── axum 0.5.17 │ └── sqlx-example-postgres-axum-social 0.1.0 ├── tower 0.4.13 │ ├── tower-http 0.3.5 │ ├── sqlx-example-postgres-axum-social 0.1.0 │ └── axum 0.5.17 ├── sqlx-sqlite 0.8.3 │ ├── sqlx-macros-core 0.8.3 │ │ └── sqlx-macros 0.8.3 │ │ └── sqlx 0.8.3 │ │ ├── sqlx-test 0.1.0 │ │ │ └── sqlx 0.8.3 │ │ ├── sqlx-sqlite 0.8.3 │ │ ├── sqlx-postgres 0.8.3 │ │ │ ├── sqlx-macros-core 0.8.3 │ │ │ └── sqlx 0.8.3 │ │ ├── sqlx-mysql 0.8.3 │ │ │ ├── sqlx-macros-core 0.8.3 │ │ │ └── sqlx 0.8.3 │ │ ├── sqlx-example-sqlite-todos 0.1.0 │ │ ├── sqlx-example-postgres-transaction 0.1.0 │ │ ├── sqlx-example-postgres-todos 0.1.0 │ │ ├── sqlx-example-postgres-mockable-todos 0.1.0 │ │ ├── sqlx-example-postgres-listen 0.1.0 │ │ ├── sqlx-example-postgres-json 0.1.0 │ │ ├── sqlx-example-postgres-files 0.1.0 │ │ ├── sqlx-example-postgres-chat 0.1.0 │ │ ├── sqlx-example-postgres-axum-social 0.1.0 │ │ ├── sqlx-example-mysql-todos 0.1.0 │ │ ├── sqlx-core 0.8.3 │ │ │ ├── sqlx-sqlite 0.8.3 │ │ │ ├── sqlx-postgres 0.8.3 │ │ │ ├── sqlx-mysql 0.8.3 │ │ │ ├── sqlx-macros-core 0.8.3 │ │ │ ├── sqlx-macros 0.8.3 │ │ │ └── sqlx 0.8.3 │ │ └── sqlx-cli 0.8.3 │ └── sqlx 0.8.3 ├── sqlx-postgres 0.8.3 ├── sqlx-mysql 0.8.3 ├── sqlx-core 0.8.3 ├── hyper 0.14.28 │ └── axum 0.5.17 ├── futures-executor 0.3.30 │ ├── sqlx-sqlite 0.8.3 │ └── futures 0.3.30 │ ├── sqlx-example-sqlite-todos 0.1.0 │ ├── sqlx-example-postgres-transaction 0.1.0 │ ├── sqlx-example-postgres-todos 0.1.0 │ ├── sqlx-example-postgres-mockable-todos 0.1.0 │ ├── sqlx-example-postgres-listen 0.1.0 │ ├── sqlx-example-postgres-json 0.1.0 │ ├── sqlx-example-postgres-chat 0.1.0 │ ├── sqlx-example-mysql-todos 0.1.0 │ ├── sqlx-cli 0.8.3 │ ├── sqlx 0.8.3 │ └── criterion 0.5.1 │ └── sqlx 0.8.3 ├── futures 0.3.30 ├── axum-core 0.2.9 │ └── axum 0.5.17 └── axum 0.5.17 Crate: url Version: 2.5.3 Warning: yanked Dependency tree: url 2.5.3 ├── validator 0.16.1 │ └── sqlx-example-postgres-axum-social 0.1.0 ├── sqlx-sqlite 0.8.3 │ ├── sqlx-macros-core 0.8.3 │ │ └── sqlx-macros 0.8.3 │ │ └── sqlx 0.8.3 │ │ ├── sqlx-test 0.1.0 │ │ │ └── sqlx 0.8.3 │ │ ├── sqlx-sqlite 0.8.3 │ │ ├── sqlx-postgres 0.8.3 │ │ │ ├── sqlx-macros-core 0.8.3 │ │ │ └── sqlx 0.8.3 │ │ ├── sqlx-mysql 0.8.3 │ │ │ ├── sqlx-macros-core 0.8.3 │ │ │ └── sqlx 0.8.3 │ │ ├── sqlx-example-sqlite-todos 0.1.0 │ │ ├── sqlx-example-postgres-transaction 0.1.0 │ │ ├── sqlx-example-postgres-todos 0.1.0 │ │ ├── sqlx-example-postgres-mockable-todos 0.1.0 │ │ ├── sqlx-example-postgres-listen 0.1.0 │ │ ├── sqlx-example-postgres-json 0.1.0 │ │ ├── sqlx-example-postgres-files 0.1.0 │ │ ├── sqlx-example-postgres-chat 0.1.0 │ │ ├── sqlx-example-postgres-axum-social 0.1.0 │ │ ├── sqlx-example-mysql-todos 0.1.0 │ │ ├── sqlx-core 0.8.3 │ │ │ ├── sqlx-sqlite 0.8.3 │ │ │ ├── sqlx-postgres 0.8.3 │ │ │ ├── sqlx-mysql 0.8.3 │ │ │ ├── sqlx-macros-core 0.8.3 │ │ │ ├── sqlx-macros 0.8.3 │ │ │ └── sqlx 0.8.3 │ │ └── sqlx-cli 0.8.3 │ └── sqlx 0.8.3 ├── sqlx-macros-core 0.8.3 ├── sqlx-core 0.8.3 └── sqlx 0.8.3 ``` Result of cargo update: ``` Updating crates.io index Updating addr2line v0.21.0 -> v0.24.2 Removing adler v1.0.2 Adding adler2 v2.0.0 Removing ahash v0.8.11 Updating aho-corasick v1.1.2 -> v1.1.3 Updating allocator-api2 v0.2.16 -> v0.2.21 Updating anstream v0.6.13 -> v0.6.18 Updating anstyle v1.0.6 -> v1.0.10 Updating anstyle-parse v0.2.3 -> v0.2.6 Updating anstyle-query v1.0.2 -> v1.1.2 Updating anstyle-wincon v3.0.2 -> v3.0.6 Updating anyhow v1.0.81 -> v1.0.95 Updating arrayvec v0.7.4 -> v0.7.6 Updating assert_cmd v2.0.14 -> v2.0.16 Updating async-channel v2.2.0 -> v2.3.1 Updating async-executor v1.8.0 -> v1.13.1 Updating async-io v2.3.2 -> v2.4.0 Updating async-lock v3.3.0 -> v3.4.0 Updating async-task v4.7.0 -> v4.7.1 Updating async-trait v0.1.77 -> v0.1.85 Updating autocfg v1.1.0 -> v1.4.0 Updating aws-lc-rs v1.8.0 -> v1.12.0 Updating aws-lc-sys v0.19.0 -> v0.24.1 Updating backtrace v0.3.69 -> v0.3.74 Updating base64 v0.22.0 -> v0.22.1 Removing basic-toml v0.1.9 Updating bigdecimal v0.4.3 -> v0.4.7 Updating bindgen v0.69.4 -> v0.69.5 (latest: v0.71.1) Updating bitflags v2.4.2 -> v2.7.0 Updating blocking v1.5.1 -> v1.6.1 Updating borsh v1.5.1 -> v1.5.3 Updating borsh-derive v1.5.1 -> v1.5.3 Updating bstr v1.9.1 -> v1.11.3 Updating bumpalo v3.15.4 -> v3.16.0 Updating bytes v1.5.0 -> v1.9.0 Updating camino v1.1.6 -> v1.1.9 Updating cargo-platform v0.1.7 -> v0.1.9 Updating cc v1.1.6 -> v1.2.8 Adding cfg_aliases v0.1.1 (latest: v0.2.1) Updating chrono v0.4.35 -> v0.4.39 Updating clap v4.5.2 -> v4.5.26 Updating clap_builder v4.5.2 -> v4.5.26 Updating clap_complete v4.5.1 -> v4.5.42 Updating clap_derive v4.5.0 -> v4.5.24 Updating clap_lex v0.7.0 -> v0.7.4 Updating cmake v0.1.50 -> v0.1.52 Updating colorchoice v1.0.0 -> v1.0.3 Updating concurrent-queue v2.4.0 -> v2.5.0 Updating console v0.15.8 -> v0.15.10 Adding core-foundation v0.10.0 Updating core-foundation-sys v0.8.6 -> v0.8.7 Updating cpufeatures v0.2.12 -> v0.2.16 Updating crc v3.0.1 -> v3.2.1 Updating crossbeam-deque v0.8.5 -> v0.8.6 Updating crossbeam-queue v0.3.11 -> v0.3.12 Updating crossbeam-utils v0.8.19 -> v0.8.21 Updating darling v0.20.8 -> v0.20.10 Updating darling_core v0.20.8 -> v0.20.10 Updating darling_macro v0.20.8 -> v0.20.10 Updating der v0.7.8 -> v0.7.9 Updating dunce v1.0.4 -> v1.0.5 Updating either v1.10.0 -> v1.13.0 Updating encode_unicode v0.3.6 -> v1.0.0 Updating env_filter v0.1.0 -> v0.1.3 Updating env_logger v0.11.3 -> v0.11.6 Adding erased-serde v0.4.5 Updating errno v0.3.8 -> v0.3.10 Removing event-listener v4.0.3 Removing event-listener v5.2.0 Adding event-listener v5.4.0 Removing event-listener-strategy v0.4.0 Removing event-listener-strategy v0.5.0 Adding event-listener-strategy v0.5.3 Updating fastrand v2.0.1 -> v2.3.0 Updating filetime v0.2.23 -> v0.2.25 Removing finl_unicode v1.2.0 Updating flume v0.11.0 -> v0.11.1 Updating foldhash v0.1.3 -> v0.1.4 Updating futures v0.3.30 -> v0.3.31 Updating futures-channel v0.3.30 -> v0.3.31 Updating futures-core v0.3.30 -> v0.3.31 Updating futures-executor v0.3.30 -> v0.3.31 Updating futures-io v0.3.30 -> v0.3.31 Updating futures-lite v2.2.0 -> v2.5.0 Updating futures-macro v0.3.30 -> v0.3.31 Updating futures-sink v0.3.30 -> v0.3.31 Updating futures-task v0.3.30 -> v0.3.31 Updating futures-util v0.3.30 -> v0.3.31 Updating getrandom v0.2.12 -> v0.2.15 Updating gimli v0.28.1 -> v0.31.1 Updating glob v0.3.1 -> v0.3.2 Updating half v2.4.0 -> v2.4.1 Removing hashbrown v0.14.5 Adding hermit-abi v0.4.0 Updating httparse v1.8.0 -> v1.9.5 Updating hyper v0.14.28 -> v0.14.32 (latest: v1.5.2) Updating iana-time-zone v0.1.60 -> v0.1.61 Updating indexmap v2.2.5 -> v2.7.0 Updating instant v0.1.12 -> v0.1.13 Updating is-terminal v0.4.12 -> v0.4.13 Adding is_terminal_polyfill v1.70.1 Updating itoa v1.0.10 -> v1.0.14 Updating jobserver v0.1.31 -> v0.1.32 Updating js-sys v0.3.69 -> v0.3.76 Updating lazy_static v1.4.0 -> v1.5.0 Updating libc v0.2.153 -> v0.2.169 Updating libloading v0.8.4 -> v0.8.6 Updating libm v0.2.8 -> v0.2.11 Updating libredox v0.0.1 -> v0.1.3 Updating linux-raw-sys v0.4.13 -> v0.4.15 (latest: v0.7.0) Updating litemap v0.7.3 -> v0.7.4 Updating lock_api v0.4.11 -> v0.4.12 Updating log v0.4.21 -> v0.4.24 Updating lru v0.12.3 -> v0.12.5 Updating mac_address v1.1.5 -> v1.1.7 Updating memchr v2.7.1 -> v2.7.4 Adding memoffset v0.9.1 Updating miniz_oxide v0.7.2 -> v0.8.2 Adding mio v1.0.3 Removing mirai-annotations v1.12.0 Updating native-tls v0.2.11 -> v0.2.12 Adding nix v0.28.0 (latest: v0.29.0) Updating num-bigint v0.4.4 -> v0.4.6 Updating num-iter v0.1.44 -> v0.1.45 Updating num-traits v0.2.18 -> v0.2.19 Removing num_cpus v1.16.0 Updating object v0.32.2 -> v0.36.7 Updating once_cell v1.19.0 -> v1.20.2 Updating oorandom v11.1.3 -> v11.1.4 Updating openssl v0.10.64 -> v0.10.68 Updating openssl-src v300.2.3+3.2.1 -> v300.4.1+3.4.0 Updating openssl-sys v0.9.101 -> v0.9.104 Updating parking v2.2.0 -> v2.2.1 Updating parking_lot v0.12.1 -> v0.12.3 Updating parking_lot_core v0.9.9 -> v0.9.10 Updating paste v1.0.14 -> v1.0.15 Updating pin-project v1.1.5 -> v1.1.8 Updating pin-project-internal v1.1.5 -> v1.1.8 Updating pin-project-lite v0.2.13 -> v0.2.16 Updating piper v0.2.1 -> v0.2.4 Updating pkg-config v0.3.30 -> v0.3.31 Updating plotters v0.3.5 -> v0.3.7 Updating plotters-backend v0.3.5 -> v0.3.7 Updating plotters-svg v0.3.5 -> v0.3.7 Updating polling v3.5.0 -> v3.7.4 Updating ppv-lite86 v0.2.17 -> v0.2.20 Updating predicates v3.1.0 -> v3.1.3 Updating predicates-core v1.0.6 -> v1.0.9 Updating predicates-tree v1.0.9 -> v1.0.12 Updating prettyplease v0.2.17 -> v0.2.27 Updating proc-macro-crate v3.1.0 -> v3.2.0 Updating proc-macro2 v1.0.89 -> v1.0.93 Updating quote v1.0.35 -> v1.0.38 Updating rayon v1.9.0 -> v1.10.0 Updating redox_syscall v0.4.1 -> v0.5.8 Updating redox_users v0.4.4 -> v0.4.6 (latest: v0.5.0) Updating regex v1.10.3 -> v1.11.1 Updating regex-automata v0.4.6 -> v0.4.9 Updating regex-syntax v0.8.2 -> v0.8.5 Updating rkyv v0.7.44 -> v0.7.45 (latest: v0.8.9) Updating rkyv_derive v0.7.44 -> v0.7.45 (latest: v0.8.9) Updating rsa v0.9.6 -> v0.9.7 Updating rust_decimal v1.34.3 -> v1.36.0 Updating rustc-demangle v0.1.23 -> v0.1.24 Removing rustix v0.37.27 Removing rustix v0.38.31 Adding rustix v0.37.28 (latest: v0.38.43) Adding rustix v0.38.43 Updating rustls v0.23.11 -> v0.23.21 Updating rustls-native-certs v0.8.0 -> v0.8.1 Updating rustls-pemfile v2.1.2 -> v2.2.0 Updating rustls-pki-types v1.7.0 -> v1.10.1 Updating rustls-webpki v0.102.5 -> v0.102.8 Updating rustversion v1.0.17 -> v1.0.19 Updating ryu v1.0.17 -> v1.0.18 Updating schannel v0.1.23 -> v0.1.27 Removing security-framework v2.9.2 Adding security-framework v2.11.1 (latest: v3.2.0) Adding security-framework v3.2.0 Updating security-framework-sys v2.9.1 -> v2.14.0 Updating semver v1.0.22 -> v1.0.24 Updating serde v1.0.197 -> v1.0.217 Updating serde_derive v1.0.197 -> v1.0.217 Adding serde_fmt v1.0.3 Updating serde_json v1.0.114 -> v1.0.135 Adding serde_spanned v0.6.8 Updating signal-hook-mio v0.2.3 -> v0.2.4 Updating signal-hook-registry v1.4.1 -> v1.4.2 Updating simdutf8 v0.1.4 -> v0.1.5 Updating smallvec v1.13.1 -> v1.13.2 Updating socket2 v0.5.6 -> v0.5.8 Removing spin v0.5.2 Updating stringprep v0.1.4 -> v0.1.5 Removing strsim v0.10.0 Removing strsim v0.11.0 Adding strsim v0.11.1 Updating subtle v2.5.0 -> v2.6.1 Adding sval v2.13.2 Adding sval_buffer v2.13.2 Adding sval_dynamic v2.13.2 Adding sval_fmt v2.13.2 Adding sval_json v2.13.2 Adding sval_nested v2.13.2 Adding sval_ref v2.13.2 Adding sval_serde v2.13.2 Updating syn v2.0.87 -> v2.0.96 Removing syn_derive v0.1.8 Adding target-triple v0.1.3 Updating tempfile v3.10.1 -> v3.15.0 Updating termtree v0.4.1 -> v0.5.1 Removing thiserror v1.0.58 Removing thiserror v2.0.0 Adding thiserror v1.0.69 (latest: v2.0.11) Adding thiserror v2.0.11 Removing thiserror-impl v1.0.58 Removing thiserror-impl v2.0.0 Adding thiserror-impl v1.0.69 (latest: v2.0.11) Adding thiserror-impl v2.0.11 Updating time v0.3.36 -> v0.3.37 Updating time-macros v0.2.18 -> v0.2.19 Updating tinyvec v1.6.0 -> v1.8.1 Updating tokio v1.36.0 -> v1.43.0 Updating tokio-macros v2.2.0 -> v2.5.0 Updating tokio-stream v0.1.14 -> v0.1.17 Adding toml v0.8.19 Updating toml_datetime v0.6.6 -> v0.6.8 Updating toml_edit v0.21.1 -> v0.22.22 Updating tower-layer v0.3.2 -> v0.3.3 Updating tower-service v0.3.2 -> v0.3.3 Updating tracing v0.1.40 -> v0.1.41 Updating tracing-attributes v0.1.27 -> v0.1.28 Updating tracing-core v0.1.32 -> v0.1.33 Updating trybuild v1.0.89 -> v1.0.101 Adding typeid v1.0.2 Updating unicode-bidi v0.3.15 -> v0.3.18 Updating unicode-ident v1.0.12 -> v1.0.14 Updating unicode-normalization v0.1.23 -> v0.1.24 Adding unicode-properties v0.1.3 Updating unicode-segmentation v1.11.0 -> v1.12.0 Removing unicode-width v0.1.13 Adding unicode-width v0.1.14 (latest: v0.2.0) Adding unicode-width v0.2.0 Updating url v2.5.3 -> v2.5.4 Updating utf8parse v0.2.1 -> v0.2.2 Updating uuid v1.7.0 -> v1.11.1 Updating value-bag v1.8.0 -> v1.10.0 Adding value-bag-serde1 v1.10.0 Adding value-bag-sval2 v1.10.0 Updating version_check v0.9.4 -> v0.9.5 Updating waker-fn v1.1.1 -> v1.2.0 Updating wasm-bindgen v0.2.92 -> v0.2.99 Updating wasm-bindgen-backend v0.2.92 -> v0.2.99 Updating wasm-bindgen-futures v0.4.42 -> v0.4.49 Updating wasm-bindgen-macro v0.2.92 -> v0.2.99 Updating wasm-bindgen-macro-support v0.2.92 -> v0.2.99 Updating wasm-bindgen-shared v0.2.92 -> v0.2.99 Updating web-sys v0.3.69 -> v0.3.76 Updating webpki-roots v0.26.3 -> v0.26.7 Updating whoami v1.5.1 -> v1.5.2 Updating winapi-util v0.1.6 -> v0.1.9 Adding windows-sys v0.59.0 Updating windows-targets v0.52.4 -> v0.52.6 (latest: v0.53.0) Updating windows_aarch64_gnullvm v0.52.4 -> v0.52.6 (latest: v0.53.0) Updating windows_aarch64_msvc v0.52.4 -> v0.52.6 (latest: v0.53.0) Updating windows_i686_gnu v0.52.4 -> v0.52.6 (latest: v0.53.0) Adding windows_i686_gnullvm v0.52.6 (latest: v0.53.0) Updating windows_i686_msvc v0.52.4 -> v0.52.6 (latest: v0.53.0) Updating windows_x86_64_gnu v0.52.4 -> v0.52.6 (latest: v0.53.0) Updating windows_x86_64_gnullvm v0.52.4 -> v0.52.6 (latest: v0.53.0) Updating windows_x86_64_msvc v0.52.4 -> v0.52.6 (latest: v0.53.0) Updating winnow v0.5.40 -> v0.6.24 Updating yoke v0.7.4 -> v0.7.5 Updating yoke-derive v0.7.4 -> v0.7.5 Updating zerocopy v0.7.32 -> v0.7.35 (latest: v0.8.14) Updating zerocopy-derive v0.7.32 -> v0.7.35 (latest: v0.8.14) Updating zerofrom v0.1.4 -> v0.1.5 Updating zerofrom-derive v0.1.4 -> v0.1.5 Updating zeroize v1.7.0 -> v1.8.1 Removing zeroize_derive v1.4.2 note: pass `--verbose` to see 88 unchanged dependencies behind latest ``` --- Cargo.lock | 1583 ++++++++++++++++++++++++++++------------------------ 1 file changed, 869 insertions(+), 714 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68f3c84cd8..30dd3be30d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 4 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "ahash" @@ -28,32 +28,20 @@ dependencies = [ "version_check", ] -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "allocator-api2" -version = "0.2.16" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-tzdata" @@ -78,57 +66,58 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "argon2" @@ -143,20 +132,21 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "assert_cmd" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed72493ac66d5804837f480ab3766c72bdfab91a65e565fc54fa9e42db0073a8" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" dependencies = [ "anstyle", "bstr", "doc-comment", - "predicates 3.1.0", + "libc", + "predicates 3.1.3", "predicates-core", "predicates-tree", "wait-timeout", @@ -185,28 +175,26 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" dependencies = [ "concurrent-queue", - "event-listener 5.2.0", - "event-listener-strategy 0.5.0", + "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" -version = "1.8.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" dependencies = [ - "async-lock 3.3.0", "async-task", "concurrent-queue", - "fastrand 2.0.1", - "futures-lite 2.2.0", + "fastrand 2.3.0", + "futures-lite 2.5.0", "slab", ] @@ -216,12 +204,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.2.0", + "async-channel 2.3.1", "async-executor", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.4.0", + "async-lock 3.4.0", "blocking", - "futures-lite 2.2.0", + "futures-lite 2.5.0", "once_cell", ] @@ -239,7 +227,7 @@ dependencies = [ "log", "parking", "polling 2.8.0", - "rustix 0.37.27", + "rustix 0.37.28", "slab", "socket2 0.4.10", "waker-fn", @@ -247,21 +235,21 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ - "async-lock 3.3.0", + "async-lock 3.4.0", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.2.0", + "futures-lite 2.5.0", "parking", - "polling 3.5.0", - "rustix 0.38.31", + "polling 3.7.4", + "rustix 0.38.43", "slab", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -275,12 +263,12 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 4.0.3", - "event-listener-strategy 0.4.0", + "event-listener 5.4.0", + "event-listener-strategy", "pin-project-lite", ] @@ -293,13 +281,13 @@ dependencies = [ "async-attributes", "async-channel 1.9.0", "async-global-executor", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io 2.4.0", + "async-lock 3.4.0", "crossbeam-utils", "futures-channel", "futures-core", "futures-io", - "futures-lite 2.2.0", + "futures-lite 2.5.0", "gloo-timers", "kv-log-macro", "log", @@ -313,19 +301,19 @@ dependencies = [ [[package]] name = "async-task" -version = "4.7.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -345,34 +333,32 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.8.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a47f2fb521b70c11ce7369a6c5fa4bd6af7e5d62ec06303875bafe7c6ba245" +checksum = "f409eb70b561706bf8abba8ca9c112729c481595893fd06a2dd9af8ed8441148" dependencies = [ "aws-lc-sys", - "mirai-annotations", "paste", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.19.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2927c7af777b460b7ccd95f8b67acd7b4c04ec8896bf0c8e80ba30523cffc057" +checksum = "923ded50f602b3007e5e63e3f094c479d9c8a9b42d7f4034e4afe456aa48bfd2" dependencies = [ "bindgen", "cc", "cmake", "dunce", "fs_extra", - "libc", "paste", ] @@ -452,17 +438,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -473,9 +459,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -483,20 +469,11 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "basic-toml" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" -dependencies = [ - "serde", -] - [[package]] name = "bigdecimal" -version = "0.4.3" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9324c8014cd04590682b34f1e9448d38f0674d0f7b2dc553331016ef0e4e9ebc" +checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" dependencies = [ "autocfg", "libm", @@ -507,11 +484,11 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.4" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -524,7 +501,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.87", + "syn 2.0.96", "which", ] @@ -542,9 +519,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" dependencies = [ "serde", ] @@ -581,49 +558,45 @@ dependencies = [ [[package]] name = "blocking" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" dependencies = [ - "async-channel 2.2.0", - "async-lock 3.3.0", + "async-channel 2.3.1", "async-task", - "fastrand 2.0.1", "futures-io", - "futures-lite 2.2.0", + "futures-lite 2.5.0", "piper", - "tracing", ] [[package]] name = "borsh" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" dependencies = [ "borsh-derive", - "cfg_aliases", + "cfg_aliases 0.2.1", ] [[package]] name = "borsh-derive" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", - "syn_derive", + "syn 2.0.96", ] [[package]] name = "bstr" -version = "1.9.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "regex-automata", @@ -632,9 +605,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecheck" @@ -666,24 +639,24 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "camino" -version = "1.1.6" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ "serde", ] [[package]] name = "cargo-platform" -version = "0.1.7" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "694c8807f2ae16faecc43dc17d74b3eb042482789fd0eb64b39a2e04e087053f" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" dependencies = [ "serde", ] @@ -699,7 +672,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 1.0.58", + "thiserror 1.0.69", ] [[package]] @@ -725,12 +698,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.6" +version = "1.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "ad0cf6e91fde44c773c6ee7ec6bba798504641a8bc2eb7e37a04ffbf4dfaa55a" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -748,6 +722,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "cfg_aliases" version = "0.2.1" @@ -756,15 +736,15 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.4", + "windows-targets 0.52.6", ] [[package]] @@ -807,9 +787,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.2" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" dependencies = [ "clap_builder", "clap_derive", @@ -817,42 +797,42 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.0", + "strsim", ] [[package]] name = "clap_complete" -version = "4.5.1" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c" +checksum = "33a7e468e750fa4b6be660e8b5651ad47372e8fb114030b594c2d75d48c5ffd0" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "clipboard-win" @@ -867,18 +847,18 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.50" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "compact_str" @@ -895,24 +875,24 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "console" -version = "0.15.8" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "unicode-width", - "windows-sys 0.52.0", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", ] [[package]] @@ -931,26 +911,36 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] [[package]] name = "crc" -version = "3.0.1" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" dependencies = [ "crc-catalog", ] @@ -1001,9 +991,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1020,18 +1010,18 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" @@ -1039,10 +1029,10 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "crossterm_winapi", "libc", - "mio", + "mio 0.8.11", "parking_lot", "signal-hook", "signal-hook-mio", @@ -1076,9 +1066,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -1086,34 +1076,34 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn 2.0.87", + "strsim", + "syn 2.0.96", ] [[package]] name = "darling_macro" -version = "0.20.8" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "pem-rfc7468", @@ -1177,7 +1167,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -1200,24 +1190,24 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "dunce" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.10.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" dependencies = [ "serde", ] [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "endian-type" @@ -1227,9 +1217,9 @@ checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" [[package]] name = "env_filter" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", "regex", @@ -1237,9 +1227,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "anstream", "anstyle", @@ -1254,11 +1244,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1293,20 +1293,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "4.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener" -version = "5.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -1315,21 +1304,11 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" -dependencies = [ - "event-listener 4.0.3", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ - "event-listener 5.2.0", + "event-listener 5.4.0", "pin-project-lite", ] @@ -1344,9 +1323,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fd-lock" @@ -1355,28 +1334,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" dependencies = [ "cfg-if", - "rustix 0.38.31", + "rustix 0.38.43", "windows-sys 0.48.0", ] [[package]] name = "filetime" -version = "0.2.23" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", - "redox_syscall", - "windows-sys 0.52.0", + "libredox", + "windows-sys 0.59.0", ] -[[package]] -name = "finl_unicode" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" - [[package]] name = "float-cmp" version = "0.9.0" @@ -1388,13 +1361,13 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin 0.9.8", + "spin", ] [[package]] @@ -1405,9 +1378,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" [[package]] name = "foreign-types" @@ -1453,9 +1426,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1468,9 +1441,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1478,15 +1451,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1506,9 +1479,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -1527,11 +1500,11 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.2.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ - "fastrand 2.0.1", + "fastrand 2.3.0", "futures-core", "futures-io", "parking", @@ -1540,32 +1513,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1591,9 +1564,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -1602,15 +1575,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "gloo-timers" @@ -1626,9 +1599,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", @@ -1640,17 +1613,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.8", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.11", - "allocator-api2", + "ahash", ] [[package]] @@ -1691,6 +1654,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -1754,9 +1723,9 @@ checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -1772,9 +1741,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1786,7 +1755,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.6", + "socket2 0.5.8", "tokio", "tower-service", "tracing", @@ -1795,9 +1764,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1931,7 +1900,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -1990,19 +1959,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.5" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -2013,7 +1982,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] @@ -2029,15 +1998,21 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -2067,25 +2042,26 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -2100,11 +2076,11 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin", ] [[package]] @@ -2115,33 +2091,33 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.4", + "windows-targets 0.48.5", ] [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "libc", "redox_syscall", ] @@ -2166,21 +2142,21 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2188,29 +2164,29 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d" dependencies = [ "value-bag", ] [[package]] name = "lru" -version = "0.12.3" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.2", ] [[package]] name = "mac_address" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4863ee94f19ed315bf3bc00299338d857d4b5bc856af375cc97d237382ad3856" +checksum = "8836fae9d0d4be2c8b4efcdd79e828a2faa058a90d005abf42f91cac5493a08e" dependencies = [ - "nix", + "nix 0.28.0", "winapi", ] @@ -2232,9 +2208,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -2245,6 +2221,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2259,11 +2244,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ - "adler", + "adler2", ] [[package]] @@ -2279,10 +2264,15 @@ dependencies = [ ] [[package]] -name = "mirai-annotations" -version = "1.12.0" +name = "mio" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] [[package]] name = "mockall" @@ -2313,18 +2303,17 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -2348,7 +2337,20 @@ dependencies = [ "cc", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.7.0", + "cfg-if", + "cfg_aliases 0.1.1", + "libc", + "memoffset 0.9.1", ] [[package]] @@ -2369,11 +2371,10 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -2412,9 +2413,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -2423,52 +2424,42 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" -version = "0.32.2" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oorandom" -version = "11.1.3" +version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "cfg-if", "foreign-types", "libc", @@ -2485,7 +2476,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -2496,18 +2487,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.2.3+3.2.1" +version = "300.4.1+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -2518,15 +2509,15 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -2534,15 +2525,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2558,9 +2549,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pem-rfc7468" @@ -2579,29 +2570,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2611,12 +2602,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.1" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.0.1", + "fastrand 2.3.0", "futures-io", ] @@ -2643,15 +2634,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "plotters" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -2662,15 +2653,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] @@ -2693,16 +2684,17 @@ dependencies = [ [[package]] name = "polling" -version = "3.5.0" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", + "hermit-abi 0.4.0", "pin-project-lite", - "rustix 0.38.31", + "rustix 0.38.43", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2713,9 +2705,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "predicates" @@ -2733,9 +2728,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.0" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", @@ -2744,15 +2739,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", @@ -2760,19 +2755,19 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.17" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" +checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705" dependencies = [ "proc-macro2", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] @@ -2803,9 +2798,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -2841,9 +2836,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2909,7 +2904,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d16546c5b5962abf8ce6e2881e722b4e0ae3b6f1a08a26ae3573c55853ca68d3" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "cassowary", "compact_str", "crossterm", @@ -2921,14 +2916,14 @@ dependencies = [ "strum_macros", "unicode-segmentation", "unicode-truncate", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -2946,29 +2941,29 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.7.0", ] [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror 1.0.58", + "thiserror 1.0.69", ] [[package]] name = "regex" -version = "1.10.3" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2978,9 +2973,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2989,9 +2984,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rend" @@ -3012,16 +3007,16 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rkyv" -version = "0.7.44" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" dependencies = [ "bitvec", "bytecheck", @@ -3037,9 +3032,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.44" +version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" dependencies = [ "proc-macro2", "quote", @@ -3048,9 +3043,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" dependencies = [ "const-oid", "digest", @@ -3068,9 +3063,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.34.3" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" dependencies = [ "arrayvec", "borsh", @@ -3084,9 +3079,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -3096,9 +3091,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.27" +version = "0.37.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" dependencies = [ "bitflags 1.3.2", "errno", @@ -3110,22 +3105,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.7.0", "errno", "libc", - "linux-raw-sys 0.4.13", + "linux-raw-sys 0.4.15", "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.23.11" +version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "aws-lc-rs", "once_cell", @@ -3138,38 +3133,36 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.2.0", ] [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.0", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" [[package]] name = "rustls-webpki" -version = "0.102.5" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "aws-lc-rs", "ring", @@ -3179,9 +3172,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rustyline" @@ -3197,21 +3190,21 @@ dependencies = [ "libc", "log", "memchr", - "nix", + "nix 0.23.2", "radix_trie", "scopeguard", "smallvec", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", "utf8parse", "winapi", ] [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -3224,11 +3217,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3245,12 +3238,25 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 1.3.2", - "core-foundation", + "bitflags 2.7.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.7.0", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -3258,9 +3264,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -3268,44 +3274,63 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.197" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", +] + +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3343,7 +3368,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -3386,20 +3411,20 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" dependencies = [ "libc", - "mio", + "mio 0.8.11", "signal-hook", ] [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3416,9 +3441,9 @@ dependencies = [ [[package]] name = "simdutf8" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] name = "slab" @@ -3431,9 +3456,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" dependencies = [ "serde", ] @@ -3450,20 +3475,14 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -3552,14 +3571,14 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener 5.2.0", + "event-listener 5.4.0", "futures-core", "futures-intrusive", "futures-io", "futures-util", "hashbrown 0.15.2", "hashlink", - "indexmap 2.2.5", + "indexmap 2.7.0", "ipnetwork", "log", "mac_address", @@ -3577,7 +3596,7 @@ dependencies = [ "sha2", "smallvec", "sqlx", - "thiserror 2.0.0", + "thiserror 2.0.11", "time", "tokio", "tokio-stream", @@ -3613,7 +3632,7 @@ dependencies = [ "serde_json", "serde_with", "sqlx", - "thiserror 2.0.0", + "thiserror 2.0.11", "time", "tokio", "tower", @@ -3631,7 +3650,7 @@ dependencies = [ "ratatui", "sqlx", "tokio", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -3721,7 +3740,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -3743,7 +3762,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.87", + "syn 2.0.96", "tempfile", "tokio", "url", @@ -3754,9 +3773,9 @@ name = "sqlx-mysql" version = "0.8.3" dependencies = [ "atoi", - "base64 0.22.0", + "base64 0.22.1", "bigdecimal", - "bitflags 2.4.2", + "bitflags 2.7.0", "byteorder", "bytes", "chrono", @@ -3788,7 +3807,7 @@ dependencies = [ "sqlx", "sqlx-core", "stringprep", - "thiserror 2.0.0", + "thiserror 2.0.11", "time", "tracing", "uuid", @@ -3800,10 +3819,10 @@ name = "sqlx-postgres" version = "0.8.3" dependencies = [ "atoi", - "base64 0.22.0", + "base64 0.22.1", "bigdecimal", "bit-vec", - "bitflags 2.4.2", + "bitflags 2.7.0", "byteorder", "chrono", "crc", @@ -3833,7 +3852,7 @@ dependencies = [ "sqlx", "sqlx-core", "stringprep", - "thiserror 2.0.0", + "thiserror 2.0.11", "time", "tracing", "uuid", @@ -3883,7 +3902,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" dependencies = [ "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -3906,26 +3925,20 @@ checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "stringprep" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" dependencies = [ - "finl_unicode", "unicode-bidi", "unicode-normalization", + "unicode-properties", ] [[package]] name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" @@ -3946,31 +3959,98 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "syn" -version = "1.0.109" +name = "sval" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8" + +[[package]] +name = "sval_buffer" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "sval", + "sval_ref", +] + +[[package]] +name = "sval_dynamic" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_fmt" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_json" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155" +dependencies = [ + "itoa", + "ryu", + "sval", +] + +[[package]] +name = "sval_nested" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f" +dependencies = [ + "sval", + "sval_buffer", + "sval_ref", +] + +[[package]] +name = "sval_ref" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa" +dependencies = [ + "sval", +] + +[[package]] +name = "sval_serde" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a" +dependencies = [ + "serde", + "sval", + "sval_nested", ] [[package]] name = "syn" -version = "2.0.87" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -3978,15 +4058,14 @@ dependencies = [ ] [[package]] -name = "syn_derive" -version = "0.1.8" +name = "syn" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ - "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.87", + "unicode-ident", ] [[package]] @@ -4003,7 +4082,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] @@ -4012,15 +4091,23 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-triple" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" + [[package]] name = "tempfile" -version = "3.10.1" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", - "fastrand 2.0.1", - "rustix 0.38.31", + "fastrand 2.3.0", + "getrandom", + "once_cell", + "rustix 0.38.43", "windows-sys 0.52.0", ] @@ -4035,55 +4122,55 @@ dependencies = [ [[package]] name = "termtree" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.58", + "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" -version = "2.0.0" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15291287e9bff1bc6f9ff3409ed9af665bec7a5fc8ac079ea96be07bca0e2668" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.0", + "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "thiserror-impl" -version = "2.0.0" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22efd00f33f93fa62848a7cab956c3d38c8d43095efda1decfc2b3a5dc0b8972" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -4102,9 +4189,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -4132,9 +4219,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -4147,58 +4234,74 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.3", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.6", + "socket2 0.5.8", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" -version = "0.21.1" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.2.5", + "indexmap 2.7.0", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -4240,21 +4343,21 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -4264,20 +4367,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -4290,19 +4393,25 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "trybuild" -version = "1.0.89" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9d3ba662913483d6722303f619e75ea10b7855b0f8e0d72799cf8621bb488f" +checksum = "8dcd332a5496c026f1e14b7f3d2b7bd98e509660c04239c58b0ba38a12daded4" dependencies = [ - "basic-toml", "glob", - "once_cell", "serde", "serde_derive", "serde_json", + "target-triple", "termcolor", + "toml", ] +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + [[package]] name = "typenum" version = "1.17.0" @@ -4311,30 +4420,36 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + [[package]] name = "unicode-segmentation" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-truncate" @@ -4344,14 +4459,20 @@ checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ "itertools 0.13.0", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", ] [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "untrusted" @@ -4361,9 +4482,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna 1.0.3", @@ -4384,15 +4505,15 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.7.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" dependencies = [ "serde", ] @@ -4441,9 +4562,39 @@ dependencies = [ [[package]] name = "value-bag" -version = "1.8.0" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" +dependencies = [ + "value-bag-serde1", + "value-bag-sval2", +] + +[[package]] +name = "value-bag-serde1" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b" +dependencies = [ + "erased-serde", + "serde", + "serde_fmt", +] + +[[package]] +name = "value-bag-sval2" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec26a25bd6fca441cdd0f769fd7f891bae119f996de31f86a5eddccef54c1d" +checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a" +dependencies = [ + "sval", + "sval_buffer", + "sval_dynamic", + "sval_fmt", + "sval_json", + "sval_ref", + "sval_serde", +] [[package]] name = "vcpkg" @@ -4453,9 +4604,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" @@ -4468,9 +4619,9 @@ dependencies = [ [[package]] name = "waker-fn" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" [[package]] name = "walkdir" @@ -4505,46 +4656,47 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4552,28 +4704,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -4581,9 +4733,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.3" +version = "0.26.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" dependencies = [ "rustls-pki-types", ] @@ -4597,14 +4749,14 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.31", + "rustix 0.38.43", ] [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ "redox_syscall", "wasite", @@ -4628,11 +4780,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -4647,7 +4799,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.6", ] [[package]] @@ -4665,7 +4817,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -4685,17 +4846,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -4706,9 +4868,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -4718,9 +4880,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -4730,9 +4892,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -4742,9 +4910,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -4754,9 +4922,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -4766,9 +4934,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -4778,15 +4946,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -4814,9 +4982,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -4826,76 +4994,63 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", "synstructure", ] [[package]] name = "zeroize" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.87", -] +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerovec" @@ -4916,5 +5071,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.96", ] From 838a239a2ca9bee0a4c1a78806589a7197916408 Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 16 Jan 2025 00:41:57 +0800 Subject: [PATCH 05/66] docs: add example for postgres enums with type TEXT (#3655) * docs: add example for postgres enums with type TEXT Signed-off-by: tison * revert GitHub naming Signed-off-by: tison * add note Signed-off-by: tison --------- Signed-off-by: tison --- sqlx-postgres/src/types/mod.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 26feb05580..747345518a 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -179,6 +179,18 @@ //! enum Mood { Sad = 0, Ok = 1, Happy = 2 } //! ``` //! +//! Rust enumerations may also be defined to be represented as a string using `type_name = "text"`. +//! The following type expects a SQL type of `TEXT` and will convert to/from the Rust enumeration. +//! +//! ```rust,ignore +//! #[derive(sqlx::Type)] +//! #[sqlx(type_name = "text")] +//! enum Mood { Sad, Ok, Happy } +//! ``` +//! +//! Note that an error can occur if you attempt to decode a value not contained within the enum +//! definition. +//! use crate::type_info::PgTypeKind; use crate::{PgTypeInfo, Postgres}; From f6d2fa3a3de1d888f4e36096dd56a3f5e0c95771 Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Thu, 16 Jan 2025 02:08:16 +0100 Subject: [PATCH 06/66] fix: handle nullable values by printing NULL instead of panicking (#3686) --- sqlx-core/src/type_checking.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/sqlx-core/src/type_checking.rs b/sqlx-core/src/type_checking.rs index 384d15f42c..5766124530 100644 --- a/sqlx-core/src/type_checking.rs +++ b/sqlx-core/src/type_checking.rs @@ -72,11 +72,17 @@ where match T::decode(value.as_ref()) { Ok(value) => Debug::fmt(&value, f), - Err(e) => f.write_fmt(format_args!( - "(error decoding SQL type {} as {}: {e:?})", - info.name(), - std::any::type_name::() - )), + Err(e) => { + if e.is::() { + f.write_str("NULL") + } else { + f.write_fmt(format_args!( + "(error decoding SQL type {} as {}: {e:?})", + info.name(), + std::any::type_name::() + )) + } + } } }, } From aae800090b25e26c333a976abd11bac7a3d172a9 Mon Sep 17 00:00:00 2001 From: Austin Schey Date: Thu, 23 Jan 2025 16:19:45 -0800 Subject: [PATCH 07/66] feat(sqlite): add preupdate hook (#3625) * feat: add preupdate hook * address some PR comments * add SqliteValueRef variant that takes a borrowed sqlite value pointer * add PhantomData for additional lifetime check --- .github/workflows/sqlx.yml | 6 +- Cargo.lock | 1 + Cargo.toml | 3 +- README.md | 4 + sqlx-sqlite/Cargo.toml | 3 + sqlx-sqlite/src/connection/establish.rs | 2 + sqlx-sqlite/src/connection/mod.rs | 50 +++++ sqlx-sqlite/src/connection/preupdate_hook.rs | 160 ++++++++++++++ sqlx-sqlite/src/lib.rs | 2 + sqlx-sqlite/src/value.rs | 139 ++++++++---- src/lib.rs | 8 + tests/sqlite/sqlite.rs | 218 ++++++++++++++++++- 12 files changed, 545 insertions(+), 51 deletions(-) create mode 100644 sqlx-sqlite/src/connection/preupdate_hook.rs diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index 1a91e1fa83..3f1f44d393 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -39,7 +39,7 @@ jobs: - run: > cargo clippy --no-default-features - --features all-databases,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }},macros + --features all-databases,_unstable-all-types,sqlite-preupdate-hook,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }},macros -- -D warnings # Run beta for new warnings but don't break the build. @@ -47,7 +47,7 @@ jobs: - run: > cargo +beta clippy --no-default-features - --features all-databases,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }},macros + --features all-databases,_unstable-all-types,sqlite-preupdate-hook,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }},macros --target-dir target/beta/ check-minimal-versions: @@ -140,7 +140,7 @@ jobs: - run: > cargo test --no-default-features - --features any,macros,${{ matrix.linking }},_unstable-all-types,runtime-${{ matrix.runtime }} + --features any,macros,${{ matrix.linking }},${{ matrix.linking == 'sqlite' && 'sqlite-preupdate-hook,' || ''}}_unstable-all-types,runtime-${{ matrix.runtime }} -- --test-threads=1 env: diff --git a/Cargo.lock b/Cargo.lock index 30dd3be30d..43b925c108 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3879,6 +3879,7 @@ dependencies = [ "serde_urlencoded", "sqlx", "sqlx-core", + "thiserror 2.0.11", "time", "tracing", "url", diff --git a/Cargo.toml b/Cargo.toml index 316dc471e1..f93ed3dded 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ authors.workspace = true repository.workspace = true [package.metadata.docs.rs] -features = ["all-databases", "_unstable-all-types"] +features = ["all-databases", "_unstable-all-types", "sqlite-preupdate-hook"] rustdoc-args = ["--cfg", "docsrs"] [features] @@ -108,6 +108,7 @@ postgres = ["sqlx-postgres", "sqlx-macros?/postgres"] mysql = ["sqlx-mysql", "sqlx-macros?/mysql"] sqlite = ["_sqlite", "sqlx-sqlite/bundled", "sqlx-macros?/sqlite"] sqlite-unbundled = ["_sqlite", "sqlx-sqlite/unbundled", "sqlx-macros?/sqlite-unbundled"] +sqlite-preupdate-hook = ["sqlx-sqlite/preupdate-hook"] # types json = ["sqlx-macros?/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sqlite?/json"] diff --git a/README.md b/README.md index 4d4a2338ec..15d68bbb42 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,10 @@ be removed in the future. * May result in link errors if the SQLite version is too old. Version `3.20.0` or newer is recommended. * Can increase build time due to the use of bindgen. +- `sqlite-preupdate-hook`: enables SQLite's [preupdate hook](https://sqlite.org/c3ref/preupdate_count.html) API. + * Exposed as a separate feature because it's generally not enabled by default. + * Using this feature with `sqlite-unbundled` may cause linker failures if the system SQLite version does not support it. + - `any`: Add support for the `Any` database driver, which can proxy to a database driver at runtime. - `derive`: Add support for the derive family macros, those are `FromRow`, `Type`, `Encode`, `Decode`. diff --git a/sqlx-sqlite/Cargo.toml b/sqlx-sqlite/Cargo.toml index 391bf4523c..5ad57546e7 100644 --- a/sqlx-sqlite/Cargo.toml +++ b/sqlx-sqlite/Cargo.toml @@ -23,6 +23,8 @@ uuid = ["dep:uuid", "sqlx-core/uuid"] regexp = ["dep:regex"] +preupdate-hook = ["libsqlite3-sys/preupdate_hook"] + bundled = ["libsqlite3-sys/bundled"] unbundled = ["libsqlite3-sys/buildtime_bindgen"] @@ -48,6 +50,7 @@ atoi = "2.0" log = "0.4.18" tracing = { version = "0.1.37", features = ["log"] } +thiserror = "2.0.0" serde = { version = "1.0.145", features = ["derive"], optional = true } regex = { version = "1.5.5", optional = true } diff --git a/sqlx-sqlite/src/connection/establish.rs b/sqlx-sqlite/src/connection/establish.rs index 40f9b4c302..5b8aa01b62 100644 --- a/sqlx-sqlite/src/connection/establish.rs +++ b/sqlx-sqlite/src/connection/establish.rs @@ -296,6 +296,8 @@ impl EstablishParams { log_settings: self.log_settings.clone(), progress_handler_callback: None, update_hook_callback: None, + #[cfg(feature = "preupdate-hook")] + preupdate_hook_callback: None, commit_hook_callback: None, rollback_hook_callback: None, }) diff --git a/sqlx-sqlite/src/connection/mod.rs b/sqlx-sqlite/src/connection/mod.rs index a579b8a605..c1f9d46da8 100644 --- a/sqlx-sqlite/src/connection/mod.rs +++ b/sqlx-sqlite/src/connection/mod.rs @@ -14,6 +14,8 @@ use libsqlite3_sys::{ sqlite3, sqlite3_commit_hook, sqlite3_progress_handler, sqlite3_rollback_hook, sqlite3_update_hook, SQLITE_DELETE, SQLITE_INSERT, SQLITE_UPDATE, }; +#[cfg(feature = "preupdate-hook")] +pub use preupdate_hook::*; pub(crate) use handle::ConnectionHandle; use sqlx_core::common::StatementCache; @@ -36,6 +38,8 @@ mod executor; mod explain; mod handle; pub(crate) mod intmap; +#[cfg(feature = "preupdate-hook")] +mod preupdate_hook; mod worker; @@ -88,6 +92,7 @@ pub struct UpdateHookResult<'a> { pub table: &'a str, pub rowid: i64, } + pub(crate) struct UpdateHookHandler(NonNull); unsafe impl Send for UpdateHookHandler {} @@ -112,6 +117,8 @@ pub(crate) struct ConnectionState { progress_handler_callback: Option, update_hook_callback: Option, + #[cfg(feature = "preupdate-hook")] + preupdate_hook_callback: Option, commit_hook_callback: Option, @@ -138,6 +145,16 @@ impl ConnectionState { } } + #[cfg(feature = "preupdate-hook")] + pub(crate) fn remove_preupdate_hook(&mut self) { + if let Some(mut handler) = self.preupdate_hook_callback.take() { + unsafe { + libsqlite3_sys::sqlite3_preupdate_hook(self.handle.as_ptr(), None, ptr::null_mut()); + let _ = { Box::from_raw(handler.0.as_mut()) }; + } + } + } + pub(crate) fn remove_commit_hook(&mut self) { if let Some(mut handler) = self.commit_hook_callback.take() { unsafe { @@ -421,6 +438,34 @@ impl LockedSqliteHandle<'_> { } } + /// Registers a hook that is invoked prior to each `INSERT`, `UPDATE`, and `DELETE` operation on a database table. + /// At most one preupdate hook may be registered at a time on a single database connection. + /// + /// The preupdate hook only fires for changes to real database tables; + /// it is not invoked for changes to virtual tables or to system tables like sqlite_sequence or sqlite_stat1. + /// + /// See https://sqlite.org/c3ref/preupdate_count.html + #[cfg(feature = "preupdate-hook")] + pub fn set_preupdate_hook(&mut self, callback: F) + where + F: FnMut(PreupdateHookResult) + Send + 'static, + { + unsafe { + let callback_boxed = Box::new(callback); + // SAFETY: `Box::into_raw()` always returns a non-null pointer. + let callback = NonNull::new_unchecked(Box::into_raw(callback_boxed)); + let handler = callback.as_ptr() as *mut _; + self.guard.remove_preupdate_hook(); + self.guard.preupdate_hook_callback = Some(PreupdateHookHandler(callback)); + + libsqlite3_sys::sqlite3_preupdate_hook( + self.as_raw_handle().as_mut(), + Some(preupdate_hook::), + handler, + ); + } + } + /// Sets a commit hook that is invoked whenever a transaction is committed. If the commit hook callback /// returns `false`, then the operation is turned into a ROLLBACK. /// @@ -485,6 +530,11 @@ impl LockedSqliteHandle<'_> { self.guard.remove_update_hook(); } + #[cfg(feature = "preupdate-hook")] + pub fn remove_preupdate_hook(&mut self) { + self.guard.remove_preupdate_hook(); + } + pub fn remove_commit_hook(&mut self) { self.guard.remove_commit_hook(); } diff --git a/sqlx-sqlite/src/connection/preupdate_hook.rs b/sqlx-sqlite/src/connection/preupdate_hook.rs new file mode 100644 index 0000000000..edcb078124 --- /dev/null +++ b/sqlx-sqlite/src/connection/preupdate_hook.rs @@ -0,0 +1,160 @@ +use super::SqliteOperation; +use crate::type_info::DataType; +use crate::{SqliteError, SqliteTypeInfo, SqliteValueRef}; + +use libsqlite3_sys::{ + sqlite3, sqlite3_preupdate_count, sqlite3_preupdate_depth, sqlite3_preupdate_new, + sqlite3_preupdate_old, sqlite3_value, sqlite3_value_type, SQLITE_OK, +}; +use std::ffi::CStr; +use std::marker::PhantomData; +use std::os::raw::{c_char, c_int, c_void}; +use std::panic::catch_unwind; +use std::ptr; +use std::ptr::NonNull; + +#[derive(Debug, thiserror::Error)] +pub enum PreupdateError { + /// Error returned from the database. + #[error("error returned from database: {0}")] + Database(#[source] SqliteError), + /// Index is not within the valid column range + #[error("{0} is not within the valid column range")] + ColumnIndexOutOfBounds(i32), + /// Column value accessor was invoked from an invalid operation + #[error("column value accessor was invoked from an invalid operation")] + InvalidOperation, +} + +pub(crate) struct PreupdateHookHandler( + pub(super) NonNull, +); +unsafe impl Send for PreupdateHookHandler {} + +#[derive(Debug)] +pub struct PreupdateHookResult<'a> { + pub operation: SqliteOperation, + pub database: &'a str, + pub table: &'a str, + db: *mut sqlite3, + // The database pointer should not be usable after the preupdate hook. + // The lifetime on this struct needs to ensure it cannot outlive the callback. + _db_lifetime: PhantomData<&'a ()>, + old_row_id: i64, + new_row_id: i64, +} + +impl<'a> PreupdateHookResult<'a> { + /// Gets the amount of columns in the row being inserted, deleted, or updated. + pub fn get_column_count(&self) -> i32 { + unsafe { sqlite3_preupdate_count(self.db) } + } + + /// Gets the depth of the query that triggered the preupdate hook. + /// Returns 0 if the preupdate callback was invoked as a result of + /// a direct insert, update, or delete operation; + /// 1 for inserts, updates, or deletes invoked by top-level triggers; + /// 2 for changes resulting from triggers called by top-level triggers; and so forth. + pub fn get_query_depth(&self) -> i32 { + unsafe { sqlite3_preupdate_depth(self.db) } + } + + /// Gets the row id of the row being updated/deleted. + /// Returns an error if called from an insert operation. + pub fn get_old_row_id(&self) -> Result { + if self.operation == SqliteOperation::Insert { + return Err(PreupdateError::InvalidOperation); + } + Ok(self.old_row_id) + } + + /// Gets the row id of the row being inserted/updated. + /// Returns an error if called from a delete operation. + pub fn get_new_row_id(&self) -> Result { + if self.operation == SqliteOperation::Delete { + return Err(PreupdateError::InvalidOperation); + } + Ok(self.new_row_id) + } + + /// Gets the value of the row being updated/deleted at the specified index. + /// Returns an error if called from an insert operation or the index is out of bounds. + pub fn get_old_column_value(&self, i: i32) -> Result, PreupdateError> { + if self.operation == SqliteOperation::Insert { + return Err(PreupdateError::InvalidOperation); + } + self.validate_column_index(i)?; + + let mut p_value: *mut sqlite3_value = ptr::null_mut(); + unsafe { + let ret = sqlite3_preupdate_old(self.db, i, &mut p_value); + self.get_value(ret, p_value) + } + } + + /// Gets the value of the row being inserted/updated at the specified index. + /// Returns an error if called from a delete operation or the index is out of bounds. + pub fn get_new_column_value(&self, i: i32) -> Result, PreupdateError> { + if self.operation == SqliteOperation::Delete { + return Err(PreupdateError::InvalidOperation); + } + self.validate_column_index(i)?; + + let mut p_value: *mut sqlite3_value = ptr::null_mut(); + unsafe { + let ret = sqlite3_preupdate_new(self.db, i, &mut p_value); + self.get_value(ret, p_value) + } + } + + fn validate_column_index(&self, i: i32) -> Result<(), PreupdateError> { + if i < 0 || i >= self.get_column_count() { + return Err(PreupdateError::ColumnIndexOutOfBounds(i)); + } + Ok(()) + } + + unsafe fn get_value( + &self, + ret: i32, + p_value: *mut sqlite3_value, + ) -> Result, PreupdateError> { + if ret != SQLITE_OK { + return Err(PreupdateError::Database(SqliteError::new(self.db))); + } + let data_type = DataType::from_code(sqlite3_value_type(p_value)); + // SAFETY: SQLite will free the sqlite3_value when the callback returns + Ok(SqliteValueRef::borrowed(p_value, SqliteTypeInfo(data_type))) + } +} + +pub(super) extern "C" fn preupdate_hook( + callback: *mut c_void, + db: *mut sqlite3, + op_code: c_int, + database: *const c_char, + table: *const c_char, + old_row_id: i64, + new_row_id: i64, +) where + F: FnMut(PreupdateHookResult) + Send + 'static, +{ + unsafe { + let _ = catch_unwind(|| { + let callback: *mut F = callback.cast::(); + let operation: SqliteOperation = op_code.into(); + let database = CStr::from_ptr(database).to_str().unwrap_or_default(); + let table = CStr::from_ptr(table).to_str().unwrap_or_default(); + + (*callback)(PreupdateHookResult { + operation, + database, + table, + old_row_id, + new_row_id, + db, + _db_lifetime: PhantomData, + }) + }); + } +} diff --git a/sqlx-sqlite/src/lib.rs b/sqlx-sqlite/src/lib.rs index 3bcb6d148d..f1a45c3d34 100644 --- a/sqlx-sqlite/src/lib.rs +++ b/sqlx-sqlite/src/lib.rs @@ -46,6 +46,8 @@ use std::sync::atomic::AtomicBool; pub use arguments::{SqliteArgumentValue, SqliteArguments}; pub use column::SqliteColumn; +#[cfg(feature = "preupdate-hook")] +pub use connection::PreupdateHookResult; pub use connection::{LockedSqliteHandle, SqliteConnection, SqliteOperation, UpdateHookResult}; pub use database::Sqlite; pub use error::SqliteError; diff --git a/sqlx-sqlite/src/value.rs b/sqlx-sqlite/src/value.rs index 967b3f7476..469c4e70d5 100644 --- a/sqlx-sqlite/src/value.rs +++ b/sqlx-sqlite/src/value.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::marker::PhantomData; use std::ptr::NonNull; use std::slice::from_raw_parts; use std::str::from_utf8; @@ -17,6 +18,7 @@ use crate::{Sqlite, SqliteTypeInfo}; enum SqliteValueData<'r> { Value(&'r SqliteValue), + BorrowedHandle(ValueHandle<'r>), } pub struct SqliteValueRef<'r>(SqliteValueData<'r>); @@ -26,31 +28,44 @@ impl<'r> SqliteValueRef<'r> { Self(SqliteValueData::Value(value)) } + // SAFETY: The supplied sqlite3_value must not be null and SQLite must free it. It will not be freed on drop. + // The lifetime on this struct should tie it to whatever scope it's valid for before SQLite frees it. + #[allow(unused)] + pub(crate) unsafe fn borrowed(value: *mut sqlite3_value, type_info: SqliteTypeInfo) -> Self { + debug_assert!(!value.is_null()); + let handle = ValueHandle::new_borrowed(NonNull::new_unchecked(value), type_info); + Self(SqliteValueData::BorrowedHandle(handle)) + } + // NOTE: `int()` is deliberately omitted because it will silently truncate a wider value, // which is likely to cause bugs: // https://github.com/launchbadge/sqlx/issues/3179 // (Similar bug in Postgres): https://github.com/launchbadge/sqlx/issues/3161 pub(super) fn int64(&self) -> i64 { - match self.0 { - SqliteValueData::Value(v) => v.int64(), + match &self.0 { + SqliteValueData::Value(v) => v.0.int64(), + SqliteValueData::BorrowedHandle(v) => v.int64(), } } pub(super) fn double(&self) -> f64 { - match self.0 { - SqliteValueData::Value(v) => v.double(), + match &self.0 { + SqliteValueData::Value(v) => v.0.double(), + SqliteValueData::BorrowedHandle(v) => v.double(), } } pub(super) fn blob(&self) -> &'r [u8] { - match self.0 { - SqliteValueData::Value(v) => v.blob(), + match &self.0 { + SqliteValueData::Value(v) => v.0.blob(), + SqliteValueData::BorrowedHandle(v) => v.blob(), } } pub(super) fn text(&self) -> Result<&'r str, BoxDynError> { - match self.0 { - SqliteValueData::Value(v) => v.text(), + match &self.0 { + SqliteValueData::Value(v) => v.0.text(), + SqliteValueData::BorrowedHandle(v) => v.text(), } } } @@ -59,50 +74,66 @@ impl<'r> ValueRef<'r> for SqliteValueRef<'r> { type Database = Sqlite; fn to_owned(&self) -> SqliteValue { - match self.0 { - SqliteValueData::Value(v) => v.clone(), + match &self.0 { + SqliteValueData::Value(v) => (*v).clone(), + SqliteValueData::BorrowedHandle(v) => unsafe { + SqliteValue::new(v.value.as_ptr(), v.type_info.clone()) + }, } } fn type_info(&self) -> Cow<'_, SqliteTypeInfo> { - match self.0 { + match &self.0 { SqliteValueData::Value(v) => v.type_info(), + SqliteValueData::BorrowedHandle(v) => v.type_info(), } } fn is_null(&self) -> bool { - match self.0 { + match &self.0 { SqliteValueData::Value(v) => v.is_null(), + SqliteValueData::BorrowedHandle(v) => v.is_null(), } } } #[derive(Clone)] -pub struct SqliteValue { - pub(crate) handle: Arc, - pub(crate) type_info: SqliteTypeInfo, -} +pub struct SqliteValue(Arc>); -pub(crate) struct ValueHandle(NonNull); +pub(crate) struct ValueHandle<'a> { + value: NonNull, + type_info: SqliteTypeInfo, + free_on_drop: bool, + _sqlite_value_lifetime: PhantomData<&'a ()>, +} // SAFE: only protected value objects are stored in SqliteValue -unsafe impl Send for ValueHandle {} -unsafe impl Sync for ValueHandle {} +unsafe impl<'a> Send for ValueHandle<'a> {} +unsafe impl<'a> Sync for ValueHandle<'a> {} -impl SqliteValue { - pub(crate) unsafe fn new(value: *mut sqlite3_value, type_info: SqliteTypeInfo) -> Self { - debug_assert!(!value.is_null()); +impl ValueHandle<'static> { + fn new_owned(value: NonNull, type_info: SqliteTypeInfo) -> Self { + Self { + value, + type_info, + free_on_drop: true, + _sqlite_value_lifetime: PhantomData, + } + } +} +impl<'a> ValueHandle<'a> { + fn new_borrowed(value: NonNull, type_info: SqliteTypeInfo) -> Self { Self { + value, type_info, - handle: Arc::new(ValueHandle(NonNull::new_unchecked(sqlite3_value_dup( - value, - )))), + free_on_drop: false, + _sqlite_value_lifetime: PhantomData, } } fn type_info_opt(&self) -> Option { - let dt = DataType::from_code(unsafe { sqlite3_value_type(self.handle.0.as_ptr()) }); + let dt = DataType::from_code(unsafe { sqlite3_value_type(self.value.as_ptr()) }); if let DataType::Null = dt { None @@ -112,15 +143,15 @@ impl SqliteValue { } fn int64(&self) -> i64 { - unsafe { sqlite3_value_int64(self.handle.0.as_ptr()) } + unsafe { sqlite3_value_int64(self.value.as_ptr()) } } fn double(&self) -> f64 { - unsafe { sqlite3_value_double(self.handle.0.as_ptr()) } + unsafe { sqlite3_value_double(self.value.as_ptr()) } } - fn blob(&self) -> &[u8] { - let len = unsafe { sqlite3_value_bytes(self.handle.0.as_ptr()) }; + fn blob<'b>(&self) -> &'b [u8] { + let len = unsafe { sqlite3_value_bytes(self.value.as_ptr()) }; // This likely means UB in SQLite itself or our usage of it; // signed integer overflow is UB in the C standard. @@ -133,23 +164,15 @@ impl SqliteValue { return &[]; } - let ptr = unsafe { sqlite3_value_blob(self.handle.0.as_ptr()) } as *const u8; + let ptr = unsafe { sqlite3_value_blob(self.value.as_ptr()) } as *const u8; debug_assert!(!ptr.is_null()); unsafe { from_raw_parts(ptr, len) } } - fn text(&self) -> Result<&str, BoxDynError> { + fn text<'b>(&self) -> Result<&'b str, BoxDynError> { Ok(from_utf8(self.blob())?) } -} - -impl Value for SqliteValue { - type Database = Sqlite; - - fn as_ref(&self) -> SqliteValueRef<'_> { - SqliteValueRef::value(self) - } fn type_info(&self) -> Cow<'_, SqliteTypeInfo> { self.type_info_opt() @@ -158,18 +181,46 @@ impl Value for SqliteValue { } fn is_null(&self) -> bool { - unsafe { sqlite3_value_type(self.handle.0.as_ptr()) == SQLITE_NULL } + unsafe { sqlite3_value_type(self.value.as_ptr()) == SQLITE_NULL } } } -impl Drop for ValueHandle { +impl<'a> Drop for ValueHandle<'a> { fn drop(&mut self) { - unsafe { - sqlite3_value_free(self.0.as_ptr()); + if self.free_on_drop { + unsafe { + sqlite3_value_free(self.value.as_ptr()); + } } } } +impl SqliteValue { + // SAFETY: The sqlite3_value must be non-null and SQLite must not free it. It will be freed on drop. + pub(crate) unsafe fn new(value: *mut sqlite3_value, type_info: SqliteTypeInfo) -> Self { + debug_assert!(!value.is_null()); + let handle = + ValueHandle::new_owned(NonNull::new_unchecked(sqlite3_value_dup(value)), type_info); + Self(Arc::new(handle)) + } +} + +impl Value for SqliteValue { + type Database = Sqlite; + + fn as_ref(&self) -> SqliteValueRef<'_> { + SqliteValueRef::value(self) + } + + fn type_info(&self) -> Cow<'_, SqliteTypeInfo> { + self.0.type_info() + } + + fn is_null(&self) -> bool { + self.0.is_null() + } +} + // #[cfg(feature = "any")] // impl<'r> From> for crate::any::AnyValueRef<'r> { // #[inline] diff --git a/src/lib.rs b/src/lib.rs index 870fa703c5..ed76c5f5ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,14 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("lib.md")] +#[cfg(all( + feature = "sqlite-preupdate-hook", + not(any(feature = "sqlite", feature = "sqlite-unbundled")) +))] +compile_error!( + "sqlite-preupdate-hook requires either 'sqlite' or 'sqlite-unbundled' to be enabled" +); + pub use sqlx_core::acquire::Acquire; pub use sqlx_core::arguments::{Arguments, IntoArguments}; pub use sqlx_core::column::Column; diff --git a/tests/sqlite/sqlite.rs b/tests/sqlite/sqlite.rs index b733ccbb4c..d78e1151a9 100644 --- a/tests/sqlite/sqlite.rs +++ b/tests/sqlite/sqlite.rs @@ -2,11 +2,14 @@ use futures::TryStreamExt; use rand::{Rng, SeedableRng}; use rand_xoshiro::Xoshiro256PlusPlus; use sqlx::sqlite::{SqliteConnectOptions, SqliteOperation, SqlitePoolOptions}; +use sqlx::Decode; use sqlx::{ query, sqlite::Sqlite, sqlite::SqliteRow, Column, ConnectOptions, Connection, Executor, Row, SqliteConnection, SqlitePool, Statement, TypeInfo, }; +use sqlx::{Value, ValueRef}; use sqlx_test::new; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; #[sqlx_macros::test] @@ -798,7 +801,7 @@ async fn test_multiple_set_progress_handler_calls_drop_old_handler() -> anyhow:: #[sqlx_macros::test] async fn test_query_with_update_hook() -> anyhow::Result<()> { let mut conn = new::().await?; - + static CALLED: AtomicBool = AtomicBool::new(false); // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. let state = format!("test"); conn.lock_handle().await?.set_update_hook(move |result| { @@ -807,11 +810,13 @@ async fn test_query_with_update_hook() -> anyhow::Result<()> { assert_eq!(result.database, "main"); assert_eq!(result.table, "tweet"); assert_eq!(result.rowid, 2); + CALLED.store(true, Ordering::Relaxed); }); let _ = sqlx::query("INSERT INTO tweet ( id, text ) VALUES ( 3, 'Hello, World' )") .execute(&mut conn) .await?; + assert!(CALLED.load(Ordering::Relaxed)); Ok(()) } @@ -852,10 +857,11 @@ async fn test_multiple_set_update_hook_calls_drop_old_handler() -> anyhow::Resul #[sqlx_macros::test] async fn test_query_with_commit_hook() -> anyhow::Result<()> { let mut conn = new::().await?; - + static CALLED: AtomicBool = AtomicBool::new(false); // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. let state = format!("test"); conn.lock_handle().await?.set_commit_hook(move || { + CALLED.store(true, Ordering::Relaxed); assert_eq!(state, "test"); false }); @@ -870,7 +876,7 @@ async fn test_query_with_commit_hook() -> anyhow::Result<()> { } _ => panic!("expected an error"), } - + assert!(CALLED.load(Ordering::Relaxed)); Ok(()) } @@ -916,8 +922,10 @@ async fn test_query_with_rollback_hook() -> anyhow::Result<()> { // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. let state = format!("test"); + static CALLED: AtomicBool = AtomicBool::new(false); conn.lock_handle().await?.set_rollback_hook(move || { assert_eq!(state, "test"); + CALLED.store(true, Ordering::Relaxed); }); let mut tx = conn.begin().await?; @@ -925,6 +933,7 @@ async fn test_query_with_rollback_hook() -> anyhow::Result<()> { .execute(&mut *tx) .await?; tx.rollback().await?; + assert!(CALLED.load(Ordering::Relaxed)); Ok(()) } @@ -960,3 +969,206 @@ async fn test_multiple_set_rollback_hook_calls_drop_old_handler() -> anyhow::Res assert_eq!(1, Arc::strong_count(&ref_counted_object)); Ok(()) } + +#[cfg(feature = "sqlite-preupdate-hook")] +#[sqlx_macros::test] +async fn test_query_with_preupdate_hook_insert() -> anyhow::Result<()> { + let mut conn = new::().await?; + static CALLED: AtomicBool = AtomicBool::new(false); + // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. + let state = format!("test"); + conn.lock_handle().await?.set_preupdate_hook({ + move |result| { + assert_eq!(state, "test"); + assert_eq!(result.operation, SqliteOperation::Insert); + assert_eq!(result.database, "main"); + assert_eq!(result.table, "tweet"); + + assert_eq!(4, result.get_column_count()); + assert_eq!(2, result.get_new_row_id().unwrap()); + assert_eq!(0, result.get_query_depth()); + assert_eq!( + 4, + >::decode(result.get_new_column_value(0).unwrap()).unwrap() + ); + assert_eq!( + "Hello, World", + >::decode(result.get_new_column_value(1).unwrap()) + .unwrap() + ); + // out of bounds access should return an error + assert!(result.get_new_column_value(4).is_err()); + // old values aren't available for inserts + assert!(result.get_old_column_value(0).is_err()); + assert!(result.get_old_row_id().is_err()); + + CALLED.store(true, Ordering::Relaxed); + } + }); + + let _ = sqlx::query("INSERT INTO tweet ( id, text ) VALUES ( 4, 'Hello, World' )") + .execute(&mut conn) + .await?; + + assert!(CALLED.load(Ordering::Relaxed)); + conn.lock_handle().await?.remove_preupdate_hook(); + let _ = sqlx::query("DELETE FROM tweet where id = 4") + .execute(&mut conn) + .await?; + Ok(()) +} + +#[cfg(feature = "sqlite-preupdate-hook")] +#[sqlx_macros::test] +async fn test_query_with_preupdate_hook_delete() -> anyhow::Result<()> { + let mut conn = new::().await?; + let _ = sqlx::query("INSERT INTO tweet ( id, text ) VALUES ( 5, 'Hello, World' )") + .execute(&mut conn) + .await?; + static CALLED: AtomicBool = AtomicBool::new(false); + // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. + let state = format!("test"); + conn.lock_handle().await?.set_preupdate_hook(move |result| { + assert_eq!(state, "test"); + assert_eq!(result.operation, SqliteOperation::Delete); + assert_eq!(result.database, "main"); + assert_eq!(result.table, "tweet"); + + assert_eq!(4, result.get_column_count()); + assert_eq!(2, result.get_old_row_id().unwrap()); + assert_eq!(0, result.get_query_depth()); + assert_eq!( + 5, + >::decode(result.get_old_column_value(0).unwrap()).unwrap() + ); + assert_eq!( + "Hello, World", + >::decode(result.get_old_column_value(1).unwrap()).unwrap() + ); + // out of bounds access should return an error + assert!(result.get_old_column_value(4).is_err()); + // new values aren't available for deletes + assert!(result.get_new_column_value(0).is_err()); + assert!(result.get_new_row_id().is_err()); + + CALLED.store(true, Ordering::Relaxed); + }); + + let _ = sqlx::query("DELETE FROM tweet WHERE id = 5") + .execute(&mut conn) + .await?; + assert!(CALLED.load(Ordering::Relaxed)); + Ok(()) +} + +#[cfg(feature = "sqlite-preupdate-hook")] +#[sqlx_macros::test] +async fn test_query_with_preupdate_hook_update() -> anyhow::Result<()> { + let mut conn = new::().await?; + let _ = sqlx::query("INSERT INTO tweet ( id, text ) VALUES ( 6, 'Hello, World' )") + .execute(&mut conn) + .await?; + static CALLED: AtomicBool = AtomicBool::new(false); + let sqlite_value_stored: Arc>> = Default::default(); + // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. + let state = format!("test"); + conn.lock_handle().await?.set_preupdate_hook({ + let sqlite_value_stored = sqlite_value_stored.clone(); + move |result| { + assert_eq!(state, "test"); + assert_eq!(result.operation, SqliteOperation::Update); + assert_eq!(result.database, "main"); + assert_eq!(result.table, "tweet"); + + assert_eq!(4, result.get_column_count()); + assert_eq!(4, result.get_column_count()); + + assert_eq!(2, result.get_old_row_id().unwrap()); + assert_eq!(2, result.get_new_row_id().unwrap()); + + assert_eq!(0, result.get_query_depth()); + assert_eq!(0, result.get_query_depth()); + + assert_eq!( + 6, + >::decode(result.get_old_column_value(0).unwrap()).unwrap() + ); + assert_eq!( + 6, + >::decode(result.get_new_column_value(0).unwrap()).unwrap() + ); + + assert_eq!( + "Hello, World", + >::decode(result.get_old_column_value(1).unwrap()) + .unwrap() + ); + assert_eq!( + "Hello, World2", + >::decode(result.get_new_column_value(1).unwrap()) + .unwrap() + ); + *sqlite_value_stored.lock().unwrap() = + Some(result.get_old_column_value(0).unwrap().to_owned()); + + // out of bounds access should return an error + assert!(result.get_old_column_value(4).is_err()); + assert!(result.get_new_column_value(4).is_err()); + + CALLED.store(true, Ordering::Relaxed); + } + }); + + let _ = sqlx::query("UPDATE tweet SET text = 'Hello, World2' WHERE id = 6") + .execute(&mut conn) + .await?; + + assert!(CALLED.load(Ordering::Relaxed)); + conn.lock_handle().await?.remove_preupdate_hook(); + let _ = sqlx::query("DELETE FROM tweet where id = 6") + .execute(&mut conn) + .await?; + // Ensure that taking an owned SqliteValue maintains a valid reference after the callback returns + assert_eq!( + 6, + >::decode( + sqlite_value_stored.lock().unwrap().take().unwrap().as_ref() + ) + .unwrap() + ); + Ok(()) +} + +#[cfg(feature = "sqlite-preupdate-hook")] +#[sqlx_macros::test] +async fn test_multiple_set_preupdate_hook_calls_drop_old_handler() -> anyhow::Result<()> { + let ref_counted_object = Arc::new(0); + assert_eq!(1, Arc::strong_count(&ref_counted_object)); + + { + let mut conn = new::().await?; + + let o = ref_counted_object.clone(); + conn.lock_handle().await?.set_preupdate_hook(move |_| { + println!("{o:?}"); + }); + assert_eq!(2, Arc::strong_count(&ref_counted_object)); + + let o = ref_counted_object.clone(); + conn.lock_handle().await?.set_preupdate_hook(move |_| { + println!("{o:?}"); + }); + assert_eq!(2, Arc::strong_count(&ref_counted_object)); + + let o = ref_counted_object.clone(); + conn.lock_handle().await?.set_preupdate_hook(move |_| { + println!("{o:?}"); + }); + assert_eq!(2, Arc::strong_count(&ref_counted_object)); + + conn.lock_handle().await?.remove_preupdate_hook(); + } + + assert_eq!(1, Arc::strong_count(&ref_counted_object)); + Ok(()) +} From a83395a360296db1251e6e4138b6e6331912c3d4 Mon Sep 17 00:00:00 2001 From: Andreas Liljeqvist Date: Fri, 24 Jan 2025 02:36:55 +0100 Subject: [PATCH 08/66] Fix: nextest cleanup race condition (#3334) * remove unused trait fn `cleanup_test_dbs` * *wip* solve test cleanup race condition * check for exactly 63 chars in database name * move base64 dependency * change * Use url_safe base64 encoding * Assert quoting for database name * refactor * add mysql support? * borrow * fix borrows * ensure quoting * re-add trait cleanup_test_dbs * fix mysql insert * cargo lock * use actual field * cleanup converted path in sqlite * replace dashes with underscore in db name * refactor: remove redundant path conversion in cleanup_test and add db_name method --------- Co-authored-by: Austin Bonander --- Cargo.lock | 1 + sqlx-core/Cargo.toml | 1 + sqlx-core/src/testing/mod.rs | 15 ++- sqlx-mysql/src/testing/mod.rs | 183 ++++++++++++------------------- sqlx-postgres/src/testing/mod.rs | 135 ++++++++++------------- sqlx-sqlite/src/testing/mod.rs | 4 + 6 files changed, 145 insertions(+), 194 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43b925c108..58162ba669 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3563,6 +3563,7 @@ version = "0.8.3" dependencies = [ "async-io 1.13.0", "async-std", + "base64 0.22.0", "bigdecimal", "bit-vec", "bstr", diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index d662861470..f767507bb4 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -54,6 +54,7 @@ mac_address = { workspace = true, optional = true } uuid = { workspace = true, optional = true } async-io = { version = "1.9.0", optional = true } +base64 = { version = "0.22.0", default-features = false, features = ["std"] } bytes = "1.1.0" chrono = { version = "0.4.34", default-features = false, features = ["clock"], optional = true } crc = { version = "3", optional = true } diff --git a/sqlx-core/src/testing/mod.rs b/sqlx-core/src/testing/mod.rs index d82d1a3616..051353383b 100644 --- a/sqlx-core/src/testing/mod.rs +++ b/sqlx-core/src/testing/mod.rs @@ -3,7 +3,9 @@ use std::time::Duration; use futures_core::future::BoxFuture; +use base64::{engine::general_purpose::URL_SAFE, Engine as _}; pub use fixtures::FixtureSnapshot; +use sha2::{Digest, Sha512}; use crate::connection::{ConnectOptions, Connection}; use crate::database::Database; @@ -41,6 +43,17 @@ pub trait TestSupport: Database { /// This snapshot can then be used to generate test fixtures. fn snapshot(conn: &mut Self::Connection) -> BoxFuture<'_, Result, Error>>; + + /// Generate a unique database name for the given test path. + fn db_name(args: &TestArgs) -> String { + let mut hasher = Sha512::new(); + hasher.update(args.test_path.as_bytes()); + let hash = hasher.finalize(); + let hash = URL_SAFE.encode(&hash[..39]); + let db_name = format!("_sqlx_test_{}", hash).replace('-', "_"); + debug_assert!(db_name.len() == 63); + db_name + } } pub struct TestFixture { @@ -217,7 +230,7 @@ where let res = test_fn(test_context.pool_opts, test_context.connect_opts).await; if res.is_success() { - if let Err(e) = DB::cleanup_test(&test_context.db_name).await { + if let Err(e) = DB::cleanup_test(&DB::db_name(&args)).await { eprintln!( "failed to delete database {:?}: {}", test_context.db_name, e diff --git a/sqlx-mysql/src/testing/mod.rs b/sqlx-mysql/src/testing/mod.rs index 2a9216d1b8..9e462c1131 100644 --- a/sqlx-mysql/src/testing/mod.rs +++ b/sqlx-mysql/src/testing/mod.rs @@ -1,29 +1,25 @@ -use std::fmt::Write; use std::ops::Deref; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, SystemTime}; +use std::time::Duration; use futures_core::future::BoxFuture; use once_cell::sync::OnceCell; - -use crate::connection::Connection; +use sqlx_core::connection::Connection; +use sqlx_core::query_builder::QueryBuilder; +use sqlx_core::query_scalar::query_scalar; +use std::fmt::Write; use crate::error::Error; use crate::executor::Executor; use crate::pool::{Pool, PoolOptions}; use crate::query::query; -use crate::query_builder::QueryBuilder; -use crate::query_scalar::query_scalar; use crate::{MySql, MySqlConnectOptions, MySqlConnection}; pub(crate) use sqlx_core::testing::*; // Using a blocking `OnceCell` here because the critical sections are short. static MASTER_POOL: OnceCell> = OnceCell::new(); -// Automatically delete any databases created before the start of the test binary. -static DO_CLEANUP: AtomicBool = AtomicBool::new(true); impl TestSupport for MySql { fn test_context(args: &TestArgs) -> BoxFuture<'_, Result, Error>> { @@ -38,17 +34,7 @@ impl TestSupport for MySql { .acquire() .await?; - let db_id = db_id(db_name); - - conn.execute(&format!("drop database if exists {db_name};")[..]) - .await?; - - query("delete from _sqlx_test_databases where db_id = ?") - .bind(db_id) - .execute(&mut *conn) - .await?; - - Ok(()) + do_cleanup(&mut conn, db_name).await }) } @@ -58,13 +44,55 @@ impl TestSupport for MySql { let mut conn = MySqlConnection::connect(&url).await?; - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); + let delete_db_names: Vec = + query_scalar("select db_name from _sqlx_test_databases") + .fetch_all(&mut conn) + .await?; + + if delete_db_names.is_empty() { + return Ok(None); + } + + let mut deleted_db_names = Vec::with_capacity(delete_db_names.len()); + + let mut command = String::new(); + + for db_name in &delete_db_names { + command.clear(); + + let db_name = format!("_sqlx_test_database_{db_name}"); + + writeln!(command, "drop database if exists {db_name:?};").ok(); + match conn.execute(&*command).await { + Ok(_deleted) => { + deleted_db_names.push(db_name); + } + // Assume a database error just means the DB is still in use. + Err(Error::Database(dbe)) => { + eprintln!("could not clean test database {db_name:?}: {dbe}") + } + // Bubble up other errors + Err(e) => return Err(e), + } + } + + if deleted_db_names.is_empty() { + return Ok(None); + } + + let mut query = + QueryBuilder::new("delete from _sqlx_test_databases where db_name in ("); + + let mut separated = query.separated(","); + + for db_name in &deleted_db_names { + separated.push_bind(db_name); + } + + query.push(")").build().execute(&mut conn).await?; - let num_deleted = do_cleanup(&mut conn, now).await?; let _ = conn.close().await; - Ok(Some(num_deleted)) + Ok(Some(delete_db_names.len())) }) } @@ -117,7 +145,7 @@ async fn test_context(args: &TestArgs) -> Result, Error> { conn.execute( r#" create table if not exists _sqlx_test_databases ( - db_id bigint unsigned primary key auto_increment, + db_name text primary key, test_path text not null, created_at timestamp not null default current_timestamp ); @@ -125,34 +153,19 @@ async fn test_context(args: &TestArgs) -> Result, Error> { ) .await?; - // Record the current time _before_ we acquire the `DO_CLEANUP` permit. This - // prevents the first test thread from accidentally deleting new test dbs - // created by other test threads if we're a bit slow. - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); - - // Only run cleanup if the test binary just started. - if DO_CLEANUP.swap(false, Ordering::SeqCst) { - do_cleanup(&mut conn, now).await?; - } + let db_name = MySql::db_name(args); + do_cleanup(&mut conn, &db_name).await?; - query("insert into _sqlx_test_databases(test_path) values (?)") + query("insert into _sqlx_test_databases(db_name, test_path) values (?, ?)") + .bind(&db_name) .bind(args.test_path) .execute(&mut *conn) .await?; - // MySQL doesn't have `INSERT ... RETURNING` - let new_db_id: u64 = query_scalar("select last_insert_id()") - .fetch_one(&mut *conn) - .await?; - - let new_db_name = db_name(new_db_id); - - conn.execute(&format!("create database {new_db_name}")[..]) + conn.execute(&format!("create database {db_name:?}")[..]) .await?; - eprintln!("created database {new_db_name}"); + eprintln!("created database {db_name}"); Ok(TestContext { pool_opts: PoolOptions::new() @@ -167,74 +180,18 @@ async fn test_context(args: &TestArgs) -> Result, Error> { .connect_options() .deref() .clone() - .database(&new_db_name), - db_name: new_db_name, + .database(&db_name), + db_name, }) } -async fn do_cleanup(conn: &mut MySqlConnection, created_before: Duration) -> Result { - // since SystemTime is not monotonic we added a little margin here to avoid race conditions with other threads - let created_before_as_secs = created_before.as_secs() - 2; - let delete_db_ids: Vec = query_scalar( - "select db_id from _sqlx_test_databases \ - where created_at < from_unixtime(?)", - ) - .bind(created_before_as_secs) - .fetch_all(&mut *conn) - .await?; - - if delete_db_ids.is_empty() { - return Ok(0); - } - - let mut deleted_db_ids = Vec::with_capacity(delete_db_ids.len()); - - let mut command = String::new(); - - for db_id in delete_db_ids { - command.clear(); - - let db_name = db_name(db_id); - - writeln!(command, "drop database if exists {db_name}").ok(); - match conn.execute(&*command).await { - Ok(_deleted) => { - deleted_db_ids.push(db_id); - } - // Assume a database error just means the DB is still in use. - Err(Error::Database(dbe)) => { - eprintln!("could not clean test database {db_id:?}: {dbe}") - } - // Bubble up other errors - Err(e) => return Err(e), - } - } - - let mut query = QueryBuilder::new("delete from _sqlx_test_databases where db_id in ("); - - let mut separated = query.separated(","); - - for db_id in &deleted_db_ids { - separated.push_bind(db_id); - } - - query.push(")").build().execute(&mut *conn).await?; - - Ok(deleted_db_ids.len()) -} - -fn db_name(id: u64) -> String { - format!("_sqlx_test_database_{id}") -} - -fn db_id(name: &str) -> u64 { - name.trim_start_matches("_sqlx_test_database_") - .parse() - .unwrap_or_else(|_1| panic!("failed to parse ID from database name {name:?}")) -} +async fn do_cleanup(conn: &mut MySqlConnection, db_name: &str) -> Result<(), Error> { + let delete_db_command = format!("drop database if exists {db_name:?};"); + conn.execute(&*delete_db_command).await?; + query("delete from _sqlx_test.databases where db_name = $1::text") + .bind(db_name) + .execute(&mut *conn) + .await?; -#[test] -fn test_db_name_id() { - assert_eq!(db_name(12345), "_sqlx_test_database_12345"); - assert_eq!(db_id("_sqlx_test_database_12345"), 12345); + Ok(()) } diff --git a/sqlx-postgres/src/testing/mod.rs b/sqlx-postgres/src/testing/mod.rs index fb36ab4136..a927191dc9 100644 --- a/sqlx-postgres/src/testing/mod.rs +++ b/sqlx-postgres/src/testing/mod.rs @@ -1,20 +1,18 @@ use std::fmt::Write; use std::ops::Deref; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::{Duration, SystemTime}; +use std::time::Duration; use futures_core::future::BoxFuture; use once_cell::sync::OnceCell; - -use crate::connection::Connection; +use sqlx_core::connection::Connection; +use sqlx_core::query_scalar::query_scalar; use crate::error::Error; use crate::executor::Executor; use crate::pool::{Pool, PoolOptions}; use crate::query::query; -use crate::query_scalar::query_scalar; use crate::{PgConnectOptions, PgConnection, Postgres}; pub(crate) use sqlx_core::testing::*; @@ -22,7 +20,6 @@ pub(crate) use sqlx_core::testing::*; // Using a blocking `OnceCell` here because the critical sections are short. static MASTER_POOL: OnceCell> = OnceCell::new(); // Automatically delete any databases created before the start of the test binary. -static DO_CLEANUP: AtomicBool = AtomicBool::new(true); impl TestSupport for Postgres { fn test_context(args: &TestArgs) -> BoxFuture<'_, Result, Error>> { @@ -37,15 +34,7 @@ impl TestSupport for Postgres { .acquire() .await?; - conn.execute(&format!("drop database if exists {db_name:?};")[..]) - .await?; - - query("delete from _sqlx_test.databases where db_name = $1") - .bind(db_name) - .execute(&mut *conn) - .await?; - - Ok(()) + do_cleanup(&mut conn, db_name).await }) } @@ -55,13 +44,42 @@ impl TestSupport for Postgres { let mut conn = PgConnection::connect(&url).await?; - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); + let delete_db_names: Vec = + query_scalar("select db_name from _sqlx_test.databases") + .fetch_all(&mut conn) + .await?; + + if delete_db_names.is_empty() { + return Ok(None); + } + + let mut deleted_db_names = Vec::with_capacity(delete_db_names.len()); + + let mut command = String::new(); + + for db_name in &delete_db_names { + command.clear(); + writeln!(command, "drop database if exists {db_name:?};").ok(); + match conn.execute(&*command).await { + Ok(_deleted) => { + deleted_db_names.push(db_name); + } + // Assume a database error just means the DB is still in use. + Err(Error::Database(dbe)) => { + eprintln!("could not clean test database {db_name:?}: {dbe}") + } + // Bubble up other errors + Err(e) => return Err(e), + } + } + + query("delete from _sqlx_test.databases where db_name = any($1::text[])") + .bind(&deleted_db_names) + .execute(&mut conn) + .await?; - let num_deleted = do_cleanup(&mut conn, now).await?; let _ = conn.close().await; - Ok(Some(num_deleted)) + Ok(Some(delete_db_names.len())) }) } @@ -135,31 +153,22 @@ async fn test_context(args: &TestArgs) -> Result, Error> { ) .await?; - // Record the current time _before_ we acquire the `DO_CLEANUP` permit. This - // prevents the first test thread from accidentally deleting new test dbs - // created by other test threads if we're a bit slow. - let now = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap(); + let db_name = Postgres::db_name(args); + do_cleanup(&mut conn, &db_name).await?; - // Only run cleanup if the test binary just started. - if DO_CLEANUP.swap(false, Ordering::SeqCst) { - do_cleanup(&mut conn, now).await?; - } - - let new_db_name: String = query_scalar( + query( r#" - insert into _sqlx_test.databases(db_name, test_path) - select '_sqlx_test_' || nextval('_sqlx_test.database_ids'), $1 - returning db_name + insert into _sqlx_test.databases(db_name, test_path) values ($1, $2) "#, ) + .bind(&db_name) .bind(args.test_path) - .fetch_one(&mut *conn) + .execute(&mut *conn) .await?; - conn.execute(&format!("create database {new_db_name:?}")[..]) - .await?; + let create_command = format!("create database {db_name:?}"); + debug_assert!(create_command.starts_with("create database \"")); + conn.execute(&(create_command)[..]).await?; Ok(TestContext { pool_opts: PoolOptions::new() @@ -174,52 +183,18 @@ async fn test_context(args: &TestArgs) -> Result, Error> { .connect_options() .deref() .clone() - .database(&new_db_name), - db_name: new_db_name, + .database(&db_name), + db_name, }) } -async fn do_cleanup(conn: &mut PgConnection, created_before: Duration) -> Result { - // since SystemTime is not monotonic we added a little margin here to avoid race conditions with other threads - let created_before = i64::try_from(created_before.as_secs()).unwrap() - 2; - - let delete_db_names: Vec = query_scalar( - "select db_name from _sqlx_test.databases \ - where created_at < (to_timestamp($1) at time zone 'UTC')", - ) - .bind(created_before) - .fetch_all(&mut *conn) - .await?; - - if delete_db_names.is_empty() { - return Ok(0); - } - - let mut deleted_db_names = Vec::with_capacity(delete_db_names.len()); - let delete_db_names = delete_db_names.into_iter(); - - let mut command = String::new(); - - for db_name in delete_db_names { - command.clear(); - writeln!(command, "drop database if exists {db_name:?};").ok(); - match conn.execute(&*command).await { - Ok(_deleted) => { - deleted_db_names.push(db_name); - } - // Assume a database error just means the DB is still in use. - Err(Error::Database(dbe)) => { - eprintln!("could not clean test database {db_name:?}: {dbe}") - } - // Bubble up other errors - Err(e) => return Err(e), - } - } - - query("delete from _sqlx_test.databases where db_name = any($1::text[])") - .bind(&deleted_db_names) +async fn do_cleanup(conn: &mut PgConnection, db_name: &str) -> Result<(), Error> { + let delete_db_command = format!("drop database if exists {db_name:?};"); + conn.execute(&*delete_db_command).await?; + query("delete from _sqlx_test.databases where db_name = $1::text") + .bind(db_name) .execute(&mut *conn) .await?; - Ok(deleted_db_names.len()) + Ok(()) } diff --git a/sqlx-sqlite/src/testing/mod.rs b/sqlx-sqlite/src/testing/mod.rs index 3398c6b493..324b519119 100644 --- a/sqlx-sqlite/src/testing/mod.rs +++ b/sqlx-sqlite/src/testing/mod.rs @@ -30,6 +30,10 @@ impl TestSupport for Sqlite { ) -> BoxFuture<'_, Result, Error>> { todo!() } + + fn db_name(args: &TestArgs) -> String { + convert_path(args.test_path) + } } async fn test_context(args: &TestArgs) -> Result, Error> { From a408c490fdf7afbd7b0f1869ef5265b4c00ba43f Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Fri, 24 Jan 2025 23:30:02 +0100 Subject: [PATCH 09/66] fix(postgres) use signed int for length prefix in `PgCopyIn` (#3701) --- Cargo.lock | 2 +- sqlx-postgres/src/copy.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58162ba669..c112899415 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3563,7 +3563,7 @@ version = "0.8.3" dependencies = [ "async-io 1.13.0", "async-std", - "base64 0.22.0", + "base64 0.22.1", "bigdecimal", "bit-vec", "bstr", diff --git a/sqlx-postgres/src/copy.rs b/sqlx-postgres/src/copy.rs index ddc187e993..fd9ed7e85c 100644 --- a/sqlx-postgres/src/copy.rs +++ b/sqlx-postgres/src/copy.rs @@ -230,10 +230,10 @@ impl> PgCopyIn { } // Write the length - let read32 = u32::try_from(read) - .map_err(|_| err_protocol!("number of bytes read exceeds 2^32: {}", read))?; + let read32 = i32::try_from(read) + .map_err(|_| err_protocol!("number of bytes read exceeds 2^31 - 1: {}", read))?; - (&mut buf.get_mut()[1..]).put_u32(read32 + 4); + (&mut buf.get_mut()[1..]).put_i32(read32 + 4); conn.inner.stream.flush().await?; } From ad1d7a8aa5056143fc704e802cc7acc753d07aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20SAISSY?= Date: Fri, 24 Jan 2025 23:41:06 +0100 Subject: [PATCH 10/66] Derive clone and debug for postgresql arguments (#3687) --- sqlx-postgres/src/arguments.rs | 20 ++++++++++++++++---- sqlx-postgres/src/type_info.rs | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/sqlx-postgres/src/arguments.rs b/sqlx-postgres/src/arguments.rs index bc7e861c52..62a227e52d 100644 --- a/sqlx-postgres/src/arguments.rs +++ b/sqlx-postgres/src/arguments.rs @@ -22,7 +22,7 @@ use sqlx_core::error::BoxDynError; // that has a patch, we then apply the patch which should write to &mut Vec, // backtrack and update the prefixed-len, then write until the next patch offset -#[derive(Default)] +#[derive(Default, Debug, Clone)] pub struct PgArgumentBuffer { buffer: Vec, @@ -46,20 +46,32 @@ pub struct PgArgumentBuffer { type_holes: Vec<(usize, HoleKind)>, // Vec<{ offset, type_name }> } +#[derive(Debug, Clone)] enum HoleKind { Type { name: UStr }, Array(Arc), } +#[derive(Clone)] struct Patch { buf_offset: usize, arg_index: usize, #[allow(clippy::type_complexity)] - callback: Box, + callback: Arc, +} + +impl fmt::Debug for Patch { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Patch") + .field("buf_offset", &self.buf_offset) + .field("arg_index", &self.arg_index) + .field("callback", &"") + .finish() + } } /// Implementation of [`Arguments`] for PostgreSQL. -#[derive(Default)] +#[derive(Default, Debug, Clone)] pub struct PgArguments { // Types of each bind parameter pub(crate) types: Vec, @@ -194,7 +206,7 @@ impl PgArgumentBuffer { self.patches.push(Patch { buf_offset: offset, arg_index, - callback: Box::new(callback), + callback: Arc::new(callback), }); } diff --git a/sqlx-postgres/src/type_info.rs b/sqlx-postgres/src/type_info.rs index 3d948f73d4..28c56758e9 100644 --- a/sqlx-postgres/src/type_info.rs +++ b/sqlx-postgres/src/type_info.rs @@ -185,7 +185,7 @@ pub enum PgTypeKind { Range(PgTypeInfo), } -#[derive(Debug)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))] pub struct PgArrayOf { pub(crate) elem_name: UStr, From f4c74985230cc3fc5cd38f266521d2878fca3b0a Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Fri, 24 Jan 2025 23:41:29 +0100 Subject: [PATCH 11/66] fix(Sqlite): stop sending rows after first error (#3700) --- sqlx-sqlite/src/connection/worker.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sqlx-sqlite/src/connection/worker.rs b/sqlx-sqlite/src/connection/worker.rs index a01de2419c..c1c67636f1 100644 --- a/sqlx-sqlite/src/connection/worker.rs +++ b/sqlx-sqlite/src/connection/worker.rs @@ -151,7 +151,8 @@ impl ConnectionWorker { match limit { None => { for res in iter { - if tx.send(res).is_err() { + let has_error = res.is_err(); + if tx.send(res).is_err() || has_error { break; } } @@ -171,7 +172,8 @@ impl ConnectionWorker { } } } - if tx.send(res).is_err() { + let has_error = res.is_err(); + if tx.send(res).is_err() || has_error { break; } } From 74da542bf3da595ad4dcf88d886cf038534dd1eb Mon Sep 17 00:00:00 2001 From: "James H." <32926722+jayy-lmao@users.noreply.github.com> Date: Sat, 25 Jan 2025 17:34:40 +1100 Subject: [PATCH 12/66] feat: add postres geometry line segment (#3690) * feat: add postres geometry line segment * fix: add doc alias --- sqlx-postgres/src/type_checking.rs | 2 + .../src/types/geometry/line_segment.rs | 283 ++++++++++++++++++ sqlx-postgres/src/types/geometry/mod.rs | 1 + sqlx-postgres/src/types/mod.rs | 2 + tests/postgres/types.rs | 5 + 5 files changed, 293 insertions(+) create mode 100644 sqlx-postgres/src/types/geometry/line_segment.rs diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index eb18c5a999..314fda0b17 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -36,6 +36,8 @@ impl_type_checking!( sqlx::postgres::types::PgLine, + sqlx::postgres::types::PgLSeg, + #[cfg(feature = "uuid")] sqlx::types::Uuid, diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs new file mode 100644 index 0000000000..5dc5efc744 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -0,0 +1,283 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use std::str::FromStr; + +const ERROR: &str = "error decoding LSEG"; + +/// ## Postgres Geometric Line Segment type +/// +/// Description: Finite line segment +/// Representation: `((start_x,start_y),(end_x,end_y))` +/// +/// +/// Line segments are represented by pairs of points that are the endpoints of the segment. Values of type lseg are specified using any of the following syntaxes: +/// ```text +/// [ ( start_x , start_y ) , ( end_x , end_y ) ] +/// ( ( start_x , start_y ) , ( end_x , end_y ) ) +/// ( start_x , start_y ) , ( end_x , end_y ) +/// start_x , start_y , end_x , end_y +/// ``` +/// where `(start_x,start_y) and (end_x,end_y)` are the end points of the line segment. +/// +/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LSEG +#[doc(alias = "line segment")] +#[derive(Debug, Clone, PartialEq)] +pub struct PgLSeg { + pub start_x: f64, + pub start_y: f64, + pub end_x: f64, + pub end_y: f64, +} + +impl Type for PgLSeg { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("lseg") + } +} + +impl PgHasArrayType for PgLSeg { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_lseg") + } +} + +impl<'r> Decode<'r, Postgres> for PgLSeg { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgLSeg::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgLSeg::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgLSeg { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("lseg")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgLSeg { + type Err = BoxDynError; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); + let mut parts = sanitised.split(','); + + let start_x = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get start_x from {}", ERROR, s))?; + + let start_y = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get start_y from {}", ERROR, s))?; + + let end_x = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get end_x from {}", ERROR, s))?; + + let end_y = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get end_y from {}", ERROR, s))?; + + if parts.next().is_some() { + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); + } + + Ok(PgLSeg { + start_x, + start_y, + end_x, + end_y, + }) + } +} + +impl PgLSeg { + fn from_bytes(mut bytes: &[u8]) -> Result { + let start_x = bytes.get_f64(); + let start_y = bytes.get_f64(); + let end_x = bytes.get_f64(); + let end_y = bytes.get_f64(); + + Ok(PgLSeg { + start_x, + start_y, + end_x, + end_y, + }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { + buff.extend_from_slice(&self.start_x.to_be_bytes()); + buff.extend_from_slice(&self.start_y.to_be_bytes()); + buff.extend_from_slice(&self.end_x.to_be_bytes()); + buff.extend_from_slice(&self.end_y.to_be_bytes()); + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +#[cfg(test)] +mod lseg_tests { + + use std::str::FromStr; + + use super::PgLSeg; + + const LINE_SEGMENT_BYTES: &[u8] = &[ + 63, 241, 153, 153, 153, 153, 153, 154, 64, 1, 153, 153, 153, 153, 153, 154, 64, 10, 102, + 102, 102, 102, 102, 102, 64, 17, 153, 153, 153, 153, 153, 154, + ]; + + #[test] + fn can_deserialise_lseg_type_bytes() { + let lseg = PgLSeg::from_bytes(LINE_SEGMENT_BYTES).unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1.1, + start_y: 2.2, + end_x: 3.3, + end_y: 4.4 + } + ) + } + + #[test] + fn can_deserialise_lseg_type_str_first_syntax() { + let lseg = PgLSeg::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. + } + ); + } + #[test] + fn can_deserialise_lseg_type_str_second_syntax() { + let lseg = PgLSeg::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. + } + ); + } + + #[test] + fn can_deserialise_lseg_type_str_third_syntax() { + let lseg = PgLSeg::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. + } + ); + } + + #[test] + fn can_deserialise_lseg_type_str_fourth_syntax() { + let lseg = PgLSeg::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1., + start_y: 2., + end_x: 3., + end_y: 4. + } + ); + } + + #[test] + fn can_deserialise_too_many_numbers() { + let input_str = "1, 2, 3, 4, 5"; + let lseg = PgLSeg::from_str(input_str); + assert!(lseg.is_err()); + if let Err(err) = lseg { + assert_eq!( + err.to_string(), + format!("error decoding LSEG: too many numbers inputted in {input_str}") + ) + } + } + + #[test] + fn can_deserialise_too_few_numbers() { + let input_str = "1, 2, 3"; + let lseg = PgLSeg::from_str(input_str); + assert!(lseg.is_err()); + if let Err(err) = lseg { + assert_eq!( + err.to_string(), + format!("error decoding LSEG: could not get end_y from {input_str}") + ) + } + } + + #[test] + fn can_deserialise_invalid_numbers() { + let input_str = "1, 2, 3, FOUR"; + let lseg = PgLSeg::from_str(input_str); + assert!(lseg.is_err()); + if let Err(err) = lseg { + assert_eq!( + err.to_string(), + format!("error decoding LSEG: could not get end_y from {input_str}") + ) + } + } + + #[test] + fn can_deserialise_lseg_type_str_float() { + let lseg = PgLSeg::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + lseg, + PgLSeg { + start_x: 1.1, + start_y: 2.2, + end_x: 3.3, + end_y: 4.4 + } + ); + } + + #[test] + fn can_serialise_lseg_type() { + let lseg = PgLSeg { + start_x: 1.1, + start_y: 2.2, + end_x: 3.3, + end_y: 4.4, + }; + assert_eq!(lseg.serialize_to_vec(), LINE_SEGMENT_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs index daf9f1deb9..0da73fef08 100644 --- a/sqlx-postgres/src/types/geometry/mod.rs +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -1,2 +1,3 @@ pub mod line; +pub mod line_segment; pub mod point; diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 747345518a..b5b3266cbc 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -23,6 +23,7 @@ //! | [`PgCube`] | CUBE | //! | [`PgPoint] | POINT | //! | [`PgLine] | LINE | +//! | [`PgLSeg] | LSEG | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -259,6 +260,7 @@ pub use array::PgHasArrayType; pub use citext::PgCiText; pub use cube::PgCube; pub use geometry::line::PgLine; +pub use geometry::line_segment::PgLSeg; pub use geometry::point::PgPoint; pub use hstore::PgHstore; pub use interval::PgInterval; diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 3f6c362043..c1cf87983c 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -509,6 +509,11 @@ test_type!(line(Postgres, "line('((0.0, 0.0), (1.0,1.0))')" == sqlx::postgres::types::PgLine { a: 1., b: -1., c: 0. }, )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(lseg(Postgres, + "lseg('((1.0, 2.0), (3.0,4.0))')" == sqlx::postgres::types::PgLSeg { start_x: 1., start_y: 2., end_x: 3. , end_y: 4.}, +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(), From 6fa0458ff3a2921a5b2f3757b7ed39cc29d51c45 Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Sat, 25 Jan 2025 23:23:50 +0100 Subject: [PATCH 13/66] fix(Postgres) chunk pg_copy data (#3703) * fix(postgres) chunk pg_copy data * fix: cleanup after review --- sqlx-postgres/src/copy.rs | 22 +++++++++++++++------- sqlx-postgres/src/lib.rs | 3 +++ tests/postgres/postgres.rs | 22 +++++++++++++++++++++- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/sqlx-postgres/src/copy.rs b/sqlx-postgres/src/copy.rs index fd9ed7e85c..1315ea0e20 100644 --- a/sqlx-postgres/src/copy.rs +++ b/sqlx-postgres/src/copy.rs @@ -129,6 +129,9 @@ impl PgPoolCopyExt for Pool { } } +// (1 GiB - 1) - 1 - length prefix (4 bytes) +pub const PG_COPY_MAX_DATA_LEN: usize = 0x3fffffff - 1 - 4; + /// A connection in streaming `COPY FROM STDIN` mode. /// /// Created by [PgConnection::copy_in_raw] or [Pool::copy_out_raw]. @@ -186,15 +189,20 @@ impl> PgCopyIn { /// Send a chunk of `COPY` data. /// + /// The data is sent in chunks if it exceeds the maximum length of a `CopyData` message (1 GiB - 6 + /// bytes) and may be partially sent if this call is cancelled. + /// /// If you're copying data from an `AsyncRead`, maybe consider [Self::read_from] instead. pub async fn send(&mut self, data: impl Deref) -> Result<&mut Self> { - self.conn - .as_deref_mut() - .expect("send_data: conn taken") - .inner - .stream - .send(CopyData(data)) - .await?; + for chunk in data.deref().chunks(PG_COPY_MAX_DATA_LEN) { + self.conn + .as_deref_mut() + .expect("send_data: conn taken") + .inner + .stream + .send(CopyData(chunk)) + .await?; + } Ok(self) } diff --git a/sqlx-postgres/src/lib.rs b/sqlx-postgres/src/lib.rs index 792f8bbdc0..bded75491c 100644 --- a/sqlx-postgres/src/lib.rs +++ b/sqlx-postgres/src/lib.rs @@ -34,6 +34,9 @@ mod value; #[doc(hidden)] pub mod any; +#[doc(hidden)] +pub use copy::PG_COPY_MAX_DATA_LEN; + #[cfg(feature = "migrate")] mod migrate; diff --git a/tests/postgres/postgres.rs b/tests/postgres/postgres.rs index 87a18db510..d89ecbb21e 100644 --- a/tests/postgres/postgres.rs +++ b/tests/postgres/postgres.rs @@ -3,7 +3,7 @@ use futures::{Stream, StreamExt, TryStreamExt}; use sqlx::postgres::types::Oid; use sqlx::postgres::{ PgAdvisoryLock, PgConnectOptions, PgConnection, PgDatabaseError, PgErrorPosition, PgListener, - PgPoolOptions, PgRow, PgSeverity, Postgres, + PgPoolOptions, PgRow, PgSeverity, Postgres, PG_COPY_MAX_DATA_LEN, }; use sqlx::{Column, Connection, Executor, Row, Statement, TypeInfo}; use sqlx_core::{bytes::Bytes, error::BoxDynError}; @@ -2042,3 +2042,23 @@ async fn test_issue_3052() { "expected encode error, got {too_large_error:?}", ); } + +#[sqlx_macros::test] +async fn test_pg_copy_chunked() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let mut row = "1".repeat(PG_COPY_MAX_DATA_LEN / 10 - 1); + row.push_str("\n"); + + // creates a payload with COPY_MAX_DATA_LEN + 1 as size + let mut payload = row.repeat(10); + payload.push_str("12345678\n"); + + assert_eq!(payload.len(), PG_COPY_MAX_DATA_LEN + 1); + + let mut copy = conn.copy_in_raw("COPY products(name) FROM STDIN").await?; + + assert!(copy.send(payload.as_bytes()).await.is_ok()); + assert!(copy.finish().await.is_ok()); + Ok(()) +} From 6c2a29f67e1c957ca7d46702cc68b5f3124dc7eb Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:41:07 +0100 Subject: [PATCH 14/66] chore(MySql): Remove unnecessary box (#3708) --- sqlx-mysql/src/connection/executor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlx-mysql/src/connection/executor.rs b/sqlx-mysql/src/connection/executor.rs index 07c7979b08..d7f8fcfa14 100644 --- a/sqlx-mysql/src/connection/executor.rs +++ b/sqlx-mysql/src/connection/executor.rs @@ -111,7 +111,7 @@ impl MySqlConnection { self.inner.stream.wait_until_ready().await?; self.inner.stream.waiting.push_back(Waiting::Result); - Ok(Box::pin(try_stream! { + Ok(try_stream! { // make a slot for the shared column data // as long as a reference to a row is not held past one iteration, this enables us // to re-use this memory freely between result sets @@ -240,7 +240,7 @@ impl MySqlConnection { r#yield!(v); } } - })) + }) } } From 6ca52fe80c92aacf9b30483c5341dfdb2c8b32c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20S=C3=A9verin?= Date: Tue, 28 Jan 2025 01:30:02 +0000 Subject: [PATCH 15/66] Added missing special casing for encoding embedded arrays of custom types (#3603) * Added missing special casing for encoding arrays of custom types * Added the matching test * Formatting --- sqlx-postgres/src/types/record.rs | 8 ++-- tests/postgres/derives.rs | 66 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/sqlx-postgres/src/types/record.rs b/sqlx-postgres/src/types/record.rs index c4eb639368..6e37182c40 100644 --- a/sqlx-postgres/src/types/record.rs +++ b/sqlx-postgres/src/types/record.rs @@ -41,13 +41,13 @@ impl<'a> PgRecordEncoder<'a> { { let ty = value.produces().unwrap_or_else(T::type_info); - if let PgType::DeclareWithName(name) = ty.0 { + match ty.0 { // push a hole for this type ID // to be filled in on query execution - self.buf.patch_type_by_name(&name); - } else { + PgType::DeclareWithName(name) => self.buf.patch_type_by_name(&name), + PgType::DeclareArrayOf(array) => self.buf.patch_array_type(array), // write type id - self.buf.extend(&ty.0.oid().0.to_be_bytes()); + pg_type => self.buf.extend(&pg_type.oid().0.to_be_bytes()), } self.buf.encode(value)?; diff --git a/tests/postgres/derives.rs b/tests/postgres/derives.rs index dada74fe4d..13f9bf1d5d 100644 --- a/tests/postgres/derives.rs +++ b/tests/postgres/derives.rs @@ -810,3 +810,69 @@ async fn test_custom_pg_array() -> anyhow::Result<()> { } Ok(()) } + +#[sqlx_macros::test] +async fn test_record_array_type() -> anyhow::Result<()> { + let mut conn = new::().await?; + + conn.execute( + r#" +DROP TABLE IF EXISTS responses; + +DROP TYPE IF EXISTS http_response CASCADE; +DROP TYPE IF EXISTS header_pair CASCADE; + +CREATE TYPE header_pair AS ( + name TEXT, + value TEXT +); + +CREATE TYPE http_response AS ( + headers header_pair[] +); + +CREATE TABLE responses ( + response http_response NOT NULL +); + "#, + ) + .await?; + + #[derive(Debug, sqlx::Type)] + #[sqlx(type_name = "http_response")] + struct HttpResponseRecord { + headers: Vec, + } + + #[derive(Debug, sqlx::Type)] + #[sqlx(type_name = "header_pair")] + struct HeaderPairRecord { + name: String, + value: String, + } + + let value = HttpResponseRecord { + headers: vec![ + HeaderPairRecord { + name: "Content-Type".to_owned(), + value: "text/html; charset=utf-8".to_owned(), + }, + HeaderPairRecord { + name: "Cache-Control".to_owned(), + value: "max-age=0".to_owned(), + }, + ], + }; + + sqlx::query( + " +INSERT INTO responses (response) +VALUES ($1) + ", + ) + .bind(&value) + .execute(&mut conn) + .await?; + + Ok(()) +} From 546ec960a955959a649dda29180c3de52be0e181 Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Tue, 28 Jan 2025 05:56:21 +0100 Subject: [PATCH 16/66] feat(Sqlite): add LockedSqliteHandle::last_error (#3707) --- ci.db | Bin 0 -> 36864 bytes sqlx-sqlite/src/connection/mod.rs | 6 +++++- sqlx-sqlite/src/error.rs | 12 ++++++++++-- tests/sqlite/sqlite.rs | 21 +++++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 ci.db diff --git a/ci.db b/ci.db new file mode 100644 index 0000000000000000000000000000000000000000..cc158a72804c405a8716f792f5ca51ec421c6ae0 GIT binary patch literal 36864 zcmeI*?{Cva7zgmXI6vw@Fd$8qCLvu`p~e#%tI)JpAkuJcBhqBJ?z+CROma|*U_NC8#&WYq);%-0a`BJb) zVf56K>=wC9C?$6pBZM^6qN^oORh22{1+}Fo@~34RTR+JzBHIzJnV;&9DGX2)a>e(ZWf=|2~2Z7}HZ zcE4^-544)OyiC7aO=FRVLP$3flktla+44d}Cw9z(TI+5ky!q)-j45lSPOyI+nMGDS z!Ls`BFi^uZYgHJ|{8`;cPuTjv=G~2ca@Mj|zQbZRU*|UOclb6-D_j0Z4epoas@YvW z?d;83rws1ui9%)CqBYYr>8qP_u5^E?Nc4Yj3a_u(fW;z^`Ebp7@A38aPS0VEy~E!r zRky5C)x2%~cMr3^Y3jXXDS1ChF7ivpkHq*zt&ku90SG_<0uX=z1Rwwb2tWV=5I7S8 zS4$OoT_@V67>~mnY&VR?qmOAt|4P@FlMe@(AFB_&lK)d~p$ z5P$##AOHafKmY;|fB*y_0D*HPP|+yWN}8tY$^1Vxej@6D1OW&@00Izz00bZa0SG_< z0uX?}*%DY$Q~vq`eNTA%O*WhOPkyH(Muqu*YP?nR|7WX%h!z46fB*y_009U<00Izz z00bZaf%g-b&;P$6#+&zRgQFk-0SG_<0uX=z1Rwwb2tWV=5cseKuF^8G^i2^>*gduK zgE$P5@Bgct{v$yE0uX=z1Rwwb2tWV=5P$##AaITalIwrX_?{TQ89%EH5(FRs0SG_< z0uX=z1Rwwb2tWV=AFjX^O(T}vS2y=p5054P0rX0#SVap}$^HK| { pub fn remove_rollback_hook(&mut self) { self.guard.remove_rollback_hook(); } + + pub fn last_error(&mut self) -> Option { + SqliteError::try_new(self.guard.handle.as_ptr()) + } } impl Drop for ConnectionState { diff --git a/sqlx-sqlite/src/error.rs b/sqlx-sqlite/src/error.rs index c00374fe60..0d34bc1026 100644 --- a/sqlx-sqlite/src/error.rs +++ b/sqlx-sqlite/src/error.rs @@ -23,9 +23,17 @@ pub struct SqliteError { impl SqliteError { pub(crate) fn new(handle: *mut sqlite3) -> Self { + Self::try_new(handle).expect("There should be an error") + } + + pub(crate) fn try_new(handle: *mut sqlite3) -> Option { // returns the extended result code even when extended result codes are disabled let code: c_int = unsafe { sqlite3_extended_errcode(handle) }; + if code == 0 { + return None; + } + // return English-language text that describes the error let message = unsafe { let msg = sqlite3_errmsg(handle); @@ -34,10 +42,10 @@ impl SqliteError { from_utf8_unchecked(CStr::from_ptr(msg).to_bytes()) }; - Self { + Some(Self { code, message: message.to_owned(), - } + }) } /// For errors during extension load, the error message is supplied via a separate pointer diff --git a/tests/sqlite/sqlite.rs b/tests/sqlite/sqlite.rs index d78e1151a9..16b4b2d9fa 100644 --- a/tests/sqlite/sqlite.rs +++ b/tests/sqlite/sqlite.rs @@ -1172,3 +1172,24 @@ async fn test_multiple_set_preupdate_hook_calls_drop_old_handler() -> anyhow::Re assert_eq!(1, Arc::strong_count(&ref_counted_object)); Ok(()) } + +#[sqlx_macros::test] +async fn test_get_last_error() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let _ = sqlx::query("select 1").fetch_one(&mut conn).await?; + + { + let mut handle = conn.lock_handle().await?; + assert!(handle.last_error().is_none()); + } + + let _ = sqlx::query("invalid statement").fetch_one(&mut conn).await; + + { + let mut handle = conn.lock_handle().await?; + assert!(handle.last_error().is_some()); + } + + Ok(()) +} From 2aab4cd2370f006820cf4b2f537d252c251dd7c3 Mon Sep 17 00:00:00 2001 From: Sean Aye Date: Tue, 28 Jan 2025 13:56:33 -0500 Subject: [PATCH 17/66] Add json(nullable) macro attribute (#3677) * add json optional attribute parser and expansion * rename attribute * add test * fix tests * fix lints * Add docs --- sqlx-core/src/from_row.rs | 26 +++++++++++++++++++ sqlx-macros-core/src/derives/attributes.rs | 25 ++++++++++++++----- sqlx-macros-core/src/derives/row.rs | 29 +++++++++++++++------- tests/mysql/macros.rs | 25 +++++++++++++++++++ 4 files changed, 90 insertions(+), 15 deletions(-) diff --git a/sqlx-core/src/from_row.rs b/sqlx-core/src/from_row.rs index 9c647d370a..8776855d92 100644 --- a/sqlx-core/src/from_row.rs +++ b/sqlx-core/src/from_row.rs @@ -271,6 +271,32 @@ use crate::{error::Error, row::Row}; /// } /// } /// ``` +/// +/// By default the `#[sqlx(json)]` attribute will assume that the underlying database row is +/// _not_ NULL. This can cause issues when your field type is an `Option` because this would be +/// represented as the _not_ NULL (in terms of DB) JSON value of `null`. +/// +/// If you wish to describe a database row which _is_ NULLable but _cannot_ contain the JSON value `null`, +/// use the `#[sqlx(json(nullable))]` attrubute. +/// +/// For example +/// ```rust,ignore +/// #[derive(serde::Deserialize)] +/// struct Data { +/// field1: String, +/// field2: u64 +/// } +/// +/// #[derive(sqlx::FromRow)] +/// struct User { +/// id: i32, +/// name: String, +/// #[sqlx(json(nullable))] +/// metadata: Option +/// } +/// ``` +/// Would describe a database field which _is_ NULLable but if it exists it must be the JSON representation of `Data` +/// and cannot be the JSON value `null` pub trait FromRow<'r, R: Row>: Sized { fn from_row(row: &'r R) -> Result; } diff --git a/sqlx-macros-core/src/derives/attributes.rs b/sqlx-macros-core/src/derives/attributes.rs index cf18cffca4..c69687908b 100644 --- a/sqlx-macros-core/src/derives/attributes.rs +++ b/sqlx-macros-core/src/derives/attributes.rs @@ -1,8 +1,8 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote_spanned; use syn::{ - punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, LitStr, Meta, Token, Type, - Variant, + parenthesized, punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, LitStr, + Meta, Token, Type, Variant, }; macro_rules! assert_attribute { @@ -61,13 +61,18 @@ pub struct SqlxContainerAttributes { pub default: bool, } +pub enum JsonAttribute { + NonNullable, + Nullable, +} + pub struct SqlxChildAttributes { pub rename: Option, pub default: bool, pub flatten: bool, pub try_from: Option, pub skip: bool, - pub json: bool, + pub json: Option, } pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result { @@ -144,7 +149,7 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result syn::Result - (false, None, false) => { + (false, None, None) => { predicates .push(parse_quote!(#ty: ::sqlx::decode::Decode<#lifetime, R::Database>)); predicates.push(parse_quote!(#ty: ::sqlx::types::Type)); @@ -107,12 +107,12 @@ fn expand_derive_from_row_struct( parse_quote!(__row.try_get(#id_s)) } // Flatten - (true, None, false) => { + (true, None, None) => { predicates.push(parse_quote!(#ty: ::sqlx::FromRow<#lifetime, R>)); parse_quote!(<#ty as ::sqlx::FromRow<#lifetime, R>>::from_row(__row)) } // Flatten + Try from - (true, Some(try_from), false) => { + (true, Some(try_from), None) => { predicates.push(parse_quote!(#try_from: ::sqlx::FromRow<#lifetime, R>)); parse_quote!( <#try_from as ::sqlx::FromRow<#lifetime, R>>::from_row(__row) @@ -130,11 +130,11 @@ fn expand_derive_from_row_struct( ) } // Flatten + Json - (true, _, true) => { + (true, _, Some(_)) => { panic!("Cannot use both flatten and json") } // Try from - (false, Some(try_from), false) => { + (false, Some(try_from), None) => { predicates .push(parse_quote!(#try_from: ::sqlx::decode::Decode<#lifetime, R::Database>)); predicates.push(parse_quote!(#try_from: ::sqlx::types::Type)); @@ -154,8 +154,8 @@ fn expand_derive_from_row_struct( }) ) } - // Try from + Json - (false, Some(try_from), true) => { + // Try from + Json mandatory + (false, Some(try_from), Some(JsonAttribute::NonNullable)) => { predicates .push(parse_quote!(::sqlx::types::Json<#try_from>: ::sqlx::decode::Decode<#lifetime, R::Database>)); predicates.push(parse_quote!(::sqlx::types::Json<#try_from>: ::sqlx::types::Type)); @@ -175,14 +175,25 @@ fn expand_derive_from_row_struct( }) ) }, + // Try from + Json nullable + (false, Some(_), Some(JsonAttribute::Nullable)) => { + panic!("Cannot use both try from and json nullable") + }, // Json - (false, None, true) => { + (false, None, Some(JsonAttribute::NonNullable)) => { predicates .push(parse_quote!(::sqlx::types::Json<#ty>: ::sqlx::decode::Decode<#lifetime, R::Database>)); predicates.push(parse_quote!(::sqlx::types::Json<#ty>: ::sqlx::types::Type)); parse_quote!(__row.try_get::<::sqlx::types::Json<_>, _>(#id_s).map(|x| x.0)) }, + (false, None, Some(JsonAttribute::Nullable)) => { + predicates + .push(parse_quote!(::core::option::Option<::sqlx::types::Json<#ty>>: ::sqlx::decode::Decode<#lifetime, R::Database>)); + predicates.push(parse_quote!(::core::option::Option<::sqlx::types::Json<#ty>>: ::sqlx::types::Type)); + + parse_quote!(__row.try_get::<::core::option::Option<::sqlx::types::Json<_>>, _>(#id_s).map(|x| x.and_then(|y| y.0))) + }, }; if attributes.default { diff --git a/tests/mysql/macros.rs b/tests/mysql/macros.rs index f6bc75955a..8187f6d8d8 100644 --- a/tests/mysql/macros.rs +++ b/tests/mysql/macros.rs @@ -494,6 +494,31 @@ async fn test_from_row_json_attr() -> anyhow::Result<()> { Ok(()) } +#[sqlx_macros::test] +async fn test_from_row_json_attr_nullable() -> anyhow::Result<()> { + #[derive(serde::Deserialize)] + #[allow(dead_code)] + struct J { + a: u32, + b: u32, + } + + #[derive(sqlx::FromRow)] + struct Record { + #[sqlx(json(nullable))] + j: Option, + } + + let mut conn = new::().await?; + + let record = sqlx::query_as::<_, Record>("select NULL as j") + .fetch_one(&mut conn) + .await?; + + assert!(record.j.is_none()); + Ok(()) +} + #[sqlx_macros::test] async fn test_from_row_json_try_from_attr() -> anyhow::Result<()> { #[derive(serde::Deserialize)] From d8af1fa33e7d3f93ae8df1efcee440ba154eea06 Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Wed, 29 Jan 2025 23:56:46 +0100 Subject: [PATCH 18/66] chore: add pg_copy regression tests (#3715) --- tests/postgres/postgres.rs | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/postgres/postgres.rs b/tests/postgres/postgres.rs index d89ecbb21e..86707e23e6 100644 --- a/tests/postgres/postgres.rs +++ b/tests/postgres/postgres.rs @@ -2062,3 +2062,58 @@ async fn test_pg_copy_chunked() -> anyhow::Result<()> { assert!(copy.finish().await.is_ok()); Ok(()) } + +async fn test_copy_in_error_case(query: &str, expected_error: &str) -> anyhow::Result<()> { + let mut conn = new::().await?; + conn.execute("CREATE TEMPORARY TABLE IF NOT EXISTS invalid_copy_target (id int4)") + .await?; + // Try the COPY operation + match conn.copy_in_raw(query).await { + Ok(_) => anyhow::bail!("expected error"), + Err(e) => assert!( + e.to_string().contains(expected_error), + "expected error to contain: {expected_error}, got: {e:?}" + ), + } + // Verify connection is still usable + let value = sqlx::query("select 1 + 1") + .try_map(|row: PgRow| row.try_get::(0)) + .fetch_one(&mut conn) + .await?; + assert_eq!(2i32, value); + Ok(()) +} +#[sqlx_macros::test] +async fn it_can_recover_from_copy_in_to_missing_table() -> anyhow::Result<()> { + test_copy_in_error_case( + r#" + COPY nonexistent_table (id) FROM STDIN WITH (FORMAT CSV, HEADER); + "#, + "does not exist", + ) + .await +} +#[sqlx_macros::test] +async fn it_can_recover_from_copy_in_empty_query() -> anyhow::Result<()> { + test_copy_in_error_case("", "EmptyQuery").await +} +#[sqlx_macros::test] +async fn it_can_recover_from_copy_in_syntax_error() -> anyhow::Result<()> { + test_copy_in_error_case( + r#" + COPY FROM STDIN WITH (FORMAT CSV); + "#, + "syntax error", + ) + .await +} +#[sqlx_macros::test] +async fn it_can_recover_from_copy_in_invalid_params() -> anyhow::Result<()> { + test_copy_in_error_case( + r#" + COPY invalid_copy_target FROM STDIN WITH (FORMAT CSV, INVALID_PARAM true); + "#, + "invalid_param", + ) + .await +} From 97cada3e16a9c875ce4f5af38b0382e72928cb7c Mon Sep 17 00:00:00 2001 From: "James H." <32926722+jayy-lmao@users.noreply.github.com> Date: Thu, 30 Jan 2025 09:59:50 +1100 Subject: [PATCH 19/66] feat(postgres): add geometry box (#3711) * feat: add geometry box * test: cannot compare box arrays * test: regular equals check for boxes * test: try box array test --- sqlx-postgres/src/type_checking.rs | 2 + sqlx-postgres/src/types/geometry/box.rs | 321 ++++++++++++++++++++++++ sqlx-postgres/src/types/geometry/mod.rs | 1 + sqlx-postgres/src/types/mod.rs | 2 + tests/postgres/types.rs | 10 + 5 files changed, 336 insertions(+) create mode 100644 sqlx-postgres/src/types/geometry/box.rs diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index 314fda0b17..68a4fcfeff 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -38,6 +38,8 @@ impl_type_checking!( sqlx::postgres::types::PgLSeg, + sqlx::postgres::types::PgBox, + #[cfg(feature = "uuid")] sqlx::types::Uuid, diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs new file mode 100644 index 0000000000..988c028ed4 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -0,0 +1,321 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use std::str::FromStr; + +const ERROR: &str = "error decoding BOX"; + +/// ## Postgres Geometric Box type +/// +/// Description: Rectangular box +/// Representation: `((upper_right_x,upper_right_y),(lower_left_x,lower_left_y))` +/// +/// Boxes are represented by pairs of points that are opposite corners of the box. Values of type box are specified using any of the following syntaxes: +/// +/// ```text +/// ( ( upper_right_x , upper_right_y ) , ( lower_left_x , lower_left_y ) ) +/// ( upper_right_x , upper_right_y ) , ( lower_left_x , lower_left_y ) +/// upper_right_x , upper_right_y , lower_left_x , lower_left_y +/// ``` +/// where `(upper_right_x,upper_right_y) and (lower_left_x,lower_left_y)` are any two opposite corners of the box. +/// Any two opposite corners can be supplied on input, but the values will be reordered as needed to store the upper right and lower left corners, in that order. +/// +/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES +#[derive(Debug, Clone, PartialEq)] +pub struct PgBox { + pub upper_right_x: f64, + pub upper_right_y: f64, + pub lower_left_x: f64, + pub lower_left_y: f64, +} + +impl Type for PgBox { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("box") + } +} + +impl PgHasArrayType for PgBox { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_box") + } +} + +impl<'r> Decode<'r, Postgres> for PgBox { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgBox::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgBox::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgBox { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("box")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgBox { + type Err = BoxDynError; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); + let mut parts = sanitised.split(','); + + let upper_right_x = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get upper_right_x from {}", ERROR, s))?; + + let upper_right_y = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get upper_right_y from {}", ERROR, s))?; + + let lower_left_x = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get lower_left_x from {}", ERROR, s))?; + + let lower_left_y = parts + .next() + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| format!("{}: could not get lower_left_y from {}", ERROR, s))?; + + if parts.next().is_some() { + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); + } + + Ok(PgBox { + upper_right_x, + upper_right_y, + lower_left_x, + lower_left_y, + }) + } +} + +impl PgBox { + fn from_bytes(mut bytes: &[u8]) -> Result { + let upper_right_x = bytes.get_f64(); + let upper_right_y = bytes.get_f64(); + let lower_left_x = bytes.get_f64(); + let lower_left_y = bytes.get_f64(); + + Ok(PgBox { + upper_right_x, + upper_right_y, + lower_left_x, + lower_left_y, + }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { + let min_x = &self.upper_right_x.min(self.lower_left_x); + let min_y = &self.upper_right_y.min(self.lower_left_y); + let max_x = &self.upper_right_x.max(self.lower_left_x); + let max_y = &self.upper_right_y.max(self.lower_left_y); + + buff.extend_from_slice(&max_x.to_be_bytes()); + buff.extend_from_slice(&max_y.to_be_bytes()); + buff.extend_from_slice(&min_x.to_be_bytes()); + buff.extend_from_slice(&min_y.to_be_bytes()); + + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +#[cfg(test)] +mod box_tests { + + use std::str::FromStr; + + use super::PgBox; + + const BOX_BYTES: &[u8] = &[ + 64, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, + 0, 0, 0, 0, + ]; + + #[test] + fn can_deserialise_box_type_bytes_in_order() { + let pg_box = PgBox::from_bytes(BOX_BYTES).unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 2., + upper_right_y: 2., + lower_left_x: -2., + lower_left_y: -2. + } + ) + } + + #[test] + fn can_deserialise_box_type_str_first_syntax() { + let pg_box = PgBox::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. + } + ); + } + #[test] + fn can_deserialise_box_type_str_second_syntax() { + let pg_box = PgBox::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. + } + ); + } + + #[test] + fn can_deserialise_box_type_str_third_syntax() { + let pg_box = PgBox::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. + } + ); + } + + #[test] + fn can_deserialise_box_type_str_fourth_syntax() { + let pg_box = PgBox::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 1., + upper_right_y: 2., + lower_left_x: 3., + lower_left_y: 4. + } + ); + } + + #[test] + fn cannot_deserialise_too_many_numbers() { + let input_str = "1, 2, 3, 4, 5"; + let pg_box = PgBox::from_str(input_str); + assert!(pg_box.is_err()); + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding BOX: too many numbers inputted in {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_too_few_numbers() { + let input_str = "1, 2, 3 "; + let pg_box = PgBox::from_str(input_str); + assert!(pg_box.is_err()); + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding BOX: could not get lower_left_y from {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_invalid_numbers() { + let input_str = "1, 2, 3, FOUR"; + let pg_box = PgBox::from_str(input_str); + assert!(pg_box.is_err()); + if let Err(err) = pg_box { + assert_eq!( + err.to_string(), + format!("error decoding BOX: could not get lower_left_y from {input_str}") + ) + } + } + + #[test] + fn can_deserialise_box_type_str_float() { + let pg_box = PgBox::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 1.1, + upper_right_y: 2.2, + lower_left_x: 3.3, + lower_left_y: 4.4 + } + ); + } + + #[test] + fn can_serialise_box_type_in_order() { + let pg_box = PgBox { + upper_right_x: 2., + lower_left_x: -2., + upper_right_y: -2., + lower_left_y: 2., + }; + assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) + } + + #[test] + fn can_serialise_box_type_out_of_order() { + let pg_box = PgBox { + upper_right_x: -2., + lower_left_x: 2., + upper_right_y: 2., + lower_left_y: -2., + }; + assert_eq!(pg_box.serialize_to_vec(), BOX_BYTES,) + } + + #[test] + fn can_order_box() { + let pg_box = PgBox { + upper_right_x: -2., + lower_left_x: 2., + upper_right_y: 2., + lower_left_y: -2., + }; + let bytes = pg_box.serialize_to_vec(); + + let pg_box = PgBox::from_bytes(&bytes).unwrap(); + assert_eq!( + pg_box, + PgBox { + upper_right_x: 2., + upper_right_y: 2., + lower_left_x: -2., + lower_left_y: -2. + } + ) + } +} diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs index 0da73fef08..7fe2898fcd 100644 --- a/sqlx-postgres/src/types/geometry/mod.rs +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -1,3 +1,4 @@ +pub mod r#box; pub mod line; pub mod line_segment; pub mod point; diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index b5b3266cbc..e53f88ffec 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -24,6 +24,7 @@ //! | [`PgPoint] | POINT | //! | [`PgLine] | LINE | //! | [`PgLSeg] | LSEG | +//! | [`PgBox] | BOX | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -262,6 +263,7 @@ pub use cube::PgCube; pub use geometry::line::PgLine; pub use geometry::line_segment::PgLSeg; pub use geometry::point::PgPoint; +pub use geometry::r#box::PgBox; pub use hstore::PgHstore; pub use interval::PgInterval; pub use lquery::PgLQuery; diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index c1cf87983c..ccf88b1099 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -514,6 +514,16 @@ test_type!(lseg(Postgres, "lseg('((1.0, 2.0), (3.0,4.0))')" == sqlx::postgres::types::PgLSeg { start_x: 1., start_y: 2., end_x: 3. , end_y: 4.}, )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(box(Postgres, + "box('((1.0, 2.0), (3.0,4.0))')" == sqlx::postgres::types::PgBox { upper_right_x: 3., upper_right_y: 4., lower_left_x: 1. , lower_left_y: 2.}, +)); + +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(_box>(Postgres, + "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4))')]" @= vec![sqlx::postgres::types::PgBox { upper_right_x: 3., upper_right_y: 4., lower_left_x: 1., lower_left_y: 2. }, sqlx::postgres::types::PgBox { upper_right_x: 3.3, upper_right_y: 4.4, lower_left_x: 1.1, lower_left_y: 2.2 }], +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(), From 4d638c9e24a6d47f0d9aaac7a5f9147e03b99b45 Mon Sep 17 00:00:00 2001 From: Tobias Bieniek Date: Thu, 30 Jan 2025 00:00:21 +0100 Subject: [PATCH 20/66] FromRow: Fix documentation order (#3712) The `try_from` and `json` sections are "Field attributes" so they should probably be part of the corresponding section instead of subsections of "Manual implementation". `flatten` should be H4 instead of H3, since "Field attributes" is H3 and all other field attribute sections are H4 too. --- sqlx-core/src/from_row.rs | 57 ++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/sqlx-core/src/from_row.rs b/sqlx-core/src/from_row.rs index 8776855d92..66e53d55b8 100644 --- a/sqlx-core/src/from_row.rs +++ b/sqlx-core/src/from_row.rs @@ -111,7 +111,8 @@ use crate::{error::Error, row::Row}; /// different placeholder values, if applicable. /// /// This is similar to how `#[serde(default)]` behaves. -/// ### `flatten` +/// +/// #### `flatten` /// /// If you want to handle a field that implements [`FromRow`], /// you can use the `flatten` attribute to specify that you want @@ -177,33 +178,6 @@ use crate::{error::Error, row::Row}; /// assert!(user.addresses.is_empty()); /// ``` /// -/// ## Manual implementation -/// -/// You can also implement the [`FromRow`] trait by hand. This can be useful if you -/// have a struct with a field that needs manual decoding: -/// -/// -/// ```rust,ignore -/// use sqlx::{FromRow, sqlite::SqliteRow, sqlx::Row}; -/// struct MyCustomType { -/// custom: String, -/// } -/// -/// struct Foo { -/// bar: MyCustomType, -/// } -/// -/// impl FromRow<'_, SqliteRow> for Foo { -/// fn from_row(row: &SqliteRow) -> sqlx::Result { -/// Ok(Self { -/// bar: MyCustomType { -/// custom: row.try_get("custom")? -/// } -/// }) -/// } -/// } -/// ``` -/// /// #### `try_from` /// /// When your struct contains a field whose type is not matched with the database type, @@ -297,6 +271,33 @@ use crate::{error::Error, row::Row}; /// ``` /// Would describe a database field which _is_ NULLable but if it exists it must be the JSON representation of `Data` /// and cannot be the JSON value `null` +/// +/// ## Manual implementation +/// +/// You can also implement the [`FromRow`] trait by hand. This can be useful if you +/// have a struct with a field that needs manual decoding: +/// +/// +/// ```rust,ignore +/// use sqlx::{FromRow, sqlite::SqliteRow, sqlx::Row}; +/// struct MyCustomType { +/// custom: String, +/// } +/// +/// struct Foo { +/// bar: MyCustomType, +/// } +/// +/// impl FromRow<'_, SqliteRow> for Foo { +/// fn from_row(row: &SqliteRow) -> sqlx::Result { +/// Ok(Self { +/// bar: MyCustomType { +/// custom: row.try_get("custom")? +/// } +/// }) +/// } +/// } +/// ``` pub trait FromRow<'r, R: Row>: Sized { fn from_row(row: &'r R) -> Result; } From 5b26369a59a68b0c90937c188d8e1bdd233daccb Mon Sep 17 00:00:00 2001 From: Jon Thacker Date: Fri, 31 Jan 2025 15:22:47 -0800 Subject: [PATCH 21/66] Fix readme: uuid feature is gating for all repos (#3720) The readme previously stated that the uuid feature is only for postres but it actually also gates the functionality in mysql and sqlite. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 15d68bbb42..c3b501ca48 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ be removed in the future. - `migrate`: Add support for the migration management and `migrate!` macro, which allow compile-time embedded migrations. -- `uuid`: Add support for UUID (in Postgres). +- `uuid`: Add support for UUID. - `chrono`: Add support for date and time types from `chrono`. From 65229f7ff91ecd38be7c10fb61ff3e05bedabe87 Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Sun, 2 Feb 2025 01:01:56 +0100 Subject: [PATCH 22/66] Replace some futures_util APIs with std variants (#3721) --- sqlx-core/src/io/write_and_flush.rs | 5 ++--- sqlx-core/src/net/socket/mod.rs | 3 +-- sqlx-core/src/net/tls/util.rs | 3 +-- sqlx-core/src/pool/inner.rs | 16 +++++----------- sqlx-core/src/pool/mod.rs | 6 +++--- sqlx-mysql/src/any.rs | 5 ++--- sqlx-mysql/src/connection/executor.rs | 7 +++---- sqlx-postgres/src/any.rs | 5 ++--- sqlx-postgres/src/connection/executor.rs | 10 ++++------ sqlx-sqlite/src/any.rs | 13 +++++++------ sqlx-sqlite/src/connection/executor.rs | 8 +++----- tests/postgres/postgres.rs | 5 ++--- 12 files changed, 35 insertions(+), 51 deletions(-) diff --git a/sqlx-core/src/io/write_and_flush.rs b/sqlx-core/src/io/write_and_flush.rs index 9e7824af81..8a0db31293 100644 --- a/sqlx-core/src/io/write_and_flush.rs +++ b/sqlx-core/src/io/write_and_flush.rs @@ -1,10 +1,9 @@ use crate::error::Error; -use futures_core::Future; -use futures_util::ready; use sqlx_rt::AsyncWrite; +use std::future::Future; use std::io::{BufRead, Cursor}; use std::pin::Pin; -use std::task::{Context, Poll}; +use std::task::{ready, Context, Poll}; // Atomic operation that writes the full buffer to the stream, flushes the stream, and then // clears the buffer (even if either of the two previous operations failed). diff --git a/sqlx-core/src/net/socket/mod.rs b/sqlx-core/src/net/socket/mod.rs index 6b09d318f7..d11f15884e 100644 --- a/sqlx-core/src/net/socket/mod.rs +++ b/sqlx-core/src/net/socket/mod.rs @@ -2,10 +2,9 @@ use std::future::Future; use std::io; use std::path::Path; use std::pin::Pin; -use std::task::{Context, Poll}; +use std::task::{ready, Context, Poll}; use bytes::BufMut; -use futures_core::ready; pub use buffered::{BufferedSocket, WriteBuffer}; diff --git a/sqlx-core/src/net/tls/util.rs b/sqlx-core/src/net/tls/util.rs index 02a16ef5e1..ddbc7a58f2 100644 --- a/sqlx-core/src/net/tls/util.rs +++ b/sqlx-core/src/net/tls/util.rs @@ -1,9 +1,8 @@ use crate::net::Socket; use std::io::{self, Read, Write}; -use std::task::{Context, Poll}; +use std::task::{ready, Context, Poll}; -use futures_core::ready; use futures_util::future; pub struct StdSocket { diff --git a/sqlx-core/src/pool/inner.rs b/sqlx-core/src/pool/inner.rs index bbcc43134e..2066364a8e 100644 --- a/sqlx-core/src/pool/inner.rs +++ b/sqlx-core/src/pool/inner.rs @@ -10,6 +10,7 @@ use crate::sync::{AsyncSemaphore, AsyncSemaphoreReleaser}; use std::cmp; use std::future::Future; +use std::pin::pin; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use std::sync::{Arc, RwLock}; use std::task::Poll; @@ -130,19 +131,12 @@ impl PoolInner { // This is just going to cause unnecessary churn in `acquire()`. .filter(|_| self.size() < self.options.max_connections); - let acquire_self = self.semaphore.acquire(1).fuse(); - let mut close_event = self.close_event(); + let mut acquire_self = pin!(self.semaphore.acquire(1).fuse()); + let mut close_event = pin!(self.close_event()); if let Some(parent) = parent { - let acquire_parent = parent.0.semaphore.acquire(1); - let parent_close_event = parent.0.close_event(); - - futures_util::pin_mut!( - acquire_parent, - acquire_self, - close_event, - parent_close_event - ); + let mut acquire_parent = pin!(parent.0.semaphore.acquire(1)); + let mut parent_close_event = pin!(parent.0.close_event()); let mut poll_parent = false; diff --git a/sqlx-core/src/pool/mod.rs b/sqlx-core/src/pool/mod.rs index e998618413..042bc5c7bc 100644 --- a/sqlx-core/src/pool/mod.rs +++ b/sqlx-core/src/pool/mod.rs @@ -56,7 +56,7 @@ use std::fmt; use std::future::Future; -use std::pin::Pin; +use std::pin::{pin, Pin}; use std::sync::Arc; use std::task::{Context, Poll}; use std::time::{Duration, Instant}; @@ -565,11 +565,11 @@ impl CloseEvent { .await .map_or(Ok(()), |_| Err(Error::PoolClosed))?; - futures_util::pin_mut!(fut); + let mut fut = pin!(fut); // I find that this is clearer in intent than `futures_util::future::select()` // or `futures_util::select_biased!{}` (which isn't enabled anyway). - futures_util::future::poll_fn(|cx| { + std::future::poll_fn(|cx| { // Poll `fut` first as the wakeup event is more likely for it than `self`. if let Poll::Ready(ret) = fut.as_mut().poll(cx) { return Poll::Ready(Ok(ret)); diff --git a/sqlx-mysql/src/any.rs b/sqlx-mysql/src/any.rs index 0466bfc0a4..e01e41d68e 100644 --- a/sqlx-mysql/src/any.rs +++ b/sqlx-mysql/src/any.rs @@ -16,7 +16,7 @@ use sqlx_core::database::Database; use sqlx_core::describe::Describe; use sqlx_core::executor::Executor; use sqlx_core::transaction::TransactionManager; -use std::future; +use std::{future, pin::pin}; sqlx_core::declare_driver_with_optional_migrate!(DRIVER = MySql); @@ -113,8 +113,7 @@ impl AnyConnectionBackend for MySqlConnection { Box::pin(async move { let arguments = arguments?; - let stream = self.run(query, arguments, persistent).await?; - futures_util::pin_mut!(stream); + let mut stream = pin!(self.run(query, arguments, persistent).await?); while let Some(result) = stream.try_next().await? { if let Either::Right(row) = result { diff --git a/sqlx-mysql/src/connection/executor.rs b/sqlx-mysql/src/connection/executor.rs index d7f8fcfa14..d93aac0d68 100644 --- a/sqlx-mysql/src/connection/executor.rs +++ b/sqlx-mysql/src/connection/executor.rs @@ -21,8 +21,8 @@ use either::Either; use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; use futures_core::Stream; -use futures_util::{pin_mut, TryStreamExt}; -use std::{borrow::Cow, sync::Arc}; +use futures_util::TryStreamExt; +use std::{borrow::Cow, pin::pin, sync::Arc}; impl MySqlConnection { async fn prepare_statement<'c>( @@ -263,8 +263,7 @@ impl<'c> Executor<'c> for &'c mut MySqlConnection { Box::pin(try_stream! { let arguments = arguments?; - let s = self.run(sql, arguments, persistent).await?; - pin_mut!(s); + let mut s = pin!(self.run(sql, arguments, persistent).await?); while let Some(v) = s.try_next().await? { r#yield!(v); diff --git a/sqlx-postgres/src/any.rs b/sqlx-postgres/src/any.rs index efa9a044bc..a7b30fb65b 100644 --- a/sqlx-postgres/src/any.rs +++ b/sqlx-postgres/src/any.rs @@ -5,7 +5,7 @@ use crate::{ use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; use futures_util::{stream, StreamExt, TryFutureExt, TryStreamExt}; -use std::future; +use std::{future, pin::pin}; use sqlx_core::any::{ Any, AnyArguments, AnyColumn, AnyConnectOptions, AnyConnectionBackend, AnyQueryResult, AnyRow, @@ -115,8 +115,7 @@ impl AnyConnectionBackend for PgConnection { Box::pin(async move { let arguments = arguments?; - let stream = self.run(query, arguments, 1, persistent, None).await?; - futures_util::pin_mut!(stream); + let mut stream = pin!(self.run(query, arguments, 1, persistent, None).await?); if let Some(Either::Right(row)) = stream.try_next().await? { return Ok(Some(AnyRow::try_from(&row)?)); diff --git a/sqlx-postgres/src/connection/executor.rs b/sqlx-postgres/src/connection/executor.rs index 97503a5004..076c4209f6 100644 --- a/sqlx-postgres/src/connection/executor.rs +++ b/sqlx-postgres/src/connection/executor.rs @@ -15,10 +15,10 @@ use crate::{ use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; use futures_core::Stream; -use futures_util::{pin_mut, TryStreamExt}; +use futures_util::TryStreamExt; use sqlx_core::arguments::Arguments; use sqlx_core::Either; -use std::{borrow::Cow, sync::Arc}; +use std::{borrow::Cow, pin::pin, sync::Arc}; async fn prepare( conn: &mut PgConnection, @@ -393,8 +393,7 @@ impl<'c> Executor<'c> for &'c mut PgConnection { Box::pin(try_stream! { let arguments = arguments?; - let s = self.run(sql, arguments, 0, persistent, metadata).await?; - pin_mut!(s); + let mut s = pin!(self.run(sql, arguments, 0, persistent, metadata).await?); while let Some(v) = s.try_next().await? { r#yield!(v); @@ -420,8 +419,7 @@ impl<'c> Executor<'c> for &'c mut PgConnection { Box::pin(async move { let arguments = arguments?; - let s = self.run(sql, arguments, 1, persistent, metadata).await?; - pin_mut!(s); + let mut s = pin!(self.run(sql, arguments, 1, persistent, metadata).await?); // With deferred constraints we need to check all responses as we // could get a OK response (with uncommitted data), only to get an diff --git a/sqlx-sqlite/src/any.rs b/sqlx-sqlite/src/any.rs index 01600d9931..2cc5855405 100644 --- a/sqlx-sqlite/src/any.rs +++ b/sqlx-sqlite/src/any.rs @@ -17,6 +17,7 @@ use sqlx_core::database::Database; use sqlx_core::describe::Describe; use sqlx_core::executor::Executor; use sqlx_core::transaction::TransactionManager; +use std::pin::pin; sqlx_core::declare_driver_with_optional_migrate!(DRIVER = Sqlite); @@ -105,12 +106,12 @@ impl AnyConnectionBackend for SqliteConnection { let args = arguments.map(map_arguments); Box::pin(async move { - let stream = self - .worker - .execute(query, args, self.row_channel_size, persistent, Some(1)) - .map_ok(flume::Receiver::into_stream) - .await?; - futures_util::pin_mut!(stream); + let mut stream = pin!( + self.worker + .execute(query, args, self.row_channel_size, persistent, Some(1)) + .map_ok(flume::Receiver::into_stream) + .await? + ); if let Some(Either::Right(row)) = stream.try_next().await? { return Ok(Some(AnyRow::try_from(&row)?)); diff --git a/sqlx-sqlite/src/connection/executor.rs b/sqlx-sqlite/src/connection/executor.rs index 541a4f7d4d..1f6ce7726f 100644 --- a/sqlx-sqlite/src/connection/executor.rs +++ b/sqlx-sqlite/src/connection/executor.rs @@ -8,7 +8,7 @@ use sqlx_core::describe::Describe; use sqlx_core::error::Error; use sqlx_core::executor::{Execute, Executor}; use sqlx_core::Either; -use std::future; +use std::{future, pin::pin}; impl<'c> Executor<'c> for &'c mut SqliteConnection { type Database = Sqlite; @@ -56,13 +56,11 @@ impl<'c> Executor<'c> for &'c mut SqliteConnection { let persistent = query.persistent() && arguments.is_some(); Box::pin(async move { - let stream = self + let mut stream = pin!(self .worker .execute(sql, arguments, self.row_channel_size, persistent, Some(1)) .map_ok(flume::Receiver::into_stream) - .try_flatten_stream(); - - futures_util::pin_mut!(stream); + .try_flatten_stream()); while let Some(res) = stream.try_next().await? { if let Either::Right(row) = res { diff --git a/tests/postgres/postgres.rs b/tests/postgres/postgres.rs index 86707e23e6..7de4a9cdc6 100644 --- a/tests/postgres/postgres.rs +++ b/tests/postgres/postgres.rs @@ -9,7 +9,7 @@ use sqlx::{Column, Connection, Executor, Row, Statement, TypeInfo}; use sqlx_core::{bytes::Bytes, error::BoxDynError}; use sqlx_test::{new, pool, setup_if_needed}; use std::env; -use std::pin::Pin; +use std::pin::{pin, Pin}; use std::sync::Arc; use std::time::Duration; @@ -637,8 +637,7 @@ async fn pool_smoke_test() -> anyhow::Result<()> { let pool = pool.clone(); sqlx_core::rt::spawn(async move { while !pool.is_closed() { - let acquire = pool.acquire(); - futures::pin_mut!(acquire); + let mut acquire = pin!(pool.acquire()); // poll the acquire future once to put the waiter in the queue future::poll_fn(move |cx| { From 91291beb2338686423d827114e141804b4f88cbe Mon Sep 17 00:00:00 2001 From: Ben Wilber Date: Tue, 4 Feb 2025 13:52:43 -0500 Subject: [PATCH 23/66] feat(sqlx-cli): Add flag to disable automatic loading of .env files (#3724) * Add flag to disable automatic loading of .env files * Update sqlx-cli/src/opt.rs Co-authored-by: Austin Bonander --------- Co-authored-by: Austin Bonander --- sqlx-cli/src/bin/cargo-sqlx.rs | 5 ++++- sqlx-cli/src/bin/sqlx.rs | 9 +++++++-- sqlx-cli/src/opt.rs | 4 ++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/sqlx-cli/src/bin/cargo-sqlx.rs b/sqlx-cli/src/bin/cargo-sqlx.rs index 58f7b345f4..c87147b6a3 100644 --- a/sqlx-cli/src/bin/cargo-sqlx.rs +++ b/sqlx-cli/src/bin/cargo-sqlx.rs @@ -13,9 +13,12 @@ enum Cli { #[tokio::main] async fn main() { - dotenvy::dotenv().ok(); let Cli::Sqlx(opt) = Cli::parse(); + if !opt.no_dotenv { + dotenvy::dotenv().ok(); + } + if let Err(error) = sqlx_cli::run(opt).await { println!("{} {}", style("error:").bold().red(), error); process::exit(1); diff --git a/sqlx-cli/src/bin/sqlx.rs b/sqlx-cli/src/bin/sqlx.rs index 59025cd7da..c19b61f393 100644 --- a/sqlx-cli/src/bin/sqlx.rs +++ b/sqlx-cli/src/bin/sqlx.rs @@ -4,9 +4,14 @@ use sqlx_cli::Opt; #[tokio::main] async fn main() { - dotenvy::dotenv().ok(); + let opt = Opt::parse(); + + if !opt.no_dotenv { + dotenvy::dotenv().ok(); + } + // no special handling here - if let Err(error) = sqlx_cli::run(Opt::parse()).await { + if let Err(error) = sqlx_cli::run(opt).await { println!("{} {}", style("error:").bold().red(), error); std::process::exit(1); } diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index d5fe315234..07058aa147 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -7,6 +7,10 @@ use clap_complete::Shell; #[derive(Parser, Debug)] #[clap(version, about, author)] pub struct Opt { + /// Do not automatically load `.env` files. + #[clap(long)] + pub no_dotenv: bool, + #[clap(subcommand)] pub command: Command, } From f42561b8d73d591d7b91cd2d8ac2cb3fafd2f7cf Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Tue, 4 Feb 2025 19:53:05 +0100 Subject: [PATCH 24/66] chore: expose bstr feature (#3714) --- Cargo.toml | 2 ++ sqlx-core/src/types/mod.rs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index f93ed3dded..5a040e546f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ _unstable-all-types = [ "mac_address", "uuid", "bit-vec", + "bstr" ] # Base runtime features without TLS @@ -122,6 +123,7 @@ rust_decimal = ["sqlx-core/rust_decimal", "sqlx-macros?/rust_decimal", "sqlx-mys time = ["sqlx-core/time", "sqlx-macros?/time", "sqlx-mysql?/time", "sqlx-postgres?/time", "sqlx-sqlite?/time"] uuid = ["sqlx-core/uuid", "sqlx-macros?/uuid", "sqlx-mysql?/uuid", "sqlx-postgres?/uuid", "sqlx-sqlite?/uuid"] regexp = ["sqlx-sqlite?/regexp"] +bstr = ["sqlx-core/bstr"] [workspace.dependencies] # Core Crates diff --git a/sqlx-core/src/types/mod.rs b/sqlx-core/src/types/mod.rs index 25837b1e77..909dd4927b 100644 --- a/sqlx-core/src/types/mod.rs +++ b/sqlx-core/src/types/mod.rs @@ -85,6 +85,9 @@ pub mod mac_address { pub use json::{Json, JsonRawValue, JsonValue}; pub use text::Text; +#[cfg(feature = "bstr")] +pub use bstr::{BStr, BString}; + /// Indicates that a SQL type is supported for a database. /// /// ## Compile-time verification From 3a20a92a3fd90b84d5fd5fa3ebddc7f190e9ae1b Mon Sep 17 00:00:00 2001 From: tottoto Date: Thu, 6 Feb 2025 06:48:19 +0900 Subject: [PATCH 25/66] chore: replace rustls-pemfile with rustls-pki-types (#3725) --- Cargo.lock | 11 +---------- sqlx-core/Cargo.toml | 5 ++--- sqlx-core/src/net/tls/tls_rustls.rs | 22 ++++++++++------------ 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c112899415..c24d2ff8fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3143,15 +3143,6 @@ dependencies = [ "security-framework 3.2.0", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.10.1" @@ -3591,7 +3582,7 @@ dependencies = [ "rust_decimal", "rustls", "rustls-native-certs", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "sha2", diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index f767507bb4..dcd8083023 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -25,7 +25,7 @@ _tls-native-tls = ["native-tls"] _tls-rustls-aws-lc-rs = ["_tls-rustls", "rustls/aws-lc-rs", "webpki-roots"] _tls-rustls-ring-webpki = ["_tls-rustls", "rustls/ring", "webpki-roots"] _tls-rustls-ring-native-roots = ["_tls-rustls", "rustls/ring", "rustls-native-certs"] -_tls-rustls = ["rustls", "rustls-pemfile"] +_tls-rustls = ["rustls"] _tls-none = [] # support offline/decoupled building (enables serialization of `Describe`) @@ -39,8 +39,7 @@ tokio = { workspace = true, optional = true } # TLS native-tls = { version = "0.2.10", optional = true } -rustls = { version = "0.23.11", default-features = false, features = ["std", "tls12"], optional = true } -rustls-pemfile = { version = "2", optional = true } +rustls = { version = "0.23.15", default-features = false, features = ["std", "tls12"], optional = true } webpki-roots = { version = "0.26", optional = true } rustls-native-certs = { version = "0.8.0", optional = true } diff --git a/sqlx-core/src/net/tls/tls_rustls.rs b/sqlx-core/src/net/tls/tls_rustls.rs index d56859808c..1a85cf0ff9 100644 --- a/sqlx-core/src/net/tls/tls_rustls.rs +++ b/sqlx-core/src/net/tls/tls_rustls.rs @@ -1,5 +1,5 @@ use futures_util::future; -use std::io::{self, BufReader, Cursor, Read, Write}; +use std::io::{self, Read, Write}; use std::sync::Arc; use std::task::{Context, Poll}; @@ -9,7 +9,10 @@ use rustls::{ WebPkiServerVerifier, }, crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider}, - pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime}, + pki_types::{ + pem::{self, PemObject}, + CertificateDer, PrivateKeyDer, ServerName, UnixTime, + }, CertificateError, ClientConfig, ClientConnection, Error as TlsError, RootCertStore, }; @@ -141,9 +144,8 @@ where if let Some(ca) = tls_config.root_cert_path { let data = ca.data().await?; - let mut cursor = Cursor::new(data); - for result in rustls_pemfile::certs(&mut cursor) { + for result in CertificateDer::pem_slice_iter(&data) { let Ok(cert) = result else { return Err(Error::Tls(format!("Invalid certificate {ca}").into())); }; @@ -196,19 +198,15 @@ where } fn certs_from_pem(pem: Vec) -> Result>, Error> { - let cur = Cursor::new(pem); - let mut reader = BufReader::new(cur); - rustls_pemfile::certs(&mut reader) + CertificateDer::pem_slice_iter(&pem) .map(|result| result.map_err(|err| Error::Tls(err.into()))) .collect() } fn private_key_from_pem(pem: Vec) -> Result, Error> { - let cur = Cursor::new(pem); - let mut reader = BufReader::new(cur); - match rustls_pemfile::private_key(&mut reader) { - Ok(Some(key)) => Ok(key), - Ok(None) => Err(Error::Configuration("no keys found pem file".into())), + match PrivateKeyDer::from_pem_slice(&pem) { + Ok(key) => Ok(key), + Err(pem::Error::NoItemsFound) => Err(Error::Configuration("no keys found pem file".into())), Err(e) => Err(Error::Configuration(e.to_string().into())), } } From b8599141534b4bdadab218e7ace7691d5fdfb2f8 Mon Sep 17 00:00:00 2001 From: Ethan Wang Date: Tue, 18 Feb 2025 09:39:40 +0800 Subject: [PATCH 26/66] QueryBuilder: add `debug_assert` when `push_values` is passed an empty set of tuples (#3734) * throw a warning in tracing so that the empty tuples would be noticed * use debug assertion to throw a panic in debug mode --- Cargo.lock | 1 - sqlx-core/src/query_builder.rs | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index c24d2ff8fa..f34884628e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3582,7 +3582,6 @@ dependencies = [ "rust_decimal", "rustls", "rustls-native-certs", - "rustls-pki-types", "serde", "serde_json", "sha2", diff --git a/sqlx-core/src/query_builder.rs b/sqlx-core/src/query_builder.rs index 0d02048dcf..b242bf7b2a 100644 --- a/sqlx-core/src/query_builder.rs +++ b/sqlx-core/src/query_builder.rs @@ -323,6 +323,11 @@ where separated.push_unseparated(")"); } + debug_assert!( + separated.push_separator, + "No value being pushed. QueryBuilder may not build correct sql query!" + ); + separated.query_builder } From 8cdad44366cb49d0d0278e02f3bb3c2481883a3e Mon Sep 17 00:00:00 2001 From: Sam Lyon Date: Wed, 19 Feb 2025 18:23:45 -0500 Subject: [PATCH 27/66] docs: add some missing backticks (#3749) * add ending backticks to starting backticks that were missing them * fix table alignment --- sqlx-mysql/src/testing/mod.rs | 2 +- sqlx-postgres/src/testing/mod.rs | 2 +- sqlx-postgres/src/types/cube.rs | 4 ++-- sqlx-postgres/src/types/mod.rs | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sqlx-mysql/src/testing/mod.rs b/sqlx-mysql/src/testing/mod.rs index 9e462c1131..1981cf73c5 100644 --- a/sqlx-mysql/src/testing/mod.rs +++ b/sqlx-mysql/src/testing/mod.rs @@ -30,7 +30,7 @@ impl TestSupport for MySql { Box::pin(async move { let mut conn = MASTER_POOL .get() - .expect("cleanup_test() invoked outside `#[sqlx::test]") + .expect("cleanup_test() invoked outside `#[sqlx::test]`") .acquire() .await?; diff --git a/sqlx-postgres/src/testing/mod.rs b/sqlx-postgres/src/testing/mod.rs index a927191dc9..373bd772dd 100644 --- a/sqlx-postgres/src/testing/mod.rs +++ b/sqlx-postgres/src/testing/mod.rs @@ -30,7 +30,7 @@ impl TestSupport for Postgres { Box::pin(async move { let mut conn = MASTER_POOL .get() - .expect("cleanup_test() invoked outside `#[sqlx::test]") + .expect("cleanup_test() invoked outside `#[sqlx::test]`") .acquire() .await?; diff --git a/sqlx-postgres/src/types/cube.rs b/sqlx-postgres/src/types/cube.rs index f39d82651c..cc2a016090 100644 --- a/sqlx-postgres/src/types/cube.rs +++ b/sqlx-postgres/src/types/cube.rs @@ -20,7 +20,7 @@ const IS_POINT_FLAG: u32 = 1 << 31; #[derive(Debug, Clone, PartialEq)] pub enum PgCube { /// A one-dimensional point. - // FIXME: `Point1D(f64) + // FIXME: `Point1D(f64)` Point(f64), /// An N-dimensional point ("represented internally as a zero-volume cube"). // FIXME: `PointND(f64)` @@ -32,7 +32,7 @@ pub enum PgCube { // FIXME: add `Cube3D { lower_left: [f64; 3], upper_right: [f64; 3] }`? /// An N-dimensional cube with points representing lower-left and upper-right corners, respectively. - // FIXME: CubeND { lower_left: Vec, upper_right: Vec }` + // FIXME: `CubeND { lower_left: Vec, upper_right: Vec }` MultiDimension(Vec>), } diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index e53f88ffec..a5fd708366 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -21,10 +21,10 @@ //! | [`PgLQuery`] | LQUERY | //! | [`PgCiText`] | CITEXT1 | //! | [`PgCube`] | CUBE | -//! | [`PgPoint] | POINT | -//! | [`PgLine] | LINE | -//! | [`PgLSeg] | LSEG | -//! | [`PgBox] | BOX | +//! | [`PgPoint`] | POINT | +//! | [`PgLine`] | LINE | +//! | [`PgLSeg`] | LSEG | +//! | [`PgBox`] | BOX | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., From 520d25c4537ee81f57daad3bf6839ebf1742ddd7 Mon Sep 17 00:00:00 2001 From: tottoto Date: Fri, 21 Feb 2025 04:35:44 +0900 Subject: [PATCH 28/66] chore(cli): remove unused async-trait crate from dependencies (#3754) --- Cargo.lock | 1 - sqlx-cli/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f34884628e..e54d82c0c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3529,7 +3529,6 @@ version = "0.8.3" dependencies = [ "anyhow", "assert_cmd", - "async-trait", "backoff", "cargo_metadata", "chrono", diff --git a/sqlx-cli/Cargo.toml b/sqlx-cli/Cargo.toml index 0b047ab136..00ac0df886 100644 --- a/sqlx-cli/Cargo.toml +++ b/sqlx-cli/Cargo.toml @@ -37,7 +37,6 @@ clap = { version = "4.3.10", features = ["derive", "env"] } clap_complete = { version = "4.3.1", optional = true } chrono = { version = "0.4.19", default-features = false, features = ["clock"] } anyhow = "1.0.52" -async-trait = "0.1.52" console = "0.15.0" promptly = "0.3.0" serde_json = "1.0.73" From 26b4e5f16637eea789e5902381b58cc652bed2c2 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Thu, 20 Feb 2025 12:51:54 -0800 Subject: [PATCH 29/66] Update pull_request_template.md --- .github/pull_request_template.md | 50 +++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a86c28731e..6d193065c2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,2 +1,50 @@ + + ### Does your PR solve an issue? -### Delete this text and add "fixes #(issue number)" +Delete this text and add "fixes #(issue number)". + +Do *not* just list issue numbers here as they will not be automatically closed on merging this pull request unless prefixed with "fixes" or "closes". + +### Is this a breaking change? +Delete this text and answer yes/no and explain. + +If yes, this pull request will need to wait for the next major release (`0.{x + 1}.0`) + +Behavior changes _can_ be breaking if significant enough. +Consider [Hyrum's Law](https://www.hyrumslaw.com/): + +> With a sufficient number of users of an API, +> it does not matter what you promise in the contract: +> all observable behaviors of your system +> will be depended on by somebody. From 8dce6bd9e1940d11757e2e5b29d93785870dbc07 Mon Sep 17 00:00:00 2001 From: Stefan Schindler Date: Thu, 20 Feb 2025 21:57:54 +0100 Subject: [PATCH 30/66] Fix example calculation (#3741) --- sqlx-core/src/pool/options.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-core/src/pool/options.rs b/sqlx-core/src/pool/options.rs index 96dbf8ee3d..3d048f1795 100644 --- a/sqlx-core/src/pool/options.rs +++ b/sqlx-core/src/pool/options.rs @@ -484,7 +484,7 @@ impl PoolOptions { /// .await?; /// /// // Close the connection if the backend memory usage exceeds 256 MiB. - /// Ok(total_memory_usage <= (2 << 28)) + /// Ok(total_memory_usage <= (1 << 28)) /// })) /// .connect("postgres:// …").await?; /// # Ok(()) From a9dfb4f2c1f370845162a475f003919b7b6ee69f Mon Sep 17 00:00:00 2001 From: kildrens <5198060+kildrens@users.noreply.github.com> Date: Fri, 21 Feb 2025 02:13:56 +0000 Subject: [PATCH 31/66] Avoid privilege requirements by using an advisory lock in test setup (postgres). (#3753) * feat(sqlx-postgres): use advisory lock to avoid setup race condition * fix(sqlx-postgres): numeric hex constants not supported before postgres 16 --- sqlx-postgres/src/testing/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlx-postgres/src/testing/mod.rs b/sqlx-postgres/src/testing/mod.rs index 373bd772dd..af20fe87ea 100644 --- a/sqlx-postgres/src/testing/mod.rs +++ b/sqlx-postgres/src/testing/mod.rs @@ -134,8 +134,9 @@ async fn test_context(args: &TestArgs) -> Result, Error> { // I couldn't find a bug on the mailing list for `CREATE SCHEMA` specifically, // but a clearly related bug with `CREATE TABLE` has been known since 2007: // https://www.postgresql.org/message-id/200710222037.l9MKbCJZ098744%40wwwmaster.postgresql.org + // magic constant 8318549251334697844 is just 8 ascii bytes 'sqlxtest'. r#" - lock table pg_catalog.pg_namespace in share row exclusive mode; + select pg_advisory_xact_lock(8318549251334697844); create schema if not exists _sqlx_test; From 3dec1d186a10bfd82c0d19c1dc6f9bb8d7a240e7 Mon Sep 17 00:00:00 2001 From: Marti Serra Date: Sat, 22 Feb 2025 22:33:14 +0100 Subject: [PATCH 32/66] Small doc correction. (#3755) When sqlx-core/src/from_row.rs was updated to implement FromRow for tuples of up to 16 values, a comment was left stating that it was implemented up to tuples of 9 values. --- sqlx-core/src/from_row.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-core/src/from_row.rs b/sqlx-core/src/from_row.rs index 66e53d55b8..ecd5847f30 100644 --- a/sqlx-core/src/from_row.rs +++ b/sqlx-core/src/from_row.rs @@ -313,7 +313,7 @@ where } // implement FromRow for tuples of types that implement Decode -// up to tuples of 9 values +// up to tuples of 16 values macro_rules! impl_from_row_for_tuple { ($( ($idx:tt) -> $T:ident );+;) => { From 9bd3d6192959675b3b58e3a71d9a9d0e9bbaffb3 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Sat, 22 Feb 2025 18:07:43 -0800 Subject: [PATCH 33/66] Update FAQ.md --- FAQ.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/FAQ.md b/FAQ.md index f0bccd3c41..cf13cf73ee 100644 --- a/FAQ.md +++ b/FAQ.md @@ -36,6 +36,62 @@ as they can often be a whole year or more out-of-date. [`rust-version`]: https://doc.rust-lang.org/stable/cargo/reference/manifest.html#the-rust-version-field +---------------------------------------------------------------- + +### Can SQLx Add Support for New Databases? + +We are always open to discuss adding support for new databases, but as of writing, have no plans to in the short term. + +Implementing support for a new database in SQLx is a _huge_ lift. Expecting this work to be done for free is highly unrealistic. +In all likelihood, the implementation would need to be written from scratch. +Even if Rust bindings exist, they may not support `async`. +Even if they support `async`, they may only support either Tokio or `async-std`, and not both. +Even if they support Tokio and `async-std`, the API may not be flexible enough or provide sufficient information (e.g. for implementing the macros). + +If we have to write the implementation from scratch, is the protocol publicly documented, and stable? + +Even if everything is supported on the client side, how will we run tests against the database? Is it open-source, or proprietary? Will it require a paid license? + +For example, Oracle Database's protocol is proprietary and only supported through their own libraries, which do not support Rust, and only have blocking APIs (see: [Oracle Call Interface for C](https://docs.oracle.com/en/database/oracle/oracle-database/23/lnoci/index.html)). +This makes it a poor candidate for an async-native crate like SQLx--though we support SQLite, which also only has a blocking API, that's the exception and not the rule. Wrapping blocking APIs is not very scalable. + +We still have plans to bring back the MSSQL driver, but this is not feasible as of writing with the current maintenance workload. Should this change, an announcement will be made on Github as well as our [Discord server](https://discord.gg/uuruzJ7). + +### What If I'm Willing to Contribute the Implementation? + +Being willing to contribute an implementation for a new database is one thing, but there's also the ongoing maintenance burden to consider. + +Are you willing to provide support long-term? +Will there be enough users that we can rely on outside contributions? +Or is support going to fall to the current maintainer(s)? + +This is the kind of thing that will need to be supported in SQLx _long_ after the initial implementation, or else later need to be removed. +If you don't have plans for how to support a new driver long-term, then it doesn't belong as part of SQLx itself. + +However, drivers don't necessarily need to live _in_ SQLx anymore. Since 0.7.0, drivers don't need to be compiled-in to be functional. +Support for third-party drivers in `sqlx-cli` and the `query!()` macros is pending, as well as documenting the process of writing a driver, but contributions are welcome in this regard. + +For example, see [sqlx-exasol](https://crates.io/crates/sqlx-exasol). + +---------------------------------------------------------------- +### Can SQLx Add Support for New Data-Type Crates (e.g. Jiff in addition to `chrono` and `time`)? + +This has a lot of the same considerations as adding support for new databases (see above), but with one big additional problem: Semantic Versioning. + +When we add trait implementations for types from an external crate, that crate then becomes part of our public API. We become beholden to its release cycle. + +If the crate's API is still evolving, meaning they are making breaking changes frequently, and thus releasing new major versions frequently, that then becomes a burden on us to upgrade and release a new major version as well so everyone _else_ can upgrade. + +We don't have the maintainer bandwidth to support multiple major versions simultaneously (we have no Long-Term Support policy), so this means that users who want to keep up-to-date are forced to make frequent manual upgrades as well. + +Thus, it is best that we stick to only supporting crates which have a stable API, and which are not making new major releases frequently. + +Conversely, adding support for SQLx _in_ these crates may not be desirable either, since SQLx is a large dependency and a higher-level crate. In this case, the SemVer problem gets pushed onto the other crate. + +There isn't a satisfying answer to this problem, but one option is to have an intermediate wrapper crate. +For example, [`jiff-sqlx`](https://crates.io/crates/jiff-sqlx), which is maintained by the author of Jiff. +API changes to SQLx are pending to make this pattern easier to use. + ---------------------------------------------------------------- ### I'm getting `HandshakeFailure` or `CorruptMessage` when trying to connect to a server over TLS using RusTLS. What gives? From 277dd36c7868acb10eae20f50418e273b71c8499 Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Thu, 27 Feb 2025 21:17:09 +0100 Subject: [PATCH 34/66] refactor(cli): replace promptly with dialoguer (#3669) --- Cargo.lock | 183 ++++++--------------------------------- sqlx-cli/Cargo.toml | 4 +- sqlx-cli/src/database.rs | 67 +++++++++----- sqlx-cli/src/lib.rs | 21 +++++ 4 files changed, 92 insertions(+), 183 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e54d82c0c9..07754e7c22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,17 +834,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" -[[package]] -name = "clipboard-win" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" -dependencies = [ - "error-code", - "str-buf", - "winapi", -] - [[package]] name = "cmake" version = "0.1.52" @@ -1120,6 +1109,17 @@ dependencies = [ "serde", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "thiserror 1.0.69", +] + [[package]] name = "difflib" version = "0.4.0" @@ -1138,27 +1138,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -1209,12 +1188,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" -[[package]] -name = "endian-type" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" - [[package]] name = "env_filter" version = "0.1.3" @@ -1261,17 +1234,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "error-code" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" -dependencies = [ - "libc", - "str-buf", + "windows-sys 0.59.0", ] [[package]] @@ -1327,17 +1290,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fd-lock" -version = "3.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" -dependencies = [ - "cfg-if", - "rustix 0.38.43", - "windows-sys 0.48.0", -] - [[package]] name = "filetime" version = "0.2.25" @@ -2102,7 +2054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2186,7 +2138,7 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8836fae9d0d4be2c8b4efcdd79e828a2faa058a90d005abf42f91cac5493a08e" dependencies = [ - "nix 0.28.0", + "nix", "winapi", ] @@ -2212,15 +2164,6 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -2318,28 +2261,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nibble_vec" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" -dependencies = [ - "smallvec", -] - -[[package]] -name = "nix" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - [[package]] name = "nix" version = "0.28.0" @@ -2350,7 +2271,7 @@ dependencies = [ "cfg-if", "cfg_aliases 0.1.1", "libc", - "memoffset 0.9.1", + "memoffset", ] [[package]] @@ -2805,15 +2726,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "promptly" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9acbc6c5a5b029fe58342f58445acb00ccfe24624e538894bc2f04ce112980ba" -dependencies = [ - "rustyline", -] - [[package]] name = "ptr_meta" version = "0.1.4" @@ -2849,16 +2761,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" -[[package]] -name = "radix_trie" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" -dependencies = [ - "endian-type", - "nibble_vec", -] - [[package]] name = "rand" version = "0.8.5" @@ -2948,17 +2850,6 @@ dependencies = [ "bitflags 2.7.0", ] -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom", - "libredox", - "thiserror 1.0.69", -] - [[package]] name = "regex" version = "1.11.1" @@ -3113,7 +3004,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3167,30 +3058,6 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" -[[package]] -name = "rustyline" -version = "9.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7826789c0e25614b03e5a54a0717a86f9ff6e6e5247f92b369472869320039" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "clipboard-win", - "dirs-next", - "fd-lock", - "libc", - "log", - "memchr", - "nix 0.23.2", - "radix_trie", - "scopeguard", - "smallvec", - "unicode-segmentation", - "unicode-width 0.1.14", - "utf8parse", - "winapi", -] - [[package]] name = "ryu" version = "1.0.18" @@ -3384,6 +3251,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -3535,12 +3408,12 @@ dependencies = [ "clap", "clap_complete", "console", + "dialoguer", "dotenvy", "filetime", "futures", "glob", "openssl", - "promptly", "serde_json", "sqlx", "tempfile", @@ -3908,12 +3781,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "str-buf" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" - [[package]] name = "stringprep" version = "0.1.5" @@ -4099,7 +3966,7 @@ dependencies = [ "getrandom", "once_cell", "rustix 0.38.43", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4775,7 +4642,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/sqlx-cli/Cargo.toml b/sqlx-cli/Cargo.toml index 00ac0df886..582099a2f6 100644 --- a/sqlx-cli/Cargo.toml +++ b/sqlx-cli/Cargo.toml @@ -26,7 +26,7 @@ path = "src/bin/cargo-sqlx.rs" [dependencies] dotenvy = "0.15.0" -tokio = { version = "1.15.0", features = ["macros", "rt", "rt-multi-thread"] } +tokio = { version = "1.15.0", features = ["macros", "rt", "rt-multi-thread", "signal"] } sqlx = { workspace = true, default-features = false, features = [ "runtime-tokio", "migrate", @@ -38,7 +38,7 @@ clap_complete = { version = "4.3.1", optional = true } chrono = { version = "0.4.19", default-features = false, features = ["clock"] } anyhow = "1.0.52" console = "0.15.0" -promptly = "0.3.0" +dialoguer = { version = "0.11", default-features = false } serde_json = "1.0.73" glob = "0.3.0" openssl = { version = "0.10.38", optional = true } diff --git a/sqlx-cli/src/database.rs b/sqlx-cli/src/database.rs index 7a2056ab35..7a9bc6bf2f 100644 --- a/sqlx-cli/src/database.rs +++ b/sqlx-cli/src/database.rs @@ -1,9 +1,11 @@ use crate::migrate; use crate::opt::ConnectOpts; -use console::style; -use promptly::{prompt, ReadlineError}; +use console::{style, Term}; +use dialoguer::Confirm; use sqlx::any::Any; use sqlx::migrate::MigrateDatabase; +use std::{io, mem}; +use tokio::task; pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> { // NOTE: only retry the idempotent action. @@ -24,7 +26,7 @@ pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> { } pub async fn drop(connect_opts: &ConnectOpts, confirm: bool, force: bool) -> anyhow::Result<()> { - if confirm && !ask_to_continue_drop(connect_opts.required_db_url()?) { + if confirm && !ask_to_continue_drop(connect_opts.required_db_url()?.to_owned()).await { return Ok(()); } @@ -58,27 +60,46 @@ pub async fn setup(migration_source: &str, connect_opts: &ConnectOpts) -> anyhow migrate::run(migration_source, connect_opts, false, false, None).await } -fn ask_to_continue_drop(db_url: &str) -> bool { - loop { - let r: Result = - prompt(format!("Drop database at {}? (y/n)", style(db_url).cyan())); - match r { - Ok(response) => { - if response == "n" || response == "N" { - return false; - } else if response == "y" || response == "Y" { - return true; - } else { - println!( - "Response not recognized: {}\nPlease type 'y' or 'n' and press enter.", - response - ); - } - } - Err(e) => { - println!("{e}"); - return false; +async fn ask_to_continue_drop(db_url: String) -> bool { + // If the setup operation is cancelled while we are waiting for the user to decide whether + // or not to drop the database, this will restore the terminal's cursor to its normal state. + struct RestoreCursorGuard { + disarmed: bool, + } + + impl Drop for RestoreCursorGuard { + fn drop(&mut self) { + if !self.disarmed { + Term::stderr().show_cursor().unwrap() } } } + + let mut guard = RestoreCursorGuard { disarmed: false }; + + let decision_result = task::spawn_blocking(move || { + Confirm::new() + .with_prompt(format!("Drop database at {}?", style(&db_url).cyan())) + .wait_for_newline(true) + .default(false) + .show_default(true) + .interact() + }) + .await + .expect("Confirm thread panicked"); + match decision_result { + Ok(decision) => { + guard.disarmed = true; + decision + } + Err(dialoguer::Error::IO(err)) if err.kind() == io::ErrorKind::Interrupted => { + // Sometimes CTRL + C causes this error to be returned + mem::drop(guard); + false + } + Err(err) => { + mem::drop(guard); + panic!("Confirm dialog failed with {err}") + } + } } diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index bfd71e4bc1..a182f019b9 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -5,6 +5,7 @@ use anyhow::Result; use futures::{Future, TryFutureExt}; use sqlx::{AnyConnection, Connection}; +use tokio::{select, signal}; use crate::opt::{Command, ConnectOpts, DatabaseCommand, MigrateCommand}; @@ -21,6 +22,26 @@ mod prepare; pub use crate::opt::Opt; pub async fn run(opt: Opt) -> Result<()> { + // This `select!` is here so that when the process receives a `SIGINT` (CTRL + C), + // the futures currently running on this task get dropped before the program exits. + // This is currently necessary for the consumers of the `dialoguer` crate to restore + // the user's terminal if the process is interrupted while a dialog is being displayed. + + let ctrlc_fut = signal::ctrl_c(); + let do_run_fut = do_run(opt); + + select! { + biased; + _ = ctrlc_fut => { + Ok(()) + }, + do_run_outcome = do_run_fut => { + do_run_outcome + } + } +} + +async fn do_run(opt: Opt) -> Result<()> { match opt.command { Command::Migrate(migrate) => match migrate.command { MigrateCommand::Add { From 5d6d6985cd2274dc90501a29dcb58b54befe91a1 Mon Sep 17 00:00:00 2001 From: Jonas Malaco Date: Fri, 28 Feb 2025 21:42:53 -0300 Subject: [PATCH 35/66] docs(pool): recommend actix-web ThinData over Data to avoid two Arcs (#3762) Both actix_web::web::Data and sqlx::PgPool internally wrap an Arc. Thus, using Data as an extractor in an actix-web route handler results in two Arcs wrapping the data of interest, which isn't ideal. Actix-web 4.9.0 introduced a new web::ThinData extractor for cases like this, where the data is already wrapped in an `Arc` (or is otherwise similarly cheap and sensible to simply clone), which doesn't wrap the inner value in a (second) Arc. Since the new extractor is better suited to the task, suggest it in place of web::Data when giving an example on how to share a pool. --- sqlx-core/src/pool/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sqlx-core/src/pool/mod.rs b/sqlx-core/src/pool/mod.rs index 042bc5c7bc..8aa9041ab7 100644 --- a/sqlx-core/src/pool/mod.rs +++ b/sqlx-core/src/pool/mod.rs @@ -109,7 +109,8 @@ mod options; /// application/daemon/web server/etc. and then shared with all tasks throughout the process' /// lifetime. How best to accomplish this depends on your program architecture. /// -/// In Actix-Web, for example, you can share a single pool with all request handlers using [web::Data]. +/// In Actix-Web, for example, you can efficiently share a single pool with all request handlers +/// using [web::ThinData]. /// /// Cloning `Pool` is cheap as it is simply a reference-counted handle to the inner pool state. /// When the last remaining handle to the pool is dropped, the connections owned by the pool are @@ -131,7 +132,7 @@ mod options; /// * [PgPool][crate::postgres::PgPool] (PostgreSQL) /// * [SqlitePool][crate::sqlite::SqlitePool] (SQLite) /// -/// [web::Data]: https://docs.rs/actix-web/3/actix_web/web/struct.Data.html +/// [web::ThinData]: https://docs.rs/actix-web/4.9.0/actix_web/web/struct.ThinData.html /// /// ### Note: Drop Behavior /// Due to a lack of async `Drop`, dropping the last `Pool` handle may not immediately clean From c5ea6c44355292e4a03f3ec8266b085278648e44 Mon Sep 17 00:00:00 2001 From: Mattia Righetti Date: Sun, 2 Mar 2025 22:29:29 +0000 Subject: [PATCH 36/66] feat: sqlx sqlite expose de/serialize (#3745) * feat: implement serialze no copy on lockedsqlitehandle * feat: implement serialize on sqliteconnection * feat: implement deserialize on sqliteconnection and add sqlitebuf wrapper type * refactor: misc sqlite type and deserialize refactoring * chore: misc clippy refactoring * fix: misc refactoring and fixes - pass non-owned byte slice to deserialize - `SqliteBufError` and better error handling - more impl for `SqliteOnwedBuf` so it can be used as a slice - default serialize for `SqliteConnection` * refactor: move serialize and deserialize on worker thread This implements `Command::Serialize` and `Command::Deserialize` and moves the serialize and deserialize logic to the worker thread. `Serialize` will need some more iterations as it's not clear whether it would need to wait for other write transactions before running. * refactor: misc refactoring and changes - Merged deserialize module with serialize module - Moved `SqliteOwnedBuf` into serialize module - Fixed rustdocs * chore: API tweaks, better docs, tests * fix: unused import * fix: export `SqliteOwnedBuf`, docs and safety tweaks --------- Co-authored-by: Austin Bonander --- sqlx-core/src/error.rs | 14 +- sqlx-sqlite/src/connection/collation.rs | 5 +- sqlx-sqlite/src/connection/establish.rs | 36 ++- sqlx-sqlite/src/connection/handle.rs | 11 + sqlx-sqlite/src/connection/mod.rs | 3 +- sqlx-sqlite/src/connection/serialize.rs | 297 ++++++++++++++++++++++++ sqlx-sqlite/src/connection/worker.rs | 41 ++++ sqlx-sqlite/src/error.rs | 48 +++- sqlx-sqlite/src/lib.rs | 1 + sqlx-sqlite/src/statement/handle.rs | 4 +- sqlx-sqlite/src/statement/virtual.rs | 2 +- tests/sqlite/sqlite.rs | 131 ++++++++++- 12 files changed, 549 insertions(+), 44 deletions(-) create mode 100644 sqlx-sqlite/src/connection/serialize.rs diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index 17774addd2..98b42fbcde 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -34,6 +34,12 @@ pub enum Error { #[error("error with configuration: {0}")] Configuration(#[source] BoxDynError), + /// One or more of the arguments to the called function was invalid. + /// + /// The string contains more information. + #[error("{0}")] + InvalidArgument(String), + /// Error returned from the database. #[error("error returned from database: {0}")] Database(#[source] Box), @@ -79,7 +85,7 @@ pub enum Error { }, /// Error occured while encoding a value. - #[error("error occured while encoding a value: {0}")] + #[error("error occurred while encoding a value: {0}")] Encode(#[source] BoxDynError), /// Error occurred while decoding a value. @@ -136,6 +142,12 @@ impl Error { Error::Protocol(err.to_string()) } + #[doc(hidden)] + #[inline] + pub fn database(err: impl DatabaseError) -> Self { + Error::Database(Box::new(err)) + } + #[doc(hidden)] #[inline] pub fn config(err: impl StdError + Send + Sync + 'static) -> Self { diff --git a/sqlx-sqlite/src/connection/collation.rs b/sqlx-sqlite/src/connection/collation.rs index 573a9af892..e7422138bc 100644 --- a/sqlx-sqlite/src/connection/collation.rs +++ b/sqlx-sqlite/src/connection/collation.rs @@ -10,7 +10,6 @@ use libsqlite3_sys::{sqlite3_create_collation_v2, SQLITE_OK, SQLITE_UTF8}; use crate::connection::handle::ConnectionHandle; use crate::error::Error; -use crate::SqliteError; #[derive(Clone)] pub struct Collation { @@ -67,7 +66,7 @@ impl Collation { } else { // The xDestroy callback is not called if the sqlite3_create_collation_v2() function fails. drop(unsafe { Arc::from_raw(raw_f) }); - Err(Error::Database(Box::new(SqliteError::new(handle.as_ptr())))) + Err(handle.expect_error().into()) } } } @@ -112,7 +111,7 @@ where } else { // The xDestroy callback is not called if the sqlite3_create_collation_v2() function fails. drop(unsafe { Box::from_raw(boxed_f) }); - Err(Error::Database(Box::new(SqliteError::new(handle.as_ptr())))) + Err(handle.expect_error().into()) } } diff --git a/sqlx-sqlite/src/connection/establish.rs b/sqlx-sqlite/src/connection/establish.rs index 5b8aa01b62..334b1616ae 100644 --- a/sqlx-sqlite/src/connection/establish.rs +++ b/sqlx-sqlite/src/connection/establish.rs @@ -204,10 +204,10 @@ impl EstablishParams { // SAFE: tested for NULL just above // This allows any returns below to close this handle with RAII - let handle = unsafe { ConnectionHandle::new(handle) }; + let mut handle = unsafe { ConnectionHandle::new(handle) }; if status != SQLITE_OK { - return Err(Error::Database(Box::new(SqliteError::new(handle.as_ptr())))); + return Err(Error::Database(Box::new(handle.expect_error()))); } // Enable extended result codes @@ -226,33 +226,29 @@ impl EstablishParams { for ext in self.extensions.iter() { // `sqlite3_load_extension` is unusual as it returns its errors via an out-pointer // rather than by calling `sqlite3_errmsg` - let mut error = null_mut(); + let mut error_msg = null_mut(); status = unsafe { sqlite3_load_extension( handle.as_ptr(), ext.0.as_ptr(), ext.1.as_ref().map_or(null(), |e| e.as_ptr()), - addr_of_mut!(error), + addr_of_mut!(error_msg), ) }; if status != SQLITE_OK { + let mut e = handle.expect_error(); + // SAFETY: We become responsible for any memory allocation at `&error`, so test // for null and take an RAII version for returns - let err_msg = if !error.is_null() { - unsafe { - let e = CStr::from_ptr(error).into(); - sqlite3_free(error as *mut c_void); - e - } - } else { - CString::new("Unknown error when loading extension") - .expect("text should be representable as a CString") - }; - return Err(Error::Database(Box::new(SqliteError::extension( - handle.as_ptr(), - &err_msg, - )))); + if !error_msg.is_null() { + e = e.with_message(unsafe { + let msg = CStr::from_ptr(error_msg).to_string_lossy().into(); + sqlite3_free(error_msg as *mut c_void); + msg + }); + } + return Err(Error::Database(Box::new(e))); } } // Preempt any hypothetical security issues arising from leaving ENABLE_LOAD_EXTENSION // on by disabling the flag again once we've loaded all the requested modules. @@ -271,7 +267,7 @@ impl EstablishParams { // configure a `regexp` function for sqlite, it does not come with one by default let status = crate::regexp::register(handle.as_ptr()); if status != SQLITE_OK { - return Err(Error::Database(Box::new(SqliteError::new(handle.as_ptr())))); + return Err(Error::Database(Box::new(handle.expect_error()))); } } @@ -286,7 +282,7 @@ impl EstablishParams { status = unsafe { sqlite3_busy_timeout(handle.as_ptr(), ms) }; if status != SQLITE_OK { - return Err(Error::Database(Box::new(SqliteError::new(handle.as_ptr())))); + return Err(Error::Database(Box::new(handle.expect_error()))); } Ok(ConnectionState { diff --git a/sqlx-sqlite/src/connection/handle.rs b/sqlx-sqlite/src/connection/handle.rs index aaf5b74eaa..60fbe17dc6 100644 --- a/sqlx-sqlite/src/connection/handle.rs +++ b/sqlx-sqlite/src/connection/handle.rs @@ -46,6 +46,17 @@ impl ConnectionHandle { unsafe { sqlite3_last_insert_rowid(self.as_ptr()) } } + pub(crate) fn last_error(&mut self) -> Option { + // SAFETY: we have exclusive access to the database handle + unsafe { SqliteError::try_new(self.as_ptr()) } + } + + #[track_caller] + pub(crate) fn expect_error(&mut self) -> SqliteError { + self.last_error() + .expect("expected error code to be set in current context") + } + pub(crate) fn exec(&mut self, query: impl Into) -> Result<(), Error> { let query = query.into(); let query = CString::new(query).map_err(|_| err_protocol!("query contains nul bytes"))?; diff --git a/sqlx-sqlite/src/connection/mod.rs b/sqlx-sqlite/src/connection/mod.rs index 7412eef12f..3316ad40c6 100644 --- a/sqlx-sqlite/src/connection/mod.rs +++ b/sqlx-sqlite/src/connection/mod.rs @@ -40,6 +40,7 @@ mod handle; pub(crate) mod intmap; #[cfg(feature = "preupdate-hook")] mod preupdate_hook; +pub(crate) mod serialize; mod worker; @@ -544,7 +545,7 @@ impl LockedSqliteHandle<'_> { } pub fn last_error(&mut self) -> Option { - SqliteError::try_new(self.guard.handle.as_ptr()) + self.guard.handle.last_error() } } diff --git a/sqlx-sqlite/src/connection/serialize.rs b/sqlx-sqlite/src/connection/serialize.rs new file mode 100644 index 0000000000..c8835093da --- /dev/null +++ b/sqlx-sqlite/src/connection/serialize.rs @@ -0,0 +1,297 @@ +use super::ConnectionState; +use crate::{error::Error, SqliteConnection, SqliteError}; +use libsqlite3_sys::{ + sqlite3_deserialize, sqlite3_free, sqlite3_malloc64, sqlite3_serialize, + SQLITE_DESERIALIZE_FREEONCLOSE, SQLITE_DESERIALIZE_READONLY, SQLITE_DESERIALIZE_RESIZEABLE, + SQLITE_NOMEM, SQLITE_OK, +}; +use std::ffi::c_char; +use std::fmt::Debug; +use std::{ + ops::{Deref, DerefMut}, + ptr, + ptr::NonNull, +}; + +impl SqliteConnection { + /// Serialize the given SQLite database schema using [`sqlite3_serialize()`]. + /// + /// The returned buffer is a SQLite managed allocation containing the equivalent data + /// as writing the database to disk. It is freed on-drop. + /// + /// To serialize the primary, unqualified schema (`main`), pass `None` for the schema name. + /// + /// # Errors + /// * [`Error::InvalidArgument`] if the schema name contains a zero/NUL byte (`\0`). + /// * [`Error::Database`] if the schema does not exist or another error occurs. + /// + /// [`sqlite3_serialize()`]: https://sqlite.org/c3ref/serialize.html + pub async fn serialize(&mut self, schema: Option<&str>) -> Result { + let schema = schema.map(SchemaName::try_from).transpose()?; + + self.worker.serialize(schema).await + } + + /// Deserialize a SQLite database from a buffer into the specified schema using [`sqlite3_deserialize()`]. + /// + /// The given schema will be disconnected and re-connected as an in-memory database + /// backed by `data`, which should be the serialized form of a database previously returned + /// by a call to [`Self::serialize()`], documented as being equivalent to + /// the contents of the database file on disk. + /// + /// An error will be returned if a schema with the given name is not already attached. + /// You can use `ATTACH ':memory' as ""` to create an empty schema first. + /// + /// Pass `None` to deserialize to the primary, unqualified schema (`main`). + /// + /// The SQLite connection will take ownership of `data` and will free it when the connection + /// is closed or the schema is detached ([`SQLITE_DESERIALIZE_FREEONCLOSE`][deserialize-flags]). + /// + /// If `read_only` is `true`, the schema is opened as read-only ([`SQLITE_DESERIALIZE_READONLY`][deserialize-flags]). + /// If `false`, the schema is marked as resizable ([`SQLITE_DESERIALIZE_RESIZABLE`][deserialize-flags]). + /// + /// If the database is in WAL mode, an error is returned. + /// See [`sqlite3_deserialize()`] for details. + /// + /// # Errors + /// * [`Error::InvalidArgument`] if the schema name contains a zero/NUL byte (`\0`). + /// * [`Error::Database`] if an error occurs during deserialization. + /// + /// [`sqlite3_deserialize()`]: https://sqlite.org/c3ref/deserialize.html + /// [deserialize-flags]: https://sqlite.org/c3ref/c_deserialize_freeonclose.html + pub async fn deserialize( + &mut self, + schema: Option<&str>, + data: SqliteOwnedBuf, + read_only: bool, + ) -> Result<(), Error> { + let schema = schema.map(SchemaName::try_from).transpose()?; + + self.worker.deserialize(schema, data, read_only).await + } +} + +pub(crate) fn serialize( + conn: &mut ConnectionState, + schema: Option, +) -> Result { + let mut size = 0; + + let buf = unsafe { + let ptr = sqlite3_serialize( + conn.handle.as_ptr(), + schema.as_ref().map_or(ptr::null(), SchemaName::as_ptr), + &mut size, + 0, + ); + + // looking at the source, `sqlite3_serialize` actually sets `size = -1` on error: + // https://github.com/sqlite/sqlite/blob/da5f81387843f92652128087a8f8ecef0b79461d/src/memdb.c#L776 + usize::try_from(size) + .ok() + .and_then(|size| SqliteOwnedBuf::from_raw(ptr, size)) + }; + + if let Some(buf) = buf { + return Ok(buf); + } + + if let Some(error) = conn.handle.last_error() { + return Err(error.into()); + } + + if size > 0 { + // If `size` is positive but `sqlite3_serialize` still returned NULL, + // the most likely culprit is an out-of-memory condition. + return Err(SqliteError::from_code(SQLITE_NOMEM).into()); + } + + // Otherwise, the schema was probably not found. + // We return the equivalent error as when you try to execute `PRAGMA .page_count` + // against a non-existent schema. + Err(SqliteError::generic(format!( + "database {} does not exist", + schema.as_ref().map_or("main", SchemaName::as_str) + )) + .into()) +} + +pub(crate) fn deserialize( + conn: &mut ConnectionState, + schema: Option, + data: SqliteOwnedBuf, + read_only: bool, +) -> Result<(), Error> { + // SQLITE_DESERIALIZE_FREEONCLOSE causes SQLite to take ownership of the buffer + let mut flags = SQLITE_DESERIALIZE_FREEONCLOSE; + if read_only { + flags |= SQLITE_DESERIALIZE_READONLY; + } else { + flags |= SQLITE_DESERIALIZE_RESIZEABLE; + } + + let (buf, size) = data.into_raw(); + + let rc = unsafe { + sqlite3_deserialize( + conn.handle.as_ptr(), + schema.as_ref().map_or(ptr::null(), SchemaName::as_ptr), + buf, + i64::try_from(size).unwrap(), + i64::try_from(size).unwrap(), + flags, + ) + }; + + match rc { + SQLITE_OK => Ok(()), + SQLITE_NOMEM => Err(SqliteError::from_code(SQLITE_NOMEM).into()), + // SQLite unfortunately doesn't set any specific message for deserialization errors. + _ => Err(SqliteError::generic("an error occurred during deserialization").into()), + } +} + +/// Memory buffer owned and allocated by SQLite. Freed on drop. +/// +/// Intended primarily for use with [`SqliteConnection::serialize()`] and [`SqliteConnection::deserialize()`]. +/// +/// Can be created from `&[u8]` using the `TryFrom` impl. The slice must not be empty. +#[derive(Debug)] +pub struct SqliteOwnedBuf { + ptr: NonNull, + size: usize, +} + +unsafe impl Send for SqliteOwnedBuf {} +unsafe impl Sync for SqliteOwnedBuf {} + +impl Drop for SqliteOwnedBuf { + fn drop(&mut self) { + unsafe { + sqlite3_free(self.ptr.as_ptr().cast()); + } + } +} + +impl SqliteOwnedBuf { + /// Uses `sqlite3_malloc` to allocate a buffer and returns a pointer to it. + /// + /// # Safety + /// The allocated buffer is uninitialized. + unsafe fn with_capacity(size: usize) -> Option { + let ptr = sqlite3_malloc64(u64::try_from(size).unwrap()).cast::(); + Self::from_raw(ptr, size) + } + + /// Creates a new mem buffer from a pointer that has been created with sqlite_malloc + /// + /// # Safety: + /// * The pointer must point to a valid allocation created by `sqlite3_malloc()`, or `NULL`. + unsafe fn from_raw(ptr: *mut u8, size: usize) -> Option { + Some(Self { + ptr: NonNull::new(ptr)?, + size, + }) + } + + fn into_raw(self) -> (*mut u8, usize) { + let raw = (self.ptr.as_ptr(), self.size); + // this is used in sqlite_deserialize and + // underlying buffer must not be freed + std::mem::forget(self); + raw + } +} + +/// # Errors +/// Returns [`Error::InvalidArgument`] if the slice is empty. +impl TryFrom<&[u8]> for SqliteOwnedBuf { + type Error = Error; + + fn try_from(bytes: &[u8]) -> Result { + unsafe { + // SAFETY: `buf` is not initialized until `ptr::copy_nonoverlapping` completes. + let mut buf = Self::with_capacity(bytes.len()).ok_or_else(|| { + Error::InvalidArgument("SQLite owned buffer cannot be empty".to_string()) + })?; + ptr::copy_nonoverlapping(bytes.as_ptr(), buf.ptr.as_mut(), buf.size); + Ok(buf) + } + } +} + +impl Deref for SqliteOwnedBuf { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.size) } + } +} + +impl DerefMut for SqliteOwnedBuf { + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { std::slice::from_raw_parts_mut(self.ptr.as_mut(), self.size) } + } +} + +impl AsRef<[u8]> for SqliteOwnedBuf { + fn as_ref(&self) -> &[u8] { + self.deref() + } +} + +impl AsMut<[u8]> for SqliteOwnedBuf { + fn as_mut(&mut self) -> &mut [u8] { + self.deref_mut() + } +} + +/// Checked schema name to pass to SQLite. +/// +/// # Safety: +/// * Valid UTF-8 (not guaranteed by `CString`) +/// * No internal zero bytes (`\0`) (not guaranteed by `String`) +/// * Terminated with a zero byte (`\0`) (not guaranteed by `String`) +#[derive(Debug)] +pub(crate) struct SchemaName(Box); + +impl SchemaName { + /// Get the schema name as a string without the zero byte terminator. + pub fn as_str(&self) -> &str { + &self.0[..self.0.len() - 1] + } + + /// Get a pointer to the string data, suitable for passing as C's `*const char`. + /// + /// # Safety + /// The string data is guaranteed to be terminated with a zero byte. + pub fn as_ptr(&self) -> *const c_char { + self.0.as_ptr() as *const c_char + } +} + +impl<'a> TryFrom<&'a str> for SchemaName { + type Error = Error; + + fn try_from(name: &'a str) -> Result { + // SAFETY: we must ensure that the string does not contain an internal NULL byte + if let Some(pos) = name.as_bytes().iter().position(|&b| b == 0) { + return Err(Error::InvalidArgument(format!( + "schema name {name:?} contains a zero byte at index {pos}" + ))); + } + + let capacity = name.len().checked_add(1).unwrap(); + + let mut s = String::new(); + // `String::with_capacity()` does not guarantee that it will not overallocate, + // which might mean an unnecessary reallocation to make `capacity == len` + // in the conversion to `Box`. + s.reserve_exact(capacity); + + s.push_str(name); + s.push('\0'); + + Ok(SchemaName(s.into())) + } +} diff --git a/sqlx-sqlite/src/connection/worker.rs b/sqlx-sqlite/src/connection/worker.rs index c1c67636f1..8a1d140b25 100644 --- a/sqlx-sqlite/src/connection/worker.rs +++ b/sqlx-sqlite/src/connection/worker.rs @@ -21,6 +21,8 @@ use crate::connection::execute; use crate::connection::ConnectionState; use crate::{Sqlite, SqliteArguments, SqliteQueryResult, SqliteRow, SqliteStatement}; +use super::serialize::{deserialize, serialize, SchemaName, SqliteOwnedBuf}; + // Each SQLite connection has a dedicated thread. // TODO: Tweak this so that we can use a thread pool per pool of SQLite3 connections to reduce @@ -54,6 +56,16 @@ enum Command { tx: flume::Sender, Error>>, limit: Option, }, + Serialize { + schema: Option, + tx: oneshot::Sender>, + }, + Deserialize { + schema: Option, + data: SqliteOwnedBuf, + read_only: bool, + tx: oneshot::Sender>, + }, Begin { tx: rendezvous_oneshot::Sender>, }, @@ -263,6 +275,12 @@ impl ConnectionWorker { } } } + Command::Serialize { schema, tx } => { + tx.send(serialize(&mut conn, schema)).ok(); + } + Command::Deserialize { schema, data, read_only, tx } => { + tx.send(deserialize(&mut conn, schema, data, read_only)).ok(); + } Command::ClearCache { tx } => { conn.statements.clear(); update_cached_statements_size(&conn, &shared.cached_statements_size); @@ -358,6 +376,29 @@ impl ConnectionWorker { self.oneshot_cmd(|tx| Command::Ping { tx }).await } + pub(crate) async fn deserialize( + &mut self, + schema: Option, + data: SqliteOwnedBuf, + read_only: bool, + ) -> Result<(), Error> { + self.oneshot_cmd(|tx| Command::Deserialize { + schema, + data, + read_only, + tx, + }) + .await? + } + + pub(crate) async fn serialize( + &mut self, + schema: Option, + ) -> Result { + self.oneshot_cmd(|tx| Command::Serialize { schema, tx }) + .await? + } + async fn oneshot_cmd(&mut self, command: F) -> Result where F: FnOnce(oneshot::Sender) -> Command, diff --git a/sqlx-sqlite/src/error.rs b/sqlx-sqlite/src/error.rs index 0d34bc1026..eee2e8b1a2 100644 --- a/sqlx-sqlite/src/error.rs +++ b/sqlx-sqlite/src/error.rs @@ -2,12 +2,12 @@ use std::error::Error as StdError; use std::ffi::CStr; use std::fmt::{self, Display, Formatter}; use std::os::raw::c_int; -use std::{borrow::Cow, str::from_utf8_unchecked}; +use std::{borrow::Cow, str}; use libsqlite3_sys::{ - sqlite3, sqlite3_errmsg, sqlite3_extended_errcode, SQLITE_CONSTRAINT_CHECK, + sqlite3, sqlite3_errmsg, sqlite3_errstr, sqlite3_extended_errcode, SQLITE_CONSTRAINT_CHECK, SQLITE_CONSTRAINT_FOREIGNKEY, SQLITE_CONSTRAINT_NOTNULL, SQLITE_CONSTRAINT_PRIMARYKEY, - SQLITE_CONSTRAINT_UNIQUE, + SQLITE_CONSTRAINT_UNIQUE, SQLITE_ERROR, }; pub(crate) use sqlx_core::error::*; @@ -18,15 +18,15 @@ pub(crate) use sqlx_core::error::*; #[derive(Debug)] pub struct SqliteError { code: c_int, - message: String, + message: Cow<'static, str>, } impl SqliteError { - pub(crate) fn new(handle: *mut sqlite3) -> Self { + pub(crate) unsafe fn new(handle: *mut sqlite3) -> Self { Self::try_new(handle).expect("There should be an error") } - pub(crate) fn try_new(handle: *mut sqlite3) -> Option { + pub(crate) unsafe fn try_new(handle: *mut sqlite3) -> Option { // returns the extended result code even when extended result codes are disabled let code: c_int = unsafe { sqlite3_extended_errcode(handle) }; @@ -39,20 +39,44 @@ impl SqliteError { let msg = sqlite3_errmsg(handle); debug_assert!(!msg.is_null()); - from_utf8_unchecked(CStr::from_ptr(msg).to_bytes()) + str::from_utf8_unchecked(CStr::from_ptr(msg).to_bytes()).to_owned() }; Some(Self { code, - message: message.to_owned(), + message: message.into(), }) } /// For errors during extension load, the error message is supplied via a separate pointer - pub(crate) fn extension(handle: *mut sqlite3, error_msg: &CStr) -> Self { - let mut err = Self::new(handle); - err.message = unsafe { from_utf8_unchecked(error_msg.to_bytes()).to_owned() }; - err + pub(crate) fn with_message(mut self, error_msg: String) -> Self { + self.message = error_msg.into(); + self + } + + pub(crate) fn from_code(code: c_int) -> Self { + let message = unsafe { + let errstr = sqlite3_errstr(code); + + if !errstr.is_null() { + // SAFETY: `errstr` is guaranteed to be UTF-8 + // The lifetime of the string is "internally managed"; + // the implementation just selects from an array of static strings. + // We copy to an owned buffer in case `libsqlite3` is dynamically loaded somehow. + Cow::Owned(str::from_utf8_unchecked(CStr::from_ptr(errstr).to_bytes()).into()) + } else { + Cow::Borrowed("") + } + }; + + SqliteError { code, message } + } + + pub(crate) fn generic(message: impl Into>) -> Self { + Self { + code: SQLITE_ERROR, + message: message.into(), + } } } diff --git a/sqlx-sqlite/src/lib.rs b/sqlx-sqlite/src/lib.rs index f1a45c3d34..e4a122b6bd 100644 --- a/sqlx-sqlite/src/lib.rs +++ b/sqlx-sqlite/src/lib.rs @@ -46,6 +46,7 @@ use std::sync::atomic::AtomicBool; pub use arguments::{SqliteArgumentValue, SqliteArguments}; pub use column::SqliteColumn; +pub use connection::serialize::SqliteOwnedBuf; #[cfg(feature = "preupdate-hook")] pub use connection::PreupdateHookResult; pub use connection::{LockedSqliteHandle, SqliteConnection, SqliteOperation, UpdateHookResult}; diff --git a/sqlx-sqlite/src/statement/handle.rs b/sqlx-sqlite/src/statement/handle.rs index 2925d1a199..ccc299fcd2 100644 --- a/sqlx-sqlite/src/statement/handle.rs +++ b/sqlx-sqlite/src/statement/handle.rs @@ -81,8 +81,8 @@ impl StatementHandle { } #[inline] - pub(crate) fn last_error(&self) -> SqliteError { - SqliteError::new(unsafe { self.db_handle() }) + pub(crate) fn last_error(&mut self) -> SqliteError { + unsafe { SqliteError::new(self.db_handle()) } } #[inline] diff --git a/sqlx-sqlite/src/statement/virtual.rs b/sqlx-sqlite/src/statement/virtual.rs index 6be980c36a..2817146bc3 100644 --- a/sqlx-sqlite/src/statement/virtual.rs +++ b/sqlx-sqlite/src/statement/virtual.rs @@ -184,7 +184,7 @@ fn prepare( }; if status != SQLITE_OK { - return Err(SqliteError::new(conn).into()); + return Err(unsafe { SqliteError::new(conn).into() }); } // tail should point to the first byte past the end of the first SQL diff --git a/tests/sqlite/sqlite.rs b/tests/sqlite/sqlite.rs index 16b4b2d9fa..92a1138734 100644 --- a/tests/sqlite/sqlite.rs +++ b/tests/sqlite/sqlite.rs @@ -2,12 +2,10 @@ use futures::TryStreamExt; use rand::{Rng, SeedableRng}; use rand_xoshiro::Xoshiro256PlusPlus; use sqlx::sqlite::{SqliteConnectOptions, SqliteOperation, SqlitePoolOptions}; -use sqlx::Decode; use sqlx::{ query, sqlite::Sqlite, sqlite::SqliteRow, Column, ConnectOptions, Connection, Executor, Row, SqliteConnection, SqlitePool, Statement, TypeInfo, }; -use sqlx::{Value, ValueRef}; use sqlx_test::new; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -271,7 +269,7 @@ async fn it_handles_empty_queries() -> anyhow::Result<()> { } #[sqlx_macros::test] -fn it_binds_parameters() -> anyhow::Result<()> { +async fn it_binds_parameters() -> anyhow::Result<()> { let mut conn = new::().await?; let v: i32 = sqlx::query_scalar("SELECT ?") @@ -293,7 +291,7 @@ fn it_binds_parameters() -> anyhow::Result<()> { } #[sqlx_macros::test] -fn it_binds_dollar_parameters() -> anyhow::Result<()> { +async fn it_binds_dollar_parameters() -> anyhow::Result<()> { let mut conn = new::().await?; let v: (i32, i32) = sqlx::query_as("SELECT $1, $2") @@ -973,6 +971,8 @@ async fn test_multiple_set_rollback_hook_calls_drop_old_handler() -> anyhow::Res #[cfg(feature = "sqlite-preupdate-hook")] #[sqlx_macros::test] async fn test_query_with_preupdate_hook_insert() -> anyhow::Result<()> { + use sqlx::Decode; + let mut conn = new::().await?; static CALLED: AtomicBool = AtomicBool::new(false); // Using this string as a canary to ensure the callback doesn't get called with the wrong data pointer. @@ -1021,6 +1021,8 @@ async fn test_query_with_preupdate_hook_insert() -> anyhow::Result<()> { #[cfg(feature = "sqlite-preupdate-hook")] #[sqlx_macros::test] async fn test_query_with_preupdate_hook_delete() -> anyhow::Result<()> { + use sqlx::Decode; + let mut conn = new::().await?; let _ = sqlx::query("INSERT INTO tweet ( id, text ) VALUES ( 5, 'Hello, World' )") .execute(&mut conn) @@ -1064,6 +1066,9 @@ async fn test_query_with_preupdate_hook_delete() -> anyhow::Result<()> { #[cfg(feature = "sqlite-preupdate-hook")] #[sqlx_macros::test] async fn test_query_with_preupdate_hook_update() -> anyhow::Result<()> { + use sqlx::Decode; + use sqlx::{Value, ValueRef}; + let mut conn = new::().await?; let _ = sqlx::query("INSERT INTO tweet ( id, text ) VALUES ( 6, 'Hello, World' )") .execute(&mut conn) @@ -1193,3 +1198,121 @@ async fn test_get_last_error() -> anyhow::Result<()> { Ok(()) } + +#[sqlx_macros::test] +async fn test_serialize_deserialize() -> anyhow::Result<()> { + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + sqlx::raw_sql("create table foo(bar integer not null, baz text not null)") + .execute(&mut conn) + .await?; + + sqlx::query("insert into foo(bar, baz) values (1234, 'Lorem ipsum'), (5678, 'dolor sit amet')") + .execute(&mut conn) + .await?; + + let serialized = conn.serialize(None).await?; + + // Close and open a new connection to ensure cleanliness. + conn.close().await?; + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + conn.deserialize(None, serialized, false).await?; + + let rows = sqlx::query_as::<_, (i32, String)>("select bar, baz from foo") + .fetch_all(&mut conn) + .await?; + + assert_eq!(rows.len(), 2); + + assert_eq!(rows[0].0, 1234); + assert_eq!(rows[0].1, "Lorem ipsum"); + + assert_eq!(rows[1].0, 5678); + assert_eq!(rows[1].1, "dolor sit amet"); + + Ok(()) +} +#[sqlx_macros::test] +async fn test_serialize_deserialize_with_schema() -> anyhow::Result<()> { + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + sqlx::raw_sql( + "attach ':memory:' as foo; create table foo.foo(bar integer not null, baz text not null)", + ) + .execute(&mut conn) + .await?; + + sqlx::query( + "insert into foo.foo(bar, baz) values (1234, 'Lorem ipsum'), (5678, 'dolor sit amet')", + ) + .execute(&mut conn) + .await?; + + let serialized = conn.serialize(Some("foo")).await?; + + // Close and open a new connection to ensure cleanliness. + conn.close().await?; + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + // Unexpected quirk: the schema must exist before deserialization. + sqlx::raw_sql("attach ':memory:' as foo") + .execute(&mut conn) + .await?; + + conn.deserialize(Some("foo"), serialized, false).await?; + + let rows = sqlx::query_as::<_, (i32, String)>("select bar, baz from foo.foo") + .fetch_all(&mut conn) + .await?; + + assert_eq!(rows.len(), 2); + + assert_eq!(rows[0].0, 1234); + assert_eq!(rows[0].1, "Lorem ipsum"); + + assert_eq!(rows[1].0, 5678); + assert_eq!(rows[1].1, "dolor sit amet"); + + Ok(()) +} + +#[sqlx_macros::test] +async fn test_serialize_nonexistent_schema() -> anyhow::Result<()> { + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + let err = conn + .serialize(Some("foobar")) + .await + .expect_err("an error should have been returned"); + + let sqlx::Error::Database(dbe) = err else { + panic!("expected DatabaseError: {err:?}") + }; + + assert_eq!(dbe.code().as_deref(), Some("1")); + assert_eq!(dbe.message(), "database foobar does not exist"); + + Ok(()) +} + +#[sqlx_macros::test] +async fn test_serialize_invalid_schema() -> anyhow::Result<()> { + let mut conn = SqliteConnection::connect("sqlite::memory:").await?; + + let err = conn + .serialize(Some("foo\0bar")) + .await + .expect_err("an error should have been returned"); + + let sqlx::Error::InvalidArgument(msg) = err else { + panic!("expected InvalidArgument: {err:?}") + }; + + assert_eq!( + msg, + "schema name \"foo\\0bar\" contains a zero byte at index 3" + ); + + Ok(()) +} From 5c573e15eba7832f7aacd7cc2f71d4201f9c7a85 Mon Sep 17 00:00:00 2001 From: "James H." <32926722+jayy-lmao@users.noreply.github.com> Date: Mon, 3 Mar 2025 09:29:53 +1100 Subject: [PATCH 37/66] feat(postgres): add geometry path (#3716) * feat: add geometry path * fix: paths to pg point * test: remove array tests for path * Fix readme: uuid feature is gating for all repos (#3720) The readme previously stated that the uuid feature is only for postres but it actually also gates the functionality in mysql and sqlite. * Replace some futures_util APIs with std variants (#3721) * feat(sqlx-cli): Add flag to disable automatic loading of .env files (#3724) * Add flag to disable automatic loading of .env files * Update sqlx-cli/src/opt.rs Co-authored-by: Austin Bonander --------- Co-authored-by: Austin Bonander * chore: expose bstr feature (#3714) * chore: replace rustls-pemfile with rustls-pki-types (#3725) * QueryBuilder: add `debug_assert` when `push_values` is passed an empty set of tuples (#3734) * throw a warning in tracing so that the empty tuples would be noticed * use debug assertion to throw a panic in debug mode * fix: merge conflicts * chore(cli): remove unused async-trait crate from dependencies (#3754) * Update pull_request_template.md * Fix example calculation (#3741) * Avoid privilege requirements by using an advisory lock in test setup (postgres). (#3753) * feat(sqlx-postgres): use advisory lock to avoid setup race condition * fix(sqlx-postgres): numeric hex constants not supported before postgres 16 * Small doc correction. (#3755) When sqlx-core/src/from_row.rs was updated to implement FromRow for tuples of up to 16 values, a comment was left stating that it was implemented up to tuples of 9 values. * Update FAQ.md * refactor(cli): replace promptly with dialoguer (#3669) * docs(pool): recommend actix-web ThinData over Data to avoid two Arcs (#3762) Both actix_web::web::Data and sqlx::PgPool internally wrap an Arc. Thus, using Data as an extractor in an actix-web route handler results in two Arcs wrapping the data of interest, which isn't ideal. Actix-web 4.9.0 introduced a new web::ThinData extractor for cases like this, where the data is already wrapped in an `Arc` (or is otherwise similarly cheap and sensible to simply clone), which doesn't wrap the inner value in a (second) Arc. Since the new extractor is better suited to the task, suggest it in place of web::Data when giving an example on how to share a pool. * fix: merge conflicts * fix: use types mod from main * fix: merge conflicts * fix: merge conflicts * fix: merge conflicts * fix: ordering of types mod * fix: path import * test: no array test for path --------- Co-authored-by: Jon Thacker Co-authored-by: Paolo Barbolini Co-authored-by: Ben Wilber Co-authored-by: Austin Bonander Co-authored-by: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Co-authored-by: tottoto Co-authored-by: Ethan Wang Co-authored-by: Stefan Schindler Co-authored-by: kildrens <5198060+kildrens@users.noreply.github.com> Co-authored-by: Marti Serra Co-authored-by: Jonas Malaco --- sqlx-postgres/src/type_checking.rs | 2 + sqlx-postgres/src/types/geometry/mod.rs | 1 + sqlx-postgres/src/types/geometry/path.rs | 372 +++++++++++++++++++++++ sqlx-postgres/src/types/mod.rs | 2 + tests/postgres/types.rs | 6 + 5 files changed, 383 insertions(+) create mode 100644 sqlx-postgres/src/types/geometry/path.rs diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index 68a4fcfeff..5758c264a1 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -40,6 +40,8 @@ impl_type_checking!( sqlx::postgres::types::PgBox, + sqlx::postgres::types::PgPath, + #[cfg(feature = "uuid")] sqlx::types::Uuid, diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs index 7fe2898fcd..f67846fef2 100644 --- a/sqlx-postgres/src/types/geometry/mod.rs +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -1,4 +1,5 @@ pub mod r#box; pub mod line; pub mod line_segment; +pub mod path; pub mod point; diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs new file mode 100644 index 0000000000..87a3b3e8d3 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -0,0 +1,372 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::{PgPoint, Type}; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use sqlx_core::Error; +use std::mem; +use std::str::FromStr; + +const BYTE_WIDTH: usize = mem::size_of::(); + +/// ## Postgres Geometric Path type +/// +/// Description: Open path or Closed path (similar to polygon) +/// Representation: Open `[(x1,y1),...]`, Closed `((x1,y1),...)` +/// +/// Paths are represented by lists of connected points. Paths can be open, where the first and last points in the list are considered not connected, or closed, where the first and last points are considered connected. +/// Values of type path are specified using any of the following syntaxes: +/// ```text +/// [ ( x1 , y1 ) , ... , ( xn , yn ) ] +/// ( ( x1 , y1 ) , ... , ( xn , yn ) ) +/// ( x1 , y1 ) , ... , ( xn , yn ) +/// ( x1 , y1 , ... , xn , yn ) +/// x1 , y1 , ... , xn , yn +/// ``` +/// where the points are the end points of the line segments comprising the path. Square brackets `([])` indicate an open path, while parentheses `(())` indicate a closed path. +/// When the outermost parentheses are omitted, as in the third through fifth syntaxes, a closed path is assumed. +/// +/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS +#[derive(Debug, Clone, PartialEq)] +pub struct PgPath { + pub closed: bool, + pub points: Vec, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct Header { + is_closed: bool, + length: usize, +} + +impl Type for PgPath { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("path") + } +} + +impl PgHasArrayType for PgPath { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_path") + } +} + +impl<'r> Decode<'r, Postgres> for PgPath { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgPath::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgPath::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgPath { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("path")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgPath { + type Err = Error; + + fn from_str(s: &str) -> Result { + let closed = !s.contains('['); + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); + let parts = sanitised.split(',').collect::>(); + + let mut points = vec![]; + + if parts.len() % 2 != 0 { + return Err(Error::Decode( + format!("Unmatched pair in PATH: {}", s).into(), + )); + } + + for chunk in parts.chunks_exact(2) { + if let [x_str, y_str] = chunk { + let x = parse_float_from_str(x_str, "could not get x")?; + let y = parse_float_from_str(y_str, "could not get y")?; + + let point = PgPoint { x, y }; + points.push(point); + } + } + + if !points.is_empty() { + return Ok(PgPath { points, closed }); + } + + Err(Error::Decode( + format!("could not get path from {}", s).into(), + )) + } +} + +impl PgPath { + fn header(&self) -> Header { + Header { + is_closed: self.closed, + length: self.points.len(), + } + } + + fn from_bytes(mut bytes: &[u8]) -> Result { + let header = Header::try_read(&mut bytes)?; + + if bytes.len() != header.data_size() { + return Err(format!( + "expected {} bytes after header, got {}", + header.data_size(), + bytes.len() + ) + .into()); + } + + if bytes.len() % BYTE_WIDTH * 2 != 0 { + return Err(format!( + "data length not divisible by pairs of {BYTE_WIDTH}: {}", + bytes.len() + ) + .into()); + } + + let mut out_points = Vec::with_capacity(bytes.len() / (BYTE_WIDTH * 2)); + + while bytes.has_remaining() { + let point = PgPoint { + x: bytes.get_f64(), + y: bytes.get_f64(), + }; + out_points.push(point) + } + Ok(PgPath { + closed: header.is_closed, + points: out_points, + }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { + let header = self.header(); + buff.reserve(header.data_size()); + header.try_write(buff)?; + + for point in &self.points { + buff.extend_from_slice(&point.x.to_be_bytes()); + buff.extend_from_slice(&point.y.to_be_bytes()); + } + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +impl Header { + const HEADER_WIDTH: usize = mem::size_of::() + mem::size_of::(); + + fn data_size(&self) -> usize { + self.length * BYTE_WIDTH * 2 + } + + fn try_read(buf: &mut &[u8]) -> Result { + if buf.len() < Self::HEADER_WIDTH { + return Err(format!( + "expected PATH data to contain at least {} bytes, got {}", + Self::HEADER_WIDTH, + buf.len() + )); + } + + let is_closed = buf.get_i8(); + let length = buf.get_i32(); + + let length = usize::try_from(length).ok().ok_or_else(|| { + format!( + "received PATH data length: {length}. Expected length between 0 and {}", + usize::MAX + ) + })?; + + Ok(Self { + is_closed: is_closed != 0, + length, + }) + } + + fn try_write(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { + let is_closed = self.is_closed as i8; + + let length = i32::try_from(self.length).map_err(|_| { + format!( + "PATH length exceeds allowed maximum ({} > {})", + self.length, + i32::MAX + ) + })?; + + buff.extend(is_closed.to_be_bytes()); + buff.extend(length.to_be_bytes()); + + Ok(()) + } +} + +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.parse().map_err(|_| Error::Decode(error_msg.into())) +} + +#[cfg(test)] +mod path_tests { + + use std::str::FromStr; + + use crate::types::PgPoint; + + use super::PgPath; + + const PATH_CLOSED_BYTES: &[u8] = &[ + 1, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, + 64, 16, 0, 0, 0, 0, 0, 0, + ]; + + const PATH_OPEN_BYTES: &[u8] = &[ + 0, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, + 64, 16, 0, 0, 0, 0, 0, 0, + ]; + + const PATH_UNEVEN_POINTS: &[u8] = &[ + 0, 0, 0, 0, 2, 63, 240, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, + 64, 16, 0, 0, + ]; + + #[test] + fn can_deserialise_path_type_bytes_closed() { + let path = PgPath::from_bytes(PATH_CLOSED_BYTES).unwrap(); + assert_eq!( + path, + PgPath { + closed: true, + points: vec![PgPoint { x: 1.0, y: 2.0 }, PgPoint { x: 3.0, y: 4.0 }] + } + ) + } + + #[test] + fn cannot_deserialise_path_type_uneven_point_bytes() { + let path = PgPath::from_bytes(PATH_UNEVEN_POINTS); + assert!(path.is_err()); + + if let Err(err) = path { + assert_eq!( + err.to_string(), + format!("expected 32 bytes after header, got 28") + ) + } + } + + #[test] + fn can_deserialise_path_type_bytes_open() { + let path = PgPath::from_bytes(PATH_OPEN_BYTES).unwrap(); + assert_eq!( + path, + PgPath { + closed: false, + points: vec![PgPoint { x: 1.0, y: 2.0 }, PgPoint { x: 3.0, y: 4.0 }] + } + ) + } + + #[test] + fn can_deserialise_path_type_str_first_syntax() { + let path = PgPath::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + path, + PgPath { + closed: false, + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn cannot_deserialise_path_type_str_uneven_points_first_syntax() { + let input_str = "[( 1, 2), (3)]"; + let path = PgPath::from_str(input_str); + + assert!(path.is_err()); + + if let Err(err) = path { + assert_eq!( + err.to_string(), + format!("error occurred while decoding: Unmatched pair in PATH: {input_str}") + ) + } + } + + #[test] + fn can_deserialise_path_type_str_second_syntax() { + let path = PgPath::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + path, + PgPath { + closed: true, + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_path_type_str_third_syntax() { + let path = PgPath::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + path, + PgPath { + closed: true, + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_path_type_str_fourth_syntax() { + let path = PgPath::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + path, + PgPath { + closed: true, + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_path_type_str_float() { + let path = PgPath::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + path, + PgPath { + closed: true, + points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }] + } + ); + } + + #[test] + fn can_serialise_path_type() { + let path = PgPath { + closed: true, + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }], + }; + assert_eq!(path.serialize_to_vec(), PATH_CLOSED_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index a5fd708366..5d684c969e 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -25,6 +25,7 @@ //! | [`PgLine`] | LINE | //! | [`PgLSeg`] | LSEG | //! | [`PgBox`] | BOX | +//! | [`PgPath`] | PATH | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -262,6 +263,7 @@ pub use citext::PgCiText; pub use cube::PgCube; pub use geometry::line::PgLine; pub use geometry::line_segment::PgLSeg; +pub use geometry::path::PgPath; pub use geometry::point::PgPoint; pub use geometry::r#box::PgBox; pub use hstore::PgHstore; diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index ccf88b1099..0d15caf8de 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -524,6 +524,12 @@ test_type!(_box>(Postgres, "array[box('1,2,3,4'),box('((1.1, 2.2), (3.3, 4.4))')]" @= vec![sqlx::postgres::types::PgBox { upper_right_x: 3., upper_right_y: 4., lower_left_x: 1., lower_left_y: 2. }, sqlx::postgres::types::PgBox { upper_right_x: 3.3, upper_right_y: 4.4, lower_left_x: 1.1, lower_left_y: 2.2 }], )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(path(Postgres, + "path('((1.0, 2.0), (3.0,4.0))')" == sqlx::postgres::types::PgPath { closed: true, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, + "path('[(1.0, 2.0), (3.0,4.0)]')" == sqlx::postgres::types::PgPath { closed: false, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(), From 7af998c2abda9901c361bad85f124bbed9dde05e Mon Sep 17 00:00:00 2001 From: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Date: Tue, 4 Mar 2025 21:56:08 +0100 Subject: [PATCH 38/66] chore(Sqlite): remove ci.db from repo (#3768) --- ci.db | Bin 36864 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 ci.db diff --git a/ci.db b/ci.db deleted file mode 100644 index cc158a72804c405a8716f792f5ca51ec421c6ae0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36864 zcmeI*?{Cva7zgmXI6vw@Fd$8qCLvu`p~e#%tI)JpAkuJcBhqBJ?z+CROma|*U_NC8#&WYq);%-0a`BJb) zVf56K>=wC9C?$6pBZM^6qN^oORh22{1+}Fo@~34RTR+JzBHIzJnV;&9DGX2)a>e(ZWf=|2~2Z7}HZ zcE4^-544)OyiC7aO=FRVLP$3flktla+44d}Cw9z(TI+5ky!q)-j45lSPOyI+nMGDS z!Ls`BFi^uZYgHJ|{8`;cPuTjv=G~2ca@Mj|zQbZRU*|UOclb6-D_j0Z4epoas@YvW z?d;83rws1ui9%)CqBYYr>8qP_u5^E?Nc4Yj3a_u(fW;z^`Ebp7@A38aPS0VEy~E!r zRky5C)x2%~cMr3^Y3jXXDS1ChF7ivpkHq*zt&ku90SG_<0uX=z1Rwwb2tWV=5I7S8 zS4$OoT_@V67>~mnY&VR?qmOAt|4P@FlMe@(AFB_&lK)d~p$ z5P$##AOHafKmY;|fB*y_0D*HPP|+yWN}8tY$^1Vxej@6D1OW&@00Izz00bZa0SG_< z0uX?}*%DY$Q~vq`eNTA%O*WhOPkyH(Muqu*YP?nR|7WX%h!z46fB*y_009U<00Izz z00bZaf%g-b&;P$6#+&zRgQFk-0SG_<0uX=z1Rwwb2tWV=5cseKuF^8G^i2^>*gduK zgE$P5@Bgct{v$yE0uX=z1Rwwb2tWV=5P$##AaITalIwrX_?{TQ89%EH5(FRs0SG_< z0uX=z1Rwwb2tWV=AFjX^O(T}vS2y=p5054P0rX0#SVap}$^HK| Date: Tue, 4 Mar 2025 13:04:27 -0800 Subject: [PATCH 39/66] fix: CI * Fix breakage from Rustup 1.28 * Let `Swatinem/rust-cache` generate cache keys --- .github/workflows/examples.yml | 37 +++++++++-------- .github/workflows/sqlx-cli.yml | 28 +++++++------ .github/workflows/sqlx.yml | 75 +++++++++++++++++++--------------- 3 files changed, 75 insertions(+), 65 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 280d1fc4f3..0dfbcbdf26 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -14,20 +14,20 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use latest Rust - run: rustup override set stable + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install + rustup override set stable - uses: Swatinem/rust-cache@v2 - with: - key: sqlx-cli - run: > - cargo build - -p sqlx-cli - --bin sqlx - --release - --no-default-features - --features mysql,postgres,sqlite + cargo build + -p sqlx-cli + --bin sqlx + --release + --no-default-features + --features mysql,postgres,sqlite - uses: actions/upload-artifact@v4 with: @@ -63,9 +63,10 @@ jobs: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: mysql-examples - name: Todos (Setup) working-directory: examples/mysql/todos @@ -98,7 +99,7 @@ jobs: name: sqlx-cli path: /home/runner/.local/bin - - run: | + - run: | ls -R /home/runner/.local/bin chmod +x $HOME/.local/bin/sqlx echo $HOME/.local/bin >> $GITHUB_PATH @@ -106,9 +107,8 @@ jobs: - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - with: - key: pg-examples + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install - name: Axum Social with Tests (Setup) working-directory: examples/postgres/axum-social-with-tests @@ -217,9 +217,10 @@ jobs: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: sqlite-examples - name: TODOs (Setup) env: diff --git a/.github/workflows/sqlx-cli.yml b/.github/workflows/sqlx-cli.yml index 3aeb3d7d33..2250e0bfcb 100644 --- a/.github/workflows/sqlx-cli.yml +++ b/.github/workflows/sqlx-cli.yml @@ -15,8 +15,9 @@ jobs: steps: - uses: actions/checkout@v4 - - run: | - rustup update + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install rustup component add clippy rustup toolchain install beta rustup component add --toolchain beta clippy @@ -40,18 +41,19 @@ jobs: matrix: # Note: macOS-latest uses M1 Silicon (ARM64) os: - - ubuntu-latest - # FIXME: migrations tests fail on Windows for whatever reason - # - windows-latest - - macOS-13 - - macOS-latest + - ubuntu-latest + # FIXME: migrations tests fail on Windows for whatever reason + # - windows-latest + - macOS-13 + - macOS-latest steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: ${{ runner.os }}-test - run: cargo test --manifest-path sqlx-cli/Cargo.toml @@ -85,12 +87,12 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Use latest Rust - run: rustup override set stable + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install + rustup override set stable - uses: Swatinem/rust-cache@v2 - with: - key: ${{ runner.os }}-cli - run: cargo build --manifest-path sqlx-cli/Cargo.toml --bin cargo-sqlx ${{ matrix.args }} diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index 3f1f44d393..8461dd491c 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -21,21 +21,22 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - runtime: [async-std, tokio] - tls: [native-tls, rustls, none] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls, none ] steps: - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-check-${{ matrix.runtime }}-${{ matrix.tls }}" - - - run: | - rustup update + # Swatinem/rust-cache recommends setting up the rust toolchain first because it's used in cache keys + - name: Setup Rust + # https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html + run: | + rustup show active-toolchain || rustup toolchain install rustup component add clippy rustup toolchain install beta rustup component add --toolchain beta clippy + - uses: Swatinem/rust-cache@v2 + - run: > cargo clippy --no-default-features @@ -55,8 +56,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - - run: rustup update - - run: rustup toolchain install nightly + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install + rustup toolchain install nightly - run: cargo +nightly generate-lockfile -Z minimal-versions - run: cargo build --all-features @@ -66,12 +69,11 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - with: - key: ${{ runner.os }}-test + # https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install - - name: Install Rust - run: rustup update + - uses: Swatinem/rust-cache@v2 - name: Test sqlx-core run: > @@ -116,17 +118,19 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - runtime: [async-std, tokio] - linking: [sqlite, sqlite-unbundled] + runtime: [ async-std, tokio ] + linking: [ sqlite, sqlite-unbundled ] needs: check steps: - uses: actions/checkout@v4 - run: mkdir /tmp/sqlite3-lib && wget -O /tmp/sqlite3-lib/ipaddr.so https://github.com/nalgeon/sqlean/releases/download/0.15.2/ipaddr.so + # https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-${{ matrix.linking }}-${{ matrix.runtime }}-${{ matrix.tls }}" - name: Install system sqlite library if: ${{ matrix.linking == 'sqlite-unbundled' }} @@ -182,16 +186,17 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - postgres: [17, 13] - runtime: [async-std, tokio] - tls: [native-tls, rustls-aws-lc-rs, rustls-ring, none] + postgres: [ 17, 13 ] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls-aws-lc-rs, rustls-ring, none ] needs: check steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-postgres-${{ matrix.runtime }}-${{ matrix.tls }}" - env: # FIXME: needed to disable `ltree` tests in Postgres 9.6 @@ -282,16 +287,17 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - mysql: [8] - runtime: [async-std, tokio] - tls: [native-tls, rustls-aws-lc-rs, rustls-ring, none] + mysql: [ 8 ] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls-aws-lc-rs, rustls-ring, none ] needs: check steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-mysql-${{ matrix.runtime }}-${{ matrix.tls }}" - run: cargo build --features mysql,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} @@ -370,16 +376,17 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - mariadb: [verylatest, 11_4, 10_11, 10_4] - runtime: [async-std, tokio] - tls: [native-tls, rustls-aws-lc-rs, rustls-ring, none] + mariadb: [ verylatest, 11_4, 10_11, 10_4 ] + runtime: [ async-std, tokio ] + tls: [ native-tls, rustls-aws-lc-rs, rustls-ring, none ] needs: check steps: - uses: actions/checkout@v4 + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + - uses: Swatinem/rust-cache@v2 - with: - key: "${{ runner.os }}-mysql-${{ matrix.runtime }}-${{ matrix.tls }}" - run: cargo build --features mysql,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} From c3fd645409bd48fdd70b79734b27291aab6a3ec9 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Tue, 4 Mar 2025 13:51:45 -0800 Subject: [PATCH 40/66] fix(ci): upgrade Ubuntu image to 24.04 For some reason the `cargo +beta clippy` step is failing because `libsqlite3-sys` starts requiring Glibc >= 2.39 but I don't have time to figure out why and I can't reproduce it in a clean environment. --- .github/workflows/sqlx.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index 8461dd491c..7f573a6349 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -10,7 +10,7 @@ on: jobs: format: name: Format - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - run: rustup component add rustfmt @@ -18,7 +18,7 @@ jobs: check: name: Check - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: runtime: [ async-std, tokio ] @@ -53,7 +53,7 @@ jobs: check-minimal-versions: name: Check build using minimal versions - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Setup Rust @@ -65,7 +65,7 @@ jobs: test: name: Unit Tests - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -115,7 +115,7 @@ jobs: sqlite: name: SQLite - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: runtime: [ async-std, tokio ] @@ -183,7 +183,7 @@ jobs: postgres: name: Postgres - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: postgres: [ 17, 13 ] @@ -284,7 +284,7 @@ jobs: mysql: name: MySQL - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: mysql: [ 8 ] @@ -373,7 +373,7 @@ jobs: mariadb: name: MariaDB - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: mariadb: [ verylatest, 11_4, 10_11, 10_4 ] From a92626d6cc1aef88b0036d633ed370f6f9129c32 Mon Sep 17 00:00:00 2001 From: Chitoku Date: Tue, 4 Mar 2025 18:51:33 +0900 Subject: [PATCH 41/66] postgres: Use current tracing span when dropping PgListener --- sqlx-core/src/ext/async_stream.rs | 4 ++-- sqlx-postgres/src/listener.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sqlx-core/src/ext/async_stream.rs b/sqlx-core/src/ext/async_stream.rs index a83aabed14..56777ca4db 100644 --- a/sqlx-core/src/ext/async_stream.rs +++ b/sqlx-core/src/ext/async_stream.rs @@ -121,7 +121,7 @@ impl<'a, T> Stream for TryAsyncStream<'a, T> { #[macro_export] macro_rules! try_stream { ($($block:tt)*) => { - $crate::ext::async_stream::TryAsyncStream::new(move |yielder| async move { + $crate::ext::async_stream::TryAsyncStream::new(move |yielder| ::tracing::Instrument::in_current_span(async move { // Anti-footgun: effectively pins `yielder` to this future to prevent any accidental // move to another task, which could deadlock. let yielder = &yielder; @@ -133,6 +133,6 @@ macro_rules! try_stream { } $($block)* - }) + })) } } diff --git a/sqlx-postgres/src/listener.rs b/sqlx-postgres/src/listener.rs index b96f8d829f..17a46a916f 100644 --- a/sqlx-postgres/src/listener.rs +++ b/sqlx-postgres/src/listener.rs @@ -9,6 +9,7 @@ use futures_util::{FutureExt, StreamExt, TryFutureExt, TryStreamExt}; use sqlx_core::acquire::Acquire; use sqlx_core::transaction::Transaction; use sqlx_core::Either; +use tracing::Instrument; use crate::describe::Describe; use crate::error::Error; @@ -366,7 +367,7 @@ impl Drop for PgListener { }; // Unregister any listeners before returning the connection to the pool. - crate::rt::spawn(fut); + crate::rt::spawn(fut.in_current_span()); } } } From ca3a5090369238d156eb859d3fd699d86681f73f Mon Sep 17 00:00:00 2001 From: "James H." <32926722+jayy-lmao@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:25:45 +1100 Subject: [PATCH 42/66] feat(postgres): add geometry polygon (#3769) * feat: add polygon * test: paths for pgpoints in polygon test * fix: import typo * chore(Sqlite): remove ci.db from repo (#3768) * fix: CI * Fix breakage from Rustup 1.28 * Let `Swatinem/rust-cache` generate cache keys * fix(ci): upgrade Ubuntu image to 24.04 For some reason the `cargo +beta clippy` step is failing because `libsqlite3-sys` starts requiring Glibc >= 2.39 but I don't have time to figure out why and I can't reproduce it in a clean environment. --------- Co-authored-by: joeydewaal <99046430+joeydewaal@users.noreply.github.com> Co-authored-by: Austin Bonander --- sqlx-postgres/src/type_checking.rs | 2 + sqlx-postgres/src/types/geometry/mod.rs | 1 + sqlx-postgres/src/types/geometry/polygon.rs | 363 ++++++++++++++++++++ sqlx-postgres/src/types/mod.rs | 2 + tests/postgres/types.rs | 9 + 5 files changed, 377 insertions(+) create mode 100644 sqlx-postgres/src/types/geometry/polygon.rs diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index 5758c264a1..c82fd62187 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -42,6 +42,8 @@ impl_type_checking!( sqlx::postgres::types::PgPath, + sqlx::postgres::types::PgPolygon, + #[cfg(feature = "uuid")] sqlx::types::Uuid, diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs index f67846fef2..1437d72c5c 100644 --- a/sqlx-postgres/src/types/geometry/mod.rs +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -3,3 +3,4 @@ pub mod line; pub mod line_segment; pub mod path; pub mod point; +pub mod polygon; diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs new file mode 100644 index 0000000000..500c9933e9 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -0,0 +1,363 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::{PgPoint, Type}; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use sqlx_core::Error; +use std::mem; +use std::str::FromStr; + +const BYTE_WIDTH: usize = mem::size_of::(); + +/// ## Postgres Geometric Polygon type +/// +/// Description: Polygon (similar to closed polygon) +/// Representation: `((x1,y1),...)` +/// +/// Polygons are represented by lists of points (the vertexes of the polygon). Polygons are very similar to closed paths; the essential semantic difference is that a polygon is considered to include the area within it, while a path is not. +/// An important implementation difference between polygons and paths is that the stored representation of a polygon includes its smallest bounding box. This speeds up certain search operations, although computing the bounding box adds overhead while constructing new polygons. +/// Values of type polygon are specified using any of the following syntaxes: +/// +/// ```text +/// ( ( x1 , y1 ) , ... , ( xn , yn ) ) +/// ( x1 , y1 ) , ... , ( xn , yn ) +/// ( x1 , y1 , ... , xn , yn ) +/// x1 , y1 , ... , xn , yn +/// ``` +/// +/// where the points are the end points of the line segments comprising the boundary of the polygon. +/// +/// Seeh ttps://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-POLYGON +#[derive(Debug, Clone, PartialEq)] +pub struct PgPolygon { + pub points: Vec, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +struct Header { + length: usize, +} + +impl Type for PgPolygon { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("polygon") + } +} + +impl PgHasArrayType for PgPolygon { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_polygon") + } +} + +impl<'r> Decode<'r, Postgres> for PgPolygon { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgPolygon::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgPolygon::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgPolygon { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("polygon")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgPolygon { + type Err = Error; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(['(', ')', '[', ']', ' '], ""); + let parts = sanitised.split(',').collect::>(); + + let mut points = vec![]; + + if parts.len() % 2 != 0 { + return Err(Error::Decode( + format!("Unmatched pair in POLYGON: {}", s).into(), + )); + } + + for chunk in parts.chunks_exact(2) { + if let [x_str, y_str] = chunk { + let x = parse_float_from_str(x_str, "could not get x")?; + let y = parse_float_from_str(y_str, "could not get y")?; + + let point = PgPoint { x, y }; + points.push(point); + } + } + + if !points.is_empty() { + return Ok(PgPolygon { points }); + } + + Err(Error::Decode( + format!("could not get polygon from {}", s).into(), + )) + } +} + +impl PgPolygon { + fn header(&self) -> Header { + Header { + length: self.points.len(), + } + } + + fn from_bytes(mut bytes: &[u8]) -> Result { + let header = Header::try_read(&mut bytes)?; + + if bytes.len() != header.data_size() { + return Err(format!( + "expected {} bytes after header, got {}", + header.data_size(), + bytes.len() + ) + .into()); + } + + if bytes.len() % BYTE_WIDTH * 2 != 0 { + return Err(format!( + "data length not divisible by pairs of {BYTE_WIDTH}: {}", + bytes.len() + ) + .into()); + } + + let mut out_points = Vec::with_capacity(bytes.len() / (BYTE_WIDTH * 2)); + while bytes.has_remaining() { + let point = PgPoint { + x: bytes.get_f64(), + y: bytes.get_f64(), + }; + out_points.push(point) + } + Ok(PgPolygon { points: out_points }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), BoxDynError> { + let header = self.header(); + buff.reserve(header.data_size()); + header.try_write(buff)?; + + for point in &self.points { + buff.extend_from_slice(&point.x.to_be_bytes()); + buff.extend_from_slice(&point.y.to_be_bytes()); + } + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +impl Header { + const HEADER_WIDTH: usize = mem::size_of::() + mem::size_of::(); + + fn data_size(&self) -> usize { + self.length * BYTE_WIDTH * 2 + } + + fn try_read(buf: &mut &[u8]) -> Result { + if buf.len() < Self::HEADER_WIDTH { + return Err(format!( + "expected polygon data to contain at least {} bytes, got {}", + Self::HEADER_WIDTH, + buf.len() + )); + } + + let length = buf.get_i32(); + + let length = usize::try_from(length).ok().ok_or_else(|| { + format!( + "received polygon with length: {length}. Expected length between 0 and {}", + usize::MAX + ) + })?; + + Ok(Self { length }) + } + + fn try_write(&self, buff: &mut PgArgumentBuffer) -> Result<(), String> { + let length = i32::try_from(self.length).map_err(|_| { + format!( + "polygon length exceeds allowed maximum ({} > {})", + self.length, + i32::MAX + ) + })?; + + buff.extend(length.to_be_bytes()); + + Ok(()) + } +} + +fn parse_float_from_str(s: &str, error_msg: &str) -> Result { + s.parse().map_err(|_| Error::Decode(error_msg.into())) +} + +#[cfg(test)] +mod polygon_tests { + + use std::str::FromStr; + + use crate::types::PgPoint; + + use super::PgPolygon; + + const POLYGON_BYTES: &[u8] = &[ + 0, 0, 0, 12, 192, 0, 0, 0, 0, 0, 0, 0, 192, 8, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, + 0, 192, 8, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 63, + 240, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, + 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 64, 8, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 192, + 8, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, 0, 0, 0, 192, 8, 0, 0, 0, 0, 0, 0, 63, 240, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, 240, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 191, + 240, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, 0, 0, 0, 192, 0, 0, 0, 0, + 0, 0, 0, + ]; + + #[test] + fn can_deserialise_polygon_type_bytes() { + let polygon = PgPolygon::from_bytes(POLYGON_BYTES).unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![ + PgPoint { x: -2., y: -3. }, + PgPoint { x: -1., y: -3. }, + PgPoint { x: -1., y: -1. }, + PgPoint { x: 1., y: 1. }, + PgPoint { x: 1., y: 3. }, + PgPoint { x: 2., y: 3. }, + PgPoint { x: 2., y: -3. }, + PgPoint { x: 1., y: -3. }, + PgPoint { x: 1., y: 0. }, + PgPoint { x: -1., y: 0. }, + PgPoint { x: -1., y: -2. }, + PgPoint { x: -2., y: -2. } + ] + } + ) + } + + #[test] + fn can_deserialise_polygon_type_str_first_syntax() { + let polygon = PgPolygon::from_str("[( 1, 2), (3, 4 )]").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_second_syntax() { + let polygon = PgPolygon::from_str("(( 1, 2), (3, 4 ))").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn cannot_deserialise_polygon_type_str_uneven_points_first_syntax() { + let input_str = "[( 1, 2), (3)]"; + let polygon = PgPolygon::from_str(input_str); + + assert!(polygon.is_err()); + + if let Err(err) = polygon { + assert_eq!( + err.to_string(), + format!("error occurred while decoding: Unmatched pair in POLYGON: {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_polygon_type_str_invalid_numbers() { + let input_str = "[( 1, 2), (2, three)]"; + let polygon = PgPolygon::from_str(input_str); + + assert!(polygon.is_err()); + + if let Err(err) = polygon { + assert_eq!( + err.to_string(), + format!("error occurred while decoding: could not get y") + ) + } + } + + #[test] + fn can_deserialise_polygon_type_str_third_syntax() { + let polygon = PgPolygon::from_str("(1, 2), (3, 4 )").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_fourth_syntax() { + let polygon = PgPolygon::from_str("1, 2, 3, 4").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1., y: 2. }, PgPoint { x: 3., y: 4. }] + } + ); + } + + #[test] + fn can_deserialise_polygon_type_str_float() { + let polygon = PgPolygon::from_str("(1.1, 2.2), (3.3, 4.4)").unwrap(); + assert_eq!( + polygon, + PgPolygon { + points: vec![PgPoint { x: 1.1, y: 2.2 }, PgPoint { x: 3.3, y: 4.4 }] + } + ); + } + + #[test] + fn can_serialise_polygon_type() { + let polygon = PgPolygon { + points: vec![ + PgPoint { x: -2., y: -3. }, + PgPoint { x: -1., y: -3. }, + PgPoint { x: -1., y: -1. }, + PgPoint { x: 1., y: 1. }, + PgPoint { x: 1., y: 3. }, + PgPoint { x: 2., y: 3. }, + PgPoint { x: 2., y: -3. }, + PgPoint { x: 1., y: -3. }, + PgPoint { x: 1., y: 0. }, + PgPoint { x: -1., y: 0. }, + PgPoint { x: -1., y: -2. }, + PgPoint { x: -2., y: -2. }, + ], + }; + assert_eq!(polygon.serialize_to_vec(), POLYGON_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 5d684c969e..550ce62929 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -26,6 +26,7 @@ //! | [`PgLSeg`] | LSEG | //! | [`PgBox`] | BOX | //! | [`PgPath`] | PATH | +//! | [`PgPolygon`] | POLYGON | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -265,6 +266,7 @@ pub use geometry::line::PgLine; pub use geometry::line_segment::PgLSeg; pub use geometry::path::PgPath; pub use geometry::point::PgPoint; +pub use geometry::polygon::PgPolygon; pub use geometry::r#box::PgBox; pub use hstore::PgHstore; pub use interval::PgInterval; diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index 0d15caf8de..d88e1657c1 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -530,6 +530,15 @@ test_type!(path(Postgres, "path('[(1.0, 2.0), (3.0,4.0)]')" == sqlx::postgres::types::PgPath { closed: false, points: vec![ sqlx::postgres::types::PgPoint { x: 1., y: 2. }, sqlx::postgres::types::PgPoint { x: 3. , y: 4. } ]}, )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(polygon(Postgres, + "polygon('((-2,-3),(-1,-3),(-1,-1),(1,1),(1,3),(2,3),(2,-3),(1,-3),(1,0),(-1,0),(-1,-2),(-2,-2))')" ~= sqlx::postgres::types::PgPolygon { points: vec![ + sqlx::postgres::types::PgPoint { x: -2., y: -3. }, sqlx::postgres::types::PgPoint { x: -1., y: -3. }, sqlx::postgres::types::PgPoint { x: -1., y: -1. }, sqlx::postgres::types::PgPoint { x: 1., y: 1. }, + sqlx::postgres::types::PgPoint { x: 1., y: 3. }, sqlx::postgres::types::PgPoint { x: 2., y: 3. }, sqlx::postgres::types::PgPoint { x: 2., y: -3. }, sqlx::postgres::types::PgPoint { x: 1., y: -3. }, + sqlx::postgres::types::PgPoint { x: 1., y: 0. }, sqlx::postgres::types::PgPoint { x: -1., y: 0. }, sqlx::postgres::types::PgPoint { x: -1., y: -2. }, sqlx::postgres::types::PgPoint { x: -2., y: -2. }, + ]}, +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(), From 2f10c29dfd48dd9bac66b2fbabced6e8f0cfd445 Mon Sep 17 00:00:00 2001 From: "James H." <32926722+jayy-lmao@users.noreply.github.com> Date: Mon, 10 Mar 2025 09:01:30 +1100 Subject: [PATCH 43/66] feat(postgres): add geometry circle (#3773) * feat: circle * docs: comments --- sqlx-postgres/src/type_checking.rs | 2 + sqlx-postgres/src/types/geometry/box.rs | 5 +- sqlx-postgres/src/types/geometry/circle.rs | 250 ++++++++++++++++++ sqlx-postgres/src/types/geometry/line.rs | 5 +- .../src/types/geometry/line_segment.rs | 5 +- sqlx-postgres/src/types/geometry/mod.rs | 1 + sqlx-postgres/src/types/geometry/path.rs | 5 +- sqlx-postgres/src/types/geometry/point.rs | 5 +- sqlx-postgres/src/types/geometry/polygon.rs | 5 +- sqlx-postgres/src/types/mod.rs | 2 + tests/postgres/types.rs | 8 + 11 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 sqlx-postgres/src/types/geometry/circle.rs diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index c82fd62187..a28531c9b6 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -44,6 +44,8 @@ impl_type_checking!( sqlx::postgres::types::PgPolygon, + sqlx::postgres::types::PgCircle, + #[cfg(feature = "uuid")] sqlx::types::Uuid, diff --git a/sqlx-postgres/src/types/geometry/box.rs b/sqlx-postgres/src/types/geometry/box.rs index 988c028ed4..28016b2786 100644 --- a/sqlx-postgres/src/types/geometry/box.rs +++ b/sqlx-postgres/src/types/geometry/box.rs @@ -23,7 +23,10 @@ const ERROR: &str = "error decoding BOX"; /// where `(upper_right_x,upper_right_y) and (lower_left_x,lower_left_y)` are any two opposite corners of the box. /// Any two opposite corners can be supplied on input, but the values will be reordered as needed to store the upper right and lower left corners, in that order. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES +/// See [Postgres Manual, Section 8.8.4: Geometric Types - Boxes][PG.S.8.8.4] for details. +/// +/// [PG.S.8.8.4]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-BOXES +/// #[derive(Debug, Clone, PartialEq)] pub struct PgBox { pub upper_right_x: f64, diff --git a/sqlx-postgres/src/types/geometry/circle.rs b/sqlx-postgres/src/types/geometry/circle.rs new file mode 100644 index 0000000000..dde54dd276 --- /dev/null +++ b/sqlx-postgres/src/types/geometry/circle.rs @@ -0,0 +1,250 @@ +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; +use sqlx_core::bytes::Buf; +use sqlx_core::Error; +use std::str::FromStr; + +const ERROR: &str = "error decoding CIRCLE"; + +/// ## Postgres Geometric Circle type +/// +/// Description: Circle +/// Representation: `< (x, y), radius >` (center point and radius) +/// +/// ```text +/// < ( x , y ) , radius > +/// ( ( x , y ) , radius ) +/// ( x , y ) , radius +/// x , y , radius +/// ``` +/// where `(x,y)` is the center point. +/// +/// See [Postgres Manual, Section 8.8.7, Geometric Types - Circles][PG.S.8.8.7] for details. +/// +/// [PG.S.8.8.7]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-CIRCLE +/// +#[derive(Debug, Clone, PartialEq)] +pub struct PgCircle { + pub x: f64, + pub y: f64, + pub radius: f64, +} + +impl Type for PgCircle { + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name("circle") + } +} + +impl PgHasArrayType for PgCircle { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::with_name("_circle") + } +} + +impl<'r> Decode<'r, Postgres> for PgCircle { + fn decode(value: PgValueRef<'r>) -> Result> { + match value.format() { + PgValueFormat::Text => Ok(PgCircle::from_str(value.as_str()?)?), + PgValueFormat::Binary => Ok(PgCircle::from_bytes(value.as_bytes()?)?), + } + } +} + +impl<'q> Encode<'q, Postgres> for PgCircle { + fn produces(&self) -> Option { + Some(PgTypeInfo::with_name("circle")) + } + + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + self.serialize(buf)?; + Ok(IsNull::No) + } +} + +impl FromStr for PgCircle { + type Err = BoxDynError; + + fn from_str(s: &str) -> Result { + let sanitised = s.replace(['<', '>', '(', ')', ' '], ""); + let mut parts = sanitised.split(','); + + let x = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get x from {}", ERROR, s))?; + + let y = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get y from {}", ERROR, s))?; + + let radius = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .ok_or_else(|| format!("{}: could not get radius from {}", ERROR, s))?; + + if parts.next().is_some() { + return Err(format!("{}: too many numbers inputted in {}", ERROR, s).into()); + } + + if radius < 0. { + return Err(format!("{}: cannot have negative radius: {}", ERROR, s).into()); + } + + Ok(PgCircle { x, y, radius }) + } +} + +impl PgCircle { + fn from_bytes(mut bytes: &[u8]) -> Result { + let x = bytes.get_f64(); + let y = bytes.get_f64(); + let r = bytes.get_f64(); + Ok(PgCircle { x, y, radius: r }) + } + + fn serialize(&self, buff: &mut PgArgumentBuffer) -> Result<(), Error> { + buff.extend_from_slice(&self.x.to_be_bytes()); + buff.extend_from_slice(&self.y.to_be_bytes()); + buff.extend_from_slice(&self.radius.to_be_bytes()); + Ok(()) + } + + #[cfg(test)] + fn serialize_to_vec(&self) -> Vec { + let mut buff = PgArgumentBuffer::default(); + self.serialize(&mut buff).unwrap(); + buff.to_vec() + } +} + +#[cfg(test)] +mod circle_tests { + + use std::str::FromStr; + + use super::PgCircle; + + const CIRCLE_BYTES: &[u8] = &[ + 63, 241, 153, 153, 153, 153, 153, 154, 64, 1, 153, 153, 153, 153, 153, 154, 64, 10, 102, + 102, 102, 102, 102, 102, + ]; + + #[test] + fn can_deserialise_circle_type_bytes() { + let circle = PgCircle::from_bytes(CIRCLE_BYTES).unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.1, + y: 2.2, + radius: 3.3 + } + ) + } + + #[test] + fn can_deserialise_circle_type_str() { + let circle = PgCircle::from_str("<(1, 2), 3 >").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_second_syntax() { + let circle = PgCircle::from_str("((1, 2), 3 )").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_third_syntax() { + let circle = PgCircle::from_str("(1, 2), 3 ").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn can_deserialise_circle_type_str_fourth_syntax() { + let circle = PgCircle::from_str("1, 2, 3 ").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.0, + y: 2.0, + radius: 3.0 + } + ); + } + + #[test] + fn cannot_deserialise_circle_invalid_numbers() { + let input_str = "1, 2, Three"; + let circle = PgCircle::from_str(input_str); + assert!(circle.is_err()); + if let Err(err) = circle { + assert_eq!( + err.to_string(), + format!("error decoding CIRCLE: could not get radius from {input_str}") + ) + } + } + + #[test] + fn cannot_deserialise_circle_negative_radius() { + let input_str = "1, 2, -3"; + let circle = PgCircle::from_str(input_str); + assert!(circle.is_err()); + if let Err(err) = circle { + assert_eq!( + err.to_string(), + format!("error decoding CIRCLE: cannot have negative radius: {input_str}") + ) + } + } + + #[test] + fn can_deserialise_circle_type_str_float() { + let circle = PgCircle::from_str("<(1.1, 2.2), 3.3>").unwrap(); + assert_eq!( + circle, + PgCircle { + x: 1.1, + y: 2.2, + radius: 3.3 + } + ); + } + + #[test] + fn can_serialise_circle_type() { + let circle = PgCircle { + x: 1.1, + y: 2.2, + radius: 3.3, + }; + assert_eq!(circle.serialize_to_vec(), CIRCLE_BYTES,) + } +} diff --git a/sqlx-postgres/src/types/geometry/line.rs b/sqlx-postgres/src/types/geometry/line.rs index 43f93c1c33..8f08c949ef 100644 --- a/sqlx-postgres/src/types/geometry/line.rs +++ b/sqlx-postgres/src/types/geometry/line.rs @@ -15,7 +15,10 @@ const ERROR: &str = "error decoding LINE"; /// /// Lines are represented by the linear equation Ax + By + C = 0, where A and B are not both zero. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LINE +/// See [Postgres Manual, Section 8.8.2, Geometric Types - Lines][PG.S.8.8.2] for details. +/// +/// [PG.S.8.8.2]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-LINE +/// #[derive(Debug, Clone, PartialEq)] pub struct PgLine { pub a: f64, diff --git a/sqlx-postgres/src/types/geometry/line_segment.rs b/sqlx-postgres/src/types/geometry/line_segment.rs index 5dc5efc744..cd08e4da4a 100644 --- a/sqlx-postgres/src/types/geometry/line_segment.rs +++ b/sqlx-postgres/src/types/geometry/line_segment.rs @@ -23,7 +23,10 @@ const ERROR: &str = "error decoding LSEG"; /// ``` /// where `(start_x,start_y) and (end_x,end_y)` are the end points of the line segment. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-LSEG +/// See [Postgres Manual, Section 8.8.3, Geometric Types - Line Segments][PG.S.8.8.3] for details. +/// +/// [PG.S.8.8.3]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-LSEG +/// #[doc(alias = "line segment")] #[derive(Debug, Clone, PartialEq)] pub struct PgLSeg { diff --git a/sqlx-postgres/src/types/geometry/mod.rs b/sqlx-postgres/src/types/geometry/mod.rs index 1437d72c5c..c3142145ee 100644 --- a/sqlx-postgres/src/types/geometry/mod.rs +++ b/sqlx-postgres/src/types/geometry/mod.rs @@ -1,4 +1,5 @@ pub mod r#box; +pub mod circle; pub mod line; pub mod line_segment; pub mod path; diff --git a/sqlx-postgres/src/types/geometry/path.rs b/sqlx-postgres/src/types/geometry/path.rs index 87a3b3e8d3..6799289fac 100644 --- a/sqlx-postgres/src/types/geometry/path.rs +++ b/sqlx-postgres/src/types/geometry/path.rs @@ -27,7 +27,10 @@ const BYTE_WIDTH: usize = mem::size_of::(); /// where the points are the end points of the line segments comprising the path. Square brackets `([])` indicate an open path, while parentheses `(())` indicate a closed path. /// When the outermost parentheses are omitted, as in the third through fifth syntaxes, a closed path is assumed. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS +/// See [Postgres Manual, Section 8.8.5, Geometric Types - Paths][PG.S.8.8.5] for details. +/// +/// [PG.S.8.8.5]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-PATHS +/// #[derive(Debug, Clone, PartialEq)] pub struct PgPath { pub closed: bool, diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index cc10672950..83b7c24d0d 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -19,7 +19,10 @@ use std::str::FromStr; /// ```` /// where x and y are the respective coordinates, as floating-point numbers. /// -/// See https://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-GEOMETRIC-POINTS +/// See [Postgres Manual, Section 8.8.1, Geometric Types - Points][PG.S.8.8.1] for details. +/// +/// [PG.S.8.8.1]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-GEOMETRIC-POINTS +/// #[derive(Debug, Clone, PartialEq)] pub struct PgPoint { pub x: f64, diff --git a/sqlx-postgres/src/types/geometry/polygon.rs b/sqlx-postgres/src/types/geometry/polygon.rs index 500c9933e9..a5a203c680 100644 --- a/sqlx-postgres/src/types/geometry/polygon.rs +++ b/sqlx-postgres/src/types/geometry/polygon.rs @@ -28,7 +28,10 @@ const BYTE_WIDTH: usize = mem::size_of::(); /// /// where the points are the end points of the line segments comprising the boundary of the polygon. /// -/// Seeh ttps://www.postgresql.org/docs/16/datatype-geometric.html#DATATYPE-POLYGON +/// See [Postgres Manual, Section 8.8.6, Geometric Types - Polygons][PG.S.8.8.6] for details. +/// +/// [PG.S.8.8.6]: https://www.postgresql.org/docs/current/datatype-geometric.html#DATATYPE-POLYGON +/// #[derive(Debug, Clone, PartialEq)] pub struct PgPolygon { pub points: Vec, diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index 550ce62929..c3493139cd 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -27,6 +27,7 @@ //! | [`PgBox`] | BOX | //! | [`PgPath`] | PATH | //! | [`PgPolygon`] | POLYGON | +//! | [`PgCircle`] | CIRCLE | //! | [`PgHstore`] | HSTORE | //! //! 1 SQLx generally considers `CITEXT` to be compatible with `String`, `&str`, etc., @@ -262,6 +263,7 @@ mod bit_vec; pub use array::PgHasArrayType; pub use citext::PgCiText; pub use cube::PgCube; +pub use geometry::circle::PgCircle; pub use geometry::line::PgLine; pub use geometry::line_segment::PgLSeg; pub use geometry::path::PgPath; diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index d88e1657c1..da20467ea3 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -539,6 +539,14 @@ test_type!(polygon(Postgres, ]}, )); +#[cfg(any(postgres_12, postgres_13, postgres_14, postgres_15))] +test_type!(circle(Postgres, + "circle('<(1.1, -2.2), 3.3>')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('((1.1, -2.2), 3.3)')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('(1.1, -2.2), 3.3')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, + "circle('1.1, -2.2, 3.3')" ~= sqlx::postgres::types::PgCircle { x: 1.1, y:-2.2, radius: 3.3 }, +)); + #[cfg(feature = "rust_decimal")] test_type!(decimal(Postgres, "0::numeric" == sqlx::types::Decimal::from_str("0").unwrap(), From 393b731d5e04664cd0ece1bfd130086187090d79 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Mon, 10 Mar 2025 14:29:46 -0700 Subject: [PATCH 44/66] Merge of #3427 (by @mpyw) and #3614 (by @bonsairobo) (#3765) * feat: Implement `get_transaction_depth` for drivers * test: Verify `get_transaction_depth()` on postgres * Refactor: `TransactionManager` delegation without BC SQLite implementation is currently WIP * Fix: Avoid breaking changes on `AnyConnectionBackend` * Refactor: Remove verbose `SqliteConnection` typing * Feat: Implementation for SQLite I have included `AtomicUsize` in `WorkerSharedState`. Ideally, it is not desirable to execute `load` and `fetch_add` in two separate steps, but we decided to allow it here since there is only one thread writing. To prevent writing from other threads, the field itself was made private, and a getter method was provided with `pub(crate)`. * Refactor: Same approach for `cached_statements_size` ref: a66787d36d62876b55475ef2326d17bade817aed * Fix: Add missing `is_in_transaction` for backend * Doc: Remove verbose "synchronously" word * Fix: Remove useless `mut` qualifier * feat: add Connection::begin_with This patch completes the plumbing of an optional statement from these methods to `TransactionManager::begin` without any validation of the provided statement. There is a new `Error::InvalidSavePoint` which is triggered by any attempt to call `Connection::begin_with` when we are already inside of a transaction. * feat: add Pool::begin_with and Pool::try_begin_with * feat: add Error::BeginFailed and validate that custom "begin" statements are successful * chore: add tests of Error::BeginFailed * chore: add tests of Error::InvalidSavePointStatement * chore: test begin_with works for all SQLite "BEGIN" statements * chore: improve comment on Connection::begin_with * feat: add default impl of `Connection::begin_with` This makes the new method a non-breaking change. * refactor: combine if statement + unwrap_or_else into one match * feat: use in-memory SQLite DB to avoid conflicts across tests run in parallel * feedback: remove public wrapper for sqlite3_txn_state Move the wrapper directly into the test that uses it instead. * fix: cache Status on MySqlConnection * fix: compilation errors * fix: format * fix: postgres test * refactor: delete `Connection::get_transaction_depth` * fix: tests --------- Co-authored-by: mpyw Co-authored-by: Duncan Fairbanks --- Cargo.toml | 1 + sqlx-core/src/acquire.rs | 4 +- sqlx-core/src/any/connection/backend.rs | 29 +++++++++++- sqlx-core/src/any/connection/mod.rs | 13 +++++- sqlx-core/src/any/transaction.rs | 13 +++++- sqlx-core/src/connection.rs | 30 +++++++++++- sqlx-core/src/error.rs | 6 +++ sqlx-core/src/pool/connection.rs | 2 +- sqlx-core/src/pool/mod.rs | 39 +++++++++++++++- sqlx-core/src/transaction.rs | 26 +++++++++-- sqlx-mysql/src/any.rs | 12 ++++- sqlx-mysql/src/connection/establish.rs | 1 + sqlx-mysql/src/connection/executor.rs | 4 ++ sqlx-mysql/src/connection/mod.rs | 23 ++++++++- sqlx-mysql/src/protocol/response/status.rs | 2 +- sqlx-mysql/src/transaction.rs | 26 +++++++++-- sqlx-postgres/src/any.rs | 12 ++++- sqlx-postgres/src/connection/mod.rs | 20 +++++++- sqlx-postgres/src/transaction.rs | 28 +++++++++-- sqlx-sqlite/src/any.rs | 13 +++++- sqlx-sqlite/src/connection/establish.rs | 1 - sqlx-sqlite/src/connection/mod.rs | 30 ++++++++---- sqlx-sqlite/src/connection/worker.rs | 54 +++++++++++++++++----- sqlx-sqlite/src/transaction.rs | 26 +++++++++-- tests/mysql/error.rs | 28 ++++++++++- tests/postgres/error.rs | 28 ++++++++++- tests/postgres/postgres.rs | 5 ++ tests/sqlite/error.rs | 28 ++++++++++- tests/sqlite/sqlite.rs | 51 ++++++++++++++++++++ 29 files changed, 494 insertions(+), 61 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5a040e546f..f31d715b26 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -189,6 +189,7 @@ rand_xoshiro = "0.6.0" hex = "0.4.3" tempfile = "3.10.1" criterion = { version = "0.5.1", features = ["async_tokio"] } +libsqlite3-sys = { version = "0.30.1" } # If this is an unconditional dev-dependency then Cargo will *always* try to build `libsqlite3-sys`, # even when SQLite isn't the intended test target, and fail if the build environment is not set up for compiling C code. diff --git a/sqlx-core/src/acquire.rs b/sqlx-core/src/acquire.rs index c9d7fb215c..59bac9fa59 100644 --- a/sqlx-core/src/acquire.rs +++ b/sqlx-core/src/acquire.rs @@ -93,7 +93,7 @@ impl<'a, DB: Database> Acquire<'a> for &'_ Pool { let conn = self.acquire(); Box::pin(async move { - Transaction::begin(MaybePoolConnection::PoolConnection(conn.await?)).await + Transaction::begin(MaybePoolConnection::PoolConnection(conn.await?), None).await }) } } @@ -121,7 +121,7 @@ macro_rules! impl_acquire { 'c, Result<$crate::transaction::Transaction<'c, $DB>, $crate::error::Error>, > { - $crate::transaction::Transaction::begin(self) + $crate::transaction::Transaction::begin(self, None) } } }; diff --git a/sqlx-core/src/any/connection/backend.rs b/sqlx-core/src/any/connection/backend.rs index b30cbe83f3..6c84c1d8ce 100644 --- a/sqlx-core/src/any/connection/backend.rs +++ b/sqlx-core/src/any/connection/backend.rs @@ -3,6 +3,7 @@ use crate::describe::Describe; use either::Either; use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; +use std::borrow::Cow; use std::fmt::Debug; pub trait AnyConnectionBackend: std::any::Any + Debug + Send + 'static { @@ -26,7 +27,13 @@ pub trait AnyConnectionBackend: std::any::Any + Debug + Send + 'static { fn ping(&mut self) -> BoxFuture<'_, crate::Result<()>>; /// Begin a new transaction or establish a savepoint within the active transaction. - fn begin(&mut self) -> BoxFuture<'_, crate::Result<()>>; + /// + /// If this is a new transaction, `statement` may be used instead of the + /// default "BEGIN" statement. + /// + /// If we are already inside a transaction and `statement.is_some()`, then + /// `Error::InvalidSavePoint` is returned without running any statements. + fn begin(&mut self, statement: Option>) -> BoxFuture<'_, crate::Result<()>>; fn commit(&mut self) -> BoxFuture<'_, crate::Result<()>>; @@ -34,6 +41,26 @@ pub trait AnyConnectionBackend: std::any::Any + Debug + Send + 'static { fn start_rollback(&mut self); + /// Returns the current transaction depth. + /// + /// Transaction depth indicates the level of nested transactions: + /// - Level 0: No active transaction. + /// - Level 1: A transaction is active. + /// - Level 2 or higher: A transaction is active and one or more SAVEPOINTs have been created within it. + fn get_transaction_depth(&self) -> usize { + unimplemented!("get_transaction_depth() is not implemented for this backend. This is a provided method to avoid a breaking change, but it will become a required method in version 0.9 and later."); + } + + /// Checks if the connection is currently in a transaction. + /// + /// This method returns `true` if the current transaction depth is greater than 0, + /// indicating that a transaction is active. It returns `false` if the transaction depth is 0, + /// meaning no transaction is active. + #[inline] + fn is_in_transaction(&self) -> bool { + self.get_transaction_depth() != 0 + } + /// The number of statements currently cached in the connection. fn cached_statements_size(&self) -> usize { 0 diff --git a/sqlx-core/src/any/connection/mod.rs b/sqlx-core/src/any/connection/mod.rs index b6f795848a..8cf8fc510c 100644 --- a/sqlx-core/src/any/connection/mod.rs +++ b/sqlx-core/src/any/connection/mod.rs @@ -1,4 +1,5 @@ use futures_core::future::BoxFuture; +use std::borrow::Cow; use crate::any::{Any, AnyConnectOptions}; use crate::connection::{ConnectOptions, Connection}; @@ -87,7 +88,17 @@ impl Connection for AnyConnection { where Self: Sized, { - Transaction::begin(self) + Transaction::begin(self, None) + } + + fn begin_with( + &mut self, + statement: impl Into>, + ) -> BoxFuture<'_, Result, Error>> + where + Self: Sized, + { + Transaction::begin(self, Some(statement.into())) } fn cached_statements_size(&self) -> usize { diff --git a/sqlx-core/src/any/transaction.rs b/sqlx-core/src/any/transaction.rs index fce4175626..a553cda927 100644 --- a/sqlx-core/src/any/transaction.rs +++ b/sqlx-core/src/any/transaction.rs @@ -1,6 +1,8 @@ use futures_util::future::BoxFuture; +use std::borrow::Cow; use crate::any::{Any, AnyConnection}; +use crate::database::Database; use crate::error::Error; use crate::transaction::TransactionManager; @@ -9,8 +11,11 @@ pub struct AnyTransactionManager; impl TransactionManager for AnyTransactionManager { type Database = Any; - fn begin(conn: &mut AnyConnection) -> BoxFuture<'_, Result<(), Error>> { - conn.backend.begin() + fn begin<'conn>( + conn: &'conn mut AnyConnection, + statement: Option>, + ) -> BoxFuture<'conn, Result<(), Error>> { + conn.backend.begin(statement) } fn commit(conn: &mut AnyConnection) -> BoxFuture<'_, Result<(), Error>> { @@ -24,4 +29,8 @@ impl TransactionManager for AnyTransactionManager { fn start_rollback(conn: &mut AnyConnection) { conn.backend.start_rollback() } + + fn get_transaction_depth(conn: &::Connection) -> usize { + conn.backend.get_transaction_depth() + } } diff --git a/sqlx-core/src/connection.rs b/sqlx-core/src/connection.rs index ce2aa6c629..74e8cd3e8b 100644 --- a/sqlx-core/src/connection.rs +++ b/sqlx-core/src/connection.rs @@ -1,9 +1,10 @@ use crate::database::{Database, HasStatementCache}; use crate::error::Error; -use crate::transaction::Transaction; +use crate::transaction::{Transaction, TransactionManager}; use futures_core::future::BoxFuture; use log::LevelFilter; +use std::borrow::Cow; use std::fmt::Debug; use std::str::FromStr; use std::time::Duration; @@ -49,6 +50,33 @@ pub trait Connection: Send { where Self: Sized; + /// Begin a new transaction with a custom statement. + /// + /// Returns a [`Transaction`] for controlling and tracking the new transaction. + /// + /// Returns an error if the connection is already in a transaction or if + /// `statement` does not put the connection into a transaction. + fn begin_with( + &mut self, + statement: impl Into>, + ) -> BoxFuture<'_, Result, Error>> + where + Self: Sized, + { + Transaction::begin(self, Some(statement.into())) + } + + /// Returns `true` if the connection is currently in a transaction. + /// + /// # Note: Automatic Rollbacks May Not Be Counted + /// Certain database errors (such as a serializable isolation failure) + /// can cause automatic rollbacks of a transaction + /// which may not be indicated in the return value of this method. + #[inline] + fn is_in_transaction(&self) -> bool { + ::TransactionManager::get_transaction_depth(self) != 0 + } + /// Execute the function inside a transaction. /// /// If the function returns an error, the transaction will be rolled back. If it does not diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index 98b42fbcde..9ad5eff464 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -117,6 +117,12 @@ pub enum Error { #[cfg(feature = "migrate")] #[error("{0}")] Migrate(#[source] Box), + + #[error("attempted to call begin_with at non-zero transaction depth")] + InvalidSavePointStatement, + + #[error("got unexpected connection status after attempting to begin transaction")] + BeginFailed, } impl StdError for Box {} diff --git a/sqlx-core/src/pool/connection.rs b/sqlx-core/src/pool/connection.rs index bf3a6d4b1c..c029fec6eb 100644 --- a/sqlx-core/src/pool/connection.rs +++ b/sqlx-core/src/pool/connection.rs @@ -191,7 +191,7 @@ impl<'c, DB: Database> crate::acquire::Acquire<'c> for &'c mut PoolConnection futures_core::future::BoxFuture<'c, Result, Error>> { - crate::transaction::Transaction::begin(&mut **self) + crate::transaction::Transaction::begin(&mut **self, None) } } diff --git a/sqlx-core/src/pool/mod.rs b/sqlx-core/src/pool/mod.rs index 8aa9041ab7..d85bce246d 100644 --- a/sqlx-core/src/pool/mod.rs +++ b/sqlx-core/src/pool/mod.rs @@ -54,6 +54,7 @@ //! [`Pool::acquire`] or //! [`Pool::begin`]. +use std::borrow::Cow; use std::fmt; use std::future::Future; use std::pin::{pin, Pin}; @@ -368,13 +369,17 @@ impl Pool { /// Retrieves a connection and immediately begins a new transaction. pub async fn begin(&self) -> Result, Error> { - Transaction::begin(MaybePoolConnection::PoolConnection(self.acquire().await?)).await + Transaction::begin( + MaybePoolConnection::PoolConnection(self.acquire().await?), + None, + ) + .await } /// Attempts to retrieve a connection and immediately begins a new transaction if successful. pub async fn try_begin(&self) -> Result>, Error> { match self.try_acquire() { - Some(conn) => Transaction::begin(MaybePoolConnection::PoolConnection(conn)) + Some(conn) => Transaction::begin(MaybePoolConnection::PoolConnection(conn), None) .await .map(Some), @@ -382,6 +387,36 @@ impl Pool { } } + /// Retrieves a connection and immediately begins a new transaction using `statement`. + pub async fn begin_with( + &self, + statement: impl Into>, + ) -> Result, Error> { + Transaction::begin( + MaybePoolConnection::PoolConnection(self.acquire().await?), + Some(statement.into()), + ) + .await + } + + /// Attempts to retrieve a connection and, if successful, immediately begins a new + /// transaction using `statement`. + pub async fn try_begin_with( + &self, + statement: impl Into>, + ) -> Result>, Error> { + match self.try_acquire() { + Some(conn) => Transaction::begin( + MaybePoolConnection::PoolConnection(conn), + Some(statement.into()), + ) + .await + .map(Some), + + None => Ok(None), + } + } + /// Shut down the connection pool, immediately waking all tasks waiting for a connection. /// /// Upon calling this method, any currently waiting or subsequent calls to [`Pool::acquire`] and diff --git a/sqlx-core/src/transaction.rs b/sqlx-core/src/transaction.rs index 9cd38aab3a..2a84ff6555 100644 --- a/sqlx-core/src/transaction.rs +++ b/sqlx-core/src/transaction.rs @@ -16,9 +16,16 @@ pub trait TransactionManager { type Database: Database; /// Begin a new transaction or establish a savepoint within the active transaction. - fn begin( - conn: &mut ::Connection, - ) -> BoxFuture<'_, Result<(), Error>>; + /// + /// If this is a new transaction, `statement` may be used instead of the + /// default "BEGIN" statement. + /// + /// If we are already inside a transaction and `statement.is_some()`, then + /// `Error::InvalidSavePoint` is returned without running any statements. + fn begin<'conn>( + conn: &'conn mut ::Connection, + statement: Option>, + ) -> BoxFuture<'conn, Result<(), Error>>; /// Commit the active transaction or release the most recent savepoint. fn commit( @@ -32,6 +39,14 @@ pub trait TransactionManager { /// Starts to abort the active transaction or restore from the most recent snapshot. fn start_rollback(conn: &mut ::Connection); + + /// Returns the current transaction depth. + /// + /// Transaction depth indicates the level of nested transactions: + /// - Level 0: No active transaction. + /// - Level 1: A transaction is active. + /// - Level 2 or higher: A transaction is active and one or more SAVEPOINTs have been created within it. + fn get_transaction_depth(conn: &::Connection) -> usize; } /// An in-progress database transaction or savepoint. @@ -83,11 +98,12 @@ where #[doc(hidden)] pub fn begin( conn: impl Into>, + statement: Option>, ) -> BoxFuture<'c, Result> { let mut conn = conn.into(); Box::pin(async move { - DB::TransactionManager::begin(&mut conn).await?; + DB::TransactionManager::begin(&mut conn, statement).await?; Ok(Self { connection: conn, @@ -237,7 +253,7 @@ impl<'c, 't, DB: Database> crate::acquire::Acquire<'t> for &'t mut Transaction<' #[inline] fn begin(self) -> BoxFuture<'t, Result, Error>> { - Transaction::begin(&mut **self) + Transaction::begin(&mut **self, None) } } diff --git a/sqlx-mysql/src/any.rs b/sqlx-mysql/src/any.rs index e01e41d68e..19b3a6f27c 100644 --- a/sqlx-mysql/src/any.rs +++ b/sqlx-mysql/src/any.rs @@ -16,6 +16,7 @@ use sqlx_core::database::Database; use sqlx_core::describe::Describe; use sqlx_core::executor::Executor; use sqlx_core::transaction::TransactionManager; +use std::borrow::Cow; use std::{future, pin::pin}; sqlx_core::declare_driver_with_optional_migrate!(DRIVER = MySql); @@ -37,8 +38,11 @@ impl AnyConnectionBackend for MySqlConnection { Connection::ping(self) } - fn begin(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { - MySqlTransactionManager::begin(self) + fn begin( + &mut self, + statement: Option>, + ) -> BoxFuture<'_, sqlx_core::Result<()>> { + MySqlTransactionManager::begin(self, statement) } fn commit(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { @@ -53,6 +57,10 @@ impl AnyConnectionBackend for MySqlConnection { MySqlTransactionManager::start_rollback(self) } + fn get_transaction_depth(&self) -> usize { + MySqlTransactionManager::get_transaction_depth(self) + } + fn shrink_buffers(&mut self) { Connection::shrink_buffers(self); } diff --git a/sqlx-mysql/src/connection/establish.rs b/sqlx-mysql/src/connection/establish.rs index 0623a0556c..85a9d84f96 100644 --- a/sqlx-mysql/src/connection/establish.rs +++ b/sqlx-mysql/src/connection/establish.rs @@ -27,6 +27,7 @@ impl MySqlConnection { inner: Box::new(MySqlConnectionInner { stream, transaction_depth: 0, + status_flags: Default::default(), cache_statement: StatementCache::new(options.statement_cache_capacity), log_settings: options.log_settings.clone(), }), diff --git a/sqlx-mysql/src/connection/executor.rs b/sqlx-mysql/src/connection/executor.rs index d93aac0d68..4f5af4bf6d 100644 --- a/sqlx-mysql/src/connection/executor.rs +++ b/sqlx-mysql/src/connection/executor.rs @@ -166,6 +166,8 @@ impl MySqlConnection { // this indicates either a successful query with no rows at all or a failed query let ok = packet.ok()?; + self.inner.status_flags = ok.status; + let rows_affected = ok.affected_rows; logger.increase_rows_affected(rows_affected); let done = MySqlQueryResult { @@ -208,6 +210,8 @@ impl MySqlConnection { if packet[0] == 0xfe && packet.len() < 9 { let eof = packet.eof(self.inner.stream.capabilities)?; + self.inner.status_flags = eof.status; + r#yield!(Either::Left(MySqlQueryResult { rows_affected: 0, last_insert_id: 0, diff --git a/sqlx-mysql/src/connection/mod.rs b/sqlx-mysql/src/connection/mod.rs index c4978a7701..0a2f5fb839 100644 --- a/sqlx-mysql/src/connection/mod.rs +++ b/sqlx-mysql/src/connection/mod.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; use futures_core::future::BoxFuture; @@ -7,6 +8,7 @@ pub(crate) use stream::{MySqlStream, Waiting}; use crate::common::StatementCache; use crate::error::Error; +use crate::protocol::response::Status; use crate::protocol::statement::StmtClose; use crate::protocol::text::{Ping, Quit}; use crate::statement::MySqlStatementMetadata; @@ -34,6 +36,7 @@ pub(crate) struct MySqlConnectionInner { // transaction status pub(crate) transaction_depth: usize, + status_flags: Status, // cache by query string to the statement id and metadata cache_statement: StatementCache<(u32, MySqlStatementMetadata)>, @@ -41,6 +44,14 @@ pub(crate) struct MySqlConnectionInner { log_settings: LogSettings, } +impl MySqlConnection { + pub(crate) fn in_transaction(&self) -> bool { + self.inner + .status_flags + .intersects(Status::SERVER_STATUS_IN_TRANS) + } +} + impl Debug for MySqlConnection { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("MySqlConnection").finish() @@ -111,7 +122,17 @@ impl Connection for MySqlConnection { where Self: Sized, { - Transaction::begin(self) + Transaction::begin(self, None) + } + + fn begin_with( + &mut self, + statement: impl Into>, + ) -> BoxFuture<'_, Result, Error>> + where + Self: Sized, + { + Transaction::begin(self, Some(statement.into())) } fn shrink_buffers(&mut self) { diff --git a/sqlx-mysql/src/protocol/response/status.rs b/sqlx-mysql/src/protocol/response/status.rs index bf5013deed..4a8bb0375a 100644 --- a/sqlx-mysql/src/protocol/response/status.rs +++ b/sqlx-mysql/src/protocol/response/status.rs @@ -1,7 +1,7 @@ // https://dev.mysql.com/doc/dev/mysql-server/8.0.12/mysql__com_8h.html#a1d854e841086925be1883e4d7b4e8cad // https://mariadb.com/kb/en/library/mariadb-connectorc-types-and-definitions/#server-status bitflags::bitflags! { - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] pub struct Status: u16 { // Is raised when a multi-statement transaction has been started, either explicitly, // by means of BEGIN or COMMIT AND CHAIN, or implicitly, by the first diff --git a/sqlx-mysql/src/transaction.rs b/sqlx-mysql/src/transaction.rs index d8538cc2b3..545cb5f4f2 100644 --- a/sqlx-mysql/src/transaction.rs +++ b/sqlx-mysql/src/transaction.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use futures_core::future::BoxFuture; use crate::connection::Waiting; @@ -14,12 +16,24 @@ pub struct MySqlTransactionManager; impl TransactionManager for MySqlTransactionManager { type Database = MySql; - fn begin(conn: &mut MySqlConnection) -> BoxFuture<'_, Result<(), Error>> { + fn begin<'conn>( + conn: &'conn mut MySqlConnection, + statement: Option>, + ) -> BoxFuture<'conn, Result<(), Error>> { Box::pin(async move { let depth = conn.inner.transaction_depth; - - conn.execute(&*begin_ansi_transaction_sql(depth)).await?; - conn.inner.transaction_depth = depth + 1; + let statement = match statement { + // custom `BEGIN` statements are not allowed if we're already in a transaction + // (we need to issue a `SAVEPOINT` instead) + Some(_) if depth > 0 => return Err(Error::InvalidSavePointStatement), + Some(statement) => statement, + None => begin_ansi_transaction_sql(depth), + }; + conn.execute(&*statement).await?; + if !conn.in_transaction() { + return Err(Error::BeginFailed); + } + conn.inner.transaction_depth += 1; Ok(()) }) @@ -65,4 +79,8 @@ impl TransactionManager for MySqlTransactionManager { conn.inner.transaction_depth = depth - 1; } } + + fn get_transaction_depth(conn: &MySqlConnection) -> usize { + conn.inner.transaction_depth + } } diff --git a/sqlx-postgres/src/any.rs b/sqlx-postgres/src/any.rs index a7b30fb65b..762f53e5df 100644 --- a/sqlx-postgres/src/any.rs +++ b/sqlx-postgres/src/any.rs @@ -5,6 +5,7 @@ use crate::{ use futures_core::future::BoxFuture; use futures_core::stream::BoxStream; use futures_util::{stream, StreamExt, TryFutureExt, TryStreamExt}; +use std::borrow::Cow; use std::{future, pin::pin}; use sqlx_core::any::{ @@ -39,8 +40,11 @@ impl AnyConnectionBackend for PgConnection { Connection::ping(self) } - fn begin(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { - PgTransactionManager::begin(self) + fn begin( + &mut self, + statement: Option>, + ) -> BoxFuture<'_, sqlx_core::Result<()>> { + PgTransactionManager::begin(self, statement) } fn commit(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { @@ -55,6 +59,10 @@ impl AnyConnectionBackend for PgConnection { PgTransactionManager::start_rollback(self) } + fn get_transaction_depth(&self) -> usize { + PgTransactionManager::get_transaction_depth(self) + } + fn shrink_buffers(&mut self) { Connection::shrink_buffers(self); } diff --git a/sqlx-postgres/src/connection/mod.rs b/sqlx-postgres/src/connection/mod.rs index c139f8e53d..96e3e2fe12 100644 --- a/sqlx-postgres/src/connection/mod.rs +++ b/sqlx-postgres/src/connection/mod.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; @@ -127,6 +128,13 @@ impl PgConnection { Ok(()) } + + pub(crate) fn in_transaction(&self) -> bool { + match self.inner.transaction_status { + TransactionStatus::Transaction => true, + TransactionStatus::Error | TransactionStatus::Idle => false, + } + } } impl Debug for PgConnection { @@ -179,7 +187,17 @@ impl Connection for PgConnection { where Self: Sized, { - Transaction::begin(self) + Transaction::begin(self, None) + } + + fn begin_with( + &mut self, + statement: impl Into>, + ) -> BoxFuture<'_, Result, Error>> + where + Self: Sized, + { + Transaction::begin(self, Some(statement.into())) } fn cached_statements_size(&self) -> usize { diff --git a/sqlx-postgres/src/transaction.rs b/sqlx-postgres/src/transaction.rs index e7c78488eb..23352a8dcf 100644 --- a/sqlx-postgres/src/transaction.rs +++ b/sqlx-postgres/src/transaction.rs @@ -1,4 +1,6 @@ use futures_core::future::BoxFuture; +use sqlx_core::database::Database; +use std::borrow::Cow; use crate::error::Error; use crate::executor::Executor; @@ -13,13 +15,27 @@ pub struct PgTransactionManager; impl TransactionManager for PgTransactionManager { type Database = Postgres; - fn begin(conn: &mut PgConnection) -> BoxFuture<'_, Result<(), Error>> { + fn begin<'conn>( + conn: &'conn mut PgConnection, + statement: Option>, + ) -> BoxFuture<'conn, Result<(), Error>> { Box::pin(async move { + let depth = conn.inner.transaction_depth; + let statement = match statement { + // custom `BEGIN` statements are not allowed if we're already in + // a transaction (we need to issue a `SAVEPOINT` instead) + Some(_) if depth > 0 => return Err(Error::InvalidSavePointStatement), + Some(statement) => statement, + None => begin_ansi_transaction_sql(depth), + }; + let rollback = Rollback::new(conn); - let query = begin_ansi_transaction_sql(rollback.conn.inner.transaction_depth); - rollback.conn.queue_simple_query(&query)?; - rollback.conn.inner.transaction_depth += 1; + rollback.conn.queue_simple_query(&statement)?; rollback.conn.wait_until_ready().await?; + if !rollback.conn.in_transaction() { + return Err(Error::BeginFailed); + } + rollback.conn.inner.transaction_depth += 1; rollback.defuse(); Ok(()) @@ -62,6 +78,10 @@ impl TransactionManager for PgTransactionManager { conn.inner.transaction_depth -= 1; } } + + fn get_transaction_depth(conn: &::Connection) -> usize { + conn.inner.transaction_depth + } } struct Rollback<'c> { diff --git a/sqlx-sqlite/src/any.rs b/sqlx-sqlite/src/any.rs index 2cc5855405..c72370d0ff 100644 --- a/sqlx-sqlite/src/any.rs +++ b/sqlx-sqlite/src/any.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use crate::{ Either, Sqlite, SqliteArgumentValue, SqliteArguments, SqliteColumn, SqliteConnectOptions, SqliteConnection, SqliteQueryResult, SqliteRow, SqliteTransactionManager, SqliteTypeInfo, @@ -38,8 +40,11 @@ impl AnyConnectionBackend for SqliteConnection { Connection::ping(self) } - fn begin(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { - SqliteTransactionManager::begin(self) + fn begin( + &mut self, + statement: Option>, + ) -> BoxFuture<'_, sqlx_core::Result<()>> { + SqliteTransactionManager::begin(self, statement) } fn commit(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> { @@ -54,6 +59,10 @@ impl AnyConnectionBackend for SqliteConnection { SqliteTransactionManager::start_rollback(self) } + fn get_transaction_depth(&self) -> usize { + SqliteTransactionManager::get_transaction_depth(self) + } + fn shrink_buffers(&mut self) { // NO-OP. } diff --git a/sqlx-sqlite/src/connection/establish.rs b/sqlx-sqlite/src/connection/establish.rs index 334b1616ae..c5d2450fba 100644 --- a/sqlx-sqlite/src/connection/establish.rs +++ b/sqlx-sqlite/src/connection/establish.rs @@ -288,7 +288,6 @@ impl EstablishParams { Ok(ConnectionState { handle, statements: Statements::new(self.statement_cache_capacity), - transaction_depth: 0, log_settings: self.log_settings.clone(), progress_handler_callback: None, update_hook_callback: None, diff --git a/sqlx-sqlite/src/connection/mod.rs b/sqlx-sqlite/src/connection/mod.rs index 3316ad40c6..b94ad91c4d 100644 --- a/sqlx-sqlite/src/connection/mod.rs +++ b/sqlx-sqlite/src/connection/mod.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::cmp::Ordering; use std::ffi::CStr; use std::fmt::Write; @@ -11,8 +12,8 @@ use futures_core::future::BoxFuture; use futures_intrusive::sync::MutexGuard; use futures_util::future; use libsqlite3_sys::{ - sqlite3, sqlite3_commit_hook, sqlite3_progress_handler, sqlite3_rollback_hook, - sqlite3_update_hook, SQLITE_DELETE, SQLITE_INSERT, SQLITE_UPDATE, + sqlite3, sqlite3_commit_hook, sqlite3_get_autocommit, sqlite3_progress_handler, + sqlite3_rollback_hook, sqlite3_update_hook, SQLITE_DELETE, SQLITE_INSERT, SQLITE_UPDATE, }; #[cfg(feature = "preupdate-hook")] pub use preupdate_hook::*; @@ -106,9 +107,6 @@ unsafe impl Send for RollbackHookHandler {} pub(crate) struct ConnectionState { pub(crate) handle: ConnectionHandle, - // transaction status - pub(crate) transaction_depth: usize, - pub(crate) statements: Statements, log_settings: LogSettings, @@ -253,14 +251,21 @@ impl Connection for SqliteConnection { where Self: Sized, { - Transaction::begin(self) + Transaction::begin(self, None) + } + + fn begin_with( + &mut self, + statement: impl Into>, + ) -> BoxFuture<'_, Result, Error>> + where + Self: Sized, + { + Transaction::begin(self, Some(statement.into())) } fn cached_statements_size(&self) -> usize { - self.worker - .shared - .cached_statements_size - .load(std::sync::atomic::Ordering::Acquire) + self.worker.shared.get_cached_statements_size() } fn clear_cached_statements(&mut self) -> BoxFuture<'_, Result<(), Error>> { @@ -547,6 +552,11 @@ impl LockedSqliteHandle<'_> { pub fn last_error(&mut self) -> Option { self.guard.handle.last_error() } + + pub(crate) fn in_transaction(&mut self) -> bool { + let ret = unsafe { sqlite3_get_autocommit(self.as_raw_handle().as_ptr()) }; + ret == 0 + } } impl Drop for ConnectionState { diff --git a/sqlx-sqlite/src/connection/worker.rs b/sqlx-sqlite/src/connection/worker.rs index 8a1d140b25..00a4c2999c 100644 --- a/sqlx-sqlite/src/connection/worker.rs +++ b/sqlx-sqlite/src/connection/worker.rs @@ -36,10 +36,21 @@ pub(crate) struct ConnectionWorker { } pub(crate) struct WorkerSharedState { - pub(crate) cached_statements_size: AtomicUsize, + transaction_depth: AtomicUsize, + cached_statements_size: AtomicUsize, pub(crate) conn: Mutex, } +impl WorkerSharedState { + pub(crate) fn get_transaction_depth(&self) -> usize { + self.transaction_depth.load(Ordering::Acquire) + } + + pub(crate) fn get_cached_statements_size(&self) -> usize { + self.cached_statements_size.load(Ordering::Acquire) + } +} + enum Command { Prepare { query: Box, @@ -68,6 +79,7 @@ enum Command { }, Begin { tx: rendezvous_oneshot::Sender>, + statement: Option>, }, Commit { tx: rendezvous_oneshot::Sender>, @@ -105,6 +117,7 @@ impl ConnectionWorker { }; let shared = Arc::new(WorkerSharedState { + transaction_depth: AtomicUsize::new(0), cached_statements_size: AtomicUsize::new(0), // note: must be fair because in `Command::UnlockDb` we unlock the mutex // and then immediately try to relock it; an unfair mutex would immediately @@ -194,13 +207,27 @@ impl ConnectionWorker { update_cached_statements_size(&conn, &shared.cached_statements_size); } - Command::Begin { tx } => { - let depth = conn.transaction_depth; + Command::Begin { tx, statement } => { + let depth = shared.transaction_depth.load(Ordering::Acquire); + + let statement = match statement { + // custom `BEGIN` statements are not allowed if + // we're already in a transaction (we need to + // issue a `SAVEPOINT` instead) + Some(_) if depth > 0 => { + if tx.blocking_send(Err(Error::InvalidSavePointStatement)).is_err() { + break; + } + continue; + }, + Some(statement) => statement, + None => begin_ansi_transaction_sql(depth), + }; let res = conn.handle - .exec(begin_ansi_transaction_sql(depth)) + .exec(statement) .map(|_| { - conn.transaction_depth += 1; + shared.transaction_depth.fetch_add(1, Ordering::Release); }); let res_ok = res.is_ok(); @@ -213,7 +240,7 @@ impl ConnectionWorker { .handle .exec(rollback_ansi_transaction_sql(depth + 1)) .map(|_| { - conn.transaction_depth -= 1; + shared.transaction_depth.fetch_sub(1, Ordering::Release); }) { // The rollback failed. To prevent leaving the connection @@ -225,13 +252,13 @@ impl ConnectionWorker { } } Command::Commit { tx } => { - let depth = conn.transaction_depth; + let depth = shared.transaction_depth.load(Ordering::Acquire); let res = if depth > 0 { conn.handle .exec(commit_ansi_transaction_sql(depth)) .map(|_| { - conn.transaction_depth -= 1; + shared.transaction_depth.fetch_sub(1, Ordering::Release); }) } else { Ok(()) @@ -251,13 +278,13 @@ impl ConnectionWorker { continue; } - let depth = conn.transaction_depth; + let depth = shared.transaction_depth.load(Ordering::Acquire); let res = if depth > 0 { conn.handle .exec(rollback_ansi_transaction_sql(depth)) .map(|_| { - conn.transaction_depth -= 1; + shared.transaction_depth.fetch_sub(1, Ordering::Release); }) } else { Ok(()) @@ -351,8 +378,11 @@ impl ConnectionWorker { Ok(rx) } - pub(crate) async fn begin(&mut self) -> Result<(), Error> { - self.oneshot_cmd_with_ack(|tx| Command::Begin { tx }) + pub(crate) async fn begin( + &mut self, + statement: Option>, + ) -> Result<(), Error> { + self.oneshot_cmd_with_ack(|tx| Command::Begin { tx, statement }) .await? } diff --git a/sqlx-sqlite/src/transaction.rs b/sqlx-sqlite/src/transaction.rs index 24eaca51b1..55a80ab9f3 100644 --- a/sqlx-sqlite/src/transaction.rs +++ b/sqlx-sqlite/src/transaction.rs @@ -1,17 +1,33 @@ use futures_core::future::BoxFuture; +use std::borrow::Cow; -use crate::{Sqlite, SqliteConnection}; use sqlx_core::error::Error; use sqlx_core::transaction::TransactionManager; +use crate::{Sqlite, SqliteConnection}; + /// Implementation of [`TransactionManager`] for SQLite. pub struct SqliteTransactionManager; impl TransactionManager for SqliteTransactionManager { type Database = Sqlite; - fn begin(conn: &mut SqliteConnection) -> BoxFuture<'_, Result<(), Error>> { - Box::pin(conn.worker.begin()) + fn begin<'conn>( + conn: &'conn mut SqliteConnection, + statement: Option>, + ) -> BoxFuture<'conn, Result<(), Error>> { + Box::pin(async { + let is_custom_statement = statement.is_some(); + conn.worker.begin(statement).await?; + if is_custom_statement { + // Check that custom statement actually put the connection into a transaction. + let mut handle = conn.lock_handle().await?; + if !handle.in_transaction() { + return Err(Error::BeginFailed); + } + } + Ok(()) + }) } fn commit(conn: &mut SqliteConnection) -> BoxFuture<'_, Result<(), Error>> { @@ -25,4 +41,8 @@ impl TransactionManager for SqliteTransactionManager { fn start_rollback(conn: &mut SqliteConnection) { conn.worker.start_rollback().ok(); } + + fn get_transaction_depth(conn: &SqliteConnection) -> usize { + conn.worker.shared.get_transaction_depth() + } } diff --git a/tests/mysql/error.rs b/tests/mysql/error.rs index 7c84266c32..3ee1024fc8 100644 --- a/tests/mysql/error.rs +++ b/tests/mysql/error.rs @@ -1,4 +1,4 @@ -use sqlx::{error::ErrorKind, mysql::MySql, Connection}; +use sqlx::{error::ErrorKind, mysql::MySql, Connection, Error}; use sqlx_test::new; #[sqlx_macros::test] @@ -74,3 +74,29 @@ async fn it_fails_with_check_violation() -> anyhow::Result<()> { Ok(()) } + +#[sqlx_macros::test] +async fn it_fails_with_begin_failed() -> anyhow::Result<()> { + let mut conn = new::().await?; + let res = conn.begin_with("SELECT * FROM tweet").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::BeginFailed), "{err:?}"); + + Ok(()) +} + +#[sqlx_macros::test] +async fn it_fails_with_invalid_save_point_statement() -> anyhow::Result<()> { + let mut conn = new::().await?; + let mut txn = conn.begin().await?; + let txn_conn = sqlx::Acquire::acquire(&mut txn).await?; + let res = txn_conn.begin_with("BEGIN").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::InvalidSavePointStatement), "{err}"); + + Ok(()) +} diff --git a/tests/postgres/error.rs b/tests/postgres/error.rs index d6f78140da..32bf814770 100644 --- a/tests/postgres/error.rs +++ b/tests/postgres/error.rs @@ -1,4 +1,4 @@ -use sqlx::{error::ErrorKind, postgres::Postgres, Connection}; +use sqlx::{error::ErrorKind, postgres::Postgres, Connection, Error}; use sqlx_test::new; #[sqlx_macros::test] @@ -74,3 +74,29 @@ async fn it_fails_with_check_violation() -> anyhow::Result<()> { Ok(()) } + +#[sqlx_macros::test] +async fn it_fails_with_begin_failed() -> anyhow::Result<()> { + let mut conn = new::().await?; + let res = conn.begin_with("SELECT * FROM tweet").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::BeginFailed), "{err:?}"); + + Ok(()) +} + +#[sqlx_macros::test] +async fn it_fails_with_invalid_save_point_statement() -> anyhow::Result<()> { + let mut conn = new::().await?; + let mut txn = conn.begin().await?; + let txn_conn = sqlx::Acquire::acquire(&mut txn).await?; + let res = txn_conn.begin_with("BEGIN").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::InvalidSavePointStatement), "{err}"); + + Ok(()) +} diff --git a/tests/postgres/postgres.rs b/tests/postgres/postgres.rs index 7de4a9cdc6..fc7108bf4f 100644 --- a/tests/postgres/postgres.rs +++ b/tests/postgres/postgres.rs @@ -515,6 +515,7 @@ async fn it_can_work_with_transactions() -> anyhow::Result<()> { #[sqlx_macros::test] async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> { let mut conn = new::().await?; + assert!(!conn.is_in_transaction()); conn.execute("CREATE TABLE IF NOT EXISTS _sqlx_users_2523 (id INTEGER PRIMARY KEY)") .await?; @@ -523,6 +524,7 @@ async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> { // begin let mut tx = conn.begin().await?; // transaction + assert!(tx.is_in_transaction()); // insert a user sqlx::query("INSERT INTO _sqlx_users_2523 (id) VALUES ($1)") @@ -532,6 +534,7 @@ async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> { // begin once more let mut tx2 = tx.begin().await?; // savepoint + assert!(tx2.is_in_transaction()); // insert another user sqlx::query("INSERT INTO _sqlx_users_2523 (id) VALUES ($1)") @@ -541,6 +544,7 @@ async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> { // never mind, rollback tx2.rollback().await?; // roll that one back + assert!(tx.is_in_transaction()); // did we really? let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_2523") @@ -551,6 +555,7 @@ async fn it_can_work_with_nested_transactions() -> anyhow::Result<()> { // actually, commit tx.commit().await?; + assert!(!conn.is_in_transaction()); // did we really? let (count,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM _sqlx_users_2523") diff --git a/tests/sqlite/error.rs b/tests/sqlite/error.rs index 1f6b797e69..8729842b70 100644 --- a/tests/sqlite/error.rs +++ b/tests/sqlite/error.rs @@ -1,4 +1,4 @@ -use sqlx::{error::ErrorKind, sqlite::Sqlite, Connection, Executor}; +use sqlx::{error::ErrorKind, sqlite::Sqlite, Connection, Error, Executor}; use sqlx_test::new; #[sqlx_macros::test] @@ -70,3 +70,29 @@ async fn it_fails_with_check_violation() -> anyhow::Result<()> { Ok(()) } + +#[sqlx_macros::test] +async fn it_fails_with_begin_failed() -> anyhow::Result<()> { + let mut conn = new::().await?; + let res = conn.begin_with("SELECT * FROM tweet").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::BeginFailed), "{err:?}"); + + Ok(()) +} + +#[sqlx_macros::test] +async fn it_fails_with_invalid_save_point_statement() -> anyhow::Result<()> { + let mut conn = new::().await?; + let mut txn = conn.begin().await?; + let txn_conn = sqlx::Acquire::acquire(&mut txn).await?; + let res = txn_conn.begin_with("BEGIN").await; + + let err = res.unwrap_err(); + + assert!(matches!(err, Error::InvalidSavePointStatement), "{err}"); + + Ok(()) +} diff --git a/tests/sqlite/sqlite.rs b/tests/sqlite/sqlite.rs index 92a1138734..c23c4fc9ef 100644 --- a/tests/sqlite/sqlite.rs +++ b/tests/sqlite/sqlite.rs @@ -6,6 +6,7 @@ use sqlx::{ query, sqlite::Sqlite, sqlite::SqliteRow, Column, ConnectOptions, Connection, Executor, Row, SqliteConnection, SqlitePool, Statement, TypeInfo, }; +use sqlx_sqlite::LockedSqliteHandle; use sqlx_test::new; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -1316,3 +1317,53 @@ async fn test_serialize_invalid_schema() -> anyhow::Result<()> { Ok(()) } + +#[sqlx_macros::test] +async fn it_can_use_transaction_options() -> anyhow::Result<()> { + async fn check_txn_state(conn: &mut SqliteConnection, expected: SqliteTransactionState) { + let state = transaction_state(&mut conn.lock_handle().await.unwrap()); + assert_eq!(state, expected); + } + + let mut conn = SqliteConnectOptions::new() + .in_memory(true) + .connect() + .await + .unwrap(); + + check_txn_state(&mut conn, SqliteTransactionState::None).await; + + let mut tx = conn.begin_with("BEGIN DEFERRED").await?; + check_txn_state(&mut tx, SqliteTransactionState::None).await; + drop(tx); + + let mut tx = conn.begin_with("BEGIN IMMEDIATE").await?; + check_txn_state(&mut tx, SqliteTransactionState::Write).await; + drop(tx); + + let mut tx = conn.begin_with("BEGIN EXCLUSIVE").await?; + check_txn_state(&mut tx, SqliteTransactionState::Write).await; + drop(tx); + + Ok(()) +} + +fn transaction_state(handle: &mut LockedSqliteHandle) -> SqliteTransactionState { + use libsqlite3_sys::{sqlite3_txn_state, SQLITE_TXN_NONE, SQLITE_TXN_READ, SQLITE_TXN_WRITE}; + + let unchecked_state = + unsafe { sqlite3_txn_state(handle.as_raw_handle().as_ptr(), std::ptr::null()) }; + match unchecked_state { + SQLITE_TXN_NONE => SqliteTransactionState::None, + SQLITE_TXN_READ => SqliteTransactionState::Read, + SQLITE_TXN_WRITE => SqliteTransactionState::Write, + _ => panic!("unknown txn state: {unchecked_state}"), + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SqliteTransactionState { + None, + Read, + Write, +} From e474be6d4b4f7b8a1bbeb65363ef53015feebc47 Mon Sep 17 00:00:00 2001 From: Robin Schroer Date: Sun, 16 Mar 2025 15:21:56 +0900 Subject: [PATCH 45/66] docs: Fix a copy-paste error on get_username docs (#3786) I suspect this is a copy-paste error, it's meant to say username, not port. --- sqlx-mysql/src/options/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-mysql/src/options/mod.rs b/sqlx-mysql/src/options/mod.rs index db2b20c19d..87732cb40c 100644 --- a/sqlx-mysql/src/options/mod.rs +++ b/sqlx-mysql/src/options/mod.rs @@ -448,7 +448,7 @@ impl MySqlConnectOptions { self.socket.as_ref() } - /// Get the server's port. + /// Get the current username. /// /// # Example /// From 1c9cbe939ada22f377e51f3d60d538bcfc567e8f Mon Sep 17 00:00:00 2001 From: Beau Gieskens Date: Mon, 24 Mar 2025 10:19:05 +1000 Subject: [PATCH 46/66] feat: add ipnet support (#3710) * feat: add ipnet support * fix: ipnet not decoding IP address strings * fix: prefer ipnetwork to ipnet for compatibility * fix: unnecessary cfg --- Cargo.lock | 8 ++ Cargo.toml | 3 + README.md | 2 + sqlx-core/Cargo.toml | 1 + sqlx-core/src/types/mod.rs | 7 + sqlx-macros-core/Cargo.toml | 1 + sqlx-macros/Cargo.toml | 1 + sqlx-postgres/Cargo.toml | 2 + sqlx-postgres/src/type_checking.rs | 6 + sqlx-postgres/src/types/ipnet/ipaddr.rs | 62 +++++++++ sqlx-postgres/src/types/ipnet/ipnet.rs | 130 ++++++++++++++++++ sqlx-postgres/src/types/ipnet/mod.rs | 7 + .../src/types/{ => ipnetwork}/ipaddr.rs | 0 .../src/types/{ => ipnetwork}/ipnetwork.rs | 0 sqlx-postgres/src/types/ipnetwork/mod.rs | 5 + sqlx-postgres/src/types/mod.rs | 19 ++- tests/postgres/types.rs | 43 +++++- tests/ui-tests.rs | 2 +- 18 files changed, 293 insertions(+), 6 deletions(-) create mode 100644 sqlx-postgres/src/types/ipnet/ipaddr.rs create mode 100644 sqlx-postgres/src/types/ipnet/ipnet.rs create mode 100644 sqlx-postgres/src/types/ipnet/mod.rs rename sqlx-postgres/src/types/{ => ipnetwork}/ipaddr.rs (100%) rename sqlx-postgres/src/types/{ => ipnetwork}/ipnetwork.rs (100%) create mode 100644 sqlx-postgres/src/types/ipnetwork/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 07754e7c22..f1c4604c59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1939,6 +1939,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + [[package]] name = "ipnetwork" version = "0.20.0" @@ -3443,6 +3449,7 @@ dependencies = [ "hashbrown 0.15.2", "hashlink", "indexmap 2.7.0", + "ipnet", "ipnetwork", "log", "mac_address", @@ -3698,6 +3705,7 @@ dependencies = [ "hkdf", "hmac", "home", + "ipnet", "ipnetwork", "itoa", "log", diff --git a/Cargo.toml b/Cargo.toml index f31d715b26..fe2669794b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ _unstable-all-types = [ "json", "time", "chrono", + "ipnet", "ipnetwork", "mac_address", "uuid", @@ -117,6 +118,7 @@ json = ["sqlx-macros?/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sq bigdecimal = ["sqlx-core/bigdecimal", "sqlx-macros?/bigdecimal", "sqlx-mysql?/bigdecimal", "sqlx-postgres?/bigdecimal"] bit-vec = ["sqlx-core/bit-vec", "sqlx-macros?/bit-vec", "sqlx-postgres?/bit-vec"] chrono = ["sqlx-core/chrono", "sqlx-macros?/chrono", "sqlx-mysql?/chrono", "sqlx-postgres?/chrono", "sqlx-sqlite?/chrono"] +ipnet = ["sqlx-core/ipnet", "sqlx-macros?/ipnet", "sqlx-postgres?/ipnet"] ipnetwork = ["sqlx-core/ipnetwork", "sqlx-macros?/ipnetwork", "sqlx-postgres?/ipnetwork"] mac_address = ["sqlx-core/mac_address", "sqlx-macros?/mac_address", "sqlx-postgres?/mac_address"] rust_decimal = ["sqlx-core/rust_decimal", "sqlx-macros?/rust_decimal", "sqlx-mysql?/rust_decimal", "sqlx-postgres?/rust_decimal"] @@ -144,6 +146,7 @@ sqlx = { version = "=0.8.3", path = ".", default-features = false } bigdecimal = "0.4.0" bit-vec = "0.6.3" chrono = { version = "0.4.34", default-features = false, features = ["std", "clock"] } +ipnet = "2.3.0" ipnetwork = "0.20.0" mac_address = "1.1.5" rust_decimal = { version = "1.26.1", default-features = false, features = ["std"] } diff --git a/README.md b/README.md index c3b501ca48..cc0ecf2e66 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,8 @@ be removed in the future. - `rust_decimal`: Add support for `NUMERIC` using the `rust_decimal` crate. +- `ipnet`: Add support for `INET` and `CIDR` (in postgres) using the `ipnet` crate. + - `ipnetwork`: Add support for `INET` and `CIDR` (in postgres) using the `ipnetwork` crate. - `json`: Add support for `JSON` and `JSONB` (in postgres) using the `serde_json` crate. diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index dcd8083023..f6017a9fee 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -48,6 +48,7 @@ bit-vec = { workspace = true, optional = true } bigdecimal = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } time = { workspace = true, optional = true } +ipnet = { workspace = true, optional = true } ipnetwork = { workspace = true, optional = true } mac_address = { workspace = true, optional = true } uuid = { workspace = true, optional = true } diff --git a/sqlx-core/src/types/mod.rs b/sqlx-core/src/types/mod.rs index 909dd4927b..b00427daae 100644 --- a/sqlx-core/src/types/mod.rs +++ b/sqlx-core/src/types/mod.rs @@ -67,6 +67,13 @@ pub use bigdecimal::BigDecimal; #[doc(no_inline)] pub use rust_decimal::Decimal; +#[cfg(feature = "ipnet")] +#[cfg_attr(docsrs, doc(cfg(feature = "ipnet")))] +pub mod ipnet { + #[doc(no_inline)] + pub use ipnet::{IpNet, Ipv4Net, Ipv6Net}; +} + #[cfg(feature = "ipnetwork")] #[cfg_attr(docsrs, doc(cfg(feature = "ipnetwork")))] pub mod ipnetwork { diff --git a/sqlx-macros-core/Cargo.toml b/sqlx-macros-core/Cargo.toml index 46786b7d8d..85efa80912 100644 --- a/sqlx-macros-core/Cargo.toml +++ b/sqlx-macros-core/Cargo.toml @@ -38,6 +38,7 @@ json = ["sqlx-core/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sqlit bigdecimal = ["sqlx-core/bigdecimal", "sqlx-mysql?/bigdecimal", "sqlx-postgres?/bigdecimal"] bit-vec = ["sqlx-core/bit-vec", "sqlx-postgres?/bit-vec"] chrono = ["sqlx-core/chrono", "sqlx-mysql?/chrono", "sqlx-postgres?/chrono", "sqlx-sqlite?/chrono"] +ipnet = ["sqlx-core/ipnet", "sqlx-postgres?/ipnet"] ipnetwork = ["sqlx-core/ipnetwork", "sqlx-postgres?/ipnetwork"] mac_address = ["sqlx-core/mac_address", "sqlx-postgres?/mac_address"] rust_decimal = ["sqlx-core/rust_decimal", "sqlx-mysql?/rust_decimal", "sqlx-postgres?/rust_decimal"] diff --git a/sqlx-macros/Cargo.toml b/sqlx-macros/Cargo.toml index 5617d3f251..b513c3e808 100644 --- a/sqlx-macros/Cargo.toml +++ b/sqlx-macros/Cargo.toml @@ -37,6 +37,7 @@ sqlite-unbundled = ["sqlx-macros-core/sqlite-unbundled"] bigdecimal = ["sqlx-macros-core/bigdecimal"] bit-vec = ["sqlx-macros-core/bit-vec"] chrono = ["sqlx-macros-core/chrono"] +ipnet = ["sqlx-macros-core/ipnet"] ipnetwork = ["sqlx-macros-core/ipnetwork"] mac_address = ["sqlx-macros-core/mac_address"] rust_decimal = ["sqlx-macros-core/rust_decimal"] diff --git a/sqlx-postgres/Cargo.toml b/sqlx-postgres/Cargo.toml index 174a73b3fa..818aadbab7 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -19,6 +19,7 @@ offline = ["sqlx-core/offline"] bigdecimal = ["dep:bigdecimal", "dep:num-bigint", "sqlx-core/bigdecimal"] bit-vec = ["dep:bit-vec", "sqlx-core/bit-vec"] chrono = ["dep:chrono", "sqlx-core/chrono"] +ipnet = ["dep:ipnet", "sqlx-core/ipnet"] ipnetwork = ["dep:ipnetwork", "sqlx-core/ipnetwork"] mac_address = ["dep:mac_address", "sqlx-core/mac_address"] rust_decimal = ["dep:rust_decimal", "rust_decimal/maths", "sqlx-core/rust_decimal"] @@ -43,6 +44,7 @@ sha2 = { version = "0.10.0", default-features = false } bigdecimal = { workspace = true, optional = true } bit-vec = { workspace = true, optional = true } chrono = { workspace = true, optional = true } +ipnet = { workspace = true, optional = true } ipnetwork = { workspace = true, optional = true } mac_address = { workspace = true, optional = true } rust_decimal = { workspace = true, optional = true } diff --git a/sqlx-postgres/src/type_checking.rs b/sqlx-postgres/src/type_checking.rs index a28531c9b6..672d9f73e6 100644 --- a/sqlx-postgres/src/type_checking.rs +++ b/sqlx-postgres/src/type_checking.rs @@ -88,6 +88,9 @@ impl_type_checking!( #[cfg(feature = "ipnetwork")] sqlx::types::ipnetwork::IpNetwork, + #[cfg(feature = "ipnet")] + sqlx::types::ipnet::IpNet, + #[cfg(feature = "mac_address")] sqlx::types::mac_address::MacAddress, @@ -149,6 +152,9 @@ impl_type_checking!( #[cfg(feature = "ipnetwork")] Vec | &[sqlx::types::ipnetwork::IpNetwork], + #[cfg(feature = "ipnet")] + Vec | &[sqlx::types::ipnet::IpNet], + #[cfg(feature = "mac_address")] Vec | &[sqlx::types::mac_address::MacAddress], diff --git a/sqlx-postgres/src/types/ipnet/ipaddr.rs b/sqlx-postgres/src/types/ipnet/ipaddr.rs new file mode 100644 index 0000000000..b157eff3c6 --- /dev/null +++ b/sqlx-postgres/src/types/ipnet/ipaddr.rs @@ -0,0 +1,62 @@ +use std::net::IpAddr; + +use ipnet::IpNet; + +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueRef, Postgres}; + +impl Type for IpAddr +where + IpNet: Type, +{ + fn type_info() -> PgTypeInfo { + IpNet::type_info() + } + + fn compatible(ty: &PgTypeInfo) -> bool { + IpNet::compatible(ty) + } +} + +impl PgHasArrayType for IpAddr { + fn array_type_info() -> PgTypeInfo { + ::array_type_info() + } + + fn array_compatible(ty: &PgTypeInfo) -> bool { + ::array_compatible(ty) + } +} + +impl<'db> Encode<'db, Postgres> for IpAddr +where + IpNet: Encode<'db, Postgres>, +{ + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + IpNet::from(*self).encode_by_ref(buf) + } + + fn size_hint(&self) -> usize { + IpNet::from(*self).size_hint() + } +} + +impl<'db> Decode<'db, Postgres> for IpAddr +where + IpNet: Decode<'db, Postgres>, +{ + fn decode(value: PgValueRef<'db>) -> Result { + let ipnet = IpNet::decode(value)?; + + if matches!(ipnet, IpNet::V4(net) if net.prefix_len() != 32) + || matches!(ipnet, IpNet::V6(net) if net.prefix_len() != 128) + { + Err("lossy decode from inet/cidr")? + } + + Ok(ipnet.addr()) + } +} diff --git a/sqlx-postgres/src/types/ipnet/ipnet.rs b/sqlx-postgres/src/types/ipnet/ipnet.rs new file mode 100644 index 0000000000..1f986174b8 --- /dev/null +++ b/sqlx-postgres/src/types/ipnet/ipnet.rs @@ -0,0 +1,130 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +#[cfg(feature = "ipnet")] +use ipnet::{IpNet, Ipv4Net, Ipv6Net}; + +use crate::decode::Decode; +use crate::encode::{Encode, IsNull}; +use crate::error::BoxDynError; +use crate::types::Type; +use crate::{PgArgumentBuffer, PgHasArrayType, PgTypeInfo, PgValueFormat, PgValueRef, Postgres}; + +// https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/include/utils/inet.h#L39 + +// Technically this is a magic number here but it doesn't make sense to drag in the whole of `libc` +// just for one constant. +const PGSQL_AF_INET: u8 = 2; // AF_INET +const PGSQL_AF_INET6: u8 = PGSQL_AF_INET + 1; + +impl Type for IpNet { + fn type_info() -> PgTypeInfo { + PgTypeInfo::INET + } + + fn compatible(ty: &PgTypeInfo) -> bool { + *ty == PgTypeInfo::CIDR || *ty == PgTypeInfo::INET + } +} + +impl PgHasArrayType for IpNet { + fn array_type_info() -> PgTypeInfo { + PgTypeInfo::INET_ARRAY + } + + fn array_compatible(ty: &PgTypeInfo) -> bool { + *ty == PgTypeInfo::CIDR_ARRAY || *ty == PgTypeInfo::INET_ARRAY + } +} + +impl Encode<'_, Postgres> for IpNet { + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> Result { + // https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/backend/utils/adt/network.c#L293 + // https://github.com/postgres/postgres/blob/574925bfd0a8175f6e161936ea11d9695677ba09/src/backend/utils/adt/network.c#L271 + + match self { + IpNet::V4(net) => { + buf.push(PGSQL_AF_INET); // ip_family + buf.push(net.prefix_len()); // ip_bits + buf.push(0); // is_cidr + buf.push(4); // nb (number of bytes) + buf.extend_from_slice(&net.addr().octets()) // address + } + + IpNet::V6(net) => { + buf.push(PGSQL_AF_INET6); // ip_family + buf.push(net.prefix_len()); // ip_bits + buf.push(0); // is_cidr + buf.push(16); // nb (number of bytes) + buf.extend_from_slice(&net.addr().octets()); // address + } + } + + Ok(IsNull::No) + } + + fn size_hint(&self) -> usize { + match self { + IpNet::V4(_) => 8, + IpNet::V6(_) => 20, + } + } +} + +impl Decode<'_, Postgres> for IpNet { + fn decode(value: PgValueRef<'_>) -> Result { + let bytes = match value.format() { + PgValueFormat::Binary => value.as_bytes()?, + PgValueFormat::Text => { + let s = value.as_str()?; + println!("{s}"); + if s.contains('/') { + return Ok(s.parse()?); + } + // IpNet::from_str doesn't handle conversion from IpAddr to IpNet + let addr: IpAddr = s.parse()?; + return Ok(addr.into()); + } + }; + + if bytes.len() >= 8 { + let family = bytes[0]; + let prefix = bytes[1]; + let _is_cidr = bytes[2] != 0; + let len = bytes[3]; + + match family { + PGSQL_AF_INET => { + if bytes.len() == 8 && len == 4 { + let inet = Ipv4Net::new( + Ipv4Addr::new(bytes[4], bytes[5], bytes[6], bytes[7]), + prefix, + )?; + + return Ok(IpNet::V4(inet)); + } + } + + PGSQL_AF_INET6 => { + if bytes.len() == 20 && len == 16 { + let inet = Ipv6Net::new( + Ipv6Addr::from([ + bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], + bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15], + bytes[16], bytes[17], bytes[18], bytes[19], + ]), + prefix, + )?; + + return Ok(IpNet::V6(inet)); + } + } + + _ => { + return Err(format!("unknown ip family {family}").into()); + } + } + } + + Err("invalid data received when expecting an INET".into()) + } +} diff --git a/sqlx-postgres/src/types/ipnet/mod.rs b/sqlx-postgres/src/types/ipnet/mod.rs new file mode 100644 index 0000000000..cd40cf30da --- /dev/null +++ b/sqlx-postgres/src/types/ipnet/mod.rs @@ -0,0 +1,7 @@ +// Prefer `ipnetwork` over `ipnet` because it was implemented first (want to avoid breaking change). +#[cfg(not(feature = "ipnetwork"))] +mod ipaddr; + +// Parent module is named after the `ipnet` crate, this is named after the `IpNet` type. +#[allow(clippy::module_inception)] +mod ipnet; diff --git a/sqlx-postgres/src/types/ipaddr.rs b/sqlx-postgres/src/types/ipnetwork/ipaddr.rs similarity index 100% rename from sqlx-postgres/src/types/ipaddr.rs rename to sqlx-postgres/src/types/ipnetwork/ipaddr.rs diff --git a/sqlx-postgres/src/types/ipnetwork.rs b/sqlx-postgres/src/types/ipnetwork/ipnetwork.rs similarity index 100% rename from sqlx-postgres/src/types/ipnetwork.rs rename to sqlx-postgres/src/types/ipnetwork/ipnetwork.rs diff --git a/sqlx-postgres/src/types/ipnetwork/mod.rs b/sqlx-postgres/src/types/ipnetwork/mod.rs new file mode 100644 index 0000000000..de40244c65 --- /dev/null +++ b/sqlx-postgres/src/types/ipnetwork/mod.rs @@ -0,0 +1,5 @@ +mod ipaddr; + +// Parent module is named after the `ipnetwork` crate, this is named after the `IpNetwork` type. +#[allow(clippy::module_inception)] +mod ipnetwork; diff --git a/sqlx-postgres/src/types/mod.rs b/sqlx-postgres/src/types/mod.rs index c3493139cd..0faefbb482 100644 --- a/sqlx-postgres/src/types/mod.rs +++ b/sqlx-postgres/src/types/mod.rs @@ -87,7 +87,7 @@ //! //! ### [`ipnetwork`](https://crates.io/crates/ipnetwork) //! -//! Requires the `ipnetwork` Cargo feature flag. +//! Requires the `ipnetwork` Cargo feature flag (takes precedence over `ipnet` if both are used). //! //! | Rust type | Postgres type(s) | //! |---------------------------------------|------------------------------------------------------| @@ -100,6 +100,17 @@ //! //! `IpNetwork` does not have this limitation. //! +//! ### [`ipnet`](https://crates.io/crates/ipnet) +//! +//! Requires the `ipnet` Cargo feature flag. +//! +//! | Rust type | Postgres type(s) | +//! |---------------------------------------|------------------------------------------------------| +//! | `ipnet::IpNet` | INET, CIDR | +//! | `std::net::IpAddr` | INET, CIDR | +//! +//! The same `IpAddr` limitation for smaller network prefixes applies as with `ipnet`. +//! //! ### [`mac_address`](https://crates.io/crates/mac_address) //! //! Requires the `mac_address` Cargo feature flag. @@ -248,11 +259,11 @@ mod time; #[cfg(feature = "uuid")] mod uuid; -#[cfg(feature = "ipnetwork")] -mod ipnetwork; +#[cfg(feature = "ipnet")] +mod ipnet; #[cfg(feature = "ipnetwork")] -mod ipaddr; +mod ipnetwork; #[cfg(feature = "mac_address")] mod mac_address; diff --git a/tests/postgres/types.rs b/tests/postgres/types.rs index da20467ea3..d5d34bc1b3 100644 --- a/tests/postgres/types.rs +++ b/tests/postgres/types.rs @@ -2,6 +2,7 @@ extern crate time_ as time; use std::net::SocketAddr; use std::ops::Bound; +use std::str::FromStr; use sqlx::postgres::types::{Oid, PgCiText, PgInterval, PgMoney, PgRange}; use sqlx::postgres::Postgres; @@ -9,7 +10,6 @@ use sqlx_test::{new, test_decode_type, test_prepared_type, test_type}; use sqlx_core::executor::Executor; use sqlx_core::types::Text; -use std::str::FromStr; test_type!(null>(Postgres, "NULL::int2" == None:: @@ -171,6 +171,38 @@ test_type!(uuid_vec>(Postgres, ] )); +#[cfg(feature = "ipnet")] +test_type!(ipnet(Postgres, + "'127.0.0.1'::inet" + == "127.0.0.1/32" + .parse::() + .unwrap(), + "'8.8.8.8/24'::inet" + == "8.8.8.8/24" + .parse::() + .unwrap(), + "'10.1.1/24'::inet" + == "10.1.1.0/24" + .parse::() + .unwrap(), + "'::ffff:1.2.3.0'::inet" + == "::ffff:1.2.3.0/128" + .parse::() + .unwrap(), + "'2001:4f8:3:ba::/64'::inet" + == "2001:4f8:3:ba::/64" + .parse::() + .unwrap(), + "'192.168'::cidr" + == "192.168.0.0/24" + .parse::() + .unwrap(), + "'::ffff:1.2.3.0/120'::cidr" + == "::ffff:1.2.3.0/120" + .parse::() + .unwrap(), +)); + #[cfg(feature = "ipnetwork")] test_type!(ipnetwork(Postgres, "'127.0.0.1'::inet" @@ -232,6 +264,15 @@ test_type!(bitvec( }, )); +#[cfg(feature = "ipnet")] +test_type!(ipnet_vec>(Postgres, + "'{127.0.0.1,8.8.8.8/24}'::inet[]" + == vec![ + "127.0.0.1/32".parse::().unwrap(), + "8.8.8.8/24".parse::().unwrap() + ] +)); + #[cfg(feature = "ipnetwork")] test_type!(ipnetwork_vec>(Postgres, "'{127.0.0.1,8.8.8.8/24}'::inet[]" diff --git a/tests/ui-tests.rs b/tests/ui-tests.rs index f74694b870..4a5ca240e1 100644 --- a/tests/ui-tests.rs +++ b/tests/ui-tests.rs @@ -17,7 +17,7 @@ fn ui_tests() { t.compile_fail("tests/ui/postgres/gated/uuid.rs"); } - if cfg!(not(feature = "ipnetwork")) { + if cfg!(not(feature = "ipnet")) && cfg!(not(feature = "ipnetwork")) { t.compile_fail("tests/ui/postgres/gated/ipnetwork.rs"); } } From f0be19e6404a734d4d56bcc91410419ad6831ce5 Mon Sep 17 00:00:00 2001 From: thriller08 <99699797+thriller08@users.noreply.github.com> Date: Sat, 29 Mar 2025 23:05:04 -0400 Subject: [PATCH 47/66] Enable json feature without db enabled (#3801) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fe2669794b..673afcae66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,7 +113,7 @@ sqlite-unbundled = ["_sqlite", "sqlx-sqlite/unbundled", "sqlx-macros?/sqlite-unb sqlite-preupdate-hook = ["sqlx-sqlite/preupdate-hook"] # types -json = ["sqlx-macros?/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sqlite?/json"] +json = ["sqlx-core/json", "sqlx-macros?/json", "sqlx-mysql?/json", "sqlx-postgres?/json", "sqlx-sqlite?/json"] bigdecimal = ["sqlx-core/bigdecimal", "sqlx-macros?/bigdecimal", "sqlx-mysql?/bigdecimal", "sqlx-postgres?/bigdecimal"] bit-vec = ["sqlx-core/bit-vec", "sqlx-macros?/bit-vec", "sqlx-postgres?/bit-vec"] From 082aed5c2b6e68172bf29c377c3f5c87ca17cde4 Mon Sep 17 00:00:00 2001 From: TeCHiScy <741195+TeCHiScy@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:41:28 +0800 Subject: [PATCH 48/66] Fix error message typo in PgPoint::from_str (#3811) --- sqlx-postgres/src/types/geometry/point.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-postgres/src/types/geometry/point.rs b/sqlx-postgres/src/types/geometry/point.rs index 83b7c24d0d..5078ce1ee4 100644 --- a/sqlx-postgres/src/types/geometry/point.rs +++ b/sqlx-postgres/src/types/geometry/point.rs @@ -77,7 +77,7 @@ impl FromStr for PgPoint { .ok_or_else(|| format!("error decoding POINT: could not get x and y from {}", s))?; let x = parse_float_from_str(x_str, "error decoding POINT: could not get x")?; - let y = parse_float_from_str(y_str, "error decoding POINT: could not get x")?; + let y = parse_float_from_str(y_str, "error decoding POINT: could not get y")?; Ok(PgPoint { x, y }) } From e283bf9645713985f4a6c37b80b3fb5d9c07e087 Mon Sep 17 00:00:00 2001 From: Vladimir Petrzhikovskii Date: Fri, 4 Apr 2025 15:47:20 +0200 Subject: [PATCH 49/66] mysql: Fix panic on invalid text row length field Previously, `TextRow::decode_with` would read a length-encoded field size and attempt to advance the buffer by that amount. If the server sent a malformed packet containing a length value larger than the remaining data in the buffer, the call to `buf.advance(size)` would panic. eg: ``` thread 'main' panicked at /home/user/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bytes-1.10.1/src/bytes.rs:711:9: cannot advance past remaining: 8590116092 <= 0 stack backtrace: 0: 0x56119b657e00 - ::fmt::h6d42cc84fc840290 1: 0x56119b67edd3 - core::fmt::write::h5af61a909e3ec64d 2: 0x56119b653ee3 - std::io::Write::write_fmt::h5a7b54aa6e4a315d 3: 0x56119b657c52 - std::sys::backtrace::BacktraceLock::print::h555579e7396c26ac 4: 0x56119b658cef - std::panicking::default_hook::{{closure}}::h9128866118196224 5: 0x56119b658b5a - std::panicking::default_hook::h52e9e7314e0255f6 6: 0x56119b659712 - std::panicking::rust_panic_with_hook::h541791bcc774ef34 7: 0x56119b65949a - std::panicking::begin_panic_handler::{{closure}}::h6479a2f0137c7d19 8: 0x56119b658319 - std::sys::backtrace::__rust_end_short_backtrace::ha04e7c0fc61ded91 9: 0x56119b65912d - rust_begin_unwind 10: 0x56119b67c390 - core::panicking::panic_fmt::h5764ee7030b7a73d 11: 0x56119b572b18 - >::decode_with::h17ac8b44140b5469 12: 0x56119b42df1f - sqlx_mysql::connection::executor::::run::{{closure}}::{{closure}}::{{closure}}::h4874a0f73925d55a ``` This commit introduces a bounds check immediately after reading the field length from a packet. This panic condition was specifically observed when executing a TiDB `BATCH ON ... DELETE` statement via `pool.execute()`. It sends an OK packet immediately followed by a full result set describing the batch status (column defs, row data, EOF). Observed TiDB response sequence for `BATCH DML` via `COM_QUERY`: 1. OK Packet (seq=1, `SERVER_MORE_RESULTS_EXISTS` = false) 2. Column Count Packet (seq=1, non-standard, protocol violation) 3. Column Definition Packet (seq=2) 4. Column Definition Packet (seq=3) 5. Text Row Data Packet (seq=4) 6. EOF Packet (seq=5, `SERVER_MORE_RESULTS_EXISTS` = false) This differs from standard MySQL DML response (OK/ERR packet only) and causes `sqlx` using `execute()` to attempt parsing the unexpected result set packets after the initial OK packet. --- sqlx-mysql/src/protocol/text/row.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sqlx-mysql/src/protocol/text/row.rs b/sqlx-mysql/src/protocol/text/row.rs index 0b81cd7f4f..e5f820c653 100644 --- a/sqlx-mysql/src/protocol/text/row.rs +++ b/sqlx-mysql/src/protocol/text/row.rs @@ -16,13 +16,20 @@ impl<'de> ProtocolDecode<'de, &'de [MySqlColumn]> for TextRow { let mut values = Vec::with_capacity(columns.len()); - for _ in columns { + for c in columns { if buf[0] == 0xfb { // NULL is sent as 0xfb values.push(None); buf.advance(1); } else { let size = buf.get_uint_lenenc(); + if (buf.remaining() as u64) < size { + return Err(err_protocol!( + "buffer exhausted when reading data for column {:?}; decoded length is {}, but only {} bytes remain in buffer. Malformed packet or protocol error?", + c, + size, + buf.remaining())); + } let size = usize::try_from(size) .map_err(|_| err_protocol!("TextRow length out of range: {size}"))?; From 42def59973139c7265d4d4ed302a0f701235479c Mon Sep 17 00:00:00 2001 From: Joey de Waal <99046430+joeydewaal@users.noreply.github.com> Date: Mon, 14 Apr 2025 02:02:14 +0200 Subject: [PATCH 50/66] fix(macros): cache macro metadata based on `CARGO_MANIFEST_DIR` (#3815) * fix(macros): cache macro metadata based on CARGO_MANIFEST_DIR * fix unrelated typo * bump dotenvy version * fix after review --- Cargo.toml | 2 +- sqlx-macros-core/src/query/data.rs | 4 +-- sqlx-macros-core/src/query/mod.rs | 43 ++++++++++++++++++++---------- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 673afcae66..008ec2de85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -154,7 +154,7 @@ time = { version = "0.3.36", features = ["formatting", "parsing", "macros"] } uuid = "1.1.2" # Common utility crates -dotenvy = { version = "0.15.0", default-features = false } +dotenvy = { version = "0.15.7", default-features = false } # Runtimes [workspace.dependencies.async-std] diff --git a/sqlx-macros-core/src/query/data.rs b/sqlx-macros-core/src/query/data.rs index 39f62bafdc..ddf55c8bb2 100644 --- a/sqlx-macros-core/src/query/data.rs +++ b/sqlx-macros-core/src/query/data.rs @@ -86,8 +86,8 @@ impl DynQueryData { let mut cache = OFFLINE_DATA_CACHE .lock() // Just reset the cache on error - .unwrap_or_else(|posion_err| { - let mut guard = posion_err.into_inner(); + .unwrap_or_else(|poison_err| { + let mut guard = poison_err.into_inner(); *guard = Default::default(); guard }); diff --git a/sqlx-macros-core/src/query/mod.rs b/sqlx-macros-core/src/query/mod.rs index 09acff9bd2..d2392e2d0e 100644 --- a/sqlx-macros-core/src/query/mod.rs +++ b/sqlx-macros-core/src/query/mod.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::{fs, io}; @@ -106,12 +107,12 @@ impl Metadata { } } +static METADATA: Lazy>> = Lazy::new(Default::default); + // If we are in a workspace, lookup `workspace_root` since `CARGO_MANIFEST_DIR` won't // reflect the workspace dir: https://github.com/rust-lang/cargo/issues/3946 -static METADATA: Lazy = Lazy::new(|| { - let manifest_dir: PathBuf = env("CARGO_MANIFEST_DIR") - .expect("`CARGO_MANIFEST_DIR` must be set") - .into(); +fn init_metadata(manifest_dir: &String) -> Metadata { + let manifest_dir: PathBuf = manifest_dir.into(); // If a .env file exists at CARGO_MANIFEST_DIR, load environment variables from this, // otherwise fallback to default dotenv behaviour. @@ -119,14 +120,15 @@ static METADATA: Lazy = Lazy::new(|| { #[cfg_attr(not(procmacro2_semver_exempt), allow(unused_variables))] let env_path = if env_path.exists() { - let res = dotenvy::from_path(&env_path); + // Load the new environment variables and override the old ones if necessary. + let res = dotenvy::from_path_override(&env_path); if let Err(e) = res { panic!("failed to load environment from {env_path:?}, {e}"); } Some(env_path) } else { - dotenvy::dotenv().ok() + dotenvy::dotenv_override().ok() }; // tell the compiler to watch the `.env` for changes, if applicable @@ -147,32 +149,46 @@ static METADATA: Lazy = Lazy::new(|| { database_url, workspace_root: Arc::new(Mutex::new(None)), } -}); +} pub fn expand_input<'a>( input: QueryMacroInput, drivers: impl IntoIterator, ) -> crate::Result { - let data_source = match &*METADATA { + let manifest_dir = env("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` must be set"); + + let mut metadata_lock = METADATA + .lock() + // Just reset the metadata on error + .unwrap_or_else(|poison_err| { + let mut guard = poison_err.into_inner(); + *guard = Default::default(); + guard + }); + + let metadata = metadata_lock + .entry(manifest_dir) + .or_insert_with_key(init_metadata); + + let data_source = match &metadata { Metadata { offline: false, database_url: Some(db_url), .. } => QueryDataSource::live(db_url)?, - Metadata { offline, .. } => { // Try load the cached query metadata file. let filename = format!("query-{}.json", hash_string(&input.sql)); // Check SQLX_OFFLINE_DIR, then local .sqlx, then workspace .sqlx. let dirs = [ - || env("SQLX_OFFLINE_DIR").ok().map(PathBuf::from), - || Some(METADATA.manifest_dir.join(".sqlx")), - || Some(METADATA.workspace_root().join(".sqlx")), + |_: &Metadata| env("SQLX_OFFLINE_DIR").ok().map(PathBuf::from), + |meta: &Metadata| Some(meta.manifest_dir.join(".sqlx")), + |meta: &Metadata| Some(meta.workspace_root().join(".sqlx")), ]; let Some(data_file_path) = dirs .iter() - .filter_map(|path| path()) + .filter_map(|path| path(metadata)) .map(|path| path.join(&filename)) .find(|path| path.exists()) else { @@ -184,7 +200,6 @@ pub fn expand_input<'a>( }.into() ); }; - QueryDataSource::Cached(DynQueryData::from_data_file(&data_file_path, &input.sql)?) } }; From 97bf270cafc6fa6402979125188c838e1572279c Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Mon, 14 Apr 2025 02:06:17 +0200 Subject: [PATCH 51/66] Always set SQLITE_OPEN_URI (#3289) --- sqlx-sqlite/src/connection/establish.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sqlx-sqlite/src/connection/establish.rs b/sqlx-sqlite/src/connection/establish.rs index c5d2450fba..545bad747c 100644 --- a/sqlx-sqlite/src/connection/establish.rs +++ b/sqlx-sqlite/src/connection/establish.rs @@ -8,6 +8,7 @@ use libsqlite3_sys::{ sqlite3_load_extension, sqlite3_open_v2, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, SQLITE_OK, SQLITE_OPEN_CREATE, SQLITE_OPEN_FULLMUTEX, SQLITE_OPEN_MEMORY, SQLITE_OPEN_NOMUTEX, SQLITE_OPEN_PRIVATECACHE, SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_SHAREDCACHE, + SQLITE_OPEN_URI, }; use percent_encoding::NON_ALPHANUMERIC; use sqlx_core::IndexMap; @@ -67,11 +68,14 @@ impl EstablishParams { })? .to_owned(); + // Set common flags we expect to have in sqlite + let mut flags = SQLITE_OPEN_URI; + // By default, we connect to an in-memory database. // [SQLITE_OPEN_NOMUTEX] will instruct [sqlite3_open_v2] to return an error if it // cannot satisfy our wish for a thread-safe, lock-free connection object - let mut flags = if options.serialized { + flags |= if options.serialized { SQLITE_OPEN_FULLMUTEX } else { SQLITE_OPEN_NOMUTEX @@ -111,7 +115,6 @@ impl EstablishParams { percent_encoding::percent_encode(filename.as_bytes(), NON_ALPHANUMERIC), serde_urlencoded::to_string(&query_params).unwrap() ); - flags |= libsqlite3_sys::SQLITE_OPEN_URI; } let filename = CString::new(filename).map_err(|_| { From 154878547e7e78c42bf97a34dccd745edbce2ef3 Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Mon, 14 Apr 2025 00:21:15 +0000 Subject: [PATCH 52/66] fix: PgConnectOptions docs (#3809) --- sqlx-postgres/src/options/mod.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index a0b222606a..4951c8ac05 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -134,6 +134,19 @@ impl PgConnectOptions { /// # use sqlx_postgres::PgConnectOptions; /// let options = PgConnectOptions::new(); /// ``` + /// + /// Note: that unlike `libpq` the environment variables: + /// + /// * `PGSSLROOTCERT` + /// * `PGSSLCERT` + /// * `PGSSLKEY` + /// + /// Must not exclusively be path, ´sqlx-postgres` supports these variables + /// encode the certificates / keys directly. Content snooping is done via + /// `CertificateInput::from`. + /// + /// Note: Putting key material in environment variables can be subjected to risk as on + /// some platforms environment variables can be recovered by other (non root) users. pub fn new() -> Self { Self::new_without_pgpass().apply_pgpass() } From f9084035d763b541c31d8d69f543583c4bc68f0f Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Sun, 13 Apr 2025 21:55:14 -0700 Subject: [PATCH 53/66] 0.8.4 release (#3819) * chore: prepare 0.8.4 release * fix(postgres): send `limit: 0` for all `Execute` messages fixes #3673 * fix: let `CertificateInput::from` infer any PEM-encoded document https://github.com/launchbadge/sqlx/pull/3809#issuecomment-2800293813 * doc: improve documentation of `PgConnectOptions` fixes #3740 * chore: update CHANGELOG from PR --- CHANGELOG.md | 152 +++++++++++++++++++ Cargo.lock | 16 +- Cargo.toml | 16 +- sqlx-core/src/net/tls/mod.rs | 9 +- sqlx-postgres/src/any.rs | 4 +- sqlx-postgres/src/connection/executor.rs | 9 +- sqlx-postgres/src/connection/mod.rs | 2 + sqlx-postgres/src/options/doc.md | 185 +++++++++++++++++++++++ sqlx-postgres/src/options/mod.rs | 124 ++------------- 9 files changed, 383 insertions(+), 134 deletions(-) create mode 100644 sqlx-postgres/src/options/doc.md diff --git a/CHANGELOG.md b/CHANGELOG.md index c190be4af2..6e2603a808 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,134 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.8.4 - 2025-04-13 + +50 pull requests were merged this release cycle. + +As of this release, development of `0.9.0` has begun on `main`. +Barring urgent hotfixes, this is expected to be the last release of `0.8.x`. + +### Added +* [[#3603]]: Added missing special casing for encoding embedded arrays of custom types [[@nico-incubiq]] +* [[#3625]]: feat(sqlite): add preupdate hook [[@aschey]] +* [[#3655]]: docs: add example for postgres enums with type TEXT [[@tisonkun]] +* [[#3677]]: Add json(nullable) macro attribute [[@seanaye]] +* [[#3687]]: Derive clone and debug for postgresql arguments [[@remysaissy]] +* [[#3690]]: feat: add postres geometry line segment [[@jayy-lmao]] +* [[#3707]]: feat(Sqlite): add LockedSqliteHandle::last_error [[@joeydewaal]] +* [[#3710]]: feat: add ipnet support [[@BeauGieskens]] +* [[#3711]]: feat(postgres): add geometry box [[@jayy-lmao]] +* [[#3714]]: chore: expose bstr feature [[@joeydewaal]] +* [[#3716]]: feat(postgres): add geometry path [[@jayy-lmao]] +* [[#3724]]: feat(sqlx-cli): Add flag to disable automatic loading of .env files [[@benwilber]] +* [[#3734]]: QueryBuilder: add debug_assert when `push_values` is passed an empty set of tuples [[@chanmaoganda]] +* [[#3745]]: feat: sqlx sqlite expose de/serialize [[@mattrighetti]] +* [[#3765]]: Merge of #3427 (by @mpyw) and #3614 (by @bonsairobo) [[@abonander]] + * [[#3427]] Expose `transaction_depth` through `get_transaction_depth()` method [[@mpyw]] + * Changed to `Connection::is_in_transaction` in [[#3765]] + * [[#3614]] Add `begin_with` methods to support database-specific transaction options [[@bonsairobo]] +* [[#3769]]: feat(postgres): add geometry polygon [[@jayy-lmao]] +* [[#3773]]: feat(postgres): add geometry circle [[@jayy-lmao]] + +### Changed +* [[#3665]]: build(deps): bump semver compatible dependencies [[@paolobarbolini]] +* [[#3669]]: refactor(cli): replace promptly with dialoguer [[@paolobarbolini]] +* [[#3672]]: add `#[track_caller]` to `Row::get()` [[@karambarakat]] +* [[#3708]]: chore(MySql): Remove unnecessary box [[@joeydewaal]] +* [[#3715]]: chore: add pg_copy regression tests [[@joeydewaal]] +* [[#3721]]: Replace some `futures-core` / `futures-util` APIs with `std` variants [[@paolobarbolini]] +* [[#3725]]: chore: replace rustls-pemfile with rustls-pki-types [[@tottoto]] +* [[#3754]]: chore(cli): remove unused async-trait crate from dependencies [[@tottoto]] +* [[#3762]]: docs(pool): recommend actix-web ThinData over Data to avoid two Arcs [[@jonasmalacofilho]] + +### Fixed +* [[#3289]]: Always set `SQLITE_OPEN_URI` on in-memory sqlite [[@LecrisUT]] +* [[#3334]]: Fix: nextest cleanup race condition [[@bonega]] +* [[#3666]]: fix(cli): running tests on 32bit platforms [[@paolobarbolini]] +* [[#3686]]: fix: handle nullable values by printing NULL instead of panicking [[@joeydewaal]] +* [[#3700]]: fix(Sqlite): stop sending rows after first error [[@joeydewaal]] +* [[#3701]]: fix(postgres) use signed int for length prefix in `PgCopyIn` [[@joeydewaal]] +* [[#3703]]: fix(Postgres) chunk pg_copy data [[@joeydewaal]] +* [[#3712]]: FromRow: Fix documentation order [[@Turbo87]] +* [[#3720]]: Fix readme: uuid feature is gating for all repos [[@jthacker]] +* [[#3728]]: postgres: Fix tracing span when dropping PgListener [[@chitoku-k]] +* [[#3741]]: Fix example calculation in docs [[@dns2utf8]] +* [[#3749]]: docs: add some missing backticks [[@soulwa]] +* [[#3753]]: Avoid privilege requirements by using an advisory lock in test setup (postgres). [[@kildrens]] +* [[#3755]]: Fix FromRow docs for tuples [[@xvapx]] +* [[#3768]]: chore(Sqlite): remove ci.db from repo [[@joeydewaal]] +* [[#3771]]: fix(ci): breakage from Rustup 1.28 [[@abonander]] +* [[#3786]]: Fix a copy-paste error on get_username docs [[@sulami]] +* [[#3801]]: Fix: Enable Json type when db feature isn't enabled [[@thriller08]] +* [[#3809]]: fix: PgConnectOptions docs [[@mbj]] +* [[#3811]]: Fix error message typo in PgPoint::from_str [[@TeCHiScy]] +* [[#3812]]: mysql: Fix panic on invalid text row length field [[@0xdeafbeef]] +* [[#3815]]: fix(macros): cache macro metadata based on `CARGO_MANIFEST_DIR` [[@joeydewaal]] +* Fixes in release PR [[#3819]] [[@abonander]]: + * fix(postgres): send `limit: 0` for all `Execute` messages + * Addresses [[#3673]]: Parallel workers not used on Postgres + * fix: let `CertificateInput::from` infer any PEM-encoded document + * Fixes `PGSSLKEY` not being parsed correctly when containing a PEM-encoded private key. + * doc: improve documentation of `PgConnectOptions` + * `PGHOSTADDR` now can be used to override `PGHOST`. + * Addresses [[#3740]]: Document the URL syntax for Unix-domain sockets when connecting to postgres + +[#3819]: https://github.com/launchbadge/sqlx/pull/3819 + +[#3673]: https://github.com/launchbadge/sqlx/issues/3673 +[#3740]: https://github.com/launchbadge/sqlx/issues/3740 + +[#3289]: https://github.com/launchbadge/sqlx/pull/3289 +[#3334]: https://github.com/launchbadge/sqlx/pull/3334 +[#3427]: https://github.com/launchbadge/sqlx/pull/3427 +[#3603]: https://github.com/launchbadge/sqlx/pull/3603 +[#3614]: https://github.com/launchbadge/sqlx/pull/3614 +[#3625]: https://github.com/launchbadge/sqlx/pull/3625 +[#3655]: https://github.com/launchbadge/sqlx/pull/3655 +[#3665]: https://github.com/launchbadge/sqlx/pull/3665 +[#3666]: https://github.com/launchbadge/sqlx/pull/3666 +[#3669]: https://github.com/launchbadge/sqlx/pull/3669 +[#3672]: https://github.com/launchbadge/sqlx/pull/3672 +[#3677]: https://github.com/launchbadge/sqlx/pull/3677 +[#3686]: https://github.com/launchbadge/sqlx/pull/3686 +[#3687]: https://github.com/launchbadge/sqlx/pull/3687 +[#3690]: https://github.com/launchbadge/sqlx/pull/3690 +[#3700]: https://github.com/launchbadge/sqlx/pull/3700 +[#3701]: https://github.com/launchbadge/sqlx/pull/3701 +[#3703]: https://github.com/launchbadge/sqlx/pull/3703 +[#3707]: https://github.com/launchbadge/sqlx/pull/3707 +[#3708]: https://github.com/launchbadge/sqlx/pull/3708 +[#3710]: https://github.com/launchbadge/sqlx/pull/3710 +[#3711]: https://github.com/launchbadge/sqlx/pull/3711 +[#3712]: https://github.com/launchbadge/sqlx/pull/3712 +[#3714]: https://github.com/launchbadge/sqlx/pull/3714 +[#3715]: https://github.com/launchbadge/sqlx/pull/3715 +[#3716]: https://github.com/launchbadge/sqlx/pull/3716 +[#3720]: https://github.com/launchbadge/sqlx/pull/3720 +[#3721]: https://github.com/launchbadge/sqlx/pull/3721 +[#3724]: https://github.com/launchbadge/sqlx/pull/3724 +[#3725]: https://github.com/launchbadge/sqlx/pull/3725 +[#3728]: https://github.com/launchbadge/sqlx/pull/3728 +[#3734]: https://github.com/launchbadge/sqlx/pull/3734 +[#3741]: https://github.com/launchbadge/sqlx/pull/3741 +[#3745]: https://github.com/launchbadge/sqlx/pull/3745 +[#3749]: https://github.com/launchbadge/sqlx/pull/3749 +[#3753]: https://github.com/launchbadge/sqlx/pull/3753 +[#3754]: https://github.com/launchbadge/sqlx/pull/3754 +[#3755]: https://github.com/launchbadge/sqlx/pull/3755 +[#3762]: https://github.com/launchbadge/sqlx/pull/3762 +[#3765]: https://github.com/launchbadge/sqlx/pull/3765 +[#3768]: https://github.com/launchbadge/sqlx/pull/3768 +[#3769]: https://github.com/launchbadge/sqlx/pull/3769 +[#3771]: https://github.com/launchbadge/sqlx/pull/3771 +[#3773]: https://github.com/launchbadge/sqlx/pull/3773 +[#3786]: https://github.com/launchbadge/sqlx/pull/3786 +[#3801]: https://github.com/launchbadge/sqlx/pull/3801 +[#3809]: https://github.com/launchbadge/sqlx/pull/3809 +[#3811]: https://github.com/launchbadge/sqlx/pull/3811 +[#3812]: https://github.com/launchbadge/sqlx/pull/3812 +[#3815]: https://github.com/launchbadge/sqlx/pull/3815 + ## 0.8.3 - 2025-01-03 41 pull requests were merged this release cycle. @@ -2700,3 +2828,27 @@ Fix docs.rs build by enabling a runtime feature in the docs.rs metadata in `Carg [@hsivonen]: https://github.com/hsivonen [@andreweggleston]: https://github.com/andreweggleston [@Suficio]: https://github.com/Suficio +[@bonega]: https://github.com/bonega +[@nico-incubiq]: https://github.com/nico-incubiq +[@tisonkun]: https://github.com/tisonkun +[@karambarakat]: https://github.com/karambarakat +[@seanaye]: https://github.com/seanaye +[@remysaissy]: https://github.com/remysaissy +[@BeauGieskens]: https://github.com/BeauGieskens +[@Turbo87]: https://github.com/Turbo87 +[@jthacker]: https://github.com/jthacker +[@benwilber]: https://github.com/benwilber +[@chitoku-k]: https://github.com/chitoku-k +[@chanmaoganda]: https://github.com/chanmaoganda +[@dns2utf8]: https://github.com/dns2utf8 +[@mattrighetti]: https://github.com/mattrighetti +[@soulwa]: https://github.com/soulwa +[@kildrens]: https://github.com/kildrens +[@xvapx]: https://github.com/xvapx +[@jonasmalacofilho]: https://github.com/jonasmalacofilho +[@sulami]: https://github.com/sulami +[@thriller08]: https://github.com/thriller08 +[@mbj]: https://github.com/mbj +[@TeCHiScy]: https://github.com/TeCHiScy +[@mpyw]: https://github.com/mpyw +[@bonsairobo]: https://github.com/bonsairobo \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f1c4604c59..355cee90b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3374,7 +3374,7 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.3" +version = "0.8.4" dependencies = [ "anyhow", "async-std", @@ -3404,7 +3404,7 @@ dependencies = [ [[package]] name = "sqlx-cli" -version = "0.8.3" +version = "0.8.4" dependencies = [ "anyhow", "assert_cmd", @@ -3428,7 +3428,7 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.3" +version = "0.8.4" dependencies = [ "async-io 1.13.0", "async-std", @@ -3604,7 +3604,7 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.3" +version = "0.8.4" dependencies = [ "proc-macro2", "quote", @@ -3615,7 +3615,7 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.3" +version = "0.8.4" dependencies = [ "async-std", "dotenvy", @@ -3640,7 +3640,7 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.8.3" +version = "0.8.4" dependencies = [ "atoi", "base64 0.22.1", @@ -3686,7 +3686,7 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.3" +version = "0.8.4" dependencies = [ "atoi", "base64 0.22.1", @@ -3732,7 +3732,7 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.3" +version = "0.8.4" dependencies = [ "atoi", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 008ec2de85..09bdabf33b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.8.3" +version = "0.8.4" license = "MIT OR Apache-2.0" edition = "2021" repository = "https://github.com/launchbadge/sqlx" @@ -129,17 +129,17 @@ bstr = ["sqlx-core/bstr"] [workspace.dependencies] # Core Crates -sqlx-core = { version = "=0.8.3", path = "sqlx-core" } -sqlx-macros-core = { version = "=0.8.3", path = "sqlx-macros-core" } -sqlx-macros = { version = "=0.8.3", path = "sqlx-macros" } +sqlx-core = { version = "=0.8.4", path = "sqlx-core" } +sqlx-macros-core = { version = "=0.8.4", path = "sqlx-macros-core" } +sqlx-macros = { version = "=0.8.4", path = "sqlx-macros" } # Driver crates -sqlx-mysql = { version = "=0.8.3", path = "sqlx-mysql" } -sqlx-postgres = { version = "=0.8.3", path = "sqlx-postgres" } -sqlx-sqlite = { version = "=0.8.3", path = "sqlx-sqlite" } +sqlx-mysql = { version = "=0.8.4", path = "sqlx-mysql" } +sqlx-postgres = { version = "=0.8.4", path = "sqlx-postgres" } +sqlx-sqlite = { version = "=0.8.4", path = "sqlx-sqlite" } # Facade crate (for reference from sqlx-cli) -sqlx = { version = "=0.8.3", path = ".", default-features = false } +sqlx = { version = "=0.8.4", path = ".", default-features = false } # Common type integrations shared by multiple driver crates. # These are optional unless enabled in a workspace crate. diff --git a/sqlx-core/src/net/tls/mod.rs b/sqlx-core/src/net/tls/mod.rs index 3e9fd9b9a0..7bb1744189 100644 --- a/sqlx-core/src/net/tls/mod.rs +++ b/sqlx-core/src/net/tls/mod.rs @@ -25,11 +25,12 @@ pub enum CertificateInput { impl From for CertificateInput { fn from(value: String) -> Self { + // Leading and trailing whitespace/newlines let trimmed = value.trim(); - // Some heuristics according to https://tools.ietf.org/html/rfc7468 - if trimmed.starts_with("-----BEGIN CERTIFICATE-----") - && trimmed.contains("-----END CERTIFICATE-----") - { + + // Heuristic for PEM encoded inputs: + // https://tools.ietf.org/html/rfc7468 + if trimmed.starts_with("-----BEGIN") && trimmed.ends_with("-----") { CertificateInput::Inline(value.as_bytes().to_vec()) } else { CertificateInput::File(PathBuf::from(value)) diff --git a/sqlx-postgres/src/any.rs b/sqlx-postgres/src/any.rs index 762f53e5df..e5b8a36627 100644 --- a/sqlx-postgres/src/any.rs +++ b/sqlx-postgres/src/any.rs @@ -97,7 +97,7 @@ impl AnyConnectionBackend for PgConnection { }; Box::pin( - self.run(query, arguments, 0, persistent, None) + self.run(query, arguments, persistent, None) .try_flatten_stream() .map( move |res: sqlx_core::Result>| match res? { @@ -123,7 +123,7 @@ impl AnyConnectionBackend for PgConnection { Box::pin(async move { let arguments = arguments?; - let mut stream = pin!(self.run(query, arguments, 1, persistent, None).await?); + let mut stream = pin!(self.run(query, arguments, persistent, None).await?); if let Some(Either::Right(row)) = stream.try_next().await? { return Ok(Some(AnyRow::try_from(&row)?)); diff --git a/sqlx-postgres/src/connection/executor.rs b/sqlx-postgres/src/connection/executor.rs index 076c4209f6..cd3a876520 100644 --- a/sqlx-postgres/src/connection/executor.rs +++ b/sqlx-postgres/src/connection/executor.rs @@ -194,7 +194,6 @@ impl PgConnection { &'c mut self, query: &'q str, arguments: Option, - limit: u8, persistent: bool, metadata_opt: Option>, ) -> Result, Error>> + 'e, Error> { @@ -247,7 +246,9 @@ impl PgConnection { // the protocol-level limit acts nearly identically to the `LIMIT` in SQL self.inner.stream.write_msg(message::Execute { portal: PortalId::UNNAMED, - limit: limit.into(), + // Non-zero limits cause query plan pessimization by disabling parallel workers: + // https://github.com/launchbadge/sqlx/issues/3673 + limit: 0, })?; // From https://www.postgresql.org/docs/current/protocol-flow.html: // @@ -393,7 +394,7 @@ impl<'c> Executor<'c> for &'c mut PgConnection { Box::pin(try_stream! { let arguments = arguments?; - let mut s = pin!(self.run(sql, arguments, 0, persistent, metadata).await?); + let mut s = pin!(self.run(sql, arguments, persistent, metadata).await?); while let Some(v) = s.try_next().await? { r#yield!(v); @@ -419,7 +420,7 @@ impl<'c> Executor<'c> for &'c mut PgConnection { Box::pin(async move { let arguments = arguments?; - let mut s = pin!(self.run(sql, arguments, 1, persistent, metadata).await?); + let mut s = pin!(self.run(sql, arguments, persistent, metadata).await?); // With deferred constraints we need to check all responses as we // could get a OK response (with uncommitted data), only to get an diff --git a/sqlx-postgres/src/connection/mod.rs b/sqlx-postgres/src/connection/mod.rs index 96e3e2fe12..ce499ed744 100644 --- a/sqlx-postgres/src/connection/mod.rs +++ b/sqlx-postgres/src/connection/mod.rs @@ -31,6 +31,8 @@ mod stream; mod tls; /// A connection to a PostgreSQL database. +/// +/// See [`PgConnectOptions`] for connection URL reference. pub struct PgConnection { pub(crate) inner: Box, } diff --git a/sqlx-postgres/src/options/doc.md b/sqlx-postgres/src/options/doc.md new file mode 100644 index 0000000000..15c2459c81 --- /dev/null +++ b/sqlx-postgres/src/options/doc.md @@ -0,0 +1,185 @@ +Options and flags which can be used to configure a PostgreSQL connection. + +A value of `PgConnectOptions` can be parsed from a connection URL, +as described by [libpq][libpq-connstring]. + +The general form for a connection URL is: + +```text +postgresql://[user[:password]@][host][:port][/dbname][?param1=value1&...] +``` + +The URL scheme designator can be either `postgresql://` or `postgres://`. +Each of the URL parts is optional. For defaults, see the next section. + +This type also implements [`FromStr`][std::str::FromStr] so you can parse it from a string +containing a connection URL and then further adjust options if necessary (see example below). + +Note that characters not allowed in URLs must be [percent-encoded]. + +# Parameters + +This API accepts many of the same parameters as [libpq][libpq-params]; +if a parameter is not passed in via URL, it is populated by reading +[environment variables][libpq-envars] or choosing customary defaults. + +| Parameter | Environment Variable | Default / Remarks | +|--------------------|----------------------|-------------------------------------------------------------| +| `user` | `PGUSER` | The `whoami` of the currently running process. | +| `password` | `PGPASSWORD` | Read from [`passfile`], if it exists. | +| [`passfile`] | `PGPASSFILE` | `~/.pgpass` or `%APPDATA%\postgresql\pgpass.conf` (Windows) | +| `host` | `PGHOST` | See [Note: Default Host](#note-default-host). | +| `hostaddr` | `PGHOSTADDR` | See [Note: Default Host](#note-default-host). | +| `port` | `PGPORT` | `5432` | +| `dbname` | `PGDATABASE` | Unset; defaults to the username server-side. | +| `sslmode` | `PGSSLMODE` | `prefer`. See [`PgSslMode`] for details. | +| `sslrootcert` | `PGSSLROOTCERT` | Unset. See [Note: SSL](#note-ssl). | +| `sslcert` | `PGSSLCERT` | Unset. See [Note: SSL](#note-ssl). | +| `sslkey` | `PGSSLKEY` | Unset. See [Note: SSL](#note-ssl). | +| `options` | `PGOPTIONS` | Unset. | +| `application_name` | `PGAPPNAME` | Unset. | + +[`passfile`] handling may be bypassed using [`PgConnectOptions::new_without_pgpass()`]. + +## SQLx-Specific +SQLx also parses some bespoke parameters. These are _not_ configurable by environment variable. +Instead, the name is linked to the method to set the value. + +| Parameter | Default | +|--------------------------------------------------------------|-------------------------------| +| [`statement-cache-capacity`][Self::statement_cache_capacity] | `100` | + +# Example URLs +```text +postgresql:// +postgresql://:5433 +postgresql://localhost +postgresql://localhost:5433 +postgresql://localhost/mydb +postgresql://user@localhost +postgresql://user:secret@localhost +postgresql://user:correct%20horse%20battery%20staple@localhost +postgresql://localhost?dbname=mydb&user=postgres&password=postgres +``` + +See also [Note: Unix Domain Sockets](#note-unix-domain-sockets) below. + +# Note: Default Host +If the connection URL does not contain a hostname and `PGHOST` is not set, +this constructor looks for an open Unix domain socket in one of a few standard locations +(configured when Postgres is built): + +* `/var/run/postgresql/.s.PGSQL.{port}` (Debian) +* `/private/tmp/.s.PGSQL.{port}` (macOS when installed through Homebrew) +* `/tmp/.s.PGSQL.{port}` (default otherwise) + +This depends on the value of `port` being correct. +If Postgres is using a port other than the default (`5432`), `port` must be set. + +If no Unix domain socket is found, `localhost` is assumed. + +Note: this description is updated on a best-effort basis. +See `default_host()` in the same source file as this method for the current behavior. + +# Note: SSL +## Root Certs +If `sslrootcert` is not set, the default root certificates used depends on Cargo features: + +* If `tls-native-tls` is enabled, the system root certificates are used. +* If `tls-rustls-native-roots` is enabled, the system root certificates are used. +* Otherwise, TLS roots are populated using the [`webpki-roots`] crate. + +## Environment Variables +Unlike with `libpq`, the following environment variables may be _either_ +a path to a file _or_ a string value containing a [PEM-encoded value][rfc7468]: + +* `PGSSLROOTCERT` +* `PGSSLCERT` +* `PGSSLKEY` + +If the string begins with the standard `-----BEGIN -----` header +and ends with the standard `-----END -----` footer, +it is parsed directly. + +This behavior is _only_ implemented for the environment variables, not the URL parameters. + +Note: passing the SSL private key via environment variable may be a security risk. + +# Note: Unix Domain Sockets +If you want to connect to Postgres over a Unix domain socket, you can pass the path +to the _directory_ containing the socket as the `host` parameter. + +The final path to the socket will be `{host}/.s.PGSQL.{port}` as is standard for Postgres. + +If you're passing the domain socket path as the host segment of the URL, forward slashes +in the path must be [percent-encoded] (replacing `/` with `%2F`), e.g.: + +```text +postgres://%2Fvar%2Frun%2Fpostgresql/dbname + +Different port: +postgres://%2Fvar%2Frun%2Fpostgresql:5433/dbname + +With username and password: +postgres://user:password@%2Fvar%2Frun%2Fpostgresql/dbname + +With username and password, and different port: +postgres://user:password@%2Fvar%2Frun%2Fpostgresql:5432/dbname +``` + +Instead, the hostname can be passed in the query segment of the URL, +which does not require forward-slashes to be percent-encoded +(however, [other characters are][percent-encoded]): + +```text +postgres:dbname?host=/var/run/postgresql + +Different port: +postgres://:5433/dbname?host=/var/run/postgresql + +With username and password: +postgres://user:password@/dbname?host=/var/run/postgresql + +With username and password, and different port: +postgres://user:password@:5433/dbname?host=/var/run/postgresql +``` + +# Example + +```rust,no_run +use sqlx::{Connection, ConnectOptions}; +use sqlx::postgres::{PgConnectOptions, PgConnection, PgPool, PgSslMode}; + +# async fn example() -> sqlx::Result<()> { +// URL connection string +let conn = PgConnection::connect("postgres://localhost/mydb").await?; + +// Manually-constructed options +let conn = PgConnectOptions::new() + .host("secret-host") + .port(2525) + .username("secret-user") + .password("secret-password") + .ssl_mode(PgSslMode::Require) + .connect() + .await?; + +// Modifying options parsed from a string +let mut opts: PgConnectOptions = "postgres://localhost/mydb".parse()?; + +// Change the log verbosity level for queries. +// Information about SQL queries is logged at `DEBUG` level by default. +opts = opts.log_statements(log::LevelFilter::Trace); + +let pool = PgPool::connect_with(opts).await?; +# Ok(()) +# } +``` + +[percent-encoded]: https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding +[`passfile`]: https://www.postgresql.org/docs/current/libpq-pgpass.html +[libpq-connstring]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING +[libpq-params]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS +[libpq-envars]: https://www.postgresql.org/docs/current/libpq-envars.html +[rfc7468]: https://datatracker.ietf.org/doc/html/rfc7468 +[`webpki-roots`]: https://docs.rs/webpki-roots \ No newline at end of file diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index 4951c8ac05..723721a97c 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -12,80 +12,7 @@ mod parse; mod pgpass; mod ssl_mode; -/// Options and flags which can be used to configure a PostgreSQL connection. -/// -/// A value of `PgConnectOptions` can be parsed from a connection URL, -/// as described by [libpq](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). -/// -/// The general form for a connection URL is: -/// -/// ```text -/// postgresql://[user[:password]@][host][:port][/dbname][?param1=value1&...] -/// ``` -/// -/// This type also implements [`FromStr`][std::str::FromStr] so you can parse it from a string -/// containing a connection URL and then further adjust options if necessary (see example below). -/// -/// ## Parameters -/// -/// |Parameter|Default|Description| -/// |---------|-------|-----------| -/// | `sslmode` | `prefer` | Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated. See [`PgSslMode`]. | -/// | `sslrootcert` | `None` | Sets the name of a file containing a list of trusted SSL Certificate Authorities. | -/// | `statement-cache-capacity` | `100` | The maximum number of prepared statements stored in the cache. Set to `0` to disable. | -/// | `host` | `None` | Path to the directory containing a PostgreSQL unix domain socket, which will be used instead of TCP if set. | -/// | `hostaddr` | `None` | Same as `host`, but only accepts IP addresses. | -/// | `application-name` | `None` | The name will be displayed in the pg_stat_activity view and included in CSV log entries. | -/// | `user` | result of `whoami` | PostgreSQL user name to connect as. | -/// | `password` | `None` | Password to be used if the server demands password authentication. | -/// | `port` | `5432` | Port number to connect to at the server host, or socket file name extension for Unix-domain connections. | -/// | `dbname` | `None` | The database name. | -/// | `options` | `None` | The runtime parameters to send to the server at connection start. | -/// -/// The URL scheme designator can be either `postgresql://` or `postgres://`. -/// Each of the URL parts is optional. -/// -/// ```text -/// postgresql:// -/// postgresql://localhost -/// postgresql://localhost:5433 -/// postgresql://localhost/mydb -/// postgresql://user@localhost -/// postgresql://user:secret@localhost -/// postgresql://localhost?dbname=mydb&user=postgres&password=postgres -/// ``` -/// -/// # Example -/// -/// ```rust,no_run -/// use sqlx::{Connection, ConnectOptions}; -/// use sqlx::postgres::{PgConnectOptions, PgConnection, PgPool, PgSslMode}; -/// -/// # async fn example() -> sqlx::Result<()> { -/// // URL connection string -/// let conn = PgConnection::connect("postgres://localhost/mydb").await?; -/// -/// // Manually-constructed options -/// let conn = PgConnectOptions::new() -/// .host("secret-host") -/// .port(2525) -/// .username("secret-user") -/// .password("secret-password") -/// .ssl_mode(PgSslMode::Require) -/// .connect() -/// .await?; -/// -/// // Modifying options parsed from a string -/// let mut opts: PgConnectOptions = "postgres://localhost/mydb".parse()?; -/// -/// // Change the log verbosity level for queries. -/// // Information about SQL queries is logged at `DEBUG` level by default. -/// opts = opts.log_statements(log::LevelFilter::Trace); -/// -/// let pool = PgPool::connect_with(opts).await?; -/// # Ok(()) -/// # } -/// ``` +#[doc = include_str!("doc.md")] #[derive(Debug, Clone)] pub struct PgConnectOptions { pub(crate) host: String, @@ -112,52 +39,30 @@ impl Default for PgConnectOptions { } impl PgConnectOptions { - /// Creates a new, default set of options ready for configuration. - /// - /// By default, this reads the following environment variables and sets their - /// equivalent options. - /// - /// * `PGHOST` - /// * `PGPORT` - /// * `PGUSER` - /// * `PGPASSWORD` - /// * `PGDATABASE` - /// * `PGSSLROOTCERT` - /// * `PGSSLCERT` - /// * `PGSSLKEY` - /// * `PGSSLMODE` - /// * `PGAPPNAME` + /// Create a default set of connection options populated from the current environment. /// - /// # Example - /// - /// ```rust - /// # use sqlx_postgres::PgConnectOptions; - /// let options = PgConnectOptions::new(); - /// ``` + /// This behaves as if parsed from the connection string `postgres://` /// - /// Note: that unlike `libpq` the environment variables: - /// - /// * `PGSSLROOTCERT` - /// * `PGSSLCERT` - /// * `PGSSLKEY` - /// - /// Must not exclusively be path, ´sqlx-postgres` supports these variables - /// encode the certificates / keys directly. Content snooping is done via - /// `CertificateInput::from`. - /// - /// Note: Putting key material in environment variables can be subjected to risk as on - /// some platforms environment variables can be recovered by other (non root) users. + /// See the type-level documentation for details. pub fn new() -> Self { Self::new_without_pgpass().apply_pgpass() } + /// Create a default set of connection options _without_ reading from `passfile`. + /// + /// Equivalent to [`PgConnectOptions::new()`] but `passfile` is ignored. + /// + /// See the type-level documentation for details. pub fn new_without_pgpass() -> Self { let port = var("PGPORT") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(5432); - let host = var("PGHOST").ok().unwrap_or_else(|| default_host(port)); + let host = var("PGHOSTADDR") + .ok() + .or_else(|| var("PGHOST").ok()) + .unwrap_or_else(|| default_host(port)); let username = var("PGUSER").ok().unwrap_or_else(whoami::username); @@ -172,6 +77,9 @@ impl PgConnectOptions { database, ssl_root_cert: var("PGSSLROOTCERT").ok().map(CertificateInput::from), ssl_client_cert: var("PGSSLCERT").ok().map(CertificateInput::from), + // As of writing, the implementation of `From` only looks for + // `-----BEGIN CERTIFICATE-----` and so will not attempt to parse + // a PEM-encoded private key. ssl_client_key: var("PGSSLKEY").ok().map(CertificateInput::from), ssl_mode: var("PGSSLMODE") .ok() From e7236881a1d618835d074481e45306a8ddc5a916 Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Tue, 15 Apr 2025 15:11:07 -0700 Subject: [PATCH 54/66] Hotfix 0.8.5 (#3824) * fix(cli): correctly handle `.env` files again * feat(ci): add functionality tests for sqlx-cli (MySQL) * feat(ci): add functionality tests for sqlx-cli (Postgres) * feat(ci): add functionality tests for sqlx-cli (SQLite) * chore: prepare 0.8.5 release * feat(ci): run `test-attr` tests twice to catch #3825 * fix: correct bugs in MySQL implementation of `#[sqlx::test]` --- .github/workflows/sqlx-cli.yml | 233 ++++++++++++++++++++++++++++++++- .github/workflows/sqlx.yml | 76 +++++++++-- CHANGELOG.md | 22 ++++ Cargo.lock | 16 +-- Cargo.toml | 16 +-- sqlx-cli/src/bin/cargo-sqlx.rs | 6 +- sqlx-cli/src/bin/sqlx.rs | 7 +- sqlx-cli/src/lib.rs | 9 ++ sqlx-cli/src/opt.rs | 20 ++- sqlx-mysql/src/testing/mod.rs | 82 ++++++++++-- tests/mysql/test-attr.rs | 6 +- 11 files changed, 435 insertions(+), 58 deletions(-) diff --git a/.github/workflows/sqlx-cli.yml b/.github/workflows/sqlx-cli.yml index 2250e0bfcb..8ec5db63d7 100644 --- a/.github/workflows/sqlx-cli.yml +++ b/.github/workflows/sqlx-cli.yml @@ -33,8 +33,8 @@ jobs: --manifest-path sqlx-cli/Cargo.toml --target-dir target/beta/ - test: - name: Test + integration-test: + name: Integration Test runs-on: ${{ matrix.os }} strategy: @@ -57,6 +57,235 @@ jobs: - run: cargo test --manifest-path sqlx-cli/Cargo.toml + test-mysql: + name: Functional Test (MySQL) + runs-on: ubuntu-latest + # Deliberately not using `tests/docker-compose.yml` because that sets up the database automatically. + services: + mysql: + image: mysql:8 + ports: + - 3306:3306 + env: + MYSQL_ROOT_PASSWORD: password + env: + BASE_URL: mysql://root:password@localhost + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + + - name: Install SQLx-CLI + run: + cargo install --locked --debug --path sqlx-cli + + - name: Basic Test + env: + DATABASE_URL: ${{ env.BASE_URL }}/test1 + run: | + sqlx db setup --source=tests/mysql/migrations + + sqlx mig info --source=tests/mysql/migrations + + sqlx db drop -y + + - name: Test .env + run: | + echo "DATABASE_URL=${{ env.BASE_URL }}/test2" > .env + + sqlx db setup --source=tests/mysql/migrations + + sqlx mig info --source=tests/mysql/migrations + + sqlx db drop -y + + - name: Test --no-dotenv + run: | + # Allow subcommands to fail + set +e + + echo "DATABASE_URL=${{ env.BASE_URL }}/test3" > .env + + ERROR=$(sqlx db setup --no-dotenv --source=tests/mysql/migrations) + + if [[ "$ERROR" == *"--database-url"* ]]; then + exit 0 + else + echo "Unexpected error from sqlx-cli: $ERROR" + exit 1 + fi + + - name: Test Reversible Migrations + env: + DATABASE_URL: ${{ env.BASE_URL }}/test4 + run: | + sqlx db setup --source=tests/mysql/migrations_reversible + + INFO_BEFORE=$(sqlx mig info --source=tests/mysql/migrations_reversible) + + sqlx mig revert --target-version=0 --source=tests/mysql/migrations_reversible + + INFO_AFTER=$(sqlx mig info --source=tests/mysql/migrations_reversible) + + if [[ "$INFO_BEFORE" == "$INFO_AFTER" ]]; then + echo "Error: migration info is identical before and after migrating: $INFO_BEFORE" + exit 1 + fi + + test-postgres: + name: Functional Test (PostgreSQL) + runs-on: ubuntu-latest + # Deliberately not using `tests/docker-compose.yml` because that sets up the database automatically. + services: + mysql: + image: postgres:17 + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: password + env: + BASE_URL: postgres://postgres:password@localhost + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + + - name: Install SQLx-CLI + run: + cargo install --locked --debug --path sqlx-cli + + - name: Basic Test + env: + DATABASE_URL: ${{ env.BASE_URL }}/test1 + run: | + sqlx db setup --source=tests/postgres/migrations + + sqlx mig info --source=tests/postgres/migrations + + sqlx db drop -y + + - name: Test .env + run: | + echo "DATABASE_URL=${{ env.BASE_URL }}/test2" > .env + + sqlx db setup --source=tests/postgres/migrations + + sqlx mig info --source=tests/postgres/migrations + + sqlx db drop -y + + - name: Test --no-dotenv + run: | + # Allow subcommands to fail + set +e + + echo "DATABASE_URL=${{ env.BASE_URL }}/test3" > .env + + ERROR=$(sqlx db setup --no-dotenv --source=tests/postgres/migrations) + + if [[ "$ERROR" == *"--database-url"* ]]; then + exit 0 + else + echo "Unexpected error from sqlx-cli: $ERROR" + exit 1 + fi + + - name: Test Reversible Migrations + env: + DATABASE_URL: ${{ env.BASE_URL }}/test4 + run: | + sqlx db setup --source=tests/postgres/migrations_reversible + + INFO_BEFORE=$(sqlx mig info --source=tests/postgres/migrations_reversible) + + sqlx mig revert --target-version=0 --source=tests/postgres/migrations_reversible + + INFO_AFTER=$(sqlx mig info --source=tests/postgres/migrations_reversible) + + if [[ "$INFO_BEFORE" == "$INFO_AFTER" ]]; then + echo "Error: migration info is identical before and after migrating: $INFO_BEFORE" + exit 1 + fi + + test-sqlite: + name: Functional Test (SQLite) + runs-on: ubuntu-latest + env: + BASE_URL: sqlite://. + + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + run: rustup show active-toolchain || rustup toolchain install + + - uses: Swatinem/rust-cache@v2 + + - name: Install SQLx-CLI + run: + cargo install --locked --debug --path sqlx-cli + + - name: Basic Test + env: + DATABASE_URL: ${{ env.BASE_URL }}/test1 + run: | + sqlx db setup --source=tests/sqlite/migrations + + sqlx mig info --source=tests/sqlite/migrations + + sqlx db drop -y + + - name: Test .env + run: | + echo "DATABASE_URL=${{ env.BASE_URL }}/test2" > .env + + sqlx db setup --source=tests/sqlite/migrations + + sqlx mig info --source=tests/sqlite/migrations + + sqlx db drop -y + + - name: Test --no-dotenv + run: | + # Allow subcommands to fail + set +e + + echo "DATABASE_URL=${{ env.BASE_URL }}/test3" > .env + + ERROR=$(sqlx db setup --no-dotenv --source=tests/sqlite/migrations) + + if [[ "$ERROR" == *"--database-url"* ]]; then + exit 0 + else + echo "Unexpected error from sqlx-cli: $ERROR" + exit 1 + fi + + - name: Test Reversible Migrations + env: + DATABASE_URL: ${{ env.BASE_URL }}/test4 + run: | + sqlx db setup --source=tests/sqlite/migrations_reversible + + INFO_BEFORE=$(sqlx mig info --source=tests/sqlite/migrations_reversible) + + sqlx mig revert --target-version=0 --source=tests/sqlite/migrations_reversible + + INFO_AFTER=$(sqlx mig info --source=tests/sqlite/migrations_reversible) + + if [[ "$INFO_BEFORE" == "$INFO_AFTER" ]]; then + echo "Error: migration info is identical before and after migrating: $INFO_BEFORE" + exit 1 + fi + build: name: Build runs-on: ${{ matrix.os }} diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index 7f573a6349..1e3513b1ee 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -144,7 +144,23 @@ jobs: - run: > cargo test --no-default-features - --features any,macros,${{ matrix.linking }},${{ matrix.linking == 'sqlite' && 'sqlite-preupdate-hook,' || ''}}_unstable-all-types,runtime-${{ matrix.runtime }} + --features any,macros,migrate,${{ matrix.linking }},_unstable-all-types,runtime-${{ matrix.runtime }},${{ matrix.linking == 'sqlite' && 'sqlite-preupdate-hook' || ''}} + -- + --test-threads=1 + env: + DATABASE_URL: sqlite:tests/sqlite/sqlite.db + SQLX_OFFLINE_DIR: .sqlx + RUSTFLAGS: --cfg sqlite_ipaddr --cfg sqlite_test_sqlcipher + LD_LIBRARY_PATH: /tmp/sqlite3-lib + + # Run the `test-attr` test again to cover cleanup. + # The `sqlite-test-attr` test requires the `sqlite` feature. + - if: ${{ matrix.linking == 'sqlite' }} + run: > + cargo test + --test sqlite-test-attr + --no-default-features + --features any,macros,migrate,${{ matrix.linking }},_unstable-all-types,runtime-${{ matrix.runtime }},${{ matrix.linking == 'sqlite' && 'sqlite-preupdate-hook' || ''}} -- --test-threads=1 env: @@ -202,7 +218,10 @@ jobs: # FIXME: needed to disable `ltree` tests in Postgres 9.6 # but `PgLTree` should just fall back to text format RUSTFLAGS: -D warnings --cfg postgres_${{ matrix.postgres }} - run: cargo build --features postgres,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + run: > + cargo build + --no-default-features + --features postgres,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }},macros,migrate - run: | docker compose -f tests/docker-compose.yml run -d -p 5432:5432 --name postgres_${{ matrix.postgres }} postgres_${{ matrix.postgres }} @@ -222,6 +241,19 @@ jobs: # but `PgLTree` should just fall back to text format RUSTFLAGS: --cfg postgres_${{ matrix.postgres }} + # Run the `test-attr` test again to cover cleanup. + - run: > + cargo test + --test postgres-test-attr + --no-default-features + --features any,postgres,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + env: + DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx + SQLX_OFFLINE_DIR: .sqlx + # FIXME: needed to disable `ltree` tests in Postgres 9.6 + # but `PgLTree` should just fall back to text format + RUSTFLAGS: --cfg postgres_${{ matrix.postgres }} + - if: matrix.tls != 'none' run: > cargo test @@ -310,7 +342,18 @@ jobs: - run: > cargo test --no-default-features - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + env: + DATABASE_URL: mysql://root:password@localhost:3306/sqlx?ssl-mode=disabled + SQLX_OFFLINE_DIR: .sqlx + RUSTFLAGS: --cfg mysql_${{ matrix.mysql }} + + # Run the `test-attr` test again to cover cleanup. + - run: > + cargo test + --test mysql-test-attr + --no-default-features + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx?ssl-mode=disabled SQLX_OFFLINE_DIR: .sqlx @@ -321,7 +364,7 @@ jobs: run: > cargo test --no-default-features - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx SQLX_OFFLINE_DIR: .sqlx @@ -335,7 +378,7 @@ jobs: cargo build --no-default-features --test mysql-macros - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: SQLX_OFFLINE: true SQLX_OFFLINE_DIR: .sqlx @@ -347,7 +390,7 @@ jobs: cargo test --no-default-features --test mysql-macros - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx SQLX_OFFLINE: true @@ -366,7 +409,7 @@ jobs: run: > cargo test --no-default-features - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root@localhost:3306/sqlx?sslmode=verify_ca&ssl-ca=.%2Ftests%2Fcerts%2Fca.crt&ssl-key=.%2Ftests%2Fkeys%2Fclient.key&ssl-cert=.%2Ftests%2Fcerts%2Fclient.crt RUSTFLAGS: --cfg mysql_${{ matrix.mysql }} @@ -399,7 +442,18 @@ jobs: - run: > cargo test --no-default-features - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + env: + DATABASE_URL: mysql://root:password@localhost:3306/sqlx + SQLX_OFFLINE_DIR: .sqlx + RUSTFLAGS: --cfg mariadb_${{ matrix.mariadb }} + + # Run the `test-attr` test again to cover cleanup. + - run: > + cargo test + --test mysql-test-attr + --no-default-features + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx SQLX_OFFLINE_DIR: .sqlx @@ -413,7 +467,7 @@ jobs: cargo build --no-default-features --test mysql-macros - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: SQLX_OFFLINE: true SQLX_OFFLINE_DIR: .sqlx @@ -424,7 +478,7 @@ jobs: cargo test --no-default-features --test mysql-macros - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx SQLX_OFFLINE: true @@ -443,7 +497,7 @@ jobs: run: > cargo test --no-default-features - --features any,mysql,macros,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} + --features any,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }},tls-${{ matrix.tls }} env: DATABASE_URL: mysql://root@localhost:3306/sqlx?sslmode=verify_ca&ssl-ca=.%2Ftests%2Fcerts%2Fca.crt&ssl-key=.%2Ftests%2Fkeys%2Fclient.key&ssl-cert=.%2Ftests%2Fcerts%2Fclient.crt RUSTFLAGS: --cfg mariadb_${{ matrix.mariadb }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2603a808..3e5b4c9d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.8.5 - 2025-04-14 + +Hotfix release to address two new issues: +* [[#3823]]: `sqlx-cli@0.8.4` broke `.env` default resolution mechanism +* [[#3825]]: `sqlx@0.8.4` broke test fixture setup + +The `0.8.4` release will be yanked as of publishing this one. + +# Added +* In release PR: `sqlx-cli` now accepts `--no-dotenv` in subcommand arguments. +* In release PR: added functionality tests for `sqlx-cli` to CI. +* In release PR: test `#[sqlx::test]` twice in CI to cover cleanup. + +# Fixed +* In release PR: `sqlx-cli` correctly reads `.env` files by default again. + * Addresses [[#3823]]. +* In release PR: fix bugs in MySQL implementation of `#[sqlx::test]`. + * Addresses [[#3825]]. + +[#3823]: https://github.com/launchbadge/sqlx/issues/3823 +[#3825]: https://github.com/launchbadge/sqlx/issues/3825 + ## 0.8.4 - 2025-04-13 50 pull requests were merged this release cycle. diff --git a/Cargo.lock b/Cargo.lock index 355cee90b6..e19b2e33c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3374,7 +3374,7 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.4" +version = "0.8.5" dependencies = [ "anyhow", "async-std", @@ -3404,7 +3404,7 @@ dependencies = [ [[package]] name = "sqlx-cli" -version = "0.8.4" +version = "0.8.5" dependencies = [ "anyhow", "assert_cmd", @@ -3428,7 +3428,7 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.4" +version = "0.8.5" dependencies = [ "async-io 1.13.0", "async-std", @@ -3604,7 +3604,7 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.4" +version = "0.8.5" dependencies = [ "proc-macro2", "quote", @@ -3615,7 +3615,7 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.4" +version = "0.8.5" dependencies = [ "async-std", "dotenvy", @@ -3640,7 +3640,7 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.8.4" +version = "0.8.5" dependencies = [ "atoi", "base64 0.22.1", @@ -3686,7 +3686,7 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.4" +version = "0.8.5" dependencies = [ "atoi", "base64 0.22.1", @@ -3732,7 +3732,7 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.4" +version = "0.8.5" dependencies = [ "atoi", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 09bdabf33b..1aef121199 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.8.4" +version = "0.8.5" license = "MIT OR Apache-2.0" edition = "2021" repository = "https://github.com/launchbadge/sqlx" @@ -129,17 +129,17 @@ bstr = ["sqlx-core/bstr"] [workspace.dependencies] # Core Crates -sqlx-core = { version = "=0.8.4", path = "sqlx-core" } -sqlx-macros-core = { version = "=0.8.4", path = "sqlx-macros-core" } -sqlx-macros = { version = "=0.8.4", path = "sqlx-macros" } +sqlx-core = { version = "=0.8.5", path = "sqlx-core" } +sqlx-macros-core = { version = "=0.8.5", path = "sqlx-macros-core" } +sqlx-macros = { version = "=0.8.5", path = "sqlx-macros" } # Driver crates -sqlx-mysql = { version = "=0.8.4", path = "sqlx-mysql" } -sqlx-postgres = { version = "=0.8.4", path = "sqlx-postgres" } -sqlx-sqlite = { version = "=0.8.4", path = "sqlx-sqlite" } +sqlx-mysql = { version = "=0.8.5", path = "sqlx-mysql" } +sqlx-postgres = { version = "=0.8.5", path = "sqlx-postgres" } +sqlx-sqlite = { version = "=0.8.5", path = "sqlx-sqlite" } # Facade crate (for reference from sqlx-cli) -sqlx = { version = "=0.8.4", path = ".", default-features = false } +sqlx = { version = "=0.8.5", path = ".", default-features = false } # Common type integrations shared by multiple driver crates. # These are optional unless enabled in a workspace crate. diff --git a/sqlx-cli/src/bin/cargo-sqlx.rs b/sqlx-cli/src/bin/cargo-sqlx.rs index c87147b6a3..a5b5db8ea9 100644 --- a/sqlx-cli/src/bin/cargo-sqlx.rs +++ b/sqlx-cli/src/bin/cargo-sqlx.rs @@ -13,11 +13,9 @@ enum Cli { #[tokio::main] async fn main() { - let Cli::Sqlx(opt) = Cli::parse(); + sqlx_cli::maybe_apply_dotenv(); - if !opt.no_dotenv { - dotenvy::dotenv().ok(); - } + let Cli::Sqlx(opt) = Cli::parse(); if let Err(error) = sqlx_cli::run(opt).await { println!("{} {}", style("error:").bold().red(), error); diff --git a/sqlx-cli/src/bin/sqlx.rs b/sqlx-cli/src/bin/sqlx.rs index c19b61f393..c015907990 100644 --- a/sqlx-cli/src/bin/sqlx.rs +++ b/sqlx-cli/src/bin/sqlx.rs @@ -4,11 +4,10 @@ use sqlx_cli::Opt; #[tokio::main] async fn main() { - let opt = Opt::parse(); + // Checks for `--no-dotenv` before parsing. + sqlx_cli::maybe_apply_dotenv(); - if !opt.no_dotenv { - dotenvy::dotenv().ok(); - } + let opt = Opt::parse(); // no special handling here if let Err(error) = sqlx_cli::run(opt).await { diff --git a/sqlx-cli/src/lib.rs b/sqlx-cli/src/lib.rs index a182f019b9..cb31205b4f 100644 --- a/sqlx-cli/src/lib.rs +++ b/sqlx-cli/src/lib.rs @@ -21,6 +21,15 @@ mod prepare; pub use crate::opt::Opt; +/// Check arguments for `--no-dotenv` _before_ Clap parsing, and apply `.env` if not set. +pub fn maybe_apply_dotenv() { + if std::env::args().any(|arg| arg == "--no-dotenv") { + return; + } + + dotenvy::dotenv().ok(); +} + pub async fn run(opt: Opt) -> Result<()> { // This `select!` is here so that when the process receives a `SIGINT` (CTRL + C), // the futures currently running on this task get dropped before the program exits. diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index 07058aa147..a0e19e47f4 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -7,9 +7,10 @@ use clap_complete::Shell; #[derive(Parser, Debug)] #[clap(version, about, author)] pub struct Opt { - /// Do not automatically load `.env` files. - #[clap(long)] - pub no_dotenv: bool, + // https://github.com/launchbadge/sqlx/pull/3724 placed this here, + // but the intuitive place would be in the arguments for each subcommand. + #[clap(flatten)] + pub no_dotenv: NoDotenvOpt, #[clap(subcommand)] pub command: Command, @@ -245,6 +246,9 @@ impl Deref for Source { /// Argument for the database URL. #[derive(Args, Debug)] pub struct ConnectOpts { + #[clap(flatten)] + pub no_dotenv: NoDotenvOpt, + /// Location of the DB, by default will be read from the DATABASE_URL env var or `.env` files. #[clap(long, short = 'D', env)] pub database_url: Option, @@ -267,6 +271,16 @@ pub struct ConnectOpts { pub sqlite_create_db_wal: bool, } +#[derive(Args, Debug)] +pub struct NoDotenvOpt { + /// Do not automatically load `.env` files. + #[clap(long)] + // Parsing of this flag is actually handled _before_ calling Clap, + // by `crate::maybe_apply_dotenv()`. + #[allow(unused)] // TODO: switch to `#[expect]` + pub no_dotenv: bool, +} + impl ConnectOpts { /// Require a database URL to be provided, otherwise /// return an error. diff --git a/sqlx-mysql/src/testing/mod.rs b/sqlx-mysql/src/testing/mod.rs index 1981cf73c5..2b6d46718c 100644 --- a/sqlx-mysql/src/testing/mod.rs +++ b/sqlx-mysql/src/testing/mod.rs @@ -4,18 +4,17 @@ use std::time::Duration; use futures_core::future::BoxFuture; +use crate::error::Error; +use crate::executor::Executor; +use crate::pool::{Pool, PoolOptions}; +use crate::query::query; +use crate::{MySql, MySqlConnectOptions, MySqlConnection, MySqlDatabaseError}; use once_cell::sync::OnceCell; use sqlx_core::connection::Connection; use sqlx_core::query_builder::QueryBuilder; use sqlx_core::query_scalar::query_scalar; use std::fmt::Write; -use crate::error::Error; -use crate::executor::Executor; -use crate::pool::{Pool, PoolOptions}; -use crate::query::query; -use crate::{MySql, MySqlConnectOptions, MySqlConnection}; - pub(crate) use sqlx_core::testing::*; // Using a blocking `OnceCell` here because the critical sections are short. @@ -62,7 +61,7 @@ impl TestSupport for MySql { let db_name = format!("_sqlx_test_database_{db_name}"); - writeln!(command, "drop database if exists {db_name:?};").ok(); + writeln!(command, "drop database if exists {db_name};").ok(); match conn.execute(&*command).await { Ok(_deleted) => { deleted_db_names.push(db_name); @@ -141,14 +140,19 @@ async fn test_context(args: &TestArgs) -> Result, Error> { let mut conn = master_pool.acquire().await?; + cleanup_old_dbs(&mut conn).await?; + // language=MySQL conn.execute( r#" create table if not exists _sqlx_test_databases ( - db_name text primary key, + db_name text not null, test_path text not null, - created_at timestamp not null default current_timestamp - ); + created_at timestamp not null default current_timestamp, + -- BLOB/TEXT columns can only be used as index keys with a prefix length: + -- https://dev.mysql.com/doc/refman/8.4/en/column-indexes.html#column-indexes-prefix + primary key(db_name(63)) + ); "#, ) .await?; @@ -162,7 +166,7 @@ async fn test_context(args: &TestArgs) -> Result, Error> { .execute(&mut *conn) .await?; - conn.execute(&format!("create database {db_name:?}")[..]) + conn.execute(&format!("create database {db_name}")[..]) .await?; eprintln!("created database {db_name}"); @@ -186,12 +190,64 @@ async fn test_context(args: &TestArgs) -> Result, Error> { } async fn do_cleanup(conn: &mut MySqlConnection, db_name: &str) -> Result<(), Error> { - let delete_db_command = format!("drop database if exists {db_name:?};"); + let delete_db_command = format!("drop database if exists {db_name};"); conn.execute(&*delete_db_command).await?; - query("delete from _sqlx_test.databases where db_name = $1::text") + query("delete from _sqlx_test_databases where db_name = ?") .bind(db_name) .execute(&mut *conn) .await?; Ok(()) } + +/// Pre <0.8.4, test databases were stored by integer ID. +async fn cleanup_old_dbs(conn: &mut MySqlConnection) -> Result<(), Error> { + let res: Result, Error> = query_scalar("select db_id from _sqlx_test_databases") + .fetch_all(&mut *conn) + .await; + + let db_ids = match res { + Ok(db_ids) => db_ids, + Err(e) => { + if let Some(dbe) = e.as_database_error() { + match dbe.downcast_ref::().number() { + // Column `db_id` does not exist: + // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_bad_field_error + // + // The table has already been migrated. + 1054 => return Ok(()), + // Table `_sqlx_test_databases` does not exist. + // No cleanup needed. + // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_no_such_table + 1146 => return Ok(()), + _ => (), + } + } + + return Err(e); + } + }; + + // Drop old-style test databases. + for id in db_ids { + match conn + .execute(&*format!( + "drop database if exists _sqlx_test_database_{id}" + )) + .await + { + Ok(_deleted) => (), + // Assume a database error just means the DB is still in use. + Err(Error::Database(dbe)) => { + eprintln!("could not clean old test database _sqlx_test_database_{id}: {dbe}"); + } + // Bubble up other errors + Err(e) => return Err(e), + } + } + + conn.execute("drop table if exists _sqlx_test_databases") + .await?; + + Ok(()) +} diff --git a/tests/mysql/test-attr.rs b/tests/mysql/test-attr.rs index 158be8f816..75ca668602 100644 --- a/tests/mysql/test-attr.rs +++ b/tests/mysql/test-attr.rs @@ -12,11 +12,7 @@ async fn it_gets_a_pool(pool: MySqlPool) -> sqlx::Result<()> { .fetch_one(&mut *conn) .await?; - assert!( - db_name.starts_with("_sqlx_test_database_"), - "db_name: {:?}", - db_name - ); + assert!(db_name.starts_with("_sqlx_test_"), "db_name: {:?}", db_name); Ok(()) } From 91d26bad4d5e2b05fab1c86d0fbe11586d30f29d Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Tue, 15 Apr 2025 15:14:58 -0700 Subject: [PATCH 55/66] fix(CHANGELOG): section headings in 0.8.5 --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e5b4c9d53..f7a0d42a79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,12 @@ Hotfix release to address two new issues: The `0.8.4` release will be yanked as of publishing this one. -# Added +### Added * In release PR: `sqlx-cli` now accepts `--no-dotenv` in subcommand arguments. * In release PR: added functionality tests for `sqlx-cli` to CI. * In release PR: test `#[sqlx::test]` twice in CI to cover cleanup. -# Fixed +### Fixed * In release PR: `sqlx-cli` correctly reads `.env` files by default again. * Addresses [[#3823]]. * In release PR: fix bugs in MySQL implementation of `#[sqlx::test]`. @@ -2873,4 +2873,4 @@ Fix docs.rs build by enabling a runtime feature in the docs.rs metadata in `Carg [@mbj]: https://github.com/mbj [@TeCHiScy]: https://github.com/TeCHiScy [@mpyw]: https://github.com/mpyw -[@bonsairobo]: https://github.com/bonsairobo \ No newline at end of file +[@bonsairobo]: https://github.com/bonsairobo From 5736ab6c212480acf7704534cb212b03fca8a3de Mon Sep 17 00:00:00 2001 From: tison Date: Sat, 3 May 2025 09:20:34 +0800 Subject: [PATCH 56/66] chore: clean up no longer used imports (#3845) Signed-off-by: tison --- sqlx-core/src/sync.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sqlx-core/src/sync.rs b/sqlx-core/src/sync.rs index 27ad29c33e..14525611a0 100644 --- a/sqlx-core/src/sync.rs +++ b/sqlx-core/src/sync.rs @@ -4,12 +4,6 @@ // We'll generally lean towards Tokio's types as those are more featureful // (including `tokio-console` support) and more widely deployed. -#[cfg(all(feature = "_rt-async-std", not(feature = "_rt-tokio")))] -pub use async_std::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; - -#[cfg(feature = "_rt-tokio")] -pub use tokio::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; - pub struct AsyncSemaphore { // We use the semaphore from futures-intrusive as the one from async-std // is missing the ability to add arbitrary permits, and is not guaranteed to be fair: From 6b2e0247d47d020d91dc6f7402d42e4e6131af11 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Fri, 2 May 2025 18:48:46 -0700 Subject: [PATCH 57/66] Add color and wrapping to cli help text (#3849) --- Cargo.lock | 30 ++++++++++++++++++++++++++++++ sqlx-cli/Cargo.toml | 2 +- sqlx-cli/src/opt.rs | 13 +++++++++++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e19b2e33c8..7a5db0c251 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -805,6 +805,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -2104,6 +2105,12 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "litemap" version = "0.7.4" @@ -3013,6 +3020,19 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags 2.7.0", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.23.21" @@ -3986,6 +4006,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" +dependencies = [ + "rustix 1.0.5", + "windows-sys 0.59.0", +] + [[package]] name = "termtree" version = "0.5.1" diff --git a/sqlx-cli/Cargo.toml b/sqlx-cli/Cargo.toml index 582099a2f6..de87b38ba0 100644 --- a/sqlx-cli/Cargo.toml +++ b/sqlx-cli/Cargo.toml @@ -33,7 +33,7 @@ sqlx = { workspace = true, default-features = false, features = [ "any", ] } futures = "0.3.19" -clap = { version = "4.3.10", features = ["derive", "env"] } +clap = { version = "4.3.10", features = ["derive", "env", "wrap_help"] } clap_complete = { version = "4.3.1", optional = true } chrono = { version = "0.4.19", default-features = false, features = ["clock"] } anyhow = "1.0.52" diff --git a/sqlx-cli/src/opt.rs b/sqlx-cli/src/opt.rs index a0e19e47f4..133ba084f2 100644 --- a/sqlx-cli/src/opt.rs +++ b/sqlx-cli/src/opt.rs @@ -1,11 +1,20 @@ use std::ops::{Deref, Not}; -use clap::{Args, Parser}; +use clap::{ + builder::{styling::AnsiColor, Styles}, + Args, Parser, +}; #[cfg(feature = "completions")] use clap_complete::Shell; +const HELP_STYLES: Styles = Styles::styled() + .header(AnsiColor::Blue.on_default().bold()) + .usage(AnsiColor::Blue.on_default().bold()) + .literal(AnsiColor::White.on_default()) + .placeholder(AnsiColor::Green.on_default()); + #[derive(Parser, Debug)] -#[clap(version, about, author)] +#[clap(version, about, author, styles = HELP_STYLES)] pub struct Opt { // https://github.com/launchbadge/sqlx/pull/3724 placed this here, // but the intuitive place would be in the arguments for each subcommand. From 3edc6199db2793de98623b07ce3345d9564271ff Mon Sep 17 00:00:00 2001 From: Paolo Barbolini Date: Tue, 6 May 2025 00:44:56 +0200 Subject: [PATCH 58/66] build: drop unused `tempfile` dependency (#3830) --- sqlx-macros-core/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/sqlx-macros-core/Cargo.toml b/sqlx-macros-core/Cargo.toml index 85efa80912..07d9d78862 100644 --- a/sqlx-macros-core/Cargo.toml +++ b/sqlx-macros-core/Cargo.toml @@ -65,7 +65,6 @@ serde = { version = "1.0.132", features = ["derive"] } serde_json = { version = "1.0.73" } sha2 = { version = "0.10.0" } syn = { version = "2.0.52", default-features = false, features = ["full", "derive", "parsing", "printing", "clone-impls"] } -tempfile = { version = "3.10.1" } quote = { version = "1.0.26", default-features = false } url = { version = "2.2.2" } From 92c384595264eae9e57a599079d44e5ce3b9e5c1 Mon Sep 17 00:00:00 2001 From: Nikolai Vincent Vaags Date: Thu, 8 May 2025 02:06:34 +0200 Subject: [PATCH 59/66] fix `attrubute` typo in doc (#3855) --- sqlx-core/src/from_row.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlx-core/src/from_row.rs b/sqlx-core/src/from_row.rs index ecd5847f30..5b3904011e 100644 --- a/sqlx-core/src/from_row.rs +++ b/sqlx-core/src/from_row.rs @@ -251,7 +251,7 @@ use crate::{error::Error, row::Row}; /// represented as the _not_ NULL (in terms of DB) JSON value of `null`. /// /// If you wish to describe a database row which _is_ NULLable but _cannot_ contain the JSON value `null`, -/// use the `#[sqlx(json(nullable))]` attrubute. +/// use the `#[sqlx(json(nullable))]` attribute. /// /// For example /// ```rust,ignore From 1b94e1d07d7db5bd8b8f48564a17d40c03fff6fe Mon Sep 17 00:00:00 2001 From: Josh <59372145+duhby@users.noreply.github.com> Date: Sat, 17 May 2025 22:50:18 -0600 Subject: [PATCH 60/66] chore(doc): clarify compile-time verification and case conversion behavior (#3866) * chore(doc): clarify compile-time verification and case conversion behavior * apply review suggestions Co-authored-by: Austin Bonander * fix(fmt): remove trailing spaces * fix(doc): links --------- Co-authored-by: Austin Bonander --- sqlx-core/src/from_row.rs | 8 ++++++++ sqlx-core/src/types/mod.rs | 7 +++++-- src/macros/mod.rs | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/sqlx-core/src/from_row.rs b/sqlx-core/src/from_row.rs index 5b3904011e..b96801cb65 100644 --- a/sqlx-core/src/from_row.rs +++ b/sqlx-core/src/from_row.rs @@ -65,6 +65,14 @@ use crate::{error::Error, row::Row}; /// reason), `lowercase`, `UPPERCASE`, `camelCase`, `PascalCase`, `SCREAMING_SNAKE_CASE` and `kebab-case`. /// The styling of each option is intended to be an example of its behavior. /// +/// Case conversion is handled by the `heck` crate. +/// See [its documentation](https://docs.rs/heck/0.5.0/heck/#definition-of-a-word-boundary) +/// for details. +/// +/// Note that numbers are *not* considered separate words. +/// For example, `Foo1` to snake case would be `foo1`, *not* `foo_1`. +/// See [this issue](https://github.com/launchbadge/sqlx/issues/3864) for discussion. +/// /// #### `default` /// /// When your struct contains a field that is not present in your query, diff --git a/sqlx-core/src/types/mod.rs b/sqlx-core/src/types/mod.rs index b00427daae..b45fa2fa85 100644 --- a/sqlx-core/src/types/mod.rs +++ b/sqlx-core/src/types/mod.rs @@ -99,8 +99,11 @@ pub use bstr::{BStr, BString}; /// /// ## Compile-time verification /// -/// With compile-time verification, the use of type overrides is currently required to make -/// use of any user-defined types. +/// Type definitions are *not* verified against the database at compile-time. +/// The [`query!()`](macro.query.html) macros have no implicit knowledge of user-defined types. +/// +/// When using custom types in query parameters or output columns with `query!()`, +/// the use of [type overrides](macro.query.html#type-overrides-bind-parameters-postgres-only) is required. /// /// ```rust,ignore /// struct MyUser { id: UserId, name: String } diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 7f8ff747f9..9e81935876 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -249,7 +249,7 @@ /// /// ##### Force a Different/Custom Type /// Selecting a column `foo as "foo: T"` (Postgres / SQLite) or `` foo as `foo: T` `` (MySQL) -/// overrides the inferred type which is useful when selecting user-defined custom types +/// overrides the inferred type which is useful when selecting user-defined [custom types][crate::Type#compile-time-verification] /// (dynamic type checking is still done so if the types are incompatible this will be an error /// at runtime instead of compile-time). Note that this syntax alone doesn't override inferred nullability, /// but it is compatible with the forced not-null and forced nullable annotations: From 4259862adf91d42ea50e3651070ada5f45af3689 Mon Sep 17 00:00:00 2001 From: Carson McManus Date: Mon, 19 May 2025 17:55:00 -0400 Subject: [PATCH 61/66] fix(macros): slightly improve unsupported type error message (#3856) --- sqlx-macros-core/src/query/args.rs | 7 ++++++- sqlx-macros-core/src/query/output.rs | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/sqlx-macros-core/src/query/args.rs b/sqlx-macros-core/src/query/args.rs index ec17aeff65..788a9aadc5 100644 --- a/sqlx-macros-core/src/query/args.rs +++ b/sqlx-macros-core/src/query/args.rs @@ -66,7 +66,12 @@ pub fn quote_args( i + 1, ) } else { - format!("unsupported type {} for param #{}", param_ty, i + 1) + format!( + "no built in mapping found for type {} for param #{}; \ + a type override may be required, see documentation for details", + param_ty, + i + 1 + ) } })? .parse::() diff --git a/sqlx-macros-core/src/query/output.rs b/sqlx-macros-core/src/query/output.rs index 5e7cc5058d..3641e55db5 100644 --- a/sqlx-macros-core/src/query/output.rs +++ b/sqlx-macros-core/src/query/output.rs @@ -236,7 +236,7 @@ fn get_column_type(i: usize, column: &DB::Column) -> TokenStrea let message = if let Some(feature_gate) = ::get_feature_gate(type_info) { format!( - "optional sqlx feature `{feat}` required for type {ty} of {col}", + "SQLx feature `{feat}` required for type {ty} of {col}", ty = &type_info, feat = feature_gate, col = DisplayColumn { @@ -246,7 +246,8 @@ fn get_column_type(i: usize, column: &DB::Column) -> TokenStrea ) } else { format!( - "unsupported type {ty} of {col}", + "no built in mapping found for type {ty} of {col}; \ + a type override may be required, see documentation for details", ty = type_info, col = DisplayColumn { idx: i, From 760b3953ba3942f686fdfbb98af5f29a3918045d Mon Sep 17 00:00:00 2001 From: Joey de Waal <99046430+joeydewaal@users.noreply.github.com> Date: Mon, 19 May 2025 23:58:26 +0200 Subject: [PATCH 62/66] fix(macros): don't mutate environment variables (#3848) --- sqlx-macros-core/src/query/mod.rs | 82 +++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 25 deletions(-) diff --git a/sqlx-macros-core/src/query/mod.rs b/sqlx-macros-core/src/query/mod.rs index d2392e2d0e..a51137413e 100644 --- a/sqlx-macros-core/src/query/mod.rs +++ b/sqlx-macros-core/src/query/mod.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::{fs, io}; @@ -74,6 +74,7 @@ struct Metadata { manifest_dir: PathBuf, offline: bool, database_url: Option, + offline_dir: Option, workspace_root: Arc>>, } @@ -114,39 +115,21 @@ static METADATA: Lazy>> = Lazy::new(Default::def fn init_metadata(manifest_dir: &String) -> Metadata { let manifest_dir: PathBuf = manifest_dir.into(); - // If a .env file exists at CARGO_MANIFEST_DIR, load environment variables from this, - // otherwise fallback to default dotenv behaviour. - let env_path = manifest_dir.join(".env"); - - #[cfg_attr(not(procmacro2_semver_exempt), allow(unused_variables))] - let env_path = if env_path.exists() { - // Load the new environment variables and override the old ones if necessary. - let res = dotenvy::from_path_override(&env_path); - if let Err(e) = res { - panic!("failed to load environment from {env_path:?}, {e}"); - } - - Some(env_path) - } else { - dotenvy::dotenv_override().ok() - }; - - // tell the compiler to watch the `.env` for changes, if applicable - #[cfg(procmacro2_semver_exempt)] - if let Some(env_path) = env_path.as_ref().and_then(|path| path.to_str()) { - proc_macro::tracked_path::path(env_path); - } + let (database_url, offline, offline_dir) = load_dot_env(&manifest_dir); let offline = env("SQLX_OFFLINE") + .ok() + .or(offline) .map(|s| s.eq_ignore_ascii_case("true") || s == "1") .unwrap_or(false); - let database_url = env("DATABASE_URL").ok(); + let database_url = env("DATABASE_URL").ok().or(database_url); Metadata { manifest_dir, offline, database_url, + offline_dir, workspace_root: Arc::new(Mutex::new(None)), } } @@ -182,7 +165,7 @@ pub fn expand_input<'a>( // Check SQLX_OFFLINE_DIR, then local .sqlx, then workspace .sqlx. let dirs = [ - |_: &Metadata| env("SQLX_OFFLINE_DIR").ok().map(PathBuf::from), + |meta: &Metadata| meta.offline_dir.as_deref().map(PathBuf::from), |meta: &Metadata| Some(meta.manifest_dir.join(".sqlx")), |meta: &Metadata| Some(meta.workspace_root().join(".sqlx")), ]; @@ -402,3 +385,52 @@ fn env(name: &str) -> Result { std::env::var(name) } } + +/// Get `DATABASE_URL`, `SQLX_OFFLINE` and `SQLX_OFFLINE_DIR` from the `.env`. +fn load_dot_env(manifest_dir: &Path) -> (Option, Option, Option) { + let mut env_path = manifest_dir.join(".env"); + + // If a .env file exists at CARGO_MANIFEST_DIR, load environment variables from this, + // otherwise fallback to default dotenv file. + #[cfg_attr(not(procmacro2_semver_exempt), allow(unused_variables))] + let env_file = if env_path.exists() { + let res = dotenvy::from_path_iter(&env_path); + match res { + Ok(iter) => Some(iter), + Err(e) => panic!("failed to load environment from {env_path:?}, {e}"), + } + } else { + #[allow(unused_assignments)] + { + env_path = PathBuf::from(".env"); + } + dotenvy::dotenv_iter().ok() + }; + + let mut offline = None; + let mut database_url = None; + let mut offline_dir = None; + + if let Some(env_file) = env_file { + // tell the compiler to watch the `.env` for changes. + #[cfg(procmacro2_semver_exempt)] + if let Some(env_path) = env_path.to_str() { + proc_macro::tracked_path::path(env_path); + } + + for item in env_file { + let Ok((key, value)) = item else { + continue; + }; + + match key.as_str() { + "DATABASE_URL" => database_url = Some(value), + "SQLX_OFFLINE" => offline = Some(value), + "SQLX_OFFLINE_DIR" => offline_dir = Some(value), + _ => {} + }; + } + } + + (database_url, offline, offline_dir) +} From d335f782cf4a607fd2667c04ede5e10db694f5fd Mon Sep 17 00:00:00 2001 From: Thom Wright Date: Mon, 19 May 2025 22:58:40 +0100 Subject: [PATCH 63/66] Use unnamed statement in pg when not persistent (#3863) This will automatically close the prepared statement when another query is run, avoiding a memory leak. --- sqlx-postgres/src/connection/executor.rs | 17 +++++++++++------ sqlx-postgres/src/message/parse.rs | 16 ++++++++++++++++ tests/postgres/postgres.rs | 21 +++++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/sqlx-postgres/src/connection/executor.rs b/sqlx-postgres/src/connection/executor.rs index cd3a876520..3fe4f402d8 100644 --- a/sqlx-postgres/src/connection/executor.rs +++ b/sqlx-postgres/src/connection/executor.rs @@ -25,9 +25,15 @@ async fn prepare( sql: &str, parameters: &[PgTypeInfo], metadata: Option>, + persistent: bool, ) -> Result<(StatementId, Arc), Error> { - let id = conn.inner.next_statement_id; - conn.inner.next_statement_id = id.next(); + let id = if persistent { + let id = conn.inner.next_statement_id; + conn.inner.next_statement_id = id.next(); + id + } else { + StatementId::UNNAMED + }; // build a list of type OIDs to send to the database in the PARSE command // we have not yet started the query sequence, so we are *safe* to cleanly make @@ -163,8 +169,7 @@ impl PgConnection { &mut self, sql: &str, parameters: &[PgTypeInfo], - // should we store the result of this prepare to the cache - store_to_cache: bool, + persistent: bool, // optional metadata that was provided by the user, this means they are reusing // a statement object metadata: Option>, @@ -173,9 +178,9 @@ impl PgConnection { return Ok((*statement).clone()); } - let statement = prepare(self, sql, parameters, metadata).await?; + let statement = prepare(self, sql, parameters, metadata, persistent).await?; - if store_to_cache && self.inner.cache_statement.is_enabled() { + if persistent && self.inner.cache_statement.is_enabled() { if let Some((id, _)) = self.inner.cache_statement.insert(sql, statement.clone()) { self.inner.stream.write_msg(Close::Statement(id))?; self.write_sync(); diff --git a/sqlx-postgres/src/message/parse.rs b/sqlx-postgres/src/message/parse.rs index 75300c4815..62f57a1cc4 100644 --- a/sqlx-postgres/src/message/parse.rs +++ b/sqlx-postgres/src/message/parse.rs @@ -77,3 +77,19 @@ fn test_encode_parse() { assert_eq!(buf, EXPECTED); } + +#[test] +fn test_encode_parse_unnamed_statement() { + const EXPECTED: &[u8] = b"P\0\0\0\x15\0SELECT $1\0\0\x01\0\0\0\x19"; + + let mut buf = Vec::new(); + let m = Parse { + statement: StatementId::UNNAMED, + query: "SELECT $1", + param_types: &[Oid(25)], + }; + + m.encode_msg(&mut buf).unwrap(); + + assert_eq!(buf, EXPECTED); +} diff --git a/tests/postgres/postgres.rs b/tests/postgres/postgres.rs index fc7108bf4f..f0d453a9a3 100644 --- a/tests/postgres/postgres.rs +++ b/tests/postgres/postgres.rs @@ -817,6 +817,27 @@ async fn it_closes_statement_from_cache_issue_470() -> anyhow::Result<()> { Ok(()) } +#[sqlx_macros::test] +async fn it_closes_statements_when_not_persistent_issue_3850() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let _row = sqlx::query("SELECT $1 AS val") + .bind(Oid(1)) + .persistent(false) + .fetch_one(&mut conn) + .await?; + + let row = sqlx::query("SELECT count(*) AS num_prepared_statements FROM pg_prepared_statements") + .persistent(false) + .fetch_one(&mut conn) + .await?; + + let n: i64 = row.get("num_prepared_statements"); + assert_eq!(0, n, "no prepared statements should be open"); + + Ok(()) +} + #[sqlx_macros::test] async fn it_sets_application_name() -> anyhow::Result<()> { sqlx_test::setup_if_needed(); From b27b47ce5350d1110e2a32e54809da1d32ec1d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20F=C3=A9ron?= Date: Mon, 19 May 2025 23:59:01 +0200 Subject: [PATCH 64/66] Pick default features to fix docs.rs build of sqlx-sqlite (#3840) --- sqlx-sqlite/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sqlx-sqlite/Cargo.toml b/sqlx-sqlite/Cargo.toml index 5ad57546e7..ca4c84c958 100644 --- a/sqlx-sqlite/Cargo.toml +++ b/sqlx-sqlite/Cargo.toml @@ -72,3 +72,6 @@ sqlx = { workspace = true, default-features = false, features = ["macros", "runt [lints] workspace = true + +[package.metadata.docs.rs] +features = ["bundled", "any", "json", "chrono", "time", "uuid"] \ No newline at end of file From bab1b022bd56a64f9a08b46b36b97c5cff19d77e Mon Sep 17 00:00:00 2001 From: Austin Bonander Date: Mon, 19 May 2025 16:21:27 -0700 Subject: [PATCH 65/66] 0.8.6 release (#3870) --- CHANGELOG.md | 38 +++++++++++++++++++++++++++++++++++--- Cargo.lock | 17 ++++++++--------- Cargo.toml | 16 ++++++++-------- 3 files changed, 51 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a0d42a79..3fadc3d093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.8.6 - 2025-05-19 + +9 pull requests were merged this release cycle. + +### Added +* [[#3849]]: Add color and wrapping to cli help text [[@joshka]] + +### Changed +* [[#3830]]: build: drop unused `tempfile` dependency [[@paolobarbolini]] +* [[#3845]]: chore: clean up no longer used imports [[@tisonkun]] +* [[#3863]]: Use unnamed statement in pg when not persistent [[@ThomWright]] +* [[#3866]]: chore(doc): clarify compile-time verification and case conversion behavior [[@duhby]] + +### Fixed +* [[#3840]]: Fix docs.rs build of sqlx-sqlite [[@gferon]] +* [[#3848]]: fix(macros): don't mutate environment variables [[@joeydewaal]] +* [[#3855]]: fix `attrubute` typo in doc [[@kujeger]] +* [[#3856]]: fix(macros): slightly improve unsupported type error message [[@dyc3]] + +[#3830]: https://github.com/launchbadge/sqlx/pull/3830 +[#3840]: https://github.com/launchbadge/sqlx/pull/3840 +[#3845]: https://github.com/launchbadge/sqlx/pull/3845 +[#3848]: https://github.com/launchbadge/sqlx/pull/3848 +[#3849]: https://github.com/launchbadge/sqlx/pull/3849 +[#3855]: https://github.com/launchbadge/sqlx/pull/3855 +[#3856]: https://github.com/launchbadge/sqlx/pull/3856 +[#3863]: https://github.com/launchbadge/sqlx/pull/3863 +[#3866]: https://github.com/launchbadge/sqlx/pull/3866 + ## 0.8.5 - 2025-04-14 Hotfix release to address two new issues: @@ -31,9 +60,6 @@ The `0.8.4` release will be yanked as of publishing this one. 50 pull requests were merged this release cycle. -As of this release, development of `0.9.0` has begun on `main`. -Barring urgent hotfixes, this is expected to be the last release of `0.8.x`. - ### Added * [[#3603]]: Added missing special casing for encoding embedded arrays of custom types [[@nico-incubiq]] * [[#3625]]: feat(sqlite): add preupdate hook [[@aschey]] @@ -2874,3 +2900,9 @@ Fix docs.rs build by enabling a runtime feature in the docs.rs metadata in `Carg [@TeCHiScy]: https://github.com/TeCHiScy [@mpyw]: https://github.com/mpyw [@bonsairobo]: https://github.com/bonsairobo +[@gferon]: https://github.com/gferon +[@joshka]: https://github.com/joshka +[@kujeger]: https://github.com/kujeger +[@dyc3]: https://github.com/dyc3 +[@ThomWright]: https://github.com/ThomWright +[@duhby]: https://github.com/duhby diff --git a/Cargo.lock b/Cargo.lock index 7a5db0c251..64634c69da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3394,7 +3394,7 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.8.5" +version = "0.8.6" dependencies = [ "anyhow", "async-std", @@ -3424,7 +3424,7 @@ dependencies = [ [[package]] name = "sqlx-cli" -version = "0.8.5" +version = "0.8.6" dependencies = [ "anyhow", "assert_cmd", @@ -3448,7 +3448,7 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.8.5" +version = "0.8.6" dependencies = [ "async-io 1.13.0", "async-std", @@ -3624,7 +3624,7 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.8.5" +version = "0.8.6" dependencies = [ "proc-macro2", "quote", @@ -3635,7 +3635,7 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.8.5" +version = "0.8.6" dependencies = [ "async-std", "dotenvy", @@ -3653,14 +3653,13 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn 2.0.96", - "tempfile", "tokio", "url", ] [[package]] name = "sqlx-mysql" -version = "0.8.5" +version = "0.8.6" dependencies = [ "atoi", "base64 0.22.1", @@ -3706,7 +3705,7 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.8.5" +version = "0.8.6" dependencies = [ "atoi", "base64 0.22.1", @@ -3752,7 +3751,7 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.8.5" +version = "0.8.6" dependencies = [ "atoi", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 1aef121199..2419b50d97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ members = [ ] [workspace.package] -version = "0.8.5" +version = "0.8.6" license = "MIT OR Apache-2.0" edition = "2021" repository = "https://github.com/launchbadge/sqlx" @@ -129,17 +129,17 @@ bstr = ["sqlx-core/bstr"] [workspace.dependencies] # Core Crates -sqlx-core = { version = "=0.8.5", path = "sqlx-core" } -sqlx-macros-core = { version = "=0.8.5", path = "sqlx-macros-core" } -sqlx-macros = { version = "=0.8.5", path = "sqlx-macros" } +sqlx-core = { version = "=0.8.6", path = "sqlx-core" } +sqlx-macros-core = { version = "=0.8.6", path = "sqlx-macros-core" } +sqlx-macros = { version = "=0.8.6", path = "sqlx-macros" } # Driver crates -sqlx-mysql = { version = "=0.8.5", path = "sqlx-mysql" } -sqlx-postgres = { version = "=0.8.5", path = "sqlx-postgres" } -sqlx-sqlite = { version = "=0.8.5", path = "sqlx-sqlite" } +sqlx-mysql = { version = "=0.8.6", path = "sqlx-mysql" } +sqlx-postgres = { version = "=0.8.6", path = "sqlx-postgres" } +sqlx-sqlite = { version = "=0.8.6", path = "sqlx-sqlite" } # Facade crate (for reference from sqlx-cli) -sqlx = { version = "=0.8.5", path = ".", default-features = false } +sqlx = { version = "=0.8.6", path = ".", default-features = false } # Common type integrations shared by multiple driver crates. # These are optional unless enabled in a workspace crate. From 27cfabb14ca85df7b82d28e683bd45baf85899e2 Mon Sep 17 00:00:00 2001 From: Jack Fawthorpe Date: Thu, 3 Jul 2025 03:52:47 +0000 Subject: [PATCH 66/66] fix(sqlx): Add warning message for a detected bad transaction drop --- sqlx-core/src/transaction.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sqlx-core/src/transaction.rs b/sqlx-core/src/transaction.rs index 2a84ff6555..39fe0eecff 100644 --- a/sqlx-core/src/transaction.rs +++ b/sqlx-core/src/transaction.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::fmt::{self, Debug, Formatter}; use std::ops::{Deref, DerefMut}; +use std::backtrace::Backtrace; use futures_core::future::BoxFuture; @@ -268,7 +269,11 @@ where // what this does depends on the database but generally this means we queue a rollback // operation that will happen on the next asynchronous invocation of the underlying // connection (including if the connection is returned to a pool) - + let bt = Backtrace::capture(); + tracing::warn!( + backtrace = ?bt, + "Detected incorrect transaction rollback" + ); DB::TransactionManager::start_rollback(&mut self.connection); } }