Skip to content

Commit ee65422

Browse files
authored
Ormin Docs (#60)
* docs: add comprehensive README * docs: expand README JSON and return examples * docs: mention Postgres setup * docs: add join and limit examples * docs: add createProc and createIter examples * cleanup * fixups * fixups * cleanup
1 parent f35e85a commit ee65422

7 files changed

Lines changed: 250 additions & 48 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ examples/forum/forumclient.nim
1919
examples/forum/forum
2020
examples/forum/forumproto
2121
examples/tweeter/src/createdatabase
22-
examples/tweeter/src/tweeter
22+
examples/tweeter/src/tweeterdeps/
23+
deps/
24+
nim.cfg

README.md

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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.

config.nims

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
task buildimporter, "Build ormin_importer":
3+
exec "nim c -r tools/ormin_importer"
4+
5+
task test, "Run all test suite":
6+
buildimporterTask()
7+
exec "nim c -r tests/tfeature"
8+
exec "nim c -r tests/tcommon"
9+
exec "nim c -r tests/tsqlite"
10+
# Skip PostgreSQL tests as they require a running PostgreSQL server
11+
# exec "nim c -r -d:postgre tests/tfeature"
12+
# exec "nim c -r -d:postgre tests/tcommon"
13+
# exec "nim c -r tests/tpostgre"
14+
15+
task buildexamples, "Build examples: chat and forum":
16+
buildimporterTask()
17+
selfExec "c examples/chat/server"
18+
selfExec "js examples/chat/frontend"
19+
selfExec "c examples/forum/forum"
20+
selfExec "c examples/forum/forumproto"
21+
selfExec "c examples/tweeter/src/tweeter"

ormin.nim

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ import os
88
template importModel*(backend: DbBackend; filename: string) {.dirty.} =
99
## imports a model from an SQL file.
1010
bind fileExists, addFileExt, staticExec, ExeExt, parentDir, `/`
11-
static:
12-
#when not fileExists(addFileExt("tools/ormin_importer", ExeExt)):
13-
# echo staticExec("nim c tools/ormin_importer", "", "tools/ormin_importer.nim")
11+
const file = static:
1412
let path = parentDir(instantiationInfo(-1, true)[0])
15-
echo staticExec("tools/ormin_importer " & (path / filename) & ".sql")
13+
let file = path / filename & ".sql"
14+
let res = gorgeEx("tools/ormin_importer " & file)
15+
if res.exitCode != 0:
16+
raise newException(Exception, "Failed to generate model: " & res.output)
17+
file
18+
{.warning: "Imported SQL Model: " & file.}
1619

1720
const dbBackend = backend
1821

ormin.nimble

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,4 @@ bin = @["tools/ormin_importer"]
1515
skipDirs = @["examples"]
1616
installExt = @["nim"]
1717

18-
task test, "Run all test suite":
19-
exec "nim c -r tests/tfeature"
20-
exec "nim c -r tests/tcommon"
21-
exec "nim c -r tests/tsqlite"
22-
# Skip PostgreSQL tests as they require a running PostgreSQL server
23-
# exec "nim c -r -d:postgre tests/tfeature"
24-
# exec "nim c -r -d:postgre tests/tcommon"
25-
# exec "nim c -r tests/tpostgre"
26-
27-
task buildexamples, "Build examples: chat and forum":
28-
selfExec "c examples/chat/server"
29-
selfExec "js examples/chat/frontend"
30-
selfExec "c examples/forum/forum"
31-
selfExec "c examples/forum/forumproto"
32-
selfExec "c examples/tweeter/src/tweeter"
18+
include "config.nims"

readme.md

Lines changed: 0 additions & 26 deletions
This file was deleted.

tests/tpostgre.nim

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import unittest, postgres, json, strutils, macros, times, os, sequtils
1+
import unittest, json, strutils, macros, times, os, sequtils
2+
import db_connector/postgres
23
import ormin
3-
from db_postgres import exec, getValue
4+
5+
from db_connector/postgres import exec, getValue
46
import ./utils
57

68
importModel(DbBackend.postgre, "model_postgre")

0 commit comments

Comments
 (0)