diff --git a/.github/workflows/dub.yml b/.github/workflows/dub.yml index 0b80d718..6085bb7e 100644 --- a/.github/workflows/dub.yml +++ b/.github/workflows/dub.yml @@ -26,11 +26,11 @@ jobs: compiler: - dmd-latest - ldc-latest + - dmd-2.098.1 + - dmd-2.097.2 - dmd-2.096.1 - - dmd-2.095.1 - - dmd-2.094.2 - - ldc-1.25.1 # eq to dmd v2.095.1 - - ldc-1.24.0 # eq to dmd v2.094.1 + - ldc-1.28.1 # eq to dmd v2.098.1 + - ldc-1.27.1 # eq to dmd v2.097.2 steps: - uses: actions/checkout@v2 @@ -39,17 +39,16 @@ jobs: with: compiler: ${{ matrix.compiler }} - # Note: this is not needed, because vanilla mysql-natve has no dependencies - #- name: Upgrade dub dependencies - # uses: WebFreak001/dub-upgrade@v0.1 + - name: Upgrade dub dependencies + uses: WebFreak001/dub-upgrade@v0.1.1 - name: Build Library run: dub build --build=release --config=library # cache - #- uses: WebFreak001/dub-upgrade@v0.1 - # if: startsWith(matrix.os, 'windows') - # with: { store: true } + - uses: WebFreak001/dub-upgrade@v0.1.1 + if: startsWith(matrix.os, 'windows') + with: { store: true } # Older compiler versions build-older: @@ -59,6 +58,8 @@ jobs: matrix: os: [ ubuntu-latest, windows-latest ] # don't bother with macos-latest compiler: + - dmd-2.095.1 + - dmd-2.094.2 - dmd-2.093.1 - dmd-2.092.1 - dmd-2.091.1 @@ -68,15 +69,18 @@ jobs: - dmd-2.087.1 - dmd-2.086.1 - dmd-2.085.1 - - dmd-2.084.1 - - dmd-2.083.1 - - dmd-2.082.1 - - dmd-2.081.2 - - dmd-2.080.1 + # These compilers do not work with dub for downloading taggedalgebraic + #- dmd-2.084.1 + #- dmd-2.083.1 + #- dmd-2.082.1 + #- dmd-2.081.2 + #- dmd-2.080.1 + - ldc-1.27.1 # eq to dmd v2.097.2 + - ldc-1.26.0 # eq to dmd v2.096.1 + - ldc-1.25.1 # eq to dmd v2.095.1 + - ldc-1.24.0 # eq to dmd v2.094.1 - ldc-1.23.0 # eq to dmd v2.093.1 - ldc-1.22.0 # eq to dmd v2.092.1 - - ldc-1.21.0 # eq to dmd v2.091.1 - - ldc-1.20.1 # eq to dmd v2.090.1 - ldc-1.19.0 # eq to dmd v2.089.1 runs-on: ${{ matrix.os }} @@ -88,13 +92,12 @@ jobs: with: compiler: ${{ matrix.compiler }} - # Note: this is not needed, because vanilla mysql-natve has no dependencies - #- name: Upgrade dub dependencies - # uses: WebFreak001/dub-upgrade@v0.1 + - name: Upgrade dub dependencies + uses: WebFreak001/dub-upgrade@v0.1.1 - name: Build Library run: dub build --build=release --config=library # cache - #- uses: WebFreak001/dub-upgrade@v0.1 - # with: { store: true } + - uses: WebFreak001/dub-upgrade@v0.1.1 + with: { store: true } diff --git a/.github/workflows/integration-testing.yml b/.github/workflows/integration-testing.yml index 26283b50..3ff091ba 100644 --- a/.github/workflows/integration-testing.yml +++ b/.github/workflows/integration-testing.yml @@ -86,10 +86,10 @@ jobs: compiler: - dmd-latest - ldc-latest - - dmd-2.095.1 - - dmd-2.094.2 - - ldc-1.25.1 # eq to dmd v2.095.1 - - ldc-1.24.0 # eq to dmd v2.094.1 + - dmd-2.098.1 + - dmd-2.097.2 + - ldc-1.28.1 # eq to dmd v2.098.1 + - ldc-1.27.0 # eq to dmd v2.097.2 runs-on: ubuntu-20.04 @@ -157,10 +157,10 @@ jobs: compiler: - dmd-latest - ldc-latest - - dmd-2.095.1 - - dmd-2.094.2 - - ldc-1.25.1 # eq to dmd v2.095.1 - - ldc-1.24.0 # eq to dmd v2.094.1 + - dmd-2.098.1 + - dmd-2.097.2 + - ldc-1.28.1 # eq to dmd v2.098.1 + - ldc-1.27.0 # eq to dmd v2.097.2 runs-on: ubuntu-20.04 diff --git a/.gitignore b/.gitignore index 15281e79..2ff050f1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ dub.selections.json *.lib *.dll *.exe +.*.swp /bin /testConnectionStr.txt diff --git a/SAFE_MIGRATION.md b/SAFE_MIGRATION.md new file mode 100644 index 00000000..09d82c2e --- /dev/null +++ b/SAFE_MIGRATION.md @@ -0,0 +1,186 @@ +# Migrating code to use the @safe API of mysql-native + +Starting with version 3.2.0, mysql-native is transitioning to using a fully `@safe` API. This document describes how mysql-native is migrating, and how you can migrate your existing code to the new version. + +First, note that the latest version of mysql-native, while it supports a safe API, defaults to using the unsafe mechanisms. This is so the new version can be selected *without modifying* any existing code or imports. Even though the default is unsafe, many tools are provided to allow you to use both safe and unsafe APIs in the same project. I have tried to make every effort to make this transition as seamless as possible. For details, read on. + +## Table of contents + +* [Why safe?](#why-safe) +* [Roadmap](#roadmap) +* [The Major Changes](#the-major-changes) + * [The safe/unsafe API](#the-safeunsafe-api) + * [Importing both safe and unsafe](#importing-both-safe-and-unsafe) + * [Migrating from Variant to MySQLVal](#migrating-from-variant-to-mysqlval) + * [Row and ResultRange](#row-and-resultrange) + * [Prepared](#prepared) + * [Connection](#connection) + * [MySQLPool](#mysqlpool) + * [The commands module](#the-commands-module) +* [Recommended Transition Method](#recommended-transition-method) + + +## Why Safe? + +*related: please see D's [Memory Safety](https://dlang.org/spec/memory-safe-d.html) page to understand what `@safe` does in D* + +Since mysql-native is intended to be a key component of servers that are on the Internet, it must support the capability (even if not required) to be fully `@safe`. In addition, major web frameworks (e.g. [vibe.d](http://code.dlang.org/packages/vibe-d)) and arguably any other program are headed in this direction. + +In other words, the world wants memory safe code, and libraries that provide safe interfaces and guarantees will be much more appealing. It's just not acceptable any more for the components of major development projects to be careless about memory safety. + +## Roadmap + +The intended roadmap for migrating to a safe API is the following: + +* v3.2.0 - In this version, the `safe` and `unsafe` packages were introduced, providing a way to specify exactly which API you want to use. The default modules and packges in this version import the `unsafe` versions of the API to maintain full backwards compatibility. +* v4.0.0 - In this version, the `unsafe` versions of the API will be deprecated, meaning you can still use them, but you will get warnings. In addition, the default modules and packages will import the `safe` API. +* Future version (possibly v5.0.0) - In this version, the `unsafe` API will be completely removed, and the `safe` modules now simply publicly import the standard modules. The `mysql.impl` package will be removed. + +## The Major Changes + +mysql-native until now used the Phobos type `std.variant.Variant` to hold data who's type was unknown at compile time. Unfortunately, since `Variant` can hold *any* type, it must default to having a `@system` postblit/copy constructor, and a `@system` destructor. This means that just copying a `Variant`, passing it as a function parameter, or returning it from a function makes the function doing such things `@system`. This meant that we needed to move from `Variant` to a new type that allows only safe usages, but still maintained the ability to decide types at runtime. + +To this end, we used the library [taggedalgebraic](http://code.dlang.org/packages/taggedalgebraic), which supports not only safe call forwarding, but also provides a much more transparent and useful API than Variant. A `TaggedAlgebraic` allows you to limit which types MySQL deals with. This allows better implicit conversion support, and more focused code. It also prevents one from passing in parameter types that are not supported and not finding that out until runtime. + +The module `mysql.types` now contains a new type called `MySQLVal`, which should be, for the most part, a drop-in replacement for `Variant` in your code. + +### The safe/unsafe API + +In some cases, fixing memory safety in mysql-native was as simple as adding a `@safe` tag to the module or functions in the module. These functions and modules should work just as before, but are now callable from `@safe` code. + +But for the rest, to achieve full backwards compatibility, we have divided the API into two major sections -- safe and unsafe. The package `mysql.safe` will import all the safe versions of the API, the package `mysql.unsafe` will import the unsafe versions. If you import `mysql`, it will currently point at the unsafe version for backwards compatibility (see [Roadmap](#Roadmap) for details on how this will change). + +The following modules have been split into mysql.safe.*modname* and mysql.unsafe.*modname*. Importing mysql.*modname* will currently import the unsafe version for backwards compatibility. In a future major version, the default will be to import the safe api. +* `mysql.[safe|unsafe].commands` +* `mysql.[safe|unsafe].pool` +* `mysql.[safe|unsafe].result` +* `mysql.[safe|unsafe].prepared` +* `mysql.[safe|unsafe].connection` + +Each of these modules in unsafe mode provides the same API as the previous version of mysql. The safe version provides aliases to the original type names for the safe versions of types, and also provides the same functions as before that can be called via safe code. The one exception is in `mysql.safe.commands`, where some functions were for the deprecated `BackwardCompatPrepared`, which will be removed in the next major revision. + +If you are currently importing any of the above modules directly, or importing the `mysql` package, a first step to migration is to use the `mysql.safe` package. From there you will find that almost everything works exactly the same. + +In addition to these two new packages, we have introduced a package called `mysql.impl` (for internal use). This package contains the common implementations of the `safe` and `unsafe` modules, and should NEVER be directly imported. These modules are documented simply because that is where the code lives. But in a future version of mysql, this package will be removed. You should always use the `unsafe` or `safe` packages instead of trying to import the `mysql.impl` package. + +#### Importing both safe and unsafe + +It is possible to import some modules using the safe package, and some using the unsafe package in the case where you are gradually migrating to safe versions of your code. However, you should not import the same module from both safe and unsafe packages, as there will be naming conflicts. + +For example, if you import from `mysql.safe.result` and `mysql.unsafe.result`, the alias for `Row` will be tied to both `UnsafeRow` and `SafeRow`, resulting in a compilation ambiguity. + +But it is definitely possible to import `mysql.unsafe.result` and `mysql.safe.commands`. You may need to use the `safe` or `unsafe` conversion methods on the types to make your code function as desired. See details later on these conversion methods. + +### Migrating from Variant to MySQLVal + +The module `mysql.types` has been amended to contain the `MySQLVal` type. This type can hold any value type that MySQL supported originally, or a const pointer to such a type (for the purposes of prepared statements), or the value `null`. This is now the type used for all parameters to `query` and `exec` (in the safe API). The `mysql.types` import also provides compatibility shims with `Variant` such as `coerce`, `convertsTo`, `type`, `peek`, and `get` (See the documentation for [Variant](https://dlang.org/phobos/std_variant.html#.VariantN)). + +You can examine all the benefits of `TaggedAlgebraic` [here](https://vibed.org/api/taggedalgebraic.taggedalgebraic/TaggedAlgebraic). In particular, the usage of the `kind` member is preferred over using the `type` shim. Note that only safe operations are allowed, so for instance `opBinary!"+"` is not allowed on pointers. + +One pitfall of this migration has to do with `Variant`'s ability to represent *any* type -- including `MySQLVal`! If you have declared a variable of type `Variant`, and assign it to a `MySQLVal` result from a row or a query, it will compile, but it will NOT do what you are expecting. This will fail at runtime most likely. It is recommended before switching to the safe API to change those types to `MySQLVal` or use `auto` if possible. + +Example: +```D +import mysql.safe; + +Variant v = connection.queryValue("SELECT 1 AS `somevar`"); +``` + +This will compile and run, and the resulting variable `v` will be a `MySQLVal` typed as a `MySQLVal.Kind.Int` wrapped in a `Variant`. This is not what you want. In order to fix this, you should either re-type `v` as `MySQLVal` (or use `auto`) or use the `asVariant` function included in `mysql.types`: + +```D +import mysql.safe; + +// preferred +MySQLVal v = connection.queryValue("SELECT 1 AS `somevar`"); +// if necessary +Variant v2 = connection.queryValue("SELECT 1 AS `somevar`").asVariant; +``` + +One important thing to note is that the internals of mysql-native have all been switched to using `MySQLVal` instead of `Variant`. Only at the shallow API level is `Variant` used to provide the backwards compatible API. So if you do not switch, you will pay the penalty of having the library first construct a `MySQLVal` and then convert that to a `Variant` (or vice versa). + +### Row and ResultRange + +These two types were tied greatly to `Variant`. As such, they have been rewritten into `SafeRow` and `SafeResultRange` which use `MySQLVal` instead. Thin compatibility wrappers of `UnsafeRow` and `UnsafeResultRange` are available as well, which will convert the values to and from `Variant` as needed. Depending on which API you import `safe` or `unsafe`, these items are aliased to `Row` and `ResultRange` for source compatibility. + +However, each of these structures provides `unsafe` and `safe` conversion functions to convert between the two if absolutely necessary. In fact, most of the unsafe API calls that return an `UnsafeRow` or `UnsafeResultRange` are actually `@safe`, since the underlying implementation uses `MySQLVal`. It only becomes unsafe when you try to access a column as a `Variant`. + +The following example should compile with both `mysql.safe` and `mysql.unsafe`, but simply use `Variant` or `MySQLVal` as needed: +```D +import mysql; + +// assume a database table named 'mapping' with a string 'name' and int 'value' +int getMapping(Connection conn, string name) +{ + Row r = conn.queryRow("SELECT * FROM mapping WHERE name = ?", name); + assert(r[0].type == typeid(int)); + return r[0].get!int; +} +``` +While the safe version provides drop-in compatibility, it is recommended to switch to safe operations instead: + +```D +import mysql.safe; + +int getMapping(Connection conn, string name) @safe +{ + Row r = conn.queryRow("SELECT * FROM mapping WHERE name = ?", name); + //assert(r[0].type == typeid(int)); // this would work, but is @system + assert(r[0].kind == MySQLVal.Kind.Int); + return r[0].get!int; +} +``` + +In cases where current code requires the use of `Variant`, you can still use the safe API, and just do a conversion where needed: + +```D +import mysql.safe; + +struct EstablishedStruct +{ + Variant value; + int id; + void fetchFromDatabase(Connection conn) + { + // all safe calls except asVariant + value = conn.queryValue("SELECT value FROM theTable WHERE id = ?", id).asVariant; + } +} +``` + +### Prepared + +The `Prepared` struct contained support for setting/getting `Variant` parameters. These have been removed, and reimplemented as a `SafePrepared` struct, which uses `MySQLVal` instead. An `UnsafePrepared` wrapper has been provided, and like `Row`/`ResultSequence`, they have `unsafe`, and `safe` conversion functions. + +The `mysql.safe.prepared` module will alias `Prepared` as the safe version, and the `mysql.unsafe.prepared` module will alias `Prepared` as the unsafe version. + +One other aspect of `Prepared` that is different in the two versions is the `ParameterSpecialization` data. There are now two different such structs, a `SafeParameterSpecialization` and an `UnsafeParameterSpecialization`. The only difference between these two is the `chunkDelegate` being `@safe` or `@system`. If you do not use the `chunkDelegate`, or your delegate is actually `@safe`, then you should opt for the `@safe` API. + +### Connection + +The Connection class itself has not changed at all, except to add @safe attributes for all methods. However, the `mysql.connection` module contained the functions to generate `Prepared` structs. + +The `BackwardsCompatPrepared` struct defined in the original `mysql.connection` module is only available in the unsafe package. + +### MySQLPool + +`MySQLPool` has been factored into a templated type that has either a fully safe or partly safe API. The only public facing unsafe part was the user-supplied callback function to be called on every connection creation (which therefore made `lockConnection` unsafe). The unsafe version continues to use such a callback method (and is explicitly marked `@system`), whereas the safe version requires a `@safe` callback. + +If you do not use this callback mechanism, it is highly recommended that you use the safe API for the pool, as there is no actual difference between the two at that point. It's also very likely that your callback actually is `@safe`, even if you do use one. + +### The commands module + +The `mysql.commands` module has been factored into 2 versions, a safe and unsafe version. The only differences between these two are where `Variant` is concerned. All query and exec functions that accepted `Variant` explicitly have been reimplemented in the safe version to accept `MySQLVal`. All functions that return `Variant`, `Row` or `ResultRange` have been reimplemented to return `MySQLVal`, `SafeRow`, or `SafeResultRange` respectively. All functions that do not deal with these types are moved to the safe API, and aliased in the unsafe API. This means, as long as you do not use `Variant` explicitly, you should be able to switch over to the safe version of the API without changing your code. + +Even in cases where you elect to defer updating code, you can still import the `safe` API, and use `unsafe` conversion functions to keep existing code working. In most cases, this will not be necessary as the API is kept as similar as possible. + +## Recommended Transition Method + +We recommend following these steps to transition. In most cases, you should see very little breakage of code: + +1. Adjust your imports to import the safe versions of mysql modules. If you import the `mysql` package, instead import the `mysql.safe` package. If you import any of the individual modules listed in the [API](#the-safeunsafe-api) section, use the `mysql.safe.modulename` equivalent instead. +2. Adjust any explicit uses of `Variant` to `MySQLVal` or use `auto` for type inference. Remember that variables typed as `Variant` explicitly will consume `MySQLVal`, so you may not get compiler errors for these, but you will certainly get runtime errors. +3. If there are cases where you cannot stop using `Variant`, use the `asVariant` compatibility shim. +4. Adjust uses of `Variant`'s methods to use the `TaggedAlgebraic` versions. Most important is usage of the `kind` member, as comparing two `TypeInfo` objects is currently `@system`. +5. `MySQLVal` provides a richer experience of type forwarding, so you may be able to relax some of your code that is concerned with first fetching the concrete type from the `Variant`. Notably, `MySQLVal` can access directly members of any of the `std.datetime` types, such as `year`, `month`, or `day`. +6. Report ANY issues with compatibility or bugs to the issue tracker so we may deal with them ASAP. Our intention is to have you be able to use v3.2.0 without having to adjust any code that worked with v3.1.0. diff --git a/ddoc/macros.ddoc b/ddoc/macros.ddoc index 25c29fd5..3c94997b 100644 --- a/ddoc/macros.ddoc +++ b/ddoc/macros.ddoc @@ -6,9 +6,10 @@ Macros: DOLLAR = $ COLON = : EM = $(B $(I $0) ) - + TYPE_MAPPINGS = See the $(LINK2 $(DDOX_ROOT_DIR)/mysql.html, MySQL/D Type Mappings tables) - + SAFE_MIGRATION = See the $(LINK2 https://github.com/mysql-d/mysql-native/blob/master/SAFE_MIGRATION.md, Safe Migration) document for more details. + STD_DATETIME_DATE = $(D_INLINECODE $(LINK2 https://dlang.org/phobos/std_datetime_date.html#$0, $0)) STD_EXCEPTION = $(D_INLINECODE $(LINK2 https://dlang.org/phobos/std_exception.html#$0, $0)) STD_FILE = $(D_INLINECODE $(LINK2 https://dlang.org/phobos/std_file.html#$0, $0)) diff --git a/dub.sdl b/dub.sdl index b63b320e..b691ab16 100644 --- a/dub.sdl +++ b/dub.sdl @@ -1,10 +1,11 @@ name "mysql-native" description "A native MySQL driver implementation based on Steve Teale's original" license "BSL-1.0" -copyright "Copyright (c) 2011-2021 Steve Teale, James W. Oliphant, Simen Endsjø, Sönke Ludwig, Sergey Shamov, Nick Sabalausky, and Steven Schveighoffer" +copyright "Copyright (c) 2011-2022 Steve Teale, James W. Oliphant, Simen Endsjø, Sönke Ludwig, Sergey Shamov, Nick Sabalausky, and Steven Schveighoffer" authors "Steve Teale" "James W. Oliphant" "Simen Endsjø" "Sönke Ludwig" "Sergey Shamov" "Nick Sabalausky" "Steven Schveighoffer" dependency "vibe-core" version=">=1.16.0" optional=true +dependency "taggedalgebraic" version=">=0.11.22" toolchainRequirements frontend=">=2.068" diff --git a/integration-tests/source/mysql/maintests.d b/integration-tests/source/mysql/maintests.d index 6e8bfd60..6c000159 100644 --- a/integration-tests/source/mysql/maintests.d +++ b/integration-tests/source/mysql/maintests.d @@ -1,12 +1,14 @@ module mysql.maintests; import mysql.test.common; -import mysql; import mysql.protocol.constants; +import mysql.exceptions; +import mysql.types; import std.exception; import std.variant; import std.typecons; import std.array; +import std.range; import std.algorithm; // mysql.commands @@ -14,97 +16,101 @@ import std.algorithm; debug(MYSQLN_TESTS) unittest { - import std.array; - import std.range; - import mysql.test.common; - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `columnSpecial`"); - cn.exec("CREATE TABLE `columnSpecial` ( - `data` LONGBLOB - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below - auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - auto data = alph.cycle.take(totalSize).array; - cn.exec("INSERT INTO `columnSpecial` VALUES (\""~(cast(string)data)~"\")"); - - // Common stuff - int chunkSize; - immutable selectSQL = "SELECT `data` FROM `columnSpecial`"; - ubyte[] received; - bool lastValueOfFinished; - void receiver(const(ubyte)[] chunk, bool finished) + void test(bool doSafe)() { - assert(lastValueOfFinished == false); + mixin(doImports(doSafe, "commands", "connection")); + mixin(scopedCn); - if(finished) - assert(chunk.length == chunkSize); - else - assert(chunk.length < chunkSize); // Not always true in general, but true in this unittest + // Setup + cn.exec("DROP TABLE IF EXISTS `columnSpecial`"); + cn.exec("CREATE TABLE `columnSpecial` ( + `data` LONGBLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - received ~= chunk; - lastValueOfFinished = finished; - } + immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below + auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + auto data = alph.cycle.take(totalSize).array; + cn.exec("INSERT INTO `columnSpecial` VALUES (\""~(cast(const(char)[])data)~"\")"); + + // Common stuff + int chunkSize; + immutable selectSQL = "SELECT `data` FROM `columnSpecial`"; + ubyte[] received; + bool lastValueOfFinished; + void receiver(const(ubyte)[] chunk, bool finished) + { + assert(lastValueOfFinished == false); - // Sanity check - auto value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); + if(finished) + assert(chunk.length == chunkSize); + else + assert(chunk.length < chunkSize); // Not always true in general, but true in this unittest - // Use ColumnSpecialization with sql string, - // and totalSize as a multiple of chunkSize - { - chunkSize = 100; - assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); - auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + received ~= chunk; + lastValueOfFinished = finished; + } - received = null; - lastValueOfFinished = false; - value = cn.queryValue(selectSQL, [columnSpecial]); + // Sanity check + auto value = cn.queryValue(selectSQL); assert(!value.isNull); assert(value.get == data); - //TODO: ColumnSpecialization is not yet implemented - //assert(lastValueOfFinished == true); - //assert(received == data); - } - // Use ColumnSpecialization with sql string, - // and totalSize as a non-multiple of chunkSize - { - chunkSize = 64; - assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); - auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + // Use ColumnSpecialization with sql string, + // and totalSize as a multiple of chunkSize + { + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + value = cn.queryValue(selectSQL, [columnSpecial]); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } - received = null; - lastValueOfFinished = false; - value = cn.queryValue(selectSQL, [columnSpecial]); - assert(!value.isNull); - assert(value.get == data); - //TODO: ColumnSpecialization is not yet implemented - //assert(lastValueOfFinished == true); - //assert(received == data); - } + // Use ColumnSpecialization with sql string, + // and totalSize as a non-multiple of chunkSize + { + chunkSize = 64; + assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + value = cn.queryValue(selectSQL, [columnSpecial]); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } - // Use ColumnSpecialization with prepared statement, - // and totalSize as a multiple of chunkSize - { - chunkSize = 100; - assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); - auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); - - received = null; - lastValueOfFinished = false; - auto prepared = cn.prepare(selectSQL); - prepared.columnSpecials = [columnSpecial]; - value = cn.queryValue(prepared); - assert(!value.isNull); - assert(value.get == data); - //TODO: ColumnSpecialization is not yet implemented - //assert(lastValueOfFinished == true); - //assert(received == data); + // Use ColumnSpecialization with prepared statement, + // and totalSize as a multiple of chunkSize + { + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto columnSpecial = ColumnSpecialization(0, 0xfc, chunkSize, &receiver); + + received = null; + lastValueOfFinished = false; + auto prepared = cn.prepare(selectSQL); + prepared.columnSpecials = [columnSpecial]; + value = cn.queryValue(prepared); + assert(!value.isNull); + assert(value.get == data); + //TODO: ColumnSpecialization is not yet implemented + //assert(lastValueOfFinished == true); + //assert(received == data); + } } + + test!false(); + () @safe { test!true(); } (); } // Test what happens when queryRowTuple receives no rows @@ -112,7 +118,7 @@ unittest debug(MYSQLN_TESTS) unittest { - import mysql.test.common : scopedCn, createCn; + import mysql.safe.commands; mixin(scopedCn); cn.exec("DROP TABLE IF EXISTS `queryRowTuple_noRows`"); @@ -129,260 +135,279 @@ unittest debug(MYSQLN_TESTS) unittest { - import std.array; - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `execOverloads`"); - cn.exec("CREATE TABLE `execOverloads` ( - `i` INTEGER, - `s` VARCHAR(50) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable prepareSQL = "INSERT INTO `execOverloads` VALUES (?, ?)"; - - // Do the inserts, using exec - - // exec: const(char[]) sql - assert(cn.exec("INSERT INTO `execOverloads` VALUES (1, \"aa\")") == 1); - assert(cn.exec(prepareSQL, 2, "bb") == 1); - assert(cn.exec(prepareSQL, [Variant(3), Variant("cc")]) == 1); - - // exec: prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(4, "dd"); - assert(cn.exec(prepared) == 1); - - assert(cn.exec(prepared, 5, "ee") == 1); - assert(prepared.getArg(0) == 5); - assert(prepared.getArg(1) == "ee"); - - assert(cn.exec(prepared, [Variant(6), Variant("ff")]) == 1); - assert(prepared.getArg(0) == 6); - assert(prepared.getArg(1) == "ff"); - - // exec: bcPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(7, "gg"); - assert(cn.exec(bcPrepared) == 1); - assert(bcPrepared.getArg(0) == 7); - assert(bcPrepared.getArg(1) == "gg"); - - // Check results - auto rows = cn.query("SELECT * FROM `execOverloads`").array(); - assert(rows.length == 7); - - assert(rows[0].length == 2); - assert(rows[1].length == 2); - assert(rows[2].length == 2); - assert(rows[3].length == 2); - assert(rows[4].length == 2); - assert(rows[5].length == 2); - assert(rows[6].length == 2); - - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); - assert(rows[1][0] == 2); - assert(rows[1][1] == "bb"); - assert(rows[2][0] == 3); - assert(rows[2][1] == "cc"); - assert(rows[3][0] == 4); - assert(rows[3][1] == "dd"); - assert(rows[4][0] == 5); - assert(rows[4][1] == "ee"); - assert(rows[5][0] == 6); - assert(rows[5][1] == "ff"); - assert(rows[6][0] == 7); - assert(rows[6][1] == "gg"); -} + void test(bool doSafe)() + { + mixin(doImports(doSafe, "connection", "commands")); + mixin(scopedCn); + static if(doSafe) + alias MYVAL = MySQLVal; + else + alias MYVAL = Variant; -@("queryOverloads") -debug(MYSQLN_TESTS) -unittest -{ - import std.array; - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `execOverloads`"); + cn.exec("CREATE TABLE `execOverloads` ( + `i` INTEGER, + `s` VARCHAR(50) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("DROP TABLE IF EXISTS `queryOverloads`"); - cn.exec("CREATE TABLE `queryOverloads` ( - `i` INTEGER, - `s` VARCHAR(50) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `queryOverloads` VALUES (1, \"aa\"), (2, \"bb\"), (3, \"cc\")"); + immutable prepareSQL = "INSERT INTO `execOverloads` VALUES (?, ?)"; - immutable prepareSQL = "SELECT * FROM `queryOverloads` WHERE `i`=? AND `s`=?"; + // Do the inserts, using exec - // Test query - { - Row[] rows; + // exec: const(char[]) sql + assert(cn.exec("INSERT INTO `execOverloads` VALUES (1, \"aa\")") == 1); + assert(cn.exec(prepareSQL, 2, "bb") == 1); + assert(cn.exec(prepareSQL, [MYVAL(3), MYVAL("cc")]) == 1); - // String sql - rows = cn.query("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); + // exec: prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(4, "dd"); + assert(cn.exec(prepared) == 1); - rows = cn.query(prepareSQL, 2, "bb").array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 2); - assert(rows[0][1] == "bb"); + assert(cn.exec(prepared, 5, "ee") == 1); + assert(prepared.getArg(0) == 5); + assert(prepared.getArg(1) == "ee"); - rows = cn.query(prepareSQL, [Variant(3), Variant("cc")]).array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 3); - assert(rows[0][1] == "cc"); + assert(cn.exec(prepared, [MYVAL(6), MYVAL("ff")]) == 1); + assert(prepared.getArg(0) == 6); + assert(prepared.getArg(1) == "ff"); - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(1, "aa"); - rows = cn.query(prepared).array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 1); - assert(rows[0][1] == "aa"); + // exec: bcPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + static assert(doSafe || is(typeof(bcPrepared) == BackwardCompatPrepared)); + bcPrepared.setArgs(7, "gg"); + assert(cn.exec(bcPrepared) == 1); + assert(bcPrepared.getArg(0) == 7); + assert(bcPrepared.getArg(1) == "gg"); - rows = cn.query(prepared, 2, "bb").array; - assert(rows.length == 1); - assert(rows[0].length == 2); - assert(rows[0][0] == 2); - assert(rows[0][1] == "bb"); + // Check results + auto rows = cn.query("SELECT * FROM `execOverloads`").array(); + assert(rows.length == 7); - rows = cn.query(prepared, [Variant(3), Variant("cc")]).array; - assert(rows.length == 1); assert(rows[0].length == 2); - assert(rows[0][0] == 3); - assert(rows[0][1] == "cc"); + assert(rows[1].length == 2); + assert(rows[2].length == 2); + assert(rows[3].length == 2); + assert(rows[4].length == 2); + assert(rows[5].length == 2); + assert(rows[6].length == 2); - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - rows = cn.query(bcPrepared).array; - assert(rows.length == 1); - assert(rows[0].length == 2); assert(rows[0][0] == 1); assert(rows[0][1] == "aa"); + assert(rows[1][0] == 2); + assert(rows[1][1] == "bb"); + assert(rows[2][0] == 3); + assert(rows[2][1] == "cc"); + assert(rows[3][0] == 4); + assert(rows[3][1] == "dd"); + assert(rows[4][0] == 5); + assert(rows[4][1] == "ee"); + assert(rows[5][0] == 6); + assert(rows[5][1] == "ff"); + assert(rows[6][0] == 7); + assert(rows[6][1] == "gg"); } + test!false(); + () @safe { test!true(); } (); +} - // Test queryRow - { - // Note, queryRow returns Nullable, but we always expect to get a row, - // so we will let the `get` check in Nullable assert that it's not - // null. - Row row; - - // String sql - row = cn.queryRow("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").get; - assert(row.length == 2); - assert(row[0] == 1); - assert(row[1] == "aa"); - - row = cn.queryRow(prepareSQL, 2, "bb").get; - assert(row.length == 2); - assert(row[0] == 2); - assert(row[1] == "bb"); - - row = cn.queryRow(prepareSQL, [Variant(3), Variant("cc")]).get; - assert(row.length == 2); - assert(row[0] == 3); - assert(row[1] == "cc"); - - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(1, "aa"); - row = cn.queryRow(prepared).get; - assert(row.length == 2); - assert(row[0] == 1); - assert(row[1] == "aa"); - - row = cn.queryRow(prepared, 2, "bb").get; - assert(row.length == 2); - assert(row[0] == 2); - assert(row[1] == "bb"); - - row = cn.queryRow(prepared, [Variant(3), Variant("cc")]).get; - assert(row.length == 2); - assert(row[0] == 3); - assert(row[1] == "cc"); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - row = cn.queryRow(bcPrepared).get; - assert(row.length == 2); - assert(row[0] == 1); - assert(row[1] == "aa"); - } - - // Test queryRowTuple - { - int i; - string s; - - // String sql - cn.queryRowTuple("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"", i, s); - assert(i == 1); - assert(s == "aa"); - - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(2, "bb"); - cn.queryRowTuple(prepared, i, s); - assert(i == 2); - assert(s == "bb"); - - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(3, "cc"); - cn.queryRowTuple(bcPrepared, i, s); - assert(i == 3); - assert(s == "cc"); - } - - // Test queryValue +@("queryOverloads") +debug(MYSQLN_TESTS) +unittest +{ + void test(bool doSafe)() { - Variant value; - - // String sql - value = cn.queryValue("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").get; - assert(value.type != typeid(typeof(null))); - assert(value == 1); + mixin(doImports(doSafe, "connection", "commands", "result")); + mixin(scopedCn); + static if(doSafe) + alias MYVAL = MySQLVal; + else + alias MYVAL = Variant; - value = cn.queryValue(prepareSQL, 2, "bb").get; - assert(value.type != typeid(typeof(null))); - assert(value == 2); + cn.exec("DROP TABLE IF EXISTS `queryOverloads`"); + cn.exec("CREATE TABLE `queryOverloads` ( + `i` INTEGER, + `s` VARCHAR(50) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `queryOverloads` VALUES (1, \"aa\"), (2, \"bb\"), (3, \"cc\")"); - value = cn.queryValue(prepareSQL, [Variant(3), Variant("cc")]).get; - assert(value.type != typeid(typeof(null))); - assert(value == 3); + immutable prepareSQL = "SELECT * FROM `queryOverloads` WHERE `i`=? AND `s`=?"; - // Prepared sql - auto prepared = cn.prepare(prepareSQL); - prepared.setArgs(1, "aa"); - value = cn.queryValue(prepared).get; - assert(value.type != typeid(typeof(null))); - assert(value == 1); + // Test query + { + Row[] rows; + + // String sql + rows = cn.query("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + + rows = cn.query(prepareSQL, 2, "bb").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == "bb"); + + rows = cn.query(prepareSQL, [MYVAL(3), MYVAL("cc")]).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 3); + assert(rows[0][1] == "cc"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + rows = cn.query(prepared).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + + rows = cn.query(prepared, 2, "bb").array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == "bb"); + + rows = cn.query(prepared, [MYVAL(3), MYVAL("cc")]).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 3); + assert(rows[0][1] == "cc"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + static assert(doSafe || is(typeof(bcPrepared) == BackwardCompatPrepared)); + bcPrepared.setArgs(1, "aa"); + rows = cn.query(bcPrepared).array; + assert(rows.length == 1); + assert(rows[0].length == 2); + assert(rows[0][0] == 1); + assert(rows[0][1] == "aa"); + } - value = cn.queryValue(prepared, 2, "bb").get; - assert(value.type != typeid(typeof(null))); - assert(value == 2); + // Test queryRow + { + // Note, queryRow returns Nullable, but we always expect to get a row, + // so we will let the `get` check in Nullable assert that it's not + // null. + Row row; + + // String sql + row = cn.queryRow("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").get; + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + + row = cn.queryRow(prepareSQL, 2, "bb").get; + assert(row.length == 2); + assert(row[0] == 2); + assert(row[1] == "bb"); + + row = cn.queryRow(prepareSQL, [MYVAL(3), MYVAL("cc")]).get; + assert(row.length == 2); + assert(row[0] == 3); + assert(row[1] == "cc"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + row = cn.queryRow(prepared).get; + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + + row = cn.queryRow(prepared, 2, "bb").get; + assert(row.length == 2); + assert(row[0] == 2); + assert(row[1] == "bb"); + + row = cn.queryRow(prepared, [MYVAL(3), MYVAL("cc")]).get; + assert(row.length == 2); + assert(row[0] == 3); + assert(row[1] == "cc"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + static assert(doSafe || is(typeof(bcPrepared) == BackwardCompatPrepared)); + bcPrepared.setArgs(1, "aa"); + row = cn.queryRow(bcPrepared).get; + assert(row.length == 2); + assert(row[0] == 1); + assert(row[1] == "aa"); + } - value = cn.queryValue(prepared, [Variant(3), Variant("cc")]).get; - assert(value.type != typeid(typeof(null))); - assert(value == 3); + // Test queryRowTuple + { + int i; + string s; + + // String sql + cn.queryRowTuple("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"", i, s); + assert(i == 1); + assert(s == "aa"); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(2, "bb"); + cn.queryRowTuple(prepared, i, s); + assert(i == 2); + assert(s == "bb"); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + static assert(doSafe || is(typeof(bcPrepared) == BackwardCompatPrepared)); + bcPrepared.setArgs(3, "cc"); + cn.queryRowTuple(bcPrepared, i, s); + assert(i == 3); + assert(s == "cc"); + } - // BCPrepared sql - auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); - bcPrepared.setArgs(1, "aa"); - value = cn.queryValue(bcPrepared).get; - assert(value.type != typeid(typeof(null))); - assert(value == 1); + // Test queryValue + { + MYVAL value; + + // String sql + value = cn.queryValue("SELECT * FROM `queryOverloads` WHERE `i`=1 AND `s`=\"aa\"").get; + assert(!value.valIsNull); + assert(value == 1); + + value = cn.queryValue(prepareSQL, 2, "bb").get; + assert(!value.valIsNull); + assert(value == 2); + + value = cn.queryValue(prepareSQL, [MYVAL(3), MYVAL("cc")]).get; + assert(!value.valIsNull); + assert(value == 3); + + // Prepared sql + auto prepared = cn.prepare(prepareSQL); + prepared.setArgs(1, "aa"); + value = cn.queryValue(prepared).get; + assert(!value.valIsNull); + assert(value == 1); + + value = cn.queryValue(prepared, 2, "bb").get; + assert(!value.valIsNull); + assert(value == 2); + + value = cn.queryValue(prepared, [MYVAL(3), MYVAL("cc")]).get; + assert(!value.valIsNull); + assert(value == 3); + + // BCPrepared sql + auto bcPrepared = cn.prepareBackwardCompatImpl(prepareSQL); + static assert(doSafe || is(typeof(bcPrepared) == BackwardCompatPrepared)); + bcPrepared.setArgs(1, "aa"); + value = cn.queryValue(bcPrepared).get; + assert(!value.valIsNull); + assert(value == 1); + } } + test!false(); + () @safe { test!true(); } (); } // mysql.connection @@ -390,7 +415,8 @@ unittest debug(MYSQLN_TESTS) unittest { - import mysql.test.common; + import mysql.safe.connection; + import mysql.safe.commands; mixin(scopedCn); exec(cn, `DROP FUNCTION IF EXISTS hello`); @@ -411,10 +437,11 @@ unittest debug(MYSQLN_TESTS) unittest { - import mysql.test.common; import mysql.test.integration; + import mysql.safe.connection; + import mysql.safe.commands; mixin(scopedCn); - initBaseTestTables(cn); + initBaseTestTables!true(cn); exec(cn, `DROP PROCEDURE IF EXISTS insert2`); exec(cn, ` @@ -439,198 +466,213 @@ unittest debug(MYSQLN_TESTS) unittest { - import std.variant; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `reconnect`"); - cn.exec("CREATE TABLE `reconnect` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `reconnect` VALUES (1),(2),(3)"); - - enum sql = "SELECT a FROM `reconnect`"; - - // Sanity check - auto rows = cn.query(sql).array; - assert(rows[0][0] == 1); - assert(rows[1][0] == 2); - assert(rows[2][0] == 3); - - // Ensure reconnect keeps the same connection when it's supposed to - auto range = cn.query(sql); - assert(range.front[0] == 1); - cn.reconnect(); - assert(!cn.closed); // Is open? - assert(range.isValid); // Still valid? - range.popFront(); - assert(range.front[0] == 2); - - // Ensure reconnect reconnects when it's supposed to - range = cn.query(sql); - assert(range.front[0] == 1); - cn._clientCapabilities = ~cn._clientCapabilities; // Pretend that we're changing the clientCapabilities - cn.reconnect(~cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Try manually reconnecting - range = cn.query(sql); - assert(range.front[0] == 1); - cn.connect(cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Try manually closing and connecting - range = cn.query(sql); - assert(range.front[0] == 1); - cn.close(); - assert(cn.closed); // Is closed? - assert(!range.isValid); // Was invalidated? - cn.connect(cn._clientCapabilities); - assert(!cn.closed); // Is open? - assert(!range.isValid); // Was invalidated? - cn.query(sql).array; // Connection still works? - - // Auto-reconnect upon a command - cn.close(); - assert(cn.closed); - range = cn.query(sql); - assert(!cn.closed); - assert(range.front[0] == 1); -} + static void test(bool doSafe)() + { + mixin(doImports(doSafe, "commands")); + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `reconnect`"); + cn.exec("CREATE TABLE `reconnect` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `reconnect` VALUES (1),(2),(3)"); -@("releaseAll") -debug(MYSQLN_TESTS) -unittest -{ - mixin(scopedCn); + enum sql = "SELECT a FROM `reconnect`"; - cn.exec("DROP TABLE IF EXISTS `releaseAll`"); - cn.exec("CREATE TABLE `releaseAll` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - auto preparedSelect = cn.prepare("SELECT * FROM `releaseAll`"); - auto preparedInsert = cn.prepare("INSERT INTO `releaseAll` (a) VALUES (1)"); - assert(cn.isRegistered(preparedSelect)); - assert(cn.isRegistered(preparedInsert)); - - cn.releaseAll(); - assert(!cn.isRegistered(preparedSelect)); - assert(!cn.isRegistered(preparedInsert)); - cn.exec("INSERT INTO `releaseAll` (a) VALUES (1)"); - assert(!cn.isRegistered(preparedSelect)); - assert(!cn.isRegistered(preparedInsert)); - - cn.exec(preparedInsert); - cn.query(preparedSelect).array; - assert(cn.isRegistered(preparedSelect)); - assert(cn.isRegistered(preparedInsert)); + // Sanity check + auto rows = cn.query(sql).array; + assert(rows[0][0] == 1); + assert(rows[1][0] == 2); + assert(rows[2][0] == 3); + + // Ensure reconnect keeps the same connection when it's supposed to + auto range = cn.query(sql); + assert(range.front[0] == 1); + cn.reconnect(); + assert(!cn.closed); // Is open? + assert(range.isValid); // Still valid? + range.popFront(); + assert(range.front[0] == 2); + + // Ensure reconnect reconnects when it's supposed to + range = cn.query(sql); + assert(range.front[0] == 1); + cn._clientCapabilities = ~cn._clientCapabilities; // Pretend that we're changing the clientCapabilities + cn.reconnect(~cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Try manually reconnecting + range = cn.query(sql); + assert(range.front[0] == 1); + cn.connect(cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Try manually closing and connecting + range = cn.query(sql); + assert(range.front[0] == 1); + cn.close(); + assert(cn.closed); // Is closed? + assert(!range.isValid); // Was invalidated? + cn.connect(cn._clientCapabilities); + assert(!cn.closed); // Is open? + assert(!range.isValid); // Was invalidated? + cn.query(sql).array; // Connection still works? + + // Auto-reconnect upon a command + cn.close(); + assert(cn.closed); + range = cn.query(sql); + assert(!cn.closed); + assert(range.front[0] == 1); + } + test!false(); + () @safe { test!true(); } (); } -// Test register, release, isRegistered, and auto-register for prepared statements -@("autoRegistration") +@("releaseAll") debug(MYSQLN_TESTS) unittest { - import mysql.connection; - import mysql.test.common; - - Prepared preparedInsert; - Prepared preparedSelect; - immutable insertSQL = "INSERT INTO `autoRegistration` VALUES (1), (2)"; - immutable selectSQL = "SELECT `val` FROM `autoRegistration`"; - int queryTupleResult; - + static void test(bool doSafe)() { + mixin(doImports(doSafe, "commands", "connection")); mixin(scopedCn); - // Setup - cn.exec("DROP TABLE IF EXISTS `autoRegistration`"); - cn.exec("CREATE TABLE `autoRegistration` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - // Initial register - preparedInsert = cn.prepare(insertSQL); - preparedSelect = cn.prepare(selectSQL); + cn.exec("DROP TABLE IF EXISTS `releaseAll`"); + cn.exec("CREATE TABLE `releaseAll` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - // Test basic register, release, isRegistered - assert(cn.isRegistered(preparedInsert)); - assert(cn.isRegistered(preparedSelect)); - cn.release(preparedInsert); - cn.release(preparedSelect); - assert(!cn.isRegistered(preparedInsert)); - assert(!cn.isRegistered(preparedSelect)); - - // Test manual re-register - cn.register(preparedInsert); - cn.register(preparedSelect); - assert(cn.isRegistered(preparedInsert)); + auto preparedSelect = cn.prepare("SELECT * FROM `releaseAll`"); + auto preparedInsert = cn.prepare("INSERT INTO `releaseAll` (a) VALUES (1)"); assert(cn.isRegistered(preparedSelect)); - - // Test double register - cn.register(preparedInsert); - cn.register(preparedSelect); assert(cn.isRegistered(preparedInsert)); - assert(cn.isRegistered(preparedSelect)); - // Test double release - cn.release(preparedInsert); - cn.release(preparedSelect); - assert(!cn.isRegistered(preparedInsert)); + cn.releaseAll(); assert(!cn.isRegistered(preparedSelect)); - cn.release(preparedInsert); - cn.release(preparedSelect); assert(!cn.isRegistered(preparedInsert)); + cn.exec("INSERT INTO `releaseAll` (a) VALUES (1)"); assert(!cn.isRegistered(preparedSelect)); - } - - // Note that at this point, both prepared statements still exist, - // but are no longer registered on any connection. In fact, there - // are no open connections anymore. - - // Test auto-register: exec - { - mixin(scopedCn); - assert(!cn.isRegistered(preparedInsert)); + cn.exec(preparedInsert); + cn.query(preparedSelect).array; + assert(cn.isRegistered(preparedSelect)); assert(cn.isRegistered(preparedInsert)); } + test!false(); + () @safe { test!true(); } (); +} - // Test auto-register: query +// Test register, release, isRegistered, and auto-register for prepared statements +@("autoRegistration") +debug(MYSQLN_TESTS) +unittest +{ + static void test(bool doSafe)() { - mixin(scopedCn); + mixin(doImports(doSafe, "connection", "prepared", "commands")); - assert(!cn.isRegistered(preparedSelect)); - cn.query(preparedSelect).each(); - assert(cn.isRegistered(preparedSelect)); - } + Prepared preparedInsert; + Prepared preparedSelect; + immutable insertSQL = "INSERT INTO `autoRegistration` VALUES (1), (2)"; + immutable selectSQL = "SELECT `val` FROM `autoRegistration`"; + int queryTupleResult; - // Test auto-register: queryRow - { - mixin(scopedCn); + { + mixin(scopedCn); + + // Setup + cn.exec("DROP TABLE IF EXISTS `autoRegistration`"); + cn.exec("CREATE TABLE `autoRegistration` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + // Initial register + preparedInsert = cn.prepare(insertSQL); + preparedSelect = cn.prepare(selectSQL); + + // Test basic register, release, isRegistered + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + + // Test manual re-register + cn.register(preparedInsert); + cn.register(preparedSelect); + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + + // Test double register + cn.register(preparedInsert); + cn.register(preparedSelect); + assert(cn.isRegistered(preparedInsert)); + assert(cn.isRegistered(preparedSelect)); + + // Test double release + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + cn.release(preparedInsert); + cn.release(preparedSelect); + assert(!cn.isRegistered(preparedInsert)); + assert(!cn.isRegistered(preparedSelect)); + } - assert(!cn.isRegistered(preparedSelect)); - cn.queryRow(preparedSelect); - assert(cn.isRegistered(preparedSelect)); - } + // Note that at this point, both prepared statements still exist, + // but are no longer registered on any connection. In fact, there + // are no open connections anymore. - // Test auto-register: queryRowTuple - { - mixin(scopedCn); + // Test auto-register: exec + { + mixin(scopedCn); - assert(!cn.isRegistered(preparedSelect)); - cn.queryRowTuple(preparedSelect, queryTupleResult); - assert(cn.isRegistered(preparedSelect)); - } + assert(!cn.isRegistered(preparedInsert)); + cn.exec(preparedInsert); + assert(cn.isRegistered(preparedInsert)); + } - // Test auto-register: queryValue - { - mixin(scopedCn); + // Test auto-register: query + { + mixin(scopedCn); - assert(!cn.isRegistered(preparedSelect)); - cn.queryValue(preparedSelect); - assert(cn.isRegistered(preparedSelect)); + assert(!cn.isRegistered(preparedSelect)); + cn.query(preparedSelect).each(); + assert(cn.isRegistered(preparedSelect)); + } + + // Test auto-register: queryRow + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedSelect)); + cn.queryRow(preparedSelect); + assert(cn.isRegistered(preparedSelect)); + } + + // Test auto-register: queryRowTuple + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedSelect)); + cn.queryRowTuple(preparedSelect, queryTupleResult); + assert(cn.isRegistered(preparedSelect)); + } + + // Test auto-register: queryValue + { + mixin(scopedCn); + + assert(!cn.isRegistered(preparedSelect)); + cn.queryValue(preparedSelect); + assert(cn.isRegistered(preparedSelect)); + } } + test!false(); + () @safe {test!true(); } (); } // An attempt to reproduce issue #81: Using mysql-native driver with no default database @@ -639,18 +681,24 @@ unittest debug(MYSQLN_TESTS) unittest { - import mysql.escape; - import std.conv; - mixin(scopedCn); + import std.conv : text; + static void test(bool doSafe)() + { + import mysql.escape; + mixin(doImports(doSafe, "commands", "connection")); + mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `issue81`"); - cn.exec("CREATE TABLE `issue81` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `issue81` (a) VALUES (1)"); + cn.exec("DROP TABLE IF EXISTS `issue81`"); + cn.exec("CREATE TABLE `issue81` (a INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `issue81` (a) VALUES (1)"); - auto cn2 = new Connection(text("host=", cn._host, ";port=", cn._port, ";user=", cn._user, ";pwd=", cn._pwd)); - scope(exit) cn2.close(); + auto cn2 = new Connection(text("host=", cn._host, ";port=", cn._port, ";user=", cn._user, ";pwd=", cn._pwd)); + scope(exit) cn2.close(); - cn2.query("SELECT * FROM `"~mysqlEscape(cn._db).text~"`.`issue81`"); + cn2.query("SELECT * FROM `"~mysqlEscape(cn._db).text~"`.`issue81`"); + } + test!false(); + () @safe {test!true(); } (); } // Regression test for Issue #154: @@ -662,22 +710,28 @@ unittest debug(MYSQLN_TESTS) unittest { - mixin(scopedCn); - - cn.exec("DROP TABLE IF EXISTS `dropConnection`"); - cn.exec("CREATE TABLE `dropConnection` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `dropConnection` VALUES (1), (2), (3)"); - import mysql.prepared; + static void test(bool doSafe)() { - auto prep = cn.prepare("SELECT * FROM `dropConnection`"); - cn.query(prep); + mixin(doImports(doSafe, "commands", "connection", "prepared")); + mixin(scopedCn); + + cn.exec("DROP TABLE IF EXISTS `dropConnection`"); + cn.exec("CREATE TABLE `dropConnection` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `dropConnection` VALUES (1), (2), (3)"); + { + auto prep = cn.prepare("SELECT * FROM `dropConnection`"); + cn.query(prep); + } + // close the socket forcibly + cn._socket.close(); + // this should still work (it should reconnect). + cn.exec("DROP TABLE `dropConnection`"); } - // close the socket forcibly - cn._socket.close(); - // this should still work (it should reconnect). - cn.exec("DROP TABLE `dropConnection`"); + + test!false(); + () @safe {test!true(); } (); } /+ @@ -717,8 +771,6 @@ debug(MYSQLN_TESTS) ///ditto private RCPrepared rcPrepare(Connection conn, const(char[]) sql) { - import std.algorithm.mutation : move; - auto prepared = conn.prepare(sql); auto payload = RCPreparedPayload(prepared, conn); return refCounted(move(payload)); @@ -780,11 +832,9 @@ debug(MYSQLN_TESTS) debug(MYSQLN_TESTS) unittest { - import std.exception; - import mysql.commands; - import mysql.connection; - import mysql.prepared; - import mysql.test.common : scopedCn, createCn; + import mysql.safe.commands; + import mysql.safe.connection; + import mysql.safe.prepared; mixin(scopedCn); cn.exec("DROP TABLE IF EXISTS `wrongFunctionException`"); @@ -821,161 +871,177 @@ version(Have_vibe_core) debug(MYSQLN_TESTS) unittest { - auto count = 0; - void callback(Connection conn) + static void test(bool doSafe)() { - count++; - } - - // Test getting/setting - auto poolA = new MySQLPool(testConnectionStr, &callback); - auto poolB = new MySQLPool(testConnectionStr); - auto poolNoCallback = new MySQLPool(testConnectionStr); - - assert(poolA.onNewConnection == &callback); - assert(poolB.onNewConnection is null); - assert(poolNoCallback.onNewConnection is null); - - poolB.onNewConnection = &callback; - assert(poolB.onNewConnection == &callback); - assert(count == 0); - - // Ensure callback is called - { - auto connA = poolA.lockConnection(); - assert(!connA.closed); - assert(count == 1); - - auto connB = poolB.lockConnection(); - assert(!connB.closed); - assert(count == 2); - } - - // Ensure works with no callback - { - auto oldCount = count; - auto poolC = new MySQLPool(testConnectionStr); - auto connC = poolC.lockConnection(); - assert(!connC.closed); - assert(count == oldCount); + mixin(doImports(doSafe, "pool", "connection")); + auto count = 0; + void callback(Connection conn) + { + count++; + } + + // Test getting/setting + auto poolA = new MySQLPool(testConnectionStr, &callback); + auto poolB = new MySQLPool(testConnectionStr); + auto poolNoCallback = new MySQLPool(testConnectionStr); + + assert(poolA.onNewConnection == &callback); + assert(poolB.onNewConnection is null); + assert(poolNoCallback.onNewConnection is null); + + poolB.onNewConnection = &callback; + assert(poolB.onNewConnection == &callback); + assert(count == 0); + + // Ensure callback is called + { + auto connA = poolA.lockConnection(); + assert(!connA.closed); + assert(count == 1); + + auto connB = poolB.lockConnection(); + assert(!connB.closed); + assert(count == 2); + } + + // Ensure works with no callback + { + auto oldCount = count; + auto poolC = new MySQLPool(testConnectionStr); + auto connC = poolC.lockConnection(); + assert(!connC.closed); + assert(count == oldCount); + } } + test!false(); + () @safe {test!true(); } (); } @("registration") debug(MYSQLN_TESTS) unittest { - import mysql.commands; - auto pool = new MySQLPool(testConnectionStr); - - // Setup - auto cn = pool.lockConnection(); - cn.exec("DROP TABLE IF EXISTS `poolRegistration`"); - cn.exec("CREATE TABLE `poolRegistration` ( - `data` LONGBLOB - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - immutable sql = "SELECT * from `poolRegistration`"; - auto cn2 = pool.lockConnection(); - pool.applyAuto(cn2); - assert(cn !is cn2); - - // Tests: - // Initial - assert(pool.isAutoCleared(sql)); - assert(pool.isAutoRegistered(sql)); - assert(pool.isAutoReleased(sql)); - assert(!cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - // Register on connection #1 - auto prepared = cn.prepare(sql); + static void test(bool doSafe)() { + mixin(doImports(doSafe, "pool", "commands", "connection")); + auto pool = new MySQLPool(testConnectionStr); + + // Setup + auto cn = pool.lockConnection(); + cn.exec("DROP TABLE IF EXISTS `poolRegistration`"); + cn.exec("CREATE TABLE `poolRegistration` ( + `data` LONGBLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + immutable sql = "SELECT * from `poolRegistration`"; + auto cn2 = pool.lockConnection(); + pool.applyAuto(cn2); + assert(cn !is cn2); + + // Tests: + // Initial assert(pool.isAutoCleared(sql)); assert(pool.isAutoRegistered(sql)); assert(pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - auto cn3 = pool.lockConnection(); - pool.applyAuto(cn3); - assert(!cn3.isRegistered(sql)); - } - - // autoRegister - pool.autoRegister(prepared); - { - assert(!pool.isAutoCleared(sql)); - assert(pool.isAutoRegistered(sql)); - assert(!pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - auto cn3 = pool.lockConnection(); - pool.applyAuto(cn3); - assert(cn3.isRegistered(sql)); - } - - // autoRelease - pool.autoRelease(prepared); - { - assert(!pool.isAutoCleared(sql)); - assert(!pool.isAutoRegistered(sql)); - assert(pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); + assert(!cn.isRegistered(sql)); assert(!cn2.isRegistered(sql)); - auto cn3 = pool.lockConnection(); - pool.applyAuto(cn3); - assert(!cn3.isRegistered(sql)); + // Register on connection #1 + auto prepared = cn.prepare(sql); + { + assert(pool.isAutoCleared(sql)); + assert(pool.isAutoRegistered(sql)); + assert(pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + auto cn3 = pool.lockConnection(); + pool.applyAuto(cn3); + assert(!cn3.isRegistered(sql)); + } + + // autoRegister + pool.autoRegister(prepared); + { + assert(!pool.isAutoCleared(sql)); + assert(pool.isAutoRegistered(sql)); + assert(!pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + auto cn3 = pool.lockConnection(); + pool.applyAuto(cn3); + assert(cn3.isRegistered(sql)); + } + + // autoRelease + pool.autoRelease(prepared); + { + assert(!pool.isAutoCleared(sql)); + assert(!pool.isAutoRegistered(sql)); + assert(pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + auto cn3 = pool.lockConnection(); + pool.applyAuto(cn3); + assert(!cn3.isRegistered(sql)); + } + + // clearAuto + pool.clearAuto(prepared); + { + assert(pool.isAutoCleared(sql)); + assert(pool.isAutoRegistered(sql)); + assert(pool.isAutoReleased(sql)); + assert(cn.isRegistered(sql)); + assert(!cn2.isRegistered(sql)); + + auto cn3 = pool.lockConnection(); + pool.applyAuto(cn3); + assert(!cn3.isRegistered(sql)); + } } - // clearAuto - pool.clearAuto(prepared); - { - assert(pool.isAutoCleared(sql)); - assert(pool.isAutoRegistered(sql)); - assert(pool.isAutoReleased(sql)); - assert(cn.isRegistered(sql)); - assert(!cn2.isRegistered(sql)); - - auto cn3 = pool.lockConnection(); - pool.applyAuto(cn3); - assert(!cn3.isRegistered(sql)); - } + test!false(); + () @safe {test!true(); } (); } @("closedConnection") // "cct" debug(MYSQLN_TESTS) + unittest { - import mysql.pool; - MySQLPool cctPool; - int cctCount=0; - - void cctStart() + static void test(bool doSafe)() { - import std.array; - import mysql.commands; - - cctPool = new MySQLPool(testConnectionStr); - cctPool.onNewConnection = (Connection conn) { cctCount++; }; - assert(cctCount == 0); - - auto cn = cctPool.lockConnection(); - assert(!cn.closed); - cn.close(); - assert(cn.closed); - assert(cctCount == 1); + mixin(doImports(doSafe, "pool", "commands", "connection")); + MySQLPool cctPool; + int cctCount=0; + + void cctStart() + { + + cctPool = new MySQLPool(testConnectionStr); + cctPool.onNewConnection = (Connection conn) { cctCount++; }; + assert(cctCount == 0); + + auto cn = cctPool.lockConnection(); + assert(!cn.closed); + cn.close(); + assert(cn.closed); + assert(cctCount == 1); + } + + { + cctStart(); + assert(cctCount == 1); + + auto cn = cctPool.lockConnection(); + assert(cctCount == 1); + assert(!cn.closed); + } } - unittest - { - cctStart(); - assert(cctCount == 1); - - auto cn = cctPool.lockConnection(); - assert(cctCount == 1); - assert(!cn.closed); - } + test!false(); + () @safe {test!true(); } (); } } @@ -984,228 +1050,252 @@ version(Have_vibe_core) debug(MYSQLN_TESTS) unittest { - import std.array; - import std.range; - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); - - // Setup - cn.exec("DROP TABLE IF EXISTS `paramSpecial`"); - cn.exec("CREATE TABLE `paramSpecial` ( - `data` LONGBLOB - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - - immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below - auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - auto data = alph.cycle.take(totalSize).array; - - int chunkSize; - const(ubyte)[] dataToSend; - bool finished; - uint sender(ubyte[] chunk) + static void test(bool doSafe)() { - assert(!finished); - assert(chunk.length == chunkSize); + mixin(doImports(doSafe, "connection", "commands", "prepared")); + mixin(scopedCn); - if(dataToSend.length < chunkSize) - { - auto actualSize = cast(uint) dataToSend.length; - chunk[0..actualSize] = dataToSend[]; - finished = true; - dataToSend.length = 0; - return actualSize; - } - else + // Setup + cn.exec("DROP TABLE IF EXISTS `paramSpecial`"); + cn.exec("CREATE TABLE `paramSpecial` ( + `data` LONGBLOB + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + + immutable totalSize = 1000; // Deliberately not a multiple of chunkSize below + auto alph = cast(const(ubyte)[]) "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + auto data = alph.cycle.take(totalSize).array; + + int chunkSize; + const(ubyte)[] dataToSend; + bool finished; + uint sender(ubyte[] chunk) { - chunk[] = dataToSend[0..chunkSize]; - dataToSend = dataToSend[chunkSize..$]; - return chunkSize; - } - } + assert(!finished); + assert(chunk.length == chunkSize); - immutable selectSQL = "SELECT `data` FROM `paramSpecial`"; + if(dataToSend.length < chunkSize) + { + auto actualSize = cast(uint) dataToSend.length; + chunk[0..actualSize] = dataToSend[]; + finished = true; + dataToSend.length = 0; + return actualSize; + } + else + { + chunk[] = dataToSend[0..chunkSize]; + dataToSend = dataToSend[chunkSize..$]; + return chunkSize; + } + } - // Sanity check - cn.exec("INSERT INTO `paramSpecial` VALUES (\""~(cast(string)data)~"\")"); - auto value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); + immutable selectSQL = "SELECT `data` FROM `paramSpecial`"; - { - // Clear table - cn.exec("DELETE FROM `paramSpecial`"); - value = cn.queryValue(selectSQL); // Ensure deleted - assert(value.isNull); - - // Test: totalSize as a multiple of chunkSize - chunkSize = 100; - assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); - auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); - - finished = false; - dataToSend = data; - auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); - prepared.setArg(0, cast(ubyte[])[], paramSpecial); - assert(cn.exec(prepared) == 1); - value = cn.queryValue(selectSQL); + // Sanity check + cn.exec("INSERT INTO `paramSpecial` VALUES (\""~(cast(const(char)[])data)~"\")"); + auto value = cn.queryValue(selectSQL); assert(!value.isNull); assert(value.get == data); - } - { - // Clear table - cn.exec("DELETE FROM `paramSpecial`"); - value = cn.queryValue(selectSQL); // Ensure deleted - assert(value.isNull); - - // Test: totalSize as a non-multiple of chunkSize - chunkSize = 64; - assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); - auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); - - finished = false; - dataToSend = data; - auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); - prepared.setArg(0, cast(ubyte[])[], paramSpecial); - assert(cn.exec(prepared) == 1); - value = cn.queryValue(selectSQL); - assert(!value.isNull); - assert(value.get == data); + { + // Clear table + cn.exec("DELETE FROM `paramSpecial`"); + value = cn.queryValue(selectSQL); // Ensure deleted + assert(value.isNull); + + // Test: totalSize as a multiple of chunkSize + chunkSize = 100; + assert(cast(int)(totalSize / chunkSize) * chunkSize == totalSize); + auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); + + finished = false; + dataToSend = data; + auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); + prepared.setArg(0, cast(ubyte[])[], paramSpecial); + assert(cn.exec(prepared) == 1); + value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + } + + { + // Clear table + cn.exec("DELETE FROM `paramSpecial`"); + value = cn.queryValue(selectSQL); // Ensure deleted + assert(value.isNull); + + // Test: totalSize as a non-multiple of chunkSize + chunkSize = 64; + assert(cast(int)(totalSize / chunkSize) * chunkSize != totalSize); + auto paramSpecial = ParameterSpecialization(0, SQLType.INFER_FROM_D_TYPE, chunkSize, &sender); + + finished = false; + dataToSend = data; + auto prepared = cn.prepare("INSERT INTO `paramSpecial` VALUES (?)"); + prepared.setArg(0, cast(ubyte[])[], paramSpecial); + assert(cn.exec(prepared) == 1); + value = cn.queryValue(selectSQL); + assert(!value.isNull); + assert(value.get == data); + } } + test!false(); + () @safe {test!true(); } (); } @("setArg-typeMods") debug(MYSQLN_TESTS) unittest { - import mysql.test.common; - mixin(scopedCn); + static void test(bool doSafe)() + { + mixin(doImports(doSafe, "commands")); + mixin(scopedCn); - // Setup - cn.exec("DROP TABLE IF EXISTS `setArg-typeMods`"); - cn.exec("CREATE TABLE `setArg-typeMods` ( - `i` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + // Setup + cn.exec("DROP TABLE IF EXISTS `setArg-typeMods`"); + cn.exec("CREATE TABLE `setArg-typeMods` ( + `i` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - auto insertSQL = "INSERT INTO `setArg-typeMods` VALUES (?)"; + auto insertSQL = "INSERT INTO `setArg-typeMods` VALUES (?)"; - // Sanity check - { - int i = 111; - assert(cn.exec(insertSQL, i) == 1); - auto value = cn.queryValue("SELECT `i` FROM `setArg-typeMods`"); - assert(!value.isNull); - assert(value.get == i); - } + // Sanity check + { + int i = 111; + assert(cn.exec(insertSQL, i) == 1); + auto value = cn.queryValue("SELECT `i` FROM `setArg-typeMods`"); + assert(!value.isNull); + assert(value.get == i); + } - // Test const(int) - { - const(int) i = 112; - assert(cn.exec(insertSQL, i) == 1); - } + // Test const(int) + { + const(int) i = 112; + assert(cn.exec(insertSQL, i) == 1); + } - // Test immutable(int) - { - immutable(int) i = 113; - assert(cn.exec(insertSQL, i) == 1); - } + // Test immutable(int) + { + immutable(int) i = 113; + assert(cn.exec(insertSQL, i) == 1); + } - // Note: Variant doesn't seem to support - // `shared(T)` or `shared(const(T)`. Only `shared(immutable(T))`. + // Note: Variant doesn't seem to support + // `shared(T)` or `shared(const(T)`. Only `shared(immutable(T))`. - // Test shared immutable(int) - { - shared immutable(int) i = 113; - assert(cn.exec(insertSQL, i) == 1); + // Test shared immutable(int) + { + shared immutable(int) i = 113; + assert(cn.exec(insertSQL, i) == 1); + } } + + test!false(); + () @safe {test!true(); } (); } @("setNullArg") debug(MYSQLN_TESTS) unittest { - import mysql.connection; - import mysql.test.common; - mixin(scopedCn); + static void test(bool doSafe)() + { + mixin(doImports(doSafe, "connection", "commands", "result")); + mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `setNullArg`"); - cn.exec("CREATE TABLE `setNullArg` ( - `val` INTEGER - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("DROP TABLE IF EXISTS `setNullArg`"); + cn.exec("CREATE TABLE `setNullArg` ( + `val` INTEGER + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; - immutable selectSQL = "SELECT * FROM `setNullArg`"; - auto preparedInsert = cn.prepare(insertSQL); - assert(preparedInsert.sql == insertSQL); - Row[] rs; + immutable insertSQL = "INSERT INTO `setNullArg` VALUES (?)"; + immutable selectSQL = "SELECT * FROM `setNullArg`"; + auto preparedInsert = cn.prepare(insertSQL); + assert(preparedInsert.sql == insertSQL); + Row[] rs; - { - Nullable!int nullableInt; - nullableInt.nullify(); - preparedInsert.setArg(0, nullableInt); - assert(preparedInsert.getArg(0).type == typeid(typeof(null))); - nullableInt = 7; - preparedInsert.setArg(0, nullableInt); - assert(preparedInsert.getArg(0) == 7); - - nullableInt.nullify(); - preparedInsert.setArgs(nullableInt); - assert(preparedInsert.getArg(0).type == typeid(typeof(null))); - nullableInt = 7; - preparedInsert.setArgs(nullableInt); - assert(preparedInsert.getArg(0) == 7); + { + Nullable!int nullableInt; + nullableInt.nullify(); + preparedInsert.setArg(0, nullableInt); + assert(preparedInsert.getArg(0).valIsNull); + nullableInt = 7; + preparedInsert.setArg(0, nullableInt); + assert(preparedInsert.getArg(0) == 7); + + nullableInt.nullify(); + preparedInsert.setArgs(nullableInt); + assert(preparedInsert.getArg(0).valIsNull); + nullableInt = 7; + preparedInsert.setArgs(nullableInt); + assert(preparedInsert.getArg(0) == 7); + } + + preparedInsert.setArg(0, 5); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 1); + assert(rs[0][0] == 5); + + preparedInsert.setArg(0, null); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 2); + assert(rs[0][0] == 5); + assert(rs[1].isNull(0)); + assert(rs[1][0].valIsNull); + + static if(doSafe) + preparedInsert.setArg(0, MySQLVal(null)); + else + preparedInsert.setArg(0, Variant(null)); + cn.exec(preparedInsert); + rs = cn.query(selectSQL).array; + assert(rs.length == 3); + assert(rs[0][0] == 5); + assert(rs[1].isNull(0)); + assert(rs[2].isNull(0)); + assert(rs[1][0].valIsNull); + assert(rs[2][0].valIsNull); } - preparedInsert.setArg(0, 5); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 1); - assert(rs[0][0] == 5); - - preparedInsert.setArg(0, null); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 2); - assert(rs[0][0] == 5); - assert(rs[1].isNull(0)); - assert(rs[1][0].type == typeid(typeof(null))); - - preparedInsert.setArg(0, Variant(null)); - cn.exec(preparedInsert); - rs = cn.query(selectSQL).array; - assert(rs.length == 3); - assert(rs[0][0] == 5); - assert(rs[1].isNull(0)); - assert(rs[2].isNull(0)); - assert(rs[1][0].type == typeid(typeof(null))); - assert(rs[2][0].type == typeid(typeof(null))); + test!false(); + () @safe {test!true(); } (); } @("lastInsertID") debug(MYSQLN_TESTS) unittest { - import mysql.connection; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`"); - cn.exec("CREATE TABLE `testPreparedLastInsertID` ( - `a` INTEGER NOT NULL AUTO_INCREMENT, - PRIMARY KEY (a) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + static void test(bool doSafe)() + { + mixin(doImports(doSafe, "connection", "commands")); + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `testPreparedLastInsertID`"); + cn.exec("CREATE TABLE `testPreparedLastInsertID` ( + `a` INTEGER NOT NULL AUTO_INCREMENT, + PRIMARY KEY (a) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); - cn.exec(stmt); - assert(stmt.lastInsertID == 1); - cn.exec(stmt); - assert(stmt.lastInsertID == 2); - cn.exec(stmt); - assert(stmt.lastInsertID == 3); + auto stmt = cn.prepare("INSERT INTO `testPreparedLastInsertID` VALUES()"); + cn.exec(stmt); + assert(stmt.lastInsertID == 1); + cn.exec(stmt); + assert(stmt.lastInsertID == 2); + cn.exec(stmt); + assert(stmt.lastInsertID == 3); + } + + test!false(); + () @safe {test!true(); } (); } // Test PreparedRegistrations debug(MYSQLN_TESTS) { + import mysql.impl.prepared : PreparedRegistrations, + TestPreparedRegistrationsGood1, TestPreparedRegistrationsGood2; PreparedRegistrations!TestPreparedRegistrationsGood1 testPreparedRegistrationsGood1; PreparedRegistrations!TestPreparedRegistrationsGood2 testPreparedRegistrationsGood2; @@ -1315,25 +1405,30 @@ debug(MYSQLN_TESTS) debug(MYSQLN_TESTS) unittest { - import mysql.test.common; - import mysql.commands; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `row_getName`"); - cn.exec("CREATE TABLE `row_getName` (someValue INTEGER, another INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `row_getName` VALUES (1, 2), (3, 4)"); - - enum sql = "SELECT another, someValue FROM `row_getName`"; - - auto rows = cn.query(sql).array; - assert(rows.length == 2); - assert(rows[0][0] == 2); - assert(rows[0][1] == 1); - assert(rows[0].getName(0) == "another"); - assert(rows[0].getName(1) == "someValue"); - assert(rows[1][0] == 4); - assert(rows[1][1] == 3); - assert(rows[1].getName(0) == "another"); - assert(rows[1].getName(1) == "someValue"); + static void test(bool doSafe)() + { + mixin(doImports(doSafe, "commands")); + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS `row_getName`"); + cn.exec("CREATE TABLE `row_getName` (someValue INTEGER, another INTEGER) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `row_getName` VALUES (1, 2), (3, 4)"); + + enum sql = "SELECT another, someValue FROM `row_getName`"; + + auto rows = cn.query(sql).array; + assert(rows.length == 2); + assert(rows[0][0] == 2); + assert(rows[0][1] == 1); + assert(rows[0].getName(0) == "another"); + assert(rows[0].getName(1) == "someValue"); + assert(rows[1][0] == 4); + assert(rows[1][1] == 3); + assert(rows[1].getName(0) == "another"); + assert(rows[1].getName(1) == "someValue"); + } + + test!false(); + () @safe {test!true(); } (); } // issue 222, set column names when data is null. @@ -1341,8 +1436,7 @@ unittest debug(MYSQLN_TESTS) unittest { - import mysql.test.common; - import mysql.commands; + import mysql.safe.commands; mixin(scopedCn); // binary mode happens with prepared statements auto row = cn.queryRow("SELECT `colname` FROM (SELECT 1 AS `id`, NULL AS `colname`) as `tbl` WHERE `id` = ?", 1); diff --git a/integration-tests/source/mysql/test/common.d b/integration-tests/source/mysql/test/common.d index 2b36b77f..88a84385 100644 --- a/integration-tests/source/mysql/test/common.d +++ b/integration-tests/source/mysql/test/common.d @@ -17,14 +17,29 @@ import std.string; import std.traits; import std.variant; -import mysql.commands; -import mysql.connection; +import mysql.safe.commands; +import mysql.safe.connection; import mysql.exceptions; import mysql.protocol.extra_types; import mysql.protocol.sockets; -import mysql.result; +import mysql.safe.result; import mysql.types; +// shim for prepareBackwardCompatImpl so I don't have to version whole tests +alias prepareBackwardCompatImpl = prepare; + +// shim to check for null to check differences between Variant and MySQLVal +bool valIsNull(MySQLVal val) @safe +{ + return val.kind == val.Kind.Null; +} +bool valIsNull(Variant val) +{ + return val.type == typeid(typeof(null)); +} + + + /+ To enable these tests, you have to add the MYSQLN_TESTS debug specifier. The reason it uses debug and not version is because dub @@ -39,6 +54,7 @@ version(DoCoreTests) import std.conv; import std.datetime; + @safe: private @property string testConnectionStrFile() { import std.file, std.path; @@ -82,10 +98,10 @@ version(DoCoreTests) writeln(testConnectionStrFile); writeln("Halting so the user can check connection string settings."); import core.stdc.stdlib : exit; - exit(1); + () @trusted { exit(1); }(); } - cached = cast(string) std.file.read(testConnectionStrFile); + cached = std.file.readText(testConnectionStrFile); cached = cached.strip(); } @@ -144,4 +160,16 @@ version(DoCoreTests) return DateTime(year, month, day, hour, minute, second); } + + // generate safe or unsafe imports for unittests. + string doImports(bool isSafe, string[] imports...) + { + string result; + string subpackage = isSafe ? "safe" : "unsafe"; + foreach(im; imports) + { + result ~= "import mysql." ~ subpackage ~ "." ~ im ~ ";"; + } + return result; + } } diff --git a/integration-tests/source/mysql/test/integration.d b/integration-tests/source/mysql/test/integration.d index 9c3f203a..f0889135 100644 --- a/integration-tests/source/mysql/test/integration.d +++ b/integration-tests/source/mysql/test/integration.d @@ -13,16 +13,15 @@ import std.traits; import std.typecons; import std.variant; -import mysql.commands; -import mysql.connection; +import mysql.safe.connection : Connection; import mysql.exceptions; import mysql.metadata; import mysql.protocol.constants; import mysql.protocol.extra_types; import mysql.protocol.packets; import mysql.protocol.sockets; -import mysql.result; import mysql.test.common; +import mysql.types; alias indexOf = std.string.indexOf; // Needed on DMD 2.064.2 @@ -31,7 +30,7 @@ debug(MYSQLN_CORE_TESTS) version = DoCoreTests; @("connect") version(DoCoreTests) -unittest +@safe unittest { import std.stdio; writeln("Basic connect test..."); @@ -41,7 +40,7 @@ unittest } debug(MYSQLN_TESTS) -unittest +@safe unittest { mixin(scopedCn); @@ -75,8 +74,9 @@ unittest debug(MYSQLN_TESTS) { - void initBaseTestTables(Connection cn) + void initBaseTestTables(bool doSafe)(Connection cn) { + mixin(doImports(doSafe, "commands")); cn.exec("DROP TABLE IF EXISTS `basetest`"); cn.exec( "CREATE TABLE `basetest` ( @@ -112,454 +112,471 @@ debug(MYSQLN_TESTS) @("basetest") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.prepared; - - struct X + static void test(bool doSafe)() { - int a, b, c; - string s; - double d; - } - bool ok = true; + mixin(doImports(doSafe, "prepared", "commands", "connection")); - mixin(scopedCn); - initBaseTestTables(cn); - - cn.exec("delete from basetest"); - - cn.exec("insert into basetest values(" ~ - "1, -128, 255, -32768, 65535, 42, 4294967295, -9223372036854775808, 18446744073709551615, 'ABC', " ~ - "'The quick brown fox', 0x000102030405060708090a0b0c0d0e0f, '2007-01-01', " ~ - "'12:12:12', '2007-01-01 12:12:12', 1.234567890987654, 22.4, NULL, 11234.4325)"); - - auto rs = cn.query("select bytecol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == -128); - rs = cn.query("select ubytecol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs.front[0] == 255); - rs = cn.query("select shortcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == short.min); - rs = cn.query("select ushortcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == ushort.max); - rs = cn.query("select intcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == 42); - rs = cn.query("select uintcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == uint.max); - rs = cn.query("select longcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == long.min); - rs = cn.query("select ulongcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == ulong.max); - rs = cn.query("select charscol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0].toString() == "ABC"); - rs = cn.query("select stringcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0].toString() == "The quick brown fox"); - rs = cn.query("select bytescol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); - rs = cn.query("select datecol from basetest limit 1").array; - assert(rs.length == 1); - Date d = rs[0][0].get!(Date); - assert(d.year == 2007 && d.month == 1 && d.day == 1); - rs = cn.query("select timecol from basetest limit 1").array; - assert(rs.length == 1); - TimeOfDay t = rs[0][0].get!(TimeOfDay); - assert(t.hour == 12 && t.minute == 12 && t.second == 12); - rs = cn.query("select dtcol from basetest limit 1").array; - assert(rs.length == 1); - DateTime dt = rs[0][0].get!(DateTime); - assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); - rs = cn.query("select doublecol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0].toString() == "1.23457"); - rs = cn.query("select floatcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0].toString() == "22.4"); - rs = cn.query("select decimalcol from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == "11234.4325"); - - rs = cn.query("select * from basetest limit 1").array; - assert(rs.length == 1); - assert(rs[0][0] == true); - assert(rs[0][1] == -128); - assert(rs[0][2] == 255); - assert(rs[0][3] == short.min); - assert(rs[0][4] == ushort.max); - assert(rs[0][5] == 42); - assert(rs[0][6] == uint.max); - assert(rs[0][7] == long.min); - assert(rs[0][8] == ulong.max); - assert(rs[0][9].toString() == "ABC"); - assert(rs[0][10].toString() == "The quick brown fox"); - assert(rs[0][11].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); - d = rs[0][12].get!(Date); - assert(d.year == 2007 && d.month == 1 && d.day == 1); - t = rs[0][13].get!(TimeOfDay); - assert(t.hour == 12 && t.minute == 12 && t.second == 12); - dt = rs[0][14].get!(DateTime); - assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); - assert(rs[0][15].toString() == "1.23457"); - assert(rs[0][16].toString() == "22.4"); - assert(rs[0].isNull(17) == true); - assert(rs[0][18] == "11234.4325", rs[0][18].toString()); - - rs = cn.query("select bytecol, ushortcol, intcol, charscol, floatcol from basetest limit 1").array; - X x; - rs[0].toStruct(x); - assert(x.a == -128 && x.b == 65535 && x.c == 42 && x.s == "ABC" && to!string(x.d) == "22.4"); - - auto stmt = cn.prepare("select * from basetest limit 1"); - rs = cn.query(stmt).array; - assert(rs.length == 1); - assert(rs[0][0] == true); - assert(rs[0][1] == -128); - assert(rs[0][2] == 255); - assert(rs[0][3] == short.min); - assert(rs[0][4] == ushort.max); - assert(rs[0][5] == 42); - assert(rs[0][6] == uint.max); - assert(rs[0][7] == long.min); - assert(rs[0][8] == ulong.max); - assert(rs[0][9].toString() == "ABC"); - assert(rs[0][10].toString() == "The quick brown fox"); - assert(rs[0][11].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); - d = rs[0][12].get!(Date); - assert(d.year == 2007 && d.month == 1 && d.day == 1); - t = rs[0][13].get!(TimeOfDay); - assert(t.hour == 12 && t.minute == 12 && t.second == 12); - dt = rs[0][14].get!(DateTime); - assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); - assert(rs[0][15].toString() == "1.23457"); - assert(rs[0][16].toString() == "22.4"); - assert(rs[0].isNull(17) == true); - assert(rs[0][18] == "11234.4325", rs[0][18].toString()); - - stmt = cn.prepare("insert into basetest (intcol, stringcol) values(?, ?)"); - Variant[] va; - va.length = 2; - va[0] = 42; - va[1] = "The quick brown fox x"; - stmt.setArgs(va); - foreach (int i; 0..20) - { - cn.exec(stmt); - stmt.setArg(0, stmt.getArg(0) + 1); - stmt.setArg(1, stmt.getArg(1) ~ "x"); - } + struct X + { + int a, b, c; + string s; + double d; + } + bool ok = true; - stmt = cn.prepare("insert into basetest (intcol, stringcol) values(?, ?)"); - //Variant[] va; - va.length = 2; - va[0] = 42; - va[1] = "The quick brown fox x"; - stmt.setArgs(va); - foreach (int i; 0..20) - { - cn.exec(stmt); + mixin(scopedCn); + initBaseTestTables!doSafe(cn); + + cn.exec("delete from basetest"); + + cn.exec("insert into basetest values(" ~ + "1, -128, 255, -32768, 65535, 42, 4294967295, -9223372036854775808, 18446744073709551615, 'ABC', " ~ + "'The quick brown fox', 0x000102030405060708090a0b0c0d0e0f, '2007-01-01', " ~ + "'12:12:12', '2007-01-01 12:12:12', 1.234567890987654, 22.4, NULL, 11234.4325)"); + + auto rs = cn.query("select bytecol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == -128); + rs = cn.query("select ubytecol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs.front[0] == 255); + rs = cn.query("select shortcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == short.min); + rs = cn.query("select ushortcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == ushort.max); + rs = cn.query("select intcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == 42); + rs = cn.query("select uintcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == uint.max); + rs = cn.query("select longcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == long.min); + rs = cn.query("select ulongcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == ulong.max); + rs = cn.query("select charscol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0].toString() == "ABC"); + rs = cn.query("select stringcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0].toString() == "The quick brown fox"); + rs = cn.query("select bytescol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); + rs = cn.query("select datecol from basetest limit 1").array; + assert(rs.length == 1); + Date d = rs[0][0].get!(Date); + assert(d.year == 2007 && d.month == 1 && d.day == 1); + rs = cn.query("select timecol from basetest limit 1").array; + assert(rs.length == 1); + TimeOfDay t = rs[0][0].get!(TimeOfDay); + assert(t.hour == 12 && t.minute == 12 && t.second == 12); + rs = cn.query("select dtcol from basetest limit 1").array; + assert(rs.length == 1); + DateTime dt = rs[0][0].get!(DateTime); + assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); + rs = cn.query("select doublecol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0].toString() == "1.23457"); + rs = cn.query("select floatcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0].toString() == "22.4"); + rs = cn.query("select decimalcol from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == "11234.4325"); + + rs = cn.query("select * from basetest limit 1").array; + assert(rs.length == 1); + assert(rs[0][0] == true); + assert(rs[0][1] == -128); + assert(rs[0][2] == 255); + assert(rs[0][3] == short.min); + assert(rs[0][4] == ushort.max); + assert(rs[0][5] == 42); + assert(rs[0][6] == uint.max); + assert(rs[0][7] == long.min); + assert(rs[0][8] == ulong.max); + assert(rs[0][9].toString() == "ABC"); + assert(rs[0][10].toString() == "The quick brown fox"); + assert(rs[0][11].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); + d = rs[0][12].get!(Date); + assert(d.year == 2007 && d.month == 1 && d.day == 1); + t = rs[0][13].get!(TimeOfDay); + assert(t.hour == 12 && t.minute == 12 && t.second == 12); + dt = rs[0][14].get!(DateTime); + assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); + assert(rs[0][15].toString() == "1.23457"); + assert(rs[0][16].toString() == "22.4"); + assert(rs[0].isNull(17) == true); + assert(rs[0][18] == "11234.4325", rs[0][18].toString()); + + rs = cn.query("select bytecol, ushortcol, intcol, charscol, floatcol from basetest limit 1").array; + X x; + rs[0].toStruct(x); + assert(x.a == -128 && x.b == 65535 && x.c == 42 && x.s == "ABC" && to!string(x.d) == "22.4"); + + auto stmt = cn.prepare("select * from basetest limit 1"); + rs = cn.query(stmt).array; + assert(rs.length == 1); + assert(rs[0][0] == true); + assert(rs[0][1] == -128); + assert(rs[0][2] == 255); + assert(rs[0][3] == short.min); + assert(rs[0][4] == ushort.max); + assert(rs[0][5] == 42); + assert(rs[0][6] == uint.max); + assert(rs[0][7] == long.min); + assert(rs[0][8] == ulong.max); + assert(rs[0][9].toString() == "ABC"); + assert(rs[0][10].toString() == "The quick brown fox"); + assert(rs[0][11].toString() == "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]"); + d = rs[0][12].get!(Date); + assert(d.year == 2007 && d.month == 1 && d.day == 1); + t = rs[0][13].get!(TimeOfDay); + assert(t.hour == 12 && t.minute == 12 && t.second == 12); + dt = rs[0][14].get!(DateTime); + assert(dt.year == 2007 && dt.month == 1 && dt.day == 1 && dt.hour == 12 && dt.minute == 12 && dt.second == 12); + assert(rs[0][15].toString() == "1.23457"); + assert(rs[0][16].toString() == "22.4"); + assert(rs[0].isNull(17) == true); + assert(rs[0][18] == "11234.4325", rs[0][18].toString()); + + stmt = cn.prepare("insert into basetest (intcol, stringcol) values(?, ?)"); + static if(doSafe) + MySQLVal[] va; + else + Variant[] va; + va.length = 2; + va[0] = 42; + va[1] = "The quick brown fox x"; + stmt.setArgs(va); + foreach (int i; 0..20) + { + cn.exec(stmt); + stmt.setArg(0, stmt.getArg(0) + 1); + stmt.setArg(1, stmt.getArg(1) ~ "x"); + } - va[0] = stmt.getArg(0).get!int + 1; - va[1] = stmt.getArg(1).get!string ~ "x"; + stmt = cn.prepare("insert into basetest (intcol, stringcol) values(?, ?)"); + va.length = 2; + va[0] = 42; + va[1] = "The quick brown fox x"; stmt.setArgs(va); - } + foreach (int i; 0..20) + { + cn.exec(stmt); - int a; - string b; - cn.queryRowTuple("select intcol, stringcol from basetest where bytecol=-128 limit 1", a, b); - assert(a == 42 && b == "The quick brown fox"); - - stmt = cn.prepare("select intcol, stringcol from basetest where bytecol=? limit 1"); - Variant[] va2; - va2.length = 1; - va2[0] = cast(byte) -128; - stmt.setArgs(va2); - a = 0; - b = ""; - cn.queryRowTuple(stmt, a, b); - assert(a == 42 && b == "The quick brown fox"); - - stmt = cn.prepare("update basetest set intcol=? where bytecol=-128"); - int referred = 555; - stmt.setArgs(referred); - cn.exec(stmt); - referred = 666; - stmt.setArgs(referred); - cn.exec(stmt); - auto referredBack = cn.queryValue("select intcol from basetest where bytecol = -128"); - assert(!referredBack.isNull); - assert(referredBack.get == 666); - - // Test execFunction() - exec(cn, `DROP FUNCTION IF EXISTS hello`); - exec(cn, ` - CREATE FUNCTION hello (s CHAR(20)) - RETURNS CHAR(50) DETERMINISTIC - RETURN CONCAT('Hello ',s,'!') - `); - - rs = query(cn, "select hello ('World')").array; - assert(rs.length == 1); - assert(rs[0][0] == "Hello World!"); - - string g = "Gorgeous"; - string reply; - - auto func = cn.prepareFunction("hello", 1); - func.setArgs(g); - auto funcResult = cn.queryValue(func); - assert(!funcResult.isNull && funcResult.get == "Hello Gorgeous!"); - g = "Hotlips"; - func.setArgs(g); - funcResult = cn.queryValue(func); - assert(!funcResult.isNull && funcResult.get == "Hello Hotlips!"); - - // Test execProcedure() - exec(cn, `DROP PROCEDURE IF EXISTS insert2`); - exec(cn, ` - CREATE PROCEDURE insert2 (IN p1 INT, IN p2 CHAR(50)) - BEGIN - INSERT INTO basetest (intcol, stringcol) VALUES(p1, p2); - END - `); - g = "inserted string 1"; - int m = 2001; - auto proc = cn.prepareProcedure("insert2", 2); - proc.setArgs(m, g); - cn.exec(proc); - - cn.queryRowTuple("select stringcol from basetest where intcol=2001", reply); - assert(reply == g); - - g = "inserted string 2"; - m = 2002; - proc.setArgs(m, g); - cn.exec(proc); - - cn.queryRowTuple("select stringcol from basetest where intcol=2002", reply); - assert(reply == g); + va[0] = stmt.getArg(0).get!int + 1; + va[1] = stmt.getArg(1).get!string ~ "x"; + stmt.setArgs(va); + } -/+ - cn.exec("delete from tblob"); - cn.exec("insert into tblob values(321, NULL, 22.4, NULL, '2011-11-05 11:52:00')"); -+/ - size_t delegate(ubyte[]) foo() - { - size_t n = 20000000; - uint cp = 0; + int a; + string b; + cn.queryRowTuple("select intcol, stringcol from basetest where bytecol=-128 limit 1", a, b); + assert(a == 42 && b == "The quick brown fox"); + + stmt = cn.prepare("select intcol, stringcol from basetest where bytecol=? limit 1"); + static if(doSafe) + MySQLVal[] va2; + else + Variant[] va2; + va2.length = 1; + va2[0] = cast(byte) -128; + stmt.setArgs(va2); + a = 0; + b = ""; + cn.queryRowTuple(stmt, a, b); + assert(a == 42 && b == "The quick brown fox"); + + stmt = cn.prepare("update basetest set intcol=? where bytecol=-128"); + int referred = 555; + stmt.setArgs(referred); + cn.exec(stmt); + referred = 666; + stmt.setArgs(referred); + cn.exec(stmt); + auto referredBack = cn.queryValue("select intcol from basetest where bytecol = -128"); + assert(!referredBack.isNull); + assert(referredBack.get == 666); + + // Test execFunction() + exec(cn, `DROP FUNCTION IF EXISTS hello`); + exec(cn, ` + CREATE FUNCTION hello (s CHAR(20)) + RETURNS CHAR(50) DETERMINISTIC + RETURN CONCAT('Hello ',s,'!') + `); + + rs = query(cn, "select hello ('World')").array; + assert(rs.length == 1); + assert(rs[0][0] == "Hello World!"); + + string g = "Gorgeous"; + string reply; + + auto func = cn.prepareFunction("hello", 1); + func.setArgs(g); + auto funcResult = cn.queryValue(func); + assert(!funcResult.isNull && funcResult.get == "Hello Gorgeous!"); + g = "Hotlips"; + func.setArgs(g); + funcResult = cn.queryValue(func); + assert(!funcResult.isNull && funcResult.get == "Hello Hotlips!"); + + // Test execProcedure() + exec(cn, `DROP PROCEDURE IF EXISTS insert2`); + exec(cn, ` + CREATE PROCEDURE insert2 (IN p1 INT, IN p2 CHAR(50)) + BEGIN + INSERT INTO basetest (intcol, stringcol) VALUES(p1, p2); + END + `); + g = "inserted string 1"; + int m = 2001; + auto proc = cn.prepareProcedure("insert2", 2); + proc.setArgs(m, g); + cn.exec(proc); + + cn.queryRowTuple("select stringcol from basetest where intcol=2001", reply); + assert(reply == g); + + g = "inserted string 2"; + m = 2002; + proc.setArgs(m, g); + cn.exec(proc); + + cn.queryRowTuple("select stringcol from basetest where intcol=2002", reply); + assert(reply == g); - void fill(ubyte[] a, size_t m) + /+ + cn.exec("delete from tblob"); + cn.exec("insert into tblob values(321, NULL, 22.4, NULL, '2011-11-05 11:52:00')"); + +/ + size_t delegate(ubyte[]) foo() { - foreach (size_t i; 0..m) + size_t n = 20000000; + uint cp = 0; + + void fill(ubyte[] a, size_t m) { - a[i] = cast(ubyte) (cp & 0xff); - cp++; + foreach (size_t i; 0..m) + { + a[i] = cast(ubyte) (cp & 0xff); + cp++; + } } - } - size_t dg(ubyte[] dest) - { - size_t len = dest.length; - if (n >= len) + size_t dg(ubyte[] dest) { - fill(dest, len); - n -= len; - return len; + size_t len = dest.length; + if (n >= len) + { + fill(dest, len); + n -= len; + return len; + } + fill(dest, n); + return n; } - fill(dest, n); - return n; - } - return &dg; - } -/+ - stmt = cn.prepare("update tblob set lob=?, lob2=? where ikey=321"); - ubyte[] uba; - ubyte[] uba2; - stmt.bindParameter(uba, 0, PSN(0, false, SQLType.LONGBLOB, 10000, foo())); - stmt.bindParameter(uba2, 1, PSN(1, false, SQLType.LONGBLOB, 10000, foo())); - stmt.exec(); - - uint got1, got2; - bool verified1, verified2; - void delegate(ubyte[], bool) bar1(ref uint got, ref bool verified) - { - got = 0; - verified = true; - - void dg(ubyte[] ba, bool finished) + return &dg; + } + /+ + stmt = cn.prepare("update tblob set lob=?, lob2=? where ikey=321"); + ubyte[] uba; + ubyte[] uba2; + stmt.bindParameter(uba, 0, PSN(0, false, SQLType.LONGBLOB, 10000, foo())); + stmt.bindParameter(uba2, 1, PSN(1, false, SQLType.LONGBLOB, 10000, foo())); + stmt.exec(); + + uint got1, got2; + bool verified1, verified2; + void delegate(ubyte[], bool) bar1(ref uint got, ref bool verified) { - foreach (uint; 0..ba.length) + got = 0; + verified = true; + + void dg(ubyte[] ba, bool finished) { - if (verified && ba[i] != ((got+i) & 0xff)) - verified = false; + foreach (uint; 0..ba.length) + { + if (verified && ba[i] != ((got+i) & 0xff)) + verified = false; + } + got += ba.length; } - got += ba.length; + return &dg; } - return &dg; - } - void delegate(ubyte[], bool) bar2(ref uint got, ref bool verified) - { - got = 0; - verified = true; - - void dg(ubyte[] ba, bool finished) + void delegate(ubyte[], bool) bar2(ref uint got, ref bool verified) { - foreach (size_t i; 0..ba.length) + got = 0; + verified = true; + + void dg(ubyte[] ba, bool finished) { - if (verified && ba[i] != ((got+i) & 0xff)) - verified = false; + foreach (size_t i; 0..ba.length) + { + if (verified && ba[i] != ((got+i) & 0xff)) + verified = false; + } + got += ba.length; } - got += ba.length; + return &dg; } - return &dg; - } - rs = cn.query("select * from tblob limit 1"); - ubyte[] blob = rs[0][1].get!(ubyte[]); - ubyte[] blob2 = rs[0][3].get!(ubyte[]); - DateTime dt4 = rs[0][4].get!(DateTime); - writefln("blob. lengths %d %d", blob.length, blob2.length); - writeln(to!string(dt4)); + rs = cn.query("select * from tblob limit 1"); + ubyte[] blob = rs[0][1].get!(ubyte[]); + ubyte[] blob2 = rs[0][3].get!(ubyte[]); + DateTime dt4 = rs[0][4].get!(DateTime); + writefln("blob. lengths %d %d", blob.length, blob2.length); + writeln(to!string(dt4)); - CSN[] csa = [ CSN(1, 0xfc, 100000, bar1(got1, verified1)), CSN(3, 0xfc, 100000, bar2(got2, verified2)) ]; - rs = cn.query("select * from tblob limit 1", csa); - writefln("1) %d, %s", got1, verified1); - writefln("2) %d, %s", got2, verified2); - DateTime dt4 = rs[0][4].get!(DateTime); - writeln(to!string(dt4)); -+/ + CSN[] csa = [ CSN(1, 0xfc, 100000, bar1(got1, verified1)), CSN(3, 0xfc, 100000, bar2(got2, verified2)) ]; + rs = cn.query("select * from tblob limit 1", csa); + writefln("1) %d, %s", got1, verified1); + writefln("2) %d, %s", got2, verified2); + DateTime dt4 = rs[0][4].get!(DateTime); + writeln(to!string(dt4)); + +/ + } + + test!false(); + () @safe { test!true(); } (); } @("MetaData") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - auto schemaName = cn.currentDB; - MetaData md = MetaData(cn); - string[] dbList = md.databases(); - int count = 0; - foreach (string db; dbList) + static void test(bool doSafe)() { - if (db == schemaName || db == "information_schema") - count++; - } - assert(count == 2); + mixin(scopedCn); + auto schemaName = cn.currentDB; + MetaData md = MetaData(cn); + string[] dbList = md.databases(); + int count = 0; + foreach (string db; dbList) + { + if (db == schemaName || db == "information_schema") + count++; + } + assert(count == 2); - initBaseTestTables(cn); + initBaseTestTables!doSafe(cn); - string[] tList = md.tables(); - count = 0; - foreach (string t; tList) - { - if (t == "basetest" || t == "tblob") - count++; + string[] tList = md.tables(); + count = 0; + foreach (string t; tList) + { + if (t == "basetest" || t == "tblob") + count++; + } + assert(count == 2); + + /+ + Don't check defaultNull or defaultValue here because, for columns + with a default value of NULL, their values could be different + depending on the server. + + See "COLUMN_DEFAULT" at: + https://mariadb.com/kb/en/library/information-schema-columns-table/ + +/ + + ColumnInfo[] ca = md.columns("basetest"); + assert( ca[0].schema == schemaName && ca[0].table == "basetest" && ca[0].name == "boolcol" && ca[0].index == 0 && + ca[0].nullable && ca[0].type == "bit" && ca[0].charsMax == -1 && ca[0].octetsMax == -1 && + ca[0].numericPrecision == 1 && ca[0].numericScale == -1 && ca[0].charSet == "" && ca[0].collation == "" && + ca[0].colType == "bit(1)"); + assert( ca[1].schema == schemaName && ca[1].table == "basetest" && ca[1].name == "bytecol" && ca[1].index == 1 && + ca[1].nullable && ca[1].type == "tinyint" && ca[1].charsMax == -1 && ca[1].octetsMax == -1 && + ca[1].numericPrecision == 3 && ca[1].numericScale == 0 && ca[1].charSet == "" && ca[1].collation == "" && + ca[1].colType == "tinyint(4)"); + assert( ca[2].schema == schemaName && ca[2].table == "basetest" && ca[2].name == "ubytecol" && ca[2].index == 2 && + ca[2].nullable && ca[2].type == "tinyint" && ca[2].charsMax == -1 && ca[2].octetsMax == -1 && + ca[2].numericPrecision == 3 && ca[2].numericScale == 0 && ca[2].charSet == "" && ca[2].collation == "" && + ca[2].colType == "tinyint(3) unsigned"); + assert( ca[3].schema == schemaName && ca[3].table == "basetest" && ca[3].name == "shortcol" && ca[3].index == 3 && + ca[3].nullable && ca[3].type == "smallint" && ca[3].charsMax == -1 && ca[3].octetsMax == -1 && + ca[3].numericPrecision == 5 && ca[3].numericScale == 0 && ca[3].charSet == "" && ca[3].collation == "" && + ca[3].colType == "smallint(6)"); + assert( ca[4].schema == schemaName && ca[4].table == "basetest" && ca[4].name == "ushortcol" && ca[4].index == 4 && + ca[4].nullable && ca[4].type == "smallint" && ca[4].charsMax == -1 && ca[4].octetsMax == -1 && + ca[4].numericPrecision == 5 && ca[4].numericScale == 0 && ca[4].charSet == "" && ca[4].collation == "" && + ca[4].colType == "smallint(5) unsigned"); + assert( ca[5].schema == schemaName && ca[5].table == "basetest" && ca[5].name == "intcol" && ca[5].index == 5 && + ca[5].nullable && ca[5].type == "int" && ca[5].charsMax == -1 && ca[5].octetsMax == -1 && + ca[5].numericPrecision == 10 && ca[5].numericScale == 0 && ca[5].charSet == "" && ca[5].collation == "" && + ca[5].colType == "int(11)"); + assert( ca[6].schema == schemaName && ca[6].table == "basetest" && ca[6].name == "uintcol" && ca[6].index == 6 && + ca[6].nullable && ca[6].type == "int" && ca[6].charsMax == -1 && ca[6].octetsMax == -1 && + ca[6].numericPrecision == 10 && ca[6].numericScale == 0 && ca[6].charSet == "" && ca[6].collation == "" && + ca[6].colType == "int(10) unsigned"); + assert( ca[7].schema == schemaName && ca[7].table == "basetest" && ca[7].name == "longcol" && ca[7].index == 7 && + ca[7].nullable && ca[7].type == "bigint" && ca[7].charsMax == -1 && ca[7].octetsMax == -1 && + ca[7].numericPrecision == 19 && ca[7].numericScale == 0 && ca[7].charSet == "" && ca[7].collation == "" && + ca[7].colType == "bigint(20)"); + assert( ca[8].schema == schemaName && ca[8].table == "basetest" && ca[8].name == "ulongcol" && ca[8].index == 8 && + ca[8].nullable && ca[8].type == "bigint" && ca[8].charsMax == -1 && ca[8].octetsMax == -1 && + //TODO: I'm getting numericPrecision==19, figure it out later + /+ca[8].numericPrecision == 20 &&+/ ca[8].numericScale == 0 && ca[8].charSet == "" && ca[8].collation == "" && + ca[8].colType == "bigint(20) unsigned"); + assert( ca[9].schema == schemaName && ca[9].table == "basetest" && ca[9].name == "charscol" && ca[9].index == 9 && + ca[9].nullable && ca[9].type == "char" && ca[9].charsMax == 10 && ca[9].octetsMax == 10 && + ca[9].numericPrecision == -1 && ca[9].numericScale == -1 && ca[9].charSet == "latin1" && ca[9].collation == "latin1_swedish_ci" && + ca[9].colType == "char(10)"); + assert( ca[10].schema == schemaName && ca[10].table == "basetest" && ca[10].name == "stringcol" && ca[10].index == 10 && + ca[10].nullable && ca[10].type == "varchar" && ca[10].charsMax == 50 && ca[10].octetsMax == 50 && + ca[10].numericPrecision == -1 && ca[10].numericScale == -1 && ca[10].charSet == "latin1" && ca[10].collation == "latin1_swedish_ci" && + ca[10].colType == "varchar(50)"); + assert( ca[11].schema == schemaName && ca[11].table == "basetest" && ca[11].name == "bytescol" && ca[11].index == 11 && + ca[11].nullable && ca[11].type == "tinyblob" && ca[11].charsMax == 255 && ca[11].octetsMax == 255 && + ca[11].numericPrecision == -1 && ca[11].numericScale == -1 && ca[11].charSet == "" && ca[11].collation == "" && + ca[11].colType == "tinyblob"); + assert( ca[12].schema == schemaName && ca[12].table == "basetest" && ca[12].name == "datecol" && ca[12].index == 12 && + ca[12].nullable && ca[12].type == "date" && ca[12].charsMax == -1 && ca[12].octetsMax == -1 && + ca[12].numericPrecision == -1 && ca[12].numericScale == -1 && ca[12].charSet == "" && ca[12].collation == "" && + ca[12].colType == "date"); + assert( ca[13].schema == schemaName && ca[13].table == "basetest" && ca[13].name == "timecol" && ca[13].index == 13 && + ca[13].nullable && ca[13].type == "time" && ca[13].charsMax == -1 && ca[13].octetsMax == -1 && + ca[13].numericPrecision == -1 && ca[13].numericScale == -1 && ca[13].charSet == "" && ca[13].collation == "" && + ca[13].colType == "time"); + assert( ca[14].schema == schemaName && ca[14].table == "basetest" && ca[14].name == "dtcol" && ca[14].index == 14 && + ca[14].nullable && ca[14].type == "datetime" && ca[14].charsMax == -1 && ca[14].octetsMax == -1 && + ca[14].numericPrecision == -1 && ca[14].numericScale == -1 && ca[14].charSet == "" && ca[14].collation == "" && + ca[14].colType == "datetime"); + assert( ca[15].schema == schemaName && ca[15].table == "basetest" && ca[15].name == "doublecol" && ca[15].index == 15 && + ca[15].nullable && ca[15].type == "double" && ca[15].charsMax == -1 && ca[15].octetsMax == -1 && + ca[15].numericPrecision == 22 && ca[15].numericScale == -1 && ca[15].charSet == "" && ca[15].collation == "" && + ca[15].colType == "double"); + assert( ca[16].schema == schemaName && ca[16].table == "basetest" && ca[16].name == "floatcol" && ca[16].index == 16 && + ca[16].nullable && ca[16].type == "float" && ca[16].charsMax == -1 && ca[16].octetsMax == -1 && + ca[16].numericPrecision == 12 && ca[16].numericScale == -1 && ca[16].charSet == "" && ca[16].collation == "" && + ca[16].colType == "float"); + assert( ca[17].schema == schemaName && ca[17].table == "basetest" && ca[17].name == "nullcol" && ca[17].index == 17 && + ca[17].nullable && ca[17].type == "int" && ca[17].charsMax == -1 && ca[17].octetsMax == -1 && + ca[17].numericPrecision == 10 && ca[17].numericScale == 0 && ca[17].charSet == "" && ca[17].collation == "" && + ca[17].colType == "int(11)"); + assert( ca[18].schema == schemaName && ca[18].table == "basetest" && ca[18].name == "decimalcol" && ca[18].index == 18 && + ca[18].nullable && ca[18].type == "decimal" && ca[18].charsMax == -1 && ca[18].octetsMax == -1 && + ca[18].numericPrecision == 11 && ca[18].numericScale == 4 && ca[18].charSet == "" && ca[18].collation == "" && + ca[18].colType == "decimal(11,4)"); + MySQLProcedure[] pa = md.functions(); + //assert(pa[0].db == schemaName && pa[0].name == "hello" && pa[0].type == "FUNCTION"); + //pa = md.procedures(); + //assert(pa[0].db == schemaName && pa[0].name == "insert2" && pa[0].type == "PROCEDURE"); } - assert(count == 2); - - /+ - Don't check defaultNull or defaultValue here because, for columns - with a default value of NULL, their values could be different - depending on the server. - See "COLUMN_DEFAULT" at: - https://mariadb.com/kb/en/library/information-schema-columns-table/ - +/ - - ColumnInfo[] ca = md.columns("basetest"); - assert( ca[0].schema == schemaName && ca[0].table == "basetest" && ca[0].name == "boolcol" && ca[0].index == 0 && - ca[0].nullable && ca[0].type == "bit" && ca[0].charsMax == -1 && ca[0].octetsMax == -1 && - ca[0].numericPrecision == 1 && ca[0].numericScale == -1 && ca[0].charSet == "" && ca[0].collation == "" && - ca[0].colType == "bit(1)"); - assert( ca[1].schema == schemaName && ca[1].table == "basetest" && ca[1].name == "bytecol" && ca[1].index == 1 && - ca[1].nullable && ca[1].type == "tinyint" && ca[1].charsMax == -1 && ca[1].octetsMax == -1 && - ca[1].numericPrecision == 3 && ca[1].numericScale == 0 && ca[1].charSet == "" && ca[1].collation == "" && - ca[1].colType == "tinyint(4)"); - assert( ca[2].schema == schemaName && ca[2].table == "basetest" && ca[2].name == "ubytecol" && ca[2].index == 2 && - ca[2].nullable && ca[2].type == "tinyint" && ca[2].charsMax == -1 && ca[2].octetsMax == -1 && - ca[2].numericPrecision == 3 && ca[2].numericScale == 0 && ca[2].charSet == "" && ca[2].collation == "" && - ca[2].colType == "tinyint(3) unsigned"); - assert( ca[3].schema == schemaName && ca[3].table == "basetest" && ca[3].name == "shortcol" && ca[3].index == 3 && - ca[3].nullable && ca[3].type == "smallint" && ca[3].charsMax == -1 && ca[3].octetsMax == -1 && - ca[3].numericPrecision == 5 && ca[3].numericScale == 0 && ca[3].charSet == "" && ca[3].collation == "" && - ca[3].colType == "smallint(6)"); - assert( ca[4].schema == schemaName && ca[4].table == "basetest" && ca[4].name == "ushortcol" && ca[4].index == 4 && - ca[4].nullable && ca[4].type == "smallint" && ca[4].charsMax == -1 && ca[4].octetsMax == -1 && - ca[4].numericPrecision == 5 && ca[4].numericScale == 0 && ca[4].charSet == "" && ca[4].collation == "" && - ca[4].colType == "smallint(5) unsigned"); - assert( ca[5].schema == schemaName && ca[5].table == "basetest" && ca[5].name == "intcol" && ca[5].index == 5 && - ca[5].nullable && ca[5].type == "int" && ca[5].charsMax == -1 && ca[5].octetsMax == -1 && - ca[5].numericPrecision == 10 && ca[5].numericScale == 0 && ca[5].charSet == "" && ca[5].collation == "" && - ca[5].colType == "int(11)"); - assert( ca[6].schema == schemaName && ca[6].table == "basetest" && ca[6].name == "uintcol" && ca[6].index == 6 && - ca[6].nullable && ca[6].type == "int" && ca[6].charsMax == -1 && ca[6].octetsMax == -1 && - ca[6].numericPrecision == 10 && ca[6].numericScale == 0 && ca[6].charSet == "" && ca[6].collation == "" && - ca[6].colType == "int(10) unsigned"); - assert( ca[7].schema == schemaName && ca[7].table == "basetest" && ca[7].name == "longcol" && ca[7].index == 7 && - ca[7].nullable && ca[7].type == "bigint" && ca[7].charsMax == -1 && ca[7].octetsMax == -1 && - ca[7].numericPrecision == 19 && ca[7].numericScale == 0 && ca[7].charSet == "" && ca[7].collation == "" && - ca[7].colType == "bigint(20)"); - assert( ca[8].schema == schemaName && ca[8].table == "basetest" && ca[8].name == "ulongcol" && ca[8].index == 8 && - ca[8].nullable && ca[8].type == "bigint" && ca[8].charsMax == -1 && ca[8].octetsMax == -1 && - //TODO: I'm getting numericPrecision==19, figure it out later - /+ca[8].numericPrecision == 20 &&+/ ca[8].numericScale == 0 && ca[8].charSet == "" && ca[8].collation == "" && - ca[8].colType == "bigint(20) unsigned"); - assert( ca[9].schema == schemaName && ca[9].table == "basetest" && ca[9].name == "charscol" && ca[9].index == 9 && - ca[9].nullable && ca[9].type == "char" && ca[9].charsMax == 10 && ca[9].octetsMax == 10 && - ca[9].numericPrecision == -1 && ca[9].numericScale == -1 && ca[9].charSet == "latin1" && ca[9].collation == "latin1_swedish_ci" && - ca[9].colType == "char(10)"); - assert( ca[10].schema == schemaName && ca[10].table == "basetest" && ca[10].name == "stringcol" && ca[10].index == 10 && - ca[10].nullable && ca[10].type == "varchar" && ca[10].charsMax == 50 && ca[10].octetsMax == 50 && - ca[10].numericPrecision == -1 && ca[10].numericScale == -1 && ca[10].charSet == "latin1" && ca[10].collation == "latin1_swedish_ci" && - ca[10].colType == "varchar(50)"); - assert( ca[11].schema == schemaName && ca[11].table == "basetest" && ca[11].name == "bytescol" && ca[11].index == 11 && - ca[11].nullable && ca[11].type == "tinyblob" && ca[11].charsMax == 255 && ca[11].octetsMax == 255 && - ca[11].numericPrecision == -1 && ca[11].numericScale == -1 && ca[11].charSet == "" && ca[11].collation == "" && - ca[11].colType == "tinyblob"); - assert( ca[12].schema == schemaName && ca[12].table == "basetest" && ca[12].name == "datecol" && ca[12].index == 12 && - ca[12].nullable && ca[12].type == "date" && ca[12].charsMax == -1 && ca[12].octetsMax == -1 && - ca[12].numericPrecision == -1 && ca[12].numericScale == -1 && ca[12].charSet == "" && ca[12].collation == "" && - ca[12].colType == "date"); - assert( ca[13].schema == schemaName && ca[13].table == "basetest" && ca[13].name == "timecol" && ca[13].index == 13 && - ca[13].nullable && ca[13].type == "time" && ca[13].charsMax == -1 && ca[13].octetsMax == -1 && - ca[13].numericPrecision == -1 && ca[13].numericScale == -1 && ca[13].charSet == "" && ca[13].collation == "" && - ca[13].colType == "time"); - assert( ca[14].schema == schemaName && ca[14].table == "basetest" && ca[14].name == "dtcol" && ca[14].index == 14 && - ca[14].nullable && ca[14].type == "datetime" && ca[14].charsMax == -1 && ca[14].octetsMax == -1 && - ca[14].numericPrecision == -1 && ca[14].numericScale == -1 && ca[14].charSet == "" && ca[14].collation == "" && - ca[14].colType == "datetime"); - assert( ca[15].schema == schemaName && ca[15].table == "basetest" && ca[15].name == "doublecol" && ca[15].index == 15 && - ca[15].nullable && ca[15].type == "double" && ca[15].charsMax == -1 && ca[15].octetsMax == -1 && - ca[15].numericPrecision == 22 && ca[15].numericScale == -1 && ca[15].charSet == "" && ca[15].collation == "" && - ca[15].colType == "double"); - assert( ca[16].schema == schemaName && ca[16].table == "basetest" && ca[16].name == "floatcol" && ca[16].index == 16 && - ca[16].nullable && ca[16].type == "float" && ca[16].charsMax == -1 && ca[16].octetsMax == -1 && - ca[16].numericPrecision == 12 && ca[16].numericScale == -1 && ca[16].charSet == "" && ca[16].collation == "" && - ca[16].colType == "float"); - assert( ca[17].schema == schemaName && ca[17].table == "basetest" && ca[17].name == "nullcol" && ca[17].index == 17 && - ca[17].nullable && ca[17].type == "int" && ca[17].charsMax == -1 && ca[17].octetsMax == -1 && - ca[17].numericPrecision == 10 && ca[17].numericScale == 0 && ca[17].charSet == "" && ca[17].collation == "" && - ca[17].colType == "int(11)"); - assert( ca[18].schema == schemaName && ca[18].table == "basetest" && ca[18].name == "decimalcol" && ca[18].index == 18 && - ca[18].nullable && ca[18].type == "decimal" && ca[18].charsMax == -1 && ca[18].octetsMax == -1 && - ca[18].numericPrecision == 11 && ca[18].numericScale == 4 && ca[18].charSet == "" && ca[18].collation == "" && - ca[18].colType == "decimal(11,4)"); - MySQLProcedure[] pa = md.functions(); - //assert(pa[0].db == schemaName && pa[0].name == "hello" && pa[0].type == "FUNCTION"); - //pa = md.procedures(); - //assert(pa[0].db == schemaName && pa[0].name == "insert2" && pa[0].type == "PROCEDURE"); + test!false(); + () @safe { test!true(); } (); } /+ @@ -570,124 +587,130 @@ https://github.com/simendsjo/mysqln // Bind values in prepared statements @("bind-values") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.prepared; - mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS manytypes"); - cn.exec( "CREATE TABLE manytypes (" - ~" i INT" - ~", f FLOAT" - ~", dttm DATETIME" - ~", dt DATE" - ~")"); - - //DataSet ds; - Row[] rs; - //Table tbl; - Row row; - Prepared stmt; - - // Index out of bounds throws - /+ - try + void test(bool doSafe)() { - cn.query_("SELECT TRUE", 1); - assert(0); - } - catch(Exception ex) {} - +/ + mixin(doImports(doSafe, "result", "prepared", "connection", "commands")); + mixin(scopedCn); + cn.exec("DROP TABLE IF EXISTS manytypes"); + cn.exec( "CREATE TABLE manytypes (" + ~" i INT" + ~", f FLOAT" + ~", dttm DATETIME" + ~", dt DATE" + ~")"); + + //DataSet ds; + Row[] rs; + //Table tbl; + Row row; + Prepared stmt; + + // Index out of bounds throws + /+ + try + { + cn.query_("SELECT TRUE", 1); + assert(0); + } + catch(Exception ex) {} + +/ - // Select without result - cn.truncate("manytypes"); - cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); - stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ?"); - { - auto val = 2; - stmt.setArg(0, val); - } - //ds = cn.query_(stmt); - //assert(ds.length == 1); - //assert(ds[0].length == 0); - rs = cn.query(stmt).array; - assert(rs.length == 0); - - // Bind single primitive value - cn.truncate("manytypes"); - cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); - stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ?"); - { - auto val = 1; - stmt.setArg(0, val); - } - cn.queryValue(stmt); + // Select without result + cn.truncate("manytypes"); + cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); + stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ?"); + { + auto val = 2; + stmt.setArg(0, val); + } + //ds = cn.query_(stmt); + //assert(ds.length == 1); + //assert(ds[0].length == 0); + rs = cn.query(stmt).array; + assert(rs.length == 0); - // Bind multiple primitive values - cn.truncate("manytypes"); - cn.exec("INSERT INTO manytypes (i, f) VALUES (1, 2)"); - { - auto val1 = 1; - auto val2 = 2; - stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ? AND f = ?"); - stmt.setArgs(val1, val2); - row = cn.queryRow(stmt).get; - } - assert(row[0] == 1); - assert(row[1] == 2); + // Bind single primitive value + cn.truncate("manytypes"); + cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); + stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ?"); + { + auto val = 1; + stmt.setArg(0, val); + } + cn.queryValue(stmt); - /+ - // Commented out because leaving args unspecified is currently unsupported, - // and I'm not convinced it should be allowed. + // Bind multiple primitive values + cn.truncate("manytypes"); + cn.exec("INSERT INTO manytypes (i, f) VALUES (1, 2)"); + { + auto val1 = 1; + auto val2 = 2; + stmt = cn.prepare("SELECT * FROM manytypes WHERE i = ? AND f = ?"); + stmt.setArgs(val1, val2); + row = cn.queryRow(stmt).get; + } + assert(row[0] == 1); + assert(row[1] == 2); - // Insert null - params defaults to null - { + /+ + // Commented out because leaving args unspecified is currently unsupported, + // and I'm not convinced it should be allowed. + + // Insert null - params defaults to null + { + cn.truncate("manytypes"); + auto prep = cn.prepare("INSERT INTO manytypes (i, f) VALUES (1, ?)"); + cn.exec(prep); + cn.assertScalar!int("SELECT i FROM manytypes WHERE f IS NULL", 1); + } + +/ + + // Insert null cn.truncate("manytypes"); - auto prep = cn.prepare("INSERT INTO manytypes (i, f) VALUES (1, ?)"); - cn.exec(prep); + { + auto prepared = cn.prepare("INSERT INTO manytypes (i, f) VALUES (1, ?)"); + //TODO: Using `prepared.setArgs(null);` results in: Param count supplied does not match prepared statement + // Can anything be done about that? + prepared.setArg(0, null); + cn.exec(prepared); + } cn.assertScalar!int("SELECT i FROM manytypes WHERE f IS NULL", 1); - } - +/ - // Insert null - cn.truncate("manytypes"); - { - auto prepared = cn.prepare("INSERT INTO manytypes (i, f) VALUES (1, ?)"); - //TODO: Using `prepared.setArgs(null);` results in: Param count supplied does not match prepared statement - // Can anything be done about that? - prepared.setArg(0, null); - cn.exec(prepared); - } - cn.assertScalar!int("SELECT i FROM manytypes WHERE f IS NULL", 1); + // select where null + cn.truncate("manytypes"); + cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); + { + stmt = cn.prepare("SELECT i FROM manytypes WHERE f <=> ?"); + //TODO: Using `stmt.setArgs(null);` results in: Param count supplied does not match prepared statement + // Can anything be done about that? + stmt.setArg(0, null); + auto value = cn.queryValue(stmt); + assert(!value.isNull); + assert(value.get.get!int == 1); + } - // select where null - cn.truncate("manytypes"); - cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); - { - stmt = cn.prepare("SELECT i FROM manytypes WHERE f <=> ?"); - //TODO: Using `stmt.setArgs(null);` results in: Param count supplied does not match prepared statement - // Can anything be done about that? - stmt.setArg(0, null); - auto value = cn.queryValue(stmt); - assert(!value.isNull); - assert(value.get.get!int == 1); + // rebind parameter + cn.truncate("manytypes"); + cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); + auto cmd = cn.prepare("SELECT i FROM manytypes WHERE f <=> ?"); + cmd.setArg(0, 1); + auto tbl = cn.query(cmd).array(); + assert(tbl.length == 0); + cmd.setArg(0, null); + assert(cn.queryValue(cmd).get.get!int == 1); } - // rebind parameter - cn.truncate("manytypes"); - cn.exec("INSERT INTO manytypes (i, f) VALUES (1, NULL)"); - auto cmd = cn.prepare("SELECT i FROM manytypes WHERE f <=> ?"); - cmd.setArg(0, 1); - auto tbl = cn.query(cmd).array(); - assert(tbl.length == 0); - cmd.setArg(0, null); - assert(cn.queryValue(cmd).get.get!int == 1); + test!false(); + () @safe { test!true(); }(); } // Simple commands @("simple-commands") debug(MYSQLN_TESTS) -unittest +@system unittest { mixin(scopedCn); @@ -807,20 +830,27 @@ unittest // Simple text queries @("simple-text-queries") debug(MYSQLN_TESTS) -unittest +@system unittest { - mixin(scopedCn); - auto ds = cn.query("SELECT 1").array; - assert(ds.length == 1); - //auto rs = ds[0]; - //assert(rs.rows.length == 1); - //auto row = rs.rows[0]; - auto rs = ds; - assert(rs.length == 1); - auto row = rs[0]; - //assert(row.length == 1); - assert(row._values.length == 1); - assert(row[0].get!long == 1); + static void test(bool doSafe)() + { + mixin(scopedCn); + mixin(doImports(doSafe, "commands")); + auto ds = cn.query("SELECT 1").array; + assert(ds.length == 1); + //auto rs = ds[0]; + //assert(rs.rows.length == 1); + //auto row = rs.rows[0]; + auto rs = ds; + assert(rs.length == 1); + auto row = rs[0]; + //assert(row.length == 1); + assert(row._values.length == 1); + assert(row[0].get!long == 1); + } + + test!false(); + () @safe { test!true(); }(); } /+ @@ -848,318 +878,345 @@ unittest // Create and query table @("create-and-query-table") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.prepared; - mixin(scopedCn); - - void assertBasicTests(T, U)(string sqlType, U[] values ...) + static void test(bool doSafe)() { - import std.array; - immutable tablename = "`basic_"~sqlType.replace(" ", "")~"`"; - cn.exec("CREATE TABLE IF NOT EXISTS "~tablename~" (value "~sqlType~ ")"); - - // Missing and NULL - cn.exec("TRUNCATE "~tablename); - immutable selectOneSql = "SELECT value FROM "~tablename~" LIMIT 1"; - //assert(cn.query_(selectOneSql)[0].length == 0); - assert(cn.query(selectOneSql).array.length == 0); - - immutable insertNullSql = "INSERT INTO "~tablename~" VALUES (NULL)"; - auto okp = cn.exec(insertNullSql); - //assert(okp.affectedRows == 1); - assert(okp == 1); - auto insertNullStmt = cn.prepare(insertNullSql); - okp = cn.exec(insertNullStmt); - //assert(okp.affectedRows == 1); - assert(okp == 1); - - //assert(!cn.queryScalar(selectOneSql).hasValue); - auto x = cn.queryValue(selectOneSql); - assert(!x.isNull); - assert(x.get.type == typeid(typeof(null))); - - // NULL as bound param - auto inscmd = cn.prepare("INSERT INTO "~tablename~" VALUES (?)"); - cn.exec("TRUNCATE "~tablename); - - inscmd.setArgs([Variant(null)]); - okp = cn.exec(inscmd); - //assert(okp.affectedRows == 1, "value not inserted"); - assert(okp == 1, "value not inserted"); - - //assert(!cn.queryScalar(selectOneSql).hasValue); - x = cn.queryValue(selectOneSql); - assert(!x.isNull); - assert(x.get.type == typeid(typeof(null))); - - // Values - void assertBasicTestsValue(T, U)(U val) + mixin(doImports(doSafe, "prepared", "commands", "connection", "result")); + mixin(scopedCn); + + void assertBasicTests(T, U)(string sqlType, U[] values ...) { + import std.array; + immutable tablename = "`basic_"~sqlType.replace(" ", "")~"`"; + cn.exec("CREATE TABLE IF NOT EXISTS "~tablename~" (value "~sqlType~ ")"); + + // Missing and NULL + cn.exec("TRUNCATE "~tablename); + immutable selectOneSql = "SELECT value FROM "~tablename~" LIMIT 1"; + //assert(cn.query_(selectOneSql)[0].length == 0); + assert(cn.query(selectOneSql).array.length == 0); + + immutable insertNullSql = "INSERT INTO "~tablename~" VALUES (NULL)"; + auto okp = cn.exec(insertNullSql); + //assert(okp.affectedRows == 1); + assert(okp == 1); + auto insertNullStmt = cn.prepare(insertNullSql); + okp = cn.exec(insertNullStmt); + //assert(okp.affectedRows == 1); + assert(okp == 1); + + //assert(!cn.queryScalar(selectOneSql).hasValue); + auto x = cn.queryValue(selectOneSql); + assert(!x.isNull); + assert(x.get.valIsNull); + + // NULL as bound param + auto inscmd = cn.prepare("INSERT INTO "~tablename~" VALUES (?)"); cn.exec("TRUNCATE "~tablename); - inscmd.setArg(0, val); - auto ra = cn.exec(inscmd); - assert(ra == 1, "value not inserted"); + static if(doSafe) + inscmd.setArgs([MySQLVal(null)]); + else + inscmd.setArgs([Variant(null)]); + okp = cn.exec(inscmd); + //assert(okp.affectedRows == 1, "value not inserted"); + assert(okp == 1, "value not inserted"); + + //assert(!cn.queryScalar(selectOneSql).hasValue); + x = cn.queryValue(selectOneSql); + assert(!x.isNull); + assert(x.get.valIsNull); + + // Values + void assertBasicTestsValue(T, U)(U val) + { + cn.exec("TRUNCATE "~tablename); - cn.assertScalar!(Unqual!T)(selectOneSql, val); - } - foreach(value; values) - { - assertBasicTestsValue!(T)(value); - assertBasicTestsValue!(T)(cast(const(U))value); - assertBasicTestsValue!(T)(cast(immutable(U))value); - assertBasicTestsValue!(T)(cast(shared(immutable(U)))value); + inscmd.setArg(0, val); + auto ra = cn.exec(inscmd); + assert(ra == 1, "value not inserted"); + + cn.assertScalar!(Unqual!T)(selectOneSql, val); + } + foreach(value; values) + { + assertBasicTestsValue!(T)(value); + assertBasicTestsValue!(T)(cast(const(U))value); + assertBasicTestsValue!(T)((() @trusted => cast(immutable(U))value)()); + // Note, shared(immutable(U)) is equivalent to immutable(U), so we + // are avoiding doing that test 2x. + static assert(is(shared(immutable(U)) == immutable(U))); + //assertBasicTestsValue!(T)(cast(shared(immutable(U)))value); + } } + + // TODO: Add tests for epsilon + assertBasicTests!float("FLOAT", 0.0f, 0.1f, -0.1f, 1.0f, -1.0f); + assertBasicTests!double("DOUBLE", 0.0, 0.1, -0.1, 1.0, -1.0); + + // TODO: Why don't these work? + //assertBasicTests!bool("BOOL", true, false); + //assertBasicTests!bool("TINYINT(1)", true, false); + assertBasicTests!byte("BOOl", cast(byte)1, cast(byte)0); + assertBasicTests!byte("TINYINT(1)", cast(byte)1, cast(byte)0); + + assertBasicTests!byte("TINYINT", + cast(byte)0, cast(byte)1, cast(byte)-1, byte.min, byte.max); + assertBasicTests!ubyte("TINYINT UNSIGNED", + cast(ubyte)0, cast(ubyte)1, ubyte.max); + assertBasicTests!short("SMALLINT", + cast(short)0, cast(short)1, cast(short)-1, short.min, short.max); + assertBasicTests!ushort("SMALLINT UNSIGNED", + cast(ushort)0, cast(ushort)1, ushort.max); + assertBasicTests!int("INT", 0, 1, -1, int.min, int.max); + assertBasicTests!uint("INT UNSIGNED", 0U, 1U, uint.max); + assertBasicTests!long("BIGINT", 0L, 1L, -1L, long.min, long.max); + assertBasicTests!ulong("BIGINT UNSIGNED", 0LU, 1LU, ulong.max); + + assertBasicTests!string("VARCHAR(10)", "", "aoeu"); + assertBasicTests!string("CHAR(10)", "", "aoeu"); + + assertBasicTests!string("TINYTEXT", "", "aoeu"); + assertBasicTests!string("MEDIUMTEXT", "", "aoeu"); + assertBasicTests!string("TEXT", "", "aoeu"); + assertBasicTests!string("LONGTEXT", "", "aoeu"); + + assertBasicTests!(ubyte[])("TINYBLOB", "", "aoeu"); + assertBasicTests!(ubyte[])("MEDIUMBLOB", "", "aoeu"); + assertBasicTests!(ubyte[])("BLOB", "", "aoeu"); + assertBasicTests!(ubyte[])("LONGBLOB", "", "aoeu"); + + assertBasicTests!(ubyte[])("TINYBLOB", cast(ubyte[])"".dup, cast(ubyte[])"aoeu".dup); + assertBasicTests!(ubyte[])("TINYBLOB", "".dup, "aoeu".dup); + + assertBasicTests!Date("DATE", Date(2013, 10, 03)); + assertBasicTests!DateTime("DATETIME", DateTime(2013, 10, 03, 12, 55, 35)); + assertBasicTests!TimeOfDay("TIME", TimeOfDay(12, 55, 35)); + //assertBasicTests!DateTime("TIMESTAMP NULL", Timestamp(2013_10_03_12_55_35)); + //TODO: Add Timestamp } - // TODO: Add tests for epsilon - assertBasicTests!float("FLOAT", 0.0f, 0.1f, -0.1f, 1.0f, -1.0f); - assertBasicTests!double("DOUBLE", 0.0, 0.1, -0.1, 1.0, -1.0); - - // TODO: Why don't these work? - //assertBasicTests!bool("BOOL", true, false); - //assertBasicTests!bool("TINYINT(1)", true, false); - assertBasicTests!byte("BOOl", cast(byte)1, cast(byte)0); - assertBasicTests!byte("TINYINT(1)", cast(byte)1, cast(byte)0); - - assertBasicTests!byte("TINYINT", - cast(byte)0, cast(byte)1, cast(byte)-1, byte.min, byte.max); - assertBasicTests!ubyte("TINYINT UNSIGNED", - cast(ubyte)0, cast(ubyte)1, ubyte.max); - assertBasicTests!short("SMALLINT", - cast(short)0, cast(short)1, cast(short)-1, short.min, short.max); - assertBasicTests!ushort("SMALLINT UNSIGNED", - cast(ushort)0, cast(ushort)1, ushort.max); - assertBasicTests!int("INT", 0, 1, -1, int.min, int.max); - assertBasicTests!uint("INT UNSIGNED", 0U, 1U, uint.max); - assertBasicTests!long("BIGINT", 0L, 1L, -1L, long.min, long.max); - assertBasicTests!ulong("BIGINT UNSIGNED", 0LU, 1LU, ulong.max); - - assertBasicTests!string("VARCHAR(10)", "", "aoeu"); - assertBasicTests!string("CHAR(10)", "", "aoeu"); - - assertBasicTests!string("TINYTEXT", "", "aoeu"); - assertBasicTests!string("MEDIUMTEXT", "", "aoeu"); - assertBasicTests!string("TEXT", "", "aoeu"); - assertBasicTests!string("LONGTEXT", "", "aoeu"); - - assertBasicTests!(ubyte[])("TINYBLOB", "", "aoeu"); - assertBasicTests!(ubyte[])("MEDIUMBLOB", "", "aoeu"); - assertBasicTests!(ubyte[])("BLOB", "", "aoeu"); - assertBasicTests!(ubyte[])("LONGBLOB", "", "aoeu"); - - assertBasicTests!(ubyte[])("TINYBLOB", cast(byte[])"", cast(byte[])"aoeu"); - assertBasicTests!(ubyte[])("TINYBLOB", cast(char[])"", cast(char[])"aoeu"); - - assertBasicTests!Date("DATE", Date(2013, 10, 03)); - assertBasicTests!DateTime("DATETIME", DateTime(2013, 10, 03, 12, 55, 35)); - assertBasicTests!TimeOfDay("TIME", TimeOfDay(12, 55, 35)); - //assertBasicTests!DateTime("TIMESTAMP NULL", Timestamp(2013_10_03_12_55_35)); - //TODO: Add Timestamp + test!false(); + () @safe { test!true(); }(); } @("info_character_sets") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.prepared; - mixin(scopedCn); - auto stmt = cn.prepare( - "SELECT * FROM information_schema.character_sets"~ - " WHERE CHARACTER_SET_NAME=?"); - auto val = "utf8mb4"; - stmt.setArg(0, val); - auto row = cn.queryRow(stmt).get; - assert(row.length == 4); - assert(row[0] == "utf8mb4"); - assert(row[1] == "utf8mb4_general_ci"); - assert(row[2] == "UTF-8 Unicode"); - assert(row[3] == 4); + static void test(bool doSafe)() + { + mixin(doImports(doSafe, "commands", "connection", "result")); + mixin(scopedCn); + auto stmt = cn.prepare( + "SELECT * FROM information_schema.character_sets"~ + " WHERE CHARACTER_SET_NAME=?"); + auto val = "utf8mb4"; + stmt.setArg(0, val); + auto row = cn.queryRow(stmt).get; + assert(row.length == 4); + assert(row[0] == "utf8mb4"); + assert(row[1] == "utf8mb4_general_ci"); + assert(row[2] == "UTF-8 Unicode"); + assert(row[3] == 4); + } + + test!false(); + () @safe { test!true(); }(); } @("coupleTypes") debug(MYSQLN_TESTS) -unittest +@system unittest { - import mysql.prepared; - mixin(scopedCn); + static void test(bool doSafe)() + { + mixin(doImports(doSafe, "prepared", "commands", "connection", "result")); + mixin(scopedCn); - cn.exec("DROP TABLE IF EXISTS `coupleTypes`"); - cn.exec("CREATE TABLE `coupleTypes` ( - `i` INTEGER, - `s` VARCHAR(50) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); - cn.exec("INSERT INTO `coupleTypes` VALUES (11, 'aaa'), (22, 'bbb'), (33, 'ccc')"); + cn.exec("DROP TABLE IF EXISTS `coupleTypes`"); + cn.exec("CREATE TABLE `coupleTypes` ( + `i` INTEGER, + `s` VARCHAR(50) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + cn.exec("INSERT INTO `coupleTypes` VALUES (11, 'aaa'), (22, 'bbb'), (33, 'ccc')"); - immutable selectSQL = "SELECT * FROM `coupleTypes` ORDER BY i ASC"; - immutable selectBackwardsSQL = "SELECT `s`,`i` FROM `coupleTypes` ORDER BY i DESC"; - immutable selectNoRowsSQL = "SELECT * FROM `coupleTypes` WHERE s='no such match'"; - auto prepared = cn.prepare(selectSQL); - auto preparedSelectNoRows = cn.prepare(selectNoRowsSQL); + immutable selectSQL = "SELECT * FROM `coupleTypes` ORDER BY i ASC"; + immutable selectBackwardsSQL = "SELECT `s`,`i` FROM `coupleTypes` ORDER BY i DESC"; + immutable selectNoRowsSQL = "SELECT * FROM `coupleTypes` WHERE s='no such match'"; + auto prepared = cn.prepare(selectSQL); + auto preparedSelectNoRows = cn.prepare(selectNoRowsSQL); - { - // Test query - ResultRange rseq = cn.query(selectSQL); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 11); - assert(rseq.front[1] == "aaa"); - rseq.popFront(); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 22); - assert(rseq.front[1] == "bbb"); - rseq.popFront(); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 33); - assert(rseq.front[1] == "ccc"); - rseq.popFront(); - assert(rseq.empty); - } + { + // Test query + ResultRange rseq = cn.query(selectSQL); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 11); + assert(rseq.front[1] == "aaa"); + rseq.popFront(); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 22); + assert(rseq.front[1] == "bbb"); + rseq.popFront(); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 33); + assert(rseq.front[1] == "ccc"); + rseq.popFront(); + assert(rseq.empty); + } - { - // Test prepared query - ResultRange rseq = cn.query(prepared); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 11); - assert(rseq.front[1] == "aaa"); - rseq.popFront(); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 22); - assert(rseq.front[1] == "bbb"); - rseq.popFront(); - assert(!rseq.empty); - assert(rseq.front.length == 2); - assert(rseq.front[0] == 33); - assert(rseq.front[1] == "ccc"); - rseq.popFront(); - assert(rseq.empty); - } + { + // Test prepared query + ResultRange rseq = cn.query(prepared); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 11); + assert(rseq.front[1] == "aaa"); + rseq.popFront(); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 22); + assert(rseq.front[1] == "bbb"); + rseq.popFront(); + assert(!rseq.empty); + assert(rseq.front.length == 2); + assert(rseq.front[0] == 33); + assert(rseq.front[1] == "ccc"); + rseq.popFront(); + assert(rseq.empty); + } - { - // Test reusing the same ResultRange - ResultRange rseq = cn.query(selectSQL); - assert(!rseq.empty); - rseq.each(); - assert(rseq.empty); - rseq = cn.query(selectSQL); - //assert(!rseq.empty); //TODO: Why does this fail??? - rseq.each(); - assert(rseq.empty); - } + { + // Test reusing the same ResultRange + ResultRange rseq = cn.query(selectSQL); + assert(!rseq.empty); + rseq.each(); + assert(rseq.empty); + rseq = cn.query(selectSQL); + //assert(!rseq.empty); //TODO: Why does this fail??? + rseq.each(); + assert(rseq.empty); + } - { - Nullable!Row nullableRow; - - // Test queryRow - nullableRow = cn.queryRow(selectSQL); - assert(!nullableRow.isNull); - assert(nullableRow.get[0] == 11); - assert(nullableRow.get[1] == "aaa"); - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - - nullableRow = cn.queryRow(selectNoRowsSQL); - assert(nullableRow.isNull); - - // Test prepared queryRow - nullableRow = cn.queryRow(prepared); - assert(!nullableRow.isNull); - assert(nullableRow.get[0] == 11); - assert(nullableRow.get[1] == "aaa"); - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - - nullableRow = cn.queryRow(preparedSelectNoRows); - assert(nullableRow.isNull); - } + { + Nullable!Row nullableRow; + + // Test queryRow + nullableRow = cn.queryRow(selectSQL); + assert(!nullableRow.isNull); + assert(nullableRow.get[0] == 11); + assert(nullableRow.get[1] == "aaa"); + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + + nullableRow = cn.queryRow(selectNoRowsSQL); + assert(nullableRow.isNull); + + // Test prepared queryRow + nullableRow = cn.queryRow(prepared); + assert(!nullableRow.isNull); + assert(nullableRow.get[0] == 11); + assert(nullableRow.get[1] == "aaa"); + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + + nullableRow = cn.queryRow(preparedSelectNoRows); + assert(nullableRow.isNull); + } - { - int resultI; - string resultS; - - // Test queryRowTuple - cn.queryRowTuple(selectSQL, resultI, resultS); - assert(resultI == 11); - assert(resultS == "aaa"); - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - - // Test prepared queryRowTuple - cn.queryRowTuple(prepared, resultI, resultS); - assert(resultI == 11); - assert(resultS == "aaa"); - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - } + { + int resultI; + string resultS; + + // Test queryRowTuple + cn.queryRowTuple(selectSQL, resultI, resultS); + assert(resultI == 11); + assert(resultS == "aaa"); + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + + // Test prepared queryRowTuple + cn.queryRowTuple(prepared, resultI, resultS); + assert(resultI == 11); + assert(resultS == "aaa"); + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + } - { - Nullable!Variant result; - - // Test queryValue - result = cn.queryValue(selectSQL); - assert(!result.isNull); - assert(result.get == 11); // Explicit "get" here works around DMD #17482 - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - - result = cn.queryValue(selectNoRowsSQL); - assert(result.isNull); - - // Test prepared queryValue - result = cn.queryValue(prepared); - assert(!result.isNull); - assert(result.get == 11); // Explicit "get" here works around DMD #17482 - // Were all results correctly purged? Can I still issue another command? - cn.query(selectSQL).array; - - result = cn.queryValue(preparedSelectNoRows); - assert(result.isNull); - } + { + static if(doSafe) + Nullable!MySQLVal result; + else + Nullable!Variant result; + + // Test queryValue + result = cn.queryValue(selectSQL); + assert(!result.isNull); + assert(result.get == 11); // Explicit "get" here works around DMD #17482 + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + + result = cn.queryValue(selectNoRowsSQL); + assert(result.isNull); + + // Test prepared queryValue + result = cn.queryValue(prepared); + assert(!result.isNull); + assert(result.get == 11); // Explicit "get" here works around DMD #17482 + // Were all results correctly purged? Can I still issue another command? + cn.query(selectSQL).array; + + result = cn.queryValue(preparedSelectNoRows); + assert(result.isNull); + } - { - // Issue new command before old command was purged - // Ensure old result set is auto-purged and invalidated. - ResultRange rseq1 = cn.query(selectSQL); - rseq1.popFront(); - assert(!rseq1.empty); - assert(rseq1.isValid); - assert(rseq1.front[0] == 22); - - cn.query(selectBackwardsSQL); - assert(rseq1.empty); - assert(!rseq1.isValid); - } + { + // Issue new command before old command was purged + // Ensure old result set is auto-purged and invalidated. + ResultRange rseq1 = cn.query(selectSQL); + rseq1.popFront(); + assert(!rseq1.empty); + assert(rseq1.isValid); + assert(rseq1.front[0] == 22); + + cn.query(selectBackwardsSQL); + assert(rseq1.empty); + assert(!rseq1.isValid); + } - { - // Test using outdated ResultRange - ResultRange rseq1 = cn.query(selectSQL); - rseq1.popFront(); - assert(!rseq1.empty); - assert(rseq1.front[0] == 22); - - cn.purgeResult(); - - assert(rseq1.empty); - assertThrown!MYXInvalidatedRange(rseq1.front); - assertThrown!MYXInvalidatedRange(rseq1.popFront()); - assertThrown!MYXInvalidatedRange(rseq1.asAA()); - - ResultRange rseq2 = cn.query(selectBackwardsSQL); - assert(!rseq2.empty); - assert(rseq2.front.length == 2); - assert(rseq2.front[0] == "ccc"); - assert(rseq2.front[1] == 33); - - assert(rseq1.empty); - assertThrown!MYXInvalidatedRange(rseq1.front); - assertThrown!MYXInvalidatedRange(rseq1.popFront()); - assertThrown!MYXInvalidatedRange(rseq1.asAA()); + { + // Test using outdated ResultRange + ResultRange rseq1 = cn.query(selectSQL); + rseq1.popFront(); + assert(!rseq1.empty); + assert(rseq1.front[0] == 22); + + cn.purgeResult(); + + assert(rseq1.empty); + assertThrown!MYXInvalidatedRange(rseq1.front); + assertThrown!MYXInvalidatedRange(rseq1.popFront()); + assertThrown!MYXInvalidatedRange(rseq1.asAA()); + + ResultRange rseq2 = cn.query(selectBackwardsSQL); + assert(!rseq2.empty); + assert(rseq2.front.length == 2); + assert(rseq2.front[0] == "ccc"); + assert(rseq2.front[1] == 33); + + assert(rseq1.empty); + assertThrown!MYXInvalidatedRange(rseq1.front); + assertThrown!MYXInvalidatedRange(rseq1.popFront()); + assertThrown!MYXInvalidatedRange(rseq1.asAA()); + } } + + test!false(); + () @safe { test!true(); }(); } // Test example.d @@ -1178,6 +1235,7 @@ unittest // Setup DB for test { mixin(scopedCn); + import mysql.safe.commands; cn.exec("DROP TABLE IF EXISTS `tablename`"); cn.exec("CREATE TABLE `tablename` ( diff --git a/source/mysql/commands.d b/source/mysql/commands.d index ab64c4fd..86afba3d 100644 --- a/source/mysql/commands.d +++ b/source/mysql/commands.d @@ -1,629 +1,10 @@ -/++ -Use a DB via plain SQL statements. - -Commands that are expected to return a result set - queries - have distinctive -methods that are enforced. That is it will be an error to call such a method -with an SQL command that does not produce a result set. So for commands like -SELECT, use the `query` functions. For other commands, like -INSERT/UPDATE/CREATE/etc, use `exec`. -+/ - -module mysql.commands; - -import std.conv; -import std.exception; -import std.range; -import std.typecons; -import std.variant; - -import mysql.connection; -import mysql.exceptions; -import mysql.prepared; -import mysql.protocol.comms; -import mysql.protocol.constants; -import mysql.protocol.extra_types; -import mysql.protocol.packets; -import mysql.result; - -/// This feature is not yet implemented. It currently has no effect. -/+ -A struct to represent specializations of returned statement columns. - -If you are executing a query that will include result columns that are large objects, -it may be expedient to deal with the data as it is received rather than first buffering -it to some sort of byte array. These two variables allow for this. If both are provided -then the corresponding column will be fed to the stipulated delegate in chunks of -`chunkSize`, with the possible exception of the last chunk, which may be smaller. -The bool argument `finished` will be set to true when the last chunk is set. - -Be aware when specifying types for column specializations that for some reason the -field descriptions returned for a resultset have all of the types TINYTEXT, MEDIUMTEXT, -TEXT, LONGTEXT, TINYBLOB, MEDIUMBLOB, BLOB, and LONGBLOB lumped as type 0xfc -contrary to what it says in the protocol documentation. -+/ -struct ColumnSpecialization -{ - size_t cIndex; // parameter number 0 - number of params-1 - ushort type; - uint chunkSize; /// In bytes - void delegate(const(ubyte)[] chunk, bool finished) chunkDelegate; -} -///ditto -alias CSN = ColumnSpecialization; - -/++ -Execute an SQL command or prepared statement, such as INSERT/UPDATE/CREATE/etc. - -This method is intended for commands such as which do not produce a result set -(otherwise, use one of the `query` functions instead.) If the SQL command does -produces a result set (such as SELECT), `mysql.exceptions.MYXResultRecieved` -will be thrown. - -If `args` is supplied, the sql string will automatically be used as a prepared -statement. Prepared statements are automatically cached by mysql-native, -so there's no performance penalty for using this multiple times for the -same statement instead of manually preparing a statement. - -If `args` and `prepared` are both provided, `args` will be used, -and any arguments that are already set in the prepared statement -will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). - -Only use the `const(char[]) sql` overload that doesn't take `args` -when you are not going to be using the same -command repeatedly and you are CERTAIN all the data you're sending is properly -escaped. Otherwise, consider using overload that takes a `Prepared`. - -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. - -Type_Mappings: $(TYPE_MAPPINGS) - -Params: -conn = An open `mysql.connection.Connection` to the database. -sql = The SQL command to be run. -prepared = The prepared statement to be run. - -Returns: The number of rows affected. - -Example: ---- -auto myInt = 7; -auto rowsAffected = myConnection.exec("INSERT INTO `myTable` (`a`) VALUES (?)", myInt); ---- -+/ -ulong exec(Connection conn, const(char[]) sql) -{ - return execImpl(conn, ExecQueryImplInfo(false, sql)); -} -///ditto -ulong exec(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[])) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return exec(conn, prepared); -} -///ditto -ulong exec(Connection conn, const(char[]) sql, Variant[] args) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return exec(conn, prepared); -} - -///ditto -ulong exec(Connection conn, ref Prepared prepared) -{ - auto preparedInfo = conn.registerIfNeeded(prepared.sql); - auto ra = execImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); - prepared._lastInsertID = conn.lastInsertID; - return ra; -} -///ditto -ulong exec(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[])) -{ - prepared.setArgs(args); - return exec(conn, prepared); -} -///ditto -ulong exec(Connection conn, ref Prepared prepared, Variant[] args) -{ - prepared.setArgs(args); - return exec(conn, prepared); -} - -///ditto -ulong exec(Connection conn, ref BackwardCompatPrepared prepared) -{ - auto p = prepared.prepared; - auto result = exec(conn, p); - prepared._prepared = p; - return result; -} - -/// Common implementation for `exec` overloads -package ulong execImpl(Connection conn, ExecQueryImplInfo info) -{ - ulong rowsAffected; - bool receivedResultSet = execQueryImpl(conn, info, rowsAffected); - if(receivedResultSet) - { - conn.purgeResult(); - throw new MYXResultRecieved(); - } - - return rowsAffected; -} - -/++ -Execute an SQL SELECT command or prepared statement. - -This returns an input range of `mysql.result.Row`, so if you need random access -to the `mysql.result.Row` elements, simply call -$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`) -on the result. - -If the SQL command does not produce a result set (such as INSERT/CREATE/etc), -then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use -`exec` instead for such commands. - -If `args` is supplied, the sql string will automatically be used as a prepared -statement. Prepared statements are automatically cached by mysql-native, -so there's no performance penalty for using this multiple times for the -same statement instead of manually preparing a statement. - -If `args` and `prepared` are both provided, `args` will be used, -and any arguments that are already set in the prepared statement -will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). - -Only use the `const(char[]) sql` overload that doesn't take `args` -when you are not going to be using the same -command repeatedly and you are CERTAIN all the data you're sending is properly -escaped. Otherwise, consider using overload that takes a `Prepared`. - -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. - -Type_Mappings: $(TYPE_MAPPINGS) - -Params: -conn = An open `mysql.connection.Connection` to the database. -sql = The SQL command to be run. -prepared = The prepared statement to be run. -csa = Not yet implemented. - -Returns: A (possibly empty) `mysql.result.ResultRange`. - -Example: ---- -ResultRange oneAtATime = myConnection.query("SELECT * from `myTable`"); -Row[] allAtOnce = myConnection.query("SELECT * from `myTable`").array; - -auto myInt = 7; -ResultRange rows = myConnection.query("SELECT * FROM `myTable` WHERE `a` = ?", myInt); ---- -+/ -/+ -Future text: -If there are long data items among the expected result columns you can use -the `csa` param to specify that they are to be subject to chunked transfer via a -delegate. - -csa = An optional array of `ColumnSpecialization` structs. If you need to -use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. -+/ -ResultRange query(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) -{ - return queryImpl(csa, conn, ExecQueryImplInfo(false, sql)); -} -///ditto -ResultRange query(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return query(conn, prepared); -} -///ditto -ResultRange query(Connection conn, const(char[]) sql, Variant[] args) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return query(conn, prepared); -} - -///ditto -ResultRange query(Connection conn, ref Prepared prepared) -{ - auto preparedInfo = conn.registerIfNeeded(prepared.sql); - auto result = queryImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); - prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. - return result; -} -///ditto -ResultRange query(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - prepared.setArgs(args); - return query(conn, prepared); -} -///ditto -ResultRange query(Connection conn, ref Prepared prepared, Variant[] args) -{ - prepared.setArgs(args); - return query(conn, prepared); -} - -///ditto -ResultRange query(Connection conn, ref BackwardCompatPrepared prepared) -{ - auto p = prepared.prepared; - auto result = query(conn, p); - prepared._prepared = p; - return result; -} - -/// Common implementation for `query` overloads -package ResultRange queryImpl(ColumnSpecialization[] csa, - Connection conn, ExecQueryImplInfo info) -{ - ulong ra; - enforce!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); - - conn._rsh = ResultSetHeaders(conn, conn._fieldCount); - if(csa !is null) - conn._rsh.addSpecializations(csa); - - conn._headersPending = false; - return ResultRange(conn, conn._rsh, conn._rsh.fieldNames); -} - -/++ -Execute an SQL SELECT command or prepared statement where you only want the -first `mysql.result.Row`, if any. - -If the SQL command does not produce a result set (such as INSERT/CREATE/etc), -then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use -`exec` instead for such commands. - -If `args` is supplied, the sql string will automatically be used as a prepared -statement. Prepared statements are automatically cached by mysql-native, -so there's no performance penalty for using this multiple times for the -same statement instead of manually preparing a statement. - -If `args` and `prepared` are both provided, `args` will be used, -and any arguments that are already set in the prepared statement -will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). - -Only use the `const(char[]) sql` overload that doesn't take `args` -when you are not going to be using the same -command repeatedly and you are CERTAIN all the data you're sending is properly -escaped. Otherwise, consider using overload that takes a `Prepared`. - -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. - -Type_Mappings: $(TYPE_MAPPINGS) - -Params: -conn = An open `mysql.connection.Connection` to the database. -sql = The SQL command to be run. -prepared = The prepared statement to be run. -csa = Not yet implemented. - -Returns: `Nullable!(mysql.result.Row)`: This will be null (check via `Nullable.isNull`) if the -query resulted in an empty result set. - -Example: ---- -auto myInt = 7; -Nullable!Row row = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); ---- -+/ -/+ -Future text: -If there are long data items among the expected result columns you can use -the `csa` param to specify that they are to be subject to chunked transfer via a -delegate. - -csa = An optional array of `ColumnSpecialization` structs. If you need to -use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. -+/ -/+ -Future text: -If there are long data items among the expected result columns you can use -the `csa` param to specify that they are to be subject to chunked transfer via a -delegate. - -csa = An optional array of `ColumnSpecialization` structs. If you need to -use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. -+/ -Nullable!Row queryRow(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) -{ - return queryRowImpl(csa, conn, ExecQueryImplInfo(false, sql)); -} -///ditto -Nullable!Row queryRow(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return queryRow(conn, prepared); -} -///ditto -Nullable!Row queryRow(Connection conn, const(char[]) sql, Variant[] args) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return queryRow(conn, prepared); -} - -///ditto -Nullable!Row queryRow(Connection conn, ref Prepared prepared) -{ - auto preparedInfo = conn.registerIfNeeded(prepared.sql); - auto result = queryRowImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); - prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. - return result; -} -///ditto -Nullable!Row queryRow(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - prepared.setArgs(args); - return queryRow(conn, prepared); -} -///ditto -Nullable!Row queryRow(Connection conn, ref Prepared prepared, Variant[] args) -{ - prepared.setArgs(args); - return queryRow(conn, prepared); -} - -///ditto -Nullable!Row queryRow(Connection conn, ref BackwardCompatPrepared prepared) -{ - auto p = prepared.prepared; - auto result = queryRow(conn, p); - prepared._prepared = p; - return result; -} - -/// Common implementation for `querySet` overloads. -package Nullable!Row queryRowImpl(ColumnSpecialization[] csa, Connection conn, - ExecQueryImplInfo info) -{ - auto results = queryImpl(csa, conn, info); - if(results.empty) - return Nullable!Row(); - else - { - auto row = results.front; - results.close(); - return Nullable!Row(row); - } -} - /++ -Execute an SQL SELECT command or prepared statement where you only want the -first `mysql.result.Row`, and place result values into a set of D variables. - -This method will throw if any column type is incompatible with the corresponding D variable. +This module publicly imports `mysql.unsafe.commands`. Please see that module for more documentation. -Unlike the other query functions, queryRowTuple will throw -`mysql.exceptions.MYX` if the result set is empty -(and thus the reference variables passed in cannot be filled). +In the far future, the unsafe version will be deprecated and removed, and the +safe version moved to this location. -If the SQL command does not produce a result set (such as INSERT/CREATE/etc), -then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use -`exec` instead for such commands. - -Only use the `const(char[]) sql` overload when you are not going to be using the same -command repeatedly and you are CERTAIN all the data you're sending is properly -escaped. Otherwise, consider using overload that takes a `Prepared`. - -Type_Mappings: $(TYPE_MAPPINGS) - -Params: -conn = An open `mysql.connection.Connection` to the database. -sql = The SQL command to be run. -prepared = The prepared statement to be run. -args = The variables, taken by reference, to receive the values. +$(SAFE_MIGRATION) +/ -void queryRowTuple(T...)(Connection conn, const(char[]) sql, ref T args) -{ - return queryRowTupleImpl(conn, ExecQueryImplInfo(false, sql), args); -} - -///ditto -void queryRowTuple(T...)(Connection conn, ref Prepared prepared, ref T args) -{ - auto preparedInfo = conn.registerIfNeeded(prepared.sql); - queryRowTupleImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId), args); - prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. -} - -///ditto -void queryRowTuple(T...)(Connection conn, ref BackwardCompatPrepared prepared, ref T args) -{ - auto p = prepared.prepared; - queryRowTuple(conn, p, args); - prepared._prepared = p; -} - -/// Common implementation for `queryRowTuple` overloads. -package void queryRowTupleImpl(T...)(Connection conn, ExecQueryImplInfo info, ref T args) -{ - ulong ra; - enforce!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); - - Row rr = conn.getNextRow(); - /+if (!rr._valid) // The result set was empty - not a crime. - return;+/ - enforce!MYX(rr._values.length == args.length, "Result column count does not match the target tuple."); - foreach (size_t i, dummy; args) - { - enforce!MYX(typeid(args[i]).toString() == rr._values[i].type.toString(), - "Tuple "~to!string(i)~" type and column type are not compatible."); - args[i] = rr._values[i].get!(typeof(args[i])); - } - // If there were more rows, flush them away - // Question: Should I check in purgeResult and throw if there were - it's very inefficient to - // allow sloppy SQL that does not ensure just one row! - conn.purgeResult(); -} - -/++ -Execute an SQL SELECT command or prepared statement and return a single value: -the first column of the first row received. - -If the query did not produce any rows, or the rows it produced have zero columns, -this will return `Nullable!Variant()`, ie, null. Test for this with `result.isNull`. - -If the query DID produce a result, but the value actually received is NULL, -then `result.isNull` will be FALSE, and `result.get` will produce a Variant -which CONTAINS null. Check for this with `result.get.type == typeid(typeof(null))`. - -If the SQL command does not produce a result set (such as INSERT/CREATE/etc), -then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use -`exec` instead for such commands. - -If `args` is supplied, the sql string will automatically be used as a prepared -statement. Prepared statements are automatically cached by mysql-native, -so there's no performance penalty for using this multiple times for the -same statement instead of manually preparing a statement. - -If `args` and `prepared` are both provided, `args` will be used, -and any arguments that are already set in the prepared statement -will automatically be replaced with `args` (note, just like calling -`mysql.prepared.Prepared.setArgs`, this will also remove all -`mysql.prepared.ParameterSpecialization` that may have been applied). - -Only use the `const(char[]) sql` overload that doesn't take `args` -when you are not going to be using the same -command repeatedly and you are CERTAIN all the data you're sending is properly -escaped. Otherwise, consider using overload that takes a `Prepared`. - -If you need to use any `mysql.prepared.ParameterSpecialization`, use -`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, -and set your parameter specializations using `mysql.prepared.Prepared.setArg` -or `mysql.prepared.Prepared.setArgs`. - -Type_Mappings: $(TYPE_MAPPINGS) - -Params: -conn = An open `mysql.connection.Connection` to the database. -sql = The SQL command to be run. -prepared = The prepared statement to be run. -csa = Not yet implemented. - -Returns: `Nullable!Variant`: This will be null (check via `Nullable.isNull`) if the -query resulted in an empty result set. - -Example: ---- -auto myInt = 7; -Nullable!Variant value = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); ---- -+/ -/+ -Future text: -If there are long data items among the expected result columns you can use -the `csa` param to specify that they are to be subject to chunked transfer via a -delegate. - -csa = An optional array of `ColumnSpecialization` structs. If you need to -use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. -+/ -/+ -Future text: -If there are long data items among the expected result columns you can use -the `csa` param to specify that they are to be subject to chunked transfer via a -delegate. - -csa = An optional array of `ColumnSpecialization` structs. If you need to -use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. -+/ -Nullable!Variant queryValue(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) -{ - return queryValueImpl(csa, conn, ExecQueryImplInfo(false, sql)); -} -///ditto -Nullable!Variant queryValue(T...)(Connection conn, const(char[]) sql, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return queryValue(conn, prepared); -} -///ditto -Nullable!Variant queryValue(Connection conn, const(char[]) sql, Variant[] args) -{ - auto prepared = conn.prepare(sql); - prepared.setArgs(args); - return queryValue(conn, prepared); -} - -///ditto -Nullable!Variant queryValue(Connection conn, ref Prepared prepared) -{ - auto preparedInfo = conn.registerIfNeeded(prepared.sql); - auto result = queryValueImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); - prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. - return result; -} -///ditto -Nullable!Variant queryValue(T...)(Connection conn, ref Prepared prepared, T args) - if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) -{ - prepared.setArgs(args); - return queryValue(conn, prepared); -} -///ditto -Nullable!Variant queryValue(Connection conn, ref Prepared prepared, Variant[] args) -{ - prepared.setArgs(args); - return queryValue(conn, prepared); -} - -///ditto -Nullable!Variant queryValue(Connection conn, ref BackwardCompatPrepared prepared) -{ - auto p = prepared.prepared; - auto result = queryValue(conn, p); - prepared._prepared = p; - return result; -} - -/// Common implementation for `queryValue` overloads. -package Nullable!Variant queryValueImpl(ColumnSpecialization[] csa, Connection conn, - ExecQueryImplInfo info) -{ - auto results = queryImpl(csa, conn, info); - if(results.empty) - return Nullable!Variant(); - else - { - auto row = results.front; - results.close(); - - if(row.length == 0) - return Nullable!Variant(); - else - return Nullable!Variant(row[0]); - } -} - +module mysql.commands; +public import mysql.unsafe.commands; diff --git a/source/mysql/connection.d b/source/mysql/connection.d index 46cd5200..577dbeaa 100644 --- a/source/mysql/connection.d +++ b/source/mysql/connection.d @@ -1,1061 +1,13 @@ -/// Connect to a MySQL/MariaDB server. -module mysql.connection; - -import std.algorithm; -import std.conv; -import std.exception; -import std.range; -import std.socket; -import std.string; -import std.typecons; - -import mysql.commands; -import mysql.exceptions; -import mysql.logger; -import mysql.prepared; -import mysql.protocol.comms; -import mysql.protocol.constants; -import mysql.protocol.packets; -import mysql.protocol.sockets; -import mysql.result; - -version(Have_vibe_core) -{ - static if(__traits(compiles, (){ import vibe.core.net; } )) - import vibe.core.net; - else - static assert(false, "mysql-native can't find Vibe.d's 'vibe.core.net'."); -} - -/// The default `mysql.protocol.constants.SvrCapFlags` used when creating a connection. -immutable SvrCapFlags defaultClientFlags = - SvrCapFlags.OLD_LONG_PASSWORD | SvrCapFlags.ALL_COLUMN_FLAGS | - SvrCapFlags.WITH_DB | SvrCapFlags.PROTOCOL41 | - SvrCapFlags.SECURE_CONNECTION;// | SvrCapFlags.MULTI_STATEMENTS | - //SvrCapFlags.MULTI_RESULTS; - -/++ -Submit an SQL command to the server to be compiled into a prepared statement. - -This will automatically register the prepared statement on the provided connection. -The resulting `mysql.prepared.Prepared` can then be used freely on ANY `Connection`, -as it will automatically be registered upon its first use on other connections. -Or, pass it to `Connection.register` if you prefer eager registration. - -Internally, the result of a successful outcome will be a statement handle - an ID - -for the prepared statement, a count of the parameters required for -execution of the statement, and a count of the columns that will be present -in any result set that the command generates. - -The server will then proceed to send prepared statement headers, -including parameter descriptions, and result set field descriptions, -followed by an EOF packet. - -Throws: `mysql.exceptions.MYX` if the server has a problem. -+/ -Prepared prepare(Connection conn, const(char[]) sql) -{ - auto info = conn.registerIfNeeded(sql); - return Prepared(sql, info.headers, info.numParams); -} - -/++ -This function is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. - -See `BackwardCompatPrepared` for more info. -+/ -deprecated("This is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. You should migrate from this to the Prepared-compatible exec/query overloads in 'mysql.commands'.") -BackwardCompatPrepared prepareBackwardCompat(Connection conn, const(char[]) sql) -{ - return prepareBackwardCompatImpl(conn, sql); -} - -/// Allow mysql-native tests to get around the deprecation message -package BackwardCompatPrepared prepareBackwardCompatImpl(Connection conn, const(char[]) sql) -{ - return BackwardCompatPrepared(conn, prepare(conn, sql)); -} - -/++ -Convenience function to create a prepared statement which calls a stored function. - -Be careful that your `numArgs` is correct. If it isn't, you may get a -`mysql.exceptions.MYX` with a very unclear error message. - -Throws: `mysql.exceptions.MYX` if the server has a problem. - -Params: - conn = The connection. - name = The name of the stored function. - numArgs = The number of arguments the stored procedure takes. -+/ -Prepared prepareFunction(Connection conn, string name, int numArgs) -{ - auto sql = "select " ~ name ~ preparedPlaceholderArgs(numArgs); - return prepare(conn, sql); -} - /++ -Convenience function to create a prepared statement which calls a stored procedure. +This module publicly imports `mysql.unsafe.connection`. Please see that module +for more documentation. -OUT parameters are currently not supported. It should generally be -possible with MySQL to present them as a result set. - -Be careful that your `numArgs` is correct. If it isn't, you may get a -`mysql.exceptions.MYX` with a very unclear error message. - -Throws: `mysql.exceptions.MYX` if the server has a problem. - -Params: - conn = The connection. - name = The name of the stored procedure. - numArgs = The number of arguments the stored procedure takes. +In the future, this will migrate to importing `mysql.safe.connection`. In the +far future, the unsafe version will be deprecated and removed, and the safe +version moved to this location. +$(SAFE_MIGRATION) +/ -Prepared prepareProcedure(Connection conn, string name, int numArgs) -{ - auto sql = "call " ~ name ~ preparedPlaceholderArgs(numArgs); - return prepare(conn, sql); -} - -private string preparedPlaceholderArgs(int numArgs) -{ - auto sql = "("; - bool comma = false; - foreach(i; 0..numArgs) - { - if (comma) - sql ~= ",?"; - else - { - sql ~= "?"; - comma = true; - } - } - sql ~= ")"; - - return sql; -} - -@("preparedPlaceholderArgs") -debug(MYSQLN_TESTS) -unittest -{ - assert(preparedPlaceholderArgs(3) == "(?,?,?)"); - assert(preparedPlaceholderArgs(2) == "(?,?)"); - assert(preparedPlaceholderArgs(1) == "(?)"); - assert(preparedPlaceholderArgs(0) == "()"); -} - -/// Per-connection info from the server about a registered prepared statement. -package struct PreparedServerInfo -{ - /// Server's identifier for this prepared statement. - /// Apperently, this is never 0 if it's been registered, - /// although mysql-native no longer relies on that. - uint statementId; - - ushort psWarnings; - - /// Number of parameters this statement takes. - /// - /// This will be the same on all connections, but it's returned - /// by the server upon registration, so it's stored here. - ushort numParams; - - /// Prepared statement headers - /// - /// This will be the same on all connections, but it's returned - /// by the server upon registration, so it's stored here. - PreparedStmtHeaders headers; - - /// Not actually from the server. Connection uses this to keep track - /// of statements that should be treated as having been released. - bool queuedForRelease = false; -} - -/++ -This is a wrapper over `mysql.prepared.Prepared`, provided ONLY as a -temporary aid in upgrading to mysql-native v2.0.0 and its -new connection-independent model of prepared statements. See the -$(LINK2 https://github.com/mysql-d/mysql-native/blob/master/MIGRATING_TO_V2.md, migration guide) -for more info. - -In most cases, this layer shouldn't even be needed. But if you have many -lines of code making calls to exec/query the same prepared statement, -then this may be helpful. - -To use this temporary compatability layer, change instances of: - ---- -auto stmt = conn.prepare(...); ---- - -to this: - ---- -auto stmt = conn.prepareBackwardCompat(...); ---- - -And then your prepared statement should work as before. - -BUT DO NOT LEAVE IT LIKE THIS! Ultimately, you should update -your prepared statement code to the mysql-native v2.0.0 API, by changing -instances of: - ---- -stmt.exec() -stmt.query() -stmt.queryRow() -stmt.queryRowTuple(outputArgs...) -stmt.queryValue() ---- - -to this: - ---- -conn.exec(stmt) -conn.query(stmt) -conn.queryRow(stmt) -conn.queryRowTuple(stmt, outputArgs...) -conn.queryValue(stmt) ---- - -Both of the above syntaxes can be used with a `BackwardCompatPrepared` -(the `Connection` passed directly to `mysql.commands.exec`/`mysql.commands.query` -will override the one embedded associated with your `BackwardCompatPrepared`). - -Once all of your code is updated, you can change `prepareBackwardCompat` -back to `prepare` again, and your upgrade will be complete. -+/ -struct BackwardCompatPrepared -{ - import std.variant; - - private Connection _conn; - Prepared _prepared; - - /// Access underlying `Prepared` - @property Prepared prepared() { return _prepared; } - - alias _prepared this; - - /++ - This function is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. - - See `BackwardCompatPrepared` for more info. - +/ - deprecated("Change 'preparedStmt.exec()' to 'conn.exec(preparedStmt)'") - ulong exec() - { - return .exec(_conn, _prepared); - } - - ///ditto - deprecated("Change 'preparedStmt.query()' to 'conn.query(preparedStmt)'") - ResultRange query() - { - return .query(_conn, _prepared); - } - - ///ditto - deprecated("Change 'preparedStmt.queryRow()' to 'conn.queryRow(preparedStmt)'") - Nullable!Row queryRow() - { - return .queryRow(_conn, _prepared); - } - - ///ditto - deprecated("Change 'preparedStmt.queryRowTuple(outArgs...)' to 'conn.queryRowTuple(preparedStmt, outArgs...)'") - void queryRowTuple(T...)(ref T args) if(T.length == 0 || !is(T[0] : Connection)) - { - return .queryRowTuple(_conn, _prepared, args); - } - - ///ditto - deprecated("Change 'preparedStmt.queryValue()' to 'conn.queryValue(preparedStmt)'") - Nullable!Variant queryValue() - { - return .queryValue(_conn, _prepared); - } -} - -/++ -A class representing a database connection. - -If you are using Vibe.d, consider using `mysql.pool.MySQLPool` instead of -creating a new Connection directly. That will provide certain benefits, -such as reusing old connections and automatic cleanup (no need to close -the connection when done). - ------------------- -// Suggested usage: - -{ - auto con = new Connection("host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"); - scope(exit) con.close(); - - // Use the connection - ... -} ------------------- -+/ -//TODO: All low-level commms should be moved into the mysql.protocol package. -class Connection -{ -/+ -The Connection is responsible for handshaking with the server to establish -authentication. It then passes client preferences to the server, and -subsequently is the channel for all command packets that are sent, and all -response packets received. - -Uncompressed packets consist of a 4 byte header - 3 bytes of length, and one -byte as a packet number. Connection deals with the headers and ensures that -packet numbers are sequential. - -The initial packet is sent by the server - essentially a 'hello' packet -inviting login. That packet has a sequence number of zero. That sequence -number is the incremented by client and server packets through the handshake -sequence. - -After login all further sequences are initialized by the client sending a -command packet with a zero sequence number, to which the server replies with -zero or more packets with sequential sequence numbers. -+/ -package: - enum OpenState - { - /// We have not yet connected to the server, or have sent QUIT to the - /// server and closed the connection - notConnected, - /// We have connected to the server and parsed the greeting, but not - /// yet authenticated - connected, - /// We have successfully authenticated against the server, and need to - /// send QUIT to the server when closing the connection - authenticated - } - OpenState _open; - MySQLSocket _socket; - - SvrCapFlags _sCaps, _cCaps; - uint _sThread; - ushort _serverStatus; - ubyte _sCharSet, _protocol; - string _serverVersion; - - string _host, _user, _pwd, _db; - ushort _port; - - MySQLSocketType _socketType; - - OpenSocketCallbackPhobos _openSocketPhobos; - OpenSocketCallbackVibeD _openSocketVibeD; - - ulong _insertID; - - // This gets incremented every time a command is issued or results are purged, - // so a ResultRange can tell whether it's been invalidated. - ulong _lastCommandID; - - // Whether there are rows, headers or bimary data waiting to be retreived. - // MySQL protocol doesn't permit performing any other action until all - // such data is read. - bool _rowsPending, _headersPending, _binaryPending; - - // Field count of last performed command. - //TODO: Does Connection need to store this? - ushort _fieldCount; - - // ResultSetHeaders of last performed command. - //TODO: Does Connection need to store this? Is this even used? - ResultSetHeaders _rsh; - - // This tiny thing here is pretty critical. Pay great attention to it's maintenance, otherwise - // you'll get the dreaded "packet out of order" message. It, and the socket connection are - // the reason why most other objects require a connection object for their construction. - ubyte _cpn; /// Packet Number in packet header. Serial number to ensure correct - /// ordering. First packet should have 0 - @property ubyte pktNumber() { return _cpn; } - void bumpPacket() { _cpn++; } - void resetPacket() { _cpn = 0; } - - version(Have_vibe_core) {} else - pure const nothrow invariant() - { - assert(_socketType != MySQLSocketType.vibed); - } - - static PlainPhobosSocket defaultOpenSocketPhobos(string host, ushort port) - { - logDebug("opening phobos socket %s:%d", host, port); - auto s = new PlainPhobosSocket(); - s.connect(new InternetAddress(host, port)); - s.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, true); - s.setOption(SocketOptionLevel.SOCKET, SocketOption.KEEPALIVE, true); - return s; - } - - static PlainVibeDSocket defaultOpenSocketVibeD(string host, ushort port) - { - version(Have_vibe_core) - { - logDebug("opening vibe-d socket %s:%d", host, port); - auto s = vibe.core.net.connectTCP(host, port); - s.tcpNoDelay = true; - s.keepAlive = true; - return s; - } - else - assert(0); - } - - void initConnection() - { - kill(); // Ensure internal state gets reset - - resetPacket(); - final switch(_socketType) - { - case MySQLSocketType.phobos: - _socket = new MySQLSocketPhobos(_openSocketPhobos(_host, _port)); - break; - - case MySQLSocketType.vibed: - version(Have_vibe_core) { - _socket = new MySQLSocketVibeD(_openSocketVibeD(_host, _port)); - break; - } else assert(0, "Unsupported socket type. Need version Have_vibe_core."); - } - } - - SvrCapFlags _clientCapabilities; - - void connect(SvrCapFlags clientCapabilities) - out - { - assert(_open == OpenState.authenticated); - } - do - { - initConnection(); - auto greeting = this.parseGreeting(); - _open = OpenState.connected; - - _clientCapabilities = clientCapabilities; - _cCaps = setClientFlags(_sCaps, clientCapabilities); - this.authenticate(greeting); - } - - /++ - Forcefully close the socket without sending the quit command. - - Also resets internal state regardless of whether the connection is open or not. - - Needed in case an error leaves communatations in an undefined or non-recoverable state. - +/ - void kill() - { - if(_socket && _socket.connected) - _socket.close(); - _open = OpenState.notConnected; - // any pending data is gone. Any statements to release will be released - // on the server automatically. - _headersPending = _rowsPending = _binaryPending = false; - - preparedRegistrations.clear(); - - _lastCommandID++; // Invalidate result sets - } - - // autoPurge is called every time a command is sent, - // so detect & prevent infinite recursion. - private bool isAutoPurging = false; - - - /// Called whenever mysql-native needs to send a command to the server - /// and be sure there aren't any pending results (which would prevent - /// a new command from being sent). - void autoPurge() - { - if(isAutoPurging) - return; - - isAutoPurging = true; - scope(exit) isAutoPurging = false; - - try - { - purgeResult(); - releaseQueued(); - } - catch(Exception e) - { - // Likely the connection was closed, so reset any state (and force-close if needed). - // Don't treat this as a real error, because everything will be reset when we - // reconnect. - kill(); - } - } - - /// Lookup per-connection prepared statement info by SQL - private PreparedRegistrations!PreparedServerInfo preparedRegistrations; - - /// Releases all prepared statements that are queued for release. - void releaseQueued() - { - foreach(sql, info; preparedRegistrations.directLookup) - if(info.queuedForRelease) - { - immediateReleasePrepared(this, info.statementId); - preparedRegistrations.directLookup.remove(sql); - } - } - - /// Returns null if not found - Nullable!PreparedServerInfo getPreparedServerInfo(const(char[]) sql) pure nothrow - { - return preparedRegistrations[sql]; - } - - /// If already registered, simply returns the cached `PreparedServerInfo`. - PreparedServerInfo registerIfNeeded(const(char[]) sql) - { - return preparedRegistrations.registerIfNeeded(sql, sql => performRegister(this, sql)); - } - -public: - - /++ - Construct opened connection. - - Throws `mysql.exceptions.MYX` upon failure to connect. - - If you are using Vibe.d, consider using `mysql.pool.MySQLPool` instead of - creating a new Connection directly. That will provide certain benefits, - such as reusing old connections and automatic cleanup (no need to close - the connection when done). - - ------------------ - // Suggested usage: - - { - auto con = new Connection("host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"); - scope(exit) con.close(); - - // Use the connection - ... - } - ------------------ - - Params: - cs = A connection string of the form "host=localhost;user=user;pwd=password;db=mysqld" - (TODO: The connection string needs work to allow for semicolons in its parts!) - socketType = Whether to use a Phobos or Vibe.d socket. Default is Phobos, - unless compiled with `-version=Have_vibe_core` (set automatically - if using $(LINK2 http://code.dlang.org/getting_started, DUB)). - openSocket = Optional callback which should return a newly-opened Phobos - or Vibe.d TCP socket. This allows custom sockets to be used, - subclassed from Phobos's or Vibe.d's sockets. - host = An IP address in numeric dotted form, or as a host name. - user = The user name to authenticate. - pwd = User's password. - db = Desired initial database. - capFlags = The set of flag bits from the server's capabilities that the client requires - +/ - //After the connection is created, and the initial invitation is received from the server - //client preferences can be set, and authentication can then be attempted. - this(string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) - { - version(Have_vibe_core) - enum defaultSocketType = MySQLSocketType.vibed; - else - enum defaultSocketType = MySQLSocketType.phobos; - - this(defaultSocketType, host, user, pwd, db, port, capFlags); - } - - ///ditto - this(MySQLSocketType socketType, string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) - { - version(Have_vibe_core) {} else - enforce!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_core"); - - this(socketType, &defaultOpenSocketPhobos, &defaultOpenSocketVibeD, - host, user, pwd, db, port, capFlags); - } - - ///ditto - this(OpenSocketCallbackPhobos openSocket, - string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) - { - this(MySQLSocketType.phobos, openSocket, null, host, user, pwd, db, port, capFlags); - } - - version(Have_vibe_core) - ///ditto - this(OpenSocketCallbackVibeD openSocket, - string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) - { - this(MySQLSocketType.vibed, null, openSocket, host, user, pwd, db, port, capFlags); - } - - ///ditto - private this(MySQLSocketType socketType, - OpenSocketCallbackPhobos openSocketPhobos, OpenSocketCallbackVibeD openSocketVibeD, - string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) - in - { - final switch(socketType) - { - case MySQLSocketType.phobos: assert(openSocketPhobos !is null); break; - case MySQLSocketType.vibed: assert(openSocketVibeD !is null); break; - } - } - do - { - enforce!MYX(capFlags & SvrCapFlags.PROTOCOL41, "This client only supports protocol v4.1"); - enforce!MYX(capFlags & SvrCapFlags.SECURE_CONNECTION, "This client only supports protocol v4.1 connection"); - version(Have_vibe_core) {} else - enforce!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_core"); - - _socketType = socketType; - _host = host; - _user = user; - _pwd = pwd; - _db = db; - _port = port; - - _openSocketPhobos = openSocketPhobos; - _openSocketVibeD = openSocketVibeD; - - connect(capFlags); - } - - ///ditto - //After the connection is created, and the initial invitation is received from the server - //client preferences can be set, and authentication can then be attempted. - this(string cs, SvrCapFlags capFlags = defaultClientFlags) - { - string[] a = parseConnectionString(cs); - this(a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); - } - - ///ditto - this(MySQLSocketType socketType, string cs, SvrCapFlags capFlags = defaultClientFlags) - { - string[] a = parseConnectionString(cs); - this(socketType, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); - } - - ///ditto - this(OpenSocketCallbackPhobos openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) - { - string[] a = parseConnectionString(cs); - this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); - } - - version(Have_vibe_core) - ///ditto - this(OpenSocketCallbackVibeD openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) - { - string[] a = parseConnectionString(cs); - this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); - } - - /++ - Check whether this `Connection` is still connected to the server, or if - the connection has been closed. - +/ - @property bool closed() - { - return _open == OpenState.notConnected || !_socket.connected; - } - - /++ - Explicitly close the connection. - - Idiomatic use as follows is suggested: - ------------------ - { - auto con = new Connection("localhost:user:password:mysqld"); - scope(exit) con.close(); - // Use the connection - ... - } - ------------------ - +/ - void close() - { - // This is a two-stage process. First tell the server we are quitting this - // connection, and then close the socket. - - if (_open == OpenState.authenticated && _socket.connected) - quit(); - - if (_open == OpenState.connected) - kill(); - resetPacket(); - } - - /++ - Reconnects to the server using the same connection settings originally - used to create the `Connection`. - - Optionally takes a `mysql.protocol.constants.SvrCapFlags`, allowing you to - reconnect using a different set of server capability flags. - - Normally, if the connection is already open, this will do nothing. However, - if you request a different set of `mysql.protocol.constants.SvrCapFlags` - then was originally used to create the `Connection`, the connection will - be closed and then reconnected using the new `mysql.protocol.constants.SvrCapFlags`. - +/ - void reconnect() - { - reconnect(_clientCapabilities); - } - - ///ditto - void reconnect(SvrCapFlags clientCapabilities) - { - bool sameCaps = clientCapabilities == _clientCapabilities; - if(!closed) - { - // Same caps as before? - if(clientCapabilities == _clientCapabilities) - return; // Nothing to do, just keep current connection - - close(); - } - - connect(clientCapabilities); - } - - private void quit() - in - { - assert(_open == OpenState.authenticated); - } - do - { - this.sendCmd(CommandType.QUIT, []); - // No response is sent for a quit packet - _open = OpenState.connected; - } - - /++ - Parses a connection string of the form - `"host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"` - - Port is optional and defaults to 3306. - - Whitespace surrounding any name or value is automatically stripped. - - Returns a five-element array of strings in this order: - $(UL - $(LI [0]: host) - $(LI [1]: user) - $(LI [2]: pwd) - $(LI [3]: db) - $(LI [4]: port) - ) - - (TODO: The connection string needs work to allow for semicolons in its parts!) - +/ - //TODO: Replace the return value with a proper struct. - static string[] parseConnectionString(string cs) - { - string[] rv; - rv.length = 5; - rv[4] = "3306"; // Default port - string[] a = split(cs, ";"); - foreach (s; a) - { - string[] a2 = split(s, "="); - enforce!MYX(a2.length == 2, "Bad connection string: " ~ cs); - string name = strip(a2[0]); - string val = strip(a2[1]); - switch (name) - { - case "host": - rv[0] = val; - break; - case "user": - rv[1] = val; - break; - case "pwd": - rv[2] = val; - break; - case "db": - rv[3] = val; - break; - case "port": - rv[4] = val; - break; - default: - throw new MYX("Bad connection string: " ~ cs, __FILE__, __LINE__); - } - } - return rv; - } - - /++ - Select a current database. - - Throws `mysql.exceptions.MYX` upon failure. - - Params: dbName = Name of the requested database - +/ - void selectDB(string dbName) - { - this.sendCmd(CommandType.INIT_DB, dbName); - this.getCmdResponse(); - _db = dbName; - } - - /++ - Check the server status. - - Throws `mysql.exceptions.MYX` upon failure. - - Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined - +/ - OKErrorPacket pingServer() - { - this.sendCmd(CommandType.PING, []); - return this.getCmdResponse(); - } - - /++ - Refresh some feature(s) of the server. - - Throws `mysql.exceptions.MYX` upon failure. - - Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined - +/ - OKErrorPacket refreshServer(RefreshFlags flags) - { - this.sendCmd(CommandType.REFRESH, [flags]); - return this.getCmdResponse(); - } - - /++ - Flush any outstanding result set elements. - - When the server responds to a command that produces a result set, it - queues the whole set of corresponding packets over the current connection. - Before that `Connection` can embark on any new command, it must receive - all of those packets and junk them. - - As of v1.1.4, this is done automatically as needed. But you can still - call this manually to force a purge to occur when you want. - - See_Also: $(LINK http://www.mysqlperformanceblog.com/2007/07/08/mysql-net_write_timeout-vs-wait_timeout-and-protocol-notes/) - +/ - ulong purgeResult() - { - return mysql.protocol.comms.purgeResult(this); - } - - /++ - Get a textual report on the server status. - - (COM_STATISTICS) - +/ - string serverStats() - { - return mysql.protocol.comms.serverStats(this); - } - - /++ - Enable multiple statement commands. - - This can be used later if this feature was not requested in the client capability flags. - - Warning: This functionality is currently untested. - - Params: on = Boolean value to turn the capability on or off. - +/ - //TODO: Need to test this - void enableMultiStatements(bool on) - { - mysql.protocol.comms.enableMultiStatements(this, on); - } - - /// Return the in-force protocol number. - @property ubyte protocol() pure const nothrow { return _protocol; } - /// Server version - @property string serverVersion() pure const nothrow { return _serverVersion; } - /// Server capability flags - @property uint serverCapabilities() pure const nothrow { return _sCaps; } - /// Server status - @property ushort serverStatus() pure const nothrow { return _serverStatus; } - /// Current character set - @property ubyte charSet() pure const nothrow { return _sCharSet; } - /// Current database - @property string currentDB() pure const nothrow { return _db; } - /// Socket type being used, Phobos or Vibe.d - @property MySQLSocketType socketType() pure const nothrow { return _socketType; } - - /// After a command that inserted a row into a table with an auto-increment - /// ID column, this method allows you to retrieve the last insert ID. - @property ulong lastInsertID() pure const nothrow { return _insertID; } - - /// This gets incremented every time a command is issued or results are purged, - /// so a `mysql.result.ResultRange` can tell whether it's been invalidated. - @property ulong lastCommandID() pure const nothrow { return _lastCommandID; } - - /// Gets whether rows are pending. - /// - /// Note, you may want `hasPending` instead. - @property bool rowsPending() pure const nothrow { return _rowsPending; } - - /// Gets whether anything (rows, headers or binary) is pending. - /// New commands cannot be sent on a connection while anything is pending - /// (the pending data will automatically be purged.) - @property bool hasPending() pure const nothrow - { - return _rowsPending || _headersPending || _binaryPending; - } - - /// Gets the result header's field descriptions. - @property FieldDescription[] resultFieldDescriptions() pure { return _rsh.fieldDescriptions; } - - /++ - Manually register a prepared statement on this connection. - - Does nothing if statement is already registered on this connection. - - Calling this is not strictly necessary, as the prepared statement will - automatically be registered upon its first use on any `Connection`. - This is provided for those who prefer eager registration over lazy - for performance reasons. - +/ - void register(Prepared prepared) - { - register(prepared.sql); - } - - ///ditto - void register(const(char[]) sql) - { - registerIfNeeded(sql); - } - - /++ - Manually release a prepared statement on this connection. - - This method tells the server that it can dispose of the information it - holds about the current prepared statement. - - Calling this is not strictly necessary. The server considers prepared - statements to be per-connection, so they'll go away when the connection - closes anyway. This is provided in case direct control is actually needed. - - If you choose to use a reference counted struct to call this automatically, - be aware that embedding reference counted structs inside garbage collectible - heap objects is dangerous and should be avoided, as it can lead to various - hidden problems, from crashes to race conditions. (See the discussion at issue - $(LINK2 https://github.com/mysql-d/mysql-native/issues/159, #159) - for details.) Instead, it may be better to simply avoid trying to manage - their release at all, as it's not usually necessary. Or to periodically - release all prepared statements, and simply allow mysql-native to - automatically re-register them upon their next use. - - Notes: - - In actuality, the server might not immediately be told to release the - statement (although `isRegistered` will still report `false`). - - This is because there could be a `mysql.result.ResultRange` with results - still pending for retrieval, and the protocol doesn't allow sending commands - (such as "release a prepared statement") to the server while data is pending. - Therefore, this function may instead queue the statement to be released - when it is safe to do so: Either the next time a result set is purged or - the next time a command (such as `mysql.commands.query` or - `mysql.commands.exec`) is performed (because such commands automatically - purge any pending results). - - This function does NOT auto-purge because, if this is ever called from - automatic resource management cleanup (refcounting, RAII, etc), that - would create ugly situations where hidden, implicit behavior triggers - an unexpected auto-purge. - +/ - void release(Prepared prepared) - { - release(prepared.sql); - } - - ///ditto - void release(const(char[]) sql) - { - //TODO: Don't queue it if nothing is pending. Just do it immediately. - // But need to be certain both situations are unittested. - preparedRegistrations.queueForRelease(sql); - } - - /++ - Manually release all prepared statements on this connection. - - While minimal, every prepared statement registered on a connection does - use up a small amount of resources in both mysql-native and on the server. - Additionally, servers can be configured - $(LINK2 https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_prepared_stmt_count, - to limit the number of prepared statements) - allowed on a connection at one time (the default, however - is quite high). Note also, that certain overloads of `mysql.commands.exec`, - `mysql.commands.query`, etc. register prepared statements behind-the-scenes - which are cached for quick re-use later. - - Therefore, it may occasionally be useful to clear out all prepared - statements on a connection, together with all resources used by them (or - at least leave the resources ready for garbage-collection). This function - does just that. - - Note that this is ALWAYS COMPLETELY SAFE to call, even if you still have - live prepared statements you intend to use again. This is safe because - mysql-native will automatically register or re-register prepared statements - as-needed. - - Notes: - - In actuality, the prepared statements might not be immediately released - (although `isRegistered` will still report `false` for them). - - This is because there could be a `mysql.result.ResultRange` with results - still pending for retrieval, and the protocol doesn't allow sending commands - (such as "release a prepared statement") to the server while data is pending. - Therefore, this function may instead queue the statement to be released - when it is safe to do so: Either the next time a result set is purged or - the next time a command (such as `mysql.commands.query` or - `mysql.commands.exec`) is performed (because such commands automatically - purge any pending results). - - This function does NOT auto-purge because, if this is ever called from - automatic resource management cleanup (refcounting, RAII, etc), that - would create ugly situations where hidden, implicit behavior triggers - an unexpected auto-purge. - +/ - void releaseAll() - { - preparedRegistrations.queueAllForRelease(); - } - - /// Is the given statement registered on this connection as a prepared statement? - bool isRegistered(Prepared prepared) - { - return isRegistered( prepared.sql ); - } - - ///ditto - bool isRegistered(const(char[]) sql) - { - return isRegistered( preparedRegistrations[sql] ); - } +module mysql.connection; - ///ditto - package bool isRegistered(Nullable!PreparedServerInfo info) - { - return !info.isNull && !info.get.queuedForRelease; - } -} +public import mysql.unsafe.connection; diff --git a/source/mysql/escape.d b/source/mysql/escape.d index 7654f5b4..be2eaf96 100644 --- a/source/mysql/escape.d +++ b/source/mysql/escape.d @@ -14,9 +14,9 @@ Simple escape function for dangerous SQL characters Params: input = string to escape - buffer = buffer to use for the output + output = output range to write to +/ -void mysql_escape ( Buffer, Input ) ( Input input, Buffer buffer ) +void mysql_escape ( Output, Input ) ( Input input, Output output ) { import std.string : translate; @@ -30,30 +30,27 @@ void mysql_escape ( Buffer, Input ) ( Input input, Buffer buffer ) '\032' : "\\Z" ]; - translate(input, transTable, null, buffer); + translate(input, transTable, null, output); } /++ -Struct to wrap around a string so it can be passed to formattedWrite and be -properly escaped all using the buffer that formattedWrite provides. +Struct to wrap around an input range so it can be passed to formattedWrite and be +properly escaped without allocating a temporary buffer Params: - Input = (Template Param) Type of the input + Input = (Template Param) Type of the input range + +Note: + The delegate is expected to be @safe as of version 3.2.0. +/ struct MysqlEscape ( Input ) { Input input; - const void toString ( scope void delegate(const(char)[]) sink ) + const void toString ( scope void delegate(scope const(char)[]) @safe sink ) { - struct SinkOutputRange - { - void put ( const(char)[] t ) { sink(t); } - } - - SinkOutputRange r; - mysql_escape(input, r); + mysql_escape(input, sink); } } @@ -61,7 +58,7 @@ struct MysqlEscape ( Input ) Helper function to easily construct a escape wrapper struct Params: - T = (Template Param) Type of the input + T = (Template Param) Type of the input range input = Input to escape +/ MysqlEscape!(T) mysqlEscape ( T ) ( T input ) @@ -71,7 +68,7 @@ MysqlEscape!(T) mysqlEscape ( T ) ( T input ) @("mysqlEscape") debug(MYSQLN_TESTS) -unittest +@safe unittest { import std.array : appender; diff --git a/source/mysql/exceptions.d b/source/mysql/exceptions.d index 115cb560..64ec0bce 100644 --- a/source/mysql/exceptions.d +++ b/source/mysql/exceptions.d @@ -1,4 +1,4 @@ -/// Exceptions defined by mysql-native. +/// Exceptions defined by mysql-native. module mysql.exceptions; import std.algorithm; @@ -9,7 +9,8 @@ An exception type to distinguish exceptions thrown by this package. +/ class MYX: Exception { - this(string msg, string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string msg, string file = __FILE__, size_t line = __LINE__) { super(msg, file, line); } @@ -26,12 +27,14 @@ class MYXReceived: MYX ushort errorCode; char[5] sqlState; - this(OKErrorPacket okp, string file, size_t line) pure +@safe pure: + + this(OKErrorPacket okp, string file, size_t line) { this(okp.message, okp.serverStatus, okp.sqlState, file, line); } - this(string msg, ushort errorCode, char[5] sqlState, string file, size_t line) pure + this(string msg, ushort errorCode, char[5] sqlState, string file, size_t line) { this.errorCode = errorCode; this.sqlState = sqlState; @@ -47,7 +50,8 @@ if you receive this.) +/ class MYXProtocol: MYX { - this(string msg, string file, size_t line) pure +@safe pure: + this(string msg, string file, size_t line) { super(msg, file, line); } @@ -66,7 +70,8 @@ is no longer used. deprecated("No longer thrown by mysql-native. You can safely remove all handling of this exception from your code.") class MYXNotPrepared: MYX { - this(string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string file = __FILE__, size_t line = __LINE__) { super("The prepared statement has already been released.", file, line); } @@ -90,7 +95,8 @@ results in an exception derived from this. +/ class MYXWrongFunction: MYX { - this(string msg, string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string msg, string file = __FILE__, size_t line = __LINE__) { super(msg, file, line); } @@ -105,7 +111,8 @@ that return result sets (such as SELECT), even if the result set has zero elemen +/ class MYXResultRecieved: MYXWrongFunction { - this(string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string file = __FILE__, size_t line = __LINE__) { super( "A result set was returned. Use the query functions, not exec, "~ @@ -124,7 +131,8 @@ for commands that don't produce result sets (such as INSERT). +/ class MYXNoResultRecieved: MYXWrongFunction { - this(string msg, string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string msg, string file = __FILE__, size_t line = __LINE__) { super( "The executed query did not produce a result set. Use the exec "~ @@ -142,7 +150,8 @@ has been issued on the same connection. +/ class MYXInvalidatedRange: MYX { - this(string msg, string file = __FILE__, size_t line = __LINE__) pure +@safe pure: + this(string msg, string file = __FILE__, size_t line = __LINE__) { super(msg, file, line); } diff --git a/source/mysql/impl/connection.d b/source/mysql/impl/connection.d new file mode 100644 index 00000000..f9dc33b6 --- /dev/null +++ b/source/mysql/impl/connection.d @@ -0,0 +1,902 @@ +/++ +Implementation - Connection class. + +WARNING: +This module is used to consolidate the common implementation of the safe and +unafe API. DO NOT directly import this module, please import one of +`mysql.connection`, `mysql.safe.connection`, or `mysql.unsafe.connection`. This +module will be removed in a future version without deprecation. + +$(SAFE_MIGRATION) ++/ +module mysql.impl.connection; + +import std.algorithm; +import std.conv; +import std.exception; +import std.range; +import std.socket; +import std.string; +import std.typecons; + +import mysql.exceptions; +import mysql.logger; +import mysql.protocol.comms; +import mysql.protocol.constants; +import mysql.protocol.packets; +import mysql.protocol.sockets; +import mysql.impl.result; +import mysql.impl.prepared; +import mysql.types; + +@safe: + +version(Have_vibe_core) +{ + static if(__traits(compiles, (){ import vibe.core.net; } )) + import vibe.core.net; + else + static assert(false, "mysql-native can't find Vibe.d's 'vibe.core.net'."); +} + +/// The default `mysql.protocol.constants.SvrCapFlags` used when creating a connection. +immutable SvrCapFlags defaultClientFlags = + SvrCapFlags.OLD_LONG_PASSWORD | SvrCapFlags.ALL_COLUMN_FLAGS | + SvrCapFlags.WITH_DB | SvrCapFlags.PROTOCOL41 | + SvrCapFlags.SECURE_CONNECTION;// | SvrCapFlags.MULTI_STATEMENTS | + //SvrCapFlags.MULTI_RESULTS; + +package(mysql) string preparedPlaceholderArgs(int numArgs) +{ + auto sql = "("; + bool comma = false; + foreach(i; 0..numArgs) + { + if (comma) + sql ~= ",?"; + else + { + sql ~= "?"; + comma = true; + } + } + sql ~= ")"; + + return sql; +} + +@("preparedPlaceholderArgs") +debug(MYSQLN_TESTS) +unittest +{ + assert(preparedPlaceholderArgs(3) == "(?,?,?)"); + assert(preparedPlaceholderArgs(2) == "(?,?)"); + assert(preparedPlaceholderArgs(1) == "(?)"); + assert(preparedPlaceholderArgs(0) == "()"); +} + +/// Per-connection info from the server about a registered prepared statement. +package(mysql) struct PreparedServerInfo +{ + /// Server's identifier for this prepared statement. + /// Apperently, this is never 0 if it's been registered, + /// although mysql-native no longer relies on that. + uint statementId; + + ushort psWarnings; + + /// Number of parameters this statement takes. + /// + /// This will be the same on all connections, but it's returned + /// by the server upon registration, so it's stored here. + ushort numParams; + + /// Prepared statement headers + /// + /// This will be the same on all connections, but it's returned + /// by the server upon registration, so it's stored here. + PreparedStmtHeaders headers; + + /// Not actually from the server. Connection uses this to keep track + /// of statements that should be treated as having been released. + bool queuedForRelease = false; +} + +/++ +A class representing a database connection. + +If you are using Vibe.d, consider using `mysql.pool.MySQLPool` instead of +creating a new Connection directly. That will provide certain benefits, +such as reusing old connections and automatic cleanup (no need to close +the connection when done). + +------------------ +// Suggested usage: + +{ + auto con = new Connection("host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"); + scope(exit) con.close(); + + // Use the connection + ... +} +------------------ ++/ +//TODO: All low-level commms should be moved into the mysql.protocol package. +class Connection +{ + @safe: +/+ +The Connection is responsible for handshaking with the server to establish +authentication. It then passes client preferences to the server, and +subsequently is the channel for all command packets that are sent, and all +response packets received. + +Uncompressed packets consist of a 4 byte header - 3 bytes of length, and one +byte as a packet number. Connection deals with the headers and ensures that +packet numbers are sequential. + +The initial packet is sent by the server - essentially a 'hello' packet +inviting login. That packet has a sequence number of zero. That sequence +number is the incremented by client and server packets through the handshake +sequence. + +After login all further sequences are initialized by the client sending a +command packet with a zero sequence number, to which the server replies with +zero or more packets with sequential sequence numbers. ++/ +package(mysql): + enum OpenState + { + /// We have not yet connected to the server, or have sent QUIT to the + /// server and closed the connection + notConnected, + /// We have connected to the server and parsed the greeting, but not + /// yet authenticated + connected, + /// We have successfully authenticated against the server, and need to + /// send QUIT to the server when closing the connection + authenticated + } + OpenState _open; + MySQLSocket _socket; + + SvrCapFlags _sCaps, _cCaps; + uint _sThread; + ushort _serverStatus; + ubyte _sCharSet, _protocol; + string _serverVersion; + + string _host, _user, _pwd, _db; + ushort _port; + + MySQLSocketType _socketType; + + OpenSocketCallbackPhobos _openSocketPhobos; + OpenSocketCallbackVibeD _openSocketVibeD; + + ulong _insertID; + + // This gets incremented every time a command is issued or results are purged, + // so a ResultRange can tell whether it's been invalidated. + ulong _lastCommandID; + + // Whether there are rows, headers or bimary data waiting to be retreived. + // MySQL protocol doesn't permit performing any other action until all + // such data is read. + bool _rowsPending, _headersPending, _binaryPending; + + // Field count of last performed command. + //TODO: Does Connection need to store this? + ushort _fieldCount; + + // ResultSetHeaders of last performed command. + //TODO: Does Connection need to store this? Is this even used? + ResultSetHeaders _rsh; + + // This tiny thing here is pretty critical. Pay great attention to it's maintenance, otherwise + // you'll get the dreaded "packet out of order" message. It, and the socket connection are + // the reason why most other objects require a connection object for their construction. + ubyte _cpn; /// Packet Number in packet header. Serial number to ensure correct + /// ordering. First packet should have 0 + @property ubyte pktNumber() { return _cpn; } + void bumpPacket() { _cpn++; } + void resetPacket() { _cpn = 0; } + + version(Have_vibe_core) {} else + pure const nothrow invariant() + { + assert(_socketType != MySQLSocketType.vibed); + } + + static PlainPhobosSocket defaultOpenSocketPhobos(string host, ushort port) + { + logDebug("opening phobos socket %s:%d", host, port); + auto s = new PlainPhobosSocket(); + s.connect(new InternetAddress(host, port)); + s.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, true); + s.setOption(SocketOptionLevel.SOCKET, SocketOption.KEEPALIVE, true); + return s; + } + + static PlainVibeDSocket defaultOpenSocketVibeD(string host, ushort port) + { + version(Have_vibe_core) + { + logDebug("opening vibe-d socket %s:%d", host, port); + auto s = vibe.core.net.connectTCP(host, port); + s.tcpNoDelay = true; + s.keepAlive = true; + return s; + } + else + assert(0); + } + + void initConnection() + { + kill(); // Ensure internal state gets reset + + resetPacket(); + final switch(_socketType) + { + case MySQLSocketType.phobos: + _socket = new MySQLSocketPhobos(_openSocketPhobos(_host, _port)); + break; + + case MySQLSocketType.vibed: + version(Have_vibe_core) { + _socket = new MySQLSocketVibeD(_openSocketVibeD(_host, _port)); + break; + } else assert(0, "Unsupported socket type. Need version Have_vibe_core."); + } + } + + SvrCapFlags _clientCapabilities; + + void connect(SvrCapFlags clientCapabilities) + out + { + assert(_open == OpenState.authenticated); + } + do + { + initConnection(); + auto greeting = this.parseGreeting(); + _open = OpenState.connected; + + _clientCapabilities = clientCapabilities; + _cCaps = setClientFlags(_sCaps, clientCapabilities); + this.authenticate(greeting); + } + + /++ + Forcefully close the socket without sending the quit command. + + Also resets internal state regardless of whether the connection is open or not. + + Needed in case an error leaves communatations in an undefined or non-recoverable state. + +/ + void kill() + { + if(_socket && _socket.connected) + _socket.close(); + _open = OpenState.notConnected; + // any pending data is gone. Any statements to release will be released + // on the server automatically. + _headersPending = _rowsPending = _binaryPending = false; + + preparedRegistrations.clear(); + + _lastCommandID++; // Invalidate result sets + } + + // autoPurge is called every time a command is sent, + // so detect & prevent infinite recursion. + private bool isAutoPurging = false; + + + /// Called whenever mysql-native needs to send a command to the server + /// and be sure there aren't any pending results (which would prevent + /// a new command from being sent). + void autoPurge() + { + if(isAutoPurging) + return; + + isAutoPurging = true; + scope(exit) isAutoPurging = false; + + try + { + purgeResult(); + releaseQueued(); + } + catch(Exception e) + { + // Likely the connection was closed, so reset any state (and force-close if needed). + // Don't treat this as a real error, because everything will be reset when we + // reconnect. + kill(); + } + } + + /// Lookup per-connection prepared statement info by SQL + private PreparedRegistrations!PreparedServerInfo preparedRegistrations; + + /// Releases all prepared statements that are queued for release. + void releaseQueued() + { + foreach(sql, info; preparedRegistrations.directLookup) + if(info.queuedForRelease) + { + immediateReleasePrepared(this, info.statementId); + preparedRegistrations.directLookup.remove(sql); + } + } + + /// Returns null if not found + Nullable!PreparedServerInfo getPreparedServerInfo(const(char[]) sql) pure nothrow + { + return preparedRegistrations[sql]; + } + + /// If already registered, simply returns the cached `PreparedServerInfo`. + PreparedServerInfo registerIfNeeded(const(char[]) sql) + { + return preparedRegistrations.registerIfNeeded(sql, sql => performRegister(this, sql)); + } + +public: + + /++ + Construct opened connection. + + Throws `mysql.exceptions.MYX` upon failure to connect. + + If you are using Vibe.d, consider using `mysql.pool.MySQLPool` instead of + creating a new Connection directly. That will provide certain benefits, + such as reusing old connections and automatic cleanup (no need to close + the connection when done). + + ------------------ + // Suggested usage: + + { + auto con = new Connection("host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"); + scope(exit) con.close(); + + // Use the connection + ... + } + ------------------ + + Params: + cs = A connection string of the form "host=localhost;user=user;pwd=password;db=mysqld" + (TODO: The connection string needs work to allow for semicolons in its parts!) + socketType = Whether to use a Phobos or Vibe.d socket. Default is Phobos, + unless compiled with `-version=Have_vibe_core` (set automatically + if using $(LINK2 http://code.dlang.org/getting_started, DUB)). + openSocket = Optional callback which should return a newly-opened Phobos + or Vibe.d TCP socket. This allows custom sockets to be used, + subclassed from Phobos's or Vibe.d's sockets. + host = An IP address in numeric dotted form, or as a host name. + user = The user name to authenticate. + pwd = User's password. + db = Desired initial database. + capFlags = The set of flag bits from the server's capabilities that the client requires + +/ + //After the connection is created, and the initial invitation is received from the server + //client preferences can be set, and authentication can then be attempted. + this(string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) + { + version(Have_vibe_core) + enum defaultSocketType = MySQLSocketType.vibed; + else + enum defaultSocketType = MySQLSocketType.phobos; + + this(defaultSocketType, host, user, pwd, db, port, capFlags); + } + + ///ditto + this(MySQLSocketType socketType, string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) + { + version(Have_vibe_core) {} else + enforce!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_core"); + + this(socketType, &defaultOpenSocketPhobos, &defaultOpenSocketVibeD, + host, user, pwd, db, port, capFlags); + } + + ///ditto + this(OpenSocketCallbackPhobos openSocket, + string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) + { + this(MySQLSocketType.phobos, openSocket, null, host, user, pwd, db, port, capFlags); + } + + version(Have_vibe_core) + ///ditto + this(OpenSocketCallbackVibeD openSocket, + string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) + { + this(MySQLSocketType.vibed, null, openSocket, host, user, pwd, db, port, capFlags); + } + + ///ditto + private this(MySQLSocketType socketType, + OpenSocketCallbackPhobos openSocketPhobos, OpenSocketCallbackVibeD openSocketVibeD, + string host, string user, string pwd, string db, ushort port = 3306, SvrCapFlags capFlags = defaultClientFlags) + in + { + final switch(socketType) + { + case MySQLSocketType.phobos: assert(openSocketPhobos !is null); break; + case MySQLSocketType.vibed: assert(openSocketVibeD !is null); break; + } + } + do + { + enforce!MYX(capFlags & SvrCapFlags.PROTOCOL41, "This client only supports protocol v4.1"); + enforce!MYX(capFlags & SvrCapFlags.SECURE_CONNECTION, "This client only supports protocol v4.1 connection"); + version(Have_vibe_core) {} else + enforce!MYX(socketType != MySQLSocketType.vibed, "Cannot use Vibe.d sockets without -version=Have_vibe_core"); + + _socketType = socketType; + _host = host; + _user = user; + _pwd = pwd; + _db = db; + _port = port; + + _openSocketPhobos = openSocketPhobos; + _openSocketVibeD = openSocketVibeD; + + connect(capFlags); + } + + ///ditto + //After the connection is created, and the initial invitation is received from the server + //client preferences can be set, and authentication can then be attempted. + this(string cs, SvrCapFlags capFlags = defaultClientFlags) + { + string[] a = parseConnectionString(cs); + this(a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); + } + + ///ditto + this(MySQLSocketType socketType, string cs, SvrCapFlags capFlags = defaultClientFlags) + { + string[] a = parseConnectionString(cs); + this(socketType, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); + } + + ///ditto + this(OpenSocketCallbackPhobos openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) + { + string[] a = parseConnectionString(cs); + this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); + } + + version(Have_vibe_core) + ///ditto + this(OpenSocketCallbackVibeD openSocket, string cs, SvrCapFlags capFlags = defaultClientFlags) + { + string[] a = parseConnectionString(cs); + this(openSocket, a[0], a[1], a[2], a[3], to!ushort(a[4]), capFlags); + } + + /++ + Check whether this `Connection` is still connected to the server, or if + the connection has been closed. + +/ + @property bool closed() + { + return _open == OpenState.notConnected || !_socket.connected; + } + + /++ + Explicitly close the connection. + + Idiomatic use as follows is suggested: + ------------------ + { + auto con = new Connection("localhost:user:password:mysqld"); + scope(exit) con.close(); + // Use the connection + ... + } + ------------------ + +/ + void close() + { + // This is a two-stage process. First tell the server we are quitting this + // connection, and then close the socket. + + if (_open == OpenState.authenticated && _socket.connected) + quit(); + + if (_open == OpenState.connected) + kill(); + resetPacket(); + } + + /++ + Reconnects to the server using the same connection settings originally + used to create the `Connection`. + + Optionally takes a `mysql.protocol.constants.SvrCapFlags`, allowing you to + reconnect using a different set of server capability flags. + + Normally, if the connection is already open, this will do nothing. However, + if you request a different set of `mysql.protocol.constants.SvrCapFlags` + then was originally used to create the `Connection`, the connection will + be closed and then reconnected using the new `mysql.protocol.constants.SvrCapFlags`. + +/ + void reconnect() + { + reconnect(_clientCapabilities); + } + + ///ditto + void reconnect(SvrCapFlags clientCapabilities) + { + bool sameCaps = clientCapabilities == _clientCapabilities; + if(!closed) + { + // Same caps as before? + if(clientCapabilities == _clientCapabilities) + return; // Nothing to do, just keep current connection + + close(); + } + + connect(clientCapabilities); + } + + private void quit() + in + { + assert(_open == OpenState.authenticated); + } + do + { + this.sendCmd(CommandType.QUIT, []); + // No response is sent for a quit packet + _open = OpenState.connected; + } + + /++ + Parses a connection string of the form + `"host=localhost;port=3306;user=joe;pwd=pass123;db=myappsdb"` + + Port is optional and defaults to 3306. + + Whitespace surrounding any name or value is automatically stripped. + + Returns a five-element array of strings in this order: + $(UL + $(LI [0]: host) + $(LI [1]: user) + $(LI [2]: pwd) + $(LI [3]: db) + $(LI [4]: port) + ) + + (TODO: The connection string needs work to allow for semicolons in its parts!) + +/ + //TODO: Replace the return value with a proper struct. + static string[] parseConnectionString(string cs) + { + string[] rv; + rv.length = 5; + rv[4] = "3306"; // Default port + string[] a = split(cs, ";"); + foreach (s; a) + { + string[] a2 = split(s, "="); + enforce!MYX(a2.length == 2, "Bad connection string: " ~ cs); + string name = strip(a2[0]); + string val = strip(a2[1]); + switch (name) + { + case "host": + rv[0] = val; + break; + case "user": + rv[1] = val; + break; + case "pwd": + rv[2] = val; + break; + case "db": + rv[3] = val; + break; + case "port": + rv[4] = val; + break; + default: + throw new MYX("Bad connection string: " ~ cs, __FILE__, __LINE__); + } + } + return rv; + } + + /++ + Select a current database. + + Throws `mysql.exceptions.MYX` upon failure. + + Params: dbName = Name of the requested database + +/ + void selectDB(string dbName) + { + this.sendCmd(CommandType.INIT_DB, dbName); + this.getCmdResponse(); + _db = dbName; + } + + /++ + Check the server status. + + Throws `mysql.exceptions.MYX` upon failure. + + Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined + +/ + OKErrorPacket pingServer() + { + this.sendCmd(CommandType.PING, []); + return this.getCmdResponse(); + } + + /++ + Refresh some feature(s) of the server. + + Throws `mysql.exceptions.MYX` upon failure. + + Returns: An `mysql.protocol.packets.OKErrorPacket` from which server status can be determined + +/ + OKErrorPacket refreshServer(RefreshFlags flags) + { + this.sendCmd(CommandType.REFRESH, [flags]); + return this.getCmdResponse(); + } + + /++ + Flush any outstanding result set elements. + + When the server responds to a command that produces a result set, it + queues the whole set of corresponding packets over the current connection. + Before that `Connection` can embark on any new command, it must receive + all of those packets and junk them. + + As of v1.1.4, this is done automatically as needed. But you can still + call this manually to force a purge to occur when you want. + + See_Also: $(LINK http://www.mysqlperformanceblog.com/2007/07/08/mysql-net_write_timeout-vs-wait_timeout-and-protocol-notes/) + +/ + ulong purgeResult() + { + return mysql.protocol.comms.purgeResult(this); + } + + /++ + Get a textual report on the server status. + + (COM_STATISTICS) + +/ + string serverStats() + { + return mysql.protocol.comms.serverStats(this); + } + + /++ + Enable multiple statement commands. + + This can be used later if this feature was not requested in the client capability flags. + + Warning: This functionality is currently untested. + + Params: on = Boolean value to turn the capability on or off. + +/ + //TODO: Need to test this + void enableMultiStatements(bool on) + { + mysql.protocol.comms.enableMultiStatements(this, on); + } + + /// Return the in-force protocol number. + @property ubyte protocol() pure const nothrow { return _protocol; } + /// Server version + @property string serverVersion() pure const nothrow { return _serverVersion; } + /// Server capability flags + @property uint serverCapabilities() pure const nothrow { return _sCaps; } + /// Server status + @property ushort serverStatus() pure const nothrow { return _serverStatus; } + /// Current character set + @property ubyte charSet() pure const nothrow { return _sCharSet; } + /// Current database + @property string currentDB() pure const nothrow { return _db; } + /// Socket type being used, Phobos or Vibe.d + @property MySQLSocketType socketType() pure const nothrow { return _socketType; } + + /// After a command that inserted a row into a table with an auto-increment + /// ID column, this method allows you to retrieve the last insert ID. + @property ulong lastInsertID() pure const nothrow { return _insertID; } + + /// This gets incremented every time a command is issued or results are purged, + /// so a `mysql.result.ResultRange` can tell whether it's been invalidated. + @property ulong lastCommandID() pure const nothrow { return _lastCommandID; } + + /// Gets whether rows are pending. + /// + /// Note, you may want `hasPending` instead. + @property bool rowsPending() pure const nothrow { return _rowsPending; } + + /// Gets whether anything (rows, headers or binary) is pending. + /// New commands cannot be sent on a connection while anything is pending + /// (the pending data will automatically be purged.) + @property bool hasPending() pure const nothrow + { + return _rowsPending || _headersPending || _binaryPending; + } + + /// Gets the result header's field descriptions. + @property FieldDescription[] resultFieldDescriptions() pure { return _rsh.fieldDescriptions; } + + /++ + Manually register a prepared statement on this connection. + + Does nothing if statement is already registered on this connection. + + Calling this is not strictly necessary, as the prepared statement will + automatically be registered upon its first use on any `Connection`. + This is provided for those who prefer eager registration over lazy + for performance reasons. + +/ + void register(SafePrepared prepared) + { + register(prepared.sql); + } + + ///ditto + void register(UnsafePrepared prepared) + { + register(prepared.sql); + } + + ///ditto + void register(const(char[]) sql) + { + registerIfNeeded(sql); + } + + /++ + Manually release a prepared statement on this connection. + + This method tells the server that it can dispose of the information it + holds about the current prepared statement. + + Calling this is not strictly necessary. The server considers prepared + statements to be per-connection, so they'll go away when the connection + closes anyway. This is provided in case direct control is actually needed. + + If you choose to use a reference counted struct to call this automatically, + be aware that embedding reference counted structs inside garbage collectible + heap objects is dangerous and should be avoided, as it can lead to various + hidden problems, from crashes to race conditions. (See the discussion at issue + $(LINK2 https://github.com/mysql-d/mysql-native/issues/159, #159) + for details.) Instead, it may be better to simply avoid trying to manage + their release at all, as it's not usually necessary. Or to periodically + release all prepared statements, and simply allow mysql-native to + automatically re-register them upon their next use. + + Notes: + + In actuality, the server might not immediately be told to release the + statement (although `isRegistered` will still report `false`). + + This is because there could be a `mysql.result.ResultRange` with results + still pending for retrieval, and the protocol doesn't allow sending commands + (such as "release a prepared statement") to the server while data is pending. + Therefore, this function may instead queue the statement to be released + when it is safe to do so: Either the next time a result set is purged or + the next time a command (such as `mysql.commands.query` or + `mysql.commands.exec`) is performed (because such commands automatically + purge any pending results). + + This function does NOT auto-purge because, if this is ever called from + automatic resource management cleanup (refcounting, RAII, etc), that + would create ugly situations where hidden, implicit behavior triggers + an unexpected auto-purge. + +/ + void release(SafePrepared prepared) + { + release(prepared.sql); + } + + ///ditto + void release(UnsafePrepared prepared) + { + release(prepared.sql); + } + + ///ditto + void release(const(char[]) sql) + { + //TODO: Don't queue it if nothing is pending. Just do it immediately. + // But need to be certain both situations are unittested. + preparedRegistrations.queueForRelease(sql); + } + + /++ + Manually release all prepared statements on this connection. + + While minimal, every prepared statement registered on a connection does + use up a small amount of resources in both mysql-native and on the server. + Additionally, servers can be configured + $(LINK2 https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_prepared_stmt_count, + to limit the number of prepared statements) + allowed on a connection at one time (the default, however + is quite high). Note also, that certain overloads of `mysql.commands.exec`, + `mysql.commands.query`, etc. register prepared statements behind-the-scenes + which are cached for quick re-use later. + + Therefore, it may occasionally be useful to clear out all prepared + statements on a connection, together with all resources used by them (or + at least leave the resources ready for garbage-collection). This function + does just that. + + Note that this is ALWAYS COMPLETELY SAFE to call, even if you still have + live prepared statements you intend to use again. This is safe because + mysql-native will automatically register or re-register prepared statements + as-needed. + + Notes: + + In actuality, the prepared statements might not be immediately released + (although `isRegistered` will still report `false` for them). + + This is because there could be a `mysql.result.ResultRange` with results + still pending for retrieval, and the protocol doesn't allow sending commands + (such as "release a prepared statement") to the server while data is pending. + Therefore, this function may instead queue the statement to be released + when it is safe to do so: Either the next time a result set is purged or + the next time a command (such as `mysql.commands.query` or + `mysql.commands.exec`) is performed (because such commands automatically + purge any pending results). + + This function does NOT auto-purge because, if this is ever called from + automatic resource management cleanup (refcounting, RAII, etc), that + would create ugly situations where hidden, implicit behavior triggers + an unexpected auto-purge. + +/ + void releaseAll() + { + preparedRegistrations.queueAllForRelease(); + } + + /// Is the given statement registered on this connection as a prepared statement? + bool isRegistered(SafePrepared prepared) + { + return isRegistered( prepared.sql ); + } + + ///ditto + bool isRegistered(UnsafePrepared prepared) + { + return isRegistered( prepared.sql ); + } + + ///ditto + bool isRegistered(const(char[]) sql) + { + return isRegistered( preparedRegistrations[sql] ); + } + + ///ditto + package bool isRegistered(Nullable!PreparedServerInfo info) + { + return !info.isNull && !info.get.queuedForRelease; + } +} diff --git a/source/mysql/impl/pool.d b/source/mysql/impl/pool.d new file mode 100644 index 00000000..71a5333c --- /dev/null +++ b/source/mysql/impl/pool.d @@ -0,0 +1,482 @@ +/++ +Connect to a MySQL/MariaDB database using a connection pool. + +This provides various benefits over creating a new connection manually, +such as automatically reusing old connections, and automatic cleanup (no need to close +the connection when done). + +Internally, this is based on vibe.d's +$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). +You have to include vibe.d in your project to be able to use this class. +If you don't want to, refer to `mysql.connection.Connection`. + +WARNING: +This module is used to consolidate the common implementation of the safe and +unafe API. DO NOT directly import this module, please import one of +`mysql.pool`, `mysql.safe.pool`, or `mysql.unsafe.pool`. This module will be +removed in a future version without deprecation. + +$(SAFE_MIGRATION) ++/ +module mysql.impl.pool; + +import std.conv; +import std.typecons; +import mysql.impl.connection; +import mysql.impl.prepared; +import mysql.protocol.constants; + +version(Have_vibe_core) +{ + version = IncludeMySQLPool; + static if(is(typeof(ConnectionPool!Connection.init.removeUnused((c){})))) + version = HaveCleanupFunction; +} +version(MySQLDocs) +{ + version = IncludeMySQLPool; + version = HaveCleanupFunction; +} + +version(IncludeMySQLPool) +{ + version(Have_vibe_core) + import vibe.core.connectionpool; + else version(MySQLDocs) + { + /++ + Vibe.d's + $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) + class. + + Not actually included in module `mysql.pool`. Only listed here for + documentation purposes. For ConnectionPool and it's documentation, see: + $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool) + +/ + class ConnectionPool(T) + { + /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.this) + this(Connection delegate() connection_factory, uint max_concurrent = (uint).max) + {} + + /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.lockConnection) + LockedConnection!T lockConnection() { return LockedConnection!T(); } + + /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency) + uint maxConcurrency; + + /// See: $(LINK https://github.com/vibe-d/vibe-core/blob/24a83434e4c788ebb9859dfaecbe60ad0f6e9983/source/vibe/core/connectionpool.d#L113) + void removeUnused(scope void delegate(Connection conn) @safe nothrow disconnect_callback) + {} + } + + /++ + Vibe.d's + $(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection) + struct. + + Not actually included in module `mysql.pool`. Only listed here for + documentation purposes. For LockedConnection and it's documentation, see: + $(LINK http://vibed.org/api/vibe.core.connectionpool/LockedConnection) + +/ + struct LockedConnection(Connection) { Connection c; alias c this; } + } + + /++ + Connect to a MySQL/MariaDB database using a connection pool. + + This provides various benefits over creating a new connection manually, + such as automatically reusing old connections, and automatic cleanup (no need to close + the connection when done). + + Internally, this is based on vibe.d's + $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). + You have to include vibe.d in your project to be able to use this class. + If you don't want to, refer to `mysql.connection.Connection`. + + You should not use this template directly, but rather import + `mysql.safe.pool` or `mysql.unsafe.pool` or `mysql.pool`, which will alias + MySQLPool to the correct instantiation. The boolean parameter here + specifies whether the pool is operating in safe mode or unsafe mode. + +/ + class MySQLPoolImpl(bool isSafe) + { + private + { + string m_host; + string m_user; + string m_password; + string m_database; + ushort m_port; + SvrCapFlags m_capFlags; + static if(isSafe) + alias NewConnectionDelegate = void delegate(Connection) @safe; + else + alias NewConnectionDelegate = void delegate(Connection) @system; + NewConnectionDelegate m_onNewConnection; + ConnectionPool!Connection m_pool; + PreparedRegistrations!PreparedInfo preparedRegistrations; + + struct PreparedInfo + { + bool queuedForRelease = false; + } + + } + + /// Sets up a connection pool with the provided connection settings. + /// + /// The optional `onNewConnection` param allows you to set a callback + /// which will be run every time a new connection is created. + this(string host, string user, string password, string database, + ushort port = 3306, uint maxConcurrent = (uint).max, + SvrCapFlags capFlags = defaultClientFlags, + NewConnectionDelegate onNewConnection = null) + { + m_host = host; + m_user = user; + m_password = password; + m_database = database; + m_port = port; + m_capFlags = capFlags; + m_onNewConnection = onNewConnection; + m_pool = new ConnectionPool!Connection(&createConnection); + } + + ///ditto + this(string host, string user, string password, string database, + ushort port, SvrCapFlags capFlags, NewConnectionDelegate onNewConnection = null) + { + this(host, user, password, database, port, (uint).max, capFlags, onNewConnection); + } + + ///ditto + this(string host, string user, string password, string database, + ushort port, NewConnectionDelegate onNewConnection) + { + this(host, user, password, database, port, (uint).max, defaultClientFlags, onNewConnection); + } + + ///ditto + this(string connStr, uint maxConcurrent = (uint).max, SvrCapFlags capFlags = defaultClientFlags, + NewConnectionDelegate onNewConnection = null) + { + auto parts = Connection.parseConnectionString(connStr); + this(parts[0], parts[1], parts[2], parts[3], to!ushort(parts[4]), capFlags, onNewConnection); + } + + ///ditto + this(string connStr, SvrCapFlags capFlags, NewConnectionDelegate onNewConnection = null) + { + this(connStr, (uint).max, capFlags, onNewConnection); + } + + ///ditto + this(string connStr, NewConnectionDelegate onNewConnection) + { + this(connStr, (uint).max, defaultClientFlags, onNewConnection); + } + + /++ + Obtain a connection. If one isn't available, a new one will be created. + + The connection returned is actually a `LockedConnection!Connection`, + but it uses `alias this`, and so can be used just like a Connection. + (See vibe.d's + $(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection documentation).) + + No other fiber will be given this `mysql.connection.Connection` as long as your fiber still holds it. + + There is no need to close, release or unlock this connection. It is + reference-counted and will automatically be returned to the pool once + your fiber is done with it. + + If you have passed any prepared statements to `autoRegister` + or `autoRelease`, then those statements will automatically be + registered/released on the connection. (Currently, this automatic + register/release may actually occur upon the first command sent via + the connection.) + +/ + static if(isSafe) + LockedConnection!Connection lockConnection() @safe + { + return lockConnectionImpl(); + } + else + LockedConnection!Connection lockConnection() + { + return lockConnectionImpl(); + } + + // the implementation we want to infer attributes + private final lockConnectionImpl() + { + auto conn = m_pool.lockConnection(); + if(conn.closed) + conn.reconnect(); + + applyAuto(conn); + return conn; + } + + /// Applies any `autoRegister`/`autoRelease` settings to a connection, + /// if necessary. + package(mysql) void applyAuto(T)(T conn) + { + foreach(sql, info; preparedRegistrations.directLookup) + { + auto registeredOnPool = !info.queuedForRelease; + auto registeredOnConnection = conn.isRegistered(sql); + + if(registeredOnPool && !registeredOnConnection) // Need to register? + conn.register(sql); + else if(!registeredOnPool && registeredOnConnection) // Need to release? + conn.release(sql); + } + } + + private Connection createConnection() + { + auto conn = new Connection(m_host, m_user, m_password, m_database, m_port, m_capFlags); + + if(m_onNewConnection) + m_onNewConnection(conn); + + return conn; + } + + /// Get/set a callback delegate to be run every time a new connection + /// is created. + @property void onNewConnection(NewConnectionDelegate onNewConnection) @safe + { + m_onNewConnection = onNewConnection; + } + + ///ditto + @property NewConnectionDelegate onNewConnection() @safe + { + return m_onNewConnection; + } + + /++ + Forwards to vibe.d's + $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency, ConnectionPool.maxConcurrency) + +/ + @property uint maxConcurrency() @safe + { + return m_pool.maxConcurrency; + } + + ///ditto + @property void maxConcurrency(uint maxConcurrent) @safe + { + m_pool.maxConcurrency = maxConcurrent; + } + + /++ + Set a prepared statement to be automatically registered on all + connections received from this pool. + + This also clears any `autoRelease` which may have been set for this statement. + + Calling this is not strictly necessary, as a prepared statement will + automatically be registered upon its first use on any `Connection`. + This is provided for those who prefer eager registration over lazy + for performance reasons. + + Once this has been called, obtaining a connection via `lockConnection` + will automatically register the prepared statement on the connection + if it isn't already registered on the connection. This single + registration safely persists after the connection is reclaimed by the + pool and locked again by another Vibe.d task. + + Note, due to the way Vibe.d works, it is not possible to eagerly + register or release a statement on all connections already sitting + in the pool. This can only be done when locking a connection. + + You can stop the pool from continuing to auto-register the statement + by calling either `autoRelease` or `clearAuto`. + +/ + void autoRegister(SafePrepared prepared) @safe + { + autoRegister(prepared.sql); + } + + ///ditto + void autoRegister(UnsafePrepared prepared) @safe + { + autoRegister(prepared.sql); + } + + ///ditto + void autoRegister(const(char[]) sql) @safe + { + preparedRegistrations.registerIfNeeded(sql, (sql) => PreparedInfo()); + } + + /++ + Set a prepared statement to be automatically released from all + connections received from this pool. + + This also clears any `autoRegister` which may have been set for this statement. + + Calling this is not strictly necessary. The server considers prepared + statements to be per-connection, so they'll go away when the connection + closes anyway. This is provided in case direct control is actually needed. + + Once this has been called, obtaining a connection via `lockConnection` + will automatically release the prepared statement from the connection + if it isn't already releases from the connection. + + Note, due to the way Vibe.d works, it is not possible to eagerly + register or release a statement on all connections already sitting + in the pool. This can only be done when locking a connection. + + You can stop the pool from continuing to auto-release the statement + by calling either `autoRegister` or `clearAuto`. + +/ + void autoRelease(SafePrepared prepared) @safe + { + autoRelease(prepared.sql); + } + + ///ditto + void autoRelease(UnsafePrepared prepared) @safe + { + autoRelease(prepared.sql); + } + + ///ditto + void autoRelease(const(char[]) sql) @safe + { + preparedRegistrations.queueForRelease(sql); + } + + /// Is the given statement set to be automatically registered on all + /// connections obtained from this connection pool? + bool isAutoRegistered(SafePrepared prepared) @safe + { + return isAutoRegistered(prepared.sql); + } + ///ditto + bool isAutoRegistered(UnsafePrepared prepared) @safe + { + return isAutoRegistered(prepared.sql); + } + ///ditto + bool isAutoRegistered(const(char[]) sql) @safe + { + return isAutoRegistered(preparedRegistrations[sql]); + } + ///ditto + package bool isAutoRegistered(Nullable!PreparedInfo info) @safe + { + return info.isNull || !info.get.queuedForRelease; + } + + /// Is the given statement set to be automatically released on all + /// connections obtained from this connection pool? + bool isAutoReleased(SafePrepared prepared) @safe + { + return isAutoReleased(prepared.sql); + } + ///ditto + bool isAutoReleased(UnsafePrepared prepared) @safe + { + return isAutoReleased(prepared.sql); + } + ///ditto + bool isAutoReleased(const(char[]) sql) @safe + { + return isAutoReleased(preparedRegistrations[sql]); + } + ///ditto + package bool isAutoReleased(Nullable!PreparedInfo info) @safe + { + return info.isNull || info.get.queuedForRelease; + } + + /++ + Is the given statement set for NEITHER auto-register + NOR auto-release on connections obtained from + this connection pool? + + Equivalent to `!isAutoRegistered && !isAutoReleased`. + +/ + bool isAutoCleared(SafePrepared prepared) @safe + { + return isAutoCleared(prepared.sql); + } + ///ditto + bool isAutoCleared(const(char[]) sql) @safe + { + return isAutoCleared(preparedRegistrations[sql]); + } + ///ditto + package bool isAutoCleared(Nullable!PreparedInfo info) @safe + { + return info.isNull; + } + + /++ + Removes any `autoRegister` or `autoRelease` which may have been set + for this prepared statement. + + Does nothing if the statement has not been set for auto-register or auto-release. + + This releases any relevent memory for potential garbage collection. + +/ + void clearAuto(SafePrepared prepared) @safe + { + return clearAuto(prepared.sql); + } + ///ditto + void clearAuto(UnsafePrepared prepared) @safe + { + return clearAuto(prepared.sql); + } + ///ditto + void clearAuto(const(char[]) sql) @safe + { + preparedRegistrations.directLookup.remove(sql); + } + + /++ + Removes ALL prepared statement `autoRegister` and `autoRelease` which have been set. + + This releases all relevent memory for potential garbage collection. + +/ + void clearAllRegistrations() @safe + { + preparedRegistrations.clear(); + } + + version(MySQLDocs) + { + /++ + Removes all unused connections from the pool. This can + be used to clean up before exiting the program to + ensure the event core driver can be properly shut down. + + Note: this is only available if vibe-core 1.7.0 or later is being + used. + +/ + void removeUnusedConnections() @safe {} + } + else version(HaveCleanupFunction) + { + void removeUnusedConnections() @safe + { + // Note: we squelch all exceptions here, because vibe-core + // requires the function be nothrow, and because an exception + // thrown while closing is probably not important enough to + // interrupt cleanup. + m_pool.removeUnused((conn) @trusted nothrow { + try { + conn.close(); + } catch(Exception) {} + }); + } + } + } +} diff --git a/source/mysql/impl/prepared.d b/source/mysql/impl/prepared.d new file mode 100644 index 00000000..85b5ca64 --- /dev/null +++ b/source/mysql/impl/prepared.d @@ -0,0 +1,576 @@ +/++ +Implementation - Prepared statements. + +WARNING: +This module is used to consolidate the common implementation of the safe and +unafe API. DO NOT directly import this module, please import one of +`mysql.prepared`, `mysql.safe.prepared`, or `mysql.unsafe.prepared`. This +module will be removed in a future version without deprecation. + +$(SAFE_MIGRATION) ++/ +module mysql.impl.prepared; + +import std.exception; +import std.range; +import std.traits; +import std.typecons; +import std.variant; + +import mysql.exceptions; +import mysql.protocol.comms; +import mysql.protocol.constants; +import mysql.protocol.packets; +import mysql.types; +import mysql.impl.result; +import mysql.safe.commands : ColumnSpecialization, CSN; + +/++ +A struct to represent specializations of prepared statement parameters. + +If you need to send large objects to the database it might be convenient to +send them in pieces. The `chunkSize` and `chunkDelegate` variables allow for this. +If both are provided then the corresponding column will be populated by calling the delegate repeatedly. +The source should fill the indicated slice with data and arrange for the delegate to +return the length of the data supplied (in bytes). If that is less than the `chunkSize` +then the chunk will be assumed to be the last one. + +Please use one of the aliases instead of the Impl struct, as this name likely will be removed without deprecation in a future release. ++/ +struct ParameterSpecializationImpl(bool isSafe) +{ + import mysql.protocol.constants; + + size_t pIndex; //parameter number 0 - number of params-1 + SQLType type = SQLType.INFER_FROM_D_TYPE; + uint chunkSize; /// In bytes + static if(isSafe) + uint delegate(ubyte[]) @safe chunkDelegate; + else + uint delegate(ubyte[]) @system chunkDelegate; +} + +/// ditto +alias SafeParameterSpecialization = ParameterSpecializationImpl!true; +/// ditto +alias UnsafeParameterSpecialization = ParameterSpecializationImpl!false; +/// ditto +alias SPSN = SafeParameterSpecialization; +/// ditto +alias UPSN = UnsafeParameterSpecialization; + + +/++ +Encapsulation of a prepared statement. + +Create this via the function `mysql.safe.connection.prepare`. Set your arguments (if any) via +the functions provided, and then run the statement by passing it to +`mysql.safe.commands.exec`/`mysql.safe.commands.query`/etc in place of the sql string parameter. + +Commands that are expected to return a result set - queries - have distinctive +methods that are enforced. That is it will be an error to call such a method +with an SQL command that does not produce a result set. So for commands like +SELECT, use the `mysql.safe.commands.query` functions. For other commands, like +INSERT/UPDATE/CREATE/etc, use `mysql.safe.commands.exec`. ++/ +struct SafePrepared +{ + @safe: +private: + const(char)[] _sql; + +package(mysql): + ushort _numParams; /// Number of parameters this prepared statement takes + PreparedStmtHeaders _headers; + MySQLVal[] _inParams; + SPSN[] _psa; + CSN[] _columnSpecials; + ulong _lastInsertID; + + ExecQueryImplInfo getExecQueryImplInfo(uint statementId) + { + return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa); + } + +public: + /++ + Constructor. You probably want `mysql.safe.connection.prepare` instead of this. + + Call `mysqln.safe.connection.prepare` instead of this, unless you are creating + your own transport bypassing `mysql.impl.connection.Connection` entirely. + The prepared statement must be registered on the server BEFORE this is + called (which `mysqln.safe.connection.prepare` does). + + Internally, the result of a successful outcome will be a statement handle - an ID - + for the prepared statement, a count of the parameters required for + execution of the statement, and a count of the columns that will be present + in any result set that the command generates. + + The server will then proceed to send prepared statement headers, + including parameter descriptions, and result set field descriptions, + followed by an EOF packet. + +/ + this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) + { + this._sql = sql; + this._headers = headers; + this._numParams = numParams; + _inParams.length = numParams; + _psa.length = numParams; + } + + /++ + Prepared statement parameter setter. + + The value may, but doesn't have to be, wrapped in a MySQLVal. If so, + null is handled correctly. + + The value may, but doesn't have to be, a pointer to the desired value. + + The value may, but doesn't have to be, wrapped in a Nullable!T. If so, + null is handled correctly. + + The value can be null. + + Parameter specializations (ie, for chunked transfer) can be added if required. + If you wish to use chunked transfer (via `psn`), note that you must supply + a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: index = The zero based index + +/ + void setArg(T)(size_t index, T val, SafeParameterSpecialization psn = SPSN.init) + if(!isInstanceOf!(Nullable, T) && !is(T == Variant)) + { + // Now in theory we should be able to check the parameter type here, since the + // protocol is supposed to send us type information for the parameters, but this + // capability seems to be broken. This assertion is supported by the fact that + // the same information is not available via the MySQL C API either. It is up + // to the programmer to ensure that appropriate type information is embodied + // in the variant array, or provided explicitly. This sucks, but short of + // having a client side SQL parser I don't see what can be done. + + enforce!MYX(index < _numParams, "Parameter index out of range."); + + _inParams[index] = val; + psn.pIndex = index; + _psa[index] = psn; + } + + ///ditto + void setArg(T)(size_t index, Nullable!T val, SafeParameterSpecialization psn = SPSN.init) + { + if(val.isNull) + setArg(index, null, psn); + else + setArg(index, val.get(), psn); + } + + /++ + Bind a tuple of D variables to the parameters of a prepared statement. + + You can use this method to bind a set of variables if you don't need any specialization, + that is chunked transfer is not neccessary. + + The tuple must match the required number of parameters, and it is the programmer's + responsibility to ensure that they are of appropriate types. + + Type_Mappings: $(TYPE_MAPPINGS) + +/ + void setArgs(T...)(T args) + if(T.length == 0 || (!is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]))) + { + enforce!MYX(args.length == _numParams, "Argument list supplied does not match the number of parameters."); + + foreach (size_t i, arg; args) + setArg(i, arg); + } + + /++ + Bind a MySQLVal[] as the parameters of a prepared statement. + + You can use this method to bind a set of variables in MySQLVal form to + the parameters of a prepared statement. + + Parameter specializations (ie, for chunked transfer) can be added if required. + If you wish to use chunked transfer (via `psn`), note that you must supply + a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. + + This method could be + used to add records from a data entry form along the lines of + ------------ + auto stmt = conn.prepare("INSERT INTO `table42` VALUES(?, ?, ?)"); + DataRecord dr; // Some data input facility + ulong ra; + do + { + dr.get(); + stmt.setArgs(dr("Name"), dr("City"), dr("Whatever")); + ulong rowsAffected = conn.exec(stmt); + } while(!dr.done); + ------------ + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: + args = External list of MySQLVal to be used as parameters + psnList = Any required specializations + +/ + void setArgs(MySQLVal[] args, SafeParameterSpecialization[] psnList=null) + { + enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); + _inParams[] = args[]; + if (psnList !is null) + { + foreach (psn; psnList) + _psa[psn.pIndex] = psn; + } + } + + /++ + Prepared statement parameter getter. + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: index = The zero based index + Returns: The MySQLVal representing the argument. + +/ + MySQLVal getArg(size_t index) + { + enforce!MYX(index < _numParams, "Parameter index out of range."); + return _inParams[index]; + } + + /++ + Sets a prepared statement parameter to NULL. + + This is here mainly for legacy reasons. You can set a field to null + simply by saying `prepared.setArg(index, null);` + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: index = The zero based index + +/ + deprecated("Please use setArg(index, null)") + void setNullArg(size_t index) + { + setArg(index, null); + } + + /// Gets the SQL command for this prepared statement. + const(char)[] sql() pure const + { + return _sql; + } + + /// Gets the number of arguments this prepared statement expects to be passed in. + @property ushort numArgs() pure const nothrow + { + return _numParams; + } + + /// After a command that inserted a row into a table with an auto-increment + /// ID column, this method allows you to retrieve the last insert ID generated + /// from this prepared statement. + @property ulong lastInsertID() pure const nothrow { return _lastInsertID; } + + /// Gets the prepared header's field descriptions. + @property FieldDescription[] preparedFieldDescriptions() pure { return _headers.fieldDescriptions; } + + /// Gets the prepared header's param descriptions. + @property ParamDescription[] preparedParamDescriptions() pure { return _headers.paramDescriptions; } + + /// Get/set the column specializations. + @property ColumnSpecialization[] columnSpecials() pure { return _columnSpecials; } + + ///ditto + @property void columnSpecials(ColumnSpecialization[] csa) pure { _columnSpecials = csa; } +} + +/++ +Unsafe wrapper for SafePrepared. + +This wrapper contains a SafePrepared, and forwards common functionality to that +type. It overrides the setting and fetching of arguments, converting them to +and from Variant for backwards compatibility. + +It also sets up UnsafeParameterSpecialization items for the parameters. Note +that these are simply cast to SafeParameterSpecialization. There are runtime +guards in place to ensure a SafeParameterSpecialization with an unsafe delegate +is not accessible as a safe delegate. + +$(SAFE_MIGRATION) ++/ +struct UnsafePrepared +{ + private SafePrepared _safe; + + private this(SafePrepared sp) @safe + { + _safe = sp; + } + + this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) @safe + { + _safe = SafePrepared(sql, headers, numParams); + } + + /++ + Redefine all functions that deal with MySQLVal to deal with Variant instead. Please see SafePrepared for details on how the methods work. + + $(SAFE_MIGRATION) + +/ + void setArg(T)(size_t index, T val, UnsafeParameterSpecialization psn = UPSN.init) @system + if(!is(T == Variant)) + { + _safe.setArg(index, val, cast(SPSN)psn); + } + + /// ditto + void setArg(size_t index, Variant val, UnsafeParameterSpecialization psn = UPSN.init) @system + { + _safe.setArg(index, _toVal(val), cast(SPSN)psn); + } + + /// ditto + void setArgs(T...)(T args) + if(T.length == 0 || (!is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]))) + { + _safe.setArgs(args); + } + + /// ditto + void setArgs(Variant[] args, UnsafeParameterSpecialization[] psnList=null) @system + { + enforce!MYX(args.length == _safe._numParams, "Param count supplied does not match prepared statement"); + foreach(i, ref arg; args) + _safe.setArg(i, _toVal(arg)); + if (psnList !is null) + { + foreach (psn; psnList) + _safe._psa[psn.pIndex] = cast(SPSN)psn; + } + } + + /// ditto + Variant getArg(size_t index) @system + { + return _safe.getArg(index).asVariant; + } + + /++ + Allow conversion to a SafePrepared. UnsafePrepared with + UnsafeParameterSpecialization items that have chunk delegates are not + allowed to convert, because the delegates are possibly unsafe. + +/ + ref SafePrepared safe() scope return @safe + { + // first, ensure there are no parameter specializations with delegates as + // those are possibly unsafe. + foreach(ref s; _safe._psa) + enforce!MYX(s.chunkDelegate is null, "Cannot convert UnsafePrepared into SafePrepared with unsafe chunk delegates"); + return _safe; + } + + // this package method is to skip the ckeck for parameter specializations + // with chunk delegates. It can only be used when using the safe prepared + // statement for execution. + package(mysql) ref SafePrepared safeForExec() return @system + { + return _safe; + } + + /// forward all the methods from the safe struct. See `SafePrepared` for + /// details. + deprecated("Please use setArg(index, null)") + void setNullArg(size_t index) @safe + { + _safe.setArg(index, null); + } + + @safe pure @property: + + /// ditto + const(char)[] sql() const + { + return _safe.sql; + } + + /// ditto + ushort numArgs() const nothrow + { + return _safe.numArgs; + } + + /// ditto + ulong lastInsertID() const nothrow + { + return _safe.lastInsertID; + } + /// ditto + FieldDescription[] preparedFieldDescriptions() + { + return _safe.preparedFieldDescriptions; + } + + /// ditto + ParamDescription[] preparedParamDescriptions() + { + return _safe.preparedParamDescriptions; + } + + /// ditto + ColumnSpecialization[] columnSpecials() + { + return _safe.columnSpecials; + } + + ///ditto + void columnSpecials(ColumnSpecialization[] csa) + { + _safe.columnSpecials(csa); + } + +} + +/// Allow conversion to UnsafePrepared from SafePrepared. +UnsafePrepared unsafe(SafePrepared p) @safe +{ + return UnsafePrepared(p); +} + +/// Template constraint for `PreparedRegistrations` +private enum isPreparedRegistrationsPayload(Payload) = + __traits(compiles, (){ + static assert(Payload.init.queuedForRelease == false); + Payload p; + p.queuedForRelease = true; + }); + +debug(MYSQLN_TESTS) +{ + // Test template constraint + struct TestPreparedRegistrationsBad1 { } + struct TestPreparedRegistrationsBad2 { bool foo = false; } + struct TestPreparedRegistrationsBad3 { int queuedForRelease = 1; } + struct TestPreparedRegistrationsBad4 { bool queuedForRelease = true; } + struct TestPreparedRegistrationsGood1 { bool queuedForRelease = false; } + struct TestPreparedRegistrationsGood2 { bool queuedForRelease = false; const(char)[] id; } + + static assert(!isPreparedRegistrationsPayload!int); + static assert(!isPreparedRegistrationsPayload!bool); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad1); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad2); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad3); + static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad4); + //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood1); + //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood2); +} + +/++ +Common functionality for recordkeeping of prepared statement registration +and queueing for unregister. + +Used by `Connection` and `MySQLPoolImpl`. + +Templated on payload type. The payload should be an aggregate that includes +the field: `bool queuedForRelease = false;` + +Allowing access to `directLookup` from other parts of mysql-native IS intentional. +`PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just +to factor out common functionality needed by both `Connection` and `MySQLPool`. ++/ +package(mysql) struct PreparedRegistrations(Payload) + if( isPreparedRegistrationsPayload!Payload) +{ + @safe: + /++ + Lookup payload by sql string. + + Allowing access to `directLookup` from other parts of mysql-native IS intentional. + `PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just + to factor out common functionality needed by both `Connection` and `MySQLPool`. + +/ + Payload[const(char[])] directLookup; + + /// Returns null if not found + Nullable!Payload opIndex(const(char[]) sql) pure nothrow + { + Nullable!Payload result; + + auto pInfo = sql in directLookup; + if(pInfo) + result = *pInfo; + + return result; + } + + /// Set `queuedForRelease` flag for a statement in `directLookup`. + /// Does nothing if statement not in `directLookup`. + private void setQueuedForRelease(const(char[]) sql, bool value) + { + if(auto pInfo = sql in directLookup) + { + pInfo.queuedForRelease = value; + directLookup[sql] = *pInfo; + } + } + + /// Queue a prepared statement for release. + void queueForRelease(const(char[]) sql) + { + setQueuedForRelease(sql, true); + } + + /// Remove a statement from the queue to be released. + void unqueueForRelease(const(char[]) sql) + { + setQueuedForRelease(sql, false); + } + + /// Queues all prepared statements for release. + void queueAllForRelease() + { + foreach(sql, info; directLookup) + queueForRelease(sql); + } + + // Note: AA.clear does not invalidate any keys or values. In fact, it + // should really be safe/trusted, but is not. Therefore, we mark this + // as trusted. + /// Eliminate all records of both registered AND queued-for-release statements. + void clear() @trusted + { + static if(__traits(compiles, (){ int[int] aa; aa.clear(); })) + directLookup.clear(); + else + directLookup = null; + } + + /// If already registered, simply returns the cached Payload. + Payload registerIfNeeded(const(char[]) sql, Payload delegate(const(char[])) @safe doRegister) + out(info) + { + // I'm confident this can't currently happen, but + // let's make sure that doesn't change. + assert(!info.queuedForRelease); + } + do + { + if(auto pInfo = sql in directLookup) + { + // The statement is registered. It may, or may not, be queued + // for release. Either way, all we need to do is make sure it's + // un-queued and then return. + pInfo.queuedForRelease = false; + return *pInfo; + } + + auto info = doRegister(sql); + directLookup[sql] = info; + + return info; + } +} + diff --git a/source/mysql/impl/result.d b/source/mysql/impl/result.d new file mode 100644 index 00000000..b0f5b011 --- /dev/null +++ b/source/mysql/impl/result.d @@ -0,0 +1,405 @@ +/++ +Implementation - Structures for data received: rows and result sets (ie, a range of rows). + +WARNING: +This module is used to consolidate the common implementation of the safe and +unafe API. DO NOT directly import this module, please import one of +`mysql.result`, `mysql.safe.result`, or `mysql.unsafe.result`. This module will +be removed in a future version without deprecation. + +$(SAFE_MIGRATION) ++/ +module mysql.impl.result; + +import std.conv; +import std.exception; +import std.range; +import std.string; + +import mysql.exceptions; +import mysql.protocol.comms; +import mysql.protocol.extra_types; +import mysql.protocol.packets; +public import mysql.types; +import std.typecons : Nullable; +import std.variant; + +/++ +A struct to represent a single row of a result set. + +Type_Mappings: $(TYPE_MAPPINGS) ++/ +/+ +The row struct is used for both 'traditional' and 'prepared' result sets. +It consists of parallel arrays of MySQLVal and bool, with the bool array +indicating which of the result set columns are NULL. + +I have been agitating for some kind of null indicator that can be set for a +MySQLVal without destroying its inherent type information. If this were the +case, then the bool array could disappear. ++/ +struct SafeRow +{ + +package(mysql): + MySQLVal[] _values; // Temporarily "package" instead of "private" +private: + import mysql.impl.connection; + bool[] _nulls; + string[] _names; + +public: + @safe: + + /++ + A constructor to extract the column data from a row data packet. + + If the data for the row exceeds the server's maximum packet size, then several packets will be + sent for the row that taken together constitute a logical row data packet. The logic of the data + recovery for a Row attempts to minimize the quantity of data that is bufferred. Users can assist + in this by specifying chunked data transfer in cases where results sets can include long + column values. + + Type_Mappings: $(TYPE_MAPPINGS) + +/ + this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary) + { + ctorRow(con, packet, rh, binary, _values, _nulls, _names); + } + + /++ + Simplify retrieval of a column value by index. + + To check for null, use MySQLVal's `kind` property: + `row[index].kind == MySQLVal.Kind.Null` + or use a direct comparison to null: + `row[index] == null` + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: i = the zero based index of the column whose value is required. + Returns: A MySQLVal holding the column value. + +/ + ref inout(MySQLVal) opIndex(size_t i) inout + { + enforce!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i)); + enforce!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1)); + return _values[i]; + } + + /++ + Get the name of the column with specified index. + +/ + string getName(size_t index) const + { + return _names[index]; + } + + /++ + Check if a column in the result row was NULL + + Params: i = The zero based column index. + +/ + bool isNull(size_t i) const pure nothrow { return _nulls[i]; } + + /++ + Get the number of elements (columns) in this row. + +/ + @property size_t length() const pure nothrow { return _values.length; } + + ///ditto + alias opDollar = length; + + /++ + Move the content of the row into a compatible struct + + This method takes no account of NULL column values. If a column was NULL, + the corresponding MySQLVal value would be unchanged in those cases. + + The method will throw if the type of the MySQLVal is not implicitly + convertible to the corresponding struct member. + + Type_Mappings: $(TYPE_MAPPINGS) + + Params: + S = A struct type. + s = A ref instance of the type + +/ + void toStruct(S)(ref S s) if (is(S == struct)) + { + foreach (i, dummy; s.tupleof) + { + static if(__traits(hasMember, s.tupleof[i], "nullify") && + is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get))) + { + if(!_nulls[i]) + { + enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(), + "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); + s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i].get)); + } + else + s.tupleof[i].nullify(); + } + else + { + if(!_nulls[i]) + { + enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(), + "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); + s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i])); + } + else + s.tupleof[i] = typeof(s.tupleof[i]).init; + } + } + } + + void show() + { + import std.stdio; + + writefln("%(%s, %)", _values); + } +} + +/++ +An UnsafeRow is almost identical to a SafeRow, except that it provides access +to its values via Variant instead of MySQLVal. This makes the access unsafe. +Only value access is unsafe, every other operation is forwarded to the internal +SafeRow. + +Use the safe or unsafe UFCS methods to convert to and from these two types if +needed. + +Note that there is a performance penalty when accessing via a Variant as the MySQLVal must be converted on every access. + +$(SAFE_MIGRATION) ++/ +struct UnsafeRow +{ + SafeRow _safe; + alias _safe this; + /// Converts SafeRow.opIndex result to Variant. + Variant opIndex(size_t idx) { + return _safe[idx].asVariant; + } +} + +/// ditto +UnsafeRow unsafe(SafeRow r) @safe +{ + return UnsafeRow(r); +} + +/// ditto +Nullable!UnsafeRow unsafe(Nullable!SafeRow r) @safe +{ + if(r.isNull) + return Nullable!UnsafeRow(); + return Nullable!UnsafeRow(r.get.unsafe); +} + + +/// ditto +SafeRow safe(UnsafeRow r) @safe +{ + return r._safe; +} + + +/// ditto +Nullable!SafeRow safe(Nullable!UnsafeRow r) @safe +{ + if(r.isNull) + return Nullable!SafeRow(); + return Nullable!SafeRow(r.get.safe); +} + +/++ +An $(LINK2 http://dlang.org/phobos/std_range_primitives.html#isInputRange, input range) +of SafeRow. + +This is returned by the `mysql.safe.commands.query` functions. + +The rows are downloaded one-at-a-time, as you iterate the range. This allows +for low memory usage, and quick access to the results as they are downloaded. +This is especially ideal in case your query results in a large number of rows. + +However, because of that, this `SafeResultRange` cannot offer random access or +a `length` member. If you need random access, then just like any other range, +you can simply convert this range to an array via +$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). + +A `SafeResultRange` becomes invalidated (and thus cannot be used) when the server +is sent another command on the same connection. When an invalidated +`SafeResultRange` is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. +If you need to send the server another command, but still access these results +afterwords, you can save the results for later by converting this range to an +array via +$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). + +Type_Mappings: $(TYPE_MAPPINGS) + +Example: +--- +SafeResultRange oneAtATime = myConnection.query("SELECT * from myTable"); +SafeRow[] allAtOnce = myConnection.query("SELECT * from myTable").array; +--- ++/ +struct SafeResultRange +{ +private: + import mysql.impl.connection; +@safe: + Connection _con; + ResultSetHeaders _rsh; + SafeRow _row; // current row + string[] _colNames; + size_t[string] _colNameIndicies; + ulong _numRowsFetched; + ulong _commandID; // So we can keep track of when this is invalidated + + void ensureValid() const pure + { + enforce!MYXInvalidatedRange(isValid, + "This ResultRange has been invalidated and can no longer be used."); + } + +package(mysql): + this (Connection con, ResultSetHeaders rsh, string[] colNames) + { + _con = con; + _rsh = rsh; + _colNames = colNames; + _commandID = con.lastCommandID; + popFront(); + } + +public: + /++ + Check whether the range can still be used, or has been invalidated. + + A `SafeResultRange` becomes invalidated (and thus cannot be used) when the + server is sent another command on the same connection. When an invalidated + `SafeResultRange` is used, a `mysql.exceptions.MYXInvalidatedRange` is + thrown. If you need to send the server another command, but still access + these results afterwords, you can save the results for later by converting + this range to an array via + $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). + +/ + @property bool isValid() const pure nothrow + { + return _con !is null && _commandID == _con.lastCommandID; + } + + /// Check whether there are any rows left + @property bool empty() const pure nothrow + { + if(!isValid) + return true; + + return !_con._rowsPending; + } + + /++ + Gets the current row + +/ + @property inout(SafeRow) front() pure inout + { + ensureValid(); + enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); + return _row; + } + + /++ + Progresses to the next row of the result set - that will then be 'front' + +/ + void popFront() + { + ensureValid(); + enforce!MYX(!empty, "Attempted 'popFront' when no more rows available"); + _row = _con.getNextRow(); + _numRowsFetched++; + } + + /++ + Get the current row as an associative array by column name + + Type_Mappings: $(TYPE_MAPPINGS) + +/ + MySQLVal[string] asAA() + { + ensureValid(); + enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); + MySQLVal[string] aa; + foreach (size_t i, string s; _colNames) + aa[s] = _row._values[i]; + return aa; + } + + /// Get the names of all the columns + @property const(string)[] colNames() const pure nothrow { return _colNames; } + + /// An AA to lookup a column's index by name + @property const(size_t[string]) colNameIndicies() pure nothrow + { + if(_colNameIndicies is null) + { + foreach(index, name; _colNames) + _colNameIndicies[name] = index; + } + + return _colNameIndicies; + } + + /// Explicitly clean up the MySQL resources and cancel pending results + void close() + out{ assert(!isValid); } + do + { + if(isValid) + _con.purgeResult(); + } + + /++ + Get the number of rows retrieved so far. + + Note that this is not neccessarlly the same as the length of the range. + +/ + @property ulong rowCount() const pure nothrow { return _numRowsFetched; } +} + +/+ +A wrapper of a SafeResultRange which converts each row into an UnsafeRow. + +Use the safe or unsafe UFCS methods to convert to and from these two types if +needed. + +$(SAFE_MIGRATION) ++/ +struct UnsafeResultRange +{ + /// The underlying range is a SafeResultRange. + SafeResultRange safe; + alias safe this; + /// Equivalent to SafeResultRange.front, but wraps as an UnsafeRow. + inout(UnsafeRow) front() inout { return inout(UnsafeRow)(safe.front); } + + /// Equivalent to SafeResultRange.asAA, but converts each value to a Variant + Variant[string] asAA() + { + ensureValid(); + enforce!MYX(!safe.empty, "Attempted 'front' on exhausted result sequence."); + Variant[string] aa; + foreach (size_t i, string s; _colNames) + aa[s] = _row._values[i].asVariant; + return aa; + } +} + +/// Wrap a SafeResultRange as an UnsafeResultRange. +UnsafeResultRange unsafe(SafeResultRange r) @safe +{ + return UnsafeResultRange(r); +} diff --git a/source/mysql/logger.d b/source/mysql/logger.d index f06a2dbd..49d165ce 100644 --- a/source/mysql/logger.d +++ b/source/mysql/logger.d @@ -1,5 +1,7 @@ module mysql.logger; +@safe: + /* The aliased log functions in this module map to equivelant functions in either vibe.core.log or std.experimental.logger. For this reason, only log levels common to both are used. The exception to this is logDebug which is uses trace when diff --git a/source/mysql/metadata.d b/source/mysql/metadata.d index ff298319..166c2fa4 100644 --- a/source/mysql/metadata.d +++ b/source/mysql/metadata.d @@ -1,4 +1,4 @@ -/// Retrieve metadata from a DB. +/// Retrieve metadata from a DB. module mysql.metadata; import std.array; @@ -6,10 +6,13 @@ import std.conv; import std.datetime; import std.exception; -import mysql.commands; +import mysql.safe.commands; import mysql.exceptions; import mysql.protocol.sockets; -import mysql.result; +import mysql.safe.result; +import mysql.types; + +@safe: /// A struct to hold column metadata struct ColumnInfo diff --git a/source/mysql/package.d b/source/mysql/package.d index b0f919c3..7a2a067c 100644 --- a/source/mysql/package.d +++ b/source/mysql/package.d @@ -1,4 +1,4 @@ -/++ +/++ Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native). MySQL_to_D_Type_Mappings: @@ -40,7 +40,7 @@ D_to_MySQL_Type_Mappings: $(TABLE $(TR $(TH D ) $(TH MySQL )) - + $(TR $(TD typeof(null) ) $(TD NULL )) $(TR $(TD bool ) $(TD BIT )) $(TR $(TD (u)byte ) $(TD (UNSIGNED) TINY )) @@ -49,7 +49,7 @@ $(TABLE $(TR $(TD (u)long ) $(TD (UNSIGNED) LONGLONG )) $(TR $(TD float ) $(TD (UNSIGNED) FLOAT )) $(TR $(TD double ) $(TD (UNSIGNED) DOUBLE )) - + $(TR $(TD $(STD_DATETIME_DATE Date) ) $(TD DATE )) $(TR $(TD $(STD_DATETIME_DATE TimeOfDay)) $(TD TIME )) $(TR $(TD $(STD_DATETIME_DATE Time) ) $(TD TIME )) @@ -59,19 +59,18 @@ $(TABLE $(TR $(TD string ) $(TD VARCHAR )) $(TR $(TD char[] ) $(TD VARCHAR )) $(TR $(TD (u)byte[] ) $(TD SIGNED TINYBLOB )) - $(TR $(TD other ) $(TD unsupported (throws) )) + $(TR $(TD other ) $(TD unsupported with Variant (throws) or MySQLVal (compiler error) )) ) +Note: This by default imports the unsafe version of the MySQL API. Please +switch to the safe version (`import mysql.safe`) as this will be the default in +the future. If you would prefer to use the unsafe version, it is advised to use +the import `mysql.unsafe`, as this will be supported for at least one more +major version, albeit deprecated. + +$(SAFE_MIGRATION) +/ module mysql; -public import mysql.commands; -public import mysql.connection; -public import mysql.escape; -public import mysql.exceptions; -public import mysql.metadata; -public import mysql.pool; -public import mysql.prepared; -public import mysql.protocol.constants : SvrCapFlags; -public import mysql.result; -public import mysql.types; +// by default we do the unsafe API. +public import mysql.unsafe; diff --git a/source/mysql/pool.d b/source/mysql/pool.d index 2d89c1af..8cd50845 100644 --- a/source/mysql/pool.d +++ b/source/mysql/pool.d @@ -1,425 +1,12 @@ -/++ -Connect to a MySQL/MariaDB database using a connection pool. +/++ +This module publicly imports `mysql.unsafe.pool`. Please see that module for more documentation. -This provides various benefits over creating a new connection manually, -such as automatically reusing old connections, and automatic cleanup (no need to close -the connection when done). +In the future, this will migrate to importing `mysql.safe.pool`. In the far +future, the unsafe version will be deprecated and removed, and the safe version +moved to this location. -Internally, this is based on vibe.d's -$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). -You have to include vibe.d in your project to be able to use this class. -If you don't want to, refer to `mysql.connection.Connection`. +$(SAFE_MIGRATION) +/ module mysql.pool; -import std.conv; -import std.typecons; -import mysql.connection; -import mysql.prepared; -import mysql.protocol.constants; - -version(Have_vibe_core) -{ - version = IncludeMySQLPool; - static if(is(typeof(ConnectionPool!Connection.init.removeUnused((c){})))) - version = HaveCleanupFunction; -} -version(MySQLDocs) -{ - version = IncludeMySQLPool; -} - -version(IncludeMySQLPool) -{ - version(Have_vibe_core) - import vibe.core.connectionpool; - else version(MySQLDocs) - { - /++ - Vibe.d's - $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) - class. - - Not actually included in module `mysql.pool`. Only listed here for - documentation purposes. For ConnectionPool and it's documentation, see: - $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool) - +/ - class ConnectionPool(T) - { - /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.this) - this(Connection delegate() connection_factory, uint max_concurrent = (uint).max) - {} - - /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.lockConnection) - LockedConnection!T lockConnection() { return LockedConnection!T(); } - - /// See: $(LINK http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency) - uint maxConcurrency; - - /// See: $(LINK https://github.com/vibe-d/vibe-core/blob/24a83434e4c788ebb9859dfaecbe60ad0f6e9983/source/vibe/core/connectionpool.d#L113) - void removeUnused(scope void delegate(Connection conn) @safe nothrow disconnect_callback) - {} - } - - /++ - Vibe.d's - $(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection) - struct. - - Not actually included in module `mysql.pool`. Only listed here for - documentation purposes. For LockedConnection and it's documentation, see: - $(LINK http://vibed.org/api/vibe.core.connectionpool/LockedConnection) - +/ - struct LockedConnection(Connection) { Connection c; alias c this; } - } - - /++ - Connect to a MySQL/MariaDB database using a connection pool. - - This provides various benefits over creating a new connection manually, - such as automatically reusing old connections, and automatic cleanup (no need to close - the connection when done). - - Internally, this is based on vibe.d's - $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool). - You have to include vibe.d in your project to be able to use this class. - If you don't want to, refer to `mysql.connection.Connection`. - +/ - class MySQLPool - { - private - { - string m_host; - string m_user; - string m_password; - string m_database; - ushort m_port; - SvrCapFlags m_capFlags; - void delegate(Connection) m_onNewConnection; - ConnectionPool!Connection m_pool; - PreparedRegistrations!PreparedInfo preparedRegistrations; - - struct PreparedInfo - { - bool queuedForRelease = false; - } - - } - - /// Sets up a connection pool with the provided connection settings. - /// - /// The optional `onNewConnection` param allows you to set a callback - /// which will be run every time a new connection is created. - this(string host, string user, string password, string database, - ushort port = 3306, uint maxConcurrent = (uint).max, - SvrCapFlags capFlags = defaultClientFlags, - void delegate(Connection) onNewConnection = null) - { - m_host = host; - m_user = user; - m_password = password; - m_database = database; - m_port = port; - m_capFlags = capFlags; - m_onNewConnection = onNewConnection; - m_pool = new ConnectionPool!Connection(&createConnection); - } - - ///ditto - this(string host, string user, string password, string database, - ushort port, SvrCapFlags capFlags, void delegate(Connection) onNewConnection = null) - { - this(host, user, password, database, port, (uint).max, capFlags, onNewConnection); - } - - ///ditto - this(string host, string user, string password, string database, - ushort port, void delegate(Connection) onNewConnection) - { - this(host, user, password, database, port, (uint).max, defaultClientFlags, onNewConnection); - } - - ///ditto - this(string connStr, uint maxConcurrent = (uint).max, SvrCapFlags capFlags = defaultClientFlags, - void delegate(Connection) onNewConnection = null) - { - auto parts = Connection.parseConnectionString(connStr); - this(parts[0], parts[1], parts[2], parts[3], to!ushort(parts[4]), capFlags, onNewConnection); - } - - ///ditto - this(string connStr, SvrCapFlags capFlags, void delegate(Connection) onNewConnection = null) - { - this(connStr, (uint).max, capFlags, onNewConnection); - } - - ///ditto - this(string connStr, void delegate(Connection) onNewConnection) - { - this(connStr, (uint).max, defaultClientFlags, onNewConnection); - } - - /++ - Obtain a connection. If one isn't available, a new one will be created. - - The connection returned is actually a `LockedConnection!Connection`, - but it uses `alias this`, and so can be used just like a Connection. - (See vibe.d's - $(LINK2 http://vibed.org/api/vibe.core.connectionpool/LockedConnection, LockedConnection documentation).) - - No other fiber will be given this `mysql.connection.Connection` as long as your fiber still holds it. - - There is no need to close, release or unlock this connection. It is - reference-counted and will automatically be returned to the pool once - your fiber is done with it. - - If you have passed any prepared statements to `autoRegister` - or `autoRelease`, then those statements will automatically be - registered/released on the connection. (Currently, this automatic - register/release may actually occur upon the first command sent via - the connection.) - +/ - LockedConnection!Connection lockConnection() - { - auto conn = m_pool.lockConnection(); - if(conn.closed) - conn.reconnect(); - - applyAuto(conn); - return conn; - } - - /// Applies any `autoRegister`/`autoRelease` settings to a connection, - /// if necessary. - package void applyAuto(T)(T conn) - { - foreach(sql, info; preparedRegistrations.directLookup) - { - auto registeredOnPool = !info.queuedForRelease; - auto registeredOnConnection = conn.isRegistered(sql); - - if(registeredOnPool && !registeredOnConnection) // Need to register? - conn.register(sql); - else if(!registeredOnPool && registeredOnConnection) // Need to release? - conn.release(sql); - } - } - - private Connection createConnection() - { - auto conn = new Connection(m_host, m_user, m_password, m_database, m_port, m_capFlags); - - if(m_onNewConnection) - m_onNewConnection(conn); - - return conn; - } - - /// Get/set a callback delegate to be run every time a new connection - /// is created. - @property void onNewConnection(void delegate(Connection) onNewConnection) - { - m_onNewConnection = onNewConnection; - } - - ///ditto - @property void delegate(Connection) onNewConnection() - { - return m_onNewConnection; - } - - /++ - Forwards to vibe.d's - $(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool.maxConcurrency, ConnectionPool.maxConcurrency) - +/ - @property uint maxConcurrency() - { - return m_pool.maxConcurrency; - } - - ///ditto - @property void maxConcurrency(uint maxConcurrent) - { - m_pool.maxConcurrency = maxConcurrent; - } - - /++ - Set a prepared statement to be automatically registered on all - connections received from this pool. - - This also clears any `autoRelease` which may have been set for this statement. - - Calling this is not strictly necessary, as a prepared statement will - automatically be registered upon its first use on any `Connection`. - This is provided for those who prefer eager registration over lazy - for performance reasons. - - Once this has been called, obtaining a connection via `lockConnection` - will automatically register the prepared statement on the connection - if it isn't already registered on the connection. This single - registration safely persists after the connection is reclaimed by the - pool and locked again by another Vibe.d task. - - Note, due to the way Vibe.d works, it is not possible to eagerly - register or release a statement on all connections already sitting - in the pool. This can only be done when locking a connection. - - You can stop the pool from continuing to auto-register the statement - by calling either `autoRelease` or `clearAuto`. - +/ - void autoRegister(Prepared prepared) - { - autoRegister(prepared.sql); - } - - ///ditto - void autoRegister(const(char[]) sql) - { - preparedRegistrations.registerIfNeeded(sql, (sql) => PreparedInfo()); - } - - /++ - Set a prepared statement to be automatically released from all - connections received from this pool. - - This also clears any `autoRegister` which may have been set for this statement. - - Calling this is not strictly necessary. The server considers prepared - statements to be per-connection, so they'll go away when the connection - closes anyway. This is provided in case direct control is actually needed. - - Once this has been called, obtaining a connection via `lockConnection` - will automatically release the prepared statement from the connection - if it isn't already releases from the connection. - - Note, due to the way Vibe.d works, it is not possible to eagerly - register or release a statement on all connections already sitting - in the pool. This can only be done when locking a connection. - - You can stop the pool from continuing to auto-release the statement - by calling either `autoRegister` or `clearAuto`. - +/ - void autoRelease(Prepared prepared) - { - autoRelease(prepared.sql); - } - - ///ditto - void autoRelease(const(char[]) sql) - { - preparedRegistrations.queueForRelease(sql); - } - - /// Is the given statement set to be automatically registered on all - /// connections obtained from this connection pool? - bool isAutoRegistered(Prepared prepared) - { - return isAutoRegistered(prepared.sql); - } - ///ditto - bool isAutoRegistered(const(char[]) sql) - { - return isAutoRegistered(preparedRegistrations[sql]); - } - ///ditto - package bool isAutoRegistered(Nullable!PreparedInfo info) - { - return info.isNull || !info.get.queuedForRelease; - } - - /// Is the given statement set to be automatically released on all - /// connections obtained from this connection pool? - bool isAutoReleased(Prepared prepared) - { - return isAutoReleased(prepared.sql); - } - ///ditto - bool isAutoReleased(const(char[]) sql) - { - return isAutoReleased(preparedRegistrations[sql]); - } - ///ditto - package bool isAutoReleased(Nullable!PreparedInfo info) - { - return info.isNull || info.get.queuedForRelease; - } - - /++ - Is the given statement set for NEITHER auto-register - NOR auto-release on connections obtained from - this connection pool? - - Equivalent to `!isAutoRegistered && !isAutoReleased`. - +/ - bool isAutoCleared(Prepared prepared) - { - return isAutoCleared(prepared.sql); - } - ///ditto - bool isAutoCleared(const(char[]) sql) - { - return isAutoCleared(preparedRegistrations[sql]); - } - ///ditto - package bool isAutoCleared(Nullable!PreparedInfo info) - { - return info.isNull; - } - - /++ - Removes any `autoRegister` or `autoRelease` which may have been set - for this prepared statement. - - Does nothing if the statement has not been set for auto-register or auto-release. - - This releases any relevent memory for potential garbage collection. - +/ - void clearAuto(Prepared prepared) - { - return clearAuto(prepared.sql); - } - ///ditto - void clearAuto(const(char[]) sql) - { - preparedRegistrations.directLookup.remove(sql); - } - - /++ - Removes ALL prepared statement `autoRegister` and `autoRelease` which have been set. - - This releases all relevent memory for potential garbage collection. - +/ - void clearAllRegistrations() - { - preparedRegistrations.clear(); - } - - version(MySQLDocs) - { - /++ - Removes all unused connections from the pool. This can - be used to clean up before exiting the program to - ensure the event core driver can be properly shut down. - - Note: this is only available if vibe-core 1.7.0 or later is being - used. - +/ - void removeUnusedConnections() @safe {} - } - else version(HaveCleanupFunction) - { - void removeUnusedConnections() @safe - { - // Note: we squelch all exceptions here, because vibe-core - // requires the function be nothrow, and because an exception - // thrown while closing is probably not important enough to - // interrupt cleanup. - m_pool.removeUnused((conn) @trusted nothrow { - try { - conn.close(); - } catch(Exception) {} - }); - } - } - } -} +public import mysql.unsafe.pool; diff --git a/source/mysql/prepared.d b/source/mysql/prepared.d index 139f82bd..7a7b4f14 100644 --- a/source/mysql/prepared.d +++ b/source/mysql/prepared.d @@ -1,393 +1,12 @@ -/// Use a DB via SQL prepared statements. -module mysql.prepared; - -import std.exception; -import std.range; -import std.traits; -import std.typecons; -import std.variant; - -import mysql.commands; -import mysql.exceptions; -import mysql.protocol.comms; -import mysql.protocol.constants; -import mysql.protocol.packets; -import mysql.result; - -/++ -A struct to represent specializations of prepared statement parameters. - -If you need to send large objects to the database it might be convenient to -send them in pieces. The `chunkSize` and `chunkDelegate` variables allow for this. -If both are provided then the corresponding column will be populated by calling the delegate repeatedly. -The source should fill the indicated slice with data and arrange for the delegate to -return the length of the data supplied (in bytes). If that is less than the `chunkSize` -then the chunk will be assumed to be the last one. -+/ -struct ParameterSpecialization -{ - import mysql.protocol.constants; - - size_t pIndex; //parameter number 0 - number of params-1 - SQLType type = SQLType.INFER_FROM_D_TYPE; - uint chunkSize; /// In bytes - uint delegate(ubyte[]) chunkDelegate; -} -///ditto -alias PSN = ParameterSpecialization; - -/++ -Encapsulation of a prepared statement. - -Create this via the function `mysql.connection.prepare`. Set your arguments (if any) via -the functions provided, and then run the statement by passing it to -`mysql.commands.exec`/`mysql.commands.query`/etc in place of the sql string parameter. - -Commands that are expected to return a result set - queries - have distinctive -methods that are enforced. That is it will be an error to call such a method -with an SQL command that does not produce a result set. So for commands like -SELECT, use the `mysql.commands.query` functions. For other commands, like -INSERT/UPDATE/CREATE/etc, use `mysql.commands.exec`. -+/ -struct Prepared -{ -private: - const(char)[] _sql; - -package: - ushort _numParams; /// Number of parameters this prepared statement takes - PreparedStmtHeaders _headers; - Variant[] _inParams; - ParameterSpecialization[] _psa; - ColumnSpecialization[] _columnSpecials; - ulong _lastInsertID; - - ExecQueryImplInfo getExecQueryImplInfo(uint statementId) - { - return ExecQueryImplInfo(true, null, statementId, _headers, _inParams, _psa); - } - -public: - /++ - Constructor. You probably want `mysql.connection.prepare` instead of this. - - Call `mysqln.connection.prepare` instead of this, unless you are creating - your own transport bypassing `mysql.connection.Connection` entirely. - The prepared statement must be registered on the server BEFORE this is - called (which `mysqln.connection.prepare` does). - - Internally, the result of a successful outcome will be a statement handle - an ID - - for the prepared statement, a count of the parameters required for - execution of the statement, and a count of the columns that will be present - in any result set that the command generates. - - The server will then proceed to send prepared statement headers, - including parameter descriptions, and result set field descriptions, - followed by an EOF packet. - +/ - this(const(char[]) sql, PreparedStmtHeaders headers, ushort numParams) - { - this._sql = sql; - this._headers = headers; - this._numParams = numParams; - _inParams.length = numParams; - _psa.length = numParams; - } - - /++ - Prepared statement parameter setter. - - The value may, but doesn't have to be, wrapped in a Variant. If so, - null is handled correctly. - - The value may, but doesn't have to be, a pointer to the desired value. - - The value may, but doesn't have to be, wrapped in a Nullable!T. If so, - null is handled correctly. - - The value can be null. - - Parameter specializations (ie, for chunked transfer) can be added if required. - If you wish to use chunked transfer (via `psn`), note that you must supply - a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. - - Type_Mappings: $(TYPE_MAPPINGS) - - Params: index = The zero based index - +/ - void setArg(T)(size_t index, T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) - if(!isInstanceOf!(Nullable, T)) - { - // Now in theory we should be able to check the parameter type here, since the - // protocol is supposed to send us type information for the parameters, but this - // capability seems to be broken. This assertion is supported by the fact that - // the same information is not available via the MySQL C API either. It is up - // to the programmer to ensure that appropriate type information is embodied - // in the variant array, or provided explicitly. This sucks, but short of - // having a client side SQL parser I don't see what can be done. - - enforce!MYX(index < _numParams, "Parameter index out of range."); - - _inParams[index] = val; - psn.pIndex = index; - _psa[index] = psn; - } - - ///ditto - void setArg(T)(size_t index, Nullable!T val, ParameterSpecialization psn = PSN(0, SQLType.INFER_FROM_D_TYPE, 0, null)) - { - if(val.isNull) - setArg(index, null, psn); - else - setArg(index, val.get(), psn); - } - - /++ - Bind a tuple of D variables to the parameters of a prepared statement. - - You can use this method to bind a set of variables if you don't need any specialization, - that is chunked transfer is not neccessary. - - The tuple must match the required number of parameters, and it is the programmer's - responsibility to ensure that they are of appropriate types. - - Type_Mappings: $(TYPE_MAPPINGS) - +/ - void setArgs(T...)(T args) - if(T.length == 0 || !is(T[0] == Variant[])) - { - enforce!MYX(args.length == _numParams, "Argument list supplied does not match the number of parameters."); - - foreach (size_t i, arg; args) - setArg(i, arg); - } - - /++ - Bind a Variant[] as the parameters of a prepared statement. - - You can use this method to bind a set of variables in Variant form to - the parameters of a prepared statement. - - Parameter specializations (ie, for chunked transfer) can be added if required. - If you wish to use chunked transfer (via `psn`), note that you must supply - a dummy value for `val` that's typed `ubyte[]`. For example: `cast(ubyte[])[]`. - - This method could be - used to add records from a data entry form along the lines of - ------------ - auto stmt = conn.prepare("INSERT INTO `table42` VALUES(?, ?, ?)"); - DataRecord dr; // Some data input facility - ulong ra; - do - { - dr.get(); - stmt.setArgs(dr("Name"), dr("City"), dr("Whatever")); - ulong rowsAffected = conn.exec(stmt); - } while(!dr.done); - ------------ - - Type_Mappings: $(TYPE_MAPPINGS) - - Params: - args = External list of Variants to be used as parameters - psnList = Any required specializations - +/ - void setArgs(Variant[] args, ParameterSpecialization[] psnList=null) - { - enforce!MYX(args.length == _numParams, "Param count supplied does not match prepared statement"); - _inParams[] = args[]; - if (psnList !is null) - { - foreach (PSN psn; psnList) - _psa[psn.pIndex] = psn; - } - } - - /++ - Prepared statement parameter getter. - - Type_Mappings: $(TYPE_MAPPINGS) - - Params: index = The zero based index - +/ - Variant getArg(size_t index) - { - enforce!MYX(index < _numParams, "Parameter index out of range."); - return _inParams[index]; - } - - /++ - Sets a prepared statement parameter to NULL. - - This is here mainly for legacy reasons. You can set a field to null - simply by saying `prepared.setArg(index, null);` - - Type_Mappings: $(TYPE_MAPPINGS) - - Params: index = The zero based index - +/ - void setNullArg(size_t index) - { - setArg(index, null); - } - - /// Gets the SQL command for this prepared statement. - const(char)[] sql() - { - return _sql; - } - - /// Gets the number of arguments this prepared statement expects to be passed in. - @property ushort numArgs() pure const nothrow - { - return _numParams; - } - - /// After a command that inserted a row into a table with an auto-increment - /// ID column, this method allows you to retrieve the last insert ID generated - /// from this prepared statement. - @property ulong lastInsertID() pure const nothrow { return _lastInsertID; } - - /// Gets the prepared header's field descriptions. - @property FieldDescription[] preparedFieldDescriptions() pure { return _headers.fieldDescriptions; } - - /// Gets the prepared header's param descriptions. - @property ParamDescription[] preparedParamDescriptions() pure { return _headers.paramDescriptions; } - - /// Get/set the column specializations. - @property ColumnSpecialization[] columnSpecials() pure { return _columnSpecials; } - - ///ditto - @property void columnSpecials(ColumnSpecialization[] csa) pure { _columnSpecials = csa; } -} - -/// Template constraint for `PreparedRegistrations` -private enum isPreparedRegistrationsPayload(Payload) = - __traits(compiles, (){ - static assert(Payload.init.queuedForRelease == false); - Payload p; - p.queuedForRelease = true; - }); - -debug(MYSQLN_TESTS) -{ - // Test template constraint - struct TestPreparedRegistrationsBad1 { } - struct TestPreparedRegistrationsBad2 { bool foo = false; } - struct TestPreparedRegistrationsBad3 { int queuedForRelease = 1; } - struct TestPreparedRegistrationsBad4 { bool queuedForRelease = true; } - struct TestPreparedRegistrationsGood1 { bool queuedForRelease = false; } - struct TestPreparedRegistrationsGood2 { bool queuedForRelease = false; const(char)[] id; } - - static assert(!isPreparedRegistrationsPayload!int); - static assert(!isPreparedRegistrationsPayload!bool); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad1); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad2); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad3); - static assert(!isPreparedRegistrationsPayload!TestPreparedRegistrationsBad4); - //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood1); - //static assert(isPreparedRegistrationsPayload!TestPreparedRegistrationsGood2); -} - /++ -Common functionality for recordkeeping of prepared statement registration -and queueing for unregister. +This module publicly imports `mysql.unsafe.prepared`. Please see that module for more documentation. -Used by `Connection` and `MySQLPool`. +In the future, this will migrate to importing `mysql.safe.prepared`. In the +far future, the unsafe version will be deprecated and removed, and the safe +version moved to this location. -Templated on payload type. The payload should be an aggregate that includes -the field: `bool queuedForRelease = false;` - -Allowing access to `directLookup` from other parts of mysql-native IS intentional. -`PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just -to factor out common functionality needed by both `Connection` and `MySQLPool`. +$(SAFE_MIGRATION) +/ -package struct PreparedRegistrations(Payload) - if( isPreparedRegistrationsPayload!Payload) -{ - /++ - Lookup payload by sql string. - - Allowing access to `directLookup` from other parts of mysql-native IS intentional. - `PreparedRegistrations` isn't intended as 100% encapsulation, it's mainly just - to factor out common functionality needed by both `Connection` and `MySQLPool`. - +/ - Payload[const(char[])] directLookup; - - /// Returns null if not found - Nullable!Payload opIndex(const(char[]) sql) pure nothrow - { - Nullable!Payload result; - - auto pInfo = sql in directLookup; - if(pInfo) - result = *pInfo; - - return result; - } - - /// Set `queuedForRelease` flag for a statement in `directLookup`. - /// Does nothing if statement not in `directLookup`. - private void setQueuedForRelease(const(char[]) sql, bool value) - { - if(auto pInfo = sql in directLookup) - { - pInfo.queuedForRelease = value; - directLookup[sql] = *pInfo; - } - } - - /// Queue a prepared statement for release. - void queueForRelease(const(char[]) sql) - { - setQueuedForRelease(sql, true); - } - - /// Remove a statement from the queue to be released. - void unqueueForRelease(const(char[]) sql) - { - setQueuedForRelease(sql, false); - } - - /// Queues all prepared statements for release. - void queueAllForRelease() - { - foreach(sql, info; directLookup) - queueForRelease(sql); - } - - /// Eliminate all records of both registered AND queued-for-release statements. - void clear() - { - static if(__traits(compiles, (){ int[int] aa; aa.clear(); })) - directLookup.clear(); - else - directLookup = null; - } - - /// If already registered, simply returns the cached Payload. - Payload registerIfNeeded(const(char[]) sql, Payload delegate(const(char[])) doRegister) - out(info) - { - // I'm confident this can't currently happen, but - // let's make sure that doesn't change. - assert(!info.queuedForRelease); - } - do - { - if(auto pInfo = sql in directLookup) - { - // The statement is registered. It may, or may not, be queued - // for release. Either way, all we need to do is make sure it's - // un-queued and then return. - pInfo.queuedForRelease = false; - return *pInfo; - } - - auto info = doRegister(sql); - directLookup[sql] = info; - - return info; - } -} +module mysql.prepared; +public import mysql.unsafe.prepared; diff --git a/source/mysql/protocol/comms.d b/source/mysql/protocol/comms.d index bf95c334..2f23adb0 100644 --- a/source/mysql/protocol/comms.d +++ b/source/mysql/protocol/comms.d @@ -21,18 +21,17 @@ Next tasks for this sub-package's cleanup: module mysql.protocol.comms; import std.algorithm; -import std.array; import std.conv; import std.digest.sha; import std.exception; import std.range; -import std.variant; import mysql.connection; import mysql.exceptions; import mysql.logger; -import mysql.prepared; +import mysql.safe.prepared; import mysql.result; +import mysql.types; import mysql.protocol.constants; import mysql.protocol.extra_types; @@ -40,22 +39,24 @@ import mysql.protocol.packet_helpers; import mysql.protocol.packets; import mysql.protocol.sockets; +@safe: + /// Low-level comms code relating to prepared statements. package struct ProtocolPrepared { + @safe: import std.conv; import std.datetime; - import std.variant; import mysql.types; - static ubyte[] makeBitmap(in Variant[] inParams) + static ubyte[] makeBitmap(in MySQLVal[] inParams) { size_t bml = (inParams.length+7)/8; ubyte[] bma; bma.length = bml; foreach (i; 0..inParams.length) { - if(inParams[i].type != typeid(typeof(null))) + if(inParams[i].kind != MySQLVal.Kind.Null) continue; size_t bn = i/8; size_t bb = i%8; @@ -82,9 +83,11 @@ package struct ProtocolPrepared return prefix; } - static ubyte[] analyseParams(Variant[] inParams, ParameterSpecialization[] psa, + static ubyte[] analyseParams(MySQLVal[] inParams, ParameterSpecialization[] psa, out ubyte[] vals, out bool longData) { + import taggedalgebraic.taggedalgebraic : get; + size_t pc = inParams.length; ubyte[] types; types.length = pc*2; @@ -110,112 +113,100 @@ package struct ProtocolPrepared enum SIGNED = 0; if (psa[i].chunkSize) longData= true; - if (inParams[i].type == typeid(typeof(null))) + if (inParams[i].kind == MySQLVal.Kind.Null) { types[ct++] = SQLType.NULL; types[ct++] = SIGNED; continue; } - Variant v = inParams[i]; + MySQLVal v = inParams[i]; SQLType ext = psa[i].type; - string ts = v.type.toString(); - bool isRef; - if (ts[$-1] == '*') - { - ts.length = ts.length-1; - isRef= true; - } + auto ts = v.kind; + bool isRef = false; - switch (ts) + // TODO: use v.visit instead for more efficiency and shorter code. + with(MySQLVal.Kind) final switch (ts) { - case "bool": - case "const(bool)": - case "immutable(bool)": - case "shared(immutable(bool))": + case BitRef: + isRef = true; goto case; + case Bit: if (ext == SQLType.INFER_FROM_D_TYPE) types[ct++] = SQLType.BIT; else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; reAlloc(2); - bool bv = isRef? *(v.get!(const(bool*))): v.get!(const(bool)); + bool bv = isRef? *v.get!BitRef : v.get!Bit; vals[vcl++] = 1; vals[vcl++] = bv? 0x31: 0x30; break; - case "byte": - case "const(byte)": - case "immutable(byte)": - case "shared(immutable(byte))": + case ByteRef: + isRef = true; goto case; + case Byte: types[ct++] = SQLType.TINY; types[ct++] = SIGNED; reAlloc(1); - vals[vcl++] = isRef? *(v.get!(const(byte*))): v.get!(const(byte)); + vals[vcl++] = isRef? *v.get!ByteRef : v.get!Byte; break; - case "ubyte": - case "const(ubyte)": - case "immutable(ubyte)": - case "shared(immutable(ubyte))": + case UByteRef: + isRef = true; goto case; + case UByte: types[ct++] = SQLType.TINY; types[ct++] = UNSIGNED; reAlloc(1); - vals[vcl++] = isRef? *(v.get!(const(ubyte*))): v.get!(const(ubyte)); + vals[vcl++] = isRef? *v.get!UByteRef : v.get!UByte; break; - case "short": - case "const(short)": - case "immutable(short)": - case "shared(immutable(short))": + case ShortRef: + isRef = true; goto case; + case Short: types[ct++] = SQLType.SHORT; types[ct++] = SIGNED; reAlloc(2); - short si = isRef? *(v.get!(const(short*))): v.get!(const(short)); + short si = isRef? *v.get!ShortRef : v.get!Short; vals[vcl++] = cast(ubyte) (si & 0xff); vals[vcl++] = cast(ubyte) ((si >> 8) & 0xff); break; - case "ushort": - case "const(ushort)": - case "immutable(ushort)": - case "shared(immutable(ushort))": + case UShortRef: + isRef = true; goto case; + case UShort: types[ct++] = SQLType.SHORT; types[ct++] = UNSIGNED; reAlloc(2); - ushort us = isRef? *(v.get!(const(ushort*))): v.get!(const(ushort)); + ushort us = isRef? *v.get!UShortRef : v.get!UShort; vals[vcl++] = cast(ubyte) (us & 0xff); vals[vcl++] = cast(ubyte) ((us >> 8) & 0xff); break; - case "int": - case "const(int)": - case "immutable(int)": - case "shared(immutable(int))": + case IntRef: + isRef = true; goto case; + case Int: types[ct++] = SQLType.INT; types[ct++] = SIGNED; reAlloc(4); - int ii = isRef? *(v.get!(const(int*))): v.get!(const(int)); + int ii = isRef? *v.get!IntRef : v.get!Int; vals[vcl++] = cast(ubyte) (ii & 0xff); vals[vcl++] = cast(ubyte) ((ii >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ii >> 16) & 0xff); vals[vcl++] = cast(ubyte) ((ii >> 24) & 0xff); break; - case "uint": - case "const(uint)": - case "immutable(uint)": - case "shared(immutable(uint))": + case UIntRef: + isRef = true; goto case; + case UInt: types[ct++] = SQLType.INT; types[ct++] = UNSIGNED; reAlloc(4); - uint ui = isRef? *(v.get!(const(uint*))): v.get!(const(uint)); + uint ui = isRef? *v.get!UIntRef : v.get!UInt; vals[vcl++] = cast(ubyte) (ui & 0xff); vals[vcl++] = cast(ubyte) ((ui >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ui >> 16) & 0xff); vals[vcl++] = cast(ubyte) ((ui >> 24) & 0xff); break; - case "long": - case "const(long)": - case "immutable(long)": - case "shared(immutable(long))": + case LongRef: + isRef = true; goto case; + case Long: types[ct++] = SQLType.LONGLONG; types[ct++] = SIGNED; reAlloc(8); - long li = isRef? *(v.get!(const(long*))): v.get!(const(long)); + long li = isRef? *v.get!LongRef : v.get!Long; vals[vcl++] = cast(ubyte) (li & 0xff); vals[vcl++] = cast(ubyte) ((li >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((li >> 16) & 0xff); @@ -225,14 +216,13 @@ package struct ProtocolPrepared vals[vcl++] = cast(ubyte) ((li >> 48) & 0xff); vals[vcl++] = cast(ubyte) ((li >> 56) & 0xff); break; - case "ulong": - case "const(ulong)": - case "immutable(ulong)": - case "shared(immutable(ulong))": + case ULongRef: + isRef = true; goto case; + case ULong: types[ct++] = SQLType.LONGLONG; types[ct++] = UNSIGNED; reAlloc(8); - ulong ul = isRef? *(v.get!(const(ulong*))): v.get!(const(ulong)); + ulong ul = isRef? *v.get!ULongRef : v.get!ULong; vals[vcl++] = cast(ubyte) (ul & 0xff); vals[vcl++] = cast(ubyte) ((ul >> 8) & 0xff); vals[vcl++] = cast(ubyte) ((ul >> 16) & 0xff); @@ -242,172 +232,124 @@ package struct ProtocolPrepared vals[vcl++] = cast(ubyte) ((ul >> 48) & 0xff); vals[vcl++] = cast(ubyte) ((ul >> 56) & 0xff); break; - case "float": - case "const(float)": - case "immutable(float)": - case "shared(immutable(float))": + case FloatRef: + isRef = true; goto case; + case Float: types[ct++] = SQLType.FLOAT; types[ct++] = SIGNED; reAlloc(4); - float f = isRef? *(v.get!(const(float*))): v.get!(const(float)); - ubyte* ubp = cast(ubyte*) &f; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp; + float[1] f = isRef? *v.get!FloatRef : v.get!Float; + ubyte[] uba = cast(ubyte[]) f[]; + vals[vcl .. vcl + uba.length] = uba[]; + vcl += uba.length; break; - case "double": - case "const(double)": - case "immutable(double)": - case "shared(immutable(double))": + case DoubleRef: + isRef = true; goto case; + case Double: types[ct++] = SQLType.DOUBLE; types[ct++] = SIGNED; reAlloc(8); - double d = isRef? *(v.get!(const(double*))): v.get!(const(double)); - ubyte* ubp = cast(ubyte*) &d; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp++; - vals[vcl++] = *ubp; + double[1] d = isRef? *v.get!DoubleRef : v.get!Double; + ubyte[] uba = cast(ubyte[]) d[]; + vals[vcl .. uba.length] = uba[]; + vcl += uba.length; break; - case "std.datetime.date.Date": - case "const(std.datetime.date.Date)": - case "immutable(std.datetime.date.Date)": - case "shared(immutable(std.datetime.date.Date))": - - case "std.datetime.Date": - case "const(std.datetime.Date)": - case "immutable(std.datetime.Date)": - case "shared(immutable(std.datetime.Date))": + case DateRef: + isRef = true; goto case; + case Date: types[ct++] = SQLType.DATE; types[ct++] = SIGNED; - Date date = isRef? *(v.get!(const(Date*))): v.get!(const(Date)); + auto date = isRef? *v.get!DateRef : v.get!Date; ubyte[] da = pack(date); size_t l = da.length; reAlloc(l); vals[vcl..vcl+l] = da[]; vcl += l; break; - case "std.datetime.TimeOfDay": - case "const(std.datetime.TimeOfDay)": - case "immutable(std.datetime.TimeOfDay)": - case "shared(immutable(std.datetime.TimeOfDay))": - - case "std.datetime.date.TimeOfDay": - case "const(std.datetime.date.TimeOfDay)": - case "immutable(std.datetime.date.TimeOfDay)": - case "shared(immutable(std.datetime.date.TimeOfDay))": - - case "std.datetime.Time": - case "const(std.datetime.Time)": - case "immutable(std.datetime.Time)": - case "shared(immutable(std.datetime.Time))": + case TimeRef: + isRef = true; goto case; + case Time: types[ct++] = SQLType.TIME; types[ct++] = SIGNED; - TimeOfDay time = isRef? *(v.get!(const(TimeOfDay*))): v.get!(const(TimeOfDay)); + auto time = isRef? *v.get!TimeRef : v.get!Time; ubyte[] ta = pack(time); size_t l = ta.length; reAlloc(l); vals[vcl..vcl+l] = ta[]; vcl += l; break; - case "std.datetime.date.DateTime": - case "const(std.datetime.date.DateTime)": - case "immutable(std.datetime.date.DateTime)": - case "shared(immutable(std.datetime.date.DateTime))": - - case "std.datetime.DateTime": - case "const(std.datetime.DateTime)": - case "immutable(std.datetime.DateTime)": - case "shared(immutable(std.datetime.DateTime))": + case DateTimeRef: + isRef = true; goto case; + case DateTime: types[ct++] = SQLType.DATETIME; types[ct++] = SIGNED; - DateTime dt = isRef? *(v.get!(const(DateTime*))): v.get!(const(DateTime)); + auto dt = isRef? *v.get!DateTimeRef : v.get!DateTime; ubyte[] da = pack(dt); size_t l = da.length; reAlloc(l); vals[vcl..vcl+l] = da[]; vcl += l; break; - case "mysql.types.Timestamp": - case "const(mysql.types.Timestamp)": - case "immutable(mysql.types.Timestamp)": - case "shared(immutable(mysql.types.Timestamp))": + case TimestampRef: + isRef = true; goto case; + case Timestamp: types[ct++] = SQLType.TIMESTAMP; types[ct++] = SIGNED; - Timestamp tms = isRef? *(v.get!(const(Timestamp*))): v.get!(const(Timestamp)); - DateTime dt = mysql.protocol.packet_helpers.toDateTime(tms.rep); + auto tms = isRef? *v.get!TimestampRef : v.get!Timestamp; + auto dt = mysql.protocol.packet_helpers.toDateTime(tms.rep); ubyte[] da = pack(dt); size_t l = da.length; reAlloc(l); vals[vcl..vcl+l] = da[]; vcl += l; break; - case "char[]": - case "const(char[])": - case "immutable(char[])": - case "const(char)[]": - case "immutable(char)[]": - case "shared(immutable(char)[])": - case "shared(immutable(char))[]": - case "shared(immutable(char[]))": + case TextRef: + isRef = true; goto case; + case Text: if (ext == SQLType.INFER_FROM_D_TYPE) types[ct++] = SQLType.VARCHAR; else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; - const char[] ca = isRef? *(v.get!(const(char[]*))): v.get!(const(char[])); - ubyte[] packed = packLCS(cast(void[]) ca); + const char[] ca = isRef? *v.get!TextRef : v.get!Text; + ubyte[] packed = packLCS(ca); reAlloc(packed.length); vals[vcl..vcl+packed.length] = packed[]; vcl += packed.length; break; - case "byte[]": - case "const(byte[])": - case "immutable(byte[])": - case "const(byte)[]": - case "immutable(byte)[]": - case "shared(immutable(byte)[])": - case "shared(immutable(byte))[]": - case "shared(immutable(byte[]))": + // TODO: this is the same as the Text case except for the get + // call. These should be combined somehow. + case CTextRef: + isRef = true; goto case; + case CText: if (ext == SQLType.INFER_FROM_D_TYPE) - types[ct++] = SQLType.TINYBLOB; + types[ct++] = SQLType.VARCHAR; else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; - const byte[] ba = isRef? *(v.get!(const(byte[]*))): v.get!(const(byte[])); - ubyte[] packed = packLCS(cast(void[]) ba); + const char[] ca = isRef? *v.get!CTextRef : v.get!CText; + ubyte[] packed = packLCS(ca); reAlloc(packed.length); vals[vcl..vcl+packed.length] = packed[]; vcl += packed.length; break; - case "ubyte[]": - case "const(ubyte[])": - case "immutable(ubyte[])": - case "const(ubyte)[]": - case "immutable(ubyte)[]": - case "shared(immutable(ubyte)[])": - case "shared(immutable(ubyte))[]": - case "shared(immutable(ubyte[]))": + case BlobRef: + isRef = true; goto case; + case Blob: + case CBlob: if (ext == SQLType.INFER_FROM_D_TYPE) types[ct++] = SQLType.TINYBLOB; else types[ct++] = cast(ubyte) ext; types[ct++] = SIGNED; - const ubyte[] uba = isRef? *(v.get!(const(ubyte[]*))): v.get!(const(ubyte[])); - ubyte[] packed = packLCS(cast(void[]) uba); + const ubyte[] uba = isRef? *v.get!BlobRef : (ts == Blob ? v.get!Blob : v.get!CBlob); + ubyte[] packed = packLCS(uba); reAlloc(packed.length); vals[vcl..vcl+packed.length] = packed[]; vcl += packed.length; break; - case "void": + case Null: throw new MYX("Unbound parameter " ~ to!string(i), __FILE__, __LINE__); - default: - throw new MYX("Unsupported parameter type " ~ ts, __FILE__, __LINE__); } } vals.length = vcl; @@ -421,7 +363,7 @@ package struct ProtocolPrepared { if (!psn.chunkSize) continue; uint cs = psn.chunkSize; - uint delegate(ubyte[]) dg = psn.chunkDelegate; + uint delegate(ubyte[]) @safe dg = psn.chunkDelegate; ubyte[] chunk; chunk.length = cs+11; @@ -450,7 +392,7 @@ package struct ProtocolPrepared } static void sendCommand(Connection conn, uint hStmt, PreparedStmtHeaders psh, - Variant[] inParams, ParameterSpecialization[] psa) + MySQLVal[] inParams, ParameterSpecialization[] psa) { conn.autoPurge(); @@ -492,7 +434,7 @@ package(mysql) struct ExecQueryImplInfo // For prepared statements: uint hStmt; PreparedStmtHeaders psh; - Variant[] inParams; + MySQLVal[] inParams; ParameterSpecialization[] psa; } @@ -649,7 +591,7 @@ do // Moved here from `struct Row.this` package(mysql) void ctorRow(Connection conn, ref ubyte[] packet, ResultSetHeaders rh, bool binary, - out Variant[] _values, out bool[] _nulls, out string[] _names) + out MySQLVal[] _values, out bool[] _nulls, out string[] _names) in { assert(rh.fieldCount <= uint.max); @@ -874,7 +816,7 @@ package(mysql) ubyte[] makeToken(string password, ubyte[] authBuf) } /// Get the next `mysql.result.Row` of a pending result set. -package(mysql) Row getNextRow(Connection conn) +package(mysql) SafeRow getNextRow(Connection conn) { scope(failure) conn.kill(); @@ -884,7 +826,7 @@ package(mysql) Row getNextRow(Connection conn) conn._headersPending = false; } ubyte[] packet; - Row rr; + SafeRow rr; packet = conn.getPacket(); if(packet.front == ResultPacketMarker.error) throw new MYXReceived(OKErrorPacket(packet), __FILE__, __LINE__); @@ -895,9 +837,9 @@ package(mysql) Row getNextRow(Connection conn) return rr; } if (conn._binaryPending) - rr = Row(conn, packet, conn._rsh, true); + rr = SafeRow(conn, packet, conn._rsh, true); else - rr = Row(conn, packet, conn._rsh, false); + rr = SafeRow(conn, packet, conn._rsh, false); //rr._valid = true; return rr; } @@ -1105,7 +1047,8 @@ Get a textual report on the server status. package(mysql) string serverStats(Connection conn) { conn.sendCmd(CommandType.STATISTICS, []); - return cast(string) conn.getPacket(); + auto result = conn.getPacket(); + return (() @trusted => cast(string)result)(); } /++ @@ -1137,6 +1080,7 @@ package(mysql) void enableMultiStatements(Connection conn, bool on) private ubyte getDefaultCollation(string serverVersion) { + import std.array : array; // MySQL >= 5.5.3 supports utf8mb4 const v = serverVersion .splitter('.') diff --git a/source/mysql/protocol/extra_types.d b/source/mysql/protocol/extra_types.d index c1cb2910..1125e36a 100644 --- a/source/mysql/protocol/extra_types.d +++ b/source/mysql/protocol/extra_types.d @@ -1,22 +1,22 @@ -/// Internal - Protocol-related data types. +/// Internal - Protocol-related data types. module mysql.protocol.extra_types; import std.exception; import std.variant; -import mysql.commands; import mysql.exceptions; import mysql.protocol.sockets; -import mysql.result; +import mysql.types; struct SQLValue { bool isNull; bool isIncomplete; - Variant _value; + MySQLVal _value; + @safe: // empty template as a template and non-template won't be added to the same overload set - @property inout(Variant) value()() inout + @property inout(MySQLVal) value()() inout { enforce!MYX(!isNull, "SQL value is null"); enforce!MYX(!isIncomplete, "SQL value not complete"); @@ -41,6 +41,7 @@ struct SQLValue /// Length Coded Binary Value struct LCB { + @safe: /// True if the `LCB` contains a null value bool isNull; diff --git a/source/mysql/protocol/packet_helpers.d b/source/mysql/protocol/packet_helpers.d index 178b679a..35f3979a 100644 --- a/source/mysql/protocol/packet_helpers.d +++ b/source/mysql/protocol/packet_helpers.d @@ -1,4 +1,4 @@ -/// Internal - Helper functions for the communication protocol. +/// Internal - Helper functions for the communication protocol. module mysql.protocol.packet_helpers; import std.algorithm; @@ -15,6 +15,8 @@ import mysql.protocol.extra_types; import mysql.protocol.sockets; import mysql.types; +@safe: + /++ Function to extract a time difference from a binary encoded row. @@ -105,7 +107,7 @@ Text representations of a time of day are as in 14:22:02 Params: s = A string representation of the time. Returns: A populated or default initialized std.datetime.TimeOfDay struct. +/ -TimeOfDay toTimeOfDay(string s) +TimeOfDay toTimeOfDay(const(char)[] s) { TimeOfDay tod; tod.hour = parse!int(s); @@ -176,7 +178,7 @@ Text representations of a Date are as in 2011-11-11 Params: s = A string representation of the time difference. Returns: A populated or default initialized `std.datetime.Date` struct. +/ -Date toDate(string s) +Date toDate(const(char)[] s) { int year = parse!(ushort)(s); enforce!MYXProtocol(s.skipOver("-"), `Expected: "-"`); @@ -259,7 +261,7 @@ Text representations of a DateTime are as in 2011-11-11 12:20:02 Params: s = A string representation of the time difference. Returns: A populated or default initialized `std.datetime.DateTime` struct. +/ -DateTime toDateTime(string s) +DateTime toDateTime(const(char)[] s) { int year = parse!(ushort)(s); enforce!MYXProtocol(s.skipOver("-"), `Expected: "-"`); @@ -356,7 +358,8 @@ in } do { - return cast(string)packet.consume(N); + auto result = packet.consume(N); + return (() @trusted => cast(string)result)(); } /// Returns N number of bytes from the packet and advances the array @@ -529,7 +532,7 @@ do } -T myto(T)(string value) +T myto(T)(const(char)[] value) { static if(is(T == DateTime)) return toDateTime(value); @@ -541,7 +544,7 @@ T myto(T)(string value) return to!T(value); } -T decode(T, ubyte N=T.sizeof)(in ubyte[] packet) pure nothrow +T decode(T, ubyte N=T.sizeof)(in ubyte[] packet) pure nothrow @trusted if(isFloatingPoint!T) in { @@ -612,7 +615,7 @@ SQLValue consumeNonBinaryValueIfComplete(T)(ref ubyte[] packet, bool unsigned) // and convert the data packet.skip(lcb.totalBytes); assert(packet.length >= lcb.value); - auto value = cast(string) packet.consume(cast(size_t)lcb.value); + auto value = cast(char[]) packet.consume(cast(size_t)lcb.value); if(!result.isNull) { @@ -627,22 +630,7 @@ SQLValue consumeNonBinaryValueIfComplete(T)(ref ubyte[] packet, bool unsigned) } else { - static if(isArray!T) - { - // to!() crashes when trying to convert empty strings - // to arrays, so we have this hack to just store any - // empty array in those cases - if(!value.length) - result.value = T.init; - else - result.value = cast(T)value.dup; - - } - else - { - // TODO: DateTime values etc might be incomplete! - result.value = myto!T(value); - } + result.value = myto!T(value); } } } @@ -724,7 +712,7 @@ SQLValue consumeIfComplete()(ref ubyte[] packet, SQLType sqlType, bool binary, b if(charSet == 0x3F) // CharacterSet == binary result.value = data; // BLOB-ish else - result.value = cast(string)data; // TEXT-ish + result.value = (() @trusted => cast(string)data)(); // TEXT-ish } // Type BIT is treated as a length coded binary (like a BLOB or VARCHAR), @@ -996,12 +984,12 @@ do return t; } -ubyte[] packLCS(void[] a) pure nothrow +ubyte[] packLCS(const(void)[] a) pure nothrow { size_t offset; ubyte[] t = packLength(a.length, offset); if (t[0]) - t[offset..$] = (cast(ubyte[]) a)[0..$]; + t[offset..$] = (cast(const(ubyte)[]) a)[0..$]; return t; } diff --git a/source/mysql/protocol/packets.d b/source/mysql/protocol/packets.d index 4cefd48d..630315b0 100644 --- a/source/mysql/protocol/packets.d +++ b/source/mysql/protocol/packets.d @@ -1,11 +1,11 @@ -/// Internal - Tools for working with MySQL's communications packets. +/// Internal - Tools for working with MySQL's communications packets. module mysql.protocol.packets; import std.exception; import std.range; import std.string; -import mysql.commands : ColumnSpecialization, CSN; +import mysql.safe.commands : ColumnSpecialization, CSN; import mysql.exceptions; import mysql.protocol.comms; import mysql.protocol.constants; @@ -13,6 +13,8 @@ import mysql.protocol.extra_types; import mysql.protocol.sockets; public import mysql.protocol.packet_helpers; +@safe: + void enforcePacketOK(string file = __FILE__, size_t line = __LINE__)(OKErrorPacket okp) { enforce(!okp.error, new MYXReceived(okp, file, line)); @@ -27,6 +29,7 @@ See_Also: $(LINK http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protoc +/ struct OKErrorPacket { + @safe: bool error; ulong affected; ulong insertID; @@ -93,6 +96,7 @@ See_Also: $(LINK http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protoc +/ struct FieldDescription { + @safe: private: string _db; string _table; @@ -111,7 +115,7 @@ private: public: /++ Construct a `FieldDescription` from the raw data packet - + Params: packet = The packet contents excluding the 4 byte packet header +/ @@ -217,6 +221,7 @@ packets is sent, but they contain no useful information and are all the same. +/ struct ParamDescription { + @safe: private: ushort _type; FieldFlags _flags; @@ -264,6 +269,7 @@ See_Also: $(LINK http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protoc +/ struct EOFPacket { + @safe: private: ushort _warnings; ushort _serverStatus; @@ -272,7 +278,7 @@ public: /++ Construct an `EOFPacket` struct from the raw data packet - + Params: packet = The packet contents excluding the 4 byte packet header +/ @@ -310,6 +316,7 @@ before the row data packets can be read. +/ struct ResultSetHeaders { + @safe: import mysql.connection; private: @@ -322,7 +329,7 @@ public: /++ Construct a `ResultSetHeaders` struct from a sequence of `FieldDescription` packets and an EOF packet. - + Params: con = A `mysql.connection.Connection` via which the packets are read fieldCount = the number of fields/columns generated by the query @@ -351,7 +358,7 @@ public: /++ Add specialization information to one or more field descriptions. - + Currently, no specializations are implemented yet. Params: @@ -399,8 +406,9 @@ As noted in `ParamDescription` description, parameter descriptions are not fully +/ struct PreparedStmtHeaders { + @safe: import mysql.connection; - + package: Connection _con; ushort _colCount, _paramCount; diff --git a/source/mysql/protocol/sockets.d b/source/mysql/protocol/sockets.d index cb065168..a9b2468d 100644 --- a/source/mysql/protocol/sockets.d +++ b/source/mysql/protocol/sockets.d @@ -1,4 +1,4 @@ -/// Internal - Phobos and vibe.d sockets. +/// Internal - Phobos and vibe.d sockets. module mysql.protocol.sockets; import std.exception; @@ -27,8 +27,8 @@ else alias PlainVibeDSocket = Object; ///ditto } -alias OpenSocketCallbackPhobos = PlainPhobosSocket function(string,ushort); -alias OpenSocketCallbackVibeD = PlainVibeDSocket function(string,ushort); +alias OpenSocketCallbackPhobos = PlainPhobosSocket function(string,ushort) @safe; +alias OpenSocketCallbackVibeD = PlainVibeDSocket function(string,ushort) @safe; enum MySQLSocketType { phobos, vibed } @@ -36,6 +36,7 @@ enum MySQLSocketType { phobos, vibed } /// Used to wrap both Phobos and Vibe.d sockets with a common interface. interface MySQLSocket { +@safe: void close(); @property bool connected() const; void read(ubyte[] dst); @@ -50,6 +51,7 @@ interface MySQLSocket /// Wraps a Phobos socket with the common interface class MySQLSocketPhobos : MySQLSocket { +@safe: private PlainPhobosSocket socket; /// The socket should already be open @@ -106,6 +108,7 @@ version(Have_vibe_core) { /// Wraps a Vibe.d socket with the common interface class MySQLSocketVibeD : MySQLSocket { + @safe: private PlainVibeDSocket socket; /// The socket should already be open diff --git a/source/mysql/result.d b/source/mysql/result.d index 22c3287b..b47ea376 100644 --- a/source/mysql/result.d +++ b/source/mysql/result.d @@ -1,302 +1,13 @@ -/// Structures for data received: rows and result sets (ie, a range of rows). -module mysql.result; - -import std.conv; -import std.exception; -import std.range; -import std.string; -import std.variant; - -import mysql.connection; -import mysql.exceptions; -import mysql.protocol.comms; -import mysql.protocol.extra_types; -import mysql.protocol.packets; - /++ -A struct to represent a single row of a result set. - -Type_Mappings: $(TYPE_MAPPINGS) -+/ -/+ -The row struct is used for both 'traditional' and 'prepared' result sets. -It consists of parallel arrays of Variant and bool, with the bool array -indicating which of the result set columns are NULL. - -I have been agitating for some kind of null indicator that can be set for a -Variant without destroying its inherent type information. If this were the -case, then the bool array could disappear. -+/ -struct Row -{ - import mysql.connection; - -package: - Variant[] _values; // Temporarily "package" instead of "private" -private: - bool[] _nulls; - string[] _names; - -public: - - /++ - A constructor to extract the column data from a row data packet. - - If the data for the row exceeds the server's maximum packet size, then several packets will be - sent for the row that taken together constitute a logical row data packet. The logic of the data - recovery for a Row attempts to minimize the quantity of data that is bufferred. Users can assist - in this by specifying chunked data transfer in cases where results sets can include long - column values. - - Type_Mappings: $(TYPE_MAPPINGS) - +/ - this(Connection con, ref ubyte[] packet, ResultSetHeaders rh, bool binary) - { - ctorRow(con, packet, rh, binary, _values, _nulls, _names); - } - - /++ - Simplify retrieval of a column value by index. - - To check for null, use Variant's `type` property: - `row[index].type == typeid(typeof(null))` - - Type_Mappings: $(TYPE_MAPPINGS) +This module publicly imports `mysql.unsafe.result`. Please see that module for +more documentation. - Params: i = the zero based index of the column whose value is required. - Returns: A Variant holding the column value. - +/ - inout(Variant) opIndex(size_t i) inout - { - enforce!MYX(_nulls.length > 0, format("Cannot get column index %d. There are no columns", i)); - enforce!MYX(i < _nulls.length, format("Cannot get column index %d. The last available index is %d", i, _nulls.length-1)); - return _values[i]; - } - - /++ - Get the name of the column with specified index. - +/ - inout(string) getName(size_t index) inout - { - return _names[index]; - } - - /++ - Check if a column in the result row was NULL - - Params: i = The zero based column index. - +/ - bool isNull(size_t i) const pure nothrow { return _nulls[i]; } - - /++ - Get the number of elements (columns) in this row. - +/ - @property size_t length() const pure nothrow { return _values.length; } - - ///ditto - alias opDollar = length; - - /++ - Move the content of the row into a compatible struct - - This method takes no account of NULL column values. If a column was NULL, - the corresponding Variant value would be unchanged in those cases. - - The method will throw if the type of the Variant is not implicitly - convertible to the corresponding struct member. - - Type_Mappings: $(TYPE_MAPPINGS) - - Params: - S = A struct type. - s = A ref instance of the type - +/ - void toStruct(S)(ref S s) if (is(S == struct)) - { - foreach (i, dummy; s.tupleof) - { - static if(__traits(hasMember, s.tupleof[i], "nullify") && - is(typeof(s.tupleof[i].nullify())) && is(typeof(s.tupleof[i].get))) - { - if(!_nulls[i]) - { - enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i].get))(), - "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); - s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i].get)); - } - else - s.tupleof[i].nullify(); - } - else - { - if(!_nulls[i]) - { - enforce!MYX(_values[i].convertsTo!(typeof(s.tupleof[i]))(), - "At col "~to!string(i)~" the value is not implicitly convertible to the structure type"); - s.tupleof[i] = _values[i].get!(typeof(s.tupleof[i])); - } - else - s.tupleof[i] = typeof(s.tupleof[i]).init; - } - } - } - - void show() - { - import std.stdio; - - foreach(Variant v; _values) - writef("%s, ", v.toString()); - writeln(""); - } -} - -/++ -An $(LINK2 http://dlang.org/phobos/std_range_primitives.html#isInputRange, input range) -of Row. +In the future, this will migrate to importing `mysql.safe.result`. In the far +future, the unsafe version will be deprecated and removed, and the safe version +moved to this location. -This is returned by the `mysql.commands.query` functions. - -The rows are downloaded one-at-a-time, as you iterate the range. This allows -for low memory usage, and quick access to the results as they are downloaded. -This is especially ideal in case your query results in a large number of rows. - -However, because of that, this `ResultRange` cannot offer random access or -a `length` member. If you need random access, then just like any other range, -you can simply convert this range to an array via -$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). - -A `ResultRange` becomes invalidated (and thus cannot be used) when the server -is sent another command on the same connection. When an invalidated `ResultRange` -is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to -send the server another command, but still access these results afterwords, -you can save the results for later by converting this range to an array via -$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). - -Type_Mappings: $(TYPE_MAPPINGS) - -Example: ---- -ResultRange oneAtATime = myConnection.query("SELECT * from myTable"); -Row[] allAtOnce = myConnection.query("SELECT * from myTable").array; ---- -+/ -struct ResultRange -{ -private: - Connection _con; - ResultSetHeaders _rsh; - Row _row; // current row - string[] _colNames; - size_t[string] _colNameIndicies; - ulong _numRowsFetched; - ulong _commandID; // So we can keep track of when this is invalidated - - void ensureValid() const pure - { - enforce!MYXInvalidatedRange(isValid, - "This ResultRange has been invalidated and can no longer be used."); - } - -package: - this (Connection con, ResultSetHeaders rsh, string[] colNames) - { - _con = con; - _rsh = rsh; - _colNames = colNames; - _commandID = con.lastCommandID; - popFront(); - } - -public: - /++ - Check whether the range can still be used, or has been invalidated. - - A `ResultRange` becomes invalidated (and thus cannot be used) when the server - is sent another command on the same connection. When an invalidated `ResultRange` - is used, a `mysql.exceptions.MYXInvalidatedRange` is thrown. If you need to - send the server another command, but still access these results afterwords, - you can save the results for later by converting this range to an array via - $(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`). - +/ - @property bool isValid() const pure nothrow - { - return _con !is null && _commandID == _con.lastCommandID; - } - - /// Check whether there are any rows left - @property bool empty() const pure nothrow - { - if(!isValid) - return true; - - return !_con._rowsPending; - } - - /++ - Gets the current row - +/ - @property inout(Row) front() pure inout - { - ensureValid(); - enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); - return _row; - } - - /++ - Progresses to the next row of the result set - that will then be 'front' - +/ - void popFront() - { - ensureValid(); - enforce!MYX(!empty, "Attempted 'popFront' when no more rows available"); - _row = _con.getNextRow(); - _numRowsFetched++; - } - - /++ - Get the current row as an associative array by column name - - Type_Mappings: $(TYPE_MAPPINGS) - +/ - Variant[string] asAA() - { - ensureValid(); - enforce!MYX(!empty, "Attempted 'front' on exhausted result sequence."); - Variant[string] aa; - foreach (size_t i, string s; _colNames) - aa[s] = _row._values[i]; - return aa; - } - - /// Get the names of all the columns - @property const(string)[] colNames() const pure nothrow { return _colNames; } - - /// An AA to lookup a column's index by name - @property const(size_t[string]) colNameIndicies() pure nothrow - { - if(_colNameIndicies is null) - { - foreach(index, name; _colNames) - _colNameIndicies[name] = index; - } - - return _colNameIndicies; - } - - /// Explicitly clean up the MySQL resources and cancel pending results - void close() - out{ assert(!isValid); } - do - { - if(isValid) - _con.purgeResult(); - } +$(SAFE_MIGRATION) +++/ +module mysql.result; - /++ - Get the number of rows retrieved so far. - - Note that this is not neccessarlly the same as the length of the range. - +/ - @property ulong rowCount() const pure nothrow { return _numRowsFetched; } -} +public import mysql.unsafe.result; diff --git a/source/mysql/safe/commands.d b/source/mysql/safe/commands.d new file mode 100644 index 00000000..1df2fe9b --- /dev/null +++ b/source/mysql/safe/commands.d @@ -0,0 +1,605 @@ +/++ +Use a DB via plain SQL statements (safe version). + +Commands that are expected to return a result set - queries - have distinctive +methods that are enforced. That is it will be an error to call such a method +with an SQL command that does not produce a result set. So for commands like +SELECT, use the `query` functions. For other commands, like +INSERT/UPDATE/CREATE/etc, use `exec`. + +This is the @safe version of mysql's command module, and as such uses the @safe +rows and result ranges, and the `MySQLVal` type. For the `Variant` unsafe +version, please import `mysql.unsafe.commands`. + +$(SAFE_MIGRATION) ++/ + +module mysql.safe.commands; + +import std.conv; +import std.exception; +import std.range; +import std.typecons; +import std.variant; + +import mysql.safe.connection; +import mysql.exceptions; +import mysql.safe.prepared; +import mysql.protocol.comms; +import mysql.protocol.constants; +import mysql.protocol.extra_types; +import mysql.protocol.packets; +import mysql.impl.result; +import mysql.types; + +/// This feature is not yet implemented. It currently has no effect. +/+ +A struct to represent specializations of returned statement columns. + +If you are executing a query that will include result columns that are large objects, +it may be expedient to deal with the data as it is received rather than first buffering +it to some sort of byte array. These two variables allow for this. If both are provided +then the corresponding column will be fed to the stipulated delegate in chunks of +`chunkSize`, with the possible exception of the last chunk, which may be smaller. +The bool argument `finished` will be set to true when the last chunk is set. + +Be aware when specifying types for column specializations that for some reason the +field descriptions returned for a resultset have all of the types TINYTEXT, MEDIUMTEXT, +TEXT, LONGTEXT, TINYBLOB, MEDIUMBLOB, BLOB, and LONGBLOB lumped as type 0xfc +contrary to what it says in the protocol documentation. ++/ +struct ColumnSpecialization +{ + size_t cIndex; // parameter number 0 - number of params-1 + ushort type; + uint chunkSize; /// In bytes + void delegate(const(ubyte)[] chunk, bool finished) @safe chunkDelegate; +} +///ditto +alias CSN = ColumnSpecialization; + +@safe: + +/++ +Execute an SQL command or prepared statement, such as INSERT/UPDATE/CREATE/etc. + +This method is intended for commands such as which do not produce a result set +(otherwise, use one of the `query` functions instead.) If the SQL command does +produces a result set (such as SELECT), `mysql.exceptions.MYXResultRecieved` +will be thrown. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.impl.prepared.SafePrepared.setArgs`, this will also remove all +`mysql.impl.prepared.SafeParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.impl.prepared.SafeParameterSpecialization`, use +`mysql.safe.connection.prepare` to manually create a +`mysql.impl.prepared.SafePrepared`, and set your parameter specializations using +`mysql.impl.prepared.SafePrepared.setArg` or +`mysql.impl.prepared.SafePrepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.impl.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +args = The arguments to be passed in the `mysql.impl.prepared.SafePrepared`. + +Returns: The number of rows affected. + +Example: +--- +auto myInt = 7; +auto rowsAffected = myConnection.exec("INSERT INTO `myTable` (`a`) VALUES (?)", myInt); +--- ++/ +ulong exec(Connection conn, const(char[]) sql) +{ + return execImpl(conn, ExecQueryImplInfo(false, sql)); +} +///ditto +ulong exec(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[])) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return exec(conn, prepared); +} +///ditto +ulong exec(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return exec(conn, prepared); +} + +///ditto +ulong exec(Connection conn, ref Prepared prepared) +{ + auto preparedInfo = conn.registerIfNeeded(prepared.sql); + auto ra = execImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); + prepared._lastInsertID = conn.lastInsertID; + return ra; +} +///ditto +ulong exec(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[])) +{ + prepared.setArgs(args); + return exec(conn, prepared); +} + +///ditto +ulong exec(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return exec(conn, prepared); +} + +/// Common implementation for `exec` overloads +package ulong execImpl(Connection conn, ExecQueryImplInfo info) +{ + ulong rowsAffected; + bool receivedResultSet = execQueryImpl(conn, info, rowsAffected); + if(receivedResultSet) + { + conn.purgeResult(); + throw new MYXResultRecieved(); + } + + return rowsAffected; +} + +/++ +Execute an SQL SELECT command or prepared statement. + +This returns an input range of `mysql.impl.result.SafeRow`, so if you need random +access to the `mysql.impl.result.SafeRow` elements, simply call +$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`) +on the result. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.impl.prepared.SafePrepared.setArgs`, this will also remove all +`mysql.impl.prepared.SafeParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.safe.prepared.ParameterSpecialization`, use +`mysql.safe.connection.prepare` to manually create a +`mysql.impl.prepared.SafePrepared`, and set your parameter specializations using +`mysql.impl.prepared.SafePrepared.setArg` or +`mysql.impl.prepared.SafePrepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.impl.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. +args = Arguments to the SQL statement or `mysql.safe.prepared.Prepared` struct. + +Returns: A (possibly empty) `mysql.safe.result.ResultRange`. + +Example: +--- +ResultRange oneAtATime = myConnection.query("SELECT * from `myTable`"); +Row[] allAtOnce = myConnection.query("SELECT * from `myTable`").array; + +auto myInt = 7; +ResultRange rows = myConnection.query("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +SafeResultRange query(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) +{ + return queryImpl(csa, conn, ExecQueryImplInfo(false, sql)); +} +///ditto +SafeResultRange query(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return query(conn, prepared); +} + +///ditto +SafeResultRange query(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return query(conn, prepared); +} + +///ditto +SafeResultRange query(Connection conn, ref Prepared prepared) +{ + auto preparedInfo = conn.registerIfNeeded(prepared.sql); + auto result = queryImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); + prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. + return result; +} +///ditto +SafeResultRange query(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + prepared.setArgs(args); + return query(conn, prepared); +} +///ditto +SafeResultRange query(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return query(conn, prepared); +} + +/// Common implementation for `query` overloads +package SafeResultRange queryImpl(ColumnSpecialization[] csa, + Connection conn, ExecQueryImplInfo info) +{ + ulong ra; + enforce!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); + + conn._rsh = ResultSetHeaders(conn, conn._fieldCount); + if(csa !is null) + conn._rsh.addSpecializations(csa); + + conn._headersPending = false; + return SafeResultRange(conn, conn._rsh, conn._rsh.fieldNames); +} + +/++ +Execute an SQL SELECT command or prepared statement where you only want the +first `mysql.impl.result.SafeRow`, if any. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.impl.prepared.SafePrepared.setArgs`, this will also remove all +`mysql.impl.prepared.SafeParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.impl.prepared.SafeParameterSpecialization`, use +`mysql.safe.connection.prepare` to manually create a +`mysql.impl.prepared.SafePrepared`, and set your parameter specializations using +`mysql.impl.prepared.SafePrepared.setArg` or +`mysql.impl.prepared.SafePrepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.impl.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. +args = Arguments to SQL statement or `mysql.impl.prepared.SafePrepared` struct. + +Returns: `Nullable!(mysql.impl.result.SafeRow)`: This will be null (check + via `Nullable.isNull`) if the query resulted in an empty result set. + +Example: +--- +auto myInt = 7; +Nullable!Row row = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +Nullable!SafeRow queryRow(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) +{ + return queryRowImpl(csa, conn, ExecQueryImplInfo(false, sql)); +} +///ditto +Nullable!SafeRow queryRow(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryRow(conn, prepared); +} +///ditto +Nullable!SafeRow queryRow(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryRow(conn, prepared); +} + +///ditto +Nullable!SafeRow queryRow(Connection conn, ref Prepared prepared) +{ + auto preparedInfo = conn.registerIfNeeded(prepared.sql); + auto result = queryRowImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); + prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. + return result; +} +///ditto +Nullable!SafeRow queryRow(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + prepared.setArgs(args); + return queryRow(conn, prepared); +} +///ditto +Nullable!SafeRow queryRow(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return queryRow(conn, prepared); +} + +/// Common implementation for `querySet` overloads. +package Nullable!SafeRow queryRowImpl(ColumnSpecialization[] csa, Connection conn, + ExecQueryImplInfo info) +{ + auto results = queryImpl(csa, conn, info); + if(results.empty) + return Nullable!SafeRow(); + else + { + auto row = results.front; + results.close(); + return Nullable!SafeRow(row); + } +} + +/++ +Execute an SQL SELECT command or prepared statement where you only want the +first `mysql.result.Row`, and place result values into a set of D variables. + +This method will throw if any column type is incompatible with the corresponding D variable. + +Unlike the other query functions, queryRowTuple will throw +`mysql.exceptions.MYX` if the result set is empty +(and thus the reference variables passed in cannot be filled). + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +Only use the `const(char[]) sql` overload when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.impl.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +args = The variables, taken by reference, to receive the values. ++/ +void queryRowTuple(T...)(Connection conn, const(char[]) sql, ref T args) +{ + return queryRowTupleImpl(conn, ExecQueryImplInfo(false, sql), args); +} + +///ditto +void queryRowTuple(T...)(Connection conn, ref Prepared prepared, ref T args) +{ + auto preparedInfo = conn.registerIfNeeded(prepared.sql); + queryRowTupleImpl(conn, prepared.getExecQueryImplInfo(preparedInfo.statementId), args); + prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. +} + +/// Common implementation for `queryRowTuple` overloads. +package(mysql) void queryRowTupleImpl(T...)(Connection conn, ExecQueryImplInfo info, ref T args) +{ + ulong ra; + enforce!MYXNoResultRecieved(execQueryImpl(conn, info, ra)); + + auto rr = conn.getNextRow(); + /+if (!rr._valid) // The result set was empty - not a crime. + return;+/ + enforce!MYX(rr._values.length == args.length, "Result column count does not match the target tuple."); + foreach (size_t i, dummy; args) + { + import taggedalgebraic.taggedalgebraic : get, hasType; + enforce!MYX(rr._values[i].hasType!(T[i]), + "Tuple "~to!string(i)~" type and column type are not compatible."); + // use taggedalgebraic get to avoid extra calls. + args[i] = get!(T[i])(rr._values[i]); + } + // If there were more rows, flush them away + // Question: Should I check in purgeResult and throw if there were - it's very inefficient to + // allow sloppy SQL that does not ensure just one row! + conn.purgeResult(); +} + +/++ +Execute an SQL SELECT command or prepared statement and return a single value: +the first column of the first row received. + +If the query did not produce any rows, or the rows it produced have zero columns, +this will return `Nullable!MySQLVal()`, ie, null. Test for this with +`result.isNull`. + +If the query DID produce a result, but the value actually received is NULL, +then `result.isNull` will be FALSE, and `result.get` will produce a MySQLVal +which CONTAINS null. Check for this with `result.get.kind == MySQLVal.Kind.Null` +or `result.get == null`. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.impl.prepared.SafePrepared.setArgs`, this will also remove all +`mysql.impl.prepared.SafeParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.impl.prepared.SafeParameterSpecialization`, use +`mysql.safe.connection.prepare` to manually create a `mysql.impl.prepared.SafePrepared`, +and set your parameter specializations using `mysql.impl.prepared.SafePrepared.setArg` +or `mysql.impl.prepared.SafePrepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.impl.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. + +Returns: `Nullable!MySQLVal`: This will be null (check via `Nullable.isNull`) if the +query resulted in an empty result set. + +Example: +--- +auto myInt = 7; +Nullable!MySQLVal value = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) +{ + return queryValueImpl(csa, conn, ExecQueryImplInfo(false, sql)); +} +///ditto +Nullable!MySQLVal queryValue(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +Nullable!MySQLVal queryValue(Connection conn, const(char[]) sql, MySQLVal[] args) +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared) +{ + auto preparedInfo = conn.registerIfNeeded(prepared.sql); + auto result = queryValueImpl(prepared.columnSpecials, conn, prepared.getExecQueryImplInfo(preparedInfo.statementId)); + prepared._lastInsertID = conn.lastInsertID; // Conceivably, this might be needed when multi-statements are enabled. + return result; +} +///ditto +Nullable!MySQLVal queryValue(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +Nullable!MySQLVal queryValue(Connection conn, ref Prepared prepared, MySQLVal[] args) +{ + prepared.setArgs(args); + return queryValue(conn, prepared); +} + +/// Common implementation for `queryValue` overloads. +package Nullable!MySQLVal queryValueImpl(ColumnSpecialization[] csa, Connection conn, + ExecQueryImplInfo info) +{ + auto results = queryImpl(csa, conn, info); + if(results.empty) + return Nullable!MySQLVal(); + else + { + auto row = results.front; + results.close(); + + if(row.length == 0) + return Nullable!MySQLVal(); + else + return Nullable!MySQLVal(row[0]); + } +} + diff --git a/source/mysql/safe/connection.d b/source/mysql/safe/connection.d new file mode 100644 index 00000000..cbad32e9 --- /dev/null +++ b/source/mysql/safe/connection.d @@ -0,0 +1,86 @@ +/++ +Connect to a MySQL/MariaDB server (safe version). + +This is the @safe API for the Connection type. It publicly imports `mysql.impl.connection`, and also provides the safe version of the API for preparing statements. + +Note that the common pieces of the connection are documented and currently +reside in `mysql.impl.connection`. Please see this module for documentation of +the connection object. + +$(SAFE_MIGRATION) ++/ +module mysql.safe.connection; + +public import mysql.impl.connection; +import mysql.safe.prepared; +import mysql.safe.commands; + + +@safe: + +/++ +Submit an SQL command to the server to be compiled into a prepared statement. + +This will automatically register the prepared statement on the provided connection. +The resulting `mysql.impl.prepared.SafePrepared` can then be used freely on ANY +`mysql.impl.connection.Connection`, as it will automatically be registered upon +its first use on other connections. Or, pass it to +`mysql.impl.connection.Connection.register` if you prefer eager registration. + +Internally, the result of a successful outcome will be a statement handle - an ID - +for the prepared statement, a count of the parameters required for +execution of the statement, and a count of the columns that will be present +in any result set that the command generates. + +The server will then proceed to send prepared statement headers, +including parameter descriptions, and result set field descriptions, +followed by an EOF packet. + +Throws: `mysql.exceptions.MYX` if the server has a problem. ++/ +SafePrepared prepare(Connection conn, const(char[]) sql) +{ + auto info = conn.registerIfNeeded(sql); + return SafePrepared(sql, info.headers, info.numParams); +} + +/++ +Convenience function to create a prepared statement which calls a stored function. + +Be careful that your `numArgs` is correct. If it isn't, you may get a +`mysql.exceptions.MYX` with a very unclear error message. + +Throws: `mysql.exceptions.MYX` if the server has a problem. + +Params: + name = The name of the stored function. + numArgs = The number of arguments the stored procedure takes. ++/ +SafePrepared prepareFunction(Connection conn, string name, int numArgs) +{ + auto sql = "select " ~ name ~ preparedPlaceholderArgs(numArgs); + return prepare(conn, sql); +} + +/++ +Convenience function to create a prepared statement which calls a stored procedure. + +OUT parameters are currently not supported. It should generally be +possible with MySQL to present them as a result set. + +Be careful that your `numArgs` is correct. If it isn't, you may get a +`mysql.exceptions.MYX` with a very unclear error message. + +Throws: `mysql.exceptions.MYX` if the server has a problem. + +Params: + name = The name of the stored procedure. + numArgs = The number of arguments the stored procedure takes. + ++/ +SafePrepared prepareProcedure(Connection conn, string name, int numArgs) +{ + auto sql = "call " ~ name ~ preparedPlaceholderArgs(numArgs); + return prepare(conn, sql); +} + diff --git a/source/mysql/safe/package.d b/source/mysql/safe/package.d new file mode 100644 index 00000000..c9fcf1c0 --- /dev/null +++ b/source/mysql/safe/package.d @@ -0,0 +1,22 @@ +/++ +Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native) (safe versions). + +This module will import all modules that use the safe API of the mysql library. +In a future version, this will become the default. + +$(SAFE_MIGRATION) ++/ +module mysql.safe; + +public import mysql.safe.commands; +public import mysql.safe.result; +public import mysql.safe.pool; +public import mysql.safe.prepared; +public import mysql.safe.connection; + +// common imports +public import mysql.escape; +public import mysql.exceptions; +public import mysql.metadata; +public import mysql.protocol.constants : SvrCapFlags; +public import mysql.types; diff --git a/source/mysql/safe/pool.d b/source/mysql/safe/pool.d new file mode 100644 index 00000000..2269a52f --- /dev/null +++ b/source/mysql/safe/pool.d @@ -0,0 +1,20 @@ +/++ +Connect to a MySQL/MariaDB database using vibe.d's +$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) (safe version). + +This aliases `mysql.impl.pool.MySQLPoolImpl!true` as `MySQLPool`. Please see the +`mysql.impl.pool` moddule for documentation on how to use `MySQLPool`. + +This is the @safe version of mysql's pool module, and as such uses only @safe +callback delegates. If you wish to use @system callbacks, import +`mysql.unsafe.pool`. + +$(SAFE_MIGRATION) ++/ + +module mysql.safe.pool; + +import mysql.impl.pool; +// need to check if mysqlpool was enabled +static if(__traits(compiles, () { alias p = MySQLPoolImpl!true; })) + alias MySQLPool = MySQLPoolImpl!true; diff --git a/source/mysql/safe/prepared.d b/source/mysql/safe/prepared.d new file mode 100644 index 00000000..aafbfe2e --- /dev/null +++ b/source/mysql/safe/prepared.d @@ -0,0 +1,22 @@ +/++ +This module publicly imports `mysql.impl.prepared` (safe version). See that +module for documentation on using prepared statements with a MySQL server. + +This module also aliases the safe versions of structs to the original struct +names to aid in transitioning to using safe code with minimal impact. + +$(SAFE_MIGRATION) ++/ +module mysql.safe.prepared; + +public import mysql.impl.prepared; + +/++ +Safe aliases. Use these instead of the real name. See the documentation on +the aliased types for usage. ++/ +alias Prepared = SafePrepared; +/// ditto +alias ParameterSpecialization = SafeParameterSpecialization; +/// ditto +alias PSN = SafeParameterSpecialization; diff --git a/source/mysql/safe/result.d b/source/mysql/safe/result.d new file mode 100644 index 00000000..96e4e5b8 --- /dev/null +++ b/source/mysql/safe/result.d @@ -0,0 +1,19 @@ +/++ +This module publicly imports `mysql.impl.result`. See that module for documentation on how to use result and result range structures (safe versions). + +This module also aliases the safe versions of these structs to the original +struct names to aid in transitioning to using safe code with minimal impact. + +$(SAFE_MIGRATION) ++/ +module mysql.safe.result; + +public import mysql.impl.result; + +/++ +Safe aliases. Use these instead of the real name. See the documentation on +the aliased types for usage. ++/ +alias Row = SafeRow; +/// ditto +alias ResultRange = SafeResultRange; diff --git a/source/mysql/types.d b/source/mysql/types.d index dc247223..4efb07bb 100644 --- a/source/mysql/types.d +++ b/source/mysql/types.d @@ -1,5 +1,8 @@ -/// Structures for MySQL types not built-in to D/Phobos. +/// Structures for MySQL types not built-in to D/Phobos. module mysql.types; +import taggedalgebraic.taggedalgebraic; +import std.datetime : DateTime, TimeOfDay, Date; +import std.typecons : Nullable; /++ A simple struct to represent time difference. @@ -29,3 +32,308 @@ struct Timestamp { ulong rep; } + +private union _MYTYPE +{ +@safeOnly: + // blobs are const because of the indirection. In this case, it's not + // important because nobody is going to use MySQLVal to maintain their + // ubyte array. + ubyte[] Blob; + const(ubyte)[] CBlob; + + typeof(null) Null; + bool Bit; + ubyte UByte; + byte Byte; + ushort UShort; + short Short; + uint UInt; + int Int; + ulong ULong; + long Long; + float Float; + double Double; + .DateTime DateTime; + TimeOfDay Time; + .Timestamp Timestamp; + .Date Date; + + @disableIndex string Text; + @disableIndex const(char)[] CText; + + // pointers + const(bool)* BitRef; + const(ubyte)* UByteRef; + const(byte)* ByteRef; + const(ushort)* UShortRef; + const(short)* ShortRef; + const(uint)* UIntRef; + const(int)* IntRef; + const(ulong)* ULongRef; + const(long)* LongRef; + const(float)* FloatRef; + const(double)* DoubleRef; + const(.DateTime)* DateTimeRef; + const(TimeOfDay)* TimeRef; + const(.Date)* DateRef; + const(string)* TextRef; + const(char[])* CTextRef; + const(ubyte[])* BlobRef; + const(.Timestamp)* TimestampRef; +} + +/++ +MySQLVal is mysql-native's tagged algebraic type that supports only @safe usage +(see $(LINK2, http://code.dlang.org/packages/taggedalgebraic, TaggedAlgebraic) +for more information on the features of this type). Note that TaggedAlgebraic +has UFCS methods that are not available without importing that module in your +code. + +The type can hold any possible type that MySQL can use or return. The _MYTYPE +union, which is a private union for the project, defines the names of the types +that can be stored. These names double as the names for the MySQLVal.Kind +enumeration. To that end, this is the entire union definition: + +------ +private union _MYTYPE +{ + ubyte[] Blob; + const(ubyte)[] CBlob; + + typeof(null) Null; + bool Bit; + ubyte UByte; + byte Byte; + ushort UShort; + short Short; + uint UInt; + int Int; + ulong ULong; + long Long; + float Float; + double Double; + std.datetime.DateTime DateTime; + std.datetime.TimeOfDay Time; + mysql.types.Timestamp Timestamp; + std.datetime.Date Date; + + string Text; + const(char)[] CText; + + // pointers + const(bool)* BitRef; + const(ubyte)* UByteRef; + const(byte)* ByteRef; + const(ushort)* UShortRef; + const(short)* ShortRef; + const(uint)* UIntRef; + const(int)* IntRef; + const(ulong)* ULongRef; + const(long)* LongRef; + const(float)* FloatRef; + const(double)* DoubleRef; + const(DateTime)* DateTimeRef; + const(TimeOfDay)* TimeRef; + const(Date)* DateRef; + const(string)* TextRef; + const(char[])* CTextRef; + const(ubyte[])* BlobRef; + const(Timestamp)* TimestampRef; +} +------ + +Note that the pointers are all const, as the only use case in mysql-native for them is as rebindable parameters to a Prepared struct. + +MySQLVal allows operations, field, and member function access for each of the supported types without unwrapping the MySQLVal value. For example: + +------ +import mysql.safe; + +// support for comparison is valid for any type that supports it +assert(conn.queryValue("SELECT COUNT(*) FROM sometable") > 20); + +// access members of supporting types without unwrapping or verifying type first +assert(conn.queryValue("SELECT updated_date FROM someTable WHERE id=5").year == 2020); + +// arithmetic is supported, return type may vary +auto val = conn.queryValue("SELECT some_integer FROM sometable WHERE id=5") + 100; +static assert(is(typeof(val) == MySQLVal)); +assert(val.kind == MySQLVal.Kind.Int); + +// this will be a double and not a MySQLVal, because all types that support +// addition with a double result in a double. +auto val2 = conn.queryValue("SELECT some_float FROM sometable WHERE id=5") + 100.0; +static assert(is(typeof(val2) == double)); +------ + +Note that per [TaggedAlgebraic's API](https://vibed.org/api/taggedalgebraic.taggedalgebraic/TaggedAlgebraic), +using operators or members of a MySQLVal that aren't valid for the currently +held type will throw an assertion error. If you wish to avoid this, and are not +sure of the actual type, first validate the type is as you expect using the +`kind` member. + +MySQLVal is used in all operations interally for mysql-native, and explicitly +for all safe API calls. Version 3.0.x and earlier of the mysql-native library +used Variant, so this module provides multiple shims to allow code to "just +work", and also provides conversion back to Variant. + +$(SAFE_MIGRATION) ++/ +alias MySQLVal = TaggedAlgebraic!_MYTYPE; + +// helper to convert variants to MySQLVal. Used wherever variant is still used. +import std.variant : Variant; +package MySQLVal _toVal(Variant v) +{ + int x; + // unfortunately, we need to use a giant switch. But hopefully people will stop using Variant, and this will go away. + string ts = v.type.toString(); + bool isRef; + if (ts[$-1] == '*') + { + ts.length = ts.length-1; + isRef= true; + } + + import std.meta; + import std.traits; + import mysql.exceptions; + alias BasicTypes = AliasSeq!(bool, byte, ubyte, short, ushort, int, uint, long, ulong, float, double, DateTime, TimeOfDay, Date, Timestamp); + alias ArrayTypes = AliasSeq!(char[], const(char)[], ubyte[], const(ubyte)[], immutable(ubyte)[]); + switch (ts) + { + static foreach(Type; BasicTypes) + { + case fullyQualifiedName!Type: + case "const(" ~ fullyQualifiedName!Type ~ ")": + case "immutable(" ~ fullyQualifiedName!Type ~ ")": + case "shared(immutable(" ~ fullyQualifiedName!Type ~ "))": + if(isRef) + return MySQLVal(v.get!(const(Type*))); + else + return MySQLVal(v.get!(const(Type))); + } + static foreach(Type; ArrayTypes) + { + case Type.stringof: + { + alias ET = Unqual!(typeof(Type.init[0])); + if(isRef) + return MySQLVal(v.get!(const(ET[]*))); + else + return MySQLVal(v.get!(Type)); + } + } + case "immutable(char)[]": + // have to do this separately, because everything says "string" but + // Variant says "immutable(char)[]" + if(isRef) + return MySQLVal(v.get!(const(char[]*))); + else + return MySQLVal(v.get!(string)); + case "typeof(null)": + return MySQLVal(null); + default: + throw new MYX("Unsupported Database Variant Type: " ~ ts); + } +} + +/++ +Convert a MySQLVal into a Variant. This provides a backwards-compatible shim to use if necessary when transitioning to the safe API. + +$(SAFE_MIGRATION) ++/ +Variant asVariant(MySQLVal v) +{ + return v.apply!((a) => Variant(a)); +} + +/// ditto +Nullable!Variant asVariant(Nullable!MySQLVal v) +{ + if(v.isNull) + return Nullable!Variant(); + return Nullable!Variant(v.get.asVariant); +} + +/++ +Compatibility layer for MySQLVal. These functions provide methods that +$(LINK2, http://code.dlang.org/packages/taggedalgebraic, TaggedAlgebraic) +does not provide in order to keep functionality that was available with Variant. + +Notes: + +The `type` shim should be avoided in favor of using the `kind` property of +TaggedAlgebraic. + +The `get` shim works differently than the TaggedAlgebraic version, as the +Variant get function would provide implicit type conversions, but the +TaggedAlgebraic version does not. + +All shims other than `type` will likely remain as convenience features. + +Note that `peek` is inferred @system because it returns a pointer to the +provided value. + +$(SAFE_MIGRATION) ++/ +bool convertsTo(T)(ref MySQLVal val) +{ + return val.apply!((a) => is(typeof(a) : T)); +} + +/// ditto +T get(T)(auto ref MySQLVal val) +{ + static T convert(V)(ref V v) + { + static if(is(V : T)) + return v; + else + { + import mysql.exceptions; + throw new MYX("Cannot get type " ~ T.stringof ~ " from MySQLVal storing type " ~ V.stringof); + } + } + return val.apply!convert(); +} + +/// ditto +T coerce(T)(auto ref MySQLVal val) +{ + import std.conv : to; + static T convert(V)(ref V v) + { + static if(is(V : T)) + { + return v; + } + else static if(is(typeof(v.to!T()))) + { + return v.to!T; + } + else + { + import mysql.exceptions; + throw new MYX("Cannot coerce type " ~ V.stringof ~ " into type " ~ T.stringof); + } + } + return val.apply!convert(); +} + +/// ditto +TypeInfo type(MySQLVal val) @safe pure nothrow +{ + return val.apply!((ref v) => typeid(v)); +} + +/// ditto +T *peek(T)(ref MySQLVal val) +{ + // use exact type. + import taggedalgebraic.taggedalgebraic : get; + if(val.hasType!T) + return &val.get!T; + return null; +} diff --git a/source/mysql/unsafe/commands.d b/source/mysql/unsafe/commands.d new file mode 100644 index 00000000..3dc91f71 --- /dev/null +++ b/source/mysql/unsafe/commands.d @@ -0,0 +1,502 @@ +/++ +Use a DB via plain SQL statements (unsafe version). + +Commands that are expected to return a result set - queries - have distinctive +methods that are enforced. That is it will be an error to call such a method +with an SQL command that does not produce a result set. So for commands like +SELECT, use the `query` functions. For other commands, like +INSERT/UPDATE/CREATE/etc, use `exec`. + +This is the @system version of mysql's command module, and as such uses the @system +rows and result ranges, and the `Variant` type. For the `MySQLVal` safe +version, please import `mysql.safe.commands`. ++/ + +module mysql.unsafe.commands; +import SC = mysql.safe.commands; + +import std.conv; +import std.exception; +import std.range; +import std.typecons; +import std.variant; + +import mysql.unsafe.connection; +import mysql.exceptions; +import mysql.unsafe.prepared; +import mysql.protocol.comms; +import mysql.protocol.constants; +import mysql.protocol.extra_types; +import mysql.protocol.packets; +import mysql.impl.result; +import mysql.types; + +alias ColumnSpecialization = SC.ColumnSpecialization; +alias CSN = ColumnSpecialization; + +/++ +Execute an SQL command or prepared statement, such as INSERT/UPDATE/CREATE/etc. + +This method is intended for commands such as which do not produce a result set +(otherwise, use one of the `query` functions instead.) If the SQL command does +produces a result set (such as SELECT), `mysql.exceptions.MYXResultRecieved` +will be thrown. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. + +Returns: The number of rows affected. + +Example: +--- +auto myInt = 7; +auto rowsAffected = myConnection.exec("INSERT INTO `myTable` (`a`) VALUES (?)", myInt); +--- ++/ +ulong exec(Connection conn, const(char[]) sql, Variant[] args) @system +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return exec(conn, prepared); +} +///ditto +ulong exec(Connection conn, ref Prepared prepared, Variant[] args) @system +{ + prepared.setArgs(args); + return exec(conn, prepared); +} + +///ditto +ulong exec(Connection conn, ref BackwardCompatPrepared prepared) @system +{ + auto p = prepared.prepared; + auto result = exec(conn, p); + prepared._prepared = p; + return result; +} + +///ditto +ulong exec(Connection conn, ref Prepared prepared) @system +{ + return SC.exec(conn, prepared.safeForExec); +} + +///ditto +ulong exec(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[])) +{ + // we are about to set all args, which will clear any parameter specializations. + prepared.setArgs(args); + return SC.exec(conn, prepared.safe); +} + +// Note: this is a wrapper for the safe commands exec functions that do not +// involve a Prepared struct directly. +///ditto +@safe ulong exec(T...)(Connection conn, const(char[]) sql, T args) + if(!is(T[0] == Variant[])) +{ + return SC.exec(conn, sql, args); +} + +/++ +Execute an SQL SELECT command or prepared statement. + +This returns an input range of `mysql.result.UnsafeRow`, so if you need random access +to the `mysql.result.UnsafeRow` elements, simply call +$(LINK2 https://dlang.org/phobos/std_array.html#array, `std.array.array()`) +on the result. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. + +Returns: A (possibly empty) `mysql.result.UnsafeResultRange`. + +Example: +--- +UnsafeResultRange oneAtATime = myConnection.query("SELECT * from `myTable`"); +UnsafeRow[] allAtOnce = myConnection.query("SELECT * from `myTable`").array; + +auto myInt = 7; +UnsafeResultRange rows = myConnection.query("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +UnsafeResultRange query(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) @safe +{ + return SC.query(conn, sql, csa).unsafe; +} +///ditto +UnsafeResultRange query(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + return SC.query(conn, sql, args).unsafe; +} +///ditto +UnsafeResultRange query(Connection conn, const(char[]) sql, Variant[] args) @system +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return query(conn, prepared); +} +///ditto +UnsafeResultRange query(Connection conn, ref Prepared prepared) @system +{ + return SC.query(conn, prepared.safeForExec).unsafe; +} +///ditto +UnsafeResultRange query(T...)(Connection conn, ref Prepared prepared, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + // this is going to clear any parameter specialization + prepared.setArgs(args); + return SC.query(conn, prepared.safe, args).unsafe; +} +///ditto +UnsafeResultRange query(Connection conn, ref Prepared prepared, Variant[] args) @system +{ + prepared.setArgs(args); + return query(conn, prepared); +} + +///ditto +UnsafeResultRange query(Connection conn, ref BackwardCompatPrepared prepared) @system +{ + auto p = prepared.prepared; + auto result = query(conn, p); + prepared._prepared = p; + return result; +} + +/++ +Execute an SQL SELECT command or prepared statement where you only want the +first `mysql.result.UnsafeRow`, if any. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. + +Returns: `Nullable!(mysql.result.UnsafeRow)`: This will be null (check via `Nullable.isNull`) if the +query resulted in an empty result set. + +Example: +--- +auto myInt = 7; +Nullable!UnsafeRow row = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +Nullable!UnsafeRow queryRow(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) @safe +{ + return SC.queryRow(conn, sql, csa).unsafe; +} +///ditto +Nullable!UnsafeRow queryRow(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + return SC.queryRow(conn, sql, args).unsafe; +} +///ditto +Nullable!UnsafeRow queryRow(Connection conn, const(char[]) sql, Variant[] args) @system +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryRow(conn, prepared); +} +///ditto +Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared) @system +{ + return SC.queryRow(conn, prepared.safeForExec).unsafe; +} +///ditto +Nullable!UnsafeRow queryRow(T...)(Connection conn, ref Prepared prepared, T args) @system + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + prepared.setArgs(args); + return SC.queryRow(conn, prepared.safe, args).unsafe; +} +///ditto +Nullable!UnsafeRow queryRow(Connection conn, ref Prepared prepared, Variant[] args) @system +{ + prepared.setArgs(args); + return queryRow(conn, prepared); +} + +///ditto +Nullable!UnsafeRow queryRow(Connection conn, ref BackwardCompatPrepared prepared) @system +{ + auto p = prepared.prepared; + auto result = queryRow(conn, p); + prepared._prepared = p; + return result; +} + +/++ +Execute an SQL SELECT command or prepared statement where you only want the +first `mysql.result.UnsafeRow`, and place result values into a set of D variables. + +This method will throw if any column type is incompatible with the corresponding D variable. + +Unlike the other query functions, queryRowTuple will throw +`mysql.exceptions.MYX` if the result set is empty +(and thus the reference variables passed in cannot be filled). + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +Only use the `const(char[]) sql` overload when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +args = The variables, taken by reference, to receive the values. ++/ +void queryRowTuple(T...)(Connection conn, const(char[]) sql, ref T args) +{ + return SC.queryRowTuple(conn, sql, args); +} + +///ditto +void queryRowTuple(T...)(Connection conn, ref Prepared prepared, ref T args) +{ + SC.queryRowTuple(conn, prepared.safeForExec, args); +} + +///ditto +void queryRowTuple(T...)(Connection conn, ref BackwardCompatPrepared prepared, ref T args) @system +{ + auto p = prepared.prepared; + SC.queryRowTuple(conn, p.safeForExec, args); + prepared._prepared = p; +} + + +/++ +Execute an SQL SELECT command or prepared statement and return a single value: +the first column of the first row received. + +If the query did not produce any rows, or the rows it produced have zero columns, +this will return `Nullable!Variant()`, ie, null. Test for this with `result.isNull`. + +If the query DID produce a result, but the value actually received is NULL, +then `result.isNull` will be FALSE, and `result.get` will produce a Variant +which CONTAINS null. Check for this with `result.get.type == typeid(typeof(null))`. + +If the SQL command does not produce a result set (such as INSERT/CREATE/etc), +then `mysql.exceptions.MYXNoResultRecieved` will be thrown. Use +`exec` instead for such commands. + +If `args` is supplied, the sql string will automatically be used as a prepared +statement. Prepared statements are automatically cached by mysql-native, +so there's no performance penalty for using this multiple times for the +same statement instead of manually preparing a statement. + +If `args` and `prepared` are both provided, `args` will be used, +and any arguments that are already set in the prepared statement +will automatically be replaced with `args` (note, just like calling +`mysql.prepared.Prepared.setArgs`, this will also remove all +`mysql.prepared.ParameterSpecialization` that may have been applied). + +Only use the `const(char[]) sql` overload that doesn't take `args` +when you are not going to be using the same +command repeatedly and you are CERTAIN all the data you're sending is properly +escaped. Otherwise, consider using overload that takes a `Prepared`. + +If you need to use any `mysql.prepared.ParameterSpecialization`, use +`mysql.connection.prepare` to manually create a `mysql.prepared.Prepared`, +and set your parameter specializations using `mysql.prepared.Prepared.setArg` +or `mysql.prepared.Prepared.setArgs`. + +Type_Mappings: $(TYPE_MAPPINGS) + +Params: +conn = An open `mysql.connection.Connection` to the database. +sql = The SQL command to be run. +prepared = The prepared statement to be run. +csa = Not yet implemented. + +Returns: `Nullable!Variant`: This will be null (check via `Nullable.isNull`) if the +query resulted in an empty result set. + +Example: +--- +auto myInt = 7; +Nullable!Variant value = myConnection.queryRow("SELECT * FROM `myTable` WHERE `a` = ?", myInt); +--- ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +/+ +Future text: +If there are long data items among the expected result columns you can use +the `csa` param to specify that they are to be subject to chunked transfer via a +delegate. + +csa = An optional array of `ColumnSpecialization` structs. If you need to +use this with a prepared statement, please use `mysql.prepared.Prepared.columnSpecials`. ++/ +Nullable!Variant queryValue(Connection conn, const(char[]) sql, ColumnSpecialization[] csa = null) @system +{ + return SC.queryValue(conn, sql, csa).asVariant; +} +///ditto +Nullable!Variant queryValue(T...)(Connection conn, const(char[]) sql, T args) + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + return SC.queryValue(conn, sql, args).asVariant; +} +///ditto +Nullable!Variant queryValue(Connection conn, const(char[]) sql, Variant[] args) @system +{ + auto prepared = conn.prepare(sql); + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +Nullable!Variant queryValue(Connection conn, ref Prepared prepared) @system +{ + return SC.queryValue(conn, prepared.safeForExec).asVariant; +} +///ditto +Nullable!Variant queryValue(T...)(Connection conn, ref Prepared prepared, T args) @system + if(T.length > 0 && !is(T[0] == Variant[]) && !is(T[0] == MySQLVal[]) && !is(T[0] == ColumnSpecialization) && !is(T[0] == ColumnSpecialization[])) +{ + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +Nullable!Variant queryValue(Connection conn, ref Prepared prepared, Variant[] args) @system +{ + prepared.setArgs(args); + return queryValue(conn, prepared); +} +///ditto +Nullable!Variant queryValue(Connection conn, ref BackwardCompatPrepared prepared) @system +{ + auto p = prepared.prepared; + auto result = queryValue(conn, p); + prepared._prepared = p; + return result; +} diff --git a/source/mysql/unsafe/connection.d b/source/mysql/unsafe/connection.d new file mode 100644 index 00000000..65dcb282 --- /dev/null +++ b/source/mysql/unsafe/connection.d @@ -0,0 +1,172 @@ +/++ +Connect to a MySQL/MariaDB server (unsafe version). + +This is the unsafe API for the Connection type. It publicly imports +`mysql.impl.connection`, and also provides the unsafe version of the API for +preparing statements. Note that unsafe prepared statements actually use safe +code underneath. + +Note that the common pieces of the connection are documented and currently +reside in `mysql.impl.connection`. Please see this module for documentation of +the connection object. + +This module also contains the soon-to-be-deprecated BackwardCompatPrepared type. + +$(SAFE_MIGRATION) ++/ +module mysql.unsafe.connection; + +public import mysql.impl.connection; +import mysql.unsafe.prepared; +import mysql.unsafe.commands; +private import CS = mysql.safe.connection; + +/++ +Convenience functions. + +Returns: an UnsafePrepared instance based on the result of the corresponding `mysql.safe.connection` function. + +See that module for more details on how these functions work. ++/ +UnsafePrepared prepare(Connection conn, const(char[]) sql) @safe +{ + return CS.prepare(conn, sql).unsafe; +} + +/// ditto +UnsafePrepared prepareFunction(Connection conn, string name, int numArgs) @safe +{ + return CS.prepareFunction(conn, name, numArgs).unsafe; +} + +/// ditto +UnsafePrepared prepareProcedure(Connection conn, string name, int numArgs) @safe +{ + return CS.prepareProcedure(conn, name, numArgs).unsafe; +} + +/++ +This function is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. + +See `BackwardCompatPrepared` for more info. ++/ +deprecated("This is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. You should migrate from this to the Prepared-compatible exec/query overloads in 'mysql.commands'.") +BackwardCompatPrepared prepareBackwardCompat(Connection conn, const(char[]) sql) +{ + return prepareBackwardCompatImpl(conn, sql); +} + +/// Allow mysql-native tests to get around the deprecation message +package(mysql) BackwardCompatPrepared prepareBackwardCompatImpl(Connection conn, const(char[]) sql) +{ + return BackwardCompatPrepared(conn, prepare(conn, sql)); +} + +/++ +This is a wrapper over `mysql.unsafe.prepared.Prepared`, provided ONLY as a +temporary aid in upgrading to mysql-native v2.0.0 and its +new connection-independent model of prepared statements. See the +$(LINK2 https://github.com/mysql-d/mysql-native/blob/master/MIGRATING_TO_V2.md, migration guide) +for more info. + +In most cases, this layer shouldn't even be needed. But if you have many +lines of code making calls to exec/query the same prepared statement, +then this may be helpful. + +To use this temporary compatability layer, change instances of: + +--- +auto stmt = conn.prepare(...); +--- + +to this: + +--- +auto stmt = conn.prepareBackwardCompat(...); +--- + +And then your prepared statement should work as before. + +BUT DO NOT LEAVE IT LIKE THIS! Ultimately, you should update +your prepared statement code to the mysql-native v2.0.0 API, by changing +instances of: + +--- +stmt.exec() +stmt.query() +stmt.queryRow() +stmt.queryRowTuple(outputArgs...) +stmt.queryValue() +--- + +to this: + +--- +conn.exec(stmt) +conn.query(stmt) +conn.queryRow(stmt) +conn.queryRowTuple(stmt, outputArgs...) +conn.queryValue(stmt) +--- + +Both of the above syntaxes can be used with a `BackwardCompatPrepared` +(the `Connection` passed directly to `mysql.commands.exec`/`mysql.commands.query` +will override the one embedded associated with your `BackwardCompatPrepared`). + +Once all of your code is updated, you can change `prepareBackwardCompat` +back to `prepare` again, and your upgrade will be complete. ++/ +struct BackwardCompatPrepared +{ + import std.variant; + import mysql.unsafe.result; + import std.typecons; + + private Connection _conn; + Prepared _prepared; + + /// Access underlying `Prepared` + @property Prepared prepared() @safe { return _prepared; } + + alias _prepared this; + + /++ + This function is provided ONLY as a temporary aid in upgrading to mysql-native v2.0.0. + + See `BackwardCompatPrepared` for more info. + +/ + deprecated("Change 'preparedStmt.exec()' to 'conn.exec(preparedStmt)'") + ulong exec() @system + { + return .exec(_conn, _prepared); + } + + ///ditto + deprecated("Change 'preparedStmt.query()' to 'conn.query(preparedStmt)'") + ResultRange query() @system + { + return .query(_conn, _prepared); + } + + ///ditto + deprecated("Change 'preparedStmt.queryRow()' to 'conn.queryRow(preparedStmt)'") + Nullable!Row queryRow() @system + { + return .queryRow(_conn, _prepared); + } + + ///ditto + deprecated("Change 'preparedStmt.queryRowTuple(outArgs...)' to 'conn.queryRowTuple(preparedStmt, outArgs...)'") + void queryRowTuple(T...)(ref T args) if(T.length == 0 || !is(T[0] : Connection)) + { + return .queryRowTuple(_conn, _prepared, args); + } + + ///ditto + deprecated("Change 'preparedStmt.queryValue()' to 'conn.queryValue(preparedStmt)'") + Nullable!Variant queryValue() @system + { + return .queryValue(_conn, _prepared); + } +} + diff --git a/source/mysql/unsafe/package.d b/source/mysql/unsafe/package.d new file mode 100644 index 00000000..bc84821c --- /dev/null +++ b/source/mysql/unsafe/package.d @@ -0,0 +1,22 @@ +/++ +Imports all of $(LINK2 https://github.com/mysql-d/mysql-native, mysql-native) (unsafe versions). + +This module will import all modules that use the unsafe API of the mysql +library. Please import `mysql.safe` for the safe version. + +$(SAFE_MIGRATION) ++/ +module mysql.unsafe; + +public import mysql.unsafe.commands; +public import mysql.unsafe.result; +public import mysql.unsafe.pool; +public import mysql.unsafe.prepared; +public import mysql.unsafe.connection; + +// common imports +public import mysql.escape; +public import mysql.exceptions; +public import mysql.metadata; +public import mysql.protocol.constants : SvrCapFlags; +public import mysql.types; diff --git a/source/mysql/unsafe/pool.d b/source/mysql/unsafe/pool.d new file mode 100644 index 00000000..f97692f2 --- /dev/null +++ b/source/mysql/unsafe/pool.d @@ -0,0 +1,20 @@ +/++ +Connect to a MySQL/MariaDB database using vibe.d's +$(LINK2 http://vibed.org/api/vibe.core.connectionpool/ConnectionPool, ConnectionPool) (unsafe version). + +This aliases `mysql.impl.pool.MySQLPoolImpl!false` as `MySQLPool`. Please see the +`mysql.impl.pool` moddule for documentation on how to use `MySQLPool`. + +This is the unsafe version of mysql's pool module, and as such uses only @system +callback delegates. If you wish to use @safe callbacks, import +`mysql.safe.pool`. + +$(SAFE_MIGRATION) ++/ + +module mysql.unsafe.pool; + +import mysql.impl.pool; +// need to check if mysqlpool was enabled +static if(__traits(compiles, () { alias p = MySQLPoolImpl!false; })) + alias MySQLPool = MySQLPoolImpl!false; diff --git a/source/mysql/unsafe/prepared.d b/source/mysql/unsafe/prepared.d new file mode 100644 index 00000000..01a8c257 --- /dev/null +++ b/source/mysql/unsafe/prepared.d @@ -0,0 +1,22 @@ +/++ +This module publicly imports `mysql.impl.prepared` (unsafe version). See that +module for documentation on using prepared statements with a MySQL server. + +This module also aliases the unsafe versions of structs to the original struct +names to aid in backwards compatibility. + +$(SAFE_MIGRATION) +++/ +module mysql.unsafe.prepared; + +public import mysql.impl.prepared; + +/++ +Unsafe aliases. Use these instead of the real name. See the documentation on +the aliased types for usage. +++/ +alias Prepared = UnsafePrepared; +/// ditto +alias ParameterSpecialization = UnsafeParameterSpecialization; +/// ditto +alias PSN = UnsafeParameterSpecialization; diff --git a/source/mysql/unsafe/result.d b/source/mysql/unsafe/result.d new file mode 100644 index 00000000..2d0a7f80 --- /dev/null +++ b/source/mysql/unsafe/result.d @@ -0,0 +1,19 @@ +/++ +This module publicly imports `mysql.impl.result`. See that module for documentation on how to use result and result range structures (unsafe versions). + +This module also aliases the unsafe versions of structs to the original struct +names to aid in backwards compatibility. + +$(SAFE_MIGRATION) ++/ +module mysql.unsafe.result; + +public import mysql.impl.result; + +/++ +Unsafe aliases. Use these instead of the real name. See the documentation on +the aliased types for usage. ++/ +alias Row = UnsafeRow; +/// ditto +alias ResultRange = UnsafeResultRange;