Skip to content

Commit 141df5f

Browse files
committed
Allow non-primary-key generated fields, e.g. timestamps, counters, etc.
Currently, HibernateD treats `@Generated` as being inseparable from `@Id`, it is assumed that any generated field is also the primary key. However, a database table can have non-primary-key columns that are `@Generated`. For example, a database that keeps track of a timestamp, which is populated via a DB trigger or a default value, e.g. `now()`, may exist alongside a separate primary key. In order to support this kind of data, the assumption that `@Generated` implies `@Id` needs to be undone. This PR changes the core logic and also adds a basic test around generated columns to validate schema generation as well as the ability to insert and update such records.
1 parent 223a71f commit 141df5f

File tree

8 files changed

+268
-45
lines changed

8 files changed

+268
-45
lines changed

hdtest/source/embeddedidtest.d

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
module embeddedidtest;
22

3-
43
import std.algorithm : any;
54

65
import hibernated.core;
@@ -159,4 +158,31 @@ class EmbeddedIdTest : HibernateTest {
159158

160159
sess.close();
161160
}
161+
162+
@Test("embeddedid.refresh")
163+
void refreshTest() {
164+
Session sess = sessionFactory.openSession();
165+
166+
// Create a new record that we can mutate outside the session.
167+
Invoice invoice = new Invoice();
168+
invoice.invoiceId = new InvoiceId();
169+
invoice.invoiceId.vendorNo = "ABC123";
170+
invoice.invoiceId.invoiceNo = "L1005-2330";
171+
invoice.currency = "EUR";
172+
invoice.amountE4 = 54_3200;
173+
sess.save(invoice).get!InvoiceId;
174+
175+
// Modify this entity outside the session using a raw SQL query.
176+
sess.doWork(
177+
(Connection c) {
178+
Statement stmt = c.createStatement();
179+
scope (exit) stmt.close();
180+
stmt.executeUpdate("UPDATE invoice SET currency = 'USD' "
181+
~ "WHERE vendor_no = 'ABC123' AND invoice_no = 'L1005-2330'");
182+
});
183+
184+
// Make sure that the entity picks up the out-of-session changes.
185+
sess.refresh(invoice);
186+
assert(invoice.currency == "USD");
187+
}
162188
}

hdtest/source/generatedtest.d

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
module generatedtest;
2+
3+
import std.datetime;
4+
5+
import hibernated.core;
6+
7+
import testrunner : Test;
8+
import hibernatetest : HibernateTest;
9+
10+
// A class representing a entity with a single generated key.
11+
@Entity
12+
class Generated1 {
13+
// Generated, we can leave this off and the DB will create it.
14+
@Id @Generated
15+
int myId;
16+
17+
string name;
18+
}
19+
20+
// A class representing a entity with multiple generated values.
21+
@Entity
22+
class Generated2 {
23+
// Not generated, this must be set in order to save.
24+
@Id
25+
int myId;
26+
27+
// The DB will create this value and it does not need to be set.
28+
@Generated
29+
int counter1;
30+
31+
// The DB will create this value and it does not need to be set.
32+
@Generated
33+
DateTime counter2;
34+
35+
string name;
36+
}
37+
38+
class GeneratedTest : HibernateTest {
39+
override
40+
EntityMetaData buildSchema() {
41+
return new SchemaInfoImpl!(Generated1, Generated2);
42+
}
43+
44+
@Test("generated.primary-generated")
45+
void creation1Test() {
46+
Session sess = sessionFactory.openSession();
47+
scope (exit) sess.close();
48+
49+
Generated1 g1 = new Generated1();
50+
g1.name = "Bob";
51+
sess.save(g1);
52+
// This value should have been detected as empty, populated by the DB, and refreshed.
53+
int g1Id = g1.myId;
54+
assert(g1Id != 0);
55+
56+
g1.name = "Barb";
57+
sess.update(g1);
58+
// The ID should not have changed.
59+
assert(g1.myId == g1Id);
60+
}
61+
62+
@Test("generated.non-primary-generated")
63+
void creation2Test() {
64+
Session sess = sessionFactory.openSession();
65+
scope (exit) sess.close();
66+
67+
Generated2 g2 = new Generated2();
68+
g2.myId = 2;
69+
g2.name = "Sam";
70+
sess.save(g2);
71+
72+
int g2Id = g2.myId;
73+
74+
g2.name = "Slom";
75+
sess.update(g2);
76+
77+
// The ID should not have changed.
78+
assert(g2Id == g2.myId);
79+
}
80+
}

