Skip to content

DATE columns set values with timezone offsets, producing conflicts when db server is on a different timezone. #462

Open
@janigro

Description

@janigro

Problem description

First I should say that I read issue #453, although I think this one is slightly different.

When saving a date to a DATE column, the INSERT specifies a timezone offset. This is not usually a problem, if we are in the same timezone as the db server. But working with a server on America/New_York, inserting 2000-01-01 00:00:00+00:00 will actually insert 1999-12-31 (as in New York, it is not 2000-01-01 yet).

This behaviour differs from MySQL, which handles (to my understanding) this case correctly. Inspecting the statement logs in both servers, there is a clear difference in the statement they produce (bindings where substituted by their values for clarity):

MySQL:    INSERT INTO `friends` (`id`, `name`, `birthday`) VALUES (DEFAULT, 'Paul', '2000-01-01 00:00:00')
Postgres: INSERT INTO "friends" ("id", "name", "birthday") VALUES (DEFAULT, 'Paul', '1999-12-31 19:00:00-05') RETURNING "id"

In MySQL, we can ignore the server timezone, and simply use UTC. This guarantees the date stored is the one we will fetch. With Postgres however, we need to be aware of the server timezone at all times, in order to create the Date objects to be inserted, using the server's timezone.

I cannot see the reasoning behind this (I'm sure there's one, I just don't see it).

To Reproduce

A clear example for using the DATE column type, is for storing birthday dates. The time is completely irrelevant and even less is its timezone.

final class Friend: Model, Content {
    static let schema = "friends"

    @ID(custom: "id") public var id: Int?

    @Field(key: "name") public var name: String
    @Field(key: "birthday") public var birthday: Date

    init() { }
    
    init(id: Int? = nil, name: String, birthday: Date) {
        self.id = id
        self.name = name
        self.birthday = birthday
    }
}

extension Friend {
    struct Migration: Fluent.AsyncMigration {
        
        func prepare(on database: Database) async throws {
            
            try await database.schema(Friend.schema)
                .field(.id, .int, .identifier(auto: true))
                .field("name", .string, .required)
                .field("birthday", .date, .required)
                .create()
        }
        
        func revert(on database: Database) async throws {
            try await database.schema(Friend.schema).delete()
        }
    }
}
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)

try await Friend(name: "Paul", 
                 birthday: dateFormatter.date(from: "2000-01-01")!).save(on: db)

For this to work correctly on Postgres, we need to know the server's timezone and use it to create the Date object.

dateFormatter.timeZone = .init(identifier: "America/New_York")

Expected behavior

I would think that the result should be the same, no matter which database we are connecting to. I absolutely prefer MySQL's behavior.

If for any reason this is not possible, would it be too crazy/weird to ask that defining the Model property as String (instead of Date) would work with DATE column types too? That way we could do:

try await Friend(name: "Paul", birthday: "2000-01-01").save(on: db)

and that would translate into:

INSERT INTO "friends" ("id", "name", "birthday") VALUES (DEFAULT, 'Paul', '2000-01-01') RETURNING "id"

Another and better solution could be having a different swift type altogether (e.g., DateWithoutTime) and get rid of all ambiguity.

In my case, I can afford to do ALTER DATABASE SET TIMEZONE to 'UTC' and forget about it... but that may not always be possible.

Environment

framework: 4.77.0
toolbox: 18.7.4
postgres-nio: 1.17.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions