Skip to content

Embedded Swift support for SQLKit - #4

Draft
scottmarchant wants to merge 1 commit into
feat/wasi-nio-freefrom
feat/embedded-support
Draft

Embedded Swift support for SQLKit#4
scottmarchant wants to merge 1 commit into
feat/wasi-nio-freefrom
feat/embedded-support

Conversation

@scottmarchant

@scottmarchant scottmarchant commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Makes SQLKit compile under Embedded Swift. This builds on #3 (feat/wasi-nio-free) and retargets to base/vapor-main once that merges, so this diff shows only the Embedded work. It is research grade and lower priority than #3, which does not depend on it; if this direction isn't wanted, dropping it costs the WASI support nothing.

The part that needs real discussion is the bound-parameter type. Embedded Swift has no Codable, and SQLKit's bound-parameter constraint is spelled Encodable across roughly eighty public declarations, so the constraint becomes a typealias that resolves differently there:

#if hasFeature(Embedded)
public protocol SQLBindValue { var sqlDataValue: SQLDataValue { get } }
public typealias SQLBindable = SQLBindValue
#else
public typealias SQLBindable = Encodable
#endif

Off Embedded, SQLBindable is Encodable, so every some/any Encodable & Sendable that now reads some/any SQLBindable & Sendable is the same type as before. swift package diagnose-api-breaking-changes reports no breaking changes in SQLKit (the same check the required unit-tests / api-breakage job runs), and the 169-test suite is untouched and green. What remains is that it reads like a large mechanical diff across thirty files, and I know that's a lot to ask of a reviewer; the alternative, duplicating every one of those declarations under a gate, seemed worse. Happy to be told no.

The rest of the Embedded work:

  • The Codable engine (SQLQueryEncoder, SQLRowDecoder, SomeCodingKey, the SQLCodingUtilities helpers, and the model-shaped builder overloads) gates out under Embedded Swift.
  • The generic withSession(_:) moves from protocol requirement to extension default there, since a generic requirement can't be placed in a witness table under Embedded Swift without making any SQLDatabase unusable.
  • Reflection and existential-cast paths (the SQLDatabaseReportedVersion comparisons, description formatting) get Embedded-safe equivalents, implicit array upcasts are boxed explicitly, and one key-path literal becomes a closure.
  • StringHandling's trimmingPrefix(_:) and regex-based helpers fall through to the existing hand-rolled implementations, since the Embedded stdlib ships neither.
  • SQLKitBenchmark's contents gate on !hasFeature(Embedded) (it needs XCTest and Codable), not on any platform; regular WASI still compiles the benchmark target, verified.

Same prerequisite as the sqlite-nio Embedded PR: an Embedded-clean swift-log. Upstream 1.14.0 does not compile under Embedded Swift and the build dies inside Logging first. Verified against a locally patched clone; the patch is not committed here and the manifest still points at upstream apple/swift-log. swift-collections needed nothing.

Verified locally: native build and 169 tests green, no API breakage against the base, regular WASI green, and the Embedded build green (DEVELOPMENT-SNAPSHOT-2026-06-12-a-wasm32-unknown-wasip1-embedded with the patched swift-log).

Motivation:

Embedded Swift has no Codable, no reflection, no existential metatypes, and no
`_StringProcessing`. A number of SQLKit declarations depend on one of those and
are the only thing standing between the SwiftNIO-free configuration and a
`wasm32-unknown-wasip1-embedded` build.

The bound-parameter constraint is the interesting case. SQLKit spells it
`Encodable`, but SQLKit itself never encodes a bound value: `SQLBind` hands the
existential to `SQLSerializer.write(bind:)`, which appends it to `binds` and emits
a placeholder. The constraint is a marker, and the actual extraction happens in
the driver.

That makes a vacuous `#if hasFeature(Embedded) public protocol Encodable {}` shim
look attractive — it would leave every use site unchanged. It does not work. The
marker exists precisely to carry a value across the module boundary to a driver
that must get the value back out, and a protocol with no requirements carries
nothing: under Embedded there is no `encode(to:)` to call, no reflection, and no
dynamic cast to fall back on, so the bind path would compile and then be inert.
Shadowing `Swift.Encodable` in a public signature would also collide with the
same shim in other modules and cannot be made `internal`, because the constraint
appears in public and `@inlinable` declarations.

Modifications:

Introduce `SQLBindable`, the constraint used for bound parameter values. It is
`typealias SQLBindable = Encodable` except under Embedded Swift, where it resolves
to a new lightweight `SQLBindValue` protocol carrying a driver-neutral
`SQLDataValue`, with conformances for the standard primitives. `SQLBindValue` is
what replaces `encode(to:)` for the driver.

The public declarations that spelled `some`/`any Encodable & Sendable` for a bound
value now spell `some`/`any SQLBindable & Sendable`. Off Embedded that is the same
type, so this is source- and ABI-compatible. Declarations that take a Codable
*model* rather than a bound value keep saying `Encodable`; they live inside
`!hasFeature(Embedded)` regions, where the two spellings are identical anyway.

Everything else is elision:

- Gate the Codable engine (`SQLQueryEncoder`, `SQLRowDecoder`, `SomeCodingKey`,
  the `SQLCodingUtilities` helpers, and the model-shaped builder overloads
  `set(model:)` / `insert(models:)` / `decode(model:)`) behind
  `#if !hasFeature(Embedded)`.
- Move the generic `withSession(_:)` protocol requirement to an extension default
  under Embedded: a generic method cannot go in a witness table there, which would
  otherwise make `any SQLDatabase` unusable.
- Replace reflection- and existential-dependent paths (metatype casts in
  `SQLDatabaseReportedVersion` and the deprecated shims, description formatting in
  `SQLSerializer`/`SQLQueryString`, the `as?`-based `LIMIT 1` optimization in
  `first()`) with Embedded-safe equivalents.
- `StringHandling`: `trimmingPrefix(_:)` and the regex-backed replacement helper
  come from `_StringProcessing`, which the Embedded stdlib does not ship; fall
  through to the existing hand-rolled implementations there.
- `SQLKitBenchmark` needs XCTest and Codable, so the whole target's contents are
  gated on `!hasFeature(Embedded)`.

Every `#if` encloses the doc comment of the declaration it gates rather than
sitting between the two, which would detach the comment and silently empty the
published documentation for that symbol.

Result:

SQLKit compiles for `wasm32-unknown-wasip1-embedded`.

On every other target the only change to the generated symbol graph is the added
`SQLBindable` typealias: every other symbol is still present, with byte-identical
`docComment` line counts, and `diagnose-api-breaking-changes` reports no
differences.

Building for the Embedded target additionally requires an Embedded-clean
`apple/swift-log`; see the pull request description.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant