Skip to content

Commit c3d6aba

Browse files
authored
Update README.md
1 parent 8554dfb commit c3d6aba

1 file changed

Lines changed: 116 additions & 45 deletions

File tree

README.md

Lines changed: 116 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,148 @@
88

99
<p align="center">
1010
<strong>
11-
A SQLite compiler, static analyzer and code generator for Swift
11+
A SQLite compiler, static analyzer and code generator for Swift ❤️
1212
</strong>
1313
</p>
1414

1515
## 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.
1717

1818
## 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.
2020
```sql
2121
-- Located in Migrations/1.sql
2222
CREATE TABLE todo (
2323
id INTEGER,
2424
name TEXT NOT NULL,
25-
completedOn INTEGER
25+
completedOn INTEGER AS Date
2626
)
2727

28-
-- Located in Queries/Todo.sql
28+
-- Located in Queries/Todo/Todo.sql
2929
DEFINE QUERY selectTodos AS
3030
SELECT * FROM todo;
3131
```
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+
3335
```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()
3641

42+
// The `Todo` struct is automatically generated for the table
43+
// meaning your schema and swift code will never get out of sync
3744
for todo in todos {
3845
print(todo.id, todo.name, todo.completedOn)
3946
}
4047

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() {
4250
print("Got todos", todos)
4351
}
4452
```
4553

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+
46153
## Dependency Injection
47154
> TLDR; Avoid the repository pattern, inject queries.
48155
@@ -88,39 +195,3 @@ class ViewModel {
88195
let query: any LatestExpensesQuery
89196
}
90197
```
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

Comments
 (0)