Skip to content

Add default query representations for dates and UUIDs #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ struct Reminder {
var title = ""
var isCompleted = false
var priority: Int?
@Column(as: Date.ISO8601Representation?.self)
var dueDate: Date?
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ you can use the static description of its properties to build type-safe queries.
schema of your app is defined first and foremost in your database, and then you define Swift types
that represent those database definitions.

* [Defining a table](#Defining-a-table)
* [Customizing a table](#Customizing-a-table)
* [Table names](#Table-names)
* [Column names](#Column-names)
* [Custom data types](#Custom-data-types)
* [RawRepresentable](#RawRepresentable)
* [JSON](#JSON)
* [Default representations for dates and UUIDs](#Default-representations-for-dates-and-UUIDs)
* [Primary keyed tables](#Primary-keyed-tables)
* [Ephemeral columns](#Ephemeral-columns)

### Defining a table

Suppose your database has a table defined with the following create statement:
Expand Down Expand Up @@ -152,113 +163,6 @@ with your table's columns, instead. For these data types you must either define
The library comes with several `QueryRepresentable` conformances to aid in representing dates,
UUIDs, and JSON, and you can define your own conformances for your own custom data types.

#### Dates

While some relational databases, like MySQL and Postgres, have native support for dates, SQLite
does _not_. Instead, it has 3 different ways to represent dates:

* Text column interpreted as ISO-8601-formatted string.
* Int column interpreted as number of seconds since Unix epoch.
* Double column interpreted as a Julian day (number of days since November 24, 4713 BC).

Because of this ambiguity, the `@Table` macro does not know what you intend when you define a data
type like this:

```swift
@Table struct Reminder {
let id: Int
var date: Date // 🛑 'Date' column requires a query representation
}
```

In order to make it explicit how you expect to turn a `Date` into a type that SQLite understands
(_i.e._ text, integer, or double) you can use the `@Column` macro with the `as:` argument. The
library comes with 3 strategies out the box to help, ``Foundation/Date/ISO8601Representation`` for
storing the date as a string, ``Foundation/Date/UnixTimeRepresentation`` for storing the date as an
integer, and ``Foundation/Date/JulianDayRepresentation`` for storing the date as a floating point
number.

Any of these representations can be used like so:

```swift
@Table struct Reminder {
let id: Int
@Column(as: Date.ISO8601Representation.self)
var date: Date
}
```

And StructuredQueries will take care of formatting the value for the database:

@Row {
@Column {
```swift
Reminder.insert(
Reminder.Draft(date: Date())
)
```
}
@Column {
```sql
INSERT INTO "reminders"
("date")
VALUES
('2018-01-29 00:08:00.000')
```
}
}

When querying against a date column with a Swift date, you will need to explicitly bundle up the
Swift date into the appropriate representation to use various query helpers. This can be done using
the `#bind` macro:

```swift
Reminder.where { $0.created > #bind(startDate) }
```

#### UUID

SQLite also does not have native support for UUIDs. If you try to use a UUID in your tables you
will get an error:

```swift
@Table struct Reminder {
let id: UUID // 🛑 'UUID' column requires a query representation
var title = ""
}
```

To use such identifiers in your table you can store the column as a data blob, and then you can
use the ``Foundation/UUID/BytesRepresentation`` column representation:

```swift
@Table struct Reminder {
@Column(as: UUID.BytesRepresentation.self)
let id: UUID
var title = ""
}
```

Alternatively you can store the column as text and use either
``Foundation/UUID/LowercasedRepresentation`` or ``Foundation/UUID/UppercasedRepresentation`` to
translate the UUID to text:

```swift
@Table struct Reminder {
@Column(as: UUID.LowercasedRepresentation.self)
let id: UUID
var title = ""
}
```

When querying against a UUID column with a Swift UUID, you will need to explicitly bundle up the
Swift UUID into the appropriate representation to use various query helpers. This can be done using
the `#bind` macro:

```swift
Reminder.where { $0.id != #bind(reminder.id) }
```

#### RawRepresentable

Simple data types, in particular ones conforming to `RawRepresentable` whose `RawValue` is a string
Expand Down Expand Up @@ -353,6 +257,106 @@ With that you can insert reminders with notes like so:
}
}

#### Default representations for dates and UUIDs

While some relational databases, like MySQL and Postgres, have native types for dates and UUIDs,
SQLite does _not_, and instead can represent them in a variety of ways. In order to lessen the
friction of building queries with dates and UUIDs, the library has decided to provide a default
representation for dates and UUIDs, and if that choice does not fit your schema you can explicitly
specify the representation you want.

##### Dates

Dates in SQLite have 3 different representations:

* Text column interpreted as ISO-8601-formatted string.
* Int column interpreted as number of seconds since Unix epoch.
* Double column interpreted as a Julian day (number of days since November 24, 4713 BC).

By default, StructuredQueries will bind and decode dates as ISO-8601 text. If you want the library
to use a different representation (_i.e._ integer or double), you can provide an explicit query
representation to the `@Column` macro's `as:` argument. ``Foundation/Date/UnixTimeRepresentation``
will store the date as an integer, and ``Foundation/Date/JulianDayRepresentation`` will store the
date as a floating point number.

For example:

```swift
@Table struct Reminder {
let id: Int
@Column(as: Date.UnixTimeRepresentation.self)
var date: Date
}
```

And StructuredQueries will take care of formatting the value for the database:

@Row {
@Column {
```swift
Reminder.insert(
Reminder.Draft(date: Date())
)
```
}
@Column {
```sql
INSERT INTO "reminders"
("date")
VALUES
(1517184480)
```
}
}

If you use the non-default date representation in your schema, then while querying against a
date column with a Swift Date, you will need to explicitly bundle up the Swift date into the
appropriate representation to use various query helpers. This can be done using the `#bind` macro:

```swift
Reminder.where { $0.created > #bind(startDate) }
```

> Note: When using the default representation for dates (ISO-8601 text) you do not need to use
> the `#bind` macro:
>
> ```swift
> Reminder.where { $0.created > startDate }
> ```

##### UUIDs

SQLite also does not have type-level support for UUIDs. By default, the library will bind and decode
UUIDs as lowercased, hexadecimal text, but it also provides custom representations. This includes
``Foundation/UUID/UppercasedRepresentation`` for uppercased text, as well as
``Foundation/UUID/BytesRepresentation`` for raw bytes.

To use such custom representations, you can provide it to the `@Column` macro's `as:` parameter:

```swift
@Table struct Reminder {
@Column(as: UUID.BytesRepresentation.self)
let id: UUID
var title = ""
}
```

If you use the non-default UUID representation in your schema, then while querying against a UUID
column with a Swift UUID, you will need to explicitly bundle up the Swift UUID into the appropriate
representation to use various query helpers. This can be done using
the `#bind` macro:

```swift
Reminder.where { $0.id != #bind(reminder.id) }
```

> Note: When using the default representation for UUID (lower-cased text) you do not need to use
> the `#bind` macro:
>
> ```swift
> Reminder.where { $0.id != reminder.id }
> ```

### Primary keyed tables

It is possible to tell let the `@Table` macro know which property of your data type is the primary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,14 @@ could be restored for a certain amount of time. These tables can be represented
struct RemindersList: Identifiable {
let id: Int
var title = ""
@Column(as: Date.ISO8601Representation?.self)
var deletedAt: Date?
}
@Table
struct Reminder: Identifiable {
let id: Int
var title = ""
var isCompleted = false
@Column(as: Date.ISO8601Representation?.self)
var dueAt: Date?
@Column(as: Date.ISO8601Representation?.self)
var deletedAt: Date?
var remindersListID: RemindersList.ID
}
Expand Down Expand Up @@ -207,7 +204,6 @@ struct Reminder {
let id: Int
var title = ""
var isCompleted = false
@Column(as: Date.ISO8601Representation?.self)
var deletedAt: Date?

static let all = Self.where { $0.isDeleted.isNot(nil) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@

### Conformances

- ``Foundation/Date/ISO8601Representation``
- ``Swift/Decodable/JSONRepresentation``
- ``Foundation/Date/JulianDayRepresentation``
- ``Foundation/Date/UnixTimeRepresentation``
- ``Foundation/UUID/BytesRepresentation``
- ``Foundation/UUID/LowercasedRepresentation``
- ``Foundation/UUID/UppercasedRepresentation``
- ``Swift/Decodable/JSONRepresentation``

### Deprecations

- ``Foundation/Date/ISO8601Representation``
- ``Foundation/UUID/LowercasedRepresentation``
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ struct Reminder {
var title = ""
var isCompleted = false
var priority: Int?
@Column(as: Date.ISO8601Representation?.self)
var dueDate: Date?
}
```
Expand Down
1 change: 1 addition & 0 deletions Sources/StructuredQueriesCore/Exports.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@_exported import StructuredQueriesSupport
Loading