hdtest/source/htestmain.d

+14
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,19 @@ import generaltest : GeneralTest;
1414
import embeddedtest : EmbeddedTest;
1515
import embeddedidtest : EmbeddedIdTest;
1616
import transactiontest : TransactionTest;
17+
import generatedtest : GeneratedTest;
18+
19+
void enableTraceLogging() {
20+
import std.logger : sharedLog, LogLevel, globalLogLevel;
21+
(cast() sharedLog).logLevel = LogLevel.trace;
22+
globalLogLevel = LogLevel.trace;
23+
}
1724

1825
int main(string[] args) {
1926

27+
// Use this to enable trace() logs, useful to inspect generated SQL.
28+
enableTraceLogging();
29+
2030
ConnectionParams par;
2131

2232
try {
@@ -42,6 +52,10 @@ int main(string[] args) {
4252
test4.setConnectionParams(par);
4353
runTests(test4);
4454

55+
GeneratedTest test5 = new GeneratedTest();
56+
test5.setConnectionParams(par);
57+
runTests(test5);
58+
4559
writeln("All scenarios worked successfully");
4660
return 0;
4761
}

source/hibernated/annotations.d

+4
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ struct Table {
168168
struct Column {
169169
immutable string name;
170170
immutable int length;
171+
/// Whether the column is included in SQL INSERT statements generated by the persistence provider.
172+
immutable bool insertable = true;
173+
/// Whether the column is included in SQL UPDATE statements generated by the persistence provider.
174+
immutable bool updatable = true;
171175
// this(string name) { this.name = name; }
172176
// this(string name, int length) { this.name = name; this.length = length; }
173177
// this(int length) { this.length = length; }

source/hibernated/dialects/pgsqldialect.d

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/**
2-
* HibernateD - Object-Relation Mapping for D programming language, with interface similar to Hibernate.
3-
*
2+
* HibernateD - Object-Relation Mapping for D programming language, with interface similar to Hibernate.
3+
*
44
* Source file hibernated/dialects/sqlitedialect.d.
55
*
66
* This module contains implementation of PGSQLDialect class which provides implementation specific SQL syntax information.
7-
*
7+
*
88
* Copyright: Copyright 2013
99
* License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
1010
* Author: Vadim Lopatin
@@ -19,7 +19,7 @@ import hibernated.type;
1919
import ddbc.core : SqlType;
2020

2121

22-
string[] PGSQL_RESERVED_WORDS =
22+
string[] PGSQL_RESERVED_WORDS =
2323
[
2424
"ABORT",
2525
"ACTION",
@@ -151,7 +151,7 @@ class PGSQLDialect : Dialect {
151151
override char closeQuote() const { return '"'; }
152152
///The character specific to this dialect used to begin a quoted identifier.
153153
override char openQuote() const { return '"'; }
154-
154+
155155
// returns string like "BIGINT(20) NOT NULL" or "VARCHAR(255) NULL"
156156
override string getColumnTypeDefinition(const PropertyInfo pi, const PropertyInfo overrideTypeFrom = null) {
157157
immutable Type type = overrideTypeFrom !is null ? overrideTypeFrom.columnType : pi.columnType;
@@ -161,10 +161,10 @@ class PGSQLDialect : Dialect {
161161
string pk = !fk && pi.key ? " PRIMARY KEY" : "";
162162
if (!fk && pi.generated) {
163163
if (sqlType == SqlType.SMALLINT || sqlType == SqlType.TINYINT)
164-
return "SERIAL PRIMARY KEY";
164+
return "SERIAL" ~ pk;
165165
if (sqlType == SqlType.INTEGER)
166-
return "SERIAL PRIMARY KEY";
167-
return "BIGSERIAL PRIMARY KEY";
166+
return "SERIAL" ~ pk;
167+
return "BIGSERIAL" ~ pk;
168168
}
169169
string def = "";
170170
int len = 0;
@@ -219,15 +219,15 @@ class PGSQLDialect : Dialect {
219219
return "TEXT";
220220
}
221221
}
222-
222+
223223
override string getCheckTableExistsSQL(string tableName) {
224224
return "select relname from pg_class where relname = " ~ quoteSqlString(tableName) ~ " and relkind='r'";
225225
}
226-
226+
227227
override string getUniqueIndexItemSQL(string indexName, string[] columnNames) {
228228
return "UNIQUE " ~ createFieldListSQL(columnNames);
229229
}
230-
230+
231231
/// for some of RDBMS it's necessary to pass additional clauses in query to get generated value (e.g. in Postgres - " returing id"
232232
override string appendInsertToFetchGeneratedKey(string query, const EntityInfo entity) {
233233
return query ~ " RETURNING " ~ quoteIfNeeded(entity.getKeyProperty().columnName);

source/hibernated/dialects/sqlitedialect.d

+12-10
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/**
2-
* HibernateD - Object-Relation Mapping for D programming language, with interface similar to Hibernate.
3-
*
2+
* HibernateD - Object-Relation Mapping for D programming language, with interface similar to Hibernate.
3+
*
44
* Source file hibernated/dialects/sqlitedialect.d.
55
*
66
* This module contains implementation of SQLiteDialect class which provides implementation specific SQL syntax information.
7-
*
7+
*
88
* Copyright: Copyright 2013
99
* License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
1010
* Author: Vadim Lopatin
@@ -19,7 +19,7 @@ import hibernated.type;
1919
import ddbc.core : SqlType;
2020

2121

22-
string[] SQLITE_RESERVED_WORDS =
22+
string[] SQLITE_RESERVED_WORDS =
2323
[
2424
"ABORT",
2525
"ACTION",
@@ -150,17 +150,19 @@ class SQLiteDialect : Dialect {
150150
override char closeQuote() const { return '`'; }
151151
///The character specific to this dialect used to begin a quoted identifier.
152152
override char openQuote() const { return '`'; }
153-
153+
154154
// returns string like "BIGINT(20) NOT NULL" or "VARCHAR(255) NULL"
155155
override string getColumnTypeDefinition(const PropertyInfo pi, const PropertyInfo overrideTypeFrom = null) {
156156
immutable Type type = overrideTypeFrom !is null ? overrideTypeFrom.columnType : pi.columnType;
157157
immutable SqlType sqlType = type.getSqlType();
158158
bool fk = pi is null;
159159
string nullablility = !fk && pi.nullable ? " NULL" : " NOT NULL";
160160
string pk = !fk && pi.key ? " PRIMARY KEY" : "";
161-
string autoinc = !fk && pi.generated ? " AUTO_INCREMENT" : "";
162-
if (!fk && pi.generated)
163-
return "INTEGER PRIMARY KEY";
161+
string autoinc = !fk && pi.generated ? " AUTOINCREMENT" : "";
162+
if (!fk && !pi.key && pi.generated) {
163+
// SQLite3 does not support autoincrement on non-primary key fields.
164+
return "INTEGER NOT NULL DEFAULT 0";
165+
}
164166
string def = "";
165167
int len = 0;
166168
string unsigned = "";
@@ -181,7 +183,7 @@ class SQLiteDialect : Dialect {
181183
case SqlType.NUMERIC:
182184
case SqlType.SMALLINT:
183185
case SqlType.TINYINT:
184-
return "INT" ~ modifiers;
186+
return "INTEGER" ~ modifiers;
185187
case SqlType.FLOAT:
186188
case SqlType.DOUBLE:
187189
case SqlType.DECIMAL:
@@ -206,7 +208,7 @@ class SQLiteDialect : Dialect {
206208
return "TEXT";
207209
}
208210
}
209-
211+
210212
override string getCheckTableExistsSQL(string tableName) {
211213
return "SELECT name FROM sqlite_master WHERE type='table' AND name=" ~ quoteSqlString(tableName);
212214
}

0 commit comments

Comments
 (0)