Skip to content

feat(postgrest): add type-safe PostgREST query layer via Swift Macros#1036

Draft
grdsdev wants to merge 24 commits into
mainfrom
claude/great-borg-042617
Draft

feat(postgrest): add type-safe PostgREST query layer via Swift Macros#1036
grdsdev wants to merge 24 commits into
mainfrom
claude/great-borg-042617

Conversation

@grdsdev

@grdsdev grdsdev commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a type-safe PostgREST query layer to supabase-swift, allowing users to interact with their database using Swift types instead of raw strings — column names, table names, and query filters are all type-checked at compile time.

What's new

Swift Macros (@Table, @SelectionOf, @Relationship) — Applied to a Swift struct, @Table synthesizes full TableRepresentable conformance including:

  • Insert and Update nested types (with optional fields for @Default/nullable columns; @PrimaryKey fields excluded from inserts)
  • CodingKeys with automatic camelCasesnake_case conversion (overridable via @Column)
  • columnName(for:) using KeyPath identity for type-safe column lookup
  • Read-only tables/views via @Table(readOnly: true)

@SelectionOf(Table.self) declares a partial column projection or join query. Fields annotated with @Relationship(\Table.fkColumn) generate PostgREST disambiguation syntax (alias:tableName!fk_column(subselect)) — the FK column is identified via a Swift KeyPath (type-checked at compile time) and the referenced type is inferred from the field's type annotation.

@Table enforces that @Relationship fields are never declared on it — joins belong exclusively on @SelectionOf structs.

Typed query builders — Four generic wrappers delegate to the existing string-based builders:

  • TypedPostgrestQueryBuilder<Table: TableRepresentable> — insert, upsert, update, delete, select
  • TypedReadOnlyQueryBuilder<Table: ReadOnlyTableRepresentable> — select-only (insert/update/delete are compile errors)
  • TypedPostgrestFilterBuilder<Table, Selection> — KeyPath-based filters (eq, neq, in, like, order, limit, …)
  • TypedPostgrestTransformBuilder / TypedSingleResultBuilder — transform and single-row execution

Entry pointPostgrestClient.from(_ table: T.Type) returns the appropriate typed builder based on whether T conforms to TableRepresentable or ReadOnlyTableRepresentable.

Usage

// Define your table
@Table("messages")
struct Message {
  @PrimaryKey var id: UUID
  var senderId: UUID
  var body: String
}

// Declare a projection with a join
@SelectionOf(Message.self)
struct MessageWithSender {
  var id: UUID
  var body: String
  @Relationship(\Message.senderId) var sender: User
}
// MessageWithSender.selectString == "id,body,sender:users!sender_id(*)"

// Query with full type safety
let rows = try await supabase
  .from(Message.self)
  .select(MessageWithSender.self)
  .execute()
  .value  // [MessageWithSender]

// Filters use KeyPaths
let unread = try await supabase
  .from(Message.self)
  .select()
  .eq(\.senderId, value: userId)
  .execute()
  .value  // [Message]

Architecture

The typed API lives entirely in SupabaseSwiftMacros, a separate opt-in library. PostgREST and Supabase have no dependency on it, so swift-syntax is only compiled for users who explicitly import SupabaseSwiftMacros.

SupabaseMacros (compiler plugin, internal)
    ↑
SupabaseSwiftMacros (opt-in library)
    ├── Protocols.swift         — SelectionRepresentable, ReadOnlyTableRepresentable, TableRepresentable
    ├── Macros.swift            — @Table, @SelectionOf, @PrimaryKey, @Default, @Column, @Relationship
    ├── TypedBuilders/          — TypedPostgrestQueryBuilder, TypedPostgrestFilterBuilder, …
    └── PostgrestClient+Typed.swift — from(_ table: T.Type) entry point

PostgREST  ←──── no swift-syntax dependency
Supabase   ←──── no swift-syntax dependency

Diagnostics

Non-primitive @SelectionOf fields without @Relationship, @Relationship on a @Table field, and let bindings on @Table are all compile-time errors with descriptive messages.

Testing

  • 17 macro expansion tests (exact synthesized source assertions via swift-macro-testing)
  • 8 Swift Testing tests verifying KeyPath → snake_case column name translation via inline-snapshot of actual HTTP request URLs

Test plan

  • swift test --filter SupabaseMacrosTests — all 17 expansion/diagnostic tests pass
  • swift test --filter PostgRESTTests — all typed builder tests pass
  • import PostgREST alone (without import SupabaseSwiftMacros) does not pull in swift-syntax
  • @Table struct with a @Relationship field is a compile error
  • Non-primitive @SelectionOf field without @Relationship is a compile error
  • client.from(ReadOnlyView.self).insert(...) is a compile error

grdsdev added 12 commits June 24, 2026 21:49
Design for type-safe PostgREST queries via CLI codegen + Swift Macros
(@table, @SelectionOf) + a typed wrapper layer over existing string API.
7-task plan covering package infra, protocol layer, marker macros,
@table macro, @SelectionOf macro, typed builders, and client extension.
Also fixes ReadOnlyTableRepresentable protocol hierarchy in the spec.
Defines SelectionRepresentable, ReadOnlyTableRepresentable, and
TableRepresentable protocol hierarchy, plus all six macro stubs
(@table, @SelectionOf, @PrimaryKey, @default, @column, @relationship)
and an empty CompilerPlugin entry point for SupabaseMacros.
…relationship)

