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.
<attribute name="javadoc_location" value="jar:platform:/resource/iciql/ext/slf4j-api-1.6.1-javadoc.jar!/"/>\r
</attributes>\r
</classpathentry>\r
+ <classpathentry kind="lib" path="ext/hsqldb-2.2.4.jar"/>\r
<classpathentry kind="output" path="bin"/>\r
</classpath>\r
\r
Supported Databases\r
-------\r
-- [H2](http://h2database.com)\r
-- [MySQL](http://mysql.com)\r
+- [H2 1.3+](http://h2database.com)\r
+- [HSQLDB 2.2+](http://hsqldb.org)\r
- Support for others is planned and should only require creating a simple "dialect" class.\r
\r
License\r
</table>\r
\r
### Supported Databases\r
-[H2](http://h2database.com), [MySQL](http://mysql.com)\r
+[H2 1.3+](http://h2database.com), [HSQLDB 2.2+](http://hsqldb.org)\r
\r
Support for others is planned and should only require creating a simple "dialect" class.\r
\r
<table>\r
<tr><th colspan="2">All Databases</th></tr>\r
<tr><td>java.lang.String</td>\r
-<td>VARCHAR *(length > 0)* or TEXT *(length == 0)*</td></tr>\r
+<td>VARCHAR *(length > 0)* or CLOB *(length == 0)*</td></tr>\r
\r
<tr><td>java.lang.Boolean</td>\r
<td>BIT</td></tr>\r
<td>DOUBLE</td></tr>\r
\r
<tr><td>java.math.BigDecimal</td>\r
-<td>DECIMAL</td></tr>\r
+<td>DECIMAL *(length == 0)* or DECIMAL(length,scale) *(length > 0)*</td></tr>\r
\r
<tr><td>java.sql.Date</td>\r
<td>DATE</td></tr>\r
<td>TIMESTAMP</td></tr>\r
\r
<tr><td>java.lang.Enum.name()<br/>*default type*</td>\r
-<td>VARCHAR *(length > 0)* or TEXT *(length == 0)*<br/>*EnumType.NAME*</td></tr>\r
+<td>VARCHAR *(length > 0)* or CLOB *(length == 0)*<br/>*EnumType.NAME*</td></tr>\r
\r
<tr><td>java.lang.Enum.ordinal()</td>\r
<td>INT<br/>*EnumType.ORDINAL*</td></tr>\r
Your `com.iciql.DbUpgrader` implementation must specify the `IQVersion(version)` annotation\r
\r
### How does it work?\r
-If you choose to use versioning, iciql will maintain a table within your database named *_iq_versions* which is defined as:\r
+If you choose to use versioning, iciql will maintain a table within your database named *iq_versions* which is defined as:\r
\r
- CREATE TABLE _IQ_VERSIONS(SCHEMANAME VARCHAR(255) NOT NULL, TABLENAME VARCHAR(255) NOT NULL, VERSION INT NOT NULL)\r
+ CREATE TABLE IQ_VERSIONS(SCHEMANAME VARCHAR(255) NOT NULL, TABLENAME VARCHAR(255) NOT NULL, VERSION INT NOT NULL)\r
\r
This database table is automatically created if and only if at least one of your model classes specifies a *version* > 0.\r
\r
-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.\r
+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.\r
\r
When an upgrade scenario is identified, the current version and the annotated version information is passed to either:\r
\r
- `DbUpgrader.upgradeDatabase(db, fromVersion, toVersion)`\r
- `DbUpgrader.upgradeTable(db, schema, table, fromVersion, toVersion)`\r
\r
-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.\r
+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.\r
\r
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.\r
\r
**NOTE:**<br/>\r
-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
**%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%*\r
\r
- api change release (API v4)\r
+- DECIMAL(length, scale) support\r
+- 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.\r
+- Boolean now maps to BOOLEAN instead of BIT\r
+- expressions on unmapped fields will throw an IciqlException\r
+- improved exception reporting\r
- moved dialects back to main package\r
-- refined dialect loading for pooled connections\r
-- added a MySQL dialect\r
+- improved automatic dialect determination on pooled connections\r
+- moved create table and create index statement generation into dialects\r
+- added HSQL dialect. HSQL fails 4 unit tests, 2 of which are unimplemented merge, 1 has been filed as a bug in HSQL.\r
+- added MySQL dialect. Untested.\r
+- renamed <b>_ iq_versions</b> table to *iq_versions* since leading _ character is troublesome for some databases.\r
- @IQColumn(allowNull=true) -> @IQColumn(nullable=true)\r
- All columns are assumed NULLABLE unless explicitly set *@IQColumn(nullable = false)*\r
- allow using objects to assign default values<br/>\r
// 2. DatabaseMetaData.getDatabaseProductName()\r
DIALECTS.put("H2", SQLDialectH2.class);\r
DIALECTS.put("MySQL", SQLDialectMySQL.class);\r
+ DIALECTS.put("HSQL Database Engine", SQLDialectHSQL.class);\r
}\r
\r
private Db(Connection conn) {\r
data = conn.getMetaData();\r
databaseName = data.getDatabaseProductName();\r
} catch (SQLException s) {\r
- throw new IciqlException("Failed to retrieve database metadata!", s);\r
+ throw new IciqlException(s, "failed to retrieve database metadata!");\r
}\r
dialect = getDialect(databaseName, conn.getClass().getName());\r
dialect.configureDialect(databaseName, data);\r
Connection conn = JdbcUtils.getConnection(null, url, user, password);\r
return new Db(conn);\r
} catch (SQLException e) {\r
- throw convert(e);\r
+ throw new IciqlException(e);\r
}\r
}\r
\r
try {\r
return new Db(ds.getConnection());\r
} catch (SQLException e) {\r
- throw convert(e);\r
+ throw new IciqlException(e);\r
}\r
}\r
\r
Connection conn = JdbcUtils.getConnection(null, url, prop);\r
return new Db(conn);\r
} catch (SQLException e) {\r
- throw convert(e);\r
+ throw new IciqlException(e);\r
}\r
}\r
\r
- private static Error convert(Exception e) {\r
- return new Error(e);\r
- }\r
-\r
public <T> void insert(T t) {\r
Class<?> clazz = t.getClass();\r
define(clazz).createTableIfRequired(this).insert(this, t, false);\r
}\r
\r
/**\r
- * Merge usually INSERTS if the record does not exist or UPDATES the record\r
- * if it does exist. Not all databases support MERGE and the syntax varies\r
- * with the database.\r
+ * Merge INSERTS if the record does not exist or UPDATES the record if it\r
+ * does exist. Not all databases support MERGE and the syntax varies with\r
+ * the database.\r
* \r
* If the dialect does not support merge an IciqlException will be thrown.\r
* \r
* @param t\r
*/\r
public <T> void merge(T t) {\r
- if (!getDialect().supportsMerge()) {\r
- throw new IciqlException("Merge is not supported by this SQL dialect");\r
- }\r
Class<?> clazz = t.getClass();\r
define(clazz).createTableIfRequired(this).merge(this, t);\r
}\r
}\r
\r
PreparedStatement prepare(String sql, boolean returnGeneratedKeys) {\r
+ IciqlException.checkUnmappedField(sql);\r
try {\r
if (returnGeneratedKeys) {\r
return conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);\r
}\r
return conn.prepareStatement(sql);\r
} catch (SQLException e) {\r
- throw new IciqlException(e);\r
+ throw IciqlException.fromSQL(sql, e);\r
}\r
}\r
\r
/**
* 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)
\r
public static void length(Object column, int length) {\r
checkInDefine();\r
- currentTableDefinition.setMaxLength(column, length);\r
+ currentTableDefinition.setLength(column, length);\r
+ }\r
+ \r
+ public static void scale(Object column, int scale) {\r
+ checkInDefine();\r
+ currentTableDefinition.setScale(column, scale);\r
}\r
\r
public static void tableName(String tableName) {\r
* </tr>\r
* <tr>\r
* <td>java.lang.String</td>\r
- * <td>VARCHAR (length > 0) or TEXT (length == 0)</td>\r
+ * <td>VARCHAR (length > 0) or CLOB (length == 0)</td>\r
* </tr>\r
* <tr>\r
* <td>java.lang.Boolean</td>\r
* </tr>\r
* <tr>\r
* <td>java.math.BigDecimal</td>\r
- * <td>DECIMAL</td>\r
+ * <td>DECIMAL (length == 0)<br/>DECIMAL(length, scale) (length > 0)</td>\r
* </tr>\r
* <tr>\r
* <td>java.sql.Date</td>\r
* </tr>\r
* <tr>\r
* <td>java.lang.Enum.name()</td>\r
- * <td>VARCHAR (length > 0) or TEXT (length == 0)<br/>\r
+ * <td>VARCHAR (length > 0) or CLOB (length == 0)<br/>\r
* EnumType.NAME</td>\r
* </tr>\r
* <tr>\r
boolean autoIncrement() default false;\r
\r
/**\r
- * If larger than zero, it is used during the CREATE TABLE phase. It may\r
- * also be used to prevent database exceptions on INSERT and UPDATE\r
- * statements (see trim).\r
+ * Length is used to define the length of a VARCHAR column or to define\r
+ * the precision of a DECIMAL(precision, scale) expression.\r
+ * <p>\r
+ * If larger than zero, it is used during the CREATE TABLE phase. For\r
+ * string values it may also be used to prevent database exceptions on\r
+ * INSERT and UPDATE statements (see trim).\r
* <p>\r
* Any length set in define() may override this annotation setting if\r
* the model class is not annotated with IQTable. Default: 0.\r
*/\r
int length() default 0;\r
\r
+ /**\r
+ * Scale is used during the CREATE TABLE phase to define the scale of a\r
+ * DECIMAL(precision, scale) expression.\r
+ * <p>\r
+ * Any scale set in define() may override this annotation setting if\r
+ * the model class is not annotated with IQTable. Default: 0.\r
+ */\r
+ int scale() default 0;\r
+\r
/**\r
* If true, iciql will automatically trim the string if it exceeds\r
* length (value.substring(0, length)). Default: false.\r
\r
package com.iciql;\r
\r
+import java.sql.SQLException;\r
import java.text.MessageFormat;\r
+import java.util.regex.Pattern;\r
\r
/**\r
* Iciql wraps all exceptions with this class.\r
*/\r
public class IciqlException extends RuntimeException {\r
\r
+ public static final int CODE_UNMAPPED_FIELD = 1;\r
+ public static final int CODE_DUPLICATE_KEY = 2;\r
+ public static final int CODE_TABLE_NOT_FOUND = 3;\r
+ public static final int CODE_INDEX_ALREADY_EXISTS = 4;\r
+\r
+ private static final String TOKEN_UNMAPPED_FIELD = "\\? (=|\\>|\\<|!=|\\>=|\\<=|LIKE|BETWEEN) \\?"; \r
+\r
private static final long serialVersionUID = 1L;\r
\r
+ private String sql;\r
+\r
+ private int iciqlCode;\r
+\r
+ public IciqlException(Throwable t) {\r
+ super(t.getMessage(), t);\r
+ configureCode(t);\r
+ }\r
+\r
public IciqlException(String message, Object... parameters) {\r
super(parameters.length > 0 ? MessageFormat.format(message, parameters) : message);\r
-\r
}\r
\r
public IciqlException(Throwable t, String message, Object... parameters) {\r
super(parameters.length > 0 ? MessageFormat.format(message, parameters) : message, t);\r
-\r
+ configureCode(t);\r
+ }\r
+ \r
+ public static void checkUnmappedField(String sql) {\r
+ if (Pattern.compile(IciqlException.TOKEN_UNMAPPED_FIELD).matcher(sql).find()) {\r
+ IciqlException e = new IciqlException("unmapped field in statement!");\r
+ e.sql = sql;\r
+ e.iciqlCode = CODE_UNMAPPED_FIELD;\r
+ throw e;\r
+ }\r
+ }\r
+ \r
+ public static IciqlException fromSQL(String sql, Throwable t) {\r
+ if (Pattern.compile(TOKEN_UNMAPPED_FIELD).matcher(sql).find()) {\r
+ IciqlException e = new IciqlException(t, "unmapped field in statement!");\r
+ e.sql = sql;\r
+ e.iciqlCode = CODE_UNMAPPED_FIELD;\r
+ return e;\r
+ } else {\r
+ IciqlException e = new IciqlException(t, t.getMessage());\r
+ e.sql = sql;\r
+ return e;\r
+ }\r
+ }\r
+ \r
+ public void setSQL(String sql) {\r
+ this.sql = sql;\r
}\r
\r
- public IciqlException(Throwable t) {\r
- super(t);\r
+ public String getSQL() {\r
+ return sql;\r
}\r
\r
- public IciqlException(String message, Throwable t) {\r
- super(message, t);\r
+ public int getIciqlCode() {\r
+ return iciqlCode;\r
+ }\r
+ \r
+ private void configureCode(Throwable t) {\r
+ if (t == null) {\r
+ return;\r
+ }\r
+ if (t instanceof SQLException) {\r
+ // http://developer.mimer.com/documentation/html_92/Mimer_SQL_Mobile_DocSet/App_Return_Codes2.html\r
+ SQLException s = (SQLException) t;\r
+ String state = s.getSQLState();\r
+ if ("23505".equals(state)) {\r
+ iciqlCode = CODE_DUPLICATE_KEY;\r
+ } else if ("42501".equals(state)) {\r
+ iciqlCode = CODE_TABLE_NOT_FOUND;\r
+ } else if ("42S02".equals(state)) {\r
+ iciqlCode = CODE_TABLE_NOT_FOUND;\r
+ } else if ("42504".equals(state)) {\r
+ iciqlCode = CODE_INDEX_ALREADY_EXISTS;\r
+ } else if ("42S11".equals(state)) {\r
+ iciqlCode = CODE_INDEX_ALREADY_EXISTS;\r
+ }\r
+ }\r
}\r
+ \r
+ @Override\r
+ public String toString() {\r
+ StringBuilder sb = new StringBuilder();\r
+ sb.append(getClass().getName()); \r
+ String message = getLocalizedMessage();\r
+ if (message != null) {\r
+ sb.append(": ").append(message);\r
+ }\r
+ if (sql != null) {\r
+ sb.append('\n').append(sql);\r
+ }\r
+ return sb.toString();\r
+ }\r
}\r
static {
Map<Class<?>, 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");
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");
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";
long value = rs.getLong(1);\r
return value;\r
} catch (SQLException e) {\r
- throw new IciqlException(e);\r
+ throw IciqlException.fromSQL(stat.getSQL(), e);\r
} finally {\r
JdbcUtils.closeSilently(rs, true);\r
}\r
result.add(item);\r
}\r
} catch (SQLException e) {\r
- throw new IciqlException(e);\r
+ throw IciqlException.fromSQL(stat.getSQL(), e);\r
} finally {\r
JdbcUtils.closeSilently(rs, true);\r
}\r
result.add(row);\r
}\r
} catch (SQLException e) {\r
- throw new IciqlException(e);\r
+ throw IciqlException.fromSQL(stat.getSQL(), e);\r
} finally {\r
JdbcUtils.closeSilently(rs, true);\r
}\r
List<X> result = Utils.newArrayList();\r
try {\r
while (rs.next()) {\r
- try {\r
- X value;\r
- Object o = rs.getObject(1);\r
- // Convert CLOB and BLOB now because we close the resultset\r
- if (Clob.class.isAssignableFrom(o.getClass())) {\r
- value = (X) Utils.convert(o, String.class);\r
- } else if (Blob.class.isAssignableFrom(o.getClass())) {\r
- value = (X) Utils.convert(o, byte[].class);\r
- } else {\r
- value = (X) o;\r
- }\r
- result.add(value);\r
- } catch (IciqlException e) {\r
- throw e;\r
- } catch (Exception e) {\r
- throw new IciqlException(e);\r
+ X value;\r
+ Object o = rs.getObject(1);\r
+ // Convert CLOB and BLOB now because we close the resultset\r
+ if (Clob.class.isAssignableFrom(o.getClass())) {\r
+ value = (X) Utils.convert(o, String.class);\r
+ } else if (Blob.class.isAssignableFrom(o.getClass())) {\r
+ value = (X) Utils.convert(o, byte[].class);\r
+ } else {\r
+ value = (X) o;\r
}\r
+ result.add(value);\r
}\r
- } catch (SQLException e) {\r
- throw new IciqlException(e);\r
+ } catch (Exception e) {\r
+ throw IciqlException.fromSQL(stat.getSQL(), e);\r
} finally {\r
JdbcUtils.closeSilently(rs, true);\r
}\r
\r
/**\r
* This class represents a "between y and z" condition.\r
+ * @param <T>\r
+ * the return type of the query\r
+ * @param <A>\r
+ * the incomplete condition data type\r
*/\r
public class QueryBetween<T, A> {\r
\r
*/
String prepareColumnName(String name);
+ /**
+ * Get the CREATE TABLE statement.
+ *
+ * @param stat
+ * @param def
+ */
+ <T> void prepareCreateTable(SQLStatement stat, TableDefinition<T> def);
+
/**
* Get the CREATE INDEX statement.
*
* 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.
*
* @param index
* the index definition
*/
- <T> void prepareMerge(SQLStatement stat, String schemaName, String tableName, TableDefinition<T> def, Object obj);
+ <T> void prepareMerge(SQLStatement stat, String schemaName, String tableName, TableDefinition<T> def,
+ Object obj);
/**
* Append "LIMIT limit" to the SQL statement.
* @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();
+
}
import java.sql.DatabaseMetaData;\r
import java.sql.SQLException;\r
\r
+import com.iciql.TableDefinition.FieldDefinition;\r
import com.iciql.TableDefinition.IndexDefinition;\r
+import com.iciql.util.StatementBuilder;\r
import com.iciql.util.StringUtils;\r
\r
/**\r
}\r
}\r
\r
- @Override\r
- public boolean supportsMemoryTables() {\r
- return false;\r
+ /**\r
+ * Allows subclasses to change the type of a column for a CREATE statement.\r
+ * \r
+ * @param sqlType\r
+ * @return the SQL type or a preferred alternative\r
+ */\r
+ protected String convertSqlType(String sqlType) {\r
+ return sqlType;\r
}\r
\r
@Override\r
- public boolean supportsMerge() {\r
+ public boolean supportsMemoryTables() {\r
return false;\r
}\r
\r
}\r
\r
@Override\r
- public String prepareCreateIndex(String schemaName, String tableName, IndexDefinition index) {\r
+ public <T> void prepareCreateTable(SQLStatement stat, TableDefinition<T> def) {\r
+ StatementBuilder buff;\r
+ if (def.memoryTable && supportsMemoryTables()) {\r
+ buff = new StatementBuilder("CREATE MEMORY TABLE IF NOT EXISTS ");\r
+ } else {\r
+ buff = new StatementBuilder("CREATE TABLE IF NOT EXISTS ");\r
+ }\r
+\r
+ buff.append(prepareTableName(def.schemaName, def.tableName)).append('(');\r
+\r
+ boolean hasIdentityColumn = false;\r
+ for (FieldDefinition field : def.fields) {\r
+ buff.appendExceptFirst(", ");\r
+ buff.append(prepareColumnName(field.columnName)).append(' ');\r
+ String dataType = field.dataType;\r
+ if (dataType.equals("VARCHAR")) { \r
+ // check to see if we should use VARCHAR or CLOB\r
+ if (field.length <= 0) {\r
+ dataType = "CLOB";\r
+ }\r
+ buff.append(convertSqlType(dataType));\r
+ if (field.length > 0) {\r
+ buff.append('(').append(field.length).append(')');\r
+ }\r
+ } else if (dataType.equals("DECIMAL")) {\r
+ // DECIMAL(precision,scale)\r
+ buff.append(convertSqlType(dataType)); \r
+ if (field.length > 0) {\r
+ buff.append('(').append(field.length);\r
+ if (field.scale > 0) {\r
+ buff.append(',').append(field.scale);\r
+ }\r
+ buff.append(')');\r
+ }\r
+ } else {\r
+ // other\r
+ buff.append(convertSqlType(dataType));\r
+ }\r
+\r
+ hasIdentityColumn |= prepareColumnDefinition(buff, field.isAutoIncrement, field.isPrimaryKey);\r
+\r
+ if (!field.nullable) {\r
+ buff.append(" NOT NULL");\r
+ }\r
+\r
+ // default values\r
+ if (!field.isAutoIncrement && !field.isPrimaryKey) {\r
+ String dv = field.defaultValue;\r
+ if (!StringUtils.isNullOrEmpty(dv)) {\r
+ if (ModelUtils.isProperlyFormattedDefaultValue(dv)\r
+ && ModelUtils.isValidDefaultValue(field.field.getType(), dv)) {\r
+ buff.append(" DEFAULT " + dv);\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ // if table does not have identity column then specify primary key\r
+ if (!hasIdentityColumn) {\r
+ if (def.primaryKeyColumnNames != null && def.primaryKeyColumnNames.size() > 0) {\r
+ buff.append(", PRIMARY KEY(");\r
+ buff.resetCount();\r
+ for (String n : def.primaryKeyColumnNames) {\r
+ buff.appendExceptFirst(", ");\r
+ buff.append(prepareColumnName(n));\r
+ }\r
+ buff.append(')');\r
+ }\r
+ }\r
+ buff.append(')');\r
+ stat.setSQL(buff.toString());\r
+ }\r
+\r
+ protected boolean prepareColumnDefinition(StatementBuilder buff, boolean isAutoIncrement,\r
+ boolean isPrimaryKey) {\r
+ boolean isIdentity = false;\r
+ if (isAutoIncrement && isPrimaryKey) {\r
+ buff.append(" IDENTITY");\r
+ isIdentity = true;\r
+ } else if (isAutoIncrement) {\r
+ buff.append(" AUTO_INCREMENT");\r
+ }\r
+ return isIdentity;\r
+ }\r
+\r
+ @Override\r
+ public void prepareCreateIndex(SQLStatement stat, String schemaName, String tableName,\r
+ IndexDefinition index) {\r
throw new IciqlException("Dialect does not support index creation!");\r
}\r
\r
@Override\r
- public <T> void prepareMerge(SQLStatement stat, String schemaName, String tableName, TableDefinition<T> def, Object obj) {\r
+ public <T> void prepareMerge(SQLStatement stat, String schemaName, String tableName,\r
+ TableDefinition<T> def, Object obj) {\r
throw new IciqlException("Dialect does not support merge statements!");\r
}\r
\r
}\r
\r
@Override\r
- public boolean supportsMerge() {\r
- return true;\r
- }\r
-\r
- @Override\r
- public String prepareCreateIndex(String schema, String table, IndexDefinition index) {\r
+ public void prepareCreateIndex(SQLStatement stat, String schema, String table, IndexDefinition index) {\r
StatementBuilder buff = new StatementBuilder();\r
buff.append("CREATE ");\r
switch (index.type) {\r
buff.append(col);\r
}\r
buff.append(")");\r
- return buff.toString();\r
+ stat.setSQL(buff.toString()); \r
}\r
\r
@Override\r
--- /dev/null
+/*\r
+ * Copyright 2011 James Moger.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package com.iciql;\r
+\r
+import com.iciql.TableDefinition.IndexDefinition;\r
+import com.iciql.util.StatementBuilder;\r
+\r
+/**\r
+ * HyperSQL database dialect.\r
+ */\r
+public class SQLDialectHSQL extends SQLDialectDefault {\r
+ \r
+ @Override\r
+ public boolean supportsMemoryTables() {\r
+ return true;\r
+ }\r
+ \r
+ @Override\r
+ protected boolean prepareColumnDefinition(StatementBuilder buff, boolean isAutoIncrement, boolean isPrimaryKey) {\r
+ boolean isIdentity = false;\r
+ if (isAutoIncrement && isPrimaryKey) {\r
+ buff.append(" IDENTITY");\r
+ isIdentity = true;\r
+ }\r
+ return isIdentity;\r
+ }\r
+\r
+ @Override\r
+ public void prepareCreateIndex(SQLStatement stat, String schema, String table, IndexDefinition index) {\r
+ StatementBuilder buff = new StatementBuilder();\r
+ buff.append("CREATE ");\r
+ switch (index.type) {\r
+ case STANDARD:\r
+ break;\r
+ case UNIQUE:\r
+ buff.append("UNIQUE ");\r
+ break;\r
+ case UNIQUE_HASH:\r
+ buff.append("UNIQUE ");\r
+ break;\r
+ }\r
+ buff.append("INDEX ");\r
+ buff.append(index.indexName);\r
+ buff.append(" ON ");\r
+ buff.append(table);\r
+ buff.append("(");\r
+ for (String col : index.columnNames) {\r
+ buff.appendExceptFirst(", ");\r
+ buff.append(col);\r
+ }\r
+ buff.append(")");\r
+ stat.setSQL(buff.toString());\r
+ }\r
+}
\ No newline at end of file
\r
package com.iciql;\r
\r
-import com.iciql.TableDefinition.FieldDefinition;\r
import com.iciql.TableDefinition.IndexDefinition;\r
import com.iciql.util.StatementBuilder;\r
\r
public class SQLDialectMySQL extends SQLDialectDefault {\r
\r
@Override\r
- public boolean supportsMemoryTables() {\r
- return false;\r
+ protected String convertSqlType(String sqlType) {\r
+ if (sqlType.equals("CLOB")) {\r
+ return "TEXT";\r
+ }\r
+ return sqlType;\r
}\r
-\r
+ \r
@Override\r
- public boolean supportsMerge() {\r
- return true;\r
+ public boolean supportsMemoryTables() {\r
+ return false;\r
}\r
\r
@Override\r
}\r
\r
@Override\r
- public String prepareCreateIndex(String schema, String table, IndexDefinition index) {\r
+ protected boolean prepareColumnDefinition(StatementBuilder buff, boolean isAutoIncrement, boolean isPrimaryKey) {\r
+ if (isAutoIncrement) {\r
+ buff.append(" AUTO_INCREMENT");\r
+ }\r
+ return false;\r
+ }\r
+ \r
+ @Override\r
+ public void prepareCreateIndex(SQLStatement stat, String schema, String table, IndexDefinition index) {\r
StatementBuilder buff = new StatementBuilder();\r
buff.append("CREATE ");\r
switch (index.type) {\r
buff.append("USING HASH");\r
break;\r
}\r
- return buff.toString().trim();\r
- }\r
- \r
- @Override\r
- public <T> void prepareMerge(SQLStatement stat, String schemaName, String tableName, TableDefinition<T> def, Object obj) { \r
- StatementBuilder buff = new StatementBuilder("REPLACE INTO ");\r
- buff.append(prepareTableName(schemaName, tableName)).append('(');\r
- for (FieldDefinition field : def.fields) {\r
- buff.appendExceptFirst(", ");\r
- buff.append(prepareColumnName(field.columnName));\r
- }\r
- buff.append(") VALUES(");\r
- buff.resetCount();\r
- for (FieldDefinition field : def.fields) {\r
- buff.appendExceptFirst(", ");\r
- buff.append('?');\r
- Object value = def.getValue(obj, field);\r
- stat.addParameter(value);\r
- }\r
- buff.append(')');\r
- stat.setSQL(buff.toString());\r
+ stat.setSQL(buff.toString().trim());\r
}\r
}
\ No newline at end of file
public SQLStatement appendTable(String schema, String table) {\r
return appendSQL(db.getDialect().prepareTableName(schema, table));\r
}\r
- \r
+\r
public SQLStatement appendColumn(String column) {\r
return appendSQL(db.getDialect().prepareColumnName(column));\r
}\r
}\r
return sql;\r
}\r
- \r
+\r
public SQLStatement addParameter(Object o) {\r
params.add(o);\r
return this;\r
try {\r
return prepare(false).executeQuery();\r
} catch (SQLException e) {\r
- throw new IciqlException(e);\r
+ throw IciqlException.fromSQL(getSQL(), e);\r
}\r
}\r
\r
ps = prepare(false);\r
return ps.executeUpdate();\r
} catch (SQLException e) {\r
- throw new IciqlException(e);\r
+ throw IciqlException.fromSQL(getSQL(), e);\r
} finally {\r
JdbcUtils.closeSilently(ps);\r
}\r
JdbcUtils.closeSilently(rs);\r
return identity;\r
} catch (SQLException e) {\r
- throw new IciqlException(e);\r
+ throw IciqlException.fromSQL(getSQL(), e);\r
} finally {\r
JdbcUtils.closeSilently(ps);\r
}\r
}\r
\r
- private static void setValue(PreparedStatement prep, int parameterIndex, Object x) {\r
+ private void setValue(PreparedStatement prep, int parameterIndex, Object x) {\r
try {\r
prep.setObject(parameterIndex, x);\r
} catch (SQLException e) {\r
- throw new IciqlException(e);\r
+ IciqlException ix = new IciqlException(e, "error setting parameter {0} as {1}", parameterIndex, x.getClass().getSimpleName());\r
+ ix.setSQL(getSQL());\r
+ throw ix;\r
}\r
}\r
\r
String columnName;\r
Field field;\r
String dataType;\r
- int maxLength;\r
+ int length;\r
+ int scale;\r
boolean isPrimaryKey;\r
boolean isAutoIncrement;\r
- boolean trimString;\r
+ boolean trim;\r
boolean nullable;\r
String defaultValue;\r
EnumType enumType;\r
String schemaName;\r
String tableName;\r
int tableVersion;\r
- private boolean createTableIfRequired = true;\r
+ List<String> primaryKeyColumnNames;\r
+ boolean memoryTable;\r
+ \r
+ private boolean createTableIfRequired = true; \r
private Class<T> clazz;\r
private IdentityHashMap<Object, FieldDefinition> fieldMap = Utils.newIdentityHashMap();\r
-\r
- private List<String> primaryKeyColumnNames;\r
private ArrayList<IndexDefinition> indexes = Utils.newArrayList();\r
- private boolean memoryTable;\r
+ \r
\r
TableDefinition(Class<T> clazz) {\r
this.clazz = clazz;\r
}\r
}\r
\r
- public void setMaxLength(Object column, int maxLength) {\r
+ public void setLength(Object column, int length) {\r
+ FieldDefinition def = fieldMap.get(column);\r
+ if (def != null) {\r
+ def.length = length;\r
+ }\r
+ }\r
+ \r
+ public void setScale(Object column, int scale) {\r
FieldDefinition def = fieldMap.get(column);\r
if (def != null) {\r
- def.maxLength = maxLength;\r
+ def.scale = scale;\r
}\r
}\r
\r
String columnName = f.getName();\r
boolean isAutoIncrement = false;\r
boolean isPrimaryKey = false;\r
- int maxLength = 0;\r
- boolean trimString = false;\r
+ int length = 0;\r
+ int scale = 0;\r
+ boolean trim = false;\r
boolean nullable = true;\r
EnumType enumType = null;\r
String defaultValue = "";\r
}\r
isAutoIncrement = col.autoIncrement();\r
isPrimaryKey = col.primaryKey();\r
- maxLength = col.length();\r
- trimString = col.trim();\r
+ length = col.length();\r
+ scale = col.scale();\r
+ trim = col.trim();\r
nullable = col.nullable();\r
\r
// try using default object\r
}\r
}\r
} catch (IllegalAccessException e) {\r
- throw new IciqlException(e, "Failed to get default object for {0}", columnName);\r
+ throw new IciqlException(e, "failed to get default object for {0}", columnName);\r
}\r
\r
// annotation overrides\r
fieldDef.columnName = columnName;\r
fieldDef.isAutoIncrement = isAutoIncrement;\r
fieldDef.isPrimaryKey = isPrimaryKey;\r
- fieldDef.maxLength = maxLength;\r
- fieldDef.trimString = trimString;\r
+ fieldDef.length = length;\r
+ fieldDef.scale = scale;\r
+ fieldDef.trim = trim;\r
fieldDef.nullable = nullable;\r
fieldDef.defaultValue = defaultValue;\r
fieldDef.enumType = enumType;\r
Enum<?> iqenum = (Enum<?>) value;\r
switch (field.enumType) {\r
case NAME:\r
- if (field.trimString && field.maxLength > 0) {\r
- if (iqenum.name().length() > field.maxLength) {\r
- return iqenum.name().substring(0, field.maxLength);\r
+ if (field.trim && field.length > 0) {\r
+ if (iqenum.name().length() > field.length) {\r
+ return iqenum.name().substring(0, field.length);\r
}\r
}\r
return iqenum.name();\r
return enumid.enumId();\r
}\r
}\r
- if (field.trimString && field.maxLength > 0) {\r
+ \r
+ if (field.trim && field.length > 0) {\r
if (value instanceof String) {\r
// clip strings\r
String s = (String) value;\r
- if (s.length() > field.maxLength) {\r
- return s.substring(0, field.maxLength);\r
+ if (s.length() > field.length) {\r
+ return s.substring(0, field.length);\r
}\r
return s;\r
}\r
db.upgradeTable(this);\r
return this;\r
}\r
- SQLDialect dialect = db.getDialect();\r
SQLStatement stat = new SQLStatement(db);\r
- StatementBuilder buff;\r
- if (memoryTable && dialect.supportsMemoryTables()) {\r
- buff = new StatementBuilder("CREATE MEMORY TABLE IF NOT EXISTS ");\r
- } else {\r
- buff = new StatementBuilder("CREATE TABLE IF NOT EXISTS ");\r
- }\r
-\r
- buff.append(dialect.prepareTableName(schemaName, tableName)).append('(');\r
-\r
- for (FieldDefinition field : fields) {\r
- buff.appendExceptFirst(", ");\r
- buff.append(dialect.prepareColumnName(field.columnName)).append(' ').append(field.dataType);\r
- if (field.maxLength > 0) {\r
- buff.append('(').append(field.maxLength).append(')');\r
- }\r
-\r
- if (field.isAutoIncrement) {\r
- buff.append(" AUTO_INCREMENT");\r
- }\r
-\r
- if (!field.nullable) {\r
- buff.append(" NOT NULL");\r
- }\r
-\r
- // default values\r
- if (!field.isAutoIncrement && !field.isPrimaryKey) {\r
- String dv = field.defaultValue;\r
- if (!StringUtils.isNullOrEmpty(dv)) {\r
- if (ModelUtils.isProperlyFormattedDefaultValue(dv)\r
- && ModelUtils.isValidDefaultValue(field.field.getType(), dv)) {\r
- buff.append(" DEFAULT " + dv);\r
- }\r
- }\r
- }\r
- }\r
-\r
- // primary key\r
- if (primaryKeyColumnNames != null && primaryKeyColumnNames.size() > 0) {\r
- buff.append(", PRIMARY KEY(");\r
- buff.resetCount();\r
- for (String n : primaryKeyColumnNames) {\r
- buff.appendExceptFirst(", ");\r
- buff.append(n);\r
- }\r
- buff.append(')');\r
- }\r
- buff.append(')');\r
- stat.setSQL(buff.toString());\r
+ db.getDialect().prepareCreateTable(stat, this); \r
StatementLogger.create(stat.getSQL());\r
stat.executeUpdate();\r
\r
// create indexes\r
for (IndexDefinition index : indexes) {\r
- String sql = db.getDialect().prepareCreateIndex(schemaName, tableName, index);\r
- stat.setSQL(sql);\r
+ stat = new SQLStatement(db);\r
+ db.getDialect().prepareCreateIndex(stat, schemaName, tableName, index); \r
StatementLogger.create(stat.getSQL());\r
- stat.executeUpdate();\r
+ try {\r
+ stat.executeUpdate();\r
+ } catch (IciqlException e) {\r
+ if (e.getIciqlCode() != IciqlException.CODE_INDEX_ALREADY_EXISTS) {\r
+ throw e;\r
+ }\r
+ }\r
}\r
\r
// tables are created using IF NOT EXISTS\r
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);
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;
// 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())));
}
}
}
- void addEnum(String parameter, Enum value) {
+ void addEnum(String parameter, Enum<?> value) {
appendExceptFirst(", ");
if (!StringUtils.isNullOrEmpty(parameter)) {
append(parameter);
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;
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));
public static void compiletime() {\r
downloadFromApache(MavenObject.H2, BuildType.RUNTIME);\r
downloadFromApache(MavenObject.H2, BuildType.COMPILETIME);\r
+ downloadFromApache(MavenObject.HSQLDB, BuildType.RUNTIME);\r
downloadFromApache(MavenObject.JCOMMANDER, BuildType.RUNTIME);\r
downloadFromApache(MavenObject.JCOMMANDER, BuildType.COMPILETIME);\r
downloadFromApache(MavenObject.MARKDOWNPAPERS, BuildType.RUNTIME);\r
"4bac13427caeb32ef6e93b70101e61f370c7b5e2", "6bb165156a0831879fa7797df6e18bdcd4421f2d",\r
"446d3f58c44992534cb54f67134532d95961904a");\r
\r
+ public static final MavenObject HSQLDB = new MavenObject("org/hsqldb", "hsqldb", "2.2.4",\r
+ "6a6e040b07f5ee409fc825f1c5e5b574b1fa1428", "",\r
+ "");\r
+\r
public static final MavenObject JUNIT = new MavenObject("junit", "junit", "4.8.2",\r
"c94f54227b08100974c36170dcb53329435fe5ad", "", "");\r
\r
}\r
}\r
}\r
- throw new IciqlException("Missing default constructor? Exception trying to create " + clazz.getName() + ": " + e, e);\r
+ throw new IciqlException(e, "Missing default constructor? Exception trying to instantiate {0}: {1}", clazz.getName(), e.getMessage());\r
}\r
}\r
};\r
}\r
}\r
}\r
- throw new IciqlException("Exception trying to create " + clazz.getName() + ": " + e, e);\r
+ throw new IciqlException(e, "Missing default constructor?! Exception trying to instantiate {0}: {1}", clazz.getName(), e.getMessage());\r
}\r
}\r
\r
Reader r = c.getCharacterStream();\r
return readStringAndClose(r, -1);\r
} catch (Exception e) {\r
- throw new IciqlException("Error converting CLOB to String: " + e.toString(), e);\r
+ throw new IciqlException(e, "error converting CLOB to String: ", e.toString());\r
}\r
}\r
return o.toString();\r
}\r
\r
- // convert from number to boolean\r
if (Boolean.class.isAssignableFrom(targetType) || boolean.class.isAssignableFrom(targetType)) {\r
+ // convert from number to boolean\r
if (Number.class.isAssignableFrom(currentType)) {\r
Number n = (Number) o;\r
return n.intValue() > 0;\r
}\r
+ // convert from string to boolean\r
+ if (String.class.isAssignableFrom(currentType)) {\r
+ String s = o.toString().toLowerCase();\r
+ float f = 0f;\r
+ try {\r
+ f = Float.parseFloat(s);\r
+ } catch (Exception e) { \r
+ }\r
+ return f > 0 || s.equals("true") || s.equals("yes");\r
+ }\r
}\r
\r
// convert from boolean to number\r
InputStream is = b.getBinaryStream();\r
return readBlobAndClose(is, -1);\r
} catch (Exception e) {\r
- throw new IciqlException("Error converting BLOB to byte[]: " + e.toString(), e);\r
+ throw new IciqlException(e, "error converting BLOB to byte[]: ", e.toString());\r
}\r
}\r
}\r
Reader r = c.getCharacterStream();\r
name = readStringAndClose(r, -1);\r
} catch (Exception e) {\r
- throw new IciqlException("Error converting CLOB to String: " + e.toString(), e);\r
+ throw new IciqlException(e, "error converting CLOB to String: ", e.toString());\r
}\r
\r
// find name match\r
+++ /dev/null
-/*\r
- * Copyright 2011 James Moger.\r
- *\r
- * Licensed under the Apache License, Version 2.0 (the "License");\r
- * you may not use this file except in compliance with the License.\r
- * You may obtain a copy of the License at\r
- *\r
- * http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- */\r
-\r
-package com.iciql.test;\r
-\r
-import org.junit.Test;\r
-\r
-import com.iciql.Db;\r
-import com.iciql.test.models.Product;\r
-import com.iciql.util.StatementLogger;\r
-\r
-/**\r
- * Tests the statement logger. \r
- */\r
-public class StatementLoggerTest {\r
-\r
- @Test\r
- public void testStatementLogger() {\r
- StatementLogger.activateConsoleLogger();\r
- Db db = Db.open("jdbc:h2:mem:", "sa", "sa");\r
- db.insertAll(Product.getList());\r
- db.close();\r
- StatementLogger.logStats();\r
- StatementLogger.deactivateConsoleLogger();\r
- }\r
-}\r