Skip to content

Commit

Permalink
Database.indexes(on:) and Database.table(_:hasUniqueKey:) are public
Browse files Browse the repository at this point in the history
  • Loading branch information
groue committed Jul 21, 2016
1 parent b9e19e8 commit fcf90a0
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 80 deletions.
54 changes: 36 additions & 18 deletions GRDB.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

47 changes: 27 additions & 20 deletions GRDB/Core/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -896,34 +896,33 @@ extension Database {
/// The indexes on table named `tableName`; returns the empty array if the
/// table does not exist.
///
/// Note: SQLite defines no index for INTEGER PRIMARY KEY columns: this
/// method does not return any index that represents this unique constraint.
/// Note: SQLite does not define any index for INTEGER PRIMARY KEY columns:
/// this method does not return any index that represents this primary key.
///
/// If you want to know if a set of columns uniquely identify a row, prefer
/// columns(_:uniquelyIdentifyRowsIn:) instead.
func indexes(on tableName: String) -> [IndexInfo] {
/// table(_:hasUniqueKey:) instead.
public func indexes(on tableName: String) -> [TableIndex] {
if let indexes = schemaCache.indexes(on: tableName) {
return indexes
}

let indexes = Row.fetch(self, "PRAGMA index_list(\(tableName.quotedDatabaseIdentifier))").map { row -> IndexInfo in
let indexes = Row.fetch(self, "PRAGMA index_list(\(tableName.quotedDatabaseIdentifier))").map { row -> TableIndex in
let indexName: String = row.value(atIndex: 1)
let unique: Bool = row.value(atIndex: 2)
let columns = Row.fetch(self, "PRAGMA index_info(\(indexName.quotedDatabaseIdentifier))")
.map { ($0.value(atIndex: 0) as Int, $0.value(atIndex: 2) as String) }
.sort { $0.0 < $1.0 }
.map { $0.1 }
return IndexInfo(name: indexName, columns: columns, unique: unique)
return TableIndex(name: indexName, columns: columns, unique: unique)
}

schemaCache.setIndexes(indexes, forTableName: tableName)
return indexes
}

/// True if a set of columns uniquely identify a row, that is to say if
/// there is a unique index on those columns, or if the column is the
/// INTEGER PRIMARY KEY.
func columns<T: SequenceType where T.Generator.Element == String>(columns: T, uniquelyIdentifyRowsIn tableName: String) throws -> Bool {
/// True if a set of columns uniquely identifies a row, that is to say if
/// the columns are the primary key, or if there is a unique index on them.
public func table<T: SequenceType where T.Generator.Element == String>(tableName: String, hasUniqueKey columns: T) throws -> Bool {
let primaryKey = try self.primaryKey(tableName) // first, so that we fail early and consistently should the table not exist
let columns = Set(columns)
if indexes(on: tableName).contains({ index in index.isUnique && Set(index.columns) == columns }) {
Expand Down Expand Up @@ -963,17 +962,25 @@ extension Database {
primaryKeyIndex = row.value(named: "pk")
}
}
}

/// An index on a database table.
///
/// See `Database.indexes(on:)`
public struct TableIndex {
/// The name of the index
public let name: String

struct IndexInfo {
let name: String
let columns: [String]
let isUnique: Bool
init(name: String, columns: [String], unique: Bool) {
self.name = name
self.columns = columns
self.isUnique = unique
}
/// The indexed columns
public let columns: [String]

/// True if the index is unique
public let isUnique: Bool

init(name: String, columns: [String], unique: Bool) {
self.name = name
self.columns = columns
self.isUnique = unique
}
}

Expand Down
14 changes: 7 additions & 7 deletions GRDB/Core/DatabaseSchemaCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ protocol DatabaseSchemaCacheType {
func primaryKey(tableName tableName: String) -> PrimaryKey??
mutating func setPrimaryKey(primaryKey: PrimaryKey?, forTableName tableName: String)

func indexes(on tableName: String) -> [Database.IndexInfo]?
mutating func setIndexes(indexes: [Database.IndexInfo], forTableName tableName: String)
func indexes(on tableName: String) -> [TableIndex]?
mutating func setIndexes(indexes: [TableIndex], forTableName tableName: String)
}

/// A thread-unsafe database schema cache
final class DatabaseSchemaCache: DatabaseSchemaCacheType {
private var primaryKeys: [String: PrimaryKey?] = [:]
private var indexes: [String: [Database.IndexInfo]] = [:]
private var indexes: [String: [TableIndex]] = [:]

func clear() {
primaryKeys = [:]
Expand All @@ -32,11 +32,11 @@ final class DatabaseSchemaCache: DatabaseSchemaCacheType {
primaryKeys[tableName] = primaryKey
}

func indexes(on tableName: String) -> [Database.IndexInfo]? {
func indexes(on tableName: String) -> [TableIndex]? {
return indexes[tableName]
}

func setIndexes(indexes: [Database.IndexInfo], forTableName tableName: String) {
func setIndexes(indexes: [TableIndex], forTableName tableName: String) {
self.indexes[tableName] = indexes
}
}
Expand All @@ -57,11 +57,11 @@ final class SharedDatabaseSchemaCache: DatabaseSchemaCacheType {
cache.write { $0.setPrimaryKey(primaryKey, forTableName: tableName) }
}

func indexes(on tableName: String) -> [Database.IndexInfo]? {
func indexes(on tableName: String) -> [TableIndex]? {
return cache.read { $0.indexes(on: tableName) }
}

func setIndexes(indexes: [Database.IndexInfo], forTableName tableName: String) {
func setIndexes(indexes: [TableIndex], forTableName tableName: String) {
cache.write { $0.setIndexes(indexes, forTableName: tableName) }
}
}
4 changes: 2 additions & 2 deletions GRDB/Record/TableMapping.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ extension RowConvertible where Self: TableMapping {
for dictionary in keys {
GRDBPrecondition(dictionary.count > 0, "Invalid empty key dictionary")
let columns = dictionary.keys
guard try db.columns(columns, uniquelyIdentifyRowsIn: databaseTableName) else {
guard try db.table(databaseTableName, hasUniqueKey: columns) else {
if fatalErrorOnMissingUniqueIndex {
fatalError("table \(databaseTableName) has no unique index on column(s) \(columns.joinWithSeparator(", "))")
} else {
Expand Down Expand Up @@ -332,7 +332,7 @@ extension TableMapping {
for dictionary in keys {
GRDBPrecondition(dictionary.count > 0, "Invalid empty key dictionary")
let columns = dictionary.keys
guard try db.columns(columns, uniquelyIdentifyRowsIn: databaseTableName) else {
guard try db.table(databaseTableName, hasUniqueKey: columns) else {
if fatalErrorOnMissingUniqueIndex {
fatalError("table \(databaseTableName) has no unique index on column(s) \(columns.joinWithSeparator(", "))")
} else {
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1328,11 +1328,13 @@ for row in Row.fetch(db, "PRAGMA table_info('persons')") {
}
```

GRDB provides two high-level methods as well:
GRDB provides four high-level methods as well:

```swift
db.tableExists("persons") // Bool, true if the table exists
try db.primaryKey("persons") // PrimaryKey?, throws if the table does not exist
db.indexes(on: "persons") // [TableIndex], the indexes defined on the table
try db.table("persons", hasUniqueKey: ["id"]) // Bool, true if column(s) is a unique key
try db.primaryKey("persons") // PrimaryKey?
```

Primary key is nil when table has no primary key:
Expand Down
1 change: 0 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
- [ ] Fix "More, when you're interested in specific table columns, you're out of luck, because databaseDidChange does not know about columns: it just knows that a row has been inserted, deleted, or updated, without further detail"
- [ ] Make public functions that return tables indexes
- [ ] GRDBCipher: remove limitations on iOS or OS X versions
- [ ] FetchedRecordsController: take inspiration from https://github.com/jflinter/Dwifft
- [ ] File protection: Read https://github.com/ccgus/fmdb/issues/262 and understand https://lists.apple.com/archives/cocoa-dev/2012/Aug/msg00527.html
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,7 @@ private struct Person : RowConvertible, TableMapping {
}
}

private struct Citizenship : RowConvertible, TableMapping {
static func databaseTableName() -> String {
return "citizenships"
}
init(_ row: Row) {
}
}

class UniqueIndexTests: GRDBTestCase {

func testColumnsThatUniquelyIdentityRows() {
assertNoError { db in
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.execute("CREATE TABLE persons (id INTEGER PRIMARY KEY, name TEXT, email TEXT UNIQUE)")
try XCTAssertTrue(db.columns(["id"], uniquelyIdentifyRowsIn: "persons"))
try XCTAssertTrue(db.columns(["email"], uniquelyIdentifyRowsIn: "persons"))
try XCTAssertFalse(db.columns([], uniquelyIdentifyRowsIn: "persons"))
try XCTAssertFalse(db.columns(["name"], uniquelyIdentifyRowsIn: "persons"))
try XCTAssertFalse(db.columns(["id", "email"], uniquelyIdentifyRowsIn: "persons"))

try db.execute("CREATE TABLE citizenships (personId INTEGER NOT NULL, countryIsoCode TEXT NOT NULL, PRIMARY KEY (personId, countryIsoCode))")
try XCTAssertTrue(db.columns(["personId", "countryIsoCode"], uniquelyIdentifyRowsIn: "citizenships"))
try XCTAssertTrue(db.columns(["countryIsoCode", "personId"], uniquelyIdentifyRowsIn: "citizenships"))
try XCTAssertFalse(db.columns([], uniquelyIdentifyRowsIn: "persons"))
try XCTAssertFalse(db.columns(["personId"], uniquelyIdentifyRowsIn: "persons"))
try XCTAssertFalse(db.columns(["countryIsoCode"], uniquelyIdentifyRowsIn: "persons"))
}
}
}
class RecordUniqueIndexTests: GRDBTestCase {

func testFetchOneRequiresUniqueIndex() {
assertNoError { db in
Expand Down
73 changes: 73 additions & 0 deletions Tests/Public/Core/Database/TableIndexTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import XCTest
#if USING_SQLCIPHER
import GRDBCipher
#elseif USING_CUSTOMSQLITE
import GRDBCustomSQLite
#else
import GRDB
#endif

class TableIndexTests: GRDBTestCase {

func testIndexes() {
assertNoError { db in
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
do {
try db.execute("CREATE TABLE persons (id INTEGER PRIMARY KEY, name TEXT, email TEXT UNIQUE)")
let indexes = db.indexes(on: "persons")

XCTAssertEqual(indexes.count, 1)
XCTAssertEqual(indexes[0].name, "sqlite_autoindex_persons_1")
XCTAssertEqual(indexes[0].columns, ["email"])
XCTAssertTrue(indexes[0].isUnique)
}

do {
try db.execute("CREATE TABLE citizenships (year INTEGER, personId INTEGER NOT NULL, countryIsoCode TEXT NOT NULL, PRIMARY KEY (personId, countryIsoCode))")
try db.execute("CREATE INDEX citizenshipsOnYear ON citizenships(year)")
let indexes = db.indexes(on: "citizenships")

XCTAssertEqual(indexes.count, 2)
if let i = indexes.indexOf({ $0.columns == ["year"] }) {
XCTAssertEqual(indexes[i].name, "citizenshipsOnYear")
XCTAssertEqual(indexes[i].columns, ["year"])
XCTAssertFalse(indexes[i].isUnique)
} else {
XCTFail()
}
if let i = indexes.indexOf({ $0.columns == ["personId", "countryIsoCode"] }) {
XCTAssertEqual(indexes[i].name, "sqlite_autoindex_citizenships_1")
XCTAssertEqual(indexes[i].columns, ["personId", "countryIsoCode"])
XCTAssertTrue(indexes[i].isUnique)
} else {
XCTFail()
}
}
}
}
}

func testColumnsThatUniquelyIdentityRows() {
assertNoError { db in
let dbQueue = try makeDatabaseQueue()
try dbQueue.inDatabase { db in
try db.execute("CREATE TABLE persons (id INTEGER PRIMARY KEY, name TEXT, email TEXT UNIQUE)")
try XCTAssertTrue(db.table("persons", hasUniqueKey: ["id"]))
try XCTAssertTrue(db.table("persons", hasUniqueKey: ["email"]))
try XCTAssertFalse(db.table("persons", hasUniqueKey: []))
try XCTAssertFalse(db.table("persons", hasUniqueKey: ["name"]))
try XCTAssertFalse(db.table("persons", hasUniqueKey: ["id", "email"]))

try db.execute("CREATE TABLE citizenships (year INTEGER, personId INTEGER NOT NULL, countryIsoCode TEXT NOT NULL, PRIMARY KEY (personId, countryIsoCode))")
try db.execute("CREATE INDEX citizenshipsOnYear ON citizenships(year)")
try XCTAssertTrue(db.table("citizenships", hasUniqueKey: ["personId", "countryIsoCode"]))
try XCTAssertTrue(db.table("citizenships", hasUniqueKey: ["countryIsoCode", "personId"]))
try XCTAssertFalse(db.table("citizenships", hasUniqueKey: []))
try XCTAssertFalse(db.table("citizenships", hasUniqueKey: ["year"]))
try XCTAssertFalse(db.table("citizenships", hasUniqueKey: ["personId"]))
try XCTAssertFalse(db.table("citizenships", hasUniqueKey: ["countryIsoCode"]))
}
}
}
}

0 comments on commit fcf90a0

Please sign in to comment.