Index: includes/database/database.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/database/database.inc,v
retrieving revision 1.85
diff -u -9 -p -r1.85 database.inc
--- includes/database/database.inc	4 Dec 2009 16:31:04 -0000	1.85
+++ includes/database/database.inc	9 Dec 2009 19:26:51 -0000
@@ -2229,18 +2229,37 @@ function db_is_active() {
  *   The table name to escape.
  * @return
  *   The escaped table name as a string.
  */
 function db_escape_table($table) {
   return Database::getConnection()->escapeTable($table);
 }
 
 /**
+ * Escape characters that work as wildcard characters in a LIKE pattern.
+ *
+ * The wildcard characters "%" and "_" as well as backslash are prefixed with
+ * a backslash. Use this to do a seach for a verbatim string without any
+ * wildcard behavior.
+ *
+ * Backslash is defined as escape character for LIKE patterns in
+ * DatabaseCondition::mapConditionOperator().
+ *
+ * @param $string
+ *   The string to escape.
+ * @return
+ *   The escaped string.
+ */
+function db_escape_like($string) {
+  return addcslashes($string, '\%_');
+}
+
+/**
  * Retrieve the name of the currently active database driver, such as
  * "mysql" or "pgsql".
  *
  * @return The name of the currently active database driver.
  */
 function db_driver() {
   return Database::getConnection()->driver();
 }
 
Index: includes/database/query.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/database/query.inc,v
retrieving revision 1.36
diff -u -9 -p -r1.36 query.inc
--- includes/database/query.inc	4 Dec 2009 16:31:04 -0000	1.36
+++ includes/database/query.inc	9 Dec 2009 19:26:51 -0000
@@ -1324,25 +1325,26 @@ class DatabaseCondition implements Query
    */
   protected function mapConditionOperator($operator) {
     // $specials does not use drupal_static as its value never changes.
     static $specials = array(
       'BETWEEN' => array('delimiter' => ' AND '),
       'IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'),
       'NOT IN' => array('delimiter' => ', ', 'prefix' => ' (', 'postfix' => ')'),
       'IS NULL' => array('use_value' => FALSE),
       'IS NOT NULL' => array('use_value' => FALSE),
+      // Use backslash for escaping wildcard characters.
+      'LIKE' => array('postfix' => " ESCAPE '\\\\'"),
       // These ones are here for performance reasons.
       '=' => array(),
       '<' => array(),
       '>' => array(),
       '>=' => array(),
       '<=' => array(),
-      'LIKE' => array(),
     );
     if (isset($specials[$operator])) {
       $return = $specials[$operator];
     }
     else {
       // We need to upper case because PHP index matches are case sensitive but
       // do not need the more expensive drupal_strtoupper because SQL statements are ASCII.
       $operator = strtoupper($operator);
       $return = isset($specials[$operator]) ? $specials[$operator] : array();
Index: includes/database/pgsql/database.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/database/pgsql/database.inc,v
retrieving revision 1.30
diff -u -9 -p -r1.30 database.inc
--- includes/database/pgsql/database.inc	18 Sep 2009 00:04:21 -0000	1.30
+++ includes/database/pgsql/database.inc	9 Dec 2009 19:26:51 -0000
@@ -114,20 +114,21 @@ class DatabaseConnection_pgsql extends D
   }
 
   public function databaseType() {
     return 'pgsql';
   }
 
   public function mapConditionOperator($operator) {
     static $specials = array(
       // In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE
-      // statements, we need to use ILIKE instead.
-      'LIKE' => array('operator' => 'ILIKE'),
+      // statements, we need to use ILIKE instead. Use backslash for escaping
+      // wildcard characters.
+      'LIKE' => array('operator' => 'ILIKE', 'postfix' => " ESCAPE '\\\\'"),
     );
 
     return isset($specials[$operator]) ? $specials[$operator] : NULL;
   }
 }
 
 /**
  * @} End of "ingroup database".
  */
Index: modules/simpletest/tests/database_test.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/database_test.test,v
retrieving revision 1.77
diff -u -9 -p -r1.77 database_test.test
--- modules/simpletest/tests/database_test.test	9 Dec 2009 19:01:14 -0000	1.77
+++ modules/simpletest/tests/database_test.test	9 Dec 2009 19:26:51 -0000
@@ -2637,18 +2637,75 @@ class DatabaseAnsiSyntaxTestCase extends
   function testFieldConcat() {
     $result = db_query('SELECT :a1 || name || :a2 || age || :a3 FROM {test} WHERE age = :age', array(
       ':a1' => 'The age of ',
       ':a2' => ' is ',
       ':a3' => '.',
       ':age' => 25,
     ));
     $this->assertIdentical($result->fetchField(), 'The age of John is 25.', t('Field ANSI Concat works.'));
   }
+
+  /**
+   * Test escaping of LIKE wildcards.
+   */
+  function testLikeEscape() {
+    db_insert('test')
+      ->fields(array(
+        'name' => 'Ring_',
+      ))
+      ->execute();
+
+    // Match both "Ringo" and "Ring_".
+    $num_matches = db_select('test', 't')
+      ->condition('name', 'Ring_', 'LIKE')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertIdentical($num_matches, '2', t('Found 2 records.'));
+    // Match only "Ring_" using a LIKE expression with no wildcards.
+    $num_matches = db_select('test', 't')
+      ->condition('name', db_escape_like('Ring_'), 'LIKE')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertIdentical($num_matches, '1', t('Found 1 record.'));
+  }
+
+  /**
+   * Test LIKE query containing a backslash.
+   */
+  function testLikeBackslash() {
+    db_insert('test')
+      ->fields(array('name'))
+      ->values(array(
+        'name' => 'abcde\f',
+      ))
+      ->values(array(
+        'name' => 'abc%\_',
+      ))
+      ->execute();
+
+    // Match both rows using a LIKE expression with two wildcards and a verbatim
+    // backslash.
+    $num_matches = db_select('test', 't')
+      ->condition('name', 'abc%\\\\_', 'LIKE')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertIdentical($num_matches, '2', t('Found 2 records.'));
+    // Match only the former using a LIKE expression with no wildcards.
+    $num_matches = db_select('test', 't')
+      ->condition('name', db_escape_like('abc%\_'), 'LIKE')
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertIdentical($num_matches, '1', t('Found 1 record.'));
+  }
 }
 
 /**
  * Test invalid data handling.
  */
 class DatabaseInvalidDataTestCase extends DatabaseTestCase {
   public static function getInfo() {
     return array(
       'name' => 'Invalid data',
