1 package net.jaekl.cfb.db.driver;
3 // Copyright (C) 2015 Christian Jaekl
5 import static net.jaekl.cfb.db.Column.Null.NOT_NULL;
7 import java.sql.Connection;
8 import java.sql.PreparedStatement;
9 import java.sql.ResultSet;
10 import java.sql.SQLException;
11 import java.util.ArrayList;
12 import java.util.Date;
13 import java.util.List;
15 import net.jaekl.cfb.db.Column;
16 import net.jaekl.cfb.db.Column.Type;
17 import net.jaekl.cfb.db.Condition;
18 import net.jaekl.cfb.db.Row;
19 import net.jaekl.cfb.db.Sequence;
20 import net.jaekl.cfb.db.Sort;
21 import net.jaekl.cfb.db.Table;
23 public abstract class DbDriver {
24 static int PENDING_LIMIT = 1024; // Rough limit at which point we'll start a new batch for batch updates
30 // Load the JDBC driver
31 public abstract void load() throws ClassNotFoundException;
33 public abstract Connection connect(String host, int port, String dbName, String user, String pass) throws SQLException;
35 public boolean createTable(Connection con, Table table) throws SQLException
37 String sql = createTableSql(table);
38 try (PreparedStatement ps = con.prepareStatement(sql)) {
41 catch (SQLException exc) {
42 throw new SQLException("Failed to executeUpdate: " + sql, exc);
48 public void dropTable(Connection con, Table table) throws SQLException
50 String sql = dropTableSql(table);
51 try (PreparedStatement ps = con.prepareStatement(sql)) {
54 catch (SQLException exc) {
55 throw new SQLException("Failed to drop table: " + sql, exc);
59 public boolean createSequence(Connection con, Sequence seq) throws SQLException
61 String sql = createSequenceSql(seq);
62 try (PreparedStatement ps = con.prepareStatement(sql)) {
65 catch (SQLException exc) {
66 throw new SQLException("Failed to executeUpdate: " + sql, exc);
72 public void dropSequence(Connection con, Sequence seq) throws SQLException
74 String sql = dropSequenceSql(seq);
75 try (PreparedStatement ps = con.prepareStatement(sql)) {
78 catch (SQLException exc) {
79 throw new SQLException("Failed to drop sequence: " + sql, exc);
83 public Row selectExactlyOne(Connection con, Column[] columns, Table[] tables, Condition[] conditions)
86 Sort[] sorts = new Sort[0];
88 List<Row> rows = select(con, columns, tables, conditions, sorts, limit);
89 if (rows.size() < 1) {
90 throw new SQLException("Expected one result, but found none: ", selectSql(columns, tables, conditions, sorts, limit));
92 if (rows.size() > 1) {
93 throw new SQLException("Expected one result, but found more than one: " + selectSql(columns, tables, conditions, sorts, limit));
99 public List<Row> select(Connection con, Column[] columns, Table[] tables, Condition[] conditions)
102 Sort[] sorts = new Sort[0];
103 int limit = (-1); // no limit
105 return select(con, columns, tables, conditions, sorts, limit);
108 public List<Row> select(Connection con, Column[] columns, Table[] tables, Condition[] conditions, Sort[] sorts, int limit)
111 String sql = selectSql(columns, tables, conditions, sorts, limit);
112 ArrayList<Row> result = new ArrayList<Row>();
114 try (PreparedStatement ps = con.prepareStatement(sql)) {
116 for (Condition condition : conditions) {
117 if (condition.getOperation().hasParam()) {
119 ps.setObject(index, condition.getValue());
123 try (ResultSet rs = ps.executeQuery()) {
125 Object[] values = new Object[columns.length];
126 for (index = 0; index < columns.length; ++index) {
127 if (columns[index].getType().equals(Type.TIMESTAMPTZ)) {
128 long milliseconds = rs.getLong(index + 1);
129 values[index] = new java.util.Date(milliseconds);
132 values[index] = rs.getObject(index + 1);
135 Row row = new Row(columns, values);
144 // Returns the number of rows inserted
145 public int insert(Connection con, Table table, Object[][] values) throws SQLException
148 int pendingValues = 0;
150 assert( isValidInsert(table, values));
152 String sql = insertSql(table);
154 try (PreparedStatement ps = con.prepareStatement(sql))
156 for (int row = 0; row < values.length; ++row) {
157 Object[] data = values[row];
159 assert(null != data);
160 assert(data.length == table.getNumColumns());
162 for (int col = 0; col < data.length; ++col) {
163 Object obj = data[col];
164 Column column = table.getColumn(col);
165 if (column.getType().equals(Type.TIMESTAMPTZ)) {
166 // Special case: because there's no good way to read a TIMESTAMPTZ from
167 // the database using JDBC, we store it as an integer (milliseconds since
168 // the epoch, 01.01.1970 00:00:00.000 UTC).
169 Date date = (Date)obj;
170 ps.setLong(col + 1, date.getTime());
173 ps.setObject(col + 1, data[col]);
179 int rowsFlushed = checkFlushBatch(ps, pendingValues, false);
180 if (rowsFlushed > 0) {
181 count += rowsFlushed;
186 count += checkFlushBatch(ps, pendingValues, true);
192 public long nextVal(Connection con, Sequence seq) throws SQLException
194 String sql = nextValSql(seq);
196 try (PreparedStatement ps = con.prepareStatement(sql))
198 try (ResultSet rs = ps.executeQuery()) {
200 return rs.getLong(1);
205 throw new SQLException("No value returned for sequence: " + sql);
208 int checkFlushBatch(PreparedStatement ps, int pendingValues, boolean forceFlush) throws SQLException
212 if (forceFlush || (pendingValues >= PENDING_LIMIT))
214 int[] updateCounts = ps.executeBatch();
215 for (int i = 0; i < updateCounts.length; ++i) {
216 if (updateCounts[i] > 0) {
217 count += updateCounts[i];
225 String insertSql(Table table) {
226 StringBuilder sb = new StringBuilder("INSERT INTO ");
227 sb.append(table.getName())
228 .append(" VALUES (");
230 for (int i = 0; i < table.getNumColumns(); ++i) {
238 return sb.toString();
241 protected String selectSql(Column[] columns, Table[] tables, Condition[] conditions, Sort[] sorts, int limit)
243 StringBuilder sb = new StringBuilder("SELECT ");
245 boolean firstColumn = true;
246 for (Column column : columns) {
253 sb.append(column.getName());
258 boolean firstTable = true;
259 for (Table table : tables) {
266 sb.append(table.getName());
269 if (null != conditions && conditions.length > 0) {
270 sb.append(" WHERE ");
272 boolean firstCondition = true;
274 for (Condition condition : conditions) {
275 if (firstCondition) {
276 firstCondition = false;
282 sb.append(condition.getColumn().getName())
283 .append(condition.getOperation().getSql());
287 if (null != sorts && sorts.length > 0) {
288 sb.append(" ORDER BY ");
290 boolean firstSort = true;
292 for (Sort sort : sorts) {
300 sb.append(sort.getColumn().getName());
302 if (sort.getDirection().equals(Sort.Direction.ASCENDING)) {
303 sb.append(" ASCENDING ");
306 sb.append(" DESCENDING ");
312 sb.append(" LIMIT " + limit + " ");
315 return sb.toString();
318 protected String typeName(Type type) {
319 // Special case: TIMESTAMPTZ stored as INTEGER (milliseconds since the epoch)
320 // Reading a TIMESTAMPTZ back from the DB, and converting it to a java.util.Date,
321 // is fraught with peril. The best way around this is to store the dates in
322 // milliseconds-since-the-epoch (01.01.1970 00:00:00.000 UTC).
323 if (Type.TIMESTAMPTZ.equals(type)) {
324 return Type.INTEGER.toString();
327 return type.toString();
330 protected String createColumnSql(Column column)
332 String result = column.getName() + " " + typeName(column.getType());
333 if (column.getWidth() > 0) {
334 result += "(" + column.getWidth() + ")";
337 if (NOT_NULL == column.getNull()) {
338 result += " NOT NULL";
347 protected String createTableSql(Table table)
349 assert(null != table);
350 assert(null != table.getName());
351 assert(table.getNumColumns() > 0);
353 StringBuilder sb = new StringBuilder();
355 sb.append("CREATE TABLE ")
356 .append(table.getName())
359 for (int idx = 0; idx < table.getNumColumns(); ++idx) {
363 sb.append(createColumnSql(table.getColumn(idx)));
368 return sb.toString();
371 protected String dropTableSql(Table table) {
372 assert(null != table);
373 assert(null != table.getName());
375 return "DROP TABLE " + table.getName();
378 protected String createSequenceSql(Sequence seq) {
380 assert(null != seq.getName());
382 return "CREATE SEQUENCE " + seq.getName();
385 protected String dropSequenceSql(Sequence seq) {
387 assert(null != seq.getName());
389 return "DROP SEQUENCE " + seq.getName();
392 abstract protected String nextValSql(Sequence seq);
394 boolean isValidInsert(Table table, Object[][] values)
396 if (null == table) return false;
397 if (null == values) return false;
399 for (Object[] rowValues : values) {
400 if (rowValues.length != table.getNumColumns()) {
403 for (int idx = 0; idx < rowValues.length; ++idx) {