fix for bug: mozStorage doesn't handle unicode in LIKE, UPPER, or LOWER functions patch=Shawn Wilsher <sdwilsh@mozilla.com> r=sspitzer
authorsspitzer@mozilla.org
Fri, 10 Aug 2007 09:19:57 -0700
changeset 4458 7d05eb4c9f348f942bda3d6985c0c637f5612372
parent 4457 f1c56c8f1941efacbf99aa7f593cf4a76cf18405
child 4459 4b5f4fad0f8f54070890afd52b07a5cdc090da0b
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssspitzer
milestone1.9a8pre
fix for bug: mozStorage doesn't handle unicode in LIKE, UPPER, or LOWER functions patch=Shawn Wilsher <sdwilsh@mozilla.com> r=sspitzer
storage/build/Makefile.in
storage/src/Makefile.in
storage/src/mozStorageConnection.cpp
storage/src/mozStorageUnicodeFunctions.cpp
storage/src/mozStorageUnicodeFunctions.h
storage/test/unit/test_like.js
storage/test/unit/test_unicode.js
--- a/storage/build/Makefile.in
+++ b/storage/build/Makefile.in
@@ -54,16 +54,17 @@ LIBXUL_LIBRARY = 1
 
 REQUIRES = \
 	storage \
 	sqlite3 \
 	xpcom \
 	string \
 	js \
 	xpconnect \
+	intl \
 	$(NULL)
 
 EXPORTS = mozStorageCID.h
 
 CPPSRCS = mozStorageModule.cpp
 
 LOCAL_INCLUDES = \
 	-I$(srcdir)/../src
@@ -72,14 +73,15 @@ SHARED_LIBRARY_LIBS = \
 	../src/$(LIB_PREFIX)storage_s.$(LIB_SUFFIX) \
 	$(NULL)
 
 EXTRA_DSO_LIBS += sqlite3
 
 EXTRA_DSO_LDOPTS += \
 	$(LIBS_DIR) \
 	$(EXTRA_DSO_LIBS) \
