[Resolved] Model with property `title` requires a db table column named `_title` unexpectedly

I have to say in advance that I am fairly new to GRDB.

I created a little sample app with the following model:

import Foundation
import GRDB

@Observable
final class Shortcut: Identifiable {
  let id: UUID
  var title: String

  init(
    id: UUID = UUID(),
    title: String
  ) {
    self.id = id
    self.title = title
  }
}

extension Shortcut: Codable, FetchableRecord, PersistableRecord {}

In my GRDBClient, I introduced a func to create the table:

private func createShortcutTable(in db: Database) throws {
    let tableName = "shortcut"
    if try db.tableExists(tableName) == false {
      try db.create(table: tableName) { table in
        table.column("id", .text).primaryKey().notNull()
        table.column("title", .text).notNull()
      }
    }
  }

I can run the application, but as soon as I try an insert into above table, I am met with the following error message: table shortcut has no column named _title in "INSERT INTO "shortcut" ("id", "_title") VALUES (?,?)"

I now wonder as to where the underscore is coming from here–I searched in the documentation but failed to find anything …

If I change above line table.column("title", .text).notNull() to table.column("_title", .text).notNull()–just adding the underscore–the app works as expected.

What am I missing? Is there a spot where I could be handling/declaring anything wrong?
I could live with the above correction, but I'd like to understand why it is neccessary…

Hello @appfrosch,

Another GRDB user had the same issue, and the root problem is that @Observable does not play well with Codable. The macro creates underscored properties, and those are the coded ones.

Some possible workarounds include:

  • Provide a custom CodingKeys enum that fixes the default one, synthesized by the compiler. I guess this should work out of the box, but I did not test:

    extension Shortcut: Codable, FetchableRecord, PersistableRecord {
        enum CodingKeys: String, CodingKey {
            case _id = "id"
            case _title = "title"
        }
    }
    
  • Remove the Codable conformance, and provide an explicit implementation of encode(to:) and init(row:):

    extension Shortcut: FetchableRecord, PersistableRecord {
        init(row: Row) {
            // Use strings instead of CodingKeys
            // because those are "corrupted" :-/
            id = row["id"]
            title = row["title"]
        }
    
        func encode(to container: inout PersistenceContainer) {
            container["id"] = id
            container["title"] = title
        }
    }
    

    Depending on the number of properties in your model, that may be a lot of boilerplate :grimacing: That's why we love so much the Codable convenience (and it would have been so cool if @Observable would not mess with it).

  • Keep the Shortcut conformance to Codable, and have it provide a custom databaseColumnDecodingStrategy and databaseColumnEncodingStrategy that deal with the underscore added by @Observable.

  • Remove the Codable conformance, and have your Shortcut observable object delegate the implementation of database protocols to a plain Codable struct:

    extension Shortcut: FetchableRecord, PersistableRecord {
        private struct Record: Codable, FetchableRecord, PersistableRecord {
            static let databaseTableName = Shortcut.databaseTableName
            let id: UUID
            var title: String
        }
    
        convenience init(row: Row) throws {
            let record = try Record(row: row)
            self.init(id: record.id, title: record.title)
        }
    
        func encode(to container: inout PersistenceContainer) throws {
            let record = Record(id: id, title: title)
            try record.encode(to: &container)
        }
    }
    
  • Strictly separate your data layer from your view layer by removing database protocols from Shortcut. This will make it easier to make the database evolve independently from your views:

    • The record type will be responsible for the database table, and its eventual future evolutions, as recommended.
    • The observable object will be responsible for its view, and freely ignore table contents it does not care about.
    struct ShortcutRecord: Codable, FetchableRecord, PersistableRecord, Identifiable {
        static let databaseTableName = "shortcut"
        let id: UUID
        var title: String
    }
    
    extension Shortcut { // no database protocol
        convenience init(_ record: ShortcutRecord) {
            self.init(id: record.id, title: record.title)
        }
    }
    
    let shortcut = try dbQueue.read { db in
        let record = try ShortcutRecord.find(db, id: UUID())
        return Shortcut(record)
    }
    

That's a lot of options, just because @Observable does not play well with Codable :sweat_smile: You can pick any one of them: your app will work, and maybe plan for another solution in the future.

I'm not sure anything can be done at the GRDB level, unfortunately: @Observable "corrupts" the Codable facet of a model in an undetectable way. That's why I would also recommend to file a change request about this unexpected mismatch between two standard components. I don't expect the required changes to be tiny, so don't hope for a quick resolution.

2 Likes

Edit:
Welp, @gwendal.roue has it, my hunch is obsolete... :laughing:


This is simply guessing on my part, as I have not used GRDB myself, but could it be that the @Observable macro bites you in the butt here?
Technically that morphs your properties into hidden (i.e. underscored) storage properties and getters and setters (to add the necessary notification code). So this:

var title: String

becomes this:

private var _title: String
var title: String {
    get { _title }
    set { 
        objectWillChange.send()
        _title = newValue
    }
}

If GRDB tries to work with keyPaths or something to help selecting a table batching to properties or the like, this might lead to problems?

As said, I am just guessing here, it just looked so suspiciously "unexpectedly underscored by macro" to me...

2 Likes

Your hunch is correct indeed and I appreciate it :grinning:

1 Like

Wow, thanks a lot for this highly informative answer! The reason for this to happen is so obvious once you're aware, so thanks for a. pointing it out and b. delivering so many good approaches to choose from.

I'll pick one and just run with it.

1 Like

I filed one: FB13756604 (The Observable macro does not play well with the standard Codable protocol).


EDIT: After all, both Codable and Observable are part of Swift. Github issue: The Observable macro does not play well with the standard Codable protocol · Issue #73280 · apple/swift · GitHub

I went for the last option, which I clearly see why it would be recommended and introduced a ShortcutRecord type to interface with.

Adds a litte bit of boilerplate, but this decoupling might be useful anyway at some point. Thanks for the good input again!

1 Like

I took the opportunity to learn on how to create a macro creating an embedded struct that interfaces with the @Observable class for persistence. I think that could be a solution.

In case anyone is interested: you can find the macro here on my GitHub.

1 Like