Skip to main content
PersistableRecord is a protocol for types that can insert, update, and save themselves to the database.

Overview

Conform to PersistableRecord to persist your custom types:
struct Player: PersistableRecord, Encodable {
    var id: Int64?
    var name: String
    var score: Int
}

try dbQueue.write { db in
    var player = Player(id: nil, name: "Arthur", score: 100)
    try player.insert(db)
    print(player.id) // Auto-incremented ID
}
Encodable types get automatic PersistableRecord conformance with minimal setup.

Conforming to PersistableRecord

Encodable Conformance

Encodable types automatically conform:
struct Player: PersistableRecord, Encodable {
    var id: Int64?
    var name: String
    var score: Int
}

Manual Conformance

Implement the encode(to:) method:
struct Player: PersistableRecord {
    var id: Int64?
    var name: String
    var score: Int
    
    func encode(to container: inout PersistenceContainer) {
        container["id"] = id
        container["name"] = name
        container["score"] = score
    }
}

Configuring the Table

Combine with TableRecord to specify the table name:
struct Player: PersistableRecord, TableRecord, Encodable {
    static let databaseTableName = "player"
    
    var id: Int64?
    var name: String
    var score: Int
}

Inserting Records

insert(_:onConflict:)

Inserts a record into the database.
db
Database
required
A database connection
onConflict
Database.ConflictResolution?
default:"nil"
Conflict resolution strategy
var player = Player(id: nil, name: "Arthur", score: 100)
try player.insert(db)

// Insert with conflict resolution
try player.insert(db, onConflict: .ignore)
For structs, insert is mutating and updates the id property with the auto-incremented value.

insertAndFetch(_:onConflict:as:)

Inserts a record and fetches the inserted row.
var player = Player(id: nil, name: "Arthur", score: 100)
let inserted = try player.insertAndFetch(db)
print(inserted.id) // Auto-incremented ID

Updating Records

update(_:onConflict:)

Updates a record in the database.
var player = Player(id: 1, name: "Arthur", score: 100)
player.score = 150
try player.update(db)
Throws a RecordError.recordNotFound error if the record doesn’t exist in the database.

update(_:onConflict:columns:)

Updates specific columns only.
var player = Player(id: 1, name: "Arthur", score: 150)
try player.update(db, columns: ["score"])
// Only score is updated, name is unchanged in database

Saving Records

save(_:onConflict:)

Inserts or updates a record depending on whether it exists.
var player = Player(id: nil, name: "Arthur", score: 100)
try player.save(db) // Inserts

player.score = 150
try player.save(db) // Updates

saveAndFetch(_:onConflict:as:)

Saves a record and fetches the saved row.
var player = Player(id: nil, name: "Arthur", score: 100)
let saved = try player.saveAndFetch(db)
print(saved.id)

Upserting Records

upsert(_:)

Inserts or updates using SQLite’s UPSERT (INSERT … ON CONFLICT DO UPDATE).
let player = Player(id: 1, name: "Arthur", score: 100)
try player.upsert(db)
Requires SQLite 3.24.0+ and a conflict target (usually a primary key or unique constraint).

upsertAndFetch(_:onConflict:updating:doUpdate:)

Upserts and fetches the result.
let player = Player(id: 1, name: "Arthur", score: 100)
let upserted = try player.upsertAndFetch(db)

Deleting Records

delete(_:)

Deletes a record from the database.
let player = Player(id: 1, name: "Arthur", score: 100)
let deleted = try player.delete(db)
print(deleted) // true if deleted, false if not found

Persistence Callbacks

willInsert(_:)

Called before the record is inserted.
struct Player: PersistableRecord {
    var id: Int64?
    var name: String
    var createdAt: Date
    
    func willInsert(_ db: Database) throws {
        // Set createdAt before insertion
        createdAt = Date()
    }
}

didInsert(_:)

Called after successful insertion.
inserted
InsertionSuccess
required
Contains information about the inserted row
struct Player: PersistableRecord {
    var id: Int64?
    var name: String
    
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

willUpdate(_:columns:)

Called before the record is updated.
struct Player: PersistableRecord {
    var id: Int64
    var name: String
    var updatedAt: Date
    
    func willUpdate(_ db: Database, columns: Set<String>) throws {
        updatedAt = Date()
    }
}

didUpdate(_:)

Called after successful update.
func didUpdate(_ updated: UpdateSuccess) {
    print("Updated \(updated.changesCount) columns")
}

willSave(_:)

Called before save (insert or update).
func willSave(_ db: Database) throws {
    // Validation logic
    guard !name.isEmpty else {
        throw ValidationError.emptyName
    }
}

didSave(_:)

Called after successful save.
func didSave(_ saved: SaveSuccess) {
    print("Saved with \(saved.changesCount) changes")
}

willDelete(_:)

Called before deletion.
func willDelete(_ db: Database) throws {
    // Clean up related data
}

didDelete(_:)

Called after successful deletion.
func didDelete(_ deleted: DeletionSuccess) {
    print("Deleted: \(deleted.deleted)")
}

Conflict Resolution

try player.insert(db, onConflict: .ignore)    // INSERT OR IGNORE
try player.insert(db, onConflict: .replace)   // INSERT OR REPLACE
try player.update(db, onConflict: .rollback)  // UPDATE OR ROLLBACK
try player.update(db, onConflict: .abort)     // UPDATE OR ABORT
try player.update(db, onConflict: .fail)      // UPDATE OR FAIL

Customizing Encodable Encoding

Column Name Encoding

struct Player: PersistableRecord, Encodable {
    static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase
    
    var playerID: Int64  // Encoded to player_id
    var fullName: String // Encoded to full_name
}

Date Encoding

struct Player: PersistableRecord, Encodable {
    static func databaseDateEncodingStrategy(for column: String) -> DatabaseDateEncodingStrategy {
        .timeIntervalSince1970
    }
    
    var createdAt: Date // Stored as epoch timestamp
}

Data Encoding

struct Player: PersistableRecord, Encodable {
    static func databaseDataEncodingStrategy(for column: String) -> DatabaseDataEncodingStrategy {
        .deferredToData
    }
    
    var avatarData: Data
}

JSON Encoding

struct Player: PersistableRecord, Encodable {
    static func databaseJSONEncoder(for column: String) -> JSONEncoder {
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        return encoder
    }
    
    var metadata: Metadata // Encoded as JSON
}

Batch Operations

Combine with TableRecord for batch operations:
// Delete all players with score < 100
try Player.filter(Column("score") < 100).deleteAll(db)

// Update all players
try Player.updateAll(db) { player in
    [player.score.set(to: 0)]
}

See Also

Build docs developers (and LLMs) love