Bug 443068 - Move triggers into migration code
authorShawn Wilsher <sdwilsh@shawnwilsher.com>
Mon, 04 Aug 2008 12:59:56 -0400
changeset 16356 438cea3432bff642d53f1550a797a219e8b93fc1
parent 16355 410610fd3b950a4d3dc90029f42bfceaf30a50c9
child 16357 90020c4ad44663d68be8cd2aa179c93b1718b9d7
push idunknown
push userunknown
push dateunknown
bugs443068
milestone1.9.1a2pre
Bug 443068 - Move triggers into migration code This moves all the triggers created by places into the appropriate migration functions. This saves us two queries to sqlite_master every time the places service starts up. r=dietrich
toolkit/components/places/src/nsNavBookmarks.cpp
toolkit/components/places/src/nsNavHistory.cpp
toolkit/components/places/src/nsNavHistory.h
toolkit/components/places/src/nsPlacesTriggers.h
--- a/toolkit/components/places/src/nsNavBookmarks.cpp
+++ b/toolkit/components/places/src/nsNavBookmarks.cpp
@@ -45,16 +45,17 @@
 #include "nsIDynamicContainer.h"
 #include "nsUnicharUtils.h"
 #include "nsFaviconService.h"
 #include "nsAnnotationService.h"
 #include "nsPrintfCString.h"
 #include "nsIUUIDGenerator.h"
 #include "prprf.h"
 #include "nsILivemarkService.h"
+#include "nsPlacesTriggers.h"
 
 const PRInt32 nsNavBookmarks::kFindBookmarksIndex_ID = 0;
 const PRInt32 nsNavBookmarks::kFindBookmarksIndex_Type = 1;
 const PRInt32 nsNavBookmarks::kFindBookmarksIndex_ForeignKey = 2;
 const PRInt32 nsNavBookmarks::kFindBookmarksIndex_Parent = 3;
 const PRInt32 nsNavBookmarks::kFindBookmarksIndex_Position = 4;
 const PRInt32 nsNavBookmarks::kFindBookmarksIndex_Title = 5;
 
@@ -377,16 +378,20 @@ nsNavBookmarks::InitTables(mozIStorageCo
   rv = aDBConn->TableExists(NS_LITERAL_CSTRING("moz_keywords"), &exists);
   NS_ENSURE_SUCCESS(rv, rv);
   if (! exists) {
     rv = aDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
         "CREATE TABLE moz_keywords ("
         "id INTEGER PRIMARY KEY AUTOINCREMENT, "
         "keyword TEXT UNIQUE)"));
     NS_ENSURE_SUCCESS(rv, rv);
+
+    // Create trigger to update as well
+    rv = aDBConn->ExecuteSimpleSQL(CREATE_KEYWORD_VALIDITY_TRIGGER);
+    NS_ENSURE_SUCCESS(rv, rv);
   }
 
   return NS_OK;
 }
 
 
 // nsNavBookmarks::InitRoots
 //
--- a/toolkit/components/places/src/nsNavHistory.cpp
+++ b/toolkit/components/places/src/nsNavHistory.cpp
@@ -80,16 +80,17 @@
 
 #include "mozIStorageService.h"
 #include "mozIStorageConnection.h"
 #include "mozIStorageValueArray.h"
 #include "mozIStorageStatement.h"
 #include "mozIStorageFunction.h"
 #include "mozStorageCID.h"
 #include "mozStorageHelper.h"
+#include "nsPlacesTriggers.h"
 #include "nsAppDirectoryServiceDefs.h"
 #include "nsIIdleService.h"
 #include "nsILivemarkService.h"
 
 #include "nsMathUtils.h" // for NS_ceilf()
 
 // Microsecond timeout for "recent" events such as typed and bookmark following.
 // If you typed it more than this time ago, it's not recent.
@@ -627,17 +628,17 @@ nsNavHistory::InitDBFile(PRBool aForceIn
 
   return NS_OK;
 }
 
 // nsNavHistory::InitDB
 //
 
 
-#define PLACES_SCHEMA_VERSION 6
+#define PLACES_SCHEMA_VERSION 7
 
 nsresult
 nsNavHistory::InitDB(PRInt16 *aMadeChanges)
 {
   nsresult rv;
   PRBool tableExists;
   *aMadeChanges = DB_MIGRATION_NONE;
 
@@ -713,17 +714,23 @@ nsNavHistory::InitDB(PRInt16 *aMadeChang
       }
 
       // Migrate anno tables up to V6
       if (DBSchemaVersion < 6) {
         rv = MigrateV6Up(mDBConn);
         NS_ENSURE_SUCCESS(rv, rv);
       }
 
-      // XXX Upgrades >V6 must add migration code here.
+      // Migrate historyvisits and bookmarks up to V7
+      if (DBSchemaVersion < 7) {
+        rv = MigrateV7Up(mDBConn);
+        NS_ENSURE_SUCCESS(rv, rv);
+      }
+
+      // XXX Upgrades >V7 must add migration code here.
 
     } else {
       // Downgrading
 
       // XXX Need to prompt user or otherwise notify of 
       // potential dataloss when downgrading.
 
       // XXX Downgrades from >V6 must add migration code here.
@@ -841,16 +848,22 @@ nsNavHistory::InitDB(PRInt16 *aMadeChang
     // finding bookmark redirects using the referring page. 
     rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
         "CREATE INDEX moz_historyvisits_fromindex ON moz_historyvisits (from_visit)"));
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
         "CREATE INDEX moz_historyvisits_dateindex ON moz_historyvisits (visit_date)"));
     NS_ENSURE_SUCCESS(rv, rv);
