提交 c05e083d authored 作者: noelgrandin's avatar noelgrandin

Issue 522: Treat empty strings like NULL in Oracle compatibility mode, patch by Daniel Gredler.

上级 fbf8deed
......@@ -39,6 +39,7 @@ Change Log
</li><li>Add support for DB2 "WITH UR" clause, patch from litailang
</li><li>Added support for ON DUPLICATE KEY UPDATE like MySQL with the values() function to update with the value that
was to be inserted. Patch from Jean-Francois Noel.
</li><li>Issue 522: Treat empty strings like NULL in Oracle compatibility mode, patch by Daniel Gredler.
</li></ul>
<h2>Version 1.3.174 (2013-10-19)</h2>
......
......@@ -1131,6 +1131,7 @@ or the SQL statement <code>SET MODE Oracle</code>.
same values otherwise.
</li><li>Concatenating <code>NULL</code> with another value
results in the other value.
</li><li>Empty strings are treated like <code>NULL</code> values.
</li></ul>
<h3>PostgreSQL Compatibility Mode</h3>
......
......@@ -545,7 +545,6 @@ See also <a href="build.html#providing_patches">Providing Patches</a>.
(compatibility with MySQL, PostgreSQL, HSQLDB; not Derby).
</li><li>ARRAY data type: support Integer[] and so on in Java functions (currently only Object[] is supported).
</li><li>MySQL compatibility: LOCK TABLES a READ, b READ - see also http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html
</li><li>Oracle compatibility: convert empty strings to null. Also convert an empty byte array to null, but not empty varray.
</li><li>The HTML to PDF converter should use http://code.google.com/p/wkhtmltopdf/
</li><li>Issue 303: automatically convert "X NOT IN(SELECT...)" to "NOT EXISTS(...)".
</li><li>MySQL compatibility: update test1 t1, test2 t2 set t1.name=t2.name where t1.id=t2.id.
......
......@@ -3153,7 +3153,7 @@ public class Parser {
}
currentToken = "'";
checkLiterals(true);
currentValue = ValueString.get(StringUtils.fromCacheOrNew(result));
currentValue = ValueString.get(StringUtils.fromCacheOrNew(result), database.getMode().treatEmptyStringsAsNull);
parseIndex = i;
currentTokenType = VALUE;
return;
......@@ -3167,7 +3167,7 @@ public class Parser {
result = sqlCommand.substring(begin, i);
currentToken = "'";
checkLiterals(true);
currentValue = ValueString.get(StringUtils.fromCacheOrNew(result));
currentValue = ValueString.get(StringUtils.fromCacheOrNew(result), database.getMode().treatEmptyStringsAsNull);
parseIndex = i;
currentTokenType = VALUE;
return;
......
......@@ -98,6 +98,11 @@ public class Mode {
*/
public boolean uniqueIndexSingleNullExceptAllColumnsAreNull;
/**
* Empty strings are treated like NULL values. Useful for Oracle emulation.
*/
public boolean treatEmptyStringsAsNull;
/**
* Support the pseudo-table SYSIBM.SYSDUMMY1.
*/
......@@ -181,6 +186,7 @@ public class Mode {
mode = new Mode("Oracle");
mode.aliasColumnName = true;
mode.uniqueIndexSingleNullExceptAllColumnsAreNull = true;
mode.treatEmptyStringsAsNull = true;
add(mode);
mode = new Mode("PostgreSQL");
......
......@@ -588,7 +588,7 @@ public class Function extends Expression implements FunctionCall {
result = ValueLong.get(16 * length(v0));
break;
case CHAR:
result = ValueString.get(String.valueOf((char) v0.getInt()));
result = ValueString.get(String.valueOf((char) v0.getInt()), database.getMode().treatEmptyStringsAsNull);
break;
case CHAR_LENGTH:
case LENGTH:
......@@ -619,30 +619,30 @@ public class Function extends Expression implements FunctionCall {
&& !StringUtils.isNullOrEmpty(tmp)) {
tmp = separator.concat(tmp);
}
result = ValueString.get(result.getString().concat(tmp));
result = ValueString.get(result.getString().concat(tmp), database.getMode().treatEmptyStringsAsNull);
}
}
if (info.type == CONCAT_WS) {
if (separator != null && result == ValueNull.INSTANCE) {
result = ValueString.get("");
result = ValueString.get("", database.getMode().treatEmptyStringsAsNull);
}
}
break;
}
case HEXTORAW:
result = ValueString.get(hexToRaw(v0.getString()));
result = ValueString.get(hexToRaw(v0.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case LOWER:
case LCASE:
// TODO this is locale specific, need to document or provide a way
// to set the locale
result = ValueString.get(v0.getString().toLowerCase());
result = ValueString.get(v0.getString().toLowerCase(), database.getMode().treatEmptyStringsAsNull);
break;
case RAWTOHEX:
result = ValueString.get(rawToHex(v0.getString()));
result = ValueString.get(rawToHex(v0.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case SOUNDEX:
result = ValueString.get(getSoundex(v0.getString()));
result = ValueString.get(getSoundex(v0.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case SPACE: {
int len = Math.max(0, v0.getInt());
......@@ -650,39 +650,39 @@ public class Function extends Expression implements FunctionCall {
for (int i = len - 1; i >= 0; i--) {
chars[i] = ' ';
}
result = ValueString.get(new String(chars));
result = ValueString.get(new String(chars), database.getMode().treatEmptyStringsAsNull);
break;
}
case UPPER:
case UCASE:
// TODO this is locale specific, need to document or provide a way
// to set the locale
result = ValueString.get(v0.getString().toUpperCase());
result = ValueString.get(v0.getString().toUpperCase(), database.getMode().treatEmptyStringsAsNull);
break;
case STRINGENCODE:
result = ValueString.get(StringUtils.javaEncode(v0.getString()));
result = ValueString.get(StringUtils.javaEncode(v0.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case STRINGDECODE:
result = ValueString.get(StringUtils.javaDecode(v0.getString()));
result = ValueString.get(StringUtils.javaDecode(v0.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case STRINGTOUTF8:
result = ValueBytes.getNoCopy(v0.getString().getBytes(Constants.UTF8));
break;
case UTF8TOSTRING:
result = ValueString.get(new String(v0.getBytesNoCopy(), Constants.UTF8));
result = ValueString.get(new String(v0.getBytesNoCopy(), Constants.UTF8), database.getMode().treatEmptyStringsAsNull);
break;
case XMLCOMMENT:
result = ValueString.get(StringUtils.xmlComment(v0.getString()));
result = ValueString.get(StringUtils.xmlComment(v0.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case XMLCDATA:
result = ValueString.get(StringUtils.xmlCData(v0.getString()));
result = ValueString.get(StringUtils.xmlCData(v0.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case XMLSTARTDOC:
result = ValueString.get(StringUtils.xmlStartDoc());
result = ValueString.get(StringUtils.xmlStartDoc(), database.getMode().treatEmptyStringsAsNull);
break;
case DAY_NAME: {
SimpleDateFormat dayName = new SimpleDateFormat("EEEE", Locale.ENGLISH);
result = ValueString.get(dayName.format(v0.getDate()));
result = ValueString.get(dayName.format(v0.getDate()), database.getMode().treatEmptyStringsAsNull);
break;
}
case DAY_OF_MONTH:
......@@ -705,7 +705,7 @@ public class Function extends Expression implements FunctionCall {
break;
case MONTH_NAME: {
SimpleDateFormat monthName = new SimpleDateFormat("MMMM", Locale.ENGLISH);
result = ValueString.get(monthName.format(v0.getDate()));
result = ValueString.get(monthName.format(v0.getDate()), database.getMode().treatEmptyStringsAsNull);
break;
}
case QUARTER:
......@@ -755,11 +755,11 @@ public class Function extends Expression implements FunctionCall {
break;
}
case DATABASE:
result = ValueString.get(database.getShortName());
result = ValueString.get(database.getShortName(), database.getMode().treatEmptyStringsAsNull);
break;
case USER:
case CURRENT_USER:
result = ValueString.get(session.getUser().getName());
result = ValueString.get(session.getUser().getName(), database.getMode().treatEmptyStringsAsNull);
break;
case IDENTITY:
result = session.getLastIdentity();
......@@ -775,7 +775,7 @@ public class Function extends Expression implements FunctionCall {
break;
case DATABASE_PATH: {
String path = database.getDatabasePath();
result = path == null ? (Value) ValueNull.INSTANCE : ValueString.get(path);
result = path == null ? (Value) ValueNull.INSTANCE : ValueString.get(path, database.getMode().treatEmptyStringsAsNull);
break;
}
case LOCK_TIMEOUT:
......@@ -805,7 +805,7 @@ public class Function extends Expression implements FunctionCall {
result = ValueInt.get(database.getLockMode());
break;
case SCHEMA:
result = ValueString.get(session.getCurrentSchemaName());
result = ValueString.get(session.getCurrentSchemaName(), database.getMode().treatEmptyStringsAsNull);
break;
case SESSION_ID:
result = ValueInt.get(session.getId());
......@@ -1108,12 +1108,12 @@ public class Function extends Expression implements FunctionCall {
if (v1 == ValueNull.INSTANCE || v2 == ValueNull.INSTANCE) {
result = v1;
} else {
result = ValueString.get(insert(v0.getString(), v1.getInt(), v2.getInt(), v3.getString()));
result = ValueString.get(insert(v0.getString(), v1.getInt(), v2.getInt(), v3.getString()), database.getMode().treatEmptyStringsAsNull);
}
break;
}
case LEFT:
result = ValueString.get(left(v0.getString(), v1.getInt()));
result = ValueString.get(left(v0.getString(), v1.getInt()), database.getMode().treatEmptyStringsAsNull);
break;
case LOCATE: {
int start = v2 == null ? 0 : v2.getInt();
......@@ -1127,27 +1127,27 @@ public class Function extends Expression implements FunctionCall {
}
case REPEAT: {
int count = Math.max(0, v1.getInt());
result = ValueString.get(repeat(v0.getString(), count));
result = ValueString.get(repeat(v0.getString(), count), database.getMode().treatEmptyStringsAsNull);
break;
}
case REPLACE: {
String s0 = v0.getString();
String s1 = v1.getString();
String s2 = (v2 == null) ? "" : v2.getString();
result = ValueString.get(replace(s0, s1, s2));
result = ValueString.get(replace(s0, s1, s2), database.getMode().treatEmptyStringsAsNull);
break;
}
case RIGHT:
result = ValueString.get(right(v0.getString(), v1.getInt()));
result = ValueString.get(right(v0.getString(), v1.getInt()), database.getMode().treatEmptyStringsAsNull);
break;
case LTRIM:
result = ValueString.get(StringUtils.trim(v0.getString(), true, false, v1 == null ? " " : v1.getString()));
result = ValueString.get(StringUtils.trim(v0.getString(), true, false, v1 == null ? " " : v1.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case TRIM:
result = ValueString.get(StringUtils.trim(v0.getString(), true, true, v1 == null ? " " : v1.getString()));
result = ValueString.get(StringUtils.trim(v0.getString(), true, true, v1 == null ? " " : v1.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case RTRIM:
result = ValueString.get(StringUtils.trim(v0.getString(), false, true, v1 == null ? " " : v1.getString()));
result = ValueString.get(StringUtils.trim(v0.getString(), false, true, v1 == null ? " " : v1.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case SUBSTR:
case SUBSTRING: {
......@@ -1157,27 +1157,27 @@ public class Function extends Expression implements FunctionCall {
offset = s.length() + offset + 1;
}
int length = v2 == null ? s.length() : v2.getInt();
result = ValueString.get(substring(s, offset, length));
result = ValueString.get(substring(s, offset, length), database.getMode().treatEmptyStringsAsNull);
break;
}
case POSITION:
result = ValueInt.get(locate(v0.getString(), v1.getString(), 0));
break;
case XMLATTR:
result = ValueString.get(StringUtils.xmlAttr(v0.getString(), v1.getString()));
result = ValueString.get(StringUtils.xmlAttr(v0.getString(), v1.getString()), database.getMode().treatEmptyStringsAsNull);
break;
case XMLNODE: {
String attr = v1 == null ? null : v1 == ValueNull.INSTANCE ? null : v1.getString();
String content = v2 == null ? null : v2 == ValueNull.INSTANCE ? null : v2.getString();
boolean indent = v3 == null ? true : v3.getBoolean();
result = ValueString.get(StringUtils.xmlNode(v0.getString(), attr, content, indent));
result = ValueString.get(StringUtils.xmlNode(v0.getString(), attr, content, indent), database.getMode().treatEmptyStringsAsNull);
break;
}
case REGEXP_REPLACE: {
String regexp = v1.getString();
String replacement = v2.getString();
try {
result = ValueString.get(v0.getString().replaceAll(regexp, replacement));
result = ValueString.get(v0.getString().replaceAll(regexp, replacement), database.getMode().treatEmptyStringsAsNull);
} catch (StringIndexOutOfBoundsException e) {
throw DbException.get(ErrorCode.LIKE_ESCAPE_ERROR_1, e, replacement);
} catch (PatternSyntaxException e) {
......@@ -1186,13 +1186,13 @@ public class Function extends Expression implements FunctionCall {
break;
}
case RPAD:
result = ValueString.get(StringUtils.pad(v0.getString(), v1.getInt(), v2 == null ? null : v2.getString(), true));
result = ValueString.get(StringUtils.pad(v0.getString(), v1.getInt(), v2 == null ? null : v2.getString(), true), database.getMode().treatEmptyStringsAsNull);
break;
case LPAD:
result = ValueString.get(StringUtils.pad(v0.getString(), v1.getInt(), v2 == null ? null : v2.getString(), false));
result = ValueString.get(StringUtils.pad(v0.getString(), v1.getInt(), v2 == null ? null : v2.getString(), false), database.getMode().treatEmptyStringsAsNull);
break;
case H2VERSION:
result = ValueString.get(Constants.getVersion());
result = ValueString.get(Constants.getVersion(), database.getMode().treatEmptyStringsAsNull);
break;
case DATE_ADD:
result = ValueTimestamp.get(dateadd(v0.getString(), v1.getInt(), v2.getTimestamp()));
......@@ -1211,7 +1211,7 @@ public class Function extends Expression implements FunctionCall {
} else {
String locale = v2 == null ? null : v2 == ValueNull.INSTANCE ? null : v2.getString();
String tz = v3 == null ? null : v3 == ValueNull.INSTANCE ? null : v3.getString();
result = ValueString.get(DateTimeUtils.formatDateTime(v0.getTimestamp(), v1.getString(), locale, tz));
result = ValueString.get(DateTimeUtils.formatDateTime(v0.getTimestamp(), v1.getString(), locale, tz), database.getMode().treatEmptyStringsAsNull);
}
break;
}
......@@ -1342,9 +1342,9 @@ public class Function extends Expression implements FunctionCall {
}
case XMLTEXT:
if (v1 == null) {
result = ValueString.get(StringUtils.xmlText(v0.getString()));
result = ValueString.get(StringUtils.xmlText(v0.getString()), database.getMode().treatEmptyStringsAsNull);
} else {
result = ValueString.get(StringUtils.xmlText(v0.getString(), v1.getBoolean()));
result = ValueString.get(StringUtils.xmlText(v0.getString(), v1.getBoolean()), database.getMode().treatEmptyStringsAsNull);
}
break;
case VALUES:
......
......@@ -128,15 +128,26 @@ public class ValueString extends Value {
* @param s the string
* @return the value
*/
public static ValueString get(String s) {
if (s.length() == 0) {
return EMPTY;
public static Value get(String s) {
return get(s, false);
}
/**
* Get or create a string value for the given string.
*
* @param s the string
* @param treatEmptyStringsAsNull whether or not to treat empty strings as NULL
* @return the value
*/
public static Value get(String s, boolean treatEmptyStringsAsNull) {
if (s.isEmpty()) {
return treatEmptyStringsAsNull ? ValueNull.INSTANCE : EMPTY;
}
ValueString obj = new ValueString(StringUtils.cache(s));
if (s.length() > SysProperties.OBJECT_CACHE_MAX_PER_ELEMENT_SIZE) {
return obj;
}
return (ValueString) Value.cache(obj);
return Value.cache(obj);
// this saves memory, but is really slow
// return new ValueString(s.intern());
}
......@@ -148,7 +159,7 @@ public class ValueString extends Value {
* @param s the string
* @return the value
*/
protected ValueString getNew(String s) {
protected Value getNew(String s) {
return ValueString.get(s);
}
......
......@@ -45,6 +45,7 @@ import org.h2.test.db.TestMultiThread;
import org.h2.test.db.TestMultiThreadedKernel;
import org.h2.test.db.TestOpenClose;
import org.h2.test.db.TestOptimizations;
import org.h2.test.db.TestCompatibilityOracle;
import org.h2.test.db.TestOutOfMemory;
import org.h2.test.db.TestPowerOff;
import org.h2.test.db.TestQueryCache;
......@@ -628,6 +629,7 @@ kill -9 `jps -l | grep "org.h2.test." | cut -d " " -f 1`
new TestCheckpoint().runTest(this);
new TestCluster().runTest(this);
new TestCompatibility().runTest(this);
new TestCompatibilityOracle().runTest(this);
new TestCsv().runTest(this);
new TestDateStorage().runTest(this);
new TestDeadlock().runTest(this);
......
/*
* Copyright 2004-2013 H2 Group. Multiple-Licensed under the H2 License,
* Version 1.0, and under the Eclipse Public License, Version 1.0
* (http://h2database.com/html/license.html).
* Initial Developer: H2 Group
*/
package org.h2.test.db;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.Arrays;
import org.h2.test.TestBase;
import org.h2.tools.SimpleResultSet;
/**
* Test Oracle compatibility mode.
*/
public class TestCompatibilityOracle extends TestBase {
/**
* Run just this test.
*
* @param s ignored
*/
public static void main(String... s) throws Exception {
TestBase test = TestBase.createCaller().init();
test.test();
}
@Override
public void test() throws Exception {
testTreatEmptyStringsAsNull();
}
private void testTreatEmptyStringsAsNull() throws SQLException {
deleteDb("oracle");
Connection conn = getConnection("oracle;MODE=Oracle");
Statement stat = conn.createStatement();
stat.execute("CREATE TABLE A (ID NUMBER, X VARCHAR2(1))");
stat.execute("INSERT INTO A VALUES (1, 'a')");
stat.execute("INSERT INTO A VALUES (2, '')");
stat.execute("INSERT INTO A VALUES (3, ' ')");
assertResult("3", stat, "SELECT COUNT(*) FROM A");
assertResult("1", stat, "SELECT COUNT(*) FROM A WHERE X IS NULL");
assertResult("2", stat, "SELECT COUNT(*) FROM A WHERE TRIM(X) IS NULL");
assertResult("0", stat, "SELECT COUNT(*) FROM A WHERE X = ''");
assertResult(new Object[][] { { 1, "a" }, { 2, null }, { 3, " " } }, stat, "SELECT * FROM A");
assertResult(new Object[][] { { 1, "a" }, { 2, null }, { 3, null } }, stat, "SELECT ID, TRIM(X) FROM A");
stat.execute("CREATE TABLE B (ID NUMBER, X NUMBER)");
stat.execute("INSERT INTO B VALUES (1, '5')");
stat.execute("INSERT INTO B VALUES (2, '')");
assertResult("2", stat, "SELECT COUNT(*) FROM B");
assertResult("1", stat, "SELECT COUNT(*) FROM B WHERE X IS NULL");
assertResult("0", stat, "SELECT COUNT(*) FROM B WHERE X = ''");
assertResult(new Object[][] { { 1, 5 }, { 2, null } }, stat, "SELECT * FROM B");
stat.execute("CREATE TABLE C (ID NUMBER, X TIMESTAMP)");
stat.execute("INSERT INTO C VALUES (1, '1979-11-12')");
stat.execute("INSERT INTO C VALUES (2, '')");
assertResult("2", stat, "SELECT COUNT(*) FROM C");
assertResult("1", stat, "SELECT COUNT(*) FROM C WHERE X IS NULL");
assertResult("0", stat, "SELECT COUNT(*) FROM C WHERE X = ''");
assertResult(new Object[][] { { 1, "1979-11-12 00:00:00.0" }, { 2, null } }, stat, "SELECT * FROM C");
stat.execute("CREATE TABLE D (ID NUMBER, X VARCHAR2(1))");
stat.execute("INSERT INTO D VALUES (1, 'a')");
stat.execute("SET @FOO = ''");
stat.execute("INSERT INTO D VALUES (2, @FOO)");
assertResult("2", stat, "SELECT COUNT(*) FROM D");
assertResult("1", stat, "SELECT COUNT(*) FROM D WHERE X IS NULL");
assertResult("0", stat, "SELECT COUNT(*) FROM D WHERE X = ''");
assertResult(new Object[][] { { 1, "a" }, { 2, null } }, stat, "SELECT * FROM D");
stat.execute("CREATE TABLE E (ID NUMBER, X RAW(1))");
stat.execute("INSERT INTO E VALUES (1, '0A')");
stat.execute("INSERT INTO E VALUES (2, '')");
assertResult("2", stat, "SELECT COUNT(*) FROM E");
assertResult("1", stat, "SELECT COUNT(*) FROM E WHERE X IS NULL");
assertResult("0", stat, "SELECT COUNT(*) FROM E WHERE X = ''");
assertResult(new Object[][] { { 1, new byte[] { 10 } }, { 2, null } }, stat, "SELECT * FROM E");
conn.close();
}
private void assertResult(Object[][] expectedRowsOfValues, Statement stat, String sql) throws SQLException {
assertResult(newSimpleResultSet(expectedRowsOfValues), stat, sql);
}
private void assertResult(ResultSet expected, Statement stat, String sql) throws SQLException {
ResultSet actual = stat.executeQuery(sql);
int expectedColumnCount = expected.getMetaData().getColumnCount();
assertEquals(expectedColumnCount, actual.getMetaData().getColumnCount());
while (true) {
boolean expectedNext = expected.next();
boolean actualNext = actual.next();
if (!expectedNext && !actualNext) {
return;
}
if (expectedNext != actualNext) {
fail("number of rows in actual and expected results sets does not match");
}
for (int i = 0; i < expectedColumnCount; i++) {
String expectedString = columnResultToString(expected.getObject(i + 1));
String actualString = columnResultToString(actual.getObject(i + 1));
assertEquals(expectedString, actualString);
}
}
}
private static String columnResultToString(Object object) {
if (object == null) {
return null;
}
if (object instanceof Object[]) {
return Arrays.deepToString(((Object[]) object));
}
if (object instanceof byte[]) {
return Arrays.toString(((byte[]) object));
}
return object.toString();
}
private static SimpleResultSet newSimpleResultSet(Object[][] rowsOfValues) {
SimpleResultSet result = new SimpleResultSet();
for (int i = 0; i < rowsOfValues[0].length; i++) {
result.addColumn(i + "", Types.JAVA_OBJECT, 0, 0);
}
for (int i = 0; i < rowsOfValues.length; i++) {
result.addRow(rowsOfValues[i]);
}
return result;
}
}
......@@ -742,6 +742,5 @@ layers waited descent spliced abstracts planning interest among sliced
lives pauses allocates kicks introduction straightforward getenv
ordinate tweaking fetching rfe yates cookie btrfs cookies
nocycle nomaxvalue nominvalue cycling proceed prospective exhausted contingent
validities hang degenerates freezes
validities hang degenerates freezes emulation gredler cemo koc blanked
reverting gredler blanked koc cemo jump
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论