From 73e89f76aa6b6cd5faaa82054bb61559bb334a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Apr 2018 19:41:21 +0200 Subject: [PATCH 1/4] Introduce TableDefinition.autoIncrementedPrimaryKey --- GRDB/QueryInterface/TableDefinition.swift | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/GRDB/QueryInterface/TableDefinition.swift b/GRDB/QueryInterface/TableDefinition.swift index 49c3d8d410..2fadbe2cf0 100644 --- a/GRDB/QueryInterface/TableDefinition.swift +++ b/GRDB/QueryInterface/TableDefinition.swift @@ -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 From 822ff97050dc0050dc1718d97c50a6b1228f6dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Apr 2018 19:41:45 +0200 Subject: [PATCH 2/4] Tests for TableDefinition.autoIncrementedPrimaryKey --- Tests/GRDBTests/TableDefinitionTests.swift | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Tests/GRDBTests/TableDefinitionTests.swift b/Tests/GRDBTests/TableDefinitionTests.swift index f70fa0c16a..b25ec0aa21 100644 --- a/Tests/GRDBTests/TableDefinitionTests.swift +++ b/Tests/GRDBTests/TableDefinitionTests.swift @@ -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 From e80dab2ea9fbed4ac3e523f9daf63eedfde1407f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Apr 2018 19:42:27 +0200 Subject: [PATCH 3/4] Documentation for TableDefinition.autoIncrementedPrimaryKey --- README.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4dc8ba7e5c..c01429e489 100644 --- a/README.md +++ b/README.md @@ -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() @@ -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) } @@ -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) // <-- } @@ -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) } @@ -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() @@ -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() @@ -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 From 7aacaef2cd58eb9d8cd7bf8f6901e54676105b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gwendal=20Roue=CC=81?= Date: Sat, 21 Apr 2018 20:11:34 +0200 Subject: [PATCH 4/4] Foster autoIncrementedPrimaryKey in documentation --- GRDB/Core/Database.swift | 2 +- GRDB/QueryInterface/TableDefinition.swift | 6 +++--- GRDB/Record/TableRecord.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GRDB/Core/Database.swift b/GRDB/Core/Database.swift index ee961927b4..9d38e5cd91 100644 --- a/GRDB/Core/Database.swift +++ b/GRDB/Core/Database.swift @@ -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) /// } /// diff --git a/GRDB/QueryInterface/TableDefinition.swift b/GRDB/QueryInterface/TableDefinition.swift index 2fadbe2cf0..d9168e733d 100644 --- a/GRDB/QueryInterface/TableDefinition.swift +++ b/GRDB/QueryInterface/TableDefinition.swift @@ -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() @@ -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() @@ -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() diff --git a/GRDB/Record/TableRecord.swift b/GRDB/Record/TableRecord.swift index 82bf379e22..db1483e0c8 100644 --- a/GRDB/Record/TableRecord.swift +++ b/GRDB/Record/TableRecord.swift @@ -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) /// }