Open nastynate13 opened 9 months ago
Thank you for this very detailed write up!
The pattern I've settled around was your second option, as described here https://www.kodeco.com/23848990-database-migrations-with-vapor/page/2?page=2#toc-anchor-008
I do tend to suggest people follow that as a way of name-spacing and date-spacing their FieldKey
s. We should definitely have something in the docs for this as it comes up often.
This also gives us some things to take into the design of Fluent 5
Thanks for taking the time to read through! This is helpful. Somewhat related, I also wanted to quickly suggest sugar for Enum that leverages CaseIterable.
Since the current requirement for database backed Enums is only types conforming to RawRepresentable where RawValue is String, why not encapsulate this in a protocol?
protocol DatabaseEnum: CaseIterable, RawRepresentable where Self.RawValue == String {
static var fieldKey: String { get }
}
extension FooEnum: DatabaseEnum {
static var fieldKey: String { "fooEnum_type" }
}
// It would be nice to be able to write something like this
let fooEnumType = try await database.enum(FooEnum.self)
.allCases()
.create()
Implementation below:
protocol DatabaseEnum: CaseIterable, RawRepresentable where Self.RawValue == String {
static var fieldKey: String { get }
}
extension Database {
func `enum`<T: DatabaseEnum>(_ type: T.Type) -> CaseBuildingEnumBuilder<T> {
.init(type, builder: self.enum(type.fieldKey))
}
}
final class CaseBuildingEnumBuilder<DataType: DatabaseEnum> {
let dataType: DataType.Type
let builder: EnumBuilder
init(_ dataType: DataType.Type, builder: EnumBuilder) {
self.builder = builder
self.dataType = dataType.self
}
func allCases() -> EnumBuilder {
for value in dataType.allCases {
_ = builder.case(value.rawValue)
}
return builder
}
}
Without this (or something similar), defining the initial migration for the enum fields manually can be a little tedious an error prone, esp. for enum heavy projects that would like to take advantage of database backed enums.
I think the main issue here is when you add a new case to an enum in a separate migration different systems can get out of sync, so a clean DB would fail when it tries to add the new case a second time
Aha.. yes, understood!
I would also add that doing more with FieldKey
s is out of scope for Fluent 4. I don't know yet whether Fluent 5 will still have a FieldKey
concept, but my current sense of it is that it might not. I'll leave this issue open as a feature request for Fluent 5 for now.
Is your feature request related to a problem? Please describe.
Swift developers tend to avoid using string literals
@Field(key: "literal")
instead opting for an enum based approach
This solves quite a few problems - namely:
When using Fluent, keys for a Model are represented by a FieldKey which conveniently conforms to ExpressibleByStringLiteral. This affords the following options:
@Field(key: "literal")
This approach can certainly be convenient when prototyping, however it still suffers from every issue named above.
Although this solves many of the above problems, a new problem of verbosity is introduced, so I'd argue that this approach remains "not great enough" and ultimately feels like we're fighting the api a bit.
This is less verbose at the call site which is nice. However, by using a static variable we don't get type inference and instead have to declare the FieldKey type for each key. We also lose the inherent benefits of using enum cases to define our keys. This includes switching, key iteration (via CaseIterable), implicit raw value conversion (useful for non-compound names), compile time checking of raw value duplicates, and overall reduced verbosity at the creation site.
This feels like the best approach because it affords us the ability to use leading dot syntax for any FieldKey - a big win for ergonomics and code duplication for keys with the same raw value. There are still caveats to this approach though:
1) Because keys are no longer scoped to a type for a particular model, this could lead to confusion about whether the key you intend to define for a model has indeed already been defined to name a field in another model.
2) Having all keys defined in the same global space could quickly become cluttered and difficult to manage or track for larger projects. Scoping a model's keys to the model it is keying is arguably a more natural, manageable, and portable fit.
3) We miss out on many of the aforementioned benefits of defining the FieldKey as a Enum case rather than a static var.
4) Although this appears to be a leading candidate as an approach to defining keys, nowhere in the docs is this demonstrated or suggested and therefore it might not be fundamentally apparent, especially to those new to Vapor.
We now have an additional problem which I'll label "Uncertainty". Although there are various alternatives that solve part of the problem, - extending FieldKey being the leading candidate IMO - there is no standard or suggested approach.
Describe the solution you'd like
I would love to see a solution that solves :
This approach brings us the same leading dot syntax that static extensions on FieldKey affords and keys that are scoped to the model they define for more clarity.
Currently I have this implemented using an opt in approach that uses 3 new protocols.
I do not purport to say this is a great solution or implementation, but I prefer the clarity of this approach to the alternatives. Model.Keys can be loosely compared to a CodingKey for Codable types.
The implementation I'm using is admittedly rushed and hacky but I've added it below just as a proof of concept.
These extensions alone do not allow for using identical dot syntax in other places where a FieldKey is expected - namely migrations (i.e. SchemaBuilder- which - afaik - has no knowledge of the type of model it is building presumably for very good reason / by design). In order for this to be comprehensive enough across the api to be coherent, I've added a way to integrate SchemeBuilder so we could write this:
Here is an ad hoc implementation that uses a wrapper type:
Describe alternatives you've considered
Maintain the status quo in which there is flexibility for using string literals and/or porting your own type to FieldKey or statically extending FieldKey.
Improve documentation to demonstrate and or suggest statically extending FieldKey.
Some other splendid, yet heretofore unimagined solution..