Closed RafaelPlantard closed 1 year ago
Unfortunately, the dates resulting from this code don't make a great deal of sense. For example:
1> import Foundation
2> DateComponents(calendar: .init(identifier: .gregorian), timeZone: .init(secondsFromGMT: 0)!, hour: 1, minute: 0, second: 0).date
$R0: Foundation.Date? = 0000-12-30 01:00:00 UTC
The Date
type is not intended to be able to represent a time independently of a calendar date. Nor is it intended by MySQLNIO that the MySQLData.date
property be used for TIME
columns; the slightly lower-level MySQLData.time
property is provided for that purpose (and other related uses).
However, even with all of that aside, it's not a change I can approve regardless - it changes the behavior of an existing public API, one whose behavior is (no matter how undesirably) relied upon in a variety of ways by a lot of somewhat... tricky... code up in Fluent.
P.S.: Please don't take this as a discouragement of your effort; contributions are welcome and appreciated, even when we have to reject them!
I understand, could you please help me to get a TIME value from DB in Fluent? Because when I try to do something like that:
@Field(key: "column_in_db")
var time: Date?
it's not working and after debugging quite a time I just discovered that because of this guard let:
guard
let year = self.year,
let month = self.month,
let day = self.day
else {
return nil
}
a TIME when represented as a Date object is always nil, could you give a hint to solve this?
cc @gwynne
@RafaelPlantard To use such a column, you'd need to do something like this:
import Foundation
import FluentKit
import MySQLNIO
// Unfortunately, `TimestampFormat` requires its values to be `Codable`, even though the
// conformance won't be used, and `MySQLTime` does not include a `Codable` conformance,
// so we must provide it here:
extension MySQLNIO.MySQLTime: Codable {
private enum CodingKeys: String, CodingKey { case year,month,day,hour,minute,second,microsecond }
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Self.CodingKeys.self)
self.init(
year: try container.decodeIfPresent(UInt16.self, forKey: .year),
month: try container.decodeIfPresent(UInt16.self, forKey: .month),
day: try container.decodeIfPresent(UInt16.self, forKey: .day),
hour: try container.decodeIfPresent(UInt16.self, forKey: .hour),
minute: try container.decodeIfPresent(UInt16.self, forKey: .minute),
second: try container.decodeIfPresent(UInt16.self, forKey: .second),
microsecond: try container.decodeIfPresent(UInt32.self, forKey: .microsecond)
)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Self.CodingKeys.self)
try container.encodeIfPresent(self.year, forKey: .year)
try container.encodeIfPresent(self.month, forKey: .month)
try container.encodeIfPresent(self.day, forKey: .day)
try container.encodeIfPresent(self.hour, forKey: .hour)
try container.encodeIfPresent(self.minute, forKey: .minute)
try container.encodeIfPresent(self.second, forKey: .second)
try container.encodeIfPresent(self.microsecond, forKey: .microsecond)
}
}
/// A `TimestampFormat` which translates between a `MySQLTime` and a `Date` using a
/// "forgiving" algorithm which does not refuse to parse if the "date" components are missing.
/// Can be used with columns of type `TIMESTAMP`, `DATETIME`, `DATE`, and `TIME`.
///
/// `MySQLTime` is the type used to represent the various MySQL date/time-related column
/// types in parameter bindings and result rows. Normally, a time structure is converted to a
/// Foundation `Date` via the C API `timegm(3)`. However, `timegm(3)` will refuse to return a
/// result if the `year`, `month`, and `day` components are not provided. This format uses
/// Foundation's `DateComponents` type to do the conversion instead; `DateComponents` is 3-5x
/// slower than `timegm(3)`, but it will try to come up with _some_ kind of result for almost
/// any combination (or lack) of components. Be aware that the result may not always make
/// much sense; for example, a "pure time" value such as "01:00:00" results in the date
/// value `"0000-12-30 01:00:00 UTC"`.
///
/// > Note: This does not work for `YEAR` columns; use a `UInt16` field instead.
public struct ForgivingMySQLTimestampFormat: FluentKit.TimestampFormat {
public typealias Value = MySQLNIO.MySQLTime
private let calendar = Calendar(identifier: .gregorian)
private let timeZone = TimeZone(identifier: "UTC")!
public func parse(_ value: MySQLNIO.MySQLTime) -> Date? {
var components = DateComponents(
calendar: self.calendar,
timeZone: self.timeZone,
year: value.year.map(Int.init(_:)),
month: value.month.map(Int.init(_:)),
day: value.day.map(Int.init(_:)),
hour: value.hour.map(Int.init(_:)),
minute: value.minute.map(Int.init(_:)),
second: value.second.map(Int.init(_:)),
nanosecond: value.microsecond.map { Int($0) * 1000 }
)
return components.date
}
public func serialize(_ date: Date) -> MySQLNIO.MySQLTime? {
.init(date: date) // no need to be fancy when going in the other direction
}
}
extension FluentKit.TimestampFormatFactory {
public static var forgivingMySQL: FluentKit.TimestampFormatFactory<ForgivingMySQLTimestampFormat> {
.init { ForgivingMySQLTimestampFormat() }
}
}
You can then write your model's property this way:
@Timestamp(key: "column_in_db", on: .none, format: .forgivingMySQL)
var time: Date?
I know this is a ridiculous amount of work to do something that seems so simple; I wish I had something better to suggest.
Thank you so much. I'll try this!
The TIME implementation was always getting nil, because time doesn't have year, month, day.