+
+    // Create our triggers for this table
+    rv = mDBConn->ExecuteSimpleSQL(CREATE_VISIT_COUNT_INSERT_TRIGGER);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = mDBConn->ExecuteSimpleSQL(CREATE_VISIT_COUNT_DELETE_TRIGGER);
+    NS_ENSURE_SUCCESS(rv, rv);
   }
 
   // moz_inputhistory
   rv = mDBConn->TableExists(NS_LITERAL_CSTRING("moz_inputhistory"), &tableExists);
   NS_ENSURE_SUCCESS(rv, rv);
   if (!tableExists) {
     rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("CREATE TABLE moz_inputhistory ("
         "place_id INTEGER NOT NULL, "
@@ -864,142 +877,28 @@ nsNavHistory::InitDB(PRInt16 *aMadeChang
   rv = EnsureCurrentSchema(mDBConn, &migrated);
   NS_ENSURE_SUCCESS(rv, rv);
   if (migrated && *aMadeChanges != DB_MIGRATION_CREATED)
     *aMadeChanges = DB_MIGRATION_UPDATED;
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  rv = CreateTriggers();
-  NS_ENSURE_SUCCESS(rv, rv);
-
   // --- PUT SCHEMA-MODIFYING THINGS (like create table) ABOVE THIS LINE ---
 
   // DO NOT PUT ANY SCHEMA-MODIFYING THINGS HERE
 
   rv = InitFunctions();
   NS_ENSURE_SUCCESS(rv, rv);
   rv = InitStatements();
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
-// nsNavHistory::CreateTriggers
-//
-//  This creates our triggers
-//  When creating a trigger we must ensure that related records are correct
-//  and be sure that there are no other queries that could change the 
-//  triggered values, especially for counting triggers.
-//
-// NOTE: never create loops between triggers!
-
-nsresult
-nsNavHistory::CreateTriggers()
-{
-  // we are creating 2 triggers on moz_historyvisits to maintain
-  // moz_places.visit_count in sync with moz_historyvisits, for this
-  // to work we must ensure that all visit_count values are correct
-  // See bug 416313 for details
-  nsCOMPtr<mozIStorageStatement> detectVisitCountTrigger;
-  nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
-      "SELECT name FROM sqlite_master WHERE type = 'trigger' AND "
-      "name = 'moz_historyvisits_afterinsert_v1_trigger'"),
-    getter_AddRefs(detectVisitCountTrigger));
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  PRBool hasTrigger;
-  rv = detectVisitCountTrigger->ExecuteStep(&hasTrigger);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = detectVisitCountTrigger->Reset();
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  if (!hasTrigger) {
-    mozStorageTransaction createTriggersTransaction(mDBConn, PR_FALSE);
-
-    // do a one-time reset of all the visit_count values
-    rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
-      "UPDATE moz_places SET visit_count = "
-      "(SELECT count(*) FROM moz_historyvisits "
-      "WHERE place_id = moz_places.id AND visit_type NOT IN (0,4,7))"));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    // moz_historyvisits_afterinsert_v1_trigger
-    // increment visit_count by 1 for each inserted visit
-    // excluding invalid, embed, download visits
-    rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
-      "CREATE TRIGGER IF NOT EXISTS moz_historyvisits_afterinsert_v1_trigger "
-      "AFTER INSERT ON moz_historyvisits FOR EACH ROW "
-      "WHEN NEW.visit_type NOT IN (0,4,7) "
-      "BEGIN "
-        "UPDATE moz_places SET visit_count = visit_count + 1 "
-        "WHERE moz_places.id = NEW.place_id; "
-      "END"));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    // moz_historyvisits_afterdelete_v1_trigger
-    // decrement visit_count by 1 for each deleted visit
-    // ensure that we can't become negative
-    rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
-      "CREATE TRIGGER IF NOT EXISTS moz_historyvisits_afterdelete_v1_trigger "
-      "AFTER DELETE ON moz_historyvisits FOR EACH ROW "
-      "WHEN OLD.visit_type NOT IN (0,4,7) "
-      "BEGIN "
-        "UPDATE moz_places SET visit_count = visit_count - 1 "
-        "WHERE moz_places.id = OLD.place_id AND visit_count > 0; "
-      "END"));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    rv = createTriggersTransaction.Commit();
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
-
-  // we are creating 1 trigger on moz_bookmarks to remove unused keywords
-  nsCOMPtr<mozIStorageStatement> detectRemoveKeywordsTrigger;
-  rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
-      "SELECT name FROM sqlite_master WHERE type = 'trigger' AND "
-      "name = 'moz_bookmarks_beforedelete_v1_trigger'"),
-    getter_AddRefs(detectRemoveKeywordsTrigger));
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  hasTrigger = PR_FALSE;
-  rv = detectRemoveKeywordsTrigger->ExecuteStep(&hasTrigger);
-  NS_ENSURE_SUCCESS(rv, rv);
-  rv = detectRemoveKeywordsTrigger->Reset();
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  if (!hasTrigger) {
-    // Remove dangling keywords.
-    // We must remove old keywords that have not been deleted with bookmarks.
-    // See bug 421180 for details.
-    rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
-        "DELETE FROM moz_keywords WHERE id IN ("
-          "SELECT k.id FROM moz_keywords k "
-          "LEFT OUTER JOIN moz_bookmarks b ON b.keyword_id = k.id "
-          "WHERE b.id IS NULL)"));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    // moz_bookmarks_beforedelete_v1_trigger
-    // Remove keywords if there are no more bookmarks using them.
-    rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
-      "CREATE TRIGGER IF NOT EXISTS moz_bookmarks_beforedelete_v1_trigger "
-      "BEFORE DELETE ON moz_bookmarks FOR EACH ROW "
-      "WHEN OLD.keyword_id NOT NULL "
-      "BEGIN "
-        "DELETE FROM moz_keywords WHERE id = OLD.keyword_id AND "
-        " NOT EXISTS (SELECT id FROM moz_bookmarks "
-          "WHERE keyword_id = OLD.keyword_id AND id <> OLD.id LIMIT 1); "
-      "END"));
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
-
-  return NS_OK;
-}
-
 nsresult
 nsNavHistory::InitializeIdleTimer()
 {
   if (mIdleTimer) {
     mIdleTimer->Cancel();
     mIdleTimer = nsnull;
   }
   nsresult rv;
@@ -1371,16 +1270,18 @@ nsNavHistory::MigrateV3Up(mozIStorageCon
   NS_ENSURE_SUCCESS(rv, rv);
   return NS_OK;
 }
 
 // nsNavHistory::MigrateV6Up
 nsresult
 nsNavHistory::MigrateV6Up(mozIStorageConnection* aDBConn) 
 {
+  mozStorageTransaction transaction(aDBConn, PR_FALSE);
+
   // if dateAdded & lastModified cols are already there, then a partial update occurred,
   // and so we should not attempt to add these cols.
   nsCOMPtr<mozIStorageStatement> statement;
   nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING(
     "SELECT a.dateAdded, a.lastModified FROM moz_annos a"), 
     getter_AddRefs(statement));
   if (NS_FAILED(rv)) {
     // add dateAdded and lastModified columns to moz_annos
@@ -1413,17 +1314,99 @@ nsNavHistory::MigrateV6Up(mozIStorageCon
   // see bug #386303 for more details
   rv = aDBConn->ExecuteSimpleSQL(
     NS_LITERAL_CSTRING("DROP INDEX IF EXISTS moz_favicons_url"));
   NS_ENSURE_SUCCESS(rv, rv);
   rv = aDBConn->ExecuteSimpleSQL(
     NS_LITERAL_CSTRING("DROP INDEX IF EXISTS moz_anno_attributes_nameindex"));
   NS_ENSURE_SUCCESS(rv, rv);
 
-  return NS_OK;
+  return transaction.Commit();
+}
+
+// nsNavHistory::MigrateV7Up
+nsresult
+nsNavHistory::MigrateV7Up(mozIStorageConnection* aDBConn) 
+{
+  mozStorageTransaction transaction(aDBConn, PR_FALSE);
+
+  // Create a statement to test for trigger creation
+  nsCOMPtr<mozIStorageStatement> triggerDetection;
+  nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING(
+    "SELECT name "
+    "FROM sqlite_master "
+    "WHERE type = 'trigger' "
+    "AND name = ?"
+  ), getter_AddRefs(triggerDetection));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Check for exisitance
+  PRBool triggerExists;
+  rv = triggerDetection->BindUTF8StringParameter(
+    0, NS_LITERAL_CSTRING("moz_historyvisits_afterinsert_v1_trigger")
+  );
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = triggerDetection->ExecuteStep(&triggerExists);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = triggerDetection->Reset();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // We need to create two triggers on moz_historyvists to maintain the
+  // accuracy of moz_places.visit_count.  For this to work, we must ensure that
+  // all moz_places.visit_count values are correct.
+  // See bug 416313 for details.
+  if (!triggerExists) {
+    // First, we do a one-time reset of all the moz_places.visit_count values.
+    rv = aDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+      "UPDATE moz_places SET visit_count = "
+        "(SELECT count(*) FROM moz_historyvisits "
+         "WHERE place_id = moz_places.id "
+         "AND visit_type NOT IN (0, 4, 7))") /* invalid, EMBED, DOWNLOAD */
+    );
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Now we create our two triggers
+    rv = aDBConn->ExecuteSimpleSQL(CREATE_VISIT_COUNT_INSERT_TRIGGER);
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = aDBConn->ExecuteSimpleSQL(CREATE_VISIT_COUNT_DELETE_TRIGGER);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  // Check for exisitance
+  rv = triggerDetection->BindUTF8StringParameter(
+    0, NS_LITERAL_CSTRING("moz_bookmarks_beforedelete_v1_trigger")
+  );
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = triggerDetection->ExecuteStep(&triggerExists);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = triggerDetection->Reset();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // We need to create one trigger on moz_bookmarks to remove unused keywords.
+  // See bug 421180 for details.
+  if (!triggerExists) {
+    // First, remove any existing dangling keywords
+    rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+      "DELETE FROM moz_keywords "
+      "WHERE id IN ("
+        "SELECT k.id "
+        "FROM moz_keywords k "
+        "LEFT OUTER JOIN moz_bookmarks b "
+        "ON b.keyword_id = k.id "
+        "WHERE b.id IS NULL"
+      ")")
+    );
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    // Now we create our trigger
+    rv = aDBConn->ExecuteSimpleSQL(CREATE_KEYWORD_VALIDITY_TRIGGER);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
+
+  return transaction.Commit();
 }
 
 nsresult
 nsNavHistory::EnsureCurrentSchema(mozIStorageConnection* aDBConn, PRBool* aDidMigrate)
 {
   // We need an index on lastModified to catch quickly last modified bookmark
   // title for tag container's children. This will be useful for sync too.
   PRBool lastModIndexExists = PR_FALSE;
--- a/toolkit/components/places/src/nsNavHistory.h
+++ b/toolkit/components/places/src/nsNavHistory.h
@@ -456,20 +456,20 @@ protected:
    *        DB_MIGRATION_CREATED
    *          The database did not exist in the past, and was created.
    *        DB_MIGRATION_UPDATED
    *          The database was migrated to a new version.
    */
   nsresult InitDB(PRInt16 *aMadeChanges);
   nsresult InitFunctions();
   nsresult InitStatements();
-  nsresult CreateTriggers();
   nsresult ForceMigrateBookmarksDB(mozIStorageConnection *aDBConn);
   nsresult MigrateV3Up(mozIStorageConnection *aDBConn);
   nsresult MigrateV6Up(mozIStorageConnection *aDBConn);
+  nsresult MigrateV7Up(mozIStorageConnection *aDBConn);
   nsresult EnsureCurrentSchema(mozIStorageConnection* aDBConn, PRBool *aMadeChanges);
   nsresult CleanUpOnQuit();
 
   nsresult RemovePagesInternal(const nsCString& aPlaceIdsQueryString);
 
   nsresult AddURIInternal(nsIURI* aURI, PRTime aTime, PRBool aRedirect,
                           PRBool aToplevel, nsIURI* aReferrer);
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/src/nsPlacesTriggers.h
@@ -0,0 +1,96 @@
+/* vim: sw=2 ts=2 sts=2 expandtab
+ * ***** 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 Places code.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * 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 __nsPlacesTriggers_h__
+#define __nsPlacesTriggers_h__
+
+/**
+ * Trigger increments the visit count by one for each inserted visit that isn't
+ * an invalid transition, embedded transition, or a download transition.
+ */
+#define CREATE_VISIT_COUNT_INSERT_TRIGGER NS_LITERAL_CSTRING( \
+  "CREATE TRIGGER moz_historyvisits_afterinsert_v1_trigger " \
+  "AFTER INSERT ON moz_historyvisits FOR EACH ROW " \
+  "WHEN NEW.visit_type NOT IN (0, 4, 7) " /* invalid, EMBED, DOWNLOAD */ \
+  "BEGIN " \
+    "UPDATE moz_places " \
+    "SET visit_count = visit_count + 1 " \
+    "WHERE moz_places.id = NEW.place_id; " \
+  "END" \
+)
+
+/**
+ * Trigger decrements the visit count by one for each removed visit that isn't
+ * an invalid transition, embeded transition, or a download transition.  To be
+ * safe, we ensure that the visit count will not fall below zero.
+ */
+#define CREATE_VISIT_COUNT_DELETE_TRIGGER NS_LITERAL_CSTRING( \
+  "CREATE TRIGGER moz_historyvisits_afterdelete_v1_trigger " \
+  "AFTER DELETE ON moz_historyvisits FOR EACH ROW " \
+  "WHEN OLD.visit_type NOT IN (0, 4, 7) " /* invalid, EMBED, DOWNLOAD */ \
+  "BEGIN " \
+    "UPDATE moz_places " \
+    "SET visit_count = visit_count - 1 " \
+    "WHERE moz_places.id = OLD.place_id " \
+    "AND visit_count > 0; " \
+  "END" \
+)
+
+/**
+ * Trigger checks to ensure that at least one bookmark is still using a keyword
+ * when any bookmark is deleted.  If there are no more bookmarks using it, the
+ * keyword is deleted.
+ */
+#define CREATE_KEYWORD_VALIDITY_TRIGGER NS_LITERAL_CSTRING( \
+  "CREATE TRIGGER moz_bookmarks_beforedelete_v1_trigger " \
+  "BEFORE DELETE ON moz_bookmarks FOR EACH ROW " \
+  "WHEN OLD.keyword_id NOT NULL " \
+  "BEGIN " \
+    "DELETE FROM moz_keywords " \
+    "WHERE id = OLD.keyword_id " \
+    "AND NOT EXISTS ( " \
+      "SELECT id " \
+      "FROM moz_bookmarks " \
+      "WHERE keyword_id = OLD.keyword_id " \
+      "AND id <> OLD.id " \
+      "LIMIT 1 " \
+    ");" \
+  "END" \
+)
+
+#endif // __nsPlacesTriggers_h__