These four marker macros are PeerMacros that produce no expansion.
They exist solely as source annotations that the @table and @SelectionOf
macros read during compilation.
Synthesizes TableRepresentable/ReadOnlyTableRepresentable conformance,
Insert, Update, CodingKeys, and columnName for @Table-annotated structs.
Introduces TypedPostgrestQueryBuilder, TypedPostgrestFilterBuilder,
TypedPostgrestTransformBuilder, TypedSingleResultBuilder, and a
PostgrestClient extension so callers can use .from(Table.Type) with
KeyPath-based filter methods that translate to snake_case column names.
Both typed `from()` overloads were discarding the schema variable — both branches of `schema.map` returned `configuration.url`, so non-public schemas were silently ignored. Fix by mutating a copy of the configuration's schema field (mirroring the existing `schema()` method pattern), which causes `PostgrestBuilder.execute()` to emit the correct `Accept-Profile`/`Content-Profile` headers. Also removes the redundant `where Table: ReadOnlyTableRepresentable` constraint on the read-only overload.
…y point

Move PostgrestClient+Typed.swift from Sources/PostgREST/TypedBuilders/ to
the canonical location Sources/PostgREST/PostgrestClient+Typed.swift.
No code changes — the implementation is identical; only the file path is corrected.
…ix swift-syntax constraint

- Add `@_exported import SupabaseSwiftMacros` to Sources/Supabase/Exports.swift so
  consumers who write `import Supabase` gain access to @table, TableRepresentable,
  and related types without an additional import.

- Raise the swift-syntax dependency from `from: "510.0.0"` to `"600.0.0"..<"605.0.0"`.
  swift-macro-testing 0.6.5 accepts swift-syntax "509.0.0"..<"605.0.0", so the 600.x
  range is fully compatible. This prevents SPM from resolving to the old 510.x series
  (510.0.3 was previously pinned) and instead lands on 603.0.2, matching what
  Xcode 16+ ships. Also tightens swift-macro-testing minimum to 0.6.0 to align with
  the actual resolved version (0.6.5).
grdsdev added 10 commits June 25, 2026 06:05
…swift-syntax range

- Move TypedBuilders/ and PostgrestClient+Typed.swift from PostgREST to
  SupabaseSwiftMacros so swift-syntax is only compiled when the typed API
  is explicitly imported — PostgREST and Supabase no longer depend on it
- SupabaseSwiftMacros gains PostgREST as a dependency
- PostgrestClient+Typed now uses public schema()/from() API instead of
  constructing PostgrestQueryBuilder internally
- Swift-syntax range widened to 510.0.0..<605.0.0 to match the package's
  minimum Swift 5.10 support (was 600.0.0..<605.0.0)
 test signature

- Add letBindingNotAllowed diagnostic to TableMacroDiagnostic and enforce
  it in expansion(of:providingMembersOf:in:) so that let-bound stored
  properties in a @table struct emit a clear error instead of being
  silently dropped and causing cryptic compile errors or runtime panics
- Add testLetBindingDiagnostic snapshot test covering both @PrimaryKey let
  and plain let properties
- Update testRelationshipProducesNoPeers to use the current KeyPath-form
  @relationship(\Todo.userId) instead of the obsolete two-argument string form
- Annotate User, Channel, Message with @table macros
- Add MessageWithDetails @SelectionOf for the join query with @relationship
- Replace raw string queries with type-safe from(T.self), eq(\.keyPath), order(\.keyPath)
- Replace AddChannel/NewMessage with generated Insert nested types
- Remove MessagePayload in favour of Message (the @table type)
- Remove convertToSnakeCase/convertFromSnakeCase encoder/decoder strategies
  (@table CodingKeys carry explicit snake_case strings; no strategy needed)
- Add SupabaseSwiftMacros framework to SlackClone target in Xcode project
Extends SupabaseClient with the same typed from(_ table: T.Type) entry
points available on PostgrestClient, so callers can use supabase.from(T.self)
directly without going through supabase.database.from(T.self).

Adds Supabase as a dependency of SupabaseSwiftMacros to make this possible.
@coveralls

coveralls commented Jun 25, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 28173234122

Coverage increased (+0.2%) to 81.177%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 6 coverage regressions across 2 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

6 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
Sources/Auth/Internal/Keychain.swift 5 37.5%
Sources/Auth/Storage/KeychainLocalStorage.swift 1 41.67%

Coverage Stats

Coverage Status
Relevant Lines: 9244
Covered Lines: 7504
Line Coverage: 81.18%
Coverage Strength: 37.2 hits per line

💛 - Coveralls

grdsdev added 2 commits June 25, 2026 10:17
…t conformance

Swift typechecks extension macro expansion files in isolation and cannot
see member macro additions, causing conformance failures in Xcode builds.
The fix moves all protocol implementations into the MemberMacro and
requires users to declare TableRepresentable/ReadOnlyTableRepresentable
explicitly on their struct.

Also fixes SWIFT_DEFAULT_ACTOR_ISOLATION crash in SlackClone target and
updates macro test snapshots to match the new member-only output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants