|
| 1 | +<picture> |
| 2 | + <source media="(prefers-color-scheme: dark)" srcset="https://github.com/wickwirew/Feather/blob/main/Otter~dark.png?raw=true"> |
| 3 | + <source media="(prefers-color-scheme: light)" srcset="https://github.com/wickwirew/Feather/blob/main/Otter.png?raw=true"> |
| 4 | + <p align="center"> |
| 5 | + <img alt="Otter" src="https://github.com/wickwirew/Feather/blob/main/Otter.png?raw=true" width=33% height=33%> |
| 6 | + </p> |
| 7 | +</picture> |
| 8 | + |
| 9 | +<p align="center"> |
| 10 | + A fast, lightweight SQLite library for Swift |
| 11 | +</p> |
| 12 | + |
| 13 | +## Overview |
| 14 | +Otter generates type safe code from raw plain SQL. |
| 15 | + |
| 16 | +## Basic Usage |
| 17 | +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. |
| 18 | +```sql |
| 19 | +-- Located in Migrations/1.sql |
| 20 | +CREATE TABLE todo ( |
| 21 | + id INTEGER, |
| 22 | + name TEXT NOT NULL, |
| 23 | + completedOn INTEGER |
| 24 | +) |
| 25 | + |
| 26 | +-- Located in Queries/Todo.sql |
| 27 | +DEFINE QUERY selectTodos AS |
| 28 | +SELECT * FROM todo; |
| 29 | +``` |
| 30 | +Would generate the following Swift code |
| 31 | +```swift |
| 32 | +let db = DB() |
| 33 | +let todos = try await db.selectTodos.execute() |
| 34 | + |
| 35 | +for todo in todos { |
| 36 | + print(todo.id, todo.name, todo.completedOn) |
| 37 | +} |
| 38 | + |
| 39 | +for try await todos in db.selectTodos.observe() { |
| 40 | + print("Got todos", todos) |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +## Dependency Injection |
| 45 | +> TLDR; Avoid the repository pattern, inject queries. |
| 46 | +
|
| 47 | +Otter was written with application development in mind. One of the common walls when talking to a database is dependecy injection. |
| 48 | +Normally this would mean wrapping your database calls in a repository or some other layer to keep the model layer testable without needing a database connection. |
| 49 | +This is all good but that means writing different protocols and mocks. When writing the protocol you need to decide whether to just make it `async` or maybe a `publisher`. |
| 50 | +Sometimes you need both... Otter solves these problems and was designed to have injection builtin. |
| 51 | + |
| 52 | +At the core, Otter exposes one core type for injections which is `any Query<Input, Output>`. This acts as a wrapper, which knows nothing about the database that can passed into a model or view model. For example, if we have a query that takes in an `Int` and returns a `String` we can setup our view model like: |
| 53 | +```swift |
| 54 | +class ViewModel { |
| 55 | + let fetchString: any Query<Int, String> |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +Then in your live application code that will actually ship to users you can pass in your database query |
| 60 | +```swift |
| 61 | +let viewModel = ViewModel(fetchString: database.fetchString) |
| 62 | +``` |
| 63 | + |
| 64 | +In unit tests or previews where you don't want a database you can pass a `Just` or a `Fail` |
| 65 | +```swift |
| 66 | +let viewModel = ViewModel(fetchString: Queries.Just("Just a string, no database needed 😎")) |
| 67 | + |
| 68 | +// Will throw the `MockError` any time the query is executed |
| 69 | +let alwaysFails = ViewModel(fetchString: Queries.Fail(MockError())) |
| 70 | +``` |
| 71 | + |
| 72 | +#### Injecting Type Aliases |
| 73 | +The example above is nice but doesn't really represent the common use case. |
| 74 | +Most of the time we don't just have a query that has an input and output of simple builtins. |
| 75 | +They can be larger generated structs which can be a lot to type. To fix this typealiases are |
| 76 | +generated for a query to give them a simple readable name. For example |
| 77 | +```sql |
| 78 | +DEFINE QUERY latestExpenses AS |
| 79 | +SELECT id, title, amount FROM expense |
| 80 | +WHERE date BETWEEN ? AND ?; |
| 81 | +``` |
| 82 | +Would generate the following `typealias` for injection |
| 83 | +```swift |
| 84 | +class ViewModel { |
| 85 | + // Equivalent to `any Query<LatestExpensesInput, LatestExpensesOutput>` |
| 86 | + let query: any LatestExpensesQuery |
| 87 | +} |
| 88 | +``` |
0 commit comments