diff --git a/Cargo.toml b/Cargo.toml index d4471a8..cd75bc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "mysql" version = "17.0.0" authors = ["blackbeam"] description = "Mysql client library implemented in rust" -license = "MIT" +license = "MIT OR APACHE" documentation = "https://docs.rs/mysql" repository = "https://github.com/blackbeam/rust-mysql-simple" keywords = ["database", "sql"] @@ -12,27 +12,16 @@ categories = ["database"] edition = "2018" build = "build.rs" +[badges] +appveyor = { repository = "blackbeam/rust-mysql-simple", branch = "master", service = "github" } +travis-ci = { repository = "blackbeam/rust-mysql-simple", branch = "master" } + [lib] name = "mysql" path = "src/lib.rs" -[profile.dev] -opt-level = 0 -debug = true - -[profile.release] -opt-level = 3 -debug = false -lto = true - -[profile.test] -opt-level = 0 -debug = true - [profile.bench] opt-level = 3 -debug = false -lto = true [features] nightly = [] @@ -44,9 +33,9 @@ serde_derive = "1" [dependencies] bufstream = "~0.1" -fnv = "1" io-enum = "0.2.1" -mysql_common = "0.19.2" +lru = "0.4.3" +mysql_common = "0.20.1" native-tls = "0.2.3" net2 = "~0.2" percent-encoding = "2.1.0" @@ -61,4 +50,4 @@ winapi = "~0.3" [target.'cfg(unix)'.dependencies] libc = "0.2" -nix = "0.15.0" +nix = "0.17.0" diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..f8e5e5e --- /dev/null +++ b/LICENSE-APACHE @@ -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 [yyyy] [name of copyright owner] + +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. \ No newline at end of file diff --git a/LICENSE b/LICENSE-MIT similarity index 95% rename from LICENSE rename to LICENSE-MIT index 1a92cb8..e61431f 100644 --- a/LICENSE +++ b/LICENSE-MIT @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014-2015 Anatoly Ikorsky +Copyright (c) 2020 rust-mysql-common contributors 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 diff --git a/README.md b/README.md index fad55d3..b0e48db 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,617 @@ -rust-mysql-simple -================= -[![Build Status](https://travis-ci.org/blackbeam/rust-mysql-simple.png?branch=master)](https://travis-ci.org/blackbeam/rust-mysql-simple) [![Build status](https://ci.appveyor.com/api/projects/status/4te7c9q4tlmwvof0/branch/master?svg=true)](https://ci.appveyor.com/project/blackbeam/rust-mysql-simple/branch/master) +[![Crates.io](https://img.shields.io/crates/v/mysql.svg)](https://crates.io/crates/mysql) +[![Build Status](https://ci.appveyor.com/api/projects/status/github/blackbeam/rust-mysql-simple?branch=master&svg=true)](https://ci.appveyor.com/project/blackbeam/rust-mysql-simple/branch/master) +[![Build Status](https://travis-ci.org/blackbeam/rust-mysql-simple.svg?branch=master)](https://travis-ci.org/blackbeam/rust-mysql-simple) -Mysql client library implemented in rust. Feel free to open new issues and pull requests. +# mysql -### Changelog -Available [here](https://github.com/blackbeam/rust-mysql-simple/releases) +This create offers: + +* MySql database driver in pure rust; +* connection pool. + +Features: -### Documentation -Latest crate API docs are hosted on [docs.rs](https://docs.rs/crate/mysql). +* macOS, Windows and Linux support; +* TLS support via **nativetls** create; +* MySql text protocol support, i.e. support of simple text queries and text result sets; +* MySql binary protocol support, i.e. support of prepared statements and binary result sets; +* support of multi-result sets; +* support of named parameters for prepared statements; +* optional per-connection cache of prepared statements; +* support of MySql packets larger than 2^24; +* support of Unix sockets and Windows named pipes; +* support of custom LOCAL INFILE handlers; +* support of MySql protocol compression; +* support of auth plugins: + * **mysql_native_password** - for MySql prior to v8; + * **caching_sha2_password** - for MySql v8 and higher. ### Installation -Please use [crates.io](https://crates.io/crates/mysql) + +Put the desired version of the crate into the `dependencies` section of your `Cargo.toml`: ```toml [dependencies] mysql = "*" ``` -### SSL Support +### Example -rust-mysql-simple offers support for SSL via `ssl` cargo feature which is disabled by default. -Add `ssl` feature to enable: +```rust +use mysql::*; +use mysql::prelude::*; -```toml -[dependencies.mysql] -version = "*" -features = ["ssl"] +#[derive(Debug, PartialEq, Eq)] +struct Payment { + customer_id: i32, + amount: i32, + account_name: Option, +} + +let url = "mysql://root:password@localhost:3307/db_name"; + +let pool = Pool::new(url)?; + +let mut conn = pool.get_conn()?; + +// Let's create a table for payments. +conn.query_drop( + r"CREATE TEMPORARY TABLE payment ( + customer_id int not null, + amount int not null, + account_name text + )")?; + +let payments = vec![ + Payment { customer_id: 1, amount: 2, account_name: None }, + Payment { customer_id: 3, amount: 4, account_name: Some("foo".into()) }, + Payment { customer_id: 5, amount: 6, account_name: None }, + Payment { customer_id: 7, amount: 8, account_name: None }, + Payment { customer_id: 9, amount: 10, account_name: Some("bar".into()) }, +]; + +// Now let's insert payments to the database +conn.exec_batch( + r"INSERT INTO payment (customer_id, amount, account_name) + VALUES (:customer_id, :amount, :account_name)", + payments.iter().map(|p| params! { + "customer_id" => p.customer_id, + "amount" => p.amount, + "account_name" => &p.account_name, + }) +)?; + +// Let's select payments from database. Type inference should do the trick here. +let selected_payments = conn + .query_map( + "SELECT customer_id, amount, account_name from payment", + |(customer_id, amount, account_name)| { + Payment { customer_id, amount, account_name } + }, + )?; + +// Let's make sure, that `payments` equals to `selected_payments`. +// Mysql gives no guaranties on order of returned rows +// without `ORDER BY`, so assume we are lucky. +assert_eq!(payments, selected_payments); +println!("Yay!"); ``` -### JSON Support +### API Documentation -rust-mysql-simple offers [JSON](https://dev.mysql.com/doc/refman/5.7/en/json.html) support -based on *serde*, but you can switch to *rustc-serialize* using `rustc-serialize` feature: +Please refer to the [crate docs]. -```toml -[dependencies.mysql] -version = "*" -features = ["rustc-serialize"] +### Basic structures + +#### `Opts` + +This structure holds server host name, client username/password and other settings, +that controls client behavior. + +##### URL-based connection string + +Note, that you can use URL-based connection string as a source of an `Opts` instance. +URL schema must be `mysql`. Host, port and credentials, as well as query parameters, +should be given in accordance with the RFC 3986. + +Examples: + +```rust +let _ = Opts::from_url("mysql://localhost/some_db")?; +let _ = Opts::from_url("mysql://[::1]/some_db")?; +let _ = Opts::from_url("mysql://user:pass%20word@127.0.0.1:3307/some_db?")?; +``` + +Supported URL parameters (for the meaning of each field please refer to the docs on `Opts` +structure in the create API docs): + +* `prefer_socket: true | false` - defines the value of the same field in the `Opts` structure; +* `tcp_keepalive_time_ms: u32` - defines the value (in milliseconds) + of the `tcp_keepalive_time` field in the `Opts` structure; +* `tcp_connect_timeout_ms: u64` - defines the value (in milliseconds) + of the `tcp_connect_timeout` field in the `Opts` structure; +* `stmt_cache_size: u32` - defines the value of the same field in the `Opts` structure; +* `compress` - defines the value of the same field in the `Opts` structure. + Supported value are: + * `true` - enables compression with the default compression level; + * `fast` - enables compression with "fast" compression level; + * `best` - enables compression with "best" compression level; + * `1`..`9` - enables compression with the given compression level. +* `socket` - socket path on UNIX, or pipe name on Windows. + +#### `OptsBuilder` + +It's a convenient builder for the `Opts` structure. It defines setters for fields +of the `Opts` structure. + +```rust +let opts = OptsBuilder::new() + .user(Some("foo")) + .db_name(Some("bar")); +let _ = Conn::new(opts)?; +``` + +#### `Conn` + +This structure represents an active MySql connection. It also holds statement cache +and metadata for the last result set. + +#### `Transaction` + +It's a simple wrapper on top of a routine, that starts with `START TRANSACTION` +and ends with `COMMIT` or `ROLBACK`. + +```rust +use mysql::*; +use mysql::prelude::*; + +let pool = Pool::new(get_opts())?; +let mut conn = pool.get_conn()?; + +let mut tx = conn.start_transaction(false, None, None)?; +tx.query_drop("CREATE TEMPORARY TABLE tmp (TEXT a)")?; +tx.exec_drop("INSERT INTO tmp (a) VALUES (?)", ("foo",))?; +let val: Option = tx.query_first("SELECT a from tmp")?; +assert_eq!(val.unwrap(), "foo"); +// Note, that transaction will be rolled back implicitly on Drop, if not committed. +tx.rollback(); + +let val: Option = conn.query_first("SELECT a from tmp")?; +assert_eq!(val, None); +``` + +#### `Pool` + +It's a reference to a connection pool, that can be cloned and shared between threads. + +```rust +use mysql::*; +use mysql::prelude::*; + +use std::thread::spawn; + +let pool = Pool::new(get_opts())?; + +let handles = (0..4).map(|i| { + spawn({ + let pool = pool.clone(); + move || { + let mut conn = pool.get_conn()?; + conn.exec_first::("SELECT ? * 10", (i,)) + .map(Option::unwrap) + } + }) +}); + +let result: Result> = handles.map(|handle| handle.join().unwrap()).collect(); + +assert_eq!(result.unwrap(), vec![0, 10, 20, 30]); +``` + +#### `Statement` + +Statement, actually, is just an identifier coupled with statement metadata, i.e an information +about its parameters and columns. Internally the `Statement` structure also holds additional +data required to support named parameters (see bellow). + +```rust +use mysql::*; +use mysql::prelude::*; + +let pool = Pool::new(get_opts())?; +let mut conn = pool.get_conn()?; + +let stmt = conn.prep("DO ?")?; + +// The prepared statement will return no columns. +assert!(stmt.columns().is_empty()); + +// The prepared statement have one parameter. +let param = stmt.params().get(0).unwrap(); +assert_eq!(param.schema_str(), ""); +assert_eq!(param.table_str(), ""); +assert_eq!(param.name_str(), "?"); +``` + +#### `Value` + +This enumeration represents the raw value of a MySql cell. Library offers conversion between +`Value` and different rust types via `FromValue` trait described below. + +##### `FromValue` trait + +This trait is reexported from **mysql_common** create. Please refer to its +[crate docs][mysql_common docs] for the list of supported conversions. + +Trait offers conversion in two flavours: + +* `from_value(Value) -> T` - convenient, but panicking conversion. + + Note, that for any variant of `Value` there exist a type, that fully covers its domain, + i.e. for any variant of `Value` there exist `T: FromValue` such that `from_value` will never + panic. This means, that if your database schema is known, than it's possible to write your + application using only `from_value` with no fear of runtime panic. + +* `from_value_opt(Value) -> Option` - non-panicking, but less convenient conversion. + + This function is useful to probe conversion in cases, where source database schema + is unknown. + +```rust +use mysql::*; +use mysql::prelude::*; + +let via_test_protocol: u32 = from_value(Value::Bytes(b"65536".to_vec())); +let via_bin_protocol: u32 = from_value(Value::UInt(65536)); +assert_eq!(via_test_protocol, via_bin_protocol); + +let unknown_val = // ... + +// Maybe it is a float? +let unknown_val = match from_value_opt::(unknown_val) { + Ok(float) => { + println!("A float value: {}", float); + return Ok(()); + } + Err(FromValueError(unknown_val)) => unknown_val, +}; + +// Or a string? +let unknown_val = match from_value_opt::(unknown_val) { + Ok(string) => { + println!("A string value: {}", string); + return Ok(()); + } + Err(FromValueError(unknown_val)) => unknown_val, +}; + +// Screw this, I'll simply match on it +match unknown_val { + val @ Value::NULL => { + println!("An empty value: {:?}", from_value::>(val)) + }, + val @ Value::Bytes(..) => { + // It's non-utf8 bytes, since we already tried to convert it to String + println!("Bytes: {:?}", from_value::>(val)) + } + val @ Value::Int(..) => { + println!("A signed integer: {}", from_value::(val)) + } + val @ Value::UInt(..) => { + println!("An unsigned integer: {}", from_value::(val)) + } + Value::Float(..) => unreachable!("already tried"), + val @ Value::Date(..) => { + use mysql::chrono::NaiveDateTime; + println!("A date value: {}", from_value::(val)) + } + val @ Value::Time(..) => { + use std::time::Duration; + println!("A time value: {:?}", from_value::(val)) + } +} +``` + +#### `Row` + +Internally `Row` is a vector of `Value`s, that also allows indexing by a column name/offset, +and stores row metadata. Library offers conversion between `Row` and sequences of Rust types +via `FromRow` trait described below. + +##### `FromRow` trait + +This trait is reexported from **mysql_common** create. Please refer to its +[crate docs][mysql_common docs] for the list of supported conversions. + +This conversion is based on the `FromValue` and so comes in two similar flavours: + +* `from_row(Row) -> T` - same as `from_value`, but for rows; +* `from_row_opt(Row) -> Option` - same as `from_value_opt`, but for rows. + +[`Queryable`][#queryable] trait offers implicit conversion for rows of a query result, +that is based on this trait. + +```rust +use mysql::*; +use mysql::prelude::*; + +let mut conn = Conn::new(get_opts())?; + +// Single-column row can be converted to a singular value +let val: Option = conn.query_first("SELECT 'foo'")?; +assert_eq!(val.unwrap(), "foo"); + +// Example of a mutli-column row conversion to an inferred type. +let row = conn.query_first("SELECT 255, 256")?; +assert_eq!(row, Some((255u8, 256u16))); + +// Some unknown row +let row: Row = conn.query_first( + // ... + # "SELECT 255, Null", +)?.unwrap(); + +for column in row.columns_ref() { + let column_value = &row[column.name_str().as_ref()]; + println!( + "Column {} of type {:?} with value {:?}", + column.name_str(), + column.column_type(), + column_value, + ); +} ``` -### Windows support (since 0.18.0) -Windows is supported but currently rust-mysql-simple has no support for SSL on Windows. + +#### `Params` + +Represents parameters of a prepared statement, but this type won't appear directly in your code +because binary protocol API will ask for `T: Into`, where `Into` is implemented: + +* for tuples of `Into` types up to arity 12; + + **Note:** singular tuple requires extra comma, e.g. `("foo",)`; + +* for `IntoIterator>` for cases, when your statement takes more + than 12 parameters; +* for named parameters representation (the value of the `params!` macro, described below). + +```rust +use mysql::*; +use mysql::prelude::*; + +let mut conn = Conn::new(get_opts())?; + +// Singular tuple requires extra comma: +let row: Option = conn.exec_first("SELECT ?", (0,))?; +assert_eq!(row.unwrap(), 0); + +// More than 12 parameters: +let row: Option = conn.exec_first( + "SELECT ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ?", + (0..16).collect::>(), +)?; +assert_eq!(row.unwrap(), 120); +``` + +**Note:** Please refer to the [**mysql_common** crate docs][mysql_common docs] for the list +of types, that implements `Into`. + +##### `Serialized`, `Deserialized` + +Wrapper structures for cases, when you need to provide a value for a JSON cell, +or when you need to parse JSON cell as a struct. + +```rust +use mysql::*; +use mysql::prelude::*; + +/// Serializable structure. +#[derive(Debug, PartialEq, Serialize, Deserialize)] +struct Example { + foo: u32, +} + +// Value::from for Serialized will emit json string. +let value = Value::from(Serialized(Example { foo: 42 })); +assert_eq!(value, Value::Bytes(br#"{"foo":42}"#.to_vec())); + +// from_value for Deserialized will parse json string. +let structure: Deserialized = from_value(value); +assert_eq!(structure, Deserialized(Example { foo: 42 })); +``` + +#### `QueryResult` + +It's an iterator over rows of a query result with support of multi-result sets. It's intended +for cases when you need full control during result set iteration. For other cases `Conn` +provides a set of methods that will immediately consume the first result set and drop everything +else. + +This iterator is lazy so it won't read the result from server until you iterate over it. +MySql protocol is strictly sequential, so `Conn` will be mutably borrowed until the result +is fully consumed. + +```rust +use mysql::*; +use mysql::prelude::*; + +let mut conn = Conn::new(get_opts())?; + +// This query will emit two result sets. +let mut result = conn.query_iter("SELECT 1, 2; SELECT 3, 3.14;")?; + +let mut set = 0; +while result.more_results_exists() { + println!("Result set columns: {:?}", result.columns_ref()); + + for row in result.by_ref() { + match set { + 0 => { + // First result set will contain two numbers. + assert_eq!((1_u8, 2_u8), from_row(row?)); + } + 1 => { + // Second result set will contain a number and a float. + assert_eq!((3_u8, 3.14), from_row(row?)); + } + _ => unreachable!(), + } + } + set += 1; +} +``` + +### Text protocol + +MySql text protocol is implemented in the set of `Conn::query*` methods. It's useful when your +query doesn't have parameters. + +**Note:** All values of a text protocol result set will be encoded as strings by the server, +so `from_value` conversion may lead to additional parsing costs. + +Examples: + +```rust +let pool = Pool::new(get_opts())?; +let val = pool.get_conn()?.query_first("SELECT POW(2, 16)")?; + +// Text protocol returns bytes even though the result of POW +// is actually a floating point number. +assert_eq!(val, Some(Value::Bytes("65536".as_bytes().to_vec()))); +``` + +### Binary protocol and prepared statements. + +MySql binary protocol is implemented in `prep`, `close` and the set of `exec*` methods, +defined on the [`Queryable`](#queryable) trait. Prepared statements is the only way to +pass rust value to the MySql server. MySql uses `?` symbol as a parameter placeholder +and it's only possible to use parameters where a single MySql value is expected. +For example: + +```rust +let pool = Pool::new(get_opts())?; +let val = pool.get_conn()?.exec_first("SELECT POW(?, ?)", (2, 16))?; + +assert_eq!(val, Some(Value::Float(65536.0))); +``` + +#### Statements + +In MySql each prepared statement belongs to a particular connection and can't be executed +on another connection. Trying to do so will lead to an error. The driver won't tie statement +to a connection in any way, but one can look on to the connection id, that is stored +in the `Statement` structure. + +```rust +let pool = Pool::new(get_opts())?; + +let mut conn_1 = pool.get_conn()?; +let mut conn_2 = pool.get_conn()?; + +let stmt_1 = conn_1.prep("SELECT ?")?; + +// stmt_1 is for the conn_1, .. +assert!(stmt_1.connection_id() == conn_1.connection_id()); +assert!(stmt_1.connection_id() != conn_2.connection_id()); + +// .. so stmt_1 will execute only on conn_1 +assert!(conn_1.exec_drop(&stmt_1, ("foo",)).is_ok()); +assert!(conn_2.exec_drop(&stmt_1, ("foo",)).is_err()); +``` + +#### Statement cache + +`Conn` will manage the cache of prepared statements on the client side, so subsequent calls +to prepare with the same statement won't lead to a client-server roundtrip. Cache size +for each connection is determined by the `stmt_cache_size` field of the `Opts` structure. +Statements, that are out of this boundary will be closed in LRU order. + +Statement cache is completely disabled if `stmt_cache_size` is zero. + +**Caveats:** + +* disabled statement cache means, that you have to close statements yourself using + `Conn::close`, or they'll exhaust server limits/resources; + +* you should be aware of the [`max_prepared_stmt_count`][max_prepared_stmt_count] + option of the MySql server. If the number of active connections times the value + of `stmt_cache_size` is greater, than you could receive an error while prepareing + another statement. + +#### Named parameters + +MySql itself doesn't have named parameters support, so it's implemented on the client side. +One should use `:name` as a placeholder syntax for a named parameter. + +Named parameters may be repeated within the statement, e.g `SELECT :foo :foo` will require +a single named parameter `foo` that will be repeated on the corresponding positions during +statement execution. + +One should use the `params!` macro to build a parameters for execution. + +**Note:** Positional and named parameters can't be mixed within the single statement. + +Examples: + +```rust +let pool = Pool::new(get_opts())?; + +let mut conn = pool.get_conn()?; +let stmt = conn.prep("SELECT :foo, :bar, :foo")?; + +let foo = 42; + +let val_13 = conn.exec_first(&stmt, params! { "foo" => 13, "bar" => foo })?.unwrap(); +// Short syntax is available when param name is the same as variable name: +let val_42 = conn.exec_first(&stmt, params! { foo, "bar" => 13 })?.unwrap(); + +assert_eq!((foo, 13, foo), val_42); +assert_eq!((13, foo, 13), val_13); +``` + +#### `Queryable` + +The `Queryable` trait defines common methods for `Conn`, `PooledConn` and `Transaction`. +The set of basic methods consts of: + +* `query_iter` - basic methods to execute text query and get `QueryRestul`; +* `prep` - basic method to prepare a statement; +* `exec_iter` - basic method to execute statement and get `QueryResult`; +* `close` - basic method to close the statement; + +The trait also defines the set of helper methods, that is based on basic methods. +These methods will consume only the firt result set, other result sets will be dropped: + +* `{query|exec}` - to collect the result into a `Vec`; +* `{query|exec}_first` - to get the first `T: FromRow`, if any; +* `{query|exec}_map` - to map each `T: FromRow` to some `U`; +* `{query|exec}_fold` - to fold the set of `T: FromRow` to a single value; +* `{query|exec}_drop` - to immediately drop the result. + +The trait also defines additional helper for a batch statement execution. + +[crate docs]: https://docs.rs/mysql +[mysql_common docs]: https://docs.rs/mysql_common +[max_prepared_stmt_count]: https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_prepared_stmt_count + + +## Changelog + +Available [here](https://github.com/blackbeam/rust-mysql-simple/releases) + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/README.tpl b/README.tpl new file mode 100644 index 0000000..81425fa --- /dev/null +++ b/README.tpl @@ -0,0 +1,26 @@ +[![Crates.io](https://img.shields.io/crates/v/mysql.svg)](https://crates.io/crates/mysql) +{{badges}} + +# {{crate}} + +{{readme}} + +## Changelog + +Available [here](https://github.com/blackbeam/rust-mysql-simple/releases) + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 8139487..f540fdd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,13 +22,13 @@ environment: PATH: C:\msys64\mingw64\bin\;c:\rust\bin;%PATH% - TARGET: beta-i686-pc-windows-gnu PATH: C:\msys64\mingw32\bin\;c:\rust\bin;%PATH% - - TARGET: 1.38.0-x86_64-pc-windows-msvc + - TARGET: 1.41.0-x86_64-pc-windows-msvc PATH: C:\msys64\mingw64\bin\;c:\rust\bin;%PATH% - - TARGET: 1.38.0-i686-pc-windows-msvc + - TARGET: 1.41.0-i686-pc-windows-msvc PATH: C:\msys64\mingw32\bin\;c:\rust\bin;%PATH% - - TARGET: 1.38.0-x86_64-pc-windows-gnu + - TARGET: 1.41.0-x86_64-pc-windows-gnu PATH: C:\msys64\mingw64\bin\;c:\rust\bin;%PATH% - - TARGET: 1.38.0-i686-pc-windows-gnu + - TARGET: 1.41.0-i686-pc-windows-gnu PATH: C:\msys64\mingw32\bin\;c:\rust\bin;%PATH% COMPRESS: 1 services: mysql diff --git a/build.rs b/build.rs index dbbf25f..7fd933a 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,11 @@ +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + use std::env; fn main() { diff --git a/src/conn/local_infile.rs b/src/conn/local_infile.rs index 8a2b7fb..bac90c3 100644 --- a/src/conn/local_infile.rs +++ b/src/conn/local_infile.rs @@ -1,5 +1,15 @@ -use std::sync::{Arc, Mutex}; -use std::{fmt, io}; +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +use std::{ + fmt, io, + sync::{Arc, Mutex}, +}; use crate::Conn; @@ -22,8 +32,10 @@ pub(crate) type LocalInfileInner = /// # OptsBuilder, /// # LocalInfileHandler, /// # from_row, -/// # error::Error +/// # error::Error, +/// # prelude::*, /// # }; +/// use mysql::prelude::Queryable; /// # fn get_opts() -> Opts { /// # let url = if let Ok(url) = std::env::var("DATABASE_URL") { /// # let opts = Opts::from_url(&url).expect("DATABASE_URL invalid"); @@ -38,11 +50,11 @@ pub(crate) type LocalInfileInner = /// # } /// # let opts = get_opts(); /// # let pool = Pool::new_manual(1, 1, opts).unwrap(); -/// # pool.prep_exec("CREATE TEMPORARY TABLE mysql.Users (id INT, name TEXT, age INT, email TEXT)", ()).unwrap(); -/// # pool.prep_exec("INSERT INTO mysql.Users (id, name, age, email) VALUES (?, ?, ?, ?)", -/// # (1, "John", 17, "foo@bar.baz")).unwrap(); /// # let mut conn = pool.get_conn().unwrap(); -/// conn.query("CREATE TEMPORARY TABLE mysql.tbl(a TEXT)").unwrap(); +/// # conn.query_drop("CREATE TEMPORARY TABLE mysql.Users (id INT, name TEXT, age INT, email TEXT)").unwrap(); +/// # conn.exec_drop("INSERT INTO mysql.Users (id, name, age, email) VALUES (?, ?, ?, ?)", +/// # (1, "John", 17, "foo@bar.baz")).unwrap(); +/// conn.query_drop("CREATE TEMPORARY TABLE mysql.tbl(a TEXT)").unwrap(); /// /// conn.set_local_infile_handler(Some( /// LocalInfileHandler::new(|file_name, writer| { @@ -54,29 +66,23 @@ pub(crate) type LocalInfileInner = /// }) /// )); /// -/// match conn.query("LOAD DATA LOCAL INFILE 'file_name' INTO TABLE mysql.tbl") { +/// match conn.query_drop("LOAD DATA LOCAL INFILE 'file_name' INTO TABLE mysql.tbl") { /// Ok(_) => (), /// Err(Error::MySqlError(ref e)) if e.code == 1148 => { /// // functionality is not supported by the server /// return; -/// }, +/// } /// err => { /// err.unwrap(); -/// }, -/// } -/// -/// let mut row_num = 0; -/// for (row_idx, row) in conn.query("SELECT * FROM mysql.tbl").unwrap().enumerate() { -/// row_num = row_idx + 1; -/// let row: (String,) = from_row(row.unwrap()); -/// match row_num { -/// 1 => assert_eq!(row.0, "row1: file name is file_name"), -/// 2 => assert_eq!(row.0, "row2: foobar"), -/// _ => unreachable!(), /// } /// } /// -/// assert_eq!(row_num, 2); +/// let mut row_num = 0; +/// let result: Vec = conn.query("SELECT * FROM mysql.tbl").unwrap(); +/// assert_eq!( +/// result, +/// vec!["row1: file name is file_name".to_string(), "row2: foobar".to_string()], +/// ); /// ``` #[derive(Clone)] pub struct LocalInfileHandler(pub(crate) LocalInfileInner); diff --git a/src/conn/mod.rs b/src/conn/mod.rs index 7bb6c2f..dbaf7b4 100644 --- a/src/conn/mod.rs +++ b/src/conn/mod.rs @@ -1,39 +1,56 @@ -use mysql_common::crypto; -use mysql_common::io::ReadMysqlExt; -use mysql_common::named_params::parse_named_params; -use mysql_common::packets::{ - column_from_payload, parse_auth_switch_request, parse_err_packet, parse_handshake_packet, - parse_ok_packet, AuthPlugin, AuthSwitchRequest, Column, ComStmtClose, - ComStmtExecuteRequestBuilder, ComStmtSendLongData, HandshakePacket, HandshakeResponse, - OkPacket, SslRequest, +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +use mysql_common::{ + crypto, + io::ReadMysqlExt, + named_params::parse_named_params, + packets::{ + column_from_payload, parse_auth_switch_request, parse_err_packet, parse_handshake_packet, + parse_ok_packet, AuthPlugin, AuthSwitchRequest, Column, ComStmtClose, + ComStmtExecuteRequestBuilder, ComStmtSendLongData, HandshakePacket, HandshakeResponse, + OkPacket, SslRequest, + }, + proto::{codec::Compression, sync_framed::MySyncFramed}, + value::{read_bin_values, read_text_values, ServerSide}, }; -use mysql_common::proto::{codec::Compression, sync_framed::MySyncFramed}; -use mysql_common::value::{read_bin_values, read_text_values, ServerSide}; - -use std::borrow::Borrow; -use std::collections::HashMap; -use std::io::{self, Read, Write as _}; -use std::ops::{Deref, DerefMut}; -use std::{cmp, mem, process}; - -use crate::conn::local_infile::LocalInfile; -use crate::conn::query_result::ResultConnRef; -use crate::conn::stmt::InnerStmt; -use crate::conn::stmt_cache::StmtCache; -use crate::conn::transaction::IsolationLevel; -use crate::consts::{CapabilityFlags, Command, StatusFlags, MAX_PAYLOAD_LEN}; -use crate::io::Stream; -use crate::prelude::FromRow; -use crate::DriverError::{ - CouldNotConnect, MismatchedStmtParams, NamedParamsForPositionalQuery, Protocol41NotSet, - ReadOnlyTransNotSupported, SetupError, TlsNotSupported, UnexpectedPacket, UnknownAuthPlugin, - UnsupportedProtocol, + +use std::{ + borrow::Borrow, + cmp, + collections::HashMap, + io::{self, Read, Write as _}, + mem, + ops::{Deref, DerefMut}, + process, + sync::Arc, }; -use crate::Error::{DriverError, MySqlError}; -use crate::Value::{self, Bytes, NULL}; + use crate::{ - from_row, from_value, from_value_opt, LocalInfileHandler, Opts, OptsBuilder, Params, - QueryResult, Result as MyResult, SslOpts, Stmt, Transaction, + conn::{ + local_infile::LocalInfile, + stmt::{InnerStmt, Statement}, + stmt_cache::StmtCache, + transaction::IsolationLevel, + }, + consts::{CapabilityFlags, Command, StatusFlags, MAX_PAYLOAD_LEN}, + from_value, from_value_opt, + io::Stream, + prelude::*, + DriverError::{ + MismatchedStmtParams, NamedParamsForPositionalQuery, Protocol41NotSet, + ReadOnlyTransNotSupported, SetupError, TlsNotSupported, UnexpectedPacket, + UnknownAuthPlugin, UnsupportedProtocol, + }, + Error::{DriverError, MySqlError}, + LocalInfileHandler, Opts, OptsBuilder, Params, QueryResult, Result as MyResult, SslOpts, + Transaction, + Value::{self, Bytes, NULL}, }; pub mod local_infile; @@ -44,36 +61,6 @@ pub mod stmt; mod stmt_cache; pub mod transaction; -/// A trait allowing abstraction over connections and transactions -pub trait GenericConnection { - /// See - /// [`Conn#query`](struct.Conn.html#method.query). - fn query>(&mut self, query: T) -> MyResult>; - - /// See - /// [`Conn#first`](struct.Conn.html#method.first). - fn first, U: FromRow>(&mut self, query: T) -> MyResult>; - - /// See - /// [`Conn#prepare`](struct.Conn.html#method.prepare). - fn prepare>(&mut self, query: T) -> MyResult>; - - /// See - /// [`Conn#prep_exec`](struct.Conn.html#method.prep_exec). - fn prep_exec(&mut self, query: A, params: T) -> MyResult> - where - A: AsRef, - T: Into; - - /// See - /// [`Conn#first_exec`](struct.Conn.html#method.first_exec). - fn first_exec(&mut self, query: Q, params: P) -> MyResult> - where - Q: AsRef, - P: Into, - T: FromRow; -} - /// Mysql connection. #[derive(Debug)] pub struct Conn { @@ -97,6 +84,21 @@ pub struct Conn { } impl Conn { + /// Returns connection identifier. + pub fn connection_id(&self) -> u32 { + self.connection_id + } + + /// Returns number of rows affected by the last query. + pub fn affected_rows(&self) -> u64 { + self.affected_rows + } + + /// Returns last insert id of the last query. + pub fn last_insert_id(&self) -> u64 { + self.last_insert_id + } + fn stream_ref(&self) -> &MySyncFramed { self.stream.as_ref().expect("incomplete connection") } @@ -150,10 +152,9 @@ impl Conn { } if let Some(socket) = socket { if self.opts.get_socket().is_none() { - let mut socket_opts = OptsBuilder::from_opts(self.opts.clone()); + let socket_opts = OptsBuilder::from_opts(self.opts.clone()); if !socket.is_empty() { - socket_opts.socket(Some(socket)); - return Ok(Some(socket_opts.into())); + return Ok(Some(socket_opts.socket(Some(socket)).into())); } } } @@ -181,7 +182,7 @@ impl Conn { } }; for cmd in conn.opts.get_init() { - conn.query(cmd)?; + conn.query_drop(cmd)?; } Ok(conn) } @@ -237,7 +238,7 @@ impl Conn { fn switch_to_ssl(&mut self, ssl_opts: SslOpts) -> MyResult<()> { let stream = self.stream.take().expect("incomplete conn"); let (in_buf, out_buf, codec, stream) = stream.destruct(); - let stream = stream.make_secure(self.opts.get_ip_or_hostname(), ssl_opts)?; + let stream = stream.make_secure(self.opts.get_host(), ssl_opts)?; let stream = MySyncFramed::construct(in_buf, out_buf, codec, stream); self.stream = Some(stream); Ok(()) @@ -252,8 +253,13 @@ impl Conn { let bind_address = self.opts.bind_address().cloned(); let stream = if let Some(socket) = self.opts.get_socket() { Stream::connect_socket(&*socket, read_timeout, write_timeout)? - } else if let Some(ip_or_hostname) = self.opts.get_ip_or_hostname() { + } else { let port = self.opts.get_tcp_port(); + let ip_or_hostname = match self.opts.get_host() { + url::Host::Domain(domain) => domain, + url::Host::Ipv4(ip) => ip.to_string(), + url::Host::Ipv6(ip) => ip.to_string(), + }; Stream::connect_tcp( &*ip_or_hostname, port, @@ -264,8 +270,6 @@ impl Conn { tcp_connect_timeout, bind_address, )? - } else { - return Err(DriverError(CouldNotConnect(None))); }; self.stream = Some(MySyncFramed::new(stream)); Ok(()) @@ -580,7 +584,7 @@ impl Conn { self.write_command_raw(body) } - fn send_long_data(&mut self, stmt: &InnerStmt, params: &[Value]) -> MyResult<()> { + fn send_long_data(&mut self, stmt_id: u32, params: &[Value]) -> MyResult<()> { for (i, value) in params.into_iter().enumerate() { match value { Bytes(bytes) => { @@ -591,7 +595,7 @@ impl Conn { None }); for chunk in chunks { - let com = ComStmtSendLongData::new(stmt.id(), i, chunk); + let com = ComStmtSendLongData::new(stmt_id, i, chunk); self.write_command_raw(com)?; } } @@ -602,7 +606,7 @@ impl Conn { Ok(()) } - fn _execute(&mut self, stmt: &InnerStmt, params: Params) -> MyResult> { + fn _execute(&mut self, stmt: &Statement, params: Params) -> MyResult> { let exec_request = match params { Params::Empty => { if stmt.num_params() != 0 { @@ -624,16 +628,16 @@ impl Conn { ComStmtExecuteRequestBuilder::new(stmt.id()).build(&*params); if as_long_data { - self.send_long_data(stmt, &*params)?; + self.send_long_data(stmt.id(), &*params)?; } body } Params::Named(_) => { - if stmt.named_params().is_none() { + if stmt.named_params.is_none() { return Err(DriverError(NamedParamsForPositionalQuery)); } - let named_params = stmt.named_params().unwrap(); + let named_params = stmt.named_params.as_ref().unwrap(); return self._execute(stmt, params.into_positional(named_params)?); } }; @@ -641,17 +645,6 @@ impl Conn { self.handle_result_set() } - fn execute>(&mut self, stmt: &InnerStmt, params: T) -> MyResult { - match self._execute(stmt, params.into()) { - Ok(columns) => Ok(QueryResult::new( - ResultConnRef::ViaConnRef(self), - columns, - true, - )), - Err(err) => Err(err), - } - } - fn _start_transaction( &mut self, consistent_snapshot: bool, @@ -659,7 +652,7 @@ impl Conn { readonly: Option, ) -> MyResult<()> { if let Some(i_level) = isolation_level { - let _ = self.query(format!("SET TRANSACTION ISOLATION LEVEL {}", i_level))?; + self.query_drop(format!("SET TRANSACTION ISOLATION LEVEL {}", i_level))?; } if let Some(readonly) = readonly { let supported = match (self.server_version, self.mariadb_server_version) { @@ -670,16 +663,16 @@ impl Conn { if !supported { return Err(DriverError(ReadOnlyTransNotSupported)); } - let _ = if readonly { - self.query("SET TRANSACTION READ ONLY")? + if readonly { + self.query_drop("SET TRANSACTION READ ONLY")?; } else { - self.query("SET TRANSACTION READ WRITE")? - }; + self.query_drop("SET TRANSACTION READ WRITE")?; + } } - let _ = if consistent_snapshot { - self.query("START TRANSACTION WITH CONSISTENT SNAPSHOT")? + if consistent_snapshot { + self.query_drop("START TRANSACTION WITH CONSISTENT SNAPSHOT")?; } else { - self.query("START TRANSACTION")? + self.query_drop("START TRANSACTION")?; }; Ok(()) } @@ -786,47 +779,17 @@ impl Conn { Ok(Transaction::new(self)) } - /// Implements text protocol of mysql server. - /// - /// Executes mysql query on `Conn`. [`QueryResult`](struct.QueryResult.html) - /// will borrow `Conn` until the end of its scope. - pub fn query>(&mut self, query: T) -> MyResult> { - match self._query(query.as_ref()) { - Ok(columns) => Ok(QueryResult::new( - ResultConnRef::ViaConnRef(self), - columns, - false, - )), - Err(err) => Err(err), - } - } - - /// Performs query and returns first row. - pub fn first, U: FromRow>(&mut self, query: T) -> MyResult> { - self.query(query).and_then(|mut result| { - if let Some(row) = result.next() { - row.map(|x| Some(from_row(x))) - } else { - Ok(None) - } - }) - } - - fn _true_prepare( - &mut self, - query: &str, - named_params: Option>, - ) -> MyResult { + fn _true_prepare(&mut self, query: &str) -> MyResult { self.write_command(Command::COM_STMT_PREPARE, query.as_bytes())?; let pld = self.read_packet()?; - let mut stmt = InnerStmt::from_payload(pld.as_ref(), named_params)?; + let mut stmt = InnerStmt::from_payload(pld.as_ref(), self.connection_id())?; if stmt.num_params() > 0 { let mut params: Vec = Vec::with_capacity(stmt.num_params() as usize); for _ in 0..stmt.num_params() { let pld = self.read_packet()?; params.push(column_from_payload(pld)?); } - stmt.set_params(Some(params)); + stmt = stmt.with_params(Some(params)); self.read_packet()?; } if stmt.num_columns() > 0 { @@ -835,153 +798,29 @@ impl Conn { let pld = self.read_packet()?; columns.push(column_from_payload(pld)?); } - stmt.set_columns(Some(columns)); + stmt = stmt.with_columns(Some(columns)); self.read_packet()?; } Ok(stmt) } - fn _prepare(&mut self, query: &str, named_params: Option>) -> MyResult { - if let Some(inner_st) = self.stmt_cache.get(query) { - let mut inner_st = inner_st.clone(); - inner_st.set_named_params(named_params); - return Ok(inner_st); + fn _prepare(&mut self, query: &str) -> MyResult> { + if let Some(entry) = self.stmt_cache.by_query(query) { + return Ok(entry.stmt.clone()); } - let inner_st = self._true_prepare(query, named_params)?; + let inner_st = Arc::new(self._true_prepare(query)?); - if self.stmt_cache.get_cap() > 0 { - if let Some(old_st) = self.stmt_cache.put(query.into(), inner_st.clone()) { - let com_stmt_close = ComStmtClose::new(old_st.id()); - self.write_command_raw(com_stmt_close)?; - } + if let Some(old_stmt) = self + .stmt_cache + .put(Arc::new(query.into()), inner_st.clone()) + { + self.close(Statement::new(old_stmt, None))?; } Ok(inner_st) } - /// Implements binary protocol of mysql server. - /// - /// Prepares mysql statement on `Conn`. [`Stmt`](struct.Stmt.html) will - /// borrow `Conn` until the end of its scope. - /// - /// This call will take statement from cache if has been prepared on this connection. - /// - /// ### JSON caveats - /// - /// For the following statement you will get somewhat unexpected result `{"a": 0}`, because - /// booleans in mysql binary protocol is `TINYINT(1)` and will be interpreted as `0`: - /// - /// ```ignore - /// pool.prep_exec(r#"SELECT JSON_REPLACE('{"a": true}', '$.a', ?)"#, (false,)); - /// ``` - /// - /// You should wrap such parameters to a proper json value. For example: - /// - /// ```ignore - /// pool.prep_exec(r#"SELECT JSON_REPLACE('{"a": true}', '$.a', ?)"#, (Value::Bool(false),)); - /// ``` - /// - /// ### Named parameters support - /// - /// `prepare` supports named parameters in form of `:named_param_name`. Allowed characters for - /// parameter name is `[a-z_]`. Named parameters will be converted to positional before actual - /// call to prepare so `SELECT :a-:b, :a*:b` is actually `SELECT ?-?, ?*?`. - /// - /// ``` - /// # #[macro_use] extern crate mysql; fn main() { - /// # use mysql::{Pool, Opts, OptsBuilder, from_row}; - /// # use mysql::Error::DriverError; - /// # use mysql::DriverError::MixedParams; - /// # use mysql::DriverError::MissingNamedParameter; - /// # use mysql::DriverError::NamedParamsForPositionalQuery; - /// # fn get_opts() -> Opts { - /// # let url = if let Ok(url) = std::env::var("DATABASE_URL") { - /// # let opts = Opts::from_url(&url).expect("DATABASE_URL invalid"); - /// # if opts.get_db_name().expect("a database name is required").is_empty() { - /// # panic!("database name is empty"); - /// # } - /// # url - /// # } else { - /// # "mysql://root:password@127.0.0.1:3307/mysql".to_string() - /// # }; - /// # Opts::from_url(&*url).unwrap() - /// # } - /// # let opts = get_opts(); - /// # let pool = Pool::new(opts).unwrap(); - /// // Names could be repeated - /// pool.prep_exec("SELECT :a+:b, :a * :b, ':c'", params!{"a" => 2, "b" => 3}).map(|mut result| { - /// let row = result.next().unwrap().unwrap(); - /// assert_eq!((5, 6, String::from(":c")), from_row(row)); - /// }).unwrap(); - /// - /// // You can call named statement with positional parameters - /// pool.prep_exec("SELECT :a+:b, :a*:b", (2, 3, 2, 3)).map(|mut result| { - /// let row = result.next().unwrap().unwrap(); - /// assert_eq!((5, 6), from_row(row)); - /// }).unwrap(); - /// - /// // You must pass all named parameters for statement - /// let err = pool.prep_exec("SELECT :name", params!{"another_name" => 42}).unwrap_err(); - /// match err { - /// DriverError(e) => { - /// assert_eq!(MissingNamedParameter("name".into()), e); - /// } - /// _ => unreachable!(), - /// } - /// - /// // You can't call positional statement with named parameters - /// let err = pool.prep_exec("SELECT ?", params!{"first" => 42}).unwrap_err(); - /// match err { - /// DriverError(e) => assert_eq!(NamedParamsForPositionalQuery, e), - /// _ => unreachable!(), - /// } - /// - /// // You can't mix named and positional parameters - /// let err = pool.prepare("SELECT :a, ?").unwrap_err(); - /// match err { - /// DriverError(e) => assert_eq!(MixedParams, e), - /// _ => unreachable!(), - /// } - /// # } - /// ``` - pub fn prepare>(&mut self, query: T) -> MyResult> { - let query = query.as_ref(); - let (named_params, real_query) = parse_named_params(query)?; - match self._prepare(real_query.borrow(), named_params) { - Ok(stmt) => Ok(Stmt::new(stmt, self)), - Err(err) => Err(err), - } - } - - /// Prepares and executes statement in one call. See - /// ['Conn::prepare'](struct.Conn.html#method.prepare) - /// - /// This call will take statement from cache if has been prepared on this connection. - pub fn prep_exec(&mut self, query: A, params: T) -> MyResult> - where - A: AsRef, - T: Into, - { - self.prepare(query)?.prep_exec(params.into()) - } - - /// Executes statement and returns first row. - pub fn first_exec(&mut self, query: Q, params: P) -> MyResult> - where - Q: AsRef, - P: Into, - T: FromRow, - { - self.prep_exec(query, params).and_then(|mut result| { - if let Some(row) = result.next() { - row.map(|x| Some(from_row(x))) - } else { - Ok(None) - } - }) - } - fn connect(&mut self) -> MyResult<()> { if self.connected { return Ok(()); @@ -1005,14 +844,7 @@ impl Conn { } fn get_system_var(&mut self, name: &str) -> MyResult> { - for row in self.query(format!("SELECT @@{}", name))? { - if let Ok(mut r) = row { - if r.len() > 0 { - return Ok(r.take(0)); - } - } - } - Ok(None) + self.query_first(format!("SELECT @@{}", name)) } fn next_bin(&mut self, columns: &[Column]) -> MyResult>> { @@ -1046,7 +878,7 @@ impl Conn { } fn has_stmt(&self, query: &str) -> bool { - self.stmt_cache.contains(query) + self.stmt_cache.contains_query(query) } /// Sets a callback to handle requests for local files. These are @@ -1065,43 +897,47 @@ impl Conn { } } -impl GenericConnection for Conn { - fn query>(&mut self, query: T) -> MyResult> { - self.query(query) - } - - fn first, U: FromRow>(&mut self, query: T) -> MyResult> { - self.first(query) +impl crate::prelude::Queryable for Conn { + fn query_iter>(&mut self, query: T) -> MyResult> { + match self._query(query.as_ref()) { + Ok(columns) => Ok(QueryResult::new(self, columns, false)), + Err(err) => Err(err), + } } - fn prepare>(&mut self, query: T) -> MyResult> { - self.prepare(query) + fn prep>(&mut self, query: T) -> MyResult { + let query = query.as_ref(); + let (named_params, real_query) = parse_named_params(query)?; + self._prepare(real_query.borrow()) + .map(|inner| Statement::new(inner, named_params)) } - fn prep_exec(&mut self, query: A, params: T) -> MyResult> - where - A: AsRef, - T: Into, - { - self.prep_exec(query, params) + fn close(&mut self, stmt: Statement) -> MyResult<()> { + self.stmt_cache.remove(stmt.id()); + let com_stmt_close = ComStmtClose::new(stmt.id()); + self.write_command_raw(com_stmt_close)?; + Ok(()) } - fn first_exec(&mut self, query: Q, params: P) -> MyResult> + fn exec_iter(&mut self, stmt: S, params: P) -> MyResult> where - Q: AsRef, + S: AsStatement, P: Into, - T: FromRow, { - self.first_exec(query, params) + let statement = stmt.as_statement(self)?; + let columns = self._execute(&*statement, params.into())?; + Ok(QueryResult::new(self, columns, true)) } } impl Drop for Conn { fn drop(&mut self) { let stmt_cache = mem::replace(&mut self.stmt_cache, StmtCache::new(0)); - for (_, inner_st) in stmt_cache.into_iter() { - let _ = self.write_command_raw(ComStmtClose::new(inner_st.id())); + + for (_, entry) in stmt_cache.into_iter() { + let _ = self.close(Statement::new(entry.stmt, None)); } + if self.stream.is_some() { let _ = self.write_command(Command::COM_QUIT, &[]); } @@ -1139,44 +975,38 @@ impl<'a> DerefMut for ConnRef<'a> { #[allow(non_snake_case)] mod test { mod my_conn { - use std::collections::HashMap; - use std::io::Write; - use std::{iter, process}; - - use crate::prelude::{FromValue, ToValue}; - use crate::test_misc::get_opts; - use crate::time::{now, Tm}; - use crate::DriverError::{MissingNamedParameter, NamedParamsForPositionalQuery}; - use crate::Error::DriverError; - use crate::Value::{Bytes, Date, Int, NULL}; + use std::{collections::HashMap, io::Write, iter, process}; + use crate::{ - from_row, from_value, params, Conn, LocalInfileHandler, Opts, OptsBuilder, Params, + from_row, from_value, params, + prelude::*, + test_misc::get_opts, + time::Timespec, + Conn, + DriverError::{MissingNamedParameter, NamedParamsForPositionalQuery}, + Error::DriverError, + LocalInfileHandler, Opts, OptsBuilder, Params, + Value::{self, Bytes, Date, Float, Int, NULL}, }; fn get_system_variable(conn: &mut Conn, name: &str) -> T where T: FromValue, { - let row = conn - .first(format!("show variables like '{}'", name)) + conn.query_first::<(String, T), _>(format!("show variables like '{}'", name)) .unwrap() - .unwrap(); - let (_, value): (String, T) = from_row(row); - value + .unwrap() + .1 } #[test] fn should_connect() { let mut conn = Conn::new(get_opts()).unwrap(); - let mode = conn - .query("SELECT @@GLOBAL.sql_mode") - .unwrap() - .next() - .unwrap() + + let mode: String = conn + .query_first("SELECT @@GLOBAL.sql_mode") .unwrap() - .take(0) .unwrap(); - let mode = from_value::(mode); assert!(mode.contains("TRADITIONAL")); assert!(conn.ping()); @@ -1192,250 +1022,216 @@ mod test { #[test] #[should_panic(expected = "Could not connect to address")] fn should_fail_on_wrong_socket_path() { - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.socket(Some("/foo/bar/baz")); + let opts = OptsBuilder::from_opts(get_opts()).socket(Some("/foo/bar/baz")); let _ = Conn::new(opts).unwrap(); } + #[test] fn should_fallback_to_tcp_if_cant_switch_to_socket() { let mut opts = Opts::from(get_opts()); opts.0.injected_socket = Some("/foo/bar/baz".into()); let _ = Conn::new(opts).unwrap(); } + #[test] fn should_connect_with_database() { - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.db_name(Some("mysql")); + const DB_NAME: &str = "mysql"; + + let opts = OptsBuilder::from_opts(get_opts()).db_name(Some(DB_NAME)); + let mut conn = Conn::new(opts).unwrap(); - assert_eq!( - conn.query("SELECT DATABASE()") - .unwrap() - .next() - .unwrap() - .unwrap() - .unwrap(), - vec![Bytes(b"mysql".to_vec())] - ); + + let db_name: String = conn.query_first("SELECT DATABASE()").unwrap().unwrap(); + assert_eq!(db_name, DB_NAME); } + #[test] fn should_connect_by_hostname() { - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.db_name(Some("mysql")); - opts.ip_or_hostname(Some("localhost")); + let opts = OptsBuilder::from_opts(get_opts()).ip_or_hostname(Some("localhost")); let mut conn = Conn::new(opts).unwrap(); - assert_eq!( - conn.query("SELECT DATABASE()") - .unwrap() - .next() - .unwrap() - .unwrap() - .unwrap(), - vec![Bytes(b"mysql".to_vec())] - ); + assert!(conn.ping()); } + #[test] fn should_select_db() { - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.db_name(Some("mysql")); - opts.ip_or_hostname(Some("localhost")); - let mut conn = Conn::new(opts).unwrap(); - assert!(conn - .query("CREATE DATABASE IF NOT EXISTS t_select_db") - .is_ok()); - assert!(conn.select_db("t_select_db")); - assert_eq!( - conn.query("SELECT DATABASE()") - .unwrap() - .next() - .unwrap() - .unwrap() - .unwrap(), - vec![Bytes(b"t_select_db".to_vec())] - ); - assert!(conn.query("DROP DATABASE t_select_db").is_ok()); + const DB_NAME: &str = "t_select_db"; + + let mut conn = Conn::new(get_opts()).unwrap(); + conn.query_drop(format!("CREATE DATABASE IF NOT EXISTS {}", DB_NAME)) + .unwrap(); + assert!(conn.select_db(DB_NAME)); + + let db_name: String = conn.query_first("SELECT DATABASE()").unwrap().unwrap(); + assert_eq!(db_name, DB_NAME); + + conn.query_drop(format!("DROP DATABASE {}", DB_NAME)) + .unwrap(); } + #[test] fn should_execute_queryes_and_parse_results() { + type TestRow = (String, String, String, String, String, String); + + const CREATE_QUERY: &str = r"CREATE TEMPORARY TABLE mysql.tbl + (id SERIAL, a TEXT, b INT, c INT UNSIGNED, d DATE, e FLOAT)"; + const INSERT_QUERY_1: &str = r"INSERT + INTO mysql.tbl(a, b, c, d, e) + VALUES ('hello', -123, 123, '2014-05-05', 123.123)"; + const INSERT_QUERY_2: &str = r"INSERT + INTO mysql.tbl(a, b, c, d, e) + VALUES ('world', -321, 321, '2014-06-06', 321.321)"; + let mut conn = Conn::new(get_opts()).unwrap(); + + conn.query_drop(CREATE_QUERY).unwrap(); + assert_eq!(conn.affected_rows(), 0); + assert_eq!(conn.last_insert_id(), 0); + + conn.query_drop(INSERT_QUERY_1).unwrap(); + assert_eq!(conn.affected_rows(), 1); + assert_eq!(conn.last_insert_id(), 1); + + conn.query_drop(INSERT_QUERY_2).unwrap(); + assert_eq!(conn.affected_rows(), 1); + assert_eq!(conn.last_insert_id(), 2); + + conn.query_drop("SELECT * FROM unexisted").unwrap_err(); + conn.query_iter("SELECT * FROM mysql.tbl").unwrap(); // Drop::drop for QueryResult + + conn.query_drop("UPDATE mysql.tbl SET a = 'foo'").unwrap(); + assert_eq!(conn.affected_rows(), 2); + assert_eq!(conn.last_insert_id(), 0); + assert!(conn - .query( - "CREATE TEMPORARY TABLE mysql.tbl(\ - a TEXT,\ - b INT,\ - c INT UNSIGNED,\ - d DATE,\ - e FLOAT - )" - ) - .is_ok()); - assert!(conn - .query( - "INSERT INTO mysql.tbl(a, b, c, d, e) VALUES (\ - 'hello',\ - -123,\ - 123,\ - '2014-05-05',\ - 123.123\ - )" - ) - .is_ok()); - assert!(conn - .query( - "INSERT INTO mysql.tbl(a, b, c, d, e) VALUES (\ - 'world',\ - -321,\ - 321,\ - '2014-06-06',\ - 321.321\ - )" - ) - .is_ok()); - assert!(conn.query("SELECT * FROM unexisted").is_err()); - assert!(conn.query("SELECT * FROM mysql.tbl").is_ok()); - // Drop - assert!(conn.query("UPDATE mysql.tbl SET a = 'foo'").is_ok()); - assert_eq!(conn.affected_rows, 2); - assert!(conn - .query("SELECT * FROM mysql.tbl WHERE a = 'bar'") + .query_first::("SELECT * FROM mysql.tbl WHERE a = 'bar'") .unwrap() - .next() .is_none()); - for (i, row) in conn.query("SELECT * FROM mysql.tbl").unwrap().enumerate() { - let row = row.unwrap(); - if i == 0 { - assert_eq!(row[0], Bytes(b"foo".to_vec())); - assert_eq!(row[1], Bytes(b"-123".to_vec())); - assert_eq!(row[2], Bytes(b"123".to_vec())); - assert_eq!(row[3], Bytes(b"2014-05-05".to_vec())); - assert_eq!(row[4], Bytes(b"123.123".to_vec())); - } else if i == 1 { - assert_eq!(row[0], Bytes(b"foo".to_vec())); - assert_eq!(row[1], Bytes(b"-321".to_vec())); - assert_eq!(row[2], Bytes(b"321".to_vec())); - assert_eq!(row[3], Bytes(b"2014-06-06".to_vec())); - assert_eq!(row[4], Bytes(b"321.321".to_vec())); - } else { - unreachable!(); - } - } + + let rows: Vec = conn.query("SELECT * FROM mysql.tbl").unwrap(); + assert_eq!( + rows, + vec![ + ( + "1".into(), + "foo".into(), + "-123".into(), + "123".into(), + "2014-05-05".into(), + "123.123".into() + ), + ( + "2".into(), + "foo".into(), + "-321".into(), + "321".into(), + "2014-06-06".into(), + "321.321".into() + ) + ] + ); } + #[test] fn should_parse_large_text_result() { let mut conn = Conn::new(get_opts()).unwrap(); - assert_eq!( - conn.query("SELECT REPEAT('A', 20000000)") - .unwrap() - .next() - .unwrap() - .unwrap() - .unwrap(), - vec![Bytes(iter::repeat(b'A').take(20_000_000).collect())] - ); + let value: Value = conn + .query_first("SELECT REPEAT('A', 20000000)") + .unwrap() + .unwrap(); + assert_eq!(value, Bytes(iter::repeat(b'A').take(20_000_000).collect())); } + #[test] fn should_execute_statements_and_parse_results() { + const CREATE_QUERY: &str = r"CREATE TEMPORARY TABLE + mysql.tbl (a TEXT, b INT, c INT UNSIGNED, d DATE, e DOUBLE)"; + const INSERT_SMTM: &str = r"INSERT + INTO mysql.tbl (a, b, c, d, e) + VALUES (?, ?, ?, ?, ?)"; + + type RowType = (Value, Value, Value, Value, Value); + + let row1 = ( + Bytes(b"hello".to_vec()), + Int(-123_i64), + Int(123_i64), + Date(2014_u16, 5_u8, 5_u8, 0_u8, 0_u8, 0_u8, 0_u32), + Float(123.123_f64), + ); + let row2 = (Bytes(b"".to_vec()), NULL, NULL, NULL, Float(321.321_f64)); + let mut conn = Conn::new(get_opts()).unwrap(); - assert!(conn - .query( - "CREATE TEMPORARY TABLE mysql.tbl(\ - a TEXT,\ - b INT,\ - c INT UNSIGNED,\ - d DATE,\ - e DOUBLE\ - )" - ) - .is_ok()); - let _ = conn - .prepare( - "INSERT INTO mysql.tbl(a, b, c, d, e)\ - VALUES (?, ?, ?, ?, ?)", - ) - .and_then(|mut stmt| { - let tm = Tm { - tm_year: 114, - tm_mon: 4, - tm_mday: 5, - tm_hour: 0, - tm_min: 0, - tm_sec: 0, - tm_nsec: 0, - ..now() - }; - let hello = b"hello".to_vec(); - assert!(stmt - .execute((&hello, -123, 123, tm.to_timespec(), 123.123f64)) - .is_ok()); - stmt.execute( - &[ - &b"".to_vec() as &dyn ToValue, - &NULL as &dyn ToValue, - &NULL as &dyn ToValue, - &NULL as &dyn ToValue, - &321.321f64 as &dyn ToValue, - ][..], - ) - .unwrap(); - Ok(()) - }) - .unwrap(); - let _ = conn - .prepare("SELECT * from mysql.tbl") - .and_then(|mut stmt| { - for (i, row) in stmt.execute(()).unwrap().enumerate() { - let mut row = row.unwrap(); - if i == 0 { - assert_eq!(row[0], Bytes(b"hello".to_vec())); - assert_eq!(row[1], Int(-123i64)); - assert_eq!(row[2], Int(123i64)); - assert_eq!(row[3], Date(2014u16, 5u8, 5u8, 0u8, 0u8, 0u8, 0u32)); - assert_eq!(row.take::(4).unwrap(), 123.123f64); - } else if i == 1 { - assert_eq!(row[0], Bytes(b"".to_vec())); - assert_eq!(row[1], NULL); - assert_eq!(row[2], NULL); - assert_eq!(row[3], NULL); - assert_eq!(row.take::(4).unwrap(), 321.321f64); - } else { - unreachable!(); - } - } - Ok(()) - }) - .unwrap(); - let mut result = conn.prep_exec("SELECT ?, ?, ?", ("hello", 1, 1.1)).unwrap(); - let row = result.next().unwrap(); - let mut row = row.unwrap(); - assert_eq!(row.take::(0).unwrap(), "hello".to_string()); - assert_eq!(row.take::(1).unwrap(), 1i8); - assert_eq!(row.take::(2).unwrap(), 1.1f32); + conn.query_drop(CREATE_QUERY).unwrap(); + + let insert_stmt = conn.prep(INSERT_SMTM).unwrap(); + assert_eq!(insert_stmt.connection_id(), conn.connection_id()); + conn.exec_drop( + &insert_stmt, + ( + from_value::(row1.0.clone()), + from_value::(row1.1.clone()), + from_value::(row1.2.clone()), + from_value::(row1.3.clone()), + from_value::(row1.4.clone()), + ), + ) + .unwrap(); + conn.exec_drop( + &insert_stmt, + ( + from_value::(row2.0.clone()), + row2.1.clone(), + row2.2.clone(), + row2.3.clone(), + from_value::(row2.4.clone()), + ), + ) + .unwrap(); + + let select_stmt = conn.prep("SELECT * from mysql.tbl").unwrap(); + let rows: Vec = conn.exec(&select_stmt, ()).unwrap(); + + assert_eq!(rows, vec![row1, row2]); } + #[test] fn should_parse_large_binary_result() { let mut conn = Conn::new(get_opts()).unwrap(); - let mut stmt = conn.prepare("SELECT REPEAT('A', 20000000);").unwrap(); - assert_eq!( - stmt.execute(()).unwrap().next().unwrap().unwrap().unwrap(), - vec![Bytes(iter::repeat(b'A').take(20_000_000).collect())] - ); + let stmt = conn.prep("SELECT REPEAT('A', 20000000)").unwrap(); + let value: Value = conn.exec_first(&stmt, ()).unwrap().unwrap(); + assert_eq!(value, Bytes(iter::repeat(b'A').take(20_000_000).collect())); } + + #[test] + fn manually_closed_stmt() { + let opts = OptsBuilder::from(get_opts()).stmt_cache_size(1); + let mut conn = Conn::new(opts).unwrap(); + let stmt = conn.prep("SELECT 1").unwrap(); + conn.exec_drop(&stmt, ()).unwrap(); + conn.close(stmt).unwrap(); + let stmt = conn.prep("SELECT 1").unwrap(); + conn.exec_drop(&stmt, ()).unwrap(); + conn.close(stmt).unwrap(); + let stmt = conn.prep("SELECT 2").unwrap(); + conn.exec_drop(&stmt, ()).unwrap(); + } + #[test] fn should_start_commit_and_rollback_transactions() { let mut conn = Conn::new(get_opts()).unwrap(); - assert!(conn - .query("CREATE TEMPORARY TABLE mysql.tbl(a INT)") - .is_ok()); + conn.query_drop("CREATE TEMPORARY TABLE mysql.tbl(a INT)") + .unwrap(); let _ = conn .start_transaction(false, None, None) .and_then(|mut t| { - assert!(t.query("INSERT INTO mysql.tbl(a) VALUES(1)").is_ok()); - assert!(t.query("INSERT INTO mysql.tbl(a) VALUES(2)").is_ok()); - assert!(t.commit().is_ok()); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(1)").unwrap(); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(2)").unwrap(); + t.commit().unwrap(); Ok(()) }) .unwrap(); assert_eq!( - conn.query("SELECT COUNT(a) from mysql.tbl") + conn.query_iter("SELECT COUNT(a) from mysql.tbl") .unwrap() .next() .unwrap() @@ -1446,13 +1242,13 @@ mod test { let _ = conn .start_transaction(false, None, None) .and_then(|mut t| { - t.query("INSERT INTO tbl2(a) VALUES(1)").unwrap_err(); + t.query_drop("INSERT INTO tbl2(a) VALUES(1)").unwrap_err(); Ok(()) // implicit rollback }) .unwrap(); assert_eq!( - conn.query("SELECT COUNT(a) from mysql.tbl") + conn.query_iter("SELECT COUNT(a) from mysql.tbl") .unwrap() .next() .unwrap() @@ -1463,14 +1259,14 @@ mod test { let _ = conn .start_transaction(false, None, None) .and_then(|mut t| { - assert!(t.query("INSERT INTO mysql.tbl(a) VALUES(1)").is_ok()); - assert!(t.query("INSERT INTO mysql.tbl(a) VALUES(2)").is_ok()); - assert!(t.rollback().is_ok()); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(1)").unwrap(); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(2)").unwrap(); + t.rollback().unwrap(); Ok(()) }) .unwrap(); assert_eq!( - conn.query("SELECT COUNT(a) from mysql.tbl") + conn.query_iter("SELECT COUNT(a) from mysql.tbl") .unwrap() .next() .unwrap() @@ -1478,23 +1274,14 @@ mod test { .unwrap(), vec![Bytes(b"2".to_vec())] ); - let _ = conn - .start_transaction(false, None, None) - .and_then(|mut t| { - let _ = t - .prepare("INSERT INTO mysql.tbl(a) VALUES(?)") - .and_then(|mut stmt| { - assert!(stmt.execute((3,)).is_ok()); - assert!(stmt.execute((4,)).is_ok()); - Ok(()) - }) - .unwrap(); - assert!(t.commit().is_ok()); - Ok(()) - }) + let mut tx = conn.start_transaction(false, None, None).unwrap(); + tx.exec_drop("INSERT INTO mysql.tbl(a) VALUES(?)", (3,)) + .unwrap(); + tx.exec_drop("INSERT INTO mysql.tbl(a) VALUES(?)", (4,)) .unwrap(); + tx.commit().unwrap(); assert_eq!( - conn.query("SELECT COUNT(a) from mysql.tbl") + conn.query_iter("SELECT COUNT(a) from mysql.tbl") .unwrap() .next() .unwrap() @@ -1502,21 +1289,21 @@ mod test { .unwrap(), vec![Bytes(b"4".to_vec())] ); - let _ = conn - .start_transaction(false, None, None) - .and_then(|mut t| { - t.prep_exec("INSERT INTO mysql.tbl(a) VALUES(?)", (5,)) - .unwrap(); - t.prep_exec("INSERT INTO mysql.tbl(a) VALUES(?)", (6,)) - .unwrap(); - Ok(()) - }) + let mut tx = conn.start_transaction(false, None, None).unwrap(); + tx.exec_drop("INSERT INTO mysql.tbl(a) VALUES(?)", (5,)) .unwrap(); + tx.exec_drop("INSERT INTO mysql.tbl(a) VALUES(?)", (6,)) + .unwrap(); + drop(tx); + assert_eq!( + conn.query_first("SELECT COUNT(a) from mysql.tbl").unwrap(), + Some(4_usize), + ); } #[test] fn should_handle_LOCAL_INFILE_with_custom_handler() { let mut conn = Conn::new(get_opts()).unwrap(); - conn.query("CREATE TEMPORARY TABLE mysql.tbl(a TEXT)") + conn.query_drop("CREATE TEMPORARY TABLE mysql.tbl(a TEXT)") .unwrap(); conn.set_local_infile_handler(Some(LocalInfileHandler::new(|_, stream| { let mut cell_data = vec![b'Z'; 65535]; @@ -1526,7 +1313,7 @@ mod test { } Ok(()) }))); - match conn.query("LOAD DATA LOCAL INFILE 'file_name' INTO TABLE mysql.tbl") { + match conn.query_drop("LOAD DATA LOCAL INFILE 'file_name' INTO TABLE mysql.tbl") { Ok(_) => {} Err(ref err) if format!("{}", err).find("not allowed").is_some() => { return; @@ -1534,7 +1321,7 @@ mod test { Err(err) => panic!("ERROR {}", err), } let count = conn - .query("SELECT * FROM mysql.tbl") + .query_iter("SELECT * FROM mysql.tbl") .unwrap() .map(|row| { assert_eq!(from_row::<(Vec,)>(row.unwrap()).0.len(), 65535); @@ -1547,21 +1334,38 @@ mod test { #[test] fn should_reset_connection() { let mut conn = Conn::new(get_opts()).unwrap(); - assert!(conn - .query( - "CREATE TEMPORARY TABLE `mysql`.`test` \ - (`test` VARCHAR(255) NULL);" - ) - .is_ok()); - assert!(conn.query("SELECT * FROM `mysql`.`test`;").is_ok()); - assert!(conn.reset().is_ok()); - assert!(conn.query("SELECT * FROM `mysql`.`test`;").is_err()); + conn.query_drop( + "CREATE TEMPORARY TABLE `mysql`.`test` \ + (`test` VARCHAR(255) NULL);", + ) + .unwrap(); + conn.query_drop("SELECT * FROM `mysql`.`test`;").unwrap(); + conn.reset().unwrap(); + conn.query_drop("SELECT * FROM `mysql`.`test`;") + .unwrap_err(); + } + + #[test] + fn prep_exec() { + let mut conn = Conn::new(get_opts()).unwrap(); + + let stmt1 = conn.prep("SELECT :foo").unwrap(); + let stmt2 = conn.prep("SELECT :bar").unwrap(); + assert_eq!( + conn.exec::(&stmt1, params! { "foo" => "foo" }) + .unwrap(), + vec![String::from("foo")], + ); + assert_eq!( + conn.exec::(&stmt2, params! { "bar" => "bar" }) + .unwrap(), + vec![String::from("bar")], + ); } #[test] fn should_connect_via_socket_for_127_0_0_1() { - #[allow(unused_mut)] - let mut opts = OptsBuilder::from_opts(get_opts()); + let opts = OptsBuilder::from_opts(get_opts()); let conn = Conn::new(opts).unwrap(); if conn.is_insecure() { assert!(conn.is_socket()); @@ -1570,8 +1374,7 @@ mod test { #[test] fn should_connect_via_socket_localhost() { - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.ip_or_hostname(Some("localhost")); + let opts = OptsBuilder::from_opts(get_opts()).ip_or_hostname(Some("localhost")); let conn = Conn::new(opts).unwrap(); if conn.is_insecure() { assert!(conn.is_socket()); @@ -1580,30 +1383,29 @@ mod test { #[test] fn should_drop_multi_result_set() { - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.db_name(Some("mysql")); + let opts = OptsBuilder::from_opts(get_opts()).db_name(Some("mysql")); let mut conn = Conn::new(opts).unwrap(); - conn.query("CREATE TEMPORARY TABLE TEST_TABLE ( name varchar(255) )") + conn.query_drop("CREATE TEMPORARY TABLE TEST_TABLE ( name varchar(255) )") .unwrap(); - conn.prep_exec("SELECT * FROM TEST_TABLE", ()).unwrap(); - conn.query( + conn.exec_drop("SELECT * FROM TEST_TABLE", ()).unwrap(); + conn.query_drop( r" INSERT INTO TEST_TABLE (name) VALUES ('one'); INSERT INTO TEST_TABLE (name) VALUES ('two'); INSERT INTO TEST_TABLE (name) VALUES ('three');", ) .unwrap(); - conn.prep_exec("SELECT * FROM TEST_TABLE", ()).unwrap(); + conn.exec_drop("SELECT * FROM TEST_TABLE", ()).unwrap(); } #[test] fn should_handle_multi_resultset() { - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.prefer_socket(false); - opts.db_name(Some("mysql")); + let opts = OptsBuilder::from_opts(get_opts()) + .prefer_socket(false) + .db_name(Some("mysql")); let mut conn = Conn::new(opts).unwrap(); - conn.query("DROP PROCEDURE IF EXISTS multi").unwrap(); - conn.query( + conn.query_drop("DROP PROCEDURE IF EXISTS multi").unwrap(); + conn.query_drop( r#"CREATE PROCEDURE multi() BEGIN SELECT 1 UNION ALL SELECT 2; DO 1; @@ -1616,7 +1418,7 @@ mod test { ) .unwrap(); { - let mut query_result = conn.query("CALL multi()").unwrap(); + let mut query_result = conn.query_iter("CALL multi()").unwrap(); let result_set = query_result .by_ref() .map(|row| row.unwrap().unwrap().pop().unwrap()) @@ -1629,7 +1431,7 @@ mod test { .collect::>(); assert_eq!(result_set, vec![Bytes(b"3".to_vec()), Bytes(b"4".to_vec())]); } - let mut result = conn.query("SELECT 1; SELECT 2; SELECT 3;").unwrap(); + let mut result = conn.query_iter("SELECT 1; SELECT 2; SELECT 3;").unwrap(); let mut i = 0; while { i += 1; @@ -1651,16 +1453,16 @@ mod test { fn should_work_with_named_params() { let mut conn = Conn::new(get_opts()).unwrap(); { - let mut stmt = conn.prepare("SELECT :a, :b, :a, :c").unwrap(); - let mut result = stmt - .execute(params! {"a" => 1, "b" => 2, "c" => 3}) + let stmt = conn.prep("SELECT :a, :b, :a, :c").unwrap(); + let result = conn + .exec_first(&stmt, params! {"a" => 1, "b" => 2, "c" => 3}) + .unwrap() .unwrap(); - let row = result.next().unwrap().unwrap(); - assert_eq!((1, 2, 1, 3), from_row(row)); + assert_eq!((1_u8, 2_u8, 1_u8, 3_u8), result); } - let mut result = conn - .prep_exec( + let result = conn + .exec_first( "SELECT :a, :b, :a + :b, :c", params! { "a" => 1, @@ -1668,16 +1470,17 @@ mod test { "c" => 3, }, ) + .unwrap() .unwrap(); - let row = result.next().unwrap().unwrap(); - assert_eq!((1, 2, 3, 3), from_row(row)); + assert_eq!((1_u8, 2_u8, 3_u8, 3_u8), result); } #[test] fn should_return_error_on_missing_named_parameter() { let mut conn = Conn::new(get_opts()).unwrap(); - let mut stmt = conn.prepare("SELECT :a, :b, :a, :c, :d").unwrap(); - let result = stmt.execute(params! {"a" => 1, "b" => 2, "c" => 3,}); + let stmt = conn.prep("SELECT :a, :b, :a, :c, :d").unwrap(); + let result = + conn.exec_first::(&stmt, params! {"a" => 1, "b" => 2, "c" => 3,}); match result { Err(DriverError(MissingNamedParameter(ref x))) if x == "d" => (), _ => assert!(false), @@ -1687,8 +1490,8 @@ mod test { #[test] fn should_return_error_on_named_params_for_positional_statement() { let mut conn = Conn::new(get_opts()).unwrap(); - let mut stmt = conn.prepare("SELECT ?, ?, ?, ?, ?").unwrap(); - let result = stmt.execute(params! {"a" => 1, "b" => 2, "c" => 3,}); + let stmt = conn.prep("SELECT ?, ?, ?, ?, ?").unwrap(); + let result = conn.exec_drop(&stmt, params! {"a" => 1, "b" => 2, "c" => 3,}); match result { Err(DriverError(NamedParamsForPositionalQuery)) => (), _ => assert!(false), @@ -1705,15 +1508,15 @@ mod test { fn should_handle_tcp_connect_timeout() { use crate::error::{DriverError::ConnectTimeout, Error::DriverError}; - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.prefer_socket(false); - opts.tcp_connect_timeout(Some(::std::time::Duration::from_millis(1000))); + let opts = OptsBuilder::from_opts(get_opts()) + .prefer_socket(false) + .tcp_connect_timeout(Some(::std::time::Duration::from_millis(1000))); assert!(Conn::new(opts).unwrap().ping()); - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.prefer_socket(false); - opts.tcp_connect_timeout(Some(::std::time::Duration::from_millis(1000))); - opts.ip_or_hostname(Some("192.168.255.255")); + let opts = OptsBuilder::from_opts(get_opts()) + .prefer_socket(false) + .tcp_connect_timeout(Some(::std::time::Duration::from_millis(1000))) + .ip_or_hostname(Some("192.168.255.255")); match Conn::new(opts).unwrap_err() { DriverError(ConnectTimeout) => {} err => panic!("Unexpected error: {}", err), @@ -1724,16 +1527,16 @@ mod test { fn should_set_additional_capabilities() { use crate::consts::CapabilityFlags; - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.additional_capabilities(CapabilityFlags::CLIENT_FOUND_ROWS); + let opts = OptsBuilder::from_opts(get_opts()) + .additional_capabilities(CapabilityFlags::CLIENT_FOUND_ROWS); let mut conn = Conn::new(opts).unwrap(); - conn.query("CREATE TEMPORARY TABLE mysql.tbl (a INT, b TEXT)") + conn.query_drop("CREATE TEMPORARY TABLE mysql.tbl (a INT, b TEXT)") .unwrap(); - conn.query("INSERT INTO mysql.tbl (a, b) VALUES (1, 'foo')") + conn.query_drop("INSERT INTO mysql.tbl (a, b) VALUES (1, 'foo')") .unwrap(); let result = conn - .query("UPDATE mysql.tbl SET b = 'foo' WHERE a = 1") + .query_iter("UPDATE mysql.tbl SET b = 'foo' WHERE a = 1") .unwrap(); assert_eq!(result.affected_rows(), 1); } @@ -1741,10 +1544,10 @@ mod test { #[test] fn should_bind_before_connect() { let port = 27200 + (rand::random::() % 100); - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.prefer_socket(false); - opts.ip_or_hostname(Some("127.0.0.1")); - opts.bind_address(Some(([127, 0, 0, 1], port))); + let opts = OptsBuilder::from_opts(get_opts()) + .prefer_socket(false) + .ip_or_hostname(Some("127.0.0.1")) + .bind_address(Some(([127, 0, 0, 1], port))); let conn = Conn::new(opts).unwrap(); let debug_format: String = format!("{:?}", conn); assert!(debug_format.contains(&*format!("addr: V4(127.0.0.1:{})", port))); @@ -1753,11 +1556,11 @@ mod test { #[test] fn should_bind_before_connect_with_timeout() { let port = 27300 + (rand::random::() % 100); - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.prefer_socket(false); - opts.ip_or_hostname(Some("127.0.0.1")); - opts.bind_address(Some(([127, 0, 0, 1], port))); - opts.tcp_connect_timeout(Some(::std::time::Duration::from_millis(1000))); + let opts = OptsBuilder::from_opts(get_opts()) + .prefer_socket(false) + .ip_or_hostname(Some("127.0.0.1")) + .bind_address(Some(([127, 0, 0, 1], port))) + .tcp_connect_timeout(Some(::std::time::Duration::from_millis(1000))); let mut conn = Conn::new(opts).unwrap(); assert!(conn.ping()); let debug_format: String = format!("{:?}", conn); @@ -1766,44 +1569,52 @@ mod test { #[test] fn should_not_cache_statements_if_stmt_cache_size_is_zero() { - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.stmt_cache_size(0); + let opts = OptsBuilder::from_opts(get_opts()).stmt_cache_size(0); let mut conn = Conn::new(opts).unwrap(); - conn.prepare("DO 1").unwrap(); - conn.prepare("DO 2").unwrap(); - conn.prepare("DO 3").unwrap(); - let row = conn - .first("SHOW SESSION STATUS LIKE 'Com_stmt_close';") + + let stmt1 = conn.prep("DO 1").unwrap(); + let stmt2 = conn.prep("DO 2").unwrap(); + let stmt3 = conn.prep("DO 3").unwrap(); + + conn.close(stmt1).unwrap(); + conn.close(stmt2).unwrap(); + conn.close(stmt3).unwrap(); + + let status: (Value, u8) = conn + .query_first("SHOW SESSION STATUS LIKE 'Com_stmt_close';") .unwrap() .unwrap(); - assert_eq!(from_row::<(String, usize)>(row).1, 3); + assert_eq!(status.1, 3); } #[test] - fn should_hold_stmt_cache_size_bound() { - let mut opts = OptsBuilder::from_opts(get_opts()); - opts.stmt_cache_size(3); + fn should_hold_stmt_cache_size_bounds() { + let opts = OptsBuilder::from_opts(get_opts()).stmt_cache_size(3); let mut conn = Conn::new(opts).unwrap(); - conn.prepare("DO 1").unwrap(); - conn.prepare("DO 2").unwrap(); - conn.prepare("DO 3").unwrap(); - conn.prepare("DO 1").unwrap(); - conn.prepare("DO 4").unwrap(); - conn.prepare("DO 3").unwrap(); - conn.prepare("DO 5").unwrap(); - conn.prepare("DO 6").unwrap(); - let row = conn - .first("SHOW SESSION STATUS LIKE 'Com_stmt_close';") + + conn.prep("DO 1").unwrap(); + conn.prep("DO 2").unwrap(); + conn.prep("DO 3").unwrap(); + conn.prep("DO 1").unwrap(); + conn.prep("DO 4").unwrap(); + conn.prep("DO 3").unwrap(); + conn.prep("DO 5").unwrap(); + conn.prep("DO 6").unwrap(); + + let status: (String, usize) = conn + .query_first("SHOW SESSION STATUS LIKE 'Com_stmt_close'") .unwrap() .unwrap(); - assert_eq!(from_row::<(String, usize)>(row).1, 3); + + assert_eq!(status.1, 3); + let mut order = conn .stmt_cache .iter() - .map(|(stmt, i)| (stmt.as_ref(), i)) - .collect::>(); + .map(|(_, entry)| &**entry.query.0.as_ref()) + .collect::>(); order.sort(); - assert_eq!(order, &[("DO 3", 5), ("DO 5", 6), ("DO 6", 7)]); + assert_eq!(order, &["DO 3", "DO 5", "DO 6"]); } #[test] @@ -1825,20 +1636,22 @@ mod test { let mut conn = Conn::new(get_opts()).unwrap(); if conn - .query("CREATE TEMPORARY TABLE mysql.tbl(a VARCHAR(32), b JSON)") + .query_drop("CREATE TEMPORARY TABLE mysql.tbl(a VARCHAR(32), b JSON)") .is_err() { - conn.query("CREATE TEMPORARY TABLE mysql.tbl(a VARCHAR(32), b TEXT)") + conn.query_drop("CREATE TEMPORARY TABLE mysql.tbl(a VARCHAR(32), b TEXT)") .unwrap(); } - conn.prep_exec( + conn.exec_drop( r#"INSERT INTO mysql.tbl VALUES ('hello', ?)"#, (Serialized(&decodable),), ) .unwrap(); - let row = conn.first("SELECT a, b FROM mysql.tbl").unwrap().unwrap(); - let (a, b): (String, Json) = from_row(row); + let (a, b): (String, Json) = conn + .query_first("SELECT a, b FROM mysql.tbl") + .unwrap() + .unwrap(); assert_eq!( (a, b), ( @@ -1848,7 +1661,7 @@ mod test { ); let row = conn - .first_exec("SELECT a, b FROM mysql.tbl WHERE a = ?", ("hello",)) + .exec_first("SELECT a, b FROM mysql.tbl WHERE a = ?", ("hello",)) .unwrap() .unwrap(); let (a, Deserialized(b)) = from_row(row); @@ -1880,7 +1693,7 @@ mod test { fn assert_connect_attrs(conn: &mut Conn, expected_values: &[(&str, &str)]) { let mut actual_values = HashMap::new(); - for row in conn.query("SELECT attr_name, attr_value FROM performance_schema.session_account_connect_attrs WHERE processlist_id = connection_id()").unwrap() { + for row in conn.query_iter("SELECT attr_name, attr_value FROM performance_schema.session_account_connect_attrs WHERE processlist_id = connection_id()").unwrap() { let (name, value) = from_row::<(String, String)>(row.unwrap()); actual_values.insert(name, value); } @@ -1912,13 +1725,12 @@ mod test { assert_connect_attrs(&mut conn, &expected_values); // Connect attributes are added. - let mut opts = OptsBuilder::from_opts(get_opts()); + let opts = OptsBuilder::from_opts(get_opts()); let mut connect_attrs = HashMap::with_capacity(3); connect_attrs.insert("foo", "foo val"); connect_attrs.insert("bar", "bar val"); connect_attrs.insert("program_name", "my program name"); - opts.connect_attrs(connect_attrs); - let mut conn = Conn::new(opts).unwrap(); + let mut conn = Conn::new(opts.connect_attrs(connect_attrs)).unwrap(); expected_values.pop(); // remove program_name at the last expected_values.push(("foo", "foo val")); expected_values.push(("bar", "bar val")); @@ -1932,8 +1744,7 @@ mod test { mod bench { use test; - use crate::test_misc::get_opts; - use crate::{params, Conn, Value::NULL}; + use crate::{params, test_misc::get_opts, Conn, Value::NULL}; #[bench] fn simple_exec(bencher: &mut test::Bencher) { diff --git a/src/conn/opts.rs b/src/conn/opts.rs index 1fe7214..9e600e5 100644 --- a/src/conn/opts.rs +++ b/src/conn/opts.rs @@ -1,16 +1,23 @@ +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + use percent_encoding::{percent_decode, percent_decode_str}; use url::Url; -use std::borrow::Cow; -use std::collections::HashMap; -use std::hash::Hash; -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; -use std::path::Path; -use std::str::FromStr; -use std::time::Duration; +use std::{ + borrow::Cow, collections::HashMap, hash::Hash, net::SocketAddr, path::Path, str::FromStr, + time::Duration, +}; + +use crate::{consts::CapabilityFlags, LocalInfileHandler, UrlError}; -use crate::consts::CapabilityFlags; -use crate::{LocalInfileHandler, UrlError}; +/// Default value for client side per-connection statement cache. +pub const DEFAULT_STMT_CACHE_SIZE: usize = 32; /// Ssl Options. #[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] @@ -31,16 +38,13 @@ impl SslOpts { /// ```text /// openssl pkcs12 -password pass: -export -out path.p12 -inkey privatekey.pem -in cert.pem -no-CAfile /// ``` - pub fn set_pkcs12_path>>( - &mut self, - pkcs12_path: Option, - ) -> &mut Self { + pub fn with_pkcs12_path>>(mut self, pkcs12_path: Option) -> Self { self.pkcs12_path = pkcs12_path.map(Into::into); self } /// Sets the password for a pkcs12 archive (defaults to `None`). - pub fn set_password>>(&mut self, password: Option) -> &mut Self { + pub fn with_password>>(mut self, password: Option) -> Self { self.password = password.map(Into::into); self } @@ -53,24 +57,24 @@ impl SslOpts { /// ```text /// openssl x509 -outform der -in rootca.pem -out rootca.der /// ``` - pub fn set_root_cert_path>>( - &mut self, + pub fn with_root_cert_path>>( + mut self, root_cert_path: Option, - ) -> &mut Self { + ) -> Self { self.root_cert_path = root_cert_path.map(Into::into); self } /// The way to not validate the server's domain /// name against its certificate (defaults to `false`). - pub fn set_danger_skip_domain_validation(&mut self, value: bool) -> &mut Self { + pub fn with_danger_skip_domain_validation(mut self, value: bool) -> Self { self.skip_domain_validation = value; self } /// If `true` then client will accept invalid certificate (expired, not trusted, ..) /// (defaults to `false`). - pub fn set_danger_accept_invalid_certs(&mut self, value: bool) -> &mut Self { + pub fn with_danger_accept_invalid_certs(mut self, value: bool) -> Self { self.accept_invalid_certs = value; self } @@ -100,7 +104,7 @@ impl SslOpts { #[derive(Debug, Clone, Eq, PartialEq)] pub(crate) struct InnerOpts { /// Address of mysql server (defaults to `127.0.0.1`). Hostnames should also work. - ip_or_hostname: Option, + ip_or_hostname: url::Host, /// TCP port of mysql server (defaults to `3306`). tcp_port: u16, /// Path to unix socket on unix or pipe name on windows (defaults to `None`). @@ -168,7 +172,8 @@ pub(crate) struct InnerOpts { /// errors. bind_address: Option, - /// Number of prepared statements cached on the client side (per connection). Defaults to `10`. + /// Number of prepared statements cached on the client side (per connection). + /// Defaults to [`DEFAULT_STMT_CACHE_SIZE`]. /// /// Can be defined using `stmt_cache_size` connection url parameter. stmt_cache_size: usize, @@ -205,7 +210,7 @@ pub(crate) struct InnerOpts { impl Default for InnerOpts { fn default() -> Self { InnerOpts { - ip_or_hostname: Some("127.0.0.1".to_string()), + ip_or_hostname: url::Host::Domain(String::from("localhost")), tcp_port: 3306, socket: None, user: None, @@ -221,7 +226,7 @@ impl Default for InnerOpts { local_infile_handler: None, tcp_connect_timeout: None, bind_address: None, - stmt_cache_size: 10, + stmt_cache_size: DEFAULT_STMT_CACHE_SIZE, compress: None, additional_capabilities: CapabilityFlags::empty(), connect_attrs: HashMap::new(), @@ -240,20 +245,10 @@ pub struct Opts(pub(crate) Box); impl Opts { #[doc(hidden)] pub fn addr_is_loopback(&self) -> bool { - if self.0.ip_or_hostname.is_some() { - let v4addr: Option = - FromStr::from_str(self.0.ip_or_hostname.as_ref().unwrap().as_ref()).ok(); - let v6addr: Option = - FromStr::from_str(self.0.ip_or_hostname.as_ref().unwrap().as_ref()).ok(); - if let Some(addr) = v4addr { - addr.is_loopback() - } else if let Some(addr) = v6addr { - addr.is_loopback() - } else { - self.0.ip_or_hostname.as_ref().unwrap() == "localhost" - } - } else { - false + match self.0.ip_or_hostname { + url::Host::Domain(ref name) => name == "localhost", + url::Host::Ipv4(ref addr) => addr.is_loopback(), + url::Host::Ipv6(ref addr) => addr.is_loopback(), } } @@ -261,9 +256,13 @@ impl Opts { from_url(url) } + pub(crate) fn get_host(&self) -> url::Host { + self.0.ip_or_hostname.clone() + } + /// Address of mysql server (defaults to `127.0.0.1`). Hostnames should also work. - pub fn get_ip_or_hostname(&self) -> Option<&str> { - self.0.ip_or_hostname.as_ref().map(|x| &**x) + pub fn get_ip_or_hostname(&self) -> Cow { + self.0.ip_or_hostname.to_string().into() } /// TCP port of mysql server (defaults to `3306`). pub fn get_tcp_port(&self) -> u16 { @@ -348,7 +347,10 @@ impl Opts { self.0.bind_address.as_ref() } - /// Number of prepared statements cached on the client side (per connection). Defaults to `10`. + /// Number of prepared statements cached on the client side (per connection). + /// Defaults to [`DEFAULT_STMT_CACHE_SIZE`]. + /// + /// Can be defined using `stmt_cache_size` connection url parameter. pub fn get_stmt_cache_size(&self) -> usize { self.0.stmt_cache_size } @@ -449,6 +451,7 @@ impl Opts { /// let connection_url = "mysql://root:password@localhost:3307/mysql?prefer_socket=false"; /// let pool = my::Pool::new(connection_url).unwrap(); /// ``` +#[derive(Debug)] pub struct OptsBuilder { opts: Opts, } @@ -463,13 +466,18 @@ impl OptsBuilder { } /// Address of mysql server (defaults to `127.0.0.1`). Hostnames should also work. - pub fn ip_or_hostname>(&mut self, ip_or_hostname: Option) -> &mut Self { - self.opts.0.ip_or_hostname = ip_or_hostname.map(Into::into); + /// + /// **Note:** IPv6 addresses must be given in square brackets, e.g. `[::1]`. + pub fn ip_or_hostname>(mut self, ip_or_hostname: Option) -> Self { + let new = ip_or_hostname.map(Into::into).unwrap_or("127.0.0.1".into()); + self.opts.0.ip_or_hostname = url::Host::parse(&new) + .map(|host| host.to_owned()) + .unwrap_or_else(|_| url::Host::Domain(new.to_owned())); self } /// TCP port of mysql server (defaults to `3306`). - pub fn tcp_port(&mut self, tcp_port: u16) -> &mut Self { + pub fn tcp_port(mut self, tcp_port: u16) -> Self { self.opts.0.tcp_port = tcp_port; self } @@ -477,25 +485,25 @@ impl OptsBuilder { /// Socket path on unix or pipe name on windows (defaults to `None`). /// /// Can be defined using `socket` connection url parameter. - pub fn socket>(&mut self, socket: Option) -> &mut Self { + pub fn socket>(mut self, socket: Option) -> Self { self.opts.0.socket = socket.map(Into::into); self } /// User (defaults to `None`). - pub fn user>(&mut self, user: Option) -> &mut Self { + pub fn user>(mut self, user: Option) -> Self { self.opts.0.user = user.map(Into::into); self } /// Password (defaults to `None`). - pub fn pass>(&mut self, pass: Option) -> &mut Self { + pub fn pass>(mut self, pass: Option) -> Self { self.opts.0.pass = pass.map(Into::into); self } /// Database name (defaults to `None`). - pub fn db_name>(&mut self, db_name: Option) -> &mut Self { + pub fn db_name>(mut self, db_name: Option) -> Self { self.opts.0.db_name = db_name.map(Into::into); self } @@ -504,7 +512,7 @@ impl OptsBuilder { /// /// Note that named pipe connection will ignore duration's `nanos`, and also note that /// it is an error to pass the zero `Duration` to this method. - pub fn read_timeout(&mut self, read_timeout: Option) -> &mut Self { + pub fn read_timeout(mut self, read_timeout: Option) -> Self { self.opts.0.read_timeout = read_timeout; self } @@ -513,7 +521,7 @@ impl OptsBuilder { /// /// Note that named pipe connection will ignore duration's `nanos`, and also note that /// it is likely error to pass the zero `Duration` to this method. - pub fn write_timeout(&mut self, write_timeout: Option) -> &mut Self { + pub fn write_timeout(mut self, write_timeout: Option) -> Self { self.opts.0.write_timeout = write_timeout; self } @@ -522,7 +530,7 @@ impl OptsBuilder { /// `tcp_keepalive_time_ms` url parameter. /// /// Can be defined using `tcp_keepalive_time_ms` connection url parameter. - pub fn tcp_keepalive_time_ms(&mut self, tcp_keepalive_time_ms: Option) -> &mut Self { + pub fn tcp_keepalive_time_ms(mut self, tcp_keepalive_time_ms: Option) -> Self { self.opts.0.tcp_keepalive_time = tcp_keepalive_time_ms; self } @@ -531,7 +539,7 @@ impl OptsBuilder { /// /// Setting this option to false re-enables Nagle's algorithm, which can cause unusually high /// latency (~40ms) but may increase maximum throughput. See #132. - pub fn tcp_nodelay(&mut self, nodelay: bool) -> &mut Self { + pub fn tcp_nodelay(mut self, nodelay: bool) -> Self { self.opts.0.tcp_nodelay = nodelay; self } @@ -545,19 +553,19 @@ impl OptsBuilder { /// Will fall back to TCP on error. Use `socket` option to enforce socket connection. /// /// Can be defined using `prefer_socket` connection url parameter. - pub fn prefer_socket(&mut self, prefer_socket: bool) -> &mut Self { + pub fn prefer_socket(mut self, prefer_socket: bool) -> Self { self.opts.0.prefer_socket = prefer_socket; self } /// Commands to execute on each new database connection. - pub fn init>(&mut self, init: Vec) -> &mut Self { + pub fn init>(mut self, init: Vec) -> Self { self.opts.0.init = init.into_iter().map(Into::into).collect(); self } /// Driver will require SSL connection if this option isn't `None` (default to `None`). - pub fn ssl_opts>>(&mut self, ssl_opts: T) -> &mut Self { + pub fn ssl_opts>>(mut self, ssl_opts: T) -> Self { self.opts.0.ssl_opts = ssl_opts.into(); self } @@ -568,7 +576,7 @@ impl OptsBuilder { /// to receive the contents of that file. /// If unset, the default callback will read files relative to /// the current directory. - pub fn local_infile_handler(&mut self, handler: Option) -> &mut Self { + pub fn local_infile_handler(mut self, handler: Option) -> Self { self.opts.0.local_infile_handler = handler; self } @@ -577,7 +585,7 @@ impl OptsBuilder { /// url parameter. /// /// Can be defined using `tcp_connect_timeout_ms` connection url parameter. - pub fn tcp_connect_timeout(&mut self, timeout: Option) -> &mut Self { + pub fn tcp_connect_timeout(mut self, timeout: Option) -> Self { self.opts.0.tcp_connect_timeout = timeout; self } @@ -586,7 +594,7 @@ impl OptsBuilder { /// /// Use carefully. Will probably make pool unusable because of *address already in use* /// errors. - pub fn bind_address(&mut self, bind_address: Option) -> &mut Self + pub fn bind_address(mut self, bind_address: Option) -> Self where T: Into, { @@ -594,18 +602,17 @@ impl OptsBuilder { self } - /// Number of prepared statements cached on the client side (per connection). Defaults to `10`. + /// Number of prepared statements cached on the client side (per connection). + /// Defaults to [`DEFAULT_STMT_CACHE_SIZE`]. /// - /// Available as `stmt_cache_size` url parameter. + /// Can be defined using `stmt_cache_size` connection url parameter. /// /// Call with `None` to reset to default. - /// - /// Can be defined using `stmt_cache_size` connection url parameter. - pub fn stmt_cache_size(&mut self, cache_size: T) -> &mut Self + pub fn stmt_cache_size(mut self, cache_size: T) -> Self where T: Into>, { - self.opts.0.stmt_cache_size = cache_size.into().unwrap_or(10); + self.opts.0.stmt_cache_size = cache_size.into().unwrap_or(128); self } @@ -620,7 +627,7 @@ impl OptsBuilder { /// "no compression"; /// /// Note that compression level defined here will affect only outgoing packets. - pub fn compress(&mut self, compress: Option) -> &mut Self { + pub fn compress(mut self, compress: Option) -> Self { self.opts.0.compress = compress; self } @@ -635,10 +642,7 @@ impl OptsBuilder { /// won't let you to interfere with capabilities managed by other options (like /// `CLIENT_SSL` or `CLIENT_COMPRESS`). Also note that some capabilities are reserved, /// pointless or may broke the connection, so this option should be used with caution. - pub fn additional_capabilities( - &mut self, - additional_capabilities: CapabilityFlags, - ) -> &mut Self { + pub fn additional_capabilities(mut self, additional_capabilities: CapabilityFlags) -> Self { let forbidden_flags: CapabilityFlags = CapabilityFlags::CLIENT_PROTOCOL_41 | CapabilityFlags::CLIENT_SSL | CapabilityFlags::CLIENT_COMPRESS @@ -687,9 +691,9 @@ impl OptsBuilder { /// [`performance_schema_session_connect_attrs_size`]: https://dev.mysql.com/doc/refman/en/performance-schema-system-variables.html#sysvar_performance_schema_session_connect_attrs_size /// pub fn connect_attrs + Eq + Hash, T2: Into>( - &mut self, + mut self, connect_attrs: HashMap, - ) -> &mut Self { + ) -> Self { self.opts.0.connect_attrs = HashMap::with_capacity(connect_attrs.len()); for (name, value) in connect_attrs { let name = name.into(); @@ -760,12 +764,15 @@ fn from_url_basic(url_str: &str) -> Result<(Opts, Vec<(String, String)>), UrlErr if url.scheme() != "mysql" { return Err(UrlError::UnsupportedScheme(url.scheme().to_string())); } - if url.cannot_be_a_base() || !url.has_host() { + if url.cannot_be_a_base() { return Err(UrlError::BadUrl); } let user = get_opts_user_from_url(&url); let pass = get_opts_pass_from_url(&url); - let ip_or_hostname = url.host_str().map(String::from); + let ip_or_hostname = url + .host() + .ok_or(UrlError::BadUrl) + .and_then(|host| url::Host::parse(&host.to_string()).map_err(|_| UrlError::BadUrl))?; let tcp_port = url.port().unwrap_or(3306); let db_name = get_opts_db_name_from_url(&url); @@ -881,7 +888,7 @@ mod test { Opts(Box::new(InnerOpts { user: Some("us r".to_string()), pass: Some("p w".to_string()), - ip_or_hostname: Some("localhost".to_string()), + ip_or_hostname: url::Host::Domain("localhost".to_string()), tcp_port: 3308, db_name: Some("db-name".to_string()), prefer_socket: false, diff --git a/src/conn/pool.rs b/src/conn/pool.rs index 359e252..7973ed4 100644 --- a/src/conn/pool.rs +++ b/src/conn/pool.rs @@ -1,18 +1,27 @@ -use mysql_common::named_params::parse_named_params; -use mysql_common::row::convert::from_row; - -use std::borrow::Borrow; -use std::collections::VecDeque; -use std::fmt; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Condvar, Mutex}; -use std::time::Duration as StdDuration; - -use crate::prelude::{FromRow, GenericConnection}; -use crate::time::{Duration, SteadyTime}; +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +use std::{ + collections::VecDeque, + fmt, + ops::Deref, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Condvar, Mutex, + }, + time::Duration as StdDuration, +}; + use crate::{ + prelude::*, + time::{Duration, SteadyTime}, Conn, DriverError, Error, IsolationLevel, LocalInfileHandler, Opts, Params, QueryResult, - Result as MyResult, Row, Stmt, Transaction, + Result as MyResult, Statement, Transaction, }; #[derive(Debug)] @@ -56,37 +65,27 @@ impl InnerPool { /// Example of multithreaded `Pool` usage: /// /// ```rust -/// use mysql::{Pool, Opts}; -/// # use mysql::OptsBuilder; -/// use std::thread; -/// -/// fn get_opts() -> Opts { -/// // ... -/// # let url = if let Ok(url) = std::env::var("DATABASE_URL") { -/// # let opts = Opts::from_url(&url).expect("DATABASE_URL invalid"); -/// # if opts.get_db_name().expect("a database name is required").is_empty() { -/// # panic!("database name is empty"); -/// # } -/// # url -/// # } else { -/// # "mysql://root:password@127.0.0.1:3307/mysql".to_string() -/// # }; -/// # Opts::from_url(&*url).unwrap() -/// } -/// +/// # mysql::doctest_wrapper!(__result, { +/// # use mysql::*; +/// # use mysql::prelude::*; +/// # let mut conn = Conn::new(get_opts())?; /// let opts = get_opts(); /// let pool = Pool::new(opts).unwrap(); /// let mut threads = Vec::new(); +/// /// for _ in 0..100 { /// let pool = pool.clone(); -/// threads.push(thread::spawn(move || { -/// let mut result = pool.prep_exec("SELECT 1", ()).unwrap(); -/// assert_eq!(result.next().unwrap().unwrap().unwrap(), vec![1.into()]); +/// threads.push(std::thread::spawn(move || { +/// let mut conn = pool.get_conn().unwrap(); +/// let result: u8 = conn.query_first("SELECT 1").unwrap().unwrap(); +/// assert_eq!(result, 1_u8); /// })); /// } +/// /// for t in threads.into_iter() { /// assert!(t.join().is_ok()); /// } +/// # }); /// ``` /// /// For more info on how to work with mysql connection please look at @@ -232,57 +231,6 @@ impl Pool { self._get_conn(None::, Some(timeout_ms), true) } - fn get_conn_by_stmt>(&self, query: T, call_ping: bool) -> MyResult { - self._get_conn(Some(query), None, call_ping) - } - - /// Will prepare statement. See [`Conn::prepare`](struct.Conn.html#method.prepare). - /// - /// It will try to find connection which has this statement cached. - pub fn prepare>(&self, query: T) -> MyResult> { - let conn = self.get_conn_by_stmt(query.as_ref(), true)?; - conn.pooled_prepare(query) - } - - /// Shortcut for `pool.get_conn()?.prep_exec(..)`. See - /// [`Conn::prep_exec`](struct.Conn.html#method.prep_exec). - /// - /// It will try to find connection which has this statement cached. - pub fn prep_exec(&self, query: A, params: T) -> MyResult> - where - A: AsRef, - T: Into, - { - let conn = self.get_conn_by_stmt(query.as_ref(), false)?; - let params = params.into(); - match conn.pooled_prep_exec(query.as_ref(), params.clone()) { - Ok(stmt) => Ok(stmt), - Err(e) => { - if e.is_connectivity_error() { - let conn = self._get_conn(None::, None, true)?; - conn.pooled_prep_exec(query, params) - } else { - Err(e) - } - } - } - } - - /// See [`Conn::first_exec`](struct.Conn.html#method.first_exec). - pub fn first_exec(&self, query: Q, params: P) -> MyResult> - where - Q: AsRef, - P: Into, - { - self.prep_exec(query, params).and_then(|mut result| { - if let Some(row) = result.next() { - row.map(Some) - } else { - Ok(None) - } - }) - } - /// Shortcut for `pool.get_conn()?.start_transaction(..)`. pub fn start_transaction( &self, @@ -325,33 +273,24 @@ impl fmt::Debug for Pool { /// `Value::Bytes` as a result and `from_value` will need to parse it if you want, for example, `i64` /// /// ```rust -/// # use mysql::{Pool, Opts, OptsBuilder, from_value, Value}; -/// # fn get_opts() -> Opts { -/// # let url = if let Ok(url) = std::env::var("DATABASE_URL") { -/// # let opts = Opts::from_url(&url).expect("DATABASE_URL invalid"); -/// # if opts.get_db_name().expect("a database name is required").is_empty() { -/// # panic!("database name is empty"); -/// # } -/// # url -/// # } else { -/// # "mysql://root:password@127.0.0.1:3307/mysql".to_string() -/// # }; -/// # Opts::from_url(&*url).unwrap() -/// # } -/// # let opts = get_opts(); -/// # let pool = Pool::new(opts).unwrap(); +/// # mysql::doctest_wrapper!(__result, { +/// # use mysql::*; +/// # use mysql::prelude::*; +/// # let mut conn = Conn::new(get_opts())?; +/// let pool = Pool::new(get_opts()).unwrap(); /// let mut conn = pool.get_conn().unwrap(); /// -/// conn.query("SELECT 42").map(|mut result| { -/// let cell = result.next().unwrap().unwrap().take(0).unwrap(); -/// assert_eq!(cell, Value::Bytes(b"42".to_vec())); -/// assert_eq!(from_value::(cell), 42i64); +/// conn.query_first("SELECT 42").map(|result: Option| { +/// let result = result.unwrap(); +/// assert_eq!(result, Value::Bytes(b"42".to_vec())); +/// assert_eq!(from_value::(result), 42i64); /// }).unwrap(); -/// conn.prep_exec("SELECT 42", ()).map(|mut result| { +/// conn.exec_iter("SELECT 42", ()).map(|mut result| { /// let cell = result.next().unwrap().unwrap().take(0).unwrap(); /// assert_eq!(cell, Value::Int(42i64)); /// assert_eq!(from_value::(cell), 42i64); /// }).unwrap(); +/// # }); /// ``` /// /// For more info on how to work with query results please look at @@ -362,6 +301,14 @@ pub struct PooledConn { conn: Option, } +impl Deref for PooledConn { + type Target = Conn; + + fn deref(&self) -> &Self::Target { + self.conn.as_ref().expect("deref after drop") + } +} + impl Drop for PooledConn { fn drop(&mut self) { if self.pool.count.load(Ordering::Relaxed) > self.pool.max.load(Ordering::Relaxed) @@ -379,53 +326,6 @@ impl Drop for PooledConn { } impl PooledConn { - /// Redirects to - /// [`Conn#query`](struct.Conn.html#method.query). - pub fn query>(&mut self, query: T) -> MyResult> { - self.conn.as_mut().unwrap().query(query) - } - - /// See [`Conn::first`](struct.Conn.html#method.first). - pub fn first, U: FromRow>(&mut self, query: T) -> MyResult> { - self.query(query).and_then(|mut result| { - if let Some(row) = result.next() { - row.map(|x| Some(from_row(x))) - } else { - Ok(None) - } - }) - } - - /// See [`Conn::prepare`](struct.Conn.html#method.prepare). - pub fn prepare>(&mut self, query: T) -> MyResult> { - self.conn.as_mut().unwrap().prepare(query) - } - - /// See [`Conn::prep_exec`](struct.Conn.html#method.prep_exec). - pub fn prep_exec(&mut self, query: A, params: T) -> MyResult> - where - A: AsRef, - T: Into, - { - self.conn.as_mut().unwrap().prep_exec(query, params) - } - - /// See [`Conn::first_exec`](struct.Conn.html#method.first_exec). - pub fn first_exec(&mut self, query: Q, params: P) -> MyResult> - where - Q: AsRef, - P: Into, - T: FromRow, - { - self.prep_exec(query, params).and_then(|mut result| { - if let Some(row) = result.next() { - row.map(|x| Some(from_row(x))) - } else { - Ok(None) - } - }) - } - /// Redirects to /// [`Conn#start_transaction`](struct.Conn.html#method.start_transaction) pub fn start_transaction( @@ -458,26 +358,6 @@ impl PooledConn { self.conn.take().unwrap() } - fn pooled_prepare<'a, T: AsRef>(mut self, query: T) -> MyResult> { - let query = query.as_ref(); - let (named_params, real_query) = parse_named_params(query)?; - self.as_mut() - ._prepare(real_query.borrow(), named_params) - .map(|stmt| Stmt::new_pooled(stmt, self)) - } - - fn pooled_prep_exec<'a, A, T>(mut self, query: A, params: T) -> MyResult> - where - A: AsRef, - T: Into, - { - let query = query.as_ref(); - let (named_params, real_query) = parse_named_params(query)?; - let stmt = self.as_mut()._prepare(real_query.borrow(), named_params)?; - let stmt = Stmt::new_pooled(stmt, self); - stmt.prep_exec(params) - } - fn pooled_start_transaction<'a>( mut self, consistent_snapshot: bool, @@ -500,34 +380,25 @@ impl PooledConn { } } -impl GenericConnection for PooledConn { - fn query>(&mut self, query: T) -> MyResult> { - self.query(query) - } - - fn first, U: FromRow>(&mut self, query: T) -> MyResult> { - self.first(query) +impl Queryable for PooledConn { + fn query_iter>(&mut self, query: T) -> MyResult> { + self.conn.as_mut().unwrap().query_iter(query) } - fn prepare>(&mut self, query: T) -> MyResult> { - self.prepare(query) + fn prep>(&mut self, query: T) -> MyResult { + self.conn.as_mut().unwrap().prep(query) } - fn prep_exec(&mut self, query: A, params: T) -> MyResult> - where - A: AsRef, - T: Into, - { - self.prep_exec(query, params) + fn close(&mut self, stmt: Statement) -> Result<(), Error> { + self.conn.as_mut().unwrap().close(stmt) } - fn first_exec(&mut self, query: Q, params: P) -> MyResult> + fn exec_iter(&mut self, stmt: S, params: P) -> MyResult> where - Q: AsRef, + S: AsStatement, P: Into, - T: FromRow, { - self.first_exec(query, params) + self.conn.as_mut().unwrap().exec_iter(stmt, params) } } @@ -537,27 +408,46 @@ mod test { mod pool { use std::{thread, time::Duration}; - use crate::test_misc::get_opts; - use crate::{from_row, from_value, params, DriverError, Error, OptsBuilder, Pool}; + use crate::{ + from_value, prelude::*, test_misc::get_opts, DriverError, Error, OptsBuilder, Pool, + }; #[test] fn multiple_pools_should_work() { let pool = Pool::new(get_opts()).unwrap(); - pool.prep_exec("DROP DATABASE IF EXISTS A", ()).unwrap(); - pool.prep_exec("CREATE DATABASE A", ()).unwrap(); - pool.prep_exec("DROP TABLE IF EXISTS A.a", ()).unwrap(); - pool.prep_exec("CREATE TABLE IF NOT EXISTS A.a (id INT)", ()) + pool.get_conn() + .unwrap() + .exec_drop("DROP DATABASE IF EXISTS A", ()) + .unwrap(); + pool.get_conn() + .unwrap() + .exec_drop("CREATE DATABASE A", ()) + .unwrap(); + pool.get_conn() + .unwrap() + .exec_drop("DROP TABLE IF EXISTS A.a", ()) + .unwrap(); + pool.get_conn() + .unwrap() + .exec_drop("CREATE TABLE IF NOT EXISTS A.a (id INT)", ()) .unwrap(); - pool.prep_exec("INSERT INTO A.a VALUES (1)", ()).unwrap(); - let mut builder = OptsBuilder::from_opts(get_opts()); - builder.db_name(Some("A")); - let pool2 = Pool::new(builder).unwrap(); - let row = pool2 - .first_exec("SELECT COUNT(*) FROM a", ()) + pool.get_conn() .unwrap() + .exec_drop("INSERT INTO A.a VALUES (1)", ()) + .unwrap(); + let opts = OptsBuilder::from_opts(get_opts()).db_name(Some("A")); + let pool2 = Pool::new(opts).unwrap(); + let count: u8 = pool2 + .get_conn() + .unwrap() + .exec_first("SELECT COUNT(*) FROM a", ()) + .unwrap() + .unwrap(); + assert_eq!(1, count); + pool.get_conn() + .unwrap() + .exec_drop("DROP DATABASE A", ()) .unwrap(); - assert_eq!((1u8,), from_row(row)); - pool.prep_exec("DROP DATABASE A", ()).unwrap(); } struct A { @@ -576,15 +466,19 @@ mod test { let pool = Pool::new_manual(2, 2, get_opts()).unwrap(); let mut conn = pool.get_conn().unwrap(); - let row = pool - .first_exec("SELECT CONNECTION_ID();", ()) + let id: u32 = pool + .get_conn() + .unwrap() + .exec_first("SELECT CONNECTION_ID();", ()) .unwrap() .unwrap(); - let (id,): (u32,) = from_row(row); - conn.prep_exec("KILL CONNECTION ?", (id,)).unwrap(); + conn.exec_drop("KILL CONNECTION ?", (id,)).unwrap(); thread::sleep(Duration::from_millis(250)); - pool.prepare("SHOW FULL PROCESSLIST").unwrap(); + pool.get_conn() + .unwrap() + .prep("SHOW FULL PROCESSLIST") + .unwrap(); } #[test] @@ -592,28 +486,33 @@ mod test { let pool = Pool::new_manual(2, 2, get_opts()).unwrap(); let mut conn = pool.get_conn().unwrap(); - let row = pool - .first_exec("SELECT CONNECTION_ID();", ()) + let id: u32 = pool + .get_conn() + .unwrap() + .exec_first("SELECT CONNECTION_ID();", ()) .unwrap() .unwrap(); - let (id,): (u32,) = from_row(row); - conn.prep_exec("KILL CONNECTION ?", (id,)).unwrap(); + conn.exec_drop("KILL CONNECTION ?", (id,)).unwrap(); thread::sleep(Duration::from_millis(250)); - pool.prep_exec("SHOW FULL PROCESSLIST", ()).unwrap(); + pool.get_conn() + .unwrap() + .exec_drop("SHOW FULL PROCESSLIST", ()) + .unwrap(); } #[test] fn should_fix_connectivity_errors_on_start_transaction() { let pool = Pool::new_manual(2, 2, get_opts()).unwrap(); let mut conn = pool.get_conn().unwrap(); - let row = pool - .first_exec("SELECT CONNECTION_ID();", ()) + let id: u32 = pool + .get_conn() + .unwrap() + .exec_first("SELECT CONNECTION_ID();", ()) .unwrap() .unwrap(); - let (id,): (u32,) = from_row(row); - conn.prep_exec("KILL CONNECTION ?", (id,)).unwrap(); + conn.exec_drop("KILL CONNECTION ?", (id,)).unwrap(); thread::sleep(Duration::from_millis(250)); pool.start_transaction(false, None, None).unwrap(); } @@ -627,7 +526,7 @@ mod test { let conn = pool.get_conn(); assert!(conn.is_ok()); let mut conn = conn.unwrap(); - assert!(conn.query("SELECT 1").is_ok()); + conn.query_drop("SELECT 1").unwrap(); })); } for t in threads.into_iter() { @@ -647,6 +546,7 @@ mod test { drop(conn1); assert!(pool.try_get_conn(357).is_ok()); } + #[test] fn should_execute_statements_on_PooledConn() { let pool = Pool::new(get_opts()).unwrap(); @@ -655,8 +555,8 @@ mod test { let pool = pool.clone(); threads.push(thread::spawn(move || { let mut conn = pool.get_conn().unwrap(); - let mut stmt = conn.prepare("SELECT 1").unwrap(); - assert!(stmt.execute(()).is_ok()); + let stmt = conn.prep("SELECT 1").unwrap(); + conn.exec_drop(&stmt, ()).unwrap(); })); } for t in threads.into_iter() { @@ -669,104 +569,67 @@ mod test { let pool = pool.clone(); threads.push(thread::spawn(move || { let mut conn = pool.get_conn().unwrap(); - conn.prep_exec("SELECT ?", (1,)).unwrap(); + conn.exec_drop("SELECT ?", (1,)).unwrap(); })); } for t in threads.into_iter() { assert!(t.join().is_ok()); } } - #[test] - fn should_execute_statements_on_Pool() { - let pool = Pool::new(get_opts()).unwrap(); - let mut threads = Vec::new(); - for _ in 0usize..10 { - let pool = pool.clone(); - threads.push(thread::spawn(move || { - let mut stmt = pool.prepare("SELECT 1").unwrap(); - assert!(stmt.execute(()).is_ok()); - })); - } - for t in threads.into_iter() { - assert!(t.join().is_ok()); - } - let pool = Pool::new(get_opts()).unwrap(); - let mut threads = Vec::new(); - for _ in 0usize..10 { - let pool = pool.clone(); - threads.push(thread::spawn(move || { - pool.prep_exec("SELECT ?", (1,)).unwrap(); - })); - } - for t in threads.into_iter() { - assert!(t.join().is_ok()); - } - pool.prep_exec("SELECT 1", ()) - .and_then(|mut res1| { - pool.prep_exec("SELECT 2", ()).map(|mut res2| { - let (x1,) = from_row::<(u8,)>(res1.next().unwrap().unwrap()); - let (x2,) = from_row::<(u8,)>(res2.next().unwrap().unwrap()); - assert_eq!(1, x1); - assert_eq!(2, x2); - }) - }) - .unwrap() - } #[test] #[allow(unused_variables)] fn should_start_transaction_on_Pool() { let pool = Pool::new_manual(1, 10, get_opts()).unwrap(); - pool.prepare("CREATE TEMPORARY TABLE mysql.tbl(a INT)") - .ok() - .map(|mut stmt| { - assert!(stmt.execute(()).is_ok()); - }); + pool.get_conn() + .unwrap() + .query_drop("CREATE TEMPORARY TABLE mysql.tbl(a INT)") + .unwrap(); pool.start_transaction(false, None, None) .and_then(|mut t| { - t.query("INSERT INTO mysql.tbl(a) VALUES(1)").unwrap(); - t.query("INSERT INTO mysql.tbl(a) VALUES(2)").unwrap(); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(1)").unwrap(); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(2)").unwrap(); t.commit() }) .unwrap(); - pool.prepare("SELECT COUNT(a) FROM mysql.tbl") - .ok() - .map(|mut stmt| { - for x in stmt.execute(()).unwrap() { - let mut x = x.unwrap(); - assert_eq!(from_value::(x.take(0).unwrap()), 2u8); - } - }); + assert_eq!( + pool.get_conn() + .unwrap() + .query_first::("SELECT COUNT(a) FROM mysql.tbl") + .unwrap() + .unwrap(), + 2_u8 + ); pool.start_transaction(false, None, None) .and_then(|mut t| { - t.query("INSERT INTO mysql.tbl(a) VALUES(1)").unwrap(); - t.query("INSERT INTO mysql.tbl(a) VALUES(2)").unwrap(); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(1)").unwrap(); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(2)").unwrap(); t.rollback() }) .unwrap(); - pool.prepare("SELECT COUNT(a) FROM mysql.tbl") - .ok() - .map(|mut stmt| { - for x in stmt.execute(()).unwrap() { - let mut x = x.unwrap(); - assert_eq!(from_value::(x.take(0).unwrap()), 2u8); - } - }); + assert_eq!( + pool.get_conn() + .unwrap() + .query_first::("SELECT COUNT(a) FROM mysql.tbl") + .unwrap() + .unwrap(), + 2_u8 + ); pool.start_transaction(false, None, None) .and_then(|mut t| { - t.query("INSERT INTO mysql.tbl(a) VALUES(1)").unwrap(); - t.query("INSERT INTO mysql.tbl(a) VALUES(2)").unwrap(); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(1)").unwrap(); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(2)").unwrap(); Ok(()) }) .unwrap(); - pool.prepare("SELECT COUNT(a) FROM mysql.tbl") - .ok() - .map(|mut stmt| { - for x in stmt.execute(()).unwrap() { - let mut x = x.unwrap(); - assert_eq!(from_value::(x.take(0).unwrap()), 2u8); - } - }); + assert_eq!( + pool.get_conn() + .unwrap() + .query_first::("SELECT COUNT(a) FROM mysql.tbl") + .unwrap() + .unwrap(), + 2_u8 + ); let mut a = A { pool, x: 0 }; let transaction = a.pool.start_transaction(false, None, None).unwrap(); a.add(); @@ -776,71 +639,42 @@ mod test { fn should_start_transaction_on_PooledConn() { let pool = Pool::new(get_opts()).unwrap(); let mut conn = pool.get_conn().unwrap(); - assert!(conn - .query("CREATE TEMPORARY TABLE mysql.tbl(a INT)") - .is_ok()); - assert!(conn - .start_transaction(false, None, None) + conn.query_drop("CREATE TEMPORARY TABLE mysql.tbl(a INT)") + .unwrap(); + conn.start_transaction(false, None, None) .and_then(|mut t| { - assert!(t.query("INSERT INTO mysql.tbl(a) VALUES(1)").is_ok()); - assert!(t.query("INSERT INTO mysql.tbl(a) VALUES(2)").is_ok()); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(1)").unwrap(); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(2)").unwrap(); t.commit() }) - .is_ok()); - for x in conn.query("SELECT COUNT(a) FROM mysql.tbl").unwrap() { + .unwrap(); + for x in conn.query_iter("SELECT COUNT(a) FROM mysql.tbl").unwrap() { let mut x = x.unwrap(); assert_eq!(from_value::(x.take(0).unwrap()), 2u8); } - assert!(conn - .start_transaction(false, None, None) + conn.start_transaction(false, None, None) .and_then(|mut t| { - assert!(t.query("INSERT INTO mysql.tbl(a) VALUES(1)").is_ok()); - assert!(t.query("INSERT INTO mysql.tbl(a) VALUES(2)").is_ok()); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(1)").unwrap(); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(2)").unwrap(); t.rollback() }) - .is_ok()); - for x in conn.query("SELECT COUNT(a) FROM mysql.tbl").unwrap() { + .unwrap(); + for x in conn.query_iter("SELECT COUNT(a) FROM mysql.tbl").unwrap() { let mut x = x.unwrap(); assert_eq!(from_value::(x.take(0).unwrap()), 2u8); } - assert!(conn - .start_transaction(false, None, None) + conn.start_transaction(false, None, None) .and_then(|mut t| { - assert!(t.query("INSERT INTO mysql.tbl(a) VALUES(1)").is_ok()); - assert!(t.query("INSERT INTO mysql.tbl(a) VALUES(2)").is_ok()); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(1)").unwrap(); + t.query_drop("INSERT INTO mysql.tbl(a) VALUES(2)").unwrap(); Ok(()) }) - .is_ok()); - for x in conn.query("SELECT COUNT(a) FROM mysql.tbl").unwrap() { + .unwrap(); + for x in conn.query_iter("SELECT COUNT(a) FROM mysql.tbl").unwrap() { let mut x = x.unwrap(); assert_eq!(from_value::(x.take(0).unwrap()), 2u8); } } - #[test] - fn should_work_with_named_params() { - let pool = Pool::new(get_opts()).unwrap(); - pool.prepare("SELECT :a, :b, :a + :b, :abc") - .map(|mut stmt| { - let mut result = stmt - .execute(params! { - "a" => 1, - "b" => 2, - "abc" => 4, - }) - .unwrap(); - let row = result.next().unwrap().unwrap(); - assert_eq!((1, 2, 3, 4), from_row(row)); - }) - .unwrap(); - - let params = params! {"a" => 1, "b" => 2, "abc" => 4}; - pool.prep_exec("SELECT :a, :b, :a+:b, :abc", params) - .map(|mut result| { - let row = result.next().unwrap().unwrap(); - assert_eq!((1, 2, 3, 4), from_row(row)); - }) - .unwrap(); - } #[cfg(feature = "nightly")] mod bench { @@ -848,8 +682,7 @@ mod test { use std::thread; - use crate::test_misc::get_opts; - use crate::Pool; + use crate::{test_misc::get_opts, Pool}; #[bench] fn many_prepares(bencher: &mut test::Bencher) { diff --git a/src/conn/query_result.rs b/src/conn/query_result.rs index 17bce3e..58562e2 100644 --- a/src/conn/query_result.rs +++ b/src/conn/query_result.rs @@ -1,39 +1,16 @@ -use fnv::FnvHasher; -use mysql_common::row::new_row; - -use std::collections::hash_map::HashMap; -use std::hash::BuildHasherDefault as BldHshrDflt; -use std::ops::{Deref, DerefMut}; -use std::sync::Arc; - -use crate::{Column, Conn, Result as MyResult, Row, Stmt}; - -/// Possible ways to pass conn to a query result -#[derive(Debug)] -pub enum ResultConnRef<'a> { - ViaConnRef(&'a mut Conn), - ViaStmt(Stmt<'a>), -} +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. -impl<'a> Deref for ResultConnRef<'a> { - type Target = Conn; +use mysql_common::row::new_row; - fn deref(&self) -> &Conn { - match *self { - ResultConnRef::ViaConnRef(ref conn_ref) => conn_ref, - ResultConnRef::ViaStmt(ref stmt) => stmt.conn.deref(), - } - } -} +use std::{collections::hash_map::HashMap, sync::Arc}; -impl<'a> DerefMut for ResultConnRef<'a> { - fn deref_mut(&mut self) -> &mut Conn { - match *self { - ResultConnRef::ViaConnRef(ref mut conn_ref) => conn_ref, - ResultConnRef::ViaStmt(ref mut stmt) => stmt.conn.deref_mut(), - } - } -} +use crate::{Column, Conn, Result as MyResult, Row}; /// Mysql result set for text and binary protocols. /// @@ -44,46 +21,17 @@ impl<'a> DerefMut for ResultConnRef<'a> { /// [`Row`](struct.Row.html) you should rely on `FromRow` trait implemented for tuples of /// `FromValue` implementors up to arity 12, or on `FromValue` trait for rows with higher arity. /// -/// ```rust -/// # use mysql::Pool; -/// # use mysql::{Opts, OptsBuilder, from_row}; -/// # fn get_opts() -> Opts { -/// # let url = if let Ok(url) = std::env::var("DATABASE_URL") { -/// # let opts = Opts::from_url(&url).expect("DATABASE_URL invalid"); -/// # if opts.get_db_name().expect("a database name is required").is_empty() { -/// # panic!("database name is empty"); -/// # } -/// # url -/// # } else { -/// # "mysql://root:password@127.0.0.1:3307/mysql".to_string() -/// # }; -/// # Opts::from_url(&*url).unwrap() -/// # } -/// # let opts = get_opts(); -/// # let pool = Pool::new(opts).unwrap(); -/// let mut conn = pool.get_conn().unwrap(); -/// -/// for row in conn.prep_exec("SELECT ?, ?", (42, 2.5)).unwrap() { -/// let (a, b) = from_row(row.unwrap()); -/// assert_eq!((a, b), (42u8, 2.5_f32)); -/// } -/// ``` -/// /// For more info on how to work with values please look at /// [`Value`](../value/enum.Value.html) documentation. #[derive(Debug)] pub struct QueryResult<'a> { - conn: ResultConnRef<'a>, + conn: &'a mut Conn, columns: Arc>, is_bin: bool, } impl<'a> QueryResult<'a> { - pub(crate) fn new( - conn: ResultConnRef<'a>, - columns: Vec, - is_bin: bool, - ) -> QueryResult<'a> { + pub(crate) fn new(conn: &'a mut Conn, columns: Vec, is_bin: bool) -> QueryResult<'a> { QueryResult { conn, columns: Arc::new(columns), @@ -105,23 +53,17 @@ impl<'a> QueryResult<'a> { } } - /// Returns - /// [`OkPacket`'s](http://dev.mysql.com/doc/internals/en/packet-OK_Packet.html) - /// affected rows. + /// Returns number affected rows for the current result set. pub fn affected_rows(&self) -> u64 { self.conn.affected_rows } - /// Returns - /// [`OkPacket`'s](http://dev.mysql.com/doc/internals/en/packet-OK_Packet.html) - /// last insert id. + /// Returns the last insert id for the current result set. pub fn last_insert_id(&self) -> u64 { self.conn.last_insert_id } - /// Returns - /// [`OkPacket`'s](http://dev.mysql.com/doc/internals/en/packet-OK_Packet.html) - /// warnings count. + /// Returns warnings count for the current result set. pub fn warnings(&self) -> u16 { self.conn.warnings } @@ -148,8 +90,8 @@ impl<'a> QueryResult<'a> { None } - /// Returns HashMap which maps column names to column indexes. - pub fn column_indexes(&self) -> HashMap> { + /// Returns a `HashMap` which maps column names to column indexes. + pub fn column_indexes(&self) -> HashMap { let mut indexes = HashMap::default(); for (i, column) in self.columns.iter().enumerate() { indexes.insert(column.name_str().into_owned(), i); @@ -157,34 +99,40 @@ impl<'a> QueryResult<'a> { indexes } - /// Returns a slice of a [`Column`s](struct.Column.html) which represents - /// `QueryResult`'s columns if any. + /// Returns a list of columns in the current result set. pub fn columns_ref(&self) -> &[Column] { self.columns.as_ref() } - /// This predicate will help you to consume multiple result sets. + /// Returns `true` if this query result contains another result set, + /// or if last result set isn't fully consumed. /// /// # Note /// - /// Note that it'll also return `true` for unconsumed singe-result set. + /// Note, that the metadata of a query result (number of affected rows, last insert id etc.) + /// will change on the result set boundary, i.e: + /// + /// ```rust + /// # mysql::doctest_wrapper!(__result, { + /// # use mysql::*; + /// # use mysql::prelude::*; + /// # let mut conn = Conn::new(get_opts())?; + /// let mut result = conn.query_iter("SELECT 1; SELECT 'foo', 'bar';")?; + /// + /// // First result set contains one column. + /// assert_eq!(result.columns_ref().len(), 1); + /// for row in result.by_ref() {} // first result set was consumed + /// + /// // More results exists + /// assert!(result.more_results_exists()); /// - /// # Example + /// // Second result set contains two columns. + /// assert_eq!(result.columns_ref().len(), 2); + /// for row in result.by_ref() {} // second result set was consumed /// - /// ```ignore - /// conn.query(r#" - /// CREATE PROCEDURE multi() BEGIN - /// SELECT 1; - /// SELECT 2; - /// END - /// "#); - /// let mut result = conn.query("CALL multi()").unwrap(); - /// while result.more_results_exists() { - /// for x in result.by_ref() { - /// // On first iteration of `while` you will get result set from - /// // SELECT 1 and from SELECT 2 on second. - /// } - /// } + /// // No more results + /// assert!(result.more_results_exists() == false); + /// # }); /// ``` pub fn more_results_exists(&self) -> bool { self.conn.more_results_exists() || !self.consumed() diff --git a/src/conn/stmt.rs b/src/conn/stmt.rs index 88f823b..6eb3bbe 100644 --- a/src/conn/stmt.rs +++ b/src/conn/stmt.rs @@ -1,226 +1,126 @@ -use mysql_common::packets::ComStmtClose; +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. -use std::io; +use mysql_common::packets::{parse_stmt_packet, StmtPacket}; -use crate::conn::query_result::ResultConnRef; -use crate::conn::ConnRef; -use crate::{from_row, prelude::FromRow, Column, Conn, Params, PooledConn, QueryResult, Result}; +use std::{borrow::Cow, io, sync::Arc}; -#[derive(Eq, PartialEq, Clone, Debug)] +use crate::{prelude::*, Column, Result}; + +#[derive(Debug, Clone, Eq, PartialEq)] pub struct InnerStmt { - /// Positions and names of named parameters - named_params: Option>, - params: Option>, columns: Option>, - statement_id: u32, - num_columns: u16, - num_params: u16, - warning_count: u16, + params: Option>, + stmt_packet: StmtPacket, + connection_id: u32, } impl InnerStmt { - pub fn from_payload(pld: &[u8], named_params: Option>) -> io::Result { - let stmt_packet = mysql_common::packets::parse_stmt_packet(pld)?; + pub fn from_payload(pld: &[u8], connection_id: u32) -> io::Result { + let stmt_packet = parse_stmt_packet(pld)?; Ok(InnerStmt { - named_params, - statement_id: stmt_packet.statement_id(), - num_columns: stmt_packet.num_columns(), - num_params: stmt_packet.num_params(), - warning_count: stmt_packet.warning_count(), - params: None, columns: None, + params: None, + stmt_packet, + connection_id, }) } - pub fn set_named_params(&mut self, named_params: Option>) { - self.named_params = named_params; - } - - pub fn set_params(&mut self, params: Option>) { + pub fn with_params(mut self, params: Option>) -> Self { self.params = params; + self } - pub fn set_columns(&mut self, columns: Option>) { - self.columns = columns + pub fn with_columns(mut self, columns: Option>) -> Self { + self.columns = columns; + self } - pub fn columns(&self) -> Option<&[Column]> { - self.columns.as_ref().map(AsRef::as_ref) + pub fn columns(&self) -> &[Column] { + self.columns.as_ref().map(AsRef::as_ref).unwrap_or(&[]) } - pub fn params(&self) -> Option<&[Column]> { - self.params.as_ref().map(AsRef::as_ref) + pub fn params(&self) -> &[Column] { + self.params.as_ref().map(AsRef::as_ref).unwrap_or(&[]) } pub fn id(&self) -> u32 { - self.statement_id + self.stmt_packet.statement_id() } - pub fn num_params(&self) -> u16 { - self.num_params + pub const fn connection_id(&self) -> u32 { + self.connection_id } - pub fn num_columns(&self) -> u16 { - self.num_columns + pub fn num_params(&self) -> u16 { + self.stmt_packet.num_params() } - pub fn named_params(&self) -> Option<&Vec> { - self.named_params.as_ref() + pub fn num_columns(&self) -> u16 { + self.stmt_packet.num_columns() } } -/// Mysql [prepared statement][1]. -/// -/// [1]: http://dev.mysql.com/doc/internals/en/prepared-statements.html -#[derive(Debug)] -pub struct Stmt<'a> { - stmt: InnerStmt, - pub(crate) conn: ConnRef<'a>, +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Statement { + pub(crate) inner: Arc, + pub(crate) named_params: Option>, } -impl<'a> Stmt<'a> { - pub(crate) fn new(stmt: InnerStmt, conn: &'a mut Conn) -> Stmt<'a> { - Stmt { - stmt, - conn: ConnRef::ViaConnRef(conn), +impl Statement { + pub(crate) fn new(inner: Arc, named_params: Option>) -> Self { + Self { + inner, + named_params, } } - pub(crate) fn new_pooled(stmt: InnerStmt, pooled_conn: PooledConn) -> Stmt<'a> { - Stmt { - stmt, - conn: ConnRef::ViaPooledConn(pooled_conn), - } + pub fn columns(&self) -> &[Column] { + self.inner.columns() } - /// Returns a slice of a [`Column`s](struct.Column.html) which represents - /// `Stmt`'s params if any. - pub fn params_ref(&self) -> Option<&[Column]> { - self.stmt.params() + pub fn params(&self) -> &[Column] { + self.inner.params() } - /// Returns a slice of a [`Column`s](struct.Column.html) which represents - /// `Stmt`'s columns if any. - pub fn columns_ref(&self) -> Option<&[Column]> { - self.stmt.columns() + pub fn id(&self) -> u32 { + self.inner.id() } - /// Returns index of a `Stmt`'s column by name. - pub fn column_index>(&self, name: T) -> Option { - match self.stmt.columns() { - None => None, - Some(columns) => { - let name = name.as_ref().as_bytes(); - for (i, c) in columns.iter().enumerate() { - if c.name_ref() == name { - return Some(i); - } - } - None - } - } + pub fn connection_id(&self) -> u32 { + self.inner.connection_id() } - /// Executes prepared statement with parameters passed as a [`Into`] implementor. - /// - /// ```rust - /// # use mysql::{Pool, Opts, OptsBuilder, from_value, from_row, Value}; - /// # use mysql::prelude::ToValue; - /// # use std::default::Default; - /// # use std::iter::repeat; - /// # fn get_opts() -> Opts { - /// # let url = if let Ok(url) = std::env::var("DATABASE_URL") { - /// # let opts = Opts::from_url(&url).expect("DATABASE_URL invalid"); - /// # if opts.get_db_name().expect("a database name is required").is_empty() { - /// # panic!("database name is empty"); - /// # } - /// # url - /// # } else { - /// # "mysql://root:password@127.0.0.1:3307/mysql".to_string() - /// # }; - /// # Opts::from_url(&*url).unwrap() - /// # } - /// # let opts = get_opts(); - /// # let pool = Pool::new(opts).unwrap(); - /// let mut stmt0 = pool.prepare("SELECT 42").unwrap(); - /// let mut stmt1 = pool.prepare("SELECT ?").unwrap(); - /// let mut stmt2 = pool.prepare("SELECT ?, ?").unwrap(); - /// let mut stmt13 = pool.prepare("SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?").unwrap(); - /// - /// // It is better to pass params as a tuple when executing statements of arity <= 12 - /// for row in stmt0.execute(()).unwrap() { - /// let cell = from_row::(row.unwrap()); - /// assert_eq!(cell, 42u8); - /// } - /// // just do not forget about trailing comma in case of arity = 1 - /// for row in stmt1.execute((42,)).unwrap() { - /// let cell = from_row::(row.unwrap()); - /// assert_eq!(cell, 42u8); - /// } - /// - /// // If you don't want to lose ownership of param, then you should pass it by reference - /// let word = "hello".to_string(); - /// for row in stmt2.execute((&word, &word)).unwrap() { - /// let (cell1, cell2) = from_row::<(String, String)>(row.unwrap()); - /// assert_eq!(cell1, "hello"); - /// assert_eq!(cell2, "hello"); - /// } - /// - /// // If you want to execute statement of arity > 12, then you can pass params as &[&ToValue]. - /// let params: &[&dyn ToValue] = &[&1, &2, &3, &4, &5, &6, &7, &8, &9, &10, &11, &12, &13]; - /// for row in stmt13.execute(params).unwrap() { - /// let row: Vec = row.unwrap().unwrap().into_iter().map(from_value::).collect(); - /// assert_eq!(row, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); - /// } - /// // but be aware of implicit copying, so if you have huge params and do not care - /// // about ownership, then better to use plain Vec. - /// let mut params: Vec = Vec::with_capacity(13); - /// for i in 1..14 { - /// params.push(repeat('A').take(i * 1000).collect::().into()); - /// } - /// for row in stmt13.execute(params).unwrap() { - /// let row = row.unwrap(); - /// let row: Vec = row.unwrap().into_iter().map(from_value::).collect(); - /// for i in 1..14 { - /// assert_eq!(row[i-1], repeat('A').take(i * 1000).collect::()); - /// } - /// } - /// ``` - pub fn execute>(&mut self, params: T) -> Result { - self.conn.execute(&self.stmt, params) - } - - /// See [`Conn::first_exec`](struct.Conn.html#method.first_exec). - pub fn first_exec(&mut self, params: P) -> Result> - where - P: Into, - T: FromRow, - { - self.execute(params).and_then(|mut result| { - if let Some(row) = result.next() { - row.map(|x| Some(from_row(x))) - } else { - Ok(None) - } - }) + pub fn num_params(&self) -> u16 { + self.inner.num_params() } - pub(crate) fn prep_exec>(mut self, params: T) -> Result> { - let columns = self.conn._execute(&self.stmt, params.into())?; - Ok(QueryResult::new( - ResultConnRef::ViaStmt(self), - columns, - true, - )) + pub fn num_columns(&self) -> u16 { + self.inner.num_columns() } } -impl<'a> Drop for Stmt<'a> { - fn drop(&mut self) { - if self.conn.stmt_cache.get_cap() == 0 { - let com_stmt_close = ComStmtClose::new(self.stmt.id()); - let _ = self.conn.write_command_raw(com_stmt_close); - } +impl AsStatement for Statement { + fn as_statement(&self, _queryable: &mut Q) -> Result> { + Ok(Cow::Borrowed(self)) + } +} + +impl<'a> AsStatement for &'a Statement { + fn as_statement(&self, _queryable: &mut Q) -> Result> { + Ok(Cow::Borrowed(self)) + } +} + +impl> AsStatement for T { + fn as_statement(&self, queryable: &mut Q) -> Result> { + let statement = queryable.prep(self.as_ref())?; + Ok(Cow::Owned(statement)) } } diff --git a/src/conn/stmt_cache.rs b/src/conn/stmt_cache.rs index 683ce65..9f50648 100644 --- a/src/conn/stmt_cache.rs +++ b/src/conn/stmt_cache.rs @@ -1,88 +1,119 @@ +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +use lru::LruCache; use twox_hash::XxHash; -use std::borrow::Borrow; -use std::collections::HashMap; -use std::hash::{BuildHasherDefault, Hash}; +use std::{ + borrow::Borrow, + collections::HashMap, + hash::{BuildHasherDefault, Hash}, + sync::Arc, +}; use crate::conn::stmt::InnerStmt; +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct QueryString(pub Arc); + +impl Borrow for QueryString { + fn borrow(&self) -> &str { + &**self.0.as_ref() + } +} + +impl PartialEq for QueryString { + fn eq(&self, other: &str) -> bool { + &**self.0.as_ref() == other + } +} + +pub struct Entry { + pub stmt: Arc, + pub query: QueryString, +} + #[derive(Debug)] pub struct StmtCache { cap: usize, - map: HashMap>, - iter: u64, + cache: LruCache, + query_map: HashMap>, } impl StmtCache { pub fn new(cap: usize) -> StmtCache { StmtCache { cap, - map: Default::default(), - iter: 0, + cache: LruCache::unbounded(), + query_map: Default::default(), } } - pub fn contains(&self, key: &T) -> bool + pub fn contains_query(&self, key: &T) -> bool where - String: Borrow, + QueryString: Borrow, T: Hash + Eq, T: ?Sized, { - self.map.contains_key(key) + self.query_map.contains_key(key) } - pub fn get(&mut self, key: &T) -> Option<&InnerStmt> + pub fn by_query(&mut self, query: &T) -> Option<&Entry> where - String: Borrow, - String: PartialEq, + QueryString: Borrow, + QueryString: PartialEq, T: Hash + Eq, T: ?Sized, { - if let Some(&mut (ref mut last, ref st)) = self.map.get_mut(key) { - *last = self.iter; - self.iter += 1; - Some(st) - } else { - None + let id = self.query_map.get(query).cloned(); + match id { + Some(id) => self.cache.get(&id), + None => None, } } - pub fn put(&mut self, key: String, value: InnerStmt) -> Option { + pub fn put(&mut self, query: Arc, stmt: Arc) -> Option> { if self.cap == 0 { return None; } - self.map.insert(key, (self.iter, value)); - self.iter += 1; - - if self.map.len() > self.cap { - if let Some(evict) = self - .map - .iter() - .map(|(key, &(last, _))| (last, key)) - .min() - .map(|(_, key)| key.to_string()) - { - return self.map.remove(&evict).map(|(_, st)| st); + let query = QueryString(query); + + self.query_map.insert(query.clone(), stmt.id()); + self.cache.put(stmt.id(), Entry { stmt, query }); + + if self.cache.len() > self.cap { + if let Some((_, entry)) = self.cache.pop_lru() { + self.query_map.remove(&**entry.query.0.as_ref()); + return Some(entry.stmt); } } + None } pub fn clear(&mut self) { - self.map.clear(); + self.query_map.clear(); + self.cache.clear(); } - #[cfg(test)] - pub fn iter<'a>(&'a self) -> Box + 'a> { - Box::new(self.map.iter().map(|(stmt, &(i, _))| (stmt, i))) + pub fn remove(&mut self, id: u32) { + if let Some(entry) = self.cache.pop(&id) { + self.query_map.remove::(entry.query.borrow()); + } } - pub fn into_iter(self) -> Box> { - Box::new(self.map.into_iter().map(|(k, (_, v))| (k, v))) + #[cfg(test)] + pub fn iter(&self) -> impl Iterator { + self.cache.iter() } - pub fn get_cap(&self) -> usize { - self.cap + pub fn into_iter(mut self) -> impl Iterator { + std::iter::from_fn(move || self.cache.pop_lru()) } } diff --git a/src/conn/transaction.rs b/src/conn/transaction.rs index 802d2bf..d7d4b97 100644 --- a/src/conn/transaction.rs +++ b/src/conn/transaction.rs @@ -1,8 +1,17 @@ +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + use std::fmt; -use crate::conn::ConnRef; -use crate::prelude::{FromRow, GenericConnection}; -use crate::{from_row, Conn, LocalInfileHandler, Params, PooledConn, QueryResult, Result, Stmt}; +use crate::{ + conn::ConnRef, prelude::*, Conn, LocalInfileHandler, Params, PooledConn, QueryResult, Result, + Statement, +}; #[derive(PartialEq, Eq, Clone, Copy, Debug)] pub enum IsolationLevel { @@ -52,55 +61,9 @@ impl<'a> Transaction<'a> { } } - /// See [`Conn::query`](struct.Conn.html#method.query). - pub fn query>(&mut self, query: T) -> Result> { - self.conn.query(query) - } - - /// See [`Conn::first`](struct.Conn.html#method.first). - pub fn first, U: FromRow>(&mut self, query: T) -> Result> { - self.query(query).and_then(|mut result| { - if let Some(row) = result.next() { - row.map(|x| Some(from_row(x))) - } else { - Ok(None) - } - }) - } - - /// See [`Conn::prepare`](struct.Conn.html#method.prepare). - pub fn prepare>(&mut self, query: T) -> Result> { - self.conn.prepare(query) - } - - /// See [`Conn::prep_exec`](struct.Conn.html#method.prep_exec). - pub fn prep_exec, T: Into>( - &mut self, - query: A, - params: T, - ) -> Result> { - self.conn.prep_exec(query, params) - } - - /// See [`Conn::first_exec`](struct.Conn.html#method.first_exec). - pub fn first_exec(&mut self, query: Q, params: P) -> Result> - where - Q: AsRef, - P: Into, - T: FromRow, - { - self.prep_exec(query, params).and_then(|mut result| { - if let Some(row) = result.next() { - row.map(|x| Some(from_row(x))) - } else { - Ok(None) - } - }) - } - /// Will consume and commit transaction. pub fn commit(mut self) -> Result<()> { - self.conn.query("COMMIT")?; + self.conn.query_drop("COMMIT")?; self.committed = true; Ok(()) } @@ -108,7 +71,7 @@ impl<'a> Transaction<'a> { /// Will consume and rollback transaction. You also can rely on `Drop` implementation but it /// will swallow errors. pub fn rollback(mut self) -> Result<()> { - self.conn.query("ROLLBACK")?; + self.conn.query_drop("ROLLBACK")?; self.rolled_back = true; Ok(()) } @@ -120,34 +83,25 @@ impl<'a> Transaction<'a> { } } -impl<'a> GenericConnection for Transaction<'a> { - fn query>(&mut self, query: T) -> Result> { - self.query(query) +impl<'a> Queryable for Transaction<'a> { + fn query_iter>(&mut self, query: T) -> Result> { + self.conn.query_iter(query) } - fn first, U: FromRow>(&mut self, query: T) -> Result> { - self.first(query) + fn prep>(&mut self, query: T) -> Result { + self.conn.prep(query) } - fn prepare>(&mut self, query: T) -> Result> { - self.prepare(query) - } - - fn prep_exec(&mut self, query: A, params: T) -> Result> - where - A: AsRef, - T: Into, - { - self.prep_exec(query, params) + fn close(&mut self, stmt: Statement) -> Result<()> { + self.conn.close(stmt) } - fn first_exec(&mut self, query: Q, params: P) -> Result> + fn exec_iter(&mut self, stmt: S, params: P) -> Result> where - Q: AsRef, + S: AsStatement, P: Into, - T: FromRow, { - self.first_exec(query, params) + self.conn.exec_iter(stmt, params) } } @@ -155,7 +109,7 @@ impl<'a> Drop for Transaction<'a> { /// Will rollback transaction. fn drop(&mut self) { if !self.committed && !self.rolled_back { - let _ = self.conn.query("ROLLBACK"); + let _ = self.conn.query_drop("ROLLBACK"); } self.conn.local_infile_handler = self.restore_local_infile_handler.take(); } diff --git a/src/error.rs b/src/error.rs index 337a3c0..5dbb85f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,16 @@ -use mysql_common::named_params::MixedParamsError; -use mysql_common::packets::ErrPacket; -use mysql_common::params::MissingNamedParameterError; -use mysql_common::proto::codec::error::PacketCodecError; -use mysql_common::row::convert::FromRowError; -use mysql_common::value::convert::FromValueError; +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +use mysql_common::{ + named_params::MixedParamsError, packets::ErrPacket, params::MissingNamedParameterError, + proto::codec::error::PacketCodecError, row::convert::FromRowError, + value::convert::FromValueError, +}; use url::ParseError; use std::{error, fmt, io, result, sync}; diff --git a/src/io/mod.rs b/src/io/mod.rs index 836bf1c..059609b 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -1,21 +1,35 @@ +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + use bufstream::BufStream; use io_enum::*; #[cfg(windows)] use named_pipe as np; use native_tls::{Certificate, Identity, TlsConnector, TlsStream}; -use std::fmt; -use std::fs::File; -use std::io::{self, Read as _}; -use std::net::{self, SocketAddr}; #[cfg(unix)] use std::os::unix; -use std::time::Duration; +use std::{ + fmt, + fs::File, + io::{self, Read as _}, + net::{self, SocketAddr}, + time::Duration, +}; -use crate::error::DriverError::{ConnectTimeout, CouldNotConnect}; -use crate::error::Error::DriverError; -use crate::error::Result as MyResult; -use crate::SslOpts; +use crate::{ + error::{ + DriverError::{ConnectTimeout, CouldNotConnect}, + Error::DriverError, + Result as MyResult, + }, + SslOpts, +}; mod tcp; @@ -124,13 +138,18 @@ impl Stream { } } - pub fn make_secure(self, ip_or_hostname: Option<&str>, ssl_opts: SslOpts) -> MyResult { + pub fn make_secure(self, host: url::Host, ssl_opts: SslOpts) -> MyResult { if self.is_socket() { // won't secure socket connection return Ok(self); } - let domain = ip_or_hostname.unwrap_or("127.0.0.1"); + let domain = match host { + url::Host::Domain(domain) => domain, + url::Host::Ipv4(ip) => ip.to_string(), + url::Host::Ipv6(ip) => ip.to_string(), + }; + let mut builder = TlsConnector::builder(); match ssl_opts.root_cert_path() { Some(root_cert_path) => { @@ -154,7 +173,7 @@ impl Stream { Stream::TcpStream(tcp_stream) => match tcp_stream { TcpStream::Insecure(insecure_stream) => { let inner = insecure_stream.into_inner().map_err(io::Error::from)?; - let secure_stream = tls_connector.connect(domain, inner)?; + let secure_stream = tls_connector.connect(&domain, inner)?; Ok(Stream::TcpStream(TcpStream::Secure(BufStream::new( secure_stream, )))) diff --git a/src/io/tcp.rs b/src/io/tcp.rs index 75c31ab..3026928 100644 --- a/src/io/tcp.rs +++ b/src/io/tcp.rs @@ -1,3 +1,11 @@ +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + use net2::{TcpBuilder, TcpStreamExt}; #[cfg(unix)] use nix::{ @@ -8,11 +16,13 @@ use nix::{ #[cfg(target_os = "windows")] use winapi::um::winsock2::*; -use std::net::{SocketAddr, TcpStream, ToSocketAddrs}; #[cfg(unix)] use std::os::unix::prelude::*; -use std::time::Duration; -use std::{io, mem}; +use std::{ + io, mem, + net::{SocketAddr, TcpStream, ToSocketAddrs}, + time::Duration, +}; #[cfg(target_os = "windows")] use std::{os::raw::*, os::windows::prelude::*, ptr}; diff --git a/src/lib.rs b/src/lib.rs index bc854da..3198443 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,23 +1,48 @@ -//! ### rust-mysql-simple -//! Mysql client library implemented in rust. +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +//! This create offers: +//! +//! * MySql database driver in pure rust; +//! * connection pool. +//! +//! Features: +//! +//! * macOS, Windows and Linux support; +//! * TLS support via **nativetls** create; +//! * MySql text protocol support, i.e. support of simple text queries and text result sets; +//! * MySql binary protocol support, i.e. support of prepared statements and binary result sets; +//! * support of multi-result sets; +//! * support of named parameters for prepared statements; +//! * optional per-connection cache of prepared statements; +//! * support of MySql packets larger than 2^24; +//! * support of Unix sockets and Windows named pipes; +//! * support of custom LOCAL INFILE handlers; +//! * support of MySql protocol compression; +//! * support of auth plugins: +//! * **mysql_native_password** - for MySql prior to v8; +//! * **caching_sha2_password** - for MySql v8 and higher. //! -//! #### Install -//! Please use *mysql* crate: +//! ## Installation +//! +//! Put the desired version of the crate into the `dependencies` section of your `Cargo.toml`: //! //! ```toml //! [dependencies] //! mysql = "*" //! ``` //! -//! #### Example +//! ## Example //! //! ```rust -//! #[macro_use] -//! extern crate mysql; -//! // ... -//! -//! # fn main() { -//! use mysql as my; +//! # mysql::doctest_wrapper!(__result, { +//! use mysql::*; +//! use mysql::prelude::*; //! //! #[derive(Debug, PartialEq, Eq)] //! struct Payment { @@ -26,88 +51,596 @@ //! account_name: Option, //! } //! -//! # fn get_opts() -> my::Opts { -//! # let url = if let Ok(url) = std::env::var("DATABASE_URL") { -//! # let opts = my::Opts::from_url(&url).expect("DATABASE_URL invalid"); -//! # if opts.get_db_name().expect("a database name is required").is_empty() { -//! # panic!("database name is empty"); -//! # } -//! # url -//! # } else { -//! # "mysql://root:password@127.0.0.1:3307/mysql".to_string() -//! # }; -//! # my::Opts::from_url(&*url).unwrap() -//! # } -//! -//! fn main() { -//! # let opts = get_opts(); -//! # if opts.get_tcp_port() == 3307 && opts.get_pass() == Some("password") { -//! // See docs on the `OptsBuilder`'s methods for the list of options available via URL. -//! let pool = my::Pool::new("mysql://root:password@localhost:3307/mysql").unwrap(); -//! # } -//! # let pool = my::Pool::new_manual(1, 1, opts).unwrap(); -//! -//! // Let's create payment table. -//! // Unwrap just to make sure no error happened. -//! pool.prep_exec(r"CREATE TEMPORARY TABLE payment ( -//! customer_id int not null, -//! amount int not null, -//! account_name text -//! )", ()).unwrap(); -//! -//! let payments = vec![ -//! Payment { customer_id: 1, amount: 2, account_name: None }, -//! Payment { customer_id: 3, amount: 4, account_name: Some("foo".into()) }, -//! Payment { customer_id: 5, amount: 6, account_name: None }, -//! Payment { customer_id: 7, amount: 8, account_name: None }, -//! Payment { customer_id: 9, amount: 10, account_name: Some("bar".into()) }, -//! ]; -//! -//! // Let's insert payments to the database -//! // We will use into_iter() because we do not need to map Stmt to anything else. -//! // Also we assume that no error happened in `prepare`. -//! for mut stmt in pool.prepare(r"INSERT INTO payment -//! (customer_id, amount, account_name) -//! VALUES -//! (:customer_id, :amount, :account_name)").into_iter() { -//! for p in payments.iter() { -//! // `execute` takes ownership of `params` so we pass account name by reference. -//! // Unwrap each result just to make sure no errors happened. -//! stmt.execute(params!{ -//! "customer_id" => p.customer_id, -//! "amount" => p.amount, -//! "account_name" => &p.account_name, -//! }).unwrap(); +//! let url = "mysql://root:password@localhost:3307/db_name"; +//! # let url = get_opts(); +//! +//! let pool = Pool::new(url)?; +//! +//! let mut conn = pool.get_conn()?; +//! +//! // Let's create a table for payments. +//! conn.query_drop( +//! r"CREATE TEMPORARY TABLE payment ( +//! customer_id int not null, +//! amount int not null, +//! account_name text +//! )")?; +//! +//! let payments = vec![ +//! Payment { customer_id: 1, amount: 2, account_name: None }, +//! Payment { customer_id: 3, amount: 4, account_name: Some("foo".into()) }, +//! Payment { customer_id: 5, amount: 6, account_name: None }, +//! Payment { customer_id: 7, amount: 8, account_name: None }, +//! Payment { customer_id: 9, amount: 10, account_name: Some("bar".into()) }, +//! ]; +//! +//! // Now let's insert payments to the database +//! conn.exec_batch( +//! r"INSERT INTO payment (customer_id, amount, account_name) +//! VALUES (:customer_id, :amount, :account_name)", +//! payments.iter().map(|p| params! { +//! "customer_id" => p.customer_id, +//! "amount" => p.amount, +//! "account_name" => &p.account_name, +//! }) +//! )?; +//! +//! // Let's select payments from database. Type inference should do the trick here. +//! let selected_payments = conn +//! .query_map( +//! "SELECT customer_id, amount, account_name from payment", +//! |(customer_id, amount, account_name)| { +//! Payment { customer_id, amount, account_name } +//! }, +//! )?; +//! +//! // Let's make sure, that `payments` equals to `selected_payments`. +//! // Mysql gives no guaranties on order of returned rows +//! // without `ORDER BY`, so assume we are lucky. +//! assert_eq!(payments, selected_payments); +//! println!("Yay!"); +//! # }); +//! ``` +//! +//! ## API Documentation +//! +//! Please refer to the [crate docs]. +//! +//! ## Basic structures +//! +//! ### `Opts` +//! +//! This structure holds server host name, client username/password and other settings, +//! that controls client behavior. +//! +//! #### URL-based connection string +//! +//! Note, that you can use URL-based connection string as a source of an `Opts` instance. +//! URL schema must be `mysql`. Host, port and credentials, as well as query parameters, +//! should be given in accordance with the RFC 3986. +//! +//! Examples: +//! +//! ```rust +//! # mysql::doctest_wrapper!(__result, { +//! # use mysql::Opts; +//! let _ = Opts::from_url("mysql://localhost/some_db")?; +//! let _ = Opts::from_url("mysql://[::1]/some_db")?; +//! let _ = Opts::from_url("mysql://user:pass%20word@127.0.0.1:3307/some_db?")?; +//! # }); +//! ``` +//! +//! Supported URL parameters (for the meaning of each field please refer to the docs on `Opts` +//! structure in the create API docs): +//! +//! * `prefer_socket: true | false` - defines the value of the same field in the `Opts` structure; +//! * `tcp_keepalive_time_ms: u32` - defines the value (in milliseconds) +//! of the `tcp_keepalive_time` field in the `Opts` structure; +//! * `tcp_connect_timeout_ms: u64` - defines the value (in milliseconds) +//! of the `tcp_connect_timeout` field in the `Opts` structure; +//! * `stmt_cache_size: u32` - defines the value of the same field in the `Opts` structure; +//! * `compress` - defines the value of the same field in the `Opts` structure. +//! Supported value are: +//! * `true` - enables compression with the default compression level; +//! * `fast` - enables compression with "fast" compression level; +//! * `best` - enables compression with "best" compression level; +//! * `1`..`9` - enables compression with the given compression level. +//! * `socket` - socket path on UNIX, or pipe name on Windows. +//! +//! ### `OptsBuilder` +//! +//! It's a convenient builder for the `Opts` structure. It defines setters for fields +//! of the `Opts` structure. +//! +//! ```no_run +//! # mysql::doctest_wrapper!(__result, { +//! # use mysql::*; +//! let opts = OptsBuilder::new() +//! .user(Some("foo")) +//! .db_name(Some("bar")); +//! let _ = Conn::new(opts)?; +//! # }); +//! ``` +//! +//! ### `Conn` +//! +//! This structure represents an active MySql connection. It also holds statement cache +//! and metadata for the last result set. +//! +//! ### `Transaction` +//! +//! It's a simple wrapper on top of a routine, that starts with `START TRANSACTION` +//! and ends with `COMMIT` or `ROLBACK`. +//! +//! ``` +//! # mysql::doctest_wrapper!(__result, { +//! use mysql::*; +//! use mysql::prelude::*; +//! +//! let pool = Pool::new(get_opts())?; +//! let mut conn = pool.get_conn()?; +//! +//! let mut tx = conn.start_transaction(false, None, None)?; +//! tx.query_drop("CREATE TEMPORARY TABLE tmp (TEXT a)")?; +//! tx.exec_drop("INSERT INTO tmp (a) VALUES (?)", ("foo",))?; +//! let val: Option = tx.query_first("SELECT a from tmp")?; +//! assert_eq!(val.unwrap(), "foo"); +//! // Note, that transaction will be rolled back implicitly on Drop, if not committed. +//! tx.rollback(); +//! +//! let val: Option = conn.query_first("SELECT a from tmp")?; +//! assert_eq!(val, None); +//! # }); +//! ``` +//! +//! ### `Pool` +//! +//! It's a reference to a connection pool, that can be cloned and shared between threads. +//! +//! ``` +//! # mysql::doctest_wrapper!(__result, { +//! use mysql::*; +//! use mysql::prelude::*; +//! +//! use std::thread::spawn; +//! +//! let pool = Pool::new(get_opts())?; +//! +//! let handles = (0..4).map(|i| { +//! spawn({ +//! let pool = pool.clone(); +//! move || { +//! let mut conn = pool.get_conn()?; +//! conn.exec_first::("SELECT ? * 10", (i,)) +//! .map(Option::unwrap) //! } +//! }) +//! }); +//! +//! let result: Result> = handles.map(|handle| handle.join().unwrap()).collect(); +//! +//! assert_eq!(result.unwrap(), vec![0, 10, 20, 30]); +//! # }); +//! ``` +//! +//! ### `Statement` +//! +//! Statement, actually, is just an identifier coupled with statement metadata, i.e an information +//! about its parameters and columns. Internally the `Statement` structure also holds additional +//! data required to support named parameters (see bellow). +//! +//! ``` +//! # mysql::doctest_wrapper!(__result, { +//! use mysql::*; +//! use mysql::prelude::*; +//! +//! let pool = Pool::new(get_opts())?; +//! let mut conn = pool.get_conn()?; +//! +//! let stmt = conn.prep("DO ?")?; +//! +//! // The prepared statement will return no columns. +//! assert!(stmt.columns().is_empty()); +//! +//! // The prepared statement have one parameter. +//! let param = stmt.params().get(0).unwrap(); +//! assert_eq!(param.schema_str(), ""); +//! assert_eq!(param.table_str(), ""); +//! assert_eq!(param.name_str(), "?"); +//! # }); +//! ``` +//! +//! ### `Value` +//! +//! This enumeration represents the raw value of a MySql cell. Library offers conversion between +//! `Value` and different rust types via `FromValue` trait described below. +//! +//! #### `FromValue` trait +//! +//! This trait is reexported from **mysql_common** create. Please refer to its +//! [crate docs][mysql_common docs] for the list of supported conversions. +//! +//! Trait offers conversion in two flavours: +//! +//! * `from_value(Value) -> T` - convenient, but panicking conversion. +//! +//! Note, that for any variant of `Value` there exist a type, that fully covers its domain, +//! i.e. for any variant of `Value` there exist `T: FromValue` such that `from_value` will never +//! panic. This means, that if your database schema is known, than it's possible to write your +//! application using only `from_value` with no fear of runtime panic. +//! +//! * `from_value_opt(Value) -> Option` - non-panicking, but less convenient conversion. +//! +//! This function is useful to probe conversion in cases, where source database schema +//! is unknown. +//! +//! ``` +//! # mysql::doctest_wrapper!(__result, { +//! use mysql::*; +//! use mysql::prelude::*; +//! +//! let via_test_protocol: u32 = from_value(Value::Bytes(b"65536".to_vec())); +//! let via_bin_protocol: u32 = from_value(Value::UInt(65536)); +//! assert_eq!(via_test_protocol, via_bin_protocol); +//! +//! let unknown_val = // ... +//! # Value::Time(false, 10, 2, 30, 0, 0); +//! +//! // Maybe it is a float? +//! let unknown_val = match from_value_opt::(unknown_val) { +//! Ok(float) => { +//! println!("A float value: {}", float); +//! return Ok(()); +//! } +//! Err(FromValueError(unknown_val)) => unknown_val, +//! }; +//! +//! // Or a string? +//! let unknown_val = match from_value_opt::(unknown_val) { +//! Ok(string) => { +//! println!("A string value: {}", string); +//! return Ok(()); +//! } +//! Err(FromValueError(unknown_val)) => unknown_val, +//! }; +//! +//! // Screw this, I'll simply match on it +//! match unknown_val { +//! val @ Value::NULL => { +//! println!("An empty value: {:?}", from_value::>(val)) +//! }, +//! val @ Value::Bytes(..) => { +//! // It's non-utf8 bytes, since we already tried to convert it to String +//! println!("Bytes: {:?}", from_value::>(val)) +//! } +//! val @ Value::Int(..) => { +//! println!("A signed integer: {}", from_value::(val)) +//! } +//! val @ Value::UInt(..) => { +//! println!("An unsigned integer: {}", from_value::(val)) //! } +//! Value::Float(..) => unreachable!("already tried"), +//! val @ Value::Date(..) => { +//! use mysql::chrono::NaiveDateTime; +//! println!("A date value: {}", from_value::(val)) +//! } +//! val @ Value::Time(..) => { +//! use std::time::Duration; +//! println!("A time value: {:?}", from_value::(val)) +//! } +//! } +//! # }); +//! ``` +//! +//! ### `Row` +//! +//! Internally `Row` is a vector of `Value`s, that also allows indexing by a column name/offset, +//! and stores row metadata. Library offers conversion between `Row` and sequences of Rust types +//! via `FromRow` trait described below. +//! +//! #### `FromRow` trait +//! +//! This trait is reexported from **mysql_common** create. Please refer to its +//! [crate docs][mysql_common docs] for the list of supported conversions. +//! +//! This conversion is based on the `FromValue` and so comes in two similar flavours: +//! +//! * `from_row(Row) -> T` - same as `from_value`, but for rows; +//! * `from_row_opt(Row) -> Option` - same as `from_value_opt`, but for rows. +//! +//! [`Queryable`][#queryable] trait offers implicit conversion for rows of a query result, +//! that is based on this trait. +//! +//! ``` +//! # mysql::doctest_wrapper!(__result, { +//! use mysql::*; +//! use mysql::prelude::*; +//! +//! let mut conn = Conn::new(get_opts())?; +//! +//! // Single-column row can be converted to a singular value +//! let val: Option = conn.query_first("SELECT 'foo'")?; +//! assert_eq!(val.unwrap(), "foo"); +//! +//! // Example of a mutli-column row conversion to an inferred type. +//! let row = conn.query_first("SELECT 255, 256")?; +//! assert_eq!(row, Some((255u8, 256u16))); +//! +//! // Some unknown row +//! let row: Row = conn.query_first( +//! // ... +//! # "SELECT 255, Null", +//! )?.unwrap(); +//! +//! for column in row.columns_ref() { +//! let column_value = &row[column.name_str().as_ref()]; +//! println!( +//! "Column {} of type {:?} with value {:?}", +//! column.name_str(), +//! column.column_type(), +//! column_value, +//! ); +//! } +//! # }); +//! ``` +//! +//! +//! ### `Params` +//! +//! Represents parameters of a prepared statement, but this type won't appear directly in your code +//! because binary protocol API will ask for `T: Into`, where `Into` is implemented: +//! +//! * for tuples of `Into` types up to arity 12; +//! +//! **Note:** singular tuple requires extra comma, e.g. `("foo",)`; +//! +//! * for `IntoIterator>` for cases, when your statement takes more +//! than 12 parameters; +//! * for named parameters representation (the value of the `params!` macro, described below). //! -//! // Let's select payments from database -//! let selected_payments: Vec = -//! pool.prep_exec("SELECT customer_id, amount, account_name from payment", ()) -//! .map(|result| { // In this closure we will map `QueryResult` to `Vec` -//! // `QueryResult` is iterator over `MyResult` so first call to `map` -//! // will map each `MyResult` to contained `row` (no proper error handling) -//! // and second call to `map` will map each `row` to `Payment` -//! result.map(|x| x.unwrap()).map(|row| { -//! // ⚠️ Note that from_row will panic if you don't follow your schema -//! let (customer_id, amount, account_name) = my::from_row(row); -//! Payment { -//! customer_id: customer_id, -//! amount: amount, -//! account_name: account_name, +//! ``` +//! # mysql::doctest_wrapper!(__result, { +//! use mysql::*; +//! use mysql::prelude::*; +//! +//! let mut conn = Conn::new(get_opts())?; +//! +//! // Singular tuple requires extra comma: +//! let row: Option = conn.exec_first("SELECT ?", (0,))?; +//! assert_eq!(row.unwrap(), 0); +//! +//! // More than 12 parameters: +//! let row: Option = conn.exec_first( +//! "SELECT ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ? + ?", +//! (0..16).collect::>(), +//! )?; +//! assert_eq!(row.unwrap(), 120); +//! # }); +//! ``` +//! +//! **Note:** Please refer to the [**mysql_common** crate docs][mysql_common docs] for the list +//! of types, that implements `Into`. +//! +//! #### `Serialized`, `Deserialized` +//! +//! Wrapper structures for cases, when you need to provide a value for a JSON cell, +//! or when you need to parse JSON cell as a struct. +//! +//! ```rust +//! # #[macro_use] extern crate serde_derive; +//! # mysql::doctest_wrapper!(__result, { +//! use mysql::*; +//! use mysql::prelude::*; +//! +//! /// Serializable structure. +//! #[derive(Debug, PartialEq, Serialize, Deserialize)] +//! struct Example { +//! foo: u32, +//! } +//! +//! // Value::from for Serialized will emit json string. +//! let value = Value::from(Serialized(Example { foo: 42 })); +//! assert_eq!(value, Value::Bytes(br#"{"foo":42}"#.to_vec())); +//! +//! // from_value for Deserialized will parse json string. +//! let structure: Deserialized = from_value(value); +//! assert_eq!(structure, Deserialized(Example { foo: 42 })); +//! # }); +//! ``` +//! +//! ### `QueryResult` +//! +//! It's an iterator over rows of a query result with support of multi-result sets. It's intended +//! for cases when you need full control during result set iteration. For other cases `Conn` +//! provides a set of methods that will immediately consume the first result set and drop everything +//! else. +//! +//! This iterator is lazy so it won't read the result from server until you iterate over it. +//! MySql protocol is strictly sequential, so `Conn` will be mutably borrowed until the result +//! is fully consumed. +//! +//! ```rust +//! # #[macro_use] extern crate serde_derive; +//! # mysql::doctest_wrapper!(__result, { +//! use mysql::*; +//! use mysql::prelude::*; +//! +//! let mut conn = Conn::new(get_opts())?; +//! +//! // This query will emit two result sets. +//! let mut result = conn.query_iter("SELECT 1, 2; SELECT 3, 3.14;")?; +//! +//! let mut set = 0; +//! while result.more_results_exists() { +//! println!("Result set columns: {:?}", result.columns_ref()); +//! +//! for row in result.by_ref() { +//! match set { +//! 0 => { +//! // First result set will contain two numbers. +//! assert_eq!((1_u8, 2_u8), from_row(row?)); //! } -//! }).collect() // Collect payments so now `QueryResult` is mapped to `Vec` -//! }).unwrap(); // Unwrap `Vec` -//! -//! // Now make sure that `payments` equals to `selected_payments`. -//! // Mysql gives no guaranties on order of returned rows without `ORDER BY` -//! // so assume we are lukky. -//! assert_eq!(payments, selected_payments); -//! println!("Yay!"); +//! 1 => { +//! // Second result set will contain a number and a float. +//! assert_eq!((3_u8, 3.14), from_row(row?)); +//! } +//! _ => unreachable!(), +//! } +//! } +//! set += 1; //! } -//! # main(); -//! # } +//! # }); +//! ``` +//! +//! ## Text protocol +//! +//! MySql text protocol is implemented in the set of `Conn::query*` methods. It's useful when your +//! query doesn't have parameters. +//! +//! **Note:** All values of a text protocol result set will be encoded as strings by the server, +//! so `from_value` conversion may lead to additional parsing costs. +//! +//! Examples: +//! +//! ```rust +//! # mysql::doctest_wrapper!(__result, { +//! # use mysql::*; +//! # use mysql::prelude::*; +//! let pool = Pool::new(get_opts())?; +//! let val = pool.get_conn()?.query_first("SELECT POW(2, 16)")?; +//! +//! // Text protocol returns bytes even though the result of POW +//! // is actually a floating point number. +//! assert_eq!(val, Some(Value::Bytes("65536".as_bytes().to_vec()))); +//! # }); +//! ``` +//! +//! ## Binary protocol and prepared statements. +//! +//! MySql binary protocol is implemented in `prep`, `close` and the set of `exec*` methods, +//! defined on the [`Queryable`](#queryable) trait. Prepared statements is the only way to +//! pass rust value to the MySql server. MySql uses `?` symbol as a parameter placeholder +//! and it's only possible to use parameters where a single MySql value is expected. +//! For example: +//! +//! ```rust +//! # mysql::doctest_wrapper!(__result, { +//! # use mysql::*; +//! # use mysql::prelude::*; +//! let pool = Pool::new(get_opts())?; +//! let val = pool.get_conn()?.exec_first("SELECT POW(?, ?)", (2, 16))?; +//! +//! assert_eq!(val, Some(Value::Float(65536.0))); +//! # }); +//! ``` +//! +//! ### Statements +//! +//! In MySql each prepared statement belongs to a particular connection and can't be executed +//! on another connection. Trying to do so will lead to an error. The driver won't tie statement +//! to a connection in any way, but one can look on to the connection id, that is stored +//! in the `Statement` structure. +//! +//! ```rust +//! # mysql::doctest_wrapper!(__result, { +//! # use mysql::*; +//! # use mysql::prelude::*; +//! let pool = Pool::new(get_opts())?; +//! +//! let mut conn_1 = pool.get_conn()?; +//! let mut conn_2 = pool.get_conn()?; +//! +//! let stmt_1 = conn_1.prep("SELECT ?")?; +//! +//! // stmt_1 is for the conn_1, .. +//! assert!(stmt_1.connection_id() == conn_1.connection_id()); +//! assert!(stmt_1.connection_id() != conn_2.connection_id()); +//! +//! // .. so stmt_1 will execute only on conn_1 +//! assert!(conn_1.exec_drop(&stmt_1, ("foo",)).is_ok()); +//! assert!(conn_2.exec_drop(&stmt_1, ("foo",)).is_err()); +//! # }); +//! ``` +//! +//! ### Statement cache +//! +//! `Conn` will manage the cache of prepared statements on the client side, so subsequent calls +//! to prepare with the same statement won't lead to a client-server roundtrip. Cache size +//! for each connection is determined by the `stmt_cache_size` field of the `Opts` structure. +//! Statements, that are out of this boundary will be closed in LRU order. +//! +//! Statement cache is completely disabled if `stmt_cache_size` is zero. +//! +//! **Caveats:** +//! +//! * disabled statement cache means, that you have to close statements yourself using +//! `Conn::close`, or they'll exhaust server limits/resources; +//! +//! * you should be aware of the [`max_prepared_stmt_count`][max_prepared_stmt_count] +//! option of the MySql server. If the number of active connections times the value +//! of `stmt_cache_size` is greater, than you could receive an error while prepareing +//! another statement. +//! +//! ### Named parameters +//! +//! MySql itself doesn't have named parameters support, so it's implemented on the client side. +//! One should use `:name` as a placeholder syntax for a named parameter. +//! +//! Named parameters may be repeated within the statement, e.g `SELECT :foo :foo` will require +//! a single named parameter `foo` that will be repeated on the corresponding positions during +//! statement execution. +//! +//! One should use the `params!` macro to build a parameters for execution. +//! +//! **Note:** Positional and named parameters can't be mixed within the single statement. +//! +//! Examples: +//! +//! ```rust +//! # mysql::doctest_wrapper!(__result, { +//! # use mysql::*; +//! # use mysql::prelude::*; +//! let pool = Pool::new(get_opts())?; +//! +//! let mut conn = pool.get_conn()?; +//! let stmt = conn.prep("SELECT :foo, :bar, :foo")?; +//! +//! let foo = 42; +//! +//! let val_13 = conn.exec_first(&stmt, params! { "foo" => 13, "bar" => foo })?.unwrap(); +//! // Short syntax is available when param name is the same as variable name: +//! let val_42 = conn.exec_first(&stmt, params! { foo, "bar" => 13 })?.unwrap(); +//! +//! assert_eq!((foo, 13, foo), val_42); +//! assert_eq!((13, foo, 13), val_13); +//! # }); //! ``` +//! +//! ### `Queryable` +//! +//! The `Queryable` trait defines common methods for `Conn`, `PooledConn` and `Transaction`. +//! The set of basic methods consts of: +//! +//! * `query_iter` - basic methods to execute text query and get `QueryRestul`; +//! * `prep` - basic method to prepare a statement; +//! * `exec_iter` - basic method to execute statement and get `QueryResult`; +//! * `close` - basic method to close the statement; +//! +//! The trait also defines the set of helper methods, that is based on basic methods. +//! These methods will consume only the firt result set, other result sets will be dropped: +//! +//! * `{query|exec}` - to collect the result into a `Vec`; +//! * `{query|exec}_first` - to get the first `T: FromRow`, if any; +//! * `{query|exec}_map` - to map each `T: FromRow` to some `U`; +//! * `{query|exec}_fold` - to fold the set of `T: FromRow` to a single value; +//! * `{query|exec}_drop` - to immediately drop the result. +//! +//! The trait also defines additional helper for a batch statement execution. +//! +//! [crate docs]: https://docs.rs/mysql +//! [mysql_common docs]: https://docs.rs/mysql_common +//! [max_prepared_stmt_count]: https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_max_prepared_stmt_count +//! #![crate_name = "mysql"] #![crate_type = "rlib"] @@ -130,11 +663,10 @@ pub use crate::myc::time; /// Reexport of `uuid` crate. pub use crate::myc::uuid; -// Until `macro_reexport` stabilisation. - mod conn; pub mod error; mod io; +mod queryable; #[doc(inline)] pub use crate::myc::constants as consts; @@ -144,13 +676,13 @@ pub use crate::conn::local_infile::{LocalInfile, LocalInfileHandler}; #[doc(inline)] pub use crate::conn::opts::SslOpts; #[doc(inline)] -pub use crate::conn::opts::{Opts, OptsBuilder}; +pub use crate::conn::opts::{Opts, OptsBuilder, DEFAULT_STMT_CACHE_SIZE}; #[doc(inline)] pub use crate::conn::pool::{Pool, PooledConn}; #[doc(inline)] pub use crate::conn::query_result::QueryResult; #[doc(inline)] -pub use crate::conn::stmt::Stmt; +pub use crate::conn::stmt::Statement; #[doc(inline)] pub use crate::conn::transaction::{IsolationLevel, Transaction}; #[doc(inline)] @@ -175,27 +707,95 @@ pub use crate::myc::value::json::{Deserialized, Serialized}; pub use crate::myc::value::Value; pub mod prelude { - #[doc(inline)] - pub use crate::conn::GenericConnection; #[doc(inline)] pub use crate::myc::row::convert::FromRow; #[doc(inline)] pub use crate::myc::row::ColumnIndex; #[doc(inline)] pub use crate::myc::value::convert::{ConvIr, FromValue, ToValue}; + #[doc(inline)] + pub use crate::queryable::{AsStatement, Queryable}; } #[doc(inline)] pub use crate::myc::params; +#[doc(hidden)] +#[macro_export] +macro_rules! def_database_url { + () => { + if let Ok(url) = std::env::var("DATABASE_URL") { + let opts = $crate::Opts::from_url(&url).expect("DATABASE_URL invalid"); + if opts + .get_db_name() + .expect("a database name is required") + .is_empty() + { + panic!("database name is empty"); + } + url + } else { + "mysql://root:password@127.0.0.1:3307/mysql".into() + } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! def_get_opts { + () => { + pub fn test_ssl() -> bool { + let ssl = std::env::var("SSL").ok().unwrap_or("false".into()); + ssl == "true" || ssl == "1" + } + + pub fn test_compression() -> bool { + let compress = std::env::var("COMPRESS").ok().unwrap_or("false".into()); + compress == "true" || compress == "1" + } + + pub fn get_opts() -> $crate::OptsBuilder { + let database_url = $crate::def_database_url!(); + let mut builder = $crate::OptsBuilder::from_opts(&*database_url) + .init(vec!["SET GLOBAL sql_mode = 'TRADITIONAL'"]); + if test_compression() { + builder = builder.compress(Some(Default::default())); + } + if test_ssl() { + let ssl_opts = $crate::SslOpts::default() + .with_danger_skip_domain_validation(true) + .with_danger_accept_invalid_certs(true); + builder = builder.prefer_socket(false).ssl_opts(ssl_opts); + } + builder + } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! doctest_wrapper { + ($body:block) => { + fn fun() { + $crate::def_get_opts!(); + $body; + } + fun() + }; + (__result, $body:block) => { + fn fun() -> std::result::Result<(), Box> { + $crate::def_get_opts!(); + Ok($body) + } + fun() + }; +} + #[cfg(test)] mod test_misc { use lazy_static::lazy_static; - use std::env; - - use crate::conn::opts::{Opts, OptsBuilder}; - use crate::SslOpts; + use crate::{def_database_url, def_get_opts}; #[allow(dead_code)] fn error_should_implement_send_and_sync() { @@ -204,46 +804,8 @@ mod test_misc { } lazy_static! { - pub static ref DATABASE_URL: String = { - if let Ok(url) = env::var("DATABASE_URL") { - let opts = Opts::from_url(&url).expect("DATABASE_URL invalid"); - if opts - .get_db_name() - .expect("a database name is required") - .is_empty() - { - panic!("database name is empty"); - } - url - } else { - "mysql://root:password@127.0.0.1:3307/mysql".into() - } - }; + pub static ref DATABASE_URL: String = def_database_url!(); } - pub fn get_opts() -> OptsBuilder { - let mut builder = OptsBuilder::from_opts(&**DATABASE_URL); - builder.init(vec!["SET GLOBAL sql_mode = 'TRADITIONAL'"]); - if test_compression() { - builder.compress(Some(Default::default())); - } - if test_ssl() { - builder.prefer_socket(false); - let mut ssl_opts = SslOpts::default(); - ssl_opts.set_danger_skip_domain_validation(true); - ssl_opts.set_danger_accept_invalid_certs(true); - builder.ssl_opts(ssl_opts); - } - builder - } - - pub fn test_ssl() -> bool { - let ssl = env::var("SSL").ok().unwrap_or("false".into()); - ssl == "true" || ssl == "1" - } - - pub fn test_compression() -> bool { - let compress = env::var("COMPRESS").ok().unwrap_or("false".into()); - compress == "true" || compress == "1" - } + def_get_opts!(); } diff --git a/src/queryable.rs b/src/queryable.rs new file mode 100644 index 0000000..b343d91 --- /dev/null +++ b/src/queryable.rs @@ -0,0 +1,148 @@ +// Copyright (c) 2020 rust-mysql-common contributors +// +// Licensed under the Apache License, Version 2.0 +// or the MIT +// license , at your +// option. All files in the project carrying such notice may not be copied, +// modified, or distributed except according to those terms. + +use std::borrow::Cow; + +use crate::{from_row, prelude::FromRow, Params, QueryResult, Result, Statement}; + +pub trait AsStatement { + fn as_statement(&self, queryable: &mut Q) -> Result>; +} + +/// Queryable object. +pub trait Queryable { + fn query_iter>(&mut self, query: Q) -> Result>; + + fn query(&mut self, query: Q) -> Result> + where + Q: AsRef, + T: FromRow, + { + self.query_map(query, from_row) + } + + fn query_first(&mut self, query: Q) -> Result> + where + Q: AsRef, + T: FromRow, + { + self.query_iter(query)? + .next() + .map(|row| row.map(crate::from_row)) + .transpose() + } + + fn query_map(&mut self, query: Q, mut f: F) -> Result> + where + Q: AsRef, + T: FromRow, + F: FnMut(T) -> U, + { + self.query_fold(query, Vec::new(), |mut acc, row| { + acc.push(f(row)); + acc + }) + } + + fn query_fold(&mut self, query: Q, init: U, mut f: F) -> Result + where + Q: AsRef, + T: FromRow, + F: FnMut(U, T) -> U, + { + self.query_iter(query)? + .map(|row| row.map(from_row::)) + .try_fold(init, |acc, row: Result| row.map(|row| f(acc, row))) + } + + fn query_drop(&mut self, query: Q) -> Result<()> + where + Q: AsRef, + { + self.query_iter(query).map(drop) + } + + fn prep>(&mut self, query: Q) -> Result; + + /// This function will close the given statement on the server side. + fn close(&mut self, stmt: Statement) -> Result<()>; + + fn exec_iter(&mut self, stmt: S, params: P) -> Result> + where + S: AsStatement, + P: Into; + + fn exec_batch(&mut self, stmt: S, params: I) -> Result<()> + where + Self: Sized, + S: AsStatement, + P: Into, + I: IntoIterator, + { + let stmt = stmt.as_statement(self)?; + for params in params { + self.exec_drop(stmt.as_ref(), params)?; + } + + Ok(()) + } + + fn exec(&mut self, stmt: S, params: P) -> Result> + where + S: AsStatement, + P: Into, + T: FromRow, + { + self.exec_map(stmt, params, crate::from_row) + } + + fn exec_first(&mut self, stmt: S, params: P) -> Result> + where + S: AsStatement, + P: Into, + T: FromRow, + { + self.exec_iter(stmt, params)? + .next() + .map(|row| row.map(crate::from_row)) + .transpose() + } + + fn exec_map(&mut self, stmt: S, params: P, mut f: F) -> Result> + where + S: AsStatement, + P: Into, + T: FromRow, + F: FnMut(T) -> U, + { + self.exec_fold(stmt, params, Vec::new(), |mut acc, row| { + acc.push(f(row)); + acc + }) + } + + fn exec_fold(&mut self, stmt: S, params: P, init: U, mut f: F) -> Result + where + S: AsStatement, + P: Into, + T: FromRow, + F: FnMut(U, T) -> U, + { + let mut result = self.exec_iter(stmt, params)?; + let output = result.try_fold(init, |init, row| row.map(|row| f(init, from_row(row)))); + output + } + + fn exec_drop(&mut self, stmt: S, params: P) -> Result<()> + where + S: AsStatement, + P: Into, + { + self.exec_iter(stmt, params).map(drop) + } +}