From f3e1dee9700ca3ff5e1e83d480632b570d98cf60 Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Mon, 2 Jun 2014 10:40:10 +0200 Subject: [PATCH] [#3297] Bind variables are erroneously inlined into quoted identifiers, if identifiers contain question marks --- .../org/jooq/impl/DefaultRenderContext.java | 42 ++---- .../main/java/org/jooq/impl/Identifiers.java | 124 ++++++++++++++++++ jOOQ/src/main/java/org/jooq/impl/Utils.java | 67 ++++++++++ .../test/java/org/jooq/test/PlainSQLTest.java | 29 ++++ 4 files changed, 228 insertions(+), 34 deletions(-) create mode 100644 jOOQ/src/main/java/org/jooq/impl/Identifiers.java diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java b/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java index 862c661f90..f0b4847d16 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java @@ -49,6 +49,10 @@ import static org.jooq.conf.ParamType.NAMED; import static org.jooq.conf.RenderNameStyle.LOWER; import static org.jooq.conf.RenderNameStyle.QUOTED; import static org.jooq.conf.RenderNameStyle.UPPER; +import static org.jooq.impl.Identifiers.QUOTES; +import static org.jooq.impl.Identifiers.QUOTE_END_DELIMITER; +import static org.jooq.impl.Identifiers.QUOTE_END_DELIMITER_ESCAPED; +import static org.jooq.impl.Identifiers.QUOTE_START_DELIMITER; import static org.jooq.impl.Utils.DATA_COUNT_BIND_VALUES; import java.util.Arrays; @@ -365,41 +369,11 @@ class DefaultRenderContext extends AbstractContext implements Ren sql(literal); } else { - switch (family) { + String[][] quotes = QUOTES.get(family); - // MySQL supports backticks and double quotes - case MARIADB: - case MYSQL: - sql("`").sql(StringUtils.replace(literal, "`", "``")).sql("`"); - break; - - /* [pro] xx - xx xxxxx xxxxxxxxx xxx xxxxxxxx - xxxx xxxxxxx - xxxx xxxx - xxxx xxxxxxxxxx - xxxx xxxxxxx - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxx xxxxxxxxxxxxxxxx - xxxxxx - - xx [/pro] */ - // Most dialects implement the SQL standard, using double quotes - /* [pro] xx - xxxx xxxx - xxxx xxxxxxx - xxxx xxxxxxx - xx [/pro] */ - case CUBRID: - case DERBY: - case FIREBIRD: - case H2: - case HSQLDB: - case POSTGRES: - case SQLITE: - default: - sql('"').sql(StringUtils.replace(literal, "\"", "\"\"")).sql('"'); - break; - } + sql(quotes[QUOTE_START_DELIMITER][0]); + sql(StringUtils.replace(literal, quotes[QUOTE_END_DELIMITER][0], quotes[QUOTE_END_DELIMITER_ESCAPED][0])); + sql(quotes[QUOTE_END_DELIMITER][0]); } return this; diff --git a/jOOQ/src/main/java/org/jooq/impl/Identifiers.java b/jOOQ/src/main/java/org/jooq/impl/Identifiers.java new file mode 100644 index 0000000000..13ac86c53c --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/impl/Identifiers.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2009-2014, Data Geekery GmbH (http://www.datageekery.com) + * All rights reserved. + * + * This work is dual-licensed + * - under the Apache Software License 2.0 (the "ASL") + * - under the jOOQ License and Maintenance Agreement (the "jOOQ License") + * ============================================================================= + * You may choose which license applies to you: + * + * - If you're using this work with Open Source databases, you may choose + * either ASL or jOOQ License. + * - If you're using this work with at least one commercial database, you must + * choose jOOQ License + * + * For more information, please visit http://www.jooq.org/licenses + * + * Apache Software License 2.0: + * ----------------------------------------------------------------------------- + * 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. + * + * jOOQ License and Maintenance Agreement: + * ----------------------------------------------------------------------------- + * Data Geekery grants the Customer the non-exclusive, timely limited and + * non-transferable license to install and use the Software under the terms of + * the jOOQ License and Maintenance Agreement. + * + * This library is distributed with a LIMITED WARRANTY. See the jOOQ License + * and Maintenance Agreement for more details: http://www.jooq.org/licensing + */ +package org.jooq.impl; + +import java.util.EnumMap; + +import org.jooq.SQLDialect; + +/** + * An internal utility class for SQL identifiers. + * + * @author Lukas Eder + */ +class Identifiers { + + /** + * The structure is: + *

+ *

+     * SQLDialect -> {
+     *     { main         start delimiter, alternative         start delimiter, ... },
+     *     { main         end   delimiter, alternative         end   delimiter, ... },
+     *     { main escaped end   delimiter, alternative escaped end   delimiter, ... },
+     *       ...
+     * }
+     * 
+ */ + static final EnumMap QUOTES; + static final int QUOTE_START_DELIMITER = 0; + static final int QUOTE_END_DELIMITER = 1; + static final int QUOTE_END_DELIMITER_ESCAPED = 2; + + static { + QUOTES = new EnumMap(SQLDialect.class); + + for (SQLDialect family : SQLDialect.families()) { + switch (family) { + + // MySQL supports backticks and double quotes + case MARIADB: + case MYSQL: + QUOTES.put(family, new String[][] { + { "`" , "\""}, + { "`" , "\""}, + { "``", "\"\"" } + }); + break; + + /* [pro] xx + xx xxxxx xxxxxxxxx xxx xxxxxxxx + xxxx xxxxxxx + xxxx xxxx + xxxx xxxxxxxxxx + xxxx xxxxxxx + xxxxxxxxxxxxxxxxxx xxx xxxxxxxxxx x + x xxx xx + x xxx xx + x xxxx x + xxx + xxxxxx + + xx [/pro] */ + // Most dialects implement the SQL standard, using double quotes + /* [pro] xx + xxxx xxxx + xxxx xxxxxxx + xxxx xxxxxxx + xx [/pro] */ + case CUBRID: + case DERBY: + case FIREBIRD: + case H2: + case HSQLDB: + case POSTGRES: + case SQLITE: + default: + QUOTES.put(family, new String[][] { + { "\""}, + { "\""}, + { "\"\"" } + }); + break; + } + } + } +} diff --git a/jOOQ/src/main/java/org/jooq/impl/Utils.java b/jOOQ/src/main/java/org/jooq/impl/Utils.java index ac28c3cbf8..8b0d40db84 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Utils.java +++ b/jOOQ/src/main/java/org/jooq/impl/Utils.java @@ -55,6 +55,10 @@ import static org.jooq.impl.DSL.getDataType; import static org.jooq.impl.DSL.nullSafe; import static org.jooq.impl.DSL.val; import static org.jooq.impl.DefaultExecuteContext.localConnection; +import static org.jooq.impl.Identifiers.QUOTES; +import static org.jooq.impl.Identifiers.QUOTE_END_DELIMITER; +import static org.jooq.impl.Identifiers.QUOTE_END_DELIMITER_ESCAPED; +import static org.jooq.impl.Identifiers.QUOTE_START_DELIMITER; import static org.jooq.tools.jdbc.JDBCUtils.safeFree; import static org.jooq.tools.jdbc.JDBCUtils.wasNull; import static org.jooq.tools.reflect.Reflect.accessible; @@ -1030,6 +1034,10 @@ final class Utils { // [#1593] Create a dummy renderer if we're in bind mode if (render == null) render = new DefaultRenderContext(bind.configuration()); + SQLDialect dialect = render.configuration().dialect(); + SQLDialect family = dialect.family(); + String[][] quotes = QUOTES.get(family); + for (int i = 0; i < sqlChars.length; i++) { // [#1797] Skip content inside of single-line comments, e.g. @@ -1087,6 +1095,50 @@ final class Utils { render.sql(sqlChars[i]); } + // [#3297] Skip ? inside of quoted identifiers, e.g. + // update x set v = "Column Name with a ? (question mark)" + else if (peekAny(sqlChars, i, quotes[QUOTE_START_DELIMITER])) { + + // Main identifier delimiter or alternative one? + int delimiter = 0; + for (int d = 0; d < quotes[QUOTE_START_DELIMITER].length; d++) { + if (peek(sqlChars, i, quotes[QUOTE_START_DELIMITER][d])) { + delimiter = d; + break; + } + } + + // Consume the initial identifier delimiter + for (int d = 0; d < quotes[QUOTE_START_DELIMITER][delimiter].length(); d++) + render.sql(sqlChars[i++]); + + // Consume the whole identifier + for (;;) { + + // Consume an escaped quote + if (peek(sqlChars, i, quotes[QUOTE_END_DELIMITER_ESCAPED][delimiter])) { + for (int d = 0; d < quotes[QUOTE_END_DELIMITER_ESCAPED][delimiter].length(); d++) + render.sql(sqlChars[i++]); + } + + // Break on the terminal identifier delimiter + else if (peek(sqlChars, i, quotes[QUOTE_END_DELIMITER][delimiter])) { + break; + } + + // Consume identifier content + render.sql(sqlChars[i++]); + } + + // Consume the terminal identifier delimiter + for (int d = 0; d < quotes[QUOTE_END_DELIMITER][delimiter].length(); d++) { + if (d > 0) + i++; + + render.sql(sqlChars[i]); + } + } + // Inline bind variables only outside of string literals else if (sqlChars[i] == '?' && substituteIndex < substitutes.size()) { QueryPart substitute = substitutes.get(substituteIndex++); @@ -1165,6 +1217,21 @@ final class Utils { return true; } + /** + * Peek for several strings at a given index of a char[] + * + * @param sqlChars The char array to peek into + * @param index The index within the char array to peek for a string + * @param peek The strings to peek for + */ + static final boolean peekAny(char[] sqlChars, int index, String[] peekAny) { + for (String peek : peekAny) + if (peek(sqlChars, index, peek)) + return true; + + return false; + } + /** * Create {@link QueryPart} objects from bind values or substitutes */ diff --git a/jOOQ/src/test/java/org/jooq/test/PlainSQLTest.java b/jOOQ/src/test/java/org/jooq/test/PlainSQLTest.java index c7178e178f..8c62e5e745 100644 --- a/jOOQ/src/test/java/org/jooq/test/PlainSQLTest.java +++ b/jOOQ/src/test/java/org/jooq/test/PlainSQLTest.java @@ -40,11 +40,13 @@ */ package org.jooq.test; +import static org.jooq.SQLDialect.POSTGRES; import static org.jooq.impl.DSL.condition; import static org.jooq.impl.DSL.val; import org.jooq.Condition; import org.jooq.QueryPart; +import org.jooq.impl.DSL; import org.junit.Test; @@ -66,6 +68,33 @@ public class PlainSQLTest extends AbstractTest { assertEquals("((a = :1 and b = :2) and (a = :3 and b = :4))", create.renderNamedParams(q.and(q))); } + @Test + public void testStringLiterals() { + Condition q = condition("a = '?' and b = '{0}' and c = ?", 1); + + assertEquals("(a = '?' and b = '{0}' and c = ?)", create.render(q)); + assertEquals("(a = '?' and b = '{0}' and c = 1)", create.renderInlined(q)); + assertEquals("(a = '?' and b = '{0}' and c = :1)", create.renderNamedParams(q)); + } + + @Test + public void testQuotedIdentifiers() { + Condition c1 = condition("a = `?` and b = `{0}` and c = ?", 1); + Condition c2 = condition("a = \"?\" and b = \"{0}\" and c = ?", 1); + + assertEquals("(a = `?` and b = `{0}` and c = ?)", create.render(c1)); + assertEquals("(a = `?` and b = `{0}` and c = 1)", create.renderInlined(c1)); + assertEquals("(a = `?` and b = `{0}` and c = :1)", create.renderNamedParams(c1)); + + assertEquals("(a = \"?\" and b = \"{0}\" and c = ?)", create.render(c2)); + assertEquals("(a = \"?\" and b = \"{0}\" and c = 1)", create.renderInlined(c2)); + assertEquals("(a = \"?\" and b = \"{0}\" and c = :1)", create.renderNamedParams(c2)); + + assertEquals("(a = \"?\" and b = \"{0}\" and c = ?)", DSL.using(POSTGRES).render(c2)); + assertEquals("(a = \"?\" and b = \"{0}\" and c = 1)", DSL.using(POSTGRES).renderInlined(c2)); + assertEquals("(a = \"?\" and b = \"{0}\" and c = :1)", DSL.using(POSTGRES).renderNamedParams(c2)); + } + @Test public void testIndexedParameters() { QueryPart q = condition("a = ? and b = ? and ? = ?", 1, 2, "a", "b");