TableRecord
Types that adopt TableRecord
declare a particular relationship with
a database table.
Types that adopt both TableRecord
and FetchableRecord
are granted with
built-in methods that allow to fetch instances identified by key:
try Player.fetchOne(db, key: 123) // Player?
try Citizenship.fetchOne(db, key: ["citizenId": 12, "countryId": 45]) // Citizenship?
-
databaseTableName
Default implementationThe name of the database table used to build requests.
struct Player : TableRecord { static var databaseTableName = "player" } // SELECT * FROM player try Player.fetchAll(db)
Default Implementation
The default name of the database table used to build requests.
- Player -> “player”
- Place -> “place”
- PostalAddress -> “postalAddress”
- HTTPRequest -> “httpRequest”
- TOEFL -> “toefl”
-
databaseSelection
Default implementationThe default request selection.
Unless said otherwise, requests select all columns:
// SELECT * FROM player try Player.fetchAll(db)
You can provide a custom implementation and provide an explicit list of columns:
struct RestrictedPlayer : TableRecord { static var databaseTableName = "player" static var databaseSelection = [Column("id"), Column("name")] } // SELECT id, name FROM player try RestrictedPlayer.fetchAll(db)
You can also add extra columns such as the
rowid
column:struct ExtendedPlayer : TableRecord { static var databaseTableName = "player" static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID] } // SELECT *, rowid FROM player try ExtendedPlayer.fetchAll(db)
Default Implementation
Default value:
[AllColumns()]
.
-
matching(_:
Extension method) Returns a QueryInterfaceRequest with a matching predicate.
// SELECT * FROM book WHERE book MATCH '...' var request = Book.matching(pattern)
If the search pattern is nil, the request does not match any database row.
The selection defaults to all columns. This default can be changed for all requests by the
TableRecord.databaseSelection
property, or for individual requests with theTableRecord.select
method.
-
belongsTo(_:
Extension methodkey: using: ) Creates a “Belongs To” association between Self and the destination type, based on a database foreign key.
struct Author: TableRecord { ... } struct Book: TableRecord { static let author = belongsTo(Author.self) }
The association will let you define requests that load both the source and the destination type:
// A request for all books with their authors: let request = Book.including(optional: Book.author)
To consume those requests, define a type that adopts both the FetchableRecord and Decodable protocols:
struct BookInfo: FetchableRecord, Decodable { var book: Book var author: Author? } let bookInfos = try dbQueue.read { db in return try BookInfo.fetchAll(db, request) } for bookInfo in bookInfos { print("\(bookInfo.book.title) by \(bookInfo.author.name)") }
It is recommended that you define, alongside the static association, a property with the same name:
struct Book: TableRecord, EncodableRecord { static let author = belongsTo(Author.self) var author: QueryInterfaceRequest<Author> { return request(for: Book.author) } }
This property will let you navigate from the source type to the destination type:
try dbQueue.read { db in let book: Book = ... let author = try book.author.fetchOne(db) // Author? }
-
hasMany(_:
Extension methodkey: using: ) Creates a “Has many” association between Self and the destination type, based on a database foreign key.
struct Book: TableRecord { ... } struct Author: TableRecord { static let books = hasMany(Book.self) }
The association will let you define requests that load both the source and the destination type:
// A request for all (author, book) pairs: let request = Author.including(required: Author.books)
To consume those requests, define a type that adopts both the FetchableRecord and Decodable protocols:
struct Authorship: FetchableRecord, Decodable { var author: Author var book: Book } let authorships = try dbQueue.read { db in return try Authorship.fetchAll(db, request) } for authorship in authorships { print("\(authorship.author.name) wrote \(authorship.book.title)") }
It is recommended that you define, alongside the static association, a property with the same name:
struct Author: TableRecord, EncodableRecord { static let books = hasMany(Book.self) var books: QueryInterfaceRequest<Book> { return request(for: Author.books) } }
This property will let you navigate from the source type to the destination type:
try dbQueue.read { db in let author: Author = ... let books = try author.books.fetchAll(db) // [Book] }
-
hasOne(_:
Extension methodkey: using: ) Creates a “Has one” association between Self and the destination type, based on a database foreign key.
struct Demographics: TableRecord { ... } struct Country: TableRecord { static let demographics = hasOne(Demographics.self) }
The association will let you define requests that load both the source and the destination type:
// A request for all countries with their demographic profile: let request = Country.including(optional: Country.demographics)
To consume those requests, define a type that adopts both the FetchableRecord and Decodable protocols:
struct CountryInfo: FetchableRecord, Decodable { var country: Country var demographics: Demographics? } let countryInfos = try dbQueue.read { db in return try CountryInfo.fetchAll(db, request) } for countryInfo in countryInfos { print("\(countryInfo.country.name) has \(countryInfo.demographics.population) citizens") }
It is recommended that you define, alongside the static association, a property with the same name:
struct Country: TableRecord, EncodableRecord { static let demographics = hasOne(Demographics.self) var demographics: QueryInterfaceRequest<Demographics> { return request(for: Country.demographics) } }
This property will let you navigate from the source type to the destination type:
try dbQueue.read { db in let country: Country = ... let demographics = try country.demographics.fetchOne(db) // Demographics? }
-
association(to:
Extension methodon: ) Creates an association to a common table expression that you can join or include in another request.
The key of the returned association is the table name of the common table expression.
For example, you can build a request that fetches all chats with their latest message:
let latestMessageRequest = Message .annotated(with: max(Column("date"))) .group(Column("chatID")) let latestMessageCTE = CommonTableExpression( named: "latestMessage", request: latestMessageRequest) let latestMessage = Chat.association( to: latestMessageCTE, on: { chat, latestMessage in chat[Column("id")] == latestMessage[Column("chatID")] }) // WITH latestMessage AS // (SELECT *, MAX(date) FROM message GROUP BY chatID) // SELECT chat.*, latestMessage.* // FROM chat // LEFT JOIN latestMessage ON chat.id = latestMessage.chatID let request = Chat .with(latestMessageCTE) .including(optional: latestMessage)
-
association(to:
Extension method) Creates an association to a common table expression that you can join or include in another request.
The key of the returned association is the table name of the common table expression.
-
hasMany(_:
Extension methodthrough: using: key: ) Creates a “Has Many Through” association between Self and the destination type.
struct Country: TableRecord { static let passports = hasMany(Passport.self) static let citizens = hasMany(Citizen.self, through: passports, using: Passport.citizen) } struct Passport: TableRecord { static let citizen = belongsTo(Citizen.self) } struct Citizen: TableRecord { }
The association will let you define requests that load both the source and the destination type:
// A request for all (country, citizen) pairs: let request = Country.including(required: Coutry.citizens)
To consume those requests, define a type that adopts both the FetchableRecord and Decodable protocols:
struct Citizenship: FetchableRecord, Decodable { var country: Country var citizen: Citizen } let citizenships = try dbQueue.read { db in return try Citizenship.fetchAll(db, request) } for citizenship in citizenships { print("\(citizenship.citizen.name) is a citizen of \(citizenship.country.name)") }
It is recommended that you define, alongside the static association, a property with the same name:
struct Country: TableRecord, EncodableRecord { static let passports = hasMany(Passport.self) static let citizens = hasMany(Citizen.self, through: passports, using: Passport.citizen) var citizens: QueryInterfaceRequest<Citizen> { return request(for: Country.citizens) } }
This property will let you navigate from the source type to the destination type:
try dbQueue.read { db in let country: Country = ... let citizens = try country.citizens.fetchAll(db) // [Country] }
-
hasOne(_:
Extension methodthrough: using: key: ) Creates a “Has One Through” association between Self and the destination type.
struct Book: TableRecord { static let library = belongsTo(Library.self) static let returnAddress = hasOne(Address.self, through: library, using: Library.address) } struct Library: TableRecord { static let address = hasOne(Address.self) } struct Address: TableRecord { ... }
The association will let you define requests that load both the source and the destination type:
// A request for all (book, returnAddress) pairs: let request = Book.including(required: Book.returnAddress)
To consume those requests, define a type that adopts both the FetchableRecord and Decodable protocols:
struct Todo: FetchableRecord, Decodable { var book: Book var address: Address } let todos = try dbQueue.read { db in return try Todo.fetchAll(db, request) } for todo in todos { print("Please return \(todo.book) to \(todo.address)") }
It is recommended that you define, alongside the static association, a property with the same name:
struct Book: TableRecord, EncodableRecord { static let library = belongsTo(Library.self) static let returnAddress = hasOne(Address.self, through: library, using: library.address) var returnAddress: QueryInterfaceRequest<Address> { return request(for: Book.returnAddress) } }
This property will let you navigate from the source type to the destination type:
try dbQueue.read { db in let book: Book = ... let address = try book.returnAddress.fetchOne(db) // Address? }
-
request(for:
Extension method) Creates a request that fetches the associated record(s).
For example:
struct Team: TableRecord, EncodableRecord { static let players = hasMany(Player.self) var players: QueryInterfaceRequest<Player> { return request(for: Team.players) } } let team: Team = ... let players = try team.players.fetchAll(db) // [Player]
-
including(all:
Extension method) Creates a request that prefetches an association.
-
including(optional:
Extension method) Creates a request that includes an association. The columns of the associated record are selected. The returned association does not require that the associated database table contains a matching row.
-
including(required:
Extension method) Creates a request that includes an association. The columns of the associated record are selected. The returned association requires that the associated database table contains a matching row.
-
joining(optional:
Extension method) Creates a request that includes an association. The columns of the associated record are not selected. The returned association does not require that the associated database table contains a matching row.
-
joining(required:
Extension method) Creates a request that includes an association. The columns of the associated record are not selected. The returned association requires that the associated database table contains a matching row.
-
annotated(withOptional:
Extension method) Creates a request which appends columns of an associated record to the selection.
// SELECT player.*, team.color // FROM player LEFT JOIN team ... let teamColor = Player.team.select(Column("color")) let request = Player.annotated(withOptional: teamColor)
This method performs the same SQL request as
including(optional:)
. The difference is in the shape of Decodable records that decode such a request: the associated columns can be decoded at the same level as the main record:struct PlayerWithTeamColor: FetchableRecord, Decodable { var player: Player var color: String? } let players = try dbQueue.read { db in try request .asRequest(of: PlayerWithTeamColor.self) .fetchAll(db) }
Note: this is a convenience method. You can build the same request with
TableAlias
,annotated(with:)
, andjoining(optional:)
:let teamAlias = TableAlias() let request = Player .annotated(with: teamAlias[Column("color")]) .joining(optional: Player.team.aliased(teamAlias))
-
annotated(withRequired:
Extension method) Creates a request which appends columns of an associated record to the selection.
// SELECT player.*, team.color // FROM player JOIN team ... let teamColor = Player.team.select(Column("color")) let request = Player.annotated(withRequired: teamColor)
This method performs the same SQL request as
including(required:)
. The difference is in the shape of Decodable records that decode such a request: the associated columns can be decoded at the same level as the main record:struct PlayerWithTeamColor: FetchableRecord, Decodable { var player: Player var color: String } let players = try dbQueue.read { db in try request .asRequest(of: PlayerWithTeamColor.self) .fetchAll(db) }
Note: this is a convenience method. You can build the same request with
TableAlias
,annotated(with:)
, andjoining(required:)
:let teamAlias = TableAlias() let request = Player .annotated(with: teamAlias[Column("color")]) .joining(required: Player.team.aliased(teamAlias))
-
annotated(with:
Extension method) Creates a request with aggregates appended to the selection.
// SELECT player.*, COUNT(DISTINCT book.id) AS bookCount // FROM player LEFT JOIN book ... let request = Player.annotated(with: Player.books.count)
-
having(_:
Extension method) Creates a request with the provided aggregate predicate.
// SELECT player.* // FROM player LEFT JOIN book ... // HAVING COUNT(DISTINCT book.id) = 0 var request = Player.all() request = request.having(Player.books.isEmpty)
The selection defaults to all columns. This default can be changed for all requests by the
TableRecord.databaseSelection
property, or for individual requests with theTableRecord.select
method.
-
all()
Extension methodCreates a request which fetches all records.
// SELECT * FROM player let request = Player.all()
-
none()
Extension methodCreates a request which fetches no record.
-
select(_:
Extension method) Creates a request which selects selection.
// SELECT id, email FROM player let request = Player.select(Column("id"), Column("email"))
-
select(sql:
Extension methodarguments: ) Creates a request which selects sql.
// SELECT id, email FROM player let request = Player.select(sql: "id, email")
-
select(literal:
Extension method) Creates a request which selects an SQL literal.
Literals allow you to safely embed raw values in your SQL, without any risk of syntax errors or SQL injection:
// SELECT id, email, score + 1000 FROM player let bonus = 1000 let request = Player.select(literal: """ id, email, score + \(bonus) """)
-
select(_:
Extension methodas: ) Creates a request which selects selection, and fetches values of type type.
try dbQueue.read { db in // SELECT max(score) FROM player let request = Player.select([max(Column("score"))], as: Int.self) let maxScore: Int? = try request.fetchOne(db) }
-
select(sql:
Extension methodarguments: as: ) Creates a request which selects sql, and fetches values of type type.
try dbQueue.read { db in // SELECT max(score) FROM player let request = Player.select(sql: "max(score)", as: Int.self) let maxScore: Int? = try request.fetchOne(db) }
-
select(literal:
Extension methodas: ) Creates a request which selects an SQL literal, and fetches values of type type.
Literals allow you to safely embed raw values in your SQL, without any risk of syntax errors or SQL injection:
// SELECT IFNULL(name, 'Anonymous') FROM player let defaultName = "Anonymous" let request = Player.select( literal: "IFNULL(name, \(defaultName))", as: String.self) let name: String? = try request.fetchOne(db)
-
selectPrimaryKey(as:
Extension method) Creates a request which selects the primary key.
All primary keys are supported:
// SELECT id FROM player let request = try Player.selectPrimaryKey(as: Int64.self) // SELECT code FROM country let request = try Country.selectPrimaryKey(as: String.self) // SELECT citizenId, countryCode FROM citizenship let request = try Citizenship.selectPrimaryKey(as: Row.self)
-
filter(_:
Extension method) Creates a request with the provided predicate.
// SELECT * FROM player WHERE email = 'arthur@example.com' let request = Player.filter(Column("email") == "arthur@example.com")
-
filter(key:
Extension method) Creates a request with the provided primary key predicate.
// SELECT * FROM player WHERE id = 1 let request = Player.filter(key: 1)
-
filter(keys:
Extension method) Creates a request with the provided primary key predicate.
// SELECT * FROM player WHERE id IN (1, 2, 3) let request = Player.filter(keys: [1, 2, 3])
-
filter(sql:
Extension methodarguments: ) Creates a request with the provided predicate.
// SELECT * FROM player WHERE email = 'arthur@example.com' let request = Player.filter(sql: "email = ?", arguments: ["arthur@example.com"])
-
filter(literal:
Extension method) Creates a request with the provided predicate added to the eventual set of already applied predicates.
Literals allow you to safely embed raw values in your SQL, without any risk of syntax errors or SQL injection:
// SELECT * FROM player WHERE name = 'O''Brien' let name = "O'Brien" let request = Player.filter(literal: "email = \(email)")
-
order(_:
Extension method) Creates a request sorted according to the provided orderings.
// SELECT * FROM player ORDER BY name let request = Player.order(Column("name"))
-
orderByPrimaryKey()
Extension methodCreates a request sorted by primary key.
// SELECT * FROM player ORDER BY id let request = Player.orderByPrimaryKey() // SELECT * FROM country ORDER BY code let request = Country.orderByPrimaryKey()
-
order(sql:
Extension methodarguments: ) Creates a request sorted according to sql.
// SELECT * FROM player ORDER BY name let request = Player.order(sql: "name")
-
order(literal:
Extension method) Creates a request sorted according to an SQL literal.
// SELECT * FROM player ORDER BY name let request = Player.order(literal: "name")
-
limit(_:
Extension methodoffset: ) Creates a request which fetches limit rows, starting at offset.
// SELECT * FROM player LIMIT 1 let request = Player.limit(1)
-
aliased(_:
Extension method) Creates a request that allows you to define expressions that target a specific database table.
In the example below, the “team.avgScore < player.score” condition in the ON clause could be not achieved without table aliases.
struct Player: TableRecord { static let team = belongsTo(Team.self) } // SELECT player.*, team.* // JOIN team ON ... AND team.avgScore < player.score let playerAlias = TableAlias() let request = Player .aliased(playerAlias) .including(required: Player.team.filter(Column("avgScore") < playerAlias[Column("score")])
-
with(_:
Extension method) Returns a request which embeds the common table expression.
If a common table expression with the same table name had already been embedded, it is replaced by the new one.
For example, you can build a request that fetches all chats with their latest post:
let latestMessageRequest = Message .annotated(with: max(Column("date"))) .group(Column("chatID")) let latestMessageCTE = CommonTableExpression( named: "latestMessage", request: latestMessageRequest) let latestMessage = Chat.association( to: latestMessageCTE, on: { chat, latestMessage in chat[Column("id")] == latestMessage[Column("chatID")] }) // WITH latestMessage AS // (SELECT *, MAX(date) FROM message GROUP BY chatID) // SELECT chat.*, latestMessage.* // FROM chat // LEFT JOIN latestMessage ON chat.id = latestMessage.chatID let request = Chat .with(latestMessageCTE) .including(optional: latestMessage)
-
fetchCount(_:
Extension method) The number of records.
-
numberOfSelectedColumns(_:
Extension method) Returns the number of selected columns.
For example:
struct Player: TableRecord { static let databaseTableName = "player" } try dbQueue.write { db in try db.create(table: "player") { t in t.autoIncrementedPrimaryKey("id") t.column("name", .text) t.column("score", .integer) } // 3 try Player.numberOfSelectedColumns(db) }
-
deleteAll(_:
Extension method) Deletes all records, and returns the number of deleted records.
Throws
A DatabaseError is thrown whenever an SQLite error occurs.
-
exists(_:
Extension methodkey: ) Returns whether a record exists for this primary key.
try Player.exists(db, key: 123) try Country.exists(db, key: "FR")
When the table has no explicit primary key, GRDB uses the hidden “rowid” column:
try Document.exists(db, key: 1)
-
deleteAll(_:
Extension methodkeys: ) Delete records identified by their primary keys, and returns the number of deleted records.
// DELETE FROM player WHERE id IN (1, 2, 3) try Player.deleteAll(db, keys: [1, 2, 3]) // DELETE FROM country WHERE code IN ('FR', 'US', 'DE') try Country.deleteAll(db, keys: ["FR", "US", "DE"])
When the table has no explicit primary key, GRDB uses the hidden “rowid” column:
// DELETE FROM document WHERE rowid IN (1, 2, 3) try Document.deleteAll(db, keys: [1, 2, 3])
-
deleteOne(_:
Extension methodkey: ) Delete a record, identified by its primary key, and returns whether a record was deleted.
// DELETE FROM player WHERE id = 123 try Player.deleteOne(db, key: 123) // DELETE FROM country WHERE code = 'FR' try Country.deleteOne(db, key: "FR")
When the table has no explicit primary key, GRDB uses the hidden “rowid” column:
// DELETE FROM document WHERE rowid = 1 try Document.deleteOne(db, key: 1)
-
updateAll(_:
Extension methodonConflict: _: ) Updates all records, and returns the number of updated records.
For example:
try dbQueue.write { db in // UPDATE player SET score = 0 try Player.updateAll(db, [Column("score").set(to: 0)]) }
Throws
A DatabaseError is thrown whenever an SQLite error occurs.