Replies: 8 comments 24 replies
-
Are they a big deal really? In ObjC they were always so painful to use, read & maintain. Seems to me like most things expressed by macros are much better expressed with functions, but I'll be happy to be made wrong 🤷♂️ |
Beta Was this translation helpful? Give feedback.
-
Here is my first pass at a quick macro to parse stored properties of a model into a
Which then generates:
https://gist.github.com/aaronpearce/8421f743c28c5349c7d5abfbb7fa1b11 After doing this, I think there could be a nicer solution of annotating each column manually so that we can still have transient properties if need be. Something more like the below, which could also be applied at the top model as well if we wished.
Another option around that would be make it on the whole model, then let a user opt-out instead for transient properties with a |
Beta Was this translation helpful? Give feedback.
-
Now that we know what Macros really mean in the Swift context, this feels like a great possible move especially for GRDB which can try and mirror some of the niceties of SwiftData API without being crappy in the inside (uhm Core Data uhm) |
Beta Was this translation helpful? Give feedback.
-
I'm compiling below a few user feedbacks:
If you want to reply to one of them, please don't reply in this thread, but create a new one in this discussion, so that we can keep focused on each topic 🙏 |
Beta Was this translation helpful? Give feedback.
-
GRDB has never required This is currently not possible, because macros that generate initializers prevent the compiler generation of the memberwise initializer (https://forums.swift.org/t/allow-attached-macros-to-not-suppress-the-memberwise-initializer/65392). But, assuming this issue with macros is solved eventually: // What user writes
@DatabaseRecord
struct Player {
var id: String
var name: String
var score: Int
}
// What compiler sees
struct Player: FetchableRecord, PersistableRecord {
var id: String
var name: String
var score: Int
init(row: Row) {
self.id = row["id"]
self.name = row["name"]
self.score = row["score"]
}
func encode(to container: inout PersistenceContainer) throws {
container["id"] = id
container["name"] = name
container["score"] = score
}
} We could even generate optimized records out of the box: // What compiler could also see
struct Player: FetchableRecord, PersistableRecord {
var id: String
var name: String
var score: Int
static var databaseTableName: String {
// Improvement: this one is now computed at compile time
"player"
}
static var databaseSelection: [any SQLSelectable] {
// Improvement: if struct has less properties than there are table columns,
// unused columns are not selected.
//
// Before macros: SELECT * FROM player
// After macros: SELECT id, name, score FROM player
[Column("id"), Column("name"), Column("score")]
}
init(row: Row) {
// Improvement: we can access columns by index (fast)
// instead of names (slow), because we have generated
// the selection.
self.id = row[0]
self.name = row[1]
self.score = row[2]
}
func encode(to container: inout PersistenceContainer) throws {
container["id"] = id
container["name"] = name
container["score"] = score
}
} We'd probably need to link this generated code to the generated columns |
Beta Was this translation helpful? Give feedback.
-
Macros could improve associations. Simple associations can be defined as: extension Author {
static let books = hasMany(Book.self)
} But there is some boilerplate for users who want to access books from an Author instance: extension Author {
static let books = hasMany(Book.self)
var books: QueryInterfaceRequest<Book> {
request(for: Self.books)
}
} And users who refine associations have to take care of using association keys. In the example below, the custom keys are necessary, or requests that involve both novels and essays would not give the correct results: extension Author {
static let books = hasMany(Book.self)
static let novels = books.filter(kind: .novel).forKey("novels")
static let essays = books.filter(kind: .essay).forKey("essays")
} And still some boilerplate: extension Author {
static let books = hasMany(Book.self)
static let novels = books.filter(kind: .novel).forKey("novels")
static let essays = books.filter(kind: .essay).forKey("essays")
var books: QueryInterfaceRequest<Book> {
request(for: Self.books)
}
var novels: QueryInterfaceRequest<Book> {
request(for: Self.novels)
}
var essays: QueryInterfaceRequest<Book> {
request(for: Self.essays)
}
} Custom association keys are also useful for associations that do not have the same name as their target table. In the example below, the custom key is required if we want to refer to an array of struct ShelfItem { ... }
struct Shelf {
static let items = hasMany(ShelfItem.self).forKey("items")
}
struct ShelfWithItems: Decodable, FetchableRecord {
var shelf: Shelf
var items: [ShelfItem] // named after the association key
}
let shelvesWithItems = try dbQueue.read { db in
try Shelf
.including(all: Shelf.items)
.asRequest(of: ShelfWithItems.self)
.fetchAll(db)
} The pattern is usually the same:
|
Beta Was this translation helpful? Give feedback.
-
Ive been playing with a couple macros that sorta remind me of Vapor's Fluent models & property wrappers. The pattern I have tended to skew towards with GRDB for a long time is:
I know that you can get a lot for free with some of existing codable helpers in the library and reduce some of that boilerplate but ¯\_(ツ)_/¯ . Most of this boilerplate can be eliminated with macros. Along with some helpers for relationships as well (happy to share source code if anyone is interested): Usage@Table(name: "clubs")
public final class GKClub {
@Column(name: "id") public var id: UUID
@Column(name: "name") public var name: String
@Column(name: "abbreviation") public var abbreviation: String
@Column(name: "bag_id") public var bagId: GKBag.ID?
@BelongsTo public var bag: QueryInterfaceRequest<GKBag>
@HasMany public var shots: QueryInterfaceRequest<GKShot>
} Expansion@Table(name: "clubs")
public final class GKClub {
@Column(name: "id") public var id: UUID
@Column(name: "name") public var name: String
@Column(name: "abbreviation") public var abbreviation: String
@Column(name: "bag_id") public var bagId: GKBag.ID?
@BelongsTo public var bag: QueryInterfaceRequest<GKBag>
{
get {
request(for: Self.bag)
}
}
@HasMany public var shots: QueryInterfaceRequest<GKShot>
{
get {
request(for: Self.shots)
}
}
@ColumnAccessor(name: "id") public static var id
@ColumnAccessor(name: "name") public static var name
@ColumnAccessor(name: "abbreviation") public static var abbreviation
@ColumnAccessor(name: "bag_id") public static var bagId
public init(id: UUID, name: String, abbreviation: String, bagId: GKBag.ID?) {
self.id = id
self.name = name
self.abbreviation = abbreviation
self.bagId = bagId
}
public convenience init(row: Row) throws {
self.init(id: row[GKClub.$id], name: row[GKClub.$name], abbreviation: row[GKClub.$abbreviation], bagId: row[GKClub.$bagId])
}
public func encode(to container: inout PersistenceContainer) throws {
container[GKClub.$id] = id
container[GKClub.$name] = name
container[GKClub.$abbreviation] = abbreviation
container[GKClub.$bagId] = bagId
}
public static let databaseTableName = "clubs"
public static let bag = belongsTo(GKBag.self, key: GKBag.databaseTableName)
public static let shots = hasMany(GKShot.self)
}
extension GKClub : TableRecord {}
extension GKClub : FetchableRecord {}
extension GKClub : PersistableRecord {} Im using a property wrapper ColumnAccessor implementation@propertyWrapper
public struct ColumnAccessor {
public init(name: String) {
self.wrappedValue = name
}
public let wrappedValue: String
public var projectedValue: some ColumnExpression {
Column(wrappedValue)
}
} Current Limitations
Pre macro workPrior to macros I had made a property wrapper with a similar(stolen) implementation to Field property wrapper in Fluent, and then a static subscript on my own Example of property wrapper way with static subscriptpublic final class GKClub: Table, ... {
@Field(key: "id") public var id: UUID
@OptionalField(key: "name") public var name: String?
...
}
_ = row[GKClub.$name] |
Beta Was this translation helpful? Give feedback.
-
Hey y'all! I am currently working on a @DatabaseRecord
struct Author {
var id: Int64?
var name: String
var countryCode: String?
@Transient var somePropertyToExclude = true
@HasMany var books: QueryInterfaceRequest<Book>
} Will expand to: struct Author {
var id: Int64?
@Column("title") var name: String
var countryCode: String?
@Transient var somePropertyToExclude = true
var books: QueryInterfaceRequest<Book> {
get {
request(for: Self.books)
}
}
static let books = hasMany(Book.self)
}
extension Author: GRDB.FetchableRecord {
init(row: GRDB.Row) {
self.id = row[Columns.id]
self.name = row[Columns.name]
self.countryCode = row[Columns.countryCode]
}
}
extension Author: GRDB.PersistableRecord {
func encode(to container: inout GRDB.PersistenceContainer) throws {
container[Columns.id] = id
container[Columns.name] = name
container[Columns.countryCode] = countryCode
}
}
extension Author {
enum Columns {
static let id = GRDB.Column("id")
static let name = GRDB.Column("title")
static let countryCode = GRDB.Column("countryCode")
}
}
extension Author {
static let databaseSelection: [any GRDB.SQLSelectable] = [
Columns.id,
Columns.name,
Columns.countryCode
]
} Most of the suggestions come from #1323 (comment) but I am planning on adding more features, like generating SQL tables! If there's any features/additions you would like for me to add let me know! As of now the source code will be up on https://github.com/ErrorErrorError/GRDBMacros, but if the dev would want for it to be on GRDB.swift, feel free to let me know :D |
Beta Was this translation helpful? Give feedback.
-
Hello GRDB users,
Do you think the library could benefit from Swift macros? Could they solve a pain point, or help shipping a new feature?
Macros are a big deal. Please share your ideas!
Beta Was this translation helpful? Give feedback.
All reactions