chore: sync current workspace changes
Desktop CI Build / Build macOS ARM64 DMG (push) Has been cancelled
Desktop CI Build / Build Windows x64 (push) Has been cancelled
Desktop CI Build / Build macOS ARM64 DMG (push) Has been cancelled
Desktop CI Build / Build Windows x64 (push) Has been cancelled
This commit is contained in:
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"v":1}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "bab1b022bd56a64f9a08b46b36b97c5cff19d77e"
|
||||
},
|
||||
"path_in_vcs": "sqlx-mysql"
|
||||
}
|
||||
Vendored
+238
@@ -0,0 +1,238 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "sqlx-mysql"
|
||||
version = "0.8.6"
|
||||
authors = [
|
||||
"Ryan Leckey <leckey.ryan@gmail.com>",
|
||||
"Austin Bonander <austin.bonander@gmail.com>",
|
||||
"Chloe Ross <orangesnowfox@gmail.com>",
|
||||
"Daniel Akhterov <akhterovd@gmail.com>",
|
||||
]
|
||||
description = "MySQL driver implementation for SQLx. Not for direct use; see the `sqlx` crate for details."
|
||||
documentation = "https://docs.rs/sqlx"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/launchbadge/sqlx"
|
||||
|
||||
[dependencies.atoi]
|
||||
version = "2.0"
|
||||
|
||||
[dependencies.base64]
|
||||
version = "0.22.0"
|
||||
features = ["std"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.bigdecimal]
|
||||
version = "0.4.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.bitflags]
|
||||
version = "2"
|
||||
features = ["serde"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.byteorder]
|
||||
version = "1.4.3"
|
||||
features = ["std"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.bytes]
|
||||
version = "1.1.0"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4.34"
|
||||
features = [
|
||||
"std",
|
||||
"clock",
|
||||
]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.crc]
|
||||
version = "3.0.0"
|
||||
|
||||
[dependencies.digest]
|
||||
version = "0.10.0"
|
||||
features = ["std"]
|
||||
default-features = false
|
||||
|
||||
[dependencies.dotenvy]
|
||||
version = "0.15.5"
|
||||
|
||||
[dependencies.either]
|
||||
version = "1.6.1"
|
||||
|
||||
[dependencies.futures-channel]
|
||||
version = "0.3.19"
|
||||
features = [
|
||||
"sink",
|
||||
"alloc",
|
||||
"std",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.futures-core]
|
||||
version = "0.3.19"
|
||||
default-features = false
|
||||
|
||||
[dependencies.futures-io]
|
||||
version = "0.3.24"
|
||||
|
||||
[dependencies.futures-util]
|
||||
version = "0.3.19"
|
||||
features = [
|
||||
"alloc",
|
||||
"sink",
|
||||
"io",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.generic-array]
|
||||
version = "0.14.4"
|
||||
default-features = false
|
||||
|
||||
[dependencies.hex]
|
||||
version = "0.4.3"
|
||||
|
||||
[dependencies.hkdf]
|
||||
version = "0.12.0"
|
||||
|
||||
[dependencies.hmac]
|
||||
version = "0.12.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.itoa]
|
||||
version = "1.0.1"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4.18"
|
||||
|
||||
[dependencies.md-5]
|
||||
version = "0.10.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.memchr]
|
||||
version = "2.4.1"
|
||||
default-features = false
|
||||
|
||||
[dependencies.once_cell]
|
||||
version = "1.9.0"
|
||||
|
||||
[dependencies.percent-encoding]
|
||||
version = "2.1.0"
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.8.4"
|
||||
features = [
|
||||
"std",
|
||||
"std_rng",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.rsa]
|
||||
version = "0.9"
|
||||
|
||||
[dependencies.rust_decimal]
|
||||
version = "1.26.1"
|
||||
features = ["std"]
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.144"
|
||||
optional = true
|
||||
|
||||
[dependencies.sha1]
|
||||
version = "0.10.1"
|
||||
default-features = false
|
||||
|
||||
[dependencies.sha2]
|
||||
version = "0.10.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.smallvec]
|
||||
version = "1.7.0"
|
||||
|
||||
[dependencies.sqlx-core]
|
||||
version = "=0.8.6"
|
||||
|
||||
[dependencies.stringprep]
|
||||
version = "0.1.2"
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2.0.0"
|
||||
|
||||
[dependencies.time]
|
||||
version = "0.3.36"
|
||||
features = [
|
||||
"formatting",
|
||||
"parsing",
|
||||
"macros",
|
||||
]
|
||||
optional = true
|
||||
|
||||
[dependencies.tracing]
|
||||
version = "0.1.37"
|
||||
features = ["log"]
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.1.2"
|
||||
optional = true
|
||||
|
||||
[dependencies.whoami]
|
||||
version = "1.2.1"
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.sqlx]
|
||||
version = "=0.8.6"
|
||||
features = ["mysql"]
|
||||
default-features = false
|
||||
|
||||
[features]
|
||||
any = ["sqlx-core/any"]
|
||||
bigdecimal = [
|
||||
"dep:bigdecimal",
|
||||
"sqlx-core/bigdecimal",
|
||||
]
|
||||
chrono = [
|
||||
"dep:chrono",
|
||||
"sqlx-core/chrono",
|
||||
]
|
||||
json = [
|
||||
"sqlx-core/json",
|
||||
"serde",
|
||||
]
|
||||
migrate = ["sqlx-core/migrate"]
|
||||
offline = [
|
||||
"sqlx-core/offline",
|
||||
"serde/derive",
|
||||
]
|
||||
rust_decimal = [
|
||||
"dep:rust_decimal",
|
||||
"rust_decimal/maths",
|
||||
"sqlx-core/rust_decimal",
|
||||
]
|
||||
time = [
|
||||
"dep:time",
|
||||
"sqlx-core/time",
|
||||
]
|
||||
uuid = [
|
||||
"dep:uuid",
|
||||
"sqlx-core/uuid",
|
||||
]
|
||||
|
||||
[lints.clippy]
|
||||
cast_possible_truncation = "deny"
|
||||
cast_possible_wrap = "deny"
|
||||
cast_sign_loss = "deny"
|
||||
disallowed_methods = "deny"
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
[package]
|
||||
name = "sqlx-mysql"
|
||||
documentation = "https://docs.rs/sqlx"
|
||||
description = "MySQL driver implementation for SQLx. Not for direct use; see the `sqlx` crate for details."
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
json = ["sqlx-core/json", "serde"]
|
||||
any = ["sqlx-core/any"]
|
||||
offline = ["sqlx-core/offline", "serde/derive"]
|
||||
migrate = ["sqlx-core/migrate"]
|
||||
|
||||
# Type Integration features
|
||||
bigdecimal = ["dep:bigdecimal", "sqlx-core/bigdecimal"]
|
||||
chrono = ["dep:chrono", "sqlx-core/chrono"]
|
||||
rust_decimal = ["dep:rust_decimal", "rust_decimal/maths", "sqlx-core/rust_decimal"]
|
||||
time = ["dep:time", "sqlx-core/time"]
|
||||
uuid = ["dep:uuid", "sqlx-core/uuid"]
|
||||
|
||||
[dependencies]
|
||||
sqlx-core = { workspace = true }
|
||||
|
||||
# Futures crates
|
||||
futures-channel = { version = "0.3.19", default-features = false, features = ["sink", "alloc", "std"] }
|
||||
futures-core = { version = "0.3.19", default-features = false }
|
||||
futures-io = "0.3.24"
|
||||
futures-util = { version = "0.3.19", default-features = false, features = ["alloc", "sink", "io"] }
|
||||
|
||||
# Cryptographic Primitives
|
||||
crc = "3.0.0"
|
||||
digest = { version = "0.10.0", default-features = false, features = ["std"] }
|
||||
hkdf = "0.12.0"
|
||||
hmac = { version = "0.12.0", default-features = false }
|
||||
md-5 = { version = "0.10.0", default-features = false }
|
||||
rand = { version = "0.8.4", default-features = false, features = ["std", "std_rng"] }
|
||||
rsa = "0.9"
|
||||
sha1 = { version = "0.10.1", default-features = false }
|
||||
sha2 = { version = "0.10.0", default-features = false }
|
||||
|
||||
# Type Integrations (versions inherited from `[workspace.dependencies]`)
|
||||
bigdecimal = { workspace = true, optional = true }
|
||||
chrono = { workspace = true, optional = true }
|
||||
rust_decimal = { workspace = true, optional = true }
|
||||
time = { workspace = true, optional = true }
|
||||
uuid = { workspace = true, optional = true }
|
||||
|
||||
# Misc
|
||||
atoi = "2.0"
|
||||
base64 = { version = "0.22.0", default-features = false, features = ["std"] }
|
||||
bitflags = { version = "2", default-features = false, features = ["serde"] }
|
||||
byteorder = { version = "1.4.3", default-features = false, features = ["std"] }
|
||||
bytes = "1.1.0"
|
||||
dotenvy = "0.15.5"
|
||||
either = "1.6.1"
|
||||
generic-array = { version = "0.14.4", default-features = false }
|
||||
hex = "0.4.3"
|
||||
itoa = "1.0.1"
|
||||
log = "0.4.18"
|
||||
memchr = { version = "2.4.1", default-features = false }
|
||||
once_cell = "1.9.0"
|
||||
percent-encoding = "2.1.0"
|
||||
smallvec = "1.7.0"
|
||||
stringprep = "0.1.2"
|
||||
thiserror = "2.0.0"
|
||||
tracing = { version = "0.1.37", features = ["log"] }
|
||||
whoami = { version = "1.2.1", default-features = false }
|
||||
|
||||
serde = { version = "1.0.144", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
sqlx = { workspace = true, features = ["mysql"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
Vendored
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2020 LaunchBadge, LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
Copyright (c) 2020 LaunchBadge, LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+238
@@ -0,0 +1,238 @@
|
||||
use crate::protocol::text::{ColumnFlags, ColumnType};
|
||||
use crate::{
|
||||
MySql, MySqlColumn, MySqlConnectOptions, MySqlConnection, MySqlQueryResult, MySqlRow,
|
||||
MySqlTransactionManager, MySqlTypeInfo,
|
||||
};
|
||||
use either::Either;
|
||||
use futures_core::future::BoxFuture;
|
||||
use futures_core::stream::BoxStream;
|
||||
use futures_util::{stream, StreamExt, TryFutureExt, TryStreamExt};
|
||||
use sqlx_core::any::{
|
||||
Any, AnyArguments, AnyColumn, AnyConnectOptions, AnyConnectionBackend, AnyQueryResult, AnyRow,
|
||||
AnyStatement, AnyTypeInfo, AnyTypeInfoKind,
|
||||
};
|
||||
use sqlx_core::connection::Connection;
|
||||
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);
|
||||
|
||||
impl AnyConnectionBackend for MySqlConnection {
|
||||
fn name(&self) -> &str {
|
||||
<MySql as Database>::NAME
|
||||
}
|
||||
|
||||
fn close(self: Box<Self>) -> BoxFuture<'static, sqlx_core::Result<()>> {
|
||||
Connection::close(*self)
|
||||
}
|
||||
|
||||
fn close_hard(self: Box<Self>) -> BoxFuture<'static, sqlx_core::Result<()>> {
|
||||
Connection::close_hard(*self)
|
||||
}
|
||||
|
||||
fn ping(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> {
|
||||
Connection::ping(self)
|
||||
}
|
||||
|
||||
fn begin(
|
||||
&mut self,
|
||||
statement: Option<Cow<'static, str>>,
|
||||
) -> BoxFuture<'_, sqlx_core::Result<()>> {
|
||||
MySqlTransactionManager::begin(self, statement)
|
||||
}
|
||||
|
||||
fn commit(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> {
|
||||
MySqlTransactionManager::commit(self)
|
||||
}
|
||||
|
||||
fn rollback(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> {
|
||||
MySqlTransactionManager::rollback(self)
|
||||
}
|
||||
|
||||
fn start_rollback(&mut self) {
|
||||
MySqlTransactionManager::start_rollback(self)
|
||||
}
|
||||
|
||||
fn get_transaction_depth(&self) -> usize {
|
||||
MySqlTransactionManager::get_transaction_depth(self)
|
||||
}
|
||||
|
||||
fn shrink_buffers(&mut self) {
|
||||
Connection::shrink_buffers(self);
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> BoxFuture<'_, sqlx_core::Result<()>> {
|
||||
Connection::flush(self)
|
||||
}
|
||||
|
||||
fn should_flush(&self) -> bool {
|
||||
Connection::should_flush(self)
|
||||
}
|
||||
|
||||
#[cfg(feature = "migrate")]
|
||||
fn as_migrate(
|
||||
&mut self,
|
||||
) -> sqlx_core::Result<&mut (dyn sqlx_core::migrate::Migrate + Send + 'static)> {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn fetch_many<'q>(
|
||||
&'q mut self,
|
||||
query: &'q str,
|
||||
persistent: bool,
|
||||
arguments: Option<AnyArguments<'q>>,
|
||||
) -> BoxStream<'q, sqlx_core::Result<Either<AnyQueryResult, AnyRow>>> {
|
||||
let persistent = persistent && arguments.is_some();
|
||||
let arguments = match arguments.as_ref().map(AnyArguments::convert_to).transpose() {
|
||||
Ok(arguments) => arguments,
|
||||
Err(error) => {
|
||||
return stream::once(future::ready(Err(sqlx_core::Error::Encode(error)))).boxed()
|
||||
}
|
||||
};
|
||||
|
||||
Box::pin(
|
||||
self.run(query, arguments, persistent)
|
||||
.try_flatten_stream()
|
||||
.map(|res| {
|
||||
Ok(match res? {
|
||||
Either::Left(result) => Either::Left(map_result(result)),
|
||||
Either::Right(row) => Either::Right(AnyRow::try_from(&row)?),
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn fetch_optional<'q>(
|
||||
&'q mut self,
|
||||
query: &'q str,
|
||||
persistent: bool,
|
||||
arguments: Option<AnyArguments<'q>>,
|
||||
) -> BoxFuture<'q, sqlx_core::Result<Option<AnyRow>>> {
|
||||
let persistent = persistent && arguments.is_some();
|
||||
let arguments = arguments
|
||||
.as_ref()
|
||||
.map(AnyArguments::convert_to)
|
||||
.transpose()
|
||||
.map_err(sqlx_core::Error::Encode);
|
||||
|
||||
Box::pin(async move {
|
||||
let arguments = arguments?;
|
||||
let mut stream = pin!(self.run(query, arguments, persistent).await?);
|
||||
|
||||
while let Some(result) = stream.try_next().await? {
|
||||
if let Either::Right(row) = result {
|
||||
return Ok(Some(AnyRow::try_from(&row)?));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_with<'c, 'q: 'c>(
|
||||
&'c mut self,
|
||||
sql: &'q str,
|
||||
_parameters: &[AnyTypeInfo],
|
||||
) -> BoxFuture<'c, sqlx_core::Result<AnyStatement<'q>>> {
|
||||
Box::pin(async move {
|
||||
let statement = Executor::prepare_with(self, sql, &[]).await?;
|
||||
AnyStatement::try_from_statement(
|
||||
sql,
|
||||
&statement,
|
||||
statement.metadata.column_names.clone(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn describe<'q>(&'q mut self, sql: &'q str) -> BoxFuture<'q, sqlx_core::Result<Describe<Any>>> {
|
||||
Box::pin(async move {
|
||||
let describe = Executor::describe(self, sql).await?;
|
||||
describe.try_into_any()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a MySqlTypeInfo> for AnyTypeInfo {
|
||||
type Error = sqlx_core::Error;
|
||||
|
||||
fn try_from(type_info: &'a MySqlTypeInfo) -> Result<Self, Self::Error> {
|
||||
Ok(AnyTypeInfo {
|
||||
kind: match &type_info.r#type {
|
||||
ColumnType::Null => AnyTypeInfoKind::Null,
|
||||
// BoosAPI reserves TINYINT(1) for booleans in its MySQL schema.
|
||||
// SQLx Any otherwise rejects MySQL BOOLEAN columns entirely.
|
||||
ColumnType::Tiny => AnyTypeInfoKind::Bool,
|
||||
ColumnType::Short => AnyTypeInfoKind::SmallInt,
|
||||
ColumnType::Long => AnyTypeInfoKind::Integer,
|
||||
ColumnType::LongLong => AnyTypeInfoKind::BigInt,
|
||||
ColumnType::Float => AnyTypeInfoKind::Real,
|
||||
ColumnType::Double => AnyTypeInfoKind::Double,
|
||||
ColumnType::Blob
|
||||
| ColumnType::TinyBlob
|
||||
| ColumnType::MediumBlob
|
||||
| ColumnType::LongBlob
|
||||
if type_info.flags.contains(ColumnFlags::BINARY) =>
|
||||
{
|
||||
AnyTypeInfoKind::Blob
|
||||
}
|
||||
ColumnType::Blob
|
||||
| ColumnType::TinyBlob
|
||||
| ColumnType::MediumBlob
|
||||
| ColumnType::LongBlob => AnyTypeInfoKind::Text,
|
||||
ColumnType::String | ColumnType::VarString | ColumnType::VarChar => {
|
||||
AnyTypeInfoKind::Text
|
||||
}
|
||||
_ => {
|
||||
return Err(sqlx_core::Error::AnyDriverError(
|
||||
format!("Any driver does not support MySql type {type_info:?}").into(),
|
||||
))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a MySqlColumn> for AnyColumn {
|
||||
type Error = sqlx_core::Error;
|
||||
|
||||
fn try_from(column: &'a MySqlColumn) -> Result<Self, Self::Error> {
|
||||
let type_info = AnyTypeInfo::try_from(&column.type_info)?;
|
||||
|
||||
Ok(AnyColumn {
|
||||
ordinal: column.ordinal,
|
||||
name: column.name.clone(),
|
||||
type_info,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a MySqlRow> for AnyRow {
|
||||
type Error = sqlx_core::Error;
|
||||
|
||||
fn try_from(row: &'a MySqlRow) -> Result<Self, Self::Error> {
|
||||
AnyRow::map_from(row, row.column_names.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a AnyConnectOptions> for MySqlConnectOptions {
|
||||
type Error = sqlx_core::Error;
|
||||
|
||||
fn try_from(any_opts: &'a AnyConnectOptions) -> Result<Self, Self::Error> {
|
||||
let mut opts = Self::parse_from_url(&any_opts.database_url)?;
|
||||
opts.log_settings = any_opts.log_settings.clone();
|
||||
Ok(opts)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_result(result: MySqlQueryResult) -> AnyQueryResult {
|
||||
AnyQueryResult {
|
||||
rows_affected: result.rows_affected,
|
||||
// Don't expect this to be a problem
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
last_insert_id: Some(result.last_insert_id as i64),
|
||||
}
|
||||
}
|
||||
Vendored
+108
@@ -0,0 +1,108 @@
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::types::Type;
|
||||
use crate::{MySql, MySqlTypeInfo};
|
||||
pub(crate) use sqlx_core::arguments::*;
|
||||
use sqlx_core::error::BoxDynError;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Implementation of [`Arguments`] for MySQL.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MySqlArguments {
|
||||
pub(crate) values: Vec<u8>,
|
||||
pub(crate) types: Vec<MySqlTypeInfo>,
|
||||
pub(crate) null_bitmap: NullBitMap,
|
||||
}
|
||||
|
||||
impl MySqlArguments {
|
||||
pub(crate) fn add<'q, T>(&mut self, value: T) -> Result<(), BoxDynError>
|
||||
where
|
||||
T: Encode<'q, MySql> + Type<MySql>,
|
||||
{
|
||||
let ty = value.produces().unwrap_or_else(T::type_info);
|
||||
|
||||
let value_length_before_encoding = self.values.len();
|
||||
let is_null = match value.encode(&mut self.values) {
|
||||
Ok(is_null) => is_null,
|
||||
Err(error) => {
|
||||
// reset the value buffer to its previous value if encoding failed so we don't leave a half-encoded value behind
|
||||
self.values.truncate(value_length_before_encoding);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
self.types.push(ty);
|
||||
self.null_bitmap.push(is_null);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q> Arguments<'q> for MySqlArguments {
|
||||
type Database = MySql;
|
||||
|
||||
fn reserve(&mut self, len: usize, size: usize) {
|
||||
self.types.reserve(len);
|
||||
self.values.reserve(size);
|
||||
}
|
||||
|
||||
fn add<T>(&mut self, value: T) -> Result<(), BoxDynError>
|
||||
where
|
||||
T: Encode<'q, Self::Database> + Type<Self::Database>,
|
||||
{
|
||||
self.add(value)
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.types.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct NullBitMap {
|
||||
bytes: Vec<u8>,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl NullBitMap {
|
||||
fn push(&mut self, is_null: IsNull) {
|
||||
let byte_index = self.length / (u8::BITS as usize);
|
||||
let bit_offset = self.length % (u8::BITS as usize);
|
||||
|
||||
if bit_offset == 0 {
|
||||
self.bytes.push(0);
|
||||
}
|
||||
|
||||
self.bytes[byte_index] |= u8::from(is_null.is_null()) << bit_offset;
|
||||
self.length += 1;
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for NullBitMap {
|
||||
type Target = [u8];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.bytes
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn null_bit_map_should_push_is_null() {
|
||||
let mut bit_map = NullBitMap::default();
|
||||
|
||||
bit_map.push(IsNull::Yes);
|
||||
bit_map.push(IsNull::No);
|
||||
bit_map.push(IsNull::Yes);
|
||||
bit_map.push(IsNull::No);
|
||||
bit_map.push(IsNull::Yes);
|
||||
bit_map.push(IsNull::No);
|
||||
bit_map.push(IsNull::Yes);
|
||||
bit_map.push(IsNull::No);
|
||||
bit_map.push(IsNull::Yes);
|
||||
|
||||
assert_eq!([0b01010101, 0b1].as_slice(), bit_map.deref());
|
||||
}
|
||||
}
|
||||
Vendored
+900
@@ -0,0 +1,900 @@
|
||||
use crate::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) enum CharSet {
|
||||
armscii8,
|
||||
ascii,
|
||||
big5,
|
||||
binary,
|
||||
cp1250,
|
||||
cp1251,
|
||||
cp1256,
|
||||
cp1257,
|
||||
cp850,
|
||||
cp852,
|
||||
cp866,
|
||||
cp932,
|
||||
dec8,
|
||||
eucjpms,
|
||||
euckr,
|
||||
gb18030,
|
||||
gb2312,
|
||||
gbk,
|
||||
geostd8,
|
||||
greek,
|
||||
hebrew,
|
||||
hp8,
|
||||
keybcs2,
|
||||
koi8r,
|
||||
koi8u,
|
||||
latin1,
|
||||
latin2,
|
||||
latin5,
|
||||
latin7,
|
||||
macce,
|
||||
macroman,
|
||||
sjis,
|
||||
swe7,
|
||||
tis620,
|
||||
ucs2,
|
||||
ujis,
|
||||
utf16,
|
||||
utf16le,
|
||||
utf32,
|
||||
utf8,
|
||||
utf8mb4,
|
||||
}
|
||||
|
||||
impl CharSet {
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
CharSet::armscii8 => "armscii8",
|
||||
CharSet::ascii => "ascii",
|
||||
CharSet::big5 => "big5",
|
||||
CharSet::binary => "binary",
|
||||
CharSet::cp1250 => "cp1250",
|
||||
CharSet::cp1251 => "cp1251",
|
||||
CharSet::cp1256 => "cp1256",
|
||||
CharSet::cp1257 => "cp1257",
|
||||
CharSet::cp850 => "cp850",
|
||||
CharSet::cp852 => "cp852",
|
||||
CharSet::cp866 => "cp866",
|
||||
CharSet::cp932 => "cp932",
|
||||
CharSet::dec8 => "dec8",
|
||||
CharSet::eucjpms => "eucjpms",
|
||||
CharSet::euckr => "euckr",
|
||||
CharSet::gb18030 => "gb18030",
|
||||
CharSet::gb2312 => "gb2312",
|
||||
CharSet::gbk => "gbk",
|
||||
CharSet::geostd8 => "geostd8",
|
||||
CharSet::greek => "greek",
|
||||
CharSet::hebrew => "hebrew",
|
||||
CharSet::hp8 => "hp8",
|
||||
CharSet::keybcs2 => "keybcs2",
|
||||
CharSet::koi8r => "koi8r",
|
||||
CharSet::koi8u => "koi8u",
|
||||
CharSet::latin1 => "latin1",
|
||||
CharSet::latin2 => "latin2",
|
||||
CharSet::latin5 => "latin5",
|
||||
CharSet::latin7 => "latin7",
|
||||
CharSet::macce => "macce",
|
||||
CharSet::macroman => "macroman",
|
||||
CharSet::sjis => "sjis",
|
||||
CharSet::swe7 => "swe7",
|
||||
CharSet::tis620 => "tis620",
|
||||
CharSet::ucs2 => "ucs2",
|
||||
CharSet::ujis => "ujis",
|
||||
CharSet::utf16 => "utf16",
|
||||
CharSet::utf16le => "utf16le",
|
||||
CharSet::utf32 => "utf32",
|
||||
CharSet::utf8 => "utf8",
|
||||
CharSet::utf8mb4 => "utf8mb4",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn default_collation(&self) -> Collation {
|
||||
match self {
|
||||
CharSet::armscii8 => Collation::armscii8_general_ci,
|
||||
CharSet::ascii => Collation::ascii_general_ci,
|
||||
CharSet::big5 => Collation::big5_chinese_ci,
|
||||
CharSet::binary => Collation::binary,
|
||||
CharSet::cp1250 => Collation::cp1250_general_ci,
|
||||
CharSet::cp1251 => Collation::cp1251_general_ci,
|
||||
CharSet::cp1256 => Collation::cp1256_general_ci,
|
||||
CharSet::cp1257 => Collation::cp1257_general_ci,
|
||||
CharSet::cp850 => Collation::cp850_general_ci,
|
||||
CharSet::cp852 => Collation::cp852_general_ci,
|
||||
CharSet::cp866 => Collation::cp866_general_ci,
|
||||
CharSet::cp932 => Collation::cp932_japanese_ci,
|
||||
CharSet::dec8 => Collation::dec8_swedish_ci,
|
||||
CharSet::eucjpms => Collation::eucjpms_japanese_ci,
|
||||
CharSet::euckr => Collation::euckr_korean_ci,
|
||||
CharSet::gb18030 => Collation::gb18030_chinese_ci,
|
||||
CharSet::gb2312 => Collation::gb2312_chinese_ci,
|
||||
CharSet::gbk => Collation::gbk_chinese_ci,
|
||||
CharSet::geostd8 => Collation::geostd8_general_ci,
|
||||
CharSet::greek => Collation::greek_general_ci,
|
||||
CharSet::hebrew => Collation::hebrew_general_ci,
|
||||
CharSet::hp8 => Collation::hp8_english_ci,
|
||||
CharSet::keybcs2 => Collation::keybcs2_general_ci,
|
||||
CharSet::koi8r => Collation::koi8r_general_ci,
|
||||
CharSet::koi8u => Collation::koi8u_general_ci,
|
||||
CharSet::latin1 => Collation::latin1_swedish_ci,
|
||||
CharSet::latin2 => Collation::latin2_general_ci,
|
||||
CharSet::latin5 => Collation::latin5_turkish_ci,
|
||||
CharSet::latin7 => Collation::latin7_general_ci,
|
||||
CharSet::macce => Collation::macce_general_ci,
|
||||
CharSet::macroman => Collation::macroman_general_ci,
|
||||
CharSet::sjis => Collation::sjis_japanese_ci,
|
||||
CharSet::swe7 => Collation::swe7_swedish_ci,
|
||||
CharSet::tis620 => Collation::tis620_thai_ci,
|
||||
CharSet::ucs2 => Collation::ucs2_general_ci,
|
||||
CharSet::ujis => Collation::ujis_japanese_ci,
|
||||
CharSet::utf16 => Collation::utf16_general_ci,
|
||||
CharSet::utf16le => Collation::utf16le_general_ci,
|
||||
CharSet::utf32 => Collation::utf32_general_ci,
|
||||
CharSet::utf8 => Collation::utf8_unicode_ci,
|
||||
CharSet::utf8mb4 => Collation::utf8mb4_unicode_ci,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CharSet {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(char_set: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match char_set {
|
||||
"armscii8" => CharSet::armscii8,
|
||||
"ascii" => CharSet::ascii,
|
||||
"big5" => CharSet::big5,
|
||||
"binary" => CharSet::binary,
|
||||
"cp1250" => CharSet::cp1250,
|
||||
"cp1251" => CharSet::cp1251,
|
||||
"cp1256" => CharSet::cp1256,
|
||||
"cp1257" => CharSet::cp1257,
|
||||
"cp850" => CharSet::cp850,
|
||||
"cp852" => CharSet::cp852,
|
||||
"cp866" => CharSet::cp866,
|
||||
"cp932" => CharSet::cp932,
|
||||
"dec8" => CharSet::dec8,
|
||||
"eucjpms" => CharSet::eucjpms,
|
||||
"euckr" => CharSet::euckr,
|
||||
"gb18030" => CharSet::gb18030,
|
||||
"gb2312" => CharSet::gb2312,
|
||||
"gbk" => CharSet::gbk,
|
||||
"geostd8" => CharSet::geostd8,
|
||||
"greek" => CharSet::greek,
|
||||
"hebrew" => CharSet::hebrew,
|
||||
"hp8" => CharSet::hp8,
|
||||
"keybcs2" => CharSet::keybcs2,
|
||||
"koi8r" => CharSet::koi8r,
|
||||
"koi8u" => CharSet::koi8u,
|
||||
"latin1" => CharSet::latin1,
|
||||
"latin2" => CharSet::latin2,
|
||||
"latin5" => CharSet::latin5,
|
||||
"latin7" => CharSet::latin7,
|
||||
"macce" => CharSet::macce,
|
||||
"macroman" => CharSet::macroman,
|
||||
"sjis" => CharSet::sjis,
|
||||
"swe7" => CharSet::swe7,
|
||||
"tis620" => CharSet::tis620,
|
||||
"ucs2" => CharSet::ucs2,
|
||||
"ujis" => CharSet::ujis,
|
||||
"utf16" => CharSet::utf16,
|
||||
"utf16le" => CharSet::utf16le,
|
||||
"utf32" => CharSet::utf32,
|
||||
"utf8" => CharSet::utf8,
|
||||
"utf8mb4" => CharSet::utf8mb4,
|
||||
|
||||
_ => {
|
||||
return Err(Error::Configuration(
|
||||
format!("unsupported MySQL charset: {char_set}").into(),
|
||||
));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[allow(non_camel_case_types)]
|
||||
#[repr(u8)]
|
||||
pub(crate) enum Collation {
|
||||
armscii8_bin = 64,
|
||||
armscii8_general_ci = 32,
|
||||
ascii_bin = 65,
|
||||
ascii_general_ci = 11,
|
||||
big5_bin = 84,
|
||||
big5_chinese_ci = 1,
|
||||
binary = 63,
|
||||
cp1250_bin = 66,
|
||||
cp1250_croatian_ci = 44,
|
||||
cp1250_czech_cs = 34,
|
||||
cp1250_general_ci = 26,
|
||||
cp1250_polish_ci = 99,
|
||||
cp1251_bin = 50,
|
||||
cp1251_bulgarian_ci = 14,
|
||||
cp1251_general_ci = 51,
|
||||
cp1251_general_cs = 52,
|
||||
cp1251_ukrainian_ci = 23,
|
||||
cp1256_bin = 67,
|
||||
cp1256_general_ci = 57,
|
||||
cp1257_bin = 58,
|
||||
cp1257_general_ci = 59,
|
||||
cp1257_lithuanian_ci = 29,
|
||||
cp850_bin = 80,
|
||||
cp850_general_ci = 4,
|
||||
cp852_bin = 81,
|
||||
cp852_general_ci = 40,
|
||||
cp866_bin = 68,
|
||||
cp866_general_ci = 36,
|
||||
cp932_bin = 96,
|
||||
cp932_japanese_ci = 95,
|
||||
dec8_bin = 69,
|
||||
dec8_swedish_ci = 3,
|
||||
eucjpms_bin = 98,
|
||||
eucjpms_japanese_ci = 97,
|
||||
euckr_bin = 85,
|
||||
euckr_korean_ci = 19,
|
||||
gb18030_bin = 249,
|
||||
gb18030_chinese_ci = 248,
|
||||
gb18030_unicode_520_ci = 250,
|
||||
gb2312_bin = 86,
|
||||
gb2312_chinese_ci = 24,
|
||||
gbk_bin = 87,
|
||||
gbk_chinese_ci = 28,
|
||||
geostd8_bin = 93,
|
||||
geostd8_general_ci = 92,
|
||||
greek_bin = 70,
|
||||
greek_general_ci = 25,
|
||||
hebrew_bin = 71,
|
||||
hebrew_general_ci = 16,
|
||||
hp8_bin = 72,
|
||||
hp8_english_ci = 6,
|
||||
keybcs2_bin = 73,
|
||||
keybcs2_general_ci = 37,
|
||||
koi8r_bin = 74,
|
||||
koi8r_general_ci = 7,
|
||||
koi8u_bin = 75,
|
||||
koi8u_general_ci = 22,
|
||||
latin1_bin = 47,
|
||||
latin1_danish_ci = 15,
|
||||
latin1_general_ci = 48,
|
||||
latin1_general_cs = 49,
|
||||
latin1_german1_ci = 5,
|
||||
latin1_german2_ci = 31,
|
||||
latin1_spanish_ci = 94,
|
||||
latin1_swedish_ci = 8,
|
||||
latin2_bin = 77,
|
||||
latin2_croatian_ci = 27,
|
||||
latin2_czech_cs = 2,
|
||||
latin2_general_ci = 9,
|
||||
latin2_hungarian_ci = 21,
|
||||
latin5_bin = 78,
|
||||
latin5_turkish_ci = 30,
|
||||
latin7_bin = 79,
|
||||
latin7_estonian_cs = 20,
|
||||
latin7_general_ci = 41,
|
||||
latin7_general_cs = 42,
|
||||
macce_bin = 43,
|
||||
macce_general_ci = 38,
|
||||
macroman_bin = 53,
|
||||
macroman_general_ci = 39,
|
||||
sjis_bin = 88,
|
||||
sjis_japanese_ci = 13,
|
||||
swe7_bin = 82,
|
||||
swe7_swedish_ci = 10,
|
||||
tis620_bin = 89,
|
||||
tis620_thai_ci = 18,
|
||||
ucs2_bin = 90,
|
||||
ucs2_croatian_ci = 149,
|
||||
ucs2_czech_ci = 138,
|
||||
ucs2_danish_ci = 139,
|
||||
ucs2_esperanto_ci = 145,
|
||||
ucs2_estonian_ci = 134,
|
||||
ucs2_general_ci = 35,
|
||||
ucs2_general_mysql500_ci = 159,
|
||||
ucs2_german2_ci = 148,
|
||||
ucs2_hungarian_ci = 146,
|
||||
ucs2_icelandic_ci = 129,
|
||||
ucs2_latvian_ci = 130,
|
||||
ucs2_lithuanian_ci = 140,
|
||||
ucs2_persian_ci = 144,
|
||||
ucs2_polish_ci = 133,
|
||||
ucs2_roman_ci = 143,
|
||||
ucs2_romanian_ci = 131,
|
||||
ucs2_sinhala_ci = 147,
|
||||
ucs2_slovak_ci = 141,
|
||||
ucs2_slovenian_ci = 132,
|
||||
ucs2_spanish_ci = 135,
|
||||
ucs2_spanish2_ci = 142,
|
||||
ucs2_swedish_ci = 136,
|
||||
ucs2_turkish_ci = 137,
|
||||
ucs2_unicode_520_ci = 150,
|
||||
ucs2_unicode_ci = 128,
|
||||
ucs2_vietnamese_ci = 151,
|
||||
ujis_bin = 91,
|
||||
ujis_japanese_ci = 12,
|
||||
utf16_bin = 55,
|
||||
utf16_croatian_ci = 122,
|
||||
utf16_czech_ci = 111,
|
||||
utf16_danish_ci = 112,
|
||||
utf16_esperanto_ci = 118,
|
||||
utf16_estonian_ci = 107,
|
||||
utf16_general_ci = 54,
|
||||
utf16_german2_ci = 121,
|
||||
utf16_hungarian_ci = 119,
|
||||
utf16_icelandic_ci = 102,
|
||||
utf16_latvian_ci = 103,
|
||||
utf16_lithuanian_ci = 113,
|
||||
utf16_persian_ci = 117,
|
||||
utf16_polish_ci = 106,
|
||||
utf16_roman_ci = 116,
|
||||
utf16_romanian_ci = 104,
|
||||
utf16_sinhala_ci = 120,
|
||||
utf16_slovak_ci = 114,
|
||||
utf16_slovenian_ci = 105,
|
||||
utf16_spanish_ci = 108,
|
||||
utf16_spanish2_ci = 115,
|
||||
utf16_swedish_ci = 109,
|
||||
utf16_turkish_ci = 110,
|
||||
utf16_unicode_520_ci = 123,
|
||||
utf16_unicode_ci = 101,
|
||||
utf16_vietnamese_ci = 124,
|
||||
utf16le_bin = 62,
|
||||
utf16le_general_ci = 56,
|
||||
utf32_bin = 61,
|
||||
utf32_croatian_ci = 181,
|
||||
utf32_czech_ci = 170,
|
||||
utf32_danish_ci = 171,
|
||||
utf32_esperanto_ci = 177,
|
||||
utf32_estonian_ci = 166,
|
||||
utf32_general_ci = 60,
|
||||
utf32_german2_ci = 180,
|
||||
utf32_hungarian_ci = 178,
|
||||
utf32_icelandic_ci = 161,
|
||||
utf32_latvian_ci = 162,
|
||||
utf32_lithuanian_ci = 172,
|
||||
utf32_persian_ci = 176,
|
||||
utf32_polish_ci = 165,
|
||||
utf32_roman_ci = 175,
|
||||
utf32_romanian_ci = 163,
|
||||
utf32_sinhala_ci = 179,
|
||||
utf32_slovak_ci = 173,
|
||||
utf32_slovenian_ci = 164,
|
||||
utf32_spanish_ci = 167,
|
||||
utf32_spanish2_ci = 174,
|
||||
utf32_swedish_ci = 168,
|
||||
utf32_turkish_ci = 169,
|
||||
utf32_unicode_520_ci = 182,
|
||||
utf32_unicode_ci = 160,
|
||||
utf32_vietnamese_ci = 183,
|
||||
utf8_bin = 83,
|
||||
utf8_croatian_ci = 213,
|
||||
utf8_czech_ci = 202,
|
||||
utf8_danish_ci = 203,
|
||||
utf8_esperanto_ci = 209,
|
||||
utf8_estonian_ci = 198,
|
||||
utf8_general_ci = 33,
|
||||
utf8_general_mysql500_ci = 223,
|
||||
utf8_german2_ci = 212,
|
||||
utf8_hungarian_ci = 210,
|
||||
utf8_icelandic_ci = 193,
|
||||
utf8_latvian_ci = 194,
|
||||
utf8_lithuanian_ci = 204,
|
||||
utf8_persian_ci = 208,
|
||||
utf8_polish_ci = 197,
|
||||
utf8_roman_ci = 207,
|
||||
utf8_romanian_ci = 195,
|
||||
utf8_sinhala_ci = 211,
|
||||
utf8_slovak_ci = 205,
|
||||
utf8_slovenian_ci = 196,
|
||||
utf8_spanish_ci = 199,
|
||||
utf8_spanish2_ci = 206,
|
||||
utf8_swedish_ci = 200,
|
||||
utf8_tolower_ci = 76,
|
||||
utf8_turkish_ci = 201,
|
||||
utf8_unicode_520_ci = 214,
|
||||
utf8_unicode_ci = 192,
|
||||
utf8_vietnamese_ci = 215,
|
||||
utf8mb4_0900_ai_ci = 255,
|
||||
utf8mb4_bin = 46,
|
||||
utf8mb4_croatian_ci = 245,
|
||||
utf8mb4_czech_ci = 234,
|
||||
utf8mb4_danish_ci = 235,
|
||||
utf8mb4_esperanto_ci = 241,
|
||||
utf8mb4_estonian_ci = 230,
|
||||
utf8mb4_general_ci = 45,
|
||||
utf8mb4_german2_ci = 244,
|
||||
utf8mb4_hungarian_ci = 242,
|
||||
utf8mb4_icelandic_ci = 225,
|
||||
utf8mb4_latvian_ci = 226,
|
||||
utf8mb4_lithuanian_ci = 236,
|
||||
utf8mb4_persian_ci = 240,
|
||||
utf8mb4_polish_ci = 229,
|
||||
utf8mb4_roman_ci = 239,
|
||||
utf8mb4_romanian_ci = 227,
|
||||
utf8mb4_sinhala_ci = 243,
|
||||
utf8mb4_slovak_ci = 237,
|
||||
utf8mb4_slovenian_ci = 228,
|
||||
utf8mb4_spanish_ci = 231,
|
||||
utf8mb4_spanish2_ci = 238,
|
||||
utf8mb4_swedish_ci = 232,
|
||||
utf8mb4_turkish_ci = 233,
|
||||
utf8mb4_unicode_520_ci = 246,
|
||||
utf8mb4_unicode_ci = 224,
|
||||
utf8mb4_vietnamese_ci = 247,
|
||||
}
|
||||
|
||||
impl Collation {
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Collation::armscii8_bin => "armscii8_bin",
|
||||
Collation::armscii8_general_ci => "armscii8_general_ci",
|
||||
Collation::ascii_bin => "ascii_bin",
|
||||
Collation::ascii_general_ci => "ascii_general_ci",
|
||||
Collation::big5_bin => "big5_bin",
|
||||
Collation::big5_chinese_ci => "big5_chinese_ci",
|
||||
Collation::binary => "binary",
|
||||
Collation::cp1250_bin => "cp1250_bin",
|
||||
Collation::cp1250_croatian_ci => "cp1250_croatian_ci",
|
||||
Collation::cp1250_czech_cs => "cp1250_czech_cs",
|
||||
Collation::cp1250_general_ci => "cp1250_general_ci",
|
||||
Collation::cp1250_polish_ci => "cp1250_polish_ci",
|
||||
Collation::cp1251_bin => "cp1251_bin",
|
||||
Collation::cp1251_bulgarian_ci => "cp1251_bulgarian_ci",
|
||||
Collation::cp1251_general_ci => "cp1251_general_ci",
|
||||
Collation::cp1251_general_cs => "cp1251_general_cs",
|
||||
Collation::cp1251_ukrainian_ci => "cp1251_ukrainian_ci",
|
||||
Collation::cp1256_bin => "cp1256_bin",
|
||||
Collation::cp1256_general_ci => "cp1256_general_ci",
|
||||
Collation::cp1257_bin => "cp1257_bin",
|
||||
Collation::cp1257_general_ci => "cp1257_general_ci",
|
||||
Collation::cp1257_lithuanian_ci => "cp1257_lithuanian_ci",
|
||||
Collation::cp850_bin => "cp850_bin",
|
||||
Collation::cp850_general_ci => "cp850_general_ci",
|
||||
Collation::cp852_bin => "cp852_bin",
|
||||
Collation::cp852_general_ci => "cp852_general_ci",
|
||||
Collation::cp866_bin => "cp866_bin",
|
||||
Collation::cp866_general_ci => "cp866_general_ci",
|
||||
Collation::cp932_bin => "cp932_bin",
|
||||
Collation::cp932_japanese_ci => "cp932_japanese_ci",
|
||||
Collation::dec8_bin => "dec8_bin",
|
||||
Collation::dec8_swedish_ci => "dec8_swedish_ci",
|
||||
Collation::eucjpms_bin => "eucjpms_bin",
|
||||
Collation::eucjpms_japanese_ci => "eucjpms_japanese_ci",
|
||||
Collation::euckr_bin => "euckr_bin",
|
||||
Collation::euckr_korean_ci => "euckr_korean_ci",
|
||||
Collation::gb18030_bin => "gb18030_bin",
|
||||
Collation::gb18030_chinese_ci => "gb18030_chinese_ci",
|
||||
Collation::gb18030_unicode_520_ci => "gb18030_unicode_520_ci",
|
||||
Collation::gb2312_bin => "gb2312_bin",
|
||||
Collation::gb2312_chinese_ci => "gb2312_chinese_ci",
|
||||
Collation::gbk_bin => "gbk_bin",
|
||||
Collation::gbk_chinese_ci => "gbk_chinese_ci",
|
||||
Collation::geostd8_bin => "geostd8_bin",
|
||||
Collation::geostd8_general_ci => "geostd8_general_ci",
|
||||
Collation::greek_bin => "greek_bin",
|
||||
Collation::greek_general_ci => "greek_general_ci",
|
||||
Collation::hebrew_bin => "hebrew_bin",
|
||||
Collation::hebrew_general_ci => "hebrew_general_ci",
|
||||
Collation::hp8_bin => "hp8_bin",
|
||||
Collation::hp8_english_ci => "hp8_english_ci",
|
||||
Collation::keybcs2_bin => "keybcs2_bin",
|
||||
Collation::keybcs2_general_ci => "keybcs2_general_ci",
|
||||
Collation::koi8r_bin => "koi8r_bin",
|
||||
Collation::koi8r_general_ci => "koi8r_general_ci",
|
||||
Collation::koi8u_bin => "koi8u_bin",
|
||||
Collation::koi8u_general_ci => "koi8u_general_ci",
|
||||
Collation::latin1_bin => "latin1_bin",
|
||||
Collation::latin1_danish_ci => "latin1_danish_ci",
|
||||
Collation::latin1_general_ci => "latin1_general_ci",
|
||||
Collation::latin1_general_cs => "latin1_general_cs",
|
||||
Collation::latin1_german1_ci => "latin1_german1_ci",
|
||||
Collation::latin1_german2_ci => "latin1_german2_ci",
|
||||
Collation::latin1_spanish_ci => "latin1_spanish_ci",
|
||||
Collation::latin1_swedish_ci => "latin1_swedish_ci",
|
||||
Collation::latin2_bin => "latin2_bin",
|
||||
Collation::latin2_croatian_ci => "latin2_croatian_ci",
|
||||
Collation::latin2_czech_cs => "latin2_czech_cs",
|
||||
Collation::latin2_general_ci => "latin2_general_ci",
|
||||
Collation::latin2_hungarian_ci => "latin2_hungarian_ci",
|
||||
Collation::latin5_bin => "latin5_bin",
|
||||
Collation::latin5_turkish_ci => "latin5_turkish_ci",
|
||||
Collation::latin7_bin => "latin7_bin",
|
||||
Collation::latin7_estonian_cs => "latin7_estonian_cs",
|
||||
Collation::latin7_general_ci => "latin7_general_ci",
|
||||
Collation::latin7_general_cs => "latin7_general_cs",
|
||||
Collation::macce_bin => "macce_bin",
|
||||
Collation::macce_general_ci => "macce_general_ci",
|
||||
Collation::macroman_bin => "macroman_bin",
|
||||
Collation::macroman_general_ci => "macroman_general_ci",
|
||||
Collation::sjis_bin => "sjis_bin",
|
||||
Collation::sjis_japanese_ci => "sjis_japanese_ci",
|
||||
Collation::swe7_bin => "swe7_bin",
|
||||
Collation::swe7_swedish_ci => "swe7_swedish_ci",
|
||||
Collation::tis620_bin => "tis620_bin",
|
||||
Collation::tis620_thai_ci => "tis620_thai_ci",
|
||||
Collation::ucs2_bin => "ucs2_bin",
|
||||
Collation::ucs2_croatian_ci => "ucs2_croatian_ci",
|
||||
Collation::ucs2_czech_ci => "ucs2_czech_ci",
|
||||
Collation::ucs2_danish_ci => "ucs2_danish_ci",
|
||||
Collation::ucs2_esperanto_ci => "ucs2_esperanto_ci",
|
||||
Collation::ucs2_estonian_ci => "ucs2_estonian_ci",
|
||||
Collation::ucs2_general_ci => "ucs2_general_ci",
|
||||
Collation::ucs2_general_mysql500_ci => "ucs2_general_mysql500_ci",
|
||||
Collation::ucs2_german2_ci => "ucs2_german2_ci",
|
||||
Collation::ucs2_hungarian_ci => "ucs2_hungarian_ci",
|
||||
Collation::ucs2_icelandic_ci => "ucs2_icelandic_ci",
|
||||
Collation::ucs2_latvian_ci => "ucs2_latvian_ci",
|
||||
Collation::ucs2_lithuanian_ci => "ucs2_lithuanian_ci",
|
||||
Collation::ucs2_persian_ci => "ucs2_persian_ci",
|
||||
Collation::ucs2_polish_ci => "ucs2_polish_ci",
|
||||
Collation::ucs2_roman_ci => "ucs2_roman_ci",
|
||||
Collation::ucs2_romanian_ci => "ucs2_romanian_ci",
|
||||
Collation::ucs2_sinhala_ci => "ucs2_sinhala_ci",
|
||||
Collation::ucs2_slovak_ci => "ucs2_slovak_ci",
|
||||
Collation::ucs2_slovenian_ci => "ucs2_slovenian_ci",
|
||||
Collation::ucs2_spanish_ci => "ucs2_spanish_ci",
|
||||
Collation::ucs2_spanish2_ci => "ucs2_spanish2_ci",
|
||||
Collation::ucs2_swedish_ci => "ucs2_swedish_ci",
|
||||
Collation::ucs2_turkish_ci => "ucs2_turkish_ci",
|
||||
Collation::ucs2_unicode_520_ci => "ucs2_unicode_520_ci",
|
||||
Collation::ucs2_unicode_ci => "ucs2_unicode_ci",
|
||||
Collation::ucs2_vietnamese_ci => "ucs2_vietnamese_ci",
|
||||
Collation::ujis_bin => "ujis_bin",
|
||||
Collation::ujis_japanese_ci => "ujis_japanese_ci",
|
||||
Collation::utf16_bin => "utf16_bin",
|
||||
Collation::utf16_croatian_ci => "utf16_croatian_ci",
|
||||
Collation::utf16_czech_ci => "utf16_czech_ci",
|
||||
Collation::utf16_danish_ci => "utf16_danish_ci",
|
||||
Collation::utf16_esperanto_ci => "utf16_esperanto_ci",
|
||||
Collation::utf16_estonian_ci => "utf16_estonian_ci",
|
||||
Collation::utf16_general_ci => "utf16_general_ci",
|
||||
Collation::utf16_german2_ci => "utf16_german2_ci",
|
||||
Collation::utf16_hungarian_ci => "utf16_hungarian_ci",
|
||||
Collation::utf16_icelandic_ci => "utf16_icelandic_ci",
|
||||
Collation::utf16_latvian_ci => "utf16_latvian_ci",
|
||||
Collation::utf16_lithuanian_ci => "utf16_lithuanian_ci",
|
||||
Collation::utf16_persian_ci => "utf16_persian_ci",
|
||||
Collation::utf16_polish_ci => "utf16_polish_ci",
|
||||
Collation::utf16_roman_ci => "utf16_roman_ci",
|
||||
Collation::utf16_romanian_ci => "utf16_romanian_ci",
|
||||
Collation::utf16_sinhala_ci => "utf16_sinhala_ci",
|
||||
Collation::utf16_slovak_ci => "utf16_slovak_ci",
|
||||
Collation::utf16_slovenian_ci => "utf16_slovenian_ci",
|
||||
Collation::utf16_spanish_ci => "utf16_spanish_ci",
|
||||
Collation::utf16_spanish2_ci => "utf16_spanish2_ci",
|
||||
Collation::utf16_swedish_ci => "utf16_swedish_ci",
|
||||
Collation::utf16_turkish_ci => "utf16_turkish_ci",
|
||||
Collation::utf16_unicode_520_ci => "utf16_unicode_520_ci",
|
||||
Collation::utf16_unicode_ci => "utf16_unicode_ci",
|
||||
Collation::utf16_vietnamese_ci => "utf16_vietnamese_ci",
|
||||
Collation::utf16le_bin => "utf16le_bin",
|
||||
Collation::utf16le_general_ci => "utf16le_general_ci",
|
||||
Collation::utf32_bin => "utf32_bin",
|
||||
Collation::utf32_croatian_ci => "utf32_croatian_ci",
|
||||
Collation::utf32_czech_ci => "utf32_czech_ci",
|
||||
Collation::utf32_danish_ci => "utf32_danish_ci",
|
||||
Collation::utf32_esperanto_ci => "utf32_esperanto_ci",
|
||||
Collation::utf32_estonian_ci => "utf32_estonian_ci",
|
||||
Collation::utf32_general_ci => "utf32_general_ci",
|
||||
Collation::utf32_german2_ci => "utf32_german2_ci",
|
||||
Collation::utf32_hungarian_ci => "utf32_hungarian_ci",
|
||||
Collation::utf32_icelandic_ci => "utf32_icelandic_ci",
|
||||
Collation::utf32_latvian_ci => "utf32_latvian_ci",
|
||||
Collation::utf32_lithuanian_ci => "utf32_lithuanian_ci",
|
||||
Collation::utf32_persian_ci => "utf32_persian_ci",
|
||||
Collation::utf32_polish_ci => "utf32_polish_ci",
|
||||
Collation::utf32_roman_ci => "utf32_roman_ci",
|
||||
Collation::utf32_romanian_ci => "utf32_romanian_ci",
|
||||
Collation::utf32_sinhala_ci => "utf32_sinhala_ci",
|
||||
Collation::utf32_slovak_ci => "utf32_slovak_ci",
|
||||
Collation::utf32_slovenian_ci => "utf32_slovenian_ci",
|
||||
Collation::utf32_spanish_ci => "utf32_spanish_ci",
|
||||
Collation::utf32_spanish2_ci => "utf32_spanish2_ci",
|
||||
Collation::utf32_swedish_ci => "utf32_swedish_ci",
|
||||
Collation::utf32_turkish_ci => "utf32_turkish_ci",
|
||||
Collation::utf32_unicode_520_ci => "utf32_unicode_520_ci",
|
||||
Collation::utf32_unicode_ci => "utf32_unicode_ci",
|
||||
Collation::utf32_vietnamese_ci => "utf32_vietnamese_ci",
|
||||
Collation::utf8_bin => "utf8_bin",
|
||||
Collation::utf8_croatian_ci => "utf8_croatian_ci",
|
||||
Collation::utf8_czech_ci => "utf8_czech_ci",
|
||||
Collation::utf8_danish_ci => "utf8_danish_ci",
|
||||
Collation::utf8_esperanto_ci => "utf8_esperanto_ci",
|
||||
Collation::utf8_estonian_ci => "utf8_estonian_ci",
|
||||
Collation::utf8_general_ci => "utf8_general_ci",
|
||||
Collation::utf8_general_mysql500_ci => "utf8_general_mysql500_ci",
|
||||
Collation::utf8_german2_ci => "utf8_german2_ci",
|
||||
Collation::utf8_hungarian_ci => "utf8_hungarian_ci",
|
||||
Collation::utf8_icelandic_ci => "utf8_icelandic_ci",
|
||||
Collation::utf8_latvian_ci => "utf8_latvian_ci",
|
||||
Collation::utf8_lithuanian_ci => "utf8_lithuanian_ci",
|
||||
Collation::utf8_persian_ci => "utf8_persian_ci",
|
||||
Collation::utf8_polish_ci => "utf8_polish_ci",
|
||||
Collation::utf8_roman_ci => "utf8_roman_ci",
|
||||
Collation::utf8_romanian_ci => "utf8_romanian_ci",
|
||||
Collation::utf8_sinhala_ci => "utf8_sinhala_ci",
|
||||
Collation::utf8_slovak_ci => "utf8_slovak_ci",
|
||||
Collation::utf8_slovenian_ci => "utf8_slovenian_ci",
|
||||
Collation::utf8_spanish_ci => "utf8_spanish_ci",
|
||||
Collation::utf8_spanish2_ci => "utf8_spanish2_ci",
|
||||
Collation::utf8_swedish_ci => "utf8_swedish_ci",
|
||||
Collation::utf8_tolower_ci => "utf8_tolower_ci",
|
||||
Collation::utf8_turkish_ci => "utf8_turkish_ci",
|
||||
Collation::utf8_unicode_520_ci => "utf8_unicode_520_ci",
|
||||
Collation::utf8_unicode_ci => "utf8_unicode_ci",
|
||||
Collation::utf8_vietnamese_ci => "utf8_vietnamese_ci",
|
||||
Collation::utf8mb4_0900_ai_ci => "utf8mb4_0900_ai_ci",
|
||||
Collation::utf8mb4_bin => "utf8mb4_bin",
|
||||
Collation::utf8mb4_croatian_ci => "utf8mb4_croatian_ci",
|
||||
Collation::utf8mb4_czech_ci => "utf8mb4_czech_ci",
|
||||
Collation::utf8mb4_danish_ci => "utf8mb4_danish_ci",
|
||||
Collation::utf8mb4_esperanto_ci => "utf8mb4_esperanto_ci",
|
||||
Collation::utf8mb4_estonian_ci => "utf8mb4_estonian_ci",
|
||||
Collation::utf8mb4_general_ci => "utf8mb4_general_ci",
|
||||
Collation::utf8mb4_german2_ci => "utf8mb4_german2_ci",
|
||||
Collation::utf8mb4_hungarian_ci => "utf8mb4_hungarian_ci",
|
||||
Collation::utf8mb4_icelandic_ci => "utf8mb4_icelandic_ci",
|
||||
Collation::utf8mb4_latvian_ci => "utf8mb4_latvian_ci",
|
||||
Collation::utf8mb4_lithuanian_ci => "utf8mb4_lithuanian_ci",
|
||||
Collation::utf8mb4_persian_ci => "utf8mb4_persian_ci",
|
||||
Collation::utf8mb4_polish_ci => "utf8mb4_polish_ci",
|
||||
Collation::utf8mb4_roman_ci => "utf8mb4_roman_ci",
|
||||
Collation::utf8mb4_romanian_ci => "utf8mb4_romanian_ci",
|
||||
Collation::utf8mb4_sinhala_ci => "utf8mb4_sinhala_ci",
|
||||
Collation::utf8mb4_slovak_ci => "utf8mb4_slovak_ci",
|
||||
Collation::utf8mb4_slovenian_ci => "utf8mb4_slovenian_ci",
|
||||
Collation::utf8mb4_spanish_ci => "utf8mb4_spanish_ci",
|
||||
Collation::utf8mb4_spanish2_ci => "utf8mb4_spanish2_ci",
|
||||
Collation::utf8mb4_swedish_ci => "utf8mb4_swedish_ci",
|
||||
Collation::utf8mb4_turkish_ci => "utf8mb4_turkish_ci",
|
||||
Collation::utf8mb4_unicode_520_ci => "utf8mb4_unicode_520_ci",
|
||||
Collation::utf8mb4_unicode_ci => "utf8mb4_unicode_ci",
|
||||
Collation::utf8mb4_vietnamese_ci => "utf8mb4_vietnamese_ci",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handshake packet have only 1 byte for collation_id.
|
||||
// So we can't use collations with ID > 255.
|
||||
impl FromStr for Collation {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(collation: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match collation {
|
||||
"big5_chinese_ci" => Collation::big5_chinese_ci,
|
||||
"swe7_swedish_ci" => Collation::swe7_swedish_ci,
|
||||
"utf16_unicode_ci" => Collation::utf16_unicode_ci,
|
||||
"utf16_icelandic_ci" => Collation::utf16_icelandic_ci,
|
||||
"utf16_latvian_ci" => Collation::utf16_latvian_ci,
|
||||
"utf16_romanian_ci" => Collation::utf16_romanian_ci,
|
||||
"utf16_slovenian_ci" => Collation::utf16_slovenian_ci,
|
||||
"utf16_polish_ci" => Collation::utf16_polish_ci,
|
||||
"utf16_estonian_ci" => Collation::utf16_estonian_ci,
|
||||
"utf16_spanish_ci" => Collation::utf16_spanish_ci,
|
||||
"utf16_swedish_ci" => Collation::utf16_swedish_ci,
|
||||
"ascii_general_ci" => Collation::ascii_general_ci,
|
||||
"utf16_turkish_ci" => Collation::utf16_turkish_ci,
|
||||
"utf16_czech_ci" => Collation::utf16_czech_ci,
|
||||
"utf16_danish_ci" => Collation::utf16_danish_ci,
|
||||
"utf16_lithuanian_ci" => Collation::utf16_lithuanian_ci,
|
||||
"utf16_slovak_ci" => Collation::utf16_slovak_ci,
|
||||
"utf16_spanish2_ci" => Collation::utf16_spanish2_ci,
|
||||
"utf16_roman_ci" => Collation::utf16_roman_ci,
|
||||
"utf16_persian_ci" => Collation::utf16_persian_ci,
|
||||
"utf16_esperanto_ci" => Collation::utf16_esperanto_ci,
|
||||
"utf16_hungarian_ci" => Collation::utf16_hungarian_ci,
|
||||
"ujis_japanese_ci" => Collation::ujis_japanese_ci,
|
||||
"utf16_sinhala_ci" => Collation::utf16_sinhala_ci,
|
||||
"utf16_german2_ci" => Collation::utf16_german2_ci,
|
||||
"utf16_croatian_ci" => Collation::utf16_croatian_ci,
|
||||
"utf16_unicode_520_ci" => Collation::utf16_unicode_520_ci,
|
||||
"utf16_vietnamese_ci" => Collation::utf16_vietnamese_ci,
|
||||
"ucs2_unicode_ci" => Collation::ucs2_unicode_ci,
|
||||
"ucs2_icelandic_ci" => Collation::ucs2_icelandic_ci,
|
||||
"sjis_japanese_ci" => Collation::sjis_japanese_ci,
|
||||
"ucs2_latvian_ci" => Collation::ucs2_latvian_ci,
|
||||
"ucs2_romanian_ci" => Collation::ucs2_romanian_ci,
|
||||
"ucs2_slovenian_ci" => Collation::ucs2_slovenian_ci,
|
||||
"ucs2_polish_ci" => Collation::ucs2_polish_ci,
|
||||
"ucs2_estonian_ci" => Collation::ucs2_estonian_ci,
|
||||
"ucs2_spanish_ci" => Collation::ucs2_spanish_ci,
|
||||
"ucs2_swedish_ci" => Collation::ucs2_swedish_ci,
|
||||
"ucs2_turkish_ci" => Collation::ucs2_turkish_ci,
|
||||
"ucs2_czech_ci" => Collation::ucs2_czech_ci,
|
||||
"ucs2_danish_ci" => Collation::ucs2_danish_ci,
|
||||
"cp1251_bulgarian_ci" => Collation::cp1251_bulgarian_ci,
|
||||
"ucs2_lithuanian_ci" => Collation::ucs2_lithuanian_ci,
|
||||
"ucs2_slovak_ci" => Collation::ucs2_slovak_ci,
|
||||
"ucs2_spanish2_ci" => Collation::ucs2_spanish2_ci,
|
||||
"ucs2_roman_ci" => Collation::ucs2_roman_ci,
|
||||
"ucs2_persian_ci" => Collation::ucs2_persian_ci,
|
||||
"ucs2_esperanto_ci" => Collation::ucs2_esperanto_ci,
|
||||
"ucs2_hungarian_ci" => Collation::ucs2_hungarian_ci,
|
||||
"ucs2_sinhala_ci" => Collation::ucs2_sinhala_ci,
|
||||
"ucs2_german2_ci" => Collation::ucs2_german2_ci,
|
||||
"ucs2_croatian_ci" => Collation::ucs2_croatian_ci,
|
||||
"latin1_danish_ci" => Collation::latin1_danish_ci,
|
||||
"ucs2_unicode_520_ci" => Collation::ucs2_unicode_520_ci,
|
||||
"ucs2_vietnamese_ci" => Collation::ucs2_vietnamese_ci,
|
||||
"ucs2_general_mysql500_ci" => Collation::ucs2_general_mysql500_ci,
|
||||
"hebrew_general_ci" => Collation::hebrew_general_ci,
|
||||
"utf32_unicode_ci" => Collation::utf32_unicode_ci,
|
||||
"utf32_icelandic_ci" => Collation::utf32_icelandic_ci,
|
||||
"utf32_latvian_ci" => Collation::utf32_latvian_ci,
|
||||
"utf32_romanian_ci" => Collation::utf32_romanian_ci,
|
||||
"utf32_slovenian_ci" => Collation::utf32_slovenian_ci,
|
||||
"utf32_polish_ci" => Collation::utf32_polish_ci,
|
||||
"utf32_estonian_ci" => Collation::utf32_estonian_ci,
|
||||
"utf32_spanish_ci" => Collation::utf32_spanish_ci,
|
||||
"utf32_swedish_ci" => Collation::utf32_swedish_ci,
|
||||
"utf32_turkish_ci" => Collation::utf32_turkish_ci,
|
||||
"utf32_czech_ci" => Collation::utf32_czech_ci,
|
||||
"utf32_danish_ci" => Collation::utf32_danish_ci,
|
||||
"utf32_lithuanian_ci" => Collation::utf32_lithuanian_ci,
|
||||
"utf32_slovak_ci" => Collation::utf32_slovak_ci,
|
||||
"utf32_spanish2_ci" => Collation::utf32_spanish2_ci,
|
||||
"utf32_roman_ci" => Collation::utf32_roman_ci,
|
||||
"utf32_persian_ci" => Collation::utf32_persian_ci,
|
||||
"utf32_esperanto_ci" => Collation::utf32_esperanto_ci,
|
||||
"utf32_hungarian_ci" => Collation::utf32_hungarian_ci,
|
||||
"utf32_sinhala_ci" => Collation::utf32_sinhala_ci,
|
||||
"tis620_thai_ci" => Collation::tis620_thai_ci,
|
||||
"utf32_german2_ci" => Collation::utf32_german2_ci,
|
||||
"utf32_croatian_ci" => Collation::utf32_croatian_ci,
|
||||
"utf32_unicode_520_ci" => Collation::utf32_unicode_520_ci,
|
||||
"utf32_vietnamese_ci" => Collation::utf32_vietnamese_ci,
|
||||
"euckr_korean_ci" => Collation::euckr_korean_ci,
|
||||
"utf8_unicode_ci" => Collation::utf8_unicode_ci,
|
||||
"utf8_icelandic_ci" => Collation::utf8_icelandic_ci,
|
||||
"utf8_latvian_ci" => Collation::utf8_latvian_ci,
|
||||
"utf8_romanian_ci" => Collation::utf8_romanian_ci,
|
||||
"utf8_slovenian_ci" => Collation::utf8_slovenian_ci,
|
||||
"utf8_polish_ci" => Collation::utf8_polish_ci,
|
||||
"utf8_estonian_ci" => Collation::utf8_estonian_ci,
|
||||
"utf8_spanish_ci" => Collation::utf8_spanish_ci,
|
||||
"latin2_czech_cs" => Collation::latin2_czech_cs,
|
||||
"latin7_estonian_cs" => Collation::latin7_estonian_cs,
|
||||
"utf8_swedish_ci" => Collation::utf8_swedish_ci,
|
||||
"utf8_turkish_ci" => Collation::utf8_turkish_ci,
|
||||
"utf8_czech_ci" => Collation::utf8_czech_ci,
|
||||
"utf8_danish_ci" => Collation::utf8_danish_ci,
|
||||
"utf8_lithuanian_ci" => Collation::utf8_lithuanian_ci,
|
||||
"utf8_slovak_ci" => Collation::utf8_slovak_ci,
|
||||
"utf8_spanish2_ci" => Collation::utf8_spanish2_ci,
|
||||
"utf8_roman_ci" => Collation::utf8_roman_ci,
|
||||
"utf8_persian_ci" => Collation::utf8_persian_ci,
|
||||
"utf8_esperanto_ci" => Collation::utf8_esperanto_ci,
|
||||
"latin2_hungarian_ci" => Collation::latin2_hungarian_ci,
|
||||
"utf8_hungarian_ci" => Collation::utf8_hungarian_ci,
|
||||
"utf8_sinhala_ci" => Collation::utf8_sinhala_ci,
|
||||
"utf8_german2_ci" => Collation::utf8_german2_ci,
|
||||
"utf8_croatian_ci" => Collation::utf8_croatian_ci,
|
||||
"utf8_unicode_520_ci" => Collation::utf8_unicode_520_ci,
|
||||
"utf8_vietnamese_ci" => Collation::utf8_vietnamese_ci,
|
||||
"koi8u_general_ci" => Collation::koi8u_general_ci,
|
||||
"utf8_general_mysql500_ci" => Collation::utf8_general_mysql500_ci,
|
||||
"utf8mb4_unicode_ci" => Collation::utf8mb4_unicode_ci,
|
||||
"utf8mb4_icelandic_ci" => Collation::utf8mb4_icelandic_ci,
|
||||
"utf8mb4_latvian_ci" => Collation::utf8mb4_latvian_ci,
|
||||
"utf8mb4_romanian_ci" => Collation::utf8mb4_romanian_ci,
|
||||
"utf8mb4_slovenian_ci" => Collation::utf8mb4_slovenian_ci,
|
||||
"utf8mb4_polish_ci" => Collation::utf8mb4_polish_ci,
|
||||
"cp1251_ukrainian_ci" => Collation::cp1251_ukrainian_ci,
|
||||
"utf8mb4_estonian_ci" => Collation::utf8mb4_estonian_ci,
|
||||
"utf8mb4_spanish_ci" => Collation::utf8mb4_spanish_ci,
|
||||
"utf8mb4_swedish_ci" => Collation::utf8mb4_swedish_ci,
|
||||
"utf8mb4_turkish_ci" => Collation::utf8mb4_turkish_ci,
|
||||
"utf8mb4_czech_ci" => Collation::utf8mb4_czech_ci,
|
||||
"utf8mb4_danish_ci" => Collation::utf8mb4_danish_ci,
|
||||
"utf8mb4_lithuanian_ci" => Collation::utf8mb4_lithuanian_ci,
|
||||
"utf8mb4_slovak_ci" => Collation::utf8mb4_slovak_ci,
|
||||
"utf8mb4_spanish2_ci" => Collation::utf8mb4_spanish2_ci,
|
||||
"utf8mb4_roman_ci" => Collation::utf8mb4_roman_ci,
|
||||
"gb2312_chinese_ci" => Collation::gb2312_chinese_ci,
|
||||
"utf8mb4_persian_ci" => Collation::utf8mb4_persian_ci,
|
||||
"utf8mb4_esperanto_ci" => Collation::utf8mb4_esperanto_ci,
|
||||
"utf8mb4_hungarian_ci" => Collation::utf8mb4_hungarian_ci,
|
||||
"utf8mb4_sinhala_ci" => Collation::utf8mb4_sinhala_ci,
|
||||
"utf8mb4_german2_ci" => Collation::utf8mb4_german2_ci,
|
||||
"utf8mb4_croatian_ci" => Collation::utf8mb4_croatian_ci,
|
||||
"utf8mb4_unicode_520_ci" => Collation::utf8mb4_unicode_520_ci,
|
||||
"utf8mb4_vietnamese_ci" => Collation::utf8mb4_vietnamese_ci,
|
||||
"gb18030_chinese_ci" => Collation::gb18030_chinese_ci,
|
||||
"gb18030_bin" => Collation::gb18030_bin,
|
||||
"greek_general_ci" => Collation::greek_general_ci,
|
||||
"gb18030_unicode_520_ci" => Collation::gb18030_unicode_520_ci,
|
||||
"utf8mb4_0900_ai_ci" => Collation::utf8mb4_0900_ai_ci,
|
||||
"cp1250_general_ci" => Collation::cp1250_general_ci,
|
||||
"latin2_croatian_ci" => Collation::latin2_croatian_ci,
|
||||
"gbk_chinese_ci" => Collation::gbk_chinese_ci,
|
||||
"cp1257_lithuanian_ci" => Collation::cp1257_lithuanian_ci,
|
||||
"dec8_swedish_ci" => Collation::dec8_swedish_ci,
|
||||
"latin5_turkish_ci" => Collation::latin5_turkish_ci,
|
||||
"latin1_german2_ci" => Collation::latin1_german2_ci,
|
||||
"armscii8_general_ci" => Collation::armscii8_general_ci,
|
||||
"utf8_general_ci" => Collation::utf8_general_ci,
|
||||
"cp1250_czech_cs" => Collation::cp1250_czech_cs,
|
||||
"ucs2_general_ci" => Collation::ucs2_general_ci,
|
||||
"cp866_general_ci" => Collation::cp866_general_ci,
|
||||
"keybcs2_general_ci" => Collation::keybcs2_general_ci,
|
||||
"macce_general_ci" => Collation::macce_general_ci,
|
||||
"macroman_general_ci" => Collation::macroman_general_ci,
|
||||
"cp850_general_ci" => Collation::cp850_general_ci,
|
||||
"cp852_general_ci" => Collation::cp852_general_ci,
|
||||
"latin7_general_ci" => Collation::latin7_general_ci,
|
||||
"latin7_general_cs" => Collation::latin7_general_cs,
|
||||
"macce_bin" => Collation::macce_bin,
|
||||
"cp1250_croatian_ci" => Collation::cp1250_croatian_ci,
|
||||
"utf8mb4_general_ci" => Collation::utf8mb4_general_ci,
|
||||
"utf8mb4_bin" => Collation::utf8mb4_bin,
|
||||
"latin1_bin" => Collation::latin1_bin,
|
||||
"latin1_general_ci" => Collation::latin1_general_ci,
|
||||
"latin1_general_cs" => Collation::latin1_general_cs,
|
||||
"latin1_german1_ci" => Collation::latin1_german1_ci,
|
||||
"cp1251_bin" => Collation::cp1251_bin,
|
||||
"cp1251_general_ci" => Collation::cp1251_general_ci,
|
||||
"cp1251_general_cs" => Collation::cp1251_general_cs,
|
||||
"macroman_bin" => Collation::macroman_bin,
|
||||
"utf16_general_ci" => Collation::utf16_general_ci,
|
||||
"utf16_bin" => Collation::utf16_bin,
|
||||
"utf16le_general_ci" => Collation::utf16le_general_ci,
|
||||
"cp1256_general_ci" => Collation::cp1256_general_ci,
|
||||
"cp1257_bin" => Collation::cp1257_bin,
|
||||
"cp1257_general_ci" => Collation::cp1257_general_ci,
|
||||
"hp8_english_ci" => Collation::hp8_english_ci,
|
||||
"utf32_general_ci" => Collation::utf32_general_ci,
|
||||
"utf32_bin" => Collation::utf32_bin,
|
||||
"utf16le_bin" => Collation::utf16le_bin,
|
||||
"binary" => Collation::binary,
|
||||
"armscii8_bin" => Collation::armscii8_bin,
|
||||
"ascii_bin" => Collation::ascii_bin,
|
||||
"cp1250_bin" => Collation::cp1250_bin,
|
||||
"cp1256_bin" => Collation::cp1256_bin,
|
||||
"cp866_bin" => Collation::cp866_bin,
|
||||
"dec8_bin" => Collation::dec8_bin,
|
||||
"koi8r_general_ci" => Collation::koi8r_general_ci,
|
||||
"greek_bin" => Collation::greek_bin,
|
||||
"hebrew_bin" => Collation::hebrew_bin,
|
||||
"hp8_bin" => Collation::hp8_bin,
|
||||
"keybcs2_bin" => Collation::keybcs2_bin,
|
||||
"koi8r_bin" => Collation::koi8r_bin,
|
||||
"koi8u_bin" => Collation::koi8u_bin,
|
||||
"utf8_tolower_ci" => Collation::utf8_tolower_ci,
|
||||
"latin2_bin" => Collation::latin2_bin,
|
||||
"latin5_bin" => Collation::latin5_bin,
|
||||
"latin7_bin" => Collation::latin7_bin,
|
||||
"latin1_swedish_ci" => Collation::latin1_swedish_ci,
|
||||
"cp850_bin" => Collation::cp850_bin,
|
||||
"cp852_bin" => Collation::cp852_bin,
|
||||
"swe7_bin" => Collation::swe7_bin,
|
||||
"utf8_bin" => Collation::utf8_bin,
|
||||
"big5_bin" => Collation::big5_bin,
|
||||
"euckr_bin" => Collation::euckr_bin,
|
||||
"gb2312_bin" => Collation::gb2312_bin,
|
||||
"gbk_bin" => Collation::gbk_bin,
|
||||
"sjis_bin" => Collation::sjis_bin,
|
||||
"tis620_bin" => Collation::tis620_bin,
|
||||
"latin2_general_ci" => Collation::latin2_general_ci,
|
||||
"ucs2_bin" => Collation::ucs2_bin,
|
||||
"ujis_bin" => Collation::ujis_bin,
|
||||
"geostd8_general_ci" => Collation::geostd8_general_ci,
|
||||
"geostd8_bin" => Collation::geostd8_bin,
|
||||
"latin1_spanish_ci" => Collation::latin1_spanish_ci,
|
||||
"cp932_japanese_ci" => Collation::cp932_japanese_ci,
|
||||
"cp932_bin" => Collation::cp932_bin,
|
||||
"eucjpms_japanese_ci" => Collation::eucjpms_japanese_ci,
|
||||
"eucjpms_bin" => Collation::eucjpms_bin,
|
||||
"cp1250_polish_ci" => Collation::cp1250_polish_ci,
|
||||
|
||||
_ => {
|
||||
return Err(Error::Configuration(
|
||||
format!("unsupported MySQL collation: {collation}").into(),
|
||||
));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
use crate::ext::ustr::UStr;
|
||||
use crate::protocol::text::ColumnFlags;
|
||||
use crate::{MySql, MySqlTypeInfo};
|
||||
pub(crate) use sqlx_core::column::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct MySqlColumn {
|
||||
pub(crate) ordinal: usize,
|
||||
pub(crate) name: UStr,
|
||||
pub(crate) type_info: MySqlTypeInfo,
|
||||
|
||||
#[cfg_attr(feature = "offline", serde(skip))]
|
||||
pub(crate) flags: Option<ColumnFlags>,
|
||||
}
|
||||
|
||||
impl Column for MySqlColumn {
|
||||
type Database = MySql;
|
||||
|
||||
fn ordinal(&self) -> usize {
|
||||
self.ordinal
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn type_info(&self) -> &MySqlTypeInfo {
|
||||
&self.type_info
|
||||
}
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
use bytes::buf::Chain;
|
||||
use bytes::Bytes;
|
||||
use digest::{Digest, OutputSizeUser};
|
||||
use generic_array::GenericArray;
|
||||
use rand::thread_rng;
|
||||
use rsa::{pkcs8::DecodePublicKey, Oaep, RsaPublicKey};
|
||||
use sha1::Sha1;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::connection::stream::MySqlStream;
|
||||
use crate::error::Error;
|
||||
use crate::protocol::auth::AuthPlugin;
|
||||
use crate::protocol::Packet;
|
||||
|
||||
impl AuthPlugin {
|
||||
pub(super) async fn scramble(
|
||||
self,
|
||||
stream: &mut MySqlStream,
|
||||
password: &str,
|
||||
nonce: &Chain<Bytes, Bytes>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
match self {
|
||||
// https://mariadb.com/kb/en/caching_sha2_password-authentication-plugin/
|
||||
AuthPlugin::CachingSha2Password => Ok(scramble_sha256(password, nonce).to_vec()),
|
||||
|
||||
AuthPlugin::MySqlNativePassword => Ok(scramble_sha1(password, nonce).to_vec()),
|
||||
|
||||
// https://mariadb.com/kb/en/sha256_password-plugin/
|
||||
AuthPlugin::Sha256Password => encrypt_rsa(stream, 0x01, password, nonce).await,
|
||||
|
||||
AuthPlugin::MySqlClearPassword => {
|
||||
let mut pw_bytes = password.as_bytes().to_owned();
|
||||
pw_bytes.push(0); // null terminate
|
||||
Ok(pw_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn handle(
|
||||
self,
|
||||
stream: &mut MySqlStream,
|
||||
packet: Packet<Bytes>,
|
||||
password: &str,
|
||||
nonce: &Chain<Bytes, Bytes>,
|
||||
) -> Result<bool, Error> {
|
||||
match self {
|
||||
AuthPlugin::CachingSha2Password if packet[0] == 0x01 => {
|
||||
match packet[1] {
|
||||
// AUTH_OK
|
||||
0x03 => Ok(true),
|
||||
|
||||
// AUTH_CONTINUE
|
||||
0x04 => {
|
||||
let payload = encrypt_rsa(stream, 0x02, password, nonce).await?;
|
||||
|
||||
stream.write_packet(&*payload)?;
|
||||
stream.flush().await?;
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
v => {
|
||||
Err(err_protocol!("unexpected result from fast authentication 0x{:x} when expecting 0x03 (AUTH_OK) or 0x04 (AUTH_CONTINUE)", v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => Err(err_protocol!(
|
||||
"unexpected packet 0x{:02x} for auth plugin '{}' during authentication",
|
||||
packet[0],
|
||||
self.name()
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scramble_sha1(
|
||||
password: &str,
|
||||
nonce: &Chain<Bytes, Bytes>,
|
||||
) -> GenericArray<u8, <Sha1 as OutputSizeUser>::OutputSize> {
|
||||
// SHA1( password ) ^ SHA1( seed + SHA1( SHA1( password ) ) )
|
||||
// https://mariadb.com/kb/en/connection/#mysql_native_password-plugin
|
||||
|
||||
let mut ctx = Sha1::new();
|
||||
|
||||
ctx.update(password);
|
||||
|
||||
let mut pw_hash = ctx.finalize_reset();
|
||||
|
||||
ctx.update(pw_hash);
|
||||
|
||||
let pw_hash_hash = ctx.finalize_reset();
|
||||
|
||||
ctx.update(nonce.first_ref());
|
||||
ctx.update(nonce.last_ref());
|
||||
ctx.update(pw_hash_hash);
|
||||
|
||||
let pw_seed_hash_hash = ctx.finalize();
|
||||
|
||||
xor_eq(&mut pw_hash, &pw_seed_hash_hash);
|
||||
|
||||
pw_hash
|
||||
}
|
||||
|
||||
fn scramble_sha256(
|
||||
password: &str,
|
||||
nonce: &Chain<Bytes, Bytes>,
|
||||
) -> GenericArray<u8, <Sha256 as OutputSizeUser>::OutputSize> {
|
||||
// XOR(SHA256(password), SHA256(seed, SHA256(SHA256(password))))
|
||||
// https://mariadb.com/kb/en/caching_sha2_password-authentication-plugin/#sha-2-encrypted-password
|
||||
let mut ctx = Sha256::new();
|
||||
|
||||
ctx.update(password);
|
||||
|
||||
let mut pw_hash = ctx.finalize_reset();
|
||||
|
||||
ctx.update(pw_hash);
|
||||
|
||||
let pw_hash_hash = ctx.finalize_reset();
|
||||
|
||||
ctx.update(nonce.first_ref());
|
||||
ctx.update(nonce.last_ref());
|
||||
ctx.update(pw_hash_hash);
|
||||
|
||||
let pw_seed_hash_hash = ctx.finalize();
|
||||
|
||||
xor_eq(&mut pw_hash, &pw_seed_hash_hash);
|
||||
|
||||
pw_hash
|
||||
}
|
||||
|
||||
async fn encrypt_rsa<'s>(
|
||||
stream: &'s mut MySqlStream,
|
||||
public_key_request_id: u8,
|
||||
password: &'s str,
|
||||
nonce: &'s Chain<Bytes, Bytes>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
// https://mariadb.com/kb/en/caching_sha2_password-authentication-plugin/
|
||||
|
||||
if stream.is_tls {
|
||||
// If in a TLS stream, send the password directly in clear text
|
||||
return Ok(to_asciz(password));
|
||||
}
|
||||
|
||||
// client sends a public key request
|
||||
stream.write_packet(&[public_key_request_id][..])?;
|
||||
stream.flush().await?;
|
||||
|
||||
// server sends a public key response
|
||||
let packet = stream.recv_packet().await?;
|
||||
let rsa_pub_key = &packet[1..];
|
||||
|
||||
// xor the password with the given nonce
|
||||
let mut pass = to_asciz(password);
|
||||
|
||||
let (a, b) = (nonce.first_ref(), nonce.last_ref());
|
||||
let mut nonce = Vec::with_capacity(a.len() + b.len());
|
||||
nonce.extend_from_slice(a);
|
||||
nonce.extend_from_slice(b);
|
||||
|
||||
xor_eq(&mut pass, &nonce);
|
||||
|
||||
// client sends an RSA encrypted password
|
||||
let pkey = parse_rsa_pub_key(rsa_pub_key)?;
|
||||
let padding = Oaep::new::<sha1::Sha1>();
|
||||
pkey.encrypt(&mut thread_rng(), padding, &pass[..])
|
||||
.map_err(Error::protocol)
|
||||
}
|
||||
|
||||
// XOR(x, y)
|
||||
// If len(y) < len(x), wrap around inside y
|
||||
fn xor_eq(x: &mut [u8], y: &[u8]) {
|
||||
let y_len = y.len();
|
||||
|
||||
for i in 0..x.len() {
|
||||
x[i] ^= y[i % y_len];
|
||||
}
|
||||
}
|
||||
|
||||
fn to_asciz(s: &str) -> Vec<u8> {
|
||||
let mut z = String::with_capacity(s.len() + 1);
|
||||
z.push_str(s);
|
||||
z.push('\0');
|
||||
|
||||
z.into_bytes()
|
||||
}
|
||||
|
||||
// https://docs.rs/rsa/0.3.0/rsa/struct.RSAPublicKey.html?search=#example-1
|
||||
fn parse_rsa_pub_key(key: &[u8]) -> Result<RsaPublicKey, Error> {
|
||||
let pem = std::str::from_utf8(key).map_err(Error::protocol)?;
|
||||
|
||||
// This takes advantage of the knowledge that we know
|
||||
// we are receiving a PKCS#8 RSA Public Key at all
|
||||
// times from MySQL
|
||||
|
||||
RsaPublicKey::from_public_key_pem(pem).map_err(Error::protocol)
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
use bytes::buf::Buf;
|
||||
use bytes::Bytes;
|
||||
|
||||
use crate::collation::{CharSet, Collation};
|
||||
use crate::common::StatementCache;
|
||||
use crate::connection::{tls, MySqlConnectionInner, MySqlStream, MAX_PACKET_SIZE};
|
||||
use crate::error::Error;
|
||||
use crate::net::{Socket, WithSocket};
|
||||
use crate::protocol::connect::{
|
||||
AuthSwitchRequest, AuthSwitchResponse, Handshake, HandshakeResponse,
|
||||
};
|
||||
use crate::protocol::Capabilities;
|
||||
use crate::{MySqlConnectOptions, MySqlConnection, MySqlSslMode};
|
||||
|
||||
impl MySqlConnection {
|
||||
pub(crate) async fn establish(options: &MySqlConnectOptions) -> Result<Self, Error> {
|
||||
let do_handshake = DoHandshake::new(options)?;
|
||||
|
||||
let handshake = match &options.socket {
|
||||
Some(path) => crate::net::connect_uds(path, do_handshake).await?,
|
||||
None => crate::net::connect_tcp(&options.host, options.port, do_handshake).await?,
|
||||
};
|
||||
|
||||
let stream = handshake?;
|
||||
|
||||
Ok(Self {
|
||||
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(),
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct DoHandshake<'a> {
|
||||
options: &'a MySqlConnectOptions,
|
||||
charset: CharSet,
|
||||
collation: Collation,
|
||||
}
|
||||
|
||||
impl<'a> DoHandshake<'a> {
|
||||
fn new(options: &'a MySqlConnectOptions) -> Result<Self, Error> {
|
||||
let charset: CharSet = options.charset.parse()?;
|
||||
let collation: Collation = options
|
||||
.collation
|
||||
.as_deref()
|
||||
.map(|collation| collation.parse())
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| charset.default_collation());
|
||||
|
||||
if options.enable_cleartext_plugin
|
||||
&& matches!(
|
||||
options.ssl_mode,
|
||||
MySqlSslMode::Disabled | MySqlSslMode::Preferred
|
||||
)
|
||||
{
|
||||
log::warn!("Security warning: sending cleartext passwords without requiring SSL");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
options,
|
||||
charset,
|
||||
collation,
|
||||
})
|
||||
}
|
||||
|
||||
async fn do_handshake<S: Socket>(self, socket: S) -> Result<MySqlStream, Error> {
|
||||
let DoHandshake {
|
||||
options,
|
||||
charset,
|
||||
collation,
|
||||
} = self;
|
||||
|
||||
let mut stream = MySqlStream::with_socket(charset, collation, options, socket);
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/connection-phase.html
|
||||
// https://mariadb.com/kb/en/connection/
|
||||
|
||||
let handshake: Handshake = stream.recv_packet().await?.decode()?;
|
||||
|
||||
let mut plugin = handshake.auth_plugin;
|
||||
let nonce = handshake.auth_plugin_data;
|
||||
|
||||
// FIXME: server version parse is a bit ugly
|
||||
// expecting MAJOR.MINOR.PATCH
|
||||
|
||||
let mut server_version = handshake.server_version.split('.');
|
||||
|
||||
let server_version_major: u16 = server_version
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.parse()
|
||||
.unwrap_or(0);
|
||||
|
||||
let server_version_minor: u16 = server_version
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.parse()
|
||||
.unwrap_or(0);
|
||||
|
||||
let server_version_patch: u16 = server_version
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.parse()
|
||||
.unwrap_or(0);
|
||||
|
||||
stream.server_version = (
|
||||
server_version_major,
|
||||
server_version_minor,
|
||||
server_version_patch,
|
||||
);
|
||||
|
||||
stream.capabilities &= handshake.server_capabilities;
|
||||
stream.capabilities |= Capabilities::PROTOCOL_41;
|
||||
|
||||
let mut stream = tls::maybe_upgrade(stream, self.options).await?;
|
||||
|
||||
let auth_response = if let (Some(plugin), Some(password)) = (plugin, &options.password) {
|
||||
Some(plugin.scramble(&mut stream, password, &nonce).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
stream.write_packet(HandshakeResponse {
|
||||
collation: stream.collation as u8,
|
||||
max_packet_size: MAX_PACKET_SIZE,
|
||||
username: &options.username,
|
||||
database: options.database.as_deref(),
|
||||
auth_plugin: plugin,
|
||||
auth_response: auth_response.as_deref(),
|
||||
})?;
|
||||
|
||||
stream.flush().await?;
|
||||
|
||||
loop {
|
||||
let packet = stream.recv_packet().await?;
|
||||
match packet[0] {
|
||||
0x00 => {
|
||||
let _ok = packet.ok()?;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
0xfe => {
|
||||
let switch: AuthSwitchRequest =
|
||||
packet.decode_with(self.options.enable_cleartext_plugin)?;
|
||||
|
||||
plugin = Some(switch.plugin);
|
||||
let nonce = switch.data.chain(Bytes::new());
|
||||
|
||||
let response = switch
|
||||
.plugin
|
||||
.scramble(
|
||||
&mut stream,
|
||||
options.password.as_deref().unwrap_or_default(),
|
||||
&nonce,
|
||||
)
|
||||
.await?;
|
||||
|
||||
stream.write_packet(AuthSwitchResponse(response))?;
|
||||
stream.flush().await?;
|
||||
}
|
||||
|
||||
id => {
|
||||
if let (Some(plugin), Some(password)) = (plugin, &options.password) {
|
||||
if plugin.handle(&mut stream, packet, password, &nonce).await? {
|
||||
// plugin signaled authentication is ok
|
||||
break;
|
||||
}
|
||||
|
||||
// plugin signaled to continue authentication
|
||||
} else {
|
||||
return Err(err_protocol!(
|
||||
"unexpected packet 0x{:02x} during authentication",
|
||||
id
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> WithSocket for DoHandshake<'a> {
|
||||
type Output = Result<MySqlStream, Error>;
|
||||
|
||||
async fn with_socket<S: Socket>(self, socket: S) -> Self::Output {
|
||||
self.do_handshake(socket).await
|
||||
}
|
||||
}
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
use super::MySqlStream;
|
||||
use crate::connection::stream::Waiting;
|
||||
use crate::describe::Describe;
|
||||
use crate::error::Error;
|
||||
use crate::executor::{Execute, Executor};
|
||||
use crate::ext::ustr::UStr;
|
||||
use crate::io::MySqlBufExt;
|
||||
use crate::logger::QueryLogger;
|
||||
use crate::protocol::response::Status;
|
||||
use crate::protocol::statement::{
|
||||
BinaryRow, Execute as StatementExecute, Prepare, PrepareOk, StmtClose,
|
||||
};
|
||||
use crate::protocol::text::{ColumnDefinition, ColumnFlags, Query, TextRow};
|
||||
use crate::statement::{MySqlStatement, MySqlStatementMetadata};
|
||||
use crate::HashMap;
|
||||
use crate::{
|
||||
MySql, MySqlArguments, MySqlColumn, MySqlConnection, MySqlQueryResult, MySqlRow, MySqlTypeInfo,
|
||||
MySqlValueFormat,
|
||||
};
|
||||
use either::Either;
|
||||
use futures_core::future::BoxFuture;
|
||||
use futures_core::stream::BoxStream;
|
||||
use futures_core::Stream;
|
||||
use futures_util::TryStreamExt;
|
||||
use std::{borrow::Cow, pin::pin, sync::Arc};
|
||||
|
||||
impl MySqlConnection {
|
||||
async fn prepare_statement<'c>(
|
||||
&mut self,
|
||||
sql: &str,
|
||||
) -> Result<(u32, MySqlStatementMetadata), Error> {
|
||||
// https://dev.mysql.com/doc/internals/en/com-stmt-prepare.html
|
||||
// https://dev.mysql.com/doc/internals/en/com-stmt-prepare-response.html#packet-COM_STMT_PREPARE_OK
|
||||
|
||||
self.inner
|
||||
.stream
|
||||
.send_packet(Prepare { query: sql })
|
||||
.await?;
|
||||
|
||||
let ok: PrepareOk = self.inner.stream.recv().await?;
|
||||
|
||||
// the parameter definitions are very unreliable so we skip over them
|
||||
// as we have little use
|
||||
|
||||
if ok.params > 0 {
|
||||
for _ in 0..ok.params {
|
||||
let _def: ColumnDefinition = self.inner.stream.recv().await?;
|
||||
}
|
||||
|
||||
self.inner.stream.maybe_recv_eof().await?;
|
||||
}
|
||||
|
||||
// the column definitions are berefit the type information from the
|
||||
// to-be-bound parameters; we will receive the output column definitions
|
||||
// once more on execute so we wait for that
|
||||
|
||||
let mut columns = Vec::new();
|
||||
|
||||
let column_names = if ok.columns > 0 {
|
||||
recv_result_metadata(&mut self.inner.stream, ok.columns as usize, &mut columns).await?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let id = ok.statement_id;
|
||||
let metadata = MySqlStatementMetadata {
|
||||
parameters: ok.params as usize,
|
||||
columns: Arc::new(columns),
|
||||
column_names: Arc::new(column_names),
|
||||
};
|
||||
|
||||
Ok((id, metadata))
|
||||
}
|
||||
|
||||
async fn get_or_prepare_statement<'c>(
|
||||
&mut self,
|
||||
sql: &str,
|
||||
) -> Result<(u32, MySqlStatementMetadata), Error> {
|
||||
if let Some(statement) = self.inner.cache_statement.get_mut(sql) {
|
||||
// <MySqlStatementMetadata> is internally reference-counted
|
||||
return Ok((*statement).clone());
|
||||
}
|
||||
|
||||
let (id, metadata) = self.prepare_statement(sql).await?;
|
||||
|
||||
// in case of the cache being full, close the least recently used statement
|
||||
if let Some((id, _)) = self
|
||||
.inner
|
||||
.cache_statement
|
||||
.insert(sql, (id, metadata.clone()))
|
||||
{
|
||||
self.inner
|
||||
.stream
|
||||
.send_packet(StmtClose { statement: id })
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((id, metadata))
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_lifetimes)]
|
||||
pub(crate) async fn run<'e, 'c: 'e, 'q: 'e>(
|
||||
&'c mut self,
|
||||
sql: &'q str,
|
||||
arguments: Option<MySqlArguments>,
|
||||
persistent: bool,
|
||||
) -> Result<impl Stream<Item = Result<Either<MySqlQueryResult, MySqlRow>, Error>> + 'e, Error>
|
||||
{
|
||||
let mut logger = QueryLogger::new(sql, self.inner.log_settings.clone());
|
||||
|
||||
self.inner.stream.wait_until_ready().await?;
|
||||
self.inner.stream.waiting.push_back(Waiting::Result);
|
||||
|
||||
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
|
||||
let mut columns = Arc::new(Vec::new());
|
||||
|
||||
let (mut column_names, format, mut needs_metadata) = if let Some(arguments) = arguments {
|
||||
if persistent && self.inner.cache_statement.is_enabled() {
|
||||
let (id, metadata) = self
|
||||
.get_or_prepare_statement(sql)
|
||||
.await?;
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-stmt-execute.html
|
||||
self.inner.stream
|
||||
.send_packet(StatementExecute {
|
||||
statement: id,
|
||||
arguments: &arguments,
|
||||
})
|
||||
.await?;
|
||||
|
||||
(metadata.column_names, MySqlValueFormat::Binary, false)
|
||||
} else {
|
||||
let (id, metadata) = self
|
||||
.prepare_statement(sql)
|
||||
.await?;
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-stmt-execute.html
|
||||
self.inner.stream
|
||||
.send_packet(StatementExecute {
|
||||
statement: id,
|
||||
arguments: &arguments,
|
||||
})
|
||||
.await?;
|
||||
|
||||
self.inner.stream.send_packet(StmtClose { statement: id }).await?;
|
||||
|
||||
(metadata.column_names, MySqlValueFormat::Binary, false)
|
||||
}
|
||||
} else {
|
||||
// https://dev.mysql.com/doc/internals/en/com-query.html
|
||||
self.inner.stream.send_packet(Query(sql)).await?;
|
||||
|
||||
(Arc::default(), MySqlValueFormat::Text, true)
|
||||
};
|
||||
|
||||
loop {
|
||||
// query response is a meta-packet which may be one of:
|
||||
// Ok, Err, ResultSet, or (unhandled) LocalInfileRequest
|
||||
let mut packet = self.inner.stream.recv_packet().await?;
|
||||
|
||||
if packet[0] == 0x00 || packet[0] == 0xff {
|
||||
// first packet in a query response is OK or ERR
|
||||
// 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 {
|
||||
rows_affected,
|
||||
last_insert_id: ok.last_insert_id,
|
||||
};
|
||||
|
||||
r#yield!(Either::Left(done));
|
||||
|
||||
if ok.status.contains(Status::SERVER_MORE_RESULTS_EXISTS) {
|
||||
// more result sets exist, continue to the next one
|
||||
continue;
|
||||
}
|
||||
|
||||
self.inner.stream.waiting.pop_front();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// otherwise, this first packet is the start of the result-set metadata,
|
||||
*self.inner.stream.waiting.front_mut().unwrap() = Waiting::Row;
|
||||
|
||||
let num_columns = packet.get_uint_lenenc(); // column count
|
||||
let num_columns = usize::try_from(num_columns)
|
||||
.map_err(|_| err_protocol!("column count overflows usize: {num_columns}"))?;
|
||||
|
||||
if needs_metadata {
|
||||
column_names = Arc::new(recv_result_metadata(&mut self.inner.stream, num_columns, Arc::make_mut(&mut columns)).await?);
|
||||
} else {
|
||||
// next time we hit here, it'll be a new result set and we'll need the
|
||||
// full metadata
|
||||
needs_metadata = true;
|
||||
|
||||
recv_result_columns(&mut self.inner.stream, num_columns, Arc::make_mut(&mut columns)).await?;
|
||||
}
|
||||
|
||||
// finally, there will be none or many result-rows
|
||||
loop {
|
||||
let packet = self.inner.stream.recv_packet().await?;
|
||||
|
||||
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,
|
||||
}));
|
||||
|
||||
if eof.status.contains(Status::SERVER_MORE_RESULTS_EXISTS) {
|
||||
// more result sets exist, continue to the next one
|
||||
*self.inner.stream.waiting.front_mut().unwrap() = Waiting::Result;
|
||||
break;
|
||||
}
|
||||
|
||||
self.inner.stream.waiting.pop_front();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let row = match format {
|
||||
MySqlValueFormat::Binary => packet.decode_with::<BinaryRow, _>(&columns)?.0,
|
||||
MySqlValueFormat::Text => packet.decode_with::<TextRow, _>(&columns)?.0,
|
||||
};
|
||||
|
||||
let v = Either::Right(MySqlRow {
|
||||
row,
|
||||
format,
|
||||
columns: Arc::clone(&columns),
|
||||
column_names: Arc::clone(&column_names),
|
||||
});
|
||||
|
||||
logger.increment_rows_returned();
|
||||
|
||||
r#yield!(v);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'c> Executor<'c> for &'c mut MySqlConnection {
|
||||
type Database = MySql;
|
||||
|
||||
fn fetch_many<'e, 'q, E>(
|
||||
self,
|
||||
mut query: E,
|
||||
) -> BoxStream<'e, Result<Either<MySqlQueryResult, MySqlRow>, Error>>
|
||||
where
|
||||
'c: 'e,
|
||||
E: Execute<'q, Self::Database>,
|
||||
'q: 'e,
|
||||
E: 'q,
|
||||
{
|
||||
let sql = query.sql();
|
||||
let arguments = query.take_arguments().map_err(Error::Encode);
|
||||
let persistent = query.persistent();
|
||||
|
||||
Box::pin(try_stream! {
|
||||
let arguments = arguments?;
|
||||
let mut s = pin!(self.run(sql, arguments, persistent).await?);
|
||||
|
||||
while let Some(v) = s.try_next().await? {
|
||||
r#yield!(v);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_optional<'e, 'q, E>(self, query: E) -> BoxFuture<'e, Result<Option<MySqlRow>, Error>>
|
||||
where
|
||||
'c: 'e,
|
||||
E: Execute<'q, Self::Database>,
|
||||
'q: 'e,
|
||||
E: 'q,
|
||||
{
|
||||
let mut s = self.fetch_many(query);
|
||||
|
||||
Box::pin(async move {
|
||||
while let Some(v) = s.try_next().await? {
|
||||
if let Either::Right(r) = v {
|
||||
return Ok(Some(r));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_with<'e, 'q: 'e>(
|
||||
self,
|
||||
sql: &'q str,
|
||||
_parameters: &'e [MySqlTypeInfo],
|
||||
) -> BoxFuture<'e, Result<MySqlStatement<'q>, Error>>
|
||||
where
|
||||
'c: 'e,
|
||||
{
|
||||
Box::pin(async move {
|
||||
self.inner.stream.wait_until_ready().await?;
|
||||
|
||||
let metadata = if self.inner.cache_statement.is_enabled() {
|
||||
self.get_or_prepare_statement(sql).await?.1
|
||||
} else {
|
||||
let (id, metadata) = self.prepare_statement(sql).await?;
|
||||
|
||||
self.inner
|
||||
.stream
|
||||
.send_packet(StmtClose { statement: id })
|
||||
.await?;
|
||||
|
||||
metadata
|
||||
};
|
||||
|
||||
Ok(MySqlStatement {
|
||||
sql: Cow::Borrowed(sql),
|
||||
// metadata has internal Arcs for expensive data structures
|
||||
metadata: metadata.clone(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn describe<'e, 'q: 'e>(self, sql: &'q str) -> BoxFuture<'e, Result<Describe<MySql>, Error>>
|
||||
where
|
||||
'c: 'e,
|
||||
{
|
||||
Box::pin(async move {
|
||||
self.inner.stream.wait_until_ready().await?;
|
||||
|
||||
let (id, metadata) = self.prepare_statement(sql).await?;
|
||||
|
||||
self.inner
|
||||
.stream
|
||||
.send_packet(StmtClose { statement: id })
|
||||
.await?;
|
||||
|
||||
let columns = (*metadata.columns).clone();
|
||||
|
||||
let nullable = columns
|
||||
.iter()
|
||||
.map(|col| {
|
||||
col.flags
|
||||
.map(|flags| !flags.contains(ColumnFlags::NOT_NULL))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Describe {
|
||||
parameters: Some(Either::Right(metadata.parameters)),
|
||||
columns,
|
||||
nullable,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn recv_result_columns(
|
||||
stream: &mut MySqlStream,
|
||||
num_columns: usize,
|
||||
columns: &mut Vec<MySqlColumn>,
|
||||
) -> Result<(), Error> {
|
||||
columns.clear();
|
||||
columns.reserve(num_columns);
|
||||
|
||||
for ordinal in 0..num_columns {
|
||||
columns.push(recv_next_result_column(&stream.recv().await?, ordinal)?);
|
||||
}
|
||||
|
||||
if num_columns > 0 {
|
||||
stream.maybe_recv_eof().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recv_next_result_column(def: &ColumnDefinition, ordinal: usize) -> Result<MySqlColumn, Error> {
|
||||
// if the alias is empty, use the alias
|
||||
// only then use the name
|
||||
let name = match (def.name()?, def.alias()?) {
|
||||
(_, alias) if !alias.is_empty() => UStr::new(alias),
|
||||
(name, _) => UStr::new(name),
|
||||
};
|
||||
|
||||
let type_info = MySqlTypeInfo::from_column(def);
|
||||
|
||||
Ok(MySqlColumn {
|
||||
name,
|
||||
type_info,
|
||||
ordinal,
|
||||
flags: Some(def.flags),
|
||||
})
|
||||
}
|
||||
|
||||
async fn recv_result_metadata(
|
||||
stream: &mut MySqlStream,
|
||||
num_columns: usize,
|
||||
columns: &mut Vec<MySqlColumn>,
|
||||
) -> Result<HashMap<UStr, usize>, Error> {
|
||||
// the result-set metadata is primarily a listing of each output
|
||||
// column in the result-set
|
||||
|
||||
let mut column_names = HashMap::with_capacity(num_columns);
|
||||
|
||||
columns.clear();
|
||||
columns.reserve(num_columns);
|
||||
|
||||
for ordinal in 0..num_columns {
|
||||
let def: ColumnDefinition = stream.recv().await?;
|
||||
|
||||
let column = recv_next_result_column(&def, ordinal)?;
|
||||
|
||||
column_names.insert(column.name.clone(), ordinal);
|
||||
columns.push(column);
|
||||
}
|
||||
|
||||
stream.maybe_recv_eof().await?;
|
||||
|
||||
Ok(column_names)
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use futures_core::future::BoxFuture;
|
||||
use futures_util::FutureExt;
|
||||
pub(crate) use sqlx_core::connection::*;
|
||||
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;
|
||||
use crate::transaction::Transaction;
|
||||
use crate::{MySql, MySqlConnectOptions};
|
||||
|
||||
mod auth;
|
||||
mod establish;
|
||||
mod executor;
|
||||
mod stream;
|
||||
mod tls;
|
||||
|
||||
const MAX_PACKET_SIZE: u32 = 1024;
|
||||
|
||||
/// A connection to a MySQL database.
|
||||
pub struct MySqlConnection {
|
||||
pub(crate) inner: Box<MySqlConnectionInner>,
|
||||
}
|
||||
|
||||
pub(crate) struct MySqlConnectionInner {
|
||||
// underlying TCP stream,
|
||||
// wrapped in a potentially TLS stream,
|
||||
// wrapped in a buffered stream
|
||||
pub(crate) stream: MySqlStream,
|
||||
|
||||
// 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)>,
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
impl Connection for MySqlConnection {
|
||||
type Database = MySql;
|
||||
|
||||
type Options = MySqlConnectOptions;
|
||||
|
||||
fn close(mut self) -> BoxFuture<'static, Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
self.inner.stream.send_packet(Quit).await?;
|
||||
self.inner.stream.shutdown().await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn close_hard(mut self) -> BoxFuture<'static, Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
self.inner.stream.shutdown().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn ping(&mut self) -> BoxFuture<'_, Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
self.inner.stream.wait_until_ready().await?;
|
||||
self.inner.stream.send_packet(Ping).await?;
|
||||
self.inner.stream.recv_ok().await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn flush(&mut self) -> BoxFuture<'_, Result<(), Error>> {
|
||||
self.inner.stream.wait_until_ready().boxed()
|
||||
}
|
||||
|
||||
fn cached_statements_size(&self) -> usize {
|
||||
self.inner.cache_statement.len()
|
||||
}
|
||||
|
||||
fn clear_cached_statements(&mut self) -> BoxFuture<'_, Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
while let Some((statement_id, _)) = self.inner.cache_statement.remove_lru() {
|
||||
self.inner
|
||||
.stream
|
||||
.send_packet(StmtClose {
|
||||
statement: statement_id,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn should_flush(&self) -> bool {
|
||||
!self.inner.stream.write_buffer().is_empty()
|
||||
}
|
||||
|
||||
fn begin(&mut self) -> BoxFuture<'_, Result<Transaction<'_, Self::Database>, Error>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Transaction::begin(self, None)
|
||||
}
|
||||
|
||||
fn begin_with(
|
||||
&mut self,
|
||||
statement: impl Into<Cow<'static, str>>,
|
||||
) -> BoxFuture<'_, Result<Transaction<'_, Self::Database>, Error>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Transaction::begin(self, Some(statement.into()))
|
||||
}
|
||||
|
||||
fn shrink_buffers(&mut self) {
|
||||
self.inner.stream.shrink_buffers();
|
||||
}
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use bytes::{Buf, Bytes, BytesMut};
|
||||
|
||||
use crate::collation::{CharSet, Collation};
|
||||
use crate::error::Error;
|
||||
use crate::io::MySqlBufExt;
|
||||
use crate::io::{ProtocolDecode, ProtocolEncode};
|
||||
use crate::net::{BufferedSocket, Socket};
|
||||
use crate::protocol::response::{EofPacket, ErrPacket, OkPacket, Status};
|
||||
use crate::protocol::{Capabilities, Packet};
|
||||
use crate::{MySqlConnectOptions, MySqlDatabaseError};
|
||||
|
||||
pub struct MySqlStream<S = Box<dyn Socket>> {
|
||||
// Wrapping the socket in `Box` allows us to unsize in-place.
|
||||
pub(crate) socket: BufferedSocket<S>,
|
||||
pub(crate) server_version: (u16, u16, u16),
|
||||
pub(super) capabilities: Capabilities,
|
||||
pub(crate) sequence_id: u8,
|
||||
pub(crate) waiting: VecDeque<Waiting>,
|
||||
pub(crate) charset: CharSet,
|
||||
pub(crate) collation: Collation,
|
||||
pub(crate) is_tls: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Waiting {
|
||||
// waiting for a result set
|
||||
Result,
|
||||
|
||||
// waiting for a row within a result set
|
||||
Row,
|
||||
}
|
||||
|
||||
impl<S: Socket> MySqlStream<S> {
|
||||
pub(crate) fn with_socket(
|
||||
charset: CharSet,
|
||||
collation: Collation,
|
||||
options: &MySqlConnectOptions,
|
||||
socket: S,
|
||||
) -> Self {
|
||||
let mut capabilities = Capabilities::PROTOCOL_41
|
||||
| Capabilities::IGNORE_SPACE
|
||||
| Capabilities::DEPRECATE_EOF
|
||||
| Capabilities::FOUND_ROWS
|
||||
| Capabilities::TRANSACTIONS
|
||||
| Capabilities::SECURE_CONNECTION
|
||||
| Capabilities::PLUGIN_AUTH_LENENC_DATA
|
||||
| Capabilities::MULTI_STATEMENTS
|
||||
| Capabilities::MULTI_RESULTS
|
||||
| Capabilities::PLUGIN_AUTH
|
||||
| Capabilities::PS_MULTI_RESULTS
|
||||
| Capabilities::SSL;
|
||||
|
||||
if options.database.is_some() {
|
||||
capabilities |= Capabilities::CONNECT_WITH_DB;
|
||||
}
|
||||
|
||||
Self {
|
||||
waiting: VecDeque::new(),
|
||||
capabilities,
|
||||
server_version: (0, 0, 0),
|
||||
sequence_id: 0,
|
||||
collation,
|
||||
charset,
|
||||
socket: BufferedSocket::new(socket),
|
||||
is_tls: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_until_ready(&mut self) -> Result<(), Error> {
|
||||
if !self.socket.write_buffer().is_empty() {
|
||||
self.socket.flush().await?;
|
||||
}
|
||||
|
||||
while !self.waiting.is_empty() {
|
||||
while self.waiting.front() == Some(&Waiting::Row) {
|
||||
let packet = self.recv_packet().await?;
|
||||
|
||||
if !packet.is_empty() && packet[0] == 0xfe && packet.len() < 9 {
|
||||
let eof = packet.eof(self.capabilities)?;
|
||||
|
||||
if eof.status.contains(Status::SERVER_MORE_RESULTS_EXISTS) {
|
||||
*self.waiting.front_mut().unwrap() = Waiting::Result;
|
||||
} else {
|
||||
self.waiting.pop_front();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
while self.waiting.front() == Some(&Waiting::Result) {
|
||||
let packet = self.recv_packet().await?;
|
||||
|
||||
if !packet.is_empty() && (packet[0] == 0x00 || packet[0] == 0xff) {
|
||||
let ok = packet.ok()?;
|
||||
|
||||
if !ok.status.contains(Status::SERVER_MORE_RESULTS_EXISTS) {
|
||||
self.waiting.pop_front();
|
||||
}
|
||||
} else {
|
||||
*self.waiting.front_mut().unwrap() = Waiting::Row;
|
||||
self.skip_result_metadata(packet).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn send_packet<'en, T>(&mut self, payload: T) -> Result<(), Error>
|
||||
where
|
||||
T: ProtocolEncode<'en, Capabilities>,
|
||||
{
|
||||
self.sequence_id = 0;
|
||||
self.write_packet(payload)?;
|
||||
self.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_packet<'en, T>(&mut self, payload: T) -> Result<(), Error>
|
||||
where
|
||||
T: ProtocolEncode<'en, Capabilities>,
|
||||
{
|
||||
self.socket
|
||||
.write_with(Packet(payload), (self.capabilities, &mut self.sequence_id))
|
||||
}
|
||||
|
||||
async fn recv_packet_part(&mut self) -> Result<Bytes, Error> {
|
||||
// https://dev.mysql.com/doc/dev/mysql-server/8.0.12/page_protocol_basic_packets.html
|
||||
// https://mariadb.com/kb/en/library/0-packet/#standard-packet
|
||||
|
||||
let mut header: Bytes = self.socket.read(4).await?;
|
||||
|
||||
// cannot overflow
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let packet_size = header.get_uint_le(3) as usize;
|
||||
let sequence_id = header.get_u8();
|
||||
|
||||
self.sequence_id = sequence_id.wrapping_add(1);
|
||||
|
||||
let payload: Bytes = self.socket.read(packet_size).await?;
|
||||
|
||||
// TODO: packet compression
|
||||
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
// receive the next packet from the database server
|
||||
// may block (async) on more data from the server
|
||||
pub(crate) async fn recv_packet(&mut self) -> Result<Packet<Bytes>, Error> {
|
||||
let payload = self.recv_packet_part().await?;
|
||||
let payload = if payload.len() < 0xFF_FF_FF {
|
||||
payload
|
||||
} else {
|
||||
let mut final_payload = BytesMut::with_capacity(0xFF_FF_FF * 2);
|
||||
final_payload.extend_from_slice(&payload);
|
||||
|
||||
drop(payload); // we don't need the allocation anymore
|
||||
|
||||
let mut last_read = 0xFF_FF_FF;
|
||||
while last_read == 0xFF_FF_FF {
|
||||
let part = self.recv_packet_part().await?;
|
||||
last_read = part.len();
|
||||
final_payload.extend_from_slice(&part);
|
||||
}
|
||||
final_payload.into()
|
||||
};
|
||||
|
||||
if payload
|
||||
.first()
|
||||
.ok_or(err_protocol!("Packet empty"))?
|
||||
.eq(&0xff)
|
||||
{
|
||||
self.waiting.pop_front();
|
||||
|
||||
// instead of letting this packet be looked at everywhere, we check here
|
||||
// and emit a proper Error
|
||||
return Err(
|
||||
MySqlDatabaseError(ErrPacket::decode_with(payload, self.capabilities)?).into(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Packet(payload))
|
||||
}
|
||||
|
||||
pub(crate) async fn recv<'de, T>(&mut self) -> Result<T, Error>
|
||||
where
|
||||
T: ProtocolDecode<'de, Capabilities>,
|
||||
{
|
||||
self.recv_packet().await?.decode_with(self.capabilities)
|
||||
}
|
||||
|
||||
pub(crate) async fn recv_ok(&mut self) -> Result<OkPacket, Error> {
|
||||
self.recv_packet().await?.ok()
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_recv_eof(&mut self) -> Result<Option<EofPacket>, Error> {
|
||||
if self.capabilities.contains(Capabilities::DEPRECATE_EOF) {
|
||||
Ok(None)
|
||||
} else {
|
||||
self.recv().await.map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
async fn skip_result_metadata(&mut self, mut packet: Packet<Bytes>) -> Result<(), Error> {
|
||||
let num_columns: u64 = packet.get_uint_lenenc(); // column count
|
||||
|
||||
for _ in 0..num_columns {
|
||||
let _ = self.recv_packet().await?;
|
||||
}
|
||||
|
||||
self.maybe_recv_eof().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn boxed_socket(self) -> MySqlStream {
|
||||
MySqlStream {
|
||||
socket: self.socket.boxed(),
|
||||
server_version: self.server_version,
|
||||
capabilities: self.capabilities,
|
||||
sequence_id: self.sequence_id,
|
||||
waiting: self.waiting,
|
||||
charset: self.charset,
|
||||
collation: self.collation,
|
||||
is_tls: self.is_tls,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Deref for MySqlStream<S> {
|
||||
type Target = BufferedSocket<S>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.socket
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> DerefMut for MySqlStream<S> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.socket
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
use crate::collation::{CharSet, Collation};
|
||||
use crate::connection::{MySqlStream, Waiting};
|
||||
use crate::error::Error;
|
||||
use crate::net::tls::TlsConfig;
|
||||
use crate::net::{tls, BufferedSocket, Socket, WithSocket};
|
||||
use crate::protocol::connect::SslRequest;
|
||||
use crate::protocol::Capabilities;
|
||||
use crate::{MySqlConnectOptions, MySqlSslMode};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
struct MapStream {
|
||||
server_version: (u16, u16, u16),
|
||||
capabilities: Capabilities,
|
||||
sequence_id: u8,
|
||||
waiting: VecDeque<Waiting>,
|
||||
charset: CharSet,
|
||||
collation: Collation,
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_upgrade<S: Socket>(
|
||||
mut stream: MySqlStream<S>,
|
||||
options: &MySqlConnectOptions,
|
||||
) -> Result<MySqlStream, Error> {
|
||||
let server_supports_tls = stream.capabilities.contains(Capabilities::SSL);
|
||||
|
||||
if matches!(options.ssl_mode, MySqlSslMode::Disabled) || !tls::available() {
|
||||
// remove the SSL capability if SSL has been explicitly disabled
|
||||
stream.capabilities.remove(Capabilities::SSL);
|
||||
}
|
||||
|
||||
// https://www.postgresql.org/docs/12/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS
|
||||
match options.ssl_mode {
|
||||
MySqlSslMode::Disabled => return Ok(stream.boxed_socket()),
|
||||
|
||||
MySqlSslMode::Preferred => {
|
||||
if !tls::available() {
|
||||
// Client doesn't support TLS
|
||||
tracing::debug!("not performing TLS upgrade: TLS support not compiled in");
|
||||
return Ok(stream.boxed_socket());
|
||||
}
|
||||
|
||||
if !server_supports_tls {
|
||||
// Server doesn't support TLS
|
||||
tracing::debug!("not performing TLS upgrade: unsupported by server");
|
||||
return Ok(stream.boxed_socket());
|
||||
}
|
||||
}
|
||||
|
||||
MySqlSslMode::Required | MySqlSslMode::VerifyIdentity | MySqlSslMode::VerifyCa => {
|
||||
tls::error_if_unavailable()?;
|
||||
|
||||
if !server_supports_tls {
|
||||
// upgrade failed, die
|
||||
return Err(Error::Tls("server does not support TLS".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tls_config = TlsConfig {
|
||||
accept_invalid_certs: !matches!(
|
||||
options.ssl_mode,
|
||||
MySqlSslMode::VerifyCa | MySqlSslMode::VerifyIdentity
|
||||
),
|
||||
accept_invalid_hostnames: !matches!(options.ssl_mode, MySqlSslMode::VerifyIdentity),
|
||||
hostname: &options.host,
|
||||
root_cert_path: options.ssl_ca.as_ref(),
|
||||
client_cert_path: options.ssl_client_cert.as_ref(),
|
||||
client_key_path: options.ssl_client_key.as_ref(),
|
||||
};
|
||||
|
||||
// Request TLS upgrade
|
||||
stream.write_packet(SslRequest {
|
||||
max_packet_size: super::MAX_PACKET_SIZE,
|
||||
collation: stream.collation as u8,
|
||||
})?;
|
||||
|
||||
stream.flush().await?;
|
||||
|
||||
tls::handshake(
|
||||
stream.socket.into_inner(),
|
||||
tls_config,
|
||||
MapStream {
|
||||
server_version: stream.server_version,
|
||||
capabilities: stream.capabilities,
|
||||
sequence_id: stream.sequence_id,
|
||||
waiting: stream.waiting,
|
||||
charset: stream.charset,
|
||||
collation: stream.collation,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
impl WithSocket for MapStream {
|
||||
type Output = MySqlStream;
|
||||
|
||||
async fn with_socket<S: Socket>(self, socket: S) -> Self::Output {
|
||||
MySqlStream {
|
||||
socket: BufferedSocket::new(Box::new(socket)),
|
||||
server_version: self.server_version,
|
||||
capabilities: self.capabilities,
|
||||
sequence_id: self.sequence_id,
|
||||
waiting: self.waiting,
|
||||
charset: self.charset,
|
||||
collation: self.collation,
|
||||
is_tls: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
use crate::value::{MySqlValue, MySqlValueRef};
|
||||
use crate::{
|
||||
MySqlArguments, MySqlColumn, MySqlConnection, MySqlQueryResult, MySqlRow, MySqlStatement,
|
||||
MySqlTransactionManager, MySqlTypeInfo,
|
||||
};
|
||||
pub(crate) use sqlx_core::database::{Database, HasStatementCache};
|
||||
|
||||
/// MySQL database driver.
|
||||
#[derive(Debug)]
|
||||
pub struct MySql;
|
||||
|
||||
impl Database for MySql {
|
||||
type Connection = MySqlConnection;
|
||||
|
||||
type TransactionManager = MySqlTransactionManager;
|
||||
|
||||
type Row = MySqlRow;
|
||||
|
||||
type QueryResult = MySqlQueryResult;
|
||||
|
||||
type Column = MySqlColumn;
|
||||
|
||||
type TypeInfo = MySqlTypeInfo;
|
||||
|
||||
type Value = MySqlValue;
|
||||
type ValueRef<'r> = MySqlValueRef<'r>;
|
||||
|
||||
type Arguments<'q> = MySqlArguments;
|
||||
type ArgumentBuffer<'q> = Vec<u8>;
|
||||
|
||||
type Statement<'q> = MySqlStatement<'q>;
|
||||
|
||||
const NAME: &'static str = "MySQL";
|
||||
|
||||
const URL_SCHEMES: &'static [&'static str] = &["mysql", "mariadb"];
|
||||
}
|
||||
|
||||
impl HasStatementCache for MySql {}
|
||||
Vendored
+177
@@ -0,0 +1,177 @@
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
|
||||
use crate::protocol::response::ErrPacket;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub(crate) use sqlx_core::error::*;
|
||||
|
||||
/// An error returned from the MySQL database.
|
||||
pub struct MySqlDatabaseError(pub(super) ErrPacket);
|
||||
|
||||
impl MySqlDatabaseError {
|
||||
/// The [SQLSTATE](https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html) code for this error.
|
||||
pub fn code(&self) -> Option<&str> {
|
||||
self.0.sql_state.as_deref()
|
||||
}
|
||||
|
||||
/// The [number](https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html)
|
||||
/// for this error.
|
||||
///
|
||||
/// MySQL tends to use SQLSTATE as a general error category, and the error number as a more
|
||||
/// granular indication of the error.
|
||||
pub fn number(&self) -> u16 {
|
||||
self.0.error_code
|
||||
}
|
||||
|
||||
/// The human-readable error message.
|
||||
pub fn message(&self) -> &str {
|
||||
&self.0.error_message
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for MySqlDatabaseError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("MySqlDatabaseError")
|
||||
.field("code", &self.code())
|
||||
.field("number", &self.number())
|
||||
.field("message", &self.message())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MySqlDatabaseError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if let Some(code) = &self.code() {
|
||||
write!(f, "{} ({}): {}", self.number(), code, self.message())
|
||||
} else {
|
||||
write!(f, "{}: {}", self.number(), self.message())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for MySqlDatabaseError {}
|
||||
|
||||
impl DatabaseError for MySqlDatabaseError {
|
||||
#[inline]
|
||||
fn message(&self) -> &str {
|
||||
self.message()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn code(&self) -> Option<Cow<'_, str>> {
|
||||
self.code().map(Cow::Borrowed)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn as_error(&self) -> &(dyn StdError + Send + Sync + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn as_error_mut(&mut self) -> &mut (dyn StdError + Send + Sync + 'static) {
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn into_error(self: Box<Self>) -> Box<dyn StdError + Send + Sync + 'static> {
|
||||
self
|
||||
}
|
||||
|
||||
fn kind(&self) -> ErrorKind {
|
||||
match self.number() {
|
||||
error_codes::ER_DUP_KEY
|
||||
| error_codes::ER_DUP_ENTRY
|
||||
| error_codes::ER_DUP_UNIQUE
|
||||
| error_codes::ER_DUP_ENTRY_WITH_KEY_NAME
|
||||
| error_codes::ER_DUP_UNKNOWN_IN_INDEX => ErrorKind::UniqueViolation,
|
||||
|
||||
error_codes::ER_NO_REFERENCED_ROW
|
||||
| error_codes::ER_NO_REFERENCED_ROW_2
|
||||
| error_codes::ER_ROW_IS_REFERENCED
|
||||
| error_codes::ER_ROW_IS_REFERENCED_2
|
||||
| error_codes::ER_FK_COLUMN_NOT_NULL
|
||||
| error_codes::ER_FK_CANNOT_DELETE_PARENT => ErrorKind::ForeignKeyViolation,
|
||||
|
||||
error_codes::ER_BAD_NULL_ERROR | error_codes::ER_NO_DEFAULT_FOR_FIELD => {
|
||||
ErrorKind::NotNullViolation
|
||||
}
|
||||
|
||||
error_codes::ER_CHECK_CONSTRAINT_VIOLATED => ErrorKind::CheckViolation,
|
||||
|
||||
// https://mariadb.com/kb/en/e4025/
|
||||
error_codes::mariadb::ER_CONSTRAINT_FAILED
|
||||
// MySQL uses this code for a completely different error,
|
||||
// but we can differentiate by SQLSTATE:
|
||||
// <https://dev.mysql.com/doc/mysql-errors/8.4/en/server-error-reference.html#error_er_innodb_autoextend_size_out_of_range
|
||||
if self.0.sql_state.as_deref() == Some("23000") =>
|
||||
{
|
||||
ErrorKind::CheckViolation
|
||||
}
|
||||
|
||||
_ => ErrorKind::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The MySQL server uses SQLSTATEs as a generic error category,
|
||||
/// and returns a `error_code` instead within the error packet.
|
||||
///
|
||||
/// For reference: <https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html>.
|
||||
pub(crate) mod error_codes {
|
||||
/// Caused when a DDL operation creates duplicated keys.
|
||||
pub const ER_DUP_KEY: u16 = 1022;
|
||||
/// Caused when a DML operation tries create a duplicated entry for a key,
|
||||
/// be it a unique or primary one.
|
||||
pub const ER_DUP_ENTRY: u16 = 1062;
|
||||
/// Similar to `ER_DUP_ENTRY`, but only present in NDB clusters.
|
||||
///
|
||||
/// See: <https://github.com/mysql/mysql-server/blob/fbdaa4def30d269bc4de5b85de61de34b11c0afc/mysql-test/suite/stress/include/ddl7.inc#L68>.
|
||||
pub const ER_DUP_UNIQUE: u16 = 1169;
|
||||
/// Similar to `ER_DUP_ENTRY`, but with a formatted string message.
|
||||
///
|
||||
/// See: <https://bugs.mysql.com/bug.php?id=46976>.
|
||||
pub const ER_DUP_ENTRY_WITH_KEY_NAME: u16 = 1586;
|
||||
/// Caused when a DDL operation to add a unique index fails,
|
||||
/// because duplicate items were created by concurrent DML operations.
|
||||
/// When this happens, the key is unknown, so the server can't use `ER_DUP_KEY`.
|
||||
///
|
||||
/// For example: an `INSERT` operation creates duplicate `name` fields when `ALTER`ing a table and making `name` unique.
|
||||
pub const ER_DUP_UNKNOWN_IN_INDEX: u16 = 1859;
|
||||
|
||||
/// Caused when inserting an entry with a column with a value that does not reference a foreign row.
|
||||
pub const ER_NO_REFERENCED_ROW: u16 = 1216;
|
||||
/// Caused when deleting a row that is referenced in other tables.
|
||||
pub const ER_ROW_IS_REFERENCED: u16 = 1217;
|
||||
/// Caused when deleting a row that is referenced in other tables.
|
||||
/// This differs from `ER_ROW_IS_REFERENCED` in that the error message contains the affected constraint.
|
||||
pub const ER_ROW_IS_REFERENCED_2: u16 = 1451;
|
||||
/// Caused when inserting an entry with a column with a value that does not reference a foreign row.
|
||||
/// This differs from `ER_NO_REFERENCED_ROW` in that the error message contains the affected constraint.
|
||||
pub const ER_NO_REFERENCED_ROW_2: u16 = 1452;
|
||||
/// Caused when creating a FK with `ON DELETE SET NULL` or `ON UPDATE SET NULL` to a column that is `NOT NULL`, or vice-versa.
|
||||
pub const ER_FK_COLUMN_NOT_NULL: u16 = 1830;
|
||||
/// Removed in 5.7.3.
|
||||
pub const ER_FK_CANNOT_DELETE_PARENT: u16 = 1834;
|
||||
|
||||
/// Caused when inserting a NULL value to a column marked as NOT NULL.
|
||||
pub const ER_BAD_NULL_ERROR: u16 = 1048;
|
||||
/// Caused when inserting a DEFAULT value to a column marked as NOT NULL, which also doesn't have a default value set.
|
||||
pub const ER_NO_DEFAULT_FOR_FIELD: u16 = 1364;
|
||||
|
||||
/// Caused when a check constraint is violated.
|
||||
///
|
||||
/// Only available after 8.0.16.
|
||||
pub const ER_CHECK_CONSTRAINT_VIOLATED: u16 = 3819;
|
||||
|
||||
pub(crate) mod mariadb {
|
||||
/// Error code emitted by MariaDB for constraint errors: <https://mariadb.com/kb/en/e4025/>
|
||||
///
|
||||
/// MySQL emits this code for a completely different error:
|
||||
/// <https://dev.mysql.com/doc/mysql-errors/8.4/en/server-error-reference.html#error_er_innodb_autoextend_size_out_of_range>
|
||||
///
|
||||
/// You also check that SQLSTATE is `23000`.
|
||||
pub const ER_CONSTRAINT_FAILED: u16 = 4025;
|
||||
}
|
||||
}
|
||||
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::io::BufExt;
|
||||
|
||||
pub trait MySqlBufExt: Buf {
|
||||
// Read a length-encoded integer.
|
||||
// NOTE: 0xfb or NULL is only returned for binary value encoding to indicate NULL.
|
||||
// NOTE: 0xff is only returned during a result set to indicate ERR.
|
||||
// <https://dev.mysql.com/doc/internals/en/integer.html#packet-Protocol::LengthEncodedInteger>
|
||||
fn get_uint_lenenc(&mut self) -> u64;
|
||||
|
||||
// Read a length-encoded string.
|
||||
#[allow(dead_code)]
|
||||
fn get_str_lenenc(&mut self) -> Result<String, Error>;
|
||||
|
||||
// Read a length-encoded byte sequence.
|
||||
fn get_bytes_lenenc(&mut self) -> Result<Bytes, Error>;
|
||||
}
|
||||
|
||||
impl MySqlBufExt for Bytes {
|
||||
fn get_uint_lenenc(&mut self) -> u64 {
|
||||
match self.get_u8() {
|
||||
0xfc => u64::from(self.get_u16_le()),
|
||||
0xfd => self.get_uint_le(3),
|
||||
0xfe => self.get_u64_le(),
|
||||
|
||||
v => u64::from(v),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_str_lenenc(&mut self) -> Result<String, Error> {
|
||||
let size = self.get_uint_lenenc();
|
||||
let size = usize::try_from(size)
|
||||
.map_err(|_| err_protocol!("string length overflows usize: {size}"))?;
|
||||
|
||||
self.get_str(size)
|
||||
}
|
||||
|
||||
fn get_bytes_lenenc(&mut self) -> Result<Bytes, Error> {
|
||||
let size = self.get_uint_lenenc();
|
||||
let size = usize::try_from(size)
|
||||
.map_err(|_| err_protocol!("string length overflows usize: {size}"))?;
|
||||
|
||||
Ok(self.split_to(size))
|
||||
}
|
||||
}
|
||||
Vendored
+131
@@ -0,0 +1,131 @@
|
||||
use bytes::BufMut;
|
||||
|
||||
pub trait MySqlBufMutExt: BufMut {
|
||||
fn put_uint_lenenc(&mut self, v: u64);
|
||||
|
||||
fn put_str_lenenc(&mut self, v: &str);
|
||||
|
||||
fn put_bytes_lenenc(&mut self, v: &[u8]);
|
||||
}
|
||||
|
||||
impl MySqlBufMutExt for Vec<u8> {
|
||||
fn put_uint_lenenc(&mut self, v: u64) {
|
||||
// https://dev.mysql.com/doc/internals/en/integer.html
|
||||
// https://mariadb.com/kb/en/library/protocol-data-types/#length-encoded-integers
|
||||
|
||||
let encoded_le = v.to_le_bytes();
|
||||
|
||||
match v {
|
||||
0..=250 => self.push(encoded_le[0]),
|
||||
251..=0xFF_FF => {
|
||||
self.push(0xfc);
|
||||
self.extend_from_slice(&encoded_le[..2]);
|
||||
}
|
||||
0x1_00_00..=0xFF_FF_FF => {
|
||||
self.push(0xfd);
|
||||
self.extend_from_slice(&encoded_le[..3]);
|
||||
}
|
||||
_ => {
|
||||
self.push(0xfe);
|
||||
self.extend_from_slice(&encoded_le);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn put_str_lenenc(&mut self, v: &str) {
|
||||
self.put_bytes_lenenc(v.as_bytes());
|
||||
}
|
||||
|
||||
fn put_bytes_lenenc(&mut self, v: &[u8]) {
|
||||
self.put_uint_lenenc(v.len() as u64);
|
||||
self.extend(v);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encodes_int_lenenc_u8() {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
buf.put_uint_lenenc(0xFA as u64);
|
||||
|
||||
assert_eq!(&buf[..], b"\xFA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encodes_int_lenenc_u16() {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
buf.put_uint_lenenc(std::u16::MAX as u64);
|
||||
|
||||
assert_eq!(&buf[..], b"\xFC\xFF\xFF");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encodes_int_lenenc_u24() {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
buf.put_uint_lenenc(0xFF_FF_FF as u64);
|
||||
|
||||
assert_eq!(&buf[..], b"\xFD\xFF\xFF\xFF");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encodes_int_lenenc_u64() {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
buf.put_uint_lenenc(std::u64::MAX);
|
||||
|
||||
assert_eq!(&buf[..], b"\xFE\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encodes_int_lenenc_fb() {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
buf.put_uint_lenenc(0xFB as u64);
|
||||
|
||||
assert_eq!(&buf[..], b"\xFC\xFB\x00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encodes_int_lenenc_fc() {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
buf.put_uint_lenenc(0xFC as u64);
|
||||
|
||||
assert_eq!(&buf[..], b"\xFC\xFC\x00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encodes_int_lenenc_fd() {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
buf.put_uint_lenenc(0xFD as u64);
|
||||
|
||||
assert_eq!(&buf[..], b"\xFC\xFD\x00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encodes_int_lenenc_fe() {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
buf.put_uint_lenenc(0xFE as u64);
|
||||
|
||||
assert_eq!(&buf[..], b"\xFC\xFE\x00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encodes_int_lenenc_ff() {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
buf.put_uint_lenenc(0xFF as u64);
|
||||
|
||||
assert_eq!(&buf[..], b"\xFC\xFF\x00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encodes_string_lenenc() {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
buf.put_str_lenenc("random_string");
|
||||
|
||||
assert_eq!(&buf[..], b"\x0Drandom_string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encodes_byte_lenenc() {
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
buf.put_bytes_lenenc(b"random_string");
|
||||
|
||||
assert_eq!(&buf[..], b"\x0Drandom_string");
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
mod buf;
|
||||
mod buf_mut;
|
||||
|
||||
pub use buf::MySqlBufExt;
|
||||
pub use buf_mut::MySqlBufMutExt;
|
||||
|
||||
pub(crate) use sqlx_core::io::*;
|
||||
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
//! **MySQL** database driver.
|
||||
#![deny(clippy::cast_possible_truncation)]
|
||||
#![deny(clippy::cast_possible_wrap)]
|
||||
#![deny(clippy::cast_sign_loss)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate sqlx_core;
|
||||
|
||||
use crate::executor::Executor;
|
||||
|
||||
pub(crate) use sqlx_core::driver_prelude::*;
|
||||
|
||||
#[cfg(feature = "any")]
|
||||
pub mod any;
|
||||
|
||||
mod arguments;
|
||||
mod collation;
|
||||
mod column;
|
||||
mod connection;
|
||||
mod database;
|
||||
mod error;
|
||||
mod io;
|
||||
mod options;
|
||||
mod protocol;
|
||||
mod query_result;
|
||||
mod row;
|
||||
mod statement;
|
||||
mod transaction;
|
||||
mod type_checking;
|
||||
mod type_info;
|
||||
pub mod types;
|
||||
mod value;
|
||||
|
||||
#[cfg(feature = "migrate")]
|
||||
mod migrate;
|
||||
|
||||
#[cfg(feature = "migrate")]
|
||||
mod testing;
|
||||
|
||||
pub use arguments::MySqlArguments;
|
||||
pub use column::MySqlColumn;
|
||||
pub use connection::MySqlConnection;
|
||||
pub use database::MySql;
|
||||
pub use error::MySqlDatabaseError;
|
||||
pub use options::{MySqlConnectOptions, MySqlSslMode};
|
||||
pub use query_result::MySqlQueryResult;
|
||||
pub use row::MySqlRow;
|
||||
pub use statement::MySqlStatement;
|
||||
pub use transaction::MySqlTransactionManager;
|
||||
pub use type_info::MySqlTypeInfo;
|
||||
pub use value::{MySqlValue, MySqlValueFormat, MySqlValueRef};
|
||||
|
||||
/// An alias for [`Pool`][crate::pool::Pool], specialized for MySQL.
|
||||
pub type MySqlPool = crate::pool::Pool<MySql>;
|
||||
|
||||
/// An alias for [`PoolOptions`][crate::pool::PoolOptions], specialized for MySQL.
|
||||
pub type MySqlPoolOptions = crate::pool::PoolOptions<MySql>;
|
||||
|
||||
/// An alias for [`Executor<'_, Database = MySql>`][Executor].
|
||||
pub trait MySqlExecutor<'c>: Executor<'c, Database = MySql> {}
|
||||
impl<'c, T: Executor<'c, Database = MySql>> MySqlExecutor<'c> for T {}
|
||||
|
||||
/// An alias for [`Transaction`][crate::transaction::Transaction], specialized for MySQL.
|
||||
pub type MySqlTransaction<'c> = crate::transaction::Transaction<'c, MySql>;
|
||||
|
||||
// NOTE: required due to the lack of lazy normalization
|
||||
impl_into_arguments_for_arguments!(MySqlArguments);
|
||||
impl_acquire!(MySql, MySqlConnection);
|
||||
impl_column_index_for_row!(MySqlRow);
|
||||
impl_column_index_for_statement!(MySqlStatement);
|
||||
|
||||
// required because some databases have a different handling of NULL
|
||||
impl_encode_for_option!(MySql);
|
||||
Vendored
+302
@@ -0,0 +1,302 @@
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use futures_core::future::BoxFuture;
|
||||
pub(crate) use sqlx_core::migrate::*;
|
||||
|
||||
use crate::connection::{ConnectOptions, Connection};
|
||||
use crate::error::Error;
|
||||
use crate::executor::Executor;
|
||||
use crate::query::query;
|
||||
use crate::query_as::query_as;
|
||||
use crate::query_scalar::query_scalar;
|
||||
use crate::{MySql, MySqlConnectOptions, MySqlConnection};
|
||||
|
||||
fn parse_for_maintenance(url: &str) -> Result<(MySqlConnectOptions, String), Error> {
|
||||
let mut options = MySqlConnectOptions::from_str(url)?;
|
||||
|
||||
let database = if let Some(database) = &options.database {
|
||||
database.to_owned()
|
||||
} else {
|
||||
return Err(Error::Configuration(
|
||||
"DATABASE_URL does not specify a database".into(),
|
||||
));
|
||||
};
|
||||
|
||||
// switch us to <no> database for create/drop commands
|
||||
options.database = None;
|
||||
|
||||
Ok((options, database))
|
||||
}
|
||||
|
||||
impl MigrateDatabase for MySql {
|
||||
fn create_database(url: &str) -> BoxFuture<'_, Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
let (options, database) = parse_for_maintenance(url)?;
|
||||
let mut conn = options.connect().await?;
|
||||
|
||||
let _ = conn
|
||||
.execute(&*format!("CREATE DATABASE `{database}`"))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn database_exists(url: &str) -> BoxFuture<'_, Result<bool, Error>> {
|
||||
Box::pin(async move {
|
||||
let (options, database) = parse_for_maintenance(url)?;
|
||||
let mut conn = options.connect().await?;
|
||||
|
||||
let exists: bool = query_scalar(
|
||||
"select exists(SELECT 1 from INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?)",
|
||||
)
|
||||
.bind(database)
|
||||
.fetch_one(&mut conn)
|
||||
.await?;
|
||||
|
||||
Ok(exists)
|
||||
})
|
||||
}
|
||||
|
||||
fn drop_database(url: &str) -> BoxFuture<'_, Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
let (options, database) = parse_for_maintenance(url)?;
|
||||
let mut conn = options.connect().await?;
|
||||
|
||||
let _ = conn
|
||||
.execute(&*format!("DROP DATABASE IF EXISTS `{database}`"))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Migrate for MySqlConnection {
|
||||
fn ensure_migrations_table(&mut self) -> BoxFuture<'_, Result<(), MigrateError>> {
|
||||
Box::pin(async move {
|
||||
// language=MySQL
|
||||
self.execute(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS _sqlx_migrations (
|
||||
version BIGINT PRIMARY KEY,
|
||||
description TEXT NOT NULL,
|
||||
installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
success BOOLEAN NOT NULL,
|
||||
checksum BLOB NOT NULL,
|
||||
execution_time BIGINT NOT NULL
|
||||
);
|
||||
"#,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn dirty_version(&mut self) -> BoxFuture<'_, Result<Option<i64>, MigrateError>> {
|
||||
Box::pin(async move {
|
||||
// language=SQL
|
||||
let row: Option<(i64,)> = query_as(
|
||||
"SELECT version FROM _sqlx_migrations WHERE success = false ORDER BY version LIMIT 1",
|
||||
)
|
||||
.fetch_optional(self)
|
||||
.await?;
|
||||
|
||||
Ok(row.map(|r| r.0))
|
||||
})
|
||||
}
|
||||
|
||||
fn list_applied_migrations(
|
||||
&mut self,
|
||||
) -> BoxFuture<'_, Result<Vec<AppliedMigration>, MigrateError>> {
|
||||
Box::pin(async move {
|
||||
// language=SQL
|
||||
let rows: Vec<(i64, Vec<u8>)> =
|
||||
query_as("SELECT version, checksum FROM _sqlx_migrations ORDER BY version")
|
||||
.fetch_all(self)
|
||||
.await?;
|
||||
|
||||
let migrations = rows
|
||||
.into_iter()
|
||||
.map(|(version, checksum)| AppliedMigration {
|
||||
version,
|
||||
checksum: checksum.into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(migrations)
|
||||
})
|
||||
}
|
||||
|
||||
fn lock(&mut self) -> BoxFuture<'_, Result<(), MigrateError>> {
|
||||
Box::pin(async move {
|
||||
let database_name = current_database(self).await?;
|
||||
let lock_id = generate_lock_id(&database_name);
|
||||
|
||||
// create an application lock over the database
|
||||
// this function will not return until the lock is acquired
|
||||
|
||||
// https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS
|
||||
// https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS-TABLE
|
||||
|
||||
// language=MySQL
|
||||
let _ = query("SELECT GET_LOCK(?, -1)")
|
||||
.bind(lock_id)
|
||||
.execute(self)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn unlock(&mut self) -> BoxFuture<'_, Result<(), MigrateError>> {
|
||||
Box::pin(async move {
|
||||
let database_name = current_database(self).await?;
|
||||
let lock_id = generate_lock_id(&database_name);
|
||||
|
||||
// language=MySQL
|
||||
let _ = query("SELECT RELEASE_LOCK(?)")
|
||||
.bind(lock_id)
|
||||
.execute(self)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn apply<'e: 'm, 'm>(
|
||||
&'e mut self,
|
||||
migration: &'m Migration,
|
||||
) -> BoxFuture<'m, Result<Duration, MigrateError>> {
|
||||
Box::pin(async move {
|
||||
// Use a single transaction for the actual migration script and the essential bookeeping so we never
|
||||
// execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966.
|
||||
// The `execution_time` however can only be measured for the whole transaction. This value _only_ exists for
|
||||
// data lineage and debugging reasons, so it is not super important if it is lost. So we initialize it to -1
|
||||
// and update it once the actual transaction completed.
|
||||
let mut tx = self.begin().await?;
|
||||
let start = Instant::now();
|
||||
|
||||
// For MySQL we cannot really isolate migrations due to implicit commits caused by table modification, see
|
||||
// https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html
|
||||
//
|
||||
// To somewhat try to detect this, we first insert the migration into the migration table with
|
||||
// `success=FALSE` and later modify the flag.
|
||||
//
|
||||
// language=MySQL
|
||||
let _ = query(
|
||||
r#"
|
||||
INSERT INTO _sqlx_migrations ( version, description, success, checksum, execution_time )
|
||||
VALUES ( ?, ?, FALSE, ?, -1 )
|
||||
"#,
|
||||
)
|
||||
.bind(migration.version)
|
||||
.bind(&*migration.description)
|
||||
.bind(&*migration.checksum)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let _ = tx
|
||||
.execute(&*migration.sql)
|
||||
.await
|
||||
.map_err(|e| MigrateError::ExecuteMigration(e, migration.version))?;
|
||||
|
||||
// language=MySQL
|
||||
let _ = query(
|
||||
r#"
|
||||
UPDATE _sqlx_migrations
|
||||
SET success = TRUE
|
||||
WHERE version = ?
|
||||
"#,
|
||||
)
|
||||
.bind(migration.version)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
// Update `elapsed_time`.
|
||||
// NOTE: The process may disconnect/die at this point, so the elapsed time value might be lost. We accept
|
||||
// this small risk since this value is not super important.
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let _ = query(
|
||||
r#"
|
||||
UPDATE _sqlx_migrations
|
||||
SET execution_time = ?
|
||||
WHERE version = ?
|
||||
"#,
|
||||
)
|
||||
.bind(elapsed.as_nanos() as i64)
|
||||
.bind(migration.version)
|
||||
.execute(self)
|
||||
.await?;
|
||||
|
||||
Ok(elapsed)
|
||||
})
|
||||
}
|
||||
|
||||
fn revert<'e: 'm, 'm>(
|
||||
&'e mut self,
|
||||
migration: &'m Migration,
|
||||
) -> BoxFuture<'m, Result<Duration, MigrateError>> {
|
||||
Box::pin(async move {
|
||||
// Use a single transaction for the actual migration script and the essential bookeeping so we never
|
||||
// execute migrations twice. See https://github.com/launchbadge/sqlx/issues/1966.
|
||||
let mut tx = self.begin().await?;
|
||||
let start = Instant::now();
|
||||
|
||||
// For MySQL we cannot really isolate migrations due to implicit commits caused by table modification, see
|
||||
// https://dev.mysql.com/doc/refman/8.0/en/implicit-commit.html
|
||||
//
|
||||
// To somewhat try to detect this, we first insert the migration into the migration table with
|
||||
// `success=FALSE` and later remove the migration altogether.
|
||||
//
|
||||
// language=MySQL
|
||||
let _ = query(
|
||||
r#"
|
||||
UPDATE _sqlx_migrations
|
||||
SET success = FALSE
|
||||
WHERE version = ?
|
||||
"#,
|
||||
)
|
||||
.bind(migration.version)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.execute(&*migration.sql).await?;
|
||||
|
||||
// language=SQL
|
||||
let _ = query(r#"DELETE FROM _sqlx_migrations WHERE version = ?"#)
|
||||
.bind(migration.version)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
Ok(elapsed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn current_database(conn: &mut MySqlConnection) -> Result<String, MigrateError> {
|
||||
// language=MySQL
|
||||
Ok(query_scalar("SELECT DATABASE()").fetch_one(conn).await?)
|
||||
}
|
||||
|
||||
// inspired from rails: https://github.com/rails/rails/blob/6e49cc77ab3d16c06e12f93158eaf3e507d4120e/activerecord/lib/active_record/migration.rb#L1308
|
||||
fn generate_lock_id(database_name: &str) -> String {
|
||||
const CRC_IEEE: crc::Crc<u32> = crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
|
||||
// 0x3d32ad9e chosen by fair dice roll
|
||||
format!(
|
||||
"{:x}",
|
||||
0x3d32ad9e * (CRC_IEEE.checksum(database_name.as_bytes()) as i64)
|
||||
)
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
use crate::connection::ConnectOptions;
|
||||
use crate::error::Error;
|
||||
use crate::executor::Executor;
|
||||
use crate::{MySqlConnectOptions, MySqlConnection};
|
||||
use futures_core::future::BoxFuture;
|
||||
use log::LevelFilter;
|
||||
use sqlx_core::Url;
|
||||
use std::time::Duration;
|
||||
|
||||
impl ConnectOptions for MySqlConnectOptions {
|
||||
type Connection = MySqlConnection;
|
||||
|
||||
fn from_url(url: &Url) -> Result<Self, Error> {
|
||||
Self::parse_from_url(url)
|
||||
}
|
||||
|
||||
fn to_url_lossy(&self) -> Url {
|
||||
self.build_url()
|
||||
}
|
||||
|
||||
fn connect(&self) -> BoxFuture<'_, Result<Self::Connection, Error>>
|
||||
where
|
||||
Self::Connection: Sized,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let mut conn = MySqlConnection::establish(self).await?;
|
||||
|
||||
// After the connection is established, we initialize by configuring a few
|
||||
// connection parameters
|
||||
|
||||
// https://mariadb.com/kb/en/sql-mode/
|
||||
|
||||
// PIPES_AS_CONCAT - Allows using the pipe character (ASCII 124) as string concatenation operator.
|
||||
// This means that "A" || "B" can be used in place of CONCAT("A", "B").
|
||||
|
||||
// NO_ENGINE_SUBSTITUTION - If not set, if the available storage engine specified by a CREATE TABLE is
|
||||
// not available, a warning is given and the default storage
|
||||
// engine is used instead.
|
||||
|
||||
// NO_ZERO_DATE - Don't allow '0000-00-00'. This is invalid in Rust.
|
||||
|
||||
// NO_ZERO_IN_DATE - Don't allow 'YYYY-00-00'. This is invalid in Rust.
|
||||
|
||||
// --
|
||||
|
||||
// Setting the time zone allows us to assume that the output
|
||||
// from a TIMESTAMP field is UTC
|
||||
|
||||
// --
|
||||
|
||||
// https://mathiasbynens.be/notes/mysql-utf8mb4
|
||||
|
||||
let mut sql_mode = Vec::new();
|
||||
if self.pipes_as_concat {
|
||||
sql_mode.push(r#"PIPES_AS_CONCAT"#);
|
||||
}
|
||||
if self.no_engine_substitution {
|
||||
sql_mode.push(r#"NO_ENGINE_SUBSTITUTION"#);
|
||||
}
|
||||
|
||||
let mut options = Vec::new();
|
||||
if !sql_mode.is_empty() {
|
||||
options.push(format!(
|
||||
r#"sql_mode=(SELECT CONCAT(@@sql_mode, ',{}'))"#,
|
||||
sql_mode.join(",")
|
||||
));
|
||||
}
|
||||
if let Some(timezone) = &self.timezone {
|
||||
options.push(format!(r#"time_zone='{}'"#, timezone));
|
||||
}
|
||||
if self.set_names {
|
||||
options.push(format!(
|
||||
r#"NAMES {} COLLATE {}"#,
|
||||
conn.inner.stream.charset.as_str(),
|
||||
conn.inner.stream.collation.as_str()
|
||||
))
|
||||
}
|
||||
|
||||
if !options.is_empty() {
|
||||
conn.execute(&*format!(r#"SET {};"#, options.join(",")))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
})
|
||||
}
|
||||
|
||||
fn log_statements(mut self, level: LevelFilter) -> Self {
|
||||
self.log_settings.log_statements(level);
|
||||
self
|
||||
}
|
||||
|
||||
fn log_slow_statements(mut self, level: LevelFilter, duration: Duration) -> Self {
|
||||
self.log_settings.log_slow_statements(level, duration);
|
||||
self
|
||||
}
|
||||
}
|
||||
Vendored
+518
@@ -0,0 +1,518 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
mod connect;
|
||||
mod parse;
|
||||
mod ssl_mode;
|
||||
|
||||
use crate::{connection::LogSettings, net::tls::CertificateInput};
|
||||
pub use ssl_mode::MySqlSslMode;
|
||||
|
||||
/// Options and flags which can be used to configure a MySQL connection.
|
||||
///
|
||||
/// A value of `MySqlConnectOptions` can be parsed from a connection URL,
|
||||
/// as described by [MySQL](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html).
|
||||
///
|
||||
/// The generic format of the connection URL:
|
||||
///
|
||||
/// ```text
|
||||
/// mysql://[host][/database][?properties]
|
||||
/// ```
|
||||
///
|
||||
/// 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).
|
||||
///
|
||||
/// ## Properties
|
||||
///
|
||||
/// |Parameter|Default|Description|
|
||||
/// |---------|-------|-----------|
|
||||
/// | `ssl-mode` | `PREFERRED` | Determines whether or with what priority a secure SSL TCP/IP connection will be negotiated. See [`MySqlSslMode`]. |
|
||||
/// | `ssl-ca` | `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. |
|
||||
/// | `socket` | `None` | Path to the unix domain socket, which will be used instead of TCP if set. |
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # async fn example() -> sqlx::Result<()> {
|
||||
/// use sqlx::{Connection, ConnectOptions};
|
||||
/// use sqlx::mysql::{MySqlConnectOptions, MySqlConnection, MySqlPool, MySqlSslMode};
|
||||
///
|
||||
/// // URL connection string
|
||||
/// let conn = MySqlConnection::connect("mysql://root:password@localhost/db").await?;
|
||||
///
|
||||
/// // Manually-constructed options
|
||||
/// let conn = MySqlConnectOptions::new()
|
||||
/// .host("localhost")
|
||||
/// .username("root")
|
||||
/// .password("password")
|
||||
/// .database("db")
|
||||
/// .connect().await?;
|
||||
///
|
||||
/// // Modifying options parsed from a string
|
||||
/// let mut opts: MySqlConnectOptions = "mysql://root:password@localhost/db".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 = MySqlPool::connect_with(opts).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MySqlConnectOptions {
|
||||
pub(crate) host: String,
|
||||
pub(crate) port: u16,
|
||||
pub(crate) socket: Option<PathBuf>,
|
||||
pub(crate) username: String,
|
||||
pub(crate) password: Option<String>,
|
||||
pub(crate) database: Option<String>,
|
||||
pub(crate) ssl_mode: MySqlSslMode,
|
||||
pub(crate) ssl_ca: Option<CertificateInput>,
|
||||
pub(crate) ssl_client_cert: Option<CertificateInput>,
|
||||
pub(crate) ssl_client_key: Option<CertificateInput>,
|
||||
pub(crate) statement_cache_capacity: usize,
|
||||
pub(crate) charset: String,
|
||||
pub(crate) collation: Option<String>,
|
||||
pub(crate) log_settings: LogSettings,
|
||||
pub(crate) pipes_as_concat: bool,
|
||||
pub(crate) enable_cleartext_plugin: bool,
|
||||
pub(crate) no_engine_substitution: bool,
|
||||
pub(crate) timezone: Option<String>,
|
||||
pub(crate) set_names: bool,
|
||||
}
|
||||
|
||||
impl Default for MySqlConnectOptions {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MySqlConnectOptions {
|
||||
/// Creates a new, default set of options ready for configuration
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
port: 3306,
|
||||
host: String::from("localhost"),
|
||||
socket: None,
|
||||
username: String::from("root"),
|
||||
password: None,
|
||||
database: None,
|
||||
charset: String::from("utf8mb4"),
|
||||
collation: None,
|
||||
ssl_mode: MySqlSslMode::Preferred,
|
||||
ssl_ca: None,
|
||||
ssl_client_cert: None,
|
||||
ssl_client_key: None,
|
||||
statement_cache_capacity: 100,
|
||||
log_settings: Default::default(),
|
||||
pipes_as_concat: true,
|
||||
enable_cleartext_plugin: false,
|
||||
no_engine_substitution: true,
|
||||
timezone: Some(String::from("+00:00")),
|
||||
set_names: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the name of the host to connect to.
|
||||
///
|
||||
/// The default behavior when the host is not specified,
|
||||
/// is to connect to localhost.
|
||||
pub fn host(mut self, host: &str) -> Self {
|
||||
host.clone_into(&mut self.host);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the port to connect to at the server host.
|
||||
///
|
||||
/// The default port for MySQL is `3306`.
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// Pass a path to a Unix socket. This changes the connection stream from
|
||||
/// TCP to UDS.
|
||||
///
|
||||
/// By default set to `None`.
|
||||
pub fn socket(mut self, path: impl AsRef<Path>) -> Self {
|
||||
self.socket = Some(path.as_ref().to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the username to connect as.
|
||||
pub fn username(mut self, username: &str) -> Self {
|
||||
username.clone_into(&mut self.username);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the password to connect with.
|
||||
pub fn password(mut self, password: &str) -> Self {
|
||||
self.password = Some(password.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the database name.
|
||||
pub fn database(mut self, database: &str) -> Self {
|
||||
self.database = Some(database.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether or with what priority a secure SSL TCP/IP connection will be negotiated
|
||||
/// with the server.
|
||||
///
|
||||
/// By default, the SSL mode is [`Preferred`](MySqlSslMode::Preferred), and the client will
|
||||
/// first attempt an SSL connection but fallback to a non-SSL connection on failure.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::{MySqlSslMode, MySqlConnectOptions};
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .ssl_mode(MySqlSslMode::Required);
|
||||
/// ```
|
||||
pub fn ssl_mode(mut self, mode: MySqlSslMode) -> Self {
|
||||
self.ssl_mode = mode;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the name of a file containing a list of trusted SSL Certificate Authorities.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::{MySqlSslMode, MySqlConnectOptions};
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .ssl_mode(MySqlSslMode::VerifyCa)
|
||||
/// .ssl_ca("path/to/ca.crt");
|
||||
/// ```
|
||||
pub fn ssl_ca(mut self, file_name: impl AsRef<Path>) -> Self {
|
||||
self.ssl_ca = Some(CertificateInput::File(file_name.as_ref().to_owned()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets PEM encoded list of trusted SSL Certificate Authorities.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::{MySqlSslMode, MySqlConnectOptions};
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .ssl_mode(MySqlSslMode::VerifyCa)
|
||||
/// .ssl_ca_from_pem(vec![]);
|
||||
/// ```
|
||||
pub fn ssl_ca_from_pem(mut self, pem_certificate: Vec<u8>) -> Self {
|
||||
self.ssl_ca = Some(CertificateInput::Inline(pem_certificate));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the name of a file containing SSL client certificate.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::{MySqlSslMode, MySqlConnectOptions};
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .ssl_mode(MySqlSslMode::VerifyCa)
|
||||
/// .ssl_client_cert("path/to/client.crt");
|
||||
/// ```
|
||||
pub fn ssl_client_cert(mut self, cert: impl AsRef<Path>) -> Self {
|
||||
self.ssl_client_cert = Some(CertificateInput::File(cert.as_ref().to_path_buf()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the SSL client certificate as a PEM-encoded byte slice.
|
||||
///
|
||||
/// This should be an ASCII-encoded blob that starts with `-----BEGIN CERTIFICATE-----`.
|
||||
///
|
||||
/// # Example
|
||||
/// Note: embedding SSL certificates and keys in the binary is not advised.
|
||||
/// This is for illustration purposes only.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::{MySqlSslMode, MySqlConnectOptions};
|
||||
///
|
||||
/// const CERT: &[u8] = b"\
|
||||
/// -----BEGIN CERTIFICATE-----
|
||||
/// <Certificate data here.>
|
||||
/// -----END CERTIFICATE-----";
|
||||
///
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .ssl_mode(MySqlSslMode::VerifyCa)
|
||||
/// .ssl_client_cert_from_pem(CERT);
|
||||
/// ```
|
||||
pub fn ssl_client_cert_from_pem(mut self, cert: impl AsRef<[u8]>) -> Self {
|
||||
self.ssl_client_cert = Some(CertificateInput::Inline(cert.as_ref().to_vec()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the name of a file containing SSL client key.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::{MySqlSslMode, MySqlConnectOptions};
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .ssl_mode(MySqlSslMode::VerifyCa)
|
||||
/// .ssl_client_key("path/to/client.key");
|
||||
/// ```
|
||||
pub fn ssl_client_key(mut self, key: impl AsRef<Path>) -> Self {
|
||||
self.ssl_client_key = Some(CertificateInput::File(key.as_ref().to_path_buf()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the SSL client key as a PEM-encoded byte slice.
|
||||
///
|
||||
/// This should be an ASCII-encoded blob that starts with `-----BEGIN PRIVATE KEY-----`.
|
||||
///
|
||||
/// # Example
|
||||
/// Note: embedding SSL certificates and keys in the binary is not advised.
|
||||
/// This is for illustration purposes only.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::{MySqlSslMode, MySqlConnectOptions};
|
||||
///
|
||||
/// const KEY: &[u8] = b"\
|
||||
/// -----BEGIN PRIVATE KEY-----
|
||||
/// <Private key data here.>
|
||||
/// -----END PRIVATE KEY-----";
|
||||
///
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .ssl_mode(MySqlSslMode::VerifyCa)
|
||||
/// .ssl_client_key_from_pem(KEY);
|
||||
/// ```
|
||||
pub fn ssl_client_key_from_pem(mut self, key: impl AsRef<[u8]>) -> Self {
|
||||
self.ssl_client_key = Some(CertificateInput::Inline(key.as_ref().to_vec()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the capacity of the connection's statement cache in a number of stored
|
||||
/// distinct statements. Caching is handled using LRU, meaning when the
|
||||
/// amount of queries hits the defined limit, the oldest statement will get
|
||||
/// dropped.
|
||||
///
|
||||
/// The default cache capacity is 100 statements.
|
||||
pub fn statement_cache_capacity(mut self, capacity: usize) -> Self {
|
||||
self.statement_cache_capacity = capacity;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the character set for the connection.
|
||||
///
|
||||
/// The default character set is `utf8mb4`. This is supported from MySQL 5.5.3.
|
||||
/// If you need to connect to an older version, we recommend you to change this to `utf8`.
|
||||
pub fn charset(mut self, charset: &str) -> Self {
|
||||
charset.clone_into(&mut self.charset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the collation for the connection.
|
||||
///
|
||||
/// The default collation is derived from the `charset`. Normally, you should only have to set
|
||||
/// the `charset`.
|
||||
pub fn collation(mut self, collation: &str) -> Self {
|
||||
self.collation = Some(collation.to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the flag that enables or disables the `PIPES_AS_CONCAT` connection setting
|
||||
///
|
||||
/// The default value is set to true, but some MySql databases such as PlanetScale
|
||||
/// error out with this connection setting so it needs to be set false in such
|
||||
/// cases.
|
||||
pub fn pipes_as_concat(mut self, flag_val: bool) -> Self {
|
||||
self.pipes_as_concat = flag_val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables mysql_clear_password plugin support.
|
||||
///
|
||||
/// Security Note:
|
||||
/// Sending passwords as cleartext may be a security problem in some
|
||||
/// configurations. Without additional defensive configuration like
|
||||
/// ssl-mode=VERIFY_IDENTITY, an attacker can compromise a router
|
||||
/// and trick the application into divulging its credentials.
|
||||
///
|
||||
/// It is strongly recommended to set `.ssl_mode` to `Required`,
|
||||
/// `VerifyCa`, or `VerifyIdentity` when enabling cleartext plugin.
|
||||
pub fn enable_cleartext_plugin(mut self, flag_val: bool) -> Self {
|
||||
self.enable_cleartext_plugin = flag_val;
|
||||
self
|
||||
}
|
||||
|
||||
#[deprecated = "renamed to .no_engine_substitution()"]
|
||||
pub fn no_engine_subsitution(self, flag_val: bool) -> Self {
|
||||
self.no_engine_substitution(flag_val)
|
||||
}
|
||||
|
||||
/// Flag that enables or disables the `NO_ENGINE_SUBSTITUTION` sql_mode setting after
|
||||
/// connection.
|
||||
///
|
||||
/// If not set, if the available storage engine specified by a `CREATE TABLE` is not available,
|
||||
/// a warning is given and the default storage engine is used instead.
|
||||
///
|
||||
/// By default, this is `true` (`NO_ENGINE_SUBSTITUTION` is passed, forbidding engine
|
||||
/// substitution).
|
||||
///
|
||||
/// <https://mariadb.com/kb/en/sql-mode/>
|
||||
pub fn no_engine_substitution(mut self, flag_val: bool) -> Self {
|
||||
self.no_engine_substitution = flag_val;
|
||||
self
|
||||
}
|
||||
|
||||
/// If `Some`, sets the `time_zone` option to the given string after connecting to the database.
|
||||
///
|
||||
/// If `None`, no `time_zone` parameter is sent; the server timezone will be used instead.
|
||||
///
|
||||
/// Defaults to `Some(String::from("+00:00"))` to ensure all timestamps are in UTC.
|
||||
///
|
||||
/// ### Warning
|
||||
/// Changing this setting from its default will apply an unexpected skew to any
|
||||
/// `time::OffsetDateTime` or `chrono::DateTime<Utc>` value, whether passed as a parameter or
|
||||
/// decoded as a result. `TIMESTAMP` values are not encoded with their UTC offset in the MySQL
|
||||
/// protocol, so encoding and decoding of these types assumes the server timezone is *always*
|
||||
/// UTC.
|
||||
///
|
||||
/// If you are changing this option, ensure your application only uses
|
||||
/// `time::PrimitiveDateTime` or `chrono::NaiveDateTime` and that it does not assume these
|
||||
/// timestamps can be placed on a real timeline without applying the proper offset.
|
||||
pub fn timezone(mut self, value: impl Into<Option<String>>) -> Self {
|
||||
self.timezone = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// If enabled, `SET NAMES '{charset}' COLLATE '{collation}'` is passed with the values of
|
||||
/// [`.charset()`] and [`.collation()`] after connecting to the database.
|
||||
///
|
||||
/// This ensures the connection uses the specified character set and collation.
|
||||
///
|
||||
/// Enabled by default.
|
||||
///
|
||||
/// ### Warning
|
||||
/// If this is disabled and the default charset is not binary-compatible with UTF-8, query
|
||||
/// strings, column names and string values will likely not decode (or encode) correctly, which
|
||||
/// may result in unexpected errors or garbage outputs at runtime.
|
||||
///
|
||||
/// For proper functioning, you *must* ensure the server is using a binary-compatible charset,
|
||||
/// such as ASCII or Latin-1 (ISO 8859-1), and that you do not pass any strings containing
|
||||
/// codepoints not supported by said charset.
|
||||
///
|
||||
/// Instead of disabling this, you may also consider setting [`.charset()`] to a charset that
|
||||
/// is supported by your MySQL or MariaDB server version and compatible with UTF-8.
|
||||
pub fn set_names(mut self, flag_val: bool) -> Self {
|
||||
self.set_names = flag_val;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl MySqlConnectOptions {
|
||||
/// Get the current host.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::MySqlConnectOptions;
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .host("127.0.0.1");
|
||||
/// assert_eq!(options.get_host(), "127.0.0.1");
|
||||
/// ```
|
||||
pub fn get_host(&self) -> &str {
|
||||
&self.host
|
||||
}
|
||||
|
||||
/// Get the server's port.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::MySqlConnectOptions;
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .port(6543);
|
||||
/// assert_eq!(options.get_port(), 6543);
|
||||
/// ```
|
||||
pub fn get_port(&self) -> u16 {
|
||||
self.port
|
||||
}
|
||||
|
||||
/// Get the socket path.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::MySqlConnectOptions;
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .socket("/tmp");
|
||||
/// assert!(options.get_socket().is_some());
|
||||
/// ```
|
||||
pub fn get_socket(&self) -> Option<&PathBuf> {
|
||||
self.socket.as_ref()
|
||||
}
|
||||
|
||||
/// Get the current username.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::MySqlConnectOptions;
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .username("foo");
|
||||
/// assert_eq!(options.get_username(), "foo");
|
||||
/// ```
|
||||
pub fn get_username(&self) -> &str {
|
||||
&self.username
|
||||
}
|
||||
|
||||
/// Get the current database name.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::MySqlConnectOptions;
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .database("postgres");
|
||||
/// assert!(options.get_database().is_some());
|
||||
/// ```
|
||||
pub fn get_database(&self) -> Option<&str> {
|
||||
self.database.as_deref()
|
||||
}
|
||||
|
||||
/// Get the SSL mode.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::{MySqlConnectOptions, MySqlSslMode};
|
||||
/// let options = MySqlConnectOptions::new();
|
||||
/// assert!(matches!(options.get_ssl_mode(), MySqlSslMode::Preferred));
|
||||
/// ```
|
||||
pub fn get_ssl_mode(&self) -> MySqlSslMode {
|
||||
self.ssl_mode
|
||||
}
|
||||
|
||||
/// Get the server charset.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::MySqlConnectOptions;
|
||||
/// let options = MySqlConnectOptions::new();
|
||||
/// assert_eq!(options.get_charset(), "utf8mb4");
|
||||
/// ```
|
||||
pub fn get_charset(&self) -> &str {
|
||||
&self.charset
|
||||
}
|
||||
|
||||
/// Get the server collation.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use sqlx_mysql::MySqlConnectOptions;
|
||||
/// let options = MySqlConnectOptions::new()
|
||||
/// .collation("collation");
|
||||
/// assert!(options.get_collation().is_some());
|
||||
/// ```
|
||||
pub fn get_collation(&self) -> Option<&str> {
|
||||
self.collation.as_deref()
|
||||
}
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use sqlx_core::Url;
|
||||
|
||||
use crate::{error::Error, MySqlSslMode};
|
||||
|
||||
use super::MySqlConnectOptions;
|
||||
|
||||
impl MySqlConnectOptions {
|
||||
pub(crate) fn parse_from_url(url: &Url) -> Result<Self, Error> {
|
||||
let mut options = Self::new();
|
||||
|
||||
if let Some(host) = url.host_str() {
|
||||
options = options.host(host);
|
||||
}
|
||||
|
||||
if let Some(port) = url.port() {
|
||||
options = options.port(port);
|
||||
}
|
||||
|
||||
let username = url.username();
|
||||
if !username.is_empty() {
|
||||
options = options.username(
|
||||
&percent_decode_str(username)
|
||||
.decode_utf8()
|
||||
.map_err(Error::config)?,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(password) = url.password() {
|
||||
options = options.password(
|
||||
&percent_decode_str(password)
|
||||
.decode_utf8()
|
||||
.map_err(Error::config)?,
|
||||
);
|
||||
}
|
||||
|
||||
let path = url.path().trim_start_matches('/');
|
||||
if !path.is_empty() {
|
||||
options = options.database(
|
||||
&percent_decode_str(path)
|
||||
.decode_utf8()
|
||||
.map_err(Error::config)?,
|
||||
);
|
||||
}
|
||||
|
||||
for (key, value) in url.query_pairs().into_iter() {
|
||||
match &*key {
|
||||
"sslmode" | "ssl-mode" => {
|
||||
options = options.ssl_mode(value.parse().map_err(Error::config)?);
|
||||
}
|
||||
|
||||
"sslca" | "ssl-ca" => {
|
||||
options = options.ssl_ca(&*value);
|
||||
}
|
||||
|
||||
"charset" => {
|
||||
options = options.charset(&value);
|
||||
}
|
||||
|
||||
"collation" => {
|
||||
options = options.collation(&value);
|
||||
}
|
||||
|
||||
"sslcert" | "ssl-cert" => options = options.ssl_client_cert(&*value),
|
||||
|
||||
"sslkey" | "ssl-key" => options = options.ssl_client_key(&*value),
|
||||
|
||||
"statement-cache-capacity" => {
|
||||
options =
|
||||
options.statement_cache_capacity(value.parse().map_err(Error::config)?);
|
||||
}
|
||||
|
||||
"socket" => {
|
||||
options = options.socket(&*value);
|
||||
}
|
||||
|
||||
"timezone" | "time-zone" => {
|
||||
options = options.timezone(Some(value.to_string()));
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(options)
|
||||
}
|
||||
|
||||
pub(crate) fn build_url(&self) -> Url {
|
||||
let mut url = Url::parse(&format!(
|
||||
"mysql://{}@{}:{}",
|
||||
self.username, self.host, self.port
|
||||
))
|
||||
.expect("BUG: generated un-parseable URL");
|
||||
|
||||
if let Some(password) = &self.password {
|
||||
let password = utf8_percent_encode(password, NON_ALPHANUMERIC).to_string();
|
||||
let _ = url.set_password(Some(&password));
|
||||
}
|
||||
|
||||
if let Some(database) = &self.database {
|
||||
url.set_path(database);
|
||||
}
|
||||
|
||||
let ssl_mode = match self.ssl_mode {
|
||||
MySqlSslMode::Disabled => "DISABLED",
|
||||
MySqlSslMode::Preferred => "PREFERRED",
|
||||
MySqlSslMode::Required => "REQUIRED",
|
||||
MySqlSslMode::VerifyCa => "VERIFY_CA",
|
||||
MySqlSslMode::VerifyIdentity => "VERIFY_IDENTITY",
|
||||
};
|
||||
url.query_pairs_mut().append_pair("ssl-mode", ssl_mode);
|
||||
|
||||
if let Some(ssl_ca) = &self.ssl_ca {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("ssl-ca", &ssl_ca.to_string());
|
||||
}
|
||||
|
||||
url.query_pairs_mut().append_pair("charset", &self.charset);
|
||||
|
||||
if let Some(collation) = &self.collation {
|
||||
url.query_pairs_mut().append_pair("charset", collation);
|
||||
}
|
||||
|
||||
if let Some(ssl_client_cert) = &self.ssl_client_cert {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("ssl-cert", &ssl_client_cert.to_string());
|
||||
}
|
||||
|
||||
if let Some(ssl_client_key) = &self.ssl_client_key {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("ssl-key", &ssl_client_key.to_string());
|
||||
}
|
||||
|
||||
url.query_pairs_mut().append_pair(
|
||||
"statement-cache-capacity",
|
||||
&self.statement_cache_capacity.to_string(),
|
||||
);
|
||||
|
||||
if let Some(socket) = &self.socket {
|
||||
url.query_pairs_mut()
|
||||
.append_pair("socket", &socket.to_string_lossy());
|
||||
}
|
||||
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MySqlConnectOptions {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Error> {
|
||||
let url: Url = s.parse().map_err(Error::config)?;
|
||||
Self::parse_from_url(&url)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_username_with_at_sign_correctly() {
|
||||
let url = "mysql://user@hostname:password@hostname:5432/database";
|
||||
let opts = MySqlConnectOptions::from_str(url).unwrap();
|
||||
|
||||
assert_eq!("user@hostname", &opts.username);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_password_with_non_ascii_chars_correctly() {
|
||||
let url = "mysql://username:p@ssw0rd@hostname:5432/database";
|
||||
let opts = MySqlConnectOptions::from_str(url).unwrap();
|
||||
|
||||
assert_eq!(Some("p@ssw0rd".into()), opts.password);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_returns_the_parsed_url() {
|
||||
let url = "mysql://username:p@ssw0rd@hostname:3306/database";
|
||||
let opts = MySqlConnectOptions::from_str(url).unwrap();
|
||||
|
||||
let mut expected_url = Url::parse(url).unwrap();
|
||||
// MySqlConnectOptions defaults
|
||||
let query_string = "ssl-mode=PREFERRED&charset=utf8mb4&statement-cache-capacity=100";
|
||||
expected_url.set_query(Some(query_string));
|
||||
|
||||
assert_eq!(expected_url, opts.build_url());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_timezone() {
|
||||
let opts: MySqlConnectOptions = "mysql://user:password@hostname/database?timezone=%2B08:00"
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert_eq!(opts.timezone.as_deref(), Some("+08:00"));
|
||||
|
||||
let opts: MySqlConnectOptions = "mysql://user:password@hostname/database?time-zone=%2B08:00"
|
||||
.parse()
|
||||
.unwrap();
|
||||
assert_eq!(opts.timezone.as_deref(), Some("+08:00"));
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
use crate::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Options for controlling the desired security state of the connection to the MySQL server.
|
||||
///
|
||||
/// It is used by the [`ssl_mode`](super::MySqlConnectOptions::ssl_mode) method.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub enum MySqlSslMode {
|
||||
/// Establish an unencrypted connection.
|
||||
Disabled,
|
||||
|
||||
/// Establish an encrypted connection if the server supports encrypted connections, falling
|
||||
/// back to an unencrypted connection if an encrypted connection cannot be established.
|
||||
///
|
||||
/// This is the default if `ssl_mode` is not specified.
|
||||
#[default]
|
||||
Preferred,
|
||||
|
||||
/// Establish an encrypted connection if the server supports encrypted connections.
|
||||
/// The connection attempt fails if an encrypted connection cannot be established.
|
||||
Required,
|
||||
|
||||
/// Like `Required`, but additionally verify the server Certificate Authority (CA)
|
||||
/// certificate against the configured CA certificates. The connection attempt fails
|
||||
/// if no valid matching CA certificates are found.
|
||||
VerifyCa,
|
||||
|
||||
/// Like `VerifyCa`, but additionally perform host name identity verification by
|
||||
/// checking the host name the client uses for connecting to the server against the
|
||||
/// identity in the certificate that the server sends to the client.
|
||||
VerifyIdentity,
|
||||
}
|
||||
|
||||
impl FromStr for MySqlSslMode {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Error> {
|
||||
Ok(match &*s.to_ascii_lowercase() {
|
||||
"disabled" => MySqlSslMode::Disabled,
|
||||
"preferred" => MySqlSslMode::Preferred,
|
||||
"required" => MySqlSslMode::Required,
|
||||
"verify_ca" => MySqlSslMode::VerifyCa,
|
||||
"verify_identity" => MySqlSslMode::VerifyIdentity,
|
||||
|
||||
_ => {
|
||||
return Err(Error::Configuration(
|
||||
format!("unknown value {s:?} for `ssl_mode`").into(),
|
||||
));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
// These have all the same suffix but they match the auth plugin names.
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum AuthPlugin {
|
||||
MySqlNativePassword,
|
||||
CachingSha2Password,
|
||||
Sha256Password,
|
||||
MySqlClearPassword,
|
||||
}
|
||||
|
||||
impl AuthPlugin {
|
||||
pub(crate) fn name(self) -> &'static str {
|
||||
match self {
|
||||
AuthPlugin::MySqlNativePassword => "mysql_native_password",
|
||||
AuthPlugin::CachingSha2Password => "caching_sha2_password",
|
||||
AuthPlugin::Sha256Password => "sha256_password",
|
||||
AuthPlugin::MySqlClearPassword => "mysql_clear_password",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AuthPlugin {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"mysql_native_password" => Ok(AuthPlugin::MySqlNativePassword),
|
||||
"caching_sha2_password" => Ok(AuthPlugin::CachingSha2Password),
|
||||
"sha256_password" => Ok(AuthPlugin::Sha256Password),
|
||||
"mysql_clear_password" => Ok(AuthPlugin::MySqlClearPassword),
|
||||
|
||||
_ => Err(err_protocol!("unknown authentication plugin: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// https://dev.mysql.com/doc/dev/mysql-server/8.0.12/group__group__cs__capabilities__flags.html
|
||||
// https://mariadb.com/kb/en/library/connection/#capabilities
|
||||
//
|
||||
// MySQL defines the capabilities flags as fitting in an `int<4>` but MariaDB
|
||||
// extends this with more bits sent in a separate field.
|
||||
// For simplicity, we've chosen to combine these into one type.
|
||||
bitflags::bitflags! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Capabilities: u64 {
|
||||
// [MariaDB] MySQL compatibility
|
||||
const MYSQL = 1;
|
||||
|
||||
// [*] Send found rows instead of affected rows in EOF_Packet.
|
||||
const FOUND_ROWS = 2;
|
||||
|
||||
// Get all column flags.
|
||||
const LONG_FLAG = 4;
|
||||
|
||||
// [*] Database (schema) name can be specified on connect in Handshake Response Packet.
|
||||
const CONNECT_WITH_DB = 8;
|
||||
|
||||
// Don't allow database.table.column
|
||||
const NO_SCHEMA = 16;
|
||||
|
||||
// [*] Compression protocol supported
|
||||
const COMPRESS = 32;
|
||||
|
||||
// Special handling of ODBC behavior.
|
||||
const ODBC = 64;
|
||||
|
||||
// Can use LOAD DATA LOCAL
|
||||
const LOCAL_FILES = 128;
|
||||
|
||||
// [*] Ignore spaces before '('
|
||||
const IGNORE_SPACE = 256;
|
||||
|
||||
// [*] New 4.1+ protocol
|
||||
const PROTOCOL_41 = 512;
|
||||
|
||||
// This is an interactive client
|
||||
const INTERACTIVE = 1024;
|
||||
|
||||
// Use SSL encryption for this session
|
||||
const SSL = 2048;
|
||||
|
||||
// Client knows about transactions
|
||||
const TRANSACTIONS = 8192;
|
||||
|
||||
// 4.1+ authentication
|
||||
const SECURE_CONNECTION = 1 << 15;
|
||||
|
||||
// Enable/disable multi-statement support for COM_QUERY *and* COM_STMT_PREPARE
|
||||
const MULTI_STATEMENTS = 1 << 16;
|
||||
|
||||
// Enable/disable multi-results for COM_QUERY
|
||||
const MULTI_RESULTS = 1 << 17;
|
||||
|
||||
// Enable/disable multi-results for COM_STMT_PREPARE
|
||||
const PS_MULTI_RESULTS = 1 << 18;
|
||||
|
||||
// Client supports plugin authentication
|
||||
const PLUGIN_AUTH = 1 << 19;
|
||||
|
||||
// Client supports connection attributes
|
||||
const CONNECT_ATTRS = 1 << 20;
|
||||
|
||||
// Enable authentication response packet to be larger than 255 bytes.
|
||||
const PLUGIN_AUTH_LENENC_DATA = 1 << 21;
|
||||
|
||||
// Don't close the connection for a user account with expired password.
|
||||
const CAN_HANDLE_EXPIRED_PASSWORDS = 1 << 22;
|
||||
|
||||
// Capable of handling server state change information.
|
||||
const SESSION_TRACK = 1 << 23;
|
||||
|
||||
// Client no longer needs EOF_Packet and will use OK_Packet instead.
|
||||
const DEPRECATE_EOF = 1 << 24;
|
||||
|
||||
// Support ZSTD protocol compression
|
||||
const ZSTD_COMPRESSION_ALGORITHM = 1 << 26;
|
||||
|
||||
// Verify server certificate
|
||||
const SSL_VERIFY_SERVER_CERT = 1 << 30;
|
||||
|
||||
// The client can handle optional metadata information in the resultset
|
||||
const OPTIONAL_RESULTSET_METADATA = 1 << 25;
|
||||
|
||||
// Don't reset the options after an unsuccessful connect
|
||||
const REMEMBER_OPTIONS = 1 << 31;
|
||||
|
||||
// Extended capabilities (MariaDB only, as of writing)
|
||||
// Client support progress indicator (since 10.2)
|
||||
const MARIADB_CLIENT_PROGRESS = 1 << 32;
|
||||
|
||||
// Permit COM_MULTI protocol
|
||||
const MARIADB_CLIENT_MULTI = 1 << 33;
|
||||
|
||||
// Permit bulk insert
|
||||
const MARIADB_CLIENT_STMT_BULK_OPERATIONS = 1 << 34;
|
||||
|
||||
// Add extended metadata information
|
||||
const MARIADB_CLIENT_EXTENDED_TYPE_INFO = 1 << 35;
|
||||
|
||||
// Permit skipping metadata
|
||||
const MARIADB_CLIENT_CACHE_METADATA = 1 << 36;
|
||||
|
||||
// when enabled, indicate that Bulk command can use STMT_BULK_FLAG_SEND_UNIT_RESULTS flag
|
||||
// that permit to return a result-set of all affected rows and auto-increment values
|
||||
const MARIADB_CLIENT_BULK_UNIT_RESULTS = 1 << 37;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::io::ProtocolEncode;
|
||||
use crate::io::{BufExt, ProtocolDecode};
|
||||
use crate::protocol::auth::AuthPlugin;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/dev/mysql-server/8.0.12/page_protocol_connection_phase_packets_protocol_auth_switch_request.html
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthSwitchRequest {
|
||||
pub plugin: AuthPlugin,
|
||||
pub data: Bytes,
|
||||
}
|
||||
|
||||
impl ProtocolDecode<'_, bool> for AuthSwitchRequest {
|
||||
fn decode_with(mut buf: Bytes, enable_cleartext_plugin: bool) -> Result<Self, Error> {
|
||||
let header = buf.get_u8();
|
||||
if header != 0xfe {
|
||||
return Err(err_protocol!(
|
||||
"expected 0xfe (AUTH_SWITCH) but found 0x{:x}",
|
||||
header
|
||||
));
|
||||
}
|
||||
|
||||
let plugin = buf.get_str_nul()?.parse()?;
|
||||
|
||||
if matches!(plugin, AuthPlugin::MySqlClearPassword) && !enable_cleartext_plugin {
|
||||
return Err(err_protocol!("mysql_cleartext_plugin disabled"));
|
||||
}
|
||||
|
||||
if matches!(plugin, AuthPlugin::MySqlClearPassword) && buf.is_empty() {
|
||||
// Contrary to the MySQL protocol, AWS Aurora with IAM sends
|
||||
// no data. That is fine because the mysql_clear_password says to
|
||||
// ignore any data sent.
|
||||
// See: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase_authentication_methods_clear_text_password.html
|
||||
return Ok(Self {
|
||||
plugin,
|
||||
data: Bytes::new(),
|
||||
});
|
||||
}
|
||||
|
||||
// See: https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/sql/auth/sha2_password.cc#L942
|
||||
if buf.len() != 21 {
|
||||
return Err(err_protocol!(
|
||||
"expected 21 bytes but found {} bytes",
|
||||
buf.len()
|
||||
));
|
||||
}
|
||||
let data = buf.get_bytes(20);
|
||||
buf.advance(1); // NUL-terminator
|
||||
|
||||
Ok(Self { plugin, data })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthSwitchResponse(pub Vec<u8>);
|
||||
|
||||
impl ProtocolEncode<'_, Capabilities> for AuthSwitchResponse {
|
||||
fn encode_with(&self, buf: &mut Vec<u8>, _: Capabilities) -> Result<(), Error> {
|
||||
buf.extend_from_slice(&self.0);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_auth_switch_packet_data() {
|
||||
const AUTH_SWITCH_NO_DATA: &[u8] = b"\xfecaching_sha2_password\x00abcdefghijabcdefghij\x00";
|
||||
|
||||
let p = AuthSwitchRequest::decode_with(AUTH_SWITCH_NO_DATA.into(), true).unwrap();
|
||||
|
||||
assert!(matches!(p.plugin, AuthPlugin::CachingSha2Password));
|
||||
assert_eq!(p.data, &b"abcdefghijabcdefghij"[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_auth_switch_cleartext_disabled() {
|
||||
const AUTH_SWITCH_CLEARTEXT: &[u8] = b"\xfemysql_clear_password\x00abcdefghijabcdefghij\x00";
|
||||
|
||||
let e = AuthSwitchRequest::decode_with(AUTH_SWITCH_CLEARTEXT.into(), false).unwrap_err();
|
||||
|
||||
let e_str = e.to_string();
|
||||
|
||||
let expected = "encountered unexpected or invalid data: mysql_cleartext_plugin disabled";
|
||||
|
||||
assert!(
|
||||
// Don't want to assert the full string since it contains the module path now.
|
||||
e_str.starts_with(expected),
|
||||
"expected error string to start with {expected:?}, got {e_str:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_auth_switch_packet_no_data() {
|
||||
const AUTH_SWITCH_NO_DATA: &[u8] = b"\xfemysql_clear_password\x00";
|
||||
|
||||
let p = AuthSwitchRequest::decode_with(AUTH_SWITCH_NO_DATA.into(), true).unwrap();
|
||||
|
||||
assert!(matches!(p.plugin, AuthPlugin::MySqlClearPassword));
|
||||
assert_eq!(p.data, Bytes::new());
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
use bytes::buf::Chain;
|
||||
use bytes::{Buf, Bytes};
|
||||
use std::cmp;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::io::{BufExt, ProtocolDecode};
|
||||
use crate::protocol::auth::AuthPlugin;
|
||||
use crate::protocol::response::Status;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake
|
||||
// https://mariadb.com/kb/en/connection/#initial-handshake-packet
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Handshake {
|
||||
#[allow(unused)]
|
||||
pub(crate) protocol_version: u8,
|
||||
pub(crate) server_version: String,
|
||||
#[allow(unused)]
|
||||
pub(crate) connection_id: u32,
|
||||
pub(crate) server_capabilities: Capabilities,
|
||||
#[allow(unused)]
|
||||
pub(crate) server_default_collation: u8,
|
||||
#[allow(unused)]
|
||||
pub(crate) status: Status,
|
||||
pub(crate) auth_plugin: Option<AuthPlugin>,
|
||||
pub(crate) auth_plugin_data: Chain<Bytes, Bytes>,
|
||||
}
|
||||
|
||||
impl ProtocolDecode<'_> for Handshake {
|
||||
fn decode_with(mut buf: Bytes, _: ()) -> Result<Self, Error> {
|
||||
let protocol_version = buf.get_u8(); // int<1>
|
||||
let server_version = buf.get_str_nul()?; // string<NUL>
|
||||
let connection_id = buf.get_u32_le(); // int<4>
|
||||
let auth_plugin_data_1 = buf.get_bytes(8); // string<8>
|
||||
|
||||
buf.advance(1); // reserved: string<1>
|
||||
|
||||
let capabilities_1 = buf.get_u16_le(); // int<2>
|
||||
let mut capabilities = Capabilities::from_bits_truncate(capabilities_1.into());
|
||||
|
||||
let collation = buf.get_u8(); // int<1>
|
||||
let status = Status::from_bits_truncate(buf.get_u16_le());
|
||||
|
||||
let capabilities_2 = buf.get_u16_le(); // int<2>
|
||||
capabilities |= Capabilities::from_bits_truncate(((capabilities_2 as u32) << 16).into());
|
||||
|
||||
let auth_plugin_data_len = if capabilities.contains(Capabilities::PLUGIN_AUTH) {
|
||||
buf.get_u8()
|
||||
} else {
|
||||
buf.advance(1); // int<1>
|
||||
0
|
||||
};
|
||||
|
||||
buf.advance(6); // reserved: string<6>
|
||||
|
||||
if capabilities.contains(Capabilities::MYSQL) {
|
||||
buf.advance(4); // reserved: string<4>
|
||||
} else {
|
||||
let capabilities_3 = buf.get_u32_le(); // int<4>
|
||||
capabilities |= Capabilities::from_bits_truncate((capabilities_3 as u64) << 32);
|
||||
}
|
||||
|
||||
let auth_plugin_data_2 = if capabilities.contains(Capabilities::SECURE_CONNECTION) {
|
||||
let len = cmp::max(auth_plugin_data_len.saturating_sub(9), 12);
|
||||
let v = buf.get_bytes(len as usize);
|
||||
buf.advance(1); // NUL-terminator
|
||||
|
||||
v
|
||||
} else {
|
||||
Bytes::new()
|
||||
};
|
||||
|
||||
let auth_plugin = if capabilities.contains(Capabilities::PLUGIN_AUTH) {
|
||||
Some(buf.get_str_nul()?.parse()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
protocol_version,
|
||||
server_version,
|
||||
connection_id,
|
||||
server_default_collation: collation,
|
||||
status,
|
||||
server_capabilities: capabilities,
|
||||
auth_plugin,
|
||||
auth_plugin_data: auth_plugin_data_1.chain(auth_plugin_data_2),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_handshake_mysql_8_0_18() {
|
||||
const HANDSHAKE_MYSQL_8_0_18: &[u8] = b"\n8.0.18\x00\x19\x00\x00\x00\x114aB0c\x06g\x00\xff\xff\xff\x02\x00\xff\xc7\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00tL\x03s\x0f[4\rl4. \x00caching_sha2_password\x00";
|
||||
|
||||
let p = Handshake::decode(HANDSHAKE_MYSQL_8_0_18.into()).unwrap();
|
||||
|
||||
assert_eq!(p.protocol_version, 10);
|
||||
|
||||
assert_eq!(
|
||||
p.server_capabilities,
|
||||
Capabilities::MYSQL
|
||||
| Capabilities::FOUND_ROWS
|
||||
| Capabilities::LONG_FLAG
|
||||
| Capabilities::CONNECT_WITH_DB
|
||||
| Capabilities::NO_SCHEMA
|
||||
| Capabilities::COMPRESS
|
||||
| Capabilities::ODBC
|
||||
| Capabilities::LOCAL_FILES
|
||||
| Capabilities::IGNORE_SPACE
|
||||
| Capabilities::PROTOCOL_41
|
||||
| Capabilities::INTERACTIVE
|
||||
| Capabilities::SSL
|
||||
| Capabilities::TRANSACTIONS
|
||||
| Capabilities::SECURE_CONNECTION
|
||||
| Capabilities::MULTI_STATEMENTS
|
||||
| Capabilities::MULTI_RESULTS
|
||||
| Capabilities::PS_MULTI_RESULTS
|
||||
| Capabilities::PLUGIN_AUTH
|
||||
| Capabilities::CONNECT_ATTRS
|
||||
| Capabilities::PLUGIN_AUTH_LENENC_DATA
|
||||
| Capabilities::CAN_HANDLE_EXPIRED_PASSWORDS
|
||||
| Capabilities::SESSION_TRACK
|
||||
| Capabilities::DEPRECATE_EOF
|
||||
| Capabilities::ZSTD_COMPRESSION_ALGORITHM
|
||||
| Capabilities::SSL_VERIFY_SERVER_CERT
|
||||
| Capabilities::OPTIONAL_RESULTSET_METADATA
|
||||
| Capabilities::REMEMBER_OPTIONS,
|
||||
);
|
||||
|
||||
assert_eq!(p.server_default_collation, 255);
|
||||
assert!(p.status.contains(Status::SERVER_STATUS_AUTOCOMMIT));
|
||||
|
||||
assert!(matches!(
|
||||
p.auth_plugin,
|
||||
Some(AuthPlugin::CachingSha2Password)
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
&*p.auth_plugin_data.into_iter().collect::<Vec<_>>(),
|
||||
&[17, 52, 97, 66, 48, 99, 6, 103, 116, 76, 3, 115, 15, 91, 52, 13, 108, 52, 46, 32,]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_handshake_mariadb_10_4_7() {
|
||||
const HANDSHAKE_MARIA_DB_10_4_7: &[u8] = b"\n5.5.5-10.4.7-MariaDB-1:10.4.7+maria~bionic\x00\x0b\x00\x00\x00t6L\\j\"dS\x00\xfe\xf7\x08\x02\x00\xff\x81\x15\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00U14Oph9\"<H5n\x00mysql_native_password\x00";
|
||||
|
||||
let p = Handshake::decode(HANDSHAKE_MARIA_DB_10_4_7.into()).unwrap();
|
||||
|
||||
assert_eq!(p.protocol_version, 10);
|
||||
|
||||
assert_eq!(
|
||||
&*p.server_version,
|
||||
"5.5.5-10.4.7-MariaDB-1:10.4.7+maria~bionic"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
p.server_capabilities,
|
||||
Capabilities::FOUND_ROWS
|
||||
| Capabilities::LONG_FLAG
|
||||
| Capabilities::CONNECT_WITH_DB
|
||||
| Capabilities::NO_SCHEMA
|
||||
| Capabilities::COMPRESS
|
||||
| Capabilities::ODBC
|
||||
| Capabilities::LOCAL_FILES
|
||||
| Capabilities::IGNORE_SPACE
|
||||
| Capabilities::PROTOCOL_41
|
||||
| Capabilities::INTERACTIVE
|
||||
| Capabilities::TRANSACTIONS
|
||||
| Capabilities::SECURE_CONNECTION
|
||||
| Capabilities::MULTI_STATEMENTS
|
||||
| Capabilities::MULTI_RESULTS
|
||||
| Capabilities::PS_MULTI_RESULTS
|
||||
| Capabilities::PLUGIN_AUTH
|
||||
| Capabilities::CONNECT_ATTRS
|
||||
| Capabilities::PLUGIN_AUTH_LENENC_DATA
|
||||
| Capabilities::CAN_HANDLE_EXPIRED_PASSWORDS
|
||||
| Capabilities::SESSION_TRACK
|
||||
| Capabilities::DEPRECATE_EOF
|
||||
| Capabilities::REMEMBER_OPTIONS
|
||||
| Capabilities::MARIADB_CLIENT_PROGRESS
|
||||
| Capabilities::MARIADB_CLIENT_MULTI
|
||||
| Capabilities::MARIADB_CLIENT_STMT_BULK_OPERATIONS
|
||||
);
|
||||
|
||||
assert_eq!(p.server_default_collation, 8);
|
||||
assert!(p.status.contains(Status::SERVER_STATUS_AUTOCOMMIT));
|
||||
assert!(matches!(
|
||||
p.auth_plugin,
|
||||
Some(AuthPlugin::MySqlNativePassword)
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
&*p.auth_plugin_data.into_iter().collect::<Vec<_>>(),
|
||||
&[116, 54, 76, 92, 106, 34, 100, 83, 85, 49, 52, 79, 112, 104, 57, 34, 60, 72, 53, 110,]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use crate::io::MySqlBufMutExt;
|
||||
use crate::io::{BufMutExt, ProtocolEncode};
|
||||
use crate::protocol::auth::AuthPlugin;
|
||||
use crate::protocol::connect::ssl_request::SslRequest;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse
|
||||
// https://mariadb.com/kb/en/connection/#client-handshake-response
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HandshakeResponse<'a> {
|
||||
pub database: Option<&'a str>,
|
||||
|
||||
/// Max size of a command packet that the client wants to send to the server
|
||||
pub max_packet_size: u32,
|
||||
|
||||
/// Default collation for the connection
|
||||
pub collation: u8,
|
||||
|
||||
/// Name of the SQL account which client wants to log in
|
||||
pub username: &'a str,
|
||||
|
||||
/// Authentication method used by the client
|
||||
pub auth_plugin: Option<AuthPlugin>,
|
||||
|
||||
/// Opaque authentication response
|
||||
pub auth_response: Option<&'a [u8]>,
|
||||
}
|
||||
|
||||
impl ProtocolEncode<'_, Capabilities> for HandshakeResponse<'_> {
|
||||
fn encode_with(
|
||||
&self,
|
||||
buf: &mut Vec<u8>,
|
||||
mut context: Capabilities,
|
||||
) -> Result<(), crate::Error> {
|
||||
if self.auth_plugin.is_none() {
|
||||
// ensure PLUGIN_AUTH is set *only* if we have a defined plugin
|
||||
context.remove(Capabilities::PLUGIN_AUTH);
|
||||
}
|
||||
|
||||
// NOTE: Half of this packet is identical to the SSL Request packet
|
||||
SslRequest {
|
||||
max_packet_size: self.max_packet_size,
|
||||
collation: self.collation,
|
||||
}
|
||||
.encode_with(buf, context)?;
|
||||
|
||||
buf.put_str_nul(self.username);
|
||||
|
||||
if context.contains(Capabilities::PLUGIN_AUTH_LENENC_DATA) {
|
||||
buf.put_bytes_lenenc(self.auth_response.unwrap_or_default());
|
||||
} else if context.contains(Capabilities::SECURE_CONNECTION) {
|
||||
let response = self.auth_response.unwrap_or_default();
|
||||
|
||||
let response_len = u8::try_from(response.len())
|
||||
.map_err(|_| err_protocol!("auth_response.len() too long: {}", response.len()))?;
|
||||
|
||||
buf.push(response_len);
|
||||
buf.extend(response);
|
||||
} else {
|
||||
buf.push(0);
|
||||
}
|
||||
|
||||
if context.contains(Capabilities::CONNECT_WITH_DB) {
|
||||
if let Some(database) = &self.database {
|
||||
buf.put_str_nul(database);
|
||||
} else {
|
||||
buf.push(0);
|
||||
}
|
||||
}
|
||||
|
||||
if context.contains(Capabilities::PLUGIN_AUTH) {
|
||||
if let Some(plugin) = &self.auth_plugin {
|
||||
buf.put_str_nul(plugin.name());
|
||||
} else {
|
||||
buf.push(0);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
//! Connection Phase
|
||||
//!
|
||||
//! <https://dev.mysql.com/doc/internals/en/connection-phase.html>
|
||||
|
||||
mod auth_switch;
|
||||
mod handshake;
|
||||
mod handshake_response;
|
||||
mod ssl_request;
|
||||
|
||||
pub(crate) use auth_switch::{AuthSwitchRequest, AuthSwitchResponse};
|
||||
pub(crate) use handshake::Handshake;
|
||||
pub(crate) use handshake_response::HandshakeResponse;
|
||||
pub(crate) use ssl_request::SslRequest;
|
||||
@@ -0,0 +1,34 @@
|
||||
use crate::io::ProtocolEncode;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/dev/mysql-server/8.0.12/page_protocol_connection_phase_packets_protocol_handshake_response.html
|
||||
// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::SSLRequest
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SslRequest {
|
||||
pub max_packet_size: u32,
|
||||
pub collation: u8,
|
||||
}
|
||||
|
||||
impl ProtocolEncode<'_, Capabilities> for SslRequest {
|
||||
fn encode_with(&self, buf: &mut Vec<u8>, context: Capabilities) -> Result<(), crate::Error> {
|
||||
// truncation is intended
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
buf.extend(&(context.bits() as u32).to_le_bytes());
|
||||
buf.extend(&self.max_packet_size.to_le_bytes());
|
||||
buf.push(self.collation);
|
||||
|
||||
// reserved: string<19>
|
||||
buf.extend(&[0_u8; 19]);
|
||||
|
||||
if context.contains(Capabilities::MYSQL) {
|
||||
// reserved: string<4>
|
||||
buf.extend(&[0_u8; 4]);
|
||||
} else {
|
||||
// extended client capabilities (MariaDB-specified): int<4>
|
||||
buf.extend(&((context.bits() >> 32) as u32).to_le_bytes());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
pub(crate) mod auth;
|
||||
mod capabilities;
|
||||
pub(crate) mod connect;
|
||||
mod packet;
|
||||
pub(crate) mod response;
|
||||
mod row;
|
||||
pub(crate) mod statement;
|
||||
pub(crate) mod text;
|
||||
|
||||
pub(crate) use capabilities::Capabilities;
|
||||
pub(crate) use packet::Packet;
|
||||
pub(crate) use row::Row;
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
use std::cmp::min;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use bytes::Bytes;
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::io::{ProtocolDecode, ProtocolEncode};
|
||||
use crate::protocol::response::{EofPacket, OkPacket};
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Packet<T>(pub(crate) T);
|
||||
|
||||
impl<'en, 'stream, T> ProtocolEncode<'stream, (Capabilities, &'stream mut u8)> for Packet<T>
|
||||
where
|
||||
T: ProtocolEncode<'en, Capabilities>,
|
||||
{
|
||||
fn encode_with(
|
||||
&self,
|
||||
buf: &mut Vec<u8>,
|
||||
(capabilities, sequence_id): (Capabilities, &'stream mut u8),
|
||||
) -> Result<(), Error> {
|
||||
let mut next_header = |len: u32| {
|
||||
let mut buf = len.to_le_bytes();
|
||||
buf[3] = *sequence_id;
|
||||
*sequence_id = sequence_id.wrapping_add(1);
|
||||
|
||||
buf
|
||||
};
|
||||
|
||||
// reserve space to write the prefixed length
|
||||
let offset = buf.len();
|
||||
buf.extend(&[0_u8; 4]);
|
||||
|
||||
// encode the payload
|
||||
self.0.encode_with(buf, capabilities)?;
|
||||
|
||||
// determine the length of the encoded payload
|
||||
// and write to our reserved space
|
||||
let len = buf.len() - offset - 4;
|
||||
let header = &mut buf[offset..];
|
||||
|
||||
// // `min(.., 0xFF_FF_FF)` cannot overflow
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
header[..4].copy_from_slice(&next_header(min(len, 0xFF_FF_FF) as u32));
|
||||
|
||||
// add more packets if we need to split the data
|
||||
if len >= 0xFF_FF_FF {
|
||||
let rest = buf.split_off(offset + 4 + 0xFF_FF_FF);
|
||||
let mut chunks = rest.chunks_exact(0xFF_FF_FF);
|
||||
|
||||
for chunk in chunks.by_ref() {
|
||||
buf.reserve(chunk.len() + 4);
|
||||
|
||||
// `chunk.len() == 0xFF_FF_FF`
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
buf.extend(&next_header(chunk.len() as u32));
|
||||
buf.extend(chunk);
|
||||
}
|
||||
|
||||
// this will also handle adding a zero sized packet if the data size is a multiple of 0xFF_FF_FF
|
||||
let remainder = chunks.remainder();
|
||||
buf.reserve(remainder.len() + 4);
|
||||
|
||||
// `remainder.len() < 0xFF_FF_FF`
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
buf.extend(&next_header(remainder.len() as u32));
|
||||
buf.extend(remainder);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Packet<Bytes> {
|
||||
pub(crate) fn decode<'de, T>(self) -> Result<T, Error>
|
||||
where
|
||||
T: ProtocolDecode<'de, ()>,
|
||||
{
|
||||
self.decode_with(())
|
||||
}
|
||||
|
||||
pub(crate) fn decode_with<'de, T, C>(self, context: C) -> Result<T, Error>
|
||||
where
|
||||
T: ProtocolDecode<'de, C>,
|
||||
{
|
||||
T::decode_with(self.0, context)
|
||||
}
|
||||
|
||||
pub(crate) fn ok(self) -> Result<OkPacket, Error> {
|
||||
self.decode()
|
||||
}
|
||||
|
||||
pub(crate) fn eof(self, capabilities: Capabilities) -> Result<EofPacket, Error> {
|
||||
if capabilities.contains(Capabilities::DEPRECATE_EOF) {
|
||||
let ok = self.ok()?;
|
||||
|
||||
Ok(EofPacket {
|
||||
warnings: ok.warnings,
|
||||
status: ok.status,
|
||||
})
|
||||
} else {
|
||||
self.decode_with(capabilities)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Packet<Bytes> {
|
||||
type Target = Bytes;
|
||||
|
||||
fn deref(&self) -> &Bytes {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Packet<Bytes> {
|
||||
fn deref_mut(&mut self) -> &mut Bytes {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::io::ProtocolDecode;
|
||||
use crate::protocol::response::Status;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
/// Marks the end of a result set, returning status and warnings.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// The EOF packet is deprecated as of MySQL 5.7.5. SQLx only uses this packet for MySQL
|
||||
/// prior MySQL versions.
|
||||
#[derive(Debug)]
|
||||
pub struct EofPacket {
|
||||
#[allow(dead_code)]
|
||||
pub warnings: u16,
|
||||
pub status: Status,
|
||||
}
|
||||
|
||||
impl ProtocolDecode<'_, Capabilities> for EofPacket {
|
||||
fn decode_with(mut buf: Bytes, _: Capabilities) -> Result<Self, Error> {
|
||||
let header = buf.get_u8();
|
||||
if header != 0xfe {
|
||||
return Err(err_protocol!(
|
||||
"expected 0xfe (EOF_Packet) but found 0x{:x}",
|
||||
header
|
||||
));
|
||||
}
|
||||
|
||||
let warnings = buf.get_u16_le();
|
||||
let status = Status::from_bits_truncate(buf.get_u16_le());
|
||||
|
||||
Ok(Self { status, warnings })
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::io::{BufExt, ProtocolDecode};
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/dev/mysql-server/8.0.12/page_protocol_basic_err_packet.html
|
||||
// https://mariadb.com/kb/en/err_packet/
|
||||
|
||||
/// Indicates that an error occurred.
|
||||
#[derive(Debug)]
|
||||
pub struct ErrPacket {
|
||||
pub error_code: u16,
|
||||
pub sql_state: Option<String>,
|
||||
pub error_message: String,
|
||||
}
|
||||
|
||||
impl ProtocolDecode<'_, Capabilities> for ErrPacket {
|
||||
fn decode_with(mut buf: Bytes, capabilities: Capabilities) -> Result<Self, Error> {
|
||||
let header = buf.get_u8();
|
||||
if header != 0xff {
|
||||
return Err(err_protocol!(
|
||||
"expected 0xff (ERR_Packet) but found 0x{:x}",
|
||||
header
|
||||
));
|
||||
}
|
||||
|
||||
let error_code = buf.get_u16_le();
|
||||
let mut sql_state = None;
|
||||
|
||||
if capabilities.contains(Capabilities::PROTOCOL_41) {
|
||||
// If the next byte is '#' then we have a SQL STATE
|
||||
if buf.starts_with(b"#") {
|
||||
buf.advance(1);
|
||||
sql_state = Some(buf.get_str(5)?);
|
||||
}
|
||||
}
|
||||
|
||||
let error_message = buf.get_str(buf.len())?;
|
||||
|
||||
Ok(Self {
|
||||
error_code,
|
||||
sql_state,
|
||||
error_message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_err_packet_out_of_order() {
|
||||
const ERR_PACKETS_OUT_OF_ORDER: &[u8] = b"\xff\x84\x04Got packets out of order";
|
||||
|
||||
let p =
|
||||
ErrPacket::decode_with(ERR_PACKETS_OUT_OF_ORDER.into(), Capabilities::PROTOCOL_41).unwrap();
|
||||
|
||||
assert_eq!(&p.error_message, "Got packets out of order");
|
||||
assert_eq!(p.error_code, 1156);
|
||||
assert_eq!(p.sql_state, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_err_packet_unknown_database() {
|
||||
const ERR_HANDSHAKE_UNKNOWN_DB: &[u8] = b"\xff\x19\x04#42000Unknown database \'unknown\'";
|
||||
|
||||
let p =
|
||||
ErrPacket::decode_with(ERR_HANDSHAKE_UNKNOWN_DB.into(), Capabilities::PROTOCOL_41).unwrap();
|
||||
|
||||
assert_eq!(p.error_code, 1049);
|
||||
assert_eq!(p.sql_state.as_deref(), Some("42000"));
|
||||
assert_eq!(&p.error_message, "Unknown database \'unknown\'");
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
//! Generic Response Packets
|
||||
//!
|
||||
//! <https://dev.mysql.com/doc/internals/en/generic-response-packets.html>
|
||||
//! <https://mariadb.com/kb/en/4-server-response-packets/>
|
||||
|
||||
mod eof;
|
||||
mod err;
|
||||
mod ok;
|
||||
mod status;
|
||||
|
||||
pub use eof::EofPacket;
|
||||
pub use err::ErrPacket;
|
||||
pub use ok::OkPacket;
|
||||
pub use status::Status;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::io::MySqlBufExt;
|
||||
use crate::io::ProtocolDecode;
|
||||
use crate::protocol::response::Status;
|
||||
|
||||
/// Indicates successful completion of a previous command sent by the client.
|
||||
#[derive(Debug)]
|
||||
pub struct OkPacket {
|
||||
pub affected_rows: u64,
|
||||
pub last_insert_id: u64,
|
||||
pub status: Status,
|
||||
pub warnings: u16,
|
||||
}
|
||||
|
||||
impl ProtocolDecode<'_> for OkPacket {
|
||||
fn decode_with(mut buf: Bytes, _: ()) -> Result<Self, Error> {
|
||||
let header = buf.get_u8();
|
||||
if header != 0 && header != 0xfe {
|
||||
return Err(err_protocol!(
|
||||
"expected 0x00 or 0xfe (OK_Packet) but found 0x{:02x}",
|
||||
header
|
||||
));
|
||||
}
|
||||
|
||||
let affected_rows = buf.get_uint_lenenc();
|
||||
let last_insert_id = buf.get_uint_lenenc();
|
||||
let status = Status::from_bits_truncate(buf.get_u16_le());
|
||||
let warnings = buf.get_u16_le();
|
||||
|
||||
Ok(Self {
|
||||
affected_rows,
|
||||
last_insert_id,
|
||||
status,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_ok_packet() {
|
||||
const DATA: &[u8] = b"\x00\x00\x00\x02@\x00\x00";
|
||||
|
||||
let p = OkPacket::decode(DATA.into()).unwrap();
|
||||
|
||||
assert_eq!(p.affected_rows, 0);
|
||||
assert_eq!(p.last_insert_id, 0);
|
||||
assert_eq!(p.warnings, 0);
|
||||
assert!(p.status.contains(Status::SERVER_STATUS_AUTOCOMMIT));
|
||||
assert!(p.status.contains(Status::SERVER_SESSION_STATE_CHANGED));
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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, 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
|
||||
// transactional statement, when autocommit=off.
|
||||
const SERVER_STATUS_IN_TRANS = 1;
|
||||
|
||||
// Autocommit mode is set
|
||||
const SERVER_STATUS_AUTOCOMMIT = 2;
|
||||
|
||||
// Multi query - next query exists.
|
||||
const SERVER_MORE_RESULTS_EXISTS = 8;
|
||||
|
||||
const SERVER_QUERY_NO_GOOD_INDEX_USED = 16;
|
||||
const SERVER_QUERY_NO_INDEX_USED = 32;
|
||||
|
||||
// When using COM_STMT_FETCH, indicate that current cursor still has result
|
||||
const SERVER_STATUS_CURSOR_EXISTS = 64;
|
||||
|
||||
// When using COM_STMT_FETCH, indicate that current cursor has finished to send results
|
||||
const SERVER_STATUS_LAST_ROW_SENT = 128;
|
||||
|
||||
// Database has been dropped
|
||||
const SERVER_STATUS_DB_DROPPED = (1 << 8);
|
||||
|
||||
// Current escape mode is "no backslash escape"
|
||||
const SERVER_STATUS_NO_BACKSLASH_ESCAPES = (1 << 9);
|
||||
|
||||
// A DDL change did have an impact on an existing PREPARE (an automatic
|
||||
// re-prepare has been executed)
|
||||
const SERVER_STATUS_METADATA_CHANGED = (1 << 10);
|
||||
|
||||
// Last statement took more than the time value specified
|
||||
// in server variable long_query_time.
|
||||
const SERVER_QUERY_WAS_SLOW = (1 << 11);
|
||||
|
||||
// This result-set contain stored procedure output parameter.
|
||||
const SERVER_PS_OUT_PARAMS = (1 << 12);
|
||||
|
||||
// Current transaction is a read-only transaction.
|
||||
const SERVER_STATUS_IN_TRANS_READONLY = (1 << 13);
|
||||
|
||||
// This status flag, when on, implies that one of the state information has changed
|
||||
// on the server because of the execution of the last statement.
|
||||
const SERVER_SESSION_STATE_CHANGED = (1 << 14);
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use bytes::Bytes;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Row {
|
||||
pub(crate) storage: Bytes,
|
||||
pub(crate) values: Vec<Option<Range<usize>>>,
|
||||
}
|
||||
|
||||
impl Row {
|
||||
pub(crate) fn get(&self, index: usize) -> Option<&[u8]> {
|
||||
self.values[index].clone().map(|col| &self.storage[col])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use crate::io::ProtocolEncode;
|
||||
use crate::protocol::text::ColumnFlags;
|
||||
use crate::protocol::Capabilities;
|
||||
use crate::MySqlArguments;
|
||||
|
||||
// https://dev.mysql.com/doc/dev/mysql-server/8.0.12/page_protocol_com_stmt_execute.html
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Execute<'q> {
|
||||
pub statement: u32,
|
||||
pub arguments: &'q MySqlArguments,
|
||||
}
|
||||
|
||||
impl<'q> ProtocolEncode<'_, Capabilities> for Execute<'q> {
|
||||
fn encode_with(&self, buf: &mut Vec<u8>, _: Capabilities) -> Result<(), crate::Error> {
|
||||
buf.push(0x17); // COM_STMT_EXECUTE
|
||||
buf.extend(&self.statement.to_le_bytes());
|
||||
buf.push(0); // NO_CURSOR
|
||||
buf.extend(&1_u32.to_le_bytes()); // iterations (always 1): int<4>
|
||||
|
||||
if !self.arguments.types.is_empty() {
|
||||
buf.extend_from_slice(&self.arguments.null_bitmap);
|
||||
buf.push(1); // send type to server
|
||||
|
||||
for ty in &self.arguments.types {
|
||||
buf.push(ty.r#type as u8);
|
||||
|
||||
buf.push(if ty.flags.contains(ColumnFlags::UNSIGNED) {
|
||||
0x80
|
||||
} else {
|
||||
0
|
||||
});
|
||||
}
|
||||
|
||||
buf.extend(&*self.arguments.values);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
mod execute;
|
||||
mod prepare;
|
||||
mod prepare_ok;
|
||||
mod row;
|
||||
mod stmt_close;
|
||||
|
||||
pub(crate) use execute::Execute;
|
||||
pub(crate) use prepare::Prepare;
|
||||
pub(crate) use prepare_ok::PrepareOk;
|
||||
pub(crate) use row::BinaryRow;
|
||||
pub(crate) use stmt_close::StmtClose;
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::io::ProtocolEncode;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-stmt-prepare.html#packet-COM_STMT_PREPARE
|
||||
|
||||
pub struct Prepare<'a> {
|
||||
pub query: &'a str,
|
||||
}
|
||||
|
||||
impl ProtocolEncode<'_, Capabilities> for Prepare<'_> {
|
||||
fn encode_with(&self, buf: &mut Vec<u8>, _: Capabilities) -> Result<(), crate::Error> {
|
||||
buf.push(0x16); // COM_STMT_PREPARE
|
||||
buf.extend(self.query.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::io::ProtocolDecode;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-stmt-prepare-response.html#packet-COM_STMT_PREPARE_OK
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct PrepareOk {
|
||||
pub(crate) statement_id: u32,
|
||||
pub(crate) columns: u16,
|
||||
pub(crate) params: u16,
|
||||
#[allow(unused)]
|
||||
pub(crate) warnings: u16,
|
||||
}
|
||||
|
||||
impl ProtocolDecode<'_, Capabilities> for PrepareOk {
|
||||
fn decode_with(buf: Bytes, _: Capabilities) -> Result<Self, Error> {
|
||||
const SIZE: usize = 12;
|
||||
|
||||
let mut slice = buf.get(..SIZE).ok_or_else(|| {
|
||||
err_protocol!("PrepareOk expected 12 bytes but got {} bytes", buf.len())
|
||||
})?;
|
||||
|
||||
let status = slice.get_u8();
|
||||
if status != 0x00 {
|
||||
return Err(err_protocol!(
|
||||
"expected 0x00 (COM_STMT_PREPARE_OK) but found 0x{:02x}",
|
||||
status
|
||||
));
|
||||
}
|
||||
|
||||
let statement_id = slice.get_u32_le();
|
||||
let columns = slice.get_u16_le();
|
||||
let params = slice.get_u16_le();
|
||||
|
||||
slice.advance(1); // reserved: string<1>
|
||||
|
||||
let warnings = slice.get_u16_le();
|
||||
|
||||
Ok(Self {
|
||||
statement_id,
|
||||
columns,
|
||||
params,
|
||||
warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::io::MySqlBufExt;
|
||||
use crate::io::{BufExt, ProtocolDecode};
|
||||
use crate::protocol::text::ColumnType;
|
||||
use crate::protocol::Row;
|
||||
use crate::MySqlColumn;
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/binary-protocol-resultset-row.html#packet-ProtocolBinary::ResultsetRow
|
||||
// https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BinaryRow(pub(crate) Row);
|
||||
|
||||
impl<'de> ProtocolDecode<'de, &'de [MySqlColumn]> for BinaryRow {
|
||||
fn decode_with(mut buf: Bytes, columns: &'de [MySqlColumn]) -> Result<Self, Error> {
|
||||
let header = buf.get_u8();
|
||||
if header != 0 {
|
||||
return Err(err_protocol!(
|
||||
"exepcted 0x00 (ROW) but found 0x{:02x}",
|
||||
header
|
||||
));
|
||||
}
|
||||
|
||||
let storage = buf.clone();
|
||||
let offset = buf.len();
|
||||
|
||||
let null_bitmap_len = (columns.len() + 9) / 8;
|
||||
let null_bitmap = buf.get_bytes(null_bitmap_len);
|
||||
|
||||
let mut values = Vec::with_capacity(columns.len());
|
||||
|
||||
for (column_idx, column) in columns.iter().enumerate() {
|
||||
// NOTE: the column index starts at the 3rd bit
|
||||
let column_null_idx = column_idx + 2;
|
||||
|
||||
let byte_idx = column_null_idx / 8;
|
||||
let bit_idx = column_null_idx % 8;
|
||||
|
||||
let is_null = null_bitmap[byte_idx] & (1u8 << bit_idx) != 0;
|
||||
|
||||
if is_null {
|
||||
values.push(None);
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE: MySQL will never generate NULL types for non-NULL values
|
||||
let type_info = &column.type_info;
|
||||
|
||||
// Unlike Postgres, MySQL does not length-prefix every value in a binary row.
|
||||
// Values are *either* fixed-length or length-prefixed,
|
||||
// so we need to inspect the type code to be sure.
|
||||
let size: usize = match type_info.r#type {
|
||||
// All fixed-length types.
|
||||
ColumnType::LongLong => 8,
|
||||
ColumnType::Long | ColumnType::Int24 => 4,
|
||||
ColumnType::Short | ColumnType::Year => 2,
|
||||
ColumnType::Tiny => 1,
|
||||
ColumnType::Float => 4,
|
||||
ColumnType::Double => 8,
|
||||
|
||||
// Blobs and strings are prefixed with their length,
|
||||
// which is itself a length-encoded integer.
|
||||
ColumnType::String
|
||||
| ColumnType::VarChar
|
||||
| ColumnType::VarString
|
||||
| ColumnType::Enum
|
||||
| ColumnType::Set
|
||||
| ColumnType::LongBlob
|
||||
| ColumnType::MediumBlob
|
||||
| ColumnType::Blob
|
||||
| ColumnType::TinyBlob
|
||||
| ColumnType::Geometry
|
||||
| ColumnType::Bit
|
||||
| ColumnType::Decimal
|
||||
| ColumnType::Json
|
||||
| ColumnType::NewDecimal => {
|
||||
let size = buf.get_uint_lenenc();
|
||||
usize::try_from(size)
|
||||
.map_err(|_| err_protocol!("BLOB length out of range: {size}"))?
|
||||
}
|
||||
|
||||
// Like strings and blobs, these values are variable-length.
|
||||
// Unlike strings and blobs, however, they exclusively use one byte for length.
|
||||
ColumnType::Time
|
||||
| ColumnType::Timestamp
|
||||
| ColumnType::Date
|
||||
| ColumnType::Datetime => {
|
||||
// Leave the length byte on the front of the value because decoding uses it.
|
||||
buf[0] as usize + 1
|
||||
}
|
||||
|
||||
// NOTE: MySQL will never generate NULL types for non-NULL values
|
||||
ColumnType::Null => unreachable!(),
|
||||
};
|
||||
|
||||
let offset = offset - buf.len();
|
||||
|
||||
values.push(Some(offset..(offset + size)));
|
||||
|
||||
buf.advance(size);
|
||||
}
|
||||
|
||||
Ok(BinaryRow(Row { values, storage }))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use crate::io::ProtocolEncode;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-stmt-close.html
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StmtClose {
|
||||
pub statement: u32,
|
||||
}
|
||||
|
||||
impl ProtocolEncode<'_, Capabilities> for StmtClose {
|
||||
fn encode_with(&self, buf: &mut Vec<u8>, _: Capabilities) -> Result<(), crate::Error> {
|
||||
buf.push(0x19); // COM_STMT_CLOSE
|
||||
buf.extend(&self.statement.to_le_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
use std::str::from_utf8;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::io::MySqlBufExt;
|
||||
use crate::io::ProtocolDecode;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/dev/mysql-server/8.0.12/group__group__cs__column__definition__flags.html
|
||||
|
||||
bitflags! {
|
||||
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct ColumnFlags: u16 {
|
||||
/// Field can't be `NULL`.
|
||||
const NOT_NULL = 1;
|
||||
|
||||
/// Field is part of a primary key.
|
||||
const PRIMARY_KEY = 2;
|
||||
|
||||
/// Field is part of a unique key.
|
||||
const UNIQUE_KEY = 4;
|
||||
|
||||
/// Field is part of a multi-part unique or primary key.
|
||||
const MULTIPLE_KEY = 8;
|
||||
|
||||
/// Field is a blob.
|
||||
const BLOB = 16;
|
||||
|
||||
/// Field is unsigned.
|
||||
const UNSIGNED = 32;
|
||||
|
||||
/// Field is zero filled.
|
||||
const ZEROFILL = 64;
|
||||
|
||||
/// Field is binary.
|
||||
const BINARY = 128;
|
||||
|
||||
/// Field is an enumeration.
|
||||
const ENUM = 256;
|
||||
|
||||
/// Field is an auto-incement field.
|
||||
const AUTO_INCREMENT = 512;
|
||||
|
||||
/// Field is a timestamp.
|
||||
const TIMESTAMP = 1024;
|
||||
|
||||
/// Field is a set.
|
||||
const SET = 2048;
|
||||
|
||||
/// Field does not have a default value.
|
||||
const NO_DEFAULT_VALUE = 4096;
|
||||
|
||||
/// Field is set to NOW on UPDATE.
|
||||
const ON_UPDATE_NOW = 8192;
|
||||
|
||||
/// Field is a number.
|
||||
const NUM = 32768;
|
||||
}
|
||||
}
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-query-response.html#column-type
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum ColumnType {
|
||||
Decimal = 0x00,
|
||||
Tiny = 0x01,
|
||||
Short = 0x02,
|
||||
Long = 0x03,
|
||||
Float = 0x04,
|
||||
Double = 0x05,
|
||||
Null = 0x06,
|
||||
Timestamp = 0x07,
|
||||
LongLong = 0x08,
|
||||
Int24 = 0x09,
|
||||
Date = 0x0a,
|
||||
Time = 0x0b,
|
||||
Datetime = 0x0c,
|
||||
Year = 0x0d,
|
||||
VarChar = 0x0f,
|
||||
Bit = 0x10,
|
||||
Json = 0xf5,
|
||||
NewDecimal = 0xf6,
|
||||
Enum = 0xf7,
|
||||
Set = 0xf8,
|
||||
TinyBlob = 0xf9,
|
||||
MediumBlob = 0xfa,
|
||||
LongBlob = 0xfb,
|
||||
Blob = 0xfc,
|
||||
VarString = 0xfd,
|
||||
String = 0xfe,
|
||||
Geometry = 0xff,
|
||||
}
|
||||
|
||||
// https://dev.mysql.com/doc/dev/mysql-server/8.0.12/page_protocol_com_query_response_text_resultset_column_definition.html
|
||||
// https://mariadb.com/kb/en/resultset/#column-definition-packet
|
||||
// https://dev.mysql.com/doc/internals/en/com-query-response.html#packet-Protocol::ColumnDefinition41
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ColumnDefinition {
|
||||
#[allow(unused)]
|
||||
catalog: Bytes,
|
||||
#[allow(unused)]
|
||||
schema: Bytes,
|
||||
#[allow(unused)]
|
||||
table_alias: Bytes,
|
||||
#[allow(unused)]
|
||||
table: Bytes,
|
||||
alias: Bytes,
|
||||
name: Bytes,
|
||||
#[allow(unused)]
|
||||
pub(crate) collation: u16,
|
||||
pub(crate) max_size: u32,
|
||||
pub(crate) r#type: ColumnType,
|
||||
pub(crate) flags: ColumnFlags,
|
||||
#[allow(unused)]
|
||||
decimals: u8,
|
||||
}
|
||||
|
||||
impl ColumnDefinition {
|
||||
// NOTE: strings in-protocol are transmitted according to the client character set
|
||||
// as this is UTF-8, all these strings should be UTF-8
|
||||
|
||||
pub(crate) fn name(&self) -> Result<&str, Error> {
|
||||
from_utf8(&self.name).map_err(Error::protocol)
|
||||
}
|
||||
|
||||
pub(crate) fn alias(&self) -> Result<&str, Error> {
|
||||
from_utf8(&self.alias).map_err(Error::protocol)
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolDecode<'_, Capabilities> for ColumnDefinition {
|
||||
fn decode_with(mut buf: Bytes, _: Capabilities) -> Result<Self, Error> {
|
||||
let catalog = buf.get_bytes_lenenc()?;
|
||||
let schema = buf.get_bytes_lenenc()?;
|
||||
let table_alias = buf.get_bytes_lenenc()?;
|
||||
let table = buf.get_bytes_lenenc()?;
|
||||
let alias = buf.get_bytes_lenenc()?;
|
||||
let name = buf.get_bytes_lenenc()?;
|
||||
let _next_len = buf.get_uint_lenenc(); // always 0x0c
|
||||
let collation = buf.get_u16_le();
|
||||
let max_size = buf.get_u32_le();
|
||||
let type_id = buf.get_u8();
|
||||
let flags = buf.get_u16_le();
|
||||
let decimals = buf.get_u8();
|
||||
|
||||
Ok(Self {
|
||||
catalog,
|
||||
schema,
|
||||
table_alias,
|
||||
table,
|
||||
alias,
|
||||
name,
|
||||
collation,
|
||||
max_size,
|
||||
r#type: ColumnType::try_from_u16(type_id)?,
|
||||
flags: ColumnFlags::from_bits_truncate(flags),
|
||||
decimals,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnType {
|
||||
pub(crate) fn name(self, flags: ColumnFlags, max_size: Option<u32>) -> &'static str {
|
||||
let is_binary = flags.contains(ColumnFlags::BINARY);
|
||||
let is_unsigned = flags.contains(ColumnFlags::UNSIGNED);
|
||||
let is_enum = flags.contains(ColumnFlags::ENUM);
|
||||
|
||||
match self {
|
||||
ColumnType::Tiny if max_size == Some(1) => "BOOLEAN",
|
||||
ColumnType::Tiny if is_unsigned => "TINYINT UNSIGNED",
|
||||
ColumnType::Short if is_unsigned => "SMALLINT UNSIGNED",
|
||||
ColumnType::Long if is_unsigned => "INT UNSIGNED",
|
||||
ColumnType::Int24 if is_unsigned => "MEDIUMINT UNSIGNED",
|
||||
ColumnType::LongLong if is_unsigned => "BIGINT UNSIGNED",
|
||||
ColumnType::Tiny => "TINYINT",
|
||||
ColumnType::Short => "SMALLINT",
|
||||
ColumnType::Long => "INT",
|
||||
ColumnType::Int24 => "MEDIUMINT",
|
||||
ColumnType::LongLong => "BIGINT",
|
||||
ColumnType::Float => "FLOAT",
|
||||
ColumnType::Double => "DOUBLE",
|
||||
ColumnType::Null => "NULL",
|
||||
ColumnType::Timestamp => "TIMESTAMP",
|
||||
ColumnType::Date => "DATE",
|
||||
ColumnType::Time => "TIME",
|
||||
ColumnType::Datetime => "DATETIME",
|
||||
ColumnType::Year => "YEAR",
|
||||
ColumnType::Bit => "BIT",
|
||||
ColumnType::Enum => "ENUM",
|
||||
ColumnType::Set => "SET",
|
||||
ColumnType::Decimal | ColumnType::NewDecimal => "DECIMAL",
|
||||
ColumnType::Geometry => "GEOMETRY",
|
||||
ColumnType::Json => "JSON",
|
||||
|
||||
ColumnType::String if is_binary => "BINARY",
|
||||
ColumnType::String if is_enum => "ENUM",
|
||||
ColumnType::VarChar | ColumnType::VarString if is_binary => "VARBINARY",
|
||||
|
||||
ColumnType::String => "CHAR",
|
||||
ColumnType::VarChar | ColumnType::VarString => "VARCHAR",
|
||||
|
||||
ColumnType::TinyBlob if is_binary => "TINYBLOB",
|
||||
ColumnType::TinyBlob => "TINYTEXT",
|
||||
|
||||
ColumnType::Blob if is_binary => "BLOB",
|
||||
ColumnType::Blob => "TEXT",
|
||||
|
||||
ColumnType::MediumBlob if is_binary => "MEDIUMBLOB",
|
||||
ColumnType::MediumBlob => "MEDIUMTEXT",
|
||||
|
||||
ColumnType::LongBlob if is_binary => "LONGBLOB",
|
||||
ColumnType::LongBlob => "LONGTEXT",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn try_from_u16(id: u8) -> Result<Self, Error> {
|
||||
Ok(match id {
|
||||
0x00 => ColumnType::Decimal,
|
||||
0x01 => ColumnType::Tiny,
|
||||
0x02 => ColumnType::Short,
|
||||
0x03 => ColumnType::Long,
|
||||
0x04 => ColumnType::Float,
|
||||
0x05 => ColumnType::Double,
|
||||
0x06 => ColumnType::Null,
|
||||
0x07 => ColumnType::Timestamp,
|
||||
0x08 => ColumnType::LongLong,
|
||||
0x09 => ColumnType::Int24,
|
||||
0x0a => ColumnType::Date,
|
||||
0x0b => ColumnType::Time,
|
||||
0x0c => ColumnType::Datetime,
|
||||
0x0d => ColumnType::Year,
|
||||
// [internal] 0x0e => ColumnType::NewDate,
|
||||
0x0f => ColumnType::VarChar,
|
||||
0x10 => ColumnType::Bit,
|
||||
// [internal] 0x11 => ColumnType::Timestamp2,
|
||||
// [internal] 0x12 => ColumnType::Datetime2,
|
||||
// [internal] 0x13 => ColumnType::Time2,
|
||||
0xf5 => ColumnType::Json,
|
||||
0xf6 => ColumnType::NewDecimal,
|
||||
0xf7 => ColumnType::Enum,
|
||||
0xf8 => ColumnType::Set,
|
||||
0xf9 => ColumnType::TinyBlob,
|
||||
0xfa => ColumnType::MediumBlob,
|
||||
0xfb => ColumnType::LongBlob,
|
||||
0xfc => ColumnType::Blob,
|
||||
0xfd => ColumnType::VarString,
|
||||
0xfe => ColumnType::String,
|
||||
0xff => ColumnType::Geometry,
|
||||
|
||||
_ => {
|
||||
return Err(err_protocol!("unknown column type 0x{:02x}", id));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
mod column;
|
||||
mod ping;
|
||||
mod query;
|
||||
mod quit;
|
||||
mod row;
|
||||
|
||||
pub(crate) use column::{ColumnDefinition, ColumnFlags, ColumnType};
|
||||
pub(crate) use ping::Ping;
|
||||
pub(crate) use query::Query;
|
||||
pub(crate) use quit::Quit;
|
||||
pub(crate) use row::TextRow;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use crate::io::ProtocolEncode;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-ping.html
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Ping;
|
||||
|
||||
impl ProtocolEncode<'_, Capabilities> for Ping {
|
||||
fn encode_with(&self, buf: &mut Vec<u8>, _: Capabilities) -> Result<(), crate::Error> {
|
||||
buf.push(0x0e); // COM_PING
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
use crate::io::ProtocolEncode;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-query.html
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Query<'q>(pub(crate) &'q str);
|
||||
|
||||
impl ProtocolEncode<'_, Capabilities> for Query<'_> {
|
||||
fn encode_with(&self, buf: &mut Vec<u8>, _: Capabilities) -> Result<(), crate::Error> {
|
||||
buf.push(0x03); // COM_QUERY
|
||||
buf.extend(self.0.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
use crate::io::ProtocolEncode;
|
||||
use crate::protocol::Capabilities;
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-quit.html
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Quit;
|
||||
|
||||
impl ProtocolEncode<'_, Capabilities> for Quit {
|
||||
fn encode_with(&self, buf: &mut Vec<u8>, _: Capabilities) -> Result<(), crate::Error> {
|
||||
buf.push(0x01); // COM_QUIT
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
use crate::column::MySqlColumn;
|
||||
use crate::error::Error;
|
||||
use crate::io::MySqlBufExt;
|
||||
use crate::io::ProtocolDecode;
|
||||
use crate::protocol::Row;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TextRow(pub(crate) Row);
|
||||
|
||||
impl<'de> ProtocolDecode<'de, &'de [MySqlColumn]> for TextRow {
|
||||
fn decode_with(mut buf: Bytes, columns: &'de [MySqlColumn]) -> Result<Self, Error> {
|
||||
let storage = buf.clone();
|
||||
let offset = buf.len();
|
||||
|
||||
let mut values = Vec::with_capacity(columns.len());
|
||||
|
||||
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}"))?;
|
||||
|
||||
let offset = offset - buf.len();
|
||||
|
||||
values.push(Some(offset..(offset + size)));
|
||||
|
||||
buf.advance(size);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TextRow(Row { values, storage }))
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
use std::iter::{Extend, IntoIterator};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MySqlQueryResult {
|
||||
pub(super) rows_affected: u64,
|
||||
pub(super) last_insert_id: u64,
|
||||
}
|
||||
|
||||
impl MySqlQueryResult {
|
||||
pub fn last_insert_id(&self) -> u64 {
|
||||
self.last_insert_id
|
||||
}
|
||||
|
||||
pub fn rows_affected(&self) -> u64 {
|
||||
self.rows_affected
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<MySqlQueryResult> for MySqlQueryResult {
|
||||
fn extend<T: IntoIterator<Item = MySqlQueryResult>>(&mut self, iter: T) {
|
||||
for elem in iter {
|
||||
self.rows_affected += elem.rows_affected;
|
||||
self.last_insert_id = elem.last_insert_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "any")]
|
||||
/// This conversion attempts to save last_insert_id by converting to i64.
|
||||
impl From<MySqlQueryResult> for sqlx_core::any::AnyQueryResult {
|
||||
fn from(done: MySqlQueryResult) -> Self {
|
||||
sqlx_core::any::AnyQueryResult {
|
||||
rows_affected: done.rows_affected(),
|
||||
last_insert_id: done.last_insert_id().try_into().ok(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) use sqlx_core::row::*;
|
||||
|
||||
use crate::column::ColumnIndex;
|
||||
use crate::error::Error;
|
||||
use crate::ext::ustr::UStr;
|
||||
use crate::HashMap;
|
||||
use crate::{protocol, MySql, MySqlColumn, MySqlValueFormat, MySqlValueRef};
|
||||
|
||||
/// Implementation of [`Row`] for MySQL.
|
||||
#[derive(Debug)]
|
||||
pub struct MySqlRow {
|
||||
pub(crate) row: protocol::Row,
|
||||
pub(crate) format: MySqlValueFormat,
|
||||
pub(crate) columns: Arc<Vec<MySqlColumn>>,
|
||||
pub(crate) column_names: Arc<HashMap<UStr, usize>>,
|
||||
}
|
||||
|
||||
impl Row for MySqlRow {
|
||||
type Database = MySql;
|
||||
|
||||
fn columns(&self) -> &[MySqlColumn] {
|
||||
&self.columns
|
||||
}
|
||||
|
||||
fn try_get_raw<I>(&self, index: I) -> Result<MySqlValueRef<'_>, Error>
|
||||
where
|
||||
I: ColumnIndex<Self>,
|
||||
{
|
||||
let index = index.index(self)?;
|
||||
let column = &self.columns[index];
|
||||
let value = self.row.get(index);
|
||||
|
||||
Ok(MySqlValueRef {
|
||||
format: self.format,
|
||||
row: Some(&self.row.storage),
|
||||
type_info: column.type_info.clone(),
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ColumnIndex<MySqlRow> for &'_ str {
|
||||
fn index(&self, row: &MySqlRow) -> Result<usize, Error> {
|
||||
row.column_names
|
||||
.get(*self)
|
||||
.ok_or_else(|| Error::ColumnNotFound((*self).into()))
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
use super::MySqlColumn;
|
||||
use crate::column::ColumnIndex;
|
||||
use crate::error::Error;
|
||||
use crate::ext::ustr::UStr;
|
||||
use crate::HashMap;
|
||||
use crate::{MySql, MySqlArguments, MySqlTypeInfo};
|
||||
use either::Either;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) use sqlx_core::statement::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MySqlStatement<'q> {
|
||||
pub(crate) sql: Cow<'q, str>,
|
||||
pub(crate) metadata: MySqlStatementMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct MySqlStatementMetadata {
|
||||
pub(crate) columns: Arc<Vec<MySqlColumn>>,
|
||||
pub(crate) column_names: Arc<HashMap<UStr, usize>>,
|
||||
pub(crate) parameters: usize,
|
||||
}
|
||||
|
||||
impl<'q> Statement<'q> for MySqlStatement<'q> {
|
||||
type Database = MySql;
|
||||
|
||||
fn to_owned(&self) -> MySqlStatement<'static> {
|
||||
MySqlStatement::<'static> {
|
||||
sql: Cow::Owned(self.sql.clone().into_owned()),
|
||||
metadata: self.metadata.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn sql(&self) -> &str {
|
||||
&self.sql
|
||||
}
|
||||
|
||||
fn parameters(&self) -> Option<Either<&[MySqlTypeInfo], usize>> {
|
||||
Some(Either::Right(self.metadata.parameters))
|
||||
}
|
||||
|
||||
fn columns(&self) -> &[MySqlColumn] {
|
||||
&self.metadata.columns
|
||||
}
|
||||
|
||||
impl_statement_query!(MySqlArguments);
|
||||
}
|
||||
|
||||
impl ColumnIndex<MySqlStatement<'_>> for &'_ str {
|
||||
fn index(&self, statement: &MySqlStatement<'_>) -> Result<usize, Error> {
|
||||
statement
|
||||
.metadata
|
||||
.column_names
|
||||
.get(*self)
|
||||
.ok_or_else(|| Error::ColumnNotFound((*self).into()))
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
Vendored
+253
@@ -0,0 +1,253 @@
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
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;
|
||||
|
||||
pub(crate) use sqlx_core::testing::*;
|
||||
|
||||
// Using a blocking `OnceCell` here because the critical sections are short.
|
||||
static MASTER_POOL: OnceCell<Pool<MySql>> = OnceCell::new();
|
||||
|
||||
impl TestSupport for MySql {
|
||||
fn test_context(args: &TestArgs) -> BoxFuture<'_, Result<TestContext<Self>, Error>> {
|
||||
Box::pin(async move { test_context(args).await })
|
||||
}
|
||||
|
||||
fn cleanup_test(db_name: &str) -> BoxFuture<'_, Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
let mut conn = MASTER_POOL
|
||||
.get()
|
||||
.expect("cleanup_test() invoked outside `#[sqlx::test]`")
|
||||
.acquire()
|
||||
.await?;
|
||||
|
||||
do_cleanup(&mut conn, db_name).await
|
||||
})
|
||||
}
|
||||
|
||||
fn cleanup_test_dbs() -> BoxFuture<'static, Result<Option<usize>, Error>> {
|
||||
Box::pin(async move {
|
||||
let url = dotenvy::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
|
||||
let mut conn = MySqlConnection::connect(&url).await?;
|
||||
|
||||
let delete_db_names: Vec<String> =
|
||||
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 _ = conn.close().await;
|
||||
Ok(Some(delete_db_names.len()))
|
||||
})
|
||||
}
|
||||
|
||||
fn snapshot(
|
||||
_conn: &mut Self::Connection,
|
||||
) -> BoxFuture<'_, Result<FixtureSnapshot<Self>, Error>> {
|
||||
// TODO: I want to get the testing feature out the door so this will have to wait,
|
||||
// but I'm keeping the code around for now because I plan to come back to it.
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_context(args: &TestArgs) -> Result<TestContext<MySql>, Error> {
|
||||
let url = dotenvy::var("DATABASE_URL").expect("DATABASE_URL must be set");
|
||||
|
||||
let master_opts = MySqlConnectOptions::from_str(&url).expect("failed to parse DATABASE_URL");
|
||||
|
||||
let pool = PoolOptions::new()
|
||||
// MySql's normal connection limit is 150 plus 1 superuser connection
|
||||
// We don't want to use the whole cap and there may be fuzziness here due to
|
||||
// concurrently running tests anyway.
|
||||
.max_connections(20)
|
||||
// Immediately close master connections. Tokio's I/O streams don't like hopping runtimes.
|
||||
.after_release(|_conn, _| Box::pin(async move { Ok(false) }))
|
||||
.connect_lazy_with(master_opts);
|
||||
|
||||
let master_pool = match MASTER_POOL.try_insert(pool) {
|
||||
Ok(inserted) => inserted,
|
||||
Err((existing, pool)) => {
|
||||
// Sanity checks.
|
||||
assert_eq!(
|
||||
existing.connect_options().host,
|
||||
pool.connect_options().host,
|
||||
"DATABASE_URL changed at runtime, host differs"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
existing.connect_options().database,
|
||||
pool.connect_options().database,
|
||||
"DATABASE_URL changed at runtime, database differs"
|
||||
);
|
||||
|
||||
existing
|
||||
}
|
||||
};
|
||||
|
||||
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 not null,
|
||||
test_path text not null,
|
||||
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?;
|
||||
|
||||
let db_name = MySql::db_name(args);
|
||||
do_cleanup(&mut conn, &db_name).await?;
|
||||
|
||||
query("insert into _sqlx_test_databases(db_name, test_path) values (?, ?)")
|
||||
.bind(&db_name)
|
||||
.bind(args.test_path)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
conn.execute(&format!("create database {db_name}")[..])
|
||||
.await?;
|
||||
|
||||
eprintln!("created database {db_name}");
|
||||
|
||||
Ok(TestContext {
|
||||
pool_opts: PoolOptions::new()
|
||||
// Don't allow a single test to take all the connections.
|
||||
// Most tests shouldn't require more than 5 connections concurrently,
|
||||
// or else they're likely doing too much in one test.
|
||||
.max_connections(5)
|
||||
// Close connections ASAP if left in the idle queue.
|
||||
.idle_timeout(Some(Duration::from_secs(1)))
|
||||
.parent(master_pool.clone()),
|
||||
connect_opts: master_pool
|
||||
.connect_options()
|
||||
.deref()
|
||||
.clone()
|
||||
.database(&db_name),
|
||||
db_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 = ?")
|
||||
.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<Vec<u64>, 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::<MySqlDatabaseError>().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(())
|
||||
}
|
||||
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use futures_core::future::BoxFuture;
|
||||
|
||||
use crate::connection::Waiting;
|
||||
use crate::error::Error;
|
||||
use crate::executor::Executor;
|
||||
use crate::protocol::text::Query;
|
||||
use crate::{MySql, MySqlConnection};
|
||||
|
||||
pub(crate) use sqlx_core::transaction::*;
|
||||
|
||||
/// Implementation of [`TransactionManager`] for MySQL.
|
||||
pub struct MySqlTransactionManager;
|
||||
|
||||
impl TransactionManager for MySqlTransactionManager {
|
||||
type Database = MySql;
|
||||
|
||||
fn begin<'conn>(
|
||||
conn: &'conn mut MySqlConnection,
|
||||
statement: Option<Cow<'static, str>>,
|
||||
) -> 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),
|
||||
};
|
||||
conn.execute(&*statement).await?;
|
||||
if !conn.in_transaction() {
|
||||
return Err(Error::BeginFailed);
|
||||
}
|
||||
conn.inner.transaction_depth += 1;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn commit(conn: &mut MySqlConnection) -> BoxFuture<'_, Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
let depth = conn.inner.transaction_depth;
|
||||
|
||||
if depth > 0 {
|
||||
conn.execute(&*commit_ansi_transaction_sql(depth)).await?;
|
||||
conn.inner.transaction_depth = depth - 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn rollback(conn: &mut MySqlConnection) -> BoxFuture<'_, Result<(), Error>> {
|
||||
Box::pin(async move {
|
||||
let depth = conn.inner.transaction_depth;
|
||||
|
||||
if depth > 0 {
|
||||
conn.execute(&*rollback_ansi_transaction_sql(depth)).await?;
|
||||
conn.inner.transaction_depth = depth - 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn start_rollback(conn: &mut MySqlConnection) {
|
||||
let depth = conn.inner.transaction_depth;
|
||||
|
||||
if depth > 0 {
|
||||
conn.inner.stream.waiting.push_back(Waiting::Result);
|
||||
conn.inner.stream.sequence_id = 0;
|
||||
conn.inner
|
||||
.stream
|
||||
.write_packet(Query(&rollback_ansi_transaction_sql(depth)))
|
||||
.expect("BUG: unexpected error queueing ROLLBACK");
|
||||
|
||||
conn.inner.transaction_depth = depth - 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_transaction_depth(conn: &MySqlConnection) -> usize {
|
||||
conn.inner.transaction_depth
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
// Type mappings used by the macros and `Debug` impls.
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use sqlx_core as sqlx;
|
||||
|
||||
use crate::MySql;
|
||||
|
||||
impl_type_checking!(
|
||||
MySql {
|
||||
u8,
|
||||
u16,
|
||||
u32,
|
||||
u64,
|
||||
i8,
|
||||
i16,
|
||||
i32,
|
||||
i64,
|
||||
f32,
|
||||
f64,
|
||||
|
||||
// ordering is important here as otherwise we might infer strings to be binary
|
||||
// CHAR, VAR_CHAR, TEXT
|
||||
String,
|
||||
|
||||
// BINARY, VAR_BINARY, BLOB
|
||||
Vec<u8>,
|
||||
|
||||
// Types from third-party crates need to be referenced at a known path
|
||||
// for the macros to work, but we don't want to require the user to add extra dependencies.
|
||||
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
||||
sqlx::types::chrono::NaiveTime,
|
||||
|
||||
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
||||
sqlx::types::chrono::NaiveDate,
|
||||
|
||||
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
||||
sqlx::types::chrono::NaiveDateTime,
|
||||
|
||||
#[cfg(all(feature = "chrono", not(feature = "time")))]
|
||||
sqlx::types::chrono::DateTime<sqlx::types::chrono::Utc>,
|
||||
|
||||
#[cfg(feature = "time")]
|
||||
sqlx::types::time::Time,
|
||||
|
||||
#[cfg(feature = "time")]
|
||||
sqlx::types::time::Date,
|
||||
|
||||
#[cfg(feature = "time")]
|
||||
sqlx::types::time::PrimitiveDateTime,
|
||||
|
||||
#[cfg(feature = "time")]
|
||||
sqlx::types::time::OffsetDateTime,
|
||||
|
||||
#[cfg(feature = "bigdecimal")]
|
||||
sqlx::types::BigDecimal,
|
||||
|
||||
#[cfg(feature = "rust_decimal")]
|
||||
sqlx::types::Decimal,
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
sqlx::types::JsonValue,
|
||||
},
|
||||
ParamChecking::Weak,
|
||||
feature-types: info => info.__type_feature_gate(),
|
||||
);
|
||||
Vendored
+118
@@ -0,0 +1,118 @@
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
pub(crate) use sqlx_core::type_info::*;
|
||||
|
||||
use crate::protocol::text::{ColumnDefinition, ColumnFlags, ColumnType};
|
||||
|
||||
/// Type information for a MySql type.
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "offline", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct MySqlTypeInfo {
|
||||
pub(crate) r#type: ColumnType,
|
||||
pub(crate) flags: ColumnFlags,
|
||||
|
||||
// [max_size] for integer types, this is (M) in BIT(M) or TINYINT(M)
|
||||
#[cfg_attr(feature = "offline", serde(default))]
|
||||
pub(crate) max_size: Option<u32>,
|
||||
}
|
||||
|
||||
impl MySqlTypeInfo {
|
||||
pub(crate) const fn binary(ty: ColumnType) -> Self {
|
||||
Self {
|
||||
r#type: ty,
|
||||
flags: ColumnFlags::BINARY,
|
||||
max_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub const fn __enum() -> Self {
|
||||
// Newer versions of MySQL seem to expect that a parameter binding of `MYSQL_TYPE_ENUM`
|
||||
// means that the value is encoded as an integer.
|
||||
//
|
||||
// For "strong" enums inputted as strings, we need to specify this type instead
|
||||
// for wider compatibility. This works on all covered versions of MySQL and MariaDB.
|
||||
//
|
||||
// Annoyingly, MySQL's developer documentation doesn't really explain this anywhere;
|
||||
// this had to be determined experimentally.
|
||||
Self {
|
||||
r#type: ColumnType::String,
|
||||
flags: ColumnFlags::ENUM,
|
||||
max_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn __type_feature_gate(&self) -> Option<&'static str> {
|
||||
match self.r#type {
|
||||
ColumnType::Date | ColumnType::Time | ColumnType::Timestamp | ColumnType::Datetime => {
|
||||
Some("time")
|
||||
}
|
||||
|
||||
ColumnType::Json => Some("json"),
|
||||
ColumnType::NewDecimal => Some("bigdecimal"),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_column(column: &ColumnDefinition) -> Self {
|
||||
Self {
|
||||
r#type: column.r#type,
|
||||
flags: column.flags,
|
||||
max_size: Some(column.max_size),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MySqlTypeInfo {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.pad(self.name())
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeInfo for MySqlTypeInfo {
|
||||
fn is_null(&self) -> bool {
|
||||
matches!(self.r#type, ColumnType::Null)
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
self.r#type.name(self.flags, self.max_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<MySqlTypeInfo> for MySqlTypeInfo {
|
||||
fn eq(&self, other: &MySqlTypeInfo) -> bool {
|
||||
if self.r#type != other.r#type {
|
||||
return false;
|
||||
}
|
||||
|
||||
match self.r#type {
|
||||
ColumnType::Tiny
|
||||
| ColumnType::Short
|
||||
| ColumnType::Long
|
||||
| ColumnType::Int24
|
||||
| ColumnType::LongLong => {
|
||||
return self.flags.contains(ColumnFlags::UNSIGNED)
|
||||
== other.flags.contains(ColumnFlags::UNSIGNED);
|
||||
}
|
||||
|
||||
// for string types, check that our charset matches
|
||||
ColumnType::VarChar
|
||||
| ColumnType::Blob
|
||||
| ColumnType::TinyBlob
|
||||
| ColumnType::MediumBlob
|
||||
| ColumnType::LongBlob
|
||||
| ColumnType::String
|
||||
| ColumnType::VarString
|
||||
| ColumnType::Enum => {
|
||||
return self.flags == other.flags;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MySqlTypeInfo {}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
use bigdecimal::BigDecimal;
|
||||
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::io::MySqlBufMutExt;
|
||||
use crate::protocol::text::ColumnType;
|
||||
use crate::types::Type;
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueRef};
|
||||
|
||||
impl Type<MySql> for BigDecimal {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::NewDecimal)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
matches!(ty.r#type, ColumnType::Decimal | ColumnType::NewDecimal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for BigDecimal {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.put_str_lenenc(&self.to_string());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for BigDecimal {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
Ok(value.as_str()?.parse()?)
|
||||
}
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::types::Type;
|
||||
use crate::{
|
||||
protocol::text::{ColumnFlags, ColumnType},
|
||||
MySql, MySqlTypeInfo, MySqlValueRef,
|
||||
};
|
||||
|
||||
impl Type<MySql> for bool {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
// MySQL has no actual `BOOLEAN` type, the type is an alias of `TINYINT(1)`
|
||||
MySqlTypeInfo {
|
||||
flags: ColumnFlags::BINARY | ColumnFlags::UNSIGNED,
|
||||
max_size: Some(1),
|
||||
r#type: ColumnType::Tiny,
|
||||
}
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
matches!(
|
||||
ty.r#type,
|
||||
ColumnType::Tiny
|
||||
| ColumnType::Short
|
||||
| ColumnType::Long
|
||||
| ColumnType::Int24
|
||||
| ColumnType::LongLong
|
||||
| ColumnType::Bit
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for bool {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
<i8 as Encode<MySql>>::encode(*self as i8, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for bool {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
Ok(<i8 as Decode<MySql>>::decode(value)? != 0)
|
||||
}
|
||||
}
|
||||
Vendored
+85
@@ -0,0 +1,85 @@
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::io::MySqlBufMutExt;
|
||||
use crate::protocol::text::ColumnType;
|
||||
use crate::types::Type;
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueRef};
|
||||
|
||||
impl Type<MySql> for [u8] {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Blob)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
matches!(
|
||||
ty.r#type,
|
||||
ColumnType::VarChar
|
||||
| ColumnType::Blob
|
||||
| ColumnType::TinyBlob
|
||||
| ColumnType::MediumBlob
|
||||
| ColumnType::LongBlob
|
||||
| ColumnType::String
|
||||
| ColumnType::VarString
|
||||
| ColumnType::Enum
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for &'_ [u8] {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.put_bytes_lenenc(self);
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for &'r [u8] {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
value.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for Box<[u8]> {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<&[u8] as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<&[u8] as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Box<[u8]> {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
<&[u8] as Encode<MySql>>::encode(self.as_ref(), buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for Box<[u8]> {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
<&[u8] as Decode<MySql>>::decode(value).map(Box::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for Vec<u8> {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<[u8] as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<&[u8] as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Vec<u8> {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
<&[u8] as Encode<MySql>>::encode(&**self, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for Vec<u8> {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
<&[u8] as Decode<MySql>>::decode(value).map(ToOwned::to_owned)
|
||||
}
|
||||
}
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
use bytes::Buf;
|
||||
use chrono::{
|
||||
DateTime, Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike, Utc,
|
||||
};
|
||||
use sqlx_core::database::Database;
|
||||
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::{BoxDynError, UnexpectedNullError};
|
||||
use crate::protocol::text::ColumnType;
|
||||
use crate::type_info::MySqlTypeInfo;
|
||||
use crate::types::{MySqlTime, MySqlTimeSign, Type};
|
||||
use crate::{MySql, MySqlValueFormat, MySqlValueRef};
|
||||
|
||||
impl Type<MySql> for DateTime<Utc> {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Timestamp)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
matches!(ty.r#type, ColumnType::Datetime | ColumnType::Timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: assumes the connection's `time_zone` is set to `+00:00` (UTC).
|
||||
impl Encode<'_, MySql> for DateTime<Utc> {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
Encode::<MySql>::encode(self.naive_utc(), buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: assumes the connection's `time_zone` is set to `+00:00` (UTC).
|
||||
impl<'r> Decode<'r, MySql> for DateTime<Utc> {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
let naive: NaiveDateTime = Decode::<MySql>::decode(value)?;
|
||||
|
||||
Ok(Utc.from_utc_datetime(&naive))
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for DateTime<Local> {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Timestamp)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
matches!(ty.r#type, ColumnType::Datetime | ColumnType::Timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: assumes the connection's `time_zone` is set to `+00:00` (UTC).
|
||||
impl Encode<'_, MySql> for DateTime<Local> {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
Encode::<MySql>::encode(self.naive_utc(), buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Note: assumes the connection's `time_zone` is set to `+00:00` (UTC).
|
||||
impl<'r> Decode<'r, MySql> for DateTime<Local> {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
Ok(<DateTime<Utc> as Decode<'r, MySql>>::decode(value)?.with_timezone(&Local))
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for NaiveTime {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTime::type_info()
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for NaiveTime {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
let len = naive_time_encoded_len(self);
|
||||
buf.push(len);
|
||||
|
||||
// NaiveTime is not negative
|
||||
buf.push(0);
|
||||
|
||||
// Number of days in the interval; always 0 for time-of-day values.
|
||||
// https://mariadb.com/kb/en/resultset-row/#teimstamp-binary-encoding
|
||||
buf.extend_from_slice(&[0_u8; 4]);
|
||||
|
||||
encode_time(self, len > 8, buf);
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> usize {
|
||||
naive_time_encoded_len(self) as usize + 1 // plus length byte
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode from a `TIME` value.
|
||||
///
|
||||
/// ### Errors
|
||||
/// Returns an error if the `TIME` value is negative or exceeds `23:59:59.999999`.
|
||||
impl<'r> Decode<'r, MySql> for NaiveTime {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
MySqlValueFormat::Binary => {
|
||||
// Covers most possible failure modes.
|
||||
MySqlTime::decode(value)?.try_into()
|
||||
}
|
||||
// Retaining this parsing for now as it allows us to cross-check our impl.
|
||||
MySqlValueFormat::Text => {
|
||||
let s = value.as_str()?;
|
||||
NaiveTime::parse_from_str(s, "%H:%M:%S%.f").map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<MySqlTime> for NaiveTime {
|
||||
type Error = BoxDynError;
|
||||
|
||||
fn try_from(time: MySqlTime) -> Result<Self, Self::Error> {
|
||||
NaiveTime::from_hms_micro_opt(
|
||||
time.hours(),
|
||||
time.minutes() as u32,
|
||||
time.seconds() as u32,
|
||||
time.microseconds(),
|
||||
)
|
||||
.ok_or_else(|| format!("Cannot convert `MySqlTime` value to `NaiveTime`: {time}").into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MySqlTime> for chrono::TimeDelta {
|
||||
fn from(time: MySqlTime) -> Self {
|
||||
chrono::TimeDelta::new(time.whole_seconds_signed(), time.subsec_nanos())
|
||||
.expect("BUG: chrono::TimeDelta should have a greater range than MySqlTime")
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<chrono::TimeDelta> for MySqlTime {
|
||||
type Error = BoxDynError;
|
||||
|
||||
fn try_from(value: chrono::TimeDelta) -> Result<Self, Self::Error> {
|
||||
let sign = if value < chrono::TimeDelta::zero() {
|
||||
MySqlTimeSign::Negative
|
||||
} else {
|
||||
MySqlTimeSign::Positive
|
||||
};
|
||||
|
||||
Ok(
|
||||
// `std::time::Duration` has a greater positive range than `TimeDelta`
|
||||
// which makes it a great intermediate if you ignore the sign.
|
||||
MySqlTime::try_from(value.abs().to_std()?)?.with_sign(sign),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for chrono::TimeDelta {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTime::type_info()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for chrono::TimeDelta {
|
||||
fn decode(value: <MySql as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
Ok(MySqlTime::decode(value)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for NaiveDate {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Date)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for NaiveDate {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.push(4);
|
||||
|
||||
encode_date(self, buf)?;
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> usize {
|
||||
5
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for NaiveDate {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
MySqlValueFormat::Binary => {
|
||||
let buf = value.as_bytes()?;
|
||||
|
||||
// Row decoding should have left the length prefix.
|
||||
if buf.is_empty() {
|
||||
return Err("empty buffer".into());
|
||||
}
|
||||
|
||||
decode_date(&buf[1..])?.ok_or_else(|| UnexpectedNullError.into())
|
||||
}
|
||||
|
||||
MySqlValueFormat::Text => {
|
||||
let s = value.as_str()?;
|
||||
NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for NaiveDateTime {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Datetime)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for NaiveDateTime {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
let len = naive_dt_encoded_len(self);
|
||||
buf.push(len);
|
||||
|
||||
encode_date(&self.date(), buf)?;
|
||||
|
||||
if len > 4 {
|
||||
encode_time(&self.time(), len > 7, buf);
|
||||
}
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> usize {
|
||||
naive_dt_encoded_len(self) as usize + 1 // plus length byte
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for NaiveDateTime {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
MySqlValueFormat::Binary => {
|
||||
let buf = value.as_bytes()?;
|
||||
|
||||
if buf.is_empty() {
|
||||
return Err("empty buffer".into());
|
||||
}
|
||||
|
||||
let len = buf[0];
|
||||
let date = decode_date(&buf[1..])?.ok_or(UnexpectedNullError)?;
|
||||
|
||||
let dt = if len > 4 {
|
||||
date.and_time(decode_time(len - 4, &buf[5..])?)
|
||||
} else {
|
||||
date.and_hms_opt(0, 0, 0)
|
||||
.expect("expected `NaiveDate::and_hms_opt(0, 0, 0)` to be valid")
|
||||
};
|
||||
|
||||
Ok(dt)
|
||||
}
|
||||
|
||||
MySqlValueFormat::Text => {
|
||||
let s = value.as_str()?;
|
||||
NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f").map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_date(date: &NaiveDate, buf: &mut Vec<u8>) -> Result<(), BoxDynError> {
|
||||
// MySQL supports years 1000 - 9999
|
||||
let year = u16::try_from(date.year())
|
||||
.map_err(|_| format!("NaiveDateTime out of range for Mysql: {date}"))?;
|
||||
|
||||
buf.extend_from_slice(&year.to_le_bytes());
|
||||
|
||||
// `NaiveDate` guarantees the ranges of these values
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
{
|
||||
buf.push(date.month() as u8);
|
||||
buf.push(date.day() as u8);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_date(mut buf: &[u8]) -> Result<Option<NaiveDate>, BoxDynError> {
|
||||
match buf.len() {
|
||||
// MySQL specifies that if there are no bytes, this is all zeros
|
||||
0 => Ok(None),
|
||||
4.. => {
|
||||
let year = buf.get_u16_le() as i32;
|
||||
let month = buf[0] as u32;
|
||||
let day = buf[1] as u32;
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(year, month, day)
|
||||
.ok_or_else(|| format!("server returned invalid date: {year}/{month}/{day}"))?;
|
||||
|
||||
Ok(Some(date))
|
||||
}
|
||||
len => Err(format!("expected at least 4 bytes for date, got {len}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_time(time: &NaiveTime, include_micros: bool, buf: &mut Vec<u8>) {
|
||||
// `NaiveTime` API guarantees the ranges of these values
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
{
|
||||
buf.push(time.hour() as u8);
|
||||
buf.push(time.minute() as u8);
|
||||
buf.push(time.second() as u8);
|
||||
}
|
||||
|
||||
if include_micros {
|
||||
buf.extend((time.nanosecond() / 1000).to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_time(len: u8, mut buf: &[u8]) -> Result<NaiveTime, BoxDynError> {
|
||||
let hour = buf.get_u8();
|
||||
let minute = buf.get_u8();
|
||||
let seconds = buf.get_u8();
|
||||
|
||||
let micros = if len > 3 {
|
||||
// microseconds : int<EOF>
|
||||
buf.get_uint_le(buf.len())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let micros = u32::try_from(micros)
|
||||
.map_err(|_| format!("server returned microseconds out of range: {micros}"))?;
|
||||
|
||||
NaiveTime::from_hms_micro_opt(hour as u32, minute as u32, seconds as u32, micros)
|
||||
.ok_or_else(|| format!("server returned invalid time: {hour:02}:{minute:02}:{seconds:02}; micros: {micros}").into())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn naive_dt_encoded_len(time: &NaiveDateTime) -> u8 {
|
||||
// to save space the packet can be compressed:
|
||||
match (
|
||||
time.hour(),
|
||||
time.minute(),
|
||||
time.second(),
|
||||
#[allow(deprecated)]
|
||||
time.timestamp_subsec_nanos(),
|
||||
) {
|
||||
// if hour, minutes, seconds and micro_seconds are all 0,
|
||||
// length is 4 and no other field is sent
|
||||
(0, 0, 0, 0) => 4,
|
||||
|
||||
// if micro_seconds is 0, length is 7
|
||||
// and micro_seconds is not sent
|
||||
(_, _, _, 0) => 7,
|
||||
|
||||
// otherwise length is 11
|
||||
(_, _, _, _) => 11,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn naive_time_encoded_len(time: &NaiveTime) -> u8 {
|
||||
if time.nanosecond() == 0 {
|
||||
// if micro_seconds is 0, length is 8 and micro_seconds is not sent
|
||||
8
|
||||
} else {
|
||||
// otherwise length is 12
|
||||
12
|
||||
}
|
||||
}
|
||||
Vendored
+108
@@ -0,0 +1,108 @@
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::protocol::text::ColumnType;
|
||||
use crate::types::Type;
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueFormat, MySqlValueRef};
|
||||
|
||||
fn real_compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
// NOTE: `DECIMAL` is explicitly excluded because floating-point numbers have different semantics.
|
||||
matches!(ty.r#type, ColumnType::Float | ColumnType::Double)
|
||||
}
|
||||
|
||||
impl Type<MySql> for f32 {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Float)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
real_compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for f64 {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Double)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
real_compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for f32 {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.extend(&self.to_le_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for f64 {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.extend(&self.to_le_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for f32 {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
Ok(match value.format() {
|
||||
MySqlValueFormat::Binary => {
|
||||
let buf = value.as_bytes()?;
|
||||
|
||||
match buf.len() {
|
||||
// These functions panic if `buf` is not exactly the right size.
|
||||
4 => LittleEndian::read_f32(buf),
|
||||
// MySQL can return 8-byte DOUBLE values for a FLOAT
|
||||
// We take and truncate to f32 as that's the same behavior as *in* MySQL,
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
8 => LittleEndian::read_f64(buf) as f32,
|
||||
other => {
|
||||
// Users may try to decode a DECIMAL as floating point;
|
||||
// inform them why that's a bad idea.
|
||||
return Err(format!(
|
||||
"expected a FLOAT as 4 or 8 bytes, got {other} bytes; \
|
||||
note that decoding DECIMAL as `f32` is not supported \
|
||||
due to differing semantics"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MySqlValueFormat::Text => value.as_str()?.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for f64 {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
Ok(match value.format() {
|
||||
MySqlValueFormat::Binary => {
|
||||
let buf = value.as_bytes()?;
|
||||
|
||||
// The `read_*` functions panic if `buf` is not exactly the right size.
|
||||
match buf.len() {
|
||||
// Allow implicit widening here
|
||||
4 => LittleEndian::read_f32(buf) as f64,
|
||||
8 => LittleEndian::read_f64(buf),
|
||||
other => {
|
||||
// Users may try to decode a DECIMAL as floating point;
|
||||
// inform them why that's a bad idea.
|
||||
return Err(format!(
|
||||
"expected a DOUBLE as 4 or 8 bytes, got {other} bytes; \
|
||||
note that decoding DECIMAL as `f64` is not supported \
|
||||
due to differing semantics"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
MySqlValueFormat::Text => value.as_str()?.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::io::MySqlBufMutExt;
|
||||
use crate::types::Type;
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueRef};
|
||||
|
||||
impl Type<MySql> for Ipv4Addr {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<&str as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<&str as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Ipv4Addr {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.put_str_lenenc(&self.to_string());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for Ipv4Addr {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
// delegate to the &str type to decode from MySQL
|
||||
let text = <&str as Decode<MySql>>::decode(value)?;
|
||||
|
||||
// parse a Ipv4Addr from the text
|
||||
text.parse().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for Ipv6Addr {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<&str as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<&str as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Ipv6Addr {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.put_str_lenenc(&self.to_string());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for Ipv6Addr {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
// delegate to the &str type to decode from MySQL
|
||||
let text = <&str as Decode<MySql>>::decode(value)?;
|
||||
|
||||
// parse a Ipv6Addr from the text
|
||||
text.parse().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for IpAddr {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<&str as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<&str as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for IpAddr {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.put_str_lenenc(&self.to_string());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for IpAddr {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
// delegate to the &str type to decode from MySQL
|
||||
let text = <&str as Decode<MySql>>::decode(value)?;
|
||||
|
||||
// parse a IpAddr from the text
|
||||
text.parse().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
Vendored
+139
@@ -0,0 +1,139 @@
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::protocol::text::{ColumnFlags, ColumnType};
|
||||
use crate::types::Type;
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueFormat, MySqlValueRef};
|
||||
|
||||
fn int_compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
matches!(
|
||||
ty.r#type,
|
||||
ColumnType::Tiny
|
||||
| ColumnType::Short
|
||||
| ColumnType::Long
|
||||
| ColumnType::Int24
|
||||
| ColumnType::LongLong
|
||||
) && !ty.flags.contains(ColumnFlags::UNSIGNED)
|
||||
}
|
||||
|
||||
impl Type<MySql> for i8 {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Tiny)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
int_compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for i16 {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Short)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
int_compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for i32 {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Long)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
int_compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for i64 {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::LongLong)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
int_compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for i8 {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.extend(&self.to_le_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for i16 {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.extend(&self.to_le_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for i32 {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.extend(&self.to_le_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for i64 {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.extend(&self.to_le_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
fn int_decode(value: MySqlValueRef<'_>) -> Result<i64, BoxDynError> {
|
||||
Ok(match value.format() {
|
||||
MySqlValueFormat::Text => value.as_str()?.parse()?,
|
||||
MySqlValueFormat::Binary => {
|
||||
let buf = value.as_bytes()?;
|
||||
|
||||
// Check conditions that could cause `read_int()` to panic.
|
||||
if buf.is_empty() {
|
||||
return Err("empty buffer".into());
|
||||
}
|
||||
|
||||
if buf.len() > 8 {
|
||||
return Err(format!(
|
||||
"expected no more than 8 bytes for integer value, got {}",
|
||||
buf.len()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
LittleEndian::read_int(buf, buf.len())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for i8 {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
int_decode(value)?.try_into().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for i16 {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
int_decode(value)?.try_into().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for i32 {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
int_decode(value)?.try_into().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for i64 {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
int_decode(value)
|
||||
}
|
||||
}
|
||||
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::protocol::text::ColumnType;
|
||||
use crate::types::{Json, Type};
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueRef};
|
||||
|
||||
impl<T> Type<MySql> for Json<T> {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
// MySql uses the `CHAR` type to pass JSON data from and to the client
|
||||
// NOTE: This is forwards-compatible with MySQL v8+ as CHAR is a common transmission format
|
||||
// and has nothing to do with the native storage ability of MySQL v8+
|
||||
MySqlTypeInfo::binary(ColumnType::String)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
ty.r#type == ColumnType::Json
|
||||
|| <&str as Type<MySql>>::compatible(ty)
|
||||
|| <&[u8] as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Encode<'_, MySql> for Json<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
// Encode JSON as a length-prefixed string.
|
||||
//
|
||||
// The previous implementation encoded into an intermediate buffer to get the final length.
|
||||
// This is because the length prefix for the string is itself length-encoded, so we have
|
||||
// to know the length first before we can start encoding in the buffer... or do we?
|
||||
//
|
||||
// The docs suggest that the integer length-encoding doesn't actually enforce a range on
|
||||
// the value itself as long as it fits in the chosen encoding, so why not just choose
|
||||
// the full length encoding to begin with? Then we can just reserve the space up-front
|
||||
// and encode directly into the buffer.
|
||||
//
|
||||
// If someone is storing a JSON value it's likely large enough that the overhead of using
|
||||
// the full-length integer encoding doesn't really matter. And if it's so large it overflows
|
||||
// a `u64` then the process is likely to run OOM during the encoding process first anyway.
|
||||
|
||||
let lenenc_start = buf.len();
|
||||
|
||||
buf.extend_from_slice(&[0u8; 9]);
|
||||
|
||||
let encode_start = buf.len();
|
||||
self.encode_to(buf)?;
|
||||
let encoded_len = (buf.len() - encode_start) as u64;
|
||||
|
||||
// This prefix indicates that the following 8 bytes are a little-endian integer.
|
||||
buf[lenenc_start] = 0xFE;
|
||||
buf[lenenc_start + 1..][..8].copy_from_slice(&encoded_len.to_le_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, T> Decode<'r, MySql> for Json<T>
|
||||
where
|
||||
T: 'r + Deserialize<'r>,
|
||||
{
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
Json::decode_from_string(value.as_str()?)
|
||||
}
|
||||
}
|
||||
Vendored
+191
@@ -0,0 +1,191 @@
|
||||
//! Conversions between Rust and **MySQL/MariaDB** types.
|
||||
//!
|
||||
//! # Types
|
||||
//!
|
||||
//! | Rust type | MySQL/MariaDB type(s) |
|
||||
//! |---------------------------------------|------------------------------------------------------|
|
||||
//! | `bool` | TINYINT(1), BOOLEAN, BOOL (see below) |
|
||||
//! | `i8` | TINYINT |
|
||||
//! | `i16` | SMALLINT |
|
||||
//! | `i32` | INT |
|
||||
//! | `i64` | BIGINT |
|
||||
//! | `u8` | TINYINT UNSIGNED |
|
||||
//! | `u16` | SMALLINT UNSIGNED |
|
||||
//! | `u32` | INT UNSIGNED |
|
||||
//! | `u64` | BIGINT UNSIGNED |
|
||||
//! | `f32` | FLOAT |
|
||||
//! | `f64` | DOUBLE |
|
||||
//! | `&str`, [`String`] | VARCHAR, CHAR, TEXT |
|
||||
//! | `&[u8]`, `Vec<u8>` | VARBINARY, BINARY, BLOB |
|
||||
//! | `IpAddr` | VARCHAR, TEXT |
|
||||
//! | `Ipv4Addr` | INET4 (MariaDB-only), VARCHAR, TEXT |
|
||||
//! | `Ipv6Addr` | INET6 (MariaDB-only), VARCHAR, TEXT |
|
||||
//! | [`MySqlTime`] | TIME (encode and decode full range) |
|
||||
//! | [`Duration`][std::time::Duration] | TIME (for decoding positive values only) |
|
||||
//!
|
||||
//! ##### Note: `BOOLEAN`/`BOOL` Type
|
||||
//! MySQL and MariaDB treat `BOOLEAN` as an alias of the `TINYINT` type:
|
||||
//!
|
||||
//! * [Using Data Types from Other Database Engines (MySQL)](https://dev.mysql.com/doc/refman/8.0/en/other-vendor-data-types.html)
|
||||
//! * [BOOLEAN (MariaDB)](https://mariadb.com/kb/en/boolean/)
|
||||
//!
|
||||
//! For the most part, you can simply use the Rust type `bool` when encoding or decoding a value
|
||||
//! using the dynamic query interface, or passing a boolean as a parameter to the query macros
|
||||
//! (`query!()` _et al._).
|
||||
//!
|
||||
//! However, because the MySQL wire protocol does not distinguish between `TINYINT` and `BOOLEAN`,
|
||||
//! the query macros cannot know that a `TINYINT` column is semantically a boolean.
|
||||
//! By default, they will map a `TINYINT` column as `i8` instead, as that is the safer assumption.
|
||||
//!
|
||||
//! Thus, you must use the type override syntax in the query to tell the macros you are expecting
|
||||
//! a `bool` column. See the docs for `query!()` and `query_as!()` for details on this syntax.
|
||||
//!
|
||||
//! ### NOTE: MySQL's `TIME` type is signed
|
||||
//! MySQL's `TIME` type can be used as either a time-of-day value, or a signed interval.
|
||||
//! Thus, it may take on negative values.
|
||||
//!
|
||||
//! Decoding a [`std::time::Duration`] returns an error if the `TIME` value is negative.
|
||||
//!
|
||||
//! ### [`chrono`](https://crates.io/crates/chrono)
|
||||
//!
|
||||
//! Requires the `chrono` Cargo feature flag.
|
||||
//!
|
||||
//! | Rust type | MySQL/MariaDB type(s) |
|
||||
//! |---------------------------------------|------------------------------------------------------|
|
||||
//! | `chrono::DateTime<Utc>` | TIMESTAMP |
|
||||
//! | `chrono::DateTime<Local>` | TIMESTAMP |
|
||||
//! | `chrono::NaiveDateTime` | DATETIME |
|
||||
//! | `chrono::NaiveDate` | DATE |
|
||||
//! | `chrono::NaiveTime` | TIME (time-of-day only) |
|
||||
//! | `chrono::TimeDelta` | TIME (decodes full range; see note for encoding) |
|
||||
//!
|
||||
//! ### NOTE: MySQL's `TIME` type is dual-purpose
|
||||
//! MySQL's `TIME` type can be used as either a time-of-day value, or an interval.
|
||||
//! However, `chrono::NaiveTime` is designed only to represent a time-of-day.
|
||||
//!
|
||||
//! Decoding a `TIME` value as `chrono::NaiveTime` will return an error if the value is out of range.
|
||||
//!
|
||||
//! The [`MySqlTime`] type supports the full range and it also implements `TryInto<chrono::NaiveTime>`.
|
||||
//!
|
||||
//! Decoding a `chrono::TimeDelta` also supports the full range.
|
||||
//!
|
||||
//! To encode a `chrono::TimeDelta`, convert it to [`MySqlTime`] first using `TryFrom`/`TryInto`.
|
||||
//!
|
||||
//! ### [`time`](https://crates.io/crates/time)
|
||||
//!
|
||||
//! Requires the `time` Cargo feature flag.
|
||||
//!
|
||||
//! | Rust type | MySQL/MariaDB type(s) |
|
||||
//! |---------------------------------------|------------------------------------------------------|
|
||||
//! | `time::PrimitiveDateTime` | DATETIME |
|
||||
//! | `time::OffsetDateTime` | TIMESTAMP |
|
||||
//! | `time::Date` | DATE |
|
||||
//! | `time::Time` | TIME (time-of-day only) |
|
||||
//! | `time::Duration` | TIME (decodes full range; see note for encoding) |
|
||||
//!
|
||||
//! ### NOTE: MySQL's `TIME` type is dual-purpose
|
||||
//! MySQL's `TIME` type can be used as either a time-of-day value, or an interval.
|
||||
//! However, `time::Time` is designed only to represent a time-of-day.
|
||||
//!
|
||||
//! Decoding a `TIME` value as `time::Time` will return an error if the value is out of range.
|
||||
//!
|
||||
//! The [`MySqlTime`] type supports the full range, and it also implements `TryInto<time::Time>`.
|
||||
//!
|
||||
//! Decoding a `time::Duration` also supports the full range.
|
||||
//!
|
||||
//! To encode a `time::Duration`, convert it to [`MySqlTime`] first using `TryFrom`/`TryInto`.
|
||||
//!
|
||||
//! ### [`bigdecimal`](https://crates.io/crates/bigdecimal)
|
||||
//! Requires the `bigdecimal` Cargo feature flag.
|
||||
//!
|
||||
//! | Rust type | MySQL/MariaDB type(s) |
|
||||
//! |---------------------------------------|------------------------------------------------------|
|
||||
//! | `bigdecimal::BigDecimal` | DECIMAL |
|
||||
//!
|
||||
//! ### [`decimal`](https://crates.io/crates/rust_decimal)
|
||||
//! Requires the `decimal` Cargo feature flag.
|
||||
//!
|
||||
//! | Rust type | MySQL/MariaDB type(s) |
|
||||
//! |---------------------------------------|------------------------------------------------------|
|
||||
//! | `rust_decimal::Decimal` | DECIMAL |
|
||||
//!
|
||||
//! ### [`uuid`](https://crates.io/crates/uuid)
|
||||
//!
|
||||
//! Requires the `uuid` Cargo feature flag.
|
||||
//!
|
||||
//! | Rust type | MySQL/MariaDB type(s) |
|
||||
//! |---------------------------------------|------------------------------------------------------|
|
||||
//! | `uuid::Uuid` | BINARY(16) (see note) |
|
||||
//! | `uuid::fmt::Hyphenated` | CHAR(36), VARCHAR, TEXT, UUID (MariaDB-only) |
|
||||
//! | `uuid::fmt::Simple` | CHAR(32), VARCHAR, TEXT |
|
||||
//!
|
||||
//! #### Note: `Uuid` uses binary format
|
||||
//!
|
||||
//! MySQL does not have a native datatype for UUIDs.
|
||||
//! The `UUID()` function returns a 36-character `TEXT` value,
|
||||
//! which encourages storing UUIDs as text.
|
||||
//!
|
||||
//! MariaDB's `UUID` type stores and retrieves as text, though it has a better representation
|
||||
//! for index sorting (see [MariaDB manual: UUID data-type][mariadb-uuid] for details).
|
||||
//!
|
||||
//! As an opinionated library, SQLx chose to map `uuid::Uuid` to/from binary format by default
|
||||
//! (16 bytes, the raw value of a UUID; SQL type `BINARY(16)`).
|
||||
//! This saves 20 bytes over the text format for each value.
|
||||
//!
|
||||
//! The `impl Decode<MySql> for Uuid` does not support the text format, and will return an error.
|
||||
//!
|
||||
//! If you want to use the text format compatible with the `UUID()` function,
|
||||
//! use [`uuid::fmt::Hyphenated`][::uuid::fmt::Hyphenated] in the place of `Uuid`.
|
||||
//!
|
||||
//! The MySQL official blog has an article showing how to support both binary and text format UUIDs
|
||||
//! by storing the binary and adding a generated column for the text format, though this is rather
|
||||
//! verbose and fiddly: <https://dev.mysql.com/blog-archive/storing-uuid-values-in-mysql-tables/>
|
||||
//!
|
||||
//! [mariadb-uuid]: https://mariadb.com/kb/en/uuid-data-type/
|
||||
//!
|
||||
//! ### [`json`](https://crates.io/crates/serde_json)
|
||||
//!
|
||||
//! Requires the `json` Cargo feature flag.
|
||||
//!
|
||||
//! | Rust type | MySQL/MariaDB type(s) |
|
||||
//! |---------------------------------------|------------------------------------------------------|
|
||||
//! | [`Json<T>`] | JSON |
|
||||
//! | `serde_json::JsonValue` | JSON |
|
||||
//! | `&serde_json::value::RawValue` | JSON |
|
||||
//!
|
||||
//! # Nullable
|
||||
//!
|
||||
//! In addition, `Option<T>` is supported where `T` implements `Type`. An `Option<T>` represents
|
||||
//! a potentially `NULL` value from MySQL/MariaDB.
|
||||
|
||||
pub(crate) use sqlx_core::types::*;
|
||||
|
||||
pub use mysql_time::{MySqlTime, MySqlTimeError, MySqlTimeSign};
|
||||
|
||||
mod bool;
|
||||
mod bytes;
|
||||
mod float;
|
||||
mod inet;
|
||||
mod int;
|
||||
mod mysql_time;
|
||||
mod str;
|
||||
mod text;
|
||||
mod uint;
|
||||
|
||||
#[cfg(feature = "json")]
|
||||
mod json;
|
||||
|
||||
#[cfg(feature = "bigdecimal")]
|
||||
mod bigdecimal;
|
||||
|
||||
#[cfg(feature = "rust_decimal")]
|
||||
mod rust_decimal;
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
mod chrono;
|
||||
|
||||
#[cfg(feature = "time")]
|
||||
mod time;
|
||||
|
||||
#[cfg(feature = "uuid")]
|
||||
mod uuid;
|
||||
+712
@@ -0,0 +1,712 @@
|
||||
//! The [`MysqlTime`] type.
|
||||
|
||||
use crate::protocol::text::ColumnType;
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueFormat};
|
||||
use bytes::{Buf, BufMut};
|
||||
use sqlx_core::database::Database;
|
||||
use sqlx_core::decode::Decode;
|
||||
use sqlx_core::encode::{Encode, IsNull};
|
||||
use sqlx_core::error::BoxDynError;
|
||||
use sqlx_core::types::Type;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::{Debug, Display, Formatter, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
// Similar to `PgInterval`
|
||||
/// Container for a MySQL `TIME` value, which may be an interval or a time-of-day.
|
||||
///
|
||||
/// Allowed range is `-838:59:59.0` to `838:59:59.0`.
|
||||
///
|
||||
/// If this value is used for a time-of-day, the range should be `00:00:00.0` to `23:59:59.999999`.
|
||||
/// You can use [`Self::is_valid_time_of_day()`] to check this easily.
|
||||
///
|
||||
/// * [MySQL Manual 13.2.3: The TIME Type](https://dev.mysql.com/doc/refman/8.3/en/time.html)
|
||||
/// * [MariaDB Manual: TIME](https://mariadb.com/kb/en/time/)
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct MySqlTime {
|
||||
pub(crate) sign: MySqlTimeSign,
|
||||
pub(crate) magnitude: TimeMagnitude,
|
||||
}
|
||||
|
||||
// By using a subcontainer for the actual time magnitude,
|
||||
// we can still use a derived `Ord` implementation and just flip the comparison for negative values.
|
||||
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub(crate) struct TimeMagnitude {
|
||||
pub(crate) hours: u32,
|
||||
pub(crate) minutes: u8,
|
||||
pub(crate) seconds: u8,
|
||||
pub(crate) microseconds: u32,
|
||||
}
|
||||
|
||||
const MAGNITUDE_ZERO: TimeMagnitude = TimeMagnitude {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
microseconds: 0,
|
||||
};
|
||||
|
||||
/// Maximum magnitude (positive or negative).
|
||||
const MAGNITUDE_MAX: TimeMagnitude = TimeMagnitude {
|
||||
hours: MySqlTime::HOURS_MAX,
|
||||
minutes: 59,
|
||||
seconds: 59,
|
||||
// Surprisingly this is not 999_999 which is why `MySqlTimeError::SubsecondExcess`.
|
||||
microseconds: 0,
|
||||
};
|
||||
|
||||
/// The sign for a [`MySqlTime`] type.
|
||||
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum MySqlTimeSign {
|
||||
// The protocol actually specifies negative as 1 and positive as 0,
|
||||
// but by specifying variants this way we can derive `Ord` and it works as expected.
|
||||
/// The interval is negative (invalid for time-of-day values).
|
||||
Negative,
|
||||
/// The interval is positive, or represents a time-of-day.
|
||||
Positive,
|
||||
}
|
||||
|
||||
/// Errors returned by [`MySqlTime::new()`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MySqlTimeError {
|
||||
/// A field of [`MySqlTime`] exceeded its max range.
|
||||
#[error("`MySqlTime` field `{field}` cannot exceed {max}, got {value}")]
|
||||
FieldRange {
|
||||
field: &'static str,
|
||||
max: u32,
|
||||
value: u64,
|
||||
},
|
||||
/// Error returned for time magnitudes (positive or negative) between `838:59:59.0` and `839:00:00.0`.
|
||||
///
|
||||
/// Other range errors should be covered by [`Self::FieldRange`] for the `hours` field.
|
||||
///
|
||||
/// For applications which can tolerate rounding, a valid truncated value is provided.
|
||||
#[error(
|
||||
"`MySqlTime` cannot exceed +/-838:59:59.000000; got {sign}838:59:59.{microseconds:06}"
|
||||
)]
|
||||
SubsecondExcess {
|
||||
/// The sign of the magnitude.
|
||||
sign: MySqlTimeSign,
|
||||
/// The number of microseconds over the maximum.
|
||||
microseconds: u32,
|
||||
/// The truncated value,
|
||||
/// either [`MySqlTime::MIN`] if negative or [`MySqlTime::MAX`] if positive.
|
||||
truncated: MySqlTime,
|
||||
},
|
||||
/// MySQL coerces `-00:00:00` to `00:00:00` but this API considers that an error.
|
||||
///
|
||||
/// For applications which can tolerate coercion, you can convert this error to [`MySqlTime::ZERO`].
|
||||
#[error("attempted to construct a `MySqlTime` value of negative zero")]
|
||||
NegativeZero,
|
||||
}
|
||||
|
||||
impl MySqlTime {
|
||||
/// The `MySqlTime` value corresponding to `TIME '0:00:00.0'` (zero).
|
||||
pub const ZERO: Self = MySqlTime {
|
||||
sign: MySqlTimeSign::Positive,
|
||||
magnitude: MAGNITUDE_ZERO,
|
||||
};
|
||||
|
||||
/// The `MySqlTime` value corresponding to `TIME '838:59:59.0'` (max value).
|
||||
pub const MAX: Self = MySqlTime {
|
||||
sign: MySqlTimeSign::Positive,
|
||||
magnitude: MAGNITUDE_MAX,
|
||||
};
|
||||
|
||||
/// The `MySqlTime` value corresponding to `TIME '-838:59:59.0'` (min value).
|
||||
pub const MIN: Self = MySqlTime {
|
||||
sign: MySqlTimeSign::Negative,
|
||||
// Same magnitude, opposite sign.
|
||||
magnitude: MAGNITUDE_MAX,
|
||||
};
|
||||
|
||||
// The maximums for the other values are self-evident, but not necessarily this one.
|
||||
pub(crate) const HOURS_MAX: u32 = 838;
|
||||
|
||||
/// Construct a [`MySqlTime`] that is valid for use as a `TIME` value.
|
||||
///
|
||||
/// ### Errors
|
||||
/// * [`MySqlTimeError::NegativeZero`] if all fields are 0 but `sign` is [`MySqlTimeSign::Negative`].
|
||||
/// * [`MySqlTimeError::FieldRange`] if any field is out of range:
|
||||
/// * `hours > 838`
|
||||
/// * `minutes > 59`
|
||||
/// * `seconds > 59`
|
||||
/// * `microseconds > 999_999`
|
||||
/// * [`MySqlTimeError::SubsecondExcess`] if the magnitude is less than one second over the maximum.
|
||||
/// * Durations 839 hours or greater are covered by `FieldRange`.
|
||||
pub fn new(
|
||||
sign: MySqlTimeSign,
|
||||
hours: u32,
|
||||
minutes: u8,
|
||||
seconds: u8,
|
||||
microseconds: u32,
|
||||
) -> Result<Self, MySqlTimeError> {
|
||||
macro_rules! check_fields {
|
||||
($($name:ident: $max:expr),+ $(,)?) => {
|
||||
$(
|
||||
if $name > $max {
|
||||
return Err(MySqlTimeError::FieldRange {
|
||||
field: stringify!($name),
|
||||
max: $max as u32,
|
||||
value: $name as u64
|
||||
})
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
check_fields!(
|
||||
hours: Self::HOURS_MAX,
|
||||
minutes: 59,
|
||||
seconds: 59,
|
||||
microseconds: 999_999
|
||||
);
|
||||
|
||||
let values = TimeMagnitude {
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
microseconds,
|
||||
};
|
||||
|
||||
if sign.is_negative() && values == MAGNITUDE_ZERO {
|
||||
return Err(MySqlTimeError::NegativeZero);
|
||||
}
|
||||
|
||||
// This is only `true` if less than 1 second over the maximum magnitude
|
||||
if values > MAGNITUDE_MAX {
|
||||
return Err(MySqlTimeError::SubsecondExcess {
|
||||
sign,
|
||||
microseconds,
|
||||
truncated: if sign.is_positive() {
|
||||
Self::MAX
|
||||
} else {
|
||||
Self::MIN
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
sign,
|
||||
magnitude: values,
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the `sign` of this value.
|
||||
pub fn with_sign(self, sign: MySqlTimeSign) -> Self {
|
||||
Self { sign, ..self }
|
||||
}
|
||||
|
||||
/// Return the sign (positive or negative) for this TIME value.
|
||||
pub fn sign(&self) -> MySqlTimeSign {
|
||||
self.sign
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is zero (equal to [`Self::ZERO`]).
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self == &Self::ZERO
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is positive or zero, `false` if negative.
|
||||
pub fn is_positive(&self) -> bool {
|
||||
self.sign.is_positive()
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is negative, `false` if positive or zero.
|
||||
pub fn is_negative(&self) -> bool {
|
||||
self.sign.is_positive()
|
||||
}
|
||||
|
||||
/// Returns `true` if this interval is a valid time-of-day.
|
||||
///
|
||||
/// If `true`, the sign is positive and `hours` is not greater than 23.
|
||||
pub fn is_valid_time_of_day(&self) -> bool {
|
||||
self.sign.is_positive() && self.hours() < 24
|
||||
}
|
||||
|
||||
/// Get the total number of hours in this interval, from 0 to 838.
|
||||
///
|
||||
/// If this value represents a time-of-day, the range is 0 to 23.
|
||||
pub fn hours(&self) -> u32 {
|
||||
self.magnitude.hours
|
||||
}
|
||||
|
||||
/// Get the number of minutes in this interval, from 0 to 59.
|
||||
pub fn minutes(&self) -> u8 {
|
||||
self.magnitude.minutes
|
||||
}
|
||||
|
||||
/// Get the number of seconds in this interval, from 0 to 59.
|
||||
pub fn seconds(&self) -> u8 {
|
||||
self.magnitude.seconds
|
||||
}
|
||||
|
||||
/// Get the number of seconds in this interval, from 0 to 999,999.
|
||||
pub fn microseconds(&self) -> u32 {
|
||||
self.magnitude.microseconds
|
||||
}
|
||||
|
||||
/// Convert this TIME value to a [`std::time::Duration`].
|
||||
///
|
||||
/// Returns `None` if this value is negative (cannot be represented).
|
||||
pub fn to_duration(&self) -> Option<Duration> {
|
||||
self.is_positive()
|
||||
.then(|| Duration::new(self.whole_seconds() as u64, self.subsec_nanos()))
|
||||
}
|
||||
|
||||
/// Get the whole number of seconds (`seconds + (minutes * 60) + (hours * 3600)`) in this time.
|
||||
///
|
||||
/// Sign is ignored.
|
||||
pub(crate) fn whole_seconds(&self) -> u32 {
|
||||
// If `hours` does not exceed 838 then this cannot overflow.
|
||||
self.hours() * 3600 + self.minutes() as u32 * 60 + self.seconds() as u32
|
||||
}
|
||||
|
||||
#[cfg_attr(not(any(feature = "time", feature = "chrono")), allow(dead_code))]
|
||||
pub(crate) fn whole_seconds_signed(&self) -> i64 {
|
||||
self.whole_seconds() as i64 * self.sign.signum() as i64
|
||||
}
|
||||
|
||||
pub(crate) fn subsec_nanos(&self) -> u32 {
|
||||
self.microseconds() * 1000
|
||||
}
|
||||
|
||||
fn encoded_len(&self) -> u8 {
|
||||
if self.is_zero() {
|
||||
0
|
||||
} else if self.microseconds() == 0 {
|
||||
8
|
||||
} else {
|
||||
12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<MySqlTime> for MySqlTime {
|
||||
fn partial_cmp(&self, other: &MySqlTime) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for MySqlTime {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// If the sides have different signs, we just need to compare those.
|
||||
if self.sign != other.sign {
|
||||
return self.sign.cmp(&other.sign);
|
||||
}
|
||||
|
||||
// We've checked that both sides have the same sign
|
||||
match self.sign {
|
||||
MySqlTimeSign::Positive => self.magnitude.cmp(&other.magnitude),
|
||||
// Reverse the comparison for negative values (smaller negative magnitude = greater)
|
||||
MySqlTimeSign::Negative => other.magnitude.cmp(&self.magnitude),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MySqlTime {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let TimeMagnitude {
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
microseconds,
|
||||
} = self.magnitude;
|
||||
|
||||
// Obeys the `+` flag.
|
||||
Display::fmt(&self.sign(), f)?;
|
||||
|
||||
write!(f, "{hours}:{minutes:02}:{seconds:02}")?;
|
||||
|
||||
// Write microseconds if not zero or a nonzero precision was explicitly requested.
|
||||
if f.precision().map_or(microseconds != 0, |it| it != 0) {
|
||||
f.write_char('.')?;
|
||||
|
||||
let mut remaining_precision = f.precision();
|
||||
let mut remainder = microseconds;
|
||||
let mut power_of_10 = 10u32.pow(5);
|
||||
|
||||
// Write digits from most-significant to least, up to the requested precision.
|
||||
while remainder > 0 && remaining_precision != Some(0) {
|
||||
let digit = remainder / power_of_10;
|
||||
// 1 % 1 = 0
|
||||
remainder %= power_of_10;
|
||||
power_of_10 /= 10;
|
||||
|
||||
write!(f, "{digit}")?;
|
||||
|
||||
if let Some(remaining_precision) = &mut remaining_precision {
|
||||
*remaining_precision = remaining_precision.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
// If any requested precision remains, pad with zeroes.
|
||||
if let Some(precision) = remaining_precision.filter(|it| *it != 0) {
|
||||
write!(f, "{:0precision$}", 0)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for MySqlTime {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Time)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for MySqlTime {
|
||||
fn decode(value: <MySql as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
MySqlValueFormat::Binary => {
|
||||
let mut buf = value.as_bytes()?;
|
||||
|
||||
// Row decoding should have left the length byte on the front.
|
||||
if buf.is_empty() {
|
||||
return Err("empty buffer".into());
|
||||
}
|
||||
|
||||
let length = buf.get_u8();
|
||||
|
||||
// MySQL specifies that if all fields are 0 then the length is 0 and no further data is sent
|
||||
// https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
|
||||
if length == 0 {
|
||||
return Ok(Self::ZERO);
|
||||
}
|
||||
|
||||
if !matches!(buf.len(), 8 | 12) {
|
||||
return Err(format!(
|
||||
"expected 8 or 12 bytes for TIME value, got {}",
|
||||
buf.len()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let sign = MySqlTimeSign::from_byte(buf.get_u8())?;
|
||||
// The wire protocol includes days but the text format doesn't. Isn't that crazy?
|
||||
let days = buf.get_u32_le();
|
||||
let hours = buf.get_u8();
|
||||
let minutes = buf.get_u8();
|
||||
let seconds = buf.get_u8();
|
||||
|
||||
let microseconds = if !buf.is_empty() { buf.get_u32_le() } else { 0 };
|
||||
|
||||
let whole_hours = days
|
||||
.checked_mul(24)
|
||||
.and_then(|days_to_hours| days_to_hours.checked_add(hours as u32))
|
||||
.ok_or("overflow calculating whole hours from `days * 24 + hours`")?;
|
||||
|
||||
Ok(Self::new(
|
||||
sign,
|
||||
whole_hours,
|
||||
minutes,
|
||||
seconds,
|
||||
microseconds,
|
||||
)?)
|
||||
}
|
||||
MySqlValueFormat::Text => parse(value.as_str()?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q> Encode<'q, MySql> for MySqlTime {
|
||||
fn encode_by_ref(
|
||||
&self,
|
||||
buf: &mut <MySql as Database>::ArgumentBuffer<'q>,
|
||||
) -> Result<IsNull, BoxDynError> {
|
||||
if self.is_zero() {
|
||||
buf.put_u8(0);
|
||||
return Ok(IsNull::No);
|
||||
}
|
||||
|
||||
buf.put_u8(self.encoded_len());
|
||||
buf.put_u8(self.sign.to_byte());
|
||||
|
||||
let TimeMagnitude {
|
||||
hours: whole_hours,
|
||||
minutes,
|
||||
seconds,
|
||||
microseconds,
|
||||
} = self.magnitude;
|
||||
|
||||
let days = whole_hours / 24;
|
||||
let hours = (whole_hours % 24) as u8;
|
||||
|
||||
buf.put_u32_le(days);
|
||||
buf.put_u8(hours);
|
||||
buf.put_u8(minutes);
|
||||
buf.put_u8(seconds);
|
||||
|
||||
if microseconds != 0 {
|
||||
buf.put_u32_le(microseconds);
|
||||
}
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> usize {
|
||||
self.encoded_len() as usize + 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert [`MySqlTime`] from [`std::time::Duration`].
|
||||
///
|
||||
/// ### Note: Precision Truncation
|
||||
/// [`Duration`] supports nanosecond precision, but MySQL `TIME` values only support microsecond
|
||||
/// precision.
|
||||
///
|
||||
/// For simplicity, higher precision values are truncated when converting.
|
||||
/// If you prefer another rounding mode instead, you should apply that to the `Duration` first.
|
||||
///
|
||||
/// See also: [MySQL Manual, section 13.2.6: Fractional Seconds in Time Values](https://dev.mysql.com/doc/refman/8.3/en/fractional-seconds.html)
|
||||
///
|
||||
/// ### Errors:
|
||||
/// Returns [`MySqlTimeError::FieldRange`] if the given duration is longer than `838:59:59.999999`.
|
||||
///
|
||||
impl TryFrom<Duration> for MySqlTime {
|
||||
type Error = MySqlTimeError;
|
||||
|
||||
fn try_from(value: Duration) -> Result<Self, Self::Error> {
|
||||
let hours = value.as_secs() / 3600;
|
||||
let rem_seconds = value.as_secs() % 3600;
|
||||
let minutes = (rem_seconds / 60) as u8;
|
||||
let seconds = (rem_seconds % 60) as u8;
|
||||
|
||||
// Simply divides by 1000
|
||||
let microseconds = value.subsec_micros();
|
||||
|
||||
Self::new(
|
||||
MySqlTimeSign::Positive,
|
||||
hours.try_into().map_err(|_| MySqlTimeError::FieldRange {
|
||||
field: "hours",
|
||||
max: Self::HOURS_MAX,
|
||||
value: hours,
|
||||
})?,
|
||||
minutes,
|
||||
seconds,
|
||||
microseconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl MySqlTimeSign {
|
||||
fn from_byte(b: u8) -> Result<Self, BoxDynError> {
|
||||
match b {
|
||||
0 => Ok(Self::Positive),
|
||||
1 => Ok(Self::Negative),
|
||||
other => Err(format!("expected 0 or 1 for TIME sign byte, got {other}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_byte(self) -> u8 {
|
||||
match self {
|
||||
// We can't use `#[repr(u8)]` because this is opposite of the ordering we want from `Ord`
|
||||
Self::Negative => 1,
|
||||
Self::Positive => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn signum(&self) -> i32 {
|
||||
match self {
|
||||
Self::Negative => -1,
|
||||
Self::Positive => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if positive, `false` if negative.
|
||||
pub fn is_positive(&self) -> bool {
|
||||
matches!(self, Self::Positive)
|
||||
}
|
||||
|
||||
/// Returns `true` if negative, `false` if positive.
|
||||
pub fn is_negative(&self) -> bool {
|
||||
matches!(self, Self::Negative)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MySqlTimeSign {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Positive if f.sign_plus() => f.write_char('+'),
|
||||
Self::Negative => f.write_char('-'),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for Duration {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTime::type_info()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for Duration {
|
||||
fn decode(value: <MySql as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
let time = MySqlTime::decode(value)?;
|
||||
|
||||
time.to_duration().ok_or_else(|| {
|
||||
format!("`std::time::Duration` can only decode positive TIME values; got {time}").into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Not exposing this as a `FromStr` impl currently because `MySqlTime` is not designed to be
|
||||
// a general interchange type.
|
||||
fn parse(text: &str) -> Result<MySqlTime, BoxDynError> {
|
||||
let mut segments = text.split(':');
|
||||
|
||||
let hours = segments
|
||||
.next()
|
||||
.ok_or("expected hours segment, got nothing")?;
|
||||
|
||||
let minutes = segments
|
||||
.next()
|
||||
.ok_or("expected minutes segment, got nothing")?;
|
||||
|
||||
let seconds = segments
|
||||
.next()
|
||||
.ok_or("expected seconds segment, got nothing")?;
|
||||
|
||||
// Include the sign in parsing for convenience;
|
||||
// the allowed range of whole hours is much smaller than `i32`'s positive range.
|
||||
let hours: i32 = hours
|
||||
.parse()
|
||||
.map_err(|e| format!("error parsing hours from {text:?} (segment {hours:?}): {e}"))?;
|
||||
|
||||
let sign = if hours.is_negative() {
|
||||
MySqlTimeSign::Negative
|
||||
} else {
|
||||
MySqlTimeSign::Positive
|
||||
};
|
||||
|
||||
let hours = hours.unsigned_abs();
|
||||
|
||||
let minutes: u8 = minutes
|
||||
.parse()
|
||||
.map_err(|e| format!("error parsing minutes from {text:?} (segment {minutes:?}): {e}"))?;
|
||||
|
||||
let (seconds, microseconds): (u8, u32) = if let Some((seconds, microseconds)) =
|
||||
seconds.split_once('.')
|
||||
{
|
||||
(
|
||||
seconds.parse().map_err(|e| {
|
||||
format!("error parsing seconds from {text:?} (segment {seconds:?}): {e}")
|
||||
})?,
|
||||
parse_microseconds(microseconds).map_err(|e| {
|
||||
format!("error parsing microseconds from {text:?} (segment {microseconds:?}): {e}")
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
seconds.parse().map_err(|e| {
|
||||
format!("error parsing seconds from {text:?} (segment {seconds:?}): {e}")
|
||||
})?,
|
||||
0,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(MySqlTime::new(sign, hours, minutes, seconds, microseconds)?)
|
||||
}
|
||||
|
||||
/// Parse microseconds from a fractional seconds string.
|
||||
fn parse_microseconds(micros: &str) -> Result<u32, BoxDynError> {
|
||||
const EXPECTED_DIGITS: usize = 6;
|
||||
|
||||
match micros.len() {
|
||||
0 => Err("empty string".into()),
|
||||
len @ ..=EXPECTED_DIGITS => {
|
||||
// Fewer than 6 digits, multiply to the correct magnitude
|
||||
let micros: u32 = micros.parse()?;
|
||||
// cast cannot overflow
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
Ok(micros * 10u32.pow((EXPECTED_DIGITS - len) as u32))
|
||||
}
|
||||
// More digits than expected, truncate
|
||||
_ => Ok(micros[..EXPECTED_DIGITS].parse()?),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::MySqlTime;
|
||||
use crate::types::MySqlTimeSign;
|
||||
|
||||
use super::parse_microseconds;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(MySqlTime::ZERO.to_string(), "0:00:00");
|
||||
|
||||
assert_eq!(format!("{:.0}", MySqlTime::ZERO), "0:00:00");
|
||||
|
||||
assert_eq!(format!("{:.3}", MySqlTime::ZERO), "0:00:00.000");
|
||||
|
||||
assert_eq!(format!("{:.6}", MySqlTime::ZERO), "0:00:00.000000");
|
||||
|
||||
assert_eq!(format!("{:.9}", MySqlTime::ZERO), "0:00:00.000000000");
|
||||
|
||||
assert_eq!(format!("{:.0}", MySqlTime::MAX), "838:59:59");
|
||||
|
||||
assert_eq!(format!("{:.3}", MySqlTime::MAX), "838:59:59.000");
|
||||
|
||||
assert_eq!(format!("{:.6}", MySqlTime::MAX), "838:59:59.000000");
|
||||
|
||||
assert_eq!(format!("{:.9}", MySqlTime::MAX), "838:59:59.000000000");
|
||||
|
||||
assert_eq!(format!("{:+.0}", MySqlTime::MAX), "+838:59:59");
|
||||
|
||||
assert_eq!(format!("{:+.3}", MySqlTime::MAX), "+838:59:59.000");
|
||||
|
||||
assert_eq!(format!("{:+.6}", MySqlTime::MAX), "+838:59:59.000000");
|
||||
|
||||
assert_eq!(format!("{:+.9}", MySqlTime::MAX), "+838:59:59.000000000");
|
||||
|
||||
assert_eq!(format!("{:.0}", MySqlTime::MIN), "-838:59:59");
|
||||
|
||||
assert_eq!(format!("{:.3}", MySqlTime::MIN), "-838:59:59.000");
|
||||
|
||||
assert_eq!(format!("{:.6}", MySqlTime::MIN), "-838:59:59.000000");
|
||||
|
||||
assert_eq!(format!("{:.9}", MySqlTime::MIN), "-838:59:59.000000000");
|
||||
|
||||
let positive = MySqlTime::new(MySqlTimeSign::Positive, 123, 45, 56, 890011).unwrap();
|
||||
|
||||
assert_eq!(positive.to_string(), "123:45:56.890011");
|
||||
assert_eq!(format!("{positive:.0}"), "123:45:56");
|
||||
assert_eq!(format!("{positive:.3}"), "123:45:56.890");
|
||||
assert_eq!(format!("{positive:.6}"), "123:45:56.890011");
|
||||
assert_eq!(format!("{positive:.9}"), "123:45:56.890011000");
|
||||
|
||||
assert_eq!(format!("{positive:+.0}"), "+123:45:56");
|
||||
assert_eq!(format!("{positive:+.3}"), "+123:45:56.890");
|
||||
assert_eq!(format!("{positive:+.6}"), "+123:45:56.890011");
|
||||
assert_eq!(format!("{positive:+.9}"), "+123:45:56.890011000");
|
||||
|
||||
let negative = MySqlTime::new(MySqlTimeSign::Negative, 123, 45, 56, 890011).unwrap();
|
||||
|
||||
assert_eq!(negative.to_string(), "-123:45:56.890011");
|
||||
assert_eq!(format!("{negative:.0}"), "-123:45:56");
|
||||
assert_eq!(format!("{negative:.3}"), "-123:45:56.890");
|
||||
assert_eq!(format!("{negative:.6}"), "-123:45:56.890011");
|
||||
assert_eq!(format!("{negative:.9}"), "-123:45:56.890011000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_microseconds() {
|
||||
assert_eq!(parse_microseconds("010").unwrap(), 10_000);
|
||||
|
||||
assert_eq!(parse_microseconds("0100000000").unwrap(), 10_000);
|
||||
|
||||
assert_eq!(parse_microseconds("890").unwrap(), 890_000);
|
||||
|
||||
assert_eq!(parse_microseconds("0890").unwrap(), 89_000);
|
||||
|
||||
assert_eq!(
|
||||
// Case in point about not exposing this:
|
||||
// we always truncate excess precision because it's simpler than rounding
|
||||
// and MySQL should never return a higher precision.
|
||||
parse_microseconds("123456789").unwrap(),
|
||||
123456,
|
||||
);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::io::MySqlBufMutExt;
|
||||
use crate::protocol::text::ColumnType;
|
||||
use crate::types::Type;
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueRef};
|
||||
|
||||
impl Type<MySql> for Decimal {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::NewDecimal)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
matches!(ty.r#type, ColumnType::Decimal | ColumnType::NewDecimal)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Decimal {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.put_str_lenenc(&self.to_string());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for Decimal {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
Ok(value.as_str()?.parse()?)
|
||||
}
|
||||
}
|
||||
Vendored
+116
@@ -0,0 +1,116 @@
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::io::MySqlBufMutExt;
|
||||
use crate::protocol::text::{ColumnFlags, ColumnType};
|
||||
use crate::types::Type;
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueRef};
|
||||
use std::borrow::Cow;
|
||||
|
||||
impl Type<MySql> for str {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo {
|
||||
r#type: ColumnType::VarString, // VARCHAR
|
||||
flags: ColumnFlags::empty(),
|
||||
max_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
// TODO: Support more collations being returned from SQL?
|
||||
matches!(
|
||||
ty.r#type,
|
||||
ColumnType::VarChar
|
||||
| ColumnType::Blob
|
||||
| ColumnType::TinyBlob
|
||||
| ColumnType::MediumBlob
|
||||
| ColumnType::LongBlob
|
||||
| ColumnType::String
|
||||
| ColumnType::VarString
|
||||
| ColumnType::Enum
|
||||
) && !ty.flags.contains(ColumnFlags::BINARY)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for &'_ str {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.put_str_lenenc(self);
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for &'r str {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
value.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for Box<str> {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<&str as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<&str as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Box<str> {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
<&str as Encode<MySql>>::encode(&**self, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for Box<str> {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
<&str as Decode<MySql>>::decode(value).map(Box::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for String {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<str as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<str as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for String {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
<&str as Encode<MySql>>::encode(&**self, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for String {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
<&str as Decode<MySql>>::decode(value).map(ToOwned::to_owned)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for Cow<'_, str> {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<&str as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<&str as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Cow<'_, str> {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
match self {
|
||||
Cow::Borrowed(str) => <&str as Encode<MySql>>::encode(*str, buf),
|
||||
Cow::Owned(str) => <&str as Encode<MySql>>::encode(&**str, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for Cow<'r, str> {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
value.as_str().map(Cow::Borrowed)
|
||||
}
|
||||
}
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueRef};
|
||||
use sqlx_core::decode::Decode;
|
||||
use sqlx_core::encode::{Encode, IsNull};
|
||||
use sqlx_core::error::BoxDynError;
|
||||
use sqlx_core::types::{Text, Type};
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
impl<T> Type<MySql> for Text<T> {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<String as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<String as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'q, T> Encode<'q, MySql> for Text<T>
|
||||
where
|
||||
T: Display,
|
||||
{
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
// We can't really do the trick like with Postgres where we reserve the space for the
|
||||
// length up-front and then overwrite it later, because MySQL appears to enforce that
|
||||
// length-encoded integers use the smallest encoding for the value:
|
||||
// https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_basic_dt_integers.html#sect_protocol_basic_dt_int_le
|
||||
//
|
||||
// So we'd have to reserve space for the max-width encoding, format into the buffer,
|
||||
// then figure out how many bytes our length-encoded integer needs to be and move the
|
||||
// value bytes down to use up the empty space.
|
||||
//
|
||||
// Copying from a completely separate buffer instead is easier. It may or may not be faster
|
||||
// or slower depending on a ton of different variables, but I don't currently have the time
|
||||
// to implement both approaches and compare their performance.
|
||||
Encode::<MySql>::encode(self.0.to_string(), buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, T> Decode<'r, MySql> for Text<T>
|
||||
where
|
||||
T: FromStr,
|
||||
BoxDynError: From<<T as FromStr>::Err>,
|
||||
{
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
let s: &str = Decode::<MySql>::decode(value)?;
|
||||
Ok(Self(s.parse()?))
|
||||
}
|
||||
}
|
||||
Vendored
+337
@@ -0,0 +1,337 @@
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
use bytes::Buf;
|
||||
use sqlx_core::database::Database;
|
||||
use time::macros::format_description;
|
||||
use time::{Date, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset};
|
||||
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::{BoxDynError, UnexpectedNullError};
|
||||
use crate::protocol::text::ColumnType;
|
||||
use crate::type_info::MySqlTypeInfo;
|
||||
use crate::types::{MySqlTime, MySqlTimeSign, Type};
|
||||
use crate::{MySql, MySqlValueFormat, MySqlValueRef};
|
||||
|
||||
impl Type<MySql> for OffsetDateTime {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Timestamp)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
matches!(ty.r#type, ColumnType::Datetime | ColumnType::Timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for OffsetDateTime {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
let utc_dt = self.to_offset(UtcOffset::UTC);
|
||||
let primitive_dt = PrimitiveDateTime::new(utc_dt.date(), utc_dt.time());
|
||||
|
||||
Encode::<MySql>::encode(primitive_dt, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for OffsetDateTime {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
let primitive: PrimitiveDateTime = Decode::<MySql>::decode(value)?;
|
||||
|
||||
Ok(primitive.assume_utc())
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for Time {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Time)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Time {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
let len = time_encoded_len(self);
|
||||
buf.push(len);
|
||||
|
||||
// sign byte: Time is never negative
|
||||
buf.push(0);
|
||||
|
||||
// Number of days in the interval; always 0 for time-of-day values.
|
||||
// https://mariadb.com/kb/en/resultset-row/#teimstamp-binary-encoding
|
||||
buf.extend_from_slice(&[0_u8; 4]);
|
||||
|
||||
encode_time(self, len > 8, buf);
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> usize {
|
||||
time_encoded_len(self) as usize + 1 // plus length byte
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for Time {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
MySqlValueFormat::Binary => {
|
||||
// Should never panic.
|
||||
MySqlTime::decode(value)?.try_into()
|
||||
}
|
||||
|
||||
// Retaining this parsing for now as it allows us to cross-check our impl.
|
||||
MySqlValueFormat::Text => Time::parse(
|
||||
value.as_str()?,
|
||||
&format_description!("[hour]:[minute]:[second].[subsecond]"),
|
||||
)
|
||||
.map_err(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<MySqlTime> for Time {
|
||||
type Error = BoxDynError;
|
||||
|
||||
fn try_from(time: MySqlTime) -> Result<Self, Self::Error> {
|
||||
if !time.is_valid_time_of_day() {
|
||||
return Err(format!("MySqlTime value out of range for `time::Time`: {time}").into());
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
Ok(Time::from_hms_micro(
|
||||
// `is_valid_time_of_day()` ensures this won't overflow
|
||||
time.hours() as u8,
|
||||
time.minutes(),
|
||||
time.seconds(),
|
||||
time.microseconds(),
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MySqlTime> for time::Duration {
|
||||
fn from(time: MySqlTime) -> Self {
|
||||
// `subsec_nanos()` is guaranteed to be between 0 and 10^9
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
time::Duration::new(time.whole_seconds_signed(), time.subsec_nanos() as i32)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<time::Duration> for MySqlTime {
|
||||
type Error = BoxDynError;
|
||||
|
||||
fn try_from(value: time::Duration) -> Result<Self, Self::Error> {
|
||||
let sign = if value.is_negative() {
|
||||
MySqlTimeSign::Negative
|
||||
} else {
|
||||
MySqlTimeSign::Positive
|
||||
};
|
||||
|
||||
// Similar to `TryFrom<chrono::TimeDelta>`, use `std::time::Duration` as an intermediate.
|
||||
Ok(MySqlTime::try_from(std::time::Duration::try_from(value.abs())?)?.with_sign(sign))
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for time::Duration {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTime::type_info()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for time::Duration {
|
||||
fn decode(value: <MySql as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
Ok(MySqlTime::decode(value)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for Date {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Date)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Date {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.push(4);
|
||||
|
||||
encode_date(self, buf)?;
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> usize {
|
||||
5
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for Date {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
MySqlValueFormat::Binary => {
|
||||
let buf = value.as_bytes()?;
|
||||
|
||||
// Row decoding should leave the length byte on the front.
|
||||
if buf.is_empty() {
|
||||
return Err("empty buffer".into());
|
||||
}
|
||||
|
||||
Ok(decode_date(&buf[1..])?.ok_or(UnexpectedNullError)?)
|
||||
}
|
||||
MySqlValueFormat::Text => {
|
||||
let s = value.as_str()?;
|
||||
Date::parse(s, &format_description!("[year]-[month]-[day]")).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for PrimitiveDateTime {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
MySqlTypeInfo::binary(ColumnType::Datetime)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for PrimitiveDateTime {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
let len = primitive_dt_encoded_len(self);
|
||||
buf.push(len);
|
||||
|
||||
encode_date(&self.date(), buf)?;
|
||||
|
||||
if len > 4 {
|
||||
encode_time(&self.time(), len > 7, buf);
|
||||
}
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> usize {
|
||||
primitive_dt_encoded_len(self) as usize + 1 // plus length byte
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> Decode<'r, MySql> for PrimitiveDateTime {
|
||||
fn decode(value: MySqlValueRef<'r>) -> Result<Self, BoxDynError> {
|
||||
match value.format() {
|
||||
MySqlValueFormat::Binary => {
|
||||
let mut buf = value.as_bytes()?;
|
||||
|
||||
if buf.is_empty() {
|
||||
return Err("empty buffer".into());
|
||||
}
|
||||
|
||||
let len = buf.get_u8();
|
||||
|
||||
let date = decode_date(buf)?.ok_or(UnexpectedNullError)?;
|
||||
|
||||
let dt = if len > 4 {
|
||||
date.with_time(decode_time(&buf[4..])?)
|
||||
} else {
|
||||
date.midnight()
|
||||
};
|
||||
|
||||
Ok(dt)
|
||||
}
|
||||
|
||||
MySqlValueFormat::Text => {
|
||||
let s = value.as_str()?;
|
||||
|
||||
// If there are no nanoseconds parse without them
|
||||
if s.contains('.') {
|
||||
PrimitiveDateTime::parse(
|
||||
s,
|
||||
&format_description!(
|
||||
"[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]"
|
||||
),
|
||||
)
|
||||
.map_err(Into::into)
|
||||
} else {
|
||||
PrimitiveDateTime::parse(
|
||||
s,
|
||||
&format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"),
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_date(date: &Date, buf: &mut Vec<u8>) -> Result<(), BoxDynError> {
|
||||
// MySQL supports years from 1000 - 9999
|
||||
let year =
|
||||
u16::try_from(date.year()).map_err(|_| format!("Date out of range for Mysql: {date}"))?;
|
||||
|
||||
buf.extend_from_slice(&year.to_le_bytes());
|
||||
buf.push(date.month().into());
|
||||
buf.push(date.day());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_date(buf: &[u8]) -> Result<Option<Date>, BoxDynError> {
|
||||
if buf.is_empty() {
|
||||
// zero buffer means a zero date (null)
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Date::from_calendar_date(
|
||||
LittleEndian::read_u16(buf) as i32,
|
||||
time::Month::try_from(buf[2])?,
|
||||
buf[3],
|
||||
)
|
||||
.map_err(Into::into)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn encode_time(time: &Time, include_micros: bool, buf: &mut Vec<u8>) {
|
||||
buf.push(time.hour());
|
||||
buf.push(time.minute());
|
||||
buf.push(time.second());
|
||||
|
||||
if include_micros {
|
||||
buf.extend(&(time.nanosecond() / 1000).to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_time(mut buf: &[u8]) -> Result<Time, BoxDynError> {
|
||||
let hour = buf.get_u8();
|
||||
let minute = buf.get_u8();
|
||||
let seconds = buf.get_u8();
|
||||
|
||||
let micros = if !buf.is_empty() {
|
||||
// microseconds : int<EOF>
|
||||
buf.get_uint_le(buf.len())
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let micros = u32::try_from(micros)
|
||||
.map_err(|_| format!("MySQL returned microseconds out of range: {micros}"))?;
|
||||
|
||||
Time::from_hms_micro(hour, minute, seconds, micros)
|
||||
.map_err(|e| format!("Time out of range for MySQL: {e}").into())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn primitive_dt_encoded_len(time: &PrimitiveDateTime) -> u8 {
|
||||
// to save space the packet can be compressed:
|
||||
match (time.hour(), time.minute(), time.second(), time.nanosecond()) {
|
||||
// if hour, minutes, seconds and micro_seconds are all 0,
|
||||
// length is 4 and no other field is sent
|
||||
(0, 0, 0, 0) => 4,
|
||||
|
||||
// if micro_seconds is 0, length is 7
|
||||
// and micro_seconds is not sent
|
||||
(_, _, _, 0) => 7,
|
||||
|
||||
// otherwise length is 11
|
||||
(_, _, _, _) => 11,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn time_encoded_len(time: &Time) -> u8 {
|
||||
if time.nanosecond() == 0 {
|
||||
// if micro_seconds is 0, length is 8 and micro_seconds is not sent
|
||||
8
|
||||
} else {
|
||||
// otherwise length is 12
|
||||
12
|
||||
}
|
||||
}
|
||||
Vendored
+162
@@ -0,0 +1,162 @@
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::protocol::text::{ColumnFlags, ColumnType};
|
||||
use crate::types::Type;
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueFormat, MySqlValueRef};
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
|
||||
fn uint_type_info(ty: ColumnType) -> MySqlTypeInfo {
|
||||
MySqlTypeInfo {
|
||||
r#type: ty,
|
||||
flags: ColumnFlags::BINARY | ColumnFlags::UNSIGNED,
|
||||
max_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn uint_compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
matches!(
|
||||
ty.r#type,
|
||||
ColumnType::Tiny
|
||||
| ColumnType::Short
|
||||
| ColumnType::Long
|
||||
| ColumnType::Int24
|
||||
| ColumnType::LongLong
|
||||
| ColumnType::Year
|
||||
| ColumnType::Bit
|
||||
) && ty.flags.contains(ColumnFlags::UNSIGNED)
|
||||
}
|
||||
|
||||
impl Type<MySql> for u8 {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
uint_type_info(ColumnType::Tiny)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
uint_compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for u16 {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
uint_type_info(ColumnType::Short)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
uint_compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for u32 {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
uint_type_info(ColumnType::Long)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
uint_compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for u64 {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
uint_type_info(ColumnType::LongLong)
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
uint_compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for u8 {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.extend(&self.to_le_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for u16 {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.extend(&self.to_le_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for u32 {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.extend(&self.to_le_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for u64 {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.extend(&self.to_le_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
fn uint_decode(value: MySqlValueRef<'_>) -> Result<u64, BoxDynError> {
|
||||
if value.type_info.r#type == ColumnType::Bit {
|
||||
// NOTE: Regardless of the value format, there is raw binary data here
|
||||
|
||||
let buf = value.as_bytes()?;
|
||||
let mut value: u64 = 0;
|
||||
|
||||
for b in buf {
|
||||
value = (*b as u64) | (value << 8);
|
||||
}
|
||||
|
||||
return Ok(value);
|
||||
}
|
||||
|
||||
Ok(match value.format() {
|
||||
MySqlValueFormat::Text => value.as_str()?.parse()?,
|
||||
|
||||
MySqlValueFormat::Binary => {
|
||||
let buf = value.as_bytes()?;
|
||||
|
||||
// Check conditions that could cause `read_uint()` to panic.
|
||||
if buf.is_empty() {
|
||||
return Err("empty buffer".into());
|
||||
}
|
||||
|
||||
if buf.len() > 8 {
|
||||
return Err(format!(
|
||||
"expected no more than 8 bytes for unsigned integer value, got {}",
|
||||
buf.len()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
LittleEndian::read_uint(buf, buf.len())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for u8 {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
uint_decode(value)?.try_into().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for u16 {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
uint_decode(value)?.try_into().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for u32 {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
uint_decode(value)?.try_into().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for u64 {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
uint_decode(value)
|
||||
}
|
||||
}
|
||||
Vendored
+108
@@ -0,0 +1,108 @@
|
||||
use uuid::{
|
||||
fmt::{Hyphenated, Simple},
|
||||
Uuid,
|
||||
};
|
||||
|
||||
use crate::decode::Decode;
|
||||
use crate::encode::{Encode, IsNull};
|
||||
use crate::error::BoxDynError;
|
||||
use crate::io::MySqlBufMutExt;
|
||||
use crate::types::Type;
|
||||
use crate::{MySql, MySqlTypeInfo, MySqlValueRef};
|
||||
|
||||
impl Type<MySql> for Uuid {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<&[u8] as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<&[u8] as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Uuid {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.put_bytes_lenenc(self.as_bytes());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for Uuid {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
// delegate to the &[u8] type to decode from MySQL
|
||||
let bytes = <&[u8] as Decode<MySql>>::decode(value)?;
|
||||
|
||||
if bytes.len() != 16 {
|
||||
return Err(format!(
|
||||
"Expected 16 bytes, got {}; `Uuid` uses binary format for MySQL/MariaDB. \
|
||||
For text-formatted UUIDs, use `uuid::fmt::Hyphenated` instead of `Uuid`.",
|
||||
bytes.len(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// construct a Uuid from the returned bytes
|
||||
Uuid::from_slice(bytes).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for Hyphenated {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<&str as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<&str as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Hyphenated {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.put_str_lenenc(&self.to_string());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for Hyphenated {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
// delegate to the &str type to decode from MySQL
|
||||
let text = <&str as Decode<MySql>>::decode(value)?;
|
||||
|
||||
// parse a UUID from the text
|
||||
Uuid::parse_str(text)
|
||||
.map_err(Into::into)
|
||||
.map(|u| u.hyphenated())
|
||||
}
|
||||
}
|
||||
|
||||
impl Type<MySql> for Simple {
|
||||
fn type_info() -> MySqlTypeInfo {
|
||||
<&str as Type<MySql>>::type_info()
|
||||
}
|
||||
|
||||
fn compatible(ty: &MySqlTypeInfo) -> bool {
|
||||
<&str as Type<MySql>>::compatible(ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode<'_, MySql> for Simple {
|
||||
fn encode_by_ref(&self, buf: &mut Vec<u8>) -> Result<IsNull, BoxDynError> {
|
||||
buf.put_str_lenenc(&self.to_string());
|
||||
|
||||
Ok(IsNull::No)
|
||||
}
|
||||
}
|
||||
|
||||
impl Decode<'_, MySql> for Simple {
|
||||
fn decode(value: MySqlValueRef<'_>) -> Result<Self, BoxDynError> {
|
||||
// delegate to the &str type to decode from MySQL
|
||||
let text = <&str as Decode<MySql>>::decode(value)?;
|
||||
|
||||
// parse a UUID from the text
|
||||
Uuid::parse_str(text)
|
||||
.map_err(Into::into)
|
||||
.map(|u| u.simple())
|
||||
}
|
||||
}
|
||||
Vendored
+115
@@ -0,0 +1,115 @@
|
||||
use std::borrow::Cow;
|
||||
use std::str::from_utf8;
|
||||
|
||||
use bytes::Bytes;
|
||||
pub(crate) use sqlx_core::value::*;
|
||||
|
||||
use crate::error::{BoxDynError, UnexpectedNullError};
|
||||
use crate::protocol::text::ColumnType;
|
||||
use crate::{MySql, MySqlTypeInfo};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum MySqlValueFormat {
|
||||
Text,
|
||||
Binary,
|
||||
}
|
||||
|
||||
/// Implementation of [`Value`] for MySQL.
|
||||
#[derive(Clone)]
|
||||
pub struct MySqlValue {
|
||||
value: Option<Bytes>,
|
||||
type_info: MySqlTypeInfo,
|
||||
format: MySqlValueFormat,
|
||||
}
|
||||
|
||||
/// Implementation of [`ValueRef`] for MySQL.
|
||||
#[derive(Clone)]
|
||||
pub struct MySqlValueRef<'r> {
|
||||
pub(crate) value: Option<&'r [u8]>,
|
||||
pub(crate) row: Option<&'r Bytes>,
|
||||
pub(crate) type_info: MySqlTypeInfo,
|
||||
pub(crate) format: MySqlValueFormat,
|
||||
}
|
||||
|
||||
impl<'r> MySqlValueRef<'r> {
|
||||
pub(crate) fn format(&self) -> MySqlValueFormat {
|
||||
self.format
|
||||
}
|
||||
|
||||
pub(crate) fn as_bytes(&self) -> Result<&'r [u8], BoxDynError> {
|
||||
match &self.value {
|
||||
Some(v) => Ok(v),
|
||||
None => Err(UnexpectedNullError.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_str(&self) -> Result<&'r str, BoxDynError> {
|
||||
Ok(from_utf8(self.as_bytes()?)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Value for MySqlValue {
|
||||
type Database = MySql;
|
||||
|
||||
fn as_ref(&self) -> MySqlValueRef<'_> {
|
||||
MySqlValueRef {
|
||||
value: self.value.as_deref(),
|
||||
row: None,
|
||||
type_info: self.type_info.clone(),
|
||||
format: self.format,
|
||||
}
|
||||
}
|
||||
|
||||
fn type_info(&self) -> Cow<'_, MySqlTypeInfo> {
|
||||
Cow::Borrowed(&self.type_info)
|
||||
}
|
||||
|
||||
fn is_null(&self) -> bool {
|
||||
is_null(self.value.as_deref(), &self.type_info)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r> ValueRef<'r> for MySqlValueRef<'r> {
|
||||
type Database = MySql;
|
||||
|
||||
fn to_owned(&self) -> MySqlValue {
|
||||
let value = match (self.row, self.value) {
|
||||
(Some(row), Some(value)) => Some(row.slice_ref(value)),
|
||||
|
||||
(None, Some(value)) => Some(Bytes::copy_from_slice(value)),
|
||||
|
||||
_ => None,
|
||||
};
|
||||
|
||||
MySqlValue {
|
||||
value,
|
||||
format: self.format,
|
||||
type_info: self.type_info.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn type_info(&self) -> Cow<'_, MySqlTypeInfo> {
|
||||
Cow::Borrowed(&self.type_info)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_null(&self) -> bool {
|
||||
is_null(self.value, &self.type_info)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_null(value: Option<&[u8]>, ty: &MySqlTypeInfo) -> bool {
|
||||
if let Some(value) = value {
|
||||
// zero dates and date times should be treated the same as NULL
|
||||
if matches!(
|
||||
ty.r#type,
|
||||
ColumnType::Date | ColumnType::Timestamp | ColumnType::Datetime
|
||||
) && value.starts_with(b"\0")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value.is_none()
|
||||
}
|
||||
Reference in New Issue
Block a user