Skip to content

Commit c043e9e

Browse files
authored
Add Support for Transactions (#121) autocommit, commit, and rollback, and transaction isolation
* Added support for autocommit, commit, and rollback, and transaction isolation level for Postgresql, MySQL, ODBC, and Sqlite. Added tests for autocommit. * Add tests for set/get TransactionIsolation. * Remove commented-out block of code. * Remove debug writeln statements and split main test into several files.
1 parent 92d342c commit c043e9e

13 files changed

+864
-239
lines changed

.editorconfig

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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

CONTRIBUTING.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@ DDBC aims to support a range of compiler versions across from dmd 2.097 and abov
22

33
## Tests
44

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.
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.
66

77
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/`).
88

9-
unit tests can br run in the usual way with `dub test` and integration tests are run with `dub run --config=test`.
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.
1016

1117
## Requirements for developing
1218

source/ddbc/common.d

+4
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ class ConnectionWrapper : Connection {
114114
override bool getAutoCommit() { return base.getAutoCommit(); }
115115
override void setAutoCommit(bool autoCommit) { base.setAutoCommit(autoCommit); }
116116
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+
}
117121
}
118122

119123
// remove array item inplace

source/ddbc/core.d

+74-1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,64 @@ enum SqlType {
134134
VARCHAR,
135135
}
136136

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+
*/
137195
interface Connection : DialectAware {
138196
/// Releases this Connection object's database and JDBC resources immediately instead of waiting for them to be automatically released.
139197
void close();
@@ -156,6 +214,21 @@ interface Connection : DialectAware {
156214
Statement createStatement();
157215
/// Creates a PreparedStatement object for sending parameterized SQL statements to the database.
158216
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);
159232
}
160233

161234
interface ResultSetMetaData {
@@ -520,4 +593,4 @@ private unittest {
520593

521594
string url = makeDDBCUrl("odbc", params);
522595
assert(url == "odbc://?dsn=myDSN", "ODBC URL is not correct: "~url);
523-
}
596+
}

source/ddbc/drivers/mysqlddbc.d

+61
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,67 @@ public:
300300
throw new SQLException(e);
301301
}
302302
}
303+
304+
/// See_Also: https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_transaction_isolation
305+
override TransactionIsolation getTransactionIsolation() {
306+
checkClosed();
307+
lock();
308+
scope(exit) unlock();
309+
310+
try {
311+
Statement stmt = createStatement();
312+
scope(exit) stmt.close();
313+
ddbc.core.ResultSet resultSet = stmt.executeQuery("SELECT @@transaction_isolation");
314+
if (resultSet.next()) {
315+
switch (resultSet.getString(1)) {
316+
case "READ-UNCOMMITTED":
317+
return TransactionIsolation.READ_UNCOMMITTED;
318+
case "READ-COMMITTED":
319+
return TransactionIsolation.READ_COMMITTED;
320+
case "SERIALIZABLE":
321+
return TransactionIsolation.SERIALIZABLE;
322+
case "REPEATABLE-READ":
323+
default: // MySQL default
324+
return TransactionIsolation.REPEATABLE_READ;
325+
}
326+
} else {
327+
return TransactionIsolation.REPEATABLE_READ; // MySQL default
328+
}
329+
} catch (Throwable e) {
330+
throw new SQLException(e);
331+
}
332+
}
333+
334+
/// See_Also: https://dev.mysql.com/doc/refman/8.0/en/set-transaction.html
335+
override void setTransactionIsolation(TransactionIsolation level) {
336+
checkClosed();
337+
lock();
338+
scope(exit) unlock();
339+
340+
try {
341+
Statement stmt = createStatement();
342+
// See: https://dev.mysql.com/doc/refman/8.0/en/set-transaction.html
343+
string query = "SET SESSION TRANSACTION ISOLATION LEVEL ";
344+
switch (level) {
345+
case TransactionIsolation.READ_UNCOMMITTED:
346+
query ~= "READ UNCOMMITTED";
347+
break;
348+
case TransactionIsolation.READ_COMMITTED:
349+
query ~= "READ COMMITTED";
350+
break;
351+
case TransactionIsolation.SERIALIZABLE:
352+
query ~= "SERIALIZABLE";
353+
break;
354+
case TransactionIsolation.REPEATABLE_READ:
355+
default:
356+
query ~= "REPEATABLE READ";
357+
break;
358+
}
359+
stmt.executeUpdate(query);
360+
} catch (Throwable e) {
361+
throw new SQLException(e);
362+
}
363+
}
303364
}
304365

305366
class MySQLStatement : Statement {

source/ddbc/drivers/odbcddbc.d

+47-3
Original file line numberDiff line numberDiff line change
@@ -558,18 +558,62 @@ version (USE_ODBC)
558558
override void setAutoCommit(bool autoCommit)
559559
{
560560
checkClosed();
561-
if (this.autocommit != autocommit)
561+
if (this.autocommit != autoCommit)
562562
{
563563
lock();
564564
scope (exit)
565565
unlock();
566566

567567
uint ac = autoCommit ? SQL_AUTOCOMMIT_ON : SQL_AUTOCOMMIT_OFF;
568568

569-
checkdbc!SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, &ac, SQL_IS_UINTEGER);
569+
checkdbc!SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, cast(SQLPOINTER) ac, 0);
570+
this.autocommit = autoCommit;
571+
}
572+
}
570573

571-
this.autocommit = autocommit;
574+
override TransactionIsolation getTransactionIsolation() {
575+
checkClosed();
576+
lock();
577+
scope(exit) unlock;
578+
// See https://odbc.dpldocs.info/v1.0.0/source/odbc.sql.d.html#L571
579+
long level;
580+
checkdbc!SQLGetConnectAttr(conn, SQL_ATTR_TXN_ISOLATION, &level, 0, null);
581+
switch (level) {
582+
case SQL_TXN_READ_UNCOMMITTED:
583+
return TransactionIsolation.READ_UNCOMMITTED;
584+
case SQL_TXN_READ_COMMITTED:
585+
return TransactionIsolation.READ_COMMITTED;
586+
case SQL_TXN_REPEATABLE_READ:
587+
return TransactionIsolation.REPEATABLE_READ;
588+
case SQL_TXN_SERIALIZABLE:
589+
return TransactionIsolation.SERIALIZABLE;
590+
default:
591+
return TransactionIsolation.NONE;
592+
}
593+
}
594+
595+
override void setTransactionIsolation(TransactionIsolation level) {
596+
checkClosed();
597+
lock();
598+
scope(exit) unlock();
599+
600+
uint txnLevel;
601+
switch (level) {
602+
case TransactionIsolation.READ_UNCOMMITTED:
603+
txnLevel = SQL_TXN_READ_UNCOMMITTED;
604+
break;
605+
case TransactionIsolation.READ_COMMITTED:
606+
default:
607+
txnLevel = SQL_TXN_READ_COMMITTED;
608+
break;
609+
case TransactionIsolation.REPEATABLE_READ:
610+
txnLevel = SQL_TXN_REPEATABLE_READ;
611+
break;
612+
case TransactionIsolation.SERIALIZABLE:
613+
txnLevel = SQL_TXN_SERIALIZABLE;
614+
break;
572615
}
616+
checkdbc!SQLSetConnectAttr(conn, SQL_ATTR_TXN_ISOLATION, cast(SQLPOINTER) txnLevel, 0);
573617
}
574618
}
575619

source/ddbc/drivers/pgsqlddbc.d

+81-3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ version(USE_PGSQL) {
4949
//import ddbc.drivers.pgsql;
5050
import ddbc.drivers.utils;
5151

52+
// Postgresql Object ID types, which can be checked for query result columns.
53+
// See: https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.dat
5254
const int BOOLOID = 16;
5355
const int BYTEAOID = 17;
5456
const int CHAROID = 18;
@@ -336,6 +338,9 @@ version(USE_PGSQL) {
336338
Statement stmt = createStatement();
337339
scope(exit) stmt.close();
338340
stmt.executeUpdate("COMMIT");
341+
if (!autocommit) {
342+
stmt.executeUpdate("BEGIN");
343+
}
339344
}
340345

341346
override Statement createStatement() {
@@ -393,6 +398,9 @@ version(USE_PGSQL) {
393398
Statement stmt = createStatement();
394399
scope(exit) stmt.close();
395400
stmt.executeUpdate("ROLLBACK");
401+
if (!autocommit) {
402+
stmt.executeUpdate("BEGIN");
403+
}
396404
}
397405
override bool getAutoCommit() {
398406
return autocommit;
@@ -404,10 +412,80 @@ version(USE_PGSQL) {
404412
lock();
405413
scope(exit) unlock();
406414

407-
autocommit = true;
408-
409-
//assert(0, "AUTOCOMMIT is no longer supported.");
415+
try {
416+
Statement stmt = createStatement();
417+
scope(exit) stmt.close();
418+
if (autoCommit) {
419+
// If switching on autocommit, commit any ongoing transaction.
420+
stmt.executeUpdate("COMMIT");
421+
} else {
422+
// If switching off autocommit, start a transaction.
423+
stmt.executeUpdate("BEGIN");
424+
}
425+
this.autocommit = autoCommit;
426+
} catch (Throwable e) {
427+
throw new SQLException(e);
428+
}
410429
}
430+
431+
override TransactionIsolation getTransactionIsolation() {
432+
checkClosed();
433+
lock();
434+
scope(exit) unlock();
435+
436+
try {
437+
Statement stmt = createStatement();
438+
scope(exit) stmt.close();
439+
ddbc.core.ResultSet resultSet = stmt.executeQuery("SHOW TRANSACTION ISOLATION LEVEL");
440+
if (resultSet.next()) {
441+
switch (resultSet.getString(1)) {
442+
case "read uncommitted":
443+
return TransactionIsolation.READ_UNCOMMITTED;
444+
case "repeatable read":
445+
return TransactionIsolation.REPEATABLE_READ;
446+
case "serializable":
447+
return TransactionIsolation.SERIALIZABLE;
448+
case "read committed":
449+
default: // Postgresql default
450+
return TransactionIsolation.READ_COMMITTED;
451+
}
452+
} else {
453+
return TransactionIsolation.READ_COMMITTED; // Postgresql default
454+
}
455+
} catch (Throwable e) {
456+
throw new SQLException(e);
457+
}
458+
}
459+
460+
override void setTransactionIsolation(TransactionIsolation level) {
461+
checkClosed();
462+
lock();
463+
scope(exit) unlock();
464+
465+
try {
466+
Statement stmt = createStatement();
467+
// See: https://www.postgresql.org/docs/current/sql-set-transaction.html
468+
string query = "SET TRANSACTION ISOLATION LEVEL ";
469+
switch (level) {
470+
case TransactionIsolation.READ_UNCOMMITTED:
471+
query ~= "READ UNCOMMITTED";
472+
break;
473+
case TransactionIsolation.REPEATABLE_READ:
474+
query ~= "REPEATABLE READ";
475+
break;
476+
case TransactionIsolation.SERIALIZABLE:
477+
query ~= "SERIALIZABLE";
478+
break;
479+
case TransactionIsolation.READ_COMMITTED:
480+
default:
481+
query ~= "READ COMMITTED";
482+
break;
483+
}
484+
stmt.executeUpdate(query);
485+
} catch (Throwable e) {
486+
throw new SQLException(e);
487+
}
488+
}
411489
}
412490

413491
class PGSQLStatement : Statement {

0 commit comments

Comments
 (0)