├── .editorconfig ├── .github └── workflows │ └── dub.yml ├── .gitignore ├── Brewfile ├── CONTRIBUTING.md ├── README.md ├── SECURITY.md ├── docker-compose.yml ├── dub.json ├── example ├── .gitignore ├── README.md ├── dub.json └── source │ └── testddbc.d ├── examples ├── odbc_test │ ├── .gitignore │ ├── dub.json │ └── source │ │ └── main.d └── pgsql_test │ ├── dub.json │ └── source │ └── main.d ├── libs ├── win32-mscoff │ └── sqlite3.lib ├── win32 │ ├── libpq.dll │ ├── sqlite3.dll │ └── sqlite3.lib └── win64 │ ├── libpq.dll │ ├── sqlite3.dll │ └── sqlite3.lib ├── source └── ddbc │ ├── all.d │ ├── common.d │ ├── core.d │ ├── drivers │ ├── mysqlddbc.d │ ├── odbcddbc.d │ ├── pgsqlddbc.d │ ├── sqliteddbc.d │ └── utils.d │ ├── package.d │ └── pods.d └── test └── ddbctest ├── common.d ├── main.d ├── mysqltest.d ├── odbctest.d ├── postgresqltest.d └── testconnectionpool.d /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | 7 | [*.{c,h,d,di,dd,json}] 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | trim_trailing_whitespace = true 12 | 13 | [*.{yml}] 14 | indent_size = 2 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.github/workflows/dub.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Overall ddbc should work on the last 10 minor compiler releases (same as Vibe.d). 4 | # For simplicity and speed of the CI, some compiler versions are skipped. The latest 5 | # versions of dmd and ldc must be tested on all platforms (Windows, Linux, and Mac) 6 | # with older compilers only being tested on Windows/Linux. 7 | # The integration testing is done on Linux against Mysql and Postgres 8 | 9 | on: 10 | schedule: 11 | - cron: '30 7 1 * *' 12 | push: 13 | pull_request: 14 | 15 | jobs: 16 | test: 17 | name: ${{ matrix.compiler }} on ${{ matrix.os }} 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | os: [ ubuntu-latest, windows-latest ] 23 | compiler: 24 | - dmd-latest 25 | - ldc-latest 26 | - dmd-2.109.1 # (released in 2024) 27 | - dmd-2.108.1 # (released in 2024) 28 | - dmd-2.107.1 # (released in 2024) 29 | - dmd-2.106.1 # (released in 2024) 30 | - dmd-2.105.3 # (released in 2023) 31 | - dmd-2.104.2 # (released in 2023) 32 | - dmd-2.103.1 # (released in 2023) 33 | - dmd-2.102.2 # (released in 2023) 34 | - dmd-2.101.2 # (released in 2023) 35 | - dmd-2.100.2 # (released in 2022) ## GDC 12 can support 2.100 36 | - dmd-2.099.1 # (released in 2022) 37 | - dmd-2.098.1 # (released in 2021) ## Has issue re: phobos/std/variant.d 38 | - dmd-2.097.2 # (released in 2021) 39 | - ldc-1.33.0 # eq to dmd v2.103.1 40 | - ldc-1.32.2 # eq to dmd v2.102.2 41 | - ldc-1.28.1 # eq to dmd v2.098.1 42 | - ldc-1.27.1 # eq to dmd v2.097.2 43 | include: 44 | ## LDC supports Apple silicon 45 | - { os: macos-latest, compiler: ldc-latest } 46 | ## macos-13 is the latest Mac runner with Intel cpu 47 | - { os: macos-13, compiler: dmd-latest } 48 | - { os: macos-13, compiler: ldc-latest } 49 | - { os: macos-13, compiler: dmd-2.108.1 } 50 | - { os: macos-13, compiler: ldc-1.32.2 } 51 | exclude: 52 | - { os: windows-latest, compiler: dmd-2.098.1 } 53 | - { os: windows-latest, compiler: dmd-2.097.2 } 54 | 55 | steps: 56 | - uses: actions/checkout@v4 57 | 58 | - name: Install D ${{ matrix.compiler }} 59 | uses: dlang-community/setup-dlang@v1 60 | with: 61 | compiler: ${{ matrix.compiler }} 62 | 63 | - name: Install dependencies on Ubuntu 64 | if: startsWith(matrix.os, 'ubuntu') 65 | run: sudo apt-get update && sudo apt-get install libev-dev libpq-dev libevent-dev libsqlite3-dev unixodbc-dev -y 66 | 67 | - name: Install dependencies on Mac OSX 68 | if: startsWith(matrix.os, 'macos') 69 | run: brew bundle 70 | 71 | # Seems ODBC is missing on the latest macos runners so install using homebrew 72 | - name: Install ODBC on Mac OSX ${{ runner.arch }} 73 | if: ${{ startsWith(matrix.os, 'macos') && runner.arch == 'ARM64' }} 74 | run: | 75 | brew tap microsoft/mssql-release https://github.com/Microsoft/homebrew-mssql-release 76 | brew update 77 | HOMEBREW_ACCEPT_EULA=Y brew install msodbcsql18 mssql-tools18 78 | 79 | # - name: Upgrade dub dependencies 80 | # if: startsWith(matrix.os, 'windows') 81 | # uses: WebFreak001/dub-upgrade@v0.1 82 | 83 | # full build 84 | - name: dub build (FULL) ${{ runner.arch }} 85 | run: dub build --config=full 86 | 87 | # x86 (Windows Only) 88 | - name: dub test with full config (x86) 89 | if: ${{ startsWith(matrix.os, 'windows') }} 90 | run: dub test --config=full --arch=x86 91 | 92 | #- name: dub run with test config (x86) 93 | # if: ${{ startsWith(matrix.os, 'windows') }} 94 | # run: dub run --config=test --arch=x86 95 | 96 | - name: run the ddbctest project (x86) 97 | if: ${{ startsWith(matrix.os, 'windows') }} 98 | working-directory: example 99 | run: dub build --config=SQLite --arch=x86 && ./ddbctest --connection=sqlite:ddbc-test.sqlite 100 | 101 | # Use the default arch (either x86_64 or aarch64 all platforms) 102 | - name: dub test with full config (${{ runner.arch }}) 103 | run: dub test --config=full 104 | 105 | #- name: dub run with test config (x86_64) 106 | # run: dub run --config=test --arch=x86_64 107 | 108 | - name: run the ddbctest project (${{ runner.arch }}) 109 | working-directory: example 110 | run: dub build --config=SQLite && ./ddbctest --connection=sqlite:ddbc-test.sqlite 111 | 112 | # x86_mscoff (Windows with dmd Only) 113 | - name: dub test with full config (x86_mscoff) 114 | if: ${{ startsWith(matrix.os, 'windows') && startsWith(matrix.compiler, 'dmd') }} 115 | run: dub test --config=full --arch=x86_mscoff 116 | 117 | #- name: dub run with test config (x86_mscoff) 118 | # if: ${{ startsWith(matrix.os, 'windows') && startsWith(matrix.compiler, 'dmd') }} 119 | # run: dub run --config=test --arch=x86_mscoff 120 | 121 | - name: run the ddbctest project (x86_mscoff) 122 | if: ${{ startsWith(matrix.os, 'windows') && startsWith(matrix.compiler, 'dmd') }} 123 | working-directory: example 124 | run: dub build --config=SQLite --arch=x86_mscoff && ./ddbctest --connection=sqlite:ddbc-test.sqlite 125 | 126 | # # cache 127 | # - uses: WebFreak001/dub-upgrade@v0.1 128 | # if: startsWith(matrix.os, 'windows') 129 | # with: { store: true } 130 | 131 | integration-tests: 132 | name: Integration Tests 133 | runs-on: ubuntu-20.04 134 | 135 | services: 136 | mysql: 137 | image: mysql:5.7 138 | ports: [3306] 139 | env: 140 | MYSQL_ROOT_PASSWORD: f48dfhw3Hd!Asah7i2aZ 141 | MYSQL_DATABASE: testdb 142 | MYSQL_USER: testuser 143 | MYSQL_PASSWORD: passw0rd 144 | # Set health checks to wait until mysql service has started 145 | options: >- 146 | --health-cmd "mysqladmin ping" 147 | --health-interval 10s 148 | --health-timeout 3s 149 | --health-retries 4 150 | 151 | postgres: 152 | image: postgres 153 | ports: [5432] 154 | env: 155 | POSTGRES_DB: testdb 156 | POSTGRES_USER: testuser 157 | POSTGRES_PASSWORD: passw0rd 158 | # Set health checks to wait until postgres service has started 159 | options: >- 160 | --health-cmd pg_isready 161 | --health-interval 10s 162 | --health-timeout 3s 163 | --health-retries 3 164 | 165 | mssql: 166 | #image: microsoft/mssql-server-linux:2017-latest 167 | #image: mcr.microsoft.com/mssql/server:2019-latest 168 | image: mcr.microsoft.com/mssql/server:2022-latest 169 | ports: [1433] 170 | env: 171 | MSSQL_PID: Developer 172 | SA_PASSWORD: MSbbk4k77JKH88g54 173 | ACCEPT_EULA: Y 174 | # options: >- 175 | # --health-cmd "sqlcmd -S localhost -U sa -P MSbbk4k77JKH88g54 -Q 'SELECT 1' || exit 1" 176 | # --health-interval 10s 177 | # --health-timeout 3s 178 | # --health-retries 3 179 | 180 | steps: 181 | - uses: actions/checkout@v4 182 | 183 | - name: Install latest DMD 184 | uses: dlang-community/setup-dlang@v1 185 | with: 186 | compiler: dmd-latest 187 | 188 | - name: Install Microsoft ODBC 189 | run: sudo ACCEPT_EULA=Y apt-get install msodbcsql18 -y 190 | 191 | - name: Run ddbctest 192 | env: 193 | MYSQL_PORT: ${{ job.services.mysql.ports[3306] }} 194 | POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} 195 | MSSQL_PORT: ${{ job.services.mssql.ports[1433] }} 196 | run: dub run --config=test 197 | 198 | - name: Build The Example Project 199 | working-directory: ./example 200 | run: dub build 201 | 202 | - name: Run The Examples (SQLite) 203 | working-directory: ./example 204 | run: | 205 | ./ddbctest --connection=sqlite::memory: 206 | 207 | - name: Run The Examples (MySQL) 208 | working-directory: ./example 209 | env: 210 | PORT: ${{ job.services.mysql.ports[3306] }} 211 | run: | 212 | ./ddbctest --connection=mysql://127.0.0.1:$PORT --database=testdb --user=testuser --password=passw0rd 213 | 214 | - name: Run The Examples (Postgres) 215 | working-directory: ./example 216 | env: 217 | PORT: ${{ job.services.postgres.ports[5432] }} 218 | run: | 219 | ./ddbctest --connection=postgresql://127.0.0.1:$PORT --database=testdb --user=testuser --password=passw0rd 220 | 221 | - name: Run The Examples (SQL Server) 222 | working-directory: ./example 223 | env: 224 | PORT: ${{ job.services.mssql.ports[1433] }} 225 | run: | 226 | ./ddbctest --connection=odbc://127.0.0.1:$PORT --user=SA --password=MSbbk4k77JKH88g54 --driver="ODBC Driver 18 for SQL Server" --trust_server_certificate=yes 227 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #ignore thumbnails created by windows 2 | Thumbs.db 3 | 4 | # Ignore build output: 5 | *.exe 6 | *.dll 7 | 8 | #Ignore project files by Intellij 9 | .idea/ 10 | *.iml 11 | 12 | #Ignore project files by Visual Studio 13 | *.sln 14 | *.visualdproj 15 | 16 | #Ignore files build by Visual Studio 17 | *.user 18 | *.aps 19 | *.pch 20 | *.vspscc 21 | *_i.c 22 | *_p.c 23 | *.ncb 24 | *.suo 25 | *.bak 26 | *.cache 27 | *.ilk 28 | *.log 29 | [Bb]in 30 | [Dd]ebug*/ 31 | *.sbr 32 | obj/ 33 | [Rr]elease*/ 34 | _ReSharper*/ 35 | HibernatedTest/* 36 | Unittest/* 37 | 38 | .dub/ 39 | dub.selections.json 40 | test/ddbc-tests 41 | test/ddbc*-test 42 | lib/ 43 | *.sqlite 44 | 45 | #Ignore files build by Visual Studio code 46 | launch.json 47 | settings.json 48 | dub.userprefs 49 | ddbc.userprefs 50 | 51 | #private files used for tests 52 | test_connection.txt -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | # running 'brew bundle' will install required dependencies 2 | brew 'libevent' 3 | brew 'sqlite' 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | DDBC aims to support a range of compiler versions across from dmd 2.097 and above across a range of databases; SQLite, MySQL/MariaDB, Postgres, SQL Server, and Oracle. 2 | 3 | ## Tests 4 | 5 | To help with testing there is a *docker-compose.yml* file in the root of the project so that multiple databases can be run locally for testing. 6 | 7 | When making changes to DDBC please ensure that unit tests (test not requiring a working database) remain in the project source and any integration tests (those running against a local database) are placed the test project (under `./test/ddbctest/`). 8 | 9 | Unit tests can be run in the usual way with `dub test` and integration tests are run with `dub run --config=test`. 10 | 11 | To summarize, testing should be done as follows: 12 | 1. `dub test` - Runs unit tests. 13 | 2. `docker-compose up` - Creates various locally running databases in containers. 14 | 3. `dub run --config=test` - Runs integration tests aganist the local databases. 15 | 4. `docker-compose down` - Destroys the locally created databases. 16 | 17 | ## Requirements for developing 18 | 19 | Apart from a D compiler such as dmd or ldc and the dub package manager, you'll need to have docker and docker-compose installed. If you want to test against an Oracle container you'll also need to have a login for [container-registry.oracle.com](https://container-registry.oracle.com) and have accepted their terms & conditions. There's also some libraries you'll need to have installed. 20 | 21 | On Fedora Linux you can do this using: 22 | 23 | ``` 24 | sudo dnf install openssl-devel sqlite-devel libpq-devel -y 25 | ``` 26 | 27 | ### Installing Microsoft's odbc driver 28 | 29 | On Linux you can potentially use [FreeTDS](https://www.freetds.org/) as the ODBC driver when connecting to SQL Server. However, Microsoft do provide their own odbc driver and that is what's used for testing against SQL Server during CI. 30 | 31 | On Fedora Linux you can find packages under [packages.microsoft.com/config/rhel/](https://packages.microsoft.com/config/rhel/). See the documentation [here](https://learn.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server?view=sql-server-ver16#redhat18) for more details.The basic steps are: 32 | 33 | ``` 34 | # curl https://packages.microsoft.com/config/rhel/8/prod.repo > /etc/yum.repos.d/mssql-release.repo 35 | 36 | sudo yum remove unixODBC-utf16 unixODBC-utf16-devel 37 | sudo ACCEPT_EULA=Y dnf install -y unixODBC unixODBC-devel msodbcsql18 mssql-tools18 38 | 39 | sudo cat /etc/odbcinst.ini 40 | ``` 41 | 42 | ## Installing databases locally (non-containerised) 43 | 44 | ### MySQL (local) 45 | 46 | To allow unit tests using MySQL server, 47 | run mysql client using admin privileges, e.g. for MySQL server on localhost: 48 | 49 | ``` 50 | > mysql -uroot 51 | ``` 52 | 53 | Create test user and test DB: 54 | 55 | ``` 56 | mysql> CREATE DATABASE IF NOT EXISTS testdb; 57 | mysql> CREATE USER 'testuser'@'localhost' IDENTIFIED BY 'passw0rd'; 58 | mysql> GRANT ALL PRIVILEGES ON testdb.* TO 'testuser'@'localhost'; 59 | 60 | mysql> CREATE USER 'testuser'@'localhost'; 61 | mysql> GRANT ALL PRIVILEGES ON testdb.* TO 'testuser'@'localhost' IDENTIFIED BY 'passw0rd'; 62 | mysql> FLUSH PRIVILEGES; 63 | ``` 64 | 65 | ### Postgres (local) 66 | 67 | To allow unit tests using PostgreSQL server, 68 | run postgres client using admin privileges, e.g. for postgres server on localhost: 69 | 70 | ``` 71 | sudo -u postgres psql 72 | ``` 73 | 74 | Then create a user and test database: 75 | 76 | ``` 77 | postgres=# CREATE USER testuser WITH ENCRYPTED PASSWORD 'passw0rd'; 78 | postgres=# CREATE DATABASE testdb WITH OWNER testuser ENCODING 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8' TEMPLATE template0; 79 | ``` 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DDBC 2 | ==== 3 | 4 | [![DUB Package](https://img.shields.io/dub/v/ddbc.svg)](https://code.dlang.org/packages/ddbc) [![CI](https://github.com/buggins/ddbc/actions/workflows/dub.yml/badge.svg)](https://github.com/buggins/ddbc/actions/workflows/dub.yml) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/buggins/ddbc?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | DDBC is DB Connector for D language (similar to JDBC) 7 | 8 | Currently supports MySQL, PostgreSQL, SQLite and SQL Server (via ODBC). 9 | 10 | The project is hosted on [github](https://github.com/buggins/ddbc) with documentation available on the [wiki](https://github.com/buggins/ddbc/wiki). 11 | 12 | 13 | See also: [hibernated](https://github.com/buggins/hibernated) - ORM for D language which uses DDBC. 14 | 15 | 16 | NOTE: project has been moved from SourceForge to GitHub 17 | 18 | 19 | ## Sample code 20 | 21 | ```d 22 | import ddbc; 23 | import std.stdio; 24 | import std.conv; 25 | 26 | int main(string[] args) { 27 | 28 | // provide URL for proper type of DB 29 | string url = "postgresql://localhost:5432/ddbctestdb?user=ddbctest,password=ddbctestpass,ssl=true"; 30 | //string url = "mysql://localhost:3306/ddbctestdb?user=ddbctest,password=ddbctestpass"; 31 | //string url = "sqlite:testdb.sqlite"; 32 | 33 | // creating Connection 34 | auto conn = createConnection(url); 35 | scope(exit) conn.close(); 36 | 37 | // creating Statement 38 | auto stmt = conn.createStatement(); 39 | scope(exit) stmt.close(); 40 | 41 | // execute simple queries to create and fill table 42 | stmt.executeUpdate("DROP TABLE ddbct1"); 43 | stmt.executeUpdate("CREATE TABLE ddbct1 44 | (id bigint not null primary key, 45 | name varchar(250), 46 | comment text, 47 | ts datetime)"); 48 | stmt.executeUpdate("INSERT INTO ddbct1 (id, name, comment, ts) VALUES 49 | (1, 'name1', 'comment for line 1', '2016/09/14 15:24:01')"); 50 | stmt.executeUpdate("INSERT INTO ddbct1 (id, name, comment) VALUES 51 | (2, 'name2', 'comment for line 2 - can be very long')"); 52 | stmt.executeUpdate("INSERT INTO ddbct1 (id, name) values(3, 'name3')"); // comment is null here 53 | 54 | // reading DB 55 | auto rs = stmt.executeQuery("SELECT id, name name_alias, comment, ts FROM ddbct1 ORDER BY id"); 56 | while (rs.next()) 57 | writeln(to!string(rs.getLong(1)), "\t", rs.getString(2), "\t", rs.getString(3), "\t", rs.getString(4)); 58 | return 0; 59 | } 60 | ``` 61 | 62 | Module ddbc.pods implement SELECT support for POD structs (plain old data). 63 | 64 | Instead of manual reading fields one by one, it's possible to put result set value to struct fields, 65 | and generate select statements automatically. 66 | 67 | Sample of easy reading from DB using PODs support: 68 | 69 | 70 | ```d 71 | import ddbc; 72 | import std.stdio; 73 | 74 | // provide URL for proper type of DB 75 | //string url = "postgresql://localhost:5432/ddbctestdb?user=ddbctest,password=ddbctestpass,ssl=true"; 76 | //string url = "mysql://localhost:3306/ddbctestdb?user=ddbctest,password=ddbctestpass"; 77 | string url = "sqlite:testdb.sqlite"; 78 | // creating Connection 79 | auto conn = createConnection(url); 80 | scope(exit) conn.close(); 81 | Statement stmt = conn.createStatement(); 82 | scope(exit) stmt.close(); 83 | // fill database with test data 84 | stmt.executeUpdate(`DROP TABLE IF EXISTS user_data`); 85 | stmt.executeUpdate(`CREATE TABLE user_data (id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL, flags int null)`); 86 | stmt.executeUpdate(`INSERT INTO user_data (id, name, flags) VALUES (1, 'John', 5), (2, 'Andrei', 2), (3, 'Walter', 2), (4, 'Rikki', 3), (5, 'Iain', 0), (6, 'Robert', 1)`); 87 | 88 | // our POD object 89 | struct User { 90 | long id; 91 | string name; 92 | int flags; 93 | } 94 | 95 | writeln("reading all user table rows"); 96 | foreach(ref e; stmt.select!User) { 97 | writeln("id:", e.id, " name:", e.name, " flags:", e.flags); 98 | } 99 | 100 | writeln("reading user table rows with where and order by"); 101 | foreach(ref e; stmt.select!User.where("id < 6").orderBy("name desc")) { 102 | writeln("id:", e.id, " name:", e.name, " flags:", e.flags); 103 | } 104 | 105 | writeln("reading user table rows with where and order by with limit and offset"); 106 | foreach(e; stmt.select!User.where("id < 6").orderBy("name desc").limit(3).offset(1)) { 107 | writeln("id:", e.id, " name:", e.name, " flags:", e.flags); 108 | } 109 | 110 | writeln("reading all user table rows, but fetching only id and name (you will see default value 0 in flags field)"); 111 | foreach(ref e; stmt.select!(User, "id", "name")) { 112 | writeln("id:", e.id, " name:", e.name, " flags:", e.flags); 113 | } 114 | ``` 115 | 116 | ## Connections Strings 117 | 118 | Connection strings should start with `ddbc:` followed by the driver type, eg: `ddbc:mysql://localhost`. However, the _ddbc_ prefix is optional. 119 | 120 | The overall format is typically `[ddbc:]//[ HOSTNAME [ ,PORT ]] [ ? }]` except for SQLite 121 | 122 | ### SQLite 123 | 124 | SQLite can be configured for file based persistence or in-memory storage. 125 | 126 | ``` 127 | ddbc:sqlite:ddbc-test.sqlite 128 | ``` 129 | 130 | An in memory database can be configured by specifying **:memory:** instead of a filename: 131 | 132 | ``` 133 | ddbc:sqlite::memory: 134 | ``` 135 | 136 | ### MySQL 137 | 138 | ``` 139 | ddbc:mysql://127.0.0.1:3306 140 | ``` 141 | 142 | ### PostgreSQL 143 | 144 | ``` 145 | ddbc:postgresql://127.0.0.1:5432 146 | 147 | or 148 | 149 | ddbc:postgresql://hostname:5432/dbname 150 | ``` 151 | 152 | ### Microsoft SQL Server (via ODBC) 153 | 154 | ``` 155 | ddbc:sqlserver://localhost,1433?user=sa,password=bbk4k77JKH88g54,driver=FreeTDS 156 | 157 | or 158 | 159 | ddbc:odbc://localhost,1433?user=sa,password=bbk4k77JKH88g54,driver=FreeTDS 160 | ``` 161 | 162 | ### Oracle (via ODBC) **experimental** 163 | 164 | ``` 165 | ddbc:oracle://localhost:1521?user=sa,password=bbk4k77JKH88g54,driver=FreeTDS 166 | 167 | or 168 | 169 | ddbc:odbc://localhost:1521?user=sa,password=bbk4k77JKH88g54,driver=FreeTDS 170 | ``` 171 | 172 | ### DSN connections for Microsoft SQL Server 173 | The correct format to use for a dsn connection string is `odbc://?dsn=`. 174 | Note that the server portion before the `?` is empty, so the default server for 175 | the DSN name will be used. 176 | 177 | ## Contributing 178 | 179 | pull requests are welcome. Please ensure your local branch is up to date and all tests are passing locally before making a pull request. A docker-compose file is included to help with local development. Use `docker-compose up -d` then run `dub test --config=MySQL`, `dub test --config=PGSQL` and `dub test --config=ODBC`. See the `.travis.yml` file and individual driver code for details on creating the relevant databases for local testing. 180 | 181 | The examples should also run, make sure to change to the _example_ directory and run `dub build` then make sure that the compiled executable will run with each supported database (you'll need to install relevant libs and create databases and users with relevant permissions): 182 | 183 | ``` 184 | ./ddbctest --connection=sqlite::memory: 185 | ./ddbctest --connection=mysql:127.0.0.1 --database=testdb --user=travis --password=bbk4k77JKH88g54 186 | ./ddbctest --connection=postgresql:127.0.0.1 --database=testdb --user=postgres 187 | ./ddbctest --connection=odbc://localhost --database=ddbctest --user=SA --password=bbk4k77JKH88g54 --driver="ODBC Driver 17 for SQL Server" 188 | ./ddbctest --connection=odbc://localhost --database=ddbctest --user=SA --password=bbk4k77JKH88g54 --driver=FreeTDS 189 | ``` 190 | 191 | In the case of the ODBC connection _FreeTDS_ is just an example, if you have _msodbcsql17_ driver installed use that instead. 192 | 193 | Also, you may want to only run a single database image at a time. In that case you can do `docker-compose up ` 194 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We only support the latest major/minor release. Fixes for security issues that do not require breaking changes will be released a bugfix version. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 0.5.x | :white_check_mark: | 10 | | < 0.5 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Development on ddbc is not particularly active. If a vulnerability is found please consider making a pull request with a description of the problem and the appropriate fix. Appropriate testing should also be included. 15 | 16 | You can also make contact via gitter.im: [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/buggins/ddbc?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | mysql: 4 | # Don't use latest (MySQL Server 8.0) as we cannot currently authenticate to it 5 | image: mysql:5.7 6 | #image: container-registry.oracle.com/mysql/community-server:8.0 7 | #image: mariadb:latest 8 | restart: always 9 | ports: ['3306:3306', '33060:33060'] 10 | ulimits: 11 | nofile: 12 | soft: "1024" 13 | hard: "10240" 14 | environment: 15 | - MYSQL_ROOT_PASSWORD=f48dfhw3Hd!Asah7i2aZ 16 | - MYSQL_DATABASE=testdb 17 | - MYSQL_USER=testuser 18 | - MYSQL_PASSWORD=passw0rd 19 | postgres: 20 | image: postgres:latest 21 | restart: always 22 | ports: ['5432:5432'] 23 | environment: 24 | - POSTGRES_DB=testdb 25 | - POSTGRES_USER=testuser 26 | - POSTGRES_PASSWORD=passw0rd 27 | mssql: 28 | image: mcr.microsoft.com/mssql/server:2022-latest 29 | #image: mcr.microsoft.com/mssql/server:2019-latest 30 | #image: mcr.microsoft.com/mssql/server:2017-latest 31 | restart: always 32 | ports: ['1433:1433'] 33 | environment: 34 | - MSSQL_PID=Developer 35 | - SA_PASSWORD=MSbbk4k77JKH88g54 36 | - ACCEPT_EULA=Y 37 | # You'll need to have a login for https://container-registry.oracle.com and have 38 | # accepted their terms & conditions. Then prior to running 'docker-compose up -d' you 39 | # will need to run 'docker login container-registry.oracle.com' to pull the Oracle Database image. 40 | # Also, Oracle takes considerably more resources. 41 | # oracle: 42 | # image: container-registry.oracle.com/database/standard:12.1.0.2 43 | # ports: 44 | # - 1521:1521 45 | # - 8080:8080 46 | # - 5500:5500 47 | # environment: 48 | # - DB_SID=testuser 49 | # - DB_PASSWD=passw0rd 50 | # - USERNAME=testuser 51 | # - PASSWORD=passw0rd 52 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ddbc", 3 | "description": "DB Connector for D language, similar to JDBC (mysql, sqlite, postgresql, odbc)", 4 | "authors": ["Vadim Lopatin"], 5 | "homepage": "https://github.com/buggins/ddbc", 6 | "license": "BSL-1.0", 7 | "targetPath": "lib", 8 | "targetType": "staticLibrary", 9 | "buildRequirements": [ 10 | "allowWarnings" 11 | ], 12 | "toolchainRequirements": { 13 | "dub": ">=1.14.0", 14 | "frontend": ">=2.097" 15 | }, 16 | "systemDependencies": "Depending on configuration: PostgreSQL and/or SQLite v3", 17 | "configurations": [ 18 | { 19 | "name": "full", 20 | "versions": ["USE_MYSQL", "USE_SQLITE", "USE_PGSQL", "USE_ODBC"], 21 | "dependencies": { 22 | "mysql-native": "~>3.1.0", 23 | "derelict-pq": "~>2.2.0", 24 | "odbc": "~>1.0.0" 25 | }, 26 | "libs-posix": ["sqlite3", "odbc"], 27 | "libs-windows": ["odbc32"], 28 | "lflags-osx-x86_64": ["-L/usr/local/opt/sqlite3/lib/"], 29 | "lflags-osx-aarch64": ["-L/opt/homebrew/opt/sqlite3/lib/"], 30 | "copyFiles-windows-x86": [ "libs/win32/sqlite3.dll", "libs/win32/libpq.dll"], 31 | "copyFiles-windows-x86_64": [ "libs/win64/libpq.dll", "libs/win64/sqlite3.dll"], 32 | "sourceFiles-windows-x86_64" : [ "libs/win64/sqlite3.lib" ], 33 | "sourceFiles-windows-x86-ldc" : [ "libs/win32-mscoff/sqlite3.lib"], 34 | "sourceFiles-windows-x86_omf-dmd" : [ "libs/win32/sqlite3.lib"], 35 | "sourceFiles-windows-x86_mscoff-dmd" : [ "libs/win32-mscoff/sqlite3.lib"] 36 | }, 37 | { 38 | "name": "full-omf", 39 | "platforms": ["windows-x86"], 40 | "versions": ["USE_MYSQL", "USE_SQLITE", "USE_PGSQL", "USE_ODBC"], 41 | "dependencies": { 42 | "mysql-native": "~>3.1.0", 43 | "derelict-pq": "~>2.2.0", 44 | "odbc": "~>1.0.0" 45 | }, 46 | "libs-windows": ["odbc32"], 47 | "copyFiles-windows-x86": [ "libs/win32/sqlite3.dll", "libs/win32/libpq.dll"], 48 | "sourceFiles-windows-x86" : [ "libs/win32/sqlite3.lib"] 49 | }, 50 | { 51 | "name": "MySQL", 52 | "versions": ["USE_MYSQL"], 53 | "dependencies": { 54 | "mysql-native": "~>3.1.0" 55 | } 56 | }, 57 | { 58 | "name": "SQLite", 59 | "versions": ["USE_SQLITE"], 60 | "libs-posix": ["sqlite3"], 61 | "lflags-osx-x86_64": ["-L/usr/local/opt/sqlite3/lib/"], 62 | "lflags-osx-aarch64": ["-L/opt/homebrew/opt/sqlite3/lib/"], 63 | "copyFiles-windows-x86": [ "libs/win32/sqlite3.dll" ], 64 | "copyFiles-windows-x86_64": [ "libs/win64/sqlite3.dll" ], 65 | "sourceFiles-windows-x86_64" : [ "libs/win64/sqlite3.lib" ], 66 | "sourceFiles-windows-x86-ldc" : [ "libs/win32-mscoff/sqlite3.lib" ], 67 | "sourceFiles-windows-x86_omf-dmd" : [ "libs/win32/sqlite3.lib" ], 68 | "sourceFiles-windows-x86_mscoff-dmd" : [ "libs/win32-mscoff/sqlite3.lib" ] 69 | }, 70 | { 71 | "name": "SQLite-omf", 72 | "platforms": ["windows-x86-dmd"], 73 | "versions": ["USE_SQLITE"], 74 | "copyFiles-windows-x86": [ "libs/win32/sqlite3.dll" ], 75 | "sourceFiles-windows-x86" : [ "libs/win32/sqlite3.lib" ] 76 | }, 77 | { 78 | "name": "PGSQL", 79 | "versions": ["USE_PGSQL"], 80 | "libs-posix": ["pq"], 81 | "copyFiles-windows-x86": [ "libs/win32/libpq.dll"], 82 | "copyFiles-windows-x86_64": [ "libs/win64/libpq.dll"], 83 | "dependencies": { 84 | "derelict-pq": "~>2.2.0" 85 | } 86 | }, 87 | { 88 | "name": "ODBC", 89 | "versions": ["USE_ODBC"], 90 | "libs-posix": ["odbc"], 91 | "libs-windows": ["odbc32"], 92 | "dependencies": { 93 | "odbc": "~>1.0.0" 94 | } 95 | }, 96 | { 97 | "name": "API" 98 | }, 99 | { 100 | "name": "test", 101 | "sourcePaths" : ["test/ddbctest"], 102 | "mainSourceFile": "test/ddbctest/main.d", 103 | "targetName": "ddbc-tests", 104 | "targetPath": "test", 105 | "targetType": "executable", 106 | "versions": ["USE_MYSQL", "USE_SQLITE", "USE_PGSQL", "USE_ODBC"], 107 | "dependencies": { 108 | "d-unit": "~>0.10.2", 109 | "mysql-native": "~>3.1.0", 110 | "derelict-pq": "~>2.2.0", 111 | "odbc": "~>1.0.0" 112 | }, 113 | "libs-posix": ["sqlite3", "pq", "odbc"], 114 | "libs-windows": ["odbc32"], 115 | "lflags-osx-x86_64": ["-L/usr/local/opt/sqlite3/lib/"], 116 | "lflags-osx-aarch64": ["-L/opt/homebrew/opt/sqlite3/lib/"], 117 | "copyFiles-windows-x86": [ "libs/win32/sqlite3.dll" ], 118 | "copyFiles-windows-x86_64": [ "libs/win64/sqlite3.dll" ], 119 | "sourceFiles-windows-x86_64" : [ "libs/win64/sqlite3.lib" ], 120 | "sourceFiles-windows-x86-ldc" : [ "libs/win32-mscoff/sqlite3.lib" ], 121 | "sourceFiles-windows-x86_omf-dmd" : [ "libs/win32/sqlite3.lib" ], 122 | "sourceFiles-windows-x86_mscoff-dmd" : [ "libs/win32-mscoff/sqlite3.lib" ] 123 | }, 124 | { 125 | "name": "test-omf", 126 | "platforms": ["windows-x86-dmd"], 127 | "sourcePaths" : ["test/ddbctest"], 128 | "mainSourceFile": "test/ddbctest/main.d", 129 | "targetName": "ddbc-tests", 130 | "targetPath": "test", 131 | "targetType": "executable", 132 | "dependencies": { 133 | "d-unit": "~>0.10.2" 134 | }, 135 | "versions": ["USE_SQLITE"], 136 | "copyFiles-windows-x86": [ "libs/win32/sqlite3.dll" ], 137 | "sourceFiles-windows-x86-dmd" : [ "libs/win32/sqlite3.lib" ] 138 | } 139 | ] 140 | } 141 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | #ignore thumbnails created by windows 2 | Thumbs.db 3 | #Ignore files build by Visual Studio 4 | *.user 5 | *.aps 6 | *.pch 7 | *.vspscc 8 | *_i.c 9 | *_p.c 10 | *.ncb 11 | *.suo 12 | *.bak 13 | *.cache 14 | *.ilk 15 | *.log 16 | [Bb]in 17 | [Dd]ebug*/ 18 | *.sbr 19 | obj/ 20 | [Rr]elease*/ 21 | _ReSharper*/ 22 | code.visualdproj 23 | HibernatedTest/* 24 | main.d 25 | Unittest/* 26 | .dub/ 27 | dub.selections.json 28 | ddbctest 29 | *.sqlite 30 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | The following SQL was input into sqlite and saved as `ddbc-test.sqlite` 2 | 3 | ``` 4 | CREATE TABLE IF NOT EXISTS ddbct1 ( 5 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 6 | name varchar(250), 7 | comment mediumtext, 8 | ts datetime 9 | ); 10 | 11 | INSERT INTO ddbct1 (id, name, comment) VALUES 12 | (1, 'name1', 'comment for line 1'), 13 | (2, 'name2', 'comment for line 2 - can be very long'); 14 | ``` 15 | 16 | After building, the examples can be run with the provided test data: 17 | 18 | ``` 19 | ./ddbctest --connection=sqlite:ddbc-test.sqlite 20 | ``` 21 | 22 | alternatively use an in memory database: 23 | 24 | ``` 25 | ./ddbctest --connection=sqlite::memory: 26 | ``` -------------------------------------------------------------------------------- /example/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ddbctest", 3 | "description": "example for DB Connector for D language, similar to JDBC", 4 | "authors": ["Vadim Lopatin","Laeeth Isharc"], 5 | "homepage": "https://github.com/buggins/ddbc", 6 | "license": "Boost Software License (BSL 1.0)", 7 | "dependencies": { 8 | "ddbc": {"version": "~master", "path": "../"}, 9 | "vibe-core": "1.22.6" 10 | }, 11 | "targetType": "executable", 12 | "buildRequirements": [ 13 | "allowWarnings" 14 | ], 15 | "versions": ["VibeCustomMain"], 16 | "configurations": [ 17 | { 18 | "name": "default", 19 | "subConfigurations": { 20 | "ddbc": "full" 21 | } 22 | }, 23 | 24 | { 25 | "name": "default-omf", 26 | "subConfigurations": { 27 | "ddbc": "full-omf" 28 | } 29 | }, 30 | 31 | { 32 | "name": "MySQL", 33 | "subConfigurations": { 34 | "ddbc": "MySQL" 35 | } 36 | }, 37 | 38 | { 39 | "name": "SQLite", 40 | "subConfigurations": { 41 | "ddbc": "SQLite" 42 | } 43 | }, 44 | 45 | { 46 | "name": "SQLite-omf", 47 | "subConfigurations": { 48 | "ddbc": "SQLite-omf" 49 | } 50 | }, 51 | 52 | { 53 | "name": "PGSQL", 54 | "subConfigurations": { 55 | "ddbc": "PGSQL" 56 | } 57 | }, 58 | 59 | { 60 | "name": "ODBC", 61 | "subConfigurations": { 62 | "ddbc": "ODBC" 63 | } 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /example/source/testddbc.d: -------------------------------------------------------------------------------- 1 | import ddbc.all; 2 | import std.stdio; 3 | import std.conv; 4 | import std.datetime : Date, DateTime; 5 | import std.datetime.systime : SysTime, Clock; 6 | import std.algorithm; 7 | import std.getopt; 8 | import std.string; 9 | 10 | string getURIPrefix(string uri) 11 | { 12 | auto i=uri.indexOf(":"); 13 | if (i==-1) 14 | return ""; 15 | return uri[0..i]; 16 | } 17 | 18 | string getURIHost(string uri) 19 | { 20 | auto i=uri.indexOf(":"); 21 | auto j=uri.lastIndexOf(":"); 22 | if ((i==-1)||(i==uri.length)) { 23 | return uri; 24 | } else if(j > i) { 25 | return uri[i+1..j].replace("//", ""); 26 | } else { 27 | return uri[i+1..$].replace("//", ""); 28 | } 29 | } 30 | 31 | ushort getURIPort(string uri, bool useDefault) 32 | { 33 | auto i=uri.indexOf(":"); 34 | auto j=uri.lastIndexOf(":"); 35 | if ((j==i)||(j==uri.length)||(j==-1)||!uri[j+1..$].isNumeric) 36 | { 37 | if (useDefault) 38 | return getDefaultPort(getURIPrefix(uri)); 39 | else 40 | throw new Exception("No port specified when parsing URI and useDefault was not specified"); 41 | } 42 | 43 | return to!ushort(uri[j+1..$]); 44 | } 45 | 46 | short getDefaultPort(string driver) 47 | { 48 | switch(driver) 49 | { 50 | case "sqlite": 51 | return -1; 52 | case "postgresql": 53 | return 5432; 54 | case "mysql": 55 | return 3306; 56 | case "odbc": 57 | return 1433; 58 | default: 59 | return -1; 60 | } 61 | } 62 | 63 | string syntaxMessage = "\nsyntax:\n" ~ 64 | "\nneither:\n" ~ 65 | "\tddbctest --connection=sqlite://relative/path/to/file\n" ~ 66 | "or:\n" ~ 67 | "\tddbctest --connection=sqlite::memory:\n" ~ 68 | "or:\n" ~ 69 | "\tddbctest --connection= --database= --user= --password= [--port=]\n\n" ~ 70 | "\tURI is format 'mysql://hostname:port' or 'postgres://hostname'\n" ~ 71 | "\tAccepted drivers are [sqlite|postgresql|mysql|odbc]\n" ~ 72 | "\tODBC driver connection also require a --driver param with a value like FreeTDS or msodbcsql17" ~ 73 | "\tdatabase name must not be specifed for sqlite and must be specified for other drivers\n"; 74 | 75 | struct ConnectionParams 76 | { 77 | string user; 78 | string password; 79 | bool ssl; 80 | string driver; 81 | string odbcdriver; 82 | string host; 83 | ushort port; 84 | string database; 85 | string trust_server_certificate; // for MS-SQL can be set to 'yes' 86 | } 87 | int main(string[] args) 88 | { 89 | static if (__traits(compiles, (){ import std.logger; } )) { 90 | import std.logger; 91 | } else { 92 | import std.experimental.logger; 93 | } 94 | 95 | globalLogLevel(LogLevel.all); 96 | 97 | ConnectionParams par; 98 | string URI; 99 | Driver driver; 100 | 101 | try 102 | { 103 | getopt(args, "user",&par.user, "password",&par.password, "ssl",&par.ssl, 104 | "connection",&URI, "database",&par.database, "driver",&par.odbcdriver, 105 | "trust_server_certificate",&par.trust_server_certificate); 106 | } 107 | catch (GetOptException) 108 | { 109 | stderr.writefln(syntaxMessage); 110 | return 1; 111 | } 112 | 113 | if (URI.startsWith("ddbc:")) { 114 | URI = URI[5 .. $]; // strip out ddbc: prefix 115 | } 116 | 117 | par.driver=getURIPrefix(URI); 118 | par.host=getURIHost(URI); 119 | if (par.driver!="sqlite") 120 | par.port=getURIPort(URI,true); 121 | 122 | writefln("Database Driver: %s", par.driver); 123 | 124 | if (["sqlite","postgresql","mysql","odbc"].count(par.driver)==0) 125 | { 126 | stderr.writefln(syntaxMessage); 127 | stderr.writefln("\n\t*** Error: unknown driver type:"~par.driver); 128 | return 1; 129 | } 130 | 131 | string[string] params; 132 | string url; 133 | switch(par.driver) 134 | { 135 | case "sqlite": 136 | if (par.host.length==0) 137 | { 138 | stderr.writefln(syntaxMessage); 139 | stderr.writefln("\n *** Error: must specify file name in format --connection=sqlite://path/to/file"); 140 | stderr.writefln("\n"); 141 | return 1; 142 | } 143 | if (par.database.length>0) 144 | { 145 | stderr.writefln(syntaxMessage); 146 | stderr.writef("\n *** Error: should not specify database name for sqlite: you specified - "~par.database); 147 | stderr.writefln("\n"); 148 | return 1; 149 | } 150 | version( USE_SQLITE ) { 151 | driver = new SQLITEDriver(); 152 | //url = chompPrefix(URI, "sqlite:"); 153 | //url = SQLITEDriver.generateUrl(); 154 | } 155 | url = chompPrefix(URI, "sqlite:"); 156 | break; 157 | 158 | case "postgresql": 159 | if ((par.host.length==0) || (par.database.length==0) ) 160 | { 161 | stderr.writefln(syntaxMessage); 162 | stderr.writefln("\n *** Error: must specify connection and database names for pgsql " ~ 163 | "eg --connection=postgresql://localhost:5432 -- database=test"); 164 | stderr.writefln("\n"); 165 | return 1; 166 | } 167 | version( USE_PGSQL ) { 168 | driver = new PGSQLDriver(); 169 | url = PGSQLDriver.generateUrl( par.host, par.port,par.database ); 170 | params["user"] = par.user; 171 | params["password"] = par.password; 172 | params["ssl"] = to!string(par.ssl); 173 | } 174 | break; 175 | 176 | case "mysql": 177 | if ((par.host.length==0) || (par.database.length==0) ) 178 | { 179 | stderr.writefln(syntaxMessage); 180 | stderr.writefln("\n *** Error: must specify connection and database names for mysql " ~ 181 | "eg --connection=mysql://localhost -- database=test"); 182 | stderr.writefln("\n"); 183 | return 1; 184 | } 185 | version( USE_MYSQL ) { 186 | driver = new MySQLDriver(); 187 | url = MySQLDriver.generateUrl(par.host, par.port, par.database); 188 | params = MySQLDriver.setUserAndPassword(par.user, par.password); 189 | } 190 | break; 191 | case "odbc": 192 | version( USE_ODBC ) { 193 | driver = new ODBCDriver(); 194 | if ((par.user.length==0) && (par.password.length==0) ) 195 | { 196 | // presume credentials are in connection string, eg: 197 | // ./ddbctest --connection=ddbc:odbc://localhost,1433?user=sa,password=bbk4k77JKH88g54,driver=FreeTDS 198 | url = URI; 199 | } else { 200 | if (par.odbcdriver.length==0) 201 | { 202 | stderr.writefln(syntaxMessage); 203 | stderr.writefln("\n *** Error: must specify ODBC driver in format --driver=FreeTDS\n"); 204 | return 1; 205 | } 206 | // build the connection string based on args, eg: 207 | // ./ddbctest --connection=ddbc:odbc://localhost --user=SA --password=bbk4k77JKH88g54 --driver=FreeTDS 208 | params = ODBCDriver.setUserAndPassword(par.user, par.password); 209 | params["driver"] = par.odbcdriver; 210 | params["trust_server_certificate"] = par.trust_server_certificate; 211 | url = ODBCDriver.generateUrl(par.host, par.port, params); 212 | } 213 | } 214 | break; 215 | default: 216 | stderr.writefln("%s is not a valid option!", par.driver); 217 | return 1; 218 | } 219 | 220 | if(driver is null) { 221 | stderr.writeln("No database driver found!"); 222 | return 1; 223 | } 224 | 225 | writefln("Database Connection String : %s", url); 226 | if(params !is null) { 227 | writeln("Database Params: ", params); 228 | } 229 | 230 | // create connection pool 231 | //DataSource ds = createConnectionPool(url, params); 232 | DataSource ds = new ConnectionPoolDataSourceImpl(driver, url, params); 233 | 234 | // creating Connection 235 | auto conn = ds.getConnection(); 236 | scope(exit) 237 | conn.close(); 238 | 239 | // creating Statement 240 | auto stmt = conn.createStatement(); 241 | scope(exit) 242 | stmt.close(); 243 | 244 | // execute simple queries to create and fill table 245 | writeln("Creating tables and data..."); 246 | 247 | final switch(par.driver) 248 | { 249 | case "sqlite": 250 | stmt.executeUpdate("DROP TABLE IF EXISTS ddbct1"); 251 | stmt.executeUpdate("CREATE TABLE IF NOT EXISTS ddbct1 (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name VARCHAR(250), comment MEDIUMTEXT, ts DATETIME)"); 252 | stmt.executeUpdate("INSERT INTO ddbct1 (name, comment, ts) 253 | VALUES 254 | ('name1', 'comment for line 1', CURRENT_TIMESTAMP), 255 | ('name2', 'comment for line 2 - can be very long', CURRENT_TIMESTAMP)"); 256 | 257 | stmt.executeUpdate("DROP TABLE IF EXISTS employee"); 258 | stmt.executeUpdate("CREATE TABLE IF NOT EXISTS employee ( 259 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 260 | name VARCHAR(255) NOT NULL, 261 | flags int null, 262 | dob DATE, 263 | created DATETIME, 264 | updated DATETIME 265 | )"); 266 | 267 | stmt.executeUpdate(`INSERT INTO employee (name, flags, dob, created, updated) 268 | VALUES 269 | ("John", 5, "1976-04-18", "2017-11-23T20:45", "2010-12-30T00:00:00Z"), 270 | ("Andrei", 2, "1977-09-11", "2018-02-28T13:45", "2010-12-30T12:10:12Z"), 271 | ("Walter", 2, "1986-03-21", "2018-03-08T10:30", "2010-12-30T12:10:04.100Z"), 272 | ("Rikki", 3, "1979-05-24", "2018-06-13T11:45", "2010-12-30T12:10:58Z"), 273 | ("Iain", 0, "1971-11-12", "2018-11-09T09:33", "20101230T121001Z"), 274 | ("Robert", 1, "1966-03-19", CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`); 275 | break; 276 | case "postgresql": 277 | stmt.executeUpdate("DROP TABLE IF EXISTS ddbct1"); 278 | stmt.executeUpdate("CREATE TABLE ddbct1 (id SERIAL PRIMARY KEY, name VARCHAR(250), comment TEXT, ts TIMESTAMP)"); 279 | stmt.executeUpdate("INSERT INTO ddbct1 (name, comment, ts) VALUES ('name1', 'comment for line 1', CURRENT_TIMESTAMP), ('name2','comment for line 2 - can be very long', CURRENT_TIMESTAMP)"); 280 | 281 | stmt.executeUpdate(`DROP TABLE IF EXISTS "employee"`); 282 | stmt.executeUpdate(`CREATE TABLE "employee" ( 283 | id SERIAL PRIMARY KEY, 284 | name VARCHAR(255) NOT NULL, 285 | flags int null, 286 | dob DATE, 287 | created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 288 | updated TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP 289 | )`); 290 | 291 | stmt.executeUpdate(`INSERT INTO "employee" ("name", "flags", "dob", "created", "updated") 292 | VALUES 293 | ('John', 5, '1976-04-18', TIMESTAMP '2017-11-23 20:45', TIMESTAMPTZ '2010-12-30 00:00:00'), 294 | ('Andrei', 2, '1977-09-11', TIMESTAMP '2018-02-28 13:45', TIMESTAMPTZ '2010-12-30 12:10:12'), 295 | ('Walter', 2, '1986-03-21', TIMESTAMP '2018-03-08 10:30', TIMESTAMPTZ '2010-12-30 12:10:04.100'), 296 | ('Rikki', 3, '1979-05-24', TIMESTAMP '2018-06-13 11:45', TIMESTAMPTZ '2010-12-30 12:10:58'), 297 | ('Iain', 0, '1971-11-12', TIMESTAMP '2018-11-09 09:33', TIMESTAMPTZ '2010-12-30 12:10:01'), 298 | ('Robert', 1, '1966-03-19', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`); 299 | break; 300 | case "mysql": // MySQL has an underscore in 'AUTO_INCREMENT' 301 | stmt.executeUpdate("DROP TABLE IF EXISTS ddbct1"); 302 | stmt.executeUpdate("CREATE TABLE IF NOT EXISTS ddbct1 ( 303 | `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, 304 | `name` VARCHAR(250), 305 | `comment` MEDIUMTEXT, 306 | `ts` TIMESTAMP NULL DEFAULT NULL 307 | )"); 308 | 309 | stmt.executeUpdate("INSERT INTO ddbct1 (`name`, `comment`, `ts`) VALUES ('name1', 'comment for line 1', CURRENT_TIMESTAMP), ('name2','comment for line 2 - can be very long', CURRENT_TIMESTAMP)"); 310 | 311 | stmt.executeUpdate("DROP TABLE IF EXISTS employee"); 312 | stmt.executeUpdate("CREATE TABLE IF NOT EXISTS employee ( 313 | `id` INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, 314 | `name` VARCHAR(255) NOT NULL, 315 | `flags` int null, 316 | `dob` DATE, 317 | `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 318 | `updated` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 319 | )"); 320 | 321 | stmt.executeUpdate("INSERT INTO employee (`name`, `flags`, `dob`, `created`, `updated`) 322 | VALUES 323 | ('John', 5, '1976-04-18', '2017-11-23T20:45', '2010-12-30T00:00:00'), 324 | ('Andrei', 2, '1977-09-11', '2018-02-28T13:45', '2010-12-30T12:10:12'), 325 | ('Walter', 2, '1986-03-21', '2018-03-08T10:30', '2010-12-30T12:10:04.100'), 326 | ('Rikki', 3, '1979-05-24', '2018-06-13T11:45', '2010-12-30T12:10:58'), 327 | ('Iain', 0, '1971-11-12', '2018-11-09T09:33', '2010-12-30T12:10:01'), 328 | ('Robert', 1, '1966-03-19', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"); 329 | break; 330 | case "odbc": 331 | stmt.executeUpdate("DROP TABLE IF EXISTS [ddbct1]"); 332 | stmt.executeUpdate("CREATE TABLE ddbct1 ( 333 | [id] INT NOT NULL IDENTITY(1,1) PRIMARY KEY, 334 | [name] VARCHAR(250), 335 | [comment] VARCHAR(max), 336 | [ts] DATETIME 337 | )"); 338 | 339 | stmt.executeUpdate("INSERT INTO [ddbct1] ([name], [comment], [ts]) 340 | VALUES 341 | ('name1', 'comment for line 1', CURRENT_TIMESTAMP), 342 | ('name2','comment for line 2 - can be very long', CURRENT_TIMESTAMP)"); 343 | 344 | stmt.executeUpdate("DROP TABLE IF EXISTS [employee]"); 345 | stmt.executeUpdate("CREATE TABLE [employee] ( 346 | [id] INT NOT NULL IDENTITY(1,1) PRIMARY KEY, 347 | [name] VARCHAR(255) NOT NULL, 348 | [flags] int null, 349 | [dob] DATE, 350 | [created] DATETIME default CURRENT_TIMESTAMP, 351 | [updated] DATETIMEOFFSET default CURRENT_TIMESTAMP 352 | )"); 353 | 354 | stmt.executeUpdate(`INSERT INTO [employee] ([name], [flags], [dob], [created], [updated]) 355 | VALUES 356 | ('John', 5, '1976-04-18', '2017-11-23 20:45', '2010-12-30 00:00:00'), 357 | ('Andrei', 2, '1977-09-11', '2018-02-28 13:45', '2010-12-30 12:10:12'), 358 | ('Walter', 2, '1986-03-21', '2018-03-08 10:30', '2010-12-30 12:10:04.100'), 359 | ('Rikki', 3, '1979-05-24', '2018-06-13 11:45', '2010-12-30 12:10:58'), 360 | ('Iain', 0, '1971-11-12', '2018-11-09 09:33', '2010-12-30 12:10:01'), 361 | ('Robert', 1, '1966-03-19', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`); 362 | break; 363 | } 364 | write("Done.\n"); 365 | 366 | writeln(" > Testing generic SQL select statements"); 367 | 368 | ResultSet rs = stmt.executeQuery("SELECT * FROM ddbct1"); 369 | 370 | int i = 0; 371 | 372 | while (rs.next()) { 373 | writeln(" - id: " ~ to!string(rs.getLong(1)) ~ "\t" ~ rs.getString(2)); 374 | i++; 375 | } 376 | writefln("\tThere were %,d rows returned from the ddbct1 table...", i); 377 | 378 | if("mysql" == par.driver || "postgresql" == par.driver ) { 379 | ulong count = rs.getFetchSize(); // only works on Mysql & Postgres 380 | assert(i == count, "fetchSize should give the correct row count"); 381 | } 382 | assert(2 == i, "There should be 2 results but instead there was " ~ to!string(i)); 383 | 384 | 385 | rs = stmt.executeQuery("SELECT id,comment FROM ddbct1 WHERE id = 2"); 386 | i = 0; 387 | while (rs.next()) { 388 | writeln(" - id: " ~ to!string(rs.getLong(1)) ~ "\t" ~ rs.getString(2)); 389 | i++; 390 | } 391 | assert(1 == i, "There should be 1 result but instead there was " ~ to!string(i)); 392 | 393 | 394 | i = 0; 395 | rs = stmt.executeQuery("SELECT id, comment, ts FROM ddbct1 ORDER BY id DESC"); 396 | while (rs.next()) { 397 | writeln(" - id: " ~ to!string(rs.getLong(1)) ~ "\t" ~ rs.getString(2) ~ "\t" ~ to!string(rs.getDateTime(3))); 398 | i++; 399 | } 400 | assert(2 == i, "There should be 2 results but instead there was " ~ to!string(i)); 401 | 402 | // make sure that a timestamp can handle being NULL 403 | stmt.executeUpdate("UPDATE ddbct1 SET ts=NULL"); 404 | i = 0; 405 | rs = stmt.executeQuery("SELECT id, name, comment, ts FROM ddbct1 WHERE ts IS NULL"); 406 | while (rs.next()) { 407 | SysTime now = Clock.currTime(); 408 | DateTime dtNow = cast(DateTime) now; 409 | // if the column on the table is NULL ddbc will create a DateTime using Clock.currTime() 410 | // https://github.com/buggins/ddbc/issues/86 411 | assert(rs.getDateTime(4).year == dtNow.year); 412 | assert(rs.getDateTime(4).month == dtNow.month); 413 | assert(rs.getDateTime(4).day == dtNow.day); 414 | assert(rs.getDateTime(4).hour == dtNow.hour); 415 | writeln(" - id: " ~ to!string(rs.getLong(1)) ~ "\t" ~ rs.getString(2) ~ "\t" ~ to!string(rs.getDateTime(4))); 416 | 417 | // ddbc should also allow you to retrive the timestamp as a SysTime (defaulting UTC if no zone info given) 418 | assert(rs.getSysTime(4).year == now.year); 419 | assert(rs.getSysTime(4).month == now.month); 420 | assert(rs.getSysTime(4).day == now.day); 421 | //assert(rs.getSysTime(4).hour == now.hour); 422 | i++; 423 | } 424 | assert(2 == i, "There should be 2 results but instead there was " ~ to!string(i)); 425 | if(par.driver != "odbc") { 426 | // ODBC doesn't support ResultSet::getFetchSize() 427 | ulong fetchSize = rs.getFetchSize(); 428 | assert(i == fetchSize, "ResultSet::getFetchSize() should have returned " ~ to!string(i) ~ " but was " ~ to!string(fetchSize)); 429 | } 430 | 431 | writeln("\n > Testing prepared SQL statements"); 432 | PreparedStatement ps2 = conn.prepareStatement("SELECT id, name name_alias, comment, ts FROM ddbct1 WHERE id >= ?"); 433 | scope(exit) ps2.close(); 434 | ps2.setUlong(1, 1); 435 | auto prs = ps2.executeQuery(); 436 | while (prs.next()) { 437 | writeln(" - id: " ~ to!string(prs.getLong(1)) ~ "\t" ~ prs.getString(2) ~ "\t" ~ prs.getString(3) ~ "\t" ~ to!string(prs.getDateTime(4))); 438 | } 439 | 440 | writeln("\n > Testing basic POD support"); 441 | 442 | // our POD object 443 | struct Employee { 444 | long id; 445 | string name; 446 | int flags; 447 | Date dob; 448 | DateTime created; 449 | SysTime updated; 450 | } 451 | 452 | immutable DateTime now = cast(DateTime) Clock.currTime(); 453 | 454 | writeln(" > select all rows from employee table"); 455 | foreach(ref e; conn.createStatement().select!Employee) { 456 | //SysTime nextMonth = now.add!"months"(1); 457 | 458 | writeln(" - {id: ", e.id, ", name: ", e.name, ", flags: ", e.flags, ", dob: ", e.dob, ", created: ", e.created, ", updated: ", e.updated, "}"); 459 | assert(e.name !is null); 460 | assert(e.dob.year > 1950); 461 | assert(e.created <= now); 462 | assert(cast(DateTime) e.updated <= now, "Updated '" ~ to!string(e.updated) ~ "' should be <= '" ~ to!string(now) ~ "'"); 463 | } 464 | 465 | i = 0; 466 | writeln(" > select all rows from employee table WHERE id < 4 ORDER BY name DESC..."); 467 | foreach(ref e; conn.createStatement().select!Employee.where("id < 4").orderBy("name desc")) { 468 | writeln(" - {id: ", e.id, ", name: ", e.name, ", flags: ", e.flags, ", dob: ", e.dob, ", created: ", e.created, ", updated: ", e.updated, "}"); 469 | assert(e.id < 4); 470 | assert(e.name != "Iain" && e.name != "Robert"); 471 | assert(e.flags > 1); 472 | i++; 473 | } 474 | assert(3 == i, "There should be 3 results but instead there was " ~ to!string(i)); 475 | 476 | i = 0; 477 | writeln(" > select all rows from employee table WHERE id < 4 LIMIT 2..."); 478 | foreach(ref e; conn.createStatement().select!Employee.where("id < 4").orderBy("id ASC").limit(2)) { 479 | writeln(" - {id: ", e.id, ", name: ", e.name, ", flags: ", e.flags, ", dob: ", e.dob, ", created: ", e.created, ", updated: ", e.updated, "}"); 480 | assert(e.id == 1 || e.id == 2, "results should start from id 1 as there's no offset"); 481 | assert(e.name != "Iain" && e.name != "Robert"); 482 | assert(e.flags > 1); 483 | i++; 484 | } 485 | assert(2 == i, "There should be 2 results but instead there was " ~ to!string(i)); 486 | 487 | i = 0; 488 | writeln(" > select all rows from employee table WHERE id < 4 LIMIT 2 OFFSET 1..."); 489 | foreach(ref e; conn.createStatement().select!Employee.where("id < 4").orderBy("id ASC").limit(2).offset(1)) { 490 | writeln(" - {id: ", e.id, ", name: ", e.name, ", flags: ", e.flags, ", dob: ", e.dob, ", created: ", e.created, ", updated: ", e.updated, "}"); 491 | assert(e.id == 2 || e.id == 3, "results should start from id 2 due to offset"); 492 | assert(e.name != "Iain" && e.name != "Robert"); 493 | assert(e.flags > 1); 494 | i++; 495 | } 496 | assert(2 == i, "There should be 2 results but instead there was " ~ to!string(i)); 497 | 498 | // todo: Fix the UPDATE/INSERT functionality for PODs 499 | // Employee e; 500 | // e.name = "Dave Smith"; 501 | // e.flags = 35; 502 | // e.dob = Date(1979, 8, 5); 503 | // e.created = cast(DateTime) now; 504 | // e.updated = now; 505 | 506 | // if(conn.createStatement().insert!Employee(e)) { 507 | // writeln("Successfully inserted new emplyee: \t{id: ", e.id, ", name: ", e.name, ", flags: ", e.flags, ", dob: ", e.dob, ", created: ", e.created, ", updated: ", e.updated, "}"); 508 | // } else { 509 | // write("Failed to INSERT employee"); 510 | // assert(false); 511 | // } 512 | 513 | writeln("Completed tests"); 514 | return 0; 515 | } 516 | -------------------------------------------------------------------------------- /examples/odbc_test/.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | ddbc_odbc_test 3 | dub.selections.json 4 | dub.userprefs 5 | /test_connection.txt -------------------------------------------------------------------------------- /examples/odbc_test/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ddbc_odbc_test", 3 | "description": "Testing ODBC support in DDBC", 4 | "homepage": "https://github.com/buggins/ddbc", 5 | "license": "Boost Software License (BSL 1.0)", 6 | "dependencies": { 7 | "ddbc": {"version": "~master", "path": "../../"} 8 | }, 9 | "targetType": "executable", 10 | "buildRequirements": [ 11 | "allowWarnings" 12 | ], 13 | "configurations": [ 14 | { 15 | "name": "default", 16 | "subConfigurations": { 17 | "ddbc": "full" 18 | } 19 | } 20 | ], 21 | "libs": [ 22 | "odbc" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /examples/odbc_test/source/main.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | 3 | import ddbc; 4 | import std.file; 5 | 6 | int main(string[] argv) 7 | { 8 | //string url = "odbc://server-address/NamedInstance?user=sa,password=test,driver=FreeTDS,database=unittest"; 9 | string url = "ddbc:odbc://localhost,1433?user=sa,password=bbk4k77JKH88g54,driver=FreeTDS"; 10 | 11 | //url = cast(string)read("test_connection.txt"); // catch FileException 12 | 13 | //string url = "postgresql://localhost:5432/ddbctestdb?user=ddbctest,password=ddbctestpass,ssl=true"; 14 | //string url = "mysql://localhost:3306/ddbctestdb?user=ddbctest,password=ddbctestpass"; 15 | //string url = "sqlite:testdb.sqlite"; 16 | immutable string driverName = extractDriverNameFromURL(url); 17 | 18 | // creating Connection 19 | //auto conn = ds.getConnection(); 20 | Connection conn = createConnection(url); 21 | scope (exit) 22 | conn.close(); 23 | 24 | // creating Statement 25 | auto stmt = conn.createStatement(); 26 | scope (exit) 27 | stmt.close(); 28 | 29 | import std.conv : to; 30 | 31 | writeln("Hello D-World!"); 32 | // execute simple queries to create and fill table 33 | 34 | stmt.executeUpdate("IF OBJECT_ID('ddbct1', 'U') IS NOT NULL DROP TABLE ddbct1"); 35 | 36 | stmt.executeUpdate("CREATE TABLE ddbct1 37 | (id bigint NOT NULL PRIMARY KEY, 38 | name VARCHAR(250), 39 | comment VARCHAR(max), 40 | ts DATETIME)"); 41 | //conn.commit(); 42 | stmt.executeUpdate("INSERT INTO ddbct1 (id, name, comment, ts) VALUES 43 | (1, 'aaa', 'comment for line 1', '2016/09/14 15:24:01')"); 44 | stmt.executeUpdate("INSERT INTO ddbct1 (id, name, comment, ts) VALUES 45 | (2, 'bbb', 'comment for line 2 - can be very long', '2016/09/14 15:24:01')"); 46 | stmt.executeUpdate("INSERT INTO ddbct1 (id, comment, ts) VALUES 47 | (3, 'Hello World', '2016/09/14 15:24:01')"); 48 | 49 | // reading DB 50 | //auto rs = stmt.executeQuery("SELECT * FROM ddbct1"); 51 | auto rs = stmt.executeQuery("SELECT id, name name_alias, comment, ts FROM ddbct1"); 52 | 53 | // testing result set meta data 54 | ResultSetMetaData meta = rs.getMetaData(); 55 | assert(meta.getColumnCount() == 4); 56 | assert(meta.getColumnName(1) == "id"); 57 | assert(meta.getColumnLabel(1) == "id"); 58 | assert(meta.isNullable(1) == false); 59 | assert(meta.isNullable(2) == true); 60 | assert(meta.isNullable(3) == true); 61 | assert(meta.getColumnName(2) == "name_alias"); 62 | assert(meta.getColumnLabel(2) == "name_alias"); 63 | assert(meta.getColumnName(3) == "comment"); 64 | assert(meta.getColumnName(4) == "ts"); 65 | 66 | scope(exit) rs.close(); 67 | 68 | while (rs.next()) 69 | { 70 | writeln(rs.getVariant(1), "\t", rs.getVariant(2), "\t", rs.getString(3), "\t", rs.getVariant(4)); 71 | } 72 | 73 | //prepared statement 74 | PreparedStatement ps = conn.prepareStatement("UPDATE ddbct1 SET name=? WHERE id=?"); 75 | ps.setString(1, "ccc"); 76 | ps.setLong(2, 3); 77 | assert(ps.executeUpdate() == 1); 78 | 79 | PreparedStatement ps2 = conn.prepareStatement("SELECT id, name, comment FROM ddbct1 WHERE id >= ?"); 80 | scope (exit) 81 | ps2.close(); 82 | ps2.setLong(1, 3); 83 | auto rs2 = ps2.executeQuery(); 84 | scope(exit) 85 | rs2.close(); 86 | assert(rs2.getMetaData().getColumnCount() == 3); 87 | 88 | // ODBC bytea blobs test 89 | 90 | // fill database with test data 91 | stmt.executeUpdate(`IF OBJECT_ID('user_data', 'U') IS NOT NULL DROP TABLE user_data`); 92 | stmt.executeUpdate(`CREATE TABLE user_data (id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL, flags int)`); 93 | stmt.executeUpdate(`INSERT INTO user_data (id, name, flags) VALUES 94 | (1, 'John', 5), 95 | (2, 'Andrei', 2), 96 | (3, 'Walter', 2), 97 | (4, 'Rikki', 3), 98 | (5, 'Iain', 0), 99 | (6, 'Robert', 1)`); 100 | 101 | // our POD object 102 | struct UserData 103 | { 104 | long id; 105 | string name; 106 | int flags; 107 | } 108 | 109 | writeln("reading all user table rows"); 110 | foreach (ref e; stmt.select!UserData) 111 | { 112 | writeln("id:", e.id, " name:", e.name, " flags:", e.flags); 113 | } 114 | 115 | writeln("reading user table rows with where and order by"); 116 | foreach (ref e; stmt.select!UserData.where("id < 6").orderBy("name desc")) 117 | { 118 | writeln("id:", e.id, " name:", e.name, " flags:", e.flags); 119 | } 120 | 121 | writeln("reading all user table rows, but fetching only id and name (you will see default value 0 in flags field)"); 122 | foreach (ref e; stmt.select!(UserData, "id", "name")) 123 | { 124 | writeln("id:", e.id, " name:", e.name, " flags:", e.flags); 125 | } 126 | 127 | return 0; 128 | } 129 | -------------------------------------------------------------------------------- /examples/pgsql_test/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ddbc_pgsql_test", 3 | "description": "Testing Postgress support in DDBC", 4 | "homepage": "https://github.com/buggins/ddbc", 5 | "license": "Boost Software License (BSL 1.0)", 6 | "dependencies": { 7 | "ddbc": {"version": "~master", "path": "../../"} 8 | }, 9 | "targetType": "executable", 10 | "buildRequirements": [ 11 | "allowWarnings" 12 | ], 13 | "configurations": [ 14 | { 15 | "name": "default", 16 | "subConfigurations": { 17 | "ddbc": "full" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/pgsql_test/source/main.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | 3 | import ddbc; 4 | 5 | int main(string[] argv) { 6 | string url = "postgresql://localhost:5432/ddbctestdb?user=ddbctest,password=ddbctestpass,ssl=true"; 7 | //string url = "mysql://localhost:3306/ddbctestdb?user=ddbctest,password=ddbctestpass"; 8 | //string url = "sqlite:testdb.sqlite"; 9 | immutable string driverName = extractDriverNameFromURL(url); 10 | 11 | // creating Connection 12 | //auto conn = ds.getConnection(); 13 | Connection conn = createConnection(url); 14 | scope(exit) conn.close(); 15 | 16 | // creating Statement 17 | auto stmt = conn.createStatement(); 18 | scope(exit) stmt.close(); 19 | 20 | import std.conv : to; 21 | 22 | // execute simple queries to create and fill table 23 | stmt.executeUpdate("DROP TABLE IF EXISTS ddbct1"); 24 | stmt.executeUpdate("CREATE TABLE ddbct1 25 | (id bigint not null primary key, 26 | name varchar(250), 27 | comment text, 28 | ts timestamp)"); 29 | //conn.commit(); 30 | stmt.executeUpdate("INSERT INTO ddbct1 (id, name, comment, ts) VALUES 31 | (1, 'name1', 'comment for line 1', '2016/09/14 15:24:01')"); 32 | stmt.executeUpdate("INSERT INTO ddbct1 (id, name, comment) VALUES 33 | (2, 'name2', 'comment for line 2 - can be very long')"); 34 | stmt.executeUpdate("INSERT INTO ddbct1 (id, name) values(3, 'name3')"); // comment is null here 35 | 36 | // reading DB 37 | auto rs = stmt.executeQuery("SELECT id, name name_alias, comment, ts FROM ddbct1 ORDER BY id"); 38 | assert(rs.getMetaData().getColumnCount() == 4); 39 | assert(rs.getMetaData().getColumnName(1) == "id"); 40 | assert(rs.getMetaData().getColumnName(2) == "name_alias"); 41 | assert(rs.getMetaData().getColumnName(3) == "comment"); 42 | assert(rs.getMetaData().getColumnName(3) == "ts"); 43 | 44 | scope(exit) rs.close(); 45 | 46 | while (rs.next()) 47 | writeln(to!string(rs.getLong(1)), "\t", rs.getString(2), "\t", rs.getString(3), "\t", rs.getString(4)); 48 | 49 | // PostgreSQL bytea blobs test 50 | if (driverName == "postgresql") { 51 | ubyte[] bin_data = [1, 2, 3, 'a', 'b', 'c', 0xFE, 0xFF, 0, 1, 2]; 52 | stmt.executeUpdate("DROP TABLE IF EXISTS bintest"); 53 | stmt.executeUpdate("CREATE TABLE bintest (id bigint not null primary key, blob1 bytea)"); 54 | PreparedStatement ps = conn.prepareStatement("INSERT INTO bintest (id, blob1) VALUES (1, ?)"); 55 | ps.setUbytes(1, bin_data); 56 | ps.executeUpdate(); 57 | struct Bintest { 58 | long id; 59 | ubyte[] blob1; 60 | } 61 | Bintest[] rows; 62 | foreach(e; stmt.select!Bintest) 63 | rows ~= e; 64 | //stmt! 65 | auto rs2 = stmt.executeQuery("SELECT id, blob1 FROM bintest WHERE id=1"); 66 | if (rs2.next()) { 67 | ubyte[] res = rs2.getUbytes(2); 68 | assert(res == bin_data); 69 | } 70 | } 71 | 72 | // PostgreSQL uuid type test 73 | if (driverName == "postgresql") { 74 | stmt.executeUpdate("DROP TABLE IF EXISTS guidtest"); 75 | stmt.executeUpdate("CREATE TABLE guidtest (guid uuid not null primary key, name text)"); 76 | stmt.executeUpdate("INSERT INTO guidtest (guid, name) VALUES ('cd3c7ffd-7919-f6c5-999d-5586d9f3b261', 'vasia')"); 77 | struct Guidtest { 78 | string guid; 79 | string name; 80 | } 81 | Guidtest[] guidrows; 82 | foreach(e; stmt.select!Guidtest) 83 | guidrows ~= e; 84 | writeln(guidrows); 85 | } 86 | 87 | // fill database with test data 88 | stmt.executeUpdate(`DROP TABLE IF EXISTS user_data`); 89 | stmt.executeUpdate(`CREATE TABLE user_data (id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL, flags int null)`); 90 | stmt.executeUpdate(`INSERT INTO user_data (id, name, flags) VALUES (1, 'John', 5), (2, 'Andrei', 2), (3, 'Walter', 2), (4, 'Rikki', 3), (5, 'Iain', 0), (6, 'Robert', 1)`); 91 | 92 | // our POD object 93 | struct UserData { 94 | long id; 95 | string name; 96 | int flags; 97 | } 98 | 99 | writeln("reading all user table rows"); 100 | foreach(ref e; stmt.select!UserData) { 101 | writeln("id:", e.id, " name:", e.name, " flags:", e.flags); 102 | } 103 | 104 | writeln("reading user table rows with where and order by"); 105 | foreach(ref e; stmt.select!UserData.where("id < 6").orderBy("name desc")) { 106 | writeln("id:", e.id, " name:", e.name, " flags:", e.flags); 107 | } 108 | 109 | writeln("reading all user table rows, but fetching only id and name (you will see default value 0 in flags field)"); 110 | foreach(ref e; stmt.select!(UserData, "id", "name")) { 111 | writeln("id:", e.id, " name:", e.name, " flags:", e.flags); 112 | } 113 | 114 | 115 | return 0; 116 | } 117 | -------------------------------------------------------------------------------- /libs/win32-mscoff/sqlite3.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buggins/ddbc/7574333f6d95b6beddfd27777eb38feb6fd30d4d/libs/win32-mscoff/sqlite3.lib -------------------------------------------------------------------------------- /libs/win32/libpq.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buggins/ddbc/7574333f6d95b6beddfd27777eb38feb6fd30d4d/libs/win32/libpq.dll -------------------------------------------------------------------------------- /libs/win32/sqlite3.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buggins/ddbc/7574333f6d95b6beddfd27777eb38feb6fd30d4d/libs/win32/sqlite3.dll -------------------------------------------------------------------------------- /libs/win32/sqlite3.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buggins/ddbc/7574333f6d95b6beddfd27777eb38feb6fd30d4d/libs/win32/sqlite3.lib -------------------------------------------------------------------------------- /libs/win64/libpq.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buggins/ddbc/7574333f6d95b6beddfd27777eb38feb6fd30d4d/libs/win64/libpq.dll -------------------------------------------------------------------------------- /libs/win64/sqlite3.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buggins/ddbc/7574333f6d95b6beddfd27777eb38feb6fd30d4d/libs/win64/sqlite3.dll -------------------------------------------------------------------------------- /libs/win64/sqlite3.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buggins/ddbc/7574333f6d95b6beddfd27777eb38feb6fd30d4d/libs/win64/sqlite3.lib -------------------------------------------------------------------------------- /source/ddbc/all.d: -------------------------------------------------------------------------------- 1 | /** 2 | * DDBC - D DataBase Connector - abstraction layer for RDBMS access, with interface similar to JDBC. 3 | * 4 | * Source file ddbc/all.d. 5 | * 6 | * DDBC library provides implementation independent interface to different databases. 7 | * 8 | * This module allows to import all necessary modules. 9 | * 10 | * This module is deprecated. Use `import ddbc;` instead. 11 | * 12 | * Copyright: Copyright 2014 13 | * License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 14 | * Author: Vadim Lopatin 15 | */ 16 | module ddbc.all; 17 | 18 | public import ddbc.core; 19 | public import ddbc.common; 20 | public import ddbc.pods; 21 | 22 | version( USE_SQLITE ) 23 | { 24 | public import ddbc.drivers.sqliteddbc; 25 | } 26 | version( USE_PGSQL ) 27 | { 28 | public import ddbc.drivers.pgsqlddbc; 29 | } 30 | version(USE_MYSQL) 31 | { 32 | public import ddbc.drivers.mysqlddbc; 33 | } 34 | version(USE_ODBC) 35 | { 36 | public import ddbc.drivers.odbcddbc; 37 | } -------------------------------------------------------------------------------- /source/ddbc/common.d: -------------------------------------------------------------------------------- 1 | /** 2 | * DDBC - D DataBase Connector - abstraction layer for RDBMS access, with interface similar to JDBC. 3 | * 4 | * Source file ddbc/common.d. 5 | * 6 | * DDBC library attempts to provide implementation independent interface to different databases. 7 | * 8 | * Set of supported RDBMSs can be extended by writing Drivers for particular DBs. 9 | * Currently it only includes MySQL Driver which uses patched version of MYSQLN (native D implementation of MySQL connector, written by Steve Teale) 10 | * 11 | * JDBC documentation can be found here: 12 | * $(LINK http://docs.oracle.com/javase/1.5.0/docs/api/java/sql/package-summary.html)$(BR) 13 | * 14 | * This module contains some useful base class implementations for writing Driver for particular RDBMS. 15 | * As well it contains useful class - ConnectionPoolDataSourceImpl - which can be used as connection pool. 16 | * 17 | * You can find usage examples in unittest{} sections. 18 | * 19 | * Copyright: Copyright 2013 20 | * License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 21 | * Author: Vadim Lopatin 22 | */ 23 | module ddbc.common; 24 | import ddbc.core; 25 | import std.algorithm; 26 | import std.exception; 27 | static if (__traits(compiles, (){ import std.logger; } )) { 28 | import std.logger; 29 | } else { 30 | import std.experimental.logger; 31 | } 32 | 33 | import std.stdio; 34 | import std.conv; 35 | import std.variant; 36 | 37 | /// Implementation of simple DataSource: it just holds connection parameters, and can create new Connection by getConnection(). 38 | /// Method close() on such connection will really close connection. 39 | class DataSourceImpl : DataSource { 40 | Driver driver; 41 | string url; 42 | string[string] params; 43 | this(Driver driver, string url, string[string]params) { 44 | this.driver = driver; 45 | this.url = url; 46 | this.params = params; 47 | } 48 | override Connection getConnection() { 49 | return driver.connect(url, params); 50 | } 51 | } 52 | 53 | /// Delegate type to create DDBC driver instance. 54 | alias DriverFactoryDelegate = Driver delegate(); 55 | /// DDBC Driver factory. 56 | /// Can create driver by name or DDBC URL. 57 | class DriverFactory { 58 | private __gshared static DriverFactoryDelegate[string] _factoryMap; 59 | 60 | /// Registers driver factory by URL prefix, e.g. "mysql", "postgresql", "sqlite" 61 | /// Use this method to register your own custom drivers 62 | static void registerDriverFactory(string name, DriverFactoryDelegate factoryDelegate) { 63 | _factoryMap[name] = factoryDelegate; 64 | } 65 | /// Factory method to create driver by registered name found in ddbc url, e.g. "mysql", "postgresql", "sqlite" 66 | /// List of available drivers depend on configuration 67 | static Driver createDriverForURL(string url) { 68 | return createDriver(extractDriverNameFromURL(url)); 69 | } 70 | /// Factory method to create driver by registered name, e.g. "mysql", "postgresql", "sqlite" 71 | /// List of available drivers depend on configuration 72 | static Driver createDriver(string driverName) { 73 | if (auto p = (driverName in _factoryMap)) { 74 | // found: call delegate to create driver 75 | return (*p)(); 76 | } else { 77 | throw new SQLException("DriverFactory: driver is not found for name \"" ~ driverName ~ "\""); 78 | } 79 | } 80 | } 81 | 82 | /// To be called on connection close 83 | interface ConnectionCloseHandler { 84 | void onConnectionClosed(Connection connection); 85 | } 86 | 87 | /// Wrapper class for connection 88 | class ConnectionWrapper : Connection { 89 | private ConnectionCloseHandler pool; 90 | private Connection base; 91 | private bool closed; 92 | 93 | this(ConnectionCloseHandler pool, Connection base) { 94 | this.pool = pool; 95 | this.base = base; 96 | } 97 | 98 | // a db connection is DialectAware 99 | override DialectType getDialectType() { 100 | return base.getDialectType(); 101 | } 102 | 103 | override void close() { 104 | assert(!closed, "Connection is already closed"); 105 | closed = true; 106 | pool.onConnectionClosed(base); 107 | } 108 | override PreparedStatement prepareStatement(string query) { return base.prepareStatement(query); } 109 | override void commit() { base.commit(); } 110 | override Statement createStatement() { return base.createStatement(); } 111 | override string getCatalog() { return base.getCatalog(); } 112 | override bool isClosed() { return closed; } 113 | override void rollback() { base.rollback(); } 114 | override bool getAutoCommit() { return base.getAutoCommit(); } 115 | override void setAutoCommit(bool autoCommit) { base.setAutoCommit(autoCommit); } 116 | override void setCatalog(string catalog) { base.setCatalog(catalog); } 117 | override TransactionIsolation getTransactionIsolation() { return base.getTransactionIsolation(); } 118 | override void setTransactionIsolation(TransactionIsolation level) { 119 | base.setTransactionIsolation(level); 120 | } 121 | } 122 | 123 | // remove array item inplace 124 | static void myRemove(T)(ref T[] array, size_t index) { 125 | for (auto i = index; i < array.length - 1; i++) { 126 | array[i] = array[i + 1]; 127 | } 128 | array[$ - 1] = T.init; 129 | array.length = array.length - 1; 130 | } 131 | 132 | // remove array item inplace 133 | static void myRemove(T : Object)(ref T[] array, T item) { 134 | int index = -1; 135 | for (int i = 0; i < array.length; i++) { 136 | if (array[i] is item) { 137 | index = i; 138 | break; 139 | } 140 | } 141 | if (index < 0) 142 | return; 143 | for (auto i = index; i < array.length - 1; i++) { 144 | array[i] = array[i + 1]; 145 | } 146 | array[$ - 1] = T.init; 147 | array.length = array.length - 1; 148 | } 149 | 150 | // TODO: implement limits 151 | // TODO: thread safety 152 | /// Simple connection pool DataSource implementation. 153 | /// When close() is called on connection received from this pool, it will be returned to pool instead of closing. 154 | /// Next getConnection() will just return existing connection from pool, instead of slow connection establishment process. 155 | class ConnectionPoolDataSourceImpl : DataSourceImpl, ConnectionCloseHandler { 156 | private: 157 | int maxPoolSize; 158 | int timeToLive; 159 | int waitTimeOut; 160 | 161 | Connection [] activeConnections; 162 | Connection [] freeConnections; 163 | 164 | public: 165 | 166 | this(Driver driver, string url, string[string]params = null, int maxPoolSize = 1, int timeToLive = 600, int waitTimeOut = 30) { 167 | super(driver, url, params); 168 | this.maxPoolSize = maxPoolSize; 169 | this.timeToLive = timeToLive; 170 | this.waitTimeOut = waitTimeOut; 171 | } 172 | 173 | /** 174 | * If a previously closed connection is available, test it and return it instead of creating a 175 | * new connection. At most, one connection will be tested before creating a new connection, in 176 | * order to prevent large delays due to having to test multiple connections. 177 | */ 178 | override Connection getConnection() { 179 | Connection conn = null; 180 | //writeln("getConnection(): freeConnections.length = " ~ to!string(freeConnections.length)); 181 | if (freeConnections.length > 0) { 182 | tracef("Retrieving database connection from pool of %s", freeConnections.length); 183 | conn = freeConnections[freeConnections.length - 1]; // $ - 1 184 | auto oldSize = freeConnections.length; 185 | myRemove(freeConnections, freeConnections.length - 1); 186 | //freeConnections.length = oldSize - 1; // some bug in remove? length is not decreased... 187 | auto newSize = freeConnections.length; 188 | assert(newSize == oldSize - 1); 189 | 190 | // Test the connection to make sure it is still alive. 191 | Statement testStatement = conn.createStatement(); 192 | scope (exit) testStatement.close(); 193 | try { 194 | ResultSet testResultSet = testStatement.executeQuery("SELECT 1;"); 195 | scope (exit) testResultSet.close(); 196 | } catch (Exception e) { 197 | // This connection is not usable, do not add it to active connections, 198 | // and do not cycle through the rest of the freeConnections to prevent 199 | // excess delays. 200 | trace("Exception trying to use freeConnection.", e); 201 | conn = null; 202 | } 203 | } 204 | 205 | // Either there were no free connections, or the one that was picked out was invalid. 206 | if (conn is null) { 207 | tracef("Creating new database connection (%s) %s %s", driver, url, params); 208 | 209 | try { 210 | conn = super.getConnection(); 211 | } catch (Throwable e) { 212 | errorf("could not create db connection : %s", e.msg); 213 | throw e; 214 | } 215 | //writeln("getConnection(): connection created"); 216 | } 217 | auto oldSize = activeConnections.length; 218 | activeConnections ~= conn; 219 | auto newSize = activeConnections.length; 220 | assert(oldSize == newSize - 1); 221 | auto wrapper = new ConnectionWrapper(this, conn); 222 | return wrapper; 223 | } 224 | 225 | void removeUsed(Connection connection) { 226 | foreach (i, item; activeConnections) { 227 | if (item == connection) { 228 | auto oldSize = activeConnections.length; 229 | //std.algorithm.remove(activeConnections, i); 230 | myRemove(activeConnections, i); 231 | //activeConnections.length = oldSize - 1; 232 | auto newSize = activeConnections.length; 233 | assert(oldSize == newSize + 1); 234 | tracef("database connections reduced from %s to %s", oldSize, newSize); 235 | return; 236 | } 237 | } 238 | throw new SQLException("Connection being closed is not found in pool"); 239 | } 240 | 241 | override void onConnectionClosed(Connection connection) { 242 | //writeln("onConnectionClosed"); 243 | assert(connection !is null); 244 | //writeln("calling removeUsed"); 245 | removeUsed(connection); 246 | //writeln("adding to free list"); 247 | auto oldSize = freeConnections.length; 248 | freeConnections ~= connection; 249 | auto newSize = freeConnections.length; 250 | assert(newSize == oldSize + 1); 251 | } 252 | } 253 | 254 | /// Helper implementation of ResultSet - throws Method not implemented for most of methods. 255 | /// Useful for driver implementations 256 | class ResultSetImpl : ddbc.core.ResultSet { 257 | private import std.datetime : DateTime, Date, TimeOfDay; 258 | private import std.datetime.systime : SysTime; 259 | 260 | public: 261 | override int opApply(int delegate(DataSetReader) dg) { 262 | int result = 0; 263 | if (!first()) 264 | return 0; 265 | do { 266 | result = dg(cast(DataSetReader)this); 267 | if (result) break; 268 | } while (next()); 269 | return result; 270 | } 271 | override void close() { 272 | throw new SQLException("Method not implemented"); 273 | } 274 | override bool first() { 275 | throw new SQLException("Method not implemented"); 276 | } 277 | override bool isFirst() { 278 | throw new SQLException("Method not implemented"); 279 | } 280 | override bool isLast() { 281 | throw new SQLException("Method not implemented"); 282 | } 283 | /// returns true if ResultSet object contains more rows 284 | override bool next() { 285 | throw new SQLException("Method not implemented"); 286 | } 287 | 288 | override int findColumn(string columnName) { 289 | throw new SQLException("Method not implemented"); 290 | } 291 | override bool getBoolean(int columnIndex) { 292 | throw new SQLException("Method not implemented"); 293 | } 294 | override bool getBoolean(string columnName) { 295 | return getBoolean(findColumn(columnName)); 296 | } 297 | override ubyte getUbyte(int columnIndex) { 298 | throw new SQLException("Method not implemented"); 299 | } 300 | override ubyte getUbyte(string columnName) { 301 | return getUbyte(findColumn(columnName)); 302 | } 303 | override byte getByte(int columnIndex) { 304 | throw new SQLException("Method not implemented"); 305 | } 306 | override byte getByte(string columnName) { 307 | return getByte(findColumn(columnName)); 308 | } 309 | override byte[] getBytes(int columnIndex) { 310 | throw new SQLException("Method not implemented"); 311 | } 312 | override byte[] getBytes(string columnName) { 313 | return getBytes(findColumn(columnName)); 314 | } 315 | override ubyte[] getUbytes(int columnIndex) { 316 | throw new SQLException("Method not implemented"); 317 | } 318 | override ubyte[] getUbytes(string columnName) { 319 | return getUbytes(findColumn(columnName)); 320 | } 321 | override short getShort(int columnIndex) { 322 | throw new SQLException("Method not implemented"); 323 | } 324 | override short getShort(string columnName) { 325 | return getShort(findColumn(columnName)); 326 | } 327 | override ushort getUshort(int columnIndex) { 328 | throw new SQLException("Method not implemented"); 329 | } 330 | override ushort getUshort(string columnName) { 331 | return getUshort(findColumn(columnName)); 332 | } 333 | override int getInt(int columnIndex) { 334 | throw new SQLException("Method not implemented"); 335 | } 336 | override int getInt(string columnName) { 337 | return getInt(findColumn(columnName)); 338 | } 339 | override uint getUint(int columnIndex) { 340 | throw new SQLException("Method not implemented"); 341 | } 342 | override uint getUint(string columnName) { 343 | return getUint(findColumn(columnName)); 344 | } 345 | override long getLong(int columnIndex) { 346 | throw new SQLException("Method not implemented"); 347 | } 348 | override long getLong(string columnName) { 349 | return getLong(findColumn(columnName)); 350 | } 351 | override ulong getUlong(int columnIndex) { 352 | throw new SQLException("Method not implemented"); 353 | } 354 | override ulong getUlong(string columnName) { 355 | return getUlong(findColumn(columnName)); 356 | } 357 | override double getDouble(int columnIndex) { 358 | throw new SQLException("Method not implemented"); 359 | } 360 | override double getDouble(string columnName) { 361 | return getDouble(findColumn(columnName)); 362 | } 363 | override float getFloat(int columnIndex) { 364 | throw new SQLException("Method not implemented"); 365 | } 366 | override float getFloat(string columnName) { 367 | return getFloat(findColumn(columnName)); 368 | } 369 | override string getString(int columnIndex) { 370 | throw new SQLException("Method not implemented"); 371 | } 372 | override string getString(string columnName) { 373 | return getString(findColumn(columnName)); 374 | } 375 | override Variant getVariant(int columnIndex) { 376 | throw new SQLException("Method not implemented"); 377 | } 378 | override Variant getVariant(string columnName) { 379 | return getVariant(findColumn(columnName)); 380 | } 381 | 382 | override bool wasNull() { 383 | throw new SQLException("Method not implemented"); 384 | } 385 | 386 | override bool isNull(int columnIndex) { 387 | throw new SQLException("Method not implemented"); 388 | } 389 | 390 | //Retrieves the number, types and properties of this ResultSet object's columns 391 | override ResultSetMetaData getMetaData() { 392 | throw new SQLException("Method not implemented"); 393 | } 394 | //Retrieves the Statement object that produced this ResultSet object. 395 | override Statement getStatement() { 396 | throw new SQLException("Method not implemented"); 397 | } 398 | //Retrieves the current row number 399 | override int getRow() { 400 | throw new SQLException("Method not implemented"); 401 | } 402 | //Retrieves the fetch size for this ResultSet object. 403 | override ulong getFetchSize() { 404 | throw new SQLException("Method not implemented"); 405 | } 406 | 407 | override SysTime getSysTime(int columnIndex) { 408 | throw new SQLException("Method not implemented"); 409 | } 410 | override DateTime getDateTime(int columnIndex) { 411 | throw new SQLException("Method not implemented"); 412 | } 413 | override Date getDate(int columnIndex) { 414 | throw new SQLException("Method not implemented"); 415 | } 416 | override TimeOfDay getTime(int columnIndex) { 417 | throw new SQLException("Method not implemented"); 418 | } 419 | 420 | override SysTime getSysTime(string columnName) { 421 | return getSysTime(findColumn(columnName)); 422 | } 423 | override DateTime getDateTime(string columnName) { 424 | return getDateTime(findColumn(columnName)); 425 | } 426 | override Date getDate(string columnName) { 427 | return getDate(findColumn(columnName)); 428 | } 429 | override TimeOfDay getTime(string columnName) { 430 | return getTime(findColumn(columnName)); 431 | } 432 | } 433 | 434 | /// Column metadata object to be used in driver implementations 435 | class ColumnMetadataItem { 436 | string catalogName; 437 | int displaySize; 438 | string label; 439 | string name; 440 | int type; 441 | string typeName; 442 | int precision; 443 | int scale; 444 | string schemaName; 445 | string tableName; 446 | bool isAutoIncrement; 447 | bool isCaseSensitive; 448 | bool isCurrency; 449 | bool isDefinitelyWritable; 450 | int isNullable; 451 | bool isReadOnly; 452 | bool isSearchable; 453 | bool isSigned; 454 | bool isWritable; 455 | } 456 | 457 | /// parameter metadata object - to be used in driver implementations 458 | class ParameterMetaDataItem { 459 | /// Retrieves the designated parameter's mode. 460 | int mode; 461 | /// Retrieves the designated parameter's SQL type. 462 | int type; 463 | /// Retrieves the designated parameter's database-specific type name. 464 | string typeName; 465 | /// Retrieves the designated parameter's number of decimal digits. 466 | int precision; 467 | /// Retrieves the designated parameter's number of digits to right of the decimal point. 468 | int scale; 469 | /// Retrieves whether null values are allowed in the designated parameter. 470 | int isNullable; 471 | /// Retrieves whether values for the designated parameter can be signed numbers. 472 | bool isSigned; 473 | } 474 | 475 | /// parameter set metadate implementation object - to be used in driver implementations 476 | class ParameterMetaDataImpl : ParameterMetaData { 477 | ParameterMetaDataItem [] cols; 478 | this(ParameterMetaDataItem [] cols) { 479 | this.cols = cols; 480 | } 481 | ref ParameterMetaDataItem col(int column) { 482 | enforce!SQLException(column >=1 && column <= cols.length, "Parameter index out of range"); 483 | return cols[column - 1]; 484 | } 485 | // Retrieves the fully-qualified name of the Java class whose instances should be passed to the method PreparedStatement.setObject. 486 | //String getParameterClassName(int param); 487 | /// Retrieves the number of parameters in the PreparedStatement object for which this ParameterMetaData object contains information. 488 | int getParameterCount() { 489 | return cast(int)cols.length; 490 | } 491 | /// Retrieves the designated parameter's mode. 492 | int getParameterMode(int param) { return col(param).mode; } 493 | /// Retrieves the designated parameter's SQL type. 494 | int getParameterType(int param) { return col(param).type; } 495 | /// Retrieves the designated parameter's database-specific type name. 496 | string getParameterTypeName(int param) { return col(param).typeName; } 497 | /// Retrieves the designated parameter's number of decimal digits. 498 | int getPrecision(int param) { return col(param).precision; } 499 | /// Retrieves the designated parameter's number of digits to right of the decimal point. 500 | int getScale(int param) { return col(param).scale; } 501 | /// Retrieves whether null values are allowed in the designated parameter. 502 | int isNullable(int param) { return col(param).isNullable; } 503 | /// Retrieves whether values for the designated parameter can be signed numbers. 504 | bool isSigned(int param) { return col(param).isSigned; } 505 | 506 | override string toString() { 507 | return to!string(cols.map!(c => c.typeName).joiner(",")); 508 | } 509 | } 510 | 511 | /// Metadata for result set - to be used in driver implementations 512 | class ResultSetMetaDataImpl : ResultSetMetaData { 513 | private ColumnMetadataItem [] cols; 514 | this(ColumnMetadataItem [] cols) { 515 | this.cols = cols; 516 | } 517 | ref ColumnMetadataItem col(int column) { 518 | enforce!SQLException(column >=1 && column <= cols.length, "Column index out of range"); 519 | return cols[column - 1]; 520 | } 521 | //Returns the number of columns in this ResultSet object. 522 | override int getColumnCount() { return cast(int)cols.length; } 523 | // Gets the designated column's table's catalog name. 524 | override string getCatalogName(int column) { return col(column).catalogName; } 525 | // Returns the fully-qualified name of the Java class whose instances are manufactured if the method ResultSet.getObject is called to retrieve a value from the column. 526 | //override string getColumnClassName(int column) { return col(column).catalogName; } 527 | // Indicates the designated column's normal maximum width in characters. 528 | override int getColumnDisplaySize(int column) { return col(column).displaySize; } 529 | // Gets the designated column's suggested title for use in printouts and displays. 530 | override string getColumnLabel(int column) { return col(column).label; } 531 | // Get the designated column's name. 532 | override string getColumnName(int column) { return col(column).name; } 533 | // Retrieves the designated column's SQL type. 534 | override int getColumnType(int column) { return col(column).type; } 535 | // Retrieves the designated column's database-specific type name. 536 | override string getColumnTypeName(int column) { return col(column).typeName; } 537 | // Get the designated column's number of decimal digits. 538 | override int getPrecision(int column) { return col(column).precision; } 539 | // Gets the designated column's number of digits to right of the decimal point. 540 | override int getScale(int column) { return col(column).scale; } 541 | // Get the designated column's table's schema. 542 | override string getSchemaName(int column) { return col(column).schemaName; } 543 | // Gets the designated column's table name. 544 | override string getTableName(int column) { return col(column).tableName; } 545 | // Indicates whether the designated column is automatically numbered, thus read-only. 546 | override bool isAutoIncrement(int column) { return col(column).isAutoIncrement; } 547 | // Indicates whether a column's case matters. 548 | override bool isCaseSensitive(int column) { return col(column).isCaseSensitive; } 549 | // Indicates whether the designated column is a cash value. 550 | override bool isCurrency(int column) { return col(column).isCurrency; } 551 | // Indicates whether a write on the designated column will definitely succeed. 552 | override bool isDefinitelyWritable(int column) { return col(column).isDefinitelyWritable; } 553 | // Indicates the nullability of values in the designated column. 554 | override int isNullable(int column) { return col(column).isNullable; } 555 | // Indicates whether the designated column is definitely not writable. 556 | override bool isReadOnly(int column) { return col(column).isReadOnly; } 557 | // Indicates whether the designated column can be used in a where clause. 558 | override bool isSearchable(int column) { return col(column).isSearchable; } 559 | // Indicates whether values in the designated column are signed numbers. 560 | override bool isSigned(int column) { return col(column).isSigned; } 561 | // Indicates whether it is possible for a write on the designated column to succeed. 562 | override bool isWritable(int column) { return col(column).isWritable; } 563 | 564 | override string toString() { 565 | return to!string(cols.map!(c => c.name).joiner(",")); 566 | } 567 | } 568 | 569 | // version (unittest) { 570 | // void unitTestExecuteBatch(Connection conn, string[] queries) { 571 | // Statement stmt = conn.createStatement(); 572 | // foreach(query; queries) { 573 | // //writeln("query:" ~ query); 574 | // stmt.executeUpdate(query); 575 | // } 576 | // } 577 | // } 578 | 579 | // utility functions 580 | 581 | /// removes ddbc: prefix from string (if any) 582 | /// e.g., for "ddbc:postgresql://localhost/test" it will return "postgresql://localhost/test" 583 | string stripDdbcPrefix(string url) { 584 | if (url.startsWith("ddbc:")) 585 | return url[5 .. $]; // strip out ddbc: prefix 586 | return url; 587 | } 588 | 589 | /// extracts driver name from DDBC URL 590 | /// e.g., for "ddbc:postgresql://localhost/test" it will return "postgresql" 591 | string extractDriverNameFromURL(string url) { 592 | url = stripDdbcPrefix(url); 593 | import std.string; 594 | int colonPos = cast(int)url.indexOf(":"); 595 | 596 | string dbName = colonPos < 0 ? url : url[0 .. colonPos]; 597 | return dbName == "sqlserver" || dbName == "oracle" ? "odbc" : dbName; 598 | } 599 | 600 | private unittest { 601 | assert(extractDriverNameFromURL("ddbc:sqlite::memory:") == "sqlite"); 602 | assert(extractDriverNameFromURL("ddbc:sqlite:ddbc-test.sqlite") == "sqlite"); 603 | assert(extractDriverNameFromURL("ddbc:mysql://127.0.0.1:3306/mydb") == "mysql"); 604 | assert(extractDriverNameFromURL("ddbc:postgresql://127.0.0.1:5432/mydb") == "postgresql"); 605 | assert(extractDriverNameFromURL("ddbc:sqlserver://127.0.0.1:1433/mydb") == "odbc"); 606 | assert(extractDriverNameFromURL("ddbc:oracle://127.0.0.1:1521/mydb") == "odbc"); 607 | assert(extractDriverNameFromURL("ddbc:odbc://127.0.0.1:3306/mydb") == "odbc"); 608 | 609 | // same again without the ddbc prefix: 610 | assert(extractDriverNameFromURL("sqlite::memory:") == "sqlite"); 611 | assert(extractDriverNameFromURL("sqlite:ddbc-test.sqlite") == "sqlite"); 612 | assert(extractDriverNameFromURL("mysql://127.0.0.1:3306/mydb") == "mysql"); 613 | assert(extractDriverNameFromURL("postgresql://127.0.0.1:5432/mydb") == "postgresql"); 614 | assert(extractDriverNameFromURL("sqlserver://127.0.0.1:1433/mydb") == "odbc"); 615 | assert(extractDriverNameFromURL("oracle://127.0.0.1:1521/mydb") == "odbc"); 616 | assert(extractDriverNameFromURL("odbc://127.0.0.1:3306/mydb") == "odbc"); 617 | } 618 | 619 | /// extract parameters from URL string to string[string] map, update url to strip params 620 | void extractParamsFromURL(ref string url, ref string[string] params) { 621 | url = stripDdbcPrefix(url); 622 | import std.string : lastIndexOf, split; 623 | ptrdiff_t qmIndex = lastIndexOf(url, '?'); 624 | if (qmIndex >= 0) { 625 | string urlParams = url[qmIndex + 1 .. $]; 626 | url = url[0 .. qmIndex]; 627 | string[] list = urlParams.split(","); 628 | foreach(item; list) { 629 | string[] keyValue = item.split("="); 630 | if (keyValue.length == 2) { 631 | params[keyValue[0]] = keyValue[1]; 632 | } 633 | } 634 | } 635 | } 636 | 637 | private unittest { 638 | string url = "ddbc:odbc://localhost:1433?user=sa,password=p@ss,driver=FreeTDS"; 639 | string[string] params; 640 | extractParamsFromURL(url, params); 641 | 642 | assert(url == "odbc://localhost:1433"); 643 | assert(params.length == 3); 644 | assert(params["user"] == "sa"); 645 | assert(params["password"] == "p@ss"); 646 | assert(params["driver"] == "FreeTDS"); 647 | } 648 | 649 | /// sets user and password parameters in parameter map 650 | public void setUserAndPassword(ref string[string] params, string username, string password) { 651 | params["user"] = username; 652 | params["password"] = password; 653 | } 654 | 655 | // factory methods 656 | 657 | /// Helper function to create DDBC connection, automatically selecting driver based on URL 658 | Connection createConnection(string url, string[string]params = null) { 659 | Driver driver = DriverFactory.createDriverForURL(url); 660 | return driver.connect(url, params); 661 | } 662 | 663 | /// Helper function to create simple DDBC DataSource, automatically selecting driver based on URL 664 | DataSource createDataSource(string url, string[string]params = null) { 665 | Driver driver = DriverFactory.createDriverForURL(url); 666 | return new DataSourceImpl(driver, url, params); 667 | } 668 | 669 | /// Helper function to create connection pool data source, automatically selecting driver based on URL 670 | DataSource createConnectionPool(string url, string[string]params = null, int maxPoolSize = 1, int timeToLive = 600, int waitTimeOut = 30) { 671 | Driver driver = DriverFactory.createDriverForURL(url); 672 | return new ConnectionPoolDataSourceImpl(driver, url, params, maxPoolSize, timeToLive, waitTimeOut); 673 | } 674 | 675 | -------------------------------------------------------------------------------- /source/ddbc/core.d: -------------------------------------------------------------------------------- 1 | /** 2 | * DDBC - D DataBase Connector - abstraction layer for RDBMS access, with interface similar to JDBC. 3 | * 4 | * Source file ddbc/core.d. 5 | * 6 | * DDBC library attempts to provide implementation independent interface to different databases. 7 | * 8 | * Set of supported RDBMSs can be extended by writing Drivers for particular DBs. 9 | * Currently it only includes MySQL Driver which uses patched version of MYSQLN (native D implementation of MySQL connector, written by Steve Teale) 10 | * 11 | * JDBC documentation can be found here: 12 | * $(LINK http://docs.oracle.com/javase/1.5.0/docs/api/java/sql/package-summary.html)$(BR) 13 | * 14 | * Limitations of current version: readonly unidirectional resultset, completely fetched into memory. 15 | * 16 | * Its primary objects are: 17 | * $(UL 18 | * $(LI Driver: $(UL $(LI Implements interface to particular RDBMS, used to create connections))) 19 | * $(LI Connection: $(UL $(LI Connection to the server, and querying and setting of server parameters.))) 20 | * $(LI Statement: Handling of general SQL requests/queries/commands, with principal methods: 21 | * $(UL $(LI executeUpdate() - run query which doesn't return result set.) 22 | * $(LI executeQuery() - execute query which returns ResultSet interface to access rows of result.) 23 | * ) 24 | * ) 25 | * $(LI PreparedStatement: Handling of general SQL requests/queries/commands which having additional parameters, with principal methods: 26 | * $(UL $(LI executeUpdate() - run query which doesn't return result set.) 27 | * $(LI executeQuery() - execute query which returns ResultSet interface to access rows of result.) 28 | * $(LI setXXX() - setter methods to bind parameters.) 29 | * ) 30 | * ) 31 | * $(LI ResultSet: $(UL $(LI Get result of query row by row, accessing individual fields.))) 32 | * ) 33 | * 34 | * You can find usage examples in unittest{} sections. 35 | * 36 | * Copyright: Copyright 2013 37 | * License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 38 | * Author: Vadim Lopatin 39 | */ 40 | module ddbc.core; 41 | 42 | import std.exception; 43 | import std.variant; 44 | import std.datetime; 45 | 46 | class SQLException : Exception { 47 | protected string _stateString; 48 | this(string msg, string stateString, string f = __FILE__, size_t l = __LINE__) { super(msg, f, l); _stateString = stateString; } 49 | this(string msg, string f = __FILE__, size_t l = __LINE__) { super(msg, f, l); } 50 | this(Throwable causedBy, string f = __FILE__, size_t l = __LINE__) { super(causedBy.msg, causedBy, f, l); } 51 | this(string msg, Throwable causedBy, string f = __FILE__, size_t l = __LINE__) { super(causedBy.msg, causedBy, f, l); } 52 | this(string msg, string stateString, Throwable causedBy, string f = __FILE__, size_t l = __LINE__) { super(causedBy.msg, causedBy, f, l); _stateString = stateString; } 53 | } 54 | 55 | class SQLWarning { 56 | // stub 57 | } 58 | 59 | /// JDBC java.sql.Types from http://docs.oracle.com/javase/6/docs/api/java/sql/Types.html 60 | enum SqlType { 61 | //sometimes referred to as a type code, that identifies the generic SQL type ARRAY. 62 | //ARRAY, 63 | ///sometimes referred to as a type code, that identifies the generic SQL type BIGINT. 64 | BIGINT, 65 | ///sometimes referred to as a type code, that identifies the generic SQL type BINARY. 66 | //BINARY, 67 | //sometimes referred to as a type code, that identifies the generic SQL type BIT. 68 | BIT, 69 | ///sometimes referred to as a type code, that identifies the generic SQL type BLOB. 70 | BLOB, 71 | ///somtimes referred to as a type code, that identifies the generic SQL type BOOLEAN. 72 | BOOLEAN, 73 | ///sometimes referred to as a type code, that identifies the generic SQL type CHAR. 74 | CHAR, 75 | ///sometimes referred to as a type code, that identifies the generic SQL type CLOB. 76 | CLOB, 77 | //somtimes referred to as a type code, that identifies the generic SQL type DATALINK. 78 | //DATALINK, 79 | ///sometimes referred to as a type code, that identifies the generic SQL type DATE. 80 | DATE, 81 | ///sometimes referred to as a type code, that identifies the generic SQL type DATETIME. 82 | DATETIME, 83 | ///sometimes referred to as a type code, that identifies the generic SQL type DECIMAL. 84 | DECIMAL, 85 | //sometimes referred to as a type code, that identifies the generic SQL type DISTINCT. 86 | //DISTINCT, 87 | ///sometimes referred to as a type code, that identifies the generic SQL type DOUBLE. 88 | DOUBLE, 89 | ///sometimes referred to as a type code, that identifies the generic SQL type FLOAT. 90 | FLOAT, 91 | ///sometimes referred to as a type code, that identifies the generic SQL type INTEGER. 92 | INTEGER, 93 | //sometimes referred to as a type code, that identifies the generic SQL type JAVA_OBJECT. 94 | //JAVA_OBJECT, 95 | ///sometimes referred to as a type code, that identifies the generic SQL type LONGNVARCHAR. 96 | LONGNVARCHAR, 97 | ///sometimes referred to as a type code, that identifies the generic SQL type LONGVARBINARY. 98 | LONGVARBINARY, 99 | ///sometimes referred to as a type code, that identifies the generic SQL type LONGVARCHAR. 100 | LONGVARCHAR, 101 | ///sometimes referred to as a type code, that identifies the generic SQL type NCHAR 102 | NCHAR, 103 | ///sometimes referred to as a type code, that identifies the generic SQL type NCLOB. 104 | NCLOB, 105 | ///The constant in the Java programming language that identifies the generic SQL value NULL. 106 | NULL, 107 | ///sometimes referred to as a type code, that identifies the generic SQL type NUMERIC. 108 | NUMERIC, 109 | ///sometimes referred to as a type code, that identifies the generic SQL type NVARCHAR. 110 | NVARCHAR, 111 | ///indicates that the SQL type is database-specific and gets mapped to a object that can be accessed via the methods getObject and setObject. 112 | OTHER, 113 | //sometimes referred to as a type code, that identifies the generic SQL type REAL. 114 | //REAL, 115 | //sometimes referred to as a type code, that identifies the generic SQL type REF. 116 | //REF, 117 | //sometimes referred to as a type code, that identifies the generic SQL type ROWID 118 | //ROWID, 119 | ///sometimes referred to as a type code, that identifies the generic SQL type SMALLINT. 120 | SMALLINT, 121 | //sometimes referred to as a type code, that identifies the generic SQL type XML. 122 | //SQLXML, 123 | //sometimes referred to as a type code, that identifies the generic SQL type STRUCT. 124 | //STRUCT, 125 | ///sometimes referred to as a type code, that identifies the generic SQL type TIME. 126 | TIME, 127 | //sometimes referred to as a type code, that identifies the generic SQL type TIMESTAMP. 128 | //TIMESTAMP, 129 | ///sometimes referred to as a type code, that identifies the generic SQL type TINYINT. 130 | TINYINT, 131 | ///sometimes referred to as a type code, that identifies the generic SQL type VARBINARY. 132 | VARBINARY, 133 | ///sometimes referred to as a type code, that identifies the generic SQL type VARCHAR. 134 | VARCHAR, 135 | } 136 | 137 | 138 | /** 139 | * The level of isolation between transactions. Various isolation levels provide tradeoffs between 140 | * performance, reproducibility, and interactions with other simultaneous transactions. 141 | * 142 | * The default transaction isolation level depends on the DB driver. For example: 143 | * - Postgresql: Defaults to READ_COMMITTED 144 | * - MySQL: Defaults to REPEATABLE_READ 145 | * - SQLite: Defaults to SERIALIZABLE 146 | * 147 | * Generally, `SELECT` statements do see the effects of previous `UPDATE` statements within the same 148 | * transaction, despite the fact that they are not yet committed. However, there can be differences 149 | * between database implementations depending on the transaction isolation level. 150 | */ 151 | enum TransactionIsolation { 152 | /** 153 | * Transactions are not supported at all, thus there is no isolation. 154 | */ 155 | NONE, 156 | 157 | /** 158 | * Statements can read rows that have been modified by other transactions but are not yet 159 | * committed. High parallelism, but risks dirty reads, non-repeatable reads, etc. 160 | */ 161 | READ_UNCOMMITTED, 162 | 163 | /** 164 | * Statements cannot read data that has been modified by other transactions but not yet 165 | * committed. However, data can be changed when other transactions are commited between statements 166 | * of a transaction resulting in non-repeatable reads or phantom data. 167 | */ 168 | READ_COMMITTED, 169 | 170 | /** 171 | * Shared locks are used to prevent modification of data read by transactions, preventing dirty 172 | * reads and non-repeatable reads. However, new data can be inserted, causing transactions to 173 | * potentially behave differently if the transaction is retried, i.e. "phantom reads". 174 | */ 175 | REPEATABLE_READ, 176 | 177 | /** 178 | * Locks are used to make sure transactions using the same data cannot run simultaneously. This 179 | * prevents dirty reads, non-repeatable reads, and phantom reads. However, this transaction 180 | * isolation level has the worst performance. 181 | */ 182 | SERIALIZABLE, 183 | } 184 | 185 | /** 186 | * A connection represents a session with a specific database. Within the context of a Connection, 187 | * SQL statements are executed and results are returned. 188 | * 189 | * Note: By default the Connection automatically commits changes after executing each statement. If 190 | * auto commit has been disabled, an explicit commit must be done or database changes will not be 191 | * saved. 192 | * 193 | * See_Also: https://docs.oracle.com/cd/E13222_01/wls/docs45/classdocs/java.sql.Connection.html 194 | */ 195 | interface Connection : DialectAware { 196 | /// Releases this Connection object's database and JDBC resources immediately instead of waiting for them to be automatically released. 197 | void close(); 198 | /// Makes all changes made since the previous commit/rollback permanent and releases any database locks currently held by this Connection object. 199 | void commit(); 200 | /// Retrieves this Connection object's current catalog name. 201 | string getCatalog(); 202 | /// Sets the given catalog name in order to select a subspace of this Connection object's database in which to work. 203 | void setCatalog(string catalog); 204 | /// Retrieves whether this Connection object has been closed. 205 | bool isClosed(); 206 | /// Undoes all changes made in the current transaction and releases any database locks currently held by this Connection object. 207 | void rollback(); 208 | /// Retrieves the current auto-commit mode for this Connection object. 209 | bool getAutoCommit(); 210 | /// Sets this connection's auto-commit mode to the given state. 211 | void setAutoCommit(bool autoCommit); 212 | // statements 213 | /// Creates a Statement object for sending SQL statements to the database. 214 | Statement createStatement(); 215 | /// Creates a PreparedStatement object for sending parameterized SQL statements to the database. 216 | PreparedStatement prepareStatement(string query); 217 | 218 | /** 219 | * Returns the currently active transaction isolation level used by the DB connection. 220 | */ 221 | TransactionIsolation getTransactionIsolation(); 222 | 223 | /** 224 | * Attempt to change the Transaction Isolation Level used for transactions, which controls how 225 | * simultaneous transactions will interact with each other. In general, lower isolation levels 226 | * require fewer locks and have better performanc, but also have fewer guarantees for 227 | * consistency. 228 | * 229 | * Note: setTransactionIsolation cannot be called while in the middle of a transaction. 230 | */ 231 | void setTransactionIsolation(TransactionIsolation level); 232 | } 233 | 234 | interface ResultSetMetaData { 235 | //Returns the number of columns in this ResultSet object. 236 | int getColumnCount(); 237 | 238 | // Gets the designated column's table's catalog name. 239 | string getCatalogName(int column); 240 | // Returns the fully-qualified name of the Java class whose instances are manufactured if the method ResultSet.getObject is called to retrieve a value from the column. 241 | //string getColumnClassName(int column); 242 | // Indicates the designated column's normal maximum width in characters. 243 | int getColumnDisplaySize(int column); 244 | // Gets the designated column's suggested title for use in printouts and displays. 245 | string getColumnLabel(int column); 246 | // Get the designated column's name. 247 | string getColumnName(int column); 248 | // Retrieves the designated column's SQL type. 249 | int getColumnType(int column); 250 | // Retrieves the designated column's database-specific type name. 251 | string getColumnTypeName(int column); 252 | // Get the designated column's number of decimal digits. 253 | int getPrecision(int column); 254 | // Gets the designated column's number of digits to right of the decimal point. 255 | int getScale(int column); 256 | // Get the designated column's table's schema. 257 | string getSchemaName(int column); 258 | // Gets the designated column's table name. 259 | string getTableName(int column); 260 | // Indicates whether the designated column is automatically numbered, thus read-only. 261 | bool isAutoIncrement(int column); 262 | // Indicates whether a column's case matters. 263 | bool isCaseSensitive(int column); 264 | // Indicates whether the designated column is a cash value. 265 | bool isCurrency(int column); 266 | // Indicates whether a write on the designated column will definitely succeed. 267 | bool isDefinitelyWritable(int column); 268 | // Indicates the nullability of values in the designated column. 269 | int isNullable(int column); 270 | // Indicates whether the designated column is definitely not writable. 271 | bool isReadOnly(int column); 272 | // Indicates whether the designated column can be used in a where clause. 273 | bool isSearchable(int column); 274 | // Indicates whether values in the designated column are signed numbers. 275 | bool isSigned(int column); 276 | // Indicates whether it is possible for a write on the designated column to succeed. 277 | bool isWritable(int column); 278 | } 279 | 280 | interface ParameterMetaData { 281 | // Retrieves the fully-qualified name of the Java class whose instances should be passed to the method PreparedStatement.setObject. 282 | //String getParameterClassName(int param); 283 | /// Retrieves the number of parameters in the PreparedStatement object for which this ParameterMetaData object contains information. 284 | int getParameterCount(); 285 | /// Retrieves the designated parameter's mode. 286 | int getParameterMode(int param); 287 | /// Retrieves the designated parameter's SQL type. 288 | int getParameterType(int param); 289 | /// Retrieves the designated parameter's database-specific type name. 290 | string getParameterTypeName(int param); 291 | /// Retrieves the designated parameter's number of decimal digits. 292 | int getPrecision(int param); 293 | /// Retrieves the designated parameter's number of digits to right of the decimal point. 294 | int getScale(int param); 295 | /// Retrieves whether null values are allowed in the designated parameter. 296 | int isNullable(int param); 297 | /// Retrieves whether values for the designated parameter can be signed numbers. 298 | bool isSigned(int param); 299 | } 300 | 301 | interface DataSetReader { 302 | bool getBoolean(int columnIndex); 303 | ubyte getUbyte(int columnIndex); 304 | ubyte[] getUbytes(int columnIndex); 305 | byte[] getBytes(int columnIndex); 306 | byte getByte(int columnIndex); 307 | short getShort(int columnIndex); 308 | ushort getUshort(int columnIndex); 309 | int getInt(int columnIndex); 310 | uint getUint(int columnIndex); 311 | long getLong(int columnIndex); 312 | ulong getUlong(int columnIndex); 313 | double getDouble(int columnIndex); 314 | float getFloat(int columnIndex); 315 | string getString(int columnIndex); 316 | SysTime getSysTime(int columnIndex); 317 | DateTime getDateTime(int columnIndex); 318 | Date getDate(int columnIndex); 319 | TimeOfDay getTime(int columnIndex); 320 | Variant getVariant(int columnIndex); 321 | bool isNull(int columnIndex); 322 | bool wasNull(); 323 | } 324 | 325 | interface DataSetWriter { 326 | void setFloat(int parameterIndex, float x); 327 | void setDouble(int parameterIndex, double x); 328 | void setBoolean(int parameterIndex, bool x); 329 | void setLong(int parameterIndex, long x); 330 | void setInt(int parameterIndex, int x); 331 | void setShort(int parameterIndex, short x); 332 | void setByte(int parameterIndex, byte x); 333 | void setBytes(int parameterIndex, byte[] x); 334 | void setUlong(int parameterIndex, ulong x); 335 | void setUint(int parameterIndex, uint x); 336 | void setUshort(int parameterIndex, ushort x); 337 | void setUbyte(int parameterIndex, ubyte x); 338 | void setUbytes(int parameterIndex, ubyte[] x); 339 | void setString(int parameterIndex, string x); 340 | void setSysTime(int parameterIndex, SysTime x); 341 | void setDateTime(int parameterIndex, DateTime x); 342 | void setDate(int parameterIndex, Date x); 343 | void setTime(int parameterIndex, TimeOfDay x); 344 | void setVariant(int columnIndex, Variant x); 345 | 346 | void setNull(int parameterIndex); 347 | void setNull(int parameterIndex, int sqlType); 348 | } 349 | 350 | interface ResultSet : DataSetReader { 351 | void close(); 352 | bool first(); 353 | bool isFirst(); 354 | bool isLast(); 355 | bool next(); 356 | 357 | //Retrieves the number, types and properties of this ResultSet object's columns 358 | ResultSetMetaData getMetaData(); 359 | //Retrieves the Statement object that produced this ResultSet object. 360 | Statement getStatement(); 361 | //Retrieves the current row number 362 | int getRow(); 363 | //Retrieves the fetch size for this ResultSet object. 364 | deprecated("Marked for removal as Cannot be used by all supported drivers. See Github issue #85") 365 | ulong getFetchSize(); 366 | 367 | // from DataSetReader 368 | bool getBoolean(int columnIndex); 369 | ubyte getUbyte(int columnIndex); 370 | ubyte[] getUbytes(int columnIndex); 371 | byte[] getBytes(int columnIndex); 372 | byte getByte(int columnIndex); 373 | short getShort(int columnIndex); 374 | ushort getUshort(int columnIndex); 375 | int getInt(int columnIndex); 376 | uint getUint(int columnIndex); 377 | long getLong(int columnIndex); 378 | ulong getUlong(int columnIndex); 379 | double getDouble(int columnIndex); 380 | float getFloat(int columnIndex); 381 | string getString(int columnIndex); 382 | Variant getVariant(int columnIndex); 383 | SysTime getSysTime(int columnIndex); 384 | DateTime getDateTime(int columnIndex); 385 | Date getDate(int columnIndex); 386 | TimeOfDay getTime(int columnIndex); 387 | 388 | bool isNull(int columnIndex); 389 | bool wasNull(); 390 | 391 | // additional methods 392 | int findColumn(string columnName); 393 | bool getBoolean(string columnName); 394 | ubyte getUbyte(string columnName); 395 | ubyte[] getUbytes(string columnName); 396 | byte[] getBytes(string columnName); 397 | byte getByte(string columnName); 398 | short getShort(string columnName); 399 | ushort getUshort(string columnName); 400 | int getInt(string columnName); 401 | uint getUint(string columnName); 402 | long getLong(string columnName); 403 | ulong getUlong(string columnName); 404 | double getDouble(string columnName); 405 | float getFloat(string columnName); 406 | string getString(string columnName); 407 | SysTime getSysTime(string columnName); 408 | DateTime getDateTime(string columnName); 409 | Date getDate(string columnName); 410 | TimeOfDay getTime(string columnName); 411 | Variant getVariant(string columnName); 412 | 413 | /// to iterate through all rows in result set 414 | int opApply(int delegate(DataSetReader) dg); 415 | 416 | } 417 | 418 | enum DialectType : string { 419 | SQLITE = "SQLite", // SQLite has it's own quirks 420 | MYSQL5 = "MySQL 5", // for MySQL & MariaDB 421 | MYSQL8 = "MySQL 8", // todo: add support for MySQL 8 422 | PGSQL = "PL/pgSQL", // PL/pgSQL (Procedural Language/PostgreSQL) used by PostgreSQL 423 | TSQL = "T-SQL", // T-SQL (Transact-SQL) is Microsoft’s extension of SQL 424 | PLSQL = "PL/SQL" // Oracle (PL/SQL) 425 | } 426 | 427 | interface DialectAware { 428 | DialectType getDialectType(); 429 | } 430 | 431 | // statements are made via a db connection which are also DialectAware 432 | interface Statement : DialectAware { 433 | ResultSet executeQuery(string query); 434 | int executeUpdate(string query); 435 | int executeUpdate(string query, out Variant insertId); 436 | void close(); 437 | } 438 | 439 | /// An object that represents a precompiled SQL statement. 440 | interface PreparedStatement : Statement, DataSetWriter { 441 | /// Executes the SQL statement in this PreparedStatement object, which must be an SQL INSERT, UPDATE or DELETE statement; or an SQL statement that returns nothing, such as a DDL statement. 442 | int executeUpdate(); 443 | /// Executes the SQL statement in this PreparedStatement object, which must be an SQL INSERT, UPDATE or DELETE statement; or an SQL statement that returns nothing, such as a DDL statement. 444 | int executeUpdate(out Variant insertId); 445 | /// Executes the SQL query in this PreparedStatement object and returns the ResultSet object generated by the query. 446 | ResultSet executeQuery(); 447 | 448 | /// Retrieves a ResultSetMetaData object that contains information about the columns of the ResultSet object that will be returned when this PreparedStatement object is executed. 449 | ResultSetMetaData getMetaData(); 450 | /// Retrieves the number, types and properties of this PreparedStatement object's parameters. 451 | ParameterMetaData getParameterMetaData(); 452 | /// Clears the current parameter values immediately. 453 | void clearParameters(); 454 | 455 | // from DataSetWriter 456 | void setFloat(int parameterIndex, float x); 457 | void setDouble(int parameterIndex, double x); 458 | void setBoolean(int parameterIndex, bool x); 459 | void setLong(int parameterIndex, long x); 460 | void setInt(int parameterIndex, int x); 461 | void setShort(int parameterIndex, short x); 462 | void setByte(int parameterIndex, byte x); 463 | void setBytes(int parameterIndex, byte[] x); 464 | void setUlong(int parameterIndex, ulong x); 465 | void setUint(int parameterIndex, uint x); 466 | void setUshort(int parameterIndex, ushort x); 467 | void setUbyte(int parameterIndex, ubyte x); 468 | void setUbytes(int parameterIndex, ubyte[] x); 469 | void setString(int parameterIndex, string x); 470 | void setSysTime(int parameterIndex, SysTime x); 471 | void setDateTime(int parameterIndex, DateTime x); 472 | void setDate(int parameterIndex, Date x); 473 | void setTime(int parameterIndex, TimeOfDay x); 474 | void setVariant(int parameterIndex, Variant x); 475 | 476 | void setNull(int parameterIndex); 477 | void setNull(int parameterIndex, int sqlType); 478 | } 479 | 480 | interface Driver { 481 | Connection connect(string url, string[string] params); 482 | } 483 | 484 | interface DataSource { 485 | Connection getConnection(); 486 | } 487 | 488 | /// Helper function to make url in format required for DSN connections to Microsoft SQL Server 489 | string makeDDBCUrl(string driverName, string[string] params) { 490 | enforce(driverName == "odbc", "only ODBC can have Url created this way"); 491 | import std.array : byPair; 492 | import std.algorithm.iteration : map, joiner; 493 | import std.conv : to; 494 | return "odbc://?" ~ to!string(joiner(params.byPair.map!(p => p.key ~ "=" ~ p.value), ",")); 495 | } 496 | 497 | /// Helper function to make url in form driverName://host:port/dbname?param1=value1,param2=value2 498 | string makeDDBCUrl(string driverName, string host = null, int port = 0, 499 | string dbName = null, string[string] params = null) { 500 | import std.algorithm.searching : canFind; 501 | enforce(canFind(["sqlite", "postgresql", "mysql", "sqlserver", "oracle", "odbc"], driverName), "driver must be one of sqlite|postgresql|mysql|sqlserver|oracle|odbc"); 502 | import std.conv : to; 503 | char[] res; 504 | res.assumeSafeAppend; 505 | res ~= "ddbc:"; 506 | res ~= driverName; 507 | 508 | if(driverName is "sqlite") { 509 | // if it's SQLite the host arg should be a filename or ":memory:" 510 | res ~= ":"~host; 511 | } else { 512 | res ~= "://" ~ host ~ ":" ~ to!string(port); 513 | 514 | if (dbName !is null) { 515 | res ~= "/" ~ dbName; 516 | } 517 | } 518 | 519 | if(params !is null) { 520 | import std.array : byPair; 521 | import std.algorithm.iteration : map, joiner; 522 | res ~= "?" ~ to!string(joiner(params.byPair.map!(p => p.key ~ "=" ~ p.value), ",")); 523 | } 524 | 525 | return res.dup; 526 | } 527 | 528 | private unittest { 529 | assertThrown!Exception(makeDDBCUrl("bogus", "")); 530 | } 531 | 532 | private unittest { 533 | string url = makeDDBCUrl("sqlite", ":memory:"); 534 | assert(url == "ddbc:sqlite::memory:", "SQLite URL is not correct: "~url); 535 | } 536 | 537 | private unittest { 538 | string url = makeDDBCUrl("sqlite", "ddbc-test.sqlite"); 539 | assert(url == "ddbc:sqlite:ddbc-test.sqlite", "SQLite URL is not correct: "~url); 540 | } 541 | 542 | private unittest { 543 | string url = makeDDBCUrl("postgresql", "127.0.0.1", 5432, "mydb"); 544 | assert(url == "ddbc:postgresql://127.0.0.1:5432/mydb", "Postgres URL is not correct: "~url); 545 | } 546 | 547 | private unittest { 548 | string url = makeDDBCUrl("mysql", "127.0.0.1", 3306, "mydb"); 549 | assert(url == "ddbc:mysql://127.0.0.1:3306/mydb", "MySQL URL is not correct: "~url); 550 | } 551 | 552 | private unittest { 553 | string url = makeDDBCUrl("sqlserver", "127.0.0.1", 1433, "mydb"); 554 | assert(url == "ddbc:sqlserver://127.0.0.1:1433/mydb", "SQL Server URL is not correct: "~url); 555 | } 556 | 557 | private unittest { 558 | string url = makeDDBCUrl("oracle", "127.0.0.1", 1521, "mydb"); 559 | assert(url == "ddbc:oracle://127.0.0.1:1521/mydb", "Oracle URL is not correct: "~url); 560 | } 561 | 562 | private unittest { 563 | string url = makeDDBCUrl("odbc", "127.0.0.1", 3306, "mydb"); 564 | assert(url == "ddbc:odbc://127.0.0.1:3306/mydb", "ODBC URL is not correct: "~url); 565 | } 566 | 567 | private unittest { 568 | string[string] params; 569 | params["user"] = "sa"; 570 | params["password"] = "p@ss"; 571 | params["driver"] = "FreeTDS"; 572 | 573 | string url = makeDDBCUrl("odbc", "localhost", 1433, null, params); 574 | // todo: check with this URL structure is even correct 575 | assert(url == "ddbc:odbc://localhost:1433?user=sa,password=p@ss,driver=FreeTDS", "ODBC URL is not correct: "~url); 576 | } 577 | 578 | private unittest { 579 | string[string] params; 580 | params["user"] = "sa"; 581 | params["password"] = "p@ss"; 582 | params["driver"] = "msodbcsql17"; 583 | 584 | string url = makeDDBCUrl("odbc", "localhost", 1433, null, params); 585 | // todo: check with this URL structure is even correct 586 | assert(url == "ddbc:odbc://localhost:1433?user=sa,password=p@ss,driver=msodbcsql17", "ODBC URL is not correct: "~url); 587 | } 588 | 589 | private unittest { 590 | //immutable string[string] params = ["dsn","myDSN"]; 591 | string[string] params; 592 | params["dsn"] = "myDSN"; 593 | 594 | string url = makeDDBCUrl("odbc", params); 595 | assert(url == "odbc://?dsn=myDSN", "ODBC URL is not correct: "~url); 596 | } 597 | -------------------------------------------------------------------------------- /source/ddbc/drivers/utils.d: -------------------------------------------------------------------------------- 1 | /** 2 | * DDBC - D DataBase Connector - abstraction layer for RDBMS access, with interface similar to JDBC. 3 | * 4 | * Source file ddbc/drivers/mysqlddbc.d. 5 | * 6 | * DDBC library attempts to provide implementation independent interface to different databases. 7 | * 8 | * Set of supported RDBMSs can be extended by writing Drivers for particular DBs. 9 | * 10 | * JDBC documentation can be found here: 11 | * $(LINK http://docs.oracle.com/javase/1.5.0/docs/api/java/sql/package-summary.html)$(BR) 12 | * 13 | * This module contains misc utility functions which may help in implementation of DDBC drivers. 14 | * 15 | * Copyright: Copyright 2013 16 | * License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 17 | * Author: Vadim Lopatin 18 | */ 19 | module ddbc.drivers.utils; 20 | 21 | private import std.conv : ConvException; 22 | private import std.datetime : Date, DateTime, TimeOfDay; 23 | private import std.datetime.date; 24 | private import std.datetime.systime : SysTime; 25 | private import std.datetime.timezone : UTC; 26 | private import std.format : formattedRead; 27 | //private import std.traits : isSomeString; 28 | private import std.algorithm : canFind; 29 | 30 | string copyCString(T)(const T* c, int actualLength = -1) if (is(T == char) || is (T == ubyte)) { 31 | const(T)* a = c; 32 | if(a is null) 33 | return null; 34 | 35 | if(actualLength == -1) { 36 | T[] ret; 37 | while(*a) { 38 | ret ~= *a; 39 | a++; 40 | } 41 | return cast(string)ret; 42 | } else { 43 | return cast(string)(a[0..actualLength].idup); 44 | } 45 | 46 | } 47 | 48 | SysTime parseSysTime(const string timestampString) @safe { 49 | try { 50 | import std.regex : match; 51 | if(match(timestampString, r"\d{4}-\D{3}-\d{2}.*")) { 52 | return SysTime.fromSimpleString(timestampString); 53 | } else if(match(timestampString, r".*[\+|\-]\d{1,2}:\d{1,2}|.*Z")) { 54 | return timestampString.canFind('-') ? 55 | SysTime.fromISOExtString(timestampString) : 56 | SysTime.fromISOString(timestampString); 57 | } else { 58 | return SysTime(parseDateTime(timestampString), UTC()); 59 | } 60 | } catch (ConvException e) { 61 | // error("Could not parse " ~ timestampString ~ " to SysTime", e); 62 | throw new DateTimeException("Can not convert '" ~ timestampString ~ "' to SysTime"); 63 | } 64 | } 65 | 66 | private static unittest { 67 | // Accept valid (as per D language) systime formats 68 | parseSysTime("2019-May-04 13:34:10.500Z"); 69 | parseSysTime("2019-Jan-02 13:34:10-03:00"); 70 | parseSysTime("2019-05-04T13:34:10.500Z"); 71 | parseSysTime("2019-06-14T13:34:10.500+01:00"); 72 | parseSysTime("2019-02-07T13:34:10Z"); 73 | parseSysTime("2019-08-12T13:34:10+01:00"); 74 | parseSysTime("2019-09-03T13:34:10"); 75 | 76 | // Accept valid (as per D language) date & datetime timestamps (will default timezone as UTC) 77 | parseSysTime("2010-Dec-30 00:00:00"); 78 | parseSysTime("2019-05-04 13:34:10"); 79 | // parseSysTime("2019-05-08"); 80 | 81 | // Accept non-standard (as per D language) timestamp formats 82 | //parseSysTime("2019-05-07 13:32"); // todo: handle missing seconds 83 | //parseSysTime("2019/05/07 13:32"); // todo: handle slash instead of hyphen 84 | //parseSysTime("2010-12-30 12:10:04.1+00"); // postgresql 85 | } 86 | 87 | DateTime parseDateTime(const string timestampString) @safe { 88 | try { 89 | import std.regex : match; 90 | if(match(timestampString, r"\d{8}T\d{6}")) { 91 | // ISO String: 'YYYYMMDDTHHMMSS' 92 | return DateTime.fromISOString(timestampString); 93 | } else if(match(timestampString, r"\d{4}-\D{3}-\d{2}.*")) { 94 | // Simple String 'YYYY-Mon-DD HH:MM:SS' 95 | return DateTime.fromSimpleString(timestampString); 96 | } else if(match(timestampString, r"\d{4}-\d{2}-\d{2}.*")) { 97 | // ISO ext string 'YYYY-MM-DDTHH:MM:SS' 98 | import std.string : translate; 99 | return DateTime.fromISOExtString(timestampString.translate( [ ' ': 'T' ] )); 100 | } 101 | throw new DateTimeException("Can not convert " ~ timestampString); 102 | } catch (ConvException e) { 103 | // error("Could not parse " ~ timestampString ~ " to SysTime", e); 104 | throw new DateTimeException("Can not convert '" ~ timestampString ~ "' to DateTime"); 105 | } 106 | } 107 | private static unittest { 108 | // Accept valid (as per D language) datetime formats 109 | parseDateTime("20101230T000000"); 110 | parseDateTime("2019-May-04 13:34:10"); 111 | parseDateTime("2019-Jan-02 13:34:10"); 112 | parseDateTime("2019-05-04T13:34:10"); 113 | 114 | // Accept non-standard (as per D language) timestamp formats 115 | parseDateTime("2019-06-14 13:34:10"); // accept a non-standard variation (space instead of T) 116 | //parseDateTime("2019-05-07 13:32"); // todo: handle missing seconds 117 | //parseDateTime("2019/05/07 13:32"); // todo: handle slash instead of hyphen 118 | } 119 | 120 | TimeOfDay parseTimeoid(const string timeoid) 121 | { 122 | string input = timeoid.dup; 123 | int hour, min, sec; 124 | formattedRead(input, "%s:%s:%s", &hour, &min, &sec); 125 | return TimeOfDay(hour, min, sec); 126 | } 127 | 128 | Date parseDateoid(const string dateoid) 129 | { 130 | string input = dateoid.dup; 131 | int year, month, day; 132 | formattedRead(input, "%s-%s-%s", &year, &month, &day); 133 | return Date(year, month, day); 134 | } 135 | -------------------------------------------------------------------------------- /source/ddbc/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | DDBC - D DataBase Connector - abstraction layer for RDBMS access, with interface similar to JDBC. 3 | 4 | Source file ddbc/package.d 5 | 6 | DDBC library attempts to provide implementation independent interface to different databases. API is similar to Java JDBC API. 7 | http://docs.oracle.com/javase/7/docs/technotes/guides/jdbc/ 8 | 9 | For using DDBC, import this file: 10 | 11 | 12 | import ddbc; 13 | 14 | 15 | 16 | Supported (built-in) RDBMS drivers: MySQL, PostgreSQL, SQLite 17 | 18 | Configuration name Version constants Drivers included 19 | -------------------------- ---------------------------------- --------------------------------- 20 | full USE_MYSQL, USE_SQLITE, USE_PGSQL mysql, sqlite, postgresql, odbc 21 | MySQL USE_MYSQL mysql 22 | SQLite USE_SQLITE sqlite 23 | PGSQL USE_PGSQL postgresql 24 | ODBC USE_ODBC odbc 25 | API (none) (no drivers, API only) 26 | 27 | 28 | When using in DUB based project, add "ddbc" dependency to your project's dub.json: 29 | 30 | "dependencies": { 31 | "ddbc": "~>0.2.35" 32 | } 33 | 34 | Default configuration is "full". You can choose other configuration by specifying subConfiguration for ddbc, e.g.: 35 | 36 | "subConfigurations": { 37 | "ddbc": "SQLite" 38 | } 39 | 40 | 41 | If you want to support all DDBC configuration in your project, use configurations section: 42 | 43 | "configurations": [ 44 | { 45 | "name": "default", 46 | "subConfigurations": { 47 | "ddbc": "full" 48 | } 49 | }, 50 | { 51 | "name": "MySQL", 52 | "subConfigurations": { 53 | "ddbc": "MySQL" 54 | } 55 | }, 56 | { 57 | "name": "SQLite", 58 | "subConfigurations": { 59 | "ddbc": "SQLite" 60 | } 61 | }, 62 | { 63 | "name": "PGSQL", 64 | "subConfigurations": { 65 | "ddbc": "PGSQL" 66 | } 67 | }, 68 | { 69 | "name": "API", 70 | "subConfigurations": { 71 | "ddbc": "API" 72 | } 73 | }, 74 | ] 75 | 76 | 77 | DDBC URLs 78 | ========= 79 | 80 | For creation of DDBC drivers or data sources, you can use DDBC URL. 81 | 82 | Common form of DDBC URL: driver://host:port/dbname?param1=value1,param2=value2 83 | 84 | As well, you can prefix url with "ddbc:" 85 | ddbc:driver://host:port/dbname?param1=value1,param2=value2 86 | 87 | Following helper function may be used to create URL 88 | 89 | string makeDDBCUrl(string driverName, string host, int port, string dbName, string[string] params = null); 90 | 91 | 92 | For PostgreSQL, use following form of URL: 93 | 94 | postgresql://host:port/dbname 95 | 96 | Optionally you can put user name, password, and ssl option as url parameters: 97 | 98 | postgresql://host:port/dbname?user=username,password=userpassword,ssl=true 99 | 100 | 101 | For MySQL, use following form of URL: 102 | 103 | mysql://host:port/dbname 104 | 105 | Optionally you can put user name and password as url parameters: 106 | 107 | mysql://host:port/dbname?user=username,password=userpassword 108 | 109 | 110 | For SQLite, use following form of URL: 111 | 112 | sqlite:db_file_path_name 113 | 114 | Sample urls: 115 | 116 | string pgsqlurl = "postgresql://localhost:5432/ddbctestdb?user=ddbctest,password=ddbctestpass,ssl=true"; 117 | string mysqlurl = "mysql://localhost:3306/ddbctestdb?user=ddbctest,password=ddbctestpass"; 118 | string sqliteurl = "sqlite:testdb.sqlite"; 119 | 120 | 121 | Drivers, connections, data sources and connection pools. 122 | ======================================================= 123 | 124 | 125 | Driver - factory interface for DB connections. This interface implements single method to create connections: 126 | 127 | Connection connect(string url, string[string] params); 128 | 129 | DataSource - factory interface for creating connections to specific DB instance, holds enough information to create connection using simple call of getConnection() 130 | 131 | ConnectionPool - DataSource which implements pool of opened connections to avoid slow connection establishment. It keeps several connections opened in pool. 132 | 133 | Connection - main object for dealing with DB. 134 | 135 | 136 | Driver may be created using one of factory methods: 137 | 138 | /// create driver by name, e.g. "mysql", "postgresql", "sqlite" 139 | DriverFactory.createDriver(string driverName); 140 | /// create driver by url, e.g. "mysql://host:port/db", "postgresql://host:port/db", "sqlite://" 141 | DriverFactory.createDriverForURL(string url); 142 | 143 | 144 | There are helper functions to create Connection, DataSource or ConnectionPool from URL and parameters. 145 | 146 | /// Helper function to create DDBC connection, automatically selecting driver based on URL 147 | Connection createConnection(string url, string[string]params = null); 148 | 149 | /// Helper function to create simple DDBC DataSource, automatically selecting driver based on URL 150 | DataSource createDataSource(string url, string[string]params = null); 151 | 152 | /// Helper function to create connection pool data source, automatically selecting driver based on URL 153 | DataSource createConnectionPool(string url, string[string]params = null, int maxPoolSize = 1, int timeToLive = 600, int waitTimeOut = 30); 154 | 155 | 156 | If you are planning to create several connections, consider using DataSource or ConnectionPool. 157 | 158 | For simple cases, it's enough to create connection directly. 159 | 160 | Connection conn = createConnection("sqlite:testfile.sqlite"); 161 | 162 | If you need to get / release connection multiple times, it makes sense to use ConnectionPool 163 | 164 | DataSource ds = createConnectionPool("ddbc:postgresql://localhost:5432/ddbctestdb?user=ddbctest,password=ddbctestpass,ssl=true"); 165 | // now we can take connection from pool when needed 166 | auto conn = ds.getConnection(); 167 | // and then release it back to pool when no more needed 168 | conn.close(); 169 | // if we call ds.getConnection() one more time, existing connection from pool will be used 170 | 171 | 172 | Copyright: Copyright 2014 173 | License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0). 174 | Author: Vadim Lopatin 175 | */ 176 | module ddbc; 177 | 178 | public import ddbc.core; 179 | public import ddbc.common; 180 | public import ddbc.pods; 181 | 182 | version( USE_SQLITE ) 183 | { 184 | // register SQLite driver 185 | private import ddbc.drivers.sqliteddbc; 186 | } 187 | version( USE_PGSQL ) 188 | { 189 | // register Postgres driver 190 | private import ddbc.drivers.pgsqlddbc; 191 | } 192 | version(USE_MYSQL) 193 | { 194 | // register MySQL driver 195 | private import ddbc.drivers.mysqlddbc; 196 | } 197 | version(USE_ODBC) 198 | { 199 | // register ODBC driver 200 | private import ddbc.drivers.odbcddbc; 201 | } -------------------------------------------------------------------------------- /test/ddbctest/common.d: -------------------------------------------------------------------------------- 1 | module ddbc.test.common; 2 | 3 | import std.stdio : stdout, writeln; 4 | 5 | import dunit; 6 | import ddbc.core : Connection, Statement; 7 | import ddbc.common : createConnection; 8 | 9 | class DdbcTestFixture { 10 | 11 | mixin UnitTest; 12 | 13 | protected Connection conn; 14 | 15 | private immutable string connectionString; 16 | private immutable string setupSql; 17 | private immutable string teardownSql; 18 | 19 | public this(string connectionString = null, string setupSql = null, string teardownSql = null) { 20 | this.connectionString = connectionString; 21 | this.setupSql = setupSql; 22 | this.teardownSql = teardownSql; 23 | 24 | static if (__traits(compiles, (){ import std.logger; } )) { 25 | import std.logger : globalLogLevel, sharedLog, LogLevel; 26 | import std.logger.core : StdForwardLogger; 27 | } else { 28 | import std.experimental.logger : globalLogLevel, sharedLog, LogLevel; 29 | import std.experimental.logger.core : StdForwardLogger; 30 | } 31 | 32 | //pragma(msg, "Setting 'std.logger : sharedLog' to use 'stdout' logging..."); 33 | globalLogLevel(LogLevel.all); 34 | //import std.logger.filelogger : FileLogger; 35 | //sharedLog = new FileLogger(stdout); 36 | //sharedLog = new StdForwardLogger(LogLevel.all); 37 | } 38 | 39 | @BeforeEach 40 | public void setUp() { 41 | //debug writeln("@BeforeEach : creating db connection : " ~ this.connectionString); 42 | conn = createConnection(this.connectionString); 43 | conn.setAutoCommit(true); 44 | 45 | Statement stmt = conn.createStatement(); 46 | scope(exit) stmt.close(); 47 | 48 | // fill database with test data 49 | if(this.setupSql !is null) { 50 | stmt.executeUpdate(this.setupSql); 51 | } 52 | } 53 | 54 | @AfterEach 55 | public void tearDown() { 56 | conn.setAutoCommit(true); 57 | //debug writeln("@AfterEach : tear down data"); 58 | Statement stmt = conn.createStatement(); 59 | //scope(exit) stmt.close(); 60 | 61 | // fill database with test data 62 | if(this.teardownSql !is null) { 63 | stmt.executeUpdate(this.teardownSql); 64 | } 65 | //debug writeln("@AfterEach : closing statement"); 66 | stmt.close(); 67 | //debug writeln("@AfterEach : closing db connection"); 68 | conn.close(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/ddbctest/main.d: -------------------------------------------------------------------------------- 1 | module ddbc.ddbctest; 2 | 3 | import std.conv : to; 4 | import std.datetime : Date, DateTime; 5 | import std.datetime.systime : SysTime, Clock; 6 | import std.format; 7 | import std.process: environment; 8 | import std.variant; 9 | import std.stdio; 10 | 11 | import dunit; 12 | import ddbc.test.common : DdbcTestFixture; 13 | import ddbc.core : Connection, PreparedStatement, Statement, SQLException, TransactionIsolation; 14 | import ddbc.pods; 15 | 16 | static import ddbc.core; 17 | 18 | // tests the use of exec update with raw sql and prepared statements 19 | pragma(msg, "DDBC test will run SQLite tests (always enabled)"); 20 | class SQLiteTest : DdbcTestFixture { 21 | mixin UnitTest; 22 | 23 | this() { 24 | super( 25 | "sqlite::memory:", 26 | "CREATE TABLE my_first_test (id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL)", 27 | "DROP TABLE IF EXISTS my_first_test" 28 | ); 29 | } 30 | 31 | @Test 32 | public void testExecutingRawSqlInsertStatements() { 33 | Statement stmt = conn.createStatement(); 34 | scope(exit) stmt.close(); 35 | 36 | int result1 = stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('MY TEST')`); 37 | assertEquals(1, result1); 38 | 39 | Variant id; 40 | int result2 = stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('MY TEST')`, id); 41 | assertEquals(1, result2); 42 | assertEquals("long", to!string(id.type)); 43 | assertEquals(2L, id.get!(long)); 44 | } 45 | 46 | @Test 47 | public void testExecutingPreparedSqlInsertStatements() { 48 | Statement stmt = conn.createStatement(); 49 | scope(exit) stmt.close(); 50 | 51 | stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('Apple')`); 52 | stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('Orange')`); 53 | stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('Banana')`); 54 | 55 | PreparedStatement ps = conn.prepareStatement(`SELECT * FROM my_first_test WHERE name = ?`); 56 | scope(exit) ps.close(); 57 | 58 | ps.setString(1, "Orange"); 59 | 60 | ddbc.core.ResultSet resultSet = ps.executeQuery(); 61 | 62 | //assertEquals(1, resultSet.getFetchSize()); // getFetchSize() isn't supported by SQLite 63 | assertTrue(resultSet.next()); 64 | 65 | // int result1 = stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('MY TEST')`); 66 | // assertEquals(1, result1); 67 | 68 | // Variant id; 69 | // int result2 = stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('MY TEST')`, id); 70 | // assertEquals(1, result2); 71 | // assertEquals("long", to!string(id.type)); 72 | // assertEquals(2L, id.get!(long)); 73 | } 74 | 75 | @Test 76 | public void testResultSetForEach() { 77 | Statement stmt = conn.createStatement(); 78 | scope(exit) stmt.close(); 79 | 80 | stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('Goober')`); 81 | stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('Goober')`); 82 | 83 | PreparedStatement ps = conn.prepareStatement(`SELECT * FROM my_first_test WHERE name = ?`); 84 | scope(exit) ps.close(); 85 | 86 | ps.setString(1, "Goober"); 87 | 88 | ddbc.core.ResultSet resultSet = ps.executeQuery(); 89 | 90 | int count = 0; 91 | foreach (result; resultSet) { 92 | count++; 93 | } 94 | assert(count == 2); 95 | } 96 | } 97 | 98 | 99 | // tests the use of POD 100 | class SQLitePodTest : DdbcTestFixture { 101 | mixin UnitTest; 102 | 103 | // our POD object (needs to be a struct) 104 | private struct User { 105 | long id; 106 | string name; 107 | int flags; 108 | Date dob; 109 | DateTime created; 110 | SysTime updated; 111 | } 112 | 113 | // todo: look into getting the same functionality with a class 114 | // class User { 115 | // long id; 116 | // string name; 117 | // int flags; 118 | // Date dob; 119 | // DateTime created; 120 | // override string toString() { 121 | // return format("{id: %s, name: %s, flags: %s, dob: %s, created: %s}", id, name, flags, dob, created); 122 | // } 123 | // } 124 | 125 | this() { 126 | super( 127 | "sqlite::memory:", 128 | "CREATE TABLE user (id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL, flags int null, dob DATE, created DATETIME, updated DATETIME)", 129 | "DROP TABLE IF EXISTS user" 130 | ); 131 | } 132 | 133 | @Test 134 | public void testInsertingPodWithoutDefiningId() { 135 | Statement stmt = conn.createStatement(); 136 | scope(exit) stmt.close(); 137 | 138 | immutable SysTime now = Clock.currTime(); 139 | 140 | User u; 141 | u.name = "Test Person"; 142 | u.flags = 1; 143 | u.dob = Date(1979, 8, 5); 144 | u.created = cast(DateTime) now; 145 | u.updated = now; 146 | 147 | assertEquals(0, u.id, "default value is 0"); 148 | bool inserted = stmt.insert!User(u); 149 | assertTrue(inserted); 150 | assertEquals(1, u.id, "a proper value is now assigned based on the database value"); 151 | } 152 | 153 | @Test // Test for: https://github.com/buggins/ddbc/issues/89 154 | public void testInsertingPodWithZeroDefinedId() { 155 | Statement stmt = conn.createStatement(); 156 | scope(exit) stmt.close(); 157 | 158 | immutable SysTime now = Clock.currTime(); 159 | 160 | User u; 161 | u.id = 0; 162 | u.name = "Test Person"; 163 | u.flags = 1; 164 | u.dob = Date(1979, 8, 5); 165 | u.created = cast(DateTime) now; 166 | u.updated = now; 167 | 168 | assertEquals(0, u.id, "default value is 0"); 169 | bool inserted = stmt.insert!User(u); 170 | assertTrue(inserted); 171 | assertEquals(1, u.id, "a proper value is now assigned based on the database value"); 172 | 173 | immutable User result = stmt.get!User(u.id); 174 | assertEquals(u.id, result.id); 175 | assertEquals(u.name, result.name); 176 | assertEquals(u.flags, result.flags); 177 | assertEquals(u.dob, result.dob); 178 | assertEquals(u.created, result.created); 179 | assertEquals(u.updated, result.updated); 180 | } 181 | 182 | @Test // Test for: https://github.com/buggins/ddbc/issues/89 183 | public void testInsertingPodWithNonZeroDefinedId() { 184 | Statement stmt = conn.createStatement(); 185 | scope(exit) stmt.close(); 186 | 187 | immutable SysTime now = Clock.currTime(); 188 | 189 | User u; 190 | u.id = 55L; // setting a non-zero value is effectively ignored when performing an insert with a pod 191 | u.name = "Test Person"; 192 | u.flags = 1; 193 | u.dob = Date(1979, 8, 5); 194 | u.created = cast(DateTime) now; 195 | u.updated = now; 196 | 197 | assertEquals(55L, u.id, "the struct will have our assigned value prior to the insert"); 198 | bool inserted = stmt.insert!User(u); 199 | assertTrue(inserted); 200 | assertEquals(1, u.id, "a proper value is now assigned based on the database value"); 201 | 202 | immutable User result = stmt.get!User(u.id); 203 | assertEquals(u.id, result.id); 204 | assertEquals(u.name, result.name); 205 | assertEquals(u.flags, result.flags); 206 | assertEquals(u.dob, result.dob); 207 | assertEquals(u.created, result.created); 208 | assertEquals(u.updated, result.updated); 209 | } 210 | 211 | @Test // Test for: https://github.com/buggins/ddbc/issues/89 212 | public void testInsertingPodWithIdSizeT() { 213 | Statement stmt = conn.createStatement(); 214 | scope(exit) stmt.close(); 215 | 216 | // A POD with a size_t for an id 217 | struct User { 218 | size_t id; 219 | string name; 220 | int flags; 221 | Date dob; 222 | DateTime created; 223 | SysTime updated; 224 | } 225 | 226 | User u; 227 | u.id = 0; 228 | u.name = "Test 89"; 229 | u.flags = 5; 230 | 231 | assertEquals(0, u.id, "default value is 0"); 232 | bool inserted = stmt.insert!User(u); 233 | assertTrue(inserted, "Should be able to perform INSERT with pod"); 234 | assertEquals(1, u.id, "Should auto generate an ID"); 235 | 236 | immutable User result = stmt.get!User(u.id); 237 | assertEquals(u.id, result.id); 238 | assertEquals(u.name, result.name); 239 | assertEquals(u.flags, result.flags); 240 | assertEquals(u.dob, result.dob); 241 | assertEquals(u.created, result.created); 242 | assertEquals(u.updated, result.updated); 243 | } 244 | 245 | @Test // Test for: https://github.com/buggins/ddbc/issues/89 246 | public void testInsertingPodWithIdInt() { 247 | Statement stmt = conn.createStatement(); 248 | scope(exit) stmt.close(); 249 | 250 | // A POD with an int for an id 251 | struct User { 252 | int id; 253 | string name; 254 | int flags; 255 | Date dob; 256 | DateTime created; 257 | SysTime updated; 258 | } 259 | 260 | User u; 261 | u.id = 0; 262 | u.name = "Test 89"; 263 | u.flags = 5; 264 | 265 | assertEquals(0, u.id, "default value is 0"); 266 | bool inserted = stmt.insert!User(u); 267 | assertTrue(inserted, "Should be able to perform INSERT with pod"); 268 | assertEquals(1, u.id, "Should auto generate an ID"); 269 | 270 | immutable User result = stmt.get!User(u.id); 271 | assertEquals(u.id, result.id); 272 | assertEquals(u.name, result.name); 273 | assertEquals(u.flags, result.flags); 274 | assertEquals(u.dob, result.dob); 275 | assertEquals(u.created, result.created); 276 | assertEquals(u.updated, result.updated); 277 | } 278 | 279 | @Test // Test for: https://github.com/buggins/ddbc/issues/89 280 | public void testInsertingPodWithIdUint() { 281 | Statement stmt = conn.createStatement(); 282 | scope(exit) stmt.close(); 283 | 284 | // A POD with an uint for an id 285 | struct User { 286 | uint id; 287 | string name; 288 | int flags; 289 | Date dob; 290 | DateTime created; 291 | SysTime updated; 292 | } 293 | 294 | User u; 295 | u.id = 0; 296 | u.name = "Test 89"; 297 | u.flags = 5; 298 | 299 | assertEquals(0, u.id, "default value is 0"); 300 | bool inserted = stmt.insert!User(u); 301 | assertTrue(inserted, "Should be able to perform INSERT with pod"); 302 | assertEquals(1, u.id, "Should auto generate an ID"); 303 | 304 | immutable User result = stmt.get!User(u.id); 305 | assertEquals(u.id, result.id); 306 | assertEquals(u.name, result.name); 307 | assertEquals(u.flags, result.flags); 308 | assertEquals(u.dob, result.dob); 309 | assertEquals(u.created, result.created); 310 | assertEquals(u.updated, result.updated); 311 | } 312 | 313 | @Test // Test for: https://github.com/buggins/ddbc/issues/89 314 | public void testInsertingPodWithIdLong() { 315 | Statement stmt = conn.createStatement(); 316 | scope(exit) stmt.close(); 317 | 318 | // A POD with an long for an id 319 | struct User { 320 | long id; 321 | string name; 322 | int flags; 323 | Date dob; 324 | DateTime created; 325 | SysTime updated; 326 | } 327 | 328 | User u; 329 | u.id = 0; 330 | u.name = "Test 89"; 331 | u.flags = 5; 332 | 333 | assertEquals(0, u.id, "default value is 0"); 334 | bool inserted = stmt.insert!User(u); 335 | assertTrue(inserted, "Should be able to perform INSERT with pod"); 336 | assertEquals(1, u.id, "Should auto generate an ID"); 337 | 338 | immutable User result = stmt.get!User(u.id); 339 | assertEquals(u.id, result.id); 340 | assertEquals(u.name, result.name); 341 | assertEquals(u.flags, result.flags); 342 | assertEquals(u.dob, result.dob); 343 | assertEquals(u.created, result.created); 344 | assertEquals(u.updated, result.updated); 345 | } 346 | 347 | @Test // Test for: https://github.com/buggins/ddbc/issues/89 348 | public void testInsertingPodWithIdUlong() { 349 | Statement stmt = conn.createStatement(); 350 | scope(exit) stmt.close(); 351 | 352 | // A POD with an ulong for an id 353 | struct User { 354 | ulong id; 355 | string name; 356 | int flags; 357 | Date dob; 358 | DateTime created; 359 | SysTime updated; 360 | } 361 | 362 | User u; 363 | u.id = 0; 364 | u.name = "Test 89"; 365 | u.flags = 5; 366 | 367 | assertEquals(0, u.id, "default value is 0"); 368 | bool inserted = stmt.insert!User(u); 369 | assertTrue(inserted, "Should be able to perform INSERT with pod"); 370 | assertEquals(1, u.id, "Should auto generate an ID"); 371 | 372 | immutable User result = stmt.get!User(u.id); 373 | assertEquals(u.id, result.id); 374 | assertEquals(u.name, result.name); 375 | assertEquals(u.flags, result.flags); 376 | assertEquals(u.dob, result.dob); 377 | assertEquals(u.created, result.created); 378 | assertEquals(u.updated, result.updated); 379 | } 380 | 381 | @Test 382 | public void testGettingPodById() { 383 | Statement stmt = conn.createStatement(); 384 | scope(exit) stmt.close(); 385 | 386 | stmt.executeUpdate(`INSERT INTO user (id, name, flags, dob, created, updated) VALUES (12, "Jessica", 5, "1985-04-18", "2017-11-23T20:45", "2018-03-11T00:30:59Z")`); 387 | 388 | immutable User u = stmt.get!User(12L); // testing this function 389 | 390 | //writeln("id: ", u.id, " name: ", u.name, " flags: ", u.flags, ", dob: ", u.dob, ", created: ", u.created, ", updated: ", u.updated); 391 | assertEquals(12, u.id); 392 | assertEquals("immutable(long)", typeof(u.id).stringof); 393 | assertEquals("immutable(long)", typeid(u.id).toString()); 394 | assertEquals("Jessica", u.name); 395 | assertEquals(5, u.flags); 396 | 397 | // dob Date "1985-04-18": 398 | assertEquals(1985, u.dob.year); 399 | assertEquals(4, u.dob.month); 400 | assertEquals(18, u.dob.day); 401 | 402 | // created DateTime "2017-11-23T20:45": 403 | assertEquals(2017, u.created.year); 404 | assertEquals(11, u.created.month); 405 | assertEquals(23, u.created.day); 406 | assertEquals(20, u.created.hour); 407 | assertEquals(45, u.created.minute); 408 | assertEquals(0, u.created.second); 409 | 410 | // updated SysTime "2018-03-11T00:30:59Z": 411 | assertEquals(2018, u.updated.year); 412 | assertEquals(3, u.updated.month); 413 | assertEquals(11, u.updated.day); 414 | assertEquals(0, u.updated.hour); 415 | assertEquals(30, u.updated.minute); 416 | assertEquals(59, u.updated.second); 417 | } 418 | 419 | @Test // Test for: https://github.com/buggins/ddbc/issues/89 (see the equivelant insert test as well) 420 | public void testGettingPodByIdSizeT() { 421 | Statement stmt = conn.createStatement(); 422 | scope(exit) stmt.close(); 423 | 424 | stmt.executeUpdate(`INSERT INTO user (id, name, flags, dob, created, updated) VALUES (10000, "Sheila", 5, "1985-04-18", "2017-11-23T20:45", "2018-03-11T00:30:59Z")`); 425 | 426 | // A POD with a size_t for an id 427 | struct User { 428 | size_t id; 429 | string name; 430 | int flags; 431 | Date dob; 432 | DateTime created; 433 | SysTime updated; 434 | } 435 | 436 | immutable User u = stmt.get!User(10_000); // testing this function 437 | 438 | assertEquals(10_000, u.id); 439 | // assertEquals("immutable(ulong)", typeof(u.id).stringof); // different behaviour accross operating systems (Windows was uint) 440 | // assertEquals("immutable(ulong)", typeid(u.id).toString()); // different behaviour accross operating systems (Windows was uint) 441 | assertEquals("Sheila", u.name); 442 | } 443 | 444 | @Test 445 | public void testSelectAllPod() { 446 | Statement stmt = conn.createStatement(); 447 | scope(exit) stmt.close(); 448 | 449 | stmt.executeUpdate(`INSERT INTO user (id, name, flags, dob, created, updated) VALUES (1, "John", 5, "1976-04-18", "2017-11-23T20:45", "2010-12-30T00:00:00Z")`); 450 | 451 | writeln("reading all user table rows"); 452 | 453 | auto users = stmt.select!User; 454 | 455 | assertFalse(users.empty()); 456 | 457 | foreach(ref u; users) { 458 | //writeln("id: ", u.id, " name: ", u.name, " flags: ", u.flags, ", dob: ", u.dob, ", created: ", u.created, ", updated: ", u.updated); 459 | 460 | assertEquals(1, u.id); 461 | assertEquals("John", u.name); 462 | assertEquals(5, u.flags); 463 | 464 | // dob Date "1976-04-18": 465 | assertEquals(1976, u.dob.year); 466 | assertEquals(4, u.dob.month); 467 | assertEquals(18, u.dob.day); 468 | 469 | // created DateTime "2017-11-23T20:45": 470 | assertEquals(2017, u.created.year); 471 | assertEquals(11, u.created.month); 472 | assertEquals(23, u.created.day); 473 | assertEquals(20, u.created.hour); 474 | assertEquals(45, u.created.minute); 475 | assertEquals(0, u.created.second); 476 | 477 | // updated SysTime "2010-12-30T03:15:28Z": 478 | assertEquals(2010, u.updated.year); 479 | assertEquals(12, u.updated.month); 480 | assertEquals(30, u.updated.day); 481 | assertEquals(3, u.updated.hour); 482 | assertEquals(15, u.updated.minute); 483 | assertEquals(28, u.updated.second); 484 | } 485 | } 486 | 487 | @Test 488 | public void testQueryUsersWhereIdLessThanSix() { 489 | givenMultipleUsersInDatabase(); 490 | 491 | Statement stmt = conn.createStatement(); 492 | scope(exit) stmt.close(); 493 | 494 | writeln("\nReading user table rows with WHERE id < 6 ORDER BY name DESC..."); 495 | 496 | auto users = stmt.select!User.where("id < 6").orderBy("name desc"); 497 | 498 | //assertFalse(users.empty()); // this causes a bug due to empty() calling next() 499 | 500 | int count = 0; 501 | foreach(ref u; users) { 502 | count++; 503 | assertTrue(u.id < 6); 504 | writeln(" ", count, ": { id: ", u.id, " name: ", u.name, " flags: ", u.flags, ", dob: ", u.dob, ", created: ", u.created, ", updated: ", u.updated, " }"); 505 | } 506 | 507 | assertEquals(5, count); 508 | } 509 | 510 | @Test 511 | public void testQueryUsersWhereIdLessThanSixWithLimitThree() { 512 | givenMultipleUsersInDatabase(); 513 | 514 | Statement stmt = conn.createStatement(); 515 | scope(exit) stmt.close(); 516 | 517 | writeln("\nReading user table rows with WHERE id < 6 ORDER BY name DESC LIMIT 3..."); 518 | 519 | auto users = stmt.select!User.where("id < 6").orderBy("name desc").limit(3); 520 | 521 | //assertFalse(users.empty()); // this causes a bug due to empty() calling next() 522 | 523 | int count = 0; 524 | foreach(e; users) { 525 | count++; 526 | writeln(" ", count, ": { id: ", e.id, " name: ", e.name, " flags: ", e.flags, " }"); 527 | } 528 | 529 | assertEquals(3, count); 530 | } 531 | 532 | @Test 533 | public void testQueryUsersWhereIdLessThanSixWithLimitThreeAndOffsetTwo() { 534 | givenMultipleUsersInDatabase(); 535 | 536 | Statement stmt = conn.createStatement(); 537 | scope(exit) stmt.close(); 538 | 539 | writeln("\nReading user table rows with WHERE id < 6 ORDER BY name DESC LIMIT 3 OFFSET 2..."); 540 | 541 | auto users = stmt.select!User.where("id < 6").orderBy("name desc").limit(3).offset(2); 542 | 543 | //assertFalse(users.empty()); // this causes a bug due to empty() calling next() 544 | 545 | int count = 0; 546 | foreach(e; users) { 547 | count++; 548 | writeln(" ", count, ": { id: ", e.id, " name: ", e.name, " flags: ", e.flags, " }"); 549 | } 550 | 551 | assertEquals(3, count); 552 | } 553 | 554 | // Select all user table rows, but fetching only id and name (you will see default value 0 in flags field) 555 | @Test 556 | public void testQueryAllUsersJustIdAndName() { 557 | givenMultipleUsersInDatabase(); 558 | 559 | Statement stmt = conn.createStatement(); 560 | scope(exit) stmt.close(); 561 | 562 | writeln("\nReading all user table rows, but fetching only id and name (you will see default value 0 in flags field)"); 563 | int count = 0; 564 | foreach(ref u; stmt.select!(User, "id", "name")) { 565 | count++; 566 | assertTrue(u.id > 0); 567 | assertTrue(u.name.length > 0); 568 | writeln(" ", count, ": { id: ", u.id, " name: ", u.name, " flags: ", u.flags, ", dob: ", u.dob, ", created: ", u.created, ", updated: ", u.updated, " }"); 569 | } 570 | assertEquals(6, count); 571 | } 572 | 573 | // Select all user table rows, but fetching only id and name, placing result into vars 574 | @Test 575 | public void testQueryAllUsersJustIdAndName_IntoVars() { 576 | givenMultipleUsersInDatabase(); 577 | 578 | Statement stmt = conn.createStatement(); 579 | scope(exit) stmt.close(); 580 | 581 | int count = 0; 582 | writeln("\nReading all user table rows, but fetching only id and name, placing result into vars"); 583 | long id; 584 | string name; 585 | foreach(ref resultNumber; stmt.select!()("SELECT id, name FROM user", id, name)) { 586 | assertEquals(count, resultNumber); 587 | 588 | assertTrue(id > 0); 589 | assertTrue(name.length > 0); 590 | 591 | count++; 592 | } 593 | assertEquals(6, count); 594 | } 595 | 596 | // @Test 597 | // public void testQueryUserThenUpdate() { 598 | // givenMultipleUsersInDatabase(); 599 | 600 | // Statement stmt = conn.createStatement(); 601 | // scope(exit) stmt.close(); 602 | 603 | // //writeln("\nSelect user id=1, change name to 'JB' (:))"); 604 | // auto results = stmt.select!User.where("id=1"); 605 | 606 | // foreach(ref u; results) { // <--- doesn't work for some reason 607 | // u.name = "JB"; 608 | // assertTrue(stmt.update(u)); 609 | // } 610 | 611 | // User u = stmt.get!User(1L); 612 | // assertEquals("JB", u.name); 613 | // } 614 | 615 | @Test 616 | public void testGetUserThenUpdate() { 617 | givenMultipleUsersInDatabase(); 618 | 619 | Statement stmt = conn.createStatement(); 620 | scope(exit) stmt.close(); 621 | 622 | User u = stmt.get!User(3L); 623 | assertEquals("Walter", u.name); 624 | 625 | u.name = "Walter Bright"; 626 | assertTrue(stmt.update(u)); 627 | 628 | u = stmt.get!User(3L); 629 | assertEquals("Walter Bright", u.name); 630 | } 631 | 632 | @Test 633 | public void testGetNonExistingRowShouldThrowException() { 634 | Statement stmt = conn.createStatement(); 635 | scope(exit) stmt.close(); 636 | 637 | bool exCaught = false; 638 | //writeln("\nGet user id=789 (throws!)"); 639 | 640 | try { 641 | User u = stmt.get!User(789L); 642 | assertTrue(false, "Should not get here"); 643 | } catch (SQLException e) { 644 | exCaught = true; 645 | writeln("Exception thrown as expected."); 646 | } 647 | assertTrue(exCaught, "There should be an exception"); 648 | } 649 | 650 | @Test 651 | public void testRemovingPod() { 652 | Statement stmt = conn.createStatement(); 653 | scope(exit) stmt.close(); 654 | 655 | stmt.executeUpdate(`INSERT INTO user (id, name, flags, dob, created, updated) VALUES (123, "Steve", 5, "1976-04-18", "2017-11-23T20:45", "2010-12-30T00:00:00Z")`); 656 | 657 | User u = stmt.get!User(123L); 658 | //User u = stmt.select!User.where("id = 111").front(); 659 | 660 | // make sure we have the user: 661 | assertEquals(123, u.id); 662 | assertEquals("Steve", u.name); 663 | 664 | bool removed = stmt.remove!User(u); 665 | 666 | assertTrue(removed, "Should return true on successful removal"); 667 | 668 | auto users = stmt.select!User; 669 | 670 | assertTrue(users.empty(), "There shouldn't be users in the table"); 671 | } 672 | 673 | @Test 674 | public void testDeletingPodById() { 675 | Statement stmt = conn.createStatement(); 676 | scope(exit) stmt.close(); 677 | 678 | stmt.executeUpdate(`INSERT INTO user (id, name, flags, dob, created, updated) VALUES (111, "Sharon", 5, "1976-04-18", "2017-11-23T20:45", "2010-12-30T00:00:00Z")`); 679 | 680 | User u; 681 | u.id = 111; 682 | 683 | bool removed = stmt.remove!User(u); 684 | 685 | assertTrue(removed, "Should return true on successful removal"); 686 | 687 | auto users = stmt.select!User; 688 | 689 | assertTrue(users.empty(), "There shouldn't be users in the table"); 690 | } 691 | 692 | private void givenMultipleUsersInDatabase() { 693 | Statement stmt = conn.createStatement(); 694 | scope(exit) stmt.close(); 695 | 696 | stmt.executeUpdate(`INSERT INTO user (id, name, flags, dob, created, updated) VALUES (1, "John", 5, "1976-04-18", "2017-11-23T20:45", "2010-12-30T00:00:00Z")`); 697 | stmt.executeUpdate(`INSERT INTO user (id, name, flags, dob, created, updated) VALUES (2, "Andrei", 2, "1977-09-11", "2018-02-28T13:45", "2010-12-30T12:10:12Z")`); 698 | stmt.executeUpdate(`INSERT INTO user (id, name, flags, dob, created, updated) VALUES (3, "Walter", 2, "1986-03-21", "2018-03-08T10:30", "2010-12-30T12:10:04.100Z")`); 699 | stmt.executeUpdate(`INSERT INTO user (id, name, flags, dob, created, updated) VALUES (4, "Rikki", 3, "1979-05-24", "2018-06-13T11:45", "2010-12-30T12:10:58Z")`); 700 | stmt.executeUpdate(`INSERT INTO user (id, name, flags, dob, created, updated) VALUES (5, "Iain", 0, "1971-11-12", "2018-11-09T09:33", "20101230T121001Z")`); 701 | stmt.executeUpdate(`INSERT INTO user (id, name, flags, dob, created, updated) VALUES (6, "Robert", 1, "1966-03-19", CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`); 702 | } 703 | } 704 | 705 | // Test parts of the interfaces related to transactions. 706 | class SQLiteTransactionTest : DdbcTestFixture { 707 | mixin UnitTest; 708 | 709 | this() { 710 | super( 711 | "sqlite::memory:", 712 | "CREATE TABLE records (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL)", 713 | "DROP TABLE IF EXISTS records"); 714 | } 715 | 716 | @Test 717 | public void testAutocommitOn() { 718 | // This is the default state, it is merely made explicit here. 719 | conn.setAutoCommit(true); 720 | Statement stmt = conn.createStatement(); 721 | scope(exit) stmt.close(); 722 | 723 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Bob')`); 724 | conn.rollback(); 725 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Jim')`); 726 | conn.commit(); 727 | 728 | ddbc.core.ResultSet resultSet; 729 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Bob'`); 730 | assert(resultSet.next()); 731 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Jim'`); 732 | assert(resultSet.next()); 733 | } 734 | 735 | @Test 736 | public void testAutocommitOff() { 737 | // With autocommit set to false, transactions must be explicitly committed. 738 | conn.setAutoCommit(false); 739 | conn.setAutoCommit(false); // Duplicate calls should not cause errors. 740 | Statement stmt = conn.createStatement(); 741 | scope(exit) stmt.close(); 742 | 743 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Greg')`); 744 | conn.rollback(); 745 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Tom')`); 746 | conn.commit(); 747 | 748 | ddbc.core.ResultSet resultSet; 749 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Greg'`); 750 | assert(!resultSet.next()); 751 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Tom'`); 752 | assert(resultSet.next()); 753 | } 754 | 755 | @Test 756 | public void testAutocommitOffOn() { 757 | // A test with a user changing autocommit in between statements. 758 | conn.setAutoCommit(false); 759 | Statement stmt = conn.createStatement(); 760 | scope(exit) stmt.close(); 761 | 762 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Abe')`); 763 | conn.setAutoCommit(true); 764 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Bart')`); 765 | 766 | ddbc.core.ResultSet resultSet; 767 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Abe'`); 768 | assert(resultSet.next()); 769 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Bart'`); 770 | assert(resultSet.next()); 771 | } 772 | 773 | @Test 774 | public void testTransactionIsolation() { 775 | // Setting isolation level is only effective in transactions. 776 | conn.setAutoCommit(false); 777 | // In SQLite, SERIALIZABLE is the default and not settable. 778 | assert(conn.getTransactionIsolation() == TransactionIsolation.SERIALIZABLE); 779 | conn.setTransactionIsolation(TransactionIsolation.REPEATABLE_READ); 780 | assert(conn.getTransactionIsolation() == TransactionIsolation.SERIALIZABLE); 781 | } 782 | } 783 | 784 | // either use the 'Main' mixin or call 'dunit_main(args)' 785 | mixin Main; 786 | -------------------------------------------------------------------------------- /test/ddbctest/mysqltest.d: -------------------------------------------------------------------------------- 1 | module ddbc.mysqltest; 2 | 3 | import std.conv : to; 4 | import std.datetime : Date, DateTime; 5 | import std.datetime.systime : SysTime, Clock; 6 | import std.format; 7 | import std.process: environment; 8 | import std.variant; 9 | import std.stdio; 10 | 11 | import dunit; 12 | import ddbc.test.common : DdbcTestFixture; 13 | import ddbc.core : Connection, PreparedStatement, Statement, SQLException, TransactionIsolation; 14 | import ddbc.pods; 15 | 16 | static import ddbc.core; 17 | 18 | version(USE_MYSQL) { 19 | pragma(msg, "DDBC test will run MySQL tests"); 20 | 21 | class MySQLTest : DdbcTestFixture { 22 | mixin UnitTest; 23 | 24 | this() { 25 | super( 26 | "ddbc:mysql://localhost:%s/testdb?user=testuser,password=passw0rd".format(environment.get("MYSQL_PORT", "3306")), 27 | "CREATE TABLE `my_first_test` (`id` INTEGER AUTO_INCREMENT PRIMARY KEY, `name` VARCHAR(255) NOT NULL)", 28 | "DROP TABLE IF EXISTS `my_first_test`" 29 | ); 30 | } 31 | 32 | @Test 33 | public void testVerifyTableExists() { 34 | Statement stmt = conn.createStatement(); 35 | scope(exit) stmt.close(); 36 | 37 | //ddbc.core.ResultSet resultSet = stmt.executeQuery("SHOW TABLES"); 38 | ddbc.core.ResultSet resultSet = stmt.executeQuery("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_NAME = 'my_first_test'"); 39 | 40 | assertEquals(1, resultSet.getFetchSize()); // MySQL can support getFetchSize() 41 | assertTrue(resultSet.next()); 42 | } 43 | 44 | @Test 45 | public void testExecutingRawSqlInsertStatements() { 46 | Statement stmt = conn.createStatement(); 47 | scope(exit) stmt.close(); 48 | 49 | int result1 = stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('MY TEST')`); 50 | assertEquals(1, result1); 51 | 52 | Variant id; 53 | int result2 = stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('MY TEST')`, id); 54 | assertEquals(1, result2); 55 | //assertEquals("long", to!string(id.type)); 56 | //assertEquals(2L, id.get!(long)); 57 | } 58 | } 59 | 60 | // Test parts of the interfaces related to transactions. 61 | class MySQLTransactionTest : DdbcTestFixture { 62 | mixin UnitTest; 63 | 64 | this() { 65 | super( 66 | "ddbc:mysql://localhost:%s/testdb?user=testuser,password=passw0rd".format(environment.get("MYSQL_PORT", "3306")), 67 | "CREATE TABLE records (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL)", 68 | "DROP TABLE IF EXISTS records" 69 | ); 70 | } 71 | 72 | @Test 73 | public void testAutocommitOn() { 74 | // This is the default state, it is merely made explicit here. 75 | conn.setAutoCommit(true); 76 | Statement stmt = conn.createStatement(); 77 | scope(exit) stmt.close(); 78 | 79 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Bob')`); 80 | conn.rollback(); 81 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Jim')`); 82 | conn.commit(); 83 | 84 | ddbc.core.ResultSet resultSet; 85 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Bob'`); 86 | assert(resultSet.next()); 87 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Jim'`); 88 | assert(resultSet.next()); 89 | } 90 | 91 | @Test 92 | public void testAutocommitOff() { 93 | // With autocommit set to false, transactions must be explicitly committed. 94 | conn.setAutoCommit(false); 95 | conn.setAutoCommit(false); // Duplicate calls should not cause errors. 96 | Statement stmt = conn.createStatement(); 97 | scope(exit) stmt.close(); 98 | 99 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Greg')`); 100 | conn.rollback(); 101 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Tom')`); 102 | conn.commit(); 103 | 104 | ddbc.core.ResultSet resultSet; 105 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Greg'`); 106 | assert(!resultSet.next()); 107 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Tom'`); 108 | assert(resultSet.next()); 109 | } 110 | 111 | @Test 112 | public void testAutocommitOffOn() { 113 | // A test with a user changing autocommit in between statements. 114 | conn.setAutoCommit(false); 115 | Statement stmt = conn.createStatement(); 116 | scope(exit) stmt.close(); 117 | 118 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Abe')`); 119 | conn.setAutoCommit(true); 120 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Bart')`); 121 | 122 | ddbc.core.ResultSet resultSet; 123 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Abe'`); 124 | assert(resultSet.next()); 125 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Bart'`); 126 | assert(resultSet.next()); 127 | } 128 | 129 | @Test 130 | public void testTransactionIsolation() { 131 | // Setting isolation level is only effective in transactions. 132 | conn.setAutoCommit(false); 133 | // In MySQL, REPEATABLE_READ is the default. 134 | assert(conn.getTransactionIsolation() == TransactionIsolation.REPEATABLE_READ); 135 | conn.setTransactionIsolation(TransactionIsolation.READ_COMMITTED); 136 | assert(conn.getTransactionIsolation() == TransactionIsolation.READ_COMMITTED); 137 | conn.setTransactionIsolation(TransactionIsolation.SERIALIZABLE); 138 | assert(conn.getTransactionIsolation() == TransactionIsolation.SERIALIZABLE); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /test/ddbctest/odbctest.d: -------------------------------------------------------------------------------- 1 | module ddbc.odbctest; 2 | 3 | import std.conv : to; 4 | import std.datetime : Date, DateTime; 5 | import std.datetime.systime : SysTime, Clock; 6 | import std.format; 7 | import std.process: environment; 8 | import std.variant; 9 | import std.stdio; 10 | 11 | import dunit; 12 | import ddbc.test.common : DdbcTestFixture; 13 | import ddbc.core : Connection, PreparedStatement, Statement, SQLException, TransactionIsolation; 14 | import ddbc.pods; 15 | 16 | static import ddbc.core; 17 | 18 | version(USE_ODBC) { 19 | pragma(msg, "DDBC test will run SQL Server tests"); 20 | class SQLServerTest : DdbcTestFixture { 21 | mixin UnitTest; 22 | 23 | this() { 24 | // Will require MS SQL Server driver to be installed (or FreeTDS) 25 | // "ODBC Driver 17 for SQL Server" 26 | // "ODBC Driver 18 for SQL Server" 27 | // "FreeTDS" 28 | super( 29 | "odbc://localhost,%s?user=SA,password=MSbbk4k77JKH88g54,trust_server_certificate=yes,driver=ODBC Driver 18 for SQL Server".format(environment.get("MSSQL_PORT", "1433")), // don't specify database! 30 | "DROP TABLE IF EXISTS [my_first_test];CREATE TABLE [my_first_test] ([id] INT NOT NULL IDENTITY(1,1) PRIMARY KEY, [name] VARCHAR(255) NOT NULL)", 31 | "DROP TABLE IF EXISTS [my_first_test]" 32 | ); 33 | } 34 | 35 | @Test 36 | public void testVerifyTableExists() { 37 | Statement stmt = conn.createStatement(); 38 | scope(exit) stmt.close(); 39 | 40 | ddbc.core.ResultSet resultSet = stmt.executeQuery(`SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME = 'my_first_test'`); 41 | 42 | //assertEquals(1, resultSet.getFetchSize()); // getFetchSize() isn't working for ODBC 43 | assertTrue(resultSet.next()); 44 | } 45 | 46 | @Test 47 | public void testExecutingRawSqlInsertStatements() { 48 | Statement stmt = conn.createStatement(); 49 | scope(exit) stmt.close(); 50 | 51 | int result1 = stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('MY TEST')`); 52 | assertEquals(1, result1); 53 | 54 | Variant id; 55 | int result2 = stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('MY TEST')`, id); 56 | assertEquals(1, result2); 57 | //assertEquals("long", to!string(id.type)); // expected longbut was "odbc.sqltypes.SQL_NUMERIC_STRUCT" 58 | //assertEquals(2L, id.get!(long)); 59 | } 60 | } 61 | 62 | class SqlServerTransactionTest : DdbcTestFixture { 63 | mixin UnitTest; 64 | 65 | this() { 66 | super( 67 | "odbc://localhost,%s?user=SA,password=MSbbk4k77JKH88g54,trust_server_certificate=yes,driver=ODBC Driver 18 for SQL Server".format(environment.get("MSSQL_PORT", "1433")), // don't specify database! 68 | "CREATE TABLE records (id INT IDENTITY(1, 1) PRIMARY KEY, name VARCHAR(255) NOT NULL)", 69 | "DROP TABLE IF EXISTS records" 70 | ); 71 | } 72 | 73 | @Test 74 | public void testAutocommitOn() { 75 | // This is the default state, it is merely made explicit here. 76 | conn.setAutoCommit(true); 77 | Statement stmt = conn.createStatement(); 78 | //scope(exit) stmt.close(); 79 | 80 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Bob')`); 81 | stmt.close(); 82 | conn.rollback(); 83 | stmt = conn.createStatement(); 84 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Jim')`); 85 | conn.commit(); 86 | stmt.close(); 87 | 88 | ddbc.core.ResultSet resultSet; 89 | stmt = conn.createStatement(); 90 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Bob'`); 91 | assert(resultSet.next()); 92 | stmt.close(); 93 | stmt = conn.createStatement(); 94 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Jim'`); 95 | assert(resultSet.next()); 96 | stmt.close(); 97 | } 98 | 99 | @Test 100 | public void testAutocommitOff() { 101 | // With autocommit set to false, transactions must be explicitly committed. 102 | conn.setAutoCommit(false); 103 | conn.setAutoCommit(false); // Duplicate calls should not cause errors. 104 | Statement stmt; 105 | 106 | stmt = conn.createStatement(); 107 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Greg')`); 108 | stmt.close(); 109 | conn.rollback(); 110 | stmt = conn.createStatement(); 111 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Tom')`); 112 | stmt.close(); 113 | conn.commit(); 114 | 115 | ddbc.core.ResultSet resultSet; 116 | stmt = conn.createStatement(); 117 | resultSet = stmt.executeQuery(`SELECT COUNT(*) FROM records`); 118 | stmt.close(); 119 | 120 | stmt = conn.createStatement(); 121 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Greg'`); 122 | assert(!resultSet.next()); 123 | stmt.close(); 124 | stmt = conn.createStatement(); 125 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Tom'`); 126 | assert(resultSet.next()); 127 | stmt.close(); 128 | } 129 | 130 | @Test 131 | public void testAutocommitOffOn() { 132 | // A test with a user changing autocommit in between statements. 133 | conn.setAutoCommit(false); 134 | Statement stmt; 135 | 136 | stmt = conn.createStatement(); 137 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Abe')`); 138 | stmt.close(); 139 | conn.setAutoCommit(true); 140 | stmt = conn.createStatement(); 141 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Bart')`); 142 | stmt.close(); 143 | 144 | ddbc.core.ResultSet resultSet; 145 | stmt = conn.createStatement(); 146 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Abe'`); 147 | assert(resultSet.next()); 148 | stmt.close(); 149 | stmt = conn.createStatement(); 150 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Bart'`); 151 | assert(resultSet.next()); 152 | stmt.close(); 153 | } 154 | 155 | @Test 156 | public void testTransactionIsolation() { 157 | // Setting isolation level is only effective in transactions. 158 | conn.setAutoCommit(false); 159 | conn.setTransactionIsolation(TransactionIsolation.REPEATABLE_READ); 160 | assert(conn.getTransactionIsolation() == TransactionIsolation.REPEATABLE_READ); 161 | conn.setTransactionIsolation(TransactionIsolation.SERIALIZABLE); 162 | assert(conn.getTransactionIsolation() == TransactionIsolation.SERIALIZABLE); 163 | } 164 | } 165 | 166 | //pragma(msg, "DDBC test will run Oracle tests"); 167 | // 168 | //class OracleTest : DdbcTestFixture { 169 | // mixin UnitTest; 170 | // 171 | // this() { 172 | // super( 173 | // "todo Oracle", 174 | // "CREATE TABLE my_first_test (id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL)", 175 | // "DROP TABLE IF EXISTS my_first_test" 176 | // ); 177 | // } 178 | //} 179 | } 180 | -------------------------------------------------------------------------------- /test/ddbctest/postgresqltest.d: -------------------------------------------------------------------------------- 1 | module ddbc.postgresqltest; 2 | 3 | import std.conv : to; 4 | import std.datetime : Date, DateTime; 5 | import std.datetime.systime : SysTime, Clock; 6 | import std.format; 7 | import std.process: environment; 8 | import std.variant; 9 | import std.stdio; 10 | 11 | import dunit; 12 | import ddbc.test.common : DdbcTestFixture; 13 | import ddbc.core : Connection, PreparedStatement, Statement, SQLException, TransactionIsolation; 14 | import ddbc.pods; 15 | 16 | static import ddbc.core; 17 | 18 | version(USE_PGSQL) { 19 | pragma(msg, "DDBC test will run Postgres tests"); 20 | 21 | class PostgresTest : DdbcTestFixture { 22 | mixin UnitTest; 23 | 24 | this() { 25 | super( 26 | "ddbc:postgresql://localhost:%s/testdb?user=testuser,password=passw0rd,ssl=false".format(environment.get("POSTGRES_PORT", "5432")), 27 | "CREATE TABLE my_first_test (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL)", 28 | "DROP TABLE IF EXISTS my_first_test" 29 | ); 30 | } 31 | 32 | @Test 33 | public void testVerifyTableExists() { 34 | Statement stmt = conn.createStatement(); 35 | scope(exit) stmt.close(); 36 | 37 | ddbc.core.ResultSet resultSet = stmt.executeQuery(`SELECT * FROM pg_catalog.pg_tables WHERE tablename = 'my_first_test'`); 38 | 39 | assertEquals(1, resultSet.getFetchSize()); // Postgres can support getFetchSize() 40 | assertTrue(resultSet.next()); 41 | } 42 | 43 | @Test 44 | public void testExecutingRawSqlInsertStatements() { 45 | Statement stmt = conn.createStatement(); 46 | scope(exit) stmt.close(); 47 | 48 | int result1 = stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('MY TEST')`); 49 | assertEquals(1, result1); 50 | 51 | Variant id; 52 | int result2 = stmt.executeUpdate(`INSERT INTO my_first_test (name) VALUES ('MY TEST')`, id); 53 | assertEquals(1, result2); 54 | //assertEquals("long", to!string(id.type)); 55 | //assertEquals(2L, id.get!(long)); 56 | } 57 | } 58 | 59 | // Test parts of the interfaces related to transactions. 60 | class PostgresTransactionTest : DdbcTestFixture { 61 | mixin UnitTest; 62 | 63 | this() { 64 | super( 65 | "ddbc:postgresql://localhost:%s/testdb?user=testuser,password=passw0rd,ssl=false".format(environment.get("POSTGRES_PORT", "5432")), 66 | "CREATE TABLE records (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL)", 67 | "DROP TABLE IF EXISTS records" 68 | ); 69 | } 70 | 71 | @Test 72 | public void testAutocommitOn() { 73 | // This is the default state, it is merely made explicit here. 74 | conn.setAutoCommit(true); 75 | Statement stmt = conn.createStatement(); 76 | scope(exit) stmt.close(); 77 | 78 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Bob')`); 79 | conn.rollback(); 80 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Jim')`); 81 | conn.commit(); 82 | 83 | ddbc.core.ResultSet resultSet; 84 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Bob'`); 85 | assert(resultSet.next()); 86 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Jim'`); 87 | assert(resultSet.next()); 88 | } 89 | 90 | @Test 91 | public void testAutocommitOff() { 92 | // With autocommit set to false, transactions must be explicitly committed. 93 | conn.setAutoCommit(false); 94 | conn.setAutoCommit(false); // Duplicate calls should not cause errors. 95 | Statement stmt = conn.createStatement(); 96 | scope(exit) stmt.close(); 97 | 98 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Greg')`); 99 | conn.rollback(); 100 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Tom')`); 101 | conn.commit(); 102 | 103 | ddbc.core.ResultSet resultSet; 104 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Greg'`); 105 | assert(!resultSet.next()); 106 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Tom'`); 107 | assert(resultSet.next()); 108 | } 109 | 110 | @Test 111 | public void testAutocommitOffOn() { 112 | // A test with a user changing autocommit in between statements. 113 | conn.setAutoCommit(false); 114 | Statement stmt = conn.createStatement(); 115 | scope(exit) stmt.close(); 116 | 117 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Abe')`); 118 | conn.setAutoCommit(true); 119 | stmt.executeUpdate(`INSERT INTO records (name) VALUES ('Bart')`); 120 | 121 | ddbc.core.ResultSet resultSet; 122 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Abe'`); 123 | assert(resultSet.next()); 124 | resultSet = stmt.executeQuery(`SELECT * FROM records WHERE name = 'Bart'`); 125 | assert(resultSet.next()); 126 | } 127 | 128 | @Test 129 | public void testTransactionIsolation() { 130 | // Setting isolation level is only effective in transactions. 131 | conn.setAutoCommit(false); 132 | // In PostgreSQL, READ_COMMITTED is the default. 133 | assert(conn.getTransactionIsolation() == TransactionIsolation.READ_COMMITTED); 134 | conn.setTransactionIsolation(TransactionIsolation.REPEATABLE_READ); 135 | assert(conn.getTransactionIsolation() == TransactionIsolation.REPEATABLE_READ); 136 | conn.setTransactionIsolation(TransactionIsolation.SERIALIZABLE); 137 | assert(conn.getTransactionIsolation() == TransactionIsolation.SERIALIZABLE); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /test/ddbctest/testconnectionpool.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | import std.datetime; 3 | import std.variant; 4 | import std.conv; 5 | 6 | import core.thread : Thread; 7 | import core.time : seconds; 8 | 9 | import ddbc.drivers.pgsqlddbc; 10 | import ddbc.core; 11 | import ddbc.common; 12 | 13 | import dunit; 14 | import ddbc.test.common : DdbcTestFixture; 15 | import ddbc.core : Connection, PreparedStatement, Statement, SQLException, TransactionIsolation; 16 | 17 | // Used to control our fake DB connection and when it throws errors. 18 | bool throwOnConnect = false; 19 | int connectCount = 0; 20 | bool throwOnExecute = false; 21 | int executeCount = 0; 22 | 23 | // A fake query result we can use to simulate errors. 24 | class FakeResultSet : ResultSetImpl { 25 | override 26 | void close() { } 27 | } 28 | 29 | // A fake statement we can use to simulate errors on query. 30 | class FakeStatement : Statement { 31 | ResultSet executeQuery(string query) { 32 | if (throwOnExecute) { 33 | throw new SQLException("Fake execute exception."); 34 | } 35 | executeCount++; 36 | return new FakeResultSet(); 37 | } 38 | int executeUpdate(string query) { return 0; } 39 | int executeUpdate(string query, out Variant insertId) { return 0; } 40 | void close() { } 41 | 42 | DialectType getDialectType() { 43 | return DialectType.SQLITE; // Just need to pick something. 44 | } 45 | } 46 | 47 | class FakeConnection : Connection { 48 | void close() { } 49 | void commit() { } 50 | string getCatalog() { return ""; } 51 | void setCatalog(string catalog) { } 52 | bool isClosed() { return false; } 53 | void rollback() { } 54 | bool getAutoCommit() { return false; } 55 | void setAutoCommit(bool autoCommit) { } 56 | Statement createStatement() { return new FakeStatement(); } 57 | PreparedStatement prepareStatement(string query) { return null;} 58 | TransactionIsolation getTransactionIsolation() { return TransactionIsolation.READ_COMMITTED; } 59 | void setTransactionIsolation(TransactionIsolation level) { } 60 | 61 | DialectType getDialectType() { 62 | return DialectType.SQLITE; // Just need to pick something. 63 | } 64 | } 65 | 66 | // A fake driver we can use to simulate failures to connect. 67 | class FakeDriver : Driver { 68 | Connection connect(string url, string[string] params) { 69 | if (throwOnConnect) { 70 | throw new SQLException("Fake connect exception."); 71 | } 72 | connectCount++; 73 | return new FakeConnection(); 74 | } 75 | } 76 | 77 | class ConnectionPoolTest { 78 | mixin UnitTest; 79 | 80 | @Test 81 | public void testBrokenConnection() { 82 | Driver driver = new FakeDriver(); 83 | DataSource dataSource = new ConnectionPoolDataSourceImpl(driver, ""); 84 | 85 | // Test verify that when the database is down, nothing can be done. 86 | throwOnConnect = true; 87 | throwOnExecute = false; 88 | try { 89 | Connection connection = dataSource.getConnection(); 90 | assert(false, "Expected exception when no connection can be established."); 91 | } catch (Exception e) { 92 | // Ignore exception. 93 | } 94 | assert(connectCount == 0); 95 | 96 | // Obtain a working connection, and validate that it gets recycled. 97 | throwOnConnect = false; 98 | throwOnExecute = false; 99 | Connection connection = dataSource.getConnection(); 100 | connection.close(); 101 | connection = dataSource.getConnection(); 102 | connection.close(); 103 | assert(connectCount == 1); 104 | assert(executeCount == 1); 105 | 106 | // Create 2 connections, free them, and simulate errors when trying to use them. 107 | Connection c1 = dataSource.getConnection(); // Use the free connection. 108 | Connection c2 = dataSource.getConnection(); // Requres a new connection. 109 | assert(executeCount == 2); 110 | assert(connectCount == 2); 111 | c1.close(); 112 | c2.close(); 113 | // There are now 2 connections free for re-use, simulate a network disconnect. 114 | throwOnExecute = true; 115 | // One connection attempts to be re-used, but it fails and a new one is created. 116 | Connection c3 = dataSource.getConnection(); 117 | assert(executeCount == 2); 118 | assert(connectCount == 3); 119 | // Restore our network and make sure the 1 remainininig free connect is re-used. 120 | throwOnExecute = false; 121 | Connection c4 = dataSource.getConnection(); 122 | assert(executeCount == 3); 123 | assert(connectCount == 3); 124 | // We are now out of free connections, the next attempt should make a new one. 125 | Connection c5 = dataSource.getConnection(); 126 | assert(executeCount == 3); 127 | assert(connectCount == 4); 128 | } 129 | } 130 | --------------------------------------------------------------------------------