groue / GRDB.swift

A toolkit for SQLite databases, with a focus on application development

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Setting a default value for a date time column

ahartman opened this issue · comments

What did you do?

try db.create(table: "patient", ifNotExists: true) { t in
                    t.autoIncrementedPrimaryKey("id")
                    t.column("createdDate", .text)
                        .defaults(sql: "CURRENT_TIMESTAMP")
                    t.column("modifiedDate", .text)
                        .defaults(sql: "strftime('%Y-%m-%d %H:%M:%S:%s', 'now', 'localtime')")
                    t.column("patientName", .text)
                        .indexed()
                        .unique()
                        .notNull()
                    t.column("patientLatitude", .real)
                        .indexed()
                    t.column("patientLongitude", .real)
                }

What did you expect to happen?

Setting default values for createdDate and modifiedDate

What happened instead?

No values were set
I used two methods, both do not work.

Environment

**GRDB flavor(s): GRDB
**GRDB version: master
**Installation method: package
**Xcode version: 14.2
**Swift version: 5
**Platform(s) running GRDB: macOS, Catalyst
**macOS version running Xcode: 12.7.1

Demo Project

How to set a date time default value?
Regards, André Hartman

Hello @ahartman,

I suppose you wrote something as below, and ended up with nil dates (both in the database and in the inserted model):

// INSERT INTO patient (..., createdDate, modifiedDate) VALUES (..., NULL, NULL)
var patient = Patient(..., createdDate: nil, modifiedDate: nil)
try patient.insert(db)

Let's jump to the main point: GRDB record types currently do not play well with default values defined in the SQL schema. Not only insert(db) inserts explicit values for all columns, and explicit NULL dates prevent the default from being used. On top of that, the INSERT statement does not return the inserted values. Those are the two difficulties that prevent "magic" from happening.

There is a documentation article named Record Timestamps and Transaction Date that could help you making progress, though.

GRDB record types currently do not play well with default values.

There is a way to make them nicer citizens, which is to have record types avoid sending some values to the database, so that the default values defined in the schema can express themselves.

The values sent to the database are defined by the encode(to: inout PersistenceContainer) method. So we need this method to avoid sending dates.

We also need to grab the inserted dates. This is done with insertAndFetch.

Sample code
import GRDB

var configuration = Configuration()
configuration.prepareDatabase { db in
    db.trace { print("SQL> \($0.expandedDescription)") }
}
let dbQueue = try DatabaseQueue(configuration: configuration)

struct Player: Codable, FetchableRecord, MutablePersistableRecord {
    var id: Int64?
    var name: String
    var score: Int
    var createdDate: Date?
    var modifiedDate: Date?

    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
    
    func encode(to container: inout PersistenceContainer) throws {
        container["id"] = id
        container["name"] = name
        container["score"] = score
        
        // Don't send nil dates to the database, so that the default values
        // defined in the schema can express themselves.
        if createdDate != nil {
            container["createdDate"] = createdDate
        }
        if modifiedDate != nil {
            container["modifiedDate"] = modifiedDate
        }
    }
}

try dbQueue.write { db in
    try db.create(table: "player") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("name", .text).notNull()
        t.column("score", .integer).notNull()
        t.column("createdDate", .text)
            .notNull()
            .defaults(sql: "CURRENT_TIMESTAMP")
        t.column("modifiedDate", .text)
            .notNull()
            .defaults(sql: "CURRENT_TIMESTAMP")
    }
    
    do {
        // INSERT INTO "player" ("id", "name", "score") 
        // VALUES (NULL,'Arthur',100) RETURNING *
        var player = Player(id: nil, name: "Arthur", score: 100)
        let insertedPlayer = try player.insertAndFetch(db)!
        print(player.createdDate) // nil
        print(insertedPlayer.createdDate) // not nil
    }
    
    do {
        // INSERT INTO "player" ("id", "name", "score", "createdDate")
        // VALUES (NULL,'Arthur',100,'2023-12-21 14:33:33.360') RETURNING *
        var player = Player(id: nil, name: "Arthur", score: 100, createdDate: Date())
        let insertedPlayer = try player.insertAndFetch(db)!
        print(player.createdDate) // not nil
        print(insertedPlayer.createdDate) // not nil
    }
}

There are nasty consequences, though. For example:

var player = ...
player.createdDate = nil

// 😬 No error, but column is NOT updated.
// We'd expect instead SQLite error 19: NOT NULL constraint failed
try player.update(db)

All in all, I'm not sure I'd recommend this technique 😅

I'm closing this issue. Please reopen if you have further questions.