Skip to content
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 Documentation/CustomSQLiteBuilds.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Custom SQLite Builds

By default, GRDB uses the version of SQLite that ships with the target operating system.

**You can build GRDB with a custom build of [SQLite 3.28.0](https://www.sqlite.org/changes.html).**
**You can build GRDB with a custom build of [SQLite 3.33.0](https://www.sqlite.org/changes.html).**

A custom SQLite build can activate extra SQLite features, and extra GRDB features as well, such as support for the [FTS5 full-text search engine](../../../#full-text-search), and [SQLite Pre-Update Hooks](../../../#support-for-sqlite-pre-update-hooks).

Expand Down
2 changes: 1 addition & 1 deletion Documentation/ReleaseProcess.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ To release a new GRDB version:
- Archive GRDBDemoiOS
- Check for performance regression with GRDBOSXPerformanceTests
- On https://github.com/groue/sqlcipher.git upgrade, update SQLCipher version in README.md
- On https://github.com/swiftlyfalling/SQLiteLib upgrade, update SQLite version in README.md and Documentation/CustomSQLiteBuilds.md
- On https://github.com/swiftlyfalling/SQLiteLib upgrade, update SQLite version in Documentation/CustomSQLiteBuilds.md
- Update GRDB version number and release date in:
- Makefile
- CHANGELOG.md
Expand Down
49 changes: 38 additions & 11 deletions GRDB/Core/Database+Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -346,27 +346,54 @@ extension Database {
// > column in the primary key for columns that are part of the primary
// > key.
//
// CREATE TABLE players (
// sqlite> CREATE TABLE players (
// id INTEGER PRIMARY KEY,
// firstName TEXT,
// lastName TEXT)
//
// PRAGMA table_info("players")
// lastName TEXT);
//
// sqlite> PRAGMA table_info("players");
// cid | name | type | notnull | dflt_value | pk |
// 0 | id | INTEGER | 0 | NULL | 1 |
// 1 | name | TEXT | 0 | NULL | 0 |
// 2 | score | INTEGER | 0 | NULL | 0 |

if sqlite3_libversion_number() < 3008005 {
// Work around a bug in SQLite where PRAGMA table_info would
// return a result even after the table was deleted.
if try !tableExists(tableName) {
throw DatabaseError(message: "no such table: \(tableName)")
//
//
// PRAGMA table_info does not expose hidden and generated columns. For
// that, we need PRAGMA table_xinfo, introduced in SQLite 3.26.0:
// https://sqlite.org/releaselog/3_26_0.html
//
// > PRAGMA schema.table_xinfo(table-name);
//
// > This pragma returns one row for each column in the named table,
// > including hidden columns in virtual tables. The output is the same
// > as for PRAGMA table_info except that hidden columns are shown
// > rather than being omitted.
//
// sqlite> PRAGMA table_xinfo("players");
// cid | name | type | notnull | dflt_value | pk | hidden
// 0 | id | INTEGER | 0 | NULL | 1 | 0
// 1 | firstName | TEXT | 0 | NULL | 0 | 0
// 2 | lastName | TEXT | 0 | NULL | 0 | 0
let columnInfoQuery: String
if sqlite3_libversion_number() < 3026000 {
if sqlite3_libversion_number() < 3008005 {
// Work around a bug in SQLite where PRAGMA table_info would
// return a result even after the table was deleted.
if try !tableExists(tableName) {
throw DatabaseError(message: "no such table: \(tableName)")
}
}
columnInfoQuery = "PRAGMA table_info(\(tableName.quotedDatabaseIdentifier))"
} else {
// For our purposes, we look for generated columns, not hidden
// columns. The "hidden" column magic numbers come from the SQLite
// source code. The values 2 and 3 refer to virtual and stored
// generated columns, respectively. Search for COLFLAG_VIRTUAL in
// https://www.sqlite.org/cgi/src/file?name=src/pragma.c&ci=fca8dc8b578f215a
columnInfoQuery = "SELECT * FROM pragma_table_xinfo('\(tableName)') WHERE hidden IN (0,2,3)"
}
let columns = try ColumnInfo
.fetchAll(self, sql: "PRAGMA table_info(\(tableName.quotedDatabaseIdentifier))")
.fetchAll(self, sql: columnInfoQuery)
.sorted(by: { $0.cid < $1.cid })
if columns.isEmpty {
throw DatabaseError(message: "no such table: \(tableName)")
Expand Down
88 changes: 88 additions & 0 deletions GRDB/QueryInterface/Schema/TableDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,20 @@ public final class ColumnDefinition {
var deferred: Bool
}

/// The `GeneratedColumnQualification` enum defines whether a generated
/// column sis virtual or stored.
///
/// See https://sqlite.org/gencol.html#virtual_versus_stored_columns
public enum GeneratedColumnQualification {
case virtual
case stored
}

private struct GeneratedColumnConstraint {
var expression: SQLExpression
var qualification: GeneratedColumnQualification
}

fileprivate let name: String
private let type: Database.ColumnType?
fileprivate var primaryKey: (conflictResolution: Database.ConflictResolution?, autoincrement: Bool)?
Expand All @@ -628,6 +642,7 @@ public final class ColumnDefinition {
private var foreignKeyConstraints: [ForeignKeyConstraint] = []
private var defaultExpression: SQLExpression?
private var collationName: String?
private var generatedColumnConstraint: GeneratedColumnConstraint?

init(name: String, type: Database.ColumnType?) {
self.name = name
Expand Down Expand Up @@ -806,6 +821,67 @@ public final class ColumnDefinition {
return self
}

#if GRDBCUSTOMSQLITE
/// Defines the column as a generated column.
///
/// try db.create(table: "player") { t in
/// t.column("id", .integer).primaryKey()
/// t.column("score", .integer).notNull()
/// t.column("bonus", .integer).notNull()
/// t.column("totalScore", .integer).generatedAs(sql: "score + bonus", .stored)
/// }
///
/// See https://sqlite.org/gencol.html. Note particularly the limitations of
/// generated columns, e.g. they may not have a default value.
///
/// - parameters:
/// - sql: An SQL expression.
/// - qualification: The generated column's qualification, which
/// defaults to `.virtual`.
/// - returns: Self so that you can further refine the column definition.
@discardableResult
public func generatedAs(
sql: String,
_ qualification: GeneratedColumnQualification = .virtual)
-> Self
{
let expression = SQLLiteral(sql: sql).sqlExpression
generatedColumnConstraint = GeneratedColumnConstraint(
expression: expression,
qualification: qualification)
return self
}

/// Defines the column as a generated column.
///
/// try db.create(table: "player") { t in
/// t.column("id", .integer).primaryKey()
/// t.column("score", .integer).notNull()
/// t.column("bonus", .integer).notNull()
/// t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"), .stored)
/// }
///
/// See https://sqlite.org/gencol.html. Note particularly the limitations of
/// generated columns, e.g. they may not have a default value.
///
/// - parameters:
/// - expression: The generated expression.
/// - qualification: The generated column's qualification, which
/// defaults to `.virtual`.
/// - returns: Self so that you can further refine the column definition.
@discardableResult
public func generatedAs(
_ expression: SQLExpressible,
_ qualification: GeneratedColumnQualification = .virtual)
-> Self
{
generatedColumnConstraint = GeneratedColumnConstraint(
expression: expression.sqlExpression,
qualification: qualification)
return self
}
#endif

/// Defines a foreign key.
///
/// try db.create(table: "book") { t in
Expand Down Expand Up @@ -931,6 +1007,18 @@ public final class ColumnDefinition {
}
}

if let constraint = generatedColumnConstraint {
try chunks.append("GENERATED ALWAYS AS (\(constraint.expression.quotedSQL(db)))")
let qualificationLiteral: String
switch constraint.qualification {
case .stored:
qualificationLiteral = "STORED"
case .virtual:
qualificationLiteral = "VIRTUAL"
}
chunks.append(qualificationLiteral)
}

return chunks.joined(separator: " ")
}

Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ Installation

See [Encryption](#encryption) for the installation procedure of GRDB with SQLCipher.

See [Custom SQLite builds](Documentation/CustomSQLiteBuilds.md) for the installation procedure of GRDB with a customized build of SQLite 3.28.0.
See [Custom SQLite builds](Documentation/CustomSQLiteBuilds.md) for the installation procedure of GRDB with a customized build of SQLite.

See [Enabling FTS5 Support](Documentation/FullTextSearch.md#enabling-fts5-support) for the installation procedure of GRDB with support for the FTS5 full-text engine.

Expand Down Expand Up @@ -4022,6 +4022,13 @@ Other **table constraints** can involve several columns:

// CHECK (a + b < 10)
t.check(sql: "a + b < 10")
```

[Generated columns](https://sqlite.org/gencol.html) are available with a [custom SQLite build]:

```swift
t.column("totalScore", .integer).generatedAs(sql: "score + bonus")
t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"))
}
```

Expand Down
2 changes: 1 addition & 1 deletion SQLiteCustom/src
Submodule src updated 403 files
Loading