Skip to content

Commit 05da03a

Browse files
committed
Expose generated columns with availability checks
1 parent 7b63ddf commit 05da03a

File tree

2 files changed

+113
-19
lines changed

2 files changed

+113
-19
lines changed

GRDB/QueryInterface/Schema/TableDefinition.swift

Lines changed: 95 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,16 @@ extension Database {
206206
///
207207
/// For extra index options, see ``create(index:on:columns:options:condition:)``.
208208
///
209+
/// ### Generated columns
210+
///
211+
/// See [Generated columns](https://sqlite.org/gencol.html) for
212+
/// more information:
213+
///
214+
/// ```swift
215+
/// t.column("totalScore", .integer).generatedAs(sql: "score + bonus")
216+
/// t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"))
217+
/// ```
218+
///
209219
/// ### Integrity checks
210220
///
211221
/// SQLite will only let conforming rows in:
@@ -224,16 +234,6 @@ extension Database {
224234
/// t.check(sql: "a + b < 10")
225235
/// ```
226236
///
227-
/// ### Generated columns
228-
///
229-
/// [Generated columns](https://sqlite.org/gencol.html) are available with a
230-
/// [custom SQLite build](https://github.com/groue/GRDB.swift/blob/master/Documentation/CustomSQLiteBuilds.md):
231-
///
232-
/// ```swift
233-
/// t.column("totalScore", .integer).generatedAs(sql: "score + bonus")
234-
/// t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"))
235-
/// ```
236-
///
237237
/// ### Raw SQL columns and constraints
238238
///
239239
/// Columns and constraints can be defined with raw sql:
@@ -1396,6 +1396,11 @@ public final class TableAlteration {
13961396
/// - ``collate(_:)-4dljx``
13971397
/// - ``collate(_:)-9ywza``
13981398
///
1399+
/// ### Generated Columns
1400+
///
1401+
/// - ``generatedAs(_:_:)``
1402+
/// - ``generatedAs(sql:_:)``
1403+
///
13991404
/// ### Other Constraints
14001405
///
14011406
/// - ``check(_:)``
@@ -1699,7 +1704,84 @@ public final class ColumnDefinition {
16991704
return self
17001705
}
17011706

1702-
#if GRDBCUSTOMSQLITE
1707+
#if GRDBCUSTOMSQLITE || GRDBCIPHER
1708+
/// Defines the column as a generated column.
1709+
///
1710+
/// For example:
1711+
///
1712+
/// ```swift
1713+
/// // CREATE TABLE player(
1714+
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
1715+
/// // score INTEGER NOT NULL,
1716+
/// // bonus INTEGER NOT NULL,
1717+
/// // totalScore INTEGER GENERATED ALWAYS AS (score + bonus) STORED
1718+
/// // )
1719+
/// try db.create(table: "player") { t in
1720+
/// t.autoIncrementedPrimaryKey("id")
1721+
/// t.column("score", .integer).notNull()
1722+
/// t.column("bonus", .integer).notNull()
1723+
/// t.column("totalScore", .integer).generatedAs(sql: "score + bonus", .stored)
1724+
/// }
1725+
/// ```
1726+
///
1727+
/// Related SQLite documentation: <https://sqlite.org/gencol.html>
1728+
///
1729+
/// - parameters:
1730+
/// - sql: An SQL expression.
1731+
/// - qualification: The generated column's qualification, which
1732+
/// defaults to ``GeneratedColumnQualification/virtual``.
1733+
/// - returns: `self` so that you can further refine the column definition.
1734+
@discardableResult
1735+
public func generatedAs(
1736+
sql: String,
1737+
_ qualification: GeneratedColumnQualification = .virtual)
1738+
-> Self
1739+
{
1740+
let expression = SQL(sql: sql).sqlExpression
1741+
generatedColumnConstraint = GeneratedColumnConstraint(
1742+
expression: expression,
1743+
qualification: qualification)
1744+
return self
1745+
}
1746+
1747+
/// Defines the column as a generated column.
1748+
///
1749+
/// For example:
1750+
///
1751+
/// ```swift
1752+
/// // CREATE TABLE player(
1753+
/// // id INTEGER PRIMARY KEY AUTOINCREMENT,
1754+
/// // score INTEGER NOT NULL,
1755+
/// // bonus INTEGER NOT NULL,
1756+
/// // totalScore INTEGER GENERATED ALWAYS AS (score + bonus) STORED
1757+
/// // )
1758+
/// try db.create(table: "player") { t in
1759+
/// t.autoIncrementedPrimaryKey("id")
1760+
/// t.column("score", .integer).notNull()
1761+
/// t.column("bonus", .integer).notNull()
1762+
/// t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"), .stored)
1763+
/// }
1764+
/// ```
1765+
///
1766+
/// Related SQLite documentation: <https://sqlite.org/gencol.html>
1767+
///
1768+
/// - parameters:
1769+
/// - expression: The generated expression.
1770+
/// - qualification: The generated column's qualification, which
1771+
/// defaults to ``GeneratedColumnQualification/virtual``.
1772+
/// - returns: `self` so that you can further refine the column definition.
1773+
@discardableResult
1774+
public func generatedAs(
1775+
_ expression: some SQLExpressible,
1776+
_ qualification: GeneratedColumnQualification = .virtual)
1777+
-> Self
1778+
{
1779+
generatedColumnConstraint = GeneratedColumnConstraint(
1780+
expression: expression.sqlExpression,
1781+
qualification: qualification)
1782+
return self
1783+
}
1784+
#else
17031785
/// Defines the column as a generated column.
17041786
///
17051787
/// For example:
@@ -1726,6 +1808,7 @@ public final class ColumnDefinition {
17261808
/// - qualification: The generated column's qualification, which
17271809
/// defaults to ``GeneratedColumnQualification/virtual``.
17281810
/// - returns: `self` so that you can further refine the column definition.
1811+
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) // SQLite 3.35.0+ (3.31 actually)
17291812
@discardableResult
17301813
public func generatedAs(
17311814
sql: String,
@@ -1765,6 +1848,7 @@ public final class ColumnDefinition {
17651848
/// - qualification: The generated column's qualification, which
17661849
/// defaults to ``GeneratedColumnQualification/virtual``.
17671850
/// - returns: `self` so that you can further refine the column definition.
1851+
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) // SQLite 3.35.0+ (3.31 actually)
17681852
@discardableResult
17691853
public func generatedAs(
17701854
_ expression: some SQLExpressible,

Tests/GRDBTests/TableDefinitionTests.swift

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,15 @@ class TableDefinitionTests: GRDBTestCase {
357357
}
358358

359359
func testColumnGeneratedAs() throws {
360-
#if !GRDBCUSTOMSQLITE
361-
throw XCTSkip("Generated columns are not available")
362-
#else
360+
#if GRDBCUSTOMSQLITE || GRDBCIPHER
361+
guard sqlite3_libversion_number() >= 3031000 else {
362+
throw XCTSkip("Generated columns are not available")
363+
}
364+
#else
365+
guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) else {
366+
throw XCTSkip("Generated columns are not available")
367+
}
368+
#endif
363369
let dbQueue = try makeDatabaseQueue()
364370
try dbQueue.inTransaction { db in
365371
try db.create(table: "test") { t in
@@ -387,7 +393,6 @@ class TableDefinitionTests: GRDBTestCase {
387393
""")
388394
return .rollback
389395
}
390-
#endif
391396
}
392397

393398
func testTablePrimaryKey() throws {
@@ -767,9 +772,15 @@ class TableDefinitionTests: GRDBTestCase {
767772
}
768773

769774
func testAlterTableAddGeneratedVirtualColumn() throws {
770-
#if !GRDBCUSTOMSQLITE
771-
throw XCTSkip("Generated columns are not available")
772-
#else
775+
#if GRDBCUSTOMSQLITE || GRDBCIPHER
776+
guard sqlite3_libversion_number() >= 3031000 else {
777+
throw XCTSkip("Generated columns are not available")
778+
}
779+
#else
780+
guard #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) else {
781+
throw XCTSkip("Generated columns are not available")
782+
}
783+
#endif
773784
let dbQueue = try makeDatabaseQueue()
774785
try dbQueue.inDatabase { db in
775786
try db.create(table: "test") { t in
@@ -794,7 +805,6 @@ class TableDefinitionTests: GRDBTestCase {
794805
assertEqualSQL(latestQueries[3], "ALTER TABLE \"test\" ADD COLUMN \"g\" GENERATED ALWAYS AS (\"a\" * 2) VIRTUAL")
795806
assertEqualSQL(latestQueries[4], "ALTER TABLE \"test\" ADD COLUMN \"h\" GENERATED ALWAYS AS ('O''Brien') VIRTUAL")
796807
}
797-
#endif
798808
}
799809

800810
func testAlterTableDropColumn() throws {

0 commit comments

Comments
 (0)