|
8 | 8 |
|
9 | 9 | <p align="center"> |
10 | 10 | <strong> |
11 | | - A SQLite compiler, static analyzer and code generator for Swift |
| 11 | + A SQLite compiler, static analyzer and code generator for Swift ❤️ |
12 | 12 | </strong> |
13 | 13 | </p> |
14 | 14 |
|
15 | 15 | ## Overview |
16 | | -Otter aims to allow developers to write plain SQL but with compile time safety. |
| 16 | +Otter is a pure Swift SQL compiler that allow developers to write plain comile time safe SQL. |
17 | 17 |
|
18 | 18 | ## Basic Usage |
19 | | -As a primer here is a quick example. Below is some SQL. The first part is in the `/Migrations` directory. This is where you can create and modify your schema. The second part is in the `/Queries` directory. |
| 19 | +As a primer here is a quick example. First, in SQL we will create our migrations and our first query. |
20 | 20 | ```sql |
21 | 21 | -- Located in Migrations/1.sql |
22 | 22 | CREATE TABLE todo ( |
23 | 23 | id INTEGER, |
24 | 24 | name TEXT NOT NULL, |
25 | | - completedOn INTEGER |
| 25 | + completedOn INTEGER AS Date |
26 | 26 | ) |
27 | 27 |
|
28 | | --- Located in Queries/Todo.sql |
| 28 | +-- Located in Queries/Todo/Todo.sql |
29 | 29 | DEFINE QUERY selectTodos AS |
30 | 30 | SELECT * FROM todo; |
31 | 31 | ``` |
32 | | -Would generate the following Swift code |
| 32 | + |
| 33 | +Otter will automatically generate all structs for the tables and queries providing the APIs below |
| 34 | + |
33 | 35 | ```swift |
34 | | -let db = DB() |
35 | | -let todos = try await db.selectTodos.execute() |
| 36 | +// Open a connection to the database |
| 37 | +let database = try DB(path: "...") |
| 38 | + |
| 39 | +// Execute the query |
| 40 | +let todos = try await database.todoQueries.selectTodos.execute() |
36 | 41 |
|
| 42 | +// The `Todo` struct is automatically generated for the table |
| 43 | +// meaning your schema and swift code will never get out of sync |
37 | 44 | for todo in todos { |
38 | 45 | print(todo.id, todo.name, todo.completedOn) |
39 | 46 | } |
40 | 47 |
|
41 | | -for try await todos in db.selectTodos.observe() { |
| 48 | +// Easily observe any query as the database changes. |
| 49 | +for try await todos in database.todoQueries.selectTodos.observe() { |
42 | 50 | print("Got todos", todos) |
43 | 51 | } |
44 | 52 | ``` |
45 | 53 |
|
| 54 | +### Or Use the Swift Macro |
| 55 | +Otter can even run within a Swift macro by adding the `@Database` macro to a `struct`. |
| 56 | + |
| 57 | +> As of now it is not recommended for larger projects. There are quite a few limitations |
| 58 | +that won't scale well beyond a fairly simple schema and a handfull of queries. ⚠️ |
| 59 | + |
| 60 | +```swift |
| 61 | +@Database |
| 62 | +struct DB { |
| 63 | + @Query("SELECT * FROM foo") |
| 64 | + var selectFooQuery: SelectFooDatabaseQuery |
| 65 | + |
| 66 | + static var migrations: [String] { |
| 67 | + return [ |
| 68 | + """ |
| 69 | + CREATE TABLE todo ( |
| 70 | + id INTEGER, |
| 71 | + name TEXT NOT NULL, |
| 72 | + completedOn INTEGER AS Date |
| 73 | + ) |
| 74 | + """ |
| 75 | + ] |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +func main() async throws { |
| 80 | + let database = try DB(path: "...") |
| 81 | + let todos = try await database.selectTodos.execute() |
| 82 | + |
| 83 | + for todo in todos { |
| 84 | + print(todo.id, todo.name, todo.completedOn) |
| 85 | + } |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +#### Current Limitations |
| 90 | +* Since macros operate purely on the syntax, all queries must be within the `@Database` itself so it has access to the schema. |
| 91 | +* All generated types will be nested under the `@Database` struct. |
| 92 | +* All `@Query` definitions must define their type as the generated `typealias` by the `@Database` macro. |
| 93 | +* Any diagnostics will be on the entire string rather than the part that actually failed. |
| 94 | + |
| 95 | +## Opening a Connection |
| 96 | +Each database will automatically have a few initializers at hand to choose from. Each are listed below. |
| 97 | +When the connection is opened, all migrations are run instantly. |
| 98 | + |
| 99 | +All connections are automatically opened up in WAL journal mode, allowing asynchronous reads while writes are happening. And all connections will automatically handle all threading and scheduling of queries for you. |
| 100 | + |
| 101 | +```swift |
| 102 | +// Defaults to a connection pool of 5 connections |
| 103 | +let database = try DB(path: "...") |
| 104 | + |
| 105 | +// Opens the database in memory, useful for unit tests or previews |
| 106 | +let database = try DB.inMemory() |
| 107 | + |
| 108 | +// Or open up using the configuration. |
| 109 | +var config = DatbaseConfig() |
| 110 | +config.path = "" // if nil, it will be in memory |
| 111 | +config.maxConnectionCount = 8 |
| 112 | +let database = try DB(config: config) |
| 113 | +``` |
| 114 | + |
| 115 | +## Types |
| 116 | +SQLite is a unique SQL database engine in that it is fairly lawless when it comes to typing. SQLite will allow you create a column with an `INTEGER` and gladly insert a `TEXT` into it. It will even let you make up your own type names and will take them. Otter only supports the core types/affinities SQLite recognizes: |
| 117 | +``` |
| 118 | +INTEGER -> Int |
| 119 | +REAL -> Double |
| 120 | +TEXT -> String |
| 121 | +BLOB -> Data |
| 122 | +ANY -> SQLAny |
| 123 | +``` |
| 124 | + |
| 125 | +> SQLite is the Javascript of SQL databases |
| 126 | +> |
| 127 | +> Richard Hipp, creator of SQLite |
| 128 | +
|
| 129 | +#### Aliasing & Custom Types |
| 130 | +SQLite's core affinity types are few, but with aliasing types we can represent more complex types in Swift like `Date` or `UUID`. |
| 131 | + |
| 132 | +Using the `AS` keyword you can specify the type to use in `Swift` |
| 133 | +```sql |
| 134 | +TEXT as UUID |
| 135 | + |
| 136 | +-- If the type has `.` in it, put the name in quotes to escape it. |
| 137 | +TEXT as "Todo.ID" |
| 138 | +``` |
| 139 | + |
| 140 | +## Operators |
| 141 | +The library ships with a few core operators. The operators allow you to perform transformations on queries inputs or output. Or even combine queries. |
| 142 | + |
| 143 | +## Then |
| 144 | +Then is used to combine two queries together. It will execute `self` first then the input query. Each query will be run within the same transaction. |
| 145 | + |
| 146 | +```swift |
| 147 | +func then<Next>( |
| 148 | + _ next: Next, |
| 149 | + nextInput: @Sendable @escaping (Input, Output) -> Next.Input |
| 150 | +) -> Queries.Then<Self, Next> |
| 151 | +``` |
| 152 | + |
46 | 153 | ## Dependency Injection |
47 | 154 | > TLDR; Avoid the repository pattern, inject queries. |
48 | 155 |
|
@@ -88,39 +195,3 @@ class ViewModel { |
88 | 195 | let query: any LatestExpensesQuery |
89 | 196 | } |
90 | 197 | ``` |
91 | | - |
92 | | -## Swift Macros |
93 | | -> TLDR; Don't use for larger projects ⚠️ |
94 | | -
|
95 | | -Otter can even run within a Swift macro by adding the `@Database` macro to a `struct`. As of now it is not recommended for larger projects. |
96 | | -There are quite a few limitations that won't scale well beyond a fairly simple schema and a handfull of queries. |
97 | | - |
98 | | -```swift |
99 | | -@Database |
100 | | -struct DB { |
101 | | - @Query("SELECT * FROM foo") |
102 | | - var selectFooQuery: SelectFooDatabaseQuery |
103 | | - |
104 | | - @Query("INSERT INTO foo (bar, baz) VALUES (?, ?)", inputName: "FooInput") |
105 | | - var insertFooQuery: InsertFooDatabaseQuery |
106 | | - |
107 | | - static var migrations: [String] { |
108 | | - return [ |
109 | | - "CREATE TABLE foo (bar INTEGER, baz TEXT);" |
110 | | - ] |
111 | | - } |
112 | | -} |
113 | | - |
114 | | -func main() async throws { |
115 | | - let database = try DB.inMemory() |
116 | | - try await database.insertFooQuery.execute(with: .init(bar: 1, baz: "Baz")) |
117 | | - let foos = try await database.selectFooQuery.execute() |
118 | | - print(foos) |
119 | | -} |
120 | | -``` |
121 | | - |
122 | | -### Current Limitations |
123 | | -* Since macros operate purely on the syntax, all queries must be within the `@Database` itself so the schema can be inferred properly. |
124 | | -* All generated types will be nested under the `@Database` struct. |
125 | | -* All `@Query` definitions must define their type as the generated `typealias` by the `@Database` macro. |
126 | | -* Any diagnostics will be on the entire string rather than the part that actually failed. |
|
0 commit comments