Skip to content

Commit cb50670

Browse files
committed
Fix KeyPath-to-column-name resolution on Linux
String(describing: keyPath) produces <computed_0x...> on Linux instead of the property name, breaking all WHERE, ORDER BY, and SELECT clauses. Add _keyPathToColumn mapping to Schema protocol that provides explicit KeyPath→property name resolution. The @Schema macro auto-generates it; manual schemas provide their own dictionary. All extractFieldName functions check the mapping first, falling back to string parsing for backward compatibility on macOS.
1 parent 881a864 commit cb50670

6 files changed

Lines changed: 87 additions & 7 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
public protocol Schema: Sendable {
22
static var tableName: String { get }
33
init()
4+
/// KeyPath → Swift property name mapping for cross-platform query building.
5+
/// On Linux, `String(describing: keyPath)` produces garbage — this provides
6+
/// the reliable source of truth. Generated automatically by `@Schema` macro;
7+
/// manual schemas should provide their own.
8+
static var _keyPathToColumn: [AnyKeyPath: String] { get }
9+
}
10+
11+
extension Schema {
12+
// AnyKeyPath is not Sendable, but the dictionary is immutable
13+
// and only read after initialization.
14+
nonisolated public static var _keyPathToColumn: [AnyKeyPath: String] { [:] }
415
}

Sources/Spectro/Query/JoinQuery.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,6 @@ public struct JoinQueryBuilder<T: Schema, U: Schema>: Sendable {
125125
}
126126
}
127127

128-
private func extractFieldName<T: Schema, V>(from keyPath: KeyPath<T, V>, schema: T.Type) -> String {
129-
let keyPathString = "\(keyPath)"
130-
return keyPathString.components(separatedBy: ".").last ?? keyPathString
131-
}
132-
133128
// MARK: - Query Extensions for JOIN execution
134129