+	$(MOZ_UNICHARUTIL_LIBS) \
 	$(MOZ_COMPONENT_LIBS) \
 	$(MOZ_JS_LIBS) \
 	$(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
--- a/storage/src/Makefile.in
+++ b/storage/src/Makefile.in
@@ -61,11 +61,16 @@ REQUIRES = xpcom \
 
 CPPSRCS = \
 	mozStorageAsyncIO.cpp \
 	mozStorageService.cpp \
 	mozStorageConnection.cpp \
 	mozStorageStatement.cpp \
 	mozStorageStatementWrapper.cpp \
 	mozStorageValueArray.cpp \
+	mozStorageUnicodeFunctions.cpp \
 	$(NULL)
 
+# This is the default value.  If we ever change it when compiling sqlite, we
+# will need to change it here as well.
+DEFINES += -DSQLITE_MAX_LIKE_PATTERN_LENGTH=50000
+
 include $(topsrcdir)/config/rules.mk
--- a/storage/src/mozStorageConnection.cpp
+++ b/storage/src/mozStorageConnection.cpp
@@ -47,16 +47,17 @@
 #include "nsHashSets.h"
 #include "nsAutoPtr.h"
 #include "nsIFile.h"
 #include "nsIVariant.h"
 
 #include "mozIStorageAggregateFunction.h"
 #include "mozIStorageFunction.h"
 
+#include "mozStorageUnicodeFunctions.h"
 #include "mozStorageConnection.h"
 #include "mozStorageService.h"
 #include "mozStorageStatement.h"
 #include "mozStorageValueArray.h"
 #include "mozStorage.h"
 
 #include "prlog.h"
 #include "prprf.h"
@@ -130,16 +131,23 @@ mozStorageConnection::Initialize(nsIFile
 
 #ifdef PR_LOGGING
     if (! gStorageLog)
         gStorageLog = PR_NewLogModule("mozStorage");
 
     sqlite3_trace (mDBConn, tracefunc, nsnull);
 #endif
 
+    // Hook up i18n functions
+    srv = StorageUnicodeFunctions::RegisterFunctions(mDBConn);
+    if (srv != SQLITE_OK) {
+        mDBConn = nsnull;
+        return ConvertResultCode(srv);
+    }
+
     /* Execute a dummy statement to force the db open, and to verify
      * whether it's valid or not
      */
     sqlite3_stmt *stmt = nsnull;
     nsCString query("SELECT * FROM sqlite_master");
     srv = sqlite3_prepare_v2(mDBConn, query.get(), query.Length(), &stmt, NULL);
 
     if (srv == SQLITE_OK) {
new file mode 100644
--- /dev/null
+++ b/storage/src/mozStorageUnicodeFunctions.cpp
@@ -0,0 +1,211 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is unicode functions code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation. 
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * This code is based off of icu.c from the sqlite code
+ * whose original author is danielk1977
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#include "mozStorageUnicodeFunctions.h"
+#include "nsUnicharUtils.h"
+
+int
+StorageUnicodeFunctions::RegisterFunctions(sqlite3 *aDB)
+{
+  struct Functions {
+    const char *zName;
+    int nArg;
+    int enc;
+    void *pContext;
+    void (*xFunc)(sqlite3_context*, int, sqlite3_value**);
+  } functions[] = {
+    {"lower", 1, SQLITE_UTF16, 0,        caseFunction},
+    {"lower", 1, SQLITE_UTF8,  0,        caseFunction},
+    {"upper", 1, SQLITE_UTF16, (void*)1, caseFunction},
+    {"upper", 1, SQLITE_UTF8,  (void*)1, caseFunction},
+
+    {"like",  2, SQLITE_UTF16, 0,        likeFunction},
+    {"like",  2, SQLITE_UTF8,  0,        likeFunction},
+    {"like",  3, SQLITE_UTF16, 0,        likeFunction},
+    {"like",  3, SQLITE_UTF8,  0,        likeFunction},
+  };
+
+  int rv = SQLITE_OK;
+  for (unsigned i = 0; SQLITE_OK == rv && i < NS_ARRAY_LENGTH(functions); ++i) {
+    struct Functions *p = &functions[i];
+    rv = sqlite3_create_function(aDB, p->zName, p->nArg, p->enc, p->pContext,
+                                 p->xFunc, NULL, NULL);
+  }
+
+  return rv;
+}
+
+void
+StorageUnicodeFunctions::caseFunction(sqlite3_context *p,
+                                      int aArgc,
+                                      sqlite3_value **aArgv)
+{
+  NS_ASSERTION(1 == aArgc, "Invalid number of arguments!");
+
+  nsAutoString data(static_cast<const PRUnichar *>(sqlite3_value_text16(aArgv[0])));
+  PRBool toUpper = sqlite3_user_data(p) ? PR_TRUE : PR_FALSE;
+
+  if (toUpper)
+    ToUpperCase(data);
+  else 
+    ToLowerCase(data);
+
+  // Give sqlite our result
+  sqlite3_result_text16(p, nsPromiseFlatString(data).get(), -1,
+                        SQLITE_TRANSIENT);
+}
+
+static int
+likeCompare(nsAString::const_iterator aPatternItr,
+            nsAString::const_iterator aPatternEnd,
+            nsAString::const_iterator aStringItr,
+            nsAString::const_iterator aStringEnd,
+            const nsAString::char_type *aEscape)
+{
+  const PRUnichar MATCH_ALL('%');
+  const PRUnichar MATCH_ONE('_');
+
+  PRBool lastWasEscape = PR_FALSE;
+  while (aPatternItr != aPatternEnd) {
+    /**
+     * What we do in here is take a look at each character from the input
+     * pattern, and do something with it.  There are 4 possibilities:
+     * 1) character is an un-escaped match-all character
+     * 2) character is an un-escaped match-one character
+     * 3) character is an un-escaped escape character
+     * 4) character is not any of the above
+     */
+    if (!lastWasEscape && *aPatternItr == MATCH_ALL) {
+      // CASE 1
+      /**
+       * Now we need to skip any MATCH_ALL or MATCH_ONE characters that follow a
+       * MATCH_ALL character.  For each MATCH_ONE character, skip one character
+       * in the pattern string.
+       */
+      while (*aPatternItr == MATCH_ALL || *aPatternItr == MATCH_ONE) {
+        if (*aPatternItr == MATCH_ONE) {
+          // If we've hit the end of the string we are testing, no match
+          if (aStringItr == aStringEnd)
+            return 0;
+          aStringItr++;
+        }
+        aPatternItr++;
+      }
+
+      // If we've hit the end of the pattern string, match
+      if (aPatternItr == aPatternEnd)
+        return 1;
+
+      while (aStringItr != aStringEnd) {
+        if (likeCompare(aPatternItr, aPatternEnd, aStringItr, aStringEnd, aEscape)) {
+          // we've hit a match, so indicate this
+          return 1;
+        }
+        aStringItr++;
+      }
+
+      // No match
+      return 0;
+    } else if (!lastWasEscape && *aPatternItr == MATCH_ONE) {
+      // CASE 2
+      if (aStringItr == aStringEnd) {
+        // If we've hit the end of the string we are testing, no match
+        return 0;
+      }
+      aStringItr++;
+      lastWasEscape = PR_FALSE;
+    } else if (!lastWasEscape && *aPatternItr == *aEscape) {
+      // CASE 3
+      lastWasEscape = PR_TRUE;
+    } else {
+      // CASE 4
+      if (ToUpperCase(*aStringItr) != ToUpperCase(*aPatternItr)) {
+        // If we've hit a point where the strings don't match, there is no match
+        return 0;
+      }
+      aStringItr++;
+      lastWasEscape = PR_FALSE;
+    }
+    
+    aPatternItr++;
+  }
+
+  return aStringItr == aStringEnd;
+}
+
+/**
+ * This implements the like() SQL function.  This is used by the LIKE operator.
+ * The SQL statement 'A LIKE B' is implemented as 'like(B, A)', and if there is
+ * an escape character, say E, it is implemented as 'like(B, A, E)'.
+ */
+void
+StorageUnicodeFunctions::likeFunction(sqlite3_context *p,
+                                      int aArgc,
+                                      sqlite3_value **aArgv)
+{
+  NS_ASSERTION(2 == aArgc || 3 == aArgc, "Invalid number of arguments!");
+
+  if (sqlite3_value_bytes(aArgv[0]) > SQLITE_MAX_LIKE_PATTERN_LENGTH) {
+    sqlite3_result_error(p, "LIKE or GLOB pattern too complex", SQLITE_TOOBIG);
+    return;
+  }
+
+  nsAutoString A(static_cast<const PRUnichar *>(sqlite3_value_text16(aArgv[1])));
+  nsAutoString B(static_cast<const PRUnichar *>(sqlite3_value_text16(aArgv[0])));
+  NS_ASSERTION(B.Length() != 0, "LIKE string must not be null!");
+
+  nsAutoString E;
+  if (3 == aArgc) {
+    E = static_cast<const PRUnichar *>(sqlite3_value_text16(aArgv[2]));
+    NS_ASSERTION(E.Length() == 1, "ESCAPE express must be a single character");
+  }
+
+  nsAString::const_iterator itrString, endString;
+  A.BeginReading(itrString);
+  A.EndReading(endString);
+  nsAString::const_iterator itrPattern, endPattern;
+  B.BeginReading(itrPattern);
+  B.EndReading(endPattern);
+  sqlite3_result_int(p, likeCompare(itrPattern, endPattern,
+                                    itrString, endString, E.get()));
+}
+
new file mode 100644
--- /dev/null
+++ b/storage/src/mozStorageUnicodeFunctions.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is unicode functions code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation. 
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+#ifndef _mozStorageUnicodeFunctions_h_
+#define _mozStorageUnicodeFunctions_h_
+
+#include "sqlite3.h"
+#include "nscore.h"
+
+namespace StorageUnicodeFunctions {
+
+  /**
+   * Registers the functions with sqlite.
+   *
+   * @param aDB
+   *        The database we'll be registering the functions with.
+   * @return the sqlite status code.
+   */
+  NS_HIDDEN_(int) RegisterFunctions(sqlite3 *aDB);
+
+  /**
+   * Does the upper and lowercase conversions for the SQL functions upper() and
+   * lower().
+   *
+   * @param p
+   *        The sqlite_context that this function is being called on.
+   * @param aArgc
+   *        The number of arguments the function is being called with.
+   * @param aArgv
+   *        An array of the arguments the functions is being called with.
+   */
+  NS_HIDDEN_(void) caseFunction(sqlite3_context *p,
+                                int aArgc,
+                                sqlite3_value **aArgv);
+
+  /**
+   * Performs the LIKE comparison in sqlite.
+   *
+   * @param p
+   *        The sqlite_context that this function is being called on.
+   * @param aArgc
+   *        The number of arguments the function is being called with.
+   * @param aArgv
+   *        An array of the arguments the functions is being called with.
+   */
+  NS_HIDDEN_(void) likeFunction(sqlite3_context *p,
+                                int aArgc,
+                                sqlite3_value **aArgv);
+}
+
+#endif // _mozStorageUnicodeFunctions_h_
new file mode 100644
--- /dev/null
+++ b/storage/test/unit/test_like.js
@@ -0,0 +1,209 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Storage Test Code.
+ *
+ * The Initial Developer of the Original Code is
+ *   Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * This code is based off of like.test from the sqlite code
+ *
+ * Contributor(s):
+ *   Seth Spitzer <sspitzer@mozilla.org> (Original Author)
+ *   Shawn Wilsher <me@shawnwilsher.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// This file tests our LIKE implementation since we override it for unicode
+
+function setup()
+{
+  getOpenedDatabase().createTable("t1", "x TEXT");
+
+  var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')");
+  stmt.execute();
+
+  stmt = createStatement("INSERT INTO t1 (x) VALUES ('ab')");
+  stmt.execute();
+
+  stmt = createStatement("INSERT INTO t1 (x) VALUES ('abc')");
+  stmt.execute();
+
+  stmt = createStatement("INSERT INTO t1 (x) VALUES ('abcd')");
+  stmt.execute();
+
+  stmt = createStatement("INSERT INTO t1 (x) VALUES ('acd')");
+  stmt.execute();
+
+  stmt = createStatement("INSERT INTO t1 (x) VALUES ('abd')");
+  stmt.execute();
+
+  stmt = createStatement("INSERT INTO t1 (x) VALUES ('bc')");
+  stmt.execute();
+
+  stmt = createStatement("INSERT INTO t1 (x) VALUES ('bcd')");
+  stmt.execute();
+
+  stmt = createStatement("INSERT INTO t1 (x) VALUES ('xyz')");
+  stmt.execute();
+
+  stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC')");
+  stmt.execute();
+
+  stmt = createStatement("INSERT INTO t1 (x) VALUES ('CDE')");
+  stmt.execute();
+
+  stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC abc xyz')");
+  stmt.execute();
+  stmt.reset();
+}
+
+function test_count()
+{
+  var stmt = createStatement("SELECT count(*) FROM t1;");
+  do_check_true(stmt.executeStep());
+  do_check_eq(stmt.getInt32(0), 12);
+  stmt.reset();
+}
+
+function test_like_1()
+{
+  var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE 'abc';");
+  var solutions = ["abc", "ABC"];
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_false(stmt.executeStep());
+  stmt.reset();
+}
+
+function test_like_2()
+{
+  var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE 'ABC';");
+  var solutions = ["abc", "ABC"];
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_false(stmt.executeStep());
+  stmt.reset();
+}
+    
+function test_like_3()
+{
+  var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE 'aBc';");
+  var solutions = ["abc", "ABC"];
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_false(stmt.executeStep());
+  stmt.reset();
+}
+   
+function test_like_4()
+{
+  var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE 'abc%';");
+  var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"];
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_false(stmt.executeStep());
+  stmt.reset();
+}
+
+function test_like_5()
+{
+  var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE 'a_c';");
+  var solutions = ["abc", "ABC"];
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_false(stmt.executeStep());
+  stmt.reset();
+}
+
+function test_like_6()
+{
+  var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE 'ab%d';");
+  var solutions = ["abcd", "abd"];
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_false(stmt.executeStep());
+  stmt.reset();
+}
+    
+function test_like_7()
+{
+  var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE 'a_c%';");
+  var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"];
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_false(stmt.executeStep());
+  stmt.reset();
+}
+
+function test_like_8()
+{
+  var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE '%bcd';");
+  var solutions = ["abcd", "bcd"];
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_true(stmt.executeStep());
+  do_check_true(solutions.indexOf(stmt.getString(0)) != -1);
+  do_check_false(stmt.executeStep());
+  stmt.reset();
+}
+    
+var tests = [test_count, test_like_1, test_like_2, test_like_3, test_like_4, 
+             test_like_5, test_like_6, test_like_7, test_like_8];
+
+function run_test()
+{
+  setup();
+
+  for (var i = 0; i < tests.length; i++)
+    tests[i]();
+    
+  cleanup();
+}
+
new file mode 100644
--- /dev/null
+++ b/storage/test/unit/test_unicode.js
@@ -0,0 +1,129 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Storage Test Code.
+ *
+ * The Initial Developer of the Original Code is
+ *   Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2007
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+// This file tests the unicode functions that we have added
+
+const LATIN1_AE = "\xc6"; // "Æ"
+const LATIN1_ae = "\xe6";  // "æ"
+
+function setup()
+{
+  getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT");
+
+  var stmt = createStatement("INSERT INTO test (name, id) VALUES (?1, ?2)");
+  stmt.bindStringParameter(0, LATIN1_AE);
+  stmt.bindInt32Parameter(1, 1);
+  stmt.execute();
+  stmt.bindStringParameter(0, "A");
+  stmt.bindInt32Parameter(1, 2);
+  stmt.execute();
+  stmt.bindStringParameter(0, "b");
+  stmt.bindInt32Parameter(1, 3);
+  stmt.execute();
+  stmt.bindStringParameter(0, LATIN1_ae);
+  stmt.bindInt32Parameter(1, 4);
+  stmt.execute();
+}
+
+function test_upper_ascii()
+{
+  var stmt = createStatement("SELECT name, id FROM test WHERE name = upper('a')");
+  do_check_true(stmt.executeStep());
+  do_check_eq("A", stmt.getString(0));
+  do_check_eq(2, stmt.getInt32(1));
+  stmt.reset();
+}
+
+function test_upper_non_ascii()
+{
+  var stmt = createStatement("SELECT name, id FROM test WHERE name = upper(?1)");
+  stmt.bindStringParameter(0, LATIN1_ae);
+  do_check_true(stmt.executeStep());
+  do_check_eq(LATIN1_AE, stmt.getString(0));
+  do_check_eq(1, stmt.getInt32(1));
+  stmt.reset();
+}
+
+function test_lower_ascii()
+{
+  var stmt = createStatement("SELECT name, id FROM test WHERE name = lower('B')");
+  do_check_true(stmt.executeStep());
+  do_check_eq("b", stmt.getString(0));
+  do_check_eq(3, stmt.getInt32(1));
+  stmt.reset();
+}
+
+function test_lower_non_ascii()
+{
+  var stmt = createStatement("SELECT name, id FROM test WHERE name = lower(?1)");
+  stmt.bindStringParameter(0, LATIN1_AE);
+  do_check_true(stmt.executeStep());
+  do_check_eq(LATIN1_ae, stmt.getString(0));
+  do_check_eq(4, stmt.getInt32(1));
+  stmt.reset();
+}
+
+function test_like_search_different()
+{
+  var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1");
+  stmt.bindStringParameter(0, LATIN1_AE);
+  do_check_true(stmt.executeStep());
+  do_check_eq(2, stmt.getInt32(0));
+}
+
+function test_like_search_same()
+{
+  var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1");
+  stmt.bindStringParameter(0, LATIN1_ae);
+  do_check_true(stmt.executeStep());
+  do_check_eq(2, stmt.getInt32(0));
+}
+
+var tests = [test_upper_ascii, test_upper_non_ascii, test_lower_ascii,
+             test_lower_non_ascii, test_like_search_different,
+             test_like_search_same];
+
+function run_test()
+{
+  setup();
+
+  for (var i = 0; i < tests.length; i++)
+    tests[i]();
+    
+  cleanup();
+}
+