From: James Moger Date: Thu, 11 Aug 2011 18:04:01 +0000 (-0400) Subject: Added support for HSQL database. Revised dialects some more. X-Git-Tag: v0.6.4~7 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=0333ed4cf0b5db3f9ffcb0da31787f6e44139af5;p=iciql.git Added support for HSQL database. Revised dialects some more. Moved CREATE TABLE and CREATE INDEX statement generation to the dialect. Added DECIMAL(length, scale) support. Improved automatic dialect detection. Unspecified length string is now CLOB instead of TEXT. Boolean now maps to BOOLEAN instead of BIT. Expressions on unmapped fields will throw an IciqlException. Improved exception reporting. --- diff --git a/.classpath b/.classpath index 7e65762..c52fc50 100644 --- a/.classpath +++ b/.classpath @@ -29,5 +29,6 @@ + diff --git a/README.markdown b/README.markdown index 416bba2..dc8b761 100644 --- a/README.markdown +++ b/README.markdown @@ -17,8 +17,8 @@ iciql **is not**... Supported Databases ------- -- [H2](http://h2database.com) -- [MySQL](http://mysql.com) +- [H2 1.3+](http://h2database.com) +- [HSQLDB 2.2+](http://hsqldb.org) - Support for others is planned and should only require creating a simple "dialect" class. License diff --git a/docs/00_index.mkd b/docs/00_index.mkd index 70048e7..1377db1 100644 --- a/docs/00_index.mkd +++ b/docs/00_index.mkd @@ -36,7 +36,7 @@ select * from products ### Supported Databases -[H2](http://h2database.com), [MySQL](http://mysql.com) +[H2 1.3+](http://h2database.com), [HSQLDB 2.2+](http://hsqldb.org) Support for others is planned and should only require creating a simple "dialect" class. diff --git a/docs/01_model_classes.mkd b/docs/01_model_classes.mkd index bdddd80..658ff2b 100644 --- a/docs/01_model_classes.mkd +++ b/docs/01_model_classes.mkd @@ -17,7 +17,7 @@ The following data types can be used for all iciql expressions. - + @@ -41,7 +41,7 @@ The following data types can be used for all iciql expressions. - + @@ -56,7 +56,7 @@ The following data types can be used for all iciql expressions. - + diff --git a/docs/02_table_versioning.mkd b/docs/02_table_versioning.mkd index 12cdf6c..29942fe 100644 --- a/docs/02_table_versioning.mkd +++ b/docs/02_table_versioning.mkd @@ -8,22 +8,22 @@ AND/OR
Your `com.iciql.DbUpgrader` implementation must specify the `IQVersion(version)` annotation ### How does it work? -If you choose to use versioning, iciql will maintain a table within your database named *_iq_versions* which is defined as: +If you choose to use versioning, iciql will maintain a table within your database named *iq_versions* which is defined as: - CREATE TABLE _IQ_VERSIONS(SCHEMANAME VARCHAR(255) NOT NULL, TABLENAME VARCHAR(255) NOT NULL, VERSION INT NOT NULL) + CREATE TABLE IQ_VERSIONS(SCHEMANAME VARCHAR(255) NOT NULL, TABLENAME VARCHAR(255) NOT NULL, VERSION INT NOT NULL) This database table is automatically created if and only if at least one of your model classes specifies a *version* > 0. -When you generate a statement, iciql will compare the annotated version field of your model class to its last known value in the *_iq_versions* table. If *_iq_versions* lags behind the model annotation, iciql will immediately call the registered `com.iciql.DbUpgrader` implementation before generating and executing the current statement. +When you generate a statement, iciql will compare the annotated version field of your model class to its last known value in the *iq_versions* table. If *iq_versions* lags behind the model annotation, iciql will immediately call the registered `com.iciql.DbUpgrader` implementation before generating and executing the current statement. When an upgrade scenario is identified, the current version and the annotated version information is passed to either: - `DbUpgrader.upgradeDatabase(db, fromVersion, toVersion)` - `DbUpgrader.upgradeTable(db, schema, table, fromVersion, toVersion)` -both of which allow for non-linear upgrades. If the upgrade method call is successful and returns *true*, iciql will update the *_iq_versions* table with the annotated version number. +both of which allow for non-linear upgrades. If the upgrade method call is successful and returns *true*, iciql will update the *iq_versions* table with the annotated version number. The actual upgrade procedure is beyond the scope of iciql and is your responsibility to implement. This is simply a mechanism to automatically identify when an upgrade is necessary. **NOTE:**
-The database entry of the *_iq_versions* table is specified as SCHEMANAME='' and TABLENAME=''. \ No newline at end of file +The database entry of the *iq_versions* table is specified as SCHEMANAME='' and TABLENAME=''. \ No newline at end of file diff --git a/docs/05_releases.mkd b/docs/05_releases.mkd index 7fb0113..c999062 100644 --- a/docs/05_releases.mkd +++ b/docs/05_releases.mkd @@ -7,9 +7,17 @@ **%VERSION%** ([zip](http://code.google.com/p/iciql/downloads/detail?name=%ZIP%)|[jar](http://code.google.com/p/iciql/downloads/detail?name=%JAR%))   *released %BUILDDATE%* - api change release (API v4) +- DECIMAL(length, scale) support +- unspecified length String fields are now CLOB instead of TEXT. dialects can intercept this and convert to another type. e.g. MySQL dialect can change CLOB to TEXT. +- Boolean now maps to BOOLEAN instead of BIT +- expressions on unmapped fields will throw an IciqlException +- improved exception reporting - moved dialects back to main package -- refined dialect loading for pooled connections -- added a MySQL dialect +- improved automatic dialect determination on pooled connections +- moved create table and create index statement generation into dialects +- added HSQL dialect. HSQL fails 4 unit tests, 2 of which are unimplemented merge, 1 has been filed as a bug in HSQL. +- added MySQL dialect. Untested. +- renamed _ iq_versions table to *iq_versions* since leading _ character is troublesome for some databases. - @IQColumn(allowNull=true) -> @IQColumn(nullable=true) - All columns are assumed NULLABLE unless explicitly set *@IQColumn(nullable = false)* - allow using objects to assign default values
diff --git a/src/com/iciql/Db.java b/src/com/iciql/Db.java index c7801c2..a8c0b12 100644 --- a/src/com/iciql/Db.java +++ b/src/com/iciql/Db.java @@ -73,6 +73,7 @@ public class Db { // 2. DatabaseMetaData.getDatabaseProductName() DIALECTS.put("H2", SQLDialectH2.class); DIALECTS.put("MySQL", SQLDialectMySQL.class); + DIALECTS.put("HSQL Database Engine", SQLDialectHSQL.class); } private Db(Connection conn) { @@ -83,7 +84,7 @@ public class Db { data = conn.getMetaData(); databaseName = data.getDatabaseProductName(); } catch (SQLException s) { - throw new IciqlException("Failed to retrieve database metadata!", s); + throw new IciqlException(s, "failed to retrieve database metadata!"); } dialect = getDialect(databaseName, conn.getClass().getName()); dialect.configureDialect(databaseName, data); @@ -140,7 +141,7 @@ public class Db { Connection conn = JdbcUtils.getConnection(null, url, user, password); return new Db(conn); } catch (SQLException e) { - throw convert(e); + throw new IciqlException(e); } } @@ -156,7 +157,7 @@ public class Db { try { return new Db(ds.getConnection()); } catch (SQLException e) { - throw convert(e); + throw new IciqlException(e); } } @@ -172,14 +173,10 @@ public class Db { Connection conn = JdbcUtils.getConnection(null, url, prop); return new Db(conn); } catch (SQLException e) { - throw convert(e); + throw new IciqlException(e); } } - private static Error convert(Exception e) { - return new Error(e); - } - public void insert(T t) { Class clazz = t.getClass(); define(clazz).createTableIfRequired(this).insert(this, t, false); @@ -191,18 +188,15 @@ public class Db { } /** - * Merge usually INSERTS if the record does not exist or UPDATES the record - * if it does exist. Not all databases support MERGE and the syntax varies - * with the database. + * Merge INSERTS if the record does not exist or UPDATES the record if it + * does exist. Not all databases support MERGE and the syntax varies with + * the database. * * If the dialect does not support merge an IciqlException will be thrown. * * @param t */ public void merge(T t) { - if (!getDialect().supportsMerge()) { - throw new IciqlException("Merge is not supported by this SQL dialect"); - } Class clazz = t.getClass(); define(clazz).createTableIfRequired(this).merge(this, t); } @@ -383,13 +377,14 @@ public class Db { } PreparedStatement prepare(String sql, boolean returnGeneratedKeys) { + IciqlException.checkUnmappedField(sql); try { if (returnGeneratedKeys) { return conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); } return conn.prepareStatement(sql); } catch (SQLException e) { - throw new IciqlException(e); + throw IciqlException.fromSQL(sql, e); } } diff --git a/src/com/iciql/DbVersion.java b/src/com/iciql/DbVersion.java index 8ccf66f..6270e14 100644 --- a/src/com/iciql/DbVersion.java +++ b/src/com/iciql/DbVersion.java @@ -23,7 +23,7 @@ import com.iciql.Iciql.IQTable; /** * A system table to track database and table versions. */ -@IQTable(name = "_iq_versions", primaryKey = { "schemaName", "tableName"}, memoryTable = true) +@IQTable(name = "iq_versions", primaryKey = { "schemaName", "tableName" }, memoryTable = true) public class DbVersion { @IQColumn(length = 255) diff --git a/src/com/iciql/Define.java b/src/com/iciql/Define.java index d7b42ba..5e969e1 100644 --- a/src/com/iciql/Define.java +++ b/src/com/iciql/Define.java @@ -61,7 +61,12 @@ public class Define { public static void length(Object column, int length) { checkInDefine(); - currentTableDefinition.setMaxLength(column, length); + currentTableDefinition.setLength(column, length); + } + + public static void scale(Object column, int scale) { + checkInDefine(); + currentTableDefinition.setScale(column, scale); } public static void tableName(String tableName) { diff --git a/src/com/iciql/Iciql.java b/src/com/iciql/Iciql.java index 40c412a..33b954b 100644 --- a/src/com/iciql/Iciql.java +++ b/src/com/iciql/Iciql.java @@ -40,7 +40,7 @@ import java.lang.annotation.Target; * * * - * + * * * * @@ -72,7 +72,7 @@ import java.lang.annotation.Target; * * * - * + * * * * @@ -92,7 +92,7 @@ import java.lang.annotation.Target; * * * - * * * @@ -391,15 +391,27 @@ public interface Iciql { boolean autoIncrement() default false; /** - * If larger than zero, it is used during the CREATE TABLE phase. It may - * also be used to prevent database exceptions on INSERT and UPDATE - * statements (see trim). + * Length is used to define the length of a VARCHAR column or to define + * the precision of a DECIMAL(precision, scale) expression. + *

+ * If larger than zero, it is used during the CREATE TABLE phase. For + * string values it may also be used to prevent database exceptions on + * INSERT and UPDATE statements (see trim). *

* Any length set in define() may override this annotation setting if * the model class is not annotated with IQTable. Default: 0. */ int length() default 0; + /** + * Scale is used during the CREATE TABLE phase to define the scale of a + * DECIMAL(precision, scale) expression. + *

+ * Any scale set in define() may override this annotation setting if + * the model class is not annotated with IQTable. Default: 0. + */ + int scale() default 0; + /** * If true, iciql will automatically trim the string if it exceeds * length (value.substring(0, length)). Default: false. diff --git a/src/com/iciql/IciqlException.java b/src/com/iciql/IciqlException.java index 1e2a890..6e55c79 100644 --- a/src/com/iciql/IciqlException.java +++ b/src/com/iciql/IciqlException.java @@ -16,30 +16,109 @@ package com.iciql; +import java.sql.SQLException; import java.text.MessageFormat; +import java.util.regex.Pattern; /** * Iciql wraps all exceptions with this class. */ public class IciqlException extends RuntimeException { + public static final int CODE_UNMAPPED_FIELD = 1; + public static final int CODE_DUPLICATE_KEY = 2; + public static final int CODE_TABLE_NOT_FOUND = 3; + public static final int CODE_INDEX_ALREADY_EXISTS = 4; + + private static final String TOKEN_UNMAPPED_FIELD = "\\? (=|\\>|\\<|!=|\\>=|\\<=|LIKE|BETWEEN) \\?"; + private static final long serialVersionUID = 1L; + private String sql; + + private int iciqlCode; + + public IciqlException(Throwable t) { + super(t.getMessage(), t); + configureCode(t); + } + public IciqlException(String message, Object... parameters) { super(parameters.length > 0 ? MessageFormat.format(message, parameters) : message); - } public IciqlException(Throwable t, String message, Object... parameters) { super(parameters.length > 0 ? MessageFormat.format(message, parameters) : message, t); - + configureCode(t); + } + + public static void checkUnmappedField(String sql) { + if (Pattern.compile(IciqlException.TOKEN_UNMAPPED_FIELD).matcher(sql).find()) { + IciqlException e = new IciqlException("unmapped field in statement!"); + e.sql = sql; + e.iciqlCode = CODE_UNMAPPED_FIELD; + throw e; + } + } + + public static IciqlException fromSQL(String sql, Throwable t) { + if (Pattern.compile(TOKEN_UNMAPPED_FIELD).matcher(sql).find()) { + IciqlException e = new IciqlException(t, "unmapped field in statement!"); + e.sql = sql; + e.iciqlCode = CODE_UNMAPPED_FIELD; + return e; + } else { + IciqlException e = new IciqlException(t, t.getMessage()); + e.sql = sql; + return e; + } + } + + public void setSQL(String sql) { + this.sql = sql; } - public IciqlException(Throwable t) { - super(t); + public String getSQL() { + return sql; } - public IciqlException(String message, Throwable t) { - super(message, t); + public int getIciqlCode() { + return iciqlCode; + } + + private void configureCode(Throwable t) { + if (t == null) { + return; + } + if (t instanceof SQLException) { + // http://developer.mimer.com/documentation/html_92/Mimer_SQL_Mobile_DocSet/App_Return_Codes2.html + SQLException s = (SQLException) t; + String state = s.getSQLState(); + if ("23505".equals(state)) { + iciqlCode = CODE_DUPLICATE_KEY; + } else if ("42501".equals(state)) { + iciqlCode = CODE_TABLE_NOT_FOUND; + } else if ("42S02".equals(state)) { + iciqlCode = CODE_TABLE_NOT_FOUND; + } else if ("42504".equals(state)) { + iciqlCode = CODE_INDEX_ALREADY_EXISTS; + } else if ("42S11".equals(state)) { + iciqlCode = CODE_INDEX_ALREADY_EXISTS; + } + } } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getClass().getName()); + String message = getLocalizedMessage(); + if (message != null) { + sb.append(": ").append(message); + } + if (sql != null) { + sb.append('\n').append(sql); + } + return sb.toString(); + } } diff --git a/src/com/iciql/ModelUtils.java b/src/com/iciql/ModelUtils.java index 2e42a7d..441bca7 100644 --- a/src/com/iciql/ModelUtils.java +++ b/src/com/iciql/ModelUtils.java @@ -49,7 +49,7 @@ class ModelUtils { static { Map, String> m = SUPPORTED_TYPES; m.put(String.class, "VARCHAR"); - m.put(Boolean.class, "BIT"); + m.put(Boolean.class, "BOOLEAN"); m.put(Byte.class, "TINYINT"); m.put(Short.class, "SMALLINT"); m.put(Integer.class, "INT"); @@ -100,8 +100,8 @@ class ModelUtils { m.put("NCLOB", "VARCHAR"); // logic - m.put("BOOL", "BIT"); - m.put("BOOLEAN", "BIT"); + m.put("BIT", "BOOLEAN"); + m.put("BOOL", "BOOLEAN"); // numeric m.put("BYTE", "TINYINT"); @@ -159,19 +159,11 @@ class ModelUtils { return "INT"; case NAME: default: - if (fieldDef.maxLength <= 0) { - return "TEXT"; - } return "VARCHAR"; } } if (SUPPORTED_TYPES.containsKey(fieldClass)) { - String type = SUPPORTED_TYPES.get(fieldClass); - if (type.equals("VARCHAR") && fieldDef.maxLength <= 0) { - // unspecified length strings are TEXT, not VARCHAR - return "TEXT"; - } - return type; + return SUPPORTED_TYPES.get(fieldClass); } if (!strictTypeMapping) { return "VARCHAR"; diff --git a/src/com/iciql/Query.java b/src/com/iciql/Query.java index 2e58fe5..5c2d225 100644 --- a/src/com/iciql/Query.java +++ b/src/com/iciql/Query.java @@ -83,7 +83,7 @@ public class Query { long value = rs.getLong(1); return value; } catch (SQLException e) { - throw new IciqlException(e); + throw IciqlException.fromSQL(stat.getSQL(), e); } finally { JdbcUtils.closeSilently(rs, true); } @@ -128,7 +128,7 @@ public class Query { result.add(item); } } catch (SQLException e) { - throw new IciqlException(e); + throw IciqlException.fromSQL(stat.getSQL(), e); } finally { JdbcUtils.closeSilently(rs, true); } @@ -204,7 +204,7 @@ public class Query { result.add(row); } } catch (SQLException e) { - throw new IciqlException(e); + throw IciqlException.fromSQL(stat.getSQL(), e); } finally { JdbcUtils.closeSilently(rs, true); } @@ -220,26 +220,20 @@ public class Query { List result = Utils.newArrayList(); try { while (rs.next()) { - try { - X value; - Object o = rs.getObject(1); - // Convert CLOB and BLOB now because we close the resultset - if (Clob.class.isAssignableFrom(o.getClass())) { - value = (X) Utils.convert(o, String.class); - } else if (Blob.class.isAssignableFrom(o.getClass())) { - value = (X) Utils.convert(o, byte[].class); - } else { - value = (X) o; - } - result.add(value); - } catch (IciqlException e) { - throw e; - } catch (Exception e) { - throw new IciqlException(e); + X value; + Object o = rs.getObject(1); + // Convert CLOB and BLOB now because we close the resultset + if (Clob.class.isAssignableFrom(o.getClass())) { + value = (X) Utils.convert(o, String.class); + } else if (Blob.class.isAssignableFrom(o.getClass())) { + value = (X) Utils.convert(o, byte[].class); + } else { + value = (X) o; } + result.add(value); } - } catch (SQLException e) { - throw new IciqlException(e); + } catch (Exception e) { + throw IciqlException.fromSQL(stat.getSQL(), e); } finally { JdbcUtils.closeSilently(rs, true); } diff --git a/src/com/iciql/QueryBetween.java b/src/com/iciql/QueryBetween.java index de09515..8628f40 100644 --- a/src/com/iciql/QueryBetween.java +++ b/src/com/iciql/QueryBetween.java @@ -18,6 +18,10 @@ package com.iciql; /** * This class represents a "between y and z" condition. + * @param + * the return type of the query + * @param + * the incomplete condition data type */ public class QueryBetween { diff --git a/src/com/iciql/SQLDialect.java b/src/com/iciql/SQLDialect.java index 3ca2c4c..7821c3f 100644 --- a/src/com/iciql/SQLDialect.java +++ b/src/com/iciql/SQLDialect.java @@ -55,6 +55,14 @@ public interface SQLDialect { */ String prepareColumnName(String name); + /** + * Get the CREATE TABLE statement. + * + * @param stat + * @param def + */ + void prepareCreateTable(SQLStatement stat, TableDefinition def); + /** * Get the CREATE INDEX statement. * @@ -66,8 +74,8 @@ public interface SQLDialect { * the index definition * @return the SQL statement */ - String prepareCreateIndex(String schemaName, String tableName, IndexDefinition index); - + void prepareCreateIndex(SQLStatement stat, String schemaName, String tableName, IndexDefinition index); + /** * Get a MERGE or REPLACE INTO statement. * @@ -78,7 +86,8 @@ public interface SQLDialect { * @param index * the index definition */ - void prepareMerge(SQLStatement stat, String schemaName, String tableName, TableDefinition def, Object obj); + void prepareMerge(SQLStatement stat, String schemaName, String tableName, TableDefinition def, + Object obj); /** * Append "LIMIT limit" to the SQL statement. @@ -106,18 +115,12 @@ public interface SQLDialect { * @return true if they are */ boolean supportsMemoryTables(); - - /** - * Whether merge is a supported function. - * - * @return true if they are - */ - boolean supportsMerge(); - + /** * Whether LIMIT/OFFSET notation is supported. * * @return true if they are */ boolean supportsLimitOffset(); + } diff --git a/src/com/iciql/SQLDialectDefault.java b/src/com/iciql/SQLDialectDefault.java index 760a1f4..3fd2067 100644 --- a/src/com/iciql/SQLDialectDefault.java +++ b/src/com/iciql/SQLDialectDefault.java @@ -20,7 +20,9 @@ package com.iciql; import java.sql.DatabaseMetaData; import java.sql.SQLException; +import com.iciql.TableDefinition.FieldDefinition; import com.iciql.TableDefinition.IndexDefinition; +import com.iciql.util.StatementBuilder; import com.iciql.util.StringUtils; /** @@ -49,13 +51,18 @@ public class SQLDialectDefault implements SQLDialect { } } - @Override - public boolean supportsMemoryTables() { - return false; + /** + * Allows subclasses to change the type of a column for a CREATE statement. + * + * @param sqlType + * @return the SQL type or a preferred alternative + */ + protected String convertSqlType(String sqlType) { + return sqlType; } @Override - public boolean supportsMerge() { + public boolean supportsMemoryTables() { return false; } @@ -78,12 +85,100 @@ public class SQLDialectDefault implements SQLDialect { } @Override - public String prepareCreateIndex(String schemaName, String tableName, IndexDefinition index) { + public void prepareCreateTable(SQLStatement stat, TableDefinition def) { + StatementBuilder buff; + if (def.memoryTable && supportsMemoryTables()) { + buff = new StatementBuilder("CREATE MEMORY TABLE IF NOT EXISTS "); + } else { + buff = new StatementBuilder("CREATE TABLE IF NOT EXISTS "); + } + + buff.append(prepareTableName(def.schemaName, def.tableName)).append('('); + + boolean hasIdentityColumn = false; + for (FieldDefinition field : def.fields) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(field.columnName)).append(' '); + String dataType = field.dataType; + if (dataType.equals("VARCHAR")) { + // check to see if we should use VARCHAR or CLOB + if (field.length <= 0) { + dataType = "CLOB"; + } + buff.append(convertSqlType(dataType)); + if (field.length > 0) { + buff.append('(').append(field.length).append(')'); + } + } else if (dataType.equals("DECIMAL")) { + // DECIMAL(precision,scale) + buff.append(convertSqlType(dataType)); + if (field.length > 0) { + buff.append('(').append(field.length); + if (field.scale > 0) { + buff.append(',').append(field.scale); + } + buff.append(')'); + } + } else { + // other + buff.append(convertSqlType(dataType)); + } + + hasIdentityColumn |= prepareColumnDefinition(buff, field.isAutoIncrement, field.isPrimaryKey); + + if (!field.nullable) { + buff.append(" NOT NULL"); + } + + // default values + if (!field.isAutoIncrement && !field.isPrimaryKey) { + String dv = field.defaultValue; + if (!StringUtils.isNullOrEmpty(dv)) { + if (ModelUtils.isProperlyFormattedDefaultValue(dv) + && ModelUtils.isValidDefaultValue(field.field.getType(), dv)) { + buff.append(" DEFAULT " + dv); + } + } + } + } + + // if table does not have identity column then specify primary key + if (!hasIdentityColumn) { + if (def.primaryKeyColumnNames != null && def.primaryKeyColumnNames.size() > 0) { + buff.append(", PRIMARY KEY("); + buff.resetCount(); + for (String n : def.primaryKeyColumnNames) { + buff.appendExceptFirst(", "); + buff.append(prepareColumnName(n)); + } + buff.append(')'); + } + } + buff.append(')'); + stat.setSQL(buff.toString()); + } + + protected boolean prepareColumnDefinition(StatementBuilder buff, boolean isAutoIncrement, + boolean isPrimaryKey) { + boolean isIdentity = false; + if (isAutoIncrement && isPrimaryKey) { + buff.append(" IDENTITY"); + isIdentity = true; + } else if (isAutoIncrement) { + buff.append(" AUTO_INCREMENT"); + } + return isIdentity; + } + + @Override + public void prepareCreateIndex(SQLStatement stat, String schemaName, String tableName, + IndexDefinition index) { throw new IciqlException("Dialect does not support index creation!"); } @Override - public void prepareMerge(SQLStatement stat, String schemaName, String tableName, TableDefinition def, Object obj) { + public void prepareMerge(SQLStatement stat, String schemaName, String tableName, + TableDefinition def, Object obj) { throw new IciqlException("Dialect does not support merge statements!"); } diff --git a/src/com/iciql/SQLDialectH2.java b/src/com/iciql/SQLDialectH2.java index c65c277..80786f9 100644 --- a/src/com/iciql/SQLDialectH2.java +++ b/src/com/iciql/SQLDialectH2.java @@ -31,12 +31,7 @@ public class SQLDialectH2 extends SQLDialectDefault { } @Override - public boolean supportsMerge() { - return true; - } - - @Override - public String prepareCreateIndex(String schema, String table, IndexDefinition index) { + public void prepareCreateIndex(SQLStatement stat, String schema, String table, IndexDefinition index) { StatementBuilder buff = new StatementBuilder(); buff.append("CREATE "); switch (index.type) { @@ -62,7 +57,7 @@ public class SQLDialectH2 extends SQLDialectDefault { buff.append(col); } buff.append(")"); - return buff.toString(); + stat.setSQL(buff.toString()); } @Override diff --git a/src/com/iciql/SQLDialectHSQL.java b/src/com/iciql/SQLDialectHSQL.java new file mode 100644 index 0000000..e617758 --- /dev/null +++ b/src/com/iciql/SQLDialectHSQL.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import com.iciql.TableDefinition.IndexDefinition; +import com.iciql.util.StatementBuilder; + +/** + * HyperSQL database dialect. + */ +public class SQLDialectHSQL extends SQLDialectDefault { + + @Override + public boolean supportsMemoryTables() { + return true; + } + + @Override + protected boolean prepareColumnDefinition(StatementBuilder buff, boolean isAutoIncrement, boolean isPrimaryKey) { + boolean isIdentity = false; + if (isAutoIncrement && isPrimaryKey) { + buff.append(" IDENTITY"); + isIdentity = true; + } + return isIdentity; + } + + @Override + public void prepareCreateIndex(SQLStatement stat, String schema, String table, IndexDefinition index) { + StatementBuilder buff = new StatementBuilder(); + buff.append("CREATE "); + switch (index.type) { + case STANDARD: + break; + case UNIQUE: + buff.append("UNIQUE "); + break; + case UNIQUE_HASH: + buff.append("UNIQUE "); + break; + } + buff.append("INDEX "); + buff.append(index.indexName); + buff.append(" ON "); + buff.append(table); + buff.append("("); + for (String col : index.columnNames) { + buff.appendExceptFirst(", "); + buff.append(col); + } + buff.append(")"); + stat.setSQL(buff.toString()); + } +} \ No newline at end of file diff --git a/src/com/iciql/SQLDialectMySQL.java b/src/com/iciql/SQLDialectMySQL.java index 837d77b..2593e0a 100644 --- a/src/com/iciql/SQLDialectMySQL.java +++ b/src/com/iciql/SQLDialectMySQL.java @@ -16,7 +16,6 @@ package com.iciql; -import com.iciql.TableDefinition.FieldDefinition; import com.iciql.TableDefinition.IndexDefinition; import com.iciql.util.StatementBuilder; @@ -26,13 +25,16 @@ import com.iciql.util.StatementBuilder; public class SQLDialectMySQL extends SQLDialectDefault { @Override - public boolean supportsMemoryTables() { - return false; + protected String convertSqlType(String sqlType) { + if (sqlType.equals("CLOB")) { + return "TEXT"; + } + return sqlType; } - + @Override - public boolean supportsMerge() { - return true; + public boolean supportsMemoryTables() { + return false; } @Override @@ -41,7 +43,15 @@ public class SQLDialectMySQL extends SQLDialectDefault { } @Override - public String prepareCreateIndex(String schema, String table, IndexDefinition index) { + protected boolean prepareColumnDefinition(StatementBuilder buff, boolean isAutoIncrement, boolean isPrimaryKey) { + if (isAutoIncrement) { + buff.append(" AUTO_INCREMENT"); + } + return false; + } + + @Override + public void prepareCreateIndex(SQLStatement stat, String schema, String table, IndexDefinition index) { StatementBuilder buff = new StatementBuilder(); buff.append("CREATE "); switch (index.type) { @@ -74,26 +84,6 @@ public class SQLDialectMySQL extends SQLDialectDefault { buff.append("USING HASH"); break; } - return buff.toString().trim(); - } - - @Override - public void prepareMerge(SQLStatement stat, String schemaName, String tableName, TableDefinition def, Object obj) { - StatementBuilder buff = new StatementBuilder("REPLACE INTO "); - buff.append(prepareTableName(schemaName, tableName)).append('('); - for (FieldDefinition field : def.fields) { - buff.appendExceptFirst(", "); - buff.append(prepareColumnName(field.columnName)); - } - buff.append(") VALUES("); - buff.resetCount(); - for (FieldDefinition field : def.fields) { - buff.appendExceptFirst(", "); - buff.append('?'); - Object value = def.getValue(obj, field); - stat.addParameter(value); - } - buff.append(')'); - stat.setSQL(buff.toString()); + stat.setSQL(buff.toString().trim()); } } \ No newline at end of file diff --git a/src/com/iciql/SQLStatement.java b/src/com/iciql/SQLStatement.java index 49c7627..4962502 100644 --- a/src/com/iciql/SQLStatement.java +++ b/src/com/iciql/SQLStatement.java @@ -52,7 +52,7 @@ public class SQLStatement { public SQLStatement appendTable(String schema, String table) { return appendSQL(db.getDialect().prepareTableName(schema, table)); } - + public SQLStatement appendColumn(String column) { return appendSQL(db.getDialect().prepareColumnName(column)); } @@ -63,7 +63,7 @@ public class SQLStatement { } return sql; } - + public SQLStatement addParameter(Object o) { params.add(o); return this; @@ -73,7 +73,7 @@ public class SQLStatement { try { return prepare(false).executeQuery(); } catch (SQLException e) { - throw new IciqlException(e); + throw IciqlException.fromSQL(getSQL(), e); } } @@ -83,7 +83,7 @@ public class SQLStatement { ps = prepare(false); return ps.executeUpdate(); } catch (SQLException e) { - throw new IciqlException(e); + throw IciqlException.fromSQL(getSQL(), e); } finally { JdbcUtils.closeSilently(ps); } @@ -102,17 +102,19 @@ public class SQLStatement { JdbcUtils.closeSilently(rs); return identity; } catch (SQLException e) { - throw new IciqlException(e); + throw IciqlException.fromSQL(getSQL(), e); } finally { JdbcUtils.closeSilently(ps); } } - private static void setValue(PreparedStatement prep, int parameterIndex, Object x) { + private void setValue(PreparedStatement prep, int parameterIndex, Object x) { try { prep.setObject(parameterIndex, x); } catch (SQLException e) { - throw new IciqlException(e); + IciqlException ix = new IciqlException(e, "error setting parameter {0} as {1}", parameterIndex, x.getClass().getSimpleName()); + ix.setSQL(getSQL()); + throw ix; } } diff --git a/src/com/iciql/TableDefinition.java b/src/com/iciql/TableDefinition.java index c08a032..571ab1f 100644 --- a/src/com/iciql/TableDefinition.java +++ b/src/com/iciql/TableDefinition.java @@ -71,10 +71,11 @@ public class TableDefinition { String columnName; Field field; String dataType; - int maxLength; + int length; + int scale; boolean isPrimaryKey; boolean isAutoIncrement; - boolean trimString; + boolean trim; boolean nullable; String defaultValue; EnumType enumType; @@ -125,13 +126,14 @@ public class TableDefinition { String schemaName; String tableName; int tableVersion; - private boolean createTableIfRequired = true; + List primaryKeyColumnNames; + boolean memoryTable; + + private boolean createTableIfRequired = true; private Class clazz; private IdentityHashMap fieldMap = Utils.newIdentityHashMap(); - - private List primaryKeyColumnNames; private ArrayList indexes = Utils.newArrayList(); - private boolean memoryTable; + TableDefinition(Class clazz) { this.clazz = clazz; @@ -242,10 +244,17 @@ public class TableDefinition { } } - public void setMaxLength(Object column, int maxLength) { + public void setLength(Object column, int length) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.length = length; + } + } + + public void setScale(Object column, int scale) { FieldDefinition def = fieldMap.get(column); if (def != null) { - def.maxLength = maxLength; + def.scale = scale; } } @@ -273,8 +282,9 @@ public class TableDefinition { String columnName = f.getName(); boolean isAutoIncrement = false; boolean isPrimaryKey = false; - int maxLength = 0; - boolean trimString = false; + int length = 0; + int scale = 0; + boolean trim = false; boolean nullable = true; EnumType enumType = null; String defaultValue = ""; @@ -301,8 +311,9 @@ public class TableDefinition { } isAutoIncrement = col.autoIncrement(); isPrimaryKey = col.primaryKey(); - maxLength = col.length(); - trimString = col.trim(); + length = col.length(); + scale = col.scale(); + trim = col.trim(); nullable = col.nullable(); // try using default object @@ -321,7 +332,7 @@ public class TableDefinition { } } } catch (IllegalAccessException e) { - throw new IciqlException(e, "Failed to get default object for {0}", columnName); + throw new IciqlException(e, "failed to get default object for {0}", columnName); } // annotation overrides @@ -338,8 +349,9 @@ public class TableDefinition { fieldDef.columnName = columnName; fieldDef.isAutoIncrement = isAutoIncrement; fieldDef.isPrimaryKey = isPrimaryKey; - fieldDef.maxLength = maxLength; - fieldDef.trimString = trimString; + fieldDef.length = length; + fieldDef.scale = scale; + fieldDef.trim = trim; fieldDef.nullable = nullable; fieldDef.defaultValue = defaultValue; fieldDef.enumType = enumType; @@ -372,9 +384,9 @@ public class TableDefinition { Enum iqenum = (Enum) value; switch (field.enumType) { case NAME: - if (field.trimString && field.maxLength > 0) { - if (iqenum.name().length() > field.maxLength) { - return iqenum.name().substring(0, field.maxLength); + if (field.trim && field.length > 0) { + if (iqenum.name().length() > field.length) { + return iqenum.name().substring(0, field.length); } } return iqenum.name(); @@ -388,12 +400,13 @@ public class TableDefinition { return enumid.enumId(); } } - if (field.trimString && field.maxLength > 0) { + + if (field.trim && field.length > 0) { if (value instanceof String) { // clip strings String s = (String) value; - if (s.length() > field.maxLength) { - return s.substring(0, field.maxLength); + if (s.length() > field.length) { + return s.substring(0, field.length); } return s; } @@ -514,65 +527,23 @@ public class TableDefinition { db.upgradeTable(this); return this; } - SQLDialect dialect = db.getDialect(); SQLStatement stat = new SQLStatement(db); - StatementBuilder buff; - if (memoryTable && dialect.supportsMemoryTables()) { - buff = new StatementBuilder("CREATE MEMORY TABLE IF NOT EXISTS "); - } else { - buff = new StatementBuilder("CREATE TABLE IF NOT EXISTS "); - } - - buff.append(dialect.prepareTableName(schemaName, tableName)).append('('); - - for (FieldDefinition field : fields) { - buff.appendExceptFirst(", "); - buff.append(dialect.prepareColumnName(field.columnName)).append(' ').append(field.dataType); - if (field.maxLength > 0) { - buff.append('(').append(field.maxLength).append(')'); - } - - if (field.isAutoIncrement) { - buff.append(" AUTO_INCREMENT"); - } - - if (!field.nullable) { - buff.append(" NOT NULL"); - } - - // default values - if (!field.isAutoIncrement && !field.isPrimaryKey) { - String dv = field.defaultValue; - if (!StringUtils.isNullOrEmpty(dv)) { - if (ModelUtils.isProperlyFormattedDefaultValue(dv) - && ModelUtils.isValidDefaultValue(field.field.getType(), dv)) { - buff.append(" DEFAULT " + dv); - } - } - } - } - - // primary key - if (primaryKeyColumnNames != null && primaryKeyColumnNames.size() > 0) { - buff.append(", PRIMARY KEY("); - buff.resetCount(); - for (String n : primaryKeyColumnNames) { - buff.appendExceptFirst(", "); - buff.append(n); - } - buff.append(')'); - } - buff.append(')'); - stat.setSQL(buff.toString()); + db.getDialect().prepareCreateTable(stat, this); StatementLogger.create(stat.getSQL()); stat.executeUpdate(); // create indexes for (IndexDefinition index : indexes) { - String sql = db.getDialect().prepareCreateIndex(schemaName, tableName, index); - stat.setSQL(sql); + stat = new SQLStatement(db); + db.getDialect().prepareCreateIndex(stat, schemaName, tableName, index); StatementLogger.create(stat.getSQL()); - stat.executeUpdate(); + try { + stat.executeUpdate(); + } catch (IciqlException e) { + if (e.getIciqlCode() != IciqlException.CODE_INDEX_ALREADY_EXISTS) { + throw e; + } + } } // tables are created using IF NOT EXISTS diff --git a/src/com/iciql/TableInspector.java b/src/com/iciql/TableInspector.java index a5b6dee..e682f2f 100644 --- a/src/com/iciql/TableInspector.java +++ b/src/com/iciql/TableInspector.java @@ -118,9 +118,12 @@ public class TableInspector { indexes = Utils.newHashMap(); while (rs.next()) { IndexInspector info = new IndexInspector(rs); - if (info.type.equals(IndexType.UNIQUE) && info.name.toLowerCase().startsWith("primary")) { - // skip primary key indexes - continue; + if (info.type.equals(IndexType.UNIQUE)) { + String name = info.name.toLowerCase(); + if (name.startsWith("primary") || name.startsWith("sys_idx_sys_pk")) { + // skip primary key indexes + continue; + } } if (indexes.containsKey(info.name)) { indexes.get(info.name).addColumn(rs); @@ -140,7 +143,20 @@ public class TableInspector { col.clazz = ModelUtils.getClassForSqlType(col.type, dateTimeClass); col.size = rs.getInt("COLUMN_SIZE"); col.nullable = rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable; - col.isAutoIncrement = rs.getBoolean("IS_AUTOINCREMENT"); + try { + Object autoIncrement = rs.getObject("IS_AUTOINCREMENT"); + if (autoIncrement instanceof Boolean) { + col.isAutoIncrement = (Boolean) autoIncrement; + } else if (autoIncrement instanceof String) { + String val = autoIncrement.toString().toLowerCase(); + col.isAutoIncrement = val.equals("true") | val.equals("yes"); + } else if (autoIncrement instanceof Number) { + Number n = (Number) autoIncrement; + col.isAutoIncrement = n.intValue() > 0; + } + } catch (SQLException s) { + throw s; + } if (primaryKeys.size() == 1) { if (col.name.equalsIgnoreCase(primaryKeys.get(0))) { col.isPrimaryKey = true; @@ -499,14 +515,14 @@ public class TableInspector { // string types if (fieldClass == String.class) { - if ((fieldDef.maxLength != col.size) && (col.size < Integer.MAX_VALUE)) { + if ((fieldDef.length != col.size) && (col.size < Integer.MAX_VALUE)) { remarks.add(warn( table, col, format("{0}.length={1}, ColumnMaxLength={2}", IQColumn.class.getSimpleName(), - fieldDef.maxLength, col.size))); + fieldDef.length, col.size))); } - if (fieldDef.maxLength > 0 && !fieldDef.trimString) { + if (fieldDef.length > 0 && !fieldDef.trim) { remarks.add(consider(table, col, format("{0}.trim=true will prevent IciqlExceptions on" + " INSERT or UPDATE, but will clip data!", IQColumn.class.getSimpleName()))); } @@ -687,7 +703,7 @@ public class TableInspector { } } - void addEnum(String parameter, Enum value) { + void addEnum(String parameter, Enum value) { appendExceptFirst(", "); if (!StringUtils.isNullOrEmpty(parameter)) { append(parameter); diff --git a/src/com/iciql/ValidationRemark.java b/src/com/iciql/ValidationRemark.java index a68bf21..33320ab 100644 --- a/src/com/iciql/ValidationRemark.java +++ b/src/com/iciql/ValidationRemark.java @@ -35,11 +35,11 @@ public class ValidationRemark { CONSIDER, WARN, ERROR; } - private Level level; - private String table; - private String fieldType; - private String fieldName; - private String message; + public final Level level; + public final String table; + public final String fieldType; + public final String fieldName; + public final String message; private ValidationRemark(Level level, String table, String type, String message) { this.level = level; @@ -104,10 +104,6 @@ public class ValidationRemark { return level.equals(Level.ERROR); } - public Level getLevel() { - return level; - } - public String toString() { StringBuilder sb = new StringBuilder(); sb.append(StringUtils.pad(level.name(), 9, " ", true)); diff --git a/src/com/iciql/build/Build.java b/src/com/iciql/build/Build.java index 5963d16..cc3895c 100644 --- a/src/com/iciql/build/Build.java +++ b/src/com/iciql/build/Build.java @@ -56,6 +56,7 @@ public class Build { public static void compiletime() { downloadFromApache(MavenObject.H2, BuildType.RUNTIME); downloadFromApache(MavenObject.H2, BuildType.COMPILETIME); + downloadFromApache(MavenObject.HSQLDB, BuildType.RUNTIME); downloadFromApache(MavenObject.JCOMMANDER, BuildType.RUNTIME); downloadFromApache(MavenObject.JCOMMANDER, BuildType.COMPILETIME); downloadFromApache(MavenObject.MARKDOWNPAPERS, BuildType.RUNTIME); @@ -172,6 +173,10 @@ public class Build { "4bac13427caeb32ef6e93b70101e61f370c7b5e2", "6bb165156a0831879fa7797df6e18bdcd4421f2d", "446d3f58c44992534cb54f67134532d95961904a"); + public static final MavenObject HSQLDB = new MavenObject("org/hsqldb", "hsqldb", "2.2.4", + "6a6e040b07f5ee409fc825f1c5e5b574b1fa1428", "", + ""); + public static final MavenObject JUNIT = new MavenObject("junit", "junit", "4.8.2", "c94f54227b08100974c36170dcb53329435fe5ad", "", ""); diff --git a/src/com/iciql/util/Utils.java b/src/com/iciql/util/Utils.java index fa794cf..6f9746d 100644 --- a/src/com/iciql/util/Utils.java +++ b/src/com/iciql/util/Utils.java @@ -116,7 +116,7 @@ public class Utils { } } } - throw new IciqlException("Missing default constructor? Exception trying to create " + clazz.getName() + ": " + e, e); + throw new IciqlException(e, "Missing default constructor? Exception trying to instantiate {0}: {1}", clazz.getName(), e.getMessage()); } } }; @@ -190,7 +190,7 @@ public class Utils { } } } - throw new IciqlException("Exception trying to create " + clazz.getName() + ": " + e, e); + throw new IciqlException(e, "Missing default constructor?! Exception trying to instantiate {0}: {1}", clazz.getName(), e.getMessage()); } } @@ -220,18 +220,28 @@ public class Utils { Reader r = c.getCharacterStream(); return readStringAndClose(r, -1); } catch (Exception e) { - throw new IciqlException("Error converting CLOB to String: " + e.toString(), e); + throw new IciqlException(e, "error converting CLOB to String: ", e.toString()); } } return o.toString(); } - // convert from number to boolean if (Boolean.class.isAssignableFrom(targetType) || boolean.class.isAssignableFrom(targetType)) { + // convert from number to boolean if (Number.class.isAssignableFrom(currentType)) { Number n = (Number) o; return n.intValue() > 0; } + // convert from string to boolean + if (String.class.isAssignableFrom(currentType)) { + String s = o.toString().toLowerCase(); + float f = 0f; + try { + f = Float.parseFloat(s); + } catch (Exception e) { + } + return f > 0 || s.equals("true") || s.equals("yes"); + } } // convert from boolean to number @@ -271,7 +281,7 @@ public class Utils { InputStream is = b.getBinaryStream(); return readBlobAndClose(is, -1); } catch (Exception e) { - throw new IciqlException("Error converting BLOB to byte[]: " + e.toString(), e); + throw new IciqlException(e, "error converting BLOB to byte[]: ", e.toString()); } } } @@ -315,7 +325,7 @@ public class Utils { Reader r = c.getCharacterStream(); name = readStringAndClose(r, -1); } catch (Exception e) { - throw new IciqlException("Error converting CLOB to String: " + e.toString(), e); + throw new IciqlException(e, "error converting CLOB to String: ", e.toString()); } // find name match diff --git a/tests/com/iciql/test/StatementLoggerTest.java b/tests/com/iciql/test/StatementLoggerTest.java deleted file mode 100644 index 212faf2..0000000 --- a/tests/com/iciql/test/StatementLoggerTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2011 James Moger. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.iciql.test; - -import org.junit.Test; - -import com.iciql.Db; -import com.iciql.test.models.Product; -import com.iciql.util.StatementLogger; - -/** - * Tests the statement logger. - */ -public class StatementLoggerTest { - - @Test - public void testStatementLogger() { - StatementLogger.activateConsoleLogger(); - Db db = Db.open("jdbc:h2:mem:", "sa", "sa"); - db.insertAll(Product.getList()); - db.close(); - StatementLogger.logStats(); - StatementLogger.deactivateConsoleLogger(); - } -}

All Databases
java.lang.StringVARCHAR *(length > 0)* or TEXT *(length == 0)*
VARCHAR *(length > 0)* or CLOB *(length == 0)*
java.lang.Boolean BIT
DOUBLE
java.math.BigDecimalDECIMAL
DECIMAL *(length == 0)* or DECIMAL(length,scale) *(length > 0)*
java.sql.Date DATE
TIMESTAMP
java.lang.Enum.name()
*default type*
VARCHAR *(length > 0)* or TEXT *(length == 0)*
*EnumType.NAME*
VARCHAR *(length > 0)* or CLOB *(length == 0)*
*EnumType.NAME*
java.lang.Enum.ordinal() INT
*EnumType.ORDINAL*
java.lang.StringVARCHAR (length > 0) or TEXT (length == 0)VARCHAR (length > 0) or CLOB (length == 0)
java.lang.Boolean
java.math.BigDecimalDECIMALDECIMAL (length == 0)
DECIMAL(length, scale) (length > 0)
java.sql.Date
java.lang.Enum.name()VARCHAR (length > 0) or TEXT (length == 0)
+ *
VARCHAR (length > 0) or CLOB (length == 0)
* EnumType.NAME