Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Foster auto-incremented primary keys #337

Merged
merged 4 commits into from
Apr 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion GRDB/Core/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@ extension Database {
/// An SQL column type.
///
/// try db.create(table: "players") { t in
/// t.column("id", .integer).primaryKey()
/// t.autoIncrementedPrimaryKey("id")
/// t.column("title", .text)
/// }
///
Expand Down
38 changes: 35 additions & 3 deletions GRDB/QueryInterface/TableDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ extension Database {
/// Creates a database table.
///
/// try db.create(table: "pointOfInterests") { t in
/// t.column("id", .integer).primaryKey()
/// t.autoIncrementedPrimaryKey("id")
/// t.column("title", .text)
/// t.column("favorite", .boolean).notNull().default(false)
/// t.column("longitude", .double).notNull()
Expand Down Expand Up @@ -35,7 +35,7 @@ extension Database {
/// Creates a database table.
///
/// try db.create(table: "pointOfInterests") { t in
/// t.column("id", .integer).primaryKey()
/// t.autoIncrementedPrimaryKey("id")
/// t.column("title", .text)
/// t.column("favorite", .boolean).notNull().default(false)
/// t.column("longitude", .double).notNull()
Expand Down Expand Up @@ -67,7 +67,7 @@ extension Database {
/// Creates a database table.
///
/// try db.create(table: "pointOfInterests") { t in
/// t.column("id", .integer).primaryKey()
/// t.autoIncrementedPrimaryKey("id")
/// t.column("title", .text)
/// t.column("favorite", .boolean).notNull().default(false)
/// t.column("longitude", .double).notNull()
Expand Down Expand Up @@ -248,6 +248,38 @@ public final class TableDefinition {
self.withoutRowID = withoutRowID
}

/// Defines the auto-incremented primary key.
///
/// try db.create(table: "players") { t in
/// t.autoIncrementedPrimaryKey("id")
/// }
///
/// The auto-incremented primary key is an integer primary key that
/// automatically generates unused values when you do not explicitely
/// provide one, and prevents the reuse of ids over the lifetime of
/// the database.
///
/// **It is the preferred way to define a numeric primary key**.
///
/// The fact that an auto-incremented primary key prevents the reuse of
/// ids is an excellent guard against data races that could happen when your
/// application processes ids in an asynchronous way. The auto-incremented
/// primary key provides the guarantee that a given id can't reference a row
/// that is different from the one it used to be at the beginning of the
/// asynchronous process, even if this row gets deleted and a new one is
/// inserted in between.
///
/// See https://www.sqlite.org/lang_createtable.html#primkeyconst and
/// https://www.sqlite.org/lang_createtable.html#rowid
///
/// - parameter conflitResolution: An optional conflict resolution
/// (see https://www.sqlite.org/lang_conflict.html).
/// - returns: Self so that you can further refine the column definition.
@discardableResult
public func autoIncrementedPrimaryKey(_ name: String, onConflict conflictResolution: Database.ConflictResolution? = nil) -> ColumnDefinition {
return column(name, .integer).primaryKey(onConflict: conflictResolution, autoincrement: true)
}

/// Appends a table column.
///
/// try db.create(table: "players") { t in
Expand Down
2 changes: 1 addition & 1 deletion GRDB/Record/TableRecord.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ extension TableRecord {
///
/// try dbQueue.write { db in
/// try db.create(table: "players") { t in
/// t.column("id", .integer).primaryKey()
/// t.autoIncrementedPrimaryKey("id")
/// t.column("name", .text)
/// t.column("score", .integer)
/// }
Expand Down
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Avoid SQL with the [query interface](#the-query-interface):
```swift
try dbQueue.write { db in
try db.create(table: "places") { t in
t.column("id", .integer).primaryKey()
t.autoIncrementedPrimaryKey("id")
t.column("title", .text).notNull()
t.column("favorite", .boolean).notNull().defaults(to: false)
t.column("longitude", .double).notNull()
Expand Down Expand Up @@ -1707,7 +1707,7 @@ Int.fetchOne(db, Player.select(maxLength.apply(nameColumn))) // Int?

```swift
try db.create(table: "players") { t in
t.column("id", .integer).primaryKey()
t.autoIncrementedPrimaryKey("id")
t.column("name", .text)
}

Expand Down Expand Up @@ -2660,7 +2660,7 @@ The [five different policies](https://www.sqlite.org/lang_conflict.html) are: ab
// email TEXT UNIQUE ON CONFLICT REPLACE
// )
try db.create(table: "players") { t in
t.column("id", .integer).primaryKey()
t.autoIncrementedPrimaryKey("id")
t.column("email", .text).unique(onConflict: .replace) // <--
}

Expand All @@ -2678,7 +2678,7 @@ The [five different policies](https://www.sqlite.org/lang_conflict.html) are: ab
// email TEXT UNIQUE
// )
try db.create(table: "players") { t in
t.column("id", .integer).primaryKey()
t.autoIncrementedPrimaryKey("id")
t.column("email", .text)
}

Expand Down Expand Up @@ -3013,7 +3013,7 @@ Once granted with a [database connection](#database-connections), you can setup
// longitude DOUBLE NOT NULL
// )
try db.create(table: "places") { t in
t.column("id", .integer).primaryKey()
t.autoIncrementedPrimaryKey("id")
t.column("title", .text)
t.column("favorite", .boolean).notNull().defaults(to: false)
t.column("longitude", .double).notNull()
Expand Down Expand Up @@ -3067,8 +3067,11 @@ Define **not null** columns, and set **default** values:
Use an individual column as **primary**, **unique**, or **foreign key**. When defining a foreign key, the referenced column is the primary key of the referenced table (unless you specify otherwise):

```swift
// id INTEGER PRIMARY KEY,
t.column("id", .integer).primaryKey()
// id INTEGER PRIMARY KEY AUTOINCREMENT,
t.autoIncrementedPrimaryKey("id")

// uuid TEXT PRIMARY KEY,
t.column("uuid", .text).primaryKey()

// email TEXT UNIQUE,
t.column("email", .text).unique()
Expand All @@ -3077,6 +3080,10 @@ Use an individual column as **primary**, **unique**, or **foreign key**. When de
t.column("countryCode", .text).references("countries", onDelete: .cascade)
```

> :bulb: **Tip**: when you need an integer primary key that automatically generates unique values, it is highly recommended that you use the `autoIncrementedPrimaryKey` method.
>
> Such primary key prevents the reuse of ids, and is an excellent guard against data races that could happen when your application processes ids in an asynchronous way. The auto-incremented primary key provides the guarantee that a given id can't reference a row that is different from the one it used to be at the beginning of the asynchronous process, even if this row gets deleted and a new one is inserted in between.

**Create an index** on the column:

```swift
Expand Down
26 changes: 26 additions & 0 deletions Tests/GRDBTests/TableDefinitionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,32 @@ class TableDefinitionTests: GRDBTestCase {
}
}

func testAutoIncrementedPrimaryKey() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inTransaction { db in
try db.create(table: "test") { t in
t.autoIncrementedPrimaryKey("id")
}
assertEqualSQL(lastSQLQuery, """
CREATE TABLE "test" (\
"id" INTEGER PRIMARY KEY AUTOINCREMENT\
)
""")
return .rollback
}
try dbQueue.inTransaction { db in
try db.create(table: "test") { t in
t.autoIncrementedPrimaryKey("id", onConflict: .fail)
}
assertEqualSQL(lastSQLQuery, """
CREATE TABLE "test" (\
"id" INTEGER PRIMARY KEY ON CONFLICT FAIL AUTOINCREMENT\
)
""")
return .rollback
}
}

func testColumnPrimaryKeyOptions() throws {
let dbQueue = try makeDatabaseQueue()
try dbQueue.inTransaction { db in
Expand Down