diff --git a/Package.resolved b/Package.resolved index 51ff02b7..0c998039 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b6033fa4261d16bb8a2e8455921e171958017330c579e5ecb9ec44fe836d2099", + "originHash" : "64a0f70bd3bbae05e192502680e4627d94051e98f8d6895bb854e1fba892b8f7", "pins" : [ { "identity" : "combine-schedulers", @@ -10,6 +10,15 @@ "version" : "1.0.3" } }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "6989976265be3f8d2b5802c722f9ba168e227c71", + "version" : "1.7.2" + } + }, { "identity" : "swift-clocks", "kind" : "remoteSourceControl", @@ -43,7 +52,7 @@ "location" : "https://github.com/pointfreeco/swift-dependencies", "state" : { "revision" : "a10f9feeb214bc72b5337b6ef6d5a029360db4cc", - "version" : "1.9.5" + "version" : "1.10.0" } }, { @@ -91,6 +100,15 @@ "version" : "602.0.0" } }, + { + "identity" : "swift-tagged", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-tagged", + "state" : { + "revision" : "3907a9438f5b57d317001dc99f3f11b46882272b", + "version" : "0.10.0" + } + }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 568e1eee..6a59ed57 100644 --- a/Package.swift +++ b/Package.swift @@ -3,6 +3,12 @@ import CompilerPluginSupport import PackageDescription +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif + let package = Package( name: "swift-structured-queries", platforms: [ @@ -34,13 +40,17 @@ let package = Package( ), ], traits: [ + .trait( + name: "StructuredQueriesCasePaths", + description: "Introduce enum table support to StructuredQueries." + ), .trait( name: "StructuredQueriesTagged", - description: "Introduce StructuredQueries conformances to the swift-tagged package.", - enabledTraits: [] - ) + description: "Introduce StructuredQueries conformances to the swift-tagged package." + ), ], dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "1.0.0"), .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.3"), .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.8.1"), .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.6.3"), @@ -61,6 +71,11 @@ let package = Package( name: "StructuredQueriesCore", dependencies: [ .product(name: "IssueReporting", package: "xctest-dynamic-overlay"), + .product( + name: "CasePaths", + package: "swift-case-paths", + condition: .when(traits: ["StructuredQueriesCasePaths"]) + ), .product( name: "Tagged", package: "swift-tagged", @@ -141,6 +156,19 @@ let package = Package( swiftLanguageModes: [.v6] ) +// NB: For local testing in Xcode: +// if true { +if ProcessInfo.processInfo.environment["SPI_GENERATE_DOCS"] != nil { + package.traits.insert( + .default( + enabledTraits: [ + "StructuredQueriesCasePaths", + "StructuredQueriesTagged", + ] + ) + ) +} + let swiftSettings: [SwiftSetting] = [ .enableUpcomingFeature("MemberImportVisibility") // .unsafeFlags([ diff --git a/README.md b/README.md index e48f5ec3..b2139731 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ comfortable with the library: * [Getting started](https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore/gettingstarted) * [Defining your schema](https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore/definingyourschema) - * [Primary keyed tables](https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore/primarykeyedtables) + * [Primary-keyed tables](https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore/primarykeyedtables) * [Safe SQL strings](https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore/safesqlstrings) * [Query cookbook](https://swiftpackageindex.com/pointfreeco/swift-structured-queries/~/documentation/structuredqueriescore/querycookbook) @@ -174,19 +174,19 @@ As well as more comprehensive example usage: ## Demos There are a number of sample applications that demonstrate how to use StructuredQueries in the -[SharingGRDB](https://github.com/pointfreeco/sharing-grdb) repo. Check out -[this](https://github.com/pointfreeco/sharing-grdb/tree/main/Examples) directory to see them all, +[SQLiteData](https://github.com/pointfreeco/sqlite-data) repo. Check out +[this](https://github.com/pointfreeco/sqlite-data/tree/main/Examples) directory to see them all, including: - * [Case Studies](https://github.com/pointfreeco/sharing-grdb/tree/main/Examples/CaseStudies): + * [Case Studies](https://github.com/pointfreeco/sqlite-data/tree/main/Examples/CaseStudies): A number of case studies demonstrating the built-in features of the library. - * [Reminders](https://github.com/pointfreeco/sharing-grdb/tree/main/Examples/Reminders): A rebuild + * [Reminders](https://github.com/pointfreeco/sqlite-data/tree/main/Examples/Reminders): A rebuild of Apple's [Reminders][reminders-app-store] app that uses a SQLite database to model the reminders, lists and tags. It features many advanced queries, such as searching, and stats aggregation. - * [SyncUps](https://github.com/pointfreeco/sharing-grdb/tree/main/Examples/SyncUps): We also + * [SyncUps](https://github.com/pointfreeco/sqlite-data/tree/main/Examples/SyncUps): We also rebuilt Apple's [Scrumdinger][scrumdinger] demo application using modern, best practices for SwiftUI development, including using this library to query and persist state using SQLite. @@ -198,8 +198,8 @@ including: StructuredQueries is built with the goal of supporting any SQL database (SQLite, MySQL, Postgres, _etc._), but is currently tuned to work with SQLite. It currently has one official driver: - * [SharingGRDB](https://github.com/pointfreeco/sharing-grdb): A lightweight replacement for - SwiftData and the `@Query` macro. SharingGRDB includes `StructuredQueriesGRDB`, a library that + * [SQLiteData](https://github.com/pointfreeco/sqlite-data): A lightweight replacement for + SwiftData and the `@Query` macro. SQLiteData includes `StructuredQueriesGRDB`, a library that integrates this one with the popular [GRDB](https://github.com/groue/GRDB.swift) SQLite library. If you are interested in building a StructuredQueries integration for another database library, @@ -220,7 +220,7 @@ it's as simple as adding it to your `Package.swift`: ``` swift dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.17.0"), + .package(url: "https://github.com/pointfreeco/swift-structured-queries", from: "0.22.0"), ] ``` @@ -231,16 +231,21 @@ And then adding the product to any target that needs access to the library: ``` If you are on Swift 6.1 or greater, you can enable package traits that extend the library with -support for other libraries. For example, you can introduce type-safe identifiers to your tables -_via_ [Tagged](https://github.com/pointfreeco/swift-tagged) by enabling the -`StructuredQueriesTagged` trait: +support for other libraries: + + * `StructuredQueriesCasePaths`: Adds support for single-table inheritance _via_ "enum" tables by + leveraging the [CasePaths](https://github.com/pointfreeco/swift-case-paths) library. + + * `StructuredQueriesTagged`: Adds support for type-safe identifiers _via_ + the [Tagged](https://github.com/pointfreeco/swift-tagged) library. ```diff dependencies: [ .package( url: "https://github.com/pointfreeco/swift-structured-queries", - from: "0.17.0", + from: "0.22.0", + traits: [ ++ "StructuredQueriesCasePaths", + "StructuredQueriesTagged", + ] ), diff --git a/Sources/StructuredQueries/Documentation.docc/StructuredQueries.md b/Sources/StructuredQueries/Documentation.docc/StructuredQueries.md index 91116f71..65dfc05f 100644 --- a/Sources/StructuredQueries/Documentation.docc/StructuredQueries.md +++ b/Sources/StructuredQueries/Documentation.docc/StructuredQueries.md @@ -26,6 +26,7 @@ StructuredQueries also ships SQLite-specific helpers: - ``Table(_:)`` - ``Column(_:as:primaryKey:)`` +- ``Columns(primaryKey:)`` - ``Ephemeral()`` - ``Selection()`` - ``sql(_:as:)`` diff --git a/Sources/StructuredQueries/Macros.swift b/Sources/StructuredQueries/Macros.swift index e905ff93..f042e01f 100644 --- a/Sources/StructuredQueries/Macros.swift +++ b/Sources/StructuredQueries/Macros.swift @@ -2,8 +2,10 @@ import StructuredQueriesCore /// Defines and implements a conformance to the ``/StructuredQueriesCore/Table`` protocol. /// -/// - Parameter name: The table's name. Defaults to a lower-camel-case pluralization of the type, -/// _e.g._ `RemindersList` becomes `"remindersLists"`. +/// - Parameters +/// - name: The table's name. Defaults to a lower-camel-case pluralization of the type, +/// _e.g._ `RemindersList` becomes `"remindersLists"`. +/// - schemaName: The table's schema name. @attached( extension, conformances: Table, @@ -11,13 +13,14 @@ import StructuredQueriesCore PrimaryKeyedTable, names: named(From), named(columns), + named(_columnWidth), named(init(_:)), named(init(decoder:)), named(QueryValue), named(schemaName), named(tableName) ) -@attached(member, names: named(Draft), named(TableColumns)) +@attached(member, names: named(Draft), named(Selection), named(TableColumns)) @attached(memberAttribute) public macro Table( _ name: String = "", @@ -28,38 +31,7 @@ public macro Table( type: "TableMacro" ) -/// Customizes a column generated by the ``/StructuredQueriesCore/Table`` protocol. -/// -/// - Parameters: -/// - name: The column's name. Defaults to the property's name, _e.g._ 'id' becomes `"id"`. -/// - representableType: A type that represents the property type in a query expression. For types -/// that don't have a single representation in SQL, like `Date` and `UUID`. -/// - generated: Allows to declare the column as a read-only database computed column, making it -/// available for queries but not for updates. -/// - primaryKey: The column is its table's auto-incrementing primary key. -@attached(peer) -public macro Column( - _ name: String = "", - as representableType: (any QueryRepresentable.Type)? = nil, - generated: GeneratedColumnStorage? = nil, - primaryKey: Bool = false -) = - #externalMacro( - module: "StructuredQueriesMacros", - type: "ColumnMacro" - ) - -/// Tells StructuredQueries not to consider the annotated property a column of the table. -/// -/// Like SwiftData's `@Transient` macro, but for SQL. -@attached(peer) -public macro Ephemeral() = - #externalMacro( - module: "StructuredQueriesMacros", - type: "EphemeralMacro" - ) - -/// Defines the ability for a type to be selected from a query. +/// Defines a "selection" of columns that can be decoded from a query. /// /// When selecting tables and fields from a query, this data is bundled up into a tuple: /// @@ -87,19 +59,77 @@ public macro Ephemeral() = /// // [RemindersListWithReminderCount] /// ``` /// -/// The ``Table(_:)`` and `@Selection` macros can be composed together to describe a virtual table -/// or common table expression. +/// > Tip: `@Selection`s can also be used to build up common table expressions. +/// +/// - Parameter name: The selection's name, _i.e._ for a common table expression. Defaults to a +/// lower-camel-case pluralization of the type, _e.g._ `RemindersList` becomes `"remindersLists"`. @attached( extension, conformances: _Selection, - names: named(Columns), - named(init(decoder:)) + Table, + PartialSelectStatement, + PrimaryKeyedTable, + names: named(From), + named(columns), + named(_columnWidth), + named(init(_:)), + named(init(decoder:)), + named(QueryValue), + named(schemaName), + named(tableName) ) -@attached(member, names: named(Columns)) -public macro Selection() = +@attached(member, names: named(Draft), named(Selection), named(TableColumns)) +@attached(memberAttribute) +public macro Selection( + _ name: String = "" +) = + #externalMacro( + module: "StructuredQueriesMacros", + type: "TableMacro" + ) + +/// Customizes a column generated by the ``/StructuredQueriesCore/Table`` protocol. +/// +/// - Parameters: +/// - name: The column's name. Defaults to the property's name, _e.g._ 'id' becomes `"id"`. +/// - representableType: A type that represents the property type in a query expression. For types +/// that don't have a single representation in SQL, like `Date` and `UUID`. +/// - generated: Allows to declare the column as a read-only database computed column, making it +/// available for queries but not for updates. +/// - primaryKey: The column is its table's primary key. +@attached(peer) +public macro Column( + _ name: String = "", + as representableType: (any QueryRepresentable.Type)? = nil, + generated: GeneratedColumnStorage? = nil, + primaryKey: Bool = false +) = + #externalMacro( + module: "StructuredQueriesMacros", + type: "ColumnMacro" + ) + +/// Customizes a group of columns generated by the ``/StructuredQueriesCore/Table`` protocol. +/// +/// - Parameters primaryKey: These columns are the table's composite primary key. +@attached(peer) +public macro Columns( + // as representableType: (any QueryRepresentable.Type)? = nil, + primaryKey: Bool = false +) = #externalMacro( module: "StructuredQueriesMacros", - type: "SelectionMacro" + type: "ColumnsMacro" + ) + +/// Tells StructuredQueries not to consider the annotated property a column of the table. +/// +/// Like SwiftData's `@Transient` macro, but for SQL. +@attached(peer) +public macro Ephemeral() = + #externalMacro( + module: "StructuredQueriesMacros", + type: "EphemeralMacro" ) /// Explicitly bind a value to a query. diff --git a/Sources/StructuredQueriesCore/Bind.swift b/Sources/StructuredQueriesCore/Bind.swift index 1d30e6b4..9675c791 100644 --- a/Sources/StructuredQueriesCore/Bind.swift +++ b/Sources/StructuredQueriesCore/Bind.swift @@ -2,7 +2,8 @@ /// /// It is not common to interact with this type directly. A value of this type is returned from the /// `#bind` macro. -public struct BindQueryExpression: QueryExpression { +public struct BindQueryExpression: QueryExpression +{ public let base: QueryValue public init( diff --git a/Sources/StructuredQueriesCore/ColumnGroup.swift b/Sources/StructuredQueriesCore/ColumnGroup.swift new file mode 100644 index 00000000..2c182759 --- /dev/null +++ b/Sources/StructuredQueriesCore/ColumnGroup.swift @@ -0,0 +1,90 @@ +/// A group of table columns. +/// +/// Don't create instances of this value directly. Instead, use the `@Table` and `@Columns` macros +/// to generate values of this type. +@dynamicMemberLookup +public struct ColumnGroup: _TableColumnExpression +where Values.QueryOutput == Values { + public typealias Value = Values + + public var _names: [String] { Values.TableColumns.allColumns.map(\.name) } + + public typealias QueryValue = Values + + public let keyPath: KeyPath + + public init(keyPath: KeyPath) { + self.keyPath = keyPath + } + + public var queryFragment: QueryFragment { + _allColumns.map(\.queryFragment).joined(separator: ", ") + } + + public subscript( + dynamicMember keyPath: KeyPath> + ) -> TableColumn { + let column = Values.columns[keyPath: keyPath] + return TableColumn( + column.name, + keyPath: self.keyPath.appending(path: column.keyPath), + default: column.defaultValue + ) + } + + public subscript( + dynamicMember keyPath: KeyPath> + ) -> GeneratedColumn { + let column = Values.columns[keyPath: keyPath] + return GeneratedColumn( + column.name, + keyPath: self.keyPath.appending(path: column.keyPath), + default: column.defaultValue + ) + } + + public subscript( + dynamicMember keyPath: KeyPath> + ) -> ColumnGroup { + let column = Values.columns[keyPath: keyPath] + return ColumnGroup( + keyPath: self.keyPath.appending(path: column.keyPath) + ) + } + + public var _allColumns: [any TableColumnExpression] { + Values.TableColumns.allColumns.map { column in + func open( + _ column: some TableColumnExpression + ) -> any TableColumnExpression { + let keyPath = keyPath.appending( + path: unsafeDowncast(column.keyPath, to: KeyPath.self) + ) + return TableColumn( + column.name, + keyPath: keyPath, + default: column.defaultValue + ) + } + return open(column) + } + } + + public var _writableColumns: [any WritableTableColumnExpression] { + Values.TableColumns.writableColumns.map { column in + func open( + _ column: some WritableTableColumnExpression + ) -> any WritableTableColumnExpression { + let keyPath = keyPath.appending( + path: unsafeDowncast(column.keyPath, to: KeyPath.self) + ) + return TableColumn( + column.name, + keyPath: keyPath, + default: column.defaultValue + ) + } + return open(column) + } + } +} diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/CommonTableExpressions.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/CommonTableExpressions.md index 7661ddd0..8e347b1d 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/CommonTableExpressions.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/CommonTableExpressions.md @@ -6,9 +6,9 @@ or recursive queries of trees and graphs. ## Overview One can build [common table expressions][] (commonly referred to as CTEs) by using the ``With`` -statement, along with the `@Table` and `@Selection` macros. CTEs allow you to refactor complex -queries into smaller pieces, and they allow you to execute recursive queries that can traverse -tree-like and graph-like tables. +statement, along with the `@Selection` macro. CTEs allow you to refactor complex queries into +smaller pieces, and they allow you to execute recursive queries that can traverse tree-like and +graph-like tables. [common table expressions]: https://sqlite.org/lang_with.html @@ -47,15 +47,17 @@ selecting only lists whose average priority of reminders is greater than 1.5 int that can then be used in another query. One can define a new type that represents the data you want to pre-compute as a CTE, and you -annotate the type with both `@Table` and `@Selection`: +annotate the type with `@Selection`: ```swift -@Table @Selection +@Selection struct HighPriorityRemindersList { let id: RemindersList.ID } ``` +You can think of this as a "virtual table" of sorts, rather than an actual database table. + Then you can select into this table in the first trailing closure of ``With``: ```swift @@ -171,7 +173,7 @@ As a simple example let's construct a query that selects the numbers from 1 to 1 by defining a CTE data type that holds the data we want to compute: ```swift -@Table @Selection +@Selection struct Counts { let value: Int } @@ -263,13 +265,13 @@ This constructs the CTE table from which we can select and limit to the first 10 The previous query was a simple example of what is known as a "[recurrence relation][]." Another example of a recurrence relation is the Fibonacci sequence, where each term in the sequence is the sum of the previous two terms. We can construct a query to compute the -first 10 Fibonacci numbers by first defining a table selection data type that holds an index -and its corresponding Fibonacci number, as well as the previous Fibonacci number: +first 10 Fibonacci numbers by first defining a data type that holds an index and its corresponding +Fibonacci number, as well as the previous Fibonacci number: [recurrence relation]: https://en.wikipedia.org/wiki/Recurrence_relation ```swift -@Table @Selection +@Selection private struct Fibonacci { let n: Int let prevFib: Int diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/DefiningYourSchema.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/DefiningYourSchema.md index df715599..b385fb7f 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/DefiningYourSchema.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/DefiningYourSchema.md @@ -22,9 +22,11 @@ that represent those database definitions. * [RawRepresentable](#RawRepresentable) * [JSON](#JSON) * [Tagged identifiers](#Tagged-identifiers) -* [Primary keyed tables](#Primary-keyed-tables) +* [Primary-keyed tables](#Primary-keyed-tables) +* [Grouped columns](#Grouped-columns) * [Ephemeral columns](#Ephemeral-columns) * [Generated columns](#Generated-columns) +* [Enum tables](#Enum-tables) * [Table definition tools](#Table-definition-tools) ### Defining a table @@ -270,7 +272,7 @@ To enable the trait, specify it in the Package.swift file that depends on Struct ```diff .package( url: "https://github.com/pointfreeco/swift-structured-queries", - from: "0.17.0", + from: "0.22.0", + traits: ["StructuredQueriesTagged"] ), ``` @@ -319,15 +321,16 @@ struct RemindersList: Identifiable { } ``` -### Primary keyed tables +### Primary-keyed tables -It is possible to let the `@Table` macro know which property of your data type is the primary +It is possible to let the `@Table` macro know which field of your data type is the primary key for the table in the database, and doing so unlocks new APIs for inserting, updating, and deleting records. By default the `@Table` macro will assume any property named `id` is the primary key, or you can explicitly specify it with the `primaryKey:` argument of the `@Column` macro: ```swift +@Table struct Book { @Column(primaryKey: true) let isbn: String @@ -345,6 +348,95 @@ var id: String See for more information on tables with primary keys. +### Grouped columns + +It is possible to group many related columns into a single data type, which helps with organization +and reusing little bits of schema amongst many tables. For example, suppose many tables in your +database schema have `createdAt: Date` and `updatedAt: Date?` timestamps. You can choose to group +those columns into a dedicate data type, annotated with the `@Selection` macro: + +```swift +@Selection +struct Timestamps { + let createdAt: Date + let updatedAt: Date? +} +``` + +And then you can use `Timestamps` in tables as if it was just a single column: + +```swift +@Table +struct RemindersList { + let id: Int + var name = "" + let timestamps: Timestamps +} + +@Table +struct Reminder { + let id: Int + var name = "" + var isCompleted = false + let timestamps: Timestamps +} +``` + +> Important: Since SQLite has no concept of grouped columns you must remember to flatten all +> groupings into a single list when defining your table's schema. For example, the "CREATE TABLE" +> statement for the `RemindersList` above would look like this: +> +> ```sql +> CREATE TABLE "remindersLists" ( +> "id" INTEGER PRIMARY KEY, +> "name" TEXT NOT NULL, +> "isCompleted" INTEGER NOT NULL, +> "createdAt" TEXT NOT NULL, +> "updatedAt" TEXT +> ) STRICT +> ``` + +You can construct queries that access fields inside column groups using regular dot-syntax: + +@Row { + @Column { + ```swift + RemindersList + .where { $0.timestamps.createdAt <= date } + ``` + } + @Column { + ```sql + SELECT "id", "title", "createdAt", "updatedAt" + FROM "remindersLists" + WHERE "createdAt" <= ? + ``` + } +} + +You can even compare the `timestamps` field directly and its columns will be flattened into a +tuple in SQL: + +@Row { + @Column { + ```swift + RemindersList + .where { + $0.timestamps <= Timestamps(createdAt: date1, updatedAt: date2) + } + ``` + } + @Column { + ```sql + SELECT "id", "title", "createdAt", "updatedAt" + FROM "remindersLists" + WHERE ("createdAt", "updatedAt") <= (?, ?) + ``` + } +} + +That allows you to query against all columns of a grouping at once. + ### Ephemeral columns It is possible to store properties in a Swift data type that has no corresponding column in your SQL @@ -352,6 +444,7 @@ database. Such properties must have a default value, and can be specified using macro: ```swift +@Table struct Book { @Column(primaryKey: true) let isbn: String @@ -386,6 +479,228 @@ struct Event { } ``` +### Enum tables + +It is possible to use enums as a domain modeling tool for your table schema, which can help you +emulate "inheritance" for your tables without having the burden of using reference types. + +As an example, suppose you have a table that represents attachments that can be associated with +other tables, and an attachment can either be a link, a note or an image. One way to model this +is a struct to represent the attachment that holds onto an enum for the different kinds of +attachments supported, annotated with the `@Selection` macro: + +```swift +@Table struct Attachment { + let id: Int + let kind: Kind + + @CasePathable @Selection + enum Kind { + case link(URL) + case note(String) + case image(URL) + } +} +``` + +> Important: It is required to apply the `@CasePathable` macro in order to define columns from an +> enum. This macro comes from our [Case Paths] library and is automatically included with the +> library when the `StructuredQueriesCasePaths` trait is enabled. + +[Case Paths]: http://github.com/pointfreeco/swift-case-paths + +To create a SQL table that represents this data type you simply flatten all of the fields into +a single list of columns where each column is nullable: + +```sql +CREATE TABLE "attachments" ( + "id" INTEGER PRIMARY KEY, + "link" TEXT, + "note" TEXT, + "image" TEXT +) STRICT +``` + +With that defined you can query the table much like a regular table. For example, a simple +`Attachment.all` selects all columns, and when decoding the data from the database it will +be decided which case of the `Kind` enum is chosen: + +@Row { + @Column { + ```swift + Attachment.all + ``` + } + @Column { + ```sql + SELECT + "attachments"."id", + "attachments"."link", + "attachments"."note", + "attachments"."image" + FROM "attachments" + ``` + } +} + +You can also use `where` clauses to filter attachments by their kind, such as selecting images +only: + +@Row { + @Column { + ```swift + Attachment.where { $0.kind.image.isNot(nil) } + ``` + } + @Column { + ```sql + SELECT + "attachments"."id", + "attachments"."link", + "attachments"."note", + "attachments"."image" + FROM "attachments" + WHERE "attachments"."image" IS NOT NULL + ``` + } +} + +You can insert attachments into the database in the usual way: + +@Row { + @Column { + ```swift + Attachment.insert { + Attachment.Draft(kind: .note("Hello world!")) + } + ``` + } + @Column { + ```sql + INSERT INTO "attachments" + ("id", "link", "note", "image") + VALUES + (NULL, NULL, 'Hello world!', NULL) + ``` + } +} + +Notice that `NULL` is inserted for `link` and `image` since we are inserting an attachment +with the `note` case. + +And further, you can update attachments in the database in the usual way: + +@Row { + @Column { + ```swift + Attachment.update { + $0.kind = .note("Goodbye world!") + } + ``` + } + @Column { + ```sql + UPDATE "attachments" + SET + "link" = NULL, + "note" = 'Goodbye world!', + "image" = NULL + ``` + } +} + +Note that `link` and `image` are explicitly set to `NULL` since we are setting the kind of +the attachment to `note`. + +It is also possible to group many columns together for a case of an enum. For example, suppose +the image not only had a URL but also had a caption. Then a dedicated `@Selection` type +can be defined for that data and used in the `image` case: + +```swift +@Table struct Attachment { + let id: Int + let kind: Kind + + @CasePathable @Selection + enum Kind { + case link(URL) + case note(String) + case image(Attachment.Image) + } + @Selection + struct Image { + var caption = "" + var url: URL + } +} +``` + +> Note: Due to how macros expand it is necessary to fully qualify nested types, e.g. +> `case image(Attachment.Image)`. + +To create a SQL table that represents this data type you again must flatten all columns into a +single list of nullable columns: + +```sql +CREATE TABLE "attachments" ( + "id" INTEGER PRIMARY KEY, + "link" TEXT, + "note" TEXT, + "caption" TEXT, + "url" TEXT +) STRICT +``` +These tools allow you to emulate what is known as "single table inheritance", where you model +a class inheritance heirarchy of models as a single wide table that has columns for each +model. This allows you to share bits of data and logic amongst many models in a way that still +plays nicely with SQLite. + +SwiftData supports this kind of data modeling, but they force you to use reference +types instead of value types, you lose exhaustivity for the types of models supported, and +it's a lot more verbose: + +```swift +@available(iOS 26, *) +@Model class Attachment { + var isActive: Bool + init(isActive: Bool = false) { self.isActive = isActive } +} + +@available(iOS 26, *) +@Model class Link: Attachment { + var url: URL + init(url: URL, isActive: Bool = false) { + self.url = url + super.init(isActive: isActive) + } +} + +@available(iOS 26, *) +@Model class Note: Attachment { + var note: String + init(note: String, isActive: Bool = false) { + self.note = note + super.init(isActive: isActive) + } +} + +@available(iOS 26, *) +@Model class Image: Attachment { + var url: URL + init(url: URL, isActive: Bool = false) { + self.url = url + super.init(isActive: isActive) + } +} +``` + +> Note: The `@available(iOS 26, *)` attributes are required even if targeting iOS 26+, and +> the explicit initializers are required and must accept all arguments from all parent +> classes and pass that to `super.init`. + +Enums provide an alternative to this approach that embraces value types, is more concise, and +more powerful. + ### Table definition tools This library does not come with any tools for actually constructing table definition queries, diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/GettingStarted.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/GettingStarted.md index 60080f1e..ccc6cb8a 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/GettingStarted.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/GettingStarted.md @@ -766,8 +766,8 @@ struct ReminderResult: QueryRepresentable { ) ``` -There is also a way to streamline providing the ``QueryRepresentable`` conformance. You can apply the -`@Selection` macro to your type to generate that conformance for you automatically: +There is also a way to streamline providing the ``QueryRepresentable`` conformance. You can use +`@Selection` to describe the datatype you want to decode: ```swift @Selection diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/InsertStatements.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/InsertStatements.md index 1a8c1792..310baad6 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/InsertStatements.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/InsertStatements.md @@ -293,7 +293,7 @@ Or you can populate an entire record from the freshly-inserted database: ### Upserting drafts At times your application may want to provide the same business logic for creating a new record and -editing an existing one. Your primary keyed table's `Draft` type can be used for these kinds of +editing an existing one. Your primary-keyed table's `Draft` type can be used for these kinds of flows, and it is possible to create a draft from an existing value using ``TableDraft/init(_:)``: ```swift diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Integration.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Integration.md index c020ca21..5ec63447 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/Integration.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/Integration.md @@ -9,15 +9,15 @@ Since this library focuses only on the building of type-safe queries, it does no actually execute the query in a particular database. The library is built with the goal of being able to support any database (SQLite, MySQL, Postgres, _etc._), but currently is primarily tuned to work with SQLite. And further, it only comes with one official driver for SQLite, which is the -[SharingGRDB][sharing-grdb-gh] library that uses the popular [GRDB][grdb-gh] library under the hood -for interacting with SQLite. +[SQLiteData] library that uses the popular [GRDB] library under the hood for interacting with +SQLite. If you are interested in building an integration of StructuredQueries with another database library, please [start a discussion][sq-discussions] and let us know of any challenges you encounter. -[sharing-grdb-gh]: http://github.com/pointfreeco/sharing-grdb -[grdb-gh]: http://github.com/groue/GRDB.swift +[SQLiteData]: http://github.com/pointfreeco/sqlite-data +[GRDB]: http://github.com/groue/GRDB.swift [sq-discussions]: http://github.com/pointfreeco/swift-structured-queries/discussions/new/choose ### Case Study: SQLite @@ -37,14 +37,14 @@ even integrate StructuredQueries into a 3rd party SQLite library. ### Case Study: GRDB We provide one first-party library that integrates StructuredQueries into SQLite, and that is in -our [SharingGRDB][sharing-grdb-gh] library, which is a lightweight replacement for SwiftData and +our [SQLiteData] library, which is a lightweight replacement for SwiftData and the `@Query` macro. It brings a suite of tools allow you to fetch and observe data from a database in your feature code, and views automatically update when data in the database changes. -The integration of StructuredQueries into SharingGRDB works in the manner outlined above, in -. The code can be found [here][sq-sharing-grdb], where you will +The integration of StructuredQueries into SQLiteData works in the manner outlined above, in +. The code can be found [here][sq-sqlite-data], where you will find a ``QueryDecoder`` conformance, as well as some helper methods for fetching data using StructuredQueries and GRDB. -[sharing-grdb-gh]: http://github.com/pointfreeco/sharing-grdb -[sq-sharing-grdb]: https://github.com/pointfreeco/sharing-grdb/tree/main/Sources/StructuredQueriesGRDBCore +[SQLiteData: http://github.com/pointfreeco/sqlite-data +[sq-sqlite-data]: https://github.com/pointfreeco/sqlite-data/tree/main/Sources/StructuredQueriesGRDBCore diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.16.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.16.md index be28d410..b4f1dffe 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.16.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.16.md @@ -26,10 +26,10 @@ Reminder.select { $exclaim($0.title) } ``` For the query to successfully execute, you must also add the function to your SQLite database -connection. This can be done in [SharingGRDB] (0.7.0+) using the `Database.add(function:)` method, -_e.g._ when you first configure things: +connection. This can be done in [SQLiteData] using the `Database.add(function:)` method, _e.g._ when +you first configure things: -[SharingGRDB]: https://github.com/pointfreeco/sharing-grdb +[SQLiteData]: https://github.com/pointfreeco/sqlite-data ```swift var configuration = Configuration() @@ -61,23 +61,3 @@ func jsonArrayExclaim(_ strings: [String]) -> [String] { strings.map { $0.localizedUppercase + "!" } } ``` - -### Breaking change: user-defined representations - -To power things, a new initializer, ``QueryBindable/init(queryBinding:)``, was added to the -``QueryBindable`` protocol. While most code should continue to compile, if you define your own -query representations that conform to ``QueryRepresentable``, you will need to define this -initializer upon upgrading. - -For example, `JSONRepresentation` added the following initializer: - -```swift -public init?(queryBinding: QueryBinding) { - guard case .text(let json) = queryBinding else { return nil } - guard let queryOutput = try? jsonDecoder.decode( - QueryOutput.self, from: Data(json.utf8) - ) - else { return nil } - self.init(queryOutput: queryOutput) -} -``` diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.18.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.18.md index 859fa4bd..61907c0b 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.18.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/MigrationGuides/MigratingTo0.18.md @@ -8,7 +8,7 @@ StructuredQueries recently introduced a new module, StructuredQueriesSQLite, to SQLite-specific helpers, and in 0.18 it has migrated many of its existing SQLite helpers into this module. -If you are using SharingGRDB this migration should be mostly transparent, but if you are using +If you are using SQLiteData this migration should be mostly transparent, but if you are using StructuredQueries directly and need access to these helpers, you must now explicitly import this helper module: diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/PrimaryKeyedTables.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/PrimaryKeyedTables.md index b25463e3..dfaf77ce 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/PrimaryKeyedTables.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/PrimaryKeyedTables.md @@ -1,11 +1,11 @@ -# Primary keyed tables +# Primary-keyed tables Learn how tables with a primary key get extra tools when it comes to inserting, updating, and deleting records. ## Overview -A primary keyed table is one that has a column whose value is unique for the entire table. The most +A primary-keyed table is one that has a column whose value is unique for the entire table. The most common example is an "id" column that holds an integer, UUID, or some other kind of identifier. Typically such columns are also initialized by the database so that when inserting rows into the table you do not need to specify the primary key. The library provides extra tools that make it @@ -126,7 +126,7 @@ Or even get back the entire newly inserted row: } At times your application may want to provide the same business logic for creating a new record and -editing an existing one. Your primary keyed table's `Draft` type can be used for these kinds of +editing an existing one. Your primary-keyed table's `Draft` type can be used for these kinds of flows, and it is possible to create a draft from an existing value using ``TableDraft/init(_:)``: ```swift @@ -141,10 +141,10 @@ ReminderForm( ) ``` -### Selects, updates, upserts and deletions +### Selects, updates, upserts, and deletions -Primary keyed tables are also given special APIs for selecting, updating and deleting existing rows -in the table based on their primary key. For example, every primary keyed table is given a special +Primary-keyed tables are also given special APIs for selecting, updating and deleting existing rows +in the table based on their primary key. For example, every primary-keyed table is given a special ``PrimaryKeyedTable/find(_:)`` static method for fetching a record by its primary key: @Row { diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Articles/QueryCookbook.md b/Sources/StructuredQueriesCore/Documentation.docc/Articles/QueryCookbook.md index 9b2e83af..85ffa293 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Articles/QueryCookbook.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Articles/QueryCookbook.md @@ -261,9 +261,9 @@ you can use the ``Table/unscoped`` property: It will often be the case that you want to select very specific data from your database and then decode that data into a custom Swift data type. For example, if you are displaying a list of -reminders and only need their titles for the list, it would be wasteful to decode an array of -all reminder data. The `@Selection` macro allows you to define a custom data type of only the -fields you want to decode: +reminders and only need their titles for the list, it would be wasteful to decode an array of all +reminder data. The `@Selection` macro allows you to define a custom data type of only the fields you +want to decode: ```swift @Selection @@ -297,7 +297,7 @@ Then when selecting the columns for your query you can use this data type: } As another example, consider the query that selects all reminders lists with the count of reminders -in each list. A selection data type can be defined like so: +in each list. A data type can be defined like so: ```swift @Selection diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Extensions/PrimaryKeyedTable.md b/Sources/StructuredQueriesCore/Documentation.docc/Extensions/PrimaryKeyedTable.md index 4f9fa0f7..1254e9f9 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Extensions/PrimaryKeyedTable.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Extensions/PrimaryKeyedTable.md @@ -1,6 +1,6 @@ # ``StructuredQueriesCore/PrimaryKeyedTable`` -A primary keyed table is one that has a column whose value is unique for the entire table. The most +A primary-keyed table is one that has a column whose value is unique for the entire table. The most common example is an "id" column that holds an integer, UUID, or some other kind of identifier. Typically such columns are also initialized by the database so that when inserting rows into the table you do not need to specify the primary key. The library provides extra tools that make it @@ -25,7 +25,7 @@ struct Book { > Note: Using `primaryKey: true` does not create any kind of constraints on your table > automatically. It is up to you to actually create this table and designate the column as the -> primary key. +> primary key in its table definition. The `@Table` macro will also automatically infer a field named `id` as a primary key, and so it is not necessary to use the `@Column` macro in that case: @@ -39,7 +39,23 @@ struct Reminder { } ``` -> Note: At most one column can be designated as a primary key. +To define a composite primary key, group them together into a `@Selection` type and annotate the +field with the `@Columns` macro: + +```swift +@Table +struct Enrollment { + @Selection + struct ID { + var courseID: CourseID + var studentID: StudentID + } + + // Automatically inferred as '@Columns(primaryKey: True) + let id: ID + // ... +} +``` ### Drafts @@ -101,7 +117,7 @@ Reminder ### Updates and deletions -Primary keyed tables are also given special APIs for updating and deleting existing rows in the +Primary-keyed tables are also given special APIs for updating and deleting existing rows in the table based on their primary key. For example, the ``PrimaryKeyedTable/update(_:)`` method allows one to update all the fields of a row with the corresponding primary key: diff --git a/Sources/StructuredQueriesCore/Documentation.docc/Extensions/Table.md b/Sources/StructuredQueriesCore/Documentation.docc/Extensions/Table.md index b53cb23b..55064a7c 100644 --- a/Sources/StructuredQueriesCore/Documentation.docc/Extensions/Table.md +++ b/Sources/StructuredQueriesCore/Documentation.docc/Extensions/Table.md @@ -31,7 +31,9 @@ - ``TableColumns`` - ``TableColumn`` - ``TableColumnExpression`` +- ``ColumnGroup`` - ``TableDefinition`` +- ``TableExpression`` ### Scoping diff --git a/Sources/StructuredQueriesCore/Never.swift b/Sources/StructuredQueriesCore/Never.swift index 6ab63792..971af9cb 100644 --- a/Sources/StructuredQueriesCore/Never.swift +++ b/Sources/StructuredQueriesCore/Never.swift @@ -7,6 +7,12 @@ extension Never: Table { public static var writableColumns: [any WritableTableColumnExpression] { [] } } + public struct Selection: TableExpression { + public typealias QueryValue = Never + + public var allColumns: [any QueryExpression] { [] } + } + public static var columns: TableColumns { TableColumns() } diff --git a/Sources/StructuredQueriesCore/Operators.swift b/Sources/StructuredQueriesCore/Operators.swift index 779ac78e..e6793bb0 100644 --- a/Sources/StructuredQueriesCore/Operators.swift +++ b/Sources/StructuredQueriesCore/Operators.swift @@ -1,4 +1,4 @@ -extension QueryExpression where QueryValue: QueryBindable { +extension QueryExpression where QueryValue: QueryRepresentable { /// A predicate expression indicating whether two query expressions are equal. /// /// ```swift @@ -77,9 +77,10 @@ extension QueryExpression where QueryValue: QueryBindable { /// /// - Parameter other: An expression to compare this one to. /// - Returns: A predicate expression. - public func `is`( - _ other: some QueryExpression - ) -> some QueryExpression { + public func `is`( + _ other: some QueryExpression + ) -> some QueryExpression + where QueryValue._Optionalized.Wrapped == Other._Optionalized.Wrapped { BinaryOperator(lhs: self, operator: "IS", rhs: other) } @@ -93,9 +94,10 @@ extension QueryExpression where QueryValue: QueryBindable { /// /// - Parameter other: An expression to compare this one to. /// - Returns: A predicate expression. - public func isNot( + public func isNot( _ other: some QueryExpression - ) -> some QueryExpression { + ) -> some QueryExpression + where QueryValue._Optionalized.Wrapped == Other._Optionalized.Wrapped { BinaryOperator(lhs: self, operator: "IS NOT", rhs: other) } } @@ -104,7 +106,35 @@ private func isNull(_ expression: some QueryExpression) -> Bool { (expression as? any _OptionalProtocol).map { $0._wrapped == nil } ?? false } -extension QueryExpression where QueryValue: QueryBindable & _OptionalProtocol { +extension QueryExpression where QueryValue: QueryRepresentable & QueryExpression { + @_documentation(visibility: private) + public func `is`( + _ other: _Null + ) -> some QueryExpression { + BinaryOperator(lhs: self, operator: "IS", rhs: other) + } + + @_documentation(visibility: private) + public func isNot( + _ other: _Null + ) -> some QueryExpression { + BinaryOperator(lhs: self, operator: "IS NOT", rhs: other) + } +} + +public struct _Null: QueryExpression { + public typealias QueryValue = Wrapped? + public var queryFragment: QueryFragment { + Wrapped?.none.queryFragment + } +} + +extension _Null: ExpressibleByNilLiteral { + public init(nilLiteral: ()) {} +} + +// TODO: Remove this when we correctly unwrap `TableColumns` in `join` conditions. +extension QueryExpression where QueryValue: QueryRepresentable & _OptionalProtocol { @_documentation(visibility: private) public func eq(_ other: some QueryExpression) -> some QueryExpression { BinaryOperator(lhs: self, operator: "=", rhs: other) @@ -140,31 +170,6 @@ extension QueryExpression where QueryValue: QueryBindable & _OptionalProtocol { } } -extension QueryExpression where QueryValue: QueryBindable { - @_documentation(visibility: private) - public func `is`( - _ other: _Null - ) -> some QueryExpression { - BinaryOperator(lhs: self, operator: "IS", rhs: other) - } - - @_documentation(visibility: private) - public func isNot( - _ other: _Null - ) -> some QueryExpression { - BinaryOperator(lhs: self, operator: "IS NOT", rhs: other) - } -} - -public struct _Null: QueryExpression { - public typealias QueryValue = Wrapped? - public var queryFragment: QueryFragment { "NULL" } -} - -extension _Null: ExpressibleByNilLiteral { - public init(nilLiteral: ()) {} -} - // NB: This overload is required due to an overload resolution bug of 'Updates[dynamicMember:]'. @_disfavoredOverload @_documentation(visibility: private) @@ -241,7 +246,7 @@ public func != ( SQLQueryExpression(lhs).isNot(rhs) } -extension QueryExpression where QueryValue: QueryBindable { +extension QueryExpression where QueryValue: _OptionalPromotable { /// Returns a predicate expression indicating whether the value of the first expression is less /// than that of the second expression. /// @@ -697,7 +702,7 @@ extension QueryExpression where QueryValue == String { /// - Parameter collation: A collating sequence name. /// - Returns: An expression that is compared using the given collating sequence. public func collate(_ collation: Collation) -> some QueryExpression { - BinaryOperator(lhs: self, operator: "COLLATE", rhs: collation) + SQLQueryExpression("\(self) COLLATE \(collation)") } /// A predicate expression from this string expression matched against another _via_ the `GLOB` @@ -816,7 +821,7 @@ extension SQLQueryExpression { } } -extension QueryExpression where QueryValue: QueryBindable { +extension QueryExpression where QueryValue: QueryExpression { /// Returns a predicate expression indicating whether the expression is in a sequence. /// /// - Parameter expression: A sequence of expressions. @@ -850,11 +855,7 @@ extension QueryExpression where QueryValue: QueryBindable { _ lowerBound: some QueryExpression, and upperBound: some QueryExpression ) -> some QueryExpression { - BinaryOperator( - lhs: self, - operator: "BETWEEN", - rhs: SQLQueryExpression("\(lowerBound) AND \(upperBound)") - ) + SQLQueryExpression("\(self) BETWEEN \(lowerBound) AND \(upperBound)") } } @@ -952,7 +953,7 @@ struct BinaryOperator: QueryExpression { } var queryFragment: QueryFragment { - "(\(lhs) \(`operator`) \(rhs))" + "(\(lhs)) \(`operator`) (\(rhs))" } } @@ -976,15 +977,15 @@ private struct LikeOperator< } } -extension Sequence where Element: QueryExpression, Element.QueryValue: QueryBindable { +extension Sequence where Element: QueryExpression, Element.QueryValue: QueryExpression { fileprivate typealias Expression = _SequenceExpression } private struct _SequenceExpression: QueryExpression -where S.Element: QueryExpression, S.Element.QueryValue: QueryBindable { +where S.Element: QueryExpression, S.Element.QueryValue: QueryExpression { typealias QueryValue = S let queryFragment: QueryFragment init(elements: S) { - queryFragment = "(\(elements.map(\.queryFragment).joined(separator: ", ")))" + queryFragment = elements.map { "(\($0.queryFragment))" }.joined(separator: ", ") } } diff --git a/Sources/StructuredQueriesCore/Optional.swift b/Sources/StructuredQueriesCore/Optional.swift index af025eb6..0d084333 100644 --- a/Sources/StructuredQueriesCore/Optional.swift +++ b/Sources/StructuredQueriesCore/Optional.swift @@ -27,10 +27,6 @@ extension Optional: QueryBindable where Wrapped: QueryBindable { public var queryBinding: QueryBinding { self?.queryBinding ?? .null } - - public init?(queryBinding: QueryBinding) { - self = Wrapped(queryBinding: queryBinding) - } } extension Optional: QueryDecodable where Wrapped: QueryDecodable { @@ -48,7 +44,19 @@ extension Optional: QueryExpression where Wrapped: QueryExpression { public typealias QueryValue = Wrapped.QueryValue? public var queryFragment: QueryFragment { - self?.queryFragment ?? "NULL" + self._allColumns.map(\.queryFragment).joined(separator: ", ") + } + + public static var _columnWidth: Int { + Wrapped._columnWidth + } + + public var _allColumns: [any QueryExpression] { + self?._allColumns + ?? Array( + repeating: SQLQueryExpression("NULL") as any QueryExpression, + count: Self._columnWidth + ) } } @@ -70,7 +78,7 @@ extension Optional: QueryRepresentable where Wrapped: QueryRepresentable { } } -extension Optional: Table where Wrapped: Table { +extension Optional: Table, PartialSelectStatement, Statement where Wrapped: Table { public static var tableName: String { Wrapped.tableName } @@ -95,11 +103,39 @@ extension Optional: Table where Wrapped: Table { public typealias QueryValue = Optional public static var allColumns: [any TableColumnExpression] { - Wrapped.TableColumns.allColumns + func open( + _ column: some TableColumnExpression + ) -> any TableColumnExpression { + guard let column = column as? TableColumn + else { + let column = column as! GeneratedColumn + return GeneratedColumn( + column.name, + keyPath: \.[member: \Value.self, column: column.keyPath], + default: column.defaultValue + ) + } + return TableColumn( + column.name, + keyPath: \.[member: \Value.self, column: column.keyPath], + default: column.defaultValue + ) + } + return Wrapped.TableColumns.allColumns.map { open($0) } } public static var writableColumns: [any WritableTableColumnExpression] { - Wrapped.TableColumns.writableColumns + func open( + _ column: some WritableTableColumnExpression + ) -> any WritableTableColumnExpression { + let column = column as! TableColumn + return TableColumn( + column.name, + keyPath: \.[member: \Value.self, column: column.keyPath], + default: column.defaultValue + ) + } + return Wrapped.TableColumns.writableColumns.map { open($0) } } public subscript( @@ -108,7 +144,25 @@ extension Optional: Table where Wrapped: Table { let column = Wrapped.columns[keyPath: keyPath] return TableColumn( column.name, - keyPath: \.[member: \Member.self, column: column._keyPath] + keyPath: \.[member: \Member.self, column: column.keyPath] + ) + } + + public subscript( + dynamicMember keyPath: KeyPath> + ) -> GeneratedColumn { + let column = Wrapped.columns[keyPath: keyPath] + return GeneratedColumn( + column.name, + keyPath: \.[member: \Member.self, column: column.keyPath] + ) + } + + public subscript( + dynamicMember keyPath: KeyPath> + ) -> ColumnGroup { + ColumnGroup( + keyPath: \.[member: \Member.self, column: Wrapped.columns[keyPath: keyPath].keyPath] ) } @@ -125,6 +179,8 @@ extension Optional: Table where Wrapped: Table { Wrapped.columns[keyPath: keyPath] } } + + public typealias Selection = Wrapped.Selection? } extension Optional: PrimaryKeyedTable where Wrapped: PrimaryKeyedTable { @@ -140,10 +196,63 @@ extension Optional: TableDraft where Wrapped: TableDraft { extension Optional.TableColumns: PrimaryKeyedTableDefinition where Wrapped.TableColumns: PrimaryKeyedTableDefinition { - public typealias PrimaryKey = Wrapped.TableColumns.PrimaryKey? + public typealias PrimaryKey = Wrapped.PrimaryKey? + + public struct PrimaryColumn: _TableColumnExpression { + public typealias Root = Optional + + public typealias Value = Wrapped.PrimaryKey? + + public var _names: [String] { + Wrapped.columns.primaryKey._names + } + + public var keyPath: KeyPath { + \.[member: \Wrapped.PrimaryKey.self, column: Wrapped.columns.primaryKey.keyPath] + } + + public var queryFragment: QueryFragment { + Wrapped.columns.primaryKey.queryFragment + } + } + + public var primaryKey: PrimaryColumn { + PrimaryColumn() + } +} + +extension Optional.TableColumns.PrimaryColumn: TableColumnExpression +where Wrapped.TableColumns.PrimaryColumn: TableColumnExpression { + public var name: String { + Wrapped.columns.primaryKey.name + } + + public var defaultValue: Wrapped.PrimaryKey.QueryOutput?? { + Wrapped.columns.primaryKey.defaultValue + } + + public func _aliased( + _ alias: Name.Type + ) -> any TableColumnExpression, Wrapped.PrimaryKey?> { + GeneratedColumn(name, keyPath: \.[member: \Value.self, column: keyPath]) + } +} + +extension Optional.TableColumns.PrimaryColumn: WritableTableColumnExpression +where Wrapped.TableColumns.PrimaryColumn: WritableTableColumnExpression { + public func _aliased( + _ alias: Name.Type + ) -> any WritableTableColumnExpression, Wrapped.PrimaryKey?> { + TableColumn(name, keyPath: \.[member: \Value.self, column: keyPath]) + } +} - public var primaryKey: TableColumn { - self[dynamicMember: \.primaryKey] +extension Optional: TableExpression where Wrapped: TableExpression { + public var allColumns: [any QueryExpression] { + self?.allColumns + ?? Wrapped.QueryValue.TableColumns.allColumns.map { + SQLQueryExpression("NULL AS \(quote: $0.name)") + } } } diff --git a/Sources/StructuredQueriesCore/PrimaryKeyed.swift b/Sources/StructuredQueriesCore/PrimaryKeyed.swift index 919a9824..cc0b9467 100644 --- a/Sources/StructuredQueriesCore/PrimaryKeyed.swift +++ b/Sources/StructuredQueriesCore/PrimaryKeyed.swift @@ -4,8 +4,8 @@ where TableColumns: PrimaryKeyedTableDefinition { /// A type representing this table's primary key. /// /// For auto-incrementing tables, this is typically `Int`. - associatedtype PrimaryKey: QueryBindable - where PrimaryKey.QueryValue == PrimaryKey, PrimaryKey.QueryValue.QueryOutput: Sendable + associatedtype PrimaryKey: QueryRepresentable & QueryExpression + where PrimaryKey.QueryValue == PrimaryKey /// A type that represents this type, but with an optional primary key. /// @@ -18,7 +18,9 @@ public protocol TableDraft: Table { /// A type that represents the table with a primary key. associatedtype PrimaryTable: PrimaryKeyedTable where PrimaryTable.Draft == Self - /// Creates a draft from a primary keyed table. + typealias PrimaryKey = PrimaryTable.PrimaryKey + + /// Creates a draft from a primary-keyed table. init(_ primaryTable: PrimaryTable) } @@ -43,17 +45,19 @@ extension TableDraft { /// A type representing a database table's columns. /// /// Don't conform to this protocol directly. Instead, use the `@Table` and `@Column` macros to -/// generate a conformance. +/// generate a conformance. See for more information. public protocol PrimaryKeyedTableDefinition: TableDefinition where QueryValue: PrimaryKeyedTable { /// A type representing this table's primary key. /// /// For auto-incrementing tables, this is typically `Int`. - associatedtype PrimaryKey: QueryBindable - where PrimaryKey.QueryValue == PrimaryKey, PrimaryKey.QueryValue.QueryOutput: Sendable + associatedtype PrimaryKey: QueryRepresentable & QueryExpression + where PrimaryKey.QueryValue == PrimaryKey + + associatedtype PrimaryColumn: _TableColumnExpression /// The column representing this table's primary key. - var primaryKey: TableColumn { get } + var primaryKey: PrimaryColumn { get } } extension TableDefinition where QueryValue: TableDraft { @@ -64,7 +68,7 @@ extension TableDefinition where QueryValue: TableDraft { } } -extension PrimaryKeyedTableDefinition { +extension PrimaryKeyedTableDefinition where PrimaryColumn: TableColumnExpression { /// A query expression representing the number of rows in this table. /// /// - Parameters: @@ -86,9 +90,9 @@ extension PrimaryKeyedTable { /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: A `WHERE` clause. public static func find( - _ primaryKey: some QueryExpression + _ primaryKey: some QueryExpression ) -> Where { - Self.find([primaryKey]) + find([primaryKey]) } /// A where clause filtered by primary keys. @@ -96,7 +100,7 @@ extension PrimaryKeyedTable { /// - Parameter primaryKey: Primary keys identifying table rows. /// - Returns: A `WHERE` clause. public static func find( - _ primaryKeys: some Sequence> + _ primaryKeys: some Sequence> ) -> Where { Self.where { $0.primaryKey.in(primaryKeys) } } @@ -112,9 +116,9 @@ extension TableDraft { /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: A `WHERE` clause. public static func find( - _ primaryKey: some QueryExpression + _ primaryKey: some QueryExpression ) -> Where { - Self.find([primaryKey]) + find([primaryKey]) } /// A where clause filtered by primary keys. @@ -122,7 +126,7 @@ extension TableDraft { /// - Parameter primaryKeys: Primary keys identifying table rows. /// - Returns: A `WHERE` clause. public static func find( - _ primaryKeys: some Sequence> + _ primaryKeys: some Sequence> ) -> Where { Self.where { $0.primaryKey.in(primaryKeys) } } @@ -133,8 +137,8 @@ extension Where where From: PrimaryKeyedTable { /// /// - Parameter primaryKey: A primary key. /// - Returns: A where clause with the added primary key. - public func find(_ primaryKey: some QueryExpression) -> Self { - self.find([primaryKey]) + public func find(_ primaryKey: some QueryExpression) -> Self { + find([primaryKey]) } /// Adds a primary key condition to a where clause. @@ -142,7 +146,7 @@ extension Where where From: PrimaryKeyedTable { /// - Parameter primaryKeys: A sequence of primary keys. /// - Returns: A where clause with the added primary keys condition. public func find( - _ primaryKeys: some Sequence> + _ primaryKeys: some Sequence> ) -> Self { Self.where { $0.primaryKey.in(primaryKeys) } } @@ -153,10 +157,10 @@ extension Where where From: TableDraft { /// /// - Parameter primaryKey: A primary key. /// - Returns: A where clause with the added primary key. - public func find(_ primaryKey: some QueryExpression) + public func find(_ primaryKey: some QueryExpression) -> Self { - self.find([primaryKey]) + find([primaryKey]) } /// Adds a primary key condition to a where clause. @@ -164,7 +168,7 @@ extension Where where From: TableDraft { /// - Parameter primaryKeys: A sequence of primary keys. /// - Returns: A where clause with the added primary keys condition. public func find( - _ primaryKeys: some Sequence> + _ primaryKeys: some Sequence> ) -> Self { Self.where { $0.primaryKey.in(primaryKeys) } } @@ -175,8 +179,8 @@ extension Select where From: PrimaryKeyedTable { /// /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: A select statement filtered by the given key. - public func find(_ primaryKey: some QueryExpression) -> Self { - self.and(From.find(primaryKey)) + public func find(_ primaryKey: some QueryExpression) -> Self { + and(From.find(primaryKey)) } /// A select statement filtered by a sequence of primary keys. @@ -184,9 +188,9 @@ extension Select where From: PrimaryKeyedTable { /// - Parameter primaryKeys: A sequence of primary keys. /// - Returns: A select statement filtered by the given keys. public func find( - _ primaryKeys: some Sequence> + _ primaryKeys: some Sequence> ) -> Self { - self.and(From.find(primaryKeys)) + and(From.find(primaryKeys)) } } @@ -196,9 +200,9 @@ extension Select where From: TableDraft { /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: A select statement filtered by the given key. public func find( - _ primaryKey: some QueryExpression + _ primaryKey: some QueryExpression ) -> Self { - self.and(From.find(primaryKey)) + and(From.find(primaryKey)) } /// A select statement filtered by a sequence of primary keys. @@ -206,9 +210,9 @@ extension Select where From: TableDraft { /// - Parameter primaryKeys: A sequence of primary keys. /// - Returns: A select statement filtered by the given keys. public func find( - _ primaryKeys: some Sequence> + _ primaryKeys: some Sequence> ) -> Self { - self.and(From.find(primaryKeys)) + and(From.find(primaryKeys)) } } @@ -217,8 +221,8 @@ extension Update where From: PrimaryKeyedTable { /// /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: An update statement filtered by the given key. - public func find(_ primaryKey: some QueryExpression) -> Self { - self.find([primaryKey]) + public func find(_ primaryKey: some QueryExpression) -> Self { + find([primaryKey]) } /// An update statement filtered by a sequence of primary keys. @@ -226,7 +230,7 @@ extension Update where From: PrimaryKeyedTable { /// - Parameter primaryKeys: A sequence of primary keys. /// - Returns: An update statement filtered by the given keys. public func find( - _ primaryKeys: some Sequence> + _ primaryKeys: some Sequence> ) -> Self { self.where { $0.primaryKey.in(primaryKeys) } } @@ -237,10 +241,10 @@ extension Update where From: TableDraft { /// /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: An update statement filtered by the given key. - public func find(_ primaryKey: some QueryExpression) + public func find(_ primaryKey: some QueryExpression) -> Self { - self.find([primaryKey]) + find([primaryKey]) } /// An update statement filtered by a sequence of primary keys. @@ -248,7 +252,7 @@ extension Update where From: TableDraft { /// - Parameter primaryKeys: A sequence of primary keys. /// - Returns: An update statement filtered by the given keys. public func find( - _ primaryKeys: some Sequence> + _ primaryKeys: some Sequence> ) -> Self { self.where { $0.primaryKey.in(primaryKeys) } } @@ -259,8 +263,8 @@ extension Delete where From: PrimaryKeyedTable { /// /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: A delete statement filtered by the given key. - public func find(_ primaryKey: some QueryExpression) -> Self { - self.find([primaryKey]) + public func find(_ primaryKey: some QueryExpression) -> Self { + find([primaryKey]) } /// A delete statement filtered by a sequence of primary keys. @@ -268,7 +272,7 @@ extension Delete where From: PrimaryKeyedTable { /// - Parameter primaryKeys: A sequence of primary keys. /// - Returns: A delete statement filtered by the given keys. public func find( - _ primaryKeys: some Sequence> + _ primaryKeys: some Sequence> ) -> Self { self.where { $0.primaryKey.in(primaryKeys) } } @@ -279,10 +283,10 @@ extension Delete where From: TableDraft { /// /// - Parameter primaryKey: A primary key identifying a table row. /// - Returns: A delete statement filtered by the given key. - public func find(_ primaryKey: some QueryExpression) + public func find(_ primaryKey: some QueryExpression) -> Self { - self.find([primaryKey]) + find([primaryKey]) } /// A delete statement filtered by a sequence of primary keys. @@ -290,7 +294,7 @@ extension Delete where From: TableDraft { /// - Parameter primaryKeys: A sequence of primary keys. /// - Returns: A delete statement filtered by the given keys. public func find( - _ primaryKeys: some Sequence> + _ primaryKeys: some Sequence> ) -> Self { self.where { $0.primaryKey.in(primaryKeys) } } diff --git a/Sources/StructuredQueriesCore/QueryBindable+Foundation.swift b/Sources/StructuredQueriesCore/QueryBindable+Foundation.swift index c84cf21f..315ee362 100644 --- a/Sources/StructuredQueriesCore/QueryBindable+Foundation.swift +++ b/Sources/StructuredQueriesCore/QueryBindable+Foundation.swift @@ -5,11 +5,6 @@ extension Data: QueryBindable { .blob([UInt8](self)) } - public init?(queryBinding: QueryBinding) { - guard case .blob(let bytes) = queryBinding else { return nil } - self.init(bytes) - } - public init(decoder: inout some QueryDecoder) throws { try self.init([UInt8](decoder: &decoder)) } @@ -20,11 +15,6 @@ extension URL: QueryBindable { .text(absoluteString) } - public init?(queryBinding: QueryBinding) { - guard case .text(let string) = queryBinding else { return nil } - self.init(string: string) - } - public init(decoder: inout some QueryDecoder) throws { guard let url = Self(string: try String(decoder: &decoder)) else { throw InvalidURL() diff --git a/Sources/StructuredQueriesCore/QueryBindable.swift b/Sources/StructuredQueriesCore/QueryBindable.swift index 1e77b61d..8d48ed53 100644 --- a/Sources/StructuredQueriesCore/QueryBindable.swift +++ b/Sources/StructuredQueriesCore/QueryBindable.swift @@ -9,9 +9,6 @@ public protocol QueryBindable: QueryRepresentable, QueryExpression where QueryVa /// A value that can be bound to a parameter of a SQL statement. var queryBinding: QueryBinding { get } - - /// Initializes a bindable type from a binding. - init?(queryBinding: QueryBinding) } extension QueryBindable { @@ -20,132 +17,68 @@ extension QueryBindable { extension [UInt8]: QueryBindable, QueryExpression { public var queryBinding: QueryBinding { .blob(self) } - public init?(queryBinding: QueryBinding) { - guard case .blob(let value) = queryBinding else { return nil } - self = value - } } extension Bool: QueryBindable { public var queryBinding: QueryBinding { .int(self ? 1 : 0) } - public init?(queryBinding: QueryBinding) { - guard case .int(let value) = queryBinding else { return nil } - self = value != 0 - } } extension Double: QueryBindable { public var queryBinding: QueryBinding { .double(self) } - public init?(queryBinding: QueryBinding) { - guard case .double(let value) = queryBinding else { return nil } - self = value - } } extension Date: QueryBindable { public var queryBinding: QueryBinding { .date(self) } - public init?(queryBinding: QueryBinding) { - guard case .date(let value) = queryBinding else { return nil } - self = value - } } extension Float: QueryBindable { public var queryBinding: QueryBinding { .double(Double(self)) } - public init?(queryBinding: QueryBinding) { - guard case .double(let value) = queryBinding else { return nil } - self.init(value) - } } extension Int: QueryBindable { public var queryBinding: QueryBinding { .int(Int64(self)) } - public init?(queryBinding: QueryBinding) { - guard case .int(let value) = queryBinding else { return nil } - self.init(value) - } } extension Int8: QueryBindable { public var queryBinding: QueryBinding { .int(Int64(self)) } - public init?(queryBinding: QueryBinding) { - guard case .int(let value) = queryBinding else { return nil } - self.init(value) - } } extension Int16: QueryBindable { public var queryBinding: QueryBinding { .int(Int64(self)) } - public init?(queryBinding: QueryBinding) { - guard case .int(let value) = queryBinding else { return nil } - self.init(value) - } } extension Int32: QueryBindable { public var queryBinding: QueryBinding { .int(Int64(self)) } - public init?(queryBinding: QueryBinding) { - guard case .int(let value) = queryBinding else { return nil } - self.init(value) - } } extension Int64: QueryBindable { public var queryBinding: QueryBinding { .int(self) } - public init?(queryBinding: QueryBinding) { - guard case .int(let value) = queryBinding else { return nil } - self = value - } } extension String: QueryBindable { public var queryBinding: QueryBinding { .text(self) } - public init?(queryBinding: QueryBinding) { - guard case let .text(value) = queryBinding else { return nil } - self = value - } } extension UInt8: QueryBindable { public var queryBinding: QueryBinding { .int(Int64(self)) } - public init?(queryBinding: QueryBinding) { - guard case .int(let value) = queryBinding else { return nil } - self.init(value) - } } extension UInt16: QueryBindable { public var queryBinding: QueryBinding { .int(Int64(self)) } - public init?(queryBinding: QueryBinding) { - guard case .int(let value) = queryBinding else { return nil } - self.init(value) - } } extension UInt32: QueryBindable { public var queryBinding: QueryBinding { .int(Int64(self)) } - public init?(queryBinding: QueryBinding) { - guard case .int(let value) = queryBinding else { return nil } - self.init(value) - } } extension UInt64: QueryBindable { public var queryBinding: QueryBinding { return .uint(self) } - public init?(queryBinding: QueryBinding) { - guard case .uint(let value) = queryBinding else { return nil } - self.init(value) - } } extension UUID: QueryBindable { public var queryBinding: QueryBinding { .uuid(self) } - public init?(queryBinding: QueryBinding) { - guard case .uuid(let value) = queryBinding else { return nil } - self = value - } } extension DefaultStringInterpolation { @@ -177,16 +110,8 @@ extension DefaultStringInterpolation { extension QueryBindable where Self: LosslessStringConvertible { public var queryBinding: QueryBinding { description.queryBinding } - public init?(queryBinding: QueryBinding) { - guard let description = String(queryBinding: queryBinding) else { return nil } - self.init(description) - } } extension QueryBindable where Self: RawRepresentable, RawValue: QueryBindable { public var queryBinding: QueryBinding { rawValue.queryBinding } - public init?(queryBinding: QueryBinding) { - guard let rawValue = RawValue(queryBinding: queryBinding) else { return nil } - self.init(rawValue: rawValue) - } } diff --git a/Sources/StructuredQueriesCore/QueryExpression.swift b/Sources/StructuredQueriesCore/QueryExpression.swift index 82b18ae2..07728ddd 100644 --- a/Sources/StructuredQueriesCore/QueryExpression.swift +++ b/Sources/StructuredQueriesCore/QueryExpression.swift @@ -9,4 +9,18 @@ public protocol QueryExpression { /// The query fragment associated with this expression. var queryFragment: QueryFragment { get } + + static var _columnWidth: Int { get } + + var _allColumns: [any QueryExpression] { get } +} + +extension QueryExpression { + public static var _columnWidth: Int { + 1 + } + + public var _allColumns: [any QueryExpression] { + [self] + } } diff --git a/Sources/StructuredQueriesCore/QueryRepresentable/Codable+JSON.swift b/Sources/StructuredQueriesCore/QueryRepresentable/Codable+JSON.swift index ee574e39..84c572c7 100644 --- a/Sources/StructuredQueriesCore/QueryRepresentable/Codable+JSON.swift +++ b/Sources/StructuredQueriesCore/QueryRepresentable/Codable+JSON.swift @@ -37,13 +37,6 @@ extension _CodableJSONRepresentation: QueryBindable { return .invalid(error) } } - - public init?(queryBinding: QueryBinding) { - guard case .text(let value) = queryBinding else { return nil } - guard let queryOutput = try? jsonDecoder.decode(QueryOutput.self, from: Data(value.utf8)) - else { return nil } - self.init(queryOutput: queryOutput) - } } extension _CodableJSONRepresentation: QueryDecodable { diff --git a/Sources/StructuredQueriesCore/Seeds.swift b/Sources/StructuredQueriesCore/Seeds.swift index 131a79b7..f3c0c9b8 100644 --- a/Sources/StructuredQueriesCore/Seeds.swift +++ b/Sources/StructuredQueriesCore/Seeds.swift @@ -42,7 +42,7 @@ public struct Seeds: Sequence { /// ``` /// /// And then you can iterate over each insert statement and execute it given a database - /// connection. For example, using the [SharingGRDB][] driver: + /// connection. For example, using the [SQLiteData] driver: /// /// ```swift /// try database.write { db in @@ -55,8 +55,8 @@ public struct Seeds: Sequence { /// } /// ``` /// - /// > Tip: [SharingGRDB][] extends GRDB's `Database` connection with a `seed` method that can - /// > build and insert records in a single step: + /// > Tip: [SQLiteData] extends GRDB's `Database` connection with a `seed` method that can build + /// > and insert records in a single step: /// > /// > ```swift /// > try db.seed { @@ -64,7 +64,7 @@ public struct Seeds: Sequence { /// > } /// > ``` /// - /// [SharingGRDB]: https://github.com/pointfreeco/sharing-grdb + /// [SQLiteData]: https://github.com/pointfreeco/sqlite-data /// /// - Parameter build: A result builder closure that prepares statements to insert every built row. public init(@SeedsBuilder _ build: () -> [any Table]) { diff --git a/Sources/StructuredQueriesCore/Statements/Delete.swift b/Sources/StructuredQueriesCore/Statements/Delete.swift index b3351676..57e7c64d 100644 --- a/Sources/StructuredQueriesCore/Statements/Delete.swift +++ b/Sources/StructuredQueriesCore/Statements/Delete.swift @@ -25,7 +25,7 @@ extension PrimaryKeyedTable { public static func delete(_ row: Self) -> DeleteOf { delete() .where { - $0.primaryKey.eq(TableColumns.PrimaryKey(queryOutput: row[keyPath: $0.primaryKey.keyPath])) + $0.primaryKey.eq(PrimaryKey(queryOutput: row[keyPath: $0.primaryKey.keyPath])) } } } diff --git a/Sources/StructuredQueriesCore/Statements/Insert.swift b/Sources/StructuredQueriesCore/Statements/Insert.swift index e1110d95..abc2fbf4 100644 --- a/Sources/StructuredQueriesCore/Statements/Insert.swift +++ b/Sources/StructuredQueriesCore/Statements/Insert.swift @@ -144,12 +144,10 @@ extension Table { /// existing row. /// - updateFilter: A filter to apply to the update clause. /// - Returns: An insert statement. - public static func insert( + public static func insert( _ columns: (TableColumns) -> TableColumns = { $0 }, @InsertValuesBuilder values: () -> [[QueryFragment]], - onConflict conflictTargets: (TableColumns) -> ( - TableColumn, repeat TableColumn - ), + onConflict conflictTargets: (TableColumns) -> (T1, repeat each T2), @QueryFragmentBuilder where targetFilter: (TableColumns) -> [QueryFragment] = { _ in [] }, doUpdate updates: (inout Updates, Excluded) -> Void = { _, _ in }, @@ -179,12 +177,10 @@ extension Table { /// existing row. /// - updateFilter: A filter to apply to the update clause. /// - Returns: An insert statement. - public static func insert( + public static func insert( _ columns: (TableColumns) -> TableColumns = { $0 }, @InsertValuesBuilder values: () -> [[QueryFragment]], - onConflict conflictTargets: (TableColumns) -> ( - TableColumn, repeat TableColumn - ), + onConflict conflictTargets: (TableColumns) -> (T1, repeat each T2), @QueryFragmentBuilder where targetFilter: (TableColumns) -> [QueryFragment] = { _ in [] }, doUpdate updates: (inout Updates) -> Void, @@ -255,9 +251,9 @@ extension Table { /// existing row. /// - updateFilter: A filter to apply to the update clause. /// - Returns: An insert statement. - public static func insert( - _ columns: (TableColumns) -> (TableColumn, repeat TableColumn), - @InsertValuesBuilder<(V1, repeat each V2)> + public static func insert( + _ columns: (TableColumns) -> (V1, repeat each V2), + @InsertValuesBuilder<(V1.Value, repeat (each V2).Value)> values: () -> [[QueryFragment]], onConflictDoUpdate updates: ((inout Updates, Excluded) -> Void)? = nil, @QueryFragmentBuilder @@ -282,9 +278,9 @@ extension Table { /// existing row. /// - updateFilter: A filter to apply to the update clause. /// - Returns: An insert statement. - public static func insert( - _ columns: (TableColumns) -> (TableColumn, repeat TableColumn), - @InsertValuesBuilder<(V1, repeat each V2)> + public static func insert( + _ columns: (TableColumns) -> (V1, repeat each V2), + @InsertValuesBuilder<(V1.Value, repeat (each V2).Value)> values: () -> [[QueryFragment]], onConflictDoUpdate updates: ((inout Updates) -> Void)?, @QueryFragmentBuilder @@ -309,13 +305,16 @@ extension Table { /// existing row. /// - updateFilter: A filter to apply to the update clause. /// - Returns: An insert statement. - public static func insert( - _ columns: (TableColumns) -> (TableColumn, repeat TableColumn), - @InsertValuesBuilder<(V1, repeat each V2)> + public static func insert< + V1: _TableColumnExpression, + each V2: _TableColumnExpression, + T1: _TableColumnExpression, + each T2: _TableColumnExpression + >( + _ columns: (TableColumns) -> (V1, repeat each V2), + @InsertValuesBuilder<(V1.Value, repeat (each V2).Value)> values: () -> [[QueryFragment]], - onConflict conflictTargets: (TableColumns) -> ( - TableColumn, repeat TableColumn - ), + onConflict conflictTargets: (TableColumns) -> (T1, repeat each T2), @QueryFragmentBuilder where targetFilter: (TableColumns) -> [QueryFragment] = { _ in [] }, doUpdate updates: (inout Updates, Excluded) -> Void = { _, _ in }, @@ -345,13 +344,16 @@ extension Table { /// existing row. /// - updateFilter: A filter to apply to the update clause. /// - Returns: An insert statement. - public static func insert( - _ columns: (TableColumns) -> (TableColumn, repeat TableColumn), - @InsertValuesBuilder<(V1, repeat each V2)> + public static func insert< + V1: _TableColumnExpression, + each V2: _TableColumnExpression, + T1: _TableColumnExpression, + each T2: _TableColumnExpression + >( + _ columns: (TableColumns) -> (V1, repeat each V2), + @InsertValuesBuilder<(V1.Value, repeat (each V2).Value)> values: () -> [[QueryFragment]], - onConflict conflictTargets: (TableColumns) -> ( - TableColumn, repeat TableColumn - ), + onConflict conflictTargets: (TableColumns) -> (T1, repeat each T2), @QueryFragmentBuilder where targetFilter: (TableColumns) -> [QueryFragment] = { _ in [] }, doUpdate updates: (inout Updates) -> Void, @@ -368,11 +370,14 @@ extension Table { ) } - private static func _insert( - _ columns: (TableColumns) -> (repeat TableColumn), - @InsertValuesBuilder<(repeat each Value)> + private static func _insert< + each Value: _TableColumnExpression, + each ConflictTarget: _TableColumnExpression + >( + _ columns: (TableColumns) -> (repeat each Value), + @InsertValuesBuilder<(repeat (each Value).Value)> values: () -> [[QueryFragment]], - onConflict conflictTargets: (TableColumns) -> (repeat TableColumn)?, + onConflict conflictTargets: (TableColumns) -> (repeat each ConflictTarget)?, @QueryFragmentBuilder where targetFilter: (TableColumns) -> [QueryFragment] = { _ in [] }, doUpdate updates: ((inout Updates, Excluded) -> Void)?, @@ -381,7 +386,7 @@ extension Table { ) -> InsertOf { var columnNames: [String] = [] for column in repeat each columns(Self.columns) { - columnNames.append(column.name) + columnNames.append(contentsOf: column._names) } return _insert( columnNames: columnNames, @@ -406,11 +411,29 @@ extension Table { /// - updateFilter: A filter to apply to the update clause. /// - Returns: An insert statement. public static func insert< - V1, - each V2 + V1: _TableColumnExpression, + each V2: _TableColumnExpression >( - _ columns: (TableColumns) -> (TableColumn, repeat TableColumn), - select selection: () -> some PartialSelectStatement<(V1, repeat each V2)>, + _ columns: (TableColumns) -> (V1, repeat each V2), + select selection: () -> some PartialSelectStatement<(V1.Value, repeat (each V2).Value)>, + onConflictDoUpdate updates: ((inout Updates, Excluded) -> Void)? = nil, + @QueryFragmentBuilder + where updateFilter: (TableColumns) -> [QueryFragment] = { _ in [] } + ) -> InsertOf { + _insert( + columns, + select: selection, + onConflict: { _ -> ()? in nil }, + where: { _ in return [] }, + doUpdate: updates, + where: updateFilter + ) + } + + // NB: This overload is required due to a parameter pack bug. + public static func insert( + _ columns: (TableColumns) -> V1, + select selection: () -> some PartialSelectStatement, onConflictDoUpdate updates: ((inout Updates, Excluded) -> Void)? = nil, @QueryFragmentBuilder where updateFilter: (TableColumns) -> [QueryFragment] = { _ in [] } @@ -438,11 +461,11 @@ extension Table { /// - updateFilter: A filter to apply to the update clause. /// - Returns: An insert statement. public static func insert< - V1, - each V2 + V1: _TableColumnExpression, + each V2: _TableColumnExpression >( - _ columns: (TableColumns) -> (TableColumn, repeat TableColumn), - select selection: () -> some PartialSelectStatement<(V1, repeat each V2)>, + _ columns: (TableColumns) -> (V1, repeat each V2), + select selection: () -> some PartialSelectStatement<(V1.Value, repeat (each V2).Value)>, onConflictDoUpdate updates: ((inout Updates) -> Void)?, @QueryFragmentBuilder where updateFilter: (TableColumns) -> [QueryFragment] = { _ in [] } @@ -470,16 +493,41 @@ extension Table { /// - updateFilter: A filter to apply to the update clause. /// - Returns: An insert statement. public static func insert< - V1, - each V2, - T1, - each T2 + V1: _TableColumnExpression, + each V2: _TableColumnExpression, + T1: _TableColumnExpression, + each T2: _TableColumnExpression >( - _ columns: (TableColumns) -> (TableColumn, repeat TableColumn), - select selection: () -> some PartialSelectStatement<(V1, repeat each V2)>, - onConflict conflictTargets: (TableColumns) -> ( - TableColumn, repeat TableColumn - ), + _ columns: (TableColumns) -> (V1, repeat each V2), + select selection: () -> some PartialSelectStatement<(V1.Value, repeat (each V2).Value)>, + onConflict conflictTargets: (TableColumns) -> (T1, repeat each T2), + @QueryFragmentBuilder + where targetFilter: (TableColumns) -> [QueryFragment] = { _ in [] }, + doUpdate updates: (inout Updates, Excluded) -> Void = { _, _ in }, + @QueryFragmentBuilder + where updateFilter: (TableColumns) -> [QueryFragment] = { _ in [] } + ) -> InsertOf { + withoutActuallyEscaping(updates) { updates in + _insert( + columns, + select: selection, + onConflict: conflictTargets, + where: targetFilter, + doUpdate: updates, + where: updateFilter + ) + } + } + + // NB: This overload is required due to a parameter pack bug. + public static func insert< + V1: _TableColumnExpression, + T1: _TableColumnExpression, + each T2: _TableColumnExpression + >( + _ columns: (TableColumns) -> V1, + select selection: () -> some PartialSelectStatement, + onConflict conflictTargets: (TableColumns) -> (T1, repeat each T2), @QueryFragmentBuilder where targetFilter: (TableColumns) -> [QueryFragment] = { _ in [] }, doUpdate updates: (inout Updates, Excluded) -> Void = { _, _ in }, @@ -513,16 +561,14 @@ extension Table { /// - updateFilter: A filter to apply to the update clause. /// - Returns: An insert statement. public static func insert< - V1, - each V2, - T1, - each T2 + V1: _TableColumnExpression, + each V2: _TableColumnExpression, + T1: _TableColumnExpression, + each T2: _TableColumnExpression >( - _ columns: (TableColumns) -> (TableColumn, repeat TableColumn), - select selection: () -> some PartialSelectStatement<(V1, repeat each V2)>, - onConflict conflictTargets: (TableColumns) -> ( - TableColumn, repeat TableColumn - ), + _ columns: (TableColumns) -> (V1, repeat each V2), + select selection: () -> some PartialSelectStatement<(V1.Value, repeat (each V2).Value)>, + onConflict conflictTargets: (TableColumns) -> (T1, repeat each T2), @QueryFragmentBuilder where targetFilter: (TableColumns) -> [QueryFragment] = { _ in [] }, doUpdate updates: (inout Updates) -> Void, @@ -539,13 +585,40 @@ extension Table { ) } + // NB: This overload is required due to a parameter pack bug. + public static func insert< + V1: _TableColumnExpression, + T1: _TableColumnExpression, + each T2: _TableColumnExpression + >( + _ columns: (TableColumns) -> V1, + select selection: () -> some PartialSelectStatement, + onConflict conflictTargets: (TableColumns) -> (T1, repeat each T2), + @QueryFragmentBuilder + where targetFilter: (TableColumns) -> [QueryFragment] = { _ in [] }, + doUpdate updates: (inout Updates) -> Void, + @QueryFragmentBuilder + where updateFilter: (TableColumns) -> [QueryFragment] = { _ in [] } + ) -> InsertOf { + insert( + columns, + select: selection, + onConflict: conflictTargets, + where: targetFilter, + doUpdate: { row, _ in updates(&row) }, + where: updateFilter + ) + } + + // NB: We should constrain these generics `where Root == Self` when Swift supports same-type + // constraints in parameter packs. private static func _insert< - each Value, - each ConflictTarget + each Value: _TableColumnExpression, + each ConflictTarget: _TableColumnExpression >( - _ columns: (TableColumns) -> (repeat TableColumn), - select selection: () -> some PartialSelectStatement<(repeat each Value)>, - onConflict conflictTargets: (TableColumns) -> (repeat TableColumn)?, + _ columns: (TableColumns) -> (repeat each Value), + select selection: () -> some PartialSelectStatement<(repeat (each Value).Value)>, + onConflict conflictTargets: (TableColumns) -> (repeat each ConflictTarget)?, @QueryFragmentBuilder where targetFilter: (TableColumns) -> [QueryFragment] = { _ in [] }, doUpdate updates: ((inout Updates, Excluded) -> Void)?, @@ -554,7 +627,7 @@ extension Table { ) -> InsertOf { var columnNames: [String] = [] for column in repeat each columns(Self.columns) { - columnNames.append(column.name) + columnNames.append(contentsOf: column._names) } return _insert( columnNames: columnNames, @@ -587,10 +660,10 @@ extension Table { ) } - fileprivate static func _insert( + fileprivate static func _insert( columnNames: [String], values: InsertValues, - onConflict conflictTargets: (TableColumns) -> (repeat TableColumn)?, + onConflict conflictTargets: (TableColumns) -> (repeat each ConflictTarget)?, @QueryFragmentBuilder where targetFilter: (TableColumns) -> [QueryFragment] = { _ in [] }, doUpdate updates: ((inout Updates, Excluded) -> Void)?, @@ -600,7 +673,7 @@ extension Table { var conflictTargetColumnNames: [String] = [] if let conflictTargets = conflictTargets(Self.columns) { for column in repeat each conflictTargets { - conflictTargetColumnNames.append(column.name) + conflictTargetColumnNames.append(contentsOf: column._names) } } return Insert( @@ -640,7 +713,7 @@ extension PrimaryKeyedTable { onConflict: { $0.primaryKey }, doUpdate: { updates, _ in for (column, excluded) in zip(Draft.TableColumns.writableColumns, Excluded.writableColumns) - where column.name != columns.primaryKey.name { + where !columns.primaryKey._names.contains(column.name) { updates.set(column, excluded.queryFragment) } } @@ -941,10 +1014,10 @@ public enum InsertValuesBuilder { } public static func buildExpression( - _ expression: Value.Columns + _ expression: Value.Selection ) -> [[QueryFragment]] - where Value: _Selection { - [expression.selection.map(\.expression)] + where Value: Table { + [expression.allColumns.map(\.queryFragment)] } public static func buildArray(_ components: [[[QueryFragment]]]) -> [[QueryFragment]] { diff --git a/Sources/StructuredQueriesCore/Statements/Select.swift b/Sources/StructuredQueriesCore/Statements/Select.swift index f2c8b385..27f494cf 100644 --- a/Sources/StructuredQueriesCore/Statements/Select.swift +++ b/Sources/StructuredQueriesCore/Statements/Select.swift @@ -569,7 +569,10 @@ extension Select { Select<(repeat each C1, repeat (each C2).QueryValue), From, (repeat each J)>( isEmpty: isEmpty, distinct: distinct, - columns: columns + Array(repeat each selection((From.columns, repeat (each J).columns))), + columns: columns + + $_isSelecting.withValue(true) { + Array(repeat each selection((From.columns, repeat (each J).columns))) + }, joins: joins, where: `where`, group: group, @@ -1730,6 +1733,8 @@ public func + < ) } +@TaskLocal public var _isSelecting = false + extension Select: SelectStatement { public typealias QueryValue = Columns diff --git a/Sources/StructuredQueriesCore/Statements/SelectStatement.swift b/Sources/StructuredQueriesCore/Statements/SelectStatement.swift index bc7e386d..780b0d00 100644 --- a/Sources/StructuredQueriesCore/Statements/SelectStatement.swift +++ b/Sources/StructuredQueriesCore/Statements/SelectStatement.swift @@ -1,18 +1,5 @@ public protocol PartialSelectStatement: Statement {} -extension PartialSelectStatement where Self: Table, QueryValue == Self { - public var query: QueryFragment { - var query: QueryFragment = "SELECT " - func open(_ column: some TableColumnExpression) -> QueryFragment { - let root = self as! Root - let value = Value(queryOutput: root[keyPath: column.keyPath]) - return "\(value) AS \(quote: column.name)" - } - query.append(Self.TableColumns.allColumns.map { open($0) }.joined(separator: ", ")) - return query - } -} - /// A type representing a `SELECT` statement. public protocol SelectStatement: PartialSelectStatement { /// Creates a ``Select`` statement from this statement. diff --git a/Sources/StructuredQueriesCore/Statements/Update.swift b/Sources/StructuredQueriesCore/Statements/Update.swift index c030ca46..1e1d4596 100644 --- a/Sources/StructuredQueriesCore/Statements/Update.swift +++ b/Sources/StructuredQueriesCore/Statements/Update.swift @@ -62,7 +62,8 @@ extension PrimaryKeyedTable { _ row: Self ) -> UpdateOf { update { updates in - for column in TableColumns.writableColumns where column.name != columns.primaryKey.name { + for column in TableColumns.writableColumns + where !columns.primaryKey._names.contains(column.name) { func open(_ column: some WritableTableColumnExpression) { updates.set( column, @@ -73,7 +74,7 @@ extension PrimaryKeyedTable { } } .where { - $0.primaryKey.eq(TableColumns.PrimaryKey(queryOutput: row[keyPath: $0.primaryKey.keyPath])) + $0.primaryKey.eq(PrimaryKey(queryOutput: row[keyPath: $0.primaryKey.keyPath])) } } } diff --git a/Sources/StructuredQueriesCore/Statements/Values.swift b/Sources/StructuredQueriesCore/Statements/Values.swift index d1cafcda..faa92e6d 100644 --- a/Sources/StructuredQueriesCore/Statements/Values.swift +++ b/Sources/StructuredQueriesCore/Statements/Values.swift @@ -13,10 +13,10 @@ public struct Values: PartialSelectStatement { public typealias From = Never - let values: [QueryFragment] + let values: [any QueryExpression] public init(_ value: QueryValue) where QueryValue: QueryExpression { - self.values = [value.queryFragment] + self.values = [value] } public init( @@ -26,6 +26,8 @@ public struct Values: PartialSelectStatement { } public var query: QueryFragment { - "SELECT \(values.joined(separator: ", "))" + $_isSelecting.withValue(true) { + "SELECT \(values.map(\.queryFragment).joined(separator: ", "))" + } } } diff --git a/Sources/StructuredQueriesCore/Table.swift b/Sources/StructuredQueriesCore/Table.swift index 8cd927a8..2766c331 100644 --- a/Sources/StructuredQueriesCore/Table.swift +++ b/Sources/StructuredQueriesCore/Table.swift @@ -1,11 +1,18 @@ -/// A type representing a database table. +/// A type representing columns in a database. /// /// Don't conform to this protocol directly. Instead, use the `@Table` and `@Column` macros to -/// generate a conformance. +/// generate a conformance. See for more information. @dynamicMemberLookup -public protocol Table: QueryRepresentable where TableColumns.QueryValue == Self { +public protocol Table: QueryRepresentable, PartialSelectStatement { + associatedtype QueryValue = Self + + associatedtype From = Never + /// A type that describes this table's columns. - associatedtype TableColumns: TableDefinition + associatedtype TableColumns: TableDefinition + + /// A type that describes this table as a query expression. + associatedtype Selection: TableExpression /// A type that describes the default results of requesting all rows from the table. associatedtype DefaultScope: SelectStatement<(), Self, ()> @@ -52,6 +59,9 @@ public protocol Table: QueryRepresentable where TableColumns.QueryValue == Self static var all: DefaultScope { get } } +// NB: Distinguishes `@Selection` from `@Table`. +public protocol _Selection: Table {} + extension Table { /// A select statement on the table with no constraints. /// @@ -86,6 +96,8 @@ extension Table { Where(scope: .unscoped) } + /// A select statement that does not execute and always returns no results. + @_disfavoredOverload public static var none: Where { Where(scope: .empty) } @@ -111,11 +123,26 @@ extension Table { /// #sql("SELECT \(Reminder.id) FROM \(Reminder.self)", as: Int.self) /// // SELECT "reminders"."id" FROM "reminders /// ``` - public static subscript( - dynamicMember keyPath: KeyPath> - ) -> TableColumn { + public static subscript( + dynamicMember keyPath: KeyPath + ) -> Member { columns[keyPath: keyPath] } + + public var query: QueryFragment { + func open(_ column: some TableColumnExpression) -> QueryFragment { + let value = Value(queryOutput: (self as! Root)[keyPath: column.keyPath]) + return "\(value) AS \(quote: column.name)" + } + return "SELECT \(TableColumns.allColumns.map { open($0) }.joined(separator: ", "))" + } + + public var queryFragment: QueryFragment { + func open(_ column: some TableColumnExpression) -> QueryFragment { + Value(queryOutput: (self as! Root)[keyPath: column.keyPath]).queryFragment + } + return TableColumns.allColumns.map { open($0) }.joined(separator: ", ") + } } extension Table where DefaultScope == Where { diff --git a/Sources/StructuredQueriesCore/TableAlias.swift b/Sources/StructuredQueriesCore/TableAlias.swift index fa09b0e9..fdaedc0a 100644 --- a/Sources/StructuredQueriesCore/TableAlias.swift +++ b/Sources/StructuredQueriesCore/TableAlias.swift @@ -113,6 +113,8 @@ public struct TableAlias< @dynamicMemberLookup public struct TableColumns: Sendable, TableDefinition { + public typealias QueryValue = TableAlias + public static var allColumns: [any TableColumnExpression] { #if compiler(>=6.1) return Base.TableColumns.allColumns.map { $0._aliased(Name.self) } @@ -137,15 +139,13 @@ public struct TableAlias< #endif } - public typealias QueryValue = TableAlias - public subscript( dynamicMember keyPath: KeyPath> ) -> TableColumn { let column = Base.columns[keyPath: keyPath] return TableColumn( column.name, - keyPath: \.[member: \Member.self, column: column._keyPath] + keyPath: \.[member: \Member.self, column: column.keyPath] ) } @@ -155,10 +155,32 @@ public struct TableAlias< let column = Base.columns[keyPath: keyPath] return GeneratedColumn( column.name, - keyPath: \.[member: \Member.self, column: column._keyPath] + keyPath: \.[member: \Member.self, column: column.keyPath] + ) + } + + public subscript( + dynamicMember keyPath: KeyPath> + ) -> ColumnGroup { + ColumnGroup( + keyPath: \.[member: \Member.self, column: Base.columns[keyPath: keyPath].keyPath] ) } } + + public struct Selection: TableExpression { + public typealias QueryValue = TableAlias + + fileprivate var base: Base.Selection + + public init(_ base: Base.Selection) { + self.base = base + } + + public var allColumns: [any QueryExpression] { + base.allColumns + } + } } extension TableAlias: PrimaryKeyedTable where Base: PrimaryKeyedTable { @@ -174,10 +196,56 @@ extension TableAlias: TableDraft where Base: TableDraft { extension TableAlias.TableColumns: PrimaryKeyedTableDefinition where Base.TableColumns: PrimaryKeyedTableDefinition { - public typealias PrimaryKey = Base.TableColumns.PrimaryKey + public typealias PrimaryKey = Base.PrimaryKey - public var primaryKey: TableColumn { - self[dynamicMember: \.primaryKey] + public struct PrimaryColumn: _TableColumnExpression { + public typealias Root = TableAlias + + public typealias Value = Base.PrimaryKey + + public var _names: [String] { + Base.columns.primaryKey._names + } + + public var keyPath: KeyPath { + \.[member: \Base.PrimaryKey.self, column: Base.columns.primaryKey.keyPath] + } + + public var queryFragment: QueryFragment { + Base.columns.primaryKey._names + .map { "\(quote: Name.aliasName).\(quote: $0)" } + .joined(separator: ", ") + } + } + + public var primaryKey: PrimaryColumn { + PrimaryColumn() + } +} + +extension TableAlias.TableColumns.PrimaryColumn: TableColumnExpression +where Base.TableColumns.PrimaryColumn: TableColumnExpression { + public var name: String { + Base.columns.primaryKey.name + } + + public var defaultValue: Base.PrimaryKey.QueryOutput? { + Base.columns.primaryKey.defaultValue + } + + public func _aliased( + _ alias: N.Type + ) -> any TableColumnExpression, Base.PrimaryKey> { + GeneratedColumn(name, keyPath: \.[member: \Value.self, column: keyPath]) + } +} + +extension TableAlias.TableColumns.PrimaryColumn: WritableTableColumnExpression +where Base.TableColumns.PrimaryColumn: WritableTableColumnExpression { + public func _aliased( + _ alias: N.Type + ) -> any WritableTableColumnExpression, Base.PrimaryKey> { + TableColumn(name, keyPath: \.[member: \Value.self, column: keyPath]) } } @@ -187,17 +255,20 @@ extension TableAlias: QueryExpression where Base: QueryExpression { public var queryFragment: QueryFragment { base.queryFragment } + + public static var _columnWidth: Int { + Base._columnWidth + } + + public var _allColumns: [any QueryExpression] { + base._allColumns + } } extension TableAlias: QueryBindable where Base: QueryBindable { public var queryBinding: QueryBinding { base.queryBinding } - - public init?(queryBinding: QueryBinding) { - guard let base = Base(queryBinding: queryBinding) else { return nil } - self.init(base: base) - } } extension TableAlias: QueryDecodable where Base: QueryDecodable { @@ -247,7 +318,8 @@ extension TableAlias: Encodable where Base: Encodable { extension QueryFragment { fileprivate func replacingOccurrences( - of _: T.Type, with _: A.Type + of _: T.Type, + with _: A.Type ) -> QueryFragment { var query = self for index in query.segments.indices { diff --git a/Sources/StructuredQueriesCore/TableColumn.swift b/Sources/StructuredQueriesCore/TableColumn.swift index eb090d3c..f282aebe 100644 --- a/Sources/StructuredQueriesCore/TableColumn.swift +++ b/Sources/StructuredQueriesCore/TableColumn.swift @@ -1,25 +1,34 @@ -/// A type representing a table column. -/// -/// This protocol has a single conformance, ``TableColumn``, and simply provides type erasure over -/// a table's columns. You should not conform to this protocol directly. -public protocol TableColumnExpression: QueryExpression where Value == QueryValue { +public protocol _TableColumnExpression: QueryExpression where Value == QueryValue { associatedtype Root: Table - associatedtype Value: QueryRepresentable & QueryBindable + associatedtype Value: QueryRepresentable + + var _names: [String] { get } + /// The table model key path associated with this table column. + var keyPath: KeyPath { get } +} + +/// A type representing a table column. +/// +/// This protocol provides type erasure over a table's columns. You should not conform to this +/// protocol directly. +public protocol TableColumnExpression: _TableColumnExpression +where Value: QueryBindable { /// The name of the table column. var name: String { get } /// The default value of the table column. var defaultValue: Value.QueryOutput? { get } - /// The table model key path associated with this table column. - var keyPath: KeyPath { get } - func _aliased( _ alias: Name.Type ) -> any TableColumnExpression, Value> } +extension TableColumnExpression { + public var _names: [String] { [name] } +} + /// A type representing a _writable_ table column, _i.e._ not a generated column. public protocol WritableTableColumnExpression: TableColumnExpression { func _aliased( @@ -37,8 +46,8 @@ extension WritableTableColumnExpression { /// A type representing a table column. /// -/// Don't create instances of this value directly. Instead, use the `@Table` and `@Column` macros to -/// generate values of this type. +/// Don't create instances of this value directly. Instead, use the `@Table` and `@Column` macros +/// to generate values of this type. public struct TableColumn: WritableTableColumnExpression { @@ -48,11 +57,7 @@ public struct TableColumn - - public var keyPath: KeyPath { - _keyPath - } + public let keyPath: KeyPath public init( _ name: String, @@ -61,17 +66,17 @@ public struct TableColumn, + keyPath: KeyPath, default defaultValue: Value? = nil ) where Value == Value.QueryOutput { self.name = name self.defaultValue = defaultValue - self._keyPath = keyPath + self.keyPath = keyPath } public func decode(_ decoder: inout some QueryDecoder) throws -> Value.QueryOutput { @@ -87,9 +92,42 @@ public struct TableColumn any WritableTableColumnExpression, Value> { TableColumn, Value>( name, - keyPath: \.[member: \Value.self, column: _keyPath] + keyPath: \.[member: \Value.self, column: keyPath] ) } + + public var _allColumns: [any TableColumnExpression] { [self] } + + public var _writableColumns: [any WritableTableColumnExpression] { [self] } +} + +public enum _TableColumn { + public static func `for`( + _ name: String, + keyPath: KeyPath, + default defaultValue: Value.QueryOutput? = nil + ) -> TableColumn + where Value: QueryBindable { + TableColumn(name, keyPath: keyPath, default: defaultValue) + } + + public static func `for`( + _ name: String, + keyPath: KeyPath, + default defaultValue: Value? = nil + ) -> TableColumn + where Value: QueryBindable, Value == Value.QueryOutput { + TableColumn(name, keyPath: keyPath, default: defaultValue) + } + + public static func `for`( + _: String, + keyPath: KeyPath, + default _: Value? = nil + ) -> ColumnGroup + where Value: Table, Value == Value.QueryOutput { + ColumnGroup(keyPath: keyPath) + } } /// A type that describes how a table column is generated (_e.g._, SQLite generated columns). @@ -106,8 +144,8 @@ public enum GeneratedColumnStorage { /// A type representing a generated column. /// -/// Don't create instances of this value directly. Instead, use the `@Table` and `@Column` macros to -/// generate values of this type. +/// Don't create instances of this value directly. Instead, use the `@Table` and `@Column` macros +/// to generate values of this type. public struct GeneratedColumn: TableColumnExpression { @@ -117,11 +155,7 @@ public struct GeneratedColumn - - public var keyPath: KeyPath { - _keyPath - } + public let keyPath: KeyPath public init( _ name: String, @@ -130,7 +164,7 @@ public struct GeneratedColumn Value.QueryOutput { @@ -156,7 +190,9 @@ public struct GeneratedColumn any TableColumnExpression, Value> { TableColumn, Value>( name, - keyPath: \.[member: \Value.self, column: _keyPath] + keyPath: \.[member: \Value.self, column: keyPath] ) } + + public var _allColumns: [any TableColumnExpression] { [self] } } diff --git a/Sources/StructuredQueriesCore/TableDefinition.swift b/Sources/StructuredQueriesCore/TableDefinition.swift index 81228fca..e219dd97 100644 --- a/Sources/StructuredQueriesCore/TableDefinition.swift +++ b/Sources/StructuredQueriesCore/TableDefinition.swift @@ -1,7 +1,7 @@ /// A type representing a database table's columns. /// -/// Don't conform to this protocol directly. Instead, use the `@Table` and `@Column` macros to -/// generate a conformance. +/// Don't create instances of this value directly. Instead, use the `@Table` and `@Column` macros +/// to generate values of this type. See for more information. @dynamicMemberLookup public protocol TableDefinition: QueryExpression where QueryValue: Table { /// An array of this table's columns. @@ -26,4 +26,12 @@ extension TableDefinition { ) -> Member { self[keyPath: keyPath] } + + public static var _columnWidth: Int { + QueryValue._columnWidth + } + + public var _allColumns: [any QueryExpression] { + Self.allColumns + } } diff --git a/Sources/StructuredQueriesCore/TableExpression.swift b/Sources/StructuredQueriesCore/TableExpression.swift new file mode 100644 index 00000000..e069a4cd --- /dev/null +++ b/Sources/StructuredQueriesCore/TableExpression.swift @@ -0,0 +1,31 @@ +/// An expression of table columns. +/// +/// Don't conform to this protocol directly. Instead, use the `@Table` and `@Selection` macros to +/// generate a conformance. +public protocol TableExpression: QueryExpression where QueryValue: Table { + var allColumns: [any QueryExpression] { get } +} + +extension TableExpression { + public var queryFragment: QueryFragment { + if _isSelecting { + return zip(allColumns, QueryValue.TableColumns.allColumns) + .map { "\($0) AS \(quote: $1.name)" } + .joined(separator: ", ") + } else { + return allColumns.map(\.queryFragment).joined(separator: ", ") + } + } + + public static var _columnWidth: Int { + QueryValue._columnWidth + } + + public var _allColumns: [any QueryExpression] { + allColumns + } +} + +extension Table { + public typealias Columns = Selection +} diff --git a/Sources/StructuredQueriesCore/Traits/CasePaths.swift b/Sources/StructuredQueriesCore/Traits/CasePaths.swift new file mode 100644 index 00000000..ac2807fd --- /dev/null +++ b/Sources/StructuredQueriesCore/Traits/CasePaths.swift @@ -0,0 +1,3 @@ +#if StructuredQueriesCasePaths + @_exported import CasePaths +#endif diff --git a/Sources/StructuredQueriesCore/Updates.swift b/Sources/StructuredQueriesCore/Updates.swift index c7c97fb9..44c3602c 100644 --- a/Sources/StructuredQueriesCore/Updates.swift +++ b/Sources/StructuredQueriesCore/Updates.swift @@ -53,6 +53,33 @@ public struct Updates { ) } } + + public subscript( + dynamicMember keyPath: KeyPath> + ) -> Updates { + get { Updates { _ in } } + set { updates.append(contentsOf: newValue.updates) } + } + + @_disfavoredOverload + public subscript( + dynamicMember keyPath: KeyPath> + ) -> Value.QueryOutput { + @available(*, unavailable) + get { fatalError() } + set { + func open( + _ column: some WritableTableColumnExpression + ) -> QueryFragment { + Value(queryOutput: newValue)[keyPath: column.keyPath as! KeyPath].queryFragment + } + updates.append( + contentsOf: Value.TableColumns.writableColumns.map { column in + (column.name, open(column)) + } + ) + } + } } extension Updates: QueryExpression { diff --git a/Sources/StructuredQueriesCore/_Selection.swift b/Sources/StructuredQueriesCore/_Selection.swift deleted file mode 100644 index 1d0678fb..00000000 --- a/Sources/StructuredQueriesCore/_Selection.swift +++ /dev/null @@ -1,13 +0,0 @@ -public protocol _Selection: QueryRepresentable { - associatedtype Columns: _SelectedColumns -} - -public protocol _SelectedColumns: QueryExpression { - var selection: [(aliasName: String, expression: QueryFragment)] { get } -} - -extension _SelectedColumns { - public var queryFragment: QueryFragment { - selection.map { "\($1) AS \(quote: $0)" as QueryFragment }.joined(separator: ", ") - } -} diff --git a/Sources/StructuredQueriesMacros/ColumnsMacro.swift b/Sources/StructuredQueriesMacros/ColumnsMacro.swift new file mode 100644 index 00000000..3cae2b2f --- /dev/null +++ b/Sources/StructuredQueriesMacros/ColumnsMacro.swift @@ -0,0 +1,12 @@ +import SwiftSyntax +import SwiftSyntaxMacros + +public enum ColumnsMacro: PeerMacro { + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: D, + in context: C + ) throws -> [DeclSyntax] { + [] + } +} diff --git a/Sources/StructuredQueriesMacros/Internal/DeclGroupSyntax.swift b/Sources/StructuredQueriesMacros/Internal/DeclGroupSyntax.swift new file mode 100644 index 00000000..3c8c71c1 --- /dev/null +++ b/Sources/StructuredQueriesMacros/Internal/DeclGroupSyntax.swift @@ -0,0 +1,34 @@ +import SwiftSyntax + +extension DeclGroupSyntax { + var isTableMacroSupported: Bool { + #if StructuredQueriesCasePaths + self.is(StructDeclSyntax.self) || self.is(EnumDeclSyntax.self) + #else + self.is(StructDeclSyntax.self) + #endif + } + + var declarationName: TokenSyntax? { + self.as(StructDeclSyntax.self)?.name + ?? self.as(EnumDeclSyntax.self)?.name + } + + func macroApplication(for name: String) -> AttributeSyntax? { + for attribute in attributes { + switch attribute { + case .attribute(let attr): + if attr.attributeName.tokens(viewMode: .all).map({ $0.tokenKind }) == [.identifier(name)] { + return attr + } + default: + break + } + } + return nil + } + + func hasMacroApplication(_ name: String) -> Bool { + macroApplication(for: name) != nil + } +} diff --git a/Sources/StructuredQueriesMacros/Internal/StructDeclSyntax.swift b/Sources/StructuredQueriesMacros/Internal/StructDeclSyntax.swift deleted file mode 100644 index 18fb3222..00000000 --- a/Sources/StructuredQueriesMacros/Internal/StructDeclSyntax.swift +++ /dev/null @@ -1,17 +0,0 @@ -import SwiftSyntax - -extension StructDeclSyntax { - func hasMacroApplication(_ name: String) -> Bool { - for attribute in attributes { - switch attribute { - case .attribute(let attr): - if attr.attributeName.tokens(viewMode: .all).map({ $0.tokenKind }) == [.identifier(name)] { - return true - } - default: - break - } - } - return false - } -} diff --git a/Sources/StructuredQueriesMacros/Plugin.swift b/Sources/StructuredQueriesMacros/Plugin.swift index 3995b57d..d94502fc 100644 --- a/Sources/StructuredQueriesMacros/Plugin.swift +++ b/Sources/StructuredQueriesMacros/Plugin.swift @@ -6,8 +6,8 @@ struct StructuredQueriesPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ BindMacro.self, ColumnMacro.self, + ColumnsMacro.self, EphemeralMacro.self, - SelectionMacro.self, SQLMacro.self, TableMacro.self, ] diff --git a/Sources/StructuredQueriesMacros/SelectionMacro.swift b/Sources/StructuredQueriesMacros/SelectionMacro.swift deleted file mode 100644 index 97464b6b..00000000 --- a/Sources/StructuredQueriesMacros/SelectionMacro.swift +++ /dev/null @@ -1,383 +0,0 @@ -import SwiftBasicFormat -import SwiftDiagnostics -import SwiftSyntax -import SwiftSyntaxBuilder -import SwiftSyntaxMacros - -public enum SelectionMacro {} - -extension SelectionMacro: ExtensionMacro { - public static func expansion( - of node: AttributeSyntax, - attachedTo declaration: D, - providingExtensionsOf type: T, - conformingTo protocols: [TypeSyntax], - in context: C - ) throws -> [ExtensionDeclSyntax] { - guard - let declaration = declaration.as(StructDeclSyntax.self) - else { - context.diagnose( - Diagnostic( - node: declaration.introducer, - message: MacroExpansionErrorMessage("'@Selection' can only be applied to struct types") - ) - ) - return [] - } - var allColumns: [(name: TokenSyntax, type: TypeSyntax?)] = [] - var decodings: [String] = [] - var decodingUnwrappings: [String] = [] - var decodingAssignments: [String] = [] - var diagnostics: [Diagnostic] = [] - - let selfRewriter = SelfRewriter( - selfEquivalent: type.as(IdentifierTypeSyntax.self)?.name ?? "QueryValue" - ) - for member in declaration.memberBlock.members { - guard - let property = member.decl.as(VariableDeclSyntax.self), - !property.isStatic, - !property.isComputed, - property.bindings.count == 1, - let binding = property.bindings.first, - let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed - else { continue } - - var columnQueryValueType = - (binding.typeAnnotation?.type.trimmed - ?? binding.initializer?.value.literalType) - .map { selfRewriter.rewrite($0).cast(TypeSyntax.self) } - - for attribute in property.attributes { - guard - let attribute = attribute.as(AttributeSyntax.self), - let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text, - attributeName == "Column", - case .argumentList(let arguments) = attribute.arguments - else { continue } - - for argumentIndex in arguments.indices { - let argument = arguments[argumentIndex] - - switch argument.label { - case nil: - var newArguments = arguments - newArguments.remove(at: argumentIndex) - diagnostics.append( - Diagnostic( - node: argument, - message: MacroExpansionErrorMessage( - "'@Selection' column names are not supported" - ), - fixIt: .replace( - message: MacroExpansionFixItMessage( - "Remove '\(argument.trimmed)'" - ), - oldNode: Syntax(attribute), - newNode: Syntax(attribute.with(\.arguments, .argumentList(newArguments))) - ) - ) - ) - - case .some(let label) where label.text == "as": - guard - let memberAccess = argument.expression.as(MemberAccessExprSyntax.self), - memberAccess.declName.baseName.tokenKind == .keyword(.self), - let base = memberAccess.base - else { - diagnostics.append( - Diagnostic( - node: argument.expression, - message: MacroExpansionErrorMessage("Argument 'as' must be a type literal") - ) - ) - continue - } - - columnQueryValueType = "\(raw: base.trimmedDescription)" - - case .some(let label) where label.text == "primaryKey": - guard !declaration.hasMacroApplication("Table") - else { continue } - - var newArguments = arguments - newArguments.remove(at: argumentIndex) - diagnostics.append( - Diagnostic( - node: label, - message: MacroExpansionErrorMessage( - "'@Selection' primary keys are not supported" - ), - fixIt: .replace( - message: MacroExpansionFixItMessage( - "Remove '\(argument.trimmed)'" - ), - oldNode: Syntax(attribute), - newNode: Syntax(attribute.with(\.arguments, .argumentList(newArguments))) - ) - ) - ) - - case let argument?: - fatalError("Unexpected argument: \(argument)") - } - } - } - - var assignedType: String? { - binding - .initializer? - .value - .as(FunctionCallExprSyntax.self)? - .calledExpression - .as(DeclReferenceExprSyntax.self)? - .baseName - .text - } - - allColumns.append((identifier, columnQueryValueType)) - let decodedType = columnQueryValueType?.asNonOptionalType() - decodings.append( - """ - let \(identifier) = try decoder.decode(\(decodedType.map { "\($0).self" } ?? "")) - """ - ) - if columnQueryValueType.map({ !$0.isOptionalType }) ?? true { - decodingUnwrappings.append( - """ - guard let \(identifier) else { throw QueryDecodingError.missingRequiredColumn } - """ - ) - } - decodingAssignments.append( - """ - self.\(identifier) = \(identifier) - """ - ) - } - - var conformances: [TypeSyntax] = [] - let protocolNames: [TokenSyntax] = ["_Selection"] - if let inheritanceClause = declaration.inheritanceClause { - for type in protocolNames { - if !inheritanceClause.inheritedTypes.contains(where: { - [type.text, "\(moduleName).\(type)"].contains($0.type.trimmedDescription) - }) { - conformances.append("\(moduleName).\(type)") - } - } - } else { - conformances = protocolNames.map { "\(moduleName).\($0)" } - } - - guard diagnostics.isEmpty else { - diagnostics.forEach(context.diagnose) - return [] - } - - let initDecoder: DeclSyntax = """ - public init(decoder: inout some \(moduleName).QueryDecoder) throws { - \(raw: (decodings + decodingUnwrappings + decodingAssignments).joined(separator: "\n")) - } - """ - return [ - DeclSyntax( - """ - \(declaration.attributes.availability)extension \(type)\ - \(conformances.isEmpty ? "" : ": \(conformances, separator: ", ")") { - \(initDecoder) - } - """ - ) - .cast(ExtensionDeclSyntax.self) - ] - } -} - -extension SelectionMacro: MemberMacro { - public static func expansion( - of node: AttributeSyntax, - providingMembersOf declaration: D, - conformingTo protocols: [TypeSyntax], - in context: C - ) throws -> [DeclSyntax] { - guard - let declaration = declaration.as(StructDeclSyntax.self) - else { - return [] - } - let type = IdentifierTypeSyntax(name: declaration.name.trimmed) - var allColumns: [(name: TokenSyntax, type: TypeSyntax?, default: ExprSyntax?)] = [] - var decodings: [String] = [] - var decodingUnwrappings: [String] = [] - var decodingAssignments: [String] = [] - var expansionFailed = false - - let selfRewriter = SelfRewriter(selfEquivalent: type.name) - for member in declaration.memberBlock.members { - guard - let property = member.decl.as(VariableDeclSyntax.self), - !property.isStatic, - !property.isComputed, - property.bindings.count == 1, - let binding = property.bindings.first, - let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed - else { continue } - - var columnQueryValueType = - (binding.typeAnnotation?.type.trimmed - ?? binding.initializer?.value.literalType) - .map { selfRewriter.rewrite($0).cast(TypeSyntax.self) } - - for attribute in property.attributes { - guard - let attribute = attribute.as(AttributeSyntax.self), - let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text, - attributeName == "Column", - case .argumentList(let arguments) = attribute.arguments - else { continue } - - for argumentIndex in arguments.indices { - let argument = arguments[argumentIndex] - - switch argument.label { - case nil: - var newArguments = arguments - newArguments.remove(at: argumentIndex) - expansionFailed = true - - case .some(let label) where label.text == "as": - guard - let memberAccess = argument.expression.as(MemberAccessExprSyntax.self), - memberAccess.declName.baseName.tokenKind == .keyword(.self), - let base = memberAccess.base - else { - expansionFailed = true - continue - } - - columnQueryValueType = "\(raw: base.trimmedDescription)" - - case .some(let label) where label.text == "primaryKey": - guard !declaration.hasMacroApplication("Table") - else { continue } - - var newArguments = arguments - newArguments.remove(at: argumentIndex) - expansionFailed = true - - case let argument?: - fatalError("Unexpected argument: \(argument)") - } - } - } - - var assignedType: String? { - binding - .initializer? - .value - .as(FunctionCallExprSyntax.self)? - .calledExpression - .as(DeclReferenceExprSyntax.self)? - .baseName - .text - } - - let defaultValue: ExprSyntax? = - binding.initializer.map(\.value.trimmed) - // TODO: Revisit this with multi-column support. - // ?? (columnQueryValueType?.isOptionalType == true ? ExprSyntax(NilLiteralExprSyntax()) : nil) - - allColumns.append((identifier, columnQueryValueType, defaultValue)) - let decodedType = columnQueryValueType?.asNonOptionalType() - decodings.append( - """ - let \(identifier) = try decoder.decode(\(decodedType.map { "\($0).self" } ?? "")) - """ - ) - if columnQueryValueType.map({ !$0.isOptionalType }) ?? true { - decodingUnwrappings.append( - """ - guard let \(identifier) else { throw QueryDecodingError.missingRequiredColumn } - """ - ) - } - decodingAssignments.append( - """ - self.\(identifier) = \(identifier) - """ - ) - } - - var conformances: [TypeSyntax] = [] - let protocolNames: [TokenSyntax] = ["_Selection"] - let schemaConformances: [ExprSyntax] = ["\(moduleName)._SelectedColumns"] - if let inheritanceClause = declaration.inheritanceClause { - for type in protocolNames { - if !inheritanceClause.inheritedTypes.contains(where: { - [type.text, "\(moduleName).\(type)"].contains($0.type.trimmedDescription) - }) { - conformances.append("\(moduleName).\(type)") - } - } - } else { - conformances = protocolNames.map { "\(moduleName).\($0)" } - } - - guard !expansionFailed else { - return [] - } - - let initArguments = - allColumns - .map { - """ - \($0): some \(moduleName).QueryExpression\ - \($1.map { "<\($0)>" } ?? "")\ - \($2.map { "= \(moduleName).BindQueryExpression(\($0))" } ?? "") - """ - } - .joined(separator: ",\n") - let initAssignment: [String] = - allColumns - .map { #"(\#($0.name.text.quoted()), \#($0.name).queryFragment)"# } - - return [ - """ - public struct Columns: \(schemaConformances, separator: ", ") { - public typealias QueryValue = \(type.trimmed) - public let selection: [(aliasName: String, expression: \(moduleName).QueryFragment)] - public init( - \(raw: initArguments) - ) { - self.selection = [\(raw: initAssignment.joined(separator: ", "))] - } - } - """ - ] - } -} - -extension SelectionMacro: MemberAttributeMacro { - public static func expansion( - of node: AttributeSyntax, - attachedTo declaration: D, - providingAttributesFor member: T, - in context: C - ) throws -> [AttributeSyntax] { - guard - declaration.is(StructDeclSyntax.self), - let property = member.as(VariableDeclSyntax.self), - !property.isStatic, - !property.isComputed, - !property.hasMacroApplication("Column"), - property.bindings.count == 1 - else { return [] } - return [ - """ - @Column - """ - ] - } -} diff --git a/Sources/StructuredQueriesMacros/TableMacro.swift b/Sources/StructuredQueriesMacros/TableMacro.swift index 6ba288e2..45151ae4 100644 --- a/Sources/StructuredQueriesMacros/TableMacro.swift +++ b/Sources/StructuredQueriesMacros/TableMacro.swift @@ -14,22 +14,97 @@ extension TableMacro: ExtensionMacro { conformingTo protocols: [TypeSyntax], in context: C ) throws -> [ExtensionDeclSyntax] { + if node.attributeName.identifier == "Selection", + let tableNode = declaration.macroApplication(for: "Table") + { + context.diagnose( + Diagnostic( + node: node, + message: MacroExpansionWarningMessage( + """ + '@Table' and '@Selection' should not be applied together + + Apply '@Table' to types representing stored tables, virtual tables, and database views. + + Apply '@Selection' to types representing multiple columns that can be selected from a \ + table or query, and types that represent common table expressions. + """ + ), + fixIts: [ + .replace( + message: MacroExpansionFixItMessage("Remove '@Selection'"), + oldNode: node, + newNode: TokenSyntax("") + ), + .replace( + message: MacroExpansionFixItMessage("Remove '@Table'"), + oldNode: tableNode, + newNode: TokenSyntax("") + ), + ] + ) + ) + return [] + } guard - let declaration = declaration.as(StructDeclSyntax.self) + declaration.isTableMacroSupported, + let declarationName = declaration.declarationName else { context.diagnose( Diagnostic( node: declaration.introducer, - message: MacroExpansionErrorMessage("'@Table' can only be applied to struct types") + message: MacroExpansionErrorMessage( + declaration.is(EnumDeclSyntax.self) + ? """ + '@Table' can only be applied to enum types when the 'StructuredQueriesCasePaths' \ + package trait is enabled + """ + : """ + '@Table' can only be applied to struct types (and enum types with the \ + 'StructuredQueriesCasePaths' package trait enabled) + """ + ) ) ) return [] } + if declaration.is(EnumDeclSyntax.self), !declaration.hasMacroApplication("CasePathable") { + var newAttributes: AttributeListSyntax = declaration.attributes + newAttributes.insert( + .attribute( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: "CasePathable"), + trailingTrivia: .space + ) + ), + at: newAttributes.startIndex + ) + context.diagnose( + Diagnostic( + node: node, + message: MacroExpansionErrorMessage( + """ + '@Table' enum type missing required '@CasePathable' macro application + """ + ), + fixIt: .replace( + message: MacroExpansionFixItMessage( + """ + Insert '@CasePathable' + """ + ), + oldNode: declaration.attributes, + newNode: newAttributes + ) + ) + ) + return [] + } + var allColumns: [TokenSyntax] = [] var columnsProperties: [DeclSyntax] = [] - var decodings: [String] = [] - var decodingUnwrappings: [String] = [] - var decodingAssignments: [String] = [] + var columnWidths: [ExprSyntax] = [] var diagnostics: [Diagnostic] = [] // NB: A compiler bug prevents us from applying the '@_Draft' macro directly @@ -44,7 +119,8 @@ extension TableMacro: ExtensionMacro { identifier: TokenSyntax, label: TokenSyntax?, queryOutputType: TypeSyntax?, - queryValueType: TypeSyntax? + queryValueType: TypeSyntax?, + isColumnGroup: Bool )? let selfRewriter = SelfRewriter( selfEquivalent: type.as(IdentifierTypeSyntax.self)?.name ?? "QueryValue" @@ -52,7 +128,7 @@ extension TableMacro: ExtensionMacro { var schemaName: ExprSyntax? var tableName = ExprSyntax( StringLiteralExprSyntax( - content: declaration.name.trimmed.text.lowerCamelCased().pluralized() + content: declarationName.trimmed.text.lowerCamelCased().pluralized() ) ) if case .argumentList(let arguments) = node.arguments { @@ -100,315 +176,529 @@ extension TableMacro: ExtensionMacro { } } } - for member in declaration.memberBlock.members { - guard - let property = member.decl.as(VariableDeclSyntax.self), - !property.isStatic, - !property.isComputed, - property.bindings.count == 1, - let binding = property.bindings.first, - let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed - else { continue } - - var columnName = ExprSyntax( - StringLiteralExprSyntax(content: identifier.text.trimmingBackticks()) - ) - var columnQueryValueType = - (binding.typeAnnotation?.type.trimmed - ?? binding.initializer?.value.literalType) - .map { $0.rewritten(selfRewriter) } - var columnQueryOutputType = columnQueryValueType - var isPrimaryKey = primaryKey == nil && identifier.text == "id" - var isEphemeral = false - var isGenerated = false - - for attribute in property.attributes { + + var initDecoder: DeclSyntax? + if declaration.is(StructDeclSyntax.self) { + var decodings: [String] = [] + var decodingUnwrappings: [String] = [] + var decodingAssignments: [String] = [] + for member in declaration.memberBlock.members { guard - let attribute = attribute.as(AttributeSyntax.self), - let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text + let property = member.decl.as(VariableDeclSyntax.self), + !property.isStatic, + !property.isComputed else { continue } - isEphemeral = isEphemeral || attributeName == "Ephemeral" guard - attributeName == "Column" || isEphemeral, - case .argumentList(let arguments) = attribute.arguments - else { continue } + // TODO: Support multi-binding variables where '@Column{,s}' macro is omitted? + property.bindings.count == 1, + let binding = property.bindings.first, + let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed + else { + diagnostics.append( + Diagnostic( + node: property, + message: MacroExpansionErrorMessage( + """ + Table property must contain a single value representing one or more columns + """ + ) + ) + ) + continue + } - for argumentIndex in arguments.indices { - let argument = arguments[argumentIndex] + var columnName = ExprSyntax( + StringLiteralExprSyntax(content: identifier.text.trimmingBackticks()) + ) + var columnQueryValueType = + (binding.typeAnnotation?.type.trimmed + ?? binding.initializer?.value.literalType) + .map { $0.rewritten(selfRewriter) } + var columnQueryOutputType = columnQueryValueType + var isPrimaryKey = primaryKey == nil && identifier.text == "id" + var isColumnGroup = false + var isEphemeral = false + var isExplicitColumn = false + var isGenerated = false - switch argument.label { - case nil: - if !argument.expression.isNonEmptyStringLiteral { - diagnostics.append( - Diagnostic( - node: argument.expression, - message: MacroExpansionErrorMessage("Argument must be a non-empty string literal") + for attribute in property.attributes { + guard + let attribute = attribute.as(AttributeSyntax.self), + let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text + else { continue } + isColumnGroup = isColumnGroup || attributeName == "Columns" + isEphemeral = isEphemeral || attributeName == "Ephemeral" + isExplicitColumn = isExplicitColumn || attributeName == "Column" + guard + isExplicitColumn || isEphemeral || isColumnGroup, + case .argumentList(let arguments) = attribute.arguments + else { continue } + + for argumentIndex in arguments.indices { + let argument = arguments[argumentIndex] + + switch argument.label { + case nil: + if !argument.expression.isNonEmptyStringLiteral { + diagnostics.append( + Diagnostic( + node: argument.expression, + message: MacroExpansionErrorMessage( + "Argument must be a non-empty string literal" + ) + ) ) - ) - } - columnName = argument.expression - - case .some(let label) where label.text == "as": - guard - let memberAccess = argument.expression.as(MemberAccessExprSyntax.self), - memberAccess.declName.baseName.tokenKind == .keyword(.self), - let base = memberAccess.base - else { - diagnostics.append( - Diagnostic( - node: argument.expression, - message: MacroExpansionErrorMessage("Argument 'as' must be a type literal") + } + columnName = argument.expression + + case .some(let label) where label.text == "as": + guard + let memberAccess = argument.expression.as(MemberAccessExprSyntax.self), + memberAccess.declName.baseName.tokenKind == .keyword(.self), + let base = memberAccess.base + else { + diagnostics.append( + Diagnostic( + node: argument.expression, + message: MacroExpansionErrorMessage("Argument 'as' must be a type literal") + ) ) - ) - continue - } + continue + } - columnQueryValueType = "\(raw: base.rewritten(selfRewriter).trimmedDescription)" - columnQueryOutputType = "\(columnQueryValueType).QueryOutput" + columnQueryValueType = "\(raw: base.rewritten(selfRewriter).trimmedDescription)" + columnQueryOutputType = "\(columnQueryValueType).QueryOutput" - case .some(let label) where label.text == "primaryKey": - guard - argument.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind - == .keyword(.true) - else { - isPrimaryKey = false - break - } - if let primaryKey, let originalLabel = primaryKey.label { - var newArguments = arguments - newArguments.remove(at: argumentIndex) - diagnostics.append( - Diagnostic( - node: label, - message: MacroExpansionErrorMessage( - "'@Table' only supports a single primary key" - ), - notes: [ - Note( - node: Syntax(originalLabel), - position: originalLabel.position, - message: MacroExpansionNoteMessage( - "Primary key already applied to '\(primaryKey.identifier)'" + case .some(let label) where label.text == "primaryKey": + guard + argument.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind + == .keyword(.true) + else { + isPrimaryKey = false + break + } + if let primaryKey, let originalLabel = primaryKey.label { + var newArguments = arguments + newArguments.remove(at: argumentIndex) + // TODO: Update to suggest using '@Columns' to specify a composite primary key + diagnostics.append( + Diagnostic( + node: label, + message: MacroExpansionErrorMessage( + "'@Table' only supports a single primary key" + ), + notes: [ + Note( + node: Syntax(originalLabel), + position: originalLabel.position, + message: MacroExpansionNoteMessage( + "Primary key already applied to '\(primaryKey.identifier)'" + ) ) + ], + fixIt: .replace( + message: MacroExpansionFixItMessage("Remove 'primaryKey: true'"), + oldNode: Syntax(attribute), + newNode: Syntax(attribute.with(\.arguments, .argumentList(newArguments))) ) - ], - fixIt: .replace( - message: MacroExpansionFixItMessage("Remove 'primaryKey: true'"), - oldNode: Syntax(attribute), - newNode: Syntax(attribute.with(\.arguments, .argumentList(newArguments))) ) ) + } + isPrimaryKey = true + primaryKey = ( + identifier: identifier, + label: label, + queryOutputType: columnQueryOutputType, + queryValueType: columnQueryValueType, + isColumnGroup: isColumnGroup ) - } - primaryKey = ( - identifier: identifier, - label: label, - queryOutputType: columnQueryOutputType, - queryValueType: columnQueryValueType - ) - case .some(let label) where label.text == "generated": - guard - let memberName = argument.expression.as(MemberAccessExprSyntax.self)?.declName - .baseName.text, - ["stored", "virtual"].contains(memberName) - else { - continue - } - guard property.bindingSpecifier.tokenKind == .keyword(.let) - else { - diagnostics.append( - Diagnostic( - node: property.bindingSpecifier, - message: MacroExpansionErrorMessage( - "Generated column property must be declared with a 'let'" - ), - fixIt: .replace( - message: MacroExpansionFixItMessage("Replace 'var' with 'let'"), - oldNode: Syntax(property.bindingSpecifier), - newNode: Syntax( - property.bindingSpecifier.with(\.tokenKind, .keyword(.let)) + case .some(let label) where label.text == "generated": + guard + let memberName = argument.expression.as(MemberAccessExprSyntax.self)?.declName + .baseName.text, + ["stored", "virtual"].contains(memberName) + else { + continue + } + guard property.bindingSpecifier.tokenKind == .keyword(.let) + else { + diagnostics.append( + Diagnostic( + node: property.bindingSpecifier, + message: MacroExpansionErrorMessage( + "Generated column property must be declared with a 'let'" + ), + fixIt: .replace( + message: MacroExpansionFixItMessage("Replace 'var' with 'let'"), + oldNode: Syntax(property.bindingSpecifier), + newNode: Syntax( + property.bindingSpecifier.with(\.tokenKind, .keyword(.let)) + ) ) ) ) - ) - continue - } - isGenerated = true + continue + } + isGenerated = true - case let argument?: - fatalError("Unexpected argument: \(argument)") + case let argument?: + fatalError("Unexpected argument: \(argument)") + } } } - } - guard !isEphemeral - else { continue } - - if isPrimaryKey { - primaryKey = ( - identifier: identifier, - label: nil, - queryOutputType: columnQueryOutputType, - queryValueType: columnQueryValueType - ) - } + guard !isEphemeral + else { continue } - if !isGenerated { - // NB: A compiler bug prevents us from applying the '@_Draft' macro directly - draftBindings.append((binding, columnQueryOutputType, identifier == primaryKey?.identifier)) - // NB: End of workaround - } + if isPrimaryKey { + primaryKey = ( + identifier: identifier, + label: nil, + queryOutputType: columnQueryOutputType, + queryValueType: columnQueryValueType, + isColumnGroup: isColumnGroup + ) + } - var assignedType: String? { - binding - .initializer? - .value - .as(FunctionCallExprSyntax.self)? - .calledExpression - .as(DeclReferenceExprSyntax.self)? - .baseName - .text - } + if !isGenerated { + // NB: A compiler bug prevents us from applying the '@_Draft' macro directly + draftBindings.append( + (binding, columnQueryOutputType, identifier == primaryKey?.identifier) + ) + // NB: End of workaround + } - let defaultValue = binding.initializer?.value.rewritten(selfRewriter) - if isGenerated { - columnsProperties.append( - """ - public var \(identifier): \(moduleName).GeneratedColumn<\ - QueryValue, \ - \(raw: columnQueryValueType?.trimmedDescription ?? "_")\ - > { - \(moduleName).GeneratedColumn<\ - QueryValue, \ - \(columnQueryValueType?.rewritten(selfRewriter) ?? "_")\ - >(\ - \(columnName), \ - keyPath: \\QueryValue.\(identifier)) - } - """ - ) - } else { - columnsProperties.append( - """ - public let \(identifier) = \(moduleName).TableColumn<\ - QueryValue, \ - \(columnQueryValueType?.rewritten(selfRewriter) ?? "_")\ - >(\ - \(columnName), \ - keyPath: \\QueryValue.\(identifier)\((defaultValue?.trimmedDescription).map { ", default: \($0)" } ?? "")\ + columnWidths.append("\(columnQueryValueType)._columnWidth") + + let defaultValue = + binding.initializer?.value.rewritten(selfRewriter) + ?? (columnQueryValueType?.isOptionalType == true + ? ExprSyntax(NilLiteralExprSyntax()) : nil) + let tableColumnType = + isGenerated + ? "GeneratedColumn" + : isColumnGroup + ? "ColumnGroup" + : isExplicitColumn + ? "TableColumn" + : "_TableColumn" + let tableColumnInitializer = tableColumnType == "_TableColumn" ? ".for" : "" + let defaultParameter = + isColumnGroup + ? "" + : defaultValue.map { ", default: \($0.trimmedDescription)" } ?? "" + func appendColumnProperty(primaryKey: Bool = false) { + columnsProperties.append( + """ + public let \(primaryKey ? "primaryKey" : identifier) = \ + \(moduleName).\(raw: tableColumnType)<\ + QueryValue, \ + \(raw: columnQueryValueType?.trimmedDescription ?? "_")\ + >\(raw: tableColumnInitializer)(\ + \(raw: isColumnGroup ? "" : "\(columnName), ")\ + keyPath: \\QueryValue.\(identifier)\ + \(raw: defaultParameter)\ + ) + """ ) - """ - ) + } + appendColumnProperty() + if isPrimaryKey { + appendColumnProperty(primaryKey: true) + } allColumns.append(identifier) - } - let decodedType = columnQueryValueType?.asNonOptionalType() - if let defaultValue { - decodings.append( - """ - self.\(identifier) = try decoder.decode(\(decodedType.map { "\($0).self" } ?? "")) \ - ?? \(defaultValue) - """ - ) - } else if columnQueryValueType.map({ $0.isOptionalType }) ?? false { - decodings.append( - """ - self.\(identifier) = try decoder.decode(\(decodedType.map { "\($0).self" } ?? "")) - """ - ) - } else { - decodings.append( - """ - let \(identifier) = try decoder.decode(\(decodedType.map { "\($0).self" } ?? "")) - """ - ) - decodingUnwrappings.append( - """ - guard let \(identifier) else { throw QueryDecodingError.missingRequiredColumn } - """ - ) - decodingAssignments.append( - """ - self.\(identifier) = \(identifier) - """ - ) - } + let decodedType = columnQueryValueType?.asNonOptionalType() + if let defaultValue { + decodings.append( + """ + self.\(identifier) = try decoder.decode(\(decodedType.map { "\($0).self" } ?? "")) \ + ?? \(defaultValue) + """ + ) + } else if columnQueryValueType.map({ $0.isOptionalType }) ?? false { + decodings.append( + """ + self.\(identifier) = try decoder.decode(\(decodedType.map { "\($0).self" } ?? "")) + """ + ) + } else { + decodings.append( + """ + let \(identifier) = try decoder.decode(\(decodedType.map { "\($0).self" } ?? "")) + """ + ) + decodingUnwrappings.append( + """ + guard let \(identifier) else { + throw \(moduleName).QueryDecodingError.missingRequiredColumn + } + """ + ) + decodingAssignments.append( + """ + self.\(identifier) = \(identifier) + """ + ) + } - if !isGenerated { - if let primaryKey, primaryKey.identifier == identifier { - var hasColumnAttribute = false - var property = property - for attributeIndex in property.attributes.indices { - guard - var attribute = property.attributes[attributeIndex].as(AttributeSyntax.self), - let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text, - attributeName == "Column", - case .argumentList(var arguments) = attribute.arguments - else { continue } - hasColumnAttribute = true - var hasPrimaryKeyArgument = false - for argumentIndex in arguments.indices { - var argument = arguments[argumentIndex] - defer { arguments[argumentIndex] = argument } - switch argument.label?.text { - case "as": - if var expression = argument.expression.as(MemberAccessExprSyntax.self) { - expression.base = "\(expression.base)?" - argument.expression = ExprSyntax(expression) + if !isGenerated { + if let primaryKey, primaryKey.identifier == identifier { + var property = property + for attributeIndex in property.attributes.indices { + guard + var attribute = property.attributes[attributeIndex].as(AttributeSyntax.self)? + .trimmed, + let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name + .text, + ["Column", "Columns"].contains(attributeName) + else { continue } + var hasPrimaryKeyArgument = false + var arguments: LabeledExprListSyntax = [] + if case .argumentList(let list) = attribute.arguments { arguments = list } + for argumentIndex in arguments.indices { + var argument = arguments[argumentIndex] + defer { arguments[argumentIndex] = argument } + switch argument.label?.text { + case "as": + if var expression = argument.expression.as(MemberAccessExprSyntax.self) { + expression.base = "\(expression.base)?" + argument.expression = ExprSyntax(expression) + } + + case "primaryKey": + hasPrimaryKeyArgument = true + argument.expression = ExprSyntax(BooleanLiteralExprSyntax(false)) + + default: + break } - - case "primaryKey": - hasPrimaryKeyArgument = true - argument.expression = ExprSyntax(BooleanLiteralExprSyntax(false)) - - default: - break } + if !hasPrimaryKeyArgument { + if !arguments.isEmpty { + arguments[arguments.index(before: arguments.endIndex)].trailingComma = + .commaToken( + trailingTrivia: .space + ) + } + arguments.append( + LabeledExprSyntax( + label: "primaryKey", + expression: BooleanLiteralExprSyntax(false) + ) + ) + } + if !arguments.isEmpty { + attribute.leftParen = TokenSyntax.leftParenToken() + attribute.arguments = .argumentList(arguments) + attribute.rightParen = TokenSyntax.rightParenToken() + property.attributes[attributeIndex] = .attribute(attribute) + } + } + var binding = binding + if let type = binding.typeAnnotation?.type.asOptionalType() { + binding.typeAnnotation?.type = type } - if !hasPrimaryKeyArgument { - arguments[arguments.index(before: arguments.endIndex)].trailingComma = .commaToken( - trailingTrivia: .space + property.bindings = [binding] + draftProperties.append( + DeclSyntax( + property.trimmed + .with(\.bindingSpecifier.leadingTrivia, "") + .removingAccessors() + .rewritten(selfRewriter) ) - arguments.append( - LabeledExprSyntax( - label: "primaryKey", - expression: BooleanLiteralExprSyntax(false) - ) + ) + } else { + draftProperties.append( + DeclSyntax( + property.trimmed + .with(\.bindingSpecifier.leadingTrivia, "") + .removingAccessors() + .rewritten(selfRewriter) ) - } - attribute.arguments = .argumentList(arguments) - property.attributes[attributeIndex] = .attribute(attribute) - } - if !hasColumnAttribute { - let attribute = "@Column(primaryKey: false)\n" - property.attributes.insert( - AttributeListSyntax.Element("\(raw: attribute)"), - at: property.attributes.startIndex ) } - var binding = binding - if let type = binding.typeAnnotation?.type.asOptionalType() { - binding.typeAnnotation?.type = type - } - property.bindings = [binding] - draftProperties.append( - DeclSyntax( - property.trimmed - .with(\.bindingSpecifier.leadingTrivia, "") - .removingAccessors() - .rewritten(selfRewriter) + } + } + initDecoder = """ + + public \(nonisolated)init(decoder: inout some \(moduleName).QueryDecoder) throws { + \(raw: (decodings + decodingUnwrappings + decodingAssignments).joined(separator: "\n")) + } + """ + } else if declaration.is(EnumDeclSyntax.self) { + var decodings: [String] = [] + for member in declaration.memberBlock.members { + guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { continue } + guard + // TODO: Support multi-element cases where '@Column{,s}' macro is omitted? + caseDecl.elements.count == 1, + let caseElement = caseDecl.elements.first, + let parameters = caseElement.parameterClause?.parameters, + // TODO: Support enum cases with multiple associated values? + // TODO: Support enum case with no associated value? + parameters.count == 1, + let parameter = parameters.first + else { + diagnostics.append( + Diagnostic( + node: caseDecl, + message: MacroExpansionErrorMessage( + """ + Table case must contain a single associated value representing one or more \ + optional columns + """ + ) ) ) - } else { - draftProperties.append( - DeclSyntax( - property.trimmed - .with(\.bindingSpecifier.leadingTrivia, "") - .removingAccessors() - .rewritten(selfRewriter) + continue + } + + let identifier = caseElement.name + var columnName = ExprSyntax( + StringLiteralExprSyntax(content: identifier.text.trimmingBackticks()) + ) + var columnQueryValueType = parameter.type.trimmed.rewritten(selfRewriter) + var isColumnGroup = false + var isExplicitColumn = false + + for attribute in caseDecl.attributes { + guard + let attribute = attribute.as(AttributeSyntax.self), + let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text + else { continue } + guard + attributeName != "Ephemeral" + else { + diagnostics.append( + Diagnostic( + node: attribute, + message: MacroExpansionErrorMessage("Table case cannot be ephemeral"), + fixIt: .replace( + message: MacroExpansionFixItMessage("Remove '@Ephemeral'"), + oldNode: attribute, + newNode: TokenSyntax("") + ) + ) + ) + continue + } + isColumnGroup = isColumnGroup || attributeName == "Columns" + isExplicitColumn = isExplicitColumn || attributeName == "Column" + guard + isExplicitColumn || isColumnGroup, + case .argumentList(let arguments) = attribute.arguments + else { continue } + + for argumentIndex in arguments.indices { + let argument = arguments[argumentIndex] + + switch argument.label { + case nil: + if !argument.expression.isNonEmptyStringLiteral { + diagnostics.append( + Diagnostic( + node: argument.expression, + message: MacroExpansionErrorMessage( + "Argument must be a non-empty string literal" + ) + ) + ) + } + columnName = argument.expression + + case .some(let label) where label.text == "as": + guard + let memberAccess = argument.expression.as(MemberAccessExprSyntax.self), + memberAccess.declName.baseName.tokenKind == .keyword(.self), + let base = memberAccess.base + else { + diagnostics.append( + Diagnostic( + node: argument.expression, + message: MacroExpansionErrorMessage("Argument 'as' must be a type literal") + ) + ) + continue + } + + columnQueryValueType = "\(raw: base.rewritten(selfRewriter).trimmedDescription)" + + case .some(let label) where label.text == "primaryKey": + diagnostics.append( + Diagnostic( + node: argument.expression, + message: MacroExpansionErrorMessage( + "Argument 'primaryKey' is not supported on enum table columns" + ) + ) + ) + continue + + case .some(let label) where label.text == "generated": + diagnostics.append( + Diagnostic( + node: argument.expression, + message: MacroExpansionErrorMessage( + "Argument 'generated' is not supported on enum table columns" + ) + ) + ) + continue + + case let argument?: + fatalError("Unexpected argument: \(argument)") + } + } + } + + columnWidths.append("\(columnQueryValueType)._columnWidth") + + let defaultValue = parameter.defaultValue?.value.rewritten(selfRewriter) + let tableColumnType = + isColumnGroup + ? "ColumnGroup" + : isExplicitColumn + ? "TableColumn" + : "_TableColumn" + let tableColumnInitializer = tableColumnType == "_TableColumn" ? ".for" : "" + let defaultParameter = + isColumnGroup + ? "" + : defaultValue.map { ", default: \($0.trimmedDescription)" } ?? "" + func appendColumnProperty(primaryKey: Bool = false) { + columnsProperties.append( + """ + public let \(primaryKey ? "primaryKey" : identifier) = \ + \(moduleName).\(raw: tableColumnType)<\ + QueryValue, \ + \(raw: columnQueryValueType.trimmedDescription)?\ + >\(raw: tableColumnInitializer)(\ + \(raw: isColumnGroup ? "" : "\(columnName), ")\ + keyPath: \\QueryValue.\(identifier)\ + \(raw: defaultParameter)\ ) + """ ) } + appendColumnProperty() + allColumns.append(identifier) + let decodedType = columnQueryValueType.asNonOptionalType() + decodings.append( + """ + if let \(identifier) = try decoder.decode(\(decodedType).self) { + self = .\(identifier)(\(identifier)) + } + """ + ) } + initDecoder = """ + + public \(nonisolated)init(decoder: inout some \(moduleName).QueryDecoder) throws { + \(raw: decodings.joined(separator: " else ")) else { + throw \(moduleName).QueryDecodingError.missingRequiredColumn + } + } + """ } var draft: DeclSyntax? @@ -420,13 +710,7 @@ extension TableMacro: ExtensionMacro { \(allColumns.map { "self.\($0) = other.\($0)" as ExprSyntax }, separator: "\n") } """ - } else if let primaryKey { - columnsProperties.append( - """ - public var primaryKey: \(moduleName).TableColumn \ - { self.\(primaryKey.identifier) } - """ - ) + } else if primaryKey != nil { draft = """ @_Draft(\(type).self) @@ -477,32 +761,25 @@ extension TableMacro: ExtensionMacro { memberwise initializer """ ), - fixIts: [ - FixIt( - message: MacroExpansionFixItMessage( - """ - Insert ': <#Type#>' - """ - ), - changes: [ - .replace( - oldNode: Syntax(binding), - newNode: Syntax( - binding - .with(\.pattern.trailingTrivia, "") - .with( - \.typeAnnotation, - TypeAnnotationSyntax( - colon: .colonToken(trailingTrivia: .space), - type: IdentifierTypeSyntax(name: "<#Type#>"), - trailingTrivia: .space - ) - ) - ) + fixIt: .replace( + message: MacroExpansionFixItMessage( + """ + Insert ': <#Type#>' + """ + ), + oldNode: binding, + newNode: + binding + .with(\.pattern.trailingTrivia, "") + .with( + \.typeAnnotation, + TypeAnnotationSyntax( + colon: .colonToken(trailingTrivia: .space), + type: IdentifierTypeSyntax(name: "<#Type#>"), + trailingTrivia: .space ) - ] - ) - ] + ) + ) ) ) continue @@ -532,10 +809,13 @@ extension TableMacro: ExtensionMacro { } var conformances: [TypeSyntax] = [] - let protocolNames: [TokenSyntax] = + var protocolNames: [TokenSyntax] = primaryKey != nil ? ["Table", "PrimaryKeyedTable"] : ["Table"] + if node.attributeName.identifier == "Selection" { + protocolNames.append("_Selection") + } if let inheritanceClause = declaration.inheritanceClause { for type in protocolNames { if !inheritanceClause.inheritedTypes.contains(where: { @@ -553,9 +833,13 @@ extension TableMacro: ExtensionMacro { Diagnostic( node: node, message: MacroExpansionErrorMessage( - """ - '@Table' requires at least one stored column property to be defined on '\(type)' - """ + declaration.is(EnumDeclSyntax.self) + ? """ + '@Table' requires at least one case to be defined on '\(type)' + """ + : """ + '@Table' requires at least one stored column property to be defined on '\(type)' + """ ) ) ) @@ -574,26 +858,17 @@ extension TableMacro: ExtensionMacro { public \(nonisolated)static let schemaName: Swift.String? = \(schemaName) """ } - var initDecoder: DeclSyntax? - if declaration.hasMacroApplication("Selection") { - conformances.append("\(moduleName).PartialSelectStatement") - statics.append(contentsOf: [ - """ - - public typealias QueryValue = Self - """, - """ - public typealias From = Swift.Never - """, - ]) - } else { - initDecoder = """ + conformances.append("\(moduleName).PartialSelectStatement") + statics.append(contentsOf: [ + """ - public \(nonisolated)init(decoder: inout some \(moduleName).QueryDecoder) throws { - \(raw: (decodings + decodingUnwrappings + decodingAssignments).joined(separator: "\n")) - } - """ - } + public typealias QueryValue = Self + """, + """ + public typealias From = Swift.Never + """, + ]) + let columnWidth: ExprSyntax = "[\(columnWidths, separator: ", ")].reduce(0, +)" return [ DeclSyntax( @@ -602,7 +877,9 @@ extension TableMacro: ExtensionMacro { \(conformances.isEmpty ? "" : ": \(conformances, separator: ", ")") {\ \(statics, separator: "\n") public \(nonisolated)static var columns: TableColumns { TableColumns() } - public \(nonisolated)static var tableName: String { \(tableName) }\(letSchemaName)\(initDecoder)\(initFromOther) + public \(nonisolated)static var _columnWidth: Int { \(columnWidth) } + public \(nonisolated)static var tableName: String { \(tableName) }\ + \(letSchemaName)\(initDecoder)\(initFromOther) } """ ) @@ -618,19 +895,22 @@ extension TableMacro: MemberMacro { conformingTo protocols: [TypeSyntax], in context: C ) throws -> [DeclSyntax] { + if node.attributeName.identifier == "Selection", declaration.hasMacroApplication("Table") { + return [] + } guard - let declaration = declaration.as(StructDeclSyntax.self) + declaration.isTableMacroSupported, + let declarationName = declaration.declarationName else { return [] } - let type = IdentifierTypeSyntax(name: declaration.name.trimmed) - var allColumns: [TokenSyntax] = [] + let type = IdentifierTypeSyntax(name: declarationName.trimmed) + var allColumns: + [(name: TokenSyntax, firstName: TokenSyntax, type: TypeSyntax?, default: ExprSyntax?)] = [] + var allColumnNames: [TokenSyntax] = [] var writableColumns: [TokenSyntax] = [] var selectedColumns: [TokenSyntax] = [] var columnsProperties: [DeclSyntax] = [] - var decodings: [String] = [] - var decodingUnwrappings: [String] = [] - var decodingAssignments: [String] = [] var expansionFailed = false // NB: A compiler bug prevents us from applying the '@_Draft' macro directly @@ -644,282 +924,412 @@ extension TableMacro: MemberMacro { identifier: TokenSyntax, label: TokenSyntax?, queryOutputType: TypeSyntax?, - queryValueType: TypeSyntax? + queryValueType: TypeSyntax?, + isColumnGroup: Bool )? let selfRewriter = SelfRewriter(selfEquivalent: type.name) - for member in declaration.memberBlock.members { - guard - let property = member.decl.as(VariableDeclSyntax.self), - !property.isStatic, - !property.isComputed, - property.bindings.count == 1, - let binding = property.bindings.first, - let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed - else { continue } - - var columnName = ExprSyntax( - StringLiteralExprSyntax(content: identifier.text.trimmingBackticks()) - ) - var columnQueryValueType = - (binding.typeAnnotation?.type.trimmed - ?? binding.initializer?.value.literalType) - .map { $0.rewritten(selfRewriter) } - var columnQueryOutputType = columnQueryValueType - var isPrimaryKey = primaryKey == nil && identifier.text == "id" - var isEphemeral = false - var isGenerated = false - - for attribute in property.attributes { - guard - let attribute = attribute.as(AttributeSyntax.self), - let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text - else { continue } - isEphemeral = isEphemeral || attributeName == "Ephemeral" + var selectionInitializers: [DeclSyntax] = [] + if declaration.is(StructDeclSyntax.self) { + for member in declaration.memberBlock.members { guard - attributeName == "Column" || isEphemeral, - case .argumentList(let arguments) = attribute.arguments + let property = member.decl.as(VariableDeclSyntax.self), + !property.isStatic, + !property.isComputed, + property.bindings.count == 1, + let binding = property.bindings.first, + let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.trimmed else { continue } - for argumentIndex in arguments.indices { - let argument = arguments[argumentIndex] + var columnName = ExprSyntax( + StringLiteralExprSyntax(content: identifier.text.trimmingBackticks()) + ) + var columnQueryValueType = + (binding.typeAnnotation?.type.trimmed + ?? binding.initializer?.value.literalType) + .map { $0.rewritten(selfRewriter) } + var columnQueryOutputType = columnQueryValueType + var isPrimaryKey = + primaryKey == nil + && identifier.text == "id" + && node.attributeName.identifier != "_Draft" + var isColumnGroup = false + var isEphemeral = false + var isExplicitColumn = false + var isGenerated = false - switch argument.label { - case nil: - if !argument.expression.isNonEmptyStringLiteral { - expansionFailed = true - } - columnName = argument.expression - - case .some(let label) where label.text == "as": - guard - let memberAccess = argument.expression.as(MemberAccessExprSyntax.self), - memberAccess.declName.baseName.tokenKind == .keyword(.self), - let base = memberAccess.base - else { - expansionFailed = true - continue - } + for attribute in property.attributes { + guard + let attribute = attribute.as(AttributeSyntax.self), + let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text + else { continue } + isColumnGroup = isColumnGroup || attributeName == "Columns" + isEphemeral = isEphemeral || attributeName == "Ephemeral" + isExplicitColumn = isExplicitColumn || attributeName == "Column" + guard + isExplicitColumn || isEphemeral || isColumnGroup, + case .argumentList(let arguments) = attribute.arguments + else { continue } - columnQueryValueType = "\(raw: base.rewritten(selfRewriter).trimmedDescription)" - columnQueryOutputType = "\(columnQueryValueType).QueryOutput" + for argumentIndex in arguments.indices { + let argument = arguments[argumentIndex] - case .some(let label) where label.text == "primaryKey": - guard - argument.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind - == .keyword(.true) - else { - isPrimaryKey = false - break - } - if primaryKey != nil { - var newArguments = arguments - newArguments.remove(at: argumentIndex) - expansionFailed = true - } - primaryKey = ( - identifier: identifier, - label: label, - queryOutputType: columnQueryOutputType, - queryValueType: columnQueryValueType - ) + switch argument.label { + case nil: + if !argument.expression.isNonEmptyStringLiteral { + expansionFailed = true + } + columnName = argument.expression + + case .some(let label) where label.text == "as": + guard + let memberAccess = argument.expression.as(MemberAccessExprSyntax.self), + memberAccess.declName.baseName.tokenKind == .keyword(.self), + let base = memberAccess.base + else { + expansionFailed = true + continue + } - case .some(let label) where label.text == "generated": - guard - let memberName = argument.expression.as(MemberAccessExprSyntax.self)?.declName - .baseName.text, - ["stored", "virtual"].contains(memberName) - else { continue } - isGenerated = true + columnQueryValueType = "\(raw: base.rewritten(selfRewriter).trimmedDescription)" + columnQueryOutputType = "\(columnQueryValueType).QueryOutput" - case let argument?: - fatalError("Unexpected argument: \(argument)") + case .some(let label) where label.text == "primaryKey": + guard + argument.expression.as(BooleanLiteralExprSyntax.self)?.literal.tokenKind + == .keyword(.true) + else { + isPrimaryKey = false + break + } + isPrimaryKey = true + if primaryKey != nil { + var newArguments = arguments + newArguments.remove(at: argumentIndex) + expansionFailed = true + } + primaryKey = ( + identifier: identifier, + label: label, + queryOutputType: columnQueryOutputType, + queryValueType: columnQueryValueType, + isColumnGroup: isColumnGroup + ) + + case .some(let label) where label.text == "generated": + guard + let memberName = argument.expression.as(MemberAccessExprSyntax.self)?.declName + .baseName.text, + ["stored", "virtual"].contains(memberName) + else { continue } + isGenerated = true + + case let argument?: + fatalError("Unexpected argument: \(argument)") + } } } - } - guard !isEphemeral - else { continue } - - if isPrimaryKey { - primaryKey = ( - identifier: identifier, - label: nil, - queryOutputType: columnQueryOutputType, - queryValueType: columnQueryValueType - ) - } + guard !isEphemeral + else { continue } - selectedColumns.append(identifier) + if isPrimaryKey { + primaryKey = ( + identifier: identifier, + label: nil, + queryOutputType: columnQueryOutputType, + queryValueType: columnQueryValueType, + isColumnGroup: isColumnGroup + ) + } - if !isGenerated { - // NB: A compiler bug prevents us from applying the '@_Draft' macro directly - draftBindings.append((binding, columnQueryOutputType, identifier == primaryKey?.identifier)) - // NB: End of workaround - } + selectedColumns.append(identifier) - var assignedType: String? { - binding - .initializer? - .value - .as(FunctionCallExprSyntax.self)? - .calledExpression - .as(DeclReferenceExprSyntax.self)? - .baseName - .text - } + if !isGenerated { + // NB: A compiler bug prevents us from applying the '@_Draft' macro directly + draftBindings.append( + (binding, columnQueryOutputType, identifier == primaryKey?.identifier) + ) + // NB: End of workaround + } - let defaultValue = binding.initializer?.value.rewritten(selfRewriter) - if isGenerated { - columnsProperties.append( - """ - public var \(identifier): \(moduleName).GeneratedColumn<\ - QueryValue, \ - \(raw: columnQueryValueType?.trimmedDescription ?? "_")\ - > { \ - \(moduleName).GeneratedColumn<\ - QueryValue, \ - \(columnQueryValueType?.rewritten(selfRewriter) ?? "_")\ - >(\ - \(columnName), \ - keyPath: \\QueryValue.\(identifier)\ + let defaultValue = + binding.initializer?.value.rewritten(selfRewriter) + ?? (columnQueryValueType?.isOptionalType == true + ? ExprSyntax(NilLiteralExprSyntax()) : nil) + let tableColumnType = + isGenerated + ? "GeneratedColumn" + : isColumnGroup + ? "ColumnGroup" + : isExplicitColumn + ? "TableColumn" + : "_TableColumn" + let tableColumnInitializer = tableColumnType == "_TableColumn" ? ".for" : "" + let defaultParameter = + isColumnGroup + ? "" + : defaultValue.map { ", default: \($0.trimmedDescription)" } ?? "" + func appendColumnProperty(primaryKey: Bool = false) { + columnsProperties.append( + """ + public let \(primaryKey ? "primaryKey" : identifier) = \ + \(moduleName).\(raw: tableColumnType)<\ + QueryValue, \ + \(raw: columnQueryValueType?.trimmedDescription ?? "_")\ + >\(raw: tableColumnInitializer)(\ + \(raw: isColumnGroup ? "" : "\(columnName), ")\ + keyPath: \\QueryValue.\(identifier)\ + \(raw: defaultParameter)\ + ) + """ ) + } + appendColumnProperty() + if isPrimaryKey { + appendColumnProperty(primaryKey: true) + } + allColumns.append((identifier, "_", columnQueryValueType, defaultValue?.trimmed)) + allColumnNames.append(identifier) + if !isGenerated { + writableColumns.append(identifier) + if let primaryKey, primaryKey.identifier == identifier { + var property = property + for attributeIndex in property.attributes.indices { + guard + var attribute = property.attributes[attributeIndex].as(AttributeSyntax.self)? + .trimmed, + let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name + .text, + ["Column", "Columns"].contains(attributeName) + else { continue } + var hasPrimaryKeyArgument = false + var arguments: LabeledExprListSyntax = [] + if case .argumentList(let list) = attribute.arguments { arguments = list } + for argumentIndex in arguments.indices { + var argument = arguments[argumentIndex] + defer { arguments[argumentIndex] = argument } + switch argument.label?.text { + case "as": + if var expression = argument.expression.as(MemberAccessExprSyntax.self) { + expression.base = "\(expression.base)?" + argument.expression = ExprSyntax(expression) + } + + case "primaryKey": + hasPrimaryKeyArgument = true + argument.expression = ExprSyntax(BooleanLiteralExprSyntax(false)) + + default: + break + } + } + if !hasPrimaryKeyArgument { + if !arguments.isEmpty { + arguments[arguments.index(before: arguments.endIndex)].trailingComma = + .commaToken( + trailingTrivia: .space + ) + } + arguments.append( + LabeledExprSyntax( + label: "primaryKey", + expression: BooleanLiteralExprSyntax(false) + ) + ) + } + if !arguments.isEmpty { + attribute.leftParen = TokenSyntax.leftParenToken() + attribute.arguments = .argumentList(arguments) + attribute.rightParen = TokenSyntax.rightParenToken() + property.attributes[attributeIndex] = .attribute(attribute) + } + } + property = property.trimmed + var binding = binding + if let type = binding.typeAnnotation?.type.asOptionalType() { + binding.typeAnnotation?.type = type + } + property.bindings = [binding] + draftProperties.append( + DeclSyntax( + property + .with(\.bindingSpecifier.leadingTrivia, "") + .removingAccessors() + .rewritten(selfRewriter) + ) + ) + } else { + draftProperties.append( + DeclSyntax( + property.trimmed + .with(\.attributes.trailingTrivia, .space) + .with(\.bindingSpecifier.leadingTrivia, "") + .removingAccessors() + .rewritten(selfRewriter) + ) + ) } - """ - ) - allColumns.append(identifier) - } else { - columnsProperties.append( - """ - public let \(identifier) = \(moduleName).TableColumn<\ - QueryValue, \ - \(columnQueryValueType?.rewritten(selfRewriter) ?? "_")\ - >(\ - \(columnName), \ - keyPath: \\QueryValue.\(identifier)\((defaultValue?.trimmedDescription).map { ", default: \($0)" } ?? "")\ - ) - """ - ) - allColumns.append(identifier) - writableColumns.append(identifier) + } } - let decodedType = columnQueryValueType?.asNonOptionalType() - if let defaultValue { - decodings.append( - """ - self.\(identifier) = try decoder.decode(\(decodedType.map { "\($0).self" } ?? "")) \ - ?? \(defaultValue) - """ - ) - } else if columnQueryValueType.map({ $0.isOptionalType }) ?? false { - decodings.append( - """ - self.\(identifier) = try decoder.decode(\(decodedType.map { "\($0).self" } ?? "")) - """ - ) - } else { - decodings.append( - """ - let \(identifier) = try decoder.decode(\(decodedType.map { "\($0).self" } ?? "")) - """ - ) - decodingUnwrappings.append( - """ - guard let \(identifier) else { throw QueryDecodingError.missingRequiredColumn } - """ - ) - decodingAssignments.append( - """ - self.\(identifier) = \(identifier) - """ + let selectionInitArguments = + allColumns + .map { name, _, type, `default` in + var query = "\(name): some \(moduleName).QueryExpression" + if let type { + query.append("<\(type)>") + if let `default` { + query.append(" = \(type)(queryOutput: \(`default`))") + } + } + return query + } + .joined(separator: ",\n") + + let selectionAssignment = + selectedColumns + .map { "allColumns.append(contentsOf: \($0)._allColumns)\n" } + .joined() + + selectionInitializers.append( + """ + public init( + \(raw: selectionInitArguments) + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + \(raw: selectionAssignment)self.allColumns = allColumns + } + """ + ) + } else if declaration.is(EnumDeclSyntax.self) { + for member in declaration.memberBlock.members { + guard + let caseDecl = member.decl.as(EnumCaseDeclSyntax.self), + caseDecl.elements.count == 1, + let caseElement = caseDecl.elements.first, + let parameters = caseElement.parameterClause?.parameters, + parameters.count == 1, + let parameter = parameters.first + else { continue } + + let identifier = caseElement.name + var columnName = ExprSyntax( + StringLiteralExprSyntax(content: identifier.text.trimmingBackticks()) ) - } + var columnQueryValueType = parameter.type.trimmed.rewritten(selfRewriter) + var isColumnGroup = false + var isExplicitColumn = false - if !isGenerated { - if let primaryKey, primaryKey.identifier == identifier { - var hasColumnAttribute = false - var property = property - for attributeIndex in property.attributes.indices { - guard - var attribute = property.attributes[attributeIndex].as(AttributeSyntax.self), - let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text, - attributeName == "Column", - case .argumentList(var arguments) = attribute.arguments - else { continue } - hasColumnAttribute = true - var hasPrimaryKeyArgument = false - for argumentIndex in arguments.indices { - var argument = arguments[argumentIndex] - defer { arguments[argumentIndex] = argument } - switch argument.label?.text { - case "as": - if var expression = argument.expression.as(MemberAccessExprSyntax.self) { - expression.base = "\(expression.base)?" - argument.expression = ExprSyntax(expression) - } + for attribute in caseDecl.attributes { + guard + let attribute = attribute.as(AttributeSyntax.self), + let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text + else { continue } + isExplicitColumn = isExplicitColumn || attributeName == "Column" + isColumnGroup = isColumnGroup || attributeName == "Columns" + guard + isExplicitColumn || isColumnGroup, + case .argumentList(let arguments) = attribute.arguments + else { continue } - case "primaryKey": - hasPrimaryKeyArgument = true - argument.expression = ExprSyntax(BooleanLiteralExprSyntax(false)) + for argumentIndex in arguments.indices { + let argument = arguments[argumentIndex] - default: - break + switch argument.label { + case nil: + if !argument.expression.isNonEmptyStringLiteral { + expansionFailed = true } + columnName = argument.expression + + case .some(let label) where label.text == "as": + guard + let memberAccess = argument.expression.as(MemberAccessExprSyntax.self), + memberAccess.declName.baseName.tokenKind == .keyword(.self), + let base = memberAccess.base + else { + expansionFailed = true + continue + } + + columnQueryValueType = "\(raw: base.rewritten(selfRewriter).trimmedDescription)" + + case .some(let label) where label.text == "primaryKey": + expansionFailed = true + + case .some(let label) where label.text == "generated": + expansionFailed = true + + case let argument?: + fatalError("Unexpected argument: \(argument)") } - if !hasPrimaryKeyArgument { - arguments[arguments.index(before: arguments.endIndex)].trailingComma = .commaToken( - trailingTrivia: .space - ) - arguments.append( - LabeledExprSyntax( - label: "primaryKey", - expression: BooleanLiteralExprSyntax(false) - ) - ) - } - attribute.arguments = .argumentList(arguments) - property.attributes[attributeIndex] = .attribute(attribute) - } - property = property.trimmed - if !hasColumnAttribute { - let attribute = "@Column(primaryKey: false)\n" - property.attributes.insert( - AttributeListSyntax.Element("\(raw: attribute)"), - at: property.attributes.startIndex - ) - } - var binding = binding - if let type = binding.typeAnnotation?.type.asOptionalType() { - binding.typeAnnotation?.type = type } - property.bindings = [binding] - draftProperties.append( - DeclSyntax( - property - .with(\.bindingSpecifier.leadingTrivia, "") - .removingAccessors() - .rewritten(selfRewriter) - ) - ) - } else { - draftProperties.append( - DeclSyntax( - property.trimmed - .with(\.bindingSpecifier.leadingTrivia, "") - .removingAccessors() - .rewritten(selfRewriter) + } + + selectedColumns.append(identifier) + + let defaultValue = parameter.defaultValue?.value.rewritten(selfRewriter) + let tableColumnType = + isColumnGroup + ? "ColumnGroup" + : isExplicitColumn + ? "TableColumn" + : "_TableColumn" + let tableColumnInitializer = tableColumnType == "_TableColumn" ? ".for" : "" + let defaultParameter = + isColumnGroup + ? "" + : defaultValue.map { ", default: \($0.trimmedDescription)" } ?? "" + func appendColumnProperty(primaryKey: Bool = false) { + columnsProperties.append( + """ + public let \(primaryKey ? "primaryKey" : identifier) = \ + \(moduleName).\(raw: tableColumnType)<\ + QueryValue, \ + \(raw: columnQueryValueType.trimmedDescription)?\ + >\(raw: tableColumnInitializer)(\ + \(raw: isColumnGroup ? "" : "\(columnName), ")\ + keyPath: \\QueryValue.\(identifier)\ + \(raw: defaultParameter)\ ) + """ ) } + appendColumnProperty() + allColumns.append( + (identifier, parameter.firstName ?? "_", columnQueryValueType, defaultValue?.trimmed) + ) + allColumnNames.append(identifier) + writableColumns.append(identifier) + } + for (identifier, firstName, valueType, defaultValue) in allColumns { + var argument = """ + \(firstName) \(identifier): some \(moduleName).QueryExpression<\(type)> + """ + if let defaultValue { + argument.append(" = \(type)(queryOutput: \(defaultValue))") + } + let staticColumns = selectedColumns.map { + $0 == identifier ? "\($0)" : "\(valueType)?(queryOutput: nil)" as ExprSyntax + } + let staticInitialization = + staticColumns + .map { "allColumns.append(contentsOf: \($0)._allColumns)\n" } + .joined() + + selectionInitializers.append( + """ + public static func \(identifier)( + \(firstName) \(identifier): some \(moduleName).QueryExpression<\(valueType)> + ) -> Self { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + \(raw: staticInitialization)return Self(allColumns: allColumns) + } + """ + ) } } var draft: DeclSyntax? - if let primaryKey { - columnsProperties.append( - """ - public var primaryKey: \(moduleName).TableColumn \ - { self.\(primaryKey.identifier) } - """ - ) + if primaryKey != nil { draft = """ @_Draft(\(type).self) @@ -1014,35 +1424,58 @@ extension TableMacro: MemberMacro { } var typeAliases: [DeclSyntax] = [] - if declaration.hasMacroApplication("Selection") { - conformances.append("\(moduleName).PartialSelectStatement") - typeAliases.append(contentsOf: [ - """ + conformances.append("\(moduleName).PartialSelectStatement") + typeAliases.append(contentsOf: [ + """ - public typealias QueryValue = Self - """, - """ - public typealias From = Swift.Never - """, - ]) + public typealias QueryValue = Self + """, + """ + public typealias From = Swift.Never + """, + ]) + + let primaryKeyTypealias: DeclSyntax? = primaryKey.map { + """ + + public typealias PrimaryKey = \($0.queryValueType) + """ } + let allColumnsAssignment = + allColumnNames + .map { "allColumns.append(contentsOf: QueryValue.columns.\($0)._allColumns)\n" } + .joined() + let writableColumnsAssignment = + writableColumns + .map { "writableColumns.append(contentsOf: QueryValue.columns.\($0)._writableColumns)\n" } + .joined() + return [ """ public \(nonisolated)struct TableColumns: \(schemaConformances, separator: ", ") { - public typealias QueryValue = \(type.trimmed) + public typealias QueryValue = \(type.trimmed)\(primaryKeyTypealias) \(columnsProperties, separator: "\n") - public static var allColumns: [any \(moduleName).TableColumnExpression] { \ - [\(allColumns.map { "QueryValue.columns.\($0)" as ExprSyntax }, separator: ", ")] + public static var allColumns: [any \(moduleName).TableColumnExpression] { + var allColumns: [any \(moduleName).TableColumnExpression] = [] + \(raw: allColumnsAssignment)return allColumns } - public static var writableColumns: [any \(moduleName).WritableTableColumnExpression] { \ - [\(writableColumns.map { "QueryValue.columns.\($0)" as ExprSyntax }, separator: ", ")] + public static var writableColumns: [any \(moduleName).WritableTableColumnExpression] { + var writableColumns: [any \(moduleName).WritableTableColumnExpression] = [] + \(raw: writableColumnsAssignment)return writableColumns } public var queryFragment: QueryFragment { "\(raw: selectedColumns.map { #"\(self.\#($0))"# }.joined(separator: ", "))" } } """, + """ + public struct Selection: \(moduleName).TableExpression { + public typealias QueryValue = \(type.trimmed) + public let allColumns: [any \(moduleName).QueryExpression] + \(selectionInitializers, separator: "\n") + } + """, draft, ] .compactMap { $0 } @@ -1056,12 +1489,16 @@ extension TableMacro: MemberAttributeMacro { providingAttributesFor member: T, in context: C ) throws -> [AttributeSyntax] { + if node.attributeName.identifier == "Selection", declaration.hasMacroApplication("Table") { + return [] + } guard declaration.is(StructDeclSyntax.self), let property = member.as(VariableDeclSyntax.self), !property.isStatic, !property.isComputed, !property.hasMacroApplication("Column"), + !property.hasMacroApplication("Columns"), !property.hasMacroApplication("Ephemeral"), property.bindings.count == 1, let binding = property.bindings.first, diff --git a/Sources/StructuredQueriesSQLite/Macros.swift b/Sources/StructuredQueriesSQLite/Macros.swift index c69d06db..33d0deec 100644 --- a/Sources/StructuredQueriesSQLite/Macros.swift +++ b/Sources/StructuredQueriesSQLite/Macros.swift @@ -26,7 +26,7 @@ public macro DatabaseFunction( /// - isDeterministic: Whether or not the function is deterministic (or "pure" or "referentially /// transparent"), _i.e._ given an input it will always return the same output. @attached(peer, names: overloaded, prefixed(`$`)) -public macro DatabaseFunction( +public macro DatabaseFunction( _ name: String = "", as representableFunctionType: ((repeat each T) -> R).Type, isDeterministic: Bool = false @@ -45,7 +45,7 @@ public macro DatabaseFunction( /// - isDeterministic: Whether or not the function is deterministic (or "pure" or "referentially /// transparent"), _i.e._ given an input it will always return the same output. @attached(peer, names: overloaded, prefixed(`$`)) -public macro DatabaseFunction( +public macro DatabaseFunction( _ name: String = "", as representableFunctionType: ((repeat each T) -> Void).Type, isDeterministic: Bool = false diff --git a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift index 14df554b..a7b774a0 100644 --- a/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift +++ b/Sources/StructuredQueriesSQLiteCore/DatabaseFunction.swift @@ -25,12 +25,12 @@ public protocol DatabaseFunction { /// Don't conform to this protocol directly. Instead, use the `@DatabaseFunction` macro to generate /// a conformance. public protocol ScalarDatabaseFunction: DatabaseFunction { - /// The function body. Transforms an array of bindings handed to the function into a binding - /// returned to the query. + /// The function body. Uses a query decoder to process the input of a database function into a + /// bindable value. /// - /// - Parameter arguments: Arguments passed to the database function. - /// - Returns: A value returned from the database function. - func invoke(_ arguments: [QueryBinding]) -> QueryBinding + /// - Parameter decoder: A query decoder. + /// - Returns: A binding returned from the database function. + func invoke(_ decoder: inout some QueryDecoder) throws -> QueryBinding } extension ScalarDatabaseFunction { @@ -43,8 +43,10 @@ extension ScalarDatabaseFunction { _ input: repeat each T ) -> some QueryExpression where Input == (repeat (each T).QueryValue) { - SQLQueryExpression( - "\(quote: name)(\(Array(repeat each input).joined(separator: ", ")))" - ) + $_isSelecting.withValue(false) { + SQLQueryExpression( + "\(quote: name)(\(Array(repeat each input).joined(separator: ", ")))" + ) + } } } diff --git a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/CustomFunctions.md b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/CustomFunctions.md index be87db85..cb72050a 100644 --- a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/CustomFunctions.md +++ b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/CustomFunctions.md @@ -26,10 +26,10 @@ Reminder.select { $exclaim($0.title) } ``` For the query to successfully execute, you must also add the function to your SQLite database -connection. This can be done in [SharingGRDB] (0.7.0+) using the `Database.add(function:)` method, -_e.g._ when you first configure things: +connection. This can be done in [SQLiteData] using the `Database.add(function:)` method, _e.g._ when +you first configure things: -[SharingGRDB]: https://github.com/pointfreeco/sharing-grdb +[SQLiteData]: https://github.com/pointfreeco/sqlite-data ```swift var configuration = Configuration() diff --git a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/QueryCookbook.md b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/QueryCookbook.md index 8629a92b..906a3a00 100644 --- a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/QueryCookbook.md +++ b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/QueryCookbook.md @@ -58,8 +58,8 @@ this is doing work that SQL actually excels at. In fact, the condition inside th suspiciously like a join constraint, which should give us a hint that what we are doing is not quite right. -Another way to do this is to use the `@Selection` macro along with a `JSONRepresentation`` of the -collection of reminders you want to load for each list: +Another way to do this is to use the `@Selection` and `@Column` macros along with a +`JSONRepresentation`` of the collection of reminders you want to load for each list: ```swift @Selection @@ -75,7 +75,7 @@ struct Row { This allows the query to serialize the associated rows into JSON, which are then deserialized into a `Row` type. To construct such a query you can use the ``StructuredQueriesCore/PrimaryKeyedTableDefinition/jsonGroupArray(distinct:order:filter:)`` -property that is defined on the columns of primary keyed tables: +property that is defined on the columns of primary-keyed tables: ```swift RemindersList diff --git a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Views.md b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Views.md index f1a2b3d0..8104e192 100644 --- a/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Views.md +++ b/Sources/StructuredQueriesSQLiteCore/Documentation.docc/Articles/Views.md @@ -16,16 +16,17 @@ the title of each reminder, along with the title of each list, we can model this Swift struct: ```swift -@Table @Selection +@Table private struct ReminderWithList { let reminderTitle: String let remindersListTitle: String } ``` -Note that we have applied both the `@Table` macro and `@Selection` macro. This is similar to what -one does with common table expressions, and it allows one to represent a type that for intents and -purposes seems like a regular SQLite table, but it's not actually persisted in the database. +Note that we have applied the `@Table` macro, even though we are interacting with a database view, +_not_ a database table. This is because `@Table` can be used to describe any table-like entity, +which includes database views and virtual tables. It allows one to represent a type that for intents +and purposes seems like a regular SQLite table, but it's not actually persisted in the database. With that type defined we can use the ``StructuredQueriesCore/Table/createTemporaryView(ifNotExists:as:)`` function to create a SQL query diff --git a/Sources/StructuredQueriesSQLiteCore/Internal/Deprecations.swift b/Sources/StructuredQueriesSQLiteCore/Internal/Deprecations.swift index 798eb6ec..cb190ac0 100644 --- a/Sources/StructuredQueriesSQLiteCore/Internal/Deprecations.swift +++ b/Sources/StructuredQueriesSQLiteCore/Internal/Deprecations.swift @@ -137,14 +137,6 @@ extension Date.ISO8601Representation: QueryBindable { public var queryBinding: QueryBinding { .text(queryOutput.iso8601String) } - - public init?(queryBinding: QueryBinding) { - guard - case .text(let iso8601String) = queryBinding, - let queryOutput = try? Date(iso8601String: iso8601String) - else { return nil } - self.init(queryOutput: queryOutput) - } } @available( @@ -202,14 +194,6 @@ extension UUID.LowercasedRepresentation: QueryBindable { public var queryBinding: QueryBinding { .text(queryOutput.uuidString.lowercased()) } - - public init?(queryBinding: QueryBinding) { - guard - case .text(let uuidString) = queryBinding, - let uuid = UUID(uuidString: uuidString) - else { return nil } - self.init(queryOutput: uuid) - } } @available( diff --git a/Sources/StructuredQueriesSQLiteCore/JSONFunctions.swift b/Sources/StructuredQueriesSQLiteCore/JSONFunctions.swift index 6f9f85bc..5e62a42b 100644 --- a/Sources/StructuredQueriesSQLiteCore/JSONFunctions.swift +++ b/Sources/StructuredQueriesSQLiteCore/JSONFunctions.swift @@ -62,7 +62,7 @@ extension PrimaryKeyedTableDefinition where QueryValue: Codable { /// Constructs a JSON array of JSON objects with a field for each column of the table. This can be /// useful for loading many associated values in a single query. For example, to query for every /// reminders list, along with the array of reminders it is associated with, one can define a - /// custom `@Selection` for that data and query as follows: + /// custom data type for that data and query as follows: /// /// @Row { /// @Column { @@ -122,13 +122,17 @@ extension PrimaryKeyedTableDefinition where QueryValue: Codable { } } -extension PrimaryKeyedTableDefinition where QueryValue: _OptionalProtocol & Codable { +extension PrimaryKeyedTableDefinition +where + QueryValue: _OptionalProtocol & Codable, + PrimaryColumn: _TableColumnExpression +{ /// A JSON array representation of the aggregation of a table's columns. /// /// Constructs a JSON array of JSON objects with a field for each column of the table. This can be /// useful for loading many associated values in a single query. For example, to query for every /// reminders list, along with the array of reminders it is associated with, one can define a - /// custom `@Selection` for that data and query as follows: + /// custom data type for that data and query as follows: /// /// @Row { /// @Column { @@ -179,11 +183,22 @@ extension PrimaryKeyedTableDefinition where QueryValue: _OptionalProtocol & Coda filter: (some QueryExpression)? = Bool?.none ) -> some QueryExpression<[Wrapped].JSONRepresentation> where QueryValue == Wrapped? { + let primaryKeyColumns: QueryFragment = self.primaryKey._names + .map { "\(QueryValue.self).\(quote: $0)" } + .joined(separator: ", ") + let primaryKeyNulls: QueryFragment = Array( + repeating: QueryFragment("NULL"), count: self.primaryKey._names.count + ) + .joined(separator: ", ") + let primaryKeyFilter = SQLQueryExpression( + "(\(primaryKeyColumns)) IS NOT (\(primaryKeyNulls))", + as: Bool.self + ) let filterQueryFragment = if let filter { - self.primaryKey.isNot(nil).and(filter).queryFragment + primaryKeyFilter.and(filter).queryFragment } else { - self.primaryKey.isNot(nil).queryFragment + primaryKeyFilter.queryFragment } return AggregateFunction( "json_group_array", diff --git a/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+JulianDay.swift b/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+JulianDay.swift index cc1dac77..39288d67 100644 --- a/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+JulianDay.swift +++ b/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+JulianDay.swift @@ -30,11 +30,6 @@ extension Date.JulianDayRepresentation: QueryBindable { public var queryBinding: QueryBinding { .double(2440587.5 + queryOutput.timeIntervalSince1970 / 86400) } - - public init?(queryBinding: QueryBinding) { - guard case .double(let value) = queryBinding else { return nil } - self.init(queryOutput: Date(timeIntervalSince1970: (value - 2440587.5) * 86400)) - } } extension Date.JulianDayRepresentation: QueryDecodable { diff --git a/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+UnixTime.swift b/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+UnixTime.swift index 3b3b8092..215d3b08 100644 --- a/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+UnixTime.swift +++ b/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/Date+UnixTime.swift @@ -30,11 +30,6 @@ extension Date.UnixTimeRepresentation: QueryBindable { public var queryBinding: QueryBinding { .int(Int64(queryOutput.timeIntervalSince1970)) } - - public init?(queryBinding: QueryBinding) { - guard case .int(let timeIntervalSince1970) = queryBinding else { return nil } - self.init(queryOutput: Date(timeIntervalSince1970: Double(timeIntervalSince1970))) - } } extension Date.UnixTimeRepresentation: QueryDecodable { diff --git a/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/UUID+Bytes.swift b/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/UUID+Bytes.swift index aa98a72d..c6bbc729 100644 --- a/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/UUID+Bytes.swift +++ b/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/UUID+Bytes.swift @@ -30,16 +30,6 @@ extension UUID.BytesRepresentation: QueryBindable { public var queryBinding: QueryBinding { .blob(withUnsafeBytes(of: queryOutput.uuid, [UInt8].init)) } - - public init?(queryBinding: QueryBinding) { - guard case .blob(let bytes) = queryBinding else { return nil } - guard bytes.count == 16 else { return nil } - self.init( - queryOutput: bytes.withUnsafeBytes { - UUID(uuid: $0.load(as: uuid_t.self)) - } - ) - } } extension UUID.BytesRepresentation: QueryDecodable { diff --git a/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/UUID+Uppercased.swift b/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/UUID+Uppercased.swift index 7eeebab4..1d0dfbfe 100644 --- a/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/UUID+Uppercased.swift +++ b/Sources/StructuredQueriesSQLiteCore/QueryRepresentable/UUID+Uppercased.swift @@ -27,14 +27,6 @@ extension UUID? { } extension UUID.UppercasedRepresentation: QueryBindable { - public init?(queryBinding: QueryBinding) { - guard - case .text(let uuidString) = queryBinding, - let uuid = UUID(uuidString: uuidString) - else { return nil } - self.init(queryOutput: uuid) - } - public var queryBinding: QueryBinding { .text(queryOutput.uuidString) } diff --git a/Sources/StructuredQueriesSQLiteCore/Triggers.swift b/Sources/StructuredQueriesSQLiteCore/Triggers.swift index 64edf5b6..e34d960c 100644 --- a/Sources/StructuredQueriesSQLiteCore/Triggers.swift +++ b/Sources/StructuredQueriesSQLiteCore/Triggers.swift @@ -345,15 +345,15 @@ public struct TemporaryTrigger: Sendable, Statement { /// - perform: A statement to perform for each triggered row. /// - condition: A predicate that must be satisfied to perform the given statement. /// - Returns: An `AFTER UPDATE` trigger operation. - public static func update( - of columns: (On.TableColumns) -> (repeat TableColumn), + public static func update( + of columns: (On.TableColumns) -> (repeat each Column), @QueryFragmentBuilder forEachRow perform: (_ old: Old, _ new: New) -> [QueryFragment], when condition: ((_ old: Old, _ new: New) -> any QueryExpression)? = nil ) -> Self { var columnNames: [String] = [] for column in repeat each columns(On.columns) { - columnNames.append(column.name) + columnNames.append(contentsOf: column._names) } return Self( kind: .update( diff --git a/Sources/StructuredQueriesSQLiteCore/Views.swift b/Sources/StructuredQueriesSQLiteCore/Views.swift index 12b38534..a85cda96 100644 --- a/Sources/StructuredQueriesSQLiteCore/Views.swift +++ b/Sources/StructuredQueriesSQLiteCore/Views.swift @@ -1,4 +1,4 @@ -extension Table where Self: _Selection { +extension Table { /// A `CREATE TEMPORARY VIEW` statement. /// /// See for more information. @@ -21,7 +21,7 @@ extension Table where Self: _Selection { /// This type of statement is returned from ``Table/createTemporaryView(ifNotExists:as:)``. /// /// To learn more, see . -public struct TemporaryView: Statement +public struct TemporaryView: Statement where Selection.QueryValue == View { public typealias QueryValue = () public typealias From = Never diff --git a/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift index b7dfe0cd..97fc3f89 100644 --- a/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift +++ b/Sources/StructuredQueriesSQLiteMacros/DatabaseFunctionMacro.swift @@ -102,16 +102,20 @@ extension DatabaseFunctionMacro: PeerMacro { let functionTypeName = context.makeUniqueName(declarationName) let databaseFunctionName = StringLiteralExprSyntax(content: functionName) - let argumentCount = declaration.signature.parameterClause.parameters.count + var argumentCount: [ExprSyntax] = [] var bodyArguments: [String] = [] var representableInputTypes: [String] = [] var signature = declaration.signature var invocationArgumentTypes: [TypeSyntax] = [] var parameters: [String] = [] - var argumentBindings: [(String, String)] = [] + var argumentBindings: [String] = [] var offset = 0 var functionRepresentationIterator = functionRepresentation?.parameters.makeIterator() + + var decodings: [String] = [] + var decodingUnwrappings: [String] = [] + for index in signature.parameterClause.parameters.indices { defer { offset += 1 } var parameter = signature.parameterClause.parameters[index] @@ -125,9 +129,10 @@ extension DatabaseFunctionMacro: PeerMacro { return [] } bodyArguments.append("\(parameter.type.trimmed)") - let type = (functionRepresentationIterator?.next()?.type ?? parameter.type).trimmed - representableInputTypes.append(type.trimmedDescription) + var type = (functionRepresentationIterator?.next()?.type ?? parameter.type) parameter.type = type.asQueryExpression() + type = type.trimmed + representableInputTypes.append(type.description) if let defaultValue = parameter.defaultValue, defaultValue.value.is(NilLiteralExprSyntax.self) { @@ -137,7 +142,11 @@ extension DatabaseFunctionMacro: PeerMacro { invocationArgumentTypes.append(type) let parameterName = (parameter.secondName ?? parameter.firstName).trimmedDescription parameters.append(parameterName) - argumentBindings.append((parameterName, "\(type)(queryBinding: arguments[\(offset)])")) + argumentBindings.append(parameterName) + + argumentCount.append("\(type)") + decodings.append("let \(parameterName) = try decoder.decode(\(type).self)") + decodingUnwrappings.append("guard let \(parameterName) else { throw InvalidInvocation() }") } var representableInputType = representableInputTypes.joined(separator: ", ") let isVoidReturning = signature.returnClause == nil @@ -154,7 +163,7 @@ extension DatabaseFunctionMacro: PeerMacro { """ let bodyInvocation = """ \(declaration.signature.effectSpecifiers?.throwsClause != nil ? "try " : "")self.body(\ - \(argumentBindings.map { name, _ in "\(name).queryOutput" }.joined(separator: ", "))\ + \(argumentBindings.joined(separator: ", "))\ ) """ // TODO: Diagnose 'asyncClause'? @@ -218,25 +227,25 @@ extension DatabaseFunctionMacro: PeerMacro { public typealias Input = \(raw: representableInputType) public typealias Output = \(representableOutputType) public let name = \(databaseFunctionName) - public let argumentCount: Int? = \(raw: argumentCount) + public var argumentCount: Int? { \ + [\(raw: argumentCount.map { "\($0)._columnWidth" }.joined(separator: ", "))].reduce(0, +) \ + } public let isDeterministic = \(raw: isDeterministic) public let body: \(raw: bodyType) public init(_ body: @escaping \(raw: bodyType)) { self.body = body } public func callAsFunction\(signature.trimmed) { + StructuredQueriesCore.$_isSelecting.withValue(false) { StructuredQueriesCore.SQLQueryExpression( "\\(quote: self.name)(\(raw: parameters.map { "\\(\($0))" }.joined(separator: ", ")))" ) } - public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count\ - \(raw: argumentBindings.map { ", let \($0) = \($1)" }.joined()) \ - else { - return .invalid(InvalidInvocation()) } + public func invoke( + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { + \(raw: (decodings + decodingUnwrappings).map { "\($0)\n" }.joined())\ \(raw: invocationBody) } private struct InvalidInvocation: Error {} diff --git a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift index 8e005c4c..5fcf840e 100644 --- a/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift +++ b/Sources/_StructuredQueriesSQLite/DatabaseFunction.swift @@ -10,12 +10,17 @@ extension ScalarDatabaseFunction { SQLITE_UTF8 | (isDeterministic ? SQLITE_DETERMINISTIC : 0), box, { context, argumentCount, arguments in - Unmanaged - .fromOpaque(sqlite3_user_data(context)) - .takeUnretainedValue() - .function - .invoke([QueryBinding](argumentCount: argumentCount, arguments: arguments)) - .result(db: context) + do { + var decoder = SQLiteFunctionDecoder(argumentCount: argumentCount, arguments: arguments) + try Unmanaged + .fromOpaque(sqlite3_user_data(context)) + .takeUnretainedValue() + .function + .invoke(&decoder) + .result(db: context) + } catch { + QueryBinding.invalid(error).result(db: context) + } }, nil, nil, @@ -34,36 +39,6 @@ private final class ScalarDatabaseFunctionBox { } } -extension [QueryBinding] { - fileprivate init(argumentCount: Int32, arguments: UnsafeMutablePointer?) { - self = (0..? + + @usableFromInline + var currentIndex: Int32 = 0 + + @usableFromInline + init(argumentCount: Int32, arguments: UnsafeMutablePointer?) { + self.argumentCount = argumentCount + self.arguments = arguments + } + + @inlinable + mutating func next() { + currentIndex = 0 + } + + @inlinable + mutating func decode(_ columnType: [UInt8].Type) throws -> [UInt8]? { + defer { currentIndex += 1 } + precondition(argumentCount > currentIndex) + let value = arguments?[Int(currentIndex)] + guard sqlite3_value_type(value) != SQLITE_NULL else { return nil } + if let blob = sqlite3_value_blob(value) { + let count = Int(sqlite3_value_bytes(value)) + let buffer = UnsafeRawBufferPointer(start: blob, count: count) + return [UInt8](buffer) + } else { + return [] + } + } + + @inlinable + mutating func decode(_ columnType: Bool.Type) throws -> Bool? { + try decode(Int64.self).map { $0 != 0 } + } + + @usableFromInline + mutating func decode(_ columnType: Date.Type) throws -> Date? { + guard let iso8601String = try decode(String.self) else { return nil } + return try Date(iso8601String: iso8601String) + } + + @inlinable + mutating func decode(_ columnType: Double.Type) throws -> Double? { + defer { currentIndex += 1 } + precondition(argumentCount > currentIndex) + let value = arguments?[Int(currentIndex)] + guard sqlite3_value_type(value) != SQLITE_NULL else { return nil } + return sqlite3_value_double(value) + } + + @inlinable + mutating func decode(_ columnType: Int.Type) throws -> Int? { + try decode(Int64.self).map(Int.init) + } + + @inlinable + mutating func decode(_ columnType: Int64.Type) throws -> Int64? { + defer { currentIndex += 1 } + precondition(argumentCount > currentIndex) + let value = arguments?[Int(currentIndex)] + guard sqlite3_value_type(value) != SQLITE_NULL else { return nil } + return sqlite3_value_int64(value) + } + + @inlinable + mutating func decode(_ columnType: String.Type) throws -> String? { + defer { currentIndex += 1 } + precondition(argumentCount > currentIndex) + let value = arguments?[Int(currentIndex)] + guard sqlite3_value_type(value) != SQLITE_NULL else { return nil } + return String(cString: sqlite3_value_text(value)) + } + + @inlinable + mutating func decode(_ columnType: UInt64.Type) throws -> UInt64? { + guard let n = try decode(Int64.self) else { return nil } + guard n >= 0 else { throw UInt64OverflowError(signedInteger: n) } + return UInt64(n) + } + + @usableFromInline + mutating func decode(_ columnType: UUID.Type) throws -> UUID? { + guard let uuidString = try decode(String.self) else { return nil } + return UUID(uuidString: uuidString) + } +} diff --git a/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift b/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift index 09b47511..be998d7b 100644 --- a/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift +++ b/Sources/_StructuredQueriesSQLite/SQLiteQueryDecoder.swift @@ -84,13 +84,3 @@ struct SQLiteQueryDecoder: QueryDecoder { return UUID(uuidString: uuidString) } } - -@usableFromInline -struct UInt64OverflowError: Error { - let signedInteger: Int64 - - @usableFromInline - init(signedInteger: Int64) { - self.signedInteger = signedInteger - } -} diff --git a/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift index e8c57888..24f40b0c 100644 --- a/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift +++ b/Tests/StructuredQueriesMacrosTests/DatabaseFunctionMacroTests.swift @@ -27,23 +27,24 @@ extension SnapshotTests { public typealias Input = () public typealias Output = Date public let name = "currentDate" - public let argumentCount: Int? = 0 + public var argumentCount: Int? { + [].reduce(0, +) + } public let isDeterministic = false public let body: () -> Date public init(_ body: @escaping () -> Date) { self.body = body } public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)()" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)()" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count else { - return .invalid(InvalidInvocation()) - } + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { return Date( queryOutput: self.body() ) @@ -78,23 +79,24 @@ extension SnapshotTests { public typealias Input = () public typealias Output = Date public let name = "current_date" - public let argumentCount: Int? = 0 + public var argumentCount: Int? { + [].reduce(0, +) + } public let isDeterministic = false public let body: () -> Date public init(_ body: @escaping () -> Date) { self.body = body } public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)()" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)()" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count else { - return .invalid(InvalidInvocation()) - } + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { return Date( queryOutput: self.body() ) @@ -129,25 +131,30 @@ extension SnapshotTests { public typealias Input = [String].JSONRepresentation public typealias Output = [String].JSONRepresentation public let name = "jsonCapitalize" - public let argumentCount: Int? = 1 + public var argumentCount: Int? { + [[String].JSONRepresentation._columnWidth].reduce(0, +) + } public let isDeterministic = false public let body: ([String]) -> [String] public init(_ body: @escaping ([String]) -> [String]) { self.body = body } public func callAsFunction(_ strings: some StructuredQueriesCore.QueryExpression<[String].JSONRepresentation>) -> some StructuredQueriesCore.QueryExpression<[String].JSONRepresentation> { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)(\(strings))" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)(\(strings))" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count, let strings = [String].JSONRepresentation(queryBinding: arguments[0]) else { - return .invalid(InvalidInvocation()) + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { + let strings = try decoder.decode([String].JSONRepresentation.self) + guard let strings else { + throw InvalidInvocation() } return [String].JSONRepresentation( - queryOutput: self.body(strings.queryOutput) + queryOutput: self.body(strings) ) .queryBinding } @@ -180,23 +187,24 @@ extension SnapshotTests { public typealias Input = () public typealias Output = Int public let name = "fortyTwo" - public let argumentCount: Int? = 0 + public var argumentCount: Int? { + [].reduce(0, +) + } public let isDeterministic = true public let body: () -> Int public init(_ body: @escaping () -> Int) { self.body = body } public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)()" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)()" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count else { - return .invalid(InvalidInvocation()) - } + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { return Int( queryOutput: self.body() ) @@ -231,25 +239,30 @@ extension SnapshotTests { public typealias Input = String public typealias Output = Date? public let name = "currentDate" - public let argumentCount: Int? = 1 + public var argumentCount: Int? { + [String._columnWidth].reduce(0, +) + } public let isDeterministic = false public let body: (String) -> Date? public init(_ body: @escaping (String) -> Date?) { self.body = body } public func callAsFunction(_ format: some StructuredQueriesCore.QueryExpression) -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)(\(format))" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)(\(format))" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count, let format = String(queryBinding: arguments[0]) else { - return .invalid(InvalidInvocation()) + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { + let format = try decoder.decode(String.self) + guard let format else { + throw InvalidInvocation() } return Date?( - queryOutput: self.body(format.queryOutput) + queryOutput: self.body(format) ) .queryBinding } @@ -282,25 +295,30 @@ extension SnapshotTests { public typealias Input = String public typealias Output = Date? public let name = "currentDate" - public let argumentCount: Int? = 1 + public var argumentCount: Int? { + [String._columnWidth].reduce(0, +) + } public let isDeterministic = false public let body: (String) -> Date? public init(_ body: @escaping (String) -> Date?) { self.body = body } public func callAsFunction(format: some StructuredQueriesCore.QueryExpression) -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)(\(format))" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)(\(format))" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count, let format = String(queryBinding: arguments[0]) else { - return .invalid(InvalidInvocation()) + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { + let format = try decoder.decode(String.self) + guard let format else { + throw InvalidInvocation() } return Date?( - queryOutput: self.body(format.queryOutput) + queryOutput: self.body(format) ) .queryBinding } @@ -333,25 +351,30 @@ extension SnapshotTests { public typealias Input = String public typealias Output = Date? public let name = "currentDate" - public let argumentCount: Int? = 1 + public var argumentCount: Int? { + [String._columnWidth].reduce(0, +) + } public let isDeterministic = false public let body: (String) -> Date? public init(_ body: @escaping (String) -> Date?) { self.body = body } public func callAsFunction(_ format: some StructuredQueriesCore.QueryExpression = "") -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)(\(format))" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)(\(format))" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count, let format = String(queryBinding: arguments[0]) else { - return .invalid(InvalidInvocation()) + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { + let format = try decoder.decode(String.self) + guard let format else { + throw InvalidInvocation() } return Date?( - queryOutput: self.body(format.queryOutput) + queryOutput: self.body(format) ) .queryBinding } @@ -384,25 +407,30 @@ extension SnapshotTests { public typealias Input = String public typealias Output = Date? public let name = "currentDate" - public let argumentCount: Int? = 1 + public var argumentCount: Int? { + [String._columnWidth].reduce(0, +) + } public let isDeterministic = false public let body: (String) -> Date? public init(_ body: @escaping (String) -> Date?) { self.body = body } public func callAsFunction(format: some StructuredQueriesCore.QueryExpression = "") -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)(\(format))" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)(\(format))" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count, let format = String(queryBinding: arguments[0]) else { - return .invalid(InvalidInvocation()) + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { + let format = try decoder.decode(String.self) + guard let format else { + throw InvalidInvocation() } return Date?( - queryOutput: self.body(format.queryOutput) + queryOutput: self.body(format) ) .queryBinding } @@ -435,25 +463,34 @@ extension SnapshotTests { public typealias Input = (String, String) public typealias Output = String public let name = "concat" - public let argumentCount: Int? = 2 + public var argumentCount: Int? { + [String._columnWidth, String._columnWidth].reduce(0, +) + } public let isDeterministic = false public let body: (String, String) -> String public init(_ body: @escaping (String, String) -> String) { self.body = body } public func callAsFunction(first: some StructuredQueriesCore.QueryExpression = "", second: some StructuredQueriesCore.QueryExpression = "") -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)(\(first), \(second))" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)(\(first), \(second))" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count, let first = String(queryBinding: arguments[0]), let second = String(queryBinding: arguments[1]) else { - return .invalid(InvalidInvocation()) + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { + let first = try decoder.decode(String.self) + let second = try decoder.decode(String.self) + guard let first else { + throw InvalidInvocation() + } + guard let second else { + throw InvalidInvocation() } return String( - queryOutput: self.body(first.queryOutput, second.queryOutput) + queryOutput: self.body(first, second) ) .queryBinding } @@ -503,25 +540,30 @@ extension SnapshotTests { public typealias Input = String? public typealias Output = Date? public let name = "currentDate" - public let argumentCount: Int? = 1 + public var argumentCount: Int? { + [String?._columnWidth].reduce(0, +) + } public let isDeterministic = false public let body: (String?) -> Date? public init(_ body: @escaping (String?) -> Date?) { self.body = body } public func callAsFunction(_ format: some StructuredQueriesCore.QueryExpression = String?.none) -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)(\(format))" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)(\(format))" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count, let format = String?(queryBinding: arguments[0]) else { - return .invalid(InvalidInvocation()) + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { + let format = try decoder.decode(String?.self) + guard let format else { + throw InvalidInvocation() } return Date?( - queryOutput: self.body(format.queryOutput) + queryOutput: self.body(format) ) .queryBinding } @@ -554,23 +596,24 @@ extension SnapshotTests { public typealias Input = () public typealias Output = Date public let name = "currentDate" - public let argumentCount: Int? = 0 + public var argumentCount: Int? { + [].reduce(0, +) + } public let isDeterministic = false public let body: () throws -> Date public init(_ body: @escaping () throws -> Date) { self.body = body } public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)()" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)()" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count else { - return .invalid(InvalidInvocation()) - } + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { do { return Date( queryOutput: try self.body() @@ -609,23 +652,24 @@ extension SnapshotTests { public typealias Input = () public typealias Output = Date public let name = "currentDate" - public let argumentCount: Int? = 0 + public var argumentCount: Int? { + [].reduce(0, +) + } public let isDeterministic = false public let body: () throws(MyError) -> Date public init(_ body: @escaping () throws(MyError) -> Date) { self.body = body } public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)()" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)()" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count else { - return .invalid(InvalidInvocation()) - } + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { do { return Date( queryOutput: try self.body() @@ -664,23 +708,24 @@ extension SnapshotTests { public typealias Input = () public typealias Output = Date public let name = "currentDate" - public let argumentCount: Int? = 0 + public var argumentCount: Int? { + [].reduce(0, +) + } public let isDeterministic = false public let body: () -> Date public init(_ body: @escaping () -> Date) { self.body = body } public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)()" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)()" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count else { - return .invalid(InvalidInvocation()) - } + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { return Date( queryOutput: self.body() ) @@ -715,23 +760,24 @@ extension SnapshotTests { public typealias Input = () public typealias Output = Date public let name = "currentDate" - public let argumentCount: Int? = 0 + public var argumentCount: Int? { + [].reduce(0, +) + } public let isDeterministic = false public let body: () -> Date public init(_ body: @escaping () -> Date) { self.body = body } public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)()" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)()" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count else { - return .invalid(InvalidInvocation()) - } + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { return Date( queryOutput: self.body() ) @@ -789,23 +835,24 @@ extension SnapshotTests { public typealias Input = () public typealias Output = Date public let name = "currentDate" - public let argumentCount: Int? = 0 + public var argumentCount: Int? { + [].reduce(0, +) + } public let isDeterministic = false public let body: () -> Date public init(_ body: @escaping () -> Date) { self.body = body } public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)()" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)()" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count else { - return .invalid(InvalidInvocation()) - } + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { return Date( queryOutput: self.body() ) @@ -840,23 +887,24 @@ extension SnapshotTests { public typealias Input = () public typealias Output = Int public let name = "default" - public let argumentCount: Int? = 0 + public var argumentCount: Int? { + [].reduce(0, +) + } public let isDeterministic = false public let body: () -> Int public init(_ body: @escaping () -> Int) { self.body = body } public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)()" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)()" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count else { - return .invalid(InvalidInvocation()) - } + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { return Int( queryOutput: self.body() ) @@ -891,23 +939,24 @@ extension SnapshotTests { public typealias Input = () public typealias Output = Swift.Void public let name = "void" - public let argumentCount: Int? = 0 + public var argumentCount: Int? { + [].reduce(0, +) + } public let isDeterministic = false public let body: () -> Swift.Void public init(_ body: @escaping () -> Swift.Void) { self.body = body } public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)()" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)()" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count else { - return .invalid(InvalidInvocation()) - } + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { self.body() return .null } @@ -937,23 +986,24 @@ extension SnapshotTests { public typealias Input = () public typealias Output = Swift.Void public let name = "void" - public let argumentCount: Int? = 0 + public var argumentCount: Int? { + [].reduce(0, +) + } public let isDeterministic = false public let body: () throws -> Swift.Void public init(_ body: @escaping () throws -> Swift.Void) { self.body = body } public func callAsFunction() -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)()" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)()" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count else { - return .invalid(InvalidInvocation()) - } + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { do { try self.body() return .null @@ -996,7 +1046,9 @@ extension SnapshotTests { public typealias Input = (Int, Int) public typealias Output = Swift.Void public let name = "min" - public let argumentCount: Int? = 2 + public var argumentCount: Int? { + [Int._columnWidth, Int._columnWidth].reduce(0, +) + } public let isDeterministic = false public let body: (Int, Int) -> Swift.Void public init(_ body: @escaping (Int, Int) -> Swift.Void) { @@ -1006,17 +1058,24 @@ extension SnapshotTests { _ x: some StructuredQueriesCore.QueryExpression, _ y: some StructuredQueriesCore.QueryExpression ) -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)(\(x), \(y))" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)(\(x), \(y))" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count, let x = Int(queryBinding: arguments[0]), let y = Int(queryBinding: arguments[1]) else { - return .invalid(InvalidInvocation()) + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { + let x = try decoder.decode(Int.self) + let y = try decoder.decode(Int.self) + guard let x else { + throw InvalidInvocation() + } + guard let y else { + throw InvalidInvocation() } - self.body(x.queryOutput, y.queryOutput) + self.body(x, y) return .null } private struct InvalidInvocation: Error { @@ -1051,7 +1110,9 @@ extension SnapshotTests { public typealias Input = (Int, Int) public typealias Output = Swift.Void public let name = "min" - public let argumentCount: Int? = 2 + public var argumentCount: Int? { + [Int._columnWidth, Int._columnWidth].reduce(0, +) + } public let isDeterministic = false public let body: (Int, Int) -> Swift.Void public init(_ body: @escaping (Int, Int) -> Swift.Void) { @@ -1061,17 +1122,24 @@ extension SnapshotTests { x: some StructuredQueriesCore.QueryExpression, y: some StructuredQueriesCore.QueryExpression ) -> some StructuredQueriesCore.QueryExpression { - StructuredQueriesCore.SQLQueryExpression( - "\(quote: self.name)(\(x), \(y))" - ) + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)(\(x), \(y))" + ) + } } public func invoke( - _ arguments: [StructuredQueriesCore.QueryBinding] - ) -> StructuredQueriesCore.QueryBinding { - guard self.argumentCount == nil || self.argumentCount == arguments.count, let x = Int(queryBinding: arguments[0]), let y = Int(queryBinding: arguments[1]) else { - return .invalid(InvalidInvocation()) + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { + let x = try decoder.decode(Int.self) + let y = try decoder.decode(Int.self) + guard let x else { + throw InvalidInvocation() + } + guard let y else { + throw InvalidInvocation() } - self.body(x.queryOutput, y.queryOutput) + self.body(x, y) return .null } private struct InvalidInvocation: Error { @@ -1080,5 +1148,65 @@ extension SnapshotTests { """# } } + + @Test func argumentCount() { + assertMacro { + """ + @DatabaseFunction + func isValid(_ reminder: Reminder, _ override: Bool = false) -> Bool { + !reminder.title.isEmpty || override + } + """ + } expansion: { + #""" + func isValid(_ reminder: Reminder, _ override: Bool = false) -> Bool { + !reminder.title.isEmpty || override + } + + var $isValid: __macro_local_7isValidfMu_ { + __macro_local_7isValidfMu_(isValid) + } + + struct __macro_local_7isValidfMu_: StructuredQueriesSQLiteCore.ScalarDatabaseFunction { + public typealias Input = (Reminder, Bool) + public typealias Output = Bool + public let name = "isValid" + public var argumentCount: Int? { + [Reminder._columnWidth, Bool._columnWidth].reduce(0, +) + } + public let isDeterministic = false + public let body: (Reminder, Bool) -> Bool + public init(_ body: @escaping (Reminder, Bool) -> Bool) { + self.body = body + } + public func callAsFunction(_ reminder: some StructuredQueriesCore.QueryExpression, _ override: some StructuredQueriesCore.QueryExpression = false) -> some StructuredQueriesCore.QueryExpression { + StructuredQueriesCore.$_isSelecting.withValue(false) { + StructuredQueriesCore.SQLQueryExpression( + "\(quote: self.name)(\(reminder), \(override))" + ) + } + } + public func invoke( + _ decoder: inout some QueryDecoder + ) throws -> StructuredQueriesCore.QueryBinding { + let reminder = try decoder.decode(Reminder.self) + let override = try decoder.decode(Bool.self) + guard let reminder else { + throw InvalidInvocation() + } + guard let override else { + throw InvalidInvocation() + } + return Bool( + queryOutput: self.body(reminder, override) + ) + .queryBinding + } + private struct InvalidInvocation: Error { + } + } + """# + } + } } } diff --git a/Tests/StructuredQueriesMacrosTests/SelectionMacroTests.swift b/Tests/StructuredQueriesMacrosTests/SelectionMacroTests.swift deleted file mode 100644 index a0d6c249..00000000 --- a/Tests/StructuredQueriesMacrosTests/SelectionMacroTests.swift +++ /dev/null @@ -1,374 +0,0 @@ -import MacroTesting -import StructuredQueriesMacros -import Testing - -extension SnapshotTests { - @Suite struct SelectionMacroTests { - @Test func basics() { - assertMacro { - """ - @Selection - struct PlayerAndTeam { - let player: Player - let team: Team - } - """ - } expansion: { - """ - struct PlayerAndTeam { - let player: Player - let team: Team - - public struct Columns: StructuredQueriesCore._SelectedColumns { - public typealias QueryValue = PlayerAndTeam - public let selection: [(aliasName: String, expression: StructuredQueriesCore.QueryFragment)] - public init( - player: some StructuredQueriesCore.QueryExpression, - team: some StructuredQueriesCore.QueryExpression - ) { - self.selection = [("player", player.queryFragment), ("team", team.queryFragment)] - } - } - } - - extension PlayerAndTeam: StructuredQueriesCore._Selection { - public init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let player = try decoder.decode(Player.self) - let team = try decoder.decode(Team.self) - guard let player else { - throw QueryDecodingError.missingRequiredColumn - } - guard let team else { - throw QueryDecodingError.missingRequiredColumn - } - self.player = player - self.team = team - } - } - """ - } - } - - @Test func `enum`() { - assertMacro { - """ - @Selection - public enum S {} - """ - } diagnostics: { - """ - @Selection - public enum S {} - ┬─── - ╰─ 🛑 '@Selection' can only be applied to struct types - """ - } - } - - @Test func optionalField() { - assertMacro { - """ - @Selection - struct ReminderTitleAndListTitle { - var reminderTitle: String - var listTitle: String? - } - """ - } expansion: { - """ - struct ReminderTitleAndListTitle { - var reminderTitle: String - var listTitle: String? - - public struct Columns: StructuredQueriesCore._SelectedColumns { - public typealias QueryValue = ReminderTitleAndListTitle - public let selection: [(aliasName: String, expression: StructuredQueriesCore.QueryFragment)] - public init( - reminderTitle: some StructuredQueriesCore.QueryExpression, - listTitle: some StructuredQueriesCore.QueryExpression - ) { - self.selection = [("reminderTitle", reminderTitle.queryFragment), ("listTitle", listTitle.queryFragment)] - } - } - } - - extension ReminderTitleAndListTitle: StructuredQueriesCore._Selection { - public init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let reminderTitle = try decoder.decode(String.self) - let listTitle = try decoder.decode(String.self) - guard let reminderTitle else { - throw QueryDecodingError.missingRequiredColumn - } - self.reminderTitle = reminderTitle - self.listTitle = listTitle - } - } - """ - } - } - - @Test func date() { - assertMacro { - """ - @Selection struct ReminderDate { - @Column(as: Date.UnixTimeRepresentation.self) - var date: Date - } - """ - } expansion: { - """ - struct ReminderDate { - var date: Date - - public struct Columns: StructuredQueriesCore._SelectedColumns { - public typealias QueryValue = ReminderDate - public let selection: [(aliasName: String, expression: StructuredQueriesCore.QueryFragment)] - public init( - date: some StructuredQueriesCore.QueryExpression - ) { - self.selection = [("date", date.queryFragment)] - } - } - } - - extension ReminderDate: StructuredQueriesCore._Selection { - public init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let date = try decoder.decode(Date.UnixTimeRepresentation.self) - guard let date else { - throw QueryDecodingError.missingRequiredColumn - } - self.date = date - } - } - """ - } - } - - @Test func defaults() { - assertMacro { - """ - @Selection struct Row { - var title = "" - @Column(as: [String].JSONRepresentation.self) - var notes: [String] = [] - } - """ - } expansion: { - """ - struct Row { - var title = "" - var notes: [String] = [] - - public struct Columns: StructuredQueriesCore._SelectedColumns { - public typealias QueryValue = Row - public let selection: [(aliasName: String, expression: StructuredQueriesCore.QueryFragment)] - public init( - title: some StructuredQueriesCore.QueryExpression = StructuredQueriesCore.BindQueryExpression(""), - notes: some StructuredQueriesCore.QueryExpression<[String].JSONRepresentation> = StructuredQueriesCore.BindQueryExpression([]) - ) { - self.selection = [("title", title.queryFragment), ("notes", notes.queryFragment)] - } - } - } - - extension Row: StructuredQueriesCore._Selection { - public init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let title = try decoder.decode(Swift.String.self) - let notes = try decoder.decode([String].JSONRepresentation.self) - guard let title else { - throw QueryDecodingError.missingRequiredColumn - } - guard let notes else { - throw QueryDecodingError.missingRequiredColumn - } - self.title = title - self.notes = notes - } - } - """ - } - } - - @Test func primaryKey() { - assertMacro { - """ - @Selection struct Row { - @Column(primaryKey: true) - let id: Int - var title = "" - } - """ - } diagnostics: { - """ - @Selection struct Row { - @Column(primaryKey: true) - ┬───────── - ╰─ 🛑 '@Selection' primary keys are not supported - ✏️ Remove 'primaryKey: true' - let id: Int - var title = "" - } - """ - } fixes: { - """ - @Selection struct Row { - @Column() - let id: Int - var title = "" - } - """ - } expansion: { - """ - struct Row { - let id: Int - var title = "" - - public struct Columns: StructuredQueriesCore._SelectedColumns { - public typealias QueryValue = Row - public let selection: [(aliasName: String, expression: StructuredQueriesCore.QueryFragment)] - public init( - id: some StructuredQueriesCore.QueryExpression, - title: some StructuredQueriesCore.QueryExpression = StructuredQueriesCore.BindQueryExpression("") - ) { - self.selection = [("id", id.queryFragment), ("title", title.queryFragment)] - } - } - } - - extension Row: StructuredQueriesCore._Selection { - public init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let id = try decoder.decode(Int.self) - let title = try decoder.decode(Swift.String.self) - guard let id else { - throw QueryDecodingError.missingRequiredColumn - } - guard let title else { - throw QueryDecodingError.missingRequiredColumn - } - self.id = id - self.title = title - } - } - """ - } - } - - @Test func tableSelectionPrimaryKey() { - assertMacro { - """ - @Table @Selection struct Row { - @Column(primaryKey: true) - let id: Int - var title = "" - } - """ - } expansion: { - #""" - struct Row { - let id: Int - var title = "" - - public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { - public typealias QueryValue = Row - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let title = StructuredQueriesCore.TableColumn("title", keyPath: \QueryValue.title, default: "") - public var primaryKey: StructuredQueriesCore.TableColumn { - self.id - } - public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title] - } - public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title] - } - public var queryFragment: QueryFragment { - "\(self.id), \(self.title)" - } - } - - public struct Draft: StructuredQueriesCore.TableDraft { - public typealias PrimaryTable = Row - let id: Int? - var title = "" - public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { - public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let title = StructuredQueriesCore.TableColumn("title", keyPath: \QueryValue.title, default: "") - public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title] - } - public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title] - } - public var queryFragment: QueryFragment { - "\(self.id), \(self.title)" - } - } - public nonisolated static var columns: TableColumns { - TableColumns() - } - - public nonisolated static var tableName: String { - Row.tableName - } - - public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) - self.title = try decoder.decode(Swift.String.self) ?? "" - } - - public nonisolated init(_ other: Row) { - self.id = other.id - self.title = other.title - } - public init( - id: Int? = nil, - title: Swift.String = "" - ) { - self.id = id - self.title = title - } - } - - public struct Columns: StructuredQueriesCore._SelectedColumns { - public typealias QueryValue = Row - public let selection: [(aliasName: String, expression: StructuredQueriesCore.QueryFragment)] - public init( - id: some StructuredQueriesCore.QueryExpression, - title: some StructuredQueriesCore.QueryExpression = StructuredQueriesCore.BindQueryExpression("") - ) { - self.selection = [("id", id.queryFragment), ("title", title.queryFragment)] - } - } - } - - nonisolated extension Row: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { - public typealias QueryValue = Self - public typealias From = Swift.Never - public nonisolated static var columns: TableColumns { - TableColumns() - } - public nonisolated static var tableName: String { - "rows" - } - } - - extension Row: StructuredQueriesCore._Selection { - public init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let id = try decoder.decode(Int.self) - let title = try decoder.decode(Swift.String.self) - guard let id else { - throw QueryDecodingError.missingRequiredColumn - } - guard let title else { - throw QueryDecodingError.missingRequiredColumn - } - self.id = id - self.title = title - } - } - """# - } - } - } -} diff --git a/Tests/StructuredQueriesMacrosTests/Support/SnapshotTests.swift b/Tests/StructuredQueriesMacrosTests/Support/SnapshotTests.swift index f4542224..c13e4171 100644 --- a/Tests/StructuredQueriesMacrosTests/Support/SnapshotTests.swift +++ b/Tests/StructuredQueriesMacrosTests/Support/SnapshotTests.swift @@ -12,9 +12,10 @@ import Testing "_Draft": TableMacro.self, "bind": BindMacro.self, "Column": ColumnMacro.self, + "Columns": ColumnsMacro.self, "DatabaseFunction": DatabaseFunctionMacro.self, "Ephemeral": EphemeralMacro.self, - "Selection": SelectionMacro.self, + "Selection": TableMacro.self, "sql": SQLMacro.self, "Table": TableMacro.self, ], diff --git a/Tests/StructuredQueriesMacrosTests/TableMacroTests.swift b/Tests/StructuredQueriesMacrosTests/TableMacroTests.swift index ba6bbe38..9adbe9bb 100644 --- a/Tests/StructuredQueriesMacrosTests/TableMacroTests.swift +++ b/Tests/StructuredQueriesMacrosTests/TableMacroTests.swift @@ -20,30 +20,119 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Foo - public let bar = StructuredQueriesCore.TableColumn("bar", keyPath: \QueryValue.bar) + public let bar = StructuredQueriesCore._TableColumn.for("bar", keyPath: \QueryValue.bar) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.bar] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.bar._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.bar] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.bar._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.bar)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + bar: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: bar._allColumns) + self.allColumns = allColumns + } + } + } + + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { + "foos" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + let bar = try decoder.decode(Int.self) + guard let bar else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.bar = bar + } + } + """# + } + } + + @Test func selection() { + assertMacro { + """ + @Selection + struct Foo { + var bar: Int + } + """ + } expansion: { + #""" + struct Foo { + var bar: Int + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Foo + public let bar = StructuredQueriesCore._TableColumn.for("bar", keyPath: \QueryValue.bar) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.bar._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.bar._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.bar)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + bar: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: bar._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Foo: StructuredQueriesCore.Table { + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore._Selection, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "foos" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let bar = try decoder.decode(Int.self) guard let bar else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.bar = bar } @@ -52,6 +141,44 @@ extension SnapshotTests { } } + @Test func tableSelection() { + assertMacro { + """ + @Table @Selection + struct Foo { + var bar: Int + } + """ + } diagnostics: { + """ + @Table @Selection + ┬───────── + ╰─ ⚠️ '@Table' and '@Selection' should not be applied together + + Apply '@Table' to types representing stored tables, virtual tables, and database views. + + Apply '@Selection' to types representing multiple columns that can be selected from a table or query, and types that represent common table expressions. + ✏️ Remove '@Selection' + ✏️ Remove '@Table' + struct Foo { + var bar: Int + } + """ + } fixes: { + """ + struct Foo { + var bar: Int + } + """ + } expansion: { + """ + struct Foo { + var bar: Int + } + """ + } + } + @Test func comment() { assertMacro { """ @@ -77,23 +204,46 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { public typealias QueryValue = User - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let email = StructuredQueriesCore.TableColumn("email", keyPath: \QueryValue.email, default: "") - public let age = StructuredQueriesCore.TableColumn("age", keyPath: \QueryValue.age) - public var primaryKey: StructuredQueriesCore.TableColumn { - self.id - } + public typealias PrimaryKey = Int + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let email = StructuredQueriesCore._TableColumn.for("email", keyPath: \QueryValue.email, default: "") + public let age = StructuredQueriesCore._TableColumn.for("age", keyPath: \QueryValue.age) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.email, QueryValue.columns.age] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.email._allColumns) + allColumns.append(contentsOf: QueryValue.columns.age._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.email, QueryValue.columns.age] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.email._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.age._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.email), \(self.age)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = User + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression, + email: some StructuredQueriesCore.QueryExpression = String?(queryOutput: ""), + age: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: email._allColumns) + allColumns.append(contentsOf: age._allColumns) + self.allColumns = allColumns + } + } + public struct Draft: StructuredQueriesCore.TableDraft { public typealias PrimaryTable = User let id: /* TODO: UUID */ Int? // Primary key @@ -101,33 +251,64 @@ extension SnapshotTests { var age: Int public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let email = StructuredQueriesCore.TableColumn("email", keyPath: \QueryValue.email, default: "") - public let age = StructuredQueriesCore.TableColumn("age", keyPath: \QueryValue.age) + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public let email = StructuredQueriesCore._TableColumn.for("email", keyPath: \QueryValue.email, default: "") + public let age = StructuredQueriesCore._TableColumn.for("age", keyPath: \QueryValue.age) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.email, QueryValue.columns.age] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.email._allColumns) + allColumns.append(contentsOf: QueryValue.columns.age._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.email, QueryValue.columns.age] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.email._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.age._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.email), \(self.age)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = Int?(queryOutput: nil), + email: some StructuredQueriesCore.QueryExpression = String?(queryOutput: ""), + age: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: email._allColumns) + allColumns.append(contentsOf: age._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int?._columnWidth, String?._columnWidth, Int._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { User.tableName } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) + self.id = try decoder.decode(Int.self) ?? nil self.email = try decoder.decode(String.self) ?? "" let age = try decoder.decode(Int.self) guard let age else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.age = age } @@ -149,10 +330,15 @@ extension SnapshotTests { } } - nonisolated extension User: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable { + nonisolated extension User: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth, String?._columnWidth, Int._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "users" } @@ -161,10 +347,10 @@ extension SnapshotTests { self.email = try decoder.decode(String.self) ?? "" // TODO: Should this be non-optional? let age = try decoder.decode(Int.self) guard let id else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } guard let age else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.id = id self.age = age @@ -189,30 +375,51 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Foo - public let bar = StructuredQueriesCore.TableColumn("bar", keyPath: \QueryValue.bar) + public let bar = StructuredQueriesCore._TableColumn.for("bar", keyPath: \QueryValue.bar) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.bar] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.bar._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.bar] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.bar._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.bar)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + bar: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: bar._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Foo: StructuredQueriesCore.Table { + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "foo" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let bar = try decoder.decode(Int.self) guard let bar else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.bar = bar } @@ -276,23 +483,44 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Bar - public let baz = StructuredQueriesCore.TableColumn("baz", keyPath: \QueryValue.baz) + public let baz = StructuredQueriesCore._TableColumn.for("baz", keyPath: \QueryValue.baz) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.baz] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.baz._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.baz] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.baz._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.baz)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Bar + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + baz: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: baz._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Bar: StructuredQueriesCore.Table { + nonisolated extension Bar: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "bar" } @@ -300,7 +528,7 @@ extension SnapshotTests { public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let baz = try decoder.decode(Int.self) guard let baz else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.baz = baz } @@ -370,26 +598,59 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Foo - public let c1 = StructuredQueriesCore.TableColumn("c1", keyPath: \QueryValue.c1, default: true) - public let c2 = StructuredQueriesCore.TableColumn("c2", keyPath: \QueryValue.c2, default: 1) - public let c3 = StructuredQueriesCore.TableColumn("c3", keyPath: \QueryValue.c3, default: 1.2) - public let c4 = StructuredQueriesCore.TableColumn("c4", keyPath: \QueryValue.c4, default: "") + public let c1 = StructuredQueriesCore._TableColumn.for("c1", keyPath: \QueryValue.c1, default: true) + public let c2 = StructuredQueriesCore._TableColumn.for("c2", keyPath: \QueryValue.c2, default: 1) + public let c3 = StructuredQueriesCore._TableColumn.for("c3", keyPath: \QueryValue.c3, default: 1.2) + public let c4 = StructuredQueriesCore._TableColumn.for("c4", keyPath: \QueryValue.c4, default: "") public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.c1, QueryValue.columns.c2, QueryValue.columns.c3, QueryValue.columns.c4] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.c1._allColumns) + allColumns.append(contentsOf: QueryValue.columns.c2._allColumns) + allColumns.append(contentsOf: QueryValue.columns.c3._allColumns) + allColumns.append(contentsOf: QueryValue.columns.c4._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.c1, QueryValue.columns.c2, QueryValue.columns.c3, QueryValue.columns.c4] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.c1._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.c2._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.c3._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.c4._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.c1), \(self.c2), \(self.c3), \(self.c4)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + c1: some StructuredQueriesCore.QueryExpression = Swift.Bool(queryOutput: true), + c2: some StructuredQueriesCore.QueryExpression = Swift.Int(queryOutput: 1), + c3: some StructuredQueriesCore.QueryExpression = Swift.Double(queryOutput: 1.2), + c4: some StructuredQueriesCore.QueryExpression = Swift.String(queryOutput: "") + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: c1._allColumns) + allColumns.append(contentsOf: c2._allColumns) + allColumns.append(contentsOf: c3._allColumns) + allColumns.append(contentsOf: c4._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Foo: StructuredQueriesCore.Table { + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Swift.Bool._columnWidth, Swift.Int._columnWidth, Swift.Double._columnWidth, Swift.String._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "foos" } @@ -422,28 +683,49 @@ extension SnapshotTests { public typealias QueryValue = Foo public let bar = StructuredQueriesCore.TableColumn("Bar", keyPath: \QueryValue.bar) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.bar] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.bar._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.bar] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.bar._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.bar)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + bar: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: bar._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Foo: StructuredQueriesCore.Table { + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "foos" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let bar = try decoder.decode(Int.self) guard let bar else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.bar = bar } @@ -514,28 +796,49 @@ extension SnapshotTests { public typealias QueryValue = Foo public let bar = StructuredQueriesCore.TableColumn("bar", keyPath: \QueryValue.bar) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.bar] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.bar._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.bar] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.bar._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.bar)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + bar: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: bar._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Foo: StructuredQueriesCore.Table { + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Date.UnixTimeRepresentation._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "foos" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let bar = try decoder.decode(Date.UnixTimeRepresentation.self) guard let bar else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.bar = bar } @@ -561,26 +864,48 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = User - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name) - public var generated: StructuredQueriesCore.GeneratedColumn { - StructuredQueriesCore.GeneratedColumn("generated", keyPath: \QueryValue.generated) - } + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name) + public let generated = StructuredQueriesCore.GeneratedColumn("generated", keyPath: \QueryValue.generated) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.name, QueryValue.columns.generated] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + allColumns.append(contentsOf: QueryValue.columns.generated._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.name), \(self.generated)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = User + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + name: some StructuredQueriesCore.QueryExpression, + generated: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: name._allColumns) + allColumns.append(contentsOf: generated._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension User: StructuredQueriesCore.Table { + nonisolated extension User: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [String._columnWidth, String._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "users" } @@ -588,10 +913,10 @@ extension SnapshotTests { let name = try decoder.decode(String.self) let generated = try decoder.decode(String.self) guard let name else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } guard let generated else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.name = name self.generated = generated @@ -637,26 +962,48 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = User - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name) - public var generated: StructuredQueriesCore.GeneratedColumn { - StructuredQueriesCore.GeneratedColumn("generated", keyPath: \QueryValue.generated) - } + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name) + public let generated = StructuredQueriesCore.GeneratedColumn("generated", keyPath: \QueryValue.generated) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.name, QueryValue.columns.generated] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + allColumns.append(contentsOf: QueryValue.columns.generated._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.name), \(self.generated)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = User + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + name: some StructuredQueriesCore.QueryExpression, + generated: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: name._allColumns) + allColumns.append(contentsOf: generated._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension User: StructuredQueriesCore.Table { + nonisolated extension User: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [String._columnWidth, String._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "users" } @@ -664,10 +1011,10 @@ extension SnapshotTests { let name = try decoder.decode(String.self) let generated = try decoder.decode(String.self) guard let name else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } guard let generated else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.name = name self.generated = generated @@ -696,56 +1043,103 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { public typealias QueryValue = User - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name) - public var generated: StructuredQueriesCore.GeneratedColumn { - StructuredQueriesCore.GeneratedColumn("generated", keyPath: \QueryValue.generated) - } - public var primaryKey: StructuredQueriesCore.TableColumn { - self.id - } + public typealias PrimaryKey = Int + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name) + public let generated = StructuredQueriesCore.GeneratedColumn("generated", keyPath: \QueryValue.generated) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name, QueryValue.columns.generated] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + allColumns.append(contentsOf: QueryValue.columns.generated._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.name), \(self.generated)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = User + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression, + name: some StructuredQueriesCore.QueryExpression, + generated: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: name._allColumns) + allColumns.append(contentsOf: generated._allColumns) + self.allColumns = allColumns + } + } + public struct Draft: StructuredQueriesCore.TableDraft { public typealias PrimaryTable = User let id: Int? var name: String public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name) + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.name)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = Int?(queryOutput: nil), + name: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: name._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int?._columnWidth, String._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { User.tableName } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) + self.id = try decoder.decode(Int.self) ?? nil let name = try decoder.decode(String.self) guard let name else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.name = name } @@ -764,10 +1158,15 @@ extension SnapshotTests { } } - nonisolated extension User: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable { + nonisolated extension User: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth, String._columnWidth, Int._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "users" } @@ -776,13 +1175,13 @@ extension SnapshotTests { let name = try decoder.decode(String.self) let generated = try decoder.decode(Int.self) guard let id else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } guard let name else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } guard let generated else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.id = id self.name = name @@ -810,30 +1209,51 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Foo - public let bar = StructuredQueriesCore.TableColumn("bar", keyPath: \QueryValue.bar) + public let bar = StructuredQueriesCore._TableColumn.for("bar", keyPath: \QueryValue.bar) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.bar] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.bar._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.bar] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.bar._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.bar)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + bar: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: bar._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Foo: StructuredQueriesCore.Table { + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "foos" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let bar = try decoder.decode(Int.self) guard let bar else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.bar = bar } @@ -859,30 +1279,51 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Foo - public let bar = StructuredQueriesCore.TableColumn("bar", keyPath: \QueryValue.bar) + public let bar = StructuredQueriesCore._TableColumn.for("bar", keyPath: \QueryValue.bar) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.bar] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.bar._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.bar] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.bar._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.bar)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + bar: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: bar._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Foo: StructuredQueriesCore.Table { + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "foos" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let bar = try decoder.decode(Int.self) guard let bar else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.bar = bar } @@ -906,30 +1347,51 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Foo - public let `bar` = StructuredQueriesCore.TableColumn("bar", keyPath: \QueryValue.`bar`) + public let `bar` = StructuredQueriesCore._TableColumn.for("bar", keyPath: \QueryValue.`bar`) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.`bar`] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.`bar`._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.`bar`] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.`bar`._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.`bar`)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + `bar`: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: `bar`._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Foo: StructuredQueriesCore.Table { + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "foos" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let `bar` = try decoder.decode(Int.self) guard let `bar` else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.`bar` = `bar` } @@ -953,30 +1415,51 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Foo - public let bar = StructuredQueriesCore.TableColumn>("bar", keyPath: \QueryValue.bar) + public let bar = StructuredQueriesCore._TableColumn>.for("bar", keyPath: \QueryValue.bar) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.bar] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.bar._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.bar] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.bar._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.bar)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + bar: some StructuredQueriesCore.QueryExpression> + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: bar._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Foo: StructuredQueriesCore.Table { + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [ID._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "foos" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let bar = try decoder.decode(ID.self) guard let bar else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.bar = bar } @@ -1000,23 +1483,44 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Foo - public let bar = StructuredQueriesCore.TableColumn("bar", keyPath: \QueryValue.bar, default: ID()) + public let bar = StructuredQueriesCore._TableColumn.for("bar", keyPath: \QueryValue.bar, default: ID()) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.bar] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.bar._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.bar] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.bar._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.bar)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + bar: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: bar._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Foo: StructuredQueriesCore.Table { + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "foos" } @@ -1047,51 +1551,97 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { public typealias QueryValue = User + public typealias PrimaryKey = ID public let id = StructuredQueriesCore.TableColumn>("id", keyPath: \QueryValue.id) - public let referrerID = StructuredQueriesCore.TableColumn?>("referrerID", keyPath: \QueryValue.referrerID) - public var primaryKey: StructuredQueriesCore.TableColumn> { - self.id - } + public let primaryKey = StructuredQueriesCore.TableColumn>("id", keyPath: \QueryValue.id) + public let referrerID = StructuredQueriesCore.TableColumn?>("referrerID", keyPath: \QueryValue.referrerID, default: nil) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.referrerID] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.referrerID._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.referrerID] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.referrerID._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.referrerID)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = User + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression>, + referrerID: some StructuredQueriesCore.QueryExpression?> = ID?(queryOutput: nil) + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: referrerID._allColumns) + self.allColumns = allColumns + } + } + public struct Draft: StructuredQueriesCore.TableDraft { public typealias PrimaryTable = User let id: ID? var referrerID: ID? public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn?>("id", keyPath: \QueryValue.id) - public let referrerID = StructuredQueriesCore.TableColumn?>("referrerID", keyPath: \QueryValue.referrerID) + public let id = StructuredQueriesCore.TableColumn?>("id", keyPath: \QueryValue.id, default: nil) + public let referrerID = StructuredQueriesCore.TableColumn?>("referrerID", keyPath: \QueryValue.referrerID, default: nil) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.referrerID] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.referrerID._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.referrerID] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.referrerID._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.referrerID)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression?> = ID?(queryOutput: nil), + referrerID: some StructuredQueriesCore.QueryExpression?> = ID?(queryOutput: nil) + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: referrerID._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [ID?._columnWidth, ID?._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { User.tableName } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(ID.self) - self.referrerID = try decoder.decode(ID.self) + self.id = try decoder.decode(ID.self) ?? nil + self.referrerID = try decoder.decode(ID.self) ?? nil } public nonisolated init(_ other: User) { @@ -1108,18 +1658,23 @@ extension SnapshotTests { } } - nonisolated extension User: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable { + nonisolated extension User: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [ID._columnWidth, ID?._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "users" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let id = try decoder.decode(ID.self) - self.referrerID = try decoder.decode(ID.self) + self.referrerID = try decoder.decode(ID.self) ?? nil guard let id else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.id = id } @@ -1145,30 +1700,51 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = SyncUp - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name) + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.name] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.name)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = SyncUp + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + name: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: name._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension SyncUp: StructuredQueriesCore.Table { + nonisolated extension SyncUp: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [String._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "syncUps" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let name = try decoder.decode(String.self) guard let name else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.name = name } @@ -1196,53 +1772,99 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { public typealias QueryValue = SyncUp - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name) - public var primaryKey: StructuredQueriesCore.TableColumn { - self.id - } + public typealias PrimaryKey = Int + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.name)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = SyncUp + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression, + name: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: name._allColumns) + self.allColumns = allColumns + } + } + public struct Draft: StructuredQueriesCore.TableDraft { public typealias PrimaryTable = SyncUp let id: Int? var name: String public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name) + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.name)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = Int?(queryOutput: nil), + name: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: name._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int?._columnWidth, String._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { SyncUp.tableName } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) + self.id = try decoder.decode(Int.self) ?? nil let name = try decoder.decode(String.self) guard let name else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.name = name } @@ -1261,10 +1883,15 @@ extension SnapshotTests { } } - nonisolated extension SyncUp: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable { + nonisolated extension SyncUp: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth, String._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "syncUps" } @@ -1272,10 +1899,10 @@ extension SnapshotTests { let id = try decoder.decode(Int.self) let name = try decoder.decode(String.self) guard let id else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } guard let name else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.id = id self.name = name @@ -1318,50 +1945,96 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { public typealias QueryValue = SyncUp - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let seconds = StructuredQueriesCore.TableColumn>("seconds", keyPath: \QueryValue.seconds, default: 60 * 5) - public var primaryKey: StructuredQueriesCore.TableColumn { - self.id - } + public typealias PrimaryKey = Int + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let seconds = StructuredQueriesCore._TableColumn>.for("seconds", keyPath: \QueryValue.seconds, default: 60 * 5) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.seconds] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.seconds._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.seconds] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.seconds._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.seconds)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = SyncUp + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression, + seconds: some StructuredQueriesCore.QueryExpression<<#Type#>> = <#Type#>(queryOutput: 60 * 5) + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: seconds._allColumns) + self.allColumns = allColumns + } + } + public struct Draft: StructuredQueriesCore.TableDraft { public typealias PrimaryTable = SyncUp let id: Int? var seconds: <#Type#> = 60 * 5 public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let seconds = StructuredQueriesCore.TableColumn>("seconds", keyPath: \QueryValue.seconds, default: 60 * 5) + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public let seconds = StructuredQueriesCore._TableColumn>.for("seconds", keyPath: \QueryValue.seconds, default: 60 * 5) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.seconds] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.seconds._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.seconds] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.seconds._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.seconds)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = Int?(queryOutput: nil), + seconds: some StructuredQueriesCore.QueryExpression<<#Type#>> = <#Type#>(queryOutput: 60 * 5) + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: seconds._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int?._columnWidth, <#Type#>._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { SyncUp.tableName } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) + self.id = try decoder.decode(Int.self) ?? nil self.seconds = try decoder.decode(<#Type#>.self) ?? 60 * 5 } @@ -1379,10 +2052,15 @@ extension SnapshotTests { } } - nonisolated extension SyncUp: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable { + nonisolated extension SyncUp: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth, <#Type#>._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "syncUps" } @@ -1390,7 +2068,7 @@ extension SnapshotTests { let id = try decoder.decode(Int.self) self.seconds = try decoder.decode(<#Type#>.self) ?? 60 * 5 guard let id else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.id = id } @@ -1419,23 +2097,46 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { public typealias QueryValue = RemindersList - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) + public typealias PrimaryKey = Int + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) public let color = StructuredQueriesCore.TableColumn("color", keyPath: \QueryValue.color, default: Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255)) - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name, default: "") - public var primaryKey: StructuredQueriesCore.TableColumn { - self.id - } + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name, default: "") public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.color, QueryValue.columns.name] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.color._allColumns) + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.color, QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.color._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.color), \(self.name)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = RemindersList + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression, + color: some StructuredQueriesCore.QueryExpression = Color.HexRepresentation(queryOutput: Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255)), + name: some StructuredQueriesCore.QueryExpression = Swift.String(queryOutput: "") + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: color._allColumns) + allColumns.append(contentsOf: name._allColumns) + self.allColumns = allColumns + } + } + public struct Draft: StructuredQueriesCore.TableDraft { public typealias PrimaryTable = RemindersList var id: Int? @@ -1443,29 +2144,60 @@ extension SnapshotTests { var name = "" public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) public let color = StructuredQueriesCore.TableColumn("color", keyPath: \QueryValue.color, default: Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255)) - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name, default: "") + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name, default: "") public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.color, QueryValue.columns.name] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.color._allColumns) + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.color, QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.color._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id), \(self.color), \(self.name)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = Int?(queryOutput: nil), + color: some StructuredQueriesCore.QueryExpression = Color.HexRepresentation(queryOutput: Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255)), + name: some StructuredQueriesCore.QueryExpression = Swift.String(queryOutput: "") + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: color._allColumns) + allColumns.append(contentsOf: name._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int?._columnWidth, Color.HexRepresentation._columnWidth, Swift.String._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { RemindersList.tableName } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) + self.id = try decoder.decode(Int.self) ?? nil self.color = try decoder.decode(Color.HexRepresentation.self) ?? Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255) self.name = try decoder.decode(Swift.String.self) ?? "" } @@ -1487,10 +2219,15 @@ extension SnapshotTests { } } - nonisolated extension RemindersList: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable { + nonisolated extension RemindersList: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth, Color.HexRepresentation._columnWidth, Swift.String._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "remindersLists" } @@ -1499,7 +2236,7 @@ extension SnapshotTests { self.color = try decoder.decode(Color.HexRepresentation.self) ?? Color(red: 0x4a / 255, green: 0x99 / 255, blue: 0xef / 255) self.name = try decoder.decode(Swift.String.self) ?? "" guard let id else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.id = id } @@ -1546,30 +2283,51 @@ extension SnapshotTests { public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Foo - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name) + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.name] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.name)" } } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + name: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: name._allColumns) + self.allColumns = allColumns + } + } } - nonisolated extension Foo: StructuredQueriesCore.Table { + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [String._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { "foos" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { let name = try decoder.decode(String.self) guard let name else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.name = name } @@ -1578,496 +2336,1742 @@ extension SnapshotTests { } } - @MainActor - @Suite struct PrimaryKeyTests { - @Test func basics() { + #if StructuredQueriesCasePaths + @Test func enumBasics() { assertMacro { """ - @Table - struct Foo { - let id: Int + @CasePathable @Table + enum Post { + @Columns + case photo(Photo) + case note(String = "") } """ } expansion: { #""" - struct Foo { - let id: Int + @CasePathable + enum Post { + case photo(Photo) + case note(String = "") - public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { - public typealias QueryValue = Foo - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public var primaryKey: StructuredQueriesCore.TableColumn { - self.id - } + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Post + public let photo = StructuredQueriesCore.ColumnGroup(keyPath: \QueryValue.photo) + public let note = StructuredQueriesCore._TableColumn.for("note", keyPath: \QueryValue.note, default: "") public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.photo._allColumns) + allColumns.append(contentsOf: QueryValue.columns.note._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.photo._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.note._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { - "\(self.id)" + "\(self.photo), \(self.note)" } } - public struct Draft: StructuredQueriesCore.TableDraft { - public typealias PrimaryTable = Foo - let id: Int? - public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { - public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id] - } - public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id] - } - public var queryFragment: QueryFragment { - "\(self.id)" - } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Post + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public static func photo( + _ photo: some StructuredQueriesCore.QueryExpression + ) -> Self { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: photo._allColumns) + allColumns.append(contentsOf: Photo?(queryOutput: nil)._allColumns) + return Self(allColumns: allColumns) } - public nonisolated static var columns: TableColumns { - TableColumns() + public static func note( + _ note: some StructuredQueriesCore.QueryExpression + ) -> Self { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: String?(queryOutput: nil)._allColumns) + allColumns.append(contentsOf: note._allColumns) + return Self(allColumns: allColumns) } + } + } - public nonisolated static var tableName: String { - Foo.tableName + nonisolated extension Post: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var _columnWidth: Int { + [Photo._columnWidth, String._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { + "posts" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + if let photo = try decoder.decode(Photo.self) { + self = .photo(photo) + } else if let note = try decoder.decode(String.self) { + self = .note(note) + } else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } + } + } + """# + } + } - public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) + @Test func enumDiagnostic() { + assertMacro { + """ + @Table + enum Post { + case photo(Photo) + case note(String = "") + } + """ + } diagnostics: { + """ + @Table + ┬───── + ╰─ 🛑 '@Table' enum type missing required '@CasePathable' macro application + ✏️ Insert '@CasePathable' + enum Post { + case photo(Photo) + case note(String = "") + } + """ + } fixes: { + """ + @CasePathable @Table + enum Post { + case photo(Photo) + case note(String = "") + } + """ + } expansion: { + #""" + @CasePathable + enum Post { + case photo(Photo) + case note(String = "") + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Post + public let photo = StructuredQueriesCore._TableColumn.for("photo", keyPath: \QueryValue.photo) + public let note = StructuredQueriesCore._TableColumn.for("note", keyPath: \QueryValue.note, default: "") + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.photo._allColumns) + allColumns.append(contentsOf: QueryValue.columns.note._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.photo._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.note._writableColumns) + return writableColumns } + public var queryFragment: QueryFragment { + "\(self.photo), \(self.note)" + } + } - public nonisolated init(_ other: Foo) { - self.id = other.id + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Post + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public static func photo( + _ photo: some StructuredQueriesCore.QueryExpression + ) -> Self { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: photo._allColumns) + allColumns.append(contentsOf: Photo?(queryOutput: nil)._allColumns) + return Self(allColumns: allColumns) } - public init( - id: Int? = nil - ) { - self.id = id + public static func note( + _ note: some StructuredQueriesCore.QueryExpression + ) -> Self { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: String?(queryOutput: nil)._allColumns) + allColumns.append(contentsOf: note._allColumns) + return Self(allColumns: allColumns) } } } - nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable { + nonisolated extension Post: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Photo._columnWidth, String._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { - "foos" + "posts" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let id = try decoder.decode(Int.self) - guard let id else { - throw QueryDecodingError.missingRequiredColumn + if let photo = try decoder.decode(Photo.self) { + self = .photo(photo) + } else if let note = try decoder.decode(String.self) { + self = .note(note) + } else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } - self.id = id } } """# } + } + @Test func enumDiagnostic_SingleLine() { assertMacro { - #""" - struct Foo { - @Column("id", primaryKey: true) - let id: Int + """ + @Table enum Post { + case photo(Photo) + case note(String = "") + } + """ + } diagnostics: { + """ + @Table enum Post { + ┬───── + ╰─ 🛑 '@Table' enum type missing required '@CasePathable' macro application + ✏️ Insert '@CasePathable' + case photo(Photo) + case note(String = "") + } + """ + } fixes: { + """ + @CasePathable @Table enum Post { + case photo(Photo) + case note(String = "") } + """ + } expansion: { + #""" + @CasePathable enum Post { + case photo(Photo) + case note(String = "") - extension Foo: StructuredQueries.Table { - public struct Columns: StructuredQueries.TableDefinition { - public typealias QueryValue = Foo - public let id = StructuredQueries.Column("id", keyPath: \QueryValue.id) - public var allColumns: [any StructuredQueries.ColumnExpression] { - [self.id] + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Post + public let photo = StructuredQueriesCore._TableColumn.for("photo", keyPath: \QueryValue.photo) + public let note = StructuredQueriesCore._TableColumn.for("note", keyPath: \QueryValue.note, default: "") + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.photo._allColumns) + allColumns.append(contentsOf: QueryValue.columns.note._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.photo._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.note._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.photo), \(self.note)" } } - @_Draft(Foo.self) - public struct Draft { - @Column(primaryKey: false) - let id: Int + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Post + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public static func photo( + _ photo: some StructuredQueriesCore.QueryExpression + ) -> Self { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: photo._allColumns) + allColumns.append(contentsOf: Photo?(queryOutput: nil)._allColumns) + return Self(allColumns: allColumns) + } + public static func note( + _ note: some StructuredQueriesCore.QueryExpression + ) -> Self { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: String?(queryOutput: nil)._allColumns) + allColumns.append(contentsOf: note._allColumns) + return Self(allColumns: allColumns) + } } - public static let columns = Columns() - public static let tableName = "foos" - public init(decoder: some StructuredQueries.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) + } + + nonisolated extension Post: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var _columnWidth: Int { + [Photo._columnWidth, String._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { + "posts" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + if let photo = try decoder.decode(Photo.self) { + self = .photo(photo) + } else if let note = try decoder.decode(String.self) { + self = .note(note) + } else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } } } """# + } + } + + @Test func enumFirstNames() { + assertMacro { + """ + @CasePathable @Table + enum Post { + case photo(Photo) + case note(text: String = "") + } + """ } expansion: { #""" - struct Foo { - let id: Int + @CasePathable + enum Post { + case photo(Photo) + case note(text: String = "") + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Post + public let photo = StructuredQueriesCore._TableColumn.for("photo", keyPath: \QueryValue.photo) + public let note = StructuredQueriesCore._TableColumn.for("note", keyPath: \QueryValue.note, default: "") + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.photo._allColumns) + allColumns.append(contentsOf: QueryValue.columns.note._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.photo._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.note._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.photo), \(self.note)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Post + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public static func photo( + _ photo: some StructuredQueriesCore.QueryExpression + ) -> Self { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: photo._allColumns) + allColumns.append(contentsOf: Photo?(queryOutput: nil)._allColumns) + return Self(allColumns: allColumns) + } + public static func note( + text note: some StructuredQueriesCore.QueryExpression + ) -> Self { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: String?(queryOutput: nil)._allColumns) + allColumns.append(contentsOf: note._allColumns) + return Self(allColumns: allColumns) + } + } } - extension Foo: StructuredQueries.Table { - public struct Columns: StructuredQueries.TableDefinition { - public typealias QueryValue = Foo - public let id = StructuredQueries.Column("id", keyPath: \QueryValue.id) - public var allColumns: [any StructuredQueries.ColumnExpression] { - [self.id] + nonisolated extension Post: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var _columnWidth: Int { + [Photo._columnWidth, String._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { + "posts" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + if let photo = try decoder.decode(Photo.self) { + self = .photo(photo) + } else if let note = try decoder.decode(String.self) { + self = .note(note) + } else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } } - public struct Draft { - let id: Int + } + """# + } + } - public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { - public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id] - } - public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id] - } - public var queryFragment: QueryFragment { - "\(self.id)" - } + @Test func enumCustomColumn() { + assertMacro { + """ + @CasePathable @Table + enum Post { + @Column("note_text") + case note(text: String = "") + } + """ + } expansion: { + #""" + @CasePathable + enum Post { + case note(text: String = "") + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Post + public let note = StructuredQueriesCore.TableColumn("note_text", keyPath: \QueryValue.note, default: "") + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.note._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.note._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.note)" } } - public static let columns = Columns() - public static let tableName = "foos" - public init(decoder: some StructuredQueries.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Post + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public static func note( + text note: some StructuredQueriesCore.QueryExpression + ) -> Self { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: note._allColumns) + return Self(allColumns: allColumns) + } } } - nonisolated extension Foo.Draft: StructuredQueriesCore.Table { + nonisolated extension Post: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [String._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { - Foo.tableName + "posts" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let id = try decoder.decode(Int.self) - guard let id else { - throw QueryDecodingError.missingRequiredColumn + if let note = try decoder.decode(String.self) { + self = .note(note) + } else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } - self.id = id - } - public nonisolated init(_ other: Foo) { - self.id = other.id } } """# } } - @Test func willSet() { + @Test func enumCustomRepresentation() { assertMacro { """ - @Table - struct Foo { - var id: Int { - willSet { print(newValue) } + @CasePathable @Table + enum Post { + @Column(as: Date.UnixTimeRepresentation.self) + case timestamp(Date) + } + """ + } expansion: { + #""" + @CasePathable + enum Post { + case timestamp(Date) + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Post + public let timestamp = StructuredQueriesCore.TableColumn("timestamp", keyPath: \QueryValue.timestamp) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.timestamp._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.timestamp._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.timestamp)" + } } - var name: String { - willSet { print(newValue) } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Post + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public static func timestamp( + _ timestamp: some StructuredQueriesCore.QueryExpression + ) -> Self { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: timestamp._allColumns) + return Self(allColumns: allColumns) + } } } + + nonisolated extension Post: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var _columnWidth: Int { + [Date.UnixTimeRepresentation._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { + "posts" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + if let timestamp = try decoder.decode(Date.UnixTimeRepresentation.self) { + self = .timestamp(timestamp) + } else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + } + } + """# + } + } + #endif + + @MainActor + @Suite struct PrimaryKeyTests { + @Test func basics() { + assertMacro { + """ + @Table + struct Foo { + let id: Int + } + """ + } expansion: { + #""" + struct Foo { + let id: Int + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { + public typealias QueryValue = Foo + public typealias PrimaryKey = Int + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.id)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + self.allColumns = allColumns + } + } + + public struct Draft: StructuredQueriesCore.TableDraft { + public typealias PrimaryTable = Foo + let id: Int? + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Draft + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.id)" + } + } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = Int?(queryOutput: nil) + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + + public nonisolated static var columns: TableColumns { + TableColumns() + } + + public nonisolated static var _columnWidth: Int { + [Int?._columnWidth].reduce(0, +) + } + + public nonisolated static var tableName: String { + Foo.tableName + } + + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + self.id = try decoder.decode(Int.self) ?? nil + } + + public nonisolated init(_ other: Foo) { + self.id = other.id + } + public init( + id: Int? = nil + ) { + self.id = id + } + } + } + + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { + "foos" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + let id = try decoder.decode(Int.self) + guard let id else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.id = id + } + } + """# + } + + assertMacro { + #""" + struct Foo { + @Column("id", primaryKey: true) + let id: Int + } + + extension Foo: StructuredQueries.Table { + public struct Columns: StructuredQueries.TableDefinition { + public typealias QueryValue = Foo + public let id = StructuredQueries.Column("id", keyPath: \QueryValue.id) + public var allColumns: [any StructuredQueries.ColumnExpression] { + [self.id] + } + } + @_Draft(Foo.self) + public struct Draft { + @Column(primaryKey: false) + let id: Int + } + public static let columns = Columns() + public static let tableName = "foos" + public init(decoder: some StructuredQueries.QueryDecoder) throws { + self.id = try decoder.decode(Int.self) + } + } + """# + } expansion: { + #""" + struct Foo { + let id: Int + } + + extension Foo: StructuredQueries.Table { + public struct Columns: StructuredQueries.TableDefinition { + public typealias QueryValue = Foo + public let id = StructuredQueries.Column("id", keyPath: \QueryValue.id) + public var allColumns: [any StructuredQueries.ColumnExpression] { + [self.id] + } + } + public struct Draft { + let id: Int + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Draft + public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.id)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + self.allColumns = allColumns + } + } + } + public static let columns = Columns() + public static let tableName = "foos" + public init(decoder: some StructuredQueries.QueryDecoder) throws { + self.id = try decoder.decode(Int.self) + } + } + + nonisolated extension Foo.Draft: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { + Foo.tableName + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + let id = try decoder.decode(Int.self) + guard let id else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.id = id + } + public nonisolated init(_ other: Foo) { + self.id = other.id + } + } + """# + } + } + + @Test func willSet() { + assertMacro { + """ + @Table + struct Foo { + var id: Int { + willSet { print(newValue) } + } + var name: String { + willSet { print(newValue) } + } + } + """ + } expansion: { + #""" + struct Foo { + var id: Int { + willSet { print(newValue) } + } + var name: String { + willSet { print(newValue) } + } + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { + public typealias QueryValue = Foo + public typealias PrimaryKey = Int + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.id), \(self.name)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Foo + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression, + name: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: name._allColumns) + self.allColumns = allColumns + } + } + + public struct Draft: StructuredQueriesCore.TableDraft { + public typealias PrimaryTable = Foo + var id: Int? + var name: String + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Draft + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public let name = StructuredQueriesCore._TableColumn.for("name", keyPath: \QueryValue.name) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.name._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.name._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.id), \(self.name)" + } + } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = Int?(queryOutput: nil), + name: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: name._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + + public nonisolated static var columns: TableColumns { + TableColumns() + } + + public nonisolated static var _columnWidth: Int { + [Int?._columnWidth, String._columnWidth].reduce(0, +) + } + + public nonisolated static var tableName: String { + Foo.tableName + } + + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + self.id = try decoder.decode(Int.self) ?? nil + let name = try decoder.decode(String.self) + guard let name else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.name = name + } + + public nonisolated init(_ other: Foo) { + self.id = other.id + self.name = other.name + } + public init( + id: Int? = nil, + name: String + ) { + self.id = id + self.name = name + } + } + } + + nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth, String._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { + "foos" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + let id = try decoder.decode(Int.self) + let name = try decoder.decode(String.self) + guard let id else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + guard let name else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.id = id + self.name = name + } + } + """# + } + } + + @Test func advanced() { + assertMacro { + """ + @Table + struct Reminder { + let id: Int + var title = "" + @Column(as: Date.UnixTimeRepresentation?.self) + var date: Date? + var priority: Priority? + } + """ + } expansion: { + #""" + struct Reminder { + let id: Int + var title = "" + var date: Date? + var priority: Priority? + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { + public typealias QueryValue = Reminder + public typealias PrimaryKey = Int + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let title = StructuredQueriesCore._TableColumn.for("title", keyPath: \QueryValue.title, default: "") + public let date = StructuredQueriesCore.TableColumn("date", keyPath: \QueryValue.date, default: nil) + public let priority = StructuredQueriesCore._TableColumn.for("priority", keyPath: \QueryValue.priority, default: nil) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.title._allColumns) + allColumns.append(contentsOf: QueryValue.columns.date._allColumns) + allColumns.append(contentsOf: QueryValue.columns.priority._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.title._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.date._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.priority._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.id), \(self.title), \(self.date), \(self.priority)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Reminder + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression, + title: some StructuredQueriesCore.QueryExpression = Swift.String(queryOutput: ""), + date: some StructuredQueriesCore.QueryExpression = Date.UnixTimeRepresentation?(queryOutput: nil), + priority: some StructuredQueriesCore.QueryExpression = Priority?(queryOutput: nil) + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: title._allColumns) + allColumns.append(contentsOf: date._allColumns) + allColumns.append(contentsOf: priority._allColumns) + self.allColumns = allColumns + } + } + + public struct Draft: StructuredQueriesCore.TableDraft { + public typealias PrimaryTable = Reminder + let id: Int? + var title = "" + var date: Date? + var priority: Priority? + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Draft + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public let title = StructuredQueriesCore._TableColumn.for("title", keyPath: \QueryValue.title, default: "") + public let date = StructuredQueriesCore.TableColumn("date", keyPath: \QueryValue.date, default: nil) + public let priority = StructuredQueriesCore._TableColumn.for("priority", keyPath: \QueryValue.priority, default: nil) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.title._allColumns) + allColumns.append(contentsOf: QueryValue.columns.date._allColumns) + allColumns.append(contentsOf: QueryValue.columns.priority._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.title._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.date._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.priority._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.id), \(self.title), \(self.date), \(self.priority)" + } + } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = Int?(queryOutput: nil), + title: some StructuredQueriesCore.QueryExpression = Swift.String(queryOutput: ""), + date: some StructuredQueriesCore.QueryExpression = Date.UnixTimeRepresentation?(queryOutput: nil), + priority: some StructuredQueriesCore.QueryExpression = Priority?(queryOutput: nil) + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: title._allColumns) + allColumns.append(contentsOf: date._allColumns) + allColumns.append(contentsOf: priority._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + + public nonisolated static var columns: TableColumns { + TableColumns() + } + + public nonisolated static var _columnWidth: Int { + [Int?._columnWidth, Swift.String._columnWidth, Date.UnixTimeRepresentation?._columnWidth, Priority?._columnWidth].reduce(0, +) + } + + public nonisolated static var tableName: String { + Reminder.tableName + } + + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + self.id = try decoder.decode(Int.self) ?? nil + self.title = try decoder.decode(Swift.String.self) ?? "" + self.date = try decoder.decode(Date.UnixTimeRepresentation.self) ?? nil + self.priority = try decoder.decode(Priority.self) ?? nil + } + + public nonisolated init(_ other: Reminder) { + self.id = other.id + self.title = other.title + self.date = other.date + self.priority = other.priority + } + public init( + id: Int? = nil, + title: Swift.String = "", + date: Date? = nil, + priority: Priority? = nil + ) { + self.id = id + self.title = title + self.date = date + self.priority = priority + } + } + } + + nonisolated extension Reminder: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth, Swift.String._columnWidth, Date.UnixTimeRepresentation?._columnWidth, Priority?._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { + "reminders" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + let id = try decoder.decode(Int.self) + self.title = try decoder.decode(Swift.String.self) ?? "" + self.date = try decoder.decode(Date.UnixTimeRepresentation.self) ?? nil + self.priority = try decoder.decode(Priority.self) ?? nil + guard let id else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.id = id + } + } + """# + } + } + + @Test func uuid() { + assertMacro { + """ + @Table + struct Reminder { + @Column(as: UUID.BytesRepresentation.self) + let id: UUID + } + """ + } expansion: { + #""" + struct Reminder { + let id: UUID + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { + public typealias QueryValue = Reminder + public typealias PrimaryKey = UUID.BytesRepresentation + public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.id)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Reminder + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + self.allColumns = allColumns + } + } + + public struct Draft: StructuredQueriesCore.TableDraft { + public typealias PrimaryTable = Reminder + let id: UUID? + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Draft + public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id, default: nil) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.id)" + } + } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = UUID.BytesRepresentation?(queryOutput: nil) + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + + public nonisolated static var columns: TableColumns { + TableColumns() + } + + public nonisolated static var _columnWidth: Int { + [UUID.BytesRepresentation?._columnWidth].reduce(0, +) + } + + public nonisolated static var tableName: String { + Reminder.tableName + } + + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + self.id = try decoder.decode(UUID.BytesRepresentation.self) ?? nil + } + + public nonisolated init(_ other: Reminder) { + self.id = other.id + } + public init( + id: UUID? = nil + ) { + self.id = id + } + } + } + + nonisolated extension Reminder: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var _columnWidth: Int { + [UUID.BytesRepresentation._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { + "reminders" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + let id = try decoder.decode(UUID.BytesRepresentation.self) + guard let id else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.id = id + } + } + """# + } + } + + @Test func turnOffPrimaryKey() { + assertMacro { + """ + @Table + struct Reminder { + @Column(primaryKey: false) + let id: Int + } + """ + } expansion: { + #""" + struct Reminder { + let id: Int + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Reminder + public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.id)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Reminder + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + self.allColumns = allColumns + } + } + } + + nonisolated extension Reminder: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var _columnWidth: Int { + [Int._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { + "reminders" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + let id = try decoder.decode(Int.self) + guard let id else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.id = id + } + } + """# + } + } + + @Test func commentAfterOptionalID() { + assertMacro { + """ + @Table + struct Reminder { + let id: Int? // TODO: Migrate to UUID + var title = "" + } """ } expansion: { #""" - struct Foo { - var id: Int { - willSet { print(newValue) } - } - var name: String { - willSet { print(newValue) } - } + struct Reminder { + let id: Int? // TODO: Migrate to UUID + var title = "" public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { - public typealias QueryValue = Foo - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name) - public var primaryKey: StructuredQueriesCore.TableColumn { - self.id - } + public typealias QueryValue = Reminder + public typealias PrimaryKey = Int? + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public let primaryKey = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public let title = StructuredQueriesCore._TableColumn.for("title", keyPath: \QueryValue.title, default: "") public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.title._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.title._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { - "\(self.id), \(self.name)" + "\(self.id), \(self.title)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Reminder + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = Int?(queryOutput: nil), + title: some StructuredQueriesCore.QueryExpression = Swift.String(queryOutput: "") + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: title._allColumns) + self.allColumns = allColumns } } public struct Draft: StructuredQueriesCore.TableDraft { - public typealias PrimaryTable = Foo - var id: Int? - var name: String + public typealias PrimaryTable = Reminder + let id: Int? // TODO: Migrate to UUID + var title = "" public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let name = StructuredQueriesCore.TableColumn("name", keyPath: \QueryValue.name) + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public let title = StructuredQueriesCore._TableColumn.for("title", keyPath: \QueryValue.title, default: "") public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.title._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.name] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.title._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { - "\(self.id), \(self.name)" + "\(self.id), \(self.title)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = Int?(queryOutput: nil), + title: some StructuredQueriesCore.QueryExpression = Swift.String(queryOutput: "") + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: title._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int?._columnWidth, Swift.String._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { - Foo.tableName + Reminder.tableName } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) - let name = try decoder.decode(String.self) - guard let name else { - throw QueryDecodingError.missingRequiredColumn - } - self.name = name + self.id = try decoder.decode(Int.self) ?? nil + self.title = try decoder.decode(Swift.String.self) ?? "" } - public nonisolated init(_ other: Foo) { + public nonisolated init(_ other: Reminder) { self.id = other.id - self.name = other.name + self.title = other.title } public init( id: Int? = nil, - name: String + title: Swift.String = "" ) { self.id = id - self.name = name + self.title = title } } } - nonisolated extension Foo: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable { + nonisolated extension Reminder: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Int?._columnWidth, Swift.String._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { - "foos" + "reminders" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let id = try decoder.decode(Int.self) - let name = try decoder.decode(String.self) - guard let id else { - throw QueryDecodingError.missingRequiredColumn - } - guard let name else { - throw QueryDecodingError.missingRequiredColumn - } - self.id = id - self.name = name + self.id = try decoder.decode(Int.self) ?? nil + self.title = try decoder.decode(Swift.String.self) ?? "" } } """# } } - @Test func advanced() { + @Test func nested() { assertMacro { """ @Table - struct Reminder { - let id: Int - var title = "" - @Column(as: Date.UnixTimeRepresentation?.self) - var date: Date? - var priority: Priority? + private struct Row { + let id: UUID + @Columns + var timestamps: Timestamps } """ } expansion: { #""" - struct Reminder { - let id: Int - var title = "" - var date: Date? - var priority: Priority? + private struct Row { + let id: UUID + var timestamps: Timestamps public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { - public typealias QueryValue = Reminder - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let title = StructuredQueriesCore.TableColumn("title", keyPath: \QueryValue.title, default: "") - public let date = StructuredQueriesCore.TableColumn("date", keyPath: \QueryValue.date) - public let priority = StructuredQueriesCore.TableColumn("priority", keyPath: \QueryValue.priority) - public var primaryKey: StructuredQueriesCore.TableColumn { - self.id - } + public typealias QueryValue = Row + public typealias PrimaryKey = UUID + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let timestamps = StructuredQueriesCore.ColumnGroup(keyPath: \QueryValue.timestamps) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title, QueryValue.columns.date, QueryValue.columns.priority] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.timestamps._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title, QueryValue.columns.date, QueryValue.columns.priority] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.timestamps._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { - "\(self.id), \(self.title), \(self.date), \(self.priority)" + "\(self.id), \(self.timestamps)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Row + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression, + timestamps: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: timestamps._allColumns) + self.allColumns = allColumns } } public struct Draft: StructuredQueriesCore.TableDraft { - public typealias PrimaryTable = Reminder - let id: Int? - var title = "" - var date: Date? - var priority: Priority? + public typealias PrimaryTable = Row + let id: UUID? + var timestamps: Timestamps public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let title = StructuredQueriesCore.TableColumn("title", keyPath: \QueryValue.title, default: "") - public let date = StructuredQueriesCore.TableColumn("date", keyPath: \QueryValue.date) - public let priority = StructuredQueriesCore.TableColumn("priority", keyPath: \QueryValue.priority) + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public let timestamps = StructuredQueriesCore.ColumnGroup(keyPath: \QueryValue.timestamps) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title, QueryValue.columns.date, QueryValue.columns.priority] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.timestamps._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title, QueryValue.columns.date, QueryValue.columns.priority] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.timestamps._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { - "\(self.id), \(self.title), \(self.date), \(self.priority)" + "\(self.id), \(self.timestamps)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = UUID?(queryOutput: nil), + timestamps: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: timestamps._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [UUID?._columnWidth, Timestamps._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { - Reminder.tableName + Row.tableName } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) - self.title = try decoder.decode(Swift.String.self) ?? "" - self.date = try decoder.decode(Date.UnixTimeRepresentation.self) - self.priority = try decoder.decode(Priority.self) + self.id = try decoder.decode(UUID.self) ?? nil + let timestamps = try decoder.decode(Timestamps.self) + guard let timestamps else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.timestamps = timestamps } - public nonisolated init(_ other: Reminder) { + public nonisolated init(_ other: Row) { self.id = other.id - self.title = other.title - self.date = other.date - self.priority = other.priority + self.timestamps = other.timestamps } public init( - id: Int? = nil, - title: Swift.String = "", - date: Date? = nil, - priority: Priority? = nil + id: UUID? = nil, + timestamps: Timestamps ) { self.id = id - self.title = title - self.date = date - self.priority = priority + self.timestamps = timestamps } } } - nonisolated extension Reminder: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable { + nonisolated extension Row: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [UUID._columnWidth, Timestamps._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { - "reminders" + "rows" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let id = try decoder.decode(Int.self) - self.title = try decoder.decode(Swift.String.self) ?? "" - self.date = try decoder.decode(Date.UnixTimeRepresentation.self) - self.priority = try decoder.decode(Priority.self) + let id = try decoder.decode(UUID.self) + let timestamps = try decoder.decode(Timestamps.self) guard let id else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + guard let timestamps else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.id = id + self.timestamps = timestamps } } """# } } - @Test func uuid() { + @Test func nestedLet() { assertMacro { """ - @Table - struct Reminder { - @Column(as: UUID.BytesRepresentation.self) - let id: UUID + @Table("remindersTags") + struct ReminderTag: Identifiable { + @Columns + let id: ReminderTagID } """ } expansion: { #""" - struct Reminder { - let id: UUID + struct ReminderTag: Identifiable { + let id: ReminderTagID public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { - public typealias QueryValue = Reminder - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public var primaryKey: StructuredQueriesCore.TableColumn { - self.id - } + public typealias QueryValue = ReminderTag + public typealias PrimaryKey = ReminderTagID + public let id = StructuredQueriesCore.ColumnGroup(keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore.ColumnGroup(keyPath: \QueryValue.id) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = ReminderTag + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + self.allColumns = allColumns + } + } + public struct Draft: StructuredQueriesCore.TableDraft { - public typealias PrimaryTable = Reminder - let id: UUID? + public typealias PrimaryTable = ReminderTag + let id: ReminderTagID? public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) + public let id = StructuredQueriesCore.ColumnGroup(keyPath: \QueryValue.id) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { "\(self.id)" } } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = ReminderTagID?(queryOutput: nil) + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [ReminderTagID?._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { - Reminder.tableName + ReminderTag.tableName } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(UUID.BytesRepresentation.self) + self.id = try decoder.decode(ReminderTagID.self) ?? nil } - public nonisolated init(_ other: Reminder) { + public nonisolated init(_ other: ReminderTag) { self.id = other.id } public init( - id: UUID? = nil + id: ReminderTagID? = nil ) { self.id = id } } } - nonisolated extension Reminder: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable { + nonisolated extension ReminderTag: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [ReminderTagID._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { - "reminders" + "remindersTags" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let id = try decoder.decode(UUID.BytesRepresentation.self) + let id = try decoder.decode(ReminderTagID.self) guard let id else { - throw QueryDecodingError.missingRequiredColumn + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } self.id = id } @@ -2076,142 +4080,341 @@ extension SnapshotTests { } } - @Test func turnOffPrimaryKey() { + @Test func customPrimaryKey() { assertMacro { """ @Table - struct Reminder { - @Column(primaryKey: false) - let id: Int + private struct ReminderWithList { + @Column(primaryKey: true) + let reminderID: Reminder.ID + let reminderTitle: String + let remindersListTitle: String } """ } expansion: { #""" - struct Reminder { - let id: Int + private struct ReminderWithList { + let reminderID: Reminder.ID + let reminderTitle: String + let remindersListTitle: String - public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { - public typealias QueryValue = Reminder - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { + public typealias QueryValue = ReminderWithList + public typealias PrimaryKey = Reminder.ID + public let reminderID = StructuredQueriesCore.TableColumn("reminderID", keyPath: \QueryValue.reminderID) + public let primaryKey = StructuredQueriesCore.TableColumn("reminderID", keyPath: \QueryValue.reminderID) + public let reminderTitle = StructuredQueriesCore._TableColumn.for("reminderTitle", keyPath: \QueryValue.reminderTitle) + public let remindersListTitle = StructuredQueriesCore._TableColumn.for("remindersListTitle", keyPath: \QueryValue.remindersListTitle) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.reminderID._allColumns) + allColumns.append(contentsOf: QueryValue.columns.reminderTitle._allColumns) + allColumns.append(contentsOf: QueryValue.columns.remindersListTitle._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.reminderID._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.reminderTitle._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.remindersListTitle._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { - "\(self.id)" + "\(self.reminderID), \(self.reminderTitle), \(self.remindersListTitle)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = ReminderWithList + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + reminderID: some StructuredQueriesCore.QueryExpression, + reminderTitle: some StructuredQueriesCore.QueryExpression, + remindersListTitle: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: reminderID._allColumns) + allColumns.append(contentsOf: reminderTitle._allColumns) + allColumns.append(contentsOf: remindersListTitle._allColumns) + self.allColumns = allColumns + } + } + + public struct Draft: StructuredQueriesCore.TableDraft { + public typealias PrimaryTable = ReminderWithList + let reminderID: Reminder.ID? + let reminderTitle: String + let remindersListTitle: String + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = Draft + public let reminderID = StructuredQueriesCore.TableColumn("reminderID", keyPath: \QueryValue.reminderID, default: nil) + public let reminderTitle = StructuredQueriesCore._TableColumn.for("reminderTitle", keyPath: \QueryValue.reminderTitle) + public let remindersListTitle = StructuredQueriesCore._TableColumn.for("remindersListTitle", keyPath: \QueryValue.remindersListTitle) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.reminderID._allColumns) + allColumns.append(contentsOf: QueryValue.columns.reminderTitle._allColumns) + allColumns.append(contentsOf: QueryValue.columns.remindersListTitle._allColumns) + return allColumns + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.reminderID._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.reminderTitle._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.remindersListTitle._writableColumns) + return writableColumns + } + public var queryFragment: QueryFragment { + "\(self.reminderID), \(self.reminderTitle), \(self.remindersListTitle)" + } + } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + reminderID: some StructuredQueriesCore.QueryExpression = Reminder.ID?(queryOutput: nil), + reminderTitle: some StructuredQueriesCore.QueryExpression, + remindersListTitle: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: reminderID._allColumns) + allColumns.append(contentsOf: reminderTitle._allColumns) + allColumns.append(contentsOf: remindersListTitle._allColumns) + self.allColumns = allColumns + } + } + public typealias QueryValue = Self + + public typealias From = Swift.Never + + public nonisolated static var columns: TableColumns { + TableColumns() + } + + public nonisolated static var _columnWidth: Int { + [Reminder.ID?._columnWidth, String._columnWidth, String._columnWidth].reduce(0, +) + } + + public nonisolated static var tableName: String { + ReminderWithList.tableName + } + + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + self.reminderID = try decoder.decode(Reminder.ID.self) ?? nil + let reminderTitle = try decoder.decode(String.self) + let remindersListTitle = try decoder.decode(String.self) + guard let reminderTitle else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + guard let remindersListTitle else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.reminderTitle = reminderTitle + self.remindersListTitle = remindersListTitle + } + + public nonisolated init(_ other: ReminderWithList) { + self.reminderID = other.reminderID + self.reminderTitle = other.reminderTitle + self.remindersListTitle = other.remindersListTitle + } + public init( + reminderID: Reminder.ID? = nil, + reminderTitle: String, + remindersListTitle: String + ) { + self.reminderID = reminderID + self.reminderTitle = reminderTitle + self.remindersListTitle = remindersListTitle } } } - nonisolated extension Reminder: StructuredQueriesCore.Table { + nonisolated extension ReminderWithList: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [Reminder.ID._columnWidth, String._columnWidth, String._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { - "reminders" + "reminderWithLists" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let id = try decoder.decode(Int.self) - guard let id else { - throw QueryDecodingError.missingRequiredColumn + let reminderID = try decoder.decode(Reminder.ID.self) + let reminderTitle = try decoder.decode(String.self) + let remindersListTitle = try decoder.decode(String.self) + guard let reminderID else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn } - self.id = id + guard let reminderTitle else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + guard let remindersListTitle else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.reminderID = reminderID + self.reminderTitle = reminderTitle + self.remindersListTitle = remindersListTitle } } """# } } - @Test func commentAfterOptionalID() { + @Test func composite() { assertMacro { """ @Table - struct Reminder { - let id: Int? // TODO: Migrate to UUID - var title = "" + private struct Metadata: Identifiable { + let id: MetadataID + var userModificationDate: Date } """ } expansion: { #""" - struct Reminder { - let id: Int? // TODO: Migrate to UUID - var title = "" + private struct Metadata: Identifiable { + let id: MetadataID + var userModificationDate: Date public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition, StructuredQueriesCore.PrimaryKeyedTableDefinition { - public typealias QueryValue = Reminder - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let title = StructuredQueriesCore.TableColumn("title", keyPath: \QueryValue.title, default: "") - public var primaryKey: StructuredQueriesCore.TableColumn { - self.id - } + public typealias QueryValue = Metadata + public typealias PrimaryKey = MetadataID + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let primaryKey = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id) + public let userModificationDate = StructuredQueriesCore._TableColumn.for("userModificationDate", keyPath: \QueryValue.userModificationDate) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.userModificationDate._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.userModificationDate._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { - "\(self.id), \(self.title)" + "\(self.id), \(self.userModificationDate)" + } + } + + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Metadata + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression, + userModificationDate: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: userModificationDate._allColumns) + self.allColumns = allColumns } } public struct Draft: StructuredQueriesCore.TableDraft { - public typealias PrimaryTable = Reminder - let id: Int? // TODO: Migrate to UUID - var title = "" + public typealias PrimaryTable = Metadata + let id: MetadataID? + var userModificationDate: Date public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { public typealias QueryValue = Draft - public let id = StructuredQueriesCore.TableColumn("id", keyPath: \QueryValue.id) - public let title = StructuredQueriesCore.TableColumn("title", keyPath: \QueryValue.title, default: "") + public let id = StructuredQueriesCore._TableColumn.for("id", keyPath: \QueryValue.id, default: nil) + public let userModificationDate = StructuredQueriesCore._TableColumn.for("userModificationDate", keyPath: \QueryValue.userModificationDate) public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title] + var allColumns: [any StructuredQueriesCore.TableColumnExpression] = [] + allColumns.append(contentsOf: QueryValue.columns.id._allColumns) + allColumns.append(contentsOf: QueryValue.columns.userModificationDate._allColumns) + return allColumns } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.id, QueryValue.columns.title] + var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] = [] + writableColumns.append(contentsOf: QueryValue.columns.id._writableColumns) + writableColumns.append(contentsOf: QueryValue.columns.userModificationDate._writableColumns) + return writableColumns } public var queryFragment: QueryFragment { - "\(self.id), \(self.title)" + "\(self.id), \(self.userModificationDate)" + } + } + public struct Selection: StructuredQueriesCore.TableExpression { + public typealias QueryValue = Draft + public let allColumns: [any StructuredQueriesCore.QueryExpression] + public init( + id: some StructuredQueriesCore.QueryExpression = MetadataID?(queryOutput: nil), + userModificationDate: some StructuredQueriesCore.QueryExpression + ) { + var allColumns: [any StructuredQueriesCore.QueryExpression] = [] + allColumns.append(contentsOf: id._allColumns) + allColumns.append(contentsOf: userModificationDate._allColumns) + self.allColumns = allColumns } } + public typealias QueryValue = Self + + public typealias From = Swift.Never + public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [MetadataID?._columnWidth, Date._columnWidth].reduce(0, +) + } + public nonisolated static var tableName: String { - Reminder.tableName + Metadata.tableName } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) - self.title = try decoder.decode(Swift.String.self) ?? "" + self.id = try decoder.decode(MetadataID.self) ?? nil + let userModificationDate = try decoder.decode(Date.self) + guard let userModificationDate else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.userModificationDate = userModificationDate } - public nonisolated init(_ other: Reminder) { + public nonisolated init(_ other: Metadata) { self.id = other.id - self.title = other.title + self.userModificationDate = other.userModificationDate } public init( - id: Int? = nil, - title: Swift.String = "" + id: MetadataID? = nil, + userModificationDate: Date ) { self.id = id - self.title = title + self.userModificationDate = userModificationDate } } } - nonisolated extension Reminder: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable { + nonisolated extension Metadata: StructuredQueriesCore.Table, StructuredQueriesCore.PrimaryKeyedTable, StructuredQueriesCore.PartialSelectStatement { + public typealias QueryValue = Self + public typealias From = Swift.Never public nonisolated static var columns: TableColumns { TableColumns() } + public nonisolated static var _columnWidth: Int { + [MetadataID._columnWidth, Date._columnWidth].reduce(0, +) + } public nonisolated static var tableName: String { - "reminders" + "metadatas" } public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - self.id = try decoder.decode(Int.self) - self.title = try decoder.decode(Swift.String.self) ?? "" + let id = try decoder.decode(MetadataID.self) + let userModificationDate = try decoder.decode(Date.self) + guard let id else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + guard let userModificationDate else { + throw StructuredQueriesCore.QueryDecodingError.missingRequiredColumn + } + self.id = id + self.userModificationDate = userModificationDate } } """# diff --git a/Tests/StructuredQueriesMacrosTests/TableSelectionMacroTests.swift b/Tests/StructuredQueriesMacrosTests/TableSelectionMacroTests.swift deleted file mode 100644 index cd27cbf1..00000000 --- a/Tests/StructuredQueriesMacrosTests/TableSelectionMacroTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -import MacroTesting -import StructuredQueriesMacros -import Testing - -extension SnapshotTests { - @Suite - struct TableSelectionMacroTests { - @Test func basics() { - assertMacro { - """ - @Table @Selection - struct ReminderListWithCount { - let reminderList: ReminderList - let remindersCount: Int - } - """ - } expansion: { - #""" - struct ReminderListWithCount { - let reminderList: ReminderList - let remindersCount: Int - - public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { - public typealias QueryValue = ReminderListWithCount - public let reminderList = StructuredQueriesCore.TableColumn("reminderList", keyPath: \QueryValue.reminderList) - public let remindersCount = StructuredQueriesCore.TableColumn("remindersCount", keyPath: \QueryValue.remindersCount) - public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { - [QueryValue.columns.reminderList, QueryValue.columns.remindersCount] - } - public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { - [QueryValue.columns.reminderList, QueryValue.columns.remindersCount] - } - public var queryFragment: QueryFragment { - "\(self.reminderList), \(self.remindersCount)" - } - } - - public struct Columns: StructuredQueriesCore._SelectedColumns { - public typealias QueryValue = ReminderListWithCount - public let selection: [(aliasName: String, expression: StructuredQueriesCore.QueryFragment)] - public init( - reminderList: some StructuredQueriesCore.QueryExpression, - remindersCount: some StructuredQueriesCore.QueryExpression - ) { - self.selection = [("reminderList", reminderList.queryFragment), ("remindersCount", remindersCount.queryFragment)] - } - } - } - - nonisolated extension ReminderListWithCount: StructuredQueriesCore.Table, StructuredQueriesCore.PartialSelectStatement { - public typealias QueryValue = Self - public typealias From = Swift.Never - public nonisolated static var columns: TableColumns { - TableColumns() - } - public nonisolated static var tableName: String { - "reminderListWithCounts" - } - } - - extension ReminderListWithCount: StructuredQueriesCore._Selection { - public init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { - let reminderList = try decoder.decode(ReminderList.self) - let remindersCount = try decoder.decode(Int.self) - guard let reminderList else { - throw QueryDecodingError.missingRequiredColumn - } - guard let remindersCount else { - throw QueryDecodingError.missingRequiredColumn - } - self.reminderList = reminderList - self.remindersCount = remindersCount - } - } - """# - } - } - } -} diff --git a/Tests/StructuredQueriesTests/AggregateFunctionsTests.swift b/Tests/StructuredQueriesTests/AggregateFunctionsTests.swift index ff2f70c7..cccf6536 100644 --- a/Tests/StructuredQueriesTests/AggregateFunctionsTests.swift +++ b/Tests/StructuredQueriesTests/AggregateFunctionsTests.swift @@ -269,7 +269,7 @@ extension SnapshotTests { .order(by: \.title) ) { """ - SELECT group_concat(iif((length("tags"."title") > 5), "tags"."title", NULL)) + SELECT group_concat(iif((length("tags"."title")) > (5), "tags"."title", NULL)) FROM "tags" ORDER BY "tags"."title" """ @@ -290,7 +290,7 @@ extension SnapshotTests { .order(by: \.title) ) { """ - SELECT group_concat(CASE WHEN (length("tags"."title") > 5) THEN "tags"."title" END) + SELECT group_concat(CASE WHEN (length("tags"."title")) > (5) THEN "tags"."title" END) FROM "tags" ORDER BY "tags"."title" """ @@ -334,7 +334,7 @@ extension SnapshotTests { assertInlineSnapshot(of: (User.columns.name + "!").groupConcat(", "), as: .sql) { """ - group_concat(("users"."name" || '!'), ', ') + group_concat(("users"."name") || ('!'), ', ') """ } @@ -352,7 +352,7 @@ extension SnapshotTests { } assertQuery(Tag.select { ($0.title + "!").groupConcat(", ") }) { """ - SELECT group_concat(("tags"."title" || '!'), ', ') + SELECT group_concat(("tags"."title") || ('!'), ', ') FROM "tags" """ } results: { diff --git a/Tests/StructuredQueriesTests/BindingTests.swift b/Tests/StructuredQueriesTests/BindingTests.swift index b87505f0..43448649 100644 --- a/Tests/StructuredQueriesTests/BindingTests.swift +++ b/Tests/StructuredQueriesTests/BindingTests.swift @@ -78,7 +78,7 @@ extension SnapshotTests { } ) { """ - SELECT ('00000000-0000-0000-0000-000000000000' IN ('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002')) + SELECT ('00000000-0000-0000-0000-000000000000') IN (('00000000-0000-0000-0000-000000000001'), ('00000000-0000-0000-0000-000000000002')) """ } results: { """ diff --git a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift index 612d1d83..3a74dc03 100644 --- a/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift +++ b/Tests/StructuredQueriesTests/CommonTableExpressionTests.swift @@ -79,7 +79,7 @@ extension SnapshotTests { ) SELECT "incompleteReminders"."isFlagged", "incompleteReminders"."title" FROM "incompleteReminders" - WHERE (("incompleteReminders"."title" COLLATE "NOCASE") LIKE '%groceries%') + WHERE ("incompleteReminders"."title" COLLATE "NOCASE" LIKE '%groceries%') """ } results: { """ @@ -121,7 +121,7 @@ extension SnapshotTests { ("remindersListID", "title", "isFlagged", "isCompleted") SELECT "reminders"."remindersListID", "incompleteReminders"."title", NOT ("incompleteReminders"."isFlagged"), 1 FROM "incompleteReminders" - JOIN "reminders" ON ("incompleteReminders"."title" = "reminders"."title") + JOIN "reminders" ON ("incompleteReminders"."title") = ("reminders"."title") LIMIT 1 RETURNING "id", "title" """ @@ -155,7 +155,7 @@ extension SnapshotTests { ) UPDATE "reminders" SET "title" = upper("reminders"."title") - WHERE ("reminders"."title" IN (SELECT "incompleteReminders"."title" + WHERE ("reminders"."title") IN ((SELECT "incompleteReminders"."title" FROM "incompleteReminders")) RETURNING "title" """ @@ -194,7 +194,7 @@ extension SnapshotTests { WHERE NOT ("reminders"."isCompleted") ) DELETE FROM "reminders" - WHERE ("reminders"."title" IN (SELECT "incompleteReminders"."title" + WHERE ("reminders"."title") IN ((SELECT "incompleteReminders"."title" FROM "incompleteReminders")) RETURNING "reminders"."title" """ @@ -309,7 +309,7 @@ extension SnapshotTests { WITH "counts" AS ( SELECT 1 AS "value" UNION - SELECT ("counts"."value" + 1) AS "value" + SELECT ("counts"."value") + (1) AS "value" FROM "counts" ) SELECT "counts"."value" @@ -348,7 +348,7 @@ extension SnapshotTests { ) SELECT "incompleteReminders"."isFlagged" FROM "incompleteReminders" - WHERE (("incompleteReminders"."title" COLLATE "NOCASE") LIKE '%groceries%') + WHERE ("incompleteReminders"."title" COLLATE "NOCASE" LIKE '%groceries%') """ } results: { """ @@ -384,7 +384,7 @@ extension SnapshotTests { all: true, Employee .select { EmployeeReport.Columns(id: $0.id, height: $0.height, name: $0.name) } - .join(EmployeeReport.all) { $0.bossID.eq($1.id) } + .join(EmployeeReport.all) { $0.bossID.is($1.id) } ) } query: { EmployeeReport @@ -397,7 +397,7 @@ extension SnapshotTests { UNION ALL SELECT "employees"."id" AS "id", "employees"."height" AS "height", "employees"."name" AS "name" FROM "employees" - JOIN "employeeReports" ON ("employees"."bossID" = "employeeReports"."id") + JOIN "employeeReports" ON ("employees"."bossID") IS ("employeeReports"."id") ) SELECT avg("employeeReports"."height") FROM "employeeReports" @@ -466,7 +466,7 @@ extension SnapshotTests { WITH "fibonaccis" AS ( SELECT 1 AS "n", 0 AS "prevFib", 1 AS "fib" UNION - SELECT ("fibonaccis"."n" + 1) AS "n", "fibonaccis"."fib" AS "prevFib", ("fibonaccis"."prevFib" + "fibonaccis"."fib") AS "fib" + SELECT ("fibonaccis"."n") + (1) AS "n", "fibonaccis"."fib" AS "prevFib", ("fibonaccis"."prevFib") + ("fibonaccis"."fib") AS "fib" FROM "fibonaccis" ) SELECT "fibonaccis"."fib" @@ -511,10 +511,10 @@ extension SnapshotTests { WITH "fibonaccis" AS ( SELECT 1 AS "n", 0 AS "prevFib", 1 AS "fib" UNION - SELECT ("fibonaccis"."n" + 1) AS "n", "fibonaccis"."fib" AS "prevFib", ("fibonaccis"."prevFib" + "fibonaccis"."fib") AS "fib" + SELECT ("fibonaccis"."n") + (1) AS "n", "fibonaccis"."fib" AS "prevFib", ("fibonaccis"."prevFib") + ("fibonaccis"."fib") AS "fib" FROM "fibonaccis" ) - SELECT (CAST("fibonaccis"."fib" AS REAL) / CAST("fibonaccis"."prevFib" AS REAL)) + SELECT (CAST("fibonaccis"."fib" AS REAL)) / (CAST("fibonaccis"."prevFib" AS REAL)) FROM "fibonaccis" LIMIT 1 OFFSET 30 """ @@ -529,20 +529,20 @@ extension SnapshotTests { } } -@Table @Selection +@Table private struct Fibonacci { let n: Int let prevFib: Int let fib: Int } -@Table @Selection +@Table private struct IncompleteReminder { let isFlagged: Bool let title: String } -@Table @Selection +@Table private struct Count { let value: Int } @@ -564,14 +564,14 @@ struct Employee { var height = 100 } -@Table @Selection +@Table struct EmployeeReport { let id: Int let height: Int let name: String } -@Table @Selection +@Table struct ReminderCount { let count: Int var queryOutput: Int { @@ -582,7 +582,7 @@ struct ReminderCount { } } -@Table @Selection +@Table struct RemindersListCount { let count: Int var queryOutput: Int { diff --git a/Tests/StructuredQueriesTests/CompileTimeTests.swift b/Tests/StructuredQueriesTests/CompileTimeTests.swift index 50705a21..40a1583f 100644 --- a/Tests/StructuredQueriesTests/CompileTimeTests.swift +++ b/Tests/StructuredQueriesTests/CompileTimeTests.swift @@ -1,7 +1,7 @@ import StructuredQueries // NB: This is a compile-time test for a 'select' overload. -@Selection +@Table private struct ReminderRow { let reminder: Reminder let isPastDue: Bool diff --git a/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift index ae300f41..c13c0a9d 100644 --- a/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift +++ b/Tests/StructuredQueriesTests/DatabaseFunctionTests.swift @@ -288,10 +288,10 @@ extension SnapshotTests { .select { $joinTags($2.jsonGroupArray()) } ) { """ - SELECT "joinTags"(json_group_array(CASE WHEN ("tags"."rowid" IS NOT NULL) THEN json_object('id', json_quote("tags"."id"), 'title', json_quote("tags"."title")) END) FILTER (WHERE ("tags"."id" IS NOT NULL))) + SELECT "joinTags"(json_group_array(CASE WHEN ("tags"."rowid") IS NOT (NULL) THEN json_object('id', json_quote("tags"."id"), 'title', json_quote("tags"."title")) END) FILTER (WHERE ("tags"."id") IS NOT (NULL))) FROM "reminders" - LEFT JOIN "remindersTags" ON ("reminders"."id" = "remindersTags"."reminderID") - LEFT JOIN "tags" ON ("remindersTags"."tagID" = "tags"."id") + LEFT JOIN "remindersTags" ON ("reminders"."id") = ("remindersTags"."reminderID") + LEFT JOIN "tags" ON ("remindersTags"."tagID") = ("tags"."id") GROUP BY "reminders"."id" """ } results: { @@ -313,17 +313,41 @@ extension SnapshotTests { } @DatabaseFunction(as: ((Reminder.JSONRepresentation, Bool) -> Bool).self) - func isValid(_ reminder: Reminder, _ override: Bool = false) -> Bool { + func isJSONValid(_ reminder: Reminder, _ override: Bool = false) -> Bool { !reminder.title.isEmpty || override } @Test func jsonObject() { + $isJSONValid.install(database.handle) + + assertQuery( + Reminder.select { $isJSONValid($0.jsonObject(), true) }.limit(1) + ) { + """ + SELECT "isJSONValid"(json_object('id', json_quote("reminders"."id"), 'assignedUserID', json_quote("reminders"."assignedUserID"), 'dueDate', json_quote("reminders"."dueDate"), 'isCompleted', json(CASE "reminders"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "reminders"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("reminders"."notes"), 'priority', json_quote("reminders"."priority"), 'remindersListID', json_quote("reminders"."remindersListID"), 'title', json_quote("reminders"."title"), 'updatedAt', json_quote("reminders"."updatedAt")), 1) + FROM "reminders" + LIMIT 1 + """ + } results: { + """ + ┌──────┐ + │ true │ + └──────┘ + """ + } + } + + @DatabaseFunction + func isValid(_ reminder: Reminder, _ override: Bool = false) -> Bool { + !reminder.title.isEmpty || override + } + @Test func table() { $isValid.install(database.handle) assertQuery( - Reminder.select { $isValid($0.jsonObject(), true) }.limit(1) + Reminder.select { $isValid($0, true) }.limit(1) ) { """ - SELECT "isValid"(json_object('id', json_quote("reminders"."id"), 'assignedUserID', json_quote("reminders"."assignedUserID"), 'dueDate', json_quote("reminders"."dueDate"), 'isCompleted', json(CASE "reminders"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "reminders"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("reminders"."notes"), 'priority', json_quote("reminders"."priority"), 'remindersListID', json_quote("reminders"."remindersListID"), 'title', json_quote("reminders"."title"), 'updatedAt', json_quote("reminders"."updatedAt")), 1) + SELECT "isValid"("reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt", 1) FROM "reminders" LIMIT 1 """ @@ -334,6 +358,98 @@ extension SnapshotTests { └──────┘ """ } + assertQuery( + Reminder + .select { _ in + $isValid(Reminder.Columns(id: 1, remindersListID: 1), true) + } + .limit(1) + ) { + """ + SELECT "isValid"(1, NULL, NULL, 0, 0, '', NULL, 1, '', '2040-02-14 23:31:30.000', 1) + FROM "reminders" + LIMIT 1 + """ + } results: { + """ + ┌──────┐ + │ true │ + └──────┘ + """ + } + } + + @DatabaseFunction + func isNotNull(_ tag: Tag?) -> Bool { + tag != nil + } + @Test func optionalTable() { + $isNotNull.install(database.handle) + + assertQuery( + Tag?.select { $isNotNull($0) }.limit(1) + ) { + """ + SELECT "isNotNull"("tags"."id", "tags"."title") + FROM "tags" + LIMIT 1 + """ + } results: { + """ + ┌──────┐ + │ true │ + └──────┘ + """ + } + } + + enum T: AliasName {} + @DatabaseFunction(as: ((TableAlias) -> Bool).self) + func isValidAlias(_ tag: Tag) -> Bool { + !tag.title.isEmpty + } + @Test func tableAlias() { + $isValidAlias.install(database.handle) + + assertQuery( + Tag.as(T.self).select { $isValidAlias($0) }.limit(1) + ) { + """ + SELECT "isValidAlias"("ts"."id", "ts"."title") + FROM "tags" AS "ts" + LIMIT 1 + """ + } results: { + """ + ┌──────┐ + │ true │ + └──────┘ + """ + } + } + + @DatabaseFunction + func isValidDraft(_ tag: Tag.Draft) -> Bool { + !tag.title.isEmpty + } + @Test func tableDraft() { + $isValidDraft.install(database.handle) + + assertQuery( + Tag.Draft.select { $isValidDraft($0) }.limit(1) + ) { + """ + SELECT "isValidDraft"("tags"."id", "tags"."title") + FROM "tags" + LIMIT 1 + """ + } results: { + """ + ┌──────┐ + │ true │ + └──────┘ + """ + } } } } diff --git a/Tests/StructuredQueriesTests/DeleteTests.swift b/Tests/StructuredQueriesTests/DeleteTests.swift index e3f810d2..5de9b4be 100644 --- a/Tests/StructuredQueriesTests/DeleteTests.swift +++ b/Tests/StructuredQueriesTests/DeleteTests.swift @@ -46,7 +46,7 @@ extension SnapshotTests { assertQuery(Reminder.delete().where { $0.id == 1 }.returning(\.self)) { """ DELETE FROM "reminders" - WHERE ("reminders"."id" = 1) + WHERE ("reminders"."id") = (1) RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" """ } results: { @@ -85,7 +85,7 @@ extension SnapshotTests { assertQuery(Reminder.delete(Reminder(id: 1, remindersListID: 1))) { """ DELETE FROM "reminders" - WHERE ("reminders"."id" = 1) + WHERE ("reminders"."id") = (1) """ } assertQuery(Reminder.count()) { @@ -135,7 +135,7 @@ extension SnapshotTests { ) { """ DELETE FROM "remindersLists" AS "rs" - WHERE ("rs"."id" = 1) + WHERE ("rs"."id") = (1) RETURNING "id", "color", "title", "position" """ } results: { diff --git a/Tests/StructuredQueriesTests/EnumTableTests.swift b/Tests/StructuredQueriesTests/EnumTableTests.swift new file mode 100644 index 00000000..0f0f5979 --- /dev/null +++ b/Tests/StructuredQueriesTests/EnumTableTests.swift @@ -0,0 +1,374 @@ +#if StructuredQueriesCasePaths + import CasePaths + import Dependencies + import Foundation + import InlineSnapshotTesting + import StructuredQueries + import StructuredQueriesTestSupport + import Testing + import _StructuredQueriesSQLite + + extension SnapshotTests { + @Suite struct EnumTableTests { + @Dependency(\.defaultDatabase) var db + + init() throws { + try db.execute( + """ + CREATE TABLE "attachments" ( + "id" INTEGER PRIMARY KEY, + "link" TEXT, + "note" TEXT, + "videoURL" TEXT, + "videoKind" TEXT, + "imageCaption" TEXT, + "imageURL" TEXT + ) STRICT + """ + ) + try db.execute( + """ + INSERT INTO "attachments" + ("link") VALUES ('https://www.pointfree.co') + """ + ) + try db.execute( + """ + INSERT INTO "attachments" + ("note") VALUES ('Today was a good day') + """ + ) + try db.execute( + """ + INSERT INTO "attachments" + ("videoURL", "videoKind") VALUES ('https://www.youtube.com/video/1234', 'youtube') + """ + ) + try db.execute( + """ + INSERT INTO "attachments" + ("imageCaption", "imageURL") VALUES ('Blob', 'https://www.pointfree.co/blob.jpg') + """ + ) + } + + @Test func selectAll() { + assertQuery( + Attachment.all + ) { + """ + SELECT "attachments"."id", "attachments"."link", "attachments"."note", "attachments"."videoURL", "attachments"."videoKind", "attachments"."imageCaption", "attachments"."imageURL" + FROM "attachments" + """ + } results: { + """ + ┌─────────────────────────────────────────────────────┐ + │ Attachment( │ + │ id: 1, │ + │ kind: .link(URL(https://www.pointfree.co)) │ + │ ) │ + ├─────────────────────────────────────────────────────┤ + │ Attachment( │ + │ id: 2, │ + │ kind: .note("Today was a good day") │ + │ ) │ + ├─────────────────────────────────────────────────────┤ + │ Attachment( │ + │ id: 3, │ + │ kind: .video( │ + │ Attachment.Video( │ + │ url: URL(https://www.youtube.com/video/1234), │ + │ kind: .youtube │ + │ ) │ + │ ) │ + │ ) │ + ├─────────────────────────────────────────────────────┤ + │ Attachment( │ + │ id: 4, │ + │ kind: .image( │ + │ Attachment.Image( │ + │ caption: "Blob", │ + │ url: URL(https://www.pointfree.co/blob.jpg) │ + │ ) │ + │ ) │ + │ ) │ + └─────────────────────────────────────────────────────┘ + """ + } + } + + @Test func customSelect() { + assertQuery( + Attachment.select { $0.kind } + ) { + """ + SELECT "attachments"."link", "attachments"."note", "attachments"."videoURL", "attachments"."videoKind", "attachments"."imageCaption", "attachments"."imageURL" + FROM "attachments" + """ + } results: { + """ + ┌─────────────────────────────────────────────────────┐ + │ Attachment.Kind.link(URL(https://www.pointfree.co)) │ + ├─────────────────────────────────────────────────────┤ + │ Attachment.Kind.note("Today was a good day") │ + ├─────────────────────────────────────────────────────┤ + │ Attachment.Kind.video( │ + │ Attachment.Video( │ + │ url: URL(https://www.youtube.com/video/1234), │ + │ kind: .youtube │ + │ ) │ + │ ) │ + ├─────────────────────────────────────────────────────┤ + │ Attachment.Kind.image( │ + │ Attachment.Image( │ + │ caption: "Blob", │ + │ url: URL(https://www.pointfree.co/blob.jpg) │ + │ ) │ + │ ) │ + └─────────────────────────────────────────────────────┘ + """ + } + } + + @Test func dynamicMemberLookup_CasePath() { + assertQuery( + Attachment.select(\.kind.image) + ) { + """ + SELECT "attachments"."imageCaption", "attachments"."imageURL" + FROM "attachments" + """ + } results: { + """ + ┌───────────────────────────────────────────────┐ + │ nil │ + ├───────────────────────────────────────────────┤ + │ nil │ + ├───────────────────────────────────────────────┤ + │ nil │ + ├───────────────────────────────────────────────┤ + │ Attachment.Image( │ + │ caption: "Blob", │ + │ url: URL(https://www.pointfree.co/blob.jpg) │ + │ ) │ + └───────────────────────────────────────────────┘ + """ + } + } + + @Test func dynamicMemberLookup_MultipleLevels() { + assertQuery( + Attachment.select(\.kind.image.caption) + ) { + """ + SELECT "attachments"."imageCaption" + FROM "attachments" + """ + } results: { + """ + ┌────────┐ + │ nil │ + │ nil │ + │ nil │ + │ "Blob" │ + └────────┘ + """ + } + } + + @Test func whereClause() { + assertQuery( + Attachment.where { $0.kind.is(Attachment.Kind.note("Today was a good day")) } + ) { + """ + SELECT "attachments"."id", "attachments"."link", "attachments"."note", "attachments"."videoURL", "attachments"."videoKind", "attachments"."imageCaption", "attachments"."imageURL" + FROM "attachments" + WHERE ("attachments"."link", "attachments"."note", "attachments"."videoURL", "attachments"."videoKind", "attachments"."imageCaption", "attachments"."imageURL") IS (NULL, 'Today was a good day', NULL, NULL, NULL, NULL) + """ + } results: { + """ + ┌───────────────────────────────────────┐ + │ Attachment( │ + │ id: 2, │ + │ kind: .note("Today was a good day") │ + │ ) │ + └───────────────────────────────────────┘ + """ + } + assertQuery( + Attachment.where { $0.kind.note.is("Today was a good day") } + ) { + """ + SELECT "attachments"."id", "attachments"."link", "attachments"."note", "attachments"."videoURL", "attachments"."videoKind", "attachments"."imageCaption", "attachments"."imageURL" + FROM "attachments" + WHERE ("attachments"."note") IS ('Today was a good day') + """ + } results: { + """ + ┌───────────────────────────────────────┐ + │ Attachment( │ + │ id: 2, │ + │ kind: .note("Today was a good day") │ + │ ) │ + └───────────────────────────────────────┘ + """ + } + } + + @Test func whereClause_DynamicMemberLookup() { + assertQuery( + Attachment.where { $0.kind.image.isNot(nil) } + ) { + """ + SELECT "attachments"."id", "attachments"."link", "attachments"."note", "attachments"."videoURL", "attachments"."videoKind", "attachments"."imageCaption", "attachments"."imageURL" + FROM "attachments" + WHERE ("attachments"."imageCaption", "attachments"."imageURL") IS NOT (NULL, NULL) + """ + } results: { + """ + ┌───────────────────────────────────────────────────┐ + │ Attachment( │ + │ id: 4, │ + │ kind: .image( │ + │ Attachment.Image( │ + │ caption: "Blob", │ + │ url: URL(https://www.pointfree.co/blob.jpg) │ + │ ) │ + │ ) │ + │ ) │ + └───────────────────────────────────────────────────┘ + """ + } + } + + @Test func whereClauseEscapeHatch() { + assertQuery( + Attachment + .where { + #sql("(\($0.kind.image)) IS NOT (NULL, NULL)") + } + ) { + """ + SELECT "attachments"."id", "attachments"."link", "attachments"."note", "attachments"."videoURL", "attachments"."videoKind", "attachments"."imageCaption", "attachments"."imageURL" + FROM "attachments" + WHERE ("attachments"."imageCaption", "attachments"."imageURL") IS NOT (NULL, NULL) + """ + } results: { + """ + ┌───────────────────────────────────────────────────┐ + │ Attachment( │ + │ id: 4, │ + │ kind: .image( │ + │ Attachment.Image( │ + │ caption: "Blob", │ + │ url: URL(https://www.pointfree.co/blob.jpg) │ + │ ) │ + │ ) │ + │ ) │ + └───────────────────────────────────────────────────┘ + """ + } + } + + // TODO: write test for #sql escape hatch + + @Test func insert() { + assertQuery( + Attachment.insert { + Attachment.Draft(kind: .note("Hello world!")) + Attachment.Draft( + kind: .image( + Attachment.Image( + caption: "Image", + url: URL(string: "image.jpg")! + ) + ) + ) + } + .returning(\.self) + ) { + """ + INSERT INTO "attachments" + ("id", "link", "note", "videoURL", "videoKind", "imageCaption", "imageURL") + VALUES + (NULL, NULL, 'Hello world!', NULL, NULL, NULL, NULL), (NULL, NULL, NULL, NULL, NULL, 'Image', 'image.jpg') + RETURNING "id", "link", "note", "videoURL", "videoKind", "imageCaption", "imageURL" + """ + } results: { + """ + ┌───────────────────────────────┐ + │ Attachment( │ + │ id: 5, │ + │ kind: .note("Hello world!") │ + │ ) │ + ├───────────────────────────────┤ + │ Attachment( │ + │ id: 6, │ + │ kind: .image( │ + │ Attachment.Image( │ + │ caption: "Image", │ + │ url: URL(image.jpg) │ + │ ) │ + │ ) │ + │ ) │ + └───────────────────────────────┘ + """ + } + } + + @Test func update() { + assertQuery( + Attachment + .find(1) + .update { + $0.kind = .note("Good bye world!") + } + .returning(\.self) + ) { + """ + UPDATE "attachments" + SET "link" = NULL, "note" = 'Good bye world!', "videoURL" = NULL, "videoKind" = NULL, "imageCaption" = NULL, "imageURL" = NULL + WHERE ("attachments"."id") IN ((1)) + RETURNING "id", "link", "note", "videoURL", "videoKind", "imageCaption", "imageURL" + """ + } results: { + """ + ┌──────────────────────────────────┐ + │ Attachment( │ + │ id: 1, │ + │ kind: .note("Good bye world!") │ + │ ) │ + └──────────────────────────────────┘ + """ + } + } + } + } + + @Table private struct Attachment { + let id: Int + let kind: Kind + + @CasePathable @Selection + fileprivate enum Kind { + case link(URL) + case note(String) + case video(Attachment.Video) + case image(Attachment.Image) + } + + @Selection fileprivate struct Video { + @Column("videoURL") + let url: URL + @Column("videoKind") + var kind: Kind + fileprivate enum Kind: String, QueryBindable { case youtube, vimeo } + } + @Selection fileprivate struct Image { + @Column("imageCaption") + let caption: String + @Column("imageURL") + let url: URL + } + } +#endif diff --git a/Tests/StructuredQueriesTests/EphemeralTests.swift b/Tests/StructuredQueriesTests/EphemeralTests.swift index 7c382273..b0b18ada 100644 --- a/Tests/StructuredQueriesTests/EphemeralTests.swift +++ b/Tests/StructuredQueriesTests/EphemeralTests.swift @@ -12,7 +12,7 @@ extension SnapshotTests { as: .sql ) { """ - SELECT (("testTables"."firstName" || ', ') || "testTables"."lastName") + SELECT (("testTables"."firstName") || (', ')) || ("testTables"."lastName") FROM "testTables" """ } diff --git a/Tests/StructuredQueriesTests/FTSTests.swift b/Tests/StructuredQueriesTests/FTSTests.swift index 834b74d8..41c6ee8a 100644 --- a/Tests/StructuredQueriesTests/FTSTests.swift +++ b/Tests/StructuredQueriesTests/FTSTests.swift @@ -152,7 +152,7 @@ extension SnapshotTests { SELECT highlight("reminderTexts", (SELECT "cid" FROM pragma_table_info('reminderTexts') WHERE "name" = 'tags'), '**', '**') FROM "reminders" - LEFT JOIN "reminderTexts" ON ("reminders"."rowid" = "reminderTexts"."rowid") + LEFT JOIN "reminderTexts" ON ("reminders"."rowid") = ("reminderTexts"."rowid") ORDER BY bm25("reminderTexts") """ } results: { @@ -185,7 +185,7 @@ extension SnapshotTests { SELECT highlight("reminderTexts", (SELECT "cid" FROM pragma_table_info('reminderTexts') WHERE "name" = 'tags'), '**', '**') FROM "reminders" - LEFT JOIN "reminderTexts" AS "rTs" ON ("reminders"."rowid" = "rTs"."rowid") + LEFT JOIN "reminderTexts" AS "rTs" ON ("reminders"."rowid") = ("rTs"."rowid") ORDER BY bm25("reminderTexts") """ } results: { diff --git a/Tests/StructuredQueriesTests/InsertTests.swift b/Tests/StructuredQueriesTests/InsertTests.swift index 9bfec1d5..9cc94664 100644 --- a/Tests/StructuredQueriesTests/InsertTests.swift +++ b/Tests/StructuredQueriesTests/InsertTests.swift @@ -24,7 +24,7 @@ extension SnapshotTests { ("remindersListID", "title", "isCompleted", "dueDate", "priority") VALUES (1, 'Groceries', 1, '2001-01-01 00:00:00.000', 3), (2, 'Haircut', 0, '1970-01-01 00:00:00.000', 1), (3, 'Schedule doctor appointment', 0, NULL, 2) - ON CONFLICT DO UPDATE SET "title" = ("reminders"."title" || ' Copy') + ON CONFLICT DO UPDATE SET "title" = ("reminders"."title") || (' Copy') RETURNING "id" """ } results: { @@ -223,6 +223,93 @@ extension SnapshotTests { └─────────────────────┘ """ } + + assertQuery( + Tag.insert { + $0.title + } select: { + // NB: 'WHERE 1' is required to avoid a SQL syntax error. + RemindersList.where { _ in true }.select { $0.title.lower() } + } onConflict: { + $0.title + } doUpdate: { + $0.title = $1.title + "-copy" + } + .returning(\.self) + ) { + """ + INSERT INTO "tags" + ("title") + SELECT lower("remindersLists"."title") + FROM "remindersLists" + WHERE 1 + ON CONFLICT ("title") + DO UPDATE SET "title" = ("excluded"."title") || ('-copy') + RETURNING "id", "title" + """ + } results: { + """ + ┌──────────────────────────┐ + │ Tag( │ + │ id: 5, │ + │ title: "business-copy" │ + │ ) │ + ├──────────────────────────┤ + │ Tag( │ + │ id: 6, │ + │ title: "family-copy" │ + │ ) │ + ├──────────────────────────┤ + │ Tag( │ + │ id: 7, │ + │ title: "personal-copy" │ + │ ) │ + └──────────────────────────┘ + """ + } + assertQuery( + Tag.insert { + $0.title + } select: { + // NB: 'WHERE 1' is required to avoid a SQL syntax error. + RemindersList.where { _ in true }.select { $0.title.lower() + "-copy" } + } onConflict: { + $0.title + } doUpdate: { + $0.title += "-2" + } + .returning(\.self) + ) { + """ + INSERT INTO "tags" + ("title") + SELECT (lower("remindersLists"."title")) || ('-copy') + FROM "remindersLists" + WHERE 1 + ON CONFLICT ("title") + DO UPDATE SET "title" = ("tags"."title") || ('-2') + RETURNING "id", "title" + """ + } results: { + """ + ┌────────────────────────────┐ + │ Tag( │ + │ id: 5, │ + │ title: "business-copy-2" │ + │ ) │ + ├────────────────────────────┤ + │ Tag( │ + │ id: 6, │ + │ title: "family-copy-2" │ + │ ) │ + ├────────────────────────────┤ + │ Tag( │ + │ id: 7, │ + │ title: "personal-copy-2" │ + │ ) │ + └────────────────────────────┘ + """ + } } @Test func draft() { @@ -301,7 +388,7 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" - WHERE ("reminders"."id" = 1) + WHERE ("reminders"."id") = (1) """ } results: { """ @@ -695,7 +782,7 @@ extension SnapshotTests { } } -@Table @Selection private struct Item { +@Table private struct Item { var title = "" var quantity = 0 @Column(as: [String].JSONRepresentation.self) diff --git a/Tests/StructuredQueriesTests/JSONFunctionsTests.swift b/Tests/StructuredQueriesTests/JSONFunctionsTests.swift index 6149270f..828359bf 100644 --- a/Tests/StructuredQueriesTests/JSONFunctionsTests.swift +++ b/Tests/StructuredQueriesTests/JSONFunctionsTests.swift @@ -142,11 +142,11 @@ extension SnapshotTests { .limit(2) ) { """ - SELECT "users"."id", "users"."name" AS "assignedUser", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" AS "reminder", json_group_array(CASE WHEN ("tags"."rowid" IS NOT NULL) THEN json_object('id', json_quote("tags"."id"), 'title', json_quote("tags"."title")) END) FILTER (WHERE ("tags"."id" IS NOT NULL)) AS "tags" + SELECT "users"."id" AS "id", "users"."name" AS "name", "reminders"."id" AS "id", "reminders"."assignedUserID" AS "assignedUserID", "reminders"."dueDate" AS "dueDate", "reminders"."isCompleted" AS "isCompleted", "reminders"."isFlagged" AS "isFlagged", "reminders"."notes" AS "notes", "reminders"."priority" AS "priority", "reminders"."remindersListID" AS "remindersListID", "reminders"."title" AS "title", "reminders"."updatedAt" AS "updatedAt", json_group_array(CASE WHEN ("tags"."rowid") IS NOT (NULL) THEN json_object('id', json_quote("tags"."id"), 'title', json_quote("tags"."title")) END) FILTER (WHERE ("tags"."id") IS NOT (NULL)) AS "tags" FROM "reminders" - LEFT JOIN "remindersTags" ON ("reminders"."id" = "remindersTags"."reminderID") - LEFT JOIN "tags" ON ("remindersTags"."tagID" = "tags"."id") - LEFT JOIN "users" ON ("reminders"."assignedUserID" = "users"."id") + LEFT JOIN "remindersTags" ON ("reminders"."id") = ("remindersTags"."reminderID") + LEFT JOIN "tags" ON ("remindersTags"."tagID") = ("tags"."id") + LEFT JOIN "users" ON ("reminders"."assignedUserID") = ("users"."id") GROUP BY "reminders"."id" LIMIT 2 """ @@ -228,10 +228,10 @@ extension SnapshotTests { .limit(1) ) { """ - SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" AS "remindersList", json_group_array(DISTINCT CASE WHEN ("milestones"."rowid" IS NOT NULL) THEN json_object('id', json_quote("milestones"."id"), 'remindersListID', json_quote("milestones"."remindersListID"), 'title', json_quote("milestones"."title")) END) FILTER (WHERE ("milestones"."id" IS NOT NULL)) AS "milestones", json_group_array(DISTINCT CASE WHEN ("reminders"."rowid" IS NOT NULL) THEN json_object('id', json_quote("reminders"."id"), 'assignedUserID', json_quote("reminders"."assignedUserID"), 'dueDate', json_quote("reminders"."dueDate"), 'isCompleted', json(CASE "reminders"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "reminders"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("reminders"."notes"), 'priority', json_quote("reminders"."priority"), 'remindersListID', json_quote("reminders"."remindersListID"), 'title', json_quote("reminders"."title"), 'updatedAt', json_quote("reminders"."updatedAt")) END) FILTER (WHERE ("reminders"."id" IS NOT NULL)) AS "reminders" + SELECT "remindersLists"."id" AS "id", "remindersLists"."color" AS "color", "remindersLists"."title" AS "title", "remindersLists"."position" AS "position", json_group_array(DISTINCT CASE WHEN ("milestones"."rowid") IS NOT (NULL) THEN json_object('id', json_quote("milestones"."id"), 'remindersListID', json_quote("milestones"."remindersListID"), 'title', json_quote("milestones"."title")) END) FILTER (WHERE ("milestones"."id") IS NOT (NULL)) AS "milestones", json_group_array(DISTINCT CASE WHEN ("reminders"."rowid") IS NOT (NULL) THEN json_object('id', json_quote("reminders"."id"), 'assignedUserID', json_quote("reminders"."assignedUserID"), 'dueDate', json_quote("reminders"."dueDate"), 'isCompleted', json(CASE "reminders"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "reminders"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("reminders"."notes"), 'priority', json_quote("reminders"."priority"), 'remindersListID', json_quote("reminders"."remindersListID"), 'title', json_quote("reminders"."title"), 'updatedAt', json_quote("reminders"."updatedAt")) END) FILTER (WHERE ("reminders"."id") IS NOT (NULL)) AS "reminders" FROM "remindersLists" - LEFT JOIN "milestones" ON ("remindersLists"."id" = "milestones"."remindersListID") - LEFT JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") + LEFT JOIN "milestones" ON ("remindersLists"."id") = ("milestones"."remindersListID") + LEFT JOIN "reminders" ON ("remindersLists"."id") = ("reminders"."remindersListID") WHERE NOT ("reminders"."isCompleted") GROUP BY "remindersLists"."id" LIMIT 1 @@ -349,7 +349,7 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt", json_object('id', json_quote("remindersLists"."id"), 'color', json_quote("remindersLists"."color"), 'title', json_quote("remindersLists"."title"), 'position', json_quote("remindersLists"."position")) FROM "reminders" - JOIN "remindersLists" ON ("reminders"."remindersListID" = "remindersLists"."id") + JOIN "remindersLists" ON ("reminders"."remindersListID") = ("remindersLists"."id") """ } results: { #""" @@ -494,7 +494,7 @@ extension SnapshotTests { } } -@Selection +@Table private struct ReminderRow { let assignedUser: User? let reminder: Reminder @@ -502,7 +502,7 @@ private struct ReminderRow { let tags: [Tag] } -@Selection +@Table private struct RemindersListRow { let remindersList: RemindersList @Column(as: [Milestone].JSONRepresentation.self) diff --git a/Tests/StructuredQueriesTests/JoinTests.swift b/Tests/StructuredQueriesTests/JoinTests.swift index 098082b5..f804f3d5 100644 --- a/Tests/StructuredQueriesTests/JoinTests.swift +++ b/Tests/StructuredQueriesTests/JoinTests.swift @@ -15,7 +15,7 @@ extension SnapshotTests { """ SELECT "reminders"."title", "remindersLists"."title" FROM "reminders" - JOIN "remindersLists" ON ("reminders"."remindersListID" = "remindersLists"."id") + JOIN "remindersLists" ON ("reminders"."remindersListID") = ("remindersLists"."id") ORDER BY "reminders"."dueDate" DESC """ } results: { @@ -47,7 +47,7 @@ extension SnapshotTests { """ SELECT "reminders"."priority" AS "value" FROM "remindersLists" - LEFT JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") + LEFT JOIN "reminders" ON ("remindersLists"."id") = ("reminders"."remindersListID") """ } results: { """ @@ -88,7 +88,7 @@ extension SnapshotTests { } } -@Selection +@Table private struct PriorityRow { let value: Priority? } diff --git a/Tests/StructuredQueriesTests/KitchenSinkTests.swift b/Tests/StructuredQueriesTests/KitchenSinkTests.swift index 720416fa..d80e1ae8 100644 --- a/Tests/StructuredQueriesTests/KitchenSinkTests.swift +++ b/Tests/StructuredQueriesTests/KitchenSinkTests.swift @@ -211,9 +211,9 @@ extension SnapshotTests { .select { ($0, $1.jsonGroupArray()) } ) { """ - SELECT "kitchens"."id", json_group_array(CASE WHEN ("kitchenSinks"."rowid" IS NOT NULL) THEN json_object('id', json_quote("kitchenSinks"."id"), 'kitchenID', json_quote("kitchenSinks"."kitchenID"), 'bool', json(CASE "kitchenSinks"."bool" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'optionalBool', json(CASE "kitchenSinks"."optionalBool" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'string', json_quote("kitchenSinks"."string"), 'optionalString', json_quote("kitchenSinks"."optionalString"), 'int', json_quote("kitchenSinks"."int"), 'optionalInt', json_quote("kitchenSinks"."optionalInt"), 'double', json_quote("kitchenSinks"."double"), 'optionalDouble', json_quote("kitchenSinks"."optionalDouble"), 'rawRepresentable', json_quote("kitchenSinks"."rawRepresentable"), 'optionalRawRepresentable', json_quote("kitchenSinks"."optionalRawRepresentable"), 'iso8601Date', json_quote("kitchenSinks"."iso8601Date"), 'optionalISO8601Date', json_quote("kitchenSinks"."optionalISO8601Date"), 'unixTimeDate', datetime("kitchenSinks"."unixTimeDate", 'unixepoch'), 'optionalUnixTimeDate', datetime("kitchenSinks"."optionalUnixTimeDate", 'unixepoch'), 'julianDayDate', datetime("kitchenSinks"."julianDayDate", 'julianday'), 'optionalJulianDayDate', datetime("kitchenSinks"."optionalJulianDayDate", 'julianday'), 'jsonArray', json("kitchenSinks"."jsonArray"), 'optionalJSONArray', json("kitchenSinks"."optionalJSONArray"), 'jsonArrayOfDates', json("kitchenSinks"."jsonArrayOfDates")) END) FILTER (WHERE ("kitchenSinks"."id" IS NOT NULL)) + SELECT "kitchens"."id", json_group_array(CASE WHEN ("kitchenSinks"."rowid") IS NOT (NULL) THEN json_object('id', json_quote("kitchenSinks"."id"), 'kitchenID', json_quote("kitchenSinks"."kitchenID"), 'bool', json(CASE "kitchenSinks"."bool" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'optionalBool', json(CASE "kitchenSinks"."optionalBool" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'string', json_quote("kitchenSinks"."string"), 'optionalString', json_quote("kitchenSinks"."optionalString"), 'int', json_quote("kitchenSinks"."int"), 'optionalInt', json_quote("kitchenSinks"."optionalInt"), 'double', json_quote("kitchenSinks"."double"), 'optionalDouble', json_quote("kitchenSinks"."optionalDouble"), 'rawRepresentable', json_quote("kitchenSinks"."rawRepresentable"), 'optionalRawRepresentable', json_quote("kitchenSinks"."optionalRawRepresentable"), 'iso8601Date', json_quote("kitchenSinks"."iso8601Date"), 'optionalISO8601Date', json_quote("kitchenSinks"."optionalISO8601Date"), 'unixTimeDate', datetime("kitchenSinks"."unixTimeDate", 'unixepoch'), 'optionalUnixTimeDate', datetime("kitchenSinks"."optionalUnixTimeDate", 'unixepoch'), 'julianDayDate', datetime("kitchenSinks"."julianDayDate", 'julianday'), 'optionalJulianDayDate', datetime("kitchenSinks"."optionalJulianDayDate", 'julianday'), 'jsonArray', json("kitchenSinks"."jsonArray"), 'optionalJSONArray', json("kitchenSinks"."optionalJSONArray"), 'jsonArrayOfDates', json("kitchenSinks"."jsonArrayOfDates")) END) FILTER (WHERE ("kitchenSinks"."id") IS NOT (NULL)) FROM "kitchens" - FULL JOIN "kitchenSinks" ON ("kitchens"."id" IS "kitchenSinks"."kitchenID") + FULL JOIN "kitchenSinks" ON ("kitchens"."id") IS ("kitchenSinks"."kitchenID") """ } results: { """ diff --git a/Tests/StructuredQueriesTests/LiveTests.swift b/Tests/StructuredQueriesTests/LiveTests.swift index a2ab4bb7..5d276d44 100644 --- a/Tests/StructuredQueriesTests/LiveTests.swift +++ b/Tests/StructuredQueriesTests/LiveTests.swift @@ -168,7 +168,7 @@ extension SnapshotTests { ) FROM "reminders" WHERE ("reminders"."priority" < (SELECT avg(CAST("reminders"."priority" AS INTEGER)) - FROM "reminders") OR ("reminders"."priority" IS NULL)) + FROM "reminders")) OR (("reminders"."priority") IS (NULL)) ORDER BY "reminders"."priority" DESC """ } results: { @@ -196,7 +196,7 @@ extension SnapshotTests { """ SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position", count("reminders"."id") FROM "remindersLists" - JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") + JOIN "reminders" ON ("remindersLists"."id") = ("reminders"."remindersListID") GROUP BY "remindersLists"."id" """ } results: { @@ -238,8 +238,8 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt", group_concat("tags"."title") FROM "reminders" - JOIN "remindersTags" ON ("reminders"."id" = "remindersTags"."reminderID") - JOIN "tags" ON ("remindersTags"."tagID" = "tags"."id") + JOIN "remindersTags" ON ("reminders"."id") = ("remindersTags"."reminderID") + JOIN "tags" ON ("remindersTags"."tagID") = ("tags"."id") GROUP BY "reminders"."id" """ } results: { diff --git a/Tests/StructuredQueriesTests/NestedTests.swift b/Tests/StructuredQueriesTests/NestedTests.swift new file mode 100644 index 00000000..2c3720e6 --- /dev/null +++ b/Tests/StructuredQueriesTests/NestedTests.swift @@ -0,0 +1,712 @@ +import Dependencies +import Foundation +import InlineSnapshotTesting +import StructuredQueries +import StructuredQueriesTestSupport +import Testing +import _StructuredQueriesSQLite + +#if StructuredQueriesCasePaths + import CasePaths +#endif + +extension SnapshotTests { + @Suite struct NestedTests { + @Dependency(\.defaultDatabase) var db + + @Test func basics() throws { + try db.execute( + #sql( + """ + CREATE TABLE "items" ( + "title" TEXT NOT NULL DEFAULT '', + "quantity" INTEGER NOT NULL DEFAULT 0, + "isOutOfStock" INTEGER NOT NULL DEFAULT 0, + "isOnBackOrder" INTEGER NOT NULL DEFAULT 0 + ) + """ + ) + ) + assertQuery( + Item + .insert { + Item(title: "Phone", quantity: 1, status: Status()) + } + .returning(\.self) + ) { + """ + INSERT INTO "items" + ("title", "quantity", "isOutOfStock", "isOnBackOrder") + VALUES + ('Phone', 1, 0, 0) + RETURNING "title", "quantity", "isOutOfStock", "isOnBackOrder" + """ + } results: { + """ + ┌──────────────────────────┐ + │ Item( │ + │ title: "Phone", │ + │ quantity: 1, │ + │ status: Status( │ + │ isOutOfStock: false, │ + │ isOnBackOrder: false │ + │ ) │ + │ ) │ + └──────────────────────────┘ + """ + } + assertQuery( + Item.insert { + $0.status.isOutOfStock + } values: { + true + } + ) { + """ + INSERT INTO "items" + ("isOutOfStock") + VALUES + (1) + """ + } + assertQuery( + Item.insert { + $0.status + } values: { + Status(isOutOfStock: true, isOnBackOrder: true) + } + ) { + """ + INSERT INTO "items" + ("isOutOfStock", "isOnBackOrder") + VALUES + (1, 1) + """ + } + assertQuery( + Item.all + ) { + """ + SELECT "items"."title", "items"."quantity", "items"."isOutOfStock", "items"."isOnBackOrder" + FROM "items" + """ + } results: { + """ + ┌──────────────────────────┐ + │ Item( │ + │ title: "Phone", │ + │ quantity: 1, │ + │ status: Status( │ + │ isOutOfStock: false, │ + │ isOnBackOrder: false │ + │ ) │ + │ ) │ + ├──────────────────────────┤ + │ Item( │ + │ title: "", │ + │ quantity: 0, │ + │ status: Status( │ + │ isOutOfStock: true, │ + │ isOnBackOrder: false │ + │ ) │ + │ ) │ + ├──────────────────────────┤ + │ Item( │ + │ title: "", │ + │ quantity: 0, │ + │ status: Status( │ + │ isOutOfStock: true, │ + │ isOnBackOrder: true │ + │ ) │ + │ ) │ + └──────────────────────────┘ + """ + } + assertQuery( + Item.where { $0.status.eq(Status()) }.select(\.status) + ) { + """ + SELECT "items"."isOutOfStock", "items"."isOnBackOrder" + FROM "items" + WHERE ("items"."isOutOfStock", "items"."isOnBackOrder") = (0, 0) + """ + } results: { + """ + ┌────────────────────────┐ + │ Status( │ + │ isOutOfStock: false, │ + │ isOnBackOrder: false │ + │ ) │ + └────────────────────────┘ + """ + } + assertQuery( + Item.update { + $0.status.isOutOfStock = true + } + ) { + """ + UPDATE "items" + SET "isOutOfStock" = 1 + """ + } + assertQuery( + Item.update { + $0.status = Status(isOutOfStock: true, isOnBackOrder: true) + } + ) { + """ + UPDATE "items" + SET "isOutOfStock" = 1, "isOnBackOrder" = 1 + """ + } + // FIXME: These should decode 'nil' but because all its fields have defaults it coalesces. + assertQuery( + DefaultItem?(nil) + ) { + """ + SELECT NULL AS "title", NULL AS "quantity", NULL AS "isOutOfStock", NULL AS "isOnBackOrder" + """ + } results: { + """ + ┌──────────────────────────┐ + │ DefaultItem( │ + │ title: "", │ + │ quantity: 0, │ + │ status: Status( │ + │ isOutOfStock: false, │ + │ isOnBackOrder: false │ + │ ) │ + │ ) │ + └──────────────────────────┘ + """ + } + // NB: This tests that 'Optional.none' is favored over 'Table.none'. + assertQuery( + DefaultItem?.none + ) { + """ + SELECT NULL AS "title", NULL AS "quantity", NULL AS "isOutOfStock", NULL AS "isOnBackOrder" + """ + } results: { + """ + ┌──────────────────────────┐ + │ DefaultItem( │ + │ title: "", │ + │ quantity: 0, │ + │ status: Status( │ + │ isOutOfStock: false, │ + │ isOnBackOrder: false │ + │ ) │ + │ ) │ + └──────────────────────────┘ + """ + } + } + + @Test func optionalDoubleNested() async throws { + try db.execute( + #sql( + """ + CREATE TABLE "itemWithTimestamps" ( + "title" TEXT, + "quantity" INTEGER, + "isOutOfStock" INTEGER, + "isOnBackOrder" INTEGER, + "timestamp" TEXT NOT NULL + ) + """ + ) + ) + assertQuery( + ItemWithTimestamp.insert { + ItemWithTimestamp(item: nil, timestamp: Date(timeIntervalSinceReferenceDate: 0)) + } + ) { + """ + INSERT INTO "itemWithTimestamps" + ("title", "quantity", "isOutOfStock", "isOnBackOrder", "timestamp") + VALUES + (NULL, NULL, NULL, NULL, '2001-01-01 00:00:00.000') + """ + } + assertQuery( + ItemWithTimestamp(item: nil, timestamp: Date(timeIntervalSinceReferenceDate: 0)) + ) { + """ + SELECT NULL AS "title", NULL AS "quantity", NULL AS "isOutOfStock", NULL AS "isOnBackOrder", '2001-01-01 00:00:00.000' AS "timestamp" + """ + } results: { + """ + ┌─────────────────────────────────────────────┐ + │ ItemWithTimestamp( │ + │ item: nil, │ + │ timestamp: Date(2001-01-01T00:00:00.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ + """ + } + assertQuery( + ItemWithTimestamp.insert { + ItemWithTimestamp( + item: Item( + title: "Pencil", + quantity: 0, + status: Status(isOutOfStock: true, isOnBackOrder: true) + ), + timestamp: Date(timeIntervalSinceReferenceDate: 0) + ) + } + ) { + """ + INSERT INTO "itemWithTimestamps" + ("title", "quantity", "isOutOfStock", "isOnBackOrder", "timestamp") + VALUES + ('Pencil', 0, 1, 1, '2001-01-01 00:00:00.000') + """ + } + assertQuery( + ItemWithTimestamp.select { _ in + ItemWithTimestamp.Columns( + timestamp: Date(timeIntervalSinceReferenceDate: 0) + ) + } + ) { + """ + SELECT NULL AS "title", NULL AS "quantity", NULL AS "isOutOfStock", NULL AS "isOnBackOrder", '2001-01-01 00:00:00.000' AS "timestamp" + FROM "itemWithTimestamps" + """ + } results: { + """ + ┌─────────────────────────────────────────────┐ + │ ItemWithTimestamp( │ + │ item: nil, │ + │ timestamp: Date(2001-01-01T00:00:00.000Z) │ + │ ) │ + ├─────────────────────────────────────────────┤ + │ ItemWithTimestamp( │ + │ item: nil, │ + │ timestamp: Date(2001-01-01T00:00:00.000Z) │ + │ ) │ + └─────────────────────────────────────────────┘ + """ + } + } + + @Test func nestedGenerated() throws { + try db.execute( + #sql( + """ + CREATE TABLE "rows" ( + "id" TEXT PRIMARY KEY NOT NULL ON CONFLICT REPLACE DEFAULT '00000000-0000-0000-0000-000000000000', + "createdAt" TEXT NOT NULL, + "updatedAt" TEXT NOT NULL, + "deletedAt" TEXT, + "isDeleted" INTEGER AS ("deletedAt" IS NOT NULL) + ) + """ + ) + ) + let now = Date(timeIntervalSinceReferenceDate: 0) + assertQuery( + Row + .insert { + Row.Draft(timestamps: Timestamps(createdAt: now, updatedAt: now, isDeleted: false)) + } + .returning(\.self) + ) { + """ + INSERT INTO "rows" + ("id", "createdAt", "updatedAt", "deletedAt") + VALUES + (NULL, '2001-01-01 00:00:00.000', '2001-01-01 00:00:00.000', NULL) + RETURNING "id", "createdAt", "updatedAt", "deletedAt", "isDeleted" + """ + } results: { + """ + ┌───────────────────────────────────────────────────┐ + │ Row( │ + │ id: UUID(00000000-0000-0000-0000-000000000000), │ + │ timestamps: Timestamps( │ + │ createdAt: Date(2001-01-01T00:00:00.000Z), │ + │ updatedAt: Date(2001-01-01T00:00:00.000Z), │ + │ deletedAt: nil, │ + │ isDeleted: false │ + │ ) │ + │ ) │ + └───────────────────────────────────────────────────┘ + """ + } + } + + @Test func primaryKey() throws { + try db.execute( + #sql( + """ + CREATE TABLE "metadatas" ( + "recordID" TEXT NOT NULL ON CONFLICT REPLACE DEFAULT + '00000000-0000-0000-0000-000000000000', + "recordType" TEXT NOT NULL ON CONFLICT REPLACE DEFAULT 'reminders', + "userModificationDate" TEXT NOT NULL, + PRIMARY KEY ("recordID", "recordType") + ) + """ + ) + ) + let now = Date(timeIntervalSinceReferenceDate: 0) + assertQuery( + Metadata + .insert { + Metadata.Draft(userModificationDate: now) + } + .returning(\.self) + ) { + """ + INSERT INTO "metadatas" + ("recordID", "recordType", "userModificationDate") + VALUES + (NULL, NULL, '2001-01-01 00:00:00.000') + RETURNING "recordID", "recordType", "userModificationDate" + """ + } results: { + """ + ┌───────────────────────────────────────────────────────────┐ + │ Metadata( │ + │ id: MetadataID( │ + │ recordID: UUID(00000000-0000-0000-0000-000000000000), │ + │ recordType: "reminders" │ + │ ), │ + │ userModificationDate: Date(2001-01-01T00:00:00.000Z) │ + │ ) │ + └───────────────────────────────────────────────────────────┘ + """ + } + assertQuery( + Metadata.find(MetadataID(recordID: UUID(0), recordType: "reminders")) + ) { + """ + SELECT "metadatas"."recordID", "metadatas"."recordType", "metadatas"."userModificationDate" + FROM "metadatas" + WHERE ("metadatas"."recordID", "metadatas"."recordType") IN (('00000000-0000-0000-0000-000000000000', 'reminders')) + """ + } results: { + """ + ┌───────────────────────────────────────────────────────────┐ + │ Metadata( │ + │ id: MetadataID( │ + │ recordID: UUID(00000000-0000-0000-0000-000000000000), │ + │ recordType: "reminders" │ + │ ), │ + │ userModificationDate: Date(2001-01-01T00:00:00.000Z) │ + │ ) │ + └───────────────────────────────────────────────────────────┘ + """ + } + assertQuery( + Metadata.upsert { + Metadata( + id: MetadataID(recordID: UUID(0), recordType: "reminders"), + userModificationDate: now.addingTimeInterval(1) + ) + } + .returning(\.self) + ) { + """ + INSERT INTO "metadatas" + ("recordID", "recordType", "userModificationDate") + VALUES + ('00000000-0000-0000-0000-000000000000', 'reminders', '2001-01-01 00:00:01.000') + ON CONFLICT ("recordID", "recordType") + DO UPDATE SET "userModificationDate" = "excluded"."userModificationDate" + RETURNING "recordID", "recordType", "userModificationDate" + """ + } results: { + """ + ┌───────────────────────────────────────────────────────────┐ + │ Metadata( │ + │ id: MetadataID( │ + │ recordID: UUID(00000000-0000-0000-0000-000000000000), │ + │ recordType: "reminders" │ + │ ), │ + │ userModificationDate: Date(2001-01-01T00:00:01.000Z) │ + │ ) │ + └───────────────────────────────────────────────────────────┘ + """ + } + } + + @Test func doubleNested() throws { + try db.execute( + #sql( + """ + CREATE TABLE "as" ("d" INTEGER NOT NULL) + """ + ) + ) + assertQuery( + A.select { _ in A.Columns(b: B.Columns(c: C.Columns(d: 42))) } + ) { + """ + SELECT 42 AS "d" + FROM "as" + """ + } + assertQuery( + Values(A.Columns(b: B.Columns(c: C.Columns(d: 42)))) + ) { + """ + SELECT 42 AS "d" + """ + } results: { + """ + ┌─────────────────┐ + │ A( │ + │ b: B( │ + │ c: C(d: 42) │ + │ ) │ + │ ) │ + └─────────────────┘ + """ + } + } + + @Test func reminderListAndReminderCountPayload() { + let baseQuery = + RemindersList + .where { _ in #sql("color > 0") } + .join(Reminder.all) { $0.id.eq($1.remindersListID) } + assertQuery( + baseQuery + .select { + RemindersListAndReminderCountPayload.Columns( + payload: RemindersListAndReminderCount.Columns( + remindersList: $0, + remindersCount: $1.id.count() + ) + ) + } + ) { + """ + SELECT "remindersLists"."id" AS "id", "remindersLists"."color" AS "color", "remindersLists"."title" AS "title", "remindersLists"."position" AS "position", count("reminders"."id") AS "remindersCount" + FROM "remindersLists" + JOIN "reminders" ON ("remindersLists"."id") = ("reminders"."remindersListID") + WHERE color > 0 + """ + } results: { + """ + ┌───────────────────────────────────────────┐ + │ RemindersListAndReminderCountPayload( │ + │ payload: RemindersListAndReminderCount( │ + │ remindersList: RemindersList( │ + │ id: 1, │ + │ color: 4889071, │ + │ title: "Personal", │ + │ position: 0 │ + │ ), │ + │ remindersCount: 10 │ + │ ) │ + │ ) │ + └───────────────────────────────────────────┘ + """ + } + } + + #if StructuredQueriesCasePaths + @Test func `enum`() throws { + try db.execute( + #sql( + """ + CREATE TABLE "posts" ( + "url" TEXT, + "note" TEXT + ) + """ + ) + ) + assertQuery( + Post.insert { + Post.note("Hello world") + Post.photo(Photo(url: URL(fileURLWithPath: "/tmp/poster.png"))) + } + ) { + """ + INSERT INTO "posts" + ("url", "note") + VALUES + (NULL, 'Hello world'), ('file:///tmp/poster.png', NULL) + """ + } + assertQuery( + Post.all + ) { + """ + SELECT "posts"."url", "posts"."note" + FROM "posts" + """ + } results: { + """ + ┌───────────────────────────────────────────┐ + │ Post.note("Hello world") │ + ├───────────────────────────────────────────┤ + │ Post.photo( │ + │ Photo(url: URL(file:///tmp/poster.png)) │ + │ ) │ + └───────────────────────────────────────────┘ + """ + } + assertQuery( + Values(Post.Selection.note("Goodnight moon")) + ) { + """ + SELECT NULL AS "url", 'Goodnight moon' AS "note" + """ + } results: { + """ + ┌─────────────────────────────┐ + │ Post.note("Goodnight moon") │ + └─────────────────────────────┘ + """ + } + } + + @Test func enumRepresentation() throws { + assertQuery( + Notes.list(["Blob", "Jr"]) + ) { + """ + SELECT '[ + "Blob", + "Jr" + ]' AS "list" + """ + } results: { + """ + ┌──────────────────┐ + │ Notes.list( │ + │ [ │ + │ [0]: "Blob", │ + │ [1]: "Jr" │ + │ ] │ + │ ) │ + └──────────────────┘ + """ + } + assertQuery( + Values(Notes.Columns.list(#bind(["Blob", "Jr"]))) + ) { + """ + SELECT '[ + "Blob", + "Jr" + ]' AS "list" + """ + } results: { + """ + ┌──────────────────┐ + │ Notes.list( │ + │ [ │ + │ [0]: "Blob", │ + │ [1]: "Jr" │ + │ ] │ + │ ) │ + └──────────────────┘ + """ + } + } + #endif + } +} + +@Table +private struct Item { + var title: String + var quantity = 0 + var status: Status = Status() +} + +@Table("items") +private struct DefaultItem { + var title = "" + var quantity = 0 + var status: Status = Status() +} + +@Selection +private struct Status { + var isOutOfStock = false + var isOnBackOrder = false +} + +@Table +private struct ItemWithTimestamp { + var item: Item? + var timestamp: Date +} + +@Selection +private struct Timestamps { + var createdAt: Date + var updatedAt: Date + var deletedAt: Date? + @Column(generated: .virtual) + let isDeleted: Bool +} + +@Table +private struct Row { + let id: UUID + var timestamps: Timestamps +} + +@Table +private struct Metadata: Identifiable { + let id: MetadataID + var userModificationDate: Date +} + +@Selection +private struct MetadataID: Hashable { + let recordID: UUID + let recordType: String +} + +@Selection +struct RemindersListAndReminderCountPayload { + let payload: RemindersListAndReminderCount +} + +@Selection struct C { + var d: Int +} + +@Selection struct B { + var c: C +} + +@Table struct A { + var b: B +} + +@Selection +private struct Photo { + let url: URL +} + +@Selection +private struct Note { + let body: String +} + +#if StructuredQueriesCasePaths + @CasePathable @Table + private enum Post { + case photo(Photo) + case note(String = "") + } + + @CasePathable @Table + private enum Notes { + @Column(as: [String].JSONRepresentation.self) + case list([String]) + } +#endif diff --git a/Tests/StructuredQueriesTests/OperatorsTests.swift b/Tests/StructuredQueriesTests/OperatorsTests.swift index d4303196..35dc0879 100644 --- a/Tests/StructuredQueriesTests/OperatorsTests.swift +++ b/Tests/StructuredQueriesTests/OperatorsTests.swift @@ -10,82 +10,82 @@ extension SnapshotTests { @Test func equality() { assertInlineSnapshot(of: Row.columns.c == Row.columns.c, as: .sql) { """ - ("rows"."c" = "rows"."c") + ("rows"."c") = ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c == Row.columns.a, as: .sql) { """ - ("rows"."c" = "rows"."a") + ("rows"."c") = ("rows"."a") """ } assertInlineSnapshot(of: Row.columns.c == nil as Int?, as: .sql) { """ - ("rows"."c" IS NULL) + ("rows"."c") IS (NULL) """ } assertInlineSnapshot(of: Row.columns.a == Row.columns.c, as: .sql) { """ - ("rows"."a" IS "rows"."c") + ("rows"."a") IS ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.a == Row.columns.a, as: .sql) { """ - ("rows"."a" IS "rows"."a") + ("rows"."a") IS ("rows"."a") """ } assertInlineSnapshot(of: Row.columns.a == nil as Int?, as: .sql) { """ - ("rows"."a" IS NULL) + ("rows"."a") IS (NULL) """ } assertInlineSnapshot(of: nil as Int? == Row.columns.c, as: .sql) { """ - (NULL IS "rows"."c") + (NULL) IS ("rows"."c") """ } assertInlineSnapshot(of: nil as Int? == Row.columns.a, as: .sql) { """ - (NULL IS "rows"."a") + (NULL) IS ("rows"."a") """ } assertInlineSnapshot(of: Row.columns.c != Row.columns.c, as: .sql) { """ - ("rows"."c" <> "rows"."c") + ("rows"."c") <> ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c != Row.columns.a, as: .sql) { """ - ("rows"."c" <> "rows"."a") + ("rows"."c") <> ("rows"."a") """ } assertInlineSnapshot(of: Row.columns.c != nil as Int?, as: .sql) { """ - ("rows"."c" IS NOT NULL) + ("rows"."c") IS NOT (NULL) """ } assertInlineSnapshot(of: Row.columns.a != Row.columns.c, as: .sql) { """ - ("rows"."a" IS NOT "rows"."c") + ("rows"."a") IS NOT ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.a != Row.columns.a, as: .sql) { """ - ("rows"."a" IS NOT "rows"."a") + ("rows"."a") IS NOT ("rows"."a") """ } assertInlineSnapshot(of: Row.columns.a != nil as Int?, as: .sql) { """ - ("rows"."a" IS NOT NULL) + ("rows"."a") IS NOT (NULL) """ } assertInlineSnapshot(of: nil as Int? != Row.columns.c, as: .sql) { """ - (NULL IS NOT "rows"."c") + (NULL) IS NOT ("rows"."c") """ } assertInlineSnapshot(of: nil as Int? != Row.columns.a, as: .sql) { """ - (NULL IS NOT "rows"."a") + (NULL) IS NOT ("rows"."a") """ } } @@ -94,12 +94,12 @@ extension SnapshotTests { @Test func deprecatedEquality() { assertInlineSnapshot(of: Row.columns.c == nil, as: .sql) { """ - ("rows"."c" IS NULL) + ("rows"."c") IS (NULL) """ } assertInlineSnapshot(of: Row.columns.c != nil, as: .sql) { """ - ("rows"."c" IS NOT NULL) + ("rows"."c") IS NOT (NULL) """ } } @@ -107,27 +107,27 @@ extension SnapshotTests { @Test func comparison() { assertInlineSnapshot(of: Row.columns.c < Row.columns.c, as: .sql) { """ - ("rows"."c" < "rows"."c") + ("rows"."c") < ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c > Row.columns.c, as: .sql) { """ - ("rows"."c" > "rows"."c") + ("rows"."c") > ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c <= Row.columns.c, as: .sql) { """ - ("rows"."c" <= "rows"."c") + ("rows"."c") <= ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c >= Row.columns.c, as: .sql) { """ - ("rows"."c" >= "rows"."c") + ("rows"."c") >= ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.bool < Row.columns.bool, as: .sql) { """ - ("rows"."bool" < "rows"."bool") + ("rows"."bool") < ("rows"."bool") """ } } @@ -135,12 +135,12 @@ extension SnapshotTests { @Test func logic() { assertInlineSnapshot(of: Row.columns.bool && Row.columns.bool, as: .sql) { """ - ("rows"."bool" AND "rows"."bool") + ("rows"."bool") AND ("rows"."bool") """ } assertInlineSnapshot(of: Row.columns.bool || Row.columns.bool, as: .sql) { """ - ("rows"."bool" OR "rows"."bool") + ("rows"."bool") OR ("rows"."bool") """ } assertInlineSnapshot(of: !Row.columns.bool, as: .sql) { @@ -159,22 +159,22 @@ extension SnapshotTests { @Test func arithmetic() { assertInlineSnapshot(of: Row.columns.c + Row.columns.c, as: .sql) { """ - ("rows"."c" + "rows"."c") + ("rows"."c") + ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c - Row.columns.c, as: .sql) { """ - ("rows"."c" - "rows"."c") + ("rows"."c") - ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c * Row.columns.c, as: .sql) { """ - ("rows"."c" * "rows"."c") + ("rows"."c") * ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c / Row.columns.c, as: .sql) { """ - ("rows"."c" / "rows"."c") + ("rows"."c") / ("rows"."c") """ } assertInlineSnapshot(of: -Row.columns.c, as: .sql) { @@ -190,25 +190,25 @@ extension SnapshotTests { assertInlineSnapshot(of: Row.update { $0.c += 1 }, as: .sql) { """ UPDATE "rows" - SET "c" = ("rows"."c" + 1) + SET "c" = ("rows"."c") + (1) """ } assertInlineSnapshot(of: Row.update { $0.c -= 2 }, as: .sql) { """ UPDATE "rows" - SET "c" = ("rows"."c" - 2) + SET "c" = ("rows"."c") - (2) """ } assertInlineSnapshot(of: Row.update { $0.c *= 3 }, as: .sql) { """ UPDATE "rows" - SET "c" = ("rows"."c" * 3) + SET "c" = ("rows"."c") * (3) """ } assertInlineSnapshot(of: Row.update { $0.c /= 4 }, as: .sql) { """ UPDATE "rows" - SET "c" = ("rows"."c" / 4) + SET "c" = ("rows"."c") / (4) """ } assertInlineSnapshot(of: Row.update { $0.c = -$0.c }, as: .sql) { @@ -234,27 +234,27 @@ extension SnapshotTests { @Test func bitwise() { assertInlineSnapshot(of: Row.columns.c % Row.columns.c, as: .sql) { """ - ("rows"."c" % "rows"."c") + ("rows"."c") % ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c & Row.columns.c, as: .sql) { """ - ("rows"."c" & "rows"."c") + ("rows"."c") & ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c | Row.columns.c, as: .sql) { """ - ("rows"."c" | "rows"."c") + ("rows"."c") | ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c << Row.columns.c, as: .sql) { """ - ("rows"."c" << "rows"."c") + ("rows"."c") << ("rows"."c") """ } assertInlineSnapshot(of: Row.columns.c >> Row.columns.c, as: .sql) { """ - ("rows"."c" >> "rows"."c") + ("rows"."c") >> ("rows"."c") """ } assertInlineSnapshot(of: ~Row.columns.c, as: .sql) { @@ -265,25 +265,25 @@ extension SnapshotTests { assertInlineSnapshot(of: Row.update { $0.c &= 2 }, as: .sql) { """ UPDATE "rows" - SET "c" = ("rows"."c" & 2) + SET "c" = ("rows"."c") & (2) """ } assertInlineSnapshot(of: Row.update { $0.c |= 3 }, as: .sql) { """ UPDATE "rows" - SET "c" = ("rows"."c" | 3) + SET "c" = ("rows"."c") | (3) """ } assertInlineSnapshot(of: Row.update { $0.c <<= 4 }, as: .sql) { """ UPDATE "rows" - SET "c" = ("rows"."c" << 4) + SET "c" = ("rows"."c") << (4) """ } assertInlineSnapshot(of: Row.update { $0.c >>= 5 }, as: .sql) { """ UPDATE "rows" - SET "c" = ("rows"."c" >> 5) + SET "c" = ("rows"."c") >> (5) """ } assertInlineSnapshot(of: Row.update { $0.c = ~$0.c }, as: .sql) { @@ -305,17 +305,17 @@ extension SnapshotTests { @Test func strings() { assertInlineSnapshot(of: Row.columns.string + Row.columns.string, as: .sql) { """ - ("rows"."string" || "rows"."string") + ("rows"."string") || ("rows"."string") """ } assertInlineSnapshot(of: Row.columns.string.collate(.nocase), as: .sql) { """ - ("rows"."string" COLLATE "NOCASE") + "rows"."string" COLLATE "NOCASE" """ } assertInlineSnapshot(of: Row.columns.string.glob("a*"), as: .sql) { """ - ("rows"."string" GLOB 'a*') + ("rows"."string") GLOB ('a*') """ } assertInlineSnapshot(of: Row.columns.string.like("a%"), as: .sql) { @@ -351,19 +351,19 @@ extension SnapshotTests { assertInlineSnapshot(of: Row.update { $0.string += "!" }, as: .sql) { """ UPDATE "rows" - SET "string" = ("rows"."string" || '!') + SET "string" = ("rows"."string") || ('!') """ } assertInlineSnapshot(of: Row.update { $0.string.append("!") }, as: .sql) { """ UPDATE "rows" - SET "string" = ("rows"."string" || '!') + SET "string" = ("rows"."string") || ('!') """ } assertInlineSnapshot(of: Row.update { $0.string.append(contentsOf: "!") }, as: .sql) { """ UPDATE "rows" - SET "string" = ("rows"."string" || '!') + SET "string" = ("rows"."string") || ('!') """ } } @@ -374,7 +374,7 @@ extension SnapshotTests { as: .sql ) { """ - ("rows"."c" IN (1, 2, 3)) + ("rows"."c") IN ((1), (2), (3)) """ } assertInlineSnapshot( @@ -382,7 +382,7 @@ extension SnapshotTests { as: .sql ) { """ - ("rows"."c" IN (SELECT "rows"."c" + ("rows"."c") IN ((SELECT "rows"."c" FROM "rows")) """ } @@ -391,7 +391,7 @@ extension SnapshotTests { as: .sql ) { """ - ("rows"."c" IN (1, 2, 3)) + ("rows"."c") IN ((1), (2), (3)) """ } assertInlineSnapshot( @@ -399,7 +399,7 @@ extension SnapshotTests { as: .sql ) { """ - ("rows"."c" IN (SELECT "rows"."c" + ("rows"."c") IN ((SELECT "rows"."c" FROM "rows")) """ } @@ -411,7 +411,7 @@ extension SnapshotTests { as: .sql ) { """ - ("rows"."c" BETWEEN 0 AND 10) + "rows"."c" BETWEEN 0 AND 10 """ } assertInlineSnapshot( @@ -419,7 +419,7 @@ extension SnapshotTests { as: .sql ) { """ - ("rows"."c" BETWEEN 0 AND 10) + "rows"."c" BETWEEN 0 AND 10 """ } assertQuery( @@ -433,13 +433,13 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" - WHERE ("reminders"."id" BETWEEN coalesce(( + WHERE "reminders"."id" BETWEEN coalesce(( SELECT min("reminders"."id") FROM "reminders" ), 0) AND (coalesce(( SELECT max("reminders"."id") FROM "reminders" - ), 0) / 3)) + ), 0)) / (3) """ } results: { """ @@ -512,7 +512,7 @@ extension SnapshotTests { """ SELECT "rows"."a", "rows"."b", "rows"."c", "rows"."bool", "rows"."string" FROM "rows" - WHERE ("rows"."c" IN (SELECT CAST("rows"."bool" AS INTEGER) + WHERE ("rows"."c") IN ((SELECT CAST("rows"."bool" AS INTEGER) FROM "rows")) """ } @@ -525,10 +525,10 @@ extension SnapshotTests { """ SELECT "rows"."a", "rows"."b", "rows"."c", "rows"."bool", "rows"."string" FROM "rows" - WHERE ((CAST("rows"."c" AS REAL) >= ( + WHERE ((CAST("rows"."c" AS REAL)) >= (( SELECT coalesce(avg("rows"."c"), 0.0) FROM "rows" - )) AND (CAST("rows"."c" AS REAL) > 1.0)) + ))) AND ((CAST("rows"."c" AS REAL)) > (1.0)) """ } } @@ -540,7 +540,7 @@ extension SnapshotTests { """ SELECT "reminders"."id" FROM "reminders" - WHERE ("reminders"."id" IN (1, 2)) + WHERE ("reminders"."id") IN ((1), (2)) """ } results: { """ @@ -555,7 +555,7 @@ extension SnapshotTests { @Test func moduloZero() { assertQuery(Reminder.select { $0.id % 0 }) { """ - SELECT ("reminders"."id" % 0) + SELECT ("reminders"."id") % (0) FROM "reminders" """ } results: { @@ -596,7 +596,7 @@ extension SnapshotTests { SELECT EXISTS ( SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" - WHERE ("reminders"."id" = 1) + WHERE ("reminders"."id") = (1) ) """ } results: { @@ -611,7 +611,7 @@ extension SnapshotTests { SELECT EXISTS ( SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "reminders" - WHERE ("reminders"."id" = 100) + WHERE ("reminders"."id") = (100) ) """ } results: { diff --git a/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift b/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift index 5c7a9fd6..bf76afe9 100644 --- a/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift +++ b/Tests/StructuredQueriesTests/PrimaryKeyedTableTests.swift @@ -31,8 +31,8 @@ extension SnapshotTests { ) { """ UPDATE "reminders" - SET "title" = ("reminders"."title" || '!!!') - WHERE ("reminders"."id" IN (1)) + SET "title" = ("reminders"."title") || ('!!!') + WHERE ("reminders"."id") IN ((1)) RETURNING "title" """ } results: { @@ -49,8 +49,8 @@ extension SnapshotTests { ) { """ UPDATE "reminders" - SET "title" = ("reminders"."title" || '???') - WHERE ("reminders"."id" IN (1)) + SET "title" = ("reminders"."title") || ('???') + WHERE ("reminders"."id") IN ((1)) RETURNING "title" """ } results: { @@ -69,7 +69,7 @@ extension SnapshotTests { ) { """ DELETE FROM "reminders" - WHERE ("reminders"."id" IN (1)) + WHERE ("reminders"."id") IN ((1)) RETURNING "reminders"."id" """ } results: { @@ -86,7 +86,7 @@ extension SnapshotTests { ) { """ DELETE FROM "reminders" - WHERE ("reminders"."id" IN (2)) + WHERE ("reminders"."id") IN ((2)) RETURNING "reminders"."id" """ } results: { @@ -105,7 +105,7 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."title" FROM "reminders" - WHERE ("reminders"."id" IN (1)) + WHERE ("reminders"."id") IN ((1)) """ } results: { """ @@ -121,7 +121,7 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."title" FROM "reminders" - WHERE ("reminders"."id" IN (1)) + WHERE ("reminders"."id") IN ((1)) """ } results: { """ @@ -137,7 +137,7 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."title" FROM "reminders" - WHERE ("reminders"."id" IN (2)) + WHERE ("reminders"."id") IN ((2)) """ } results: { """ @@ -153,7 +153,7 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."title" FROM "reminders" - WHERE ("reminders"."id" IN (2, 4, 6)) + WHERE ("reminders"."id") IN ((2), (4), (6)) """ } results: { """ @@ -171,7 +171,7 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."title" FROM "reminders" - WHERE ("reminders"."id" IN (( + WHERE ("reminders"."id") IN ((( SELECT "reminders"."id" FROM "reminders" ))) @@ -199,7 +199,7 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."title" FROM "reminders" - WHERE ("reminders"."id" IN (2)) + WHERE ("reminders"."id") IN ((2)) """ } results: { """ @@ -220,8 +220,8 @@ extension SnapshotTests { """ SELECT "reminders"."title", "remindersLists"."title" FROM "reminders" - JOIN "remindersLists" ON ("reminders"."remindersListID" = "remindersLists"."id") - WHERE ("reminders"."id" IN (2)) + JOIN "remindersLists" ON ("reminders"."remindersListID") = ("remindersLists"."id") + WHERE ("reminders"."id") IN ((2)) """ } results: { """ @@ -260,7 +260,7 @@ extension SnapshotTests { """ SELECT "rows"."id", "rows"."isDeleted", "rows"."isNotDeleted" FROM "rows" - WHERE ("rows"."id" IN ('00000000-0000-0000-0000-000000000001')) + WHERE ("rows"."id") IN (('00000000-0000-0000-0000-000000000001')) """ } results: { """ @@ -306,7 +306,7 @@ extension SnapshotTests { """ UPDATE "rows" SET "isDeleted" = 1 - WHERE ("rows"."id" = '00000000-0000-0000-0000-000000000002') + WHERE ("rows"."id") = ('00000000-0000-0000-0000-000000000002') RETURNING "id", "isDeleted", "isNotDeleted" """ } results: { diff --git a/Tests/StructuredQueriesTests/SQLMacroTests.swift b/Tests/StructuredQueriesTests/SQLMacroTests.swift index 40fa5a0b..b4dc08bd 100644 --- a/Tests/StructuredQueriesTests/SQLMacroTests.swift +++ b/Tests/StructuredQueriesTests/SQLMacroTests.swift @@ -180,7 +180,7 @@ extension SnapshotTests { } } -@Selection +@Table private struct ReminderWithList { let reminder: Reminder let list: RemindersList diff --git a/Tests/StructuredQueriesTests/SchemaTests.swift b/Tests/StructuredQueriesTests/SchemaTests.swift index ff17db6b..ff7473ac 100644 --- a/Tests/StructuredQueriesTests/SchemaTests.swift +++ b/Tests/StructuredQueriesTests/SchemaTests.swift @@ -40,7 +40,7 @@ extension SnapshotTests { """ UPDATE "main"."reminders" SET "remindersListID" = 2 - WHERE ("main"."reminders"."remindersListID" = 1) + WHERE ("main"."reminders"."remindersListID") = (1) """ } } @@ -49,7 +49,7 @@ extension SnapshotTests { assertQuery(Reminder.where { $0.remindersListID.eq(1) }.delete()) { """ DELETE FROM "main"."reminders" - WHERE ("main"."reminders"."remindersListID" = 1) + WHERE ("main"."reminders"."remindersListID") = (1) """ } } diff --git a/Tests/StructuredQueriesTests/SelectTests.swift b/Tests/StructuredQueriesTests/SelectTests.swift index 639a53ea..626ae77c 100644 --- a/Tests/StructuredQueriesTests/SelectTests.swift +++ b/Tests/StructuredQueriesTests/SelectTests.swift @@ -144,7 +144,7 @@ extension SnapshotTests { """ SELECT "reminders"."id", "remindersLists"."id" FROM "reminders" - JOIN "remindersLists" ON ("reminders"."remindersListID" = "remindersLists"."id") + JOIN "remindersLists" ON ("reminders"."remindersListID") = ("remindersLists"."id") """ } results: { """ @@ -172,7 +172,7 @@ extension SnapshotTests { """ SELECT "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt", "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" FROM "reminders" - JOIN "remindersLists" ON ("reminders"."remindersListID" = "remindersLists"."id") + JOIN "remindersLists" ON ("reminders"."remindersListID") = ("remindersLists"."id") """ } results: { #""" @@ -322,7 +322,7 @@ extension SnapshotTests { """ SELECT "remindersLists"."title", "reminders"."title" FROM "remindersLists" - JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") + JOIN "reminders" ON ("remindersLists"."id") = ("reminders"."remindersListID") """ } results: { """ @@ -350,7 +350,7 @@ extension SnapshotTests { """ SELECT "reminders"."title", "users"."name" FROM "reminders" - LEFT JOIN "users" ON ("reminders"."assignedUserID" = "users"."id") + LEFT JOIN "users" ON ("reminders"."assignedUserID") = ("users"."id") LIMIT 2 """ } results: { @@ -370,7 +370,7 @@ extension SnapshotTests { """ SELECT "users"."id", "users"."name", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "users" - RIGHT JOIN "reminders" ON ("users"."id" IS "reminders"."assignedUserID") + RIGHT JOIN "reminders" ON ("users"."id") IS ("reminders"."assignedUserID") LIMIT 2 """ } results: { @@ -414,7 +414,7 @@ extension SnapshotTests { """ SELECT "users"."id", "users"."name", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "users" - RIGHT JOIN "reminders" ON ("users"."id" IS "reminders"."assignedUserID") + RIGHT JOIN "reminders" ON ("users"."id") IS ("reminders"."assignedUserID") LIMIT 2 """ } results: { @@ -458,7 +458,7 @@ extension SnapshotTests { """ SELECT "reminders"."title", "users"."name" FROM "users" - RIGHT JOIN "reminders" ON ("users"."id" IS "reminders"."assignedUserID") + RIGHT JOIN "reminders" ON ("users"."id") IS ("reminders"."assignedUserID") LIMIT 2 """ } results: { @@ -472,14 +472,14 @@ extension SnapshotTests { assertQuery( Reminder.all - .fullJoin(User.all) { $0.assignedUserID.eq($1.id) } + .fullJoin(User.all) { $0.assignedUserID.is($1.id) } .select { ($0.title, $1.name) } .limit(2) ) { """ SELECT "reminders"."title", "users"."name" FROM "reminders" - FULL JOIN "users" ON ("reminders"."assignedUserID" = "users"."id") + FULL JOIN "users" ON ("reminders"."assignedUserID") IS ("users"."id") LIMIT 2 """ } results: { @@ -692,7 +692,7 @@ extension SnapshotTests { SELECT "reminders"."isCompleted", count("reminders"."id") FROM "reminders" GROUP BY "reminders"."isCompleted" - HAVING (count("reminders"."id") > 3) + HAVING (count("reminders"."id")) > (3) """ } results: { """ @@ -719,7 +719,7 @@ extension SnapshotTests { SELECT "reminders"."isCompleted", count("reminders"."id") FROM "reminders" GROUP BY "reminders"."isCompleted" - HAVING (count("reminders"."id") > 3) + HAVING (count("reminders"."id")) > (3) """ } results: { """ @@ -826,7 +826,7 @@ extension SnapshotTests { """ SELECT "reminders"."priority", "reminders"."dueDate" FROM "reminders" - ORDER BY "reminders"."priority" ASC NULLS LAST, "reminders"."dueDate" DESC NULLS FIRST, ("reminders"."title" COLLATE "NOCASE") DESC + ORDER BY "reminders"."priority" ASC NULLS LAST, "reminders"."dueDate" DESC NULLS FIRST, "reminders"."title" COLLATE "NOCASE" DESC """ } results: { """ @@ -989,7 +989,7 @@ extension SnapshotTests { """ SELECT "remindersLists"."title", count("reminders"."id") FROM "remindersLists" - JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") + JOIN "reminders" ON ("remindersLists"."id") = ("reminders"."remindersListID") GROUP BY "remindersLists"."id" LIMIT 1 """ @@ -1014,7 +1014,7 @@ extension SnapshotTests { """ SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", "r1s"."updatedAt", "r2s"."id", "r2s"."assignedUserID", "r2s"."dueDate", "r2s"."isCompleted", "r2s"."isFlagged", "r2s"."notes", "r2s"."priority", "r2s"."remindersListID", "r2s"."title", "r2s"."updatedAt" FROM "reminders" AS "r1s" - JOIN "reminders" AS "r2s" ON ("r1s"."id" = "r2s"."id") + JOIN "reminders" AS "r2s" ON ("r1s"."id") = ("r2s"."id") LIMIT 1 """ } results: { @@ -1049,7 +1049,7 @@ extension SnapshotTests { """ SELECT "r1s"."id", "r2s"."id" FROM "reminders" AS "r1s" - LEFT JOIN "reminders" AS "r2s" ON ("r1s"."id" = "r2s"."id") + LEFT JOIN "reminders" AS "r2s" ON ("r1s"."id") = ("r2s"."id") LIMIT 1 """ } results: { @@ -1068,9 +1068,9 @@ extension SnapshotTests { .select { ($0, $1.jsonGroupArray()) } ) { """ - SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", "r1s"."updatedAt", json_group_array(CASE WHEN ("r2s"."rowid" IS NOT NULL) THEN json_object('id', json_quote("r2s"."id"), 'assignedUserID', json_quote("r2s"."assignedUserID"), 'dueDate', json_quote("r2s"."dueDate"), 'isCompleted', json(CASE "r2s"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "r2s"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("r2s"."notes"), 'priority', json_quote("r2s"."priority"), 'remindersListID', json_quote("r2s"."remindersListID"), 'title', json_quote("r2s"."title"), 'updatedAt', json_quote("r2s"."updatedAt")) END) FILTER (WHERE ("r2s"."id" IS NOT NULL)) + SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", "r1s"."updatedAt", json_group_array(CASE WHEN ("r2s"."rowid") IS NOT (NULL) THEN json_object('id', json_quote("r2s"."id"), 'assignedUserID', json_quote("r2s"."assignedUserID"), 'dueDate', json_quote("r2s"."dueDate"), 'isCompleted', json(CASE "r2s"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "r2s"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("r2s"."notes"), 'priority', json_quote("r2s"."priority"), 'remindersListID', json_quote("r2s"."remindersListID"), 'title', json_quote("r2s"."title"), 'updatedAt', json_quote("r2s"."updatedAt")) END) FILTER (WHERE ("r2s"."id") IS NOT (NULL)) FROM "reminders" AS "r1s" - LEFT JOIN "reminders" AS "r2s" ON ("r1s"."id" = "r2s"."id") + LEFT JOIN "reminders" AS "r2s" ON ("r1s"."id") = ("r2s"."id") GROUP BY "r1s"."id" LIMIT 1 """ @@ -1106,9 +1106,9 @@ extension SnapshotTests { .select { ($0, $1.jsonGroupArray()) } ) { """ - SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", "r1s"."updatedAt", json_group_array(CASE WHEN ("r2s"."rowid" IS NOT NULL) THEN json_object('id', json_quote("r2s"."id"), 'assignedUserID', json_quote("r2s"."assignedUserID"), 'dueDate', json_quote("r2s"."dueDate"), 'isCompleted', json(CASE "r2s"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "r2s"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("r2s"."notes"), 'priority', json_quote("r2s"."priority"), 'remindersListID', json_quote("r2s"."remindersListID"), 'title', json_quote("r2s"."title"), 'updatedAt', json_quote("r2s"."updatedAt")) END) FILTER (WHERE ("r2s"."id" IS NOT NULL)) + SELECT "r1s"."id", "r1s"."assignedUserID", "r1s"."dueDate", "r1s"."isCompleted", "r1s"."isFlagged", "r1s"."notes", "r1s"."priority", "r1s"."remindersListID", "r1s"."title", "r1s"."updatedAt", json_group_array(CASE WHEN ("r2s"."rowid") IS NOT (NULL) THEN json_object('id', json_quote("r2s"."id"), 'assignedUserID', json_quote("r2s"."assignedUserID"), 'dueDate', json_quote("r2s"."dueDate"), 'isCompleted', json(CASE "r2s"."isCompleted" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'isFlagged', json(CASE "r2s"."isFlagged" WHEN 0 THEN 'false' WHEN 1 THEN 'true' END), 'notes', json_quote("r2s"."notes"), 'priority', json_quote("r2s"."priority"), 'remindersListID', json_quote("r2s"."remindersListID"), 'title', json_quote("r2s"."title"), 'updatedAt', json_quote("r2s"."updatedAt")) END) FILTER (WHERE ("r2s"."id") IS NOT (NULL)) FROM "reminders" AS "r1s" - LEFT JOIN "reminders" AS "r2s" ON (("r1s"."id" = "r2s"."id") AND ("r1s"."id" = 42)) + LEFT JOIN "reminders" AS "r2s" ON (("r1s"."id") = ("r2s"."id")) AND (("r1s"."id") = (42)) GROUP BY "r1s"."id" LIMIT 1 """ @@ -1152,7 +1152,7 @@ extension SnapshotTests { } } - @Table @Selection + @Table struct VecExample { let rowid: Int let distance: Double @@ -1198,8 +1198,8 @@ extension SnapshotTests { """ SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "remindersLists" - LEFT JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") - WHERE ifnull(("reminders"."priority" IS 3), 0) + LEFT JOIN "reminders" ON ("remindersLists"."id") = ("reminders"."remindersListID") + WHERE ifnull(("reminders"."priority") IS (3), 0) """ } results: { """ @@ -1341,7 +1341,7 @@ extension SnapshotTests { Reminder.Draft.select(\.isHighPriority) ) { """ - SELECT ("reminders"."priority" IS 3) + SELECT ("reminders"."priority") IS (3) FROM "reminders" """ } results: { @@ -1369,7 +1369,7 @@ extension SnapshotTests { } assertQuery(query) { """ - SELECT ("reminders"."priority" < 3) + SELECT ("reminders"."priority") < (3) FROM "reminders" """ } results: { @@ -1551,7 +1551,7 @@ extension SnapshotTests { """ SELECT "remindersForeignKeys"."id", "remindersForeignKeys"."seq", "remindersForeignKeys"."table", "remindersForeignKeys"."from", "remindersForeignKeys"."to", "remindersForeignKeys"."on_update", "remindersForeignKeys"."on_delete", "remindersForeignKeys"."match", "remindersTableInfo"."cid", "remindersTableInfo"."name", "remindersTableInfo"."type", "remindersTableInfo"."notnull", "remindersTableInfo"."dflt_value", "remindersTableInfo"."pk" FROM pragma_foreign_key_list('reminders') AS "remindersForeignKeys" - JOIN pragma_table_info('reminders') AS "remindersTableInfo" ON ("remindersForeignKeys"."from" = "remindersTableInfo"."name") + JOIN pragma_table_info('reminders') AS "remindersTableInfo" ON ("remindersForeignKeys"."from") = ("remindersTableInfo"."name") """ } results: { """ @@ -1570,6 +1570,59 @@ extension SnapshotTests { """ } } + + @Test func tuples() { + assertQuery( + Tag.where { + $0.eq(Tag(id: 1, title: "car")) + } + ) { + """ + SELECT "tags"."id", "tags"."title" + FROM "tags" + WHERE ("tags"."id", "tags"."title") = (1, 'car') + """ + } results: { + """ + ┌────────────────┐ + │ Tag( │ + │ id: 1, │ + │ title: "car" │ + │ ) │ + └────────────────┘ + """ + } + assertQuery( + Tag.where { + $0 > Tag(id: 1, title: "car") + } + ) { + """ + SELECT "tags"."id", "tags"."title" + FROM "tags" + WHERE ("tags"."id", "tags"."title") > (1, 'car') + """ + } results: { + """ + ┌─────────────────────┐ + │ Tag( │ + │ id: 2, │ + │ title: "kids" │ + │ ) │ + ├─────────────────────┤ + │ Tag( │ + │ id: 3, │ + │ title: "someday" │ + │ ) │ + ├─────────────────────┤ + │ Tag( │ + │ id: 4, │ + │ title: "optional" │ + │ ) │ + └─────────────────────┘ + """ + } + } } } diff --git a/Tests/StructuredQueriesTests/SelectionTests.swift b/Tests/StructuredQueriesTests/SelectionTests.swift index daef8585..99812904 100644 --- a/Tests/StructuredQueriesTests/SelectionTests.swift +++ b/Tests/StructuredQueriesTests/SelectionTests.swift @@ -19,9 +19,9 @@ extension SnapshotTests { } ) { """ - SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" AS "remindersList", count("reminders"."id") AS "remindersCount" + SELECT "remindersLists"."id" AS "id", "remindersLists"."color" AS "color", "remindersLists"."title" AS "title", "remindersLists"."position" AS "position", count("reminders"."id") AS "remindersCount" FROM "remindersLists" - JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") + JOIN "reminders" ON ("remindersLists"."id") = ("reminders"."remindersListID") GROUP BY "remindersLists"."id" LIMIT 2 """ @@ -56,9 +56,9 @@ extension SnapshotTests { .map { RemindersListAndReminderCount.Columns(remindersList: $1, remindersCount: $0) } ) { """ - SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position" AS "remindersList", count("reminders"."id") AS "remindersCount" + SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position", count("reminders"."id") FROM "remindersLists" - JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") + JOIN "reminders" ON ("remindersLists"."id") = ("reminders"."remindersListID") GROUP BY "remindersLists"."id" LIMIT 2 """ @@ -87,13 +87,69 @@ extension SnapshotTests { └─────────────────────────────────┘ """ } + let remindersListAndRemindersCount = RemindersListAndReminderCount.Columns( + remindersList: RemindersList.columns, + remindersCount: Reminder.columns.count() + ) + assertQuery( + #sql( + """ + SELECT \(remindersListAndRemindersCount) + FROM \(RemindersList.self) + JOIN \(Reminder.self) ON \(RemindersList.id) = \(Reminder.remindersListID) + GROUP BY \(RemindersList.id) + """, + as: RemindersListAndReminderCount.self + ) + ) { + """ + SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position", count("reminders"."id") + FROM "remindersLists" + JOIN "reminders" ON "remindersLists"."id" = "reminders"."remindersListID" + GROUP BY "remindersLists"."id" + """ + } results: { + """ + ┌─────────────────────────────────┐ + │ RemindersListAndReminderCount( │ + │ remindersList: RemindersList( │ + │ id: 1, │ + │ color: 4889071, │ + │ title: "Personal", │ + │ position: 0 │ + │ ), │ + │ remindersCount: 5 │ + │ ) │ + ├─────────────────────────────────┤ + │ RemindersListAndReminderCount( │ + │ remindersList: RemindersList( │ + │ id: 2, │ + │ color: 15567157, │ + │ title: "Family", │ + │ position: 0 │ + │ ), │ + │ remindersCount: 3 │ + │ ) │ + ├─────────────────────────────────┤ + │ RemindersListAndReminderCount( │ + │ remindersList: RemindersList( │ + │ id: 3, │ + │ color: 11689427, │ + │ title: "Business", │ + │ position: 0 │ + │ ), │ + │ remindersCount: 2 │ + │ ) │ + └─────────────────────────────────┘ + """ + } } @Test func outerJoin() { assertQuery( Reminder .limit(2) - .leftJoin(User.all) { $0.assignedUserID.eq($1.id) } + .leftJoin(User.all) { $0.assignedUserID.is($1.id) } .select { ReminderTitleAndAssignedUserName.Columns( reminderTitle: $0.title, @@ -104,7 +160,7 @@ extension SnapshotTests { """ SELECT "reminders"."title" AS "reminderTitle", "users"."name" AS "assignedUserName" FROM "reminders" - LEFT JOIN "users" ON ("reminders"."assignedUserID" = "users"."id") + LEFT JOIN "users" ON ("reminders"."assignedUserID") IS ("users"."id") LIMIT 2 """ } results: { diff --git a/Tests/StructuredQueriesTests/Support/Schema.swift b/Tests/StructuredQueriesTests/Support/Schema.swift index 0a6e34d9..68f0d163 100644 --- a/Tests/StructuredQueriesTests/Support/Schema.swift +++ b/Tests/StructuredQueriesTests/Support/Schema.swift @@ -111,7 +111,7 @@ extension Database { try execute( """ CREATE TABLE "reminders" ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "assignedUserID" INTEGER, "dueDate" DATE, "isCompleted" BOOLEAN NOT NULL DEFAULT 0, diff --git a/Tests/StructuredQueriesTests/TableTests.swift b/Tests/StructuredQueriesTests/TableTests.swift index 2d6db820..e1004700 100644 --- a/Tests/StructuredQueriesTests/TableTests.swift +++ b/Tests/StructuredQueriesTests/TableTests.swift @@ -41,7 +41,7 @@ extension SnapshotTests { """ SELECT "rows"."id", "rows"."isDeleted" FROM "rows" - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) ORDER BY "rows"."id" DESC """ } results: { @@ -58,7 +58,7 @@ extension SnapshotTests { """ SELECT "rows"."id", "rows"."isDeleted" FROM "rows" - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) ORDER BY "rows"."id" DESC """ } results: { @@ -116,7 +116,7 @@ extension SnapshotTests { ) { """ DELETE FROM "rows" - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { @@ -137,7 +137,7 @@ extension SnapshotTests { ) { """ DELETE FROM "rows" - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { @@ -155,7 +155,7 @@ extension SnapshotTests { ) { """ DELETE FROM "rows" - WHERE ("rows"."id" > 0) + WHERE ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { @@ -180,7 +180,7 @@ extension SnapshotTests { """ UPDATE "rows" SET "isDeleted" = NOT ("rows"."isDeleted") - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { @@ -202,7 +202,7 @@ extension SnapshotTests { """ UPDATE "rows" SET "isDeleted" = NOT ("rows"."isDeleted") - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { @@ -221,7 +221,7 @@ extension SnapshotTests { """ UPDATE "rows" SET "isDeleted" = NOT ("rows"."isDeleted") - WHERE ("rows"."id" > 0) + WHERE ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { @@ -366,7 +366,7 @@ extension SnapshotTests { """ SELECT "rows"."id", "rows"."isDeleted" FROM "rows" - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) """ } results: { """ @@ -382,7 +382,7 @@ extension SnapshotTests { """ SELECT "rows"."id", "rows"."isDeleted" FROM "rows" - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) """ } results: { """ @@ -425,7 +425,7 @@ extension SnapshotTests { ) { """ DELETE FROM "rows" - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { @@ -446,7 +446,7 @@ extension SnapshotTests { ) { """ DELETE FROM "rows" - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { @@ -464,7 +464,7 @@ extension SnapshotTests { ) { """ DELETE FROM "rows" - WHERE ("rows"."id" > 0) + WHERE ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { @@ -489,7 +489,7 @@ extension SnapshotTests { """ UPDATE "rows" SET "isDeleted" = NOT ("rows"."isDeleted") - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { @@ -511,7 +511,7 @@ extension SnapshotTests { """ UPDATE "rows" SET "isDeleted" = NOT ("rows"."isDeleted") - WHERE NOT ("rows"."isDeleted") AND ("rows"."id" > 0) + WHERE NOT ("rows"."isDeleted") AND ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { @@ -530,7 +530,7 @@ extension SnapshotTests { """ UPDATE "rows" SET "isDeleted" = NOT ("rows"."isDeleted") - WHERE ("rows"."id" > 0) + WHERE ("rows"."id") > (0) RETURNING "id", "isDeleted" """ } results: { diff --git a/Tests/StructuredQueriesTests/TaggedTests.swift b/Tests/StructuredQueriesTests/TaggedTests.swift new file mode 100644 index 00000000..0d2bf062 --- /dev/null +++ b/Tests/StructuredQueriesTests/TaggedTests.swift @@ -0,0 +1,48 @@ +#if StructuredQueriesTagged + import Dependencies + import Foundation + import InlineSnapshotTesting + import StructuredQueries + import Tagged + import Testing + import _StructuredQueriesSQLite + + extension SnapshotTests { + @Suite struct TaggedTests { + @Test func basics() { + assertQuery( + Reminder + .insert { + Reminder(id: 11 as Reminder.ID, remindersListID: 1 as Tagged) + } + .returning(\.self) + ) { + """ + INSERT INTO "reminders" + ("id", "remindersListID") + VALUES + (11, 1) + RETURNING "id", "remindersListID" + """ + } results: { + """ + ┌────────────────────────────────────────┐ + │ SnapshotTests.TaggedTests.Reminder( │ + │ id: Tagged(rawValue: 11), │ + │ remindersListID: Tagged(rawValue: 1) │ + │ ) │ + └────────────────────────────────────────┘ + """ + } + } + + @Table + fileprivate struct Reminder { + typealias ID = Tagged + + let id: ID + let remindersListID: Tagged + } + } + } +#endif diff --git a/Tests/StructuredQueriesTests/TriggersTests.swift b/Tests/StructuredQueriesTests/TriggersTests.swift index 070447bd..ca507e95 100644 --- a/Tests/StructuredQueriesTests/TriggersTests.swift +++ b/Tests/StructuredQueriesTests/TriggersTests.swift @@ -7,6 +7,8 @@ import _StructuredQueriesSQLite extension SnapshotTests { @Suite struct TriggersTests { + @Dependency(\.defaultDatabase) var db + @Test func basics() { let trigger = RemindersList.createTemporaryTrigger( after: .insert { new in @@ -20,21 +22,21 @@ extension SnapshotTests { assertQuery(trigger) { """ CREATE TEMPORARY TRIGGER - "after_insert_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:11:57" + "after_insert_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:13:57" AFTER INSERT ON "remindersLists" FOR EACH ROW BEGIN UPDATE "remindersLists" SET "position" = ( - SELECT (coalesce(max("remindersLists"."position"), -1) + 1) + SELECT (coalesce(max("remindersLists"."position"), -1)) + (1) FROM "remindersLists" ) - WHERE ("remindersLists"."id" = "new"."id"); + WHERE ("remindersLists"."id") = ("new"."id"); END """ } assertQuery(trigger.drop()) { """ - DROP TRIGGER "after_insert_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:11:57" + DROP TRIGGER "after_insert_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:13:57" """ } } @@ -52,12 +54,12 @@ extension SnapshotTests { ) { """ CREATE TEMPORARY TRIGGER - "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:45:42" + "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:47:42" AFTER UPDATE ON "reminders" FOR EACH ROW BEGIN UPDATE "reminders" SET "dueDate" = '2001-01-01 00:00:00.000' - WHERE ("reminders"."id" = "new"."id"); + WHERE ("reminders"."id") = ("new"."id"); END """ } @@ -87,12 +89,12 @@ extension SnapshotTests { ) { """ CREATE TEMPORARY TRIGGER - "after_update_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:82:45" + "after_update_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:84:45" AFTER UPDATE ON "remindersLists" FOR EACH ROW BEGIN UPDATE "remindersLists" - SET "position" = ("remindersLists"."position" + 1) - WHERE ("remindersLists"."rowid" = "new"."rowid"); + SET "position" = ("remindersLists"."position") + (1) + WHERE ("remindersLists"."rowid") = ("new"."rowid"); END """ } @@ -104,12 +106,37 @@ extension SnapshotTests { ) { """ CREATE TEMPORARY TRIGGER - "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:103:40" + "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:105:40" AFTER UPDATE ON "reminders" FOR EACH ROW BEGIN UPDATE "reminders" SET "updatedAt" = datetime('subsec') - WHERE ("reminders"."rowid" = "new"."rowid"); + WHERE ("reminders"."rowid") = ("new"."rowid"); + END + """ + } + } + + @Test func afterUpdateTouchDate_NestedTimestamps() throws { + try db.execute( + """ + CREATE TABLE "episodes" ( + "id" INTEGER PRIMARY KEY, + "createdAt" TEXT NOT NULL, + "updatedAt" TEXT + ) STRICT + """) + assertQuery( + Episode.createTemporaryTrigger(afterUpdateTouch: \.timestamps.updatedAt) + ) { + """ + CREATE TEMPORARY TRIGGER + "after_update_on_episodes@StructuredQueriesTests/TriggersTests.swift:129:39" + AFTER UPDATE ON "episodes" + FOR EACH ROW BEGIN + UPDATE "episodes" + SET "updatedAt" = datetime('subsec') + WHERE ("episodes"."rowid") = ("new"."rowid"); END """ } @@ -121,12 +148,12 @@ extension SnapshotTests { ) { """ CREATE TEMPORARY TRIGGER - "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:120:40" + "after_update_on_reminders@StructuredQueriesTests/TriggersTests.swift:146:40" AFTER UPDATE ON "reminders" FOR EACH ROW BEGIN UPDATE "reminders" SET "updatedAt" = customDate() - WHERE ("reminders"."rowid" = "new"."rowid"); + WHERE ("reminders"."rowid") = ("new"."rowid"); END """ } @@ -150,17 +177,17 @@ extension SnapshotTests { assertQuery(trigger) { """ CREATE TEMPORARY TRIGGER - "after_insert_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:136:57" + "after_insert_on_remindersLists@StructuredQueriesTests/TriggersTests.swift:162:57" AFTER INSERT ON "remindersLists" FOR EACH ROW BEGIN UPDATE "remindersLists" SET "position" = ( - SELECT (coalesce(max("remindersLists"."position"), -1) + 1) + SELECT (coalesce(max("remindersLists"."position"), -1)) + (1) FROM "remindersLists" ) - WHERE ("remindersLists"."id" = "new"."id"); + WHERE ("remindersLists"."id") = ("new"."id"); DELETE FROM "remindersLists" - WHERE ("remindersLists"."position" = 0); + WHERE ("remindersLists"."position") = (0); SELECT "remindersLists"."position" FROM "remindersLists"; END @@ -169,3 +196,12 @@ extension SnapshotTests { } } } + +@Table private struct Episode { + let id: Int + let timestamps: Timestamps +} +@Selection private struct Timestamps { + let createdAt: Date + let updatedAt: Date? +} diff --git a/Tests/StructuredQueriesTests/UnionTests.swift b/Tests/StructuredQueriesTests/UnionTests.swift index 2fb8a1ed..d9299cc7 100644 --- a/Tests/StructuredQueriesTests/UnionTests.swift +++ b/Tests/StructuredQueriesTests/UnionTests.swift @@ -185,7 +185,7 @@ extension SnapshotTests { } } -@Table @Selection +@Table private struct Name { let type: String let value: String diff --git a/Tests/StructuredQueriesTests/UpdateTests.swift b/Tests/StructuredQueriesTests/UpdateTests.swift index 061c6cdc..88b2d8ea 100644 --- a/Tests/StructuredQueriesTests/UpdateTests.swift +++ b/Tests/StructuredQueriesTests/UpdateTests.swift @@ -47,7 +47,7 @@ extension SnapshotTests { """ UPDATE "reminders" SET "isCompleted" = 1 - WHERE ("reminders"."priority" IS NULL) + WHERE ("reminders"."priority") IS (NULL) RETURNING "title", "priority", "isCompleted" """ } results: { @@ -103,7 +103,7 @@ extension SnapshotTests { """ UPDATE "reminders" SET "assignedUserID" = 1, "dueDate" = '2001-01-01 00:00:00.000', "isCompleted" = 1, "isFlagged" = 0, "notes" = 'Milk, Eggs, Apples', "priority" = NULL, "remindersListID" = 1, "title" = 'Groceries', "updatedAt" = '2040-02-14 23:31:30.000' - WHERE ("reminders"."id" = 1) + WHERE ("reminders"."id") = (1) RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" """ } results: { @@ -162,7 +162,7 @@ extension SnapshotTests { ) { """ UPDATE "reminders" - SET "title" = ("reminders"."title" || '!'), "title" = ("reminders"."title" || '?') + SET "title" = ("reminders"."title") || ('!'), "title" = ("reminders"."title") || ('?') """ } } @@ -225,7 +225,7 @@ extension SnapshotTests { """ UPDATE "reminders" SET "dueDate" = CURRENT_TIMESTAMP - WHERE ("reminders"."id" = 1) + WHERE ("reminders"."id") = (1) RETURNING "title" """ } results: { @@ -270,8 +270,8 @@ extension SnapshotTests { ) { """ UPDATE "reminders" AS "rs" - SET "title" = ("rs"."title" || ' 2') - WHERE ("rs"."id" = 1) + SET "title" = ("rs"."title") || (' 2') + WHERE ("rs"."id") = (1) RETURNING "id", "assignedUserID", "dueDate", "isCompleted", "isFlagged", "notes", "priority", "remindersListID", "title", "updatedAt" """ } results: { @@ -344,7 +344,7 @@ extension SnapshotTests { """ UPDATE "reminders" SET "assignedUserID" = NULL, "dueDate" = NULL, "isCompleted" = 1, "isFlagged" = 0, "notes" = '', "priority" = NULL, "remindersListID" = 1, "title" = 'Buy iPhone', "updatedAt" = '2040-02-14 23:31:30.000' - WHERE ("reminders"."id" = 100) + WHERE ("reminders"."id") = (100) """ } } @@ -364,8 +364,8 @@ extension SnapshotTests { ) { """ UPDATE "reminders" - SET "dueDate" = CASE WHEN ("reminders"."dueDate" IS NULL) THEN '2018-01-29 00:08:00.000' END - WHERE ("reminders"."id" IN (1)) + SET "dueDate" = CASE WHEN ("reminders"."dueDate") IS (NULL) THEN '2018-01-29 00:08:00.000' END + WHERE ("reminders"."id") IN ((1)) RETURNING "dueDate" """ } results: { @@ -382,8 +382,8 @@ extension SnapshotTests { ) { """ UPDATE "reminders" - SET "dueDate" = CASE WHEN ("reminders"."dueDate" IS NULL) THEN '2018-01-29 00:08:00.000' END - WHERE ("reminders"."id" IN (1)) + SET "dueDate" = CASE WHEN ("reminders"."dueDate") IS (NULL) THEN '2018-01-29 00:08:00.000' END + WHERE ("reminders"."id") IN ((1)) RETURNING "dueDate" """ } results: { diff --git a/Tests/StructuredQueriesTests/ViewsTests.swift b/Tests/StructuredQueriesTests/ViewsTests.swift index c9dbf2fc..d0945b57 100644 --- a/Tests/StructuredQueriesTests/ViewsTests.swift +++ b/Tests/StructuredQueriesTests/ViewsTests.swift @@ -166,7 +166,7 @@ extension SnapshotTests { AS SELECT "reminders"."id" AS "reminderID", "reminders"."title" AS "reminderTitle", "remindersLists"."title" AS "remindersListTitle" FROM "reminders" - JOIN "remindersLists" ON ("reminders"."remindersListID" = "remindersLists"."id") + JOIN "remindersLists" ON ("reminders"."remindersListID") = ("remindersLists"."id") """ } @@ -197,7 +197,7 @@ extension SnapshotTests { ("new"."reminderTitle", ( SELECT "remindersLists"."id" FROM "remindersLists" - WHERE ("remindersLists"."title" = "new"."remindersListTitle") + WHERE ("remindersLists"."title") = ("new"."remindersListTitle") )); END """ @@ -241,7 +241,7 @@ extension SnapshotTests { """ SELECT "reminderWithLists"."reminderID", "reminderWithLists"."reminderTitle", "reminderWithLists"."remindersListTitle" FROM "reminderWithLists" - WHERE ("reminderWithLists"."reminderID" IN (1)) + WHERE ("reminderWithLists"."reminderID") IN ((1)) """ } results: { """ @@ -289,18 +289,17 @@ extension SnapshotTests { └────────────────────────────────────────┘ """ } - } } } -@Table @Selection +@Table private struct CompletedReminder { let reminderID: Reminder.ID let title: String } -@Table @Selection +@Table private struct ReminderWithList { @Column(primaryKey: true) let reminderID: Reminder.ID diff --git a/Tests/StructuredQueriesTests/WhereTests.swift b/Tests/StructuredQueriesTests/WhereTests.swift index 74051cd9..cd4c24d0 100644 --- a/Tests/StructuredQueriesTests/WhereTests.swift +++ b/Tests/StructuredQueriesTests/WhereTests.swift @@ -224,8 +224,8 @@ extension SnapshotTests { """ SELECT "remindersLists"."id", "remindersLists"."color", "remindersLists"."title", "remindersLists"."position", "reminders"."id", "reminders"."assignedUserID", "reminders"."dueDate", "reminders"."isCompleted", "reminders"."isFlagged", "reminders"."notes", "reminders"."priority", "reminders"."remindersListID", "reminders"."title", "reminders"."updatedAt" FROM "remindersLists" - LEFT JOIN "reminders" ON ("remindersLists"."id" = "reminders"."remindersListID") - WHERE ("remindersLists"."id" IN (4)) AND "reminders"."isCompleted" + LEFT JOIN "reminders" ON ("remindersLists"."id") = ("reminders"."remindersListID") + WHERE ("remindersLists"."id") IN ((4)) AND "reminders"."isCompleted" """ } results: { """