StructuredQueries: a SQL query builder made possible by Swift's unique feature set

We just open sourced a new Swift package, StructuredQueries, which provides a very type-safe API for building SQL in Swift. It leverages a ton of advanced Swift features—including macros, parameter packs, dynamic member lookup with metatype key paths, result builders, and more—to achieve things we haven't seen in other languages and libraries, and with a minimal amount of userland code.

For example, applying the library's @Table macro to the following two Swift data types:

@Table
struct RemindersList {
  let id: Int
  var title = ""
}

@Table
struct Reminder {
  let id: Int
  var isCompleted = false
  var remindersListID: Int
  var title = ""
}

…enables a suite of APIs that are type-safe from building the query all the way to decoding. This includes select statements with complex joins, as well as insert, update, and delete statements:

RemindersList
  .group(by: \.id)
  .leftJoin(Reminder.where { !$0.isCompleted }) { $0.id == $1.remindersListID }
  .select { ($0, $1.count()) }
// SELECT
//   "remindersLists"."id", "remindersLists"."title", count("reminders"."id")
// FROM "remindersLists"
// JOIN "reminders" ON "remindersLists"."id" = "reminders"."remindersListID"
// WHERE (NOT "reminders"."isCompleted")
// GROUP BY "remindersLists"."id"
// => (RemindersList, Int)

Reminder.insert {
  ($0.remindersListID, $0.title)
} values: {
  (1, "Get groceries")
  (2, "Take a walk")
  (1, "Get haircut")
}
.returning(\.id)
// INSERT INTO "reminders"
//   ("remindersListID", "title")
// VALUES
//   (1, 'Get groceries'),
//   (2, 'Take a walk'),
//   (1, 'Get haircut')
// RETURNING "id"
// => Int

Reminder
  .where { $0.id == 2 }
  .update { $0.isCompleted.toggle() }
  .returning(\.self)
// UPDATE "reminders"
// SET "isCompleted" = (NOT "reminders"."isCompleted")
// WHERE "reminders"."id" = 2
// RETURNING "id", "isCompleted", "remindersListID", "title"
// => Reminder

We've been truly impressed with what parameter packs unlock in the language, especially when combined with other features.

Parameter packs are definitely still young and thorny. We’ve reported many issues in the process (and have many more we will report soon), and we have employed many workarounds in the library’s implementation, but we’re excited to see any future improvements.

Metatype key paths are a recent addition to Swift that have allowed us to implement a statically type-safe version of Ruby on Rails' "scopes." By defining static lets that describe queries, one can compose them into instances via dynamic member lookup:

extension Reminder {
  static let notDeleted = Self.where { $0.deletedAt.isNot(nil) }
}

Reminder
  .where(\.isCompleted)
  .notDeleted

And while these type-safe query building APIs are fun, our library also employs a #sql macro for building "safe SQL strings" that can be composed into a query builder, or executed on their own. The macro can do rudimentary linting on the SQL string’s syntax (e.g. check for balanced delimiters and no bindings in identifiers), and then use a custom string interpolation that prevents SQL injection.

We currently have just a single SQLite driver via SharingGRDB, which uses GRDB's database observation to create a SwiftData-like experience for SQL. But we're eager to push things to other databases (Postgres, MySQL) and database libraries!

Please check out the repo and documentation for more!

41 Likes

Thanks for sharing such a cool library! Great to see that something like this is possible.

Would you consider using some other SQLite driver? GRDB's lack of support for non-Darwin platforms has been a dealbreaker in a lot of contexts so far.

5 Likes

StructuredQueries should ideally work with any database/library, and we definitely welcome more drivers! We plan on looking into a PostgresNIO driver for our Swift server code sometime in the future.

We started with GRDB for our SharingGRDB library because it's a battle-tested, popular library with great support for database observation (which made creating a @FetchAll analogue to SwiftData's @Query macro a breeze), but if folks create drivers for other libraries to support things like non-Darwin platforms, we'll happily link them!

Somewhat off-topic but - are we misreporting GRDB's Linux compatibility?

https://swiftpackageindex.com/groue/GRDB.swift

Seems wrong in general, it supports all Apple platforms at least.

According to @Joannis_Orlandos it does work on Linux after all:

At the same time README.md of GRDB currently says

Note : Linux is not currently supported.

Correct, it's my understanding that it's not actively tested and therefore not supported.

2 Likes

Indeed the README makes no promise that cannot be kept in the current state of the repo.

Linux support is currently provided by contributors. There's no guarantee that it will hold, because what's needed for this guarantee was not provided. I'd be glad if this could get better. For a longer discussion, see How hard would Linux support be? · groue/GRDB.swift · Discussion #1359 · GitHub

The SPI report for Linux is correct. Darwin platforms have been reported incorrectly for a few years now (initially reported in Incorrect package data for groue/GRDB.swift · Issue #1890 · SwiftPackageIndex/SwiftPackageIndex-Server · GitHub). The "Has data race safety errors" report is incorrect as well (False positives reported in `Sema.NumSwift6Errors` via `-stats-output-dir` · Issue #79291 · swiftlang/swift · GitHub).

3 Likes

Thank you @Max_Desiatov, I agree that the wording was confusing, and quite a turn-off. I adjusted it as below:

Note : Linux support is provided by contributors. It is not automatically tested, and not officially maintained. If you notice a build or runtime failure on Linux, please open a pull request with the necessary fix, thank you!

7 Likes