Skip to content

Commit f4a165c

Browse files
authored
Update README.md
1 parent 648dd7a commit f4a165c

1 file changed

Lines changed: 120 additions & 5 deletions

File tree

README.md

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
## Overview
1616
Otter is a pure Swift SQL compiler that allow developers to write plain comile time safe SQL.
1717

18-
## Basic Usage
18+
- [Installation](#installation)
19+
- [Queries](#queries)
20+
- [Types](#types)
21+
- [Operators](#operators)
22+
- [Dependency Injection](#dependency-injection)
23+
1924
As a primer here is a quick example. First, in SQL we will create our migrations and our first query.
2025
```sql
2126
-- Located in Migrations/1.sql
@@ -92,9 +97,53 @@ func main() async throws {
9297
* All `@Query` definitions must define their type as the generated `typealias` by the `@Database` macro.
9398
* Any diagnostics will be on the entire string rather than the part that actually failed.
9499

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.
100+
# Installation
101+
Otter supports Swift Package Manager. To install add the following to your `Package.swift` file.
102+
103+
```swift
104+
let package = Package(
105+
[...]
106+
dependencies: [
107+
.Package(url: "https://github.com/wickwirew/Feather.git", from: "...")
108+
]
109+
)
110+
```
111+
112+
Also the cli tool will be needed to be installed.
113+
```
114+
brew install TODO FIX THIS ONCE IT IS ACTUALLY IN BREW
115+
```
116+
117+
Once the project has been added it is time to setup the queries and migrations folders. In the root of the project where you want everything to live, in terminal run the following command
118+
```
119+
feather init
120+
```
121+
122+
This will create all diretories needed and will create your first migration. Your project should have 2 new folders and a swift file. In the migrations folder you will have a file named `1.sql`. You put your first migration code in there. The `Queries.swift` file is what the generated Swift code will be written too. The `gen` command will automatically recreate this if it gets deleted.
123+
```
124+
/Migrations/1.sql
125+
/Queries
126+
Queries.swift
127+
```
128+
129+
> 💡 Tip: Follow the SQL standard and use singular table names.
130+
131+
#### Generating the Database
132+
Once you have your first migration in and the project setup you can now generate the database. In the same directory where `init` was run, you run the `gen` command.
133+
```
134+
feather gen
135+
```
136+
137+
This will compile and check all migrations and queries, then generate all Swift required to talk to the database.
138+
139+
#### Adding a New Migration
140+
When a new migration is needed, you can simply add a new file with a number 1 higher than the previous. To automatically do this the cli tool can do it for you by running
141+
```
142+
feather migrate add
143+
```
144+
145+
# Opening a Connection
146+
Once you have your database being generated, you can now open a connection to it. Each database will automatically have a few initializers at hand to choose from. Each are listed below. When the connection is opened, all migrations are run instantly.
98147

99148
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.
100149

@@ -110,9 +159,75 @@ var config = DatbaseConfig()
110159
config.path = "" // if nil, it will be in memory
111160
config.maxConnectionCount = 8
112161
let database = try DB(config: config)
162+
163+
// All migrations are run on open, so it's good to use right away
164+
```
165+
166+
# Queries
167+
All queries will be stored in the `/Queries` directory. More than one query can go in each file. To get started, create a new file in the `/Queries` directory. The cli can do this automatically. In the same directory where `init` was run, execute
168+
```
169+
feather queries add --name <some-name>
170+
```
171+
172+
> 💡 Tip: Organize queries by usage, not by table. This will become more useful later on when we talk about dependency injection.
173+
174+
Open the file that was created in `/Queries`, it should be blank. Individual queries can be defined using the `DEFINE` keyword. At the moment queries can only have one statement.
175+
```sql
176+
DEFINE QUERY fetchUsers AS
177+
SELECT * FROM user;
178+
```
179+
180+
If the queries file was named `User.sql` this would be accessible via
181+
```swift
182+
let query: any FetchUsersQuery = database.userQueries.fetchUsers
183+
let users: [User] = try await query.execute()
184+
```
185+
186+
### Input and Output Types
187+
In the example above, since we selected all columns from a single table the query will return the `User` struct that was generated for the table. If additional columns are selected a new structure will be generated to match the input. In the following example we will join in the `post` table to get a users post count.
188+
```sql
189+
DEFINE QUERY fetchUsers AS
190+
SELECT user.*, COUNT(post.*) AS numberOfPosts
191+
OUTER JOIN post ON post.userId = user.id;
192+
```
193+
194+
The following `struct` would automatically be generated for the query. Since we used the syntax `user.*` it will embed the `User` struct instead of replicating it's columns. Any embeded table struct will also get a `@dynamicMemberLookup` method generated so it can be accessed directly like the other column values.
195+
```swift
196+
@dynamicMemberLookup
197+
FetchUsersOutput {
198+
let user: User
199+
let numberOfPosts: Int
200+
201+
subscript<Value>(dynamicMember dynamicMember: KeyPath<FetchUsersOutput, Value>) -> Value { ... }
202+
}
203+
```
204+
205+
### Naming
206+
The `FetchUsersOutput` name, while clear where it came from, is not too great if we want to store it in a view model or model within our app. Some queries we want to give it a better name that has more meaning. In the `DEFINE` statement we can specify a name for the inputs and outputs.
207+
```sql
208+
DEFINE QUERY queryName(input: InputName, output: OutputName) AS ...
209+
```
210+
211+
### Inputs
212+
When a query has multiple inputs it will have a struct generated for it's inputs similar to the output. Also, so the input struct does not have to be initialized everytime, an extension will be created that takes each parameter individually, rather then the full type.
213+
```sql
214+
DEFINE QUERY userPosts AS
215+
SELECT * FROM post WHERE userId = ? AND date BETWEEN ? AND ?;
216+
```
217+
218+
Would generate the following Swift code
219+
220+
```swift
221+
struct UserPostsInput {
222+
let id: Int
223+
let dateLower: Date
224+
let dateUpper: Date
225+
}
226+
227+
let posts = try await database.userQueries.userPosts.execute(id: id, dateLower: lower, dateUpper: upper)
113228
```
114229

115-
## Types
230+
# Types
116231
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 will not allow this and tends to operate more strictly like the table option `STRICT`. Only the core types that SQLite recognizes are usable for the column type.
117232
| SQLite | Swift |
118233
|---------|--------|

0 commit comments

Comments
 (0)