135130
extension Query {

Sources/Spectro/Query/Query.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ public struct Query<T: Schema>: Sendable {
533533
}
534534

535535
private func extractRelationshipName<Related>(from keyPath: KeyPath<T, SpectroLazyRelation<Related>>) -> String {
536+
if let name = T._keyPathToColumn[keyPath] { return name }
536537
if let propertyName = keyPath.propertyName { return propertyName }
537538
let keyPathString = String(describing: keyPath)
538539
if let match = keyPathString.range(of: #"\.\$?([a-zA-Z_][a-zA-Z0-9_]*)>*$"#, options: .regularExpression) {
@@ -675,6 +676,7 @@ public struct QueryBuilder<T: Schema>: Sendable {
675676
}
676677

677678
private func extractFieldName<V>(from keyPath: KeyPath<T, V>) -> String {
679+
if let name = T._keyPathToColumn[keyPath] { return name }
678680
let keyPathString = "\(keyPath)"
679681
return keyPathString.components(separatedBy: ".").last ?? keyPathString
680682
}
@@ -990,7 +992,8 @@ extension JoinField where V: Comparable {
990992

991993
// MARK: - KeyPath Field Name Extraction
992994

993-
private func extractFieldName<T: Schema, V>(from keyPath: KeyPath<T, V>, schema: T.Type) -> String {
995+
internal func extractFieldName<T: Schema, V>(from keyPath: KeyPath<T, V>, schema: T.Type) -> String {
996+
if let name = T._keyPathToColumn[keyPath] { return name }
994997
let keyPathString = "\(keyPath)"
995998
return keyPathString.components(separatedBy: ".").last ?? keyPathString
996999
}

Sources/Spectro/SchemaBuilder/SchemaMacro.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,6 @@
7070
///
7171
/// If you provide your own `tableName` or `init()` in the struct body, the macro
7272
/// skips generating those members.
73-
@attached(member, names: named(tableName), named(init))
73+
@attached(member, names: named(tableName), named(init), named(_keyPathToColumn))
7474
@attached(extension, conformances: Schema, SchemaBuilder, names: named(build))
7575
public macro Schema(_ tableName: String) = #externalMacro(module: "SpectroMacros", type: "SchemaMacro")

Sources/SpectroMacros/SchemaMacro.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,25 @@ extension SchemaMacro: MemberMacro {
314314
decls.append(convInit)
315315
}
316316

317+
// --- static let _keyPathToColumn ---
318+
let typeName = structDecl.name.text
319+
var entries: [String] = []
320+
for prop in properties {
321+
switch prop.wrapper {
322+
case .id, .column, .timestamp, .foreignKey:
323+
entries.append("\\\(typeName).\(prop.name): \"\(prop.name)\"")
324+
case .hasMany, .hasOne, .belongsTo, .manyToMany:
325+
entries.append("\\\(typeName).$\(prop.name): \"\(prop.name)\"")
326+
}
327+
}
328+
let entriesStr = entries.joined(separator: ",\n ")
329+
let keyPathDecl: DeclSyntax = """
330+
nonisolated(unsafe) static let _keyPathToColumn: [AnyKeyPath: String] = [
331+
\(raw: entriesStr)
332+
]
333+
"""
334+
decls.append(keyPathDecl)
335+
317336
return decls
318337
}
319338
}

Tests/SpectroTests/Helpers/TestSchemas.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ import Foundation
66
struct TestUser: Schema, SchemaBuilder {
77
static let tableName = "test_users"
88

9+
nonisolated(unsafe) static let _keyPathToColumn: [AnyKeyPath: String] = [
10+
\TestUser.id: "id",
11+
\TestUser.name: "name",
12+
\TestUser.email: "email",
13+
\TestUser.age: "age",
14+
\TestUser.isActive: "isActive",
15+
\TestUser.createdAt: "createdAt",
16+
]
17+
918
@ID var id: UUID
1019
@Column var name: String
1120
@Column var email: String
@@ -46,6 +55,13 @@ struct TestUser: Schema, SchemaBuilder {
4655
struct TestUserWithBio: Schema, SchemaBuilder {
4756
static let tableName = "test_users_bio"
4857

58+
nonisolated(unsafe) static let _keyPathToColumn: [AnyKeyPath: String] = [
59+
\TestUserWithBio.id: "id",
60+
\TestUserWithBio.name: "name",
61+
\TestUserWithBio.email: "email",
62+
\TestUserWithBio.bio: "bio",
63+
]
64+
4965
@ID var id: UUID
5066
@Column var name: String
5167
@Column var email: String
@@ -92,6 +108,12 @@ struct TestMacroUser {
92108
struct TestColumnOverride: Schema, SchemaBuilder {
93109
static let tableName = "test_column_overrides"
94110

111+
nonisolated(unsafe) static let _keyPathToColumn: [AnyKeyPath: String] = [
112+
\TestColumnOverride.id: "id",
113+
\TestColumnOverride.name: "name",
114+
\TestColumnOverride.email: "email",
115+
]
116+
95117
@ID var id: UUID
96118
@Column("display_name") var name: String = ""
97119
@Column var email: String = ""
@@ -113,6 +135,12 @@ struct TestColumnOverride: Schema, SchemaBuilder {
113135
struct TestWithUuidColumn: Schema, SchemaBuilder {
114136
static let tableName = "test_with_uuid_columns"
115137

138+
nonisolated(unsafe) static let _keyPathToColumn: [AnyKeyPath: String] = [
139+
\TestWithUuidColumn.id: "id",
140+
\TestWithUuidColumn.externalId: "externalId",
141+
\TestWithUuidColumn.name: "name",
142+
]
143+
116144
@ID var id: UUID
117145
@Column var externalId: UUID
118146
@Column var name: String
@@ -138,6 +166,11 @@ struct TestWithUuidColumn: Schema, SchemaBuilder {
138166
struct IntPKItem: Schema, SchemaBuilder {
139167
static let tableName = "int_pk_items"
140168

169+
nonisolated(unsafe) static let _keyPathToColumn: [AnyKeyPath: String] = [
170+
\IntPKItem.id: "id",
171+
\IntPKItem.name: "name",
172+
]
173+
141174
@ID var id: Int
142175
@Column var name: String
143176

@@ -168,6 +201,11 @@ struct IntPKItem: Schema, SchemaBuilder {
168201
struct StringPKItem: Schema, SchemaBuilder {
169202
static let tableName = "string_pk_items"
170203

204+
nonisolated(unsafe) static let _keyPathToColumn: [AnyKeyPath: String] = [
205+
\StringPKItem.id: "id",
206+
\StringPKItem.name: "name",
207+
]
208+
171209
@ID var id: String
172210
@Column var name: String
173211

@@ -193,6 +231,12 @@ struct StringPKItem: Schema, SchemaBuilder {
193231
struct IntFKChild: Schema, SchemaBuilder {
194232
static let tableName = "int_fk_children"
195233

234+
nonisolated(unsafe) static let _keyPathToColumn: [AnyKeyPath: String] = [
235+
\IntFKChild.id: "id",
236+
\IntFKChild.label: "label",
237+
\IntFKChild.parentId: "parentId",
238+
]
239+
196240
@ID var id: UUID
197241
@Column var label: String
198242
@ForeignKey var parentId: Int
@@ -265,6 +309,14 @@ struct RelUserTag {
265309
struct TestPost: Schema, SchemaBuilder {
266310
static let tableName = "test_posts"
267311

312+
nonisolated(unsafe) static let _keyPathToColumn: [AnyKeyPath: String] = [
313+
\TestPost.id: "id",
314+
\TestPost.title: "title",
315+
\TestPost.body: "body",
316+
\TestPost.userId: "userId",
317+
\TestPost.createdAt: "createdAt",
318+
]
319+
268320
@ID var id: UUID
269321
@Column var title: String
270322
@Column var body: String

0 commit comments

Comments
 (0)