mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-20 19:33:08 +09:00
1379 lines
58 KiB
Swift
1379 lines
58 KiB
Swift
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import Foundation
|
|
import Shared
|
|
import XCGLogger
|
|
|
|
let BookmarksFolderTitleMobile: String = NSLocalizedString("Mobile Bookmarks", tableName: "Storage", comment: "The title of the folder that contains mobile bookmarks. This should match bookmarks.folder.mobile.label on Android.")
|
|
let BookmarksFolderTitleMenu: String = NSLocalizedString("Bookmarks Menu", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the menu. This should match bookmarks.folder.menu.label on Android.")
|
|
let BookmarksFolderTitleToolbar: String = NSLocalizedString("Bookmarks Toolbar", tableName: "Storage", comment: "The name of the folder that contains desktop bookmarks in the toolbar. This should match bookmarks.folder.toolbar.label on Android.")
|
|
let BookmarksFolderTitleUnsorted: String = NSLocalizedString("Unsorted Bookmarks", tableName: "Storage", comment: "The name of the folder that contains unsorted desktop bookmarks. This should match bookmarks.folder.unfiled.label on Android.")
|
|
|
|
let _TableBookmarks = "bookmarks" // Removed in v12. Kept for migration.
|
|
let TableBookmarksMirror = "bookmarksMirror" // Added in v9.
|
|
let TableBookmarksMirrorStructure = "bookmarksMirrorStructure" // Added in v10.
|
|
|
|
let TableBookmarksBuffer = "bookmarksBuffer" // Added in v12. bookmarksMirror is renamed to bookmarksBuffer.
|
|
let TableBookmarksBufferStructure = "bookmarksBufferStructure" // Added in v12.
|
|
let TableBookmarksLocal = "bookmarksLocal" // Added in v12. Supersedes 'bookmarks'.
|
|
let TableBookmarksLocalStructure = "bookmarksLocalStructure" // Added in v12.
|
|
|
|
let TablePendingBookmarksDeletions = "pending_deletions" // Added in v28.
|
|
|
|
let TableFavicons = "favicons"
|
|
let TableHistory = "history"
|
|
let TableCachedTopSites = "cached_top_sites"
|
|
let TablePinnedTopSites = "pinned_top_sites"
|
|
let TableDomains = "domains"
|
|
let TableVisits = "visits"
|
|
let TableFaviconSites = "favicon_sites"
|
|
let TableQueuedTabs = "queue"
|
|
let TableSyncCommands = "commands"
|
|
let TableClients = "clients"
|
|
let TableTabs = "tabs"
|
|
|
|
let TableActivityStreamBlocklist = "activity_stream_blocklist"
|
|
let TablePageMetadata = "page_metadata"
|
|
let TableHighlights = "highlights"
|
|
|
|
let TableRemoteDevices = "remote_devices" // Added in v29.
|
|
|
|
let ViewBookmarksBufferOnMirror = "view_bookmarksBuffer_on_mirror"
|
|
let ViewBookmarksBufferWithDeletionsOnMirror = "view_bookmarksBuffer_with_deletions_on_mirror"
|
|
let ViewBookmarksBufferStructureOnMirror = "view_bookmarksBufferStructure_on_mirror"
|
|
let ViewBookmarksLocalOnMirror = "view_bookmarksLocal_on_mirror"
|
|
let ViewBookmarksLocalStructureOnMirror = "view_bookmarksLocalStructure_on_mirror"
|
|
let ViewAllBookmarks = "view_all_bookmarks"
|
|
let ViewAwesomebarBookmarks = "view_awesomebar_bookmarks"
|
|
let TempTableAwesomebarBookmarks = "awesomebar_bookmarks_temp_table"
|
|
let ViewAwesomebarBookmarksWithIcons = "view_awesomebar_bookmarks_with_favicons"
|
|
|
|
let ViewHistoryVisits = "view_history_visits"
|
|
let ViewWidestFaviconsForSites = "view_favicons_widest"
|
|
let ViewHistoryIDsWithWidestFavicons = "view_history_id_favicon"
|
|
let ViewIconForURL = "view_icon_for_url"
|
|
|
|
let IndexHistoryShouldUpload = "idx_history_should_upload"
|
|
let IndexVisitsSiteIDDate = "idx_visits_siteID_date" // Removed in v6.
|
|
let IndexVisitsSiteIDIsLocalDate = "idx_visits_siteID_is_local_date" // Added in v6.
|
|
let IndexBookmarksMirrorStructureParentIdx = "idx_bookmarksMirrorStructure_parent_idx" // Added in v10.
|
|
let IndexBookmarksLocalStructureParentIdx = "idx_bookmarksLocalStructure_parent_idx" // Added in v12.
|
|
let IndexBookmarksBufferStructureParentIdx = "idx_bookmarksBufferStructure_parent_idx" // Added in v12.
|
|
let IndexBookmarksMirrorStructureChild = "idx_bookmarksMirrorStructure_child" // Added in v14.
|
|
let IndexPageMetadataCacheKey = "idx_page_metadata_cache_key_uniqueindex" // Added in v19
|
|
let IndexPageMetadataSiteURL = "idx_page_metadata_site_url_uniqueindex" // Added in v21
|
|
|
|
private let AllTables: [String] = [
|
|
TableDomains,
|
|
TableFavicons,
|
|
TableFaviconSites,
|
|
|
|
TableHistory,
|
|
TableVisits,
|
|
TableCachedTopSites,
|
|
|
|
TableBookmarksBuffer,
|
|
TableBookmarksBufferStructure,
|
|
TableBookmarksLocal,
|
|
TableBookmarksLocalStructure,
|
|
TableBookmarksMirror,
|
|
TableBookmarksMirrorStructure,
|
|
TablePendingBookmarksDeletions,
|
|
TableQueuedTabs,
|
|
|
|
TableActivityStreamBlocklist,
|
|
TablePageMetadata,
|
|
TableHighlights,
|
|
TablePinnedTopSites,
|
|
TableRemoteDevices,
|
|
|
|
TableSyncCommands,
|
|
TableClients,
|
|
TableTabs,
|
|
]
|
|
|
|
private let AllViews: [String] = [
|
|
ViewHistoryIDsWithWidestFavicons,
|
|
ViewWidestFaviconsForSites,
|
|
ViewIconForURL,
|
|
ViewBookmarksBufferOnMirror,
|
|
ViewBookmarksBufferWithDeletionsOnMirror,
|
|
ViewBookmarksBufferStructureOnMirror,
|
|
ViewBookmarksLocalOnMirror,
|
|
ViewBookmarksLocalStructureOnMirror,
|
|
ViewAllBookmarks,
|
|
ViewAwesomebarBookmarks,
|
|
ViewAwesomebarBookmarksWithIcons,
|
|
ViewHistoryVisits,
|
|
]
|
|
|
|
private let AllIndices: [String] = [
|
|
IndexHistoryShouldUpload,
|
|
IndexVisitsSiteIDIsLocalDate,
|
|
IndexBookmarksBufferStructureParentIdx,
|
|
IndexBookmarksLocalStructureParentIdx,
|
|
IndexBookmarksMirrorStructureParentIdx,
|
|
IndexBookmarksMirrorStructureChild,
|
|
IndexPageMetadataCacheKey,
|
|
IndexPageMetadataSiteURL,
|
|
]
|
|
|
|
private let AllTablesIndicesAndViews: [String] = AllViews + AllIndices + AllTables
|
|
|
|
private let log = Logger.syncLogger
|
|
|
|
/**
|
|
* The monolithic class that manages the inter-related history etc. tables.
|
|
* We rely on SQLiteHistory having initialized the favicon table first.
|
|
*/
|
|
open class BrowserSchema: Schema {
|
|
static let DefaultVersion = 34 // Bug 1409777.
|
|
|
|
public var name: String { return "BROWSER" }
|
|
public var version: Int { return BrowserSchema.DefaultVersion }
|
|
|
|
let sqliteVersion: Int32
|
|
let supportsPartialIndices: Bool
|
|
|
|
public init() {
|
|
let v = sqlite3_libversion_number()
|
|
self.sqliteVersion = v
|
|
self.supportsPartialIndices = v >= 3008000 // 3.8.0.
|
|
let ver = String(cString: sqlite3_libversion())
|
|
log.info("SQLite version: \(ver) (\(v)).")
|
|
}
|
|
|
|
func run(_ db: SQLiteDBConnection, sql: String, args: Args? = nil) -> Bool {
|
|
do {
|
|
try db.executeChange(sql, withArgs: args)
|
|
} catch let err as NSError {
|
|
log.error("Error running SQL in BrowserSchema: \(err.localizedDescription)")
|
|
log.error("SQL was \(sql)")
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// TODO: transaction.
|
|
func run(_ db: SQLiteDBConnection, queries: [(String, Args?)]) -> Bool {
|
|
for (sql, args) in queries {
|
|
if !run(db, sql: sql, args: args) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func run(_ db: SQLiteDBConnection, queries: [String]) -> Bool {
|
|
for sql in queries {
|
|
if !run(db, sql: sql) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func runValidQueries(_ db: SQLiteDBConnection, queries: [(String?, Args?)]) -> Bool {
|
|
for (sql, args) in queries {
|
|
if let sql = sql {
|
|
if !run(db, sql: sql, args: args) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func runValidQueries(_ db: SQLiteDBConnection, queries: [String?]) -> Bool {
|
|
return self.run(db, queries: optFilter(queries))
|
|
}
|
|
|
|
func prepopulateRootFolders(_ db: SQLiteDBConnection) -> Bool {
|
|
let type = BookmarkNodeType.folder.rawValue
|
|
let now = Date.nowNumber()
|
|
let status = SyncStatus.new.rawValue
|
|
|
|
let localArgs: Args = [
|
|
BookmarkRoots.RootID, BookmarkRoots.RootGUID, type, now, BookmarkRoots.RootGUID, status, now,
|
|
BookmarkRoots.MobileID, BookmarkRoots.MobileFolderGUID, type, now, BookmarkRoots.RootGUID, status, now,
|
|
BookmarkRoots.MenuID, BookmarkRoots.MenuFolderGUID, type, now, BookmarkRoots.RootGUID, status, now,
|
|
BookmarkRoots.ToolbarID, BookmarkRoots.ToolbarFolderGUID, type, now, BookmarkRoots.RootGUID, status, now,
|
|
BookmarkRoots.UnfiledID, BookmarkRoots.UnfiledFolderGUID, type, now, BookmarkRoots.RootGUID, status, now,
|
|
]
|
|
|
|
// Compute these args using the sequence in RootChildren, rather than hard-coding.
|
|
var idx = 0
|
|
var structureArgs = Args()
|
|
structureArgs.reserveCapacity(BookmarkRoots.RootChildren.count * 3)
|
|
BookmarkRoots.RootChildren.forEach { guid in
|
|
structureArgs.append(BookmarkRoots.RootGUID)
|
|
structureArgs.append(guid)
|
|
structureArgs.append(idx)
|
|
idx += 1
|
|
}
|
|
|
|
// Note that we specify an empty title and parentName for these records. We should
|
|
// never need a parentName -- we don't use content-based reconciling or
|
|
// reparent these -- and we'll use the current locale's string, retrieved
|
|
// via titleForSpecialGUID, if necessary.
|
|
|
|
let local =
|
|
"INSERT INTO \(TableBookmarksLocal) " +
|
|
"(id, guid, type, date_added, parentid, title, parentName, sync_status, local_modified) VALUES " +
|
|
Array(repeating: "(?, ?, ?, ?, ?, '', '', ?, ?)", count: BookmarkRoots.RootChildren.count + 1).joined(separator: ", ")
|
|
|
|
let structure =
|
|
"INSERT INTO \(TableBookmarksLocalStructure) (parent, child, idx) VALUES " +
|
|
Array(repeating: "(?, ?, ?)", count: BookmarkRoots.RootChildren.count).joined(separator: ", ")
|
|
|
|
return self.run(db, queries: [(local, localArgs), (structure, structureArgs)])
|
|
}
|
|
|
|
let topSitesTableCreate =
|
|
"CREATE TABLE IF NOT EXISTS \(TableCachedTopSites) (" +
|
|
"historyID INTEGER, " +
|
|
"url TEXT NOT NULL, " +
|
|
"title TEXT NOT NULL, " +
|
|
"guid TEXT NOT NULL UNIQUE, " +
|
|
"domain_id INTEGER, " +
|
|
"domain TEXT NO NULL, " +
|
|
"localVisitDate REAL, " +
|
|
"remoteVisitDate REAL, " +
|
|
"localVisitCount INTEGER, " +
|
|
"remoteVisitCount INTEGER, " +
|
|
"iconID INTEGER, " +
|
|
"iconURL TEXT, " +
|
|
"iconDate REAL, " +
|
|
"iconType INTEGER, " +
|
|
"iconWidth INTEGER, " +
|
|
"frecencies REAL" +
|
|
")"
|
|
|
|
let pinnedTopSitesTableCreate =
|
|
"CREATE TABLE IF NOT EXISTS \(TablePinnedTopSites) (" +
|
|
"historyID INTEGER, " +
|
|
"url TEXT NOT NULL UNIQUE, " +
|
|
"title TEXT, " +
|
|
"guid TEXT, " +
|
|
"pinDate REAL, " +
|
|
"domain TEXT NOT NULL " +
|
|
")"
|
|
|
|
let domainsTableCreate =
|
|
"CREATE TABLE IF NOT EXISTS \(TableDomains) (" +
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"domain TEXT NOT NULL UNIQUE, " +
|
|
"showOnTopSites TINYINT NOT NULL DEFAULT 1" +
|
|
")"
|
|
|
|
let queueTableCreate =
|
|
"CREATE TABLE IF NOT EXISTS \(TableQueuedTabs) (" +
|
|
"url TEXT NOT NULL UNIQUE, " +
|
|
"title TEXT" +
|
|
") "
|
|
|
|
let syncCommandsTableCreate =
|
|
"CREATE TABLE IF NOT EXISTS \(TableSyncCommands) (" +
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"client_guid TEXT NOT NULL, " +
|
|
"value TEXT NOT NULL" +
|
|
")"
|
|
|
|
let clientsTableCreate =
|
|
"CREATE TABLE IF NOT EXISTS \(TableClients) (" +
|
|
"guid TEXT PRIMARY KEY, " +
|
|
"name TEXT NOT NULL, " +
|
|
"modified INTEGER NOT NULL, " +
|
|
"type TEXT, " +
|
|
"formfactor TEXT, " +
|
|
"os TEXT, " +
|
|
"version TEXT, " +
|
|
"fxaDeviceId TEXT" +
|
|
")"
|
|
|
|
let tabsTableCreate =
|
|
"CREATE TABLE IF NOT EXISTS \(TableTabs) (" +
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"client_guid TEXT REFERENCES clients(guid) ON DELETE CASCADE, " +
|
|
"url TEXT NOT NULL, " +
|
|
"title TEXT, " +
|
|
"history TEXT, " +
|
|
"last_used INTEGER" +
|
|
")"
|
|
|
|
let activityStreamBlocklistCreate =
|
|
"CREATE TABLE IF NOT EXISTS \(TableActivityStreamBlocklist) (" +
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"url TEXT NOT NULL UNIQUE, " +
|
|
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP " +
|
|
") "
|
|
|
|
let pageMetadataCreate =
|
|
"CREATE TABLE IF NOT EXISTS \(TablePageMetadata) (" +
|
|
"id INTEGER PRIMARY KEY, " +
|
|
"cache_key LONGVARCHAR UNIQUE, " +
|
|
"site_url TEXT, " +
|
|
"media_url LONGVARCHAR, " +
|
|
"title TEXT, " +
|
|
"type VARCHAR(32), " +
|
|
"description TEXT, " +
|
|
"provider_name TEXT, " +
|
|
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
|
"expired_at LONG" +
|
|
") "
|
|
|
|
let highlightsCreate =
|
|
"CREATE TABLE IF NOT EXISTS \(TableHighlights) (" +
|
|
"historyID INTEGER PRIMARY KEY," +
|
|
"cache_key LONGVARCHAR," +
|
|
"url TEXT," +
|
|
"title TEXT," +
|
|
"guid TEXT," +
|
|
"visitCount INTEGER," +
|
|
"visitDate DATETIME," +
|
|
"is_bookmarked INTEGER" +
|
|
") "
|
|
|
|
let indexPageMetadataCacheKeyCreate =
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS \(IndexPageMetadataCacheKey) ON page_metadata (cache_key)"
|
|
|
|
let indexPageMetadataSiteURLCreate =
|
|
"CREATE UNIQUE INDEX IF NOT EXISTS \(IndexPageMetadataSiteURL) ON page_metadata (site_url)"
|
|
|
|
let iconColumns = ", faviconID INTEGER REFERENCES \(TableFavicons)(id) ON DELETE SET NULL"
|
|
let mirrorColumns = ", is_overridden TINYINT NOT NULL DEFAULT 0"
|
|
|
|
let serverColumns = ", server_modified INTEGER NOT NULL" + // Milliseconds.
|
|
", hasDupe TINYINT NOT NULL DEFAULT 0" // Boolean, 0 (false) if deleted.
|
|
|
|
let localColumns = ", local_modified INTEGER" + // Can be null. Client clock. In extremis only.
|
|
", sync_status TINYINT NOT NULL" // SyncStatus enum. Set when changed or created.
|
|
|
|
func getBookmarksTableCreationStringForTable(_ table: String, withAdditionalColumns: String="") -> String {
|
|
// The stupid absence of naming conventions here is thanks to pre-Sync Weave. Sorry.
|
|
// For now we have the simplest possible schema: everything in one.
|
|
let sql =
|
|
"CREATE TABLE IF NOT EXISTS \(table) " +
|
|
|
|
// Shared fields.
|
|
"( id INTEGER PRIMARY KEY AUTOINCREMENT" +
|
|
", guid TEXT NOT NULL UNIQUE" +
|
|
", type TINYINT NOT NULL" + // Type enum.
|
|
", date_added INTEGER" +
|
|
|
|
// Record/envelope metadata that'll allow us to do merges.
|
|
", is_deleted TINYINT NOT NULL DEFAULT 0" + // Boolean
|
|
|
|
", parentid TEXT" + // GUID
|
|
", parentName TEXT" +
|
|
|
|
// Type-specific fields. These should be NOT NULL in many cases, but we're going
|
|
// for a sparse schema, so this'll do for now. Enforce these in the application code.
|
|
", feedUri TEXT, siteUri TEXT" + // LIVEMARKS
|
|
", pos INT" + // SEPARATORS
|
|
", title TEXT, description TEXT" + // FOLDERS, BOOKMARKS, QUERIES
|
|
", bmkUri TEXT, tags TEXT, keyword TEXT" + // BOOKMARKS, QUERIES
|
|
", folderName TEXT, queryId TEXT" + // QUERIES
|
|
withAdditionalColumns +
|
|
", CONSTRAINT parentidOrDeleted CHECK (parentid IS NOT NULL OR is_deleted = 1)" +
|
|
", CONSTRAINT parentNameOrDeleted CHECK (parentName IS NOT NULL OR is_deleted = 1)" +
|
|
")"
|
|
|
|
return sql
|
|
}
|
|
|
|
/**
|
|
* We need to explicitly store what's provided by the server, because we can't rely on
|
|
* referenced child nodes to exist yet!
|
|
*/
|
|
func getBookmarksStructureTableCreationStringForTable(_ table: String, referencingMirror mirror: String) -> String {
|
|
let sql =
|
|
"CREATE TABLE IF NOT EXISTS \(table) " +
|
|
"( parent TEXT NOT NULL REFERENCES \(mirror)(guid) ON DELETE CASCADE" +
|
|
", child TEXT NOT NULL" + // Should be the GUID of a child.
|
|
", idx INTEGER NOT NULL" + // Should advance from 0.
|
|
")"
|
|
|
|
return sql
|
|
}
|
|
|
|
fileprivate let bufferBookmarksView =
|
|
"CREATE VIEW \(ViewBookmarksBufferOnMirror) AS " +
|
|
"SELECT" +
|
|
" -1 AS id" +
|
|
", mirror.guid AS guid" +
|
|
", mirror.type AS type" +
|
|
", mirror.date_added AS date_added" +
|
|
", mirror.is_deleted AS is_deleted" +
|
|
", mirror.parentid AS parentid" +
|
|
", mirror.parentName AS parentName" +
|
|
", mirror.feedUri AS feedUri" +
|
|
", mirror.siteUri AS siteUri" +
|
|
", mirror.pos AS pos" +
|
|
", mirror.title AS title" +
|
|
", mirror.description AS description" +
|
|
", mirror.bmkUri AS bmkUri" +
|
|
", mirror.keyword AS keyword" +
|
|
", mirror.folderName AS folderName" +
|
|
", null AS faviconID" +
|
|
", 0 AS is_overridden" +
|
|
|
|
// LEFT EXCLUDING JOIN to get mirror records that aren't in the buffer.
|
|
// We don't have an is_overridden flag to help us here.
|
|
" FROM \(TableBookmarksMirror) mirror LEFT JOIN" +
|
|
" \(TableBookmarksBuffer) buffer ON mirror.guid = buffer.guid" +
|
|
" WHERE buffer.guid IS NULL" +
|
|
" UNION ALL " +
|
|
"SELECT" +
|
|
" -1 AS id" +
|
|
", guid" +
|
|
", type" +
|
|
", date_added" +
|
|
", is_deleted" +
|
|
", parentid" +
|
|
", parentName" +
|
|
", feedUri" +
|
|
", siteUri" +
|
|
", pos" +
|
|
", title" +
|
|
", description" +
|
|
", bmkUri" +
|
|
", keyword" +
|
|
", folderName" +
|
|
", null AS faviconID" +
|
|
", 1 AS is_overridden" +
|
|
" FROM \(TableBookmarksBuffer) WHERE is_deleted IS 0"
|
|
|
|
fileprivate let bufferBookmarksWithDeletionsView =
|
|
"CREATE VIEW \(ViewBookmarksBufferWithDeletionsOnMirror) AS " +
|
|
"SELECT" +
|
|
" -1 AS id" +
|
|
", mirror.guid AS guid" +
|
|
", mirror.type AS type" +
|
|
", mirror.date_added AS date_added" +
|
|
", mirror.is_deleted AS is_deleted" +
|
|
", mirror.parentid AS parentid" +
|
|
", mirror.parentName AS parentName" +
|
|
", mirror.feedUri AS feedUri" +
|
|
", mirror.siteUri AS siteUri" +
|
|
", mirror.pos AS pos" +
|
|
", mirror.title AS title" +
|
|
", mirror.description AS description" +
|
|
", mirror.bmkUri AS bmkUri" +
|
|
", mirror.keyword AS keyword" +
|
|
", mirror.folderName AS folderName" +
|
|
", null AS faviconID" +
|
|
", 0 AS is_overridden" +
|
|
|
|
// LEFT EXCLUDING JOIN to get mirror records that aren't in the buffer.
|
|
// We don't have an is_overridden flag to help us here.
|
|
" FROM \(TableBookmarksMirror) mirror LEFT JOIN" +
|
|
" \(TableBookmarksBuffer) buffer ON mirror.guid = buffer.guid" +
|
|
" WHERE buffer.guid IS NULL" +
|
|
" UNION ALL " +
|
|
"SELECT" +
|
|
" -1 AS id" +
|
|
", guid" +
|
|
", type" +
|
|
", date_added" +
|
|
", is_deleted" +
|
|
", parentid" +
|
|
", parentName" +
|
|
", feedUri" +
|
|
", siteUri" +
|
|
", pos" +
|
|
", title" +
|
|
", description" +
|
|
", bmkUri" +
|
|
", keyword" +
|
|
", folderName" +
|
|
", null AS faviconID" +
|
|
", 1 AS is_overridden" +
|
|
" FROM \(TableBookmarksBuffer) WHERE is_deleted IS 0" +
|
|
" AND NOT EXISTS (SELECT 1 FROM \(TablePendingBookmarksDeletions) deletions WHERE deletions.id = guid)"
|
|
|
|
// TODO: phrase this without the subselect…
|
|
fileprivate let bufferBookmarksStructureView =
|
|
// We don't need to exclude deleted parents, because we drop those from the structure
|
|
// table when we see them.
|
|
"CREATE VIEW \(ViewBookmarksBufferStructureOnMirror) AS " +
|
|
"SELECT parent, child, idx, 1 AS is_overridden FROM \(TableBookmarksBufferStructure) " +
|
|
"UNION ALL " +
|
|
|
|
// Exclude anything from the mirror that's present in the buffer -- dynamic is_overridden.
|
|
"SELECT parent, child, idx, 0 AS is_overridden FROM \(TableBookmarksMirrorStructure) " +
|
|
"LEFT JOIN \(TableBookmarksBuffer) ON parent = guid WHERE guid IS NULL"
|
|
|
|
fileprivate let localBookmarksView =
|
|
"CREATE VIEW \(ViewBookmarksLocalOnMirror) AS " +
|
|
"SELECT -1 AS id, guid, type, date_added, is_deleted, parentid, parentName, feedUri," +
|
|
" siteUri, pos, title, description, bmkUri, folderName, faviconID, NULL AS local_modified, server_modified, 0 AS is_overridden " +
|
|
"FROM \(TableBookmarksMirror) WHERE is_overridden IS NOT 1 " +
|
|
"UNION ALL " +
|
|
"SELECT -1 AS id, guid, type, date_added, is_deleted, parentid, parentName, feedUri, siteUri, pos, title, description, bmkUri, folderName, faviconID," +
|
|
" local_modified, NULL AS server_modified, 1 AS is_overridden " +
|
|
"FROM \(TableBookmarksLocal) WHERE is_deleted IS NOT 1"
|
|
|
|
// TODO: phrase this without the subselect…
|
|
fileprivate let localBookmarksStructureView =
|
|
"CREATE VIEW \(ViewBookmarksLocalStructureOnMirror) AS " +
|
|
"SELECT parent, child, idx, 1 AS is_overridden FROM \(TableBookmarksLocalStructure) " +
|
|
"WHERE " +
|
|
"((SELECT is_deleted FROM \(TableBookmarksLocal) WHERE guid = parent) IS NOT 1) " +
|
|
"UNION ALL " +
|
|
"SELECT parent, child, idx, 0 AS is_overridden FROM \(TableBookmarksMirrorStructure) " +
|
|
"WHERE " +
|
|
"((SELECT is_overridden FROM \(TableBookmarksMirror) WHERE guid = parent) IS NOT 1) "
|
|
|
|
// This view exists only to allow for text searching of URLs and titles in the awesomebar.
|
|
// As such, we cheat a little: we include buffer, non-overridden mirror, and local.
|
|
// Usually this will be indistinguishable from a more sophisticated approach, and it's way
|
|
// easier.
|
|
fileprivate let allBookmarksView =
|
|
"CREATE VIEW \(ViewAllBookmarks) AS " +
|
|
"SELECT guid, bmkUri AS url, title, description, faviconID FROM " +
|
|
"\(TableBookmarksMirror) WHERE " +
|
|
"type = \(BookmarkNodeType.bookmark.rawValue) AND is_overridden IS 0 AND is_deleted IS 0 " +
|
|
"UNION ALL " +
|
|
"SELECT guid, bmkUri AS url, title, description, faviconID FROM " +
|
|
"\(TableBookmarksLocal) WHERE " +
|
|
"type = \(BookmarkNodeType.bookmark.rawValue) AND is_deleted IS 0 " +
|
|
"UNION ALL " +
|
|
"SELECT guid, bmkUri AS url, title, description, -1 AS faviconID FROM " +
|
|
"\(TableBookmarksBuffer) bb WHERE " +
|
|
"bb.type = \(BookmarkNodeType.bookmark.rawValue) AND bb.is_deleted IS 0 " +
|
|
|
|
// Exclude pending bookmark deletions.
|
|
"AND NOT EXISTS (SELECT 1 FROM \(TablePendingBookmarksDeletions) AS pd WHERE pd.id = bb.guid)"
|
|
|
|
// This exists only to allow upgrade from old versions. We have view dependencies, so
|
|
// we can't simply skip creating ViewAllBookmarks. Here's a stub.
|
|
fileprivate let oldAllBookmarksView =
|
|
"CREATE VIEW \(ViewAllBookmarks) AS " +
|
|
"SELECT guid, bmkUri AS url, title, description, faviconID FROM " +
|
|
"\(TableBookmarksMirror) WHERE " +
|
|
"type = \(BookmarkNodeType.bookmark.rawValue) AND is_overridden IS 0 AND is_deleted IS 0"
|
|
|
|
// This smushes together remote and local visits. So it goes.
|
|
fileprivate let historyVisitsView =
|
|
"CREATE VIEW \(ViewHistoryVisits) AS " +
|
|
"SELECT h.url AS url, MAX(v.date) AS visitDate, h.domain_id AS domain_id FROM " +
|
|
"\(TableHistory) h JOIN \(TableVisits) v ON v.siteID = h.id " +
|
|
"GROUP BY h.id"
|
|
|
|
// Join all bookmarks against history to find the most recent visit.
|
|
// visits.
|
|
fileprivate let awesomebarBookmarksView =
|
|
"CREATE VIEW \(ViewAwesomebarBookmarks) AS " +
|
|
"SELECT b.guid AS guid, b.url AS url, b.title AS title, " +
|
|
"b.description AS description, b.faviconID AS faviconID, " +
|
|
"h.visitDate AS visitDate " +
|
|
"FROM \(ViewAllBookmarks) b " +
|
|
"LEFT JOIN " +
|
|
"\(ViewHistoryVisits) h ON b.url = h.url"
|
|
|
|
fileprivate let awesomebarBookmarksWithIconsView =
|
|
"CREATE VIEW \(ViewAwesomebarBookmarksWithIcons) AS " +
|
|
"SELECT b.guid AS guid, b.url AS url, b.title AS title, " +
|
|
"b.description AS description, b.visitDate AS visitDate, " +
|
|
"f.id AS iconID, f.url AS iconURL, f.date AS iconDate, " +
|
|
"f.type AS iconType, f.width AS iconWidth " +
|
|
"FROM \(ViewAwesomebarBookmarks) b " +
|
|
"LEFT JOIN " +
|
|
"\(TableFavicons) f ON f.id = b.faviconID"
|
|
|
|
fileprivate let pendingBookmarksDeletions =
|
|
"CREATE TABLE IF NOT EXISTS \(TablePendingBookmarksDeletions) (" +
|
|
"id TEXT PRIMARY KEY REFERENCES \(TableBookmarksBuffer)(guid) ON DELETE CASCADE" +
|
|
")"
|
|
|
|
fileprivate let remoteDevices =
|
|
"CREATE TABLE IF NOT EXISTS \(TableRemoteDevices) (" +
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"guid TEXT UNIQUE NOT NULL, " +
|
|
"name TEXT NOT NULL, " +
|
|
"type TEXT NOT NULL, " +
|
|
"is_current_device INTEGER NOT NULL, " +
|
|
"date_created INTEGER NOT NULL, " + // Timestamps in ms.
|
|
"date_modified INTEGER NOT NULL, " +
|
|
"last_access_time INTEGER" +
|
|
")"
|
|
|
|
public func create(_ db: SQLiteDBConnection) -> Bool {
|
|
let favicons =
|
|
"CREATE TABLE IF NOT EXISTS \(TableFavicons) (" +
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"url TEXT NOT NULL UNIQUE, " +
|
|
"width INTEGER, " +
|
|
"height INTEGER, " +
|
|
"type INTEGER NOT NULL, " +
|
|
"date REAL NOT NULL" +
|
|
") "
|
|
|
|
let history =
|
|
"CREATE TABLE IF NOT EXISTS \(TableHistory) (" +
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"guid TEXT NOT NULL UNIQUE, " + // Not null, but the value might be replaced by the server's.
|
|
"url TEXT UNIQUE, " + // May only be null for deleted records.
|
|
"title TEXT NOT NULL, " +
|
|
"server_modified INTEGER, " + // Can be null. Integer milliseconds.
|
|
"local_modified INTEGER, " + // Can be null. Client clock. In extremis only.
|
|
"is_deleted TINYINT NOT NULL, " + // Boolean. Locally deleted.
|
|
"should_upload TINYINT NOT NULL, " + // Boolean. Set when changed or visits added.
|
|
"domain_id INTEGER REFERENCES \(TableDomains)(id) ON DELETE CASCADE, " +
|
|
"CONSTRAINT urlOrDeleted CHECK (url IS NOT NULL OR is_deleted = 1)" +
|
|
")"
|
|
|
|
// Right now we don't need to track per-visit deletions: Sync can't
|
|
// represent them! See Bug 1157553 Comment 6.
|
|
// We flip the should_upload flag on the history item when we add a visit.
|
|
// If we ever want to support logic like not bothering to sync if we added
|
|
// and then rapidly removed a visit, then we need an 'is_new' flag on each visit.
|
|
let visits =
|
|
"CREATE TABLE IF NOT EXISTS \(TableVisits) (" +
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"siteID INTEGER NOT NULL REFERENCES \(TableHistory)(id) ON DELETE CASCADE, " +
|
|
"date REAL NOT NULL, " + // Microseconds since epoch.
|
|
"type INTEGER NOT NULL, " +
|
|
"is_local TINYINT NOT NULL, " + // Some visits are local. Some are remote ('mirrored'). This boolean flag is the split.
|
|
"UNIQUE (siteID, date, type) " +
|
|
") "
|
|
|
|
let indexShouldUpload: String
|
|
if self.supportsPartialIndices {
|
|
// There's no point tracking rows that are not flagged for upload.
|
|
indexShouldUpload =
|
|
"CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " +
|
|
"ON \(TableHistory) (should_upload) WHERE should_upload = 1"
|
|
} else {
|
|
indexShouldUpload =
|
|
"CREATE INDEX IF NOT EXISTS \(IndexHistoryShouldUpload) " +
|
|
"ON \(TableHistory) (should_upload)"
|
|
}
|
|
|
|
let indexSiteIDDate =
|
|
"CREATE INDEX IF NOT EXISTS \(IndexVisitsSiteIDIsLocalDate) " +
|
|
"ON \(TableVisits) (siteID, is_local, date)"
|
|
|
|
let faviconSites =
|
|
"CREATE TABLE IF NOT EXISTS \(TableFaviconSites) (" +
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
|
"siteID INTEGER NOT NULL REFERENCES \(TableHistory)(id) ON DELETE CASCADE, " +
|
|
"faviconID INTEGER NOT NULL REFERENCES \(TableFavicons)(id) ON DELETE CASCADE, " +
|
|
"UNIQUE (siteID, faviconID) " +
|
|
") "
|
|
|
|
let widestFavicons =
|
|
"CREATE VIEW IF NOT EXISTS \(ViewWidestFaviconsForSites) AS " +
|
|
"SELECT " +
|
|
"\(TableFaviconSites).siteID AS siteID, " +
|
|
"\(TableFavicons).id AS iconID, " +
|
|
"\(TableFavicons).url AS iconURL, " +
|
|
"\(TableFavicons).date AS iconDate, " +
|
|
"\(TableFavicons).type AS iconType, " +
|
|
"MAX(\(TableFavicons).width) AS iconWidth " +
|
|
"FROM \(TableFaviconSites), \(TableFavicons) WHERE " +
|
|
"\(TableFaviconSites).faviconID = \(TableFavicons).id " +
|
|
"GROUP BY siteID "
|
|
|
|
let historyIDsWithIcon =
|
|
"CREATE VIEW IF NOT EXISTS \(ViewHistoryIDsWithWidestFavicons) AS " +
|
|
"SELECT \(TableHistory).id AS id, " +
|
|
"iconID, iconURL, iconDate, iconType, iconWidth " +
|
|
"FROM \(TableHistory) " +
|
|
"LEFT OUTER JOIN " +
|
|
"\(ViewWidestFaviconsForSites) ON history.id = \(ViewWidestFaviconsForSites).siteID "
|
|
|
|
let iconForURL =
|
|
"CREATE VIEW IF NOT EXISTS \(ViewIconForURL) AS " +
|
|
"SELECT history.url AS url, icons.iconID AS iconID FROM " +
|
|
"\(TableHistory), \(ViewWidestFaviconsForSites) AS icons WHERE " +
|
|
"\(TableHistory).id = icons.siteID "
|
|
|
|
// Locally we track faviconID.
|
|
// Local changes end up in the mirror, so we track it there too.
|
|
// The buffer and the mirror additionally track some server metadata.
|
|
let bookmarksLocal = getBookmarksTableCreationStringForTable(TableBookmarksLocal, withAdditionalColumns: self.localColumns + self.iconColumns)
|
|
let bookmarksLocalStructure = getBookmarksStructureTableCreationStringForTable(TableBookmarksLocalStructure, referencingMirror: TableBookmarksLocal)
|
|
let bookmarksBuffer = getBookmarksTableCreationStringForTable(TableBookmarksBuffer, withAdditionalColumns: self.serverColumns)
|
|
let bookmarksBufferStructure = getBookmarksStructureTableCreationStringForTable(TableBookmarksBufferStructure, referencingMirror: TableBookmarksBuffer)
|
|
let bookmarksMirror = getBookmarksTableCreationStringForTable(TableBookmarksMirror, withAdditionalColumns: self.serverColumns + self.mirrorColumns + self.iconColumns)
|
|
let bookmarksMirrorStructure = getBookmarksStructureTableCreationStringForTable(TableBookmarksMirrorStructure, referencingMirror: TableBookmarksMirror)
|
|
|
|
let indexLocalStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksLocalStructureParentIdx) " +
|
|
"ON \(TableBookmarksLocalStructure) (parent, idx)"
|
|
let indexBufferStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksBufferStructureParentIdx) " +
|
|
"ON \(TableBookmarksBufferStructure) (parent, idx)"
|
|
let indexMirrorStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksMirrorStructureParentIdx) " +
|
|
"ON \(TableBookmarksMirrorStructure) (parent, idx)"
|
|
let indexMirrorStructureChild = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksMirrorStructureChild) " +
|
|
"ON \(TableBookmarksMirrorStructure) (child)"
|
|
|
|
let queries: [String] = [
|
|
// Tables.
|
|
self.domainsTableCreate,
|
|
history,
|
|
favicons,
|
|
visits,
|
|
bookmarksBuffer,
|
|
bookmarksBufferStructure,
|
|
bookmarksLocal,
|
|
bookmarksLocalStructure,
|
|
bookmarksMirror,
|
|
bookmarksMirrorStructure,
|
|
self.pendingBookmarksDeletions,
|
|
faviconSites,
|
|
widestFavicons,
|
|
historyIDsWithIcon,
|
|
iconForURL,
|
|
pageMetadataCreate,
|
|
pinnedTopSitesTableCreate,
|
|
highlightsCreate,
|
|
self.remoteDevices,
|
|
activityStreamBlocklistCreate,
|
|
indexPageMetadataSiteURLCreate,
|
|
indexPageMetadataCacheKeyCreate,
|
|
self.queueTableCreate,
|
|
self.topSitesTableCreate,
|
|
syncCommandsTableCreate,
|
|
clientsTableCreate,
|
|
tabsTableCreate,
|
|
|
|
// Indices.
|
|
indexBufferStructureParentIdx,
|
|
indexLocalStructureParentIdx,
|
|
indexMirrorStructureParentIdx,
|
|
indexMirrorStructureChild,
|
|
indexShouldUpload,
|
|
indexSiteIDDate,
|
|
|
|
// Views.
|
|
self.localBookmarksView,
|
|
self.localBookmarksStructureView,
|
|
self.bufferBookmarksView,
|
|
self.bufferBookmarksWithDeletionsView,
|
|
self.bufferBookmarksStructureView,
|
|
allBookmarksView,
|
|
historyVisitsView,
|
|
awesomebarBookmarksView,
|
|
awesomebarBookmarksWithIconsView,
|
|
]
|
|
|
|
assert(queries.count == AllTablesIndicesAndViews.count, "Did you forget to add your table, index, or view to the list?")
|
|
|
|
log.debug("Creating \(queries.count) tables, views, and indices.")
|
|
|
|
return self.run(db, queries: queries) &&
|
|
self.prepopulateRootFolders(db)
|
|
}
|
|
|
|
public func update(_ db: SQLiteDBConnection, from: Int) -> Bool {
|
|
let to = self.version
|
|
if from == to {
|
|
log.debug("Skipping update from \(from) to \(to).")
|
|
return true
|
|
}
|
|
|
|
if from == 0 {
|
|
|
|
// If we're upgrading from `0`, it is likely that we have not yet switched
|
|
// from tracking the schema version using `tableList` to `PRAGMA user_version`.
|
|
// This will write the *previous* version number from `tableList` into
|
|
// `PRAGMA user_version` if necessary in addition to upgrading the schema of
|
|
// some of the old tables that were previously managed separately.
|
|
if self.migrateFromSchemaTableIfNeeded(db) {
|
|
let version = db.version
|
|
|
|
// If the database is now properly reporting a `user_version`, it may
|
|
// still need to be upgraded further to get to the current version of
|
|
// the schema. So, let's simply call this `update()` function a second
|
|
// time to handle any remaining post-v31 migrations.
|
|
if version > 0 {
|
|
return self.update(db, from: version)
|
|
}
|
|
}
|
|
|
|
// Otherwise, this is likely an upgrade from before Bug 1160399, so
|
|
// let's drop and re-create.
|
|
log.debug("Updating schema \(self.name) from zero. Assuming drop and recreate.")
|
|
return drop(db) && create(db)
|
|
}
|
|
|
|
if from > to {
|
|
// This is likely an upgrade from before Bug 1160399.
|
|
log.debug("Downgrading browser tables. Assuming drop and recreate.")
|
|
return drop(db) && create(db)
|
|
}
|
|
|
|
log.debug("Updating schema \(self.name) from \(from) to \(to).")
|
|
|
|
if from < 4 && to >= 4 {
|
|
return drop(db) && create(db)
|
|
}
|
|
|
|
if from < 5 && to >= 5 {
|
|
if !self.run(db, sql: self.queueTableCreate) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 6 && to >= 6 {
|
|
if !self.run(db, queries: [
|
|
"DROP INDEX IF EXISTS \(IndexVisitsSiteIDDate)",
|
|
"CREATE INDEX IF NOT EXISTS \(IndexVisitsSiteIDIsLocalDate) ON \(TableVisits) (siteID, is_local, date)",
|
|
self.domainsTableCreate,
|
|
"ALTER TABLE \(TableHistory) ADD COLUMN domain_id INTEGER REFERENCES \(TableDomains)(id) ON DELETE CASCADE",
|
|
]) {
|
|
return false
|
|
}
|
|
|
|
let urls = db.executeQuery("SELECT DISTINCT url FROM \(TableHistory) WHERE url IS NOT NULL",
|
|
factory: { $0["url"] as! String })
|
|
if !fillDomainNamesFromCursor(urls, db: db) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 8 && to == 8 {
|
|
// Nothing to do: we're just shifting the favicon table to be owned by this class.
|
|
return true
|
|
}
|
|
|
|
if from < 9 && to >= 9 {
|
|
if !self.run(db, sql: getBookmarksTableCreationStringForTable(TableBookmarksMirror)) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 10 && to >= 10 {
|
|
if !self.run(db, sql: getBookmarksStructureTableCreationStringForTable(TableBookmarksMirrorStructure, referencingMirror: TableBookmarksMirror)) {
|
|
return false
|
|
}
|
|
|
|
let indexStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksMirrorStructureParentIdx) " +
|
|
"ON \(TableBookmarksMirrorStructure) (parent, idx)"
|
|
if !self.run(db, sql: indexStructureParentIdx) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 11 && to >= 11 {
|
|
if !self.run(db, sql: self.topSitesTableCreate) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 12 && to >= 12 {
|
|
let bookmarksLocal = getBookmarksTableCreationStringForTable(TableBookmarksLocal, withAdditionalColumns: self.localColumns + self.iconColumns)
|
|
let bookmarksLocalStructure = getBookmarksStructureTableCreationStringForTable(TableBookmarksLocalStructure, referencingMirror: TableBookmarksLocal)
|
|
let bookmarksMirror = getBookmarksTableCreationStringForTable(TableBookmarksMirror, withAdditionalColumns: self.serverColumns + self.mirrorColumns + self.iconColumns)
|
|
let bookmarksMirrorStructure = getBookmarksStructureTableCreationStringForTable(TableBookmarksMirrorStructure, referencingMirror: TableBookmarksMirror)
|
|
|
|
let indexLocalStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksLocalStructureParentIdx) " +
|
|
"ON \(TableBookmarksLocalStructure) (parent, idx)"
|
|
let indexBufferStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksBufferStructureParentIdx) " +
|
|
"ON \(TableBookmarksBufferStructure) (parent, idx)"
|
|
let indexMirrorStructureParentIdx = "CREATE INDEX IF NOT EXISTS \(IndexBookmarksMirrorStructureParentIdx) " +
|
|
"ON \(TableBookmarksMirrorStructure) (parent, idx)"
|
|
|
|
let prep = [
|
|
// Drop indices.
|
|
"DROP INDEX IF EXISTS idx_bookmarksMirrorStructure_parent_idx",
|
|
|
|
// Rename the old mirror tables to buffer.
|
|
// The v11 one is the same shape as the current buffer table.
|
|
"ALTER TABLE \(TableBookmarksMirror) RENAME TO \(TableBookmarksBuffer)",
|
|
"ALTER TABLE \(TableBookmarksMirrorStructure) RENAME TO \(TableBookmarksBufferStructure)",
|
|
|
|
// Create the new mirror and local tables.
|
|
bookmarksLocal,
|
|
bookmarksMirror,
|
|
bookmarksLocalStructure,
|
|
bookmarksMirrorStructure,
|
|
]
|
|
|
|
// Only migrate bookmarks. The only folders are our roots, and we'll create those later.
|
|
// There should be nothing else in the table, and no structure.
|
|
// Our old bookmarks table didn't have creation date, so we use the current timestamp.
|
|
let modified = Date.now()
|
|
let status = SyncStatus.new.rawValue
|
|
|
|
// We don't specify a title, expecting it to be generated on the fly, because we're smarter than Android.
|
|
// We also don't migrate the 'id' column; we'll generate new ones that won't conflict with our roots.
|
|
let migrateArgs: Args = [BookmarkRoots.MobileFolderGUID]
|
|
let migrateLocal =
|
|
"INSERT INTO \(TableBookmarksLocal) " +
|
|
"(guid, type, bmkUri, title, faviconID, local_modified, sync_status, parentid, parentName) " +
|
|
"SELECT guid, type, url AS bmkUri, title, faviconID, " +
|
|
"\(modified) AS local_modified, \(status) AS sync_status, ?, '' " +
|
|
"FROM \(_TableBookmarks) WHERE type IS \(BookmarkNodeType.bookmark.rawValue)"
|
|
|
|
// Create structure for our migrated bookmarks.
|
|
// In order to get contiguous positions (idx), we first insert everything we just migrated under
|
|
// Mobile Bookmarks into a temporary table, then use rowid as our idx.
|
|
|
|
let temporaryTable =
|
|
"CREATE TEMPORARY TABLE children AS " +
|
|
"SELECT guid FROM \(_TableBookmarks) WHERE " +
|
|
"type IS \(BookmarkNodeType.bookmark.rawValue) ORDER BY id ASC"
|
|
|
|
let createStructure =
|
|
"INSERT INTO \(TableBookmarksLocalStructure) (parent, child, idx) " +
|
|
"SELECT ? AS parent, guid AS child, (rowid - 1) AS idx FROM children"
|
|
|
|
let migrate: [(String, Args?)] = [
|
|
(migrateLocal, migrateArgs),
|
|
(temporaryTable, nil),
|
|
(createStructure, migrateArgs),
|
|
|
|
// Drop the temporary table.
|
|
("DROP TABLE children", nil),
|
|
|
|
// Drop the old bookmarks table.
|
|
("DROP TABLE \(_TableBookmarks)", nil),
|
|
|
|
// Create indices for each structure table.
|
|
(indexBufferStructureParentIdx, nil),
|
|
(indexLocalStructureParentIdx, nil),
|
|
(indexMirrorStructureParentIdx, nil),
|
|
]
|
|
|
|
if !self.run(db, queries: prep) ||
|
|
!self.prepopulateRootFolders(db) ||
|
|
!self.run(db, queries: migrate) {
|
|
return false
|
|
}
|
|
// TODO: trigger a sync?
|
|
}
|
|
|
|
// Add views for the overlays.
|
|
if from < 14 && to >= 14 {
|
|
let indexMirrorStructureChild =
|
|
"CREATE INDEX IF NOT EXISTS \(IndexBookmarksMirrorStructureChild) " +
|
|
"ON \(TableBookmarksMirrorStructure) (child)"
|
|
if !self.run(db, queries: [
|
|
self.bufferBookmarksStructureView,
|
|
self.localBookmarksStructureView,
|
|
indexMirrorStructureChild]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from == 14 && to >= 15 {
|
|
// We screwed up some of the views. Recreate them.
|
|
if !self.run(db, queries: [
|
|
"DROP VIEW IF EXISTS \(ViewBookmarksBufferStructureOnMirror)",
|
|
"DROP VIEW IF EXISTS \(ViewBookmarksLocalStructureOnMirror)",
|
|
self.bufferBookmarksStructureView,
|
|
self.localBookmarksStructureView]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 16 && to >= 16 {
|
|
if !self.run(db, queries: [
|
|
oldAllBookmarksView, // Replaced in v30. The new one is not compatible here.
|
|
historyVisitsView,
|
|
awesomebarBookmarksView, // … but this depends on ViewAllBookmarks.
|
|
awesomebarBookmarksWithIconsView]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// That view is re-created later
|
|
// if from < 17 && to >= 17 {
|
|
// if !self.run(db, queries: [
|
|
// // Adds the local_modified, server_modified times to the local bookmarks view
|
|
// "DROP VIEW IF EXISTS \(ViewBookmarksLocalOnMirror)",
|
|
// self.localBookmarksView]) {
|
|
// return false
|
|
// }
|
|
// }
|
|
|
|
if from < 18 && to >= 18 {
|
|
if !self.run(db, queries: [
|
|
// Adds the Activity Stream blocklist table
|
|
activityStreamBlocklistCreate]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 19 && to >= 19 {
|
|
if !self.run(db, queries: [
|
|
// Adds tables/indicies for metadata content
|
|
pageMetadataCreate,
|
|
indexPageMetadataCacheKeyCreate]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// That view is re-created later
|
|
// if from < 20 && to >= 20 {
|
|
// if !self.run(db, queries: [
|
|
// "DROP VIEW IF EXISTS \(ViewBookmarksBufferOnMirror)",
|
|
// self.bufferBookmarksView]) {
|
|
// return false
|
|
// }
|
|
// }
|
|
|
|
if from < 21 && to >= 21 {
|
|
if !self.run(db, queries: [
|
|
"DROP VIEW IF EXISTS \(ViewHistoryVisits)",
|
|
self.historyVisitsView,
|
|
indexPageMetadataSiteURLCreate]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Someone upgrading from v21 will get these tables anyway.
|
|
// So, there's no need to create them only to be dropped and
|
|
// re-created at v27 anyway.
|
|
// if from < 22 && to >= 22 {
|
|
// if !self.run(db, queries: [
|
|
// "DROP TABLE IF EXISTS \(TablePageMetadata)",
|
|
// pageMetadataCreate,
|
|
// indexPageMetadataCacheKeyCreate,
|
|
// indexPageMetadataSiteURLCreate]) {
|
|
// return false
|
|
// }
|
|
// }
|
|
//
|
|
// if from < 23 && to >= 23 {
|
|
// if !self.run(db, queries: [
|
|
// highlightsCreate]) {
|
|
// return false
|
|
// }
|
|
// }
|
|
//
|
|
// if from < 24 && to >= 24 {
|
|
// if !self.run(db, queries: [
|
|
// // We can safely drop the highlights cache table since it gets cleared on every invalidate anyways.
|
|
// "DROP TABLE IF EXISTS \(TableHighlights)",
|
|
// highlightsCreate
|
|
// ]) {
|
|
// return false
|
|
// }
|
|
// }
|
|
|
|
// Someone upgrading from v21 will get this table anyway.
|
|
// So, there's no need to create it only to be dropped and
|
|
// re-created at v26 anyway.
|
|
// if from < 25 && to >= 25 {
|
|
// if !self.run(db, queries: [
|
|
// pinnedTopSitesTableCreate
|
|
// ]) {
|
|
// return false
|
|
// }
|
|
// }
|
|
|
|
if from < 26 && to >= 26 {
|
|
if !self.run(db, queries: [
|
|
// The old pin table was never released so we can safely drop
|
|
"DROP TABLE IF EXISTS \(TablePinnedTopSites)",
|
|
pinnedTopSitesTableCreate
|
|
]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 27 && to >= 27 {
|
|
if !self.run(db, queries: [
|
|
"DROP TABLE IF EXISTS \(TablePageMetadata)",
|
|
"DROP TABLE IF EXISTS \(TableHighlights)",
|
|
pageMetadataCreate,
|
|
indexPageMetadataCacheKeyCreate,
|
|
indexPageMetadataSiteURLCreate,
|
|
highlightsCreate
|
|
]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 28 && to >= 28 {
|
|
if !self.run(db, queries: [
|
|
self.pendingBookmarksDeletions
|
|
]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 29 && to >= 29 {
|
|
if !self.run(db, queries: [
|
|
self.remoteDevices
|
|
]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 30 && to >= 30 {
|
|
// We changed this view as a follow-up to the above in order to exclude buffer
|
|
// deletions from the bookmarked set.
|
|
if !self.run(db, queries: [
|
|
"DROP VIEW IF EXISTS \(ViewAllBookmarks)",
|
|
allBookmarksView
|
|
]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// NOTE: These tables should have already existed in prior
|
|
// versions, but were managed separately via the SchemaTable.
|
|
// Here we create them if they don't already exist to handle
|
|
// cases where we are creating a brand new DB.
|
|
if from < 31 && to >= 31 {
|
|
if !self.run(db, queries: [
|
|
syncCommandsTableCreate,
|
|
clientsTableCreate,
|
|
tabsTableCreate
|
|
]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 32 && to >= 32 {
|
|
var queries: [String] = []
|
|
// If upgrading from < 12 these tables are created with that column already present.
|
|
if from > 12 {
|
|
queries.append(contentsOf: [
|
|
"ALTER TABLE \(TableBookmarksLocal) ADD date_added INTEGER",
|
|
"ALTER TABLE \(TableBookmarksMirror) ADD date_added INTEGER",
|
|
"ALTER TABLE \(TableBookmarksBuffer) ADD date_added INTEGER"
|
|
])
|
|
}
|
|
queries.append(contentsOf: [
|
|
"UPDATE \(TableBookmarksLocal) SET date_added = local_modified",
|
|
"UPDATE \(TableBookmarksMirror) SET date_added = server_modified"
|
|
])
|
|
if !self.run(db, queries: queries) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 33 && to >= 33 {
|
|
if !self.run(db, queries: [
|
|
"DROP VIEW IF EXISTS \(ViewBookmarksBufferOnMirror)",
|
|
"DROP VIEW IF EXISTS \(ViewBookmarksBufferWithDeletionsOnMirror)",
|
|
"DROP VIEW IF EXISTS \(ViewBookmarksLocalOnMirror)",
|
|
self.bufferBookmarksView,
|
|
self.bufferBookmarksWithDeletionsView,
|
|
self.localBookmarksView
|
|
]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if from < 34 && to >= 34 {
|
|
// Drop over-large items from the database, and truncate
|
|
// over-long titles.
|
|
// We do this once, and only for local bookmarks: if they
|
|
// already escaped, then it's better to let them be.
|
|
// Hard-code values here both for simplicity and to make
|
|
// migrations predictable.
|
|
// We don't need to worry about description: we never wrote it.
|
|
if !self.run(db, queries: [
|
|
"DELETE FROM \(TableHistory) WHERE is_deleted = 0 AND length(url) > 65536",
|
|
"DELETE FROM \(TablePageMetadata) WHERE length(site_url) > 65536",
|
|
"DELETE FROM \(TableBookmarksLocal) WHERE is_deleted = 0 AND length(bmkUri) > 65536",
|
|
"UPDATE \(TableBookmarksLocal) SET title = substr(title, 1, 4096)" +
|
|
" WHERE is_deleted = 0 AND length(title) > 4096",
|
|
]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
fileprivate func migrateFromSchemaTableIfNeeded(_ db: SQLiteDBConnection) -> Bool {
|
|
log.info("Checking if schema table migration is needed.")
|
|
|
|
// If `PRAGMA user_version` is v31 or later, we don't need to do anything here.
|
|
guard db.version < 31 else {
|
|
return true
|
|
}
|
|
|
|
// Query for the existence of the `tableList` table to determine if we are
|
|
// migrating from an older DB version or if this is just a brand new DB.
|
|
let sqliteMasterCursor = db.executeQueryUnsafe("SELECT COUNT(*) AS number FROM sqlite_master WHERE type = 'table' AND name = 'tableList'", factory: IntFactory, withArgs: [] as Args)
|
|
|
|
let tableListTableExists = sqliteMasterCursor[0] == 1
|
|
sqliteMasterCursor.close()
|
|
|
|
// If `tableList` still exists in this DB, then we need to continue to check if
|
|
// any table-specific migrations are required before removing it. Otherwise, if
|
|
// `tableList` does not exist, it is likely due to this being a brand new DB and
|
|
// no additional steps need to be taken at this point.
|
|
guard tableListTableExists else {
|
|
return true
|
|
}
|
|
|
|
// If we are unable to migrate the `clients` table from the schema table, we
|
|
// have failed and cannot continue.
|
|
guard migrateClientsTableFromSchemaTableIfNeeded(db) != .failure else {
|
|
return false
|
|
}
|
|
|
|
// Get the *previous* schema version (prior to v31) specified in `tableList`
|
|
// before dropping it.
|
|
let previousVersionCursor = db.executeQueryUnsafe("SELECT version FROM tableList WHERE name = 'BROWSER'", factory: IntFactory, withArgs: [] as Args)
|
|
|
|
let previousVersion = previousVersionCursor[0] ?? 0
|
|
previousVersionCursor.close()
|
|
|
|
// No other intermediate migrations are needed for the remaining tables and
|
|
// we have already captured the *previous* schema version specified in
|
|
// `tableList`, so we can now safely drop it.
|
|
log.info("Schema table migrations complete; Dropping 'tableList' table.")
|
|
|
|
let sql = "DROP TABLE IF EXISTS tableList"
|
|
do {
|
|
try db.executeChange(sql)
|
|
} catch let err as NSError {
|
|
Sentry.shared.sendWithStacktrace(message: "Error dropping tableList table", tag: SentryTag.browserDB, severity: .error, description: "\(err.localizedDescription)")
|
|
return false
|
|
}
|
|
|
|
// Lastly, write the *previous* schema version (prior to v31) to the database
|
|
// using `PRAGMA user_version = ?`.
|
|
do {
|
|
try db.setVersion(previousVersion)
|
|
} catch let err as NSError {
|
|
Sentry.shared.sendWithStacktrace(message: "Error setting database version", tag: SentryTag.browserDB, severity: .error, description: "\(err.localizedDescription)")
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Performs the intermediate migrations for the `clients` table that were previously
|
|
// being handled by the schema table. This should update older versions of the `clients`
|
|
// table prior to v31. If the `clients` table is able to be successfully migrated, this
|
|
// will return `.success`. If no migration is required because either the `clients` table
|
|
// is already at v31 or it does not exist yet at all, this will return `.skipped`.
|
|
// Otherwise, if the `clients` table migration is needed and an error was encountered, we
|
|
// return `.failure`.
|
|
fileprivate func migrateClientsTableFromSchemaTableIfNeeded(_ db: SQLiteDBConnection) -> SchemaUpgradeResult {
|
|
// Query for the existence of the `clients` table to determine if we are
|
|
// migrating from an older DB version or if this is just a brand new DB.
|
|
let sqliteMasterCursor = db.executeQueryUnsafe("SELECT COUNT(*) AS number FROM sqlite_master WHERE type = 'table' AND name = '\(TableClients)'", factory: IntFactory, withArgs: [] as Args)
|
|
|
|
let clientsTableExists = sqliteMasterCursor[0] == 1
|
|
sqliteMasterCursor.close()
|
|
|
|
guard clientsTableExists else {
|
|
return .skipped
|
|
}
|
|
|
|
// Check if intermediate migrations are necessary for the 'clients' table.
|
|
let previousVersionCursor = db.executeQueryUnsafe("SELECT version FROM tableList WHERE name = '\(TableClients)'", factory: IntFactory, withArgs: [] as Args)
|
|
|
|
let previousClientsTableVersion = previousVersionCursor[0] ?? 0
|
|
previousVersionCursor.close()
|
|
|
|
guard previousClientsTableVersion > 0 && previousClientsTableVersion <= 3 else {
|
|
return .skipped
|
|
}
|
|
|
|
log.info("Migrating '\(TableClients)' table from version \(previousClientsTableVersion).")
|
|
|
|
if previousClientsTableVersion < 2 {
|
|
let sql = "ALTER TABLE \(TableClients) ADD COLUMN version TEXT"
|
|
do {
|
|
try db.executeChange(sql)
|
|
} catch let err as NSError {
|
|
log.error("Error altering \(TableClients) table: \(err.localizedDescription); SQL was \(sql)")
|
|
let extra = ["table": "\(TableClients)", "errorDescription": "\(err.localizedDescription)", "sql": "\(sql)"]
|
|
Sentry.shared.sendWithStacktrace(message: "Error altering table", tag: SentryTag.browserDB, severity: .error, extra: extra)
|
|
return .failure
|
|
}
|
|
}
|
|
|
|
if previousClientsTableVersion < 3 {
|
|
let sql = "ALTER TABLE \(TableClients) ADD COLUMN fxaDeviceId TEXT"
|
|
do {
|
|
try db.executeChange(sql)
|
|
} catch let err as NSError {
|
|
log.error("Error altering \(TableClients) table: \(err.localizedDescription); SQL was \(sql)")
|
|
let extra = ["table": "\(TableClients)", "errorDescription": "\(err.localizedDescription)", "sql": "\(sql)"]
|
|
Sentry.shared.sendWithStacktrace(message: "Error altering table", tag: SentryTag.browserDB, severity: .error, extra: extra)
|
|
return .failure
|
|
}
|
|
}
|
|
|
|
return .success
|
|
}
|
|
|
|
fileprivate func fillDomainNamesFromCursor(_ cursor: Cursor<String>, db: SQLiteDBConnection) -> Bool {
|
|
if cursor.count == 0 {
|
|
return true
|
|
}
|
|
|
|
// URL -> hostname, flattened to make args.
|
|
var pairs = Args()
|
|
pairs.reserveCapacity(cursor.count * 2)
|
|
for url in cursor {
|
|
if let url = url, let host = url.asURL?.normalizedHost {
|
|
pairs.append(url)
|
|
pairs.append(host)
|
|
}
|
|
}
|
|
cursor.close()
|
|
|
|
let tmpTable = "tmp_hostnames"
|
|
let table = "CREATE TEMP TABLE \(tmpTable) (url TEXT NOT NULL UNIQUE, domain TEXT NOT NULL, domain_id INT)"
|
|
if !self.run(db, sql: table, args: nil) {
|
|
log.error("Can't create temporary table. Unable to migrate domain names. Top Sites is likely to be broken.")
|
|
return false
|
|
}
|
|
|
|
// Now insert these into the temporary table. Chunk by an even number, for obvious reasons.
|
|
let chunks = chunk(pairs, by: BrowserDB.MaxVariableNumber - (BrowserDB.MaxVariableNumber % 2))
|
|
for chunk in chunks {
|
|
let ins = "INSERT INTO \(tmpTable) (url, domain) VALUES " +
|
|
Array<String>(repeating: "(?, ?)", count: chunk.count / 2).joined(separator: ", ")
|
|
if !self.run(db, sql: ins, args: Array(chunk)) {
|
|
log.error("Couldn't insert domains into temporary table. Aborting migration.")
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Now make those into domains.
|
|
let domains = "INSERT OR IGNORE INTO \(TableDomains) (domain) SELECT DISTINCT domain FROM \(tmpTable)"
|
|
|
|
// … and fill that temporary column.
|
|
let domainIDs = "UPDATE \(tmpTable) SET domain_id = (SELECT id FROM \(TableDomains) WHERE \(TableDomains).domain = \(tmpTable).domain)"
|
|
|
|
// Update the history table from the temporary table.
|
|
let updateHistory = "UPDATE \(TableHistory) SET domain_id = (SELECT domain_id FROM \(tmpTable) WHERE \(tmpTable).url = \(TableHistory).url)"
|
|
|
|
// Clean up.
|
|
let dropTemp = "DROP TABLE \(tmpTable)"
|
|
|
|
// Now run these.
|
|
if !self.run(db, queries: [domains,
|
|
domainIDs,
|
|
updateHistory,
|
|
dropTemp]) {
|
|
log.error("Unable to migrate domains.")
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
public func drop(_ db: SQLiteDBConnection) -> Bool {
|
|
log.debug("Dropping all browser tables.")
|
|
let additional = [
|
|
"DROP TABLE IF EXISTS faviconSites" // We renamed it to match naming convention.
|
|
]
|
|
|
|
let views = AllViews.map { "DROP VIEW IF EXISTS \($0)" }
|
|
let indices = AllIndices.map { "DROP INDEX IF EXISTS \($0)" }
|
|
let tables = AllTables.map { "DROP TABLE IF EXISTS \($0)" }
|
|
let queries = Array([views, indices, tables, additional].joined())
|
|
return self.run(db, queries: queries)
|
|
}
|
|
}
|