|
| 1 | +# ormin |
| 2 | + |
| 3 | +Prepared SQL statement generator for Nim. A lightweight ORM. |
| 4 | + |
| 5 | +Features: |
| 6 | + |
| 7 | +- Compile time query checking: Types as well as table |
| 8 | + and column names are checked, no surprises at runtime! |
| 9 | +- Automatic join generation: Ormin knows the table |
| 10 | + relations and can compute the "natural" join for you! |
| 11 | +- Nim based DSL for queries: No syntax errors at runtime, |
| 12 | + no SQL injections possible. |
| 13 | +- Generated prepared statements: As fast as low level |
| 14 | + hand written API calls! |
| 15 | +- First class JSON support: No explicit conversions |
| 16 | + from rows to JSON objects required. |
| 17 | + |
| 18 | +TODO: |
| 19 | + |
| 20 | +- Add support for UNION, INTERSECT and EXCEPT. |
| 21 | +- Transactions. |
| 22 | +- Better support for complex nested queries. |
| 23 | +- Write mysql backend. |
| 24 | + |
| 25 | +Copyright (c) 2017 Andreas Rumpf. |
| 26 | +All rights reserved. |
| 27 | + |
| 28 | +## Schema and Database Setup |
| 29 | + |
| 30 | +1. **Generate a model from SQL** – Place your schema in an `.sql` file and import it using `importModel`. The macro runs the `ormin_importer` tool and includes the generated Nim code for you |
| 31 | +2. **Create a database connection** – Ormin expects a global connection named `db` when issuing queries. The library ships drivers for SQLite and PostgreSQL; pick the matching backend in `importModel` and open a connection with Nim's database modules. |
| 32 | + |
| 33 | +### SQLite |
| 34 | + |
| 35 | +```nim |
| 36 | +import ../ormin |
| 37 | +importModel(DbBackend.sqlite, "model_sqlite") |
| 38 | +let db {.global.} = open(":memory:", "", "", "") |
| 39 | +``` |
| 40 | + |
| 41 | +### PostgreSQL |
| 42 | + |
| 43 | +```nim |
| 44 | +import postgres |
| 45 | +import ../ormin |
| 46 | +importModel(DbBackend.postgre, "model_postgre") |
| 47 | +let db {.global.} = open("localhost", "user", "password", "dbname") |
| 48 | +``` |
| 49 | + |
| 50 | +## Query DSL |
| 51 | + |
| 52 | +`query:` blocks are turned into prepared statements at compile time. Placeholders use `?` for Nim values and `%` for JSON values; Ormin chooses JSON instead of an ad-hoc variant type so your data can flow straight from/into `JsonNode` trees. `!!` splices vendor-specific SQL fragments. Typical clauses such as `where`, `join`, `orderby`, `groupby`, `limit` and `offset` are supported, and `returning` captures generated values (e.g. inserted IDs). Referring to columns from related tables can trigger **automatic join generation** based on foreign keys, reducing boilerplate joins. |
| 53 | + |
| 54 | +Example snippets: |
| 55 | + |
| 56 | +```nim |
| 57 | +# Select recent messages with a Nim parameter |
| 58 | +let recentMessages = query: |
| 59 | + select messages(content, creation, author) |
| 60 | + orderby desc(creation) |
| 61 | + limit ?maxMessages |
| 62 | +
|
| 63 | +# Insert using Nim and JSON parameters |
| 64 | +let payload = %*{"dt2": %*"2023-10-01T00:00:00Z"} |
| 65 | +query: |
| 66 | + insert tb_timestamp(dt1 = ?dt1, dt2 = %payload["dt2"]) |
| 67 | +
|
| 68 | +# Explicit join with filter |
| 69 | +let rows = query: |
| 70 | + select post(author) |
| 71 | + join person(name) on author == id |
| 72 | + where id == ?postId |
| 73 | +
|
| 74 | +# Automatic join generated from foreign keys |
| 75 | +let postsWithAuthors = query: |
| 76 | + select post(title, author(name)) |
| 77 | + where author.name == ?userName |
| 78 | +
|
| 79 | +# Multiple joins with pagination |
| 80 | +let page = query: |
| 81 | + select post(title, person(name), category(title)) |
| 82 | + join person(name) on author == id |
| 83 | + join category(title) on category == id |
| 84 | + orderby desc(post.creation) |
| 85 | + limit 5 offset 10 |
| 86 | +
|
| 87 | +# Vendor-specific function via raw SQL splice |
| 88 | +query: |
| 89 | + update users(lastOnline = !!"DATETIME('now')") |
| 90 | + where id == ?userId |
| 91 | +``` |
| 92 | + |
| 93 | +Compile with `-d:debugOrminSql` to see the produced SQL at build time, which helps when experimenting with the DSL. |
| 94 | + |
| 95 | +`tryQuery` executes a query but ignores database errors. `createProc` and `createIter` wrap a `query` block into a callable method on `db` for reuse. |
| 96 | + |
| 97 | +### Reusable Procedures and Iterators |
| 98 | + |
| 99 | +`createProc` turns a query into a procedure that returns all rows at once: |
| 100 | + |
| 101 | +```nim |
| 102 | +createProc postsByAuthor: |
| 103 | + select post(id, title) |
| 104 | + where author == ?userId |
| 105 | +
|
| 106 | +let posts = db.postsByAuthor(userId) |
| 107 | +``` |
| 108 | + |
| 109 | +`createIter` emits an iterator that yields rows lazily: |
| 110 | + |
| 111 | +```nim |
| 112 | +createIter postsIter: |
| 113 | + select post(id, title) |
| 114 | + where author == ?userId |
| 115 | +
|
| 116 | +for row in db.postsIter(userId): |
| 117 | + echo row.title |
| 118 | +``` |
| 119 | + |
| 120 | +Both forms accept parameters matching the `?`/`%` placeholders and produce the same return types as an inline `query` block. |
| 121 | + |
| 122 | +## Return Types |
| 123 | + |
| 124 | +- Selecting multiple columns returns a sequence of tuples of the inferred Nim types. |
| 125 | +- Selecting a single column produces a sequence of that Nim type, e.g. `let names: seq[string] = query: select person(name)`. |
| 126 | +- `produce json` emits `JsonNode` objects instead of Nim tuples; `produce nim` forces standard Nim results. |
| 127 | +- `returning` or `limit 1` make the query return a single value or tuple instead of a sequence. |
| 128 | +- Generated procedures/iterators return the same types as the underlying query (see `createProc`/`createIter` tests). |
| 129 | + |
| 130 | +Examples: |
| 131 | + |
| 132 | +```nim |
| 133 | +# Sequence of tuples |
| 134 | +let threads = query: |
| 135 | + select thread(id, name) |
| 136 | +
|
| 137 | +# Sequence of simple Nim types |
| 138 | +let ids = query: |
| 139 | + select thread(id) |
| 140 | +
|
| 141 | +# JSON result |
| 142 | +let threadJson = query: |
| 143 | + select thread(id, name) |
| 144 | + produce json |
| 145 | +
|
| 146 | +# Force Nim tuple even if `produce json` was used earlier |
| 147 | +let threadNim = query: |
| 148 | + select thread(id, name) |
| 149 | + produce nim |
| 150 | +
|
| 151 | +# Single row via returning |
| 152 | +let newId = query: |
| 153 | + insert thread(name = ?"topic") |
| 154 | + returning id |
| 155 | +``` |
| 156 | + |
| 157 | +## JSON and Raw SQL |
| 158 | + |
| 159 | +JSON values can be spliced directly using `%` expressions. The `%` prefix tells Ormin to treat the following Nim expression as a `JsonNode` without conversion: |
| 160 | + |
| 161 | +```nim |
| 162 | +import json |
| 163 | +let payload = %*{"id": %*1, "meta": %*{"tags": %*["nim", "orm"]}} |
| 164 | +
|
| 165 | +# Use JSON in WHERE clause |
| 166 | +let rows = query: |
| 167 | + select post(id, title) |
| 168 | + where id == %payload["id"] |
| 169 | + produce json |
| 170 | +
|
| 171 | +# Insert a row using JSON fields |
| 172 | +query: |
| 173 | + insert post(id = %payload["id"], title = %payload["title"], info = %payload["meta"]) |
| 174 | +``` |
| 175 | + |
| 176 | +`!!"RAW"` injects a literal SQL fragment for vendor-specific functions or clauses that Ormin does not know about: |
| 177 | + |
| 178 | +```nim |
| 179 | +query: |
| 180 | + update users(lastOnline = !!"DATETIME('now')") |
| 181 | + where id == ?userId |
| 182 | +``` |
| 183 | + |
| 184 | +The tests include additional samples of JSON parameters and raw SQL expressions. |
| 185 | + |
| 186 | +## Transactions and Batching |
| 187 | + |
| 188 | +A search of the codebase shows no built-in transaction or batch APIs, so these features must be handled via the underlying `db_connector` modules (e.g. issuing `BEGIN`/`COMMIT` manually). |
| 189 | + |
| 190 | +## Additional Facilities |
| 191 | + |
| 192 | +- **Protocol DSL** – The `protocol` macro lets you describe paired server/client handlers that communicate via JSON messages. Sections use keywords like `recv`, `broadcast` and `send`, and every server block must be mirrored by a client block. The chat example demonstrates this code generation. |
| 193 | +- **JSON Dispatcher** – `createDispatcher` constructs a dispatcher for textual commands mapped to Nim procedures. |
| 194 | +- **WebSocket Server** – `serverws` provides a small WebSocket server that can broadcast messages to selected receivers via the `serve` proc. |
| 195 | + |
| 196 | +## Tooling |
| 197 | + |
| 198 | +The repository ships with `tools/ormin_importer`, invoked automatically by `importModel`, to parse SQL schema files into Nim type information. |
| 199 | + |
| 200 | +## Examples |
| 201 | + |
| 202 | +The `examples/` directory contains small applications (chat, forum, tweeter) demonstrating schema import, query blocks and the protocol/WebSocket features. |
| 203 | + |
| 204 | +## Testing |
| 205 | + |
| 206 | +Run the full test suite via Nimble: |
| 207 | + |
| 208 | +```bash |
| 209 | +nimble test |
| 210 | +``` |
| 211 | + |
| 212 | +## License |
| 213 | + |
| 214 | +Ormin is released under the MIT license. |
0 commit comments