]> source.dussan.org Git - iciql.git/commitdiff
Added methods to generate static, reusable, parameterized sql statements
authorJames Moger <james.moger@gmail.com>
Thu, 5 Jan 2012 01:48:04 +0000 (20:48 -0500)
committerJames Moger <james.moger@gmail.com>
Thu, 5 Jan 2012 01:48:04 +0000 (20:48 -0500)
src/com/iciql/Query.java
src/com/iciql/QueryCondition.java
src/com/iciql/QueryWhere.java
src/com/iciql/RuntimeParameter.java [new file with mode: 0644]
src/com/iciql/SQLDialect.java
src/com/iciql/SQLDialectDefault.java
src/com/iciql/SQLStatement.java
tests/com/iciql/test/RuntimeQueryTest.java
tests/com/iciql/test/models/StaticQueries.java [new file with mode: 0644]

index 37987ead2d54250863155488dee61d145f43105d..a8aa8b27fe7eca74ccddefd6d8d678638aff661e 100644 (file)
@@ -115,6 +115,35 @@ public class Query<T> {
                return stat.getSQL().trim();\r
        }\r
 \r
+       /**\r
+        * toSQL returns a static string version of the query with runtime variables\r
+        * properly encoded. This method is also useful when combined with the where\r
+        * clause methods like isParameter() or atLeastParameter() which allows\r
+        * iciql to generate re-usable parameterized string statements.\r
+        * \r
+        * @return the sql query as plain text\r
+        */\r
+       public String toSQL() {\r
+               return toSQL(false);\r
+       }\r
+       \r
+       /**\r
+        * toSQL returns a static string version of the query with runtime variables\r
+        * properly encoded. This method is also useful when combined with the where\r
+        * clause methods like isParameter() or atLeastParameter() which allows\r
+        * iciql to generate re-usable parameterized string statements.\r
+        * \r
+        * @param distinct\r
+        *            if true SELECT DISTINCT is used for the query\r
+        * @return the sql query as plain text\r
+        */     \r
+       public String toSQL(boolean distinct) {\r
+               SQLStatement stat = getSelectStatement(distinct);\r
+               stat.appendSQL("*");\r
+               appendFromWhere(stat);\r
+               return stat.toSQL().trim();\r
+       }\r
+\r
        private List<T> select(boolean distinct) {\r
                List<T> result = Utils.newArrayList();\r
                TableDefinition<T> def = from.getAliasDefinition();\r
@@ -607,10 +636,15 @@ public class Query<T> {
         *            the value\r
         */\r
        public void appendSQL(SQLStatement stat, Object alias, Object value) {\r
-               if (value == Function.count()) {\r
+               if (Function.count() == value) {\r
                        stat.appendSQL("COUNT(*)");\r
                        return;\r
                }\r
+               if (RuntimeParameter.PARAMETER == value) {\r
+                       stat.appendSQL("?");\r
+                       addParameter(stat, alias, value);\r
+                       return;\r
+               }\r
                Token token = Db.getToken(value);\r
                if (token != null) {\r
                        token.appendSQL(stat, this);\r
index 583f26d5c574997d3dc4ad20ccedd1687074efaf..25955314cb7efc1f2ea39004daa8b19c172e23a9 100644 (file)
@@ -85,4 +85,39 @@ public class QueryCondition<T, A> {
                return new QueryWhere<T>(query);
        }
 
+       /*
+        * These method allows you to generate "x=?", "x!=?", etc where conditions.
+        * Parameter substitution must be done manually later with db.executeQuery.
+        * This allows for building re-usable SQL string statements from your model
+        * classes.
+        */
+       public QueryWhere<T> isParameter() {
+               query.addConditionToken(new RuntimeParameter<A>(x, CompareType.EQUAL));
+               return new QueryWhere<T>(query);
+       }
+
+       public QueryWhere<T> isNotParameter() {
+               query.addConditionToken(new RuntimeParameter<A>(x, CompareType.NOT_EQUAL));
+               return new QueryWhere<T>(query);
+       }
+
+       public QueryWhere<T> exceedsParameter() {
+               query.addConditionToken(new RuntimeParameter<A>(x, CompareType.EXCEEDS));
+               return new QueryWhere<T>(query);
+       }
+
+       public QueryWhere<T> lessThanParameter() {
+               query.addConditionToken(new RuntimeParameter<A>(x, CompareType.LESS_THAN));
+               return new QueryWhere<T>(query);
+       }
+
+       public QueryWhere<T> atMostParameter() {
+               query.addConditionToken(new RuntimeParameter<A>(x, CompareType.AT_MOST));
+               return new QueryWhere<T>(query);
+       }
+
+       public QueryWhere<T> likeParameter() {
+               query.addConditionToken(new RuntimeParameter<A>(x, CompareType.LIKE));
+               return new QueryWhere<T>(query);
+       }
 }
index df93439ad3552f12d0cff4f599d3464151fb75b0..0a07c40c0a676ad1f5be59e0c2c5910972ea0041 100644 (file)
@@ -112,7 +112,7 @@ public class QueryWhere<T> {
                return addPrimitive(ConditionAndOr.AND, x);\r
        }\r
 \r
-       private <A> QueryCondition<T, A> addPrimitive(ConditionAndOr condition, A x) {          \r
+       private <A> QueryCondition<T, A> addPrimitive(ConditionAndOr condition, A x) {\r
                query.addConditionToken(condition);\r
                A alias = query.getPrimitiveAliasByValue(x);\r
                if (alias == null) {\r
@@ -234,10 +234,6 @@ public class QueryWhere<T> {
                return this;\r
        }\r
 \r
-       public <X, Z> List<X> select(Z x) {\r
-               return query.select(x);\r
-       }\r
-\r
        public String getSQL() {\r
                SQLStatement stat = new SQLStatement(query.getDb());\r
                stat.appendSQL("SELECT *");\r
@@ -245,6 +241,43 @@ public class QueryWhere<T> {
                return stat.getSQL().trim();\r
        }\r
 \r
+       /**\r
+        * toSQL returns a static string version of the query with runtime variables\r
+        * properly encoded. This method is also useful when combined with the where\r
+        * clause methods like isParameter() or atLeastParameter() which allows\r
+        * iciql to generate re-usable parameterized string statements.\r
+        * \r
+        * @return the sql query as plain text\r
+        */\r
+       public String toSQL() {\r
+               return this.toSQL(false);\r
+       }\r
+\r
+       /**\r
+        * toSQL returns a static string version of the query with runtime variables\r
+        * properly encoded. This method is also useful when combined with the where\r
+        * clause methods like isParameter() or atLeastParameter() which allows\r
+        * iciql to generate re-usable parameterized string statements.\r
+        * \r
+        * @param distinct\r
+        *            if true SELECT DISTINCT is used for the query\r
+        * @return the sql query as plain text\r
+        */\r
+       public String toSQL(boolean distinct) {\r
+               SQLStatement stat = new SQLStatement(query.getDb());\r
+               if (distinct) {\r
+                       stat.appendSQL("SELECT DISTINCT *");\r
+               } else {\r
+                       stat.appendSQL("SELECT *");\r
+               }\r
+               query.appendFromWhere(stat);\r
+               return stat.toSQL().trim();\r
+       }\r
+\r
+       public <X, Z> List<X> select(Z x) {\r
+               return query.select(x);\r
+       }\r
+\r
        public <X, Z> List<X> selectDistinct(Z x) {\r
                return query.selectDistinct(x);\r
        }\r
diff --git a/src/com/iciql/RuntimeParameter.java b/src/com/iciql/RuntimeParameter.java
new file mode 100644 (file)
index 0000000..0fbedba
--- /dev/null
@@ -0,0 +1,49 @@
+/*\r
+ * Copyright 2012 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
+/**\r
+ * A runtime parameter is used to generate x=? conditions so that iciql can\r
+ * build re-usable dynamic queries with parameter substitution done manually at\r
+ * runtime.\r
+ * \r
+ * @param <A>\r
+ *            the operand type\r
+ */\r
+\r
+class RuntimeParameter<A> implements Token {\r
+       \r
+       public final static String PARAMETER = "";\r
+       \r
+       A x;\r
+       CompareType compareType;\r
+\r
+       RuntimeParameter(A x, CompareType type) {\r
+               this.x = x;\r
+               this.compareType = type;\r
+       }\r
+\r
+       public <T> void appendSQL(SQLStatement stat, Query<T> query) {\r
+               query.appendSQL(stat, null, x);\r
+               stat.appendSQL(" ");\r
+               stat.appendSQL(compareType.getString());\r
+               if (compareType.hasRightExpression()) {\r
+                       stat.appendSQL(" ");\r
+                       query.appendSQL(stat, x, PARAMETER);\r
+               }\r
+       }\r
+}\r
index 7c29d6172c35e07cd1c7ea4ab29b29c4cd278a39..28f55664e58c6a8217e1b9e52e8b4e241e24d144 100644 (file)
@@ -125,4 +125,13 @@ public interface SQLDialect {
         * @return preferred DATETIME class
         */
        Class<? extends java.util.Date> getDateTimeClass();
+
+       /**
+        * When building static string statements this method flattens an object to
+        * a string representation suitable for a static string statement.
+        * 
+        * @param o
+        * @return the string equivalent of this object
+        */
+       String prepareParameter(Object o);
 }
index 193079f8f0b5393ffe1b0b75fefdfa2f78c1064f..617bffb4235726b645acd79823c047d7d9cec207 100644 (file)
@@ -20,6 +20,7 @@ package com.iciql;
 import java.sql.DatabaseMetaData;\r
 import java.sql.SQLException;\r
 import java.text.MessageFormat;\r
+import java.text.SimpleDateFormat;\r
 \r
 import com.iciql.TableDefinition.FieldDefinition;\r
 import com.iciql.TableDefinition.IndexDefinition;\r
@@ -31,6 +32,9 @@ import com.iciql.util.StringUtils;
  * Default implementation of an SQL dialect.\r
  */\r
 public class SQLDialectDefault implements SQLDialect {\r
+       \r
+       final String LITERAL = "'";\r
+\r
        float databaseVersion;\r
        String databaseName;\r
        String productVersion;\r
@@ -266,4 +270,20 @@ public class SQLDialectDefault implements SQLDialect {
                        stat.appendSQL(" OFFSET " + offset);\r
                }\r
        }\r
+       \r
+       @Override\r
+       public String prepareParameter(Object o) {\r
+               if (o instanceof String) {\r
+                       return LITERAL + o.toString().replace(LITERAL, "''") + LITERAL;\r
+               } else if (o instanceof Character) {\r
+                       return LITERAL + o.toString() + LITERAL;\r
+               } else if (o instanceof java.sql.Time) {\r
+                       return LITERAL + new SimpleDateFormat("HH:mm:ss").format(o) + LITERAL;\r
+               } else if (o instanceof java.sql.Date) {\r
+                       return LITERAL + new SimpleDateFormat("yyyy-MM-dd").format(o) + LITERAL;\r
+               } else if (o instanceof java.util.Date) {\r
+                       return LITERAL + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(o) + LITERAL;\r
+               }\r
+               return o.toString();\r
+       }\r
 }
\ No newline at end of file
index a33fe6c66c3f9b863a508a81d71fce103db31bf5..69e26ed20a5093303e2320e08ba4bc8cf0401e6d 100644 (file)
@@ -21,6 +21,7 @@ import java.sql.PreparedStatement;
 import java.sql.ResultSet;\r
 import java.sql.SQLException;\r
 import java.util.ArrayList;\r
+import java.util.StringTokenizer;\r
 \r
 import com.iciql.util.JdbcUtils;\r
 \r
@@ -57,6 +58,12 @@ public class SQLStatement {
                return appendSQL(db.getDialect().prepareColumnName(column));\r
        }\r
 \r
+       /**\r
+        * getSQL returns a simple string representation of the parameterized\r
+        * statement which will be used later, internally, with prepareStatement.\r
+        * \r
+        * @return a simple sql statement\r
+        */\r
        String getSQL() {\r
                if (sql == null) {\r
                        sql = buff.toString();\r
@@ -64,6 +71,40 @@ public class SQLStatement {
                return sql;\r
        }\r
 \r
+       /**\r
+        * toSQL creates a static sql statement with the referenced parameters\r
+        * encoded in the statement.\r
+        * \r
+        * @return a complete sql statement\r
+        */\r
+       String toSQL() {\r
+               if (sql == null) {\r
+                       sql = buff.toString();\r
+               }\r
+               if (params.size() == 0) {\r
+                       return sql;\r
+               }\r
+               StringBuilder sb = new StringBuilder();\r
+               // TODO this needs to me more sophisticated\r
+               StringTokenizer st = new StringTokenizer(sql, "?", false);\r
+               int i = 0;\r
+               while (st.hasMoreTokens()) {\r
+                       sb.append(st.nextToken());\r
+                       if (i < params.size()) {\r
+                               Object o = params.get(i);\r
+                               if (RuntimeParameter.PARAMETER == o) {\r
+                                       // dynamic parameter\r
+                                       sb.append('?');\r
+                               } else {\r
+                                       // static parameter\r
+                                       sb.append(db.getDialect().prepareParameter(o));\r
+                               }\r
+                               i++;\r
+                       }\r
+               }\r
+               return sb.toString();\r
+       }\r
+\r
        public SQLStatement addParameter(Object o) {\r
                // Automatically convert java.util.Date to java.sql.Timestamp\r
                // if the dialect requires java.sql.Timestamp objects (e.g. Derby)\r
index bf6f20d91cfc1213b46d1e518f8f83373df65f8b..9b306df89b766cbdb089c2e733fa21d041a7d834 100644 (file)
@@ -19,12 +19,16 @@ import static org.junit.Assert.assertEquals;
 \r
 import java.sql.ResultSet;\r
 import java.sql.SQLException;\r
+import java.text.SimpleDateFormat;\r
 import java.util.List;\r
 \r
+import org.junit.Assume;\r
 import org.junit.Test;\r
 \r
 import com.iciql.Db;\r
+import com.iciql.test.models.EnumModels.Tree;\r
 import com.iciql.test.models.Product;\r
+import com.iciql.test.models.StaticQueries;\r
 import com.iciql.util.JdbcUtils;\r
 \r
 /**\r
@@ -32,6 +36,50 @@ import com.iciql.util.JdbcUtils;
  */\r
 public class RuntimeQueryTest {\r
 \r
+       @Test\r
+       public void testParameters() {\r
+               Db db = IciqlSuite.openNewDb();\r
+               \r
+               // do not test non-H2 databases because dialects will get in the way\r
+               // e.g. column quoting, etc\r
+               Assume.assumeTrue(IciqlSuite.isH2(db));\r
+\r
+               Product p = new Product();\r
+               String q1 = db.from(p).where(p.unitsInStock).isParameter().and(p.productName).likeParameter().orderBy(p.productId).toSQL();\r
+               String q2 = db.from(p).where(p.unitsInStock).lessThan(100).and(p.productName).like("test").or(p.productName).likeParameter().orderBy(p.productId).toSQL();\r
+               \r
+               StaticQueries.StaticModel1 m1 = new StaticQueries.StaticModel1();\r
+               String q3 = db.from(m1).where(m1.myTree).is(Tree.MAPLE).and(m1.myTree).isParameter().toSQL();\r
+               \r
+               StaticQueries.StaticModel2 m2 = new StaticQueries.StaticModel2();\r
+               String q4 = db.from(m2).where(m2.myTree).is(Tree.MAPLE).and(m2.myTree).isParameter().toSQL();\r
+\r
+               StaticQueries.StaticModel3 m3 = new StaticQueries.StaticModel3();\r
+               String q5 = db.from(m3).where(m3.myTree).is(Tree.MAPLE).and(m3.myTree).isParameter().toSQL();\r
+\r
+               long now = System.currentTimeMillis();\r
+               java.sql.Date aDate = new java.sql.Date(now);\r
+               java.sql.Time aTime = new java.sql.Time(now);\r
+               java.sql.Timestamp aTimestamp = new java.sql.Timestamp(now);\r
+               \r
+               String q6 = db.from(m1).where(m1.myDate).is(aDate).and(m1.myDate).isParameter().toSQL();\r
+               String q7 = db.from(m1).where(m1.myTime).is(aTime).and(m1.myTime).isParameter().toSQL();\r
+               String q8 = db.from(m1).where(m1.myTimestamp).is(aTimestamp).and(m1.myTimestamp).isParameter().toSQL();\r
+\r
+               db.close();\r
+               assertEquals("SELECT * FROM Product WHERE unitsInStock = ? AND productName LIKE ?  ORDER BY productId", q1);\r
+               assertEquals("SELECT * FROM Product WHERE unitsInStock < 100 AND productName LIKE 'test' OR productName LIKE ?  ORDER BY productId", q2);\r
+               \r
+               assertEquals("SELECT * FROM StaticQueryTest1 WHERE myTree = 'MAPLE' AND myTree = ?", q3);\r
+               assertEquals("SELECT * FROM StaticQueryTest2 WHERE myTree = 50 AND myTree = ?", q4);\r
+               assertEquals("SELECT * FROM StaticQueryTest3 WHERE myTree = 4 AND myTree = ?", q5);\r
+\r
+               java.util.Date refDate = new java.util.Date(now);\r
+               assertEquals("SELECT * FROM StaticQueryTest1 WHERE myDate = '" + new SimpleDateFormat("yyyy-MM-dd").format(refDate) + "' AND myDate = ?", q6);\r
+               assertEquals("SELECT * FROM StaticQueryTest1 WHERE myTime = '" + new SimpleDateFormat("HH:mm:ss").format(refDate) + "' AND myTime = ?", q7);\r
+               assertEquals("SELECT * FROM StaticQueryTest1 WHERE myTimestamp = '" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(refDate) + "' AND myTimestamp = ?", q8);\r
+       }\r
+       \r
        @Test\r
        public void testRuntimeQuery() {\r
                Db db = IciqlSuite.openNewDb();\r
diff --git a/tests/com/iciql/test/models/StaticQueries.java b/tests/com/iciql/test/models/StaticQueries.java
new file mode 100644 (file)
index 0000000..09f84e6
--- /dev/null
@@ -0,0 +1,87 @@
+/*\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
+package com.iciql.test.models;\r
+\r
+import java.sql.Timestamp;\r
+\r
+import com.iciql.Iciql.EnumType;\r
+import com.iciql.Iciql.IQColumn;\r
+import com.iciql.Iciql.IQEnum;\r
+import com.iciql.Iciql.IQTable;\r
+import com.iciql.test.models.EnumModels.Tree;\r
+\r
+/**\r
+ * Static query models.\r
+ */\r
+public class StaticQueries {\r
+\r
+       @IQTable(name = "StaticQueryTest1")\r
+       public static class StaticModel1 {\r
+\r
+               @IQColumn(primaryKey = true, autoIncrement = true)\r
+               public Integer id;\r
+\r
+               @IQColumn\r
+               @IQEnum(EnumType.NAME)\r
+               public Tree myTree;\r
+\r
+               @IQColumn\r
+               public String myString;\r
+\r
+               @IQColumn\r
+               public Boolean myBool;\r
+\r
+               @IQColumn\r
+               public Timestamp myTimestamp;\r
+\r
+               @IQColumn\r
+               public java.sql.Date myDate;\r
+\r
+               @IQColumn\r
+               public java.sql.Time myTime;\r
+\r
+               public StaticModel1() {\r
+               }\r
+       }\r
+\r
+       @IQTable(name = "StaticQueryTest2")\r
+       public static class StaticModel2 {\r
+\r
+               @IQColumn(primaryKey = true, autoIncrement = true)\r
+               public Integer id;\r
+\r
+               @IQColumn\r
+               @IQEnum(EnumType.ENUMID)\r
+               public Tree myTree;\r
+\r
+               public StaticModel2() {\r
+               }\r
+       }\r
+\r
+       @IQTable(name = "StaticQueryTest3")\r
+       public static class StaticModel3 {\r
+\r
+               @IQColumn(primaryKey = true, autoIncrement = true)\r
+               public Integer id;\r
+\r
+               @IQColumn\r
+               @IQEnum(EnumType.ORDINAL)\r
+               public Tree myTree;\r
+\r
+               public StaticModel3() {\r
+               }\r
+       }\r
+}\r