diff options
-rw-r--r-- | api/v8.xml | 84 | ||||
-rw-r--r-- | docs/01_model_classes.mkd | 2 | ||||
-rw-r--r-- | docs/04_examples.mkd | 7 | ||||
-rw-r--r-- | docs/05_releases.mkd | 26 | ||||
-rw-r--r-- | docs/06_jaqu_comparison.mkd | 1 | ||||
-rw-r--r-- | src/com/iciql/Db.java | 17 | ||||
-rw-r--r-- | src/com/iciql/Iciql.java | 11 | ||||
-rw-r--r-- | src/com/iciql/Query.java | 18 | ||||
-rw-r--r-- | src/com/iciql/QueryJoin.java | 3 | ||||
-rw-r--r-- | src/com/iciql/QueryWhere.java | 5 | ||||
-rw-r--r-- | src/com/iciql/TableDefinition.java | 68 | ||||
-rw-r--r-- | tests/com/iciql/test/PrimitivesTest.java | 17 | ||||
-rw-r--r-- | tests/com/iciql/test/SamplesTest.java | 5 | ||||
-rw-r--r-- | tests/com/iciql/test/models/MultipleBoolsModel.java | 40 |
14 files changed, 289 insertions, 15 deletions
@@ -43,7 +43,7 @@ type="java.lang.String" transient="false" volatile="false" - value=""0.7.3"" + value=""0.7.4-SNAPSHOT"" static="true" final="true" deprecated="not deprecated" @@ -54,7 +54,7 @@ type="java.lang.String" transient="false" volatile="false" - value=""2011-12-06"" + value=""PENDING"" static="true" final="true" deprecated="not deprecated" @@ -70,6 +70,17 @@ deprecated="not deprecated" visibility="public" >
+<method name="activateConsoleLogger" + return="void" + abstract="false" + native="false" + synchronized="false" + static="true" + final="false" + deprecated="not deprecated" + visibility="public" +>
+</method>
<method name="buildObjects" return="java.util.List<T>" abstract="false" @@ -96,6 +107,17 @@ visibility="public" >
</method>
+<method name="deactivateConsoleLogger" + return="void" + abstract="false" + native="false" + synchronized="false" + static="true" + final="false" + deprecated="not deprecated" + visibility="public" +>
+</method>
<method name="delete" return="int" abstract="false" @@ -153,6 +175,23 @@ </parameter>
</method>
<method name="executeQuery" + return="java.util.List<T>" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +>
+<parameter name="modelClass" type="java.lang.Class<? extends T>">
+</parameter>
+<parameter name="sql" type="java.lang.String">
+</parameter>
+<parameter name="args" type="java.util.List<?>">
+</parameter>
+</method>
+<method name="executeQuery" return="java.sql.ResultSet" abstract="false" native="false" @@ -167,6 +206,21 @@ <parameter name="args" type="java.lang.Object...">
</parameter>
</method>
+<method name="executeQuery" + return="java.sql.ResultSet" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +>
+<parameter name="sql" type="java.lang.String">
+</parameter>
+<parameter name="args" type="java.util.List<?>">
+</parameter>
+</method>
<method name="executeUpdate" return="int" abstract="false" @@ -1157,6 +1211,17 @@ <implements name="java.lang.annotation.Annotation">
</implements>
</class>
+<class name="Iciql.IQFunction"
+ extends="java.lang.Object"
+ abstract="true" + static="true" + final="false" + deprecated="not deprecated" + visibility="public" +>
+<implements name="java.lang.annotation.Annotation">
+</implements>
+</class>
<class name="Iciql.IQIgnore"
extends="java.lang.Object"
abstract="true" @@ -2158,6 +2223,21 @@ </parameter>
</method>
<method name="where" + return="com.iciql.QueryWhere<T>" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +>
+<parameter name="fragment" type="java.lang.String">
+</parameter>
+<parameter name="args" type="java.util.List<?>">
+</parameter>
+</method>
+<method name="where" return="com.iciql.QueryCondition<T, java.lang.Long>" abstract="false" native="false" diff --git a/docs/01_model_classes.mkd b/docs/01_model_classes.mkd index 5babdcc..f534660 100644 --- a/docs/01_model_classes.mkd +++ b/docs/01_model_classes.mkd @@ -23,7 +23,7 @@ can be used for all iciql expressions <td>VARCHAR *(length > 0)* or CLOB *(length == 0)*</td></tr>
<tr><td>java.lang.Boolean</td><td>boolean</td>
-<td>BOOLEAN</td></tr>
+<td>BOOLEAN<br/><i>can only **declare and explicitly reference** one <u>primitive boolean</u> per model<br/>multiple primitives are allowed if not using where/set/on/and/or/groupBy/orderBy(boolean)</i></td></tr>
<tr><td>java.lang.Byte</td><td>byte</td>
<td>TINYINT</td></tr>
diff --git a/docs/04_examples.mkd b/docs/04_examples.mkd index 480e29d..a1273be 100644 --- a/docs/04_examples.mkd +++ b/docs/04_examples.mkd @@ -8,6 +8,13 @@ List<Product> allProducts = db.from(p).select(); Customer c = new Customer();
List<Customer> waCustomers = db.from(c). where(c.region).is("WA").select();
+public static class ProductPrice {
+ public String productName;
+ public String category;
+ @IQColumn(name = "unitPrice")
+ public Double price;
+}
+
// select with generation of new anonymous inner class
List<ProductPrice> productPrices =
db.from(p).
diff --git a/docs/05_releases.mkd b/docs/05_releases.mkd index 9a7d13e..d4aa8f2 100644 --- a/docs/05_releases.mkd +++ b/docs/05_releases.mkd @@ -6,9 +6,29 @@ **%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%*
-- Added list alternatives to the varargs methods because it was too easy to forget list.toArray() for the varargs methods
-List<T> Db.executeQuery(Class<? extends T> modelClass, String sql, List<?> args)
-ResultSet executeQuery(String sql, List<?> args)
+- Disallow **declaring and explicitly referencing** multiple primitive booleans in a single model.<br/>A runtime exception will be thrown if an attempt to use where/set/on/and/or/groupBy/orderBy(boolean) and your model has multiple mapped primitive boolean fields.
+- Added list alternatives to the varargs methods because it was too easy to forget list.toArray()<br/>
+*Db.executeQuery(Class<? extends T> modelClass, String sql, List<?> args)*<br/>
+*Db.executeQuery(String sql, List<?> args)*<br/>
+*Query.where(String fragment, List<?> args)*<br/>
+- Fixed inherited JaQu bug related to model classes and wildcard queries (select *).<p/>
+Iciql maps resultset columns by the index of the model class field from a list. This assumes that *all* columns in the resultset have a corresponding model field definition. This works fine for most queries because iciql explicitly selects columns from the table (*select alpha, beta...*) when you execute *select()*. The problem is when iciql issues a join or a custom wildcard query and your model does not represent all columns in the resultset: columns and fields fail to correctly line-up.<p/>
+The fix for this (building a column index from the resultset by column name lookup) breaks selecting into *some* anonymous inner classes. At issue is that the inner class field names must now match the column names or the fields must be explicitly annotated with the column names.<p/>**Example** (notice *IQColumn* annotation)<br/>
+%BEGINCODE%
+public static class ProductPrice {
+ public String productName;
+ public String category;
+ @IQColumn(name = "unitPrice")
+ public Double price;
+}
+
+db....select(new ProductPrice() {{
+ productName = p.productName;
+ category = p.category;
+ // or unitPrice = p.unitPrice;
+ price = p.unitPrice;
+}}
+%ENDCODE%
### Older Releases
diff --git a/docs/06_jaqu_comparison.mkd b/docs/06_jaqu_comparison.mkd index 33ca975..9c0f56c 100644 --- a/docs/06_jaqu_comparison.mkd +++ b/docs/06_jaqu_comparison.mkd @@ -10,6 +10,7 @@ This is an overview of the fundamental differences between the original JaQu pro <tr><td>databases</td><td>H2, HSQL, Derby, MySQL, and PostreSQL</td><td>H2 only</td></tr>
<tr><td>logging</td><td>console, SLF4J, or custom logging</td><td>console logging</td></tr>
<tr><td>exceptions</td><td>always includes generated statement in exception, when available</td><td>--</td></tr>
+<tr><td>column mappings</td><td>result sets built by column name</td><td>result sets built by field index<br/>this can fail for dynamic queries or joins</td></tr>
<tr><th colspan="3">syntax and api</th></tr>
<tr><td>dynamic queries</td><td>methods and where clauses for dynamic queries that build iciql objects</td><td>--</td></tr>
<tr><td>DROP</td><td>syntax to drop a table</td><td></td></tr>
diff --git a/src/com/iciql/Db.java b/src/com/iciql/Db.java index e05ec56..4dc0b5e 100644 --- a/src/com/iciql/Db.java +++ b/src/com/iciql/Db.java @@ -138,6 +138,20 @@ public class Db { throw new IciqlException(e);
}
}
+
+ /**
+ * Convenience function to avoid import statements in application code.
+ */
+ public static void activateConsoleLogger() {
+ IciqlLogger.activateConsoleLogger();
+ }
+
+ /**
+ * Convenience function to avoid import statements in application code.
+ */
+ public static void deactivateConsoleLogger() {
+ IciqlLogger.deactivateConsoleLogger();
+ }
public static Db open(String url) {
try {
@@ -273,9 +287,10 @@ public class Db { List<T> result = new ArrayList<T>();
TableDefinition<T> def = (TableDefinition<T>) define(modelClass);
try {
+ int [] columns = def.mapColumns(rs);
while (rs.next()) {
T item = Utils.newObject(modelClass);
- def.readRow(item, rs);
+ def.readRow(item, rs, columns);
result.add(item);
}
} catch (SQLException e) {
diff --git a/src/com/iciql/Iciql.java b/src/com/iciql/Iciql.java index eaa256a..b13baf4 100644 --- a/src/com/iciql/Iciql.java +++ b/src/com/iciql/Iciql.java @@ -500,6 +500,17 @@ public interface Iciql { }
/**
+ * Annotation to define a field that should contain the result a function.
+ * This annotation ensures that functions mapped in anonymous inner classes
+ * can still be referenced in the ResultSet after the switch to dynamic
+ * column-name mapping from fixed position column mapping.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface IQFunction{
+ }
+
+ /**
* Annotation to define an ignored field.
*/
@Retention(RetentionPolicy.RUNTIME)
diff --git a/src/com/iciql/Query.java b/src/com/iciql/Query.java index b43f774..33b6dfa 100644 --- a/src/com/iciql/Query.java +++ b/src/com/iciql/Query.java @@ -123,9 +123,10 @@ public class Query<T> { appendFromWhere(stat);
ResultSet rs = stat.executeQuery();
try {
+ int[] columns = def.mapColumns(rs);
while (rs.next()) {
T item = from.newObject();
- from.getAliasDefinition().readRow(item, rs);
+ def.readRow(item, rs, columns);
result.add(item);
}
} catch (SQLException e) {
@@ -150,6 +151,7 @@ public class Query<T> { }
public UpdateColumnSet<T, Boolean> set(boolean field) {
+ from.getAliasDefinition().checkMultipleBooleans();
return setPrimitive(field);
}
@@ -269,9 +271,10 @@ public class Query<T> { appendFromWhere(stat);
ResultSet rs = stat.executeQuery();
try {
+ int[] columns = def.mapColumns(rs);
while (rs.next()) {
X row = Utils.newObject(clazz);
- def.readRow(row, rs);
+ def.readRow(row, rs, columns);
result.add(row);
}
} catch (SQLException e) {
@@ -328,6 +331,7 @@ public class Query<T> { * @return a query condition to continue building the condition
*/
public QueryCondition<T, Boolean> where(boolean x) {
+ from.getAliasDefinition().checkMultipleBooleans();
return wherePrimitive(x);
}
@@ -449,6 +453,10 @@ public class Query<T> { return new QueryWhere<T>(this);
}
+ public QueryWhere<T> where(String fragment, List<?> args) {
+ return this.where(fragment, args.toArray());
+ }
+
public QueryWhere<T> where(String fragment, Object... args) {
conditions.add(new RuntimeToken(fragment, args));
return new QueryWhere<T>(this);
@@ -477,6 +485,7 @@ public class Query<T> { }
public Query<T> orderBy(boolean field) {
+ from.getAliasDefinition().checkMultipleBooleans();
return orderByPrimitive(field);
}
@@ -541,6 +550,7 @@ public class Query<T> { }
public Query<T> groupBy(boolean field) {
+ from.getAliasDefinition().checkMultipleBooleans();
return groupByPrimitive(field);
}
@@ -737,6 +747,10 @@ public class Query<T> { return db;
}
+ SelectTable<T> getFrom() {
+ return from;
+ }
+
boolean isJoin() {
return !joins.isEmpty();
}
diff --git a/src/com/iciql/QueryJoin.java b/src/com/iciql/QueryJoin.java index 652d937..6d0484e 100644 --- a/src/com/iciql/QueryJoin.java +++ b/src/com/iciql/QueryJoin.java @@ -32,6 +32,7 @@ public class QueryJoin<T> { }
public QueryJoinCondition<T, Boolean> on(boolean x) {
+ query.getFrom().getAliasDefinition().checkMultipleBooleans();
return addPrimitive(x);
}
@@ -59,7 +60,7 @@ public class QueryJoin<T> { return addPrimitive(x);
}
- private <A> QueryJoinCondition<T, A> addPrimitive(A x) {
+ private <A> QueryJoinCondition<T, A> addPrimitive(A x) {
A alias = query.getPrimitiveAliasByValue(x);
if (alias == null) {
// this will result in an unmapped field exception
diff --git a/src/com/iciql/QueryWhere.java b/src/com/iciql/QueryWhere.java index c1e3b03..df93439 100644 --- a/src/com/iciql/QueryWhere.java +++ b/src/com/iciql/QueryWhere.java @@ -42,6 +42,7 @@ public class QueryWhere<T> { * @return a query condition to continue building the condition
*/
public QueryCondition<T, Boolean> and(boolean x) {
+ query.getFrom().getAliasDefinition().checkMultipleBooleans();
return addPrimitive(ConditionAndOr.AND, x);
}
@@ -111,7 +112,7 @@ public class QueryWhere<T> { return addPrimitive(ConditionAndOr.AND, x);
}
- private <A> QueryCondition<T, A> addPrimitive(ConditionAndOr condition, A x) {
+ private <A> QueryCondition<T, A> addPrimitive(ConditionAndOr condition, A x) {
query.addConditionToken(condition);
A alias = query.getPrimitiveAliasByValue(x);
if (alias == null) {
@@ -141,6 +142,7 @@ public class QueryWhere<T> { * @return a query condition to continue building the condition
*/
public QueryCondition<T, Boolean> or(boolean x) {
+ query.getFrom().getAliasDefinition().checkMultipleBooleans();
return addPrimitive(ConditionAndOr.OR, x);
}
@@ -273,6 +275,7 @@ public class QueryWhere<T> { * @return the query
*/
public QueryWhere<T> orderBy(boolean field) {
+ query.getFrom().getAliasDefinition().checkMultipleBooleans();
return orderByPrimitive(field);
}
diff --git a/src/com/iciql/TableDefinition.java b/src/com/iciql/TableDefinition.java index 97060f9..1147238 100644 --- a/src/com/iciql/TableDefinition.java +++ b/src/com/iciql/TableDefinition.java @@ -30,6 +30,7 @@ import com.iciql.Iciql.EnumId; import com.iciql.Iciql.EnumType;
import com.iciql.Iciql.IQColumn;
import com.iciql.Iciql.IQEnum;
+import com.iciql.Iciql.IQFunction;
import com.iciql.Iciql.IQIgnore;
import com.iciql.Iciql.IQIndex;
import com.iciql.Iciql.IQIndexes;
@@ -73,6 +74,7 @@ public class TableDefinition<T> { String dataType;
int length;
int scale;
+ boolean isFunction;
boolean isPrimaryKey;
boolean isAutoIncrement;
boolean trim;
@@ -115,6 +117,10 @@ public class TableDefinition<T> { }
private Object read(ResultSet rs, int columnIndex) {
+ if (columnIndex == 0) {
+ // unmapped column or function field
+ return null;
+ }
try {
return rs.getObject(columnIndex);
} catch (SQLException e) {
@@ -129,6 +135,7 @@ public class TableDefinition<T> { int tableVersion;
List<String> primaryKeyColumnNames;
boolean memoryTable;
+ boolean multiplePrimitiveBools;
private boolean createIfRequired = true;
private Class<T> clazz;
@@ -354,6 +361,7 @@ public class TableDefinition<T> { throw new IciqlException(e, "failed to get default object for {0}", columnName);
}
+ boolean isFunction = f.isAnnotationPresent(IQFunction.class);
boolean hasAnnotation = f.isAnnotationPresent(IQColumn.class);
if (hasAnnotation) {
IQColumn col = f.getAnnotation(IQColumn.class);
@@ -385,6 +393,7 @@ public class TableDefinition<T> { fieldDef.scale = scale;
fieldDef.trim = trim;
fieldDef.nullable = nullable;
+ fieldDef.isFunction = isFunction;
fieldDef.defaultValue = defaultValue;
fieldDef.enumType = enumType;
fieldDef.dataType = ModelUtils.getDataType(fieldDef);
@@ -392,16 +401,32 @@ public class TableDefinition<T> { }
}
List<String> primaryKey = Utils.newArrayList();
+ int primitiveBoolean = 0;
for (FieldDefinition fieldDef : fields) {
if (fieldDef.isPrimaryKey) {
primaryKey.add(fieldDef.columnName);
}
+ if (fieldDef.isPrimitive && fieldDef.field.getType().equals(boolean.class)) {
+ primitiveBoolean++;
+ }
+ }
+ if (primitiveBoolean > 1) {
+ multiplePrimitiveBools = true;
+ IciqlLogger
+ .warn("Model {0} has multiple primitive booleans! Possible where,set,join clause problem!");
}
if (primaryKey.size() > 0) {
setPrimaryKey(primaryKey);
}
}
+ void checkMultipleBooleans() {
+ if (multiplePrimitiveBools) {
+ throw new IciqlException(
+ "Can not explicitly reference multiple primitive booleans in a model class!");
+ }
+ }
+
/**
* Optionally truncates strings to the maximum length and converts
* java.lang.Enum types to Strings or Integers.
@@ -698,10 +723,47 @@ public class TableDefinition<T> { }
}
- void readRow(Object item, ResultSet rs) {
+ /**
+ * Most queries executed by iciql have named select lists (select alpha,
+ * beta where...) but sometimes a wildcard select is executed (select *).
+ * When a wildcard query is executed on a table that has more columns than
+ * are mapped in your model object this creates a column mapping issue. JaQu
+ * assumed that you can always use the integer index of the reflectively
+ * mapped field definition to determine position in the result set.
+ *
+ * This is not always true.
+ *
+ * So iciql maps column names to column index in the result set to properly
+ * map the results of wildcard queries.
+ *
+ * @param rs
+ * @return
+ */
+ int[] mapColumns(ResultSet rs) {
+ int[] columns = new int[fields.size()];
for (int i = 0; i < fields.size(); i++) {
- FieldDefinition def = fields.get(i);
- Object o = def.read(rs, i + 1);
+ try {
+ FieldDefinition def = fields.get(i);
+ int columnIndex;
+ if (def.isFunction) {
+ // XXX review functions _always_ map after fields?
+ columnIndex = i + 1;
+ } else {
+ columnIndex = rs.findColumn(def.columnName);
+ }
+ columns[i] = columnIndex;
+ } catch (SQLException s) {
+ throw new IciqlException(s);
+ }
+ }
+ return columns;
+ }
+
+ void readRow(Object item, ResultSet rs, int[] columns) {
+ for (int i = 0; i < fields.size(); i++) {
+ FieldDefinition def = fields.get(i);
+ int index = columns[i];
+ Object o = def.read(rs, index);
def.setValue(item, o);
}
}
diff --git a/tests/com/iciql/test/PrimitivesTest.java b/tests/com/iciql/test/PrimitivesTest.java index be2b726..3d3811e 100644 --- a/tests/com/iciql/test/PrimitivesTest.java +++ b/tests/com/iciql/test/PrimitivesTest.java @@ -25,6 +25,8 @@ import java.util.List; import org.junit.Test;
import com.iciql.Db;
+import com.iciql.IciqlException;
+import com.iciql.test.models.MultipleBoolsModel;
import com.iciql.test.models.PrimitivesModel;
/**
@@ -82,4 +84,19 @@ public class PrimitivesTest { db.close();
}
+
+ @Test
+ public void testMultipleBooleans() {
+ Db db = IciqlSuite.openNewDb();
+ db.insertAll(MultipleBoolsModel.getList());
+
+ MultipleBoolsModel m = new MultipleBoolsModel();
+ try {
+ db.from(m).where(m.a).is(true).select();
+ assertTrue(false);
+ } catch (IciqlException e) {
+ assertTrue(true);
+ }
+ db.close();
+ }
}
diff --git a/tests/com/iciql/test/SamplesTest.java b/tests/com/iciql/test/SamplesTest.java index 17c2151..5c7ffc8 100644 --- a/tests/com/iciql/test/SamplesTest.java +++ b/tests/com/iciql/test/SamplesTest.java @@ -39,6 +39,8 @@ import org.junit.Test; import com.iciql.Db;
import com.iciql.Filter;
+import com.iciql.Iciql.IQColumn;
+import com.iciql.Iciql.IQFunction;
import com.iciql.test.models.ComplexObject;
import com.iciql.test.models.Customer;
import com.iciql.test.models.Order;
@@ -161,6 +163,7 @@ public class SamplesTest { public static class ProductPrice {
public String productName;
public String category;
+ @IQColumn(name = "unitPrice")
public Double price;
}
@@ -406,6 +409,7 @@ public class SamplesTest { */
public static class ProductGroup {
public String category;
+ @IQFunction
public Long productCount;
public String toString() {
@@ -432,7 +436,6 @@ public class SamplesTest { productCount = count();
}
});
-
assertEquals("[Beverages:2, Condiments:5, Meat/Poultry:1, Produce:1, Seafood:1]", list.toString());
}
diff --git a/tests/com/iciql/test/models/MultipleBoolsModel.java b/tests/com/iciql/test/models/MultipleBoolsModel.java new file mode 100644 index 0000000..7bc429c --- /dev/null +++ b/tests/com/iciql/test/models/MultipleBoolsModel.java @@ -0,0 +1,40 @@ +package com.iciql.test.models;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.iciql.Iciql.IQColumn;
+import com.iciql.Iciql.IQTable;
+
+/**
+ * Model class to test the runtime exception of too many primitive boolean
+ * fields in the model.
+ *
+ * @author James Moger
+ *
+ */
+@IQTable
+public class MultipleBoolsModel {
+
+ @IQColumn(autoIncrement = true, primaryKey = true)
+ public int id;
+
+ @IQColumn
+ public boolean a;
+
+ @IQColumn
+ public boolean b;
+
+ public MultipleBoolsModel() {
+ }
+
+ public MultipleBoolsModel(boolean a, boolean b) {
+ this.a = a;
+ this.b = b;
+ }
+
+ public static List<MultipleBoolsModel> getList() {
+ return Arrays.asList(new MultipleBoolsModel(true, true), new MultipleBoolsModel(true, false),
+ new MultipleBoolsModel(true, false), new MultipleBoolsModel(false, false));
+ }
+}
\ No newline at end of file |