Index: trac/ticket/model.py
===================================================================
--- trac/ticket/model.py	(revision 10949)
+++ trac/ticket/model.py	(working copy)
@@ -533,8 +533,9 @@
             cursor.execute("""
                 SELECT field FROM ticket_change 
                 WHERE ticket=%%s AND time=%%s AND field %s
-                """ % db.like(), (self.id, ts,
-                                  db.like_escape('_comment') + '%'))
+                """ % db.like(ignore_case=False),
+                (self.id, ts, db.like_pattern(['_comment', db.ANY_CHARS],
+                                              ignore_case=False)))
             fields = list(cursor)
             rev = fields and max(int(field[8:]) for field, in fields) + 1 or 0
             cursor.execute("""
@@ -549,8 +550,9 @@
                 cursor.execute("""
                     SELECT author FROM ticket_change 
                     WHERE ticket=%%s AND time=%%s AND NOT field %s
-                    LIMIT 1
-                    """ % db.like(), (self.id, ts, db.like_escape('_') + '%'))
+                    LIMIT 1""" % db.like(ignore_case=False),
+                    (self.id, ts, db.like_pattern(['_', db.ANY_CHARS],
+                                                  ignore_case=False)))
                 old_author = None
                 for old_author, in cursor:
                     break
@@ -584,8 +586,9 @@
                 SELECT field,author,oldvalue,newvalue 
                 FROM ticket_change 
                 WHERE ticket=%%s AND time=%%s AND field %s
-                """ % db.like(), (self.id, ts0,
-                                  db.like_escape('_comment') + '%'))
+                """ % db.like(ignore_case=False),
+                (self.id, ts0, db.like_pattern(['_comment', db.ANY_CHARS],
+                                               ignore_case=False)))
             rows = sorted((int(field[8:]), author, old, new)
                           for field, author, old, new in cursor)
             for rev, author, comment, ts in rows:
@@ -606,8 +609,9 @@
             SELECT time,author,newvalue FROM ticket_change 
             WHERE ticket=%%s AND field='comment' 
                 AND (oldvalue=%%s OR oldvalue %s)
-            """ % db.like(), (self.id, scnum,
-                              '%' + db.like_escape('.' + scnum)))
+            """ % db.like(ignore_case=False),
+            (self.id, scnum, db.like_pattern([db.ANY_CHARS, '.' + scnum],
+                                             ignore_case=False)))
         for row in cursor:
             return row
 
@@ -638,7 +642,9 @@
                 SELECT author FROM ticket_change 
                 WHERE ticket=%%s AND time=%%s AND NOT field %s 
                 LIMIT 1
-                """ % db.like(), (self.id, ts, db.like_escape('_') + '%'))
+                """ % db.like(ignore_case=False),
+                (self.id, ts, db.like_pattern(['_', db.ANY_CHARS],
+                                              ignore_case=False)))
             for author, in cursor:
                 break
         return (ts, author, comment)
Index: trac/versioncontrol/cache.py
===================================================================
--- trac/versioncontrol/cache.py	(revision 10949)
+++ trac/versioncontrol/cache.py	(working copy)
@@ -325,9 +325,11 @@
         sfirst = self.db_rev(first)
         cursor.execute("SELECT DISTINCT rev FROM node_change "
                        "WHERE repos=%%s AND rev>=%%s AND rev<=%%s "
-                       " AND (path=%%s OR path %s)" % db.like(),
+                       " AND (path=%%s OR path %s)" % \
+                       db.like(ignore_case=False),
                        (self.id, sfirst, slast, path,
-                        db.like_escape(path + '/') + '%'))
+                        db.like_pattern([path + '/', db.ANY_CHARS],
+                                        ignore_case=False)))
         return [int(row[0]) for row in cursor]
 
     def has_node(self, path, rev=None):
@@ -362,8 +364,9 @@
         if path:
             path = path.lstrip('/')
             # changes on path itself or its children
-            sql += " AND (path=%s OR path " + db.like()
-            args.extend((path, db.like_escape(path + '/') + '%'))
+            sql += " AND (path=%s OR path " + db.like(ignore_case=False)
+            args.extend((path, db.like_pattern([path + '/', db.ANY_CHARS],
+                                               ignore_case=False)))
             # deletion of path ancestors
             components = path.lstrip('/').split('/')
             parents = ','.join(('%s',) * len(components))
Index: trac/db/sqlite_backend.py
===================================================================
--- trac/db/sqlite_backend.py	(revision 10949)
+++ trac/db/sqlite_backend.py	(working copy)
@@ -26,6 +26,7 @@
 from trac.util.translation import _
 
 _like_escape_re = re.compile(r'([/_%])')
+_glob_escape_re = re.compile(r'[*?[]')
 
 try:
     import pysqlite2.dbapi2 as sqlite
@@ -297,19 +298,47 @@
     def concat(self, *args):
         return '||'.join(args)
 
-    def like(self):
-        """Return a case-insensitive LIKE clause."""
-        if sqlite_version >= (3, 1, 0):
-            return "LIKE %s ESCAPE '/'"
+    def like(self, ignore_case=True):
+        """Return a LIKE clause."""
+        if ignore_case:
+            if sqlite_version >= (3, 1, 0):
+                return "LIKE %s ESCAPE '/'"
+            else:
+                return 'LIKE %s'
         else:
-            return 'LIKE %s'
+            return 'GLOB %s'
 
-    def like_escape(self, text):
-        if sqlite_version >= (3, 1, 0):
-            return _like_escape_re.sub(r'/\1', text)
+    def like_escape(self, text, ignore_case=True):
+        if ignore_case:
+            if sqlite_version >= (3, 1, 0):
+                return _like_escape_re.sub(r'/\1', text)
+            else:
+                return text
         else:
-            return text
+            return _glob_escape_re.sub(r'[\g<0>]', text)
 
+    def like_pattern(self, texts, ignore_case=True):
+        """Return a pattern for LIKE operator."""
+        if ignore_case:
+            any_chars = '%'
+            single_char = '_'
+        else:
+            any_chars = '*'
+            single_char = '?'
+
+        pattern = []
+        if not isinstance(texts, (list, tuple)):
+            texts = [texts]
+        for text in texts:
+            if text is self.ANY_CHARS:
+                text = any_chars
+            elif text is self.SINGLE_CHAR:
+                text = single_char
+            else:
+                text = self.like_escape(text, ignore_case=ignore_case)
+            pattern.append(text)
+        return ''.join(pattern)
+
     def quote(self, identifier):
         """Return the quoted identifier."""
         return "`%s`" % identifier.replace('`', '``')
Index: trac/db/mysql_backend.py
===================================================================
--- trac/db/mysql_backend.py	(revision 10949)
+++ trac/db/mysql_backend.py	(working copy)
@@ -242,11 +242,14 @@
     def concat(self, *args):
         return 'concat(%s)' % ', '.join(args)
 
-    def like(self):
-        """Return a case-insensitive LIKE clause."""
-        return "LIKE %s COLLATE utf8_general_ci ESCAPE '/'"
+    def like(self, ignore_case=True):
+        """Return a LIKE clause."""
+        if ignore_case:
+            return "LIKE %s COLLATE utf8_general_ci ESCAPE '/'"
+        else:
+            return "LIKE %s ESCAPE '/'"
 
-    def like_escape(self, text):
+    def like_escape(self, text, ignore_case=True):
         return _like_escape_re.sub(r'/\1', text)
 
     def quote(self, identifier):
Index: trac/db/postgres_backend.py
===================================================================
--- trac/db/postgres_backend.py	(revision 10949)
+++ trac/db/postgres_backend.py	(working copy)
@@ -234,11 +234,14 @@
     def concat(self, *args):
         return '||'.join(args)
 
-    def like(self):
-        """Return a case-insensitive LIKE clause."""
-        return "ILIKE %s ESCAPE '/'"
+    def like(self, ignore_case=True):
+        """Return a LIKE clause."""
+        if ignore_case:
+            return "ILIKE %s ESCAPE '/'"
+        else:
+            return "LIKE %s ESCAPE '/'"
 
-    def like_escape(self, text):
+    def like_escape(self, text, ignore_case=True):
         return _like_escape_re.sub(r'/\1', text)
 
     def quote(self, identifier):
Index: trac/db/util.py
===================================================================
--- trac/db/util.py	(revision 10949)
+++ trac/db/util.py	(working copy)
@@ -94,9 +94,26 @@
     """
     __slots__ = ('cnx', 'log')
 
+    ANY_CHARS = object()
+    SINGLE_CHAR = object()
+
     def __init__(self, cnx, log=None):
         self.cnx = cnx
         self.log = log
 
     def __getattr__(self, name):
         return getattr(self.cnx, name)
+
+    def like_pattern(self, texts, ignore_case=True):
+        pattern = []
+        if not isinstance(texts, (list, tuple)):
+            texts = [texts]
+        for text in texts:
+            if text is self.ANY_CHARS:
+                text = '%'
+            elif text is self.SINGLE_CHAR:
+                text = '_'
+            else:
+                text = self.like_escape(text, ignore_case=ignore_case)
+            pattern.append(text)
+        return ''.join(pattern)

