Add Ormin ORM support for BaraDB (Nim client)
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled

This commit is contained in:
2026-05-15 21:49:08 +03:00
parent 2e945c1dcb
commit 4e54075078
63 changed files with 9764 additions and 0 deletions
+17
View File
@@ -141,6 +141,23 @@ nim c -r tests/test_integration.nim
Same as async but blocking. Available on `SyncClient`. Same as async but blocking. Available on `SyncClient`.
## Ormin Integration
The Nim client ships with an [Ormin](https://github.com/Araq/ormin) backend for compile-time checked SQL queries.
```nim
import ormin
importModel(DbBackend.baradb, "my_model")
let db {.global.} = open("127.0.0.1:9472", "admin", "", "default")
let rows = query:
select users(id, name)
where name == ?"alice"
```
See `examples/ormin_basic.nim` for a full sample.
## License ## License
Apache-2.0 Apache-2.0
+68
View File
@@ -0,0 +1,68 @@
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: test
POSTGRES_USER: test
POSTGRES_DB: test_ormin
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
strategy:
matrix:
nim:
- stable
name: Test on nim ${{ matrix.nim }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Cache choosenim
id: cache-choosenim
uses: actions/cache@v4
with:
path: ~/.choosenim
key: ${{ runner.os }}-choosenim-stable
- name: Cache nimble
id: cache-nimble
uses: actions/cache@v4
with:
path: ~/.nimble
key: ${{ runner.os }}-nimble-stable
- name: Setup nim
uses: jiro4989/setup-nim-action@v2
with:
nim-version: ${{ matrix.nim }}
- name: Install dependencies
run: |
nimble --useSystemNim install -d
nimble --useSystemNim install karax -y
nimble --useSystemNim install jester -y
nimble --useSystemNim install websocket -y
- name: Run test sqlite
run: nimble --useSystemNim test
- name: Run test postgres
run: nimble --useSystemNim test_postgres
env:
PGPASSWORD: test
- name: Build examples
run: nimble buildexamples
+29
View File
@@ -0,0 +1,29 @@
*
!*/
!*.*
*.db
*.js
nimcache
tools/ormin_importer
tests/forum_model_sqlite.nim
tests/forum_model_postgres.nim
tests/model_postgre.nim
tests/model_sqlite.nim
tests/tfeature
tests/tcommon
tests/tpostgre
tests/tsqlite
examples/chat/chat_model.nim
examples/chat/chatclient.nim
examples/chat/createdb
examples/chat/server
examples/forum/forum_model.nim
examples/forum/forumclient.nim
examples/forum/forum
examples/forum/forumproto
examples/tweeter/src/createdatabase
examples/tweeter/src/tweeterdeps/
deps/
nim.cfg
.nimcache
tests/tdb_utils
+28
View File
@@ -0,0 +1,28 @@
sudo: false
language: c
os: linux
install:
- git clone https://github.com/nim-lang/nim
- cd nim
- git clone --depth 1 https://github.com/nim-lang/csources.git
- cd csources
- sh build.sh
- cd ../
- bin/nim c koch
- ./koch boot -d:release
- ./koch tools
- cd ..
before_script:
- set -e
- export PATH=$(pwd)/nim/bin:$(pwd):$PATH
script:
- nimble develop -y
- nim c tools/ormin_importer
- cd examples/forum
- ../../tools/ormin_importer forum_model.sql
- nim c forum.nim
- nim c forumproto.nim
- cd ../..
- cd examples/chat
- ../../tools/ormin_importer chat_model.sql
- nim c server.nim
+135
View File
@@ -0,0 +1,135 @@
# Ormin + BaraDB
This is a fork of [Ormin](https://github.com/Araq/ormin) (prepared SQL statement generator for Nim) with added support for **BaraDB** — a multimodal database engine with a binary wire protocol.
## What changed?
- Added `DbBackend.baradb` to the backend enum.
- Added `ImportTarget.baradb` for schema import.
- New backend file `ormin/ormin_baradb.nim` that bridges Ormin's compile-time query DSL to the BaraDB Nim client (`baradb/client`).
## Installation
Install both the BaraDB client and this Ormin fork:
```bash
# Install BaraDB client (from barabadb/clients/nim)
cd clients/nim
nimble install
# Install Ormin with BaraDB support
cd ormin
nimble install
```
## Quick Start
### 1. Write your schema
Create a file named `model.sql`:
```sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255),
age INT
);
```
### 2. Use Ormin DSL in Nim
```nim
import json
import ormin
importModel(DbBackend.baradb, "model")
let db {.global.} = open("127.0.0.1:9472", "admin", "", "default")
# Select tuples
proc listUsers() =
let rows = query:
select users(id, name, email)
orderby id
for r in rows:
echo "User #", r.id, ": ", r.name
# Parameterized query
proc findUser(name: string) =
let row = query:
select users(id, name, email)
where name == ?name
limit 1
echo row
# Insert
proc addUser(name, email: string; age: int) =
query:
insert users(name = ?name, email = ?email, age = ?age)
# JSON output
proc usersAsJson(): seq[JsonNode] =
result = query:
select users(id, name)
produce json
when isMainModule:
listUsers()
```
Compile with:
```bash
nim c -r myapp.nim
```
## Connection string format
`open(host:port, username, password, database)`
```nim
let db = open("127.0.0.1:9472", "admin", "", "default")
```
If you omit the port, the default `9472` is used.
## Supported Ormin features
| Feature | Status |
|---------------------|--------|
| `select` | ✅ |
| `where` | ✅ |
| `join` / `leftjoin` | ✅ |
| `insert` | ✅ |
| `update` | ✅ |
| `delete` | ✅ |
| `orderby` | ✅ |
| `limit` / `offset` | ✅ |
| `?` placeholders | ✅ |
| `%` JSON params | ✅ |
| `produce json` | ✅ |
| `query(T)` typed | ✅ |
| `createProc` | ✅ |
| `createIter` | ✅ |
| `transaction` | ⚠️ (relies on server support) |
| `returning` | ⚠️ (single-row limit works) |
## Limitations
- **Wire protocol strings**: BaraDB returns all column values as strings over the wire. The backend parses them at runtime. This is slightly less efficient than native SQLite/PostgreSQL bindings but keeps the implementation simple and portable.
- **`getLastId`**: Currently returns `0`. If BaraDB supports `RETURNING id`, use `limit 1` queries instead.
- **Async**: Ormin's DSL is synchronous by design. This backend uses `SyncClient` under the hood. If you need async, use the raw `baradb/client` async API directly.
## Running the example
```bash
cd clients/nim/ormin/examples
nim c -r baradb_basic.nim
```
*(Requires a BaraDB server on `localhost:9472`.)*
## License
Same as upstream Ormin — MIT.
+473
View File
@@ -0,0 +1,473 @@
# ormin
Prepared SQL statement generator for Nim. A lightweight ORM.
Features:
- Compile time query checking: Types as well as table
and column names are checked, no surprises at runtime!
- Automatic join generation: Ormin knows the table
relations and can compute the "natural" join for you!
- Nim based DSL for queries: No syntax errors at runtime,
no SQL injections possible.
- Generated prepared statements: As fast as low level
hand written API calls!
- First class JSON support: No explicit conversions
from rows to JSON objects required.
TODO:
- Better support for complex nested queries.
- Write mysql backend.
## Schema and Database Setup
1. **Generate a model from SQL** Place your schema in an `.sql` file and import it using `importModel`. By default this runs `ormin_importer` and includes the generated Nim code. Pass `includeStatic = true` to generate the model directly at compile time from the SQL file instead.
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.
### Static Schema
The SQL file can be easily embedded at compile time. Import `ormin/db_utils` and use `const mySql = staticLoad("schema.sql")`, which returns a distinct string type `DbSql` after running sanity-checks the SQL. Pass that const `DbSql` to `createTable` / `dropTable` overloads:
```nim
import ormin/db_utils
const schema = staticLoad("model.sql")
db.createTable(schema)
db.createTable(schema, "quoted table")
db.dropTable(schema)
db.dropTable(schema, "quoted table")
```
If you already use `importModel`, you can opt into the same static path directly there. `includeStatic = true` skips the generated `.nim` file, builds the model metadata from the `.sql` file at compile time, and exposes `sqlSchema` automatically:
```nim
import ../ormin
importModel(DbBackend.sqlite, "model_sqlite", includeStatic = true)
db.createTable(sqlSchema)
db.dropTable(sqlSchema, "tb_timestamp")
```
### SQLite
```nim
import ../ormin
importModel(DbBackend.sqlite, "model_sqlite")
let db {.global.} = open(":memory:", "", "", "")
```
Note: Ormin now properly handles quoted table names in `dropTable`. The compile flag `-d:orminLegacySqliteDropNames` restores that older drop-table behavior by using the normalized lookup name instead of the preserved SQL identifier. The old behavior only worked in SQlite, not Postgres.
### PostgreSQL
```nim
import postgres
import ../ormin
importModel(DbBackend.postgre, "model_postgre")
let db {.global.} = open("localhost", "user", "password", "dbname")
```
### BaraDB
```nim
import ormin
importModel(DbBackend.baradb, "model_baradb")
let db {.global.} = open("127.0.0.1:9472", "admin", "", "default")
```
## Query DSL
`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 `with`, `where`, joins, `orderby`, `groupby`, `limit`, `offset`, `exists`, `distinct`, window expressions, `union`/`intersect`/`except`, `returning`, and insert upserts via `onconflict` + (`donothing` or `doupdate`) are supported. Referring to columns from related tables can trigger **automatic join generation** based on foreign keys, reducing boilerplate joins.
Example snippets:
```nim
# Select recent rows from a Messages table with a Nim parameter
let recentMessages = query:
select Messages(content, creation, author)
orderby desc(creation)
limit ?maxMessages
# Insert using Nim and JSON parameters
let payload = %*{"dt2": %*"2023-10-01T00:00:00Z"}
query:
insert tb_timestamp(dt1 = ?dt1, dt2 = %payload["dt2"])
# Upsert on conflict (SQLite/PostgreSQL)
query:
insert tb_nullable(id = ?id, note = ?note)
onconflict(id)
doupdate(note = ?note)
# Conditional upsert update
query:
insert tb_nullable(id = ?id, note = ?note)
onconflict(id)
doupdate(note = ?note)
where note != ?note
# Ignore duplicates
query:
insert tb_nullable(id = ?id, note = ?note)
onconflict(id)
donothing()
# Note: plain INSERT ... VALUES does not support `where`; use `onconflict(...)+doupdate(...)+where ...`
# Explicit join with filter
let rows = query:
select Post(author)
leftjoin Person(name) on author == id
where id == ?postId
# Automatic join generated from foreign keys
let postsWithAuthors = query:
select Post(title)
join Author(name)
where author.name == ?userName
# DISTINCT queries and COUNT(DISTINCT ...)
let authorIds = query:
select `distinct` Post(author)
let authorCount = query:
select Post(count(distinct author))
# NULL predicates use `nil` or `null`
let unassigned = query:
select Ticket(id)
where assignee == nil
# Pattern matching uses backticked infix operators
let matchingPeople = query:
select Person(id, name)
where name `like` ?"john%"
# EXISTS / NOT EXISTS subqueries
let peopleWithPosts = query:
select Person(id)
where exists(select Post(id) where author == ?personId)
# CTEs use with cteName(select ...)
let recentAuthors = query:
with recent(select Post(id, author) where id <= 3)
select recent(author)
# Window functions use over(expr, ...)
let rankedPosts = query:
select Post(author, id, over(row_number(), partitionby(author), orderby(id)) as rn)
# Set operations can be written inline between select queries
let mergedIds = query:
select Person(id) where id <= 2
union
select Person(id) where id >= 4
# Multiple joins with pagination
let page = query:
select Post(title)
join Person(name) on author == id
join Category(title) on category == id
orderby desc(post.creation)
limit 5 offset 10
# Vendor-specific function via raw SQL splice
query:
update Users(lastOnline = !!"DATETIME('now')")
where id == ?userId
```
Compile with `-d:debugOrminSql` to see the produced SQL at build time, which helps when experimenting with the DSL.
`tryQuery` executes a query but ignores database errors. `createProc` and `createIter` wrap a `query` block into a callable method on `db` for reuse.
### Select and Joins
Selecting columns for the primary table is done using the syntax `select Post(title, author, ...)` where `Post` is the table and `title`, `author`, etc are columns of that table. This will return a tuple containing `(title, author, ...)`. Only one table can be selected and columns must be from that table. Unlike in SQL, columns for joined tables are selected directly in the `join` syntax.
Joins use the syntax `join Person(name, city) on author == id` where `Person` is the table and the columns `name`, and `city` are columns of that table. Often the join condition can be inferred from foreign keys and can be left out: `join Author(name, city)`. The columns listed in the joined tabled will be appended to the results tuple, i.e. `(title, author, name, city)`. Supported joins are: `join`, `innerjoin`, `leftjoin`, `leftouterjoin`, `rightjoin`, `rightouterjoin`, `fulljoin`, `fullouterjoin`, `crossjoin`, and the legacy `outerjoin`. Runtime support for `rightjoin` / `fulljoin` still depends on the SQL backend.
The join syntax differs from SQL but simplifies selecting fields from multiple tables by making them more explicit while still maintaing SQL's full query capabilities.
### Return Types
The core return type for queries is a sequence of tuples where the tuples fields are the types of the columns. Some queries with `returning` or `limit` clauses will return singular values or raise a DbError.
- Selecting multiple columns returns a sequence of tuples of the inferred Nim types.
- Selecting a single column produces a sequence of that Nim type, e.g. `let names: seq[string] = query: select person(name)`.
- `produce json` emits `JsonNode` objects instead of Nim tuples; `produce nim` forces standard Nim results.
- `returning` or `limit 1` make the query return a single value or tuple instead of a sequence.
- Generated procedures/iterators return the same types as the underlying query (see `createProc`/`createIter` tests).
Examples:
```nim
# Sequence of tuples
let threads = query:
select thread(id, name)
# Sequence of simple Nim types
let ids = query:
select thread(id)
# JSON result
let threadJson = query:
select thread(id, name)
produce json
# Force Nim tuple even if `produce json` was used earlier
let threadNim = query:
select thread(id, name)
produce nim
```
Single tuples or values can be returned in some cases:
```nim
# Single value returning
let newId = query:
insert thread(name = ?"topic")
returning id
# Single row value returning when limit is a const `1`
let newId = query:
select thread(name = ?"topic")
orderby desc(trhead.id)
limit 1
```
**Note**: use an integer arg to limit to return a sequence instead!
```nim
let n = 1
let newId = query:
select thread(name = ?"topic")
orderby desc(trhead.id)
limit ?n
```
### Typed Queries
Use `query(T):` when you want Ormin to deserialize selected columns directly into a named Nim type instead of returning the default tuple shape. This is useful at module boundaries where a named object, ref object, or scalar domain type is clearer than a tuple.
For object results, selected column names must match fields on the destination type. Use `as` aliases when the database column name differs from the Nim field name:
```nim
type
ThreadSummary = object
id: int
title: string
let threads = query(ThreadSummary):
select thread(id, name as title)
orderby id
```
Selecting one column can map directly to a scalar type:
```nim
let names = query(string):
select thread(name)
```
Queries that return a single row, such as a `limit 1` query, return one `T` instead of `seq[T]`:
```nim
let thread = query(ThreadSummary):
select thread(id, name as title)
where id == ?threadId
limit 1
```
#### `fromQueryHook` Column Hooks
Typed queries deserialize each selected column through `fromQueryHook`. You can overload this hook for your own field or scalar destination types:
```nim
import ormin/query_hooks
type
TitleLength = distinct int
ThreadTitleSize = object
id: int
title: TitleLength
proc fromQueryHook*(val: var TitleLength, value: string) =
val = TitleLength(value.len)
let rows = query(ThreadTitleSize):
select thread(id, name as title)
```
If a hook needs to handle SQL `NULL` itself, accept a `DbValue[SourceType]`:
```nim
type
NullableTitle = distinct string
proc fromQueryHook*(val: var NullableTitle, value: DbValue[string]) =
if value.isNull:
val = NullableTitle("<untitled>")
else:
val = NullableTitle(value.value)
```
These are column deserialization hooks. In object typed queries, Ormin calls `fromQueryHook` separately for each selected column that maps to a destination field; it does not currently call a hook for the entire row object. For whole-row transformations, query into an intermediate typed result and convert it in regular Nim code.
### JSON and Raw SQL
JSON values can be spliced directly using `%` expressions. The `%` prefix tells Ormin to treat the following Nim expression as a `JsonNode` without conversion:
```nim
import json
let payload = %*{"id": %*1, "meta": %*{"tags": %*["nim", "orm"]}}
# Use JSON in WHERE clause
let rows = query:
select post(id, title)
where id == %payload["id"]
produce json
# Insert a row using JSON fields
query:
insert post(id = %payload["id"], title = %payload["title"], info = %payload["meta"])
```
`!!"RAW"` injects a literal SQL fragment for vendor-specific functions or clauses that Ormin does not know about:
```nim
query:
update users(lastOnline = !!"DATETIME('now')")
where id == ?userId
```
The tests include additional samples of JSON parameters and raw SQL expressions.
### Custom SQL Functions
Use the `{.importSql.}` pragma to tell Ormin about additional SQL functions that your database provides. Declare a Nim proc or func that mirrors the SQL signature and mark it with the pragma; the declaration does not need an implementation because Ormin only uses it to register the function for the query DSL.
```nim
proc substr(s: string; start, length: int): string {.importSql.}
let name = "foo"
let rows = query:
select tb_string(substr(typstring, 1, 5))
where substr(typstring, 1, 5) == ?name
```
Imported functions participate in compile-time checking for arity and return type so they can be composed with regular Ormin expressions.
**Limitation:** argument types are currently not validated, so using mismatched parameter types still compiles—ensure the arguments you pass match what the underlying SQL function expects.
## Transactions and Batching
Use `transaction:` to run multiple queries atomically. The block commits on success and rolls back on any exception. Nesting is supported via savepoints. `tryTransaction:` behaves the same but returns `bool` (false on database errors) without raising.
Examples:
```nim
# Commit on success
transaction:
query:
insert person(id = ?(1), name = ?"alice", password = ?"p", email = ?"a@x", salt = ?"s", status = ?"ok")
query:
update thread(views = views + 1)
where id == ?(42)
# Rollback on error
let ok = tryTransaction:
query:
insert person(id = ?(2), name = ?"bob", password = ?"p", email = ?"b@x", salt = ?"s", status = ?"ok")
# Primary key violation => entire block is rolled back, ok = false
query:
insert person(id = ?(2), name = ?"duplicate", password = ?"p", email = ?"d@x", salt = ?"s", status = ?"x")
# Nested transactions via savepoints
transaction:
query:
insert person(id = ?(3), name = ?"carol", password = ?"p", email = ?"c@x", salt = ?"s", status = ?"ok")
let innerOk = tryTransaction:
# This will fail and roll back to the savepoint
query:
insert person(id = ?(3), name = ?"duplicate", password = ?"p", email = ?"d@x", salt = ?"s", status = ?"x")
doAssert innerOk == false
# Continue outer transaction normally
```
PostgreSQL and SQLite are supported. The macros use `BEGIN/COMMIT/ROLLBACK` for the outermost transaction and `SAVEPOINT/RELEASE/ROLLBACK TO` for nested scopes.
## Reusable Procedures and Iterators
`createProc` turns a query into a procedure that returns all rows at once:
```nim
createProc postsByAuthor:
select post(id, title)
where author == ?userId
let posts = db.postsByAuthor(userId)
```
`createIter` emits an iterator that yields rows lazily:
```nim
createIter postsIter:
select post(id, title)
where author == ?userId
for row in db.postsIter(userId):
echo row.title
```
Both forms accept parameters matching the `?`/`%` placeholders and produce the same return types as an inline `query` block.
Inline `query` blocks resolve `db` from the current lexical scope, so a proc parameter or local `db` binding can override the global connection when needed. This is useful for making procs which need to do complex handling:
```nim
proc loadUser(db: DbConn; userId: int): User =
let row = query:
select user(id, name, email)
where id == ?userId
limit 1
User(id: row.id, name: row.name, email: row.email)
```
## Running Arbitrary SQL
The standard `db_connector` APIs can be imported and used. For example:
```nim
discard db.getValue(sql"select setval('antibot_id_seq', 10, false)")
```
## Additional Facilities
- **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.
- **JSON Dispatcher** `createDispatcher` constructs a dispatcher for textual commands mapped to Nim procedures.
- **WebSocket Server** `serverws` provides a small WebSocket server that can broadcast messages to selected receivers via the `serve` proc.
## Tooling
The repository ships with `tools/ormin_importer`, used by the default `importModel` path, to parse SQL schema files into Nim type information and write the generated `.nim` model file.
## Examples
The `examples/` directory contains small applications (chat, forum, tweeter) demonstrating schema import, query blocks and the protocol/WebSocket features.
## Testing
Run the full test suite via Nimble:
```bash
nimble test
```
## License
Ormin is released under the MIT license.
+47
View File
@@ -0,0 +1,47 @@
switch("nimcache", ".nimcache")
task buildimporter, "Build ormin_importer":
exec "nim c -o:./tools/ormin_importer tools/ormin_importer"
task clean, "Clean generated files":
rmFile("tests/forum_model_sqlite.nim")
rmFile("tests/model_sqlite.nim")
rmFile("tests/forum_model_postgres.nim")
rmFile("tests/model_postgre.nim")
task test, "Run all test suite":
buildimporterTask()
cleanTask()
exec "nim c -f -r tests/tfeature"
exec "nim c -f -r tests/tcommon"
exec "nim c -f -r -d:release tests/tquery_types"
exec "nim c -f -r tests/tsqlite"
exec "nim c -f -r tests/tdb_utils"
exec "nim c -f -r tests/timportstatic"
task setup_postgres, "Ensure local Postgres has test DB/user":
# Use a simple script to avoid Nim/psql quoting pitfalls
exec "bash -lc 'bash tools/setup_postgres.sh'"
task test_postgres, "Run PostgreSQL test suite":
cleanTask()
buildimporterTask()
# setup_postgresTask()
# Pre-generate Postgres models to avoid include timing issues
exec "./tools/ormin_importer tests/forum_model_postgres.sql"
exec "./tools/ormin_importer tests/model_postgre.sql"
exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tfeature"
exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tcommon"
exec "nim c -f -d:nimDebugDlOpen -r -d:release -d:postgre tests/tquery_types"
exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tpostgre"
task buildexamples, "Build examples: chat and forum":
buildimporterTask()
selfExec "c examples/chat/server"
selfExec "js examples/chat/frontend"
selfExec "c examples/forum/forum"
selfExec "c examples/forum/forumproto"
selfExec "c examples/tweeter/src/tweeter"
@@ -0,0 +1,35 @@
## Basic Ormin + BaraDB example
##
## Run with:
## nim c -r examples/baradb_basic.nim
##
## Requires a BaraDB server on localhost:9472.
import ormin
importModel(DbBackend.baradb, "baradb_model")
let db {.global.} = open("127.0.0.1:9472", "admin", "", "default")
proc listUsers() =
let rows = query:
select users(id, name, email)
orderby id
for r in rows:
echo "User #", r.id, ": ", r.name, " <", r.email, ">"
proc findUserByName(name: string) =
let row = query:
select users(id, name, email)
where name == ?name
limit 1
echo "Found: ", row
proc insertUser(name, email: string; age: int) =
query:
insert users(name = ?name, email = ?email, age = ?age)
when isMainModule:
listUsers()
findUserByName("alice")
insertUser("bob", "bob@example.com", 30)
@@ -0,0 +1,6 @@
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255),
age INT
);
@@ -0,0 +1,19 @@
create table if not exists users(
id integer primary key,
name varchar(20) not null,
password varchar(32) not null,
creation timestamp not null default (DATETIME('now')),
lastOnline timestamp not null default (DATETIME('now'))
);
/* Names need to be unique: */
create unique index if not exists UserNameIx on users(name);
create table if not exists messages(
id integer primary key,
author integer not null,
content varchar(1000) not null,
creation timestamp not null default (DATETIME('now')),
foreign key (author) references users(id)
);
@@ -0,0 +1,9 @@
import strutils, db_sqlite
var db = open(connection="chat.db", user="araq", password="",
database="chat")
let model = readFile("chat_model.sql")
for m in model.split(';'):
if m.strip != "":
db.exec(sql(m), [])
db.close()
@@ -0,0 +1,92 @@
## Frontend for our chat application example.
## You need to have 'karax' installed in order for this to work.
import karax / [kbase, karax, vdom, karaxdsl, kajax, jwebsockets, jjson,
jstrutils, errors]
# Custom UI element with input validation:
proc validateNotEmpty(field: kstring): proc () =
result = proc () =
let x = getVNodeById(field)
if x.text.isNil or x.text == "":
setError(field, field & " must not be empty")
else:
setError(field, "")
proc loginField(desc, field, class: kstring;
validator: proc (field: kstring): proc ()): VNode =
result = buildHtml(tdiv):
label(`for` = field):
text desc
input(`type` = class, id = field, onchange = validator(field))
# here we setup the connection to the server:
let conn = newWebSocket("ws://localhost:8080", "orminchat")
proc send(msg: JsonNode) =
# The Ormin "protocol" requires us to have 'send' implementation.
conn.send(toJson(msg))
type User* = ref object
name*, password: kstring
const
username = kstring"username"
password = kstring"password"
message = kstring"message"
# Include the 'chatclient' helper that Ormin produced for us:
include chatclient
template loggedIn(): bool = userId > 0
proc doLogin() =
registerUser(User(name: getVNodeById(username).text,
password: getVNodeById(password).text))
proc doSendMessage() =
let inputField = getVNodeById(message)
sendMessage(TextMessage(author: userId, content: inputField.text))
inputField.setInputText ""
proc registerOnUpdate() =
conn.onmessage =
proc (e: MessageEvent) =
let msg = fromJson[JsonNode](e.data)
# 'recvMsg' was generated for us:
recvMsg(msg)
karax.redraw()
proc main(): VNode =
result = buildHtml(tdiv):
tdiv:
if not loggedIn:
loginField("Name :", username, "input", validateNotEmpty)
loginField("Password: ", password, "password", validateNotEmpty)
button(onclick = doLogin, disabled = disableOnError()):
text "Login"
p:
text getError(username)
p:
text getError(password)
tdiv:
table:
for m in allMessages:
tr:
td:
bold:
text m.name
td:
text m.content
tdiv:
if loggedIn:
label(`for` = message):
text "Message: "
input(class = "input", id = message, onkeyupenter = doSendMessage)
registerOnUpdate()
runLater proc() =
getRecentMessages()
setRenderer(main)
@@ -0,0 +1,96 @@
import ../../ormin / [serverws]
import ../../ormin
import json
# Ormin needs to know about our SQL model:
importModel(DbBackend.sqlite, "chat_model")
# Currently Ormin assumes a global database connection that needs to be
# annotated as '.global' for technical reasons. Later versions will improve.
var db {.global.} = open("chat.db", "", "", "")
# Ormin produces the file "chatclient.nim" for our frontend:
protocol "chatclient.nim":
# A 'common' code section is shared by both the client and the server:
common:
when not defined(js):
type kstring = string
type
inetType = kstring
varcharType = kstring
timestampType = kstring
intType = int
server "get recent messages":
let lastMessages = query:
select messages(content, creation, author)
join users(name)
orderby desc(creation)
limit 100
send(lastMessages)
client "get recent messages":
type TextMessage* = ref object
# Ormin fills in the fields of 'TextMessage' for us, based on the
# query in the 'server' part.
var allMessages*: seq[TextMessage] = @[]
proc getRecentMessages*()
allMessages = recv()
server "send chat message":
let userId = arg["author"].num
# unregistered users cannot send anything:
if userId == 0: return
query:
insert messages(content = %arg["content"], author = ?userId)
query:
update users(lastOnline = !!"DATETIME('now')")
where id == ?userId
let lastMessage = query:
select messages(content, creation, author)
join users(name)
orderby desc(creation)
limit 1
broadcast(lastMessage)
client "send chat message":
proc sendMessage*(m: TextMessage)
allMessages.add recv(TextMessage)
# This is the request to register/login a new user:
server "register/login a new user":
var candidates = query:
produce nim
select users(id, password)
where name == %arg["name"]
if candidates.len == 0:
let userId = query:
produce nim
insert users(name = %arg["name"], password = %arg["password"])
returning id
send(%userId)
else:
block search:
for c in candidates:
if c[1] == arg["password"].str:
send(%c[0])
echo "login found!"
break search
# invalid login:
send(%0)
echo "no such user!"
client "register/login a new user":
proc registerUser*(u: User)
var userId: int
userId = recv()
if userId == 0: setError(username, "invalid login!")
serve "orminchat", dispatch
@@ -0,0 +1,2 @@
--define: ssl
--path:"$projectDir/../../../ormin"
@@ -0,0 +1,2 @@
--define: ssl
--path:"$projectDir/../../../ormin"
+160
View File
@@ -0,0 +1,160 @@
import ../../ormin, json
importModel(DbBackend.sqlite, "forum_model")
var db {.global.} = open("stuff", "", "", "")
#var db: DbConn
#proc getPrepStmt(idx: int): PStmt
#var gPrepStmts: array[N, cstring]
type inetType = string
const
id = 90
ip = "moo"
answer = "dunno"
pw = "mypw"
email = "some@body.com"
salt = "pepper"
name = "me"
limit = 10
offset = 5
let threads = query:
select thread(id, name, views, modified)
where id in (select post(thread) where author in
(select person(id) where status notin ("Spammer") or id == ?id))
orderby desc(modified)
limit ?limit
offset ?offset
let thisThread = tryQuery:
select thread(id)
where id == ?id
createIter allThreadIds:
select thread(id)
where id == ?id
query:
delete antibot
where ip == ?ip
query:
insert antibot(?ip, ?answer)
let something = query:
select antibot(answer & answer, (if ip == "hi": 0 else: 1))
where ip == ?ip and answer =~ "%things%"
orderby desc(ip)
limit 1
let myNewPersonId: int = query:
insert person(?name, password = ?pw, ?email, ?salt, status = !!"'EmailUnconfirmed'",
lastOnline = !!"DATETIME('now')")
returning id
query:
delete session
where ip == ?ip and password == ?pw
query:
update session(lastModified = !!"DATETIME('now')")
where ip == ?ip and password == ?pw
let myj = %*{"pw": "stuff here"}
let userId1 = query:
select session(userId)
where ip == ?ip and password == %myj["pw"]
let (name9, email9, status, ban) = query:
select person(name, email, status, ban)
where id == ?id
limit 1
let (idg, nameg, pwg, emailg, creationg, saltg, statusg, lastOnlineg, bang) = query:
select person(_)
where id == ?id
limit 1
query:
update person(lastOnline = !!"DATETIME('now')")
where id == ?id
query:
update thread(views = views + 1, modified = !!"DATETIME('now')")
where id == ?id
query:
delete thread
where id notin (select post(thread))
let (author, creation) = query:
select post(author)
join person(creation)
limit 1
let (authorB, creationB) = query:
select post(author)
join person(creation) on author == id
limit 1
let allPosts = query:
select post(count(_) as cnt)
where cnt > 0
produce json
limit 1
createProc getAllThreadIds:
select thread(id)
where id == ?id
produce json
let totalThreads = query:
select thread(count(_))
where id in (select post(thread) where author == ?id and id in (
select post(min(id)) groupby thread))
limit 1
#query:
# update thread(modified = (select post(creation) where post.thread == ?thread
# orderby creation desc limit 1 ))
#[
# Check if post is the first post of the thread.
let rows = db.getAllRows(sql("select id, thread, creation from post " &
"where thread = ? order by creation asc"), $c.threadId)
proc rateLimitCheck(c: var TForumData): bool =
sql("SELECT count(*) FROM post where author = ? and " &
"(strftime('%s', 'now') - strftime('%s', creation)) < 40")
sql("SELECT count(*) FROM post where author = ? and " &
"(strftime('%s', 'now') - strftime('%s', creation)) < 90")
sql("SELECT count(*) FROM post where author = ? and " &
"(strftime('%s', 'now') - strftime('%s', creation)) < 300")
sql "insert into thread(name, views, modified) values (?, 0, DATETIME('now'))"
sql "select id, name, password, email, salt, status, ban from person where name = ?"
sql "insert into session (ip, password, userid) values (?, ?, ?)",
sql"select password, salt, strftime('%s', lastOnline) from person where name = ?"
sql("delete from post where author = (select id from person where name = ?)")
sql("update person set status = ?, ban = ? where name = ?")
sql("update person set password = ?, salt = ? where name = ?")
sql"select count(*) from person"
sql"select count(*) from post"
sql"select count(*) from thread"
sql"select id, name, strftime('%s', lastOnline), strftime('%s', creation) from person"
sql"select count(*) from post where thread = ?"
sql"select count(*) from post p, person u where u.id = p.author and p.thread = ?"
sql"select name from thread where id = ?"
sql"select id from person where name = ?"
sql"select count(*) from post where author = ?"
sql("select count(*) from thread where id in (select thread from post where" &
" author = ? and post.id in (select min(id) from post group by thread))")
sql"""select strftime('%s', lastOnline), email, ban, status
from person where id = ?"""
]#
@@ -0,0 +1,55 @@
create table if not exists thread(
id integer primary key,
name varchar(100) not null,
views integer not null,
modified timestamp not null default (DATETIME('now'))
);
create unique index if not exists ThreadNameIx on thread (name);
create table if not exists person(
id integer primary key,
name varchar(20) not null,
password varchar(32) not null,
email varchar(30) not null,
creation timestamp not null default (DATETIME('now')),
salt varchar(128) not null,
status varchar(30) not null,
lastOnline timestamp not null default (DATETIME('now')),
ban varchar(128) not null default ''
);
create unique index if not exists UserNameIx on person (name);
create table if not exists post(
id integer primary key,
author integer not null,
ip inet not null,
header varchar(100) not null,
content varchar(1000) not null,
thread integer not null,
creation timestamp not null default (DATETIME('now')),
foreign key (thread) references thread(id),
foreign key (author) references person(id)
);
create table if not exists session(
id integer primary key,
ip inet not null,
password varchar(32) not null,
userid integer not null,
lastModified timestamp not null default (DATETIME('now')),
foreign key (userid) references person(id)
);
create table if not exists antibot(
id integer primary key,
ip inet not null,
answer varchar(30) not null,
created timestamp not null default (DATETIME('now'))
);
create index PersonStatusIdx on person(status);
create index PostByAuthorIdx on post(thread, author);
@@ -0,0 +1,37 @@
import ../../ormin, ../../ormin/serverws, json
importModel(DbBackend.sqlite, "forum_model")
var db {.global.} = open("stuff", "", "", "")
protocol "forumclient.nim":
common:
when defined(js):
type kstring = cstring
else:
type kstring = string
type
inetType = kstring
varcharType = kstring
timestampType = kstring
server:
query:
delete antibot
where ip == %arg
client:
proc deleteAntibot(ip: string)
server:
query:
update session(lastModified = !!"DATETIME('now')")
where ip == %arg["ip"] and password == %arg["pw"]
client:
proc updateSession(arg: Session)
server:
let allSessions = query:
select session(_)
send(allSessions)
client:
type Session = ref object
var gSessions: seq[Session]
gSessions = recv()
proc getAllSessions()
@@ -0,0 +1,117 @@
body {
background-color: #f1f9ea;
margin: 0;
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}
div#main {
width: 80%;
margin-left: auto;
margin-right: auto;
}
div#user {
background-color: #66ac32;
width: 100%;
color: #c7f0aa;
padding: 5pt;
}
div#user > h1 {
color: #ffffff;
}
h1 {
margin: 0;
display: inline;
padding-left: 10pt;
padding-right: 10pt;
}
div#user > form {
float: right;
margin-right: 10pt;
}
div#user > form > input[type="submit"] {
border: 0px none;
padding: 5pt;
font-size: 108%;
color: #ffffff;
background-color: #515d47;
border-radius: 5px;
cursor: pointer;
}
div#user > form > input[type="submit"]:hover {
background-color: #538c29;
}
div#messages {
background-color: #a2dc78;
width: 90%;
margin-left: auto;
margin-right: auto;
color: #1a1a1a;
}
div#messages > div {
border-left: 1px solid #869979;
border-right: 1px solid #869979;
border-bottom: 1px solid #869979;
padding: 5pt;
}
div#messages > div > a, div#messages > div > span {
color: #475340;
}
div#messages > div > a:hover {
text-decoration: none;
color: #c13746;
}
h3 {
margin-bottom: 0;
font-weight: normal;
}
div#login {
width: 200px;
margin-left: auto;
margin-right: auto;
margin-top: 20%;
font-size: 130%;
}
div#login span.small {
display: block;
font-size: 56%;
}
div#newMessage {
background-color: #538c29;
width: 90%;
margin-left: auto;
margin-right: auto;
color: #ffffff;
padding: 5pt;
}
div#newMessage span {
padding-right: 5pt;
}
div#newMessage form {
display: inline;
}
div#newMessage > form > input[type="text"] {
width: 80%;
}
div#newMessage > form > input[type="submit"] {
font-size: 80%;
}
@@ -0,0 +1,3 @@
# Tweeter example
Modified from [tweeter example](https://github.com/dom96/nim-in-action-code/tree/master/Chapter7/Tweeter) in the "nim in action" book, replace the database layer with ormin.
@@ -0,0 +1,2 @@
--define: ssl
--path:"$projectDir/../../../../ormin"
@@ -0,0 +1,11 @@
import db_sqlite, os, strutils
let db {.global.} = open("tweeter.db", "", "", "")
let sqlFile = readFile(currentSourcePath.parentDir() / "tweeter_model.sql")
for t in sqlFile.split(';'):
if t.strip() != "":
db.exec(sql(t))
echo("Database created successfully!")
db.close()
@@ -0,0 +1,51 @@
import times, strutils, strformat, sequtils
from db_connector/db_sqlite import instantRows, `[]`
import ../../../ormin
import model
importModel(DbBackend.sqlite, "tweeter_model")
var db {.global.} = open("tweeter.db", "", "", "")
proc findUser*(username: string, user: var User): bool =
let res = query:
select user(username)
where username == ?username
echo res
if res.len == 0: return false
else: user.username = res[0]
let following = query:
select following(followed_user)
where follower == ?username
user.following = following.filterIt(it.len != 0)
return true
proc create*(user: User) =
query:
insert user(username = ?user.username)
proc post*(message: Message) =
if message.msg.len > 140:
raise newException(ValueError, "Message has to be less than 140 characters.")
query:
insert message(username = ?message.username,
time = ?message.time,
msg = ?message.msg)
proc follow*(follower, user: User) =
query:
insert following(follower = ?follower.username, followed_user = ?user.username)
proc findMessage*(usernames: openArray[string], limit = 10): seq[Message] =
result = @[]
if usernames.len == 0: return
var whereClause = "WHERE "
for i in 0 ..< usernames.len:
whereClause.add("trim(username) = ?")
if i < usernames.len - 1:
whereClause.add(" or ")
let s = &"SELECT username, time, msg FROM Message {whereClause} ORDER BY time DESC LIMIT {limit}"
for row in db.instantRows(sql(s), usernames):
result.add((username: row[0], time: row[1].parseInt, msg: row[2]))
@@ -0,0 +1,11 @@
type
User* = tuple[
username: string,
following: seq[string]
]
Message* = tuple[
username: string,
time: int,
msg: string
]
@@ -0,0 +1,59 @@
import asyncdispatch, times
import jester
import database, model, views/[user, general]
proc userLogin(request: Request, user: var User): bool =
if request.cookies.hasKey("username"):
let username = request.cookies["username"]
if not findUser(username, user):
user = (username: username, following: @[])
create(user)
return true
else:
return false
routes:
get "/":
var user: User
if userLogin(request, user):
let messages = findMessage(user.following & user.username)
resp renderMain(renderTimeline(user.username, messages))
else:
resp renderMain(renderLogin())
get "/@name":
cond '.' notin @"name"
var user: User
if not findUser(@"name", user):
halt "User not found"
let messages = findMessage([user.username])
var currentUser: User
if userLogin(request, currentUser):
resp renderMain(renderUser(user, currentUser) & renderMessages(messages))
else:
resp renderMain(renderUser(user) & renderMessages(messages))
post "/follow":
var follower, target: User
if not findUser(@"follower", follower):
halt "Follower not found"
if not findUser(@"target", target):
halt "Follow target not found"
follow(follower, target)
redirect uri("/" & @"target")
post "/login":
setCookie("username", @"username", getTime().utc() + 2.hours)
redirect "/"
post "/createMessage":
let message = (
username: @"username",
time: getTime().toUnix().int,
msg: @"message"
)
post(message)
redirect "/"
runForever()
@@ -0,0 +1,24 @@
# Generated by ormin_importer. DO NOT EDIT.
type
Attr = object
name: string
tabIndex: int
typ: DbTypekind
key: int # 0 nothing special,
# +1 -- primary key
# -N -- references attribute N
const tableNames = [
"user",
"following",
"message"
]
const attributes = [
Attr(name: "username", tabIndex: 0, typ: dbVarchar, key: 1),
Attr(name: "follower", tabIndex: 1, typ: dbVarchar, key: -1),
Attr(name: "followed_user", tabIndex: 1, typ: dbVarchar, key: -1),
Attr(name: "username", tabIndex: 2, typ: dbVarchar, key: -1),
Attr(name: "time", tabIndex: 2, typ: dbInt, key: 0),
Attr(name: "msg", tabIndex: 2, typ: dbVarchar, key: 0)
]
@@ -0,0 +1,18 @@
CREATE TABLE IF NOT EXISTS User(
username text PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS Following(
follower text,
followed_user text,
PRIMARY KEY (follower, followed_user),
FOREIGN KEY (follower) REFERENCES User(username),
FOREIGN KEY (followed_user) REFERENCES User(username)
);
CREATE TABLE IF NOT EXISTS Message(
username text,
time integer,
msg text NOT NULL,
FOREIGN KEY (username) REFERENCES User(username)
);
@@ -0,0 +1,51 @@
#? stdtmpl(subsChar = '$', metaChar = '#')
#import xmltree
#import ../model
#import user
#
#proc `$!`(text: string): string = escape(text)
#end proc
#
#proc renderMain*(body: string): string =
# result = ""
<!DOCTYPE html>
<html>
<head>
<title>Tweeter written in Nim</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
${body}
</body>
</html>
#end proc
#
#proc renderLogin*(): string =
# result = ""
<div id="login">
<span>Login</span>
<span class="small">Please type in your username...</span>
<form action="login" method="post">
<input type="text" name="username">
<input type="submit" value="Login">
</form>
</div>
#end proc
#
#proc renderTimeline*(username: string, messages: openArray[Message]): string =
# result = ""
<div id="user">
<h1>${$!username} timeline</h1>
</div>
<div id="newMessage">
<span>New message</span>
<form action="createMessage" method="post">
<input type="text" name="message">
<input type="hidden" name="username" value="${$!username}">
<input type="submit" value="Tweet">
</form>
</div>
${renderMessages(messages)}
#end proc
@@ -0,0 +1,41 @@
#? stdtmpl(subsChar = '$', metaChar = '#', toString = "xmltree.escape")
#import xmltree
#import times
#import "../model"
#
#proc renderUser*(user: User): string =
# result = ""
<div id="user">
<h1>${user.username}</h1>
<span>Following: ${$user.following.len}</span>
</div>
#end proc
#
#proc renderUser*(user, currentUser: User): string =
# result = ""
<div id="user">
<h1>${user.username}</h1>
<span>Following: ${$user.following.len}</span>
#if user.username notin currentUser.following and user.username != currentUser.username:
<form action="follow" method="post">
<input type="hidden" name="follower" value="${currentUser.username}">
<input type="hidden" name="target" value="${user.username}">
<input type="submit" value="Follow">
</form>
#end if
</div>
#
#end proc
#
#proc renderMessages*(messages: openArray[Message]): string =
# result = ""
<div id="messages">
#for message in messages:
<div>
<a href="/${message.username}">${message.username}</a>
<span>${message.time.fromUnix().format("HH:mm MMMM d',' yyyy")}</span>
<h3>${message.msg}</h3>
</div>
#end for
</div>
#end proc
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Andreas Rumpf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+49
View File
@@ -0,0 +1,49 @@
type
DbBackend* {.pure.} = enum
postgre, sqlite, mysql, baradb
import os
from ormin/importer_core import generateModelCode, ImportTarget
template importModel*(backend: DbBackend, filename: string, includeStatic: static[bool] = false) {.dirty.} =
## imports a model from an SQL file.
bind fileExists, parentDir, `/`
bind generateModelCode, ImportTarget
const file = static:
let path = parentDir(instantiationInfo(-1, true)[0])
path / filename & ".sql"
const importedFile = static:
if not includeStatic:
let res =
if fileExists("./tools/ormin_importer"):
gorgeEx("./tools/ormin_importer " & file)
else:
# run ormin_importer from the PATH
gorgeEx("ormin_importer " & file)
if res.exitCode != 0:
raise newException(Exception, "Failed to generate model: " & res.output)
file
{.warning: "Imported SQL Model: " & importedFile.}
const dbBackend = backend
import db_connector/db_common
when includeStatic:
import ormin/db_utils
when dbBackend == DbBackend.sqlite:
import ormin/ormin_sqlite
elif dbBackend == DbBackend.postgre:
import ormin/ormin_postgre
elif dbBackend == DbBackend.mysql:
import ormin/ormin_mysql
elif dbBackend == DbBackend.baradb:
import ormin/ormin_baradb
else:
{.error: "unknown database backend".}
when includeStatic:
generateModelCode(staticRead(file), file, ImportTarget(ord(backend)), true)
else:
include filename
include "ormin/queries"
+23
View File
@@ -0,0 +1,23 @@
# Package
version = "0.9.0"
author = "Araq"
description = "Prepared SQL statement generator. A lightweight ORM."
license = "MIT"
bin = @["tools/ormin_importer"]
skipDirs = @["examples"]
installExt = @["nim"]
# Dependencies
requires "nim >= 2.0.0"
requires "db_connector >= 0.1.0"
feature "examples":
requires "websocket >= 0.2.2"
requires "karax"
requires "jester"
import std/os
when fileExists("config.nims"):
include "config.nims"
+43
View File
@@ -0,0 +1,43 @@
import std/strutils
import db_connector/db_common
proc dbTypFromName*(name: string): DbTypeKind =
var k = dbUnknown
case name.toLowerAscii
of "int", "integer", "int8", "smallint", "int16",
"longint", "int32", "int64", "tinyint", "hugeint": k = dbInt
of "uint", "uint8", "uint16", "uint32", "uint64": k = dbUInt
of "serial": k = dbSerial
of "bit": k = dbBit
of "bool", "boolean": k = dbBool
of "blob": k = dbBlob
of "fixedchar": k = dbFixedChar
of "varchar", "text", "string": k = dbVarchar
of "json": k = dbJson
of "xml": k = dbXml
of "decimal": k = dbDecimal
of "float", "double", "longdouble", "real": k = dbFloat
of "date", "day": k = dbDate
of "time": k = dbTime
of "datetime": k = dbDateTime
of "timestamp", "timestamptz": k = dbTimestamp
of "timeinterval": k = dbTimeInterval
of "set": k = dbSet
of "array": k = dbArray
of "composite": k = dbComposite
of "url", "uri": k = dbUrl
of "uuid": k = dbUuid
of "inet", "ip", "tcpip": k = dbInet
of "mac", "macaddress": k = dbMacAddress
of "geometry": k = dbGeometry
of "point": k = dbPoint
of "line": k = dbLine
of "lseg": k = dbLseg
of "box": k = dbBox
of "path": k = dbPath
of "polygon": k = dbPolygon
of "circle": k = dbCircle
else: discard
return k
+179
View File
@@ -0,0 +1,179 @@
import std/[os, paths]
import db_connector/db_common, strutils, strformat
import db_connector/db_postgres as db_postgres
import db_connector/db_sqlite as db_sqlite
import parsesql_tmp # import std/parsesql
export paths
type
DbConn = db_postgres.DbConn | db_sqlite.DbConn
DbId* = distinct string
DbSql* = distinct string
proc `$`*(dbId: DbId): string {.borrow.}
proc `$`*(dbSql: DbSql): string {.borrow.}
template staticLoad*(filename: string): DbSql =
bind isAbsolute, parentDir, `/`, instantiationInfo
block:
const schemaPath {.gensym.} = static:
if isAbsolute(filename):
filename
else:
parentDir(instantiationInfo(-1, true)[0]) / filename
const schemaSql {.gensym.} = staticRead(schemaPath)
static:
try:
discard parseSql(schemaSql)
except SqlParseError as e:
doAssert false, "Invalid SQL in " & schemaPath & ": " & e.msg
DbSql(schemaSql)
proc parseQualifiedIdentifier(name: string): seq[(string, bool)] =
var current = ""
var quoteChar = '\0'
var partWasQuoted = false
var i = 0
while i < name.len:
let c = name[i]
if quoteChar == '\0':
case c
of '.':
result.add((current, partWasQuoted))
current = ""
partWasQuoted = false
of '"', '\'', '`':
if current.len == 0:
quoteChar = c
partWasQuoted = true
else:
current.add(c)
else:
current.add(c)
else:
if c == quoteChar:
if i + 1 < name.len and name[i + 1] == quoteChar:
current.add(c)
inc(i)
else:
quoteChar = '\0'
else:
current.add(c)
inc(i)
result.add((current, partWasQuoted))
proc canUseBareIdentifier(part: string): bool =
if part.len == 0:
return false
if part[0] notin {'a'..'z', 'A'..'Z', '_'}:
return false
for c in part[1..^1]:
if c notin {'a'..'z', 'A'..'Z', '0'..'9', '_'}:
return false
true
proc quoteDbIdentifier*(name: string): DbId =
## Quote a database identifier for direct interpolation into SQL text.
## Dot-separated names are treated as qualified identifiers.
var partsSql: seq[string] = @[]
for (part, wasQuoted) in parseQualifiedIdentifier(name):
let normalizedPart = if wasQuoted: part else: part.toLowerAscii()
if wasQuoted or not canUseBareIdentifier(normalizedPart):
partsSql.add("\"" & normalizedPart.replace("\"", "\"\"") & "\"")
else:
partsSql.add(normalizedPart)
DbId(partsSql.join("."))
proc dropTableName(db: DbConn; tableName, lookupName: string) =
# SQL parameters bind values, not identifiers, so DROP TABLE needs the
# identifier rendered into the statement text with correct quoting.
when defined(sqlite) and defined(orminLegacySqliteDropNames):
db.exec(sql("drop table if exists " & lookupName))
else:
let tableIdentSql = quoteDbIdentifier(tableName)
when defined(postgre):
db.exec(sql("drop table if exists " & $tableIdentSql & " cascade"))
else:
db.exec(sql("drop table if exists " & $tableIdentSql))
iterator tableDefs(sql: DbSql): tuple[name, tableName, model: string] =
# Parse the entire SQL file and iterate statements via the SQL parser
var ast: SqlNode
try:
ast = parseSql($sql)
except SqlParseError as e:
echo "SQL Parse Error:\n", sql
raise e
if ast.len > 0:
# ast is a statement list; iterate each statement node
for i in 0 ..< ast.len:
let node = ast[i]
if node.kind in {nkCreateTable, nkCreateTableIfNotExists}:
yield (node[0].strVal.toLowerAscii(), $node[0], $node)
else:
# Fallback: ast might be a single statement (not a list)
let node = ast
if node.kind in {nkCreateTable, nkCreateTableIfNotExists}:
yield (node[0].strVal.toLowerAscii(), $node[0], $node)
iterator tablePairs*(sql: string): tuple[name, model: string] =
for name, _, model in tableDefs(DbSql(sql)):
yield (name, model)
iterator tablePairs*(sql: static[DbSql]): tuple[name, model: string] =
for name, _, model in tableDefs(sql):
yield (name, model)
iterator tablePairs*(sql: Path): tuple[name, model: string] =
let f = readFile($sql)
for n, m in tablePairs(f):
yield (n, m)
proc createTable*(db: DbConn; sqlFile: Path) =
for _, m in tablePairs(sqlFile):
db.exec(sql(m))
proc createTable*(db: DbConn; sqlFile: Path, name: string) =
for n, m in tablePairs(sqlFile):
if n == name:
db.exec(sql(m))
return
raiseAssert &"table: {name} not found in: {sqlFile}"
proc createTable*(db: DbConn; schemaSql: static[DbSql]) =
for _, m in tablePairs(schemaSql):
db.exec(sql(m))
proc createTable*(db: DbConn; schemaSql: static[DbSql], name: string) =
for n, m in tablePairs(schemaSql):
if n == name:
db.exec(sql(m))
return
raiseAssert &"table: {name} not found in static schema"
proc dropTable*(db: DbConn; sqlFile: Path) =
for lookupName, tableName, _ in tableDefs(DbSql(readFile($sqlFile))):
db.dropTableName(tableName, lookupName)
proc dropTable*(db: DbConn; sqlFile: Path, name: string) =
for lookupName, tableName, _ in tableDefs(DbSql(readFile($sqlFile))):
if lookupName == name:
db.dropTableName(tableName, lookupName)
return
raiseAssert &"table: {name} not found in: {sqlFile}"
proc dropTable*(db: DbConn; schemaSql: static[DbSql]) =
for lookupName, tableName, _ in tableDefs(schemaSql):
db.dropTableName(tableName, lookupName)
proc dropTable*(db: DbConn; schemaSql: static[DbSql], name: string) =
for lookupName, tableName, _ in tableDefs(schemaSql):
if lookupName == name:
db.dropTableName(tableName, lookupName)
return
raiseAssert &"table: {name} not found in static schema"
+71
View File
@@ -0,0 +1,71 @@
## This module implements a little helper macro to ease
## writing the dispatching logic for JSON based servers.
import macros
proc tbody(n: NimNode): NimNode =
# transforms::
# f fields(a, "b")
# into::
# f a = %args["a"], b = %args["b"]
case n.kind
of nnkCallKinds:
result = copyNimNode(n)
result.add tbody(n[0])
for i in 1..<n.len:
let it = n[i]
if it.kind == nnkCall and $it[0] == "fields":
for j in 1..<it.len:
let field = it[j]
let s = if field.kind in {nnkStrLit..nnkTripleStrLit}: field.strVal
else: $field
let arg = newTree(nnkPrefix, ident"%",
newTree(nnkBracketExpr, ident"arg", newLit(s)))
result.add newTree(nnkExprEqExpr, ident(s), arg)
else:
result.add tbody(it)
else:
result = copyNimNode(n)
for x in n: result.add tbody(x)
macro createDispatcher*(name, n: untyped): untyped =
expectKind n, nnkStmtList
result = newStmtList()
let disp = newTree(nnkCaseStmt, ident"cmd")
template cmdProc(name, body) {.dirty.} =
proc name(arg: JsonNode): JsonNode = body
template callCmd(name) {.dirty.} =
result = name(arg)
for x in n:
if x.kind == nnkCall and x.len == 2 and
x[0].kind == nnkIdent and x[1].kind == nnkStmtList:
result.add getAst(cmdProc(x[0], tbody x[1]))
disp.add newTree(nnkOfBranch, newLit($x[0]),
newStmtList(getAst(callCmd(x[0]))))
else:
# do not touch:
result.add n
disp.add newTree(nnkElse, newStmtList(newTree(nnkDiscardStmt, newEmptyNode())))
template dispatchProc(name, body) {.dirty.} =
proc dispatch(inp: JsonNode): JsonNode =
let arg = inp["arg"]
let cmd = inp["cmd"].getStr("")
body
result.add getAst(dispatchProc(name, disp))
when defined(debugDispatcherDsl):
echo repr result
when isMainModule:
import json, db_sqlite
createDispatcher(dispatch):
insertCustomer:
echo "insert customer", fields(a, b, c)
result = arg
selectCustomers:
echo "select customer"
result = arg
echo dispatch(nil, %*{"cmd": "insertCustomer", "arg": [1, 2, 3]})
+178
View File
@@ -0,0 +1,178 @@
import std/[macros, strutils, tables]
import db_connector/db_common
import ./db_types
import ./parsesql_tmp
const
FileHeader* = """
# Generated by ormin_importer. DO NOT EDIT.
type
Attr = object
name: string
tabIndex: int
typ: DbTypekind
key: int # 0 nothing special,
# +1 -- primary key
# -N -- references attribute N
"""
type
DbColumn* = object
name*: string
tableName*: string
typ*: DbType
primaryKey*: bool
refs*: (string, string)
DbColumns* = seq[DbColumn]
KnownTables* = OrderedTable[string, DbColumns]
ImportTarget* = enum
postgre, sqlite, mysql, baradb
proc hasAttribute(colDesc: SqlNode; k: set[SqlNodeKind]): bool =
for i in 2 ..< colDesc.len:
if colDesc[i].kind in k:
return true
proc hasRefs(colDesc: SqlNode): (string, string) =
for i in 2 ..< colDesc.len:
let c = colDesc[i]
if c.kind == nkReferences:
if c[0].kind == nkCall:
return (c[0][0].strVal, c[0][1].strVal)
elif c[0].kind == nkIdent:
return ($c[0], "id")
("", "")
proc getType(n: SqlNode): DbType =
var it = n
if it.kind == nkCall:
it = it[0]
if it.kind == nkEnumDef:
result.kind = dbEnum
result.validValues = @[]
for i in 0 ..< it.len:
assert it[i].kind == nkStringLit
result.validValues.add it[i].strVal
elif it.kind in {nkIdent, nkStringLit}:
result.kind = dbTypFromName(it.strVal)
result.name = it.strVal
proc collectTables*(n: SqlNode; t: var KnownTables) =
if n.isNil:
return
case n.kind
of nkCreateTable, nkCreateTableIfNotExists:
let tableName = n[0].strVal
var cols: DbColumns = @[]
for i in 1 ..< n.len:
let it = n[i]
if it.kind == nkColumnDef:
var typ = getType(it[1])
if hasAttribute(it, {nkNotNull}):
typ.notNull = true
cols.add DbColumn(
name: it[0].strVal,
tableName: tableName,
typ: typ,
primaryKey: hasAttribute(it, {nkPrimaryKey}),
refs: hasRefs(it)
)
for i in 1 ..< n.len:
let it = n[i]
if it.kind == nkForeignKey:
var refNode: SqlNode = nil
var localCols: seq[string] = @[]
for j in 0 ..< it.len:
let child = it[j]
if child.kind == nkReferences:
refNode = child
elif child.kind == nkIdent:
localCols.add(child.strVal)
if refNode.isNil:
continue
var r = refNode[0]
var refTable = ""
var refCols: seq[string] = @[]
if r.kind == nkColumnReference or r.kind == nkCall:
refTable = r[0].strVal
for k in 1 ..< r.len:
refCols.add(r[k].strVal)
let pairCount = min(localCols.len, refCols.len)
for k in 0 ..< pairCount:
let localName = localCols[k]
let targetCol = refCols[k]
for c in mitems(cols):
if cmpIgnoreCase(c.name, localName) == 0:
c.refs = (refTable, targetCol)
break
t[tableName] = cols
else:
for i in 0 ..< n.len:
collectTables(n[i], t)
proc attrToKey(a: DbColumn; t: KnownTables): int =
if a.primaryKey:
return 1
if a.refs[0].len > 0:
var i = 0
for k, v in pairs(t):
for b in v:
if cmpIgnoreCase(k, a.refs[0]) == 0 and cmpIgnoreCase(b.name, a.refs[1]) == 0:
return -i - 1
inc i
0
proc renderModelCode(schemaSql, schemaPath: string; target: ImportTarget; includeStatic = false): string =
discard target
let sql = parseSql(schemaSql, schemaPath)
var knownTables = initOrderedTable[string, DbColumns]()
collectTables(sql, knownTables)
result.add FileHeader
result.add "const tableNames = ["
var i = 0
for k in keys(knownTables):
if i > 0:
result.add ",\n "
else:
result.add "\n "
result.add escape(k.toLowerAscii)
inc i
result.add "\n]\n"
i = 0
var j = 0
result.add "\nconst attributes = ["
for _, v in mpairs(knownTables):
for a in v:
if j > 0:
result.add ",\n "
else:
result.add "\n "
result.add "Attr(name: "
result.add escape(a.name.toLowerAscii)
result.add ", tabIndex: "
result.add $i
result.add ", typ: "
result.add $a.typ.kind
result.add ", key: "
result.add $attrToKey(a, knownTables)
result.add ")"
inc j
inc i
result.add "\n]\n"
if includeStatic:
result.add "\nconst sqlSchema* = staticLoad("
result.add escape(schemaPath)
result.add ")\n"
proc generateModelCode*(schemaSql, schemaPath: string; target: ImportTarget; includeStatic = false): string =
renderModelCode(schemaSql, schemaPath, target, includeStatic)
macro generateModelCode*(schemaSql: static[string], schemaPath: static[string],
target: static[ImportTarget], includeStatic: static[bool] = false): untyped =
parseStmt(renderModelCode(schemaSql, schemaPath, target, includeStatic))
+231
View File
@@ -0,0 +1,231 @@
import std/[strutils, json, times, parseutils]
import db_connector/db_common
import query_hooks
export db_common
import baradb/client
export client.WireValue, client.FieldKind
type
DbConn* = SyncClient
PStmt = object
sql: string
varcharType* = string
intType* = int
floatType* = float
boolType* = bool
timestampType* = DateTime
serialType* = int
jsonType* = JsonNode
var jsonTimeFormat* = "yyyy-MM-dd HH:mm:ss"
proc dbError*(db: DbConn) {.noreturn.} =
var e: ref DbError
new(e)
e.msg = "BaraDB query failed"
raise e
proc prepareStmt*(db: DbConn; q: string): PStmt =
when defined(debugOrminTrace):
echo "[[Ormin Executing]]: ", q
result.sql = q
template startBindings*(s: PStmt; n: int) {.dirty.} =
var pparams: seq[WireValue] = newSeq[WireValue](n)
template bindParam*(db: DbConn; s: PStmt; idx: int; x: untyped; t: untyped) =
when t is DateTime:
let xx = x.format("yyyy-MM-dd HH:mm:ss")
pparams[idx-1] = WireValue(kind: fkString, strVal: $xx)
elif t is int or t is int64:
pparams[idx-1] = WireValue(kind: fkInt64, int64Val: int64(x))
elif t is float or t is float64:
pparams[idx-1] = WireValue(kind: fkFloat64, float64Val: float64(x))
elif t is bool:
pparams[idx-1] = WireValue(kind: fkBool, boolVal: bool(x))
elif t is string:
pparams[idx-1] = WireValue(kind: fkString, strVal: string(x))
elif t is JsonNode:
pparams[idx-1] = WireValue(kind: fkJson, jsonVal: $x)
else:
pparams[idx-1] = WireValue(kind: fkString, strVal: $x)
template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
pparams[idx-1] = WireValue(kind: fkNull)
template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
t: typedesc) =
let x = xx
if x.kind == JNull:
pparams[idx-1] = WireValue(kind: fkNull)
else:
bindFromJson(db, s, idx, x, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc) =
{.error: "invalid type for JSON object".}
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[string]) =
doAssert x.kind == JString
pparams[idx-1] = WireValue(kind: fkString, strVal: x.str)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[int|int64]) =
doAssert x.kind == JInt
pparams[idx-1] = WireValue(kind: fkInt64, int64Val: x.num)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[float64]) =
doAssert x.kind == JFloat
pparams[idx-1] = WireValue(kind: fkFloat64, float64Val: x.fnum)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[bool]) =
doAssert x.kind == JBool
pparams[idx-1] = WireValue(kind: fkBool, boolVal: x.bval)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[DateTime]) =
doAssert x.kind == JString
pparams[idx-1] = WireValue(kind: fkString, strVal: x.str)
template startQuery*(db: DbConn; s: PStmt) =
when declared(pparams):
var queryResult {.inject.} = db.query(s.sql, pparams)
else:
var queryResult {.inject.} = db.query(s.sql)
var queryI {.inject.} = -1
var queryLen {.inject.} = queryResult.rowCount
template stopQuery*(db: DbConn; s: PStmt) =
discard
template stepQuery*(db: DbConn; s: PStmt; returnsData: bool): bool =
inc queryI
queryI < queryLen
template getLastId*(db: DbConn; s: PStmt): int =
0
template getAffectedRows*(db: DbConn; s: PStmt): int =
queryResult.affectedRows
proc close*(db: DbConn) =
client.close(db)
proc open*(connection, user, password, database: string): DbConn =
let colonPos = connection.find(':')
let host = if colonPos < 0: connection else: connection[0..<colonPos]
let portStr = if colonPos < 0: "9472" else: connection[colonPos+1..^1]
let port = parseInt(portStr)
let cfg = ClientConfig(
host: host,
port: port,
database: database,
username: user,
password: password
)
result = newSyncClient(cfg)
result.connect()
# --- Result binding helpers ---
template currentValue(s: PStmt; idx: int): string =
queryResult.rows[queryI][idx-1]
template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
currentValue(s, idx).len == 0
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int;
t: typedesc; name: string) =
dest = int(parseBiggestInt(currentValue(s, idx)))
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int64;
t: typedesc; name: string) =
dest = parseBiggestInt(currentValue(s, idx))
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: bool;
t: typedesc; name: string) =
let v = currentValue(s, idx)
dest = v == "true" or v == "1" or v == "t"
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
t: typedesc; name: string) =
dest = currentValue(s, idx)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
t: typedesc; name: string) =
dest = parseFloat(currentValue(s, idx))
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var DateTime;
t: typedesc; name: string) =
let src = currentValue(s, idx)
if src.len > 0:
dest = parse(src, "yyyy-MM-dd HH:mm:ss")
else:
dest = initDateTime(1, 1, 1, 0, 0, 0, utc())
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
t: typedesc; name: string) =
dest = parseJson(currentValue(s, idx))
template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
t: typedesc; name: string) =
let v = currentValue(s, idx)
if v.len == 0:
dest.isNull = true
else:
dest.isNull = false
when T is string:
dest.value = v
else:
bindResult(db, s, idx, dest.value, t, name)
template createJObject*(): untyped = newJObject()
template createJArray*(): untyped = newJArray()
template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
let x = obj
doAssert x.kind == JObject
let v = currentValue(s, idx)
if v.len == 0:
x[name] = newJNull()
else:
bindToJson(db, s, idx, x, t, name)
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
{.error: "invalid type for JSON object".}
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[string]; name: string) =
obj[name] = newJString(currentValue(s, idx))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[int|int64]; name: string) =
var v: int64
discard parseBiggestInt(currentValue(s, idx), v)
obj[name] = newJInt(v)
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[float64]; name: string) =
obj[name] = newJFloat(parseFloat(currentValue(s, idx)))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[bool]; name: string) =
let v = currentValue(s, idx)
obj[name] = newJBool(v == "true" or v == "1" or v == "t")
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[DateTime]; name: string) =
var dt: DateTime
bindResult(db, s, idx, dt, t, name)
obj[name] = newJString(format(dt, jsonTimeFormat))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[JsonNode]; name: string) =
obj[name] = parseJson(currentValue(s, idx))
+274
View File
@@ -0,0 +1,274 @@
import strutils, db_connector/postgres, json, times
import db_connector/db_common
import query_hooks
export db_common
type
DbConn* = PPGconn ## encapsulates a database connection
PStmt = string ## a identifier for the prepared queries
varcharType* = string
intType* = int
floatType* = float
boolType* = bool
timestampType* = Datetime
serialType* = int
jsonType* = JsonNode
var jsonTimeFormat* = "yyyy-MM-dd HH:mm:ss"
proc dbError*(db: DbConn) {.noreturn.} =
## raises a DbError exception.
var e: ref DbError
new(e)
e.msg = $pqErrorMessage(db)
raise e
proc c_strtod(buf: cstring, endptr: ptr cstring = nil): float64 {.
importc: "strtod", header: "<stdlib.h>", noSideEffect.}
proc c_strtol(buf: cstring, endptr: ptr cstring = nil, base: cint = 10): int {.
importc: "strtol", header: "<stdlib.h>", noSideEffect.}
var sid {.compileTime.}: int
proc prepareStmt*(db: DbConn; q: string): PStmt =
when defined(debugOrminTrace):
echo "[[Ormin Executing]]: ", q
inc sid
result = "ormin" & $sid
var res = pqprepare(db, result, q, 0, nil)
if pqResultStatus(res) != PGRES_COMMAND_OK: dbError(db)
template startBindings*(s: PStmt; n: int) {.dirty.} =
# pparams is a duplicated array to keep the Nim string alive
# for the duration of the query. This is safer than relying
# on the conservative stack marking:
var pparams: array[n, string]
var parr: array[n, cstring]
template bindParam*(db: DbConn; s: PStmt; idx: int; x: untyped; t: untyped) =
# when not (x is t):
# {.error: "type mismatch for query argument at position " & $idx.}
when t is DateTime:
let xx = x.format("yyyy-MM-dd HH:mm:ss\'.\'ffffffzzzz")
pparams[idx-1] = $xx
else:
pparams[idx-1] = $x
parr[idx-1] = cstring(pparams[idx-1])
template bindParamUnchecked(db: DbConn; s: PStmt; idx: int; x: untyped; t: untyped) =
pparams[idx-1] = $x
parr[idx-1] = cstring(pparams[idx-1])
template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
parr[idx-1] = cstring(nil)
template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
t: typedesc) =
let x = xx
if x.kind == JNull:
# a NULL entry is not reflected in the 'pparams' array:
parr[idx-1] = cstring(nil)
else:
bindFromJson(db, s, idx, x, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc) =
{.error: "invalid type for JSON object".}
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[string]) =
doAssert x.kind == JString
let xs = x.str
bindParamUnchecked(db, s, idx, xs, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[int|int64]) =
doAssert x.kind == JInt
let xi = x.num
bindParamUnchecked(db, s, idx, xi, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[float64]) =
doAssert x.kind == JFloat
let xf = x.fnum
bindParamUnchecked(db, s, idx, xf, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[bool]) =
doAssert x.kind == JBool
let xb = x.bval
bindParamUnchecked(db, s, idx, xb, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[DateTime]) =
doAssert x.kind == JString
let dt = x.str
bindParamUnchecked(db, s, idx, dt, t)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int;
t: typedesc; name: string) =
dest = c_strtol(pqgetvalue(queryResult, queryI, idx.cint))
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int64;
t: typedesc; name: string) =
dest = c_strtol(pqgetvalue(queryResult, queryI, idx.cint))
# XXX This needs testing:
template isTrue(x): untyped = x[0] == 't'
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: bool;
t: typedesc; name: string) =
dest = isTrue(pqgetvalue(queryResult, queryI, idx.cint))
proc fillString(dest: var string; src: cstring; srcLen: int) {.inline.} =
dest = ""
var i = 0
while src[i] != '\0':
dest.add src[i]
inc i
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
t: typedesc; name: string) =
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
when defined(nimNoNilSeqs):
setLen(dest, 0)
else:
dest = nil
else:
let src = pqgetvalue(queryResult, queryI, idx.cint)
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
fillString(dest, src, srcLen)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
t: typedesc; name: string) =
dest = c_strtod(pqgetvalue(queryResult, queryI, idx.cint))
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var DateTime;
t: typedesc; name: string) =
let
src = $pqgetvalue(queryResult, queryI, idx.cint)
i = src.find('.')
tzflag = src[src.len-3]
if i < 0:
if tzflag in {'+', '-'}:
dest = parse(src, "yyyy-MM-dd HH:mm:sszz")
else:
dest = parse(src, "yyyy-MM-dd HH:mm:ss")
else:
if tzflag in {'+', '-'}:
let itz = src.len - 3
let dtstr = src[0..<itz] & '0'.repeat(10-src.len+i) & src[src.len-3 .. ^1]
dest = parse(dtstr, "yyyy-MM-dd HH:mm:ss\'.\'ffffffzz")
else:
let dtstr = src & '0'.repeat(7-src.len+i)
dest = parse(dtstr, "yyyy-MM-dd HH:mm:ss\'.\'ffffff")
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
t: typedesc; name: string) =
dest = parseJson($pqgetvalue(queryResult, queryI, idx.cint))
template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
t: typedesc; name: string) =
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
dest.isNull = true
else:
dest.isNull = false
when T is string:
let src = pqgetvalue(queryResult, queryI, idx.cint)
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
fillString(dest.value, src, srcLen)
else:
bindResult(db, s, idx, dest.value, t, name)
template createJObject*(): untyped = newJObject()
template createJArray*(): untyped = newJArray()
template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
let x = obj
doAssert x.kind == JObject
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
x[name] = newJNull()
else:
bindToJson(db, s, idx, x, t, name)
template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
pqgetisnull(queryResult, queryI, idx.cint) != 0
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
{.error: "invalid type for JSON object".}
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[string]; name: string) =
let src = pqgetvalue(queryResult, queryI, idx.cint)
obj[name] = newJString($src)
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[int|int64]; name: string) =
obj[name] = newJInt(c_strtol(pqgetvalue(queryResult, queryI, idx.cint)))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[float64]; name: string) =
obj[name] = newJFloat(c_strtod(pqgetvalue(queryResult, queryI, idx.cint)))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[bool]; name: string) =
obj[name] = newJBool(isTrue(pqgetvalue(queryResult, queryI, idx.cint)))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[DateTime]; name: string) =
var dt: DateTime
bindResult(db, s, idx, dt, t, name)
obj[name] = newJString(format(dt, jsonTimeFormat))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[JsonNode]; name: string) =
let src = pqgetvalue(queryResult, queryI, idx.cint)
obj[name] = parseJson($src)
template startQuery*(db: DbConn; s: PStmt) =
when declared(pparams):
var queryResult {.inject.} = pqexecPrepared(db, s, int32(parr.len),
cast[cstringArray](addr parr), nil, nil, 0)
else:
var queryResult {.inject.} = pqexecPrepared(db, s, int32(0),
nil, nil, nil, 0)
if pqResultStatus(queryResult) == PGRES_COMMAND_OK:
discard # insert does not returns data in pg
elif pqResultStatus(queryResult) != PGRES_TUPLES_OK: ormin_postgre.dbError(db)
var queryI {.inject.} = cint(-1)
var queryLen {.inject.} = pqntuples(queryResult)
template stopQuery*(db: DbConn; s: PStmt) =
pqclear(queryResult)
template stepQuery*(db: DbConn; s: PStmt; returnsData: bool): bool =
inc queryI
queryI < queryLen
template getLastId*(db: DbConn; s: PStmt): int = 0 # XXX to implement
template getAffectedRows*(db: DbConn; s: PStmt): int =
c_strtol(pqcmdTuples(queryResult))
proc close*(db: DbConn) =
## closes the database connection.
if db != nil: pqfinish(db)
proc open*(connection, user, password, database: string): DbConn =
## opens a database connection. Raises `DbError` if the connection could not
## be established.
let
colonPos = connection.find(':')
host = if colonPos < 0: connection
else: substr(connection, 0, colonPos-1)
port = if colonPos < 0: ""
else: substr(connection, colonPos+1)
result = pqsetdbLogin(host, port, nil, nil, database, user, password)
if pqStatus(result) != CONNECTION_OK: dbError(result) # result = nil
+295
View File
@@ -0,0 +1,295 @@
{.deadCodeElim: on.}
import json, times
import db_connector/db_common
import db_connector/sqlite3
import query_hooks
export db_common
type
DbConn* = PSqlite3 ## encapsulates a database connection
varcharType* = string
blobType* = seq[byte]
intType* = int
floatType* = float
boolType* = bool
timestampType* = DateTime
jsonType* = JsonNode
var jsonTimeFormat* = "yyyy-MM-dd HH:mm:ss"
proc dbError*(db: DbConn) {.noreturn.} =
## raises a DbError exception.
var e: ref DbError
new(e)
e.msg = $sqlite3.errmsg(db)
raise e
proc prepareStmt*(db: DbConn; q: string): PStmt =
when defined(debugOrminTrace):
echo "[[Ormin Executing]]: ", q
if prepare_v2(db, q, q.len.cint, result, nil) != SQLITE_OK:
dbError(db)
template startBindings*(s: PStmt; n: int) =
if clear_bindings(s) != SQLITE_OK: dbError(db)
template bindParam*(db: DbConn; s: PStmt; idx: int; x, t: untyped) =
#when not (x is t):
# {.error: "type mismatch for query argument at position " & $idx.}
when t is blobType:
let xs = x
let blobPtr = if xs.len == 0:
cast[pointer](nil)
else:
cast[pointer](unsafeAddr(xs[0]))
if bind_blob(s, idx.cint, blobPtr, xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
elif t is int or t is int64 or t is bool:
if bind_int64(s, idx.cint, x.int64) != SQLITE_OK: dbError(db)
elif t is string:
if bind_text(s, idx.cint, cstring(x), x.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
elif t is float64:
if bind_double(s, idx.cint, x) != SQLITE_OK:
dbError(db)
elif t is DateTime:
let xx = if x.nanosecond > 0:
x.utc().format("yyyy-MM-dd HH:mm:ss\'.\'fff")
else:
x.utc().format("yyyy-MM-dd HH:mm:ss")
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
elif t is JsonNode:
let xx = $x
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
else:
{.error: "type mismatch for query argument at position " & $idx.}
template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
if bind_null(s, idx.cint) != SQLITE_OK:
dbError(db)
template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
t: typedesc) =
let x = xx
if x.kind == JNull:
if bind_null(s, idx.cint) != SQLITE_OK: dbError(db)
else:
bindFromJson(db, s, idx, x, t)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc) =
{.error: "invalid type for JSON object".}
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[string]) =
doAssert x.kind == JString
let xs = x.str
if bind_text(s, idx.cint, cstring(xs), xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[int|int64]) =
doAssert x.kind == JInt
let xi = x.num
if bind_int64(s, idx.cint, xi.int64) != SQLITE_OK: dbError(db)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[float64]) =
doAssert x.kind == JFloat
let xf = x.fnum
if bind_double(s, idx.cint, xf) != SQLITE_OK:
dbError(db)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[bool]) =
doAssert x.kind == JBool
let xb = x.bval
if bind_int64(s, idx.cint, xb.int64) != SQLITE_OK: dbError(db)
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[DateTime]) =
doAssert x.kind == JString
let
dtStr = x.str
i = dtStr.find('.')
dt = if i > 0 and dtStr[i+1 .. ^1] == "000":
dtStr[0 ..< i]
else:
dtStr
if bind_text(s, idx.cint, cstring(dt), dt.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int;
t: typedesc; name: string) =
dest = int column_int64(s, idx.cint)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int64;
t: typedesc; name: string) =
dest = column_int64(s, idx.cint)
proc fillString(dest: var string; src: cstring; srcLen: int) =
when defined(nimNoNilSeqs):
setLen(dest, srcLen)
else:
if dest.isNil: dest = newString(srcLen)
else: setLen(dest, srcLen)
if srcLen > 0:
copyMem(unsafeAddr(dest[0]), src, srcLen)
proc fillBytes(dest: var seq[byte]; src: pointer; srcLen: int) =
when defined(nimNoNilSeqs):
setLen(dest, srcLen)
else:
if dest.isNil: dest = newSeq[byte](srcLen)
else: setLen(dest, srcLen)
if srcLen > 0:
copyMem(unsafeAddr(dest[0]), src, srcLen)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
t: typedesc; name: string) =
if column_type(s, idx.cint) == SQLITE_NULL:
when defined(nimNoNilSeqs):
setLen(dest, 0)
else:
dest = nil
else:
let srcLen = column_bytes(s, idx.cint)
let src = column_text(s, idx.cint)
fillString(dest, src, srcLen)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var blobType;
t: typedesc; name: string) =
let srcLen = column_bytes(s, idx.cint)
let src = column_blob(s, idx.cint)
fillBytes(dest, src, srcLen)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
t: typedesc; name: string) =
dest = column_double(s, idx.cint)
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: bool;
t: typedesc; name: string) =
dest = column_int64(s, idx.cint) != 0
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: DateTime;
t: typedesc; name: string) =
let
src = $column_text(s, idx.cint)
i = src.find('.')
if i < 0:
dest = parse(src, "yyyy-MM-dd HH:mm:ss", utc())
else:
dest = parse(src, "yyyy-MM-dd HH:mm:ss\'.\'fff", utc())
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
t: typedesc; name: string) =
let src = column_text(s, idx.cint)
dest = parseJson($src)
template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
t: typedesc; name: string) =
if column_type(s, idx.cint) == SQLITE_NULL:
dest.isNull = true
else:
dest.isNull = false
when T is string:
let srcLen = column_bytes(s, idx.cint)
let src = column_text(s, idx.cint)
fillString(dest.value, src, srcLen)
else:
bindResult(db, s, idx, dest.value, t, name)
template createJObject*(): untyped = newJObject()
template createJArray*(): untyped = newJArray()
template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
let x = obj
doAssert x.kind == JObject
if column_type(s, idx.cint) == SQLITE_NULL:
x[name] = newJNull()
else:
bindToJson(db, s, idx, x, t, name)
template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
column_type(s, idx.cint) == SQLITE_NULL
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
{.error: "invalid type for JSON object".}
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[string]; name: string) =
let dest = newJString("")
let srcLen = column_bytes(s, idx.cint)
let src = column_text(s, idx.cint)
fillString(dest.str, src, srcLen)
obj[name] = dest
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[int|int64]; name: string) =
obj[name] = newJInt(column_int64(s, idx.cint))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[blobType]; name: string) =
let srcLen = column_bytes(s, idx.cint)
let src = column_blob(s, idx.cint)
var data: blobType
fillBytes(data, src, srcLen)
let arr = newJArray()
for b in data:
arr.add newJInt(int(b))
obj[name] = arr
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[float64]; name: string) =
obj[name] = newJFloat(column_double(s, idx.cint))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[bool]; name: string) =
obj[name] = newJBool(column_int64(s, idx.cint) != 0)
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[DateTime]; name: string) =
var dt: DateTime
bindResult(db, s, idx, dt, t, name)
obj[name] = newJString(format(dt, jsonTimeFormat))
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc[JsonNode]; name: string) =
let src = column_text(s, idx.cint)
obj[name] = parseJson($src)
template startQuery*(db: DbConn; s: PStmt) = discard "nothing to do"
template stopQuery*(db: DbConn; s: PStmt) =
if sqlite3.reset(s) != SQLITE_OK: dbError(db)
template stepQuery*(db: DbConn; s: PStmt; returnsData: bool): bool =
when returnsData:
step(s) == SQLITE_ROW
else:
step(s) == SQLITE_DONE
template getLastId*(db: DbConn; s: PStmt): int =
int(last_insert_rowid(db))
template getAffectedRows*(db: DbConn; s: PStmt): int =
int(changes(db))
proc close*(db: DbConn) =
## closes the database connection.
if sqlite3.close(db) != SQLITE_OK: dbError(db)
proc open*(connection, user, password, database: string): DbConn =
## opens a database connection. Raises `EDb` if the connection could not
## be established. Only the ``connection`` parameter is used for ``sqlite``.
if sqlite3.open(connection, result) != SQLITE_OK:
dbError(result)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
import options, json
type
DbValue*[T] = object
isNull*: bool
value*: T
template fromQueryHook*[T, S](val: var T, x: S) =
## Default conversion hook used by `query(T): ...`.
## Users can overload this proc to customize field/type conversions.
val = x
template toQueryHook*[T, S](val: var T, x: S) =
## Default conversion hook used for query parameters.
## Users can overload this proc to customize parameter conversions.
val = x
proc nullQueryValueError() {.noreturn.} =
raise newException(ValueError, "cannot map NULL query result")
proc fromQueryHook*[T, S](val: var Option[T], x: var DbValue[S]) =
if x.isNull:
val = none(T)
else:
var converted: T
fromQueryHook(converted, move x.value)
val = some(converted)
proc fromQueryHook*[T, S](val: var T, x: var DbValue[S]) =
if x.isNull:
when T is string:
val = ""
elif T is JsonNode:
val = newJNull()
else:
nullQueryValueError()
else:
fromQueryHook(val, move x.value)
proc bindFromQueryHook*[T, S](dest: var T, x: var DbValue[S]) =
fromQueryHook(dest, x)
proc toQueryHook*[S, T](val: var DbValue[S], x: Option[T]) =
if x.isSome:
val.isNull = false
toQueryHook(val.value, x.get)
else:
val.isNull = true
when compiles(val.value = default(S)):
val.value = default(S)
proc toQueryHook*[S, T](val: var DbValue[S], x: T) =
val.isNull = false
toQueryHook(val.value, x)
+139
View File
@@ -0,0 +1,139 @@
import asynchttpserver, asyncdispatch, asyncnet, json,
strutils, times
import websocket
type
Receivers* {.pure} = enum
sender, allExceptSender, all
ReqHandler* = proc (msg: JsonNode; receivers: var Receivers): JsonNode {.
closure, gcsafe.}
proc error(msg: string) = echo "[Error] ", msg
proc warn(msg: string) = echo "[Warning] ", msg
proc hint(msg: string) = echo "[Hint] ", msg
type
Client = ref object
socket: AsyncWebSocket
connected: bool
hostname: string
id: int
lastMessage: float
rapidMessageCount: int
Server = ref object
clients: seq[Client]
needsUpdate: bool
receivers: Receivers
msgs: seq[(string, int)]
gid: int
handler: ReqHandler
proc newClient(s: Server; socket: AsyncWebSocket, hostname: string): Client =
inc s.gid
result = Client(socket: socket, connected: true, hostname: hostname, id: s.gid)
proc `$`(client: Client): string =
"Client(ip: $1)" % [client.hostname]
proc updateClients(server: Server) {.async.} =
while true:
var needsUpdate = false
for client in server.clients:
if not client.connected:
needsUpdate = true
break
server.needsUpdate = server.needsUpdate or needsUpdate
if server.needsUpdate and server.msgs.len != 0:
var someDead = false
# perform a copy to prevent the race condition:
var msgs = server.msgs
setLen(server.msgs, 0)
for m in msgs:
for c in server.clients:
if c.connected:
# do not send a broadcast message to the one which sent it:
if server.receivers != Receivers.allExceptSender or c.id != m[1]:
await c.socket.sendText(m[0]) #, false)
else:
someDead = true
if someDead:
var i = 0
while i < server.clients.len:
if not server.clients[i].connected: del(server.clients, i)
else: inc i
server.needsUpdate = false
# let other stuff in the main loop run:
await sleepAsync(10)
proc processMessage(server: Server, client: Client, data: string) {.async.} =
# Check if last message was relatively recent. If so, kick the user.
echo "processMessage ", data
let now = epochTime()
if now - client.lastMessage < 0.1: # 100ms
client.rapidMessageCount.inc
else:
client.rapidMessageCount = 0
client.lastMessage = now
if client.rapidMessageCount > 50:
warn("Client ($1) is firing messages too rapidly. Killing." % $client)
client.connected = false
let msgj = parseJson(data)
server.receivers = Receivers.sender
if msgj.hasKey("msg") and msgj["msg"].getStr("") == "disconnect":
client.connected = false
server.needsUpdate = true
else:
let resp = server.handler(msgj, server.receivers)
if not resp.isNil:
if server.receivers == sender:
await client.socket.sendText($resp) #, false)
else:
server.msgs.add(($resp, client.id))
server.needsUpdate = true
proc processClient(server: Server, client: Client) {.async.} =
while client.connected:
var frameFut = client.socket.readData()
yield frameFut
if frameFut.failed:
error("Error occurred handling client messages.\n" &
frameFut.error.msg)
client.connected = false
break
let frame = frameFut.read()
if frame.opcode == Opcode.Text:
let processFut = processMessage(server, client, frame.data)
if processFut.failed:
echo processFut.error.getStackTrace()
error("Client ($1) attempted to send bad JSON? " % $client & "\n" &
processFut.error.msg)
#client.connected = false
await client.socket.close()
proc onRequest(server: Server, req: Request; key: string) {.async.} =
let (ws, error) = await verifyWebsocketRequest(req, key)
if ws != nil:
hint("Client connected from " & req.hostname)
ws.protocol = key
server.clients.add(newClient(server, ws, req.hostname))
asyncCheck processClient(server, server.clients[^1])
else:
warn("WS negotiation failed: " & $error)
await req.respond(Http400, "WebSocket negotiation failed: " & $error)
req.client.close()
proc serve*(key: string; handler: ReqHandler) =
let httpServer = newAsyncHttpServer()
let server = Server(clients: @[], msgs: @[], handler: handler, gid: 0)
proc cb(req: Request): Future[void] {.async, gcsafe.} =
await onRequest(server, req, key)
asyncCheck updateClients(server)
waitFor httpServer.serve(Port(8080), cb)
+55
View File
@@ -0,0 +1,55 @@
include system/inclrtl
import macros
proc transLastStmt(n, res, bracketExpr: NimNode): (NimNode, NimNode, NimNode) =
case n.kind
of nnkIfExpr, nnkIfStmt, nnkTryStmt, nnkCaseStmt:
result[0] = copyNimTree(n)
result[1] = copyNimTree(n)
result[2] = copyNimTree(n)
for i in ord(n.kind == nnkCaseStmt)..<n.len:
(result[0][i], result[1][^1], result[2][^1]) = transLastStmt(n[i], res, bracketExpr)
of nnkStmtList, nnkStmtListExpr, nnkBlockStmt, nnkBlockExpr, nnkWhileStmt,
nnkForStmt, nnkElifBranch, nnkElse, nnkElifExpr, nnkOfBranch, nnkExceptBranch:
result[0] = copyNimTree(n)
result[1] = copyNimTree(n)
result[2] = copyNimTree(n)
if n.len >= 1:
(result[0][^1], result[1][^1], result[2][^1]) = transLastStmt(n[^1], res, bracketExpr)
of nnkTableConstr:
result[1] = n[0][0]
result[2] = n[0][1]
if bracketExpr.len == 1:
bracketExpr.add([newCall(bindSym"typeof", newEmptyNode()), newCall(
bindSym"typeof", newEmptyNode())])
template adder(res, k, v) = res[k] = v
result[0] = getAst(adder(res, n[0][0], n[0][1]))
of nnkCurly:
result[2] = n[0]
if bracketExpr.len == 1:
bracketExpr.add(newCall(bindSym"typeof", newEmptyNode()))
template adder(res, v) = res.incl(v)
result[0] = getAst(adder(res, n[0]))
else:
result[2] = n
if bracketExpr.len == 1:
bracketExpr.add(newCall(bindSym"typeof", newEmptyNode()))
template adder(res, v) = res.add(v)
result[0] = getAst(adder(res, n))
macro collect*(init, body: untyped): untyped =
let res = genSym(nskVar, "collectResult")
expectKind init, {nnkCall, nnkIdent, nnkSym}
let bracketExpr = newTree(nnkBracketExpr,
if init.kind == nnkCall: init[0] else: init)
let (resBody, keyType, valueType) = transLastStmt(body, res, bracketExpr)
if bracketExpr.len == 3:
bracketExpr[1][1] = keyType
bracketExpr[2][1] = valueType
else:
bracketExpr[1][1] = valueType
let call = newTree(nnkCall, bracketExpr)
if init.kind == nnkCall:
for i in 1 ..< init.len:
call.add init[i]
result = newTree(nnkStmtListExpr, newVarStmt(res, call), resBody, res)
+1
View File
@@ -0,0 +1 @@
--path:"$projectDir/../../ormin"
@@ -0,0 +1,26 @@
-- lower case, upper case, and quoted table names
create table lower_table (
id integer primary key
);
CREATE TABLE UPPER_TABLE (
id integer primary key
);
-- std sql quoted table name
create table "Quoted Table" (
id integer primary key
);
-- sqlite quoted table name
create table `Quoted Table2` (
id integer primary key
);
create table "UPPER_QUOTED" (
id integer primary key
);
create table "A""B" (
id integer primary key
);
@@ -0,0 +1,60 @@
create table if not exists thread(
id serial primary key,
name varchar(100) not null,
views integer not null,
modified timestamp not null default CURRENT_TIMESTAMP
);
create unique index if not exists ThreadNameIx on thread (name);
create table if not exists person(
id serial primary key,
name varchar(20) not null,
password varchar(32) not null,
email varchar(30) not null,
creation timestamp not null default CURRENT_TIMESTAMP,
salt varchar(128) not null,
status varchar(30) not null,
lastOnline timestamp not null default CURRENT_TIMESTAMP,
ban varchar(128) not null default ''
);
create unique index if not exists UserNameIx on person (name);
create table if not exists post(
id serial primary key,
author integer not null,
ip varchar(20) not null,
header varchar(100) not null,
content varchar(1000) not null,
thread integer not null,
creation timestamp not null default CURRENT_TIMESTAMP,
foreign key (thread) references thread(id),
foreign key (author) references person(id)
);
create table if not exists session(
id serial primary key,
ip varchar(20) not null,
password varchar(32) not null,
userid integer not null,
lastModified timestamp not null default CURRENT_TIMESTAMP,
foreign key (userid) references person(id)
);
create table if not exists antibot(
id serial primary key,
ip varchar(20) not null,
answer varchar(30) not null,
created timestamp not null default CURRENT_TIMESTAMP
);
create table if not exists error(
uuid varchar(36) primary key,
message varchar(1000) not null,
created timestamp not null default CURRENT_TIMESTAMP
);
create index PersonStatusIdx on person(status);
create index PostByAuthorIdx on post(thread, author);
@@ -0,0 +1,62 @@
pragma foreign_keys = on;
create table if not exists thread(
id integer primary key,
name varchar(100) not null,
views integer not null,
modified timestamp not null default (DATETIME('now'))
);
create unique index if not exists ThreadNameIx on thread (name);
create table if not exists person(
id integer primary key,
name varchar(20) not null,
password varchar(32) not null,
email varchar(30) not null,
creation timestamp not null default (DATETIME('now')),
salt varchar(128) not null,
status varchar(30) not null,
lastOnline timestamp not null default (DATETIME('now')),
ban varchar(128) not null default ''
);
create unique index if not exists UserNameIx on person (name);
create table if not exists post(
id integer primary key,
author integer not null,
ip varchar(20) not null,
header varchar(100) not null,
content varchar(1000) not null,
thread integer not null,
creation timestamp not null default (DATETIME('now')),
foreign key (thread) references thread(id),
foreign key (author) references person(id)
);
create table if not exists session(
id integer primary key,
ip varchar(20) not null,
password varchar(32) not null,
userid integer not null,
lastModified timestamp not null default (DATETIME('now')),
foreign key (userid) references person(id)
);
create table if not exists antibot(
id integer primary key,
ip varchar(20) not null,
answer varchar(30) not null,
created timestamp not null default (DATETIME('now'))
);
create table if not exists error(
uuid varchar(36) primary key,
message varchar(1000) not null,
created timestamp not null default (DATETIME('now'))
);
create index PersonStatusIdx on person(status);
create index PostByAuthorIdx on post(thread, author);
+45
View File
@@ -0,0 +1,45 @@
create table if not exists tb_serial(
typserial serial primary key,
typinteger integer not null
);
create table if not exists tb_boolean(
typboolean boolean not null
);
create table if not exists tb_float(
typfloat real not null
);
create table if not exists tb_string(
typstring text not null
);
create table if not exists tb_timestamp(
dt timestamp not null,
dtn timestamptz not null,
dtz timestamptz not null
);
create table if not exists tb_json(
typjson json not null
);
create table if not exists tb_composite_pk(
pk1 integer not null,
pk2 integer not null,
message text not null,
primary key (pk1, pk2)
);
create table if not exists tb_composite_fk(
id integer not null,
fk1 integer not null,
fk2 integer not null,
foreign key (fk1, fk2) references tb_composite_pk(pk1, pk2)
);
create table if not exists tb_nullable(
id integer primary key,
note text null
);
+48
View File
@@ -0,0 +1,48 @@
create table if not exists tb_serial(
typserial integer primary key,
typinteger integer not null
);
create table if not exists tb_boolean(
typboolean boolean not null
);
create table if not exists tb_float(
typfloat real not null
);
create table if not exists tb_string(
typstring text not null
);
create table if not exists tb_timestamp(
dt1 timestamp not null,
dt2 timestamp not null
);
create table if not exists tb_json(
typjson json not null
);
create table if not exists tb_composite_pk(
pk1 integer not null,
pk2 integer not null,
message text not null,
primary key (pk1, pk2)
);
create table if not exists tb_composite_fk(
id integer not null,
fk1 integer not null,
fk2 integer not null,
foreign key (fk1, fk2) references tb_composite_pk(pk1, pk2)
);
create table if not exists tb_blob(
id integer primary key,
typblob blob not null
);
create table if not exists tb_nullable(
id integer primary key,
note text null
);
@@ -0,0 +1,4 @@
create table user_static (
id integer primary key,
name varchar(255) not null
);
+508
View File
@@ -0,0 +1,508 @@
import unittest, json, strutils, strformat, sequtils, macros, times, os, math, unicode
import ormin
import ormin/db_utils
when defined(postgre):
when defined(macosx):
{.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
import db_connector/db_postgres as db_postgres
const backend = DbBackend.postgre
importModel(backend, "model_postgre")
const sqlFileName = "model_postgre.sql"
let db {.global.} = db_postgres.open("localhost", "test", "test", "test_ormin")
else:
from db_connector/db_sqlite import exec, getValue
const backend = DbBackend.sqlite
importModel(backend, "model_sqlite")
const sqlFileName = "model_sqlite.sql"
# let db {.global.} = open(":memory:", "", "", "")
let db {.global.} = open("test.db", "", "", "")
let
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / sqlFileName)
proc substr(s: string; start, length: int): string {.importSql.}
let
min = -3
max = 3
serial_int_seqs = [(1, min), (2, 1), (3, 2), (4, max)]
suite &"Test common database types and functions of {backend}":
discard
suite "serial_insert":
setup:
db.dropTable(sqlFile, "tb_serial")
db.createTable(sqlFile, "tb_serial")
test "insert":
for i in serial_int_seqs:
query:
insert tb_serial(typinteger = ?i[1])
check db.getValue(sql"select count(*) from tb_serial") == $serial_int_seqs.len
test "json":
for i in serial_int_seqs:
let v = %*{"typinteger": i[1]}
query:
insert tb_serial(typinteger = %v["typinteger"])
check db.getValue(sql"select count(*) from tb_serial") == $serial_int_seqs.len
suite "serial":
db.dropTable(sqlFile, "tb_serial")
db.createTable(sqlFile, "tb_serial")
let insertSql = sql"insert into tb_serial(typinteger) values (?)"
for (_, v) in serial_int_seqs:
db.exec(insertSql, v)
doAssert db.getValue(sql"select count(*) from tb_serial") == $serial_int_seqs.len
test "query":
let res = query:
select tb_serial(typserial, typinteger)
check res == serial_int_seqs
test "where":
let
id = 1
res = query:
select tb_serial(typserial, typinteger)
where typserial == ?id
check res == serial_int_seqs.filterIt(it[0] == id)
test "json":
let res = query:
select tb_serial(typserial, typinteger)
produce json
check res == %*serial_int_seqs.mapIt(%*{"typserial": it[0], "typinteger": it[1]})
test "count":
let res = query:
select tb_serial(count(_))
check res[0] == serial_int_seqs.len
test "sum":
let res = query:
select tb_serial(sum(typinteger))
check res[0] == serial_int_seqs.mapIt(it[1]).sum()
test "avg":
let res = query:
select tb_serial(avg(typinteger))
check res[0] == serial_int_seqs.mapIt(it[1]).sum() / serial_int_seqs.len()
test "min":
let res = query:
select tb_serial(min(typinteger))
check res[0] == min
test "max":
let res = query:
select tb_serial(max(typinteger))
check res[0] == max
test "abs":
let res = query:
select tb_serial(abs(typinteger))
check res == serial_int_seqs.mapIt(abs(it[1]))
let seqs = toSeq(1..3)
suite "boolean_insert":
setup:
db.dropTable(sqlFile, "tb_boolean")
db.createTable(sqlFile, "tb_boolean")
test "insert":
for i in seqs:
let b = (i mod 2 == 0)
query:
insert tb_boolean(typboolean = ?b)
check db.getValue(sql"select count(*) from tb_boolean") == $seqs.len
test "json":
for i in seqs:
let b = %*{"typboolean": (i mod 2 == 0)}
query:
insert tb_boolean(typboolean = %b["typboolean"])
check db.getValue(sql"select count(*) from tb_boolean") == $seqs.len
suite "boolean":
db.dropTable(sqlFile, "tb_boolean")
db.createTable(sqlFile, "tb_boolean")
proc toBool(x: int): bool =
if x mod 2 == 0: true else: false
proc toDbValue(x: int): auto =
when defined(postgre):
if x mod 2 == 0: "t" else: "f"
else:
if x mod 2 == 0: 1 else: 0
let insertSql = sql"insert into tb_boolean(typboolean) values (?)"
for i in seqs:
db.exec(insertSql, toDbValue(i))
doAssert db.getValue(sql"select count(*) from tb_boolean") == $seqs.len
test "query":
let res = query:
select tb_boolean(typboolean)
check res == seqs.mapIt(it mod 2 == 0)
test "where":
let
b = true
res = query:
select tb_boolean(typboolean)
where typboolean == ?b
check res == seqs.map(toBool).filterIt(it == b)
test "json":
let res = query:
select tb_boolean(typboolean)
produce json
check res == %*seqs.mapIt(%*{"typboolean": toBool(it)})
when defined(sqlite):
test "importSql substr wrong type":
## TODO: should this be allowed?
let res = query:
select tb_boolean(substr(typboolean, 1, 2))
echo "res: ", res
let fs = [-3.14, 2.56, 10.45]
suite "float_insert":
setup:
db.dropTable(sqlFile, "tb_float")
db.createTable(sqlFile, "tb_float")
test "insert":
for f in fs:
query:
insert tb_float(typfloat = ?f)
check db.getValue(sql"select count(*) from tb_float") == $fs.len
test "json":
for f in fs:
let v = %*{"typfloat": f}
query:
insert tb_float(typfloat = %v["typfloat"])
check db.getValue(sql"select count(*) from tb_float") == $fs.len
suite "float":
db.dropTable(sqlFile, "tb_float")
db.createTable(sqlFile, "tb_float")
let insertSql = sql"insert into tb_float(typfloat) values (?)"
for f in fs:
db.exec(insertSql, f)
doAssert db.getValue(sql"select count(*) from tb_float") == $fs.len
test "query":
let res = query:
select tb_float(typfloat)
check res == fs
test "where":
let res = query:
select tb_float(typfloat)
where typfloat == ?fs[0]
check res == [fs[0]]
test "json":
let res = query:
select tb_float(typfloat)
produce json
check res == %*fs.mapIt(%*{"typfloat": it})
test "abs":
let res = query:
select tb_float(abs(typfloat))
check res == fs.mapIt(abs(it))
let ss = ["one", "Two", "three", "第四", "four'th"]
suite "string_insert":
setup:
db.dropTable(sqlFile, "tb_string")
db.createTable(sqlFile, "tb_string")
test "insert":
for v in ss:
query:
insert tb_string(typstring = ?v)
check db.getValue(sql"select count(*) from tb_string") == $ss.len
test "insert":
for v in ss:
query:
insert tb_string(typstring = "can't touch this")
check db.getValue(sql"select count(*) from tb_string") == $ss.len
test "json":
for v in ss:
let j = %*{"typstring": v}
query:
insert tb_string(typstring = %j["typstring"])
check db.getValue(sql"select count(*) from tb_string") == $ss.len
suite "string":
db.dropTable(sqlFile, "tb_string")
db.createTable(sqlFile, "tb_string")
let insertsql = sql"insert into tb_string(typstring) values (?)"
for v in ss:
db.exec(insertsql, v)
doAssert db.getValue(sql"select count(*) from tb_string") == $ss.len
test "query":
let res = query:
select tb_string(typstring)
check res == ss
test "where":
let res = query:
select tb_string(typstring)
where typstring == ?ss[0]
check res[0] == ss[0]
test "json":
let res = query:
select tb_string(typstring)
produce json
check res == %*ss.mapIt(%*{"typstring": it})
test "concat_op":
let
s = "typstring: "
let res = query:
select tb_string(?s & typstring)
check res == ss.mapIt(s & it)
test "length":
let res = query:
select tb_string(length(typstring))
check res == ss.mapIt(it.runeLen)
test "lower":
let res = query:
select tb_string(lower(typstring))
check res == ss.mapIt(it.toLowerAscii)
test "upper":
let res = query:
select tb_string(upper(typstring))
check res == ss.mapIt(it.toUpperAscii)
test "replace":
let res = query:
select tb_string(replace(typstring, "e", "o"))
check res == ss.mapIt(it.replace("e", "o"))
test "importSql substr length 0":
let res = query:
select tb_string(substr(typstring, 1, 0))
let expected = ss.mapIt($(toRunes(it)[0..<0]))
check res == expected
test "importSql substr length no arg types checked":
# TODO: fixme!
# the `functions` array only contains the types of the return
# but sqlite doesn't really care...
let res = query:
select tb_string(substr(typstring, "1", 2))
let expected = ss.mapIt($(toRunes(it)[0..<2]))
check res == expected
let js = [
%*{"name": "tom", "age": 30},
%*{"name": "bob", "age": 35},
%*{"name": "jack", "age": 23}
]
suite "insert_json":
setup:
db.dropTable(sqlFile, "tb_json")
db.createTable(sqlFile, "tb_json")
test "insert":
for j in js:
query:
insert tb_json(typjson = ?j)
check db.getValue(sql"select count(*) from tb_json") == $js.len
suite "json":
db.dropTable(sqlFile, "tb_json")
db.createTable(sqlFile, "tb_json")
let insertSql = sql"insert into tb_json(typjson) values (?)"
for j in js:
db.exec(insertSql, j)
doAssert db.getValue(sql"select count(*) from tb_json") == $js.len
test "query":
let res = query:
select tb_json(typjson)
check res == js
test "json":
let res = query:
select tb_json(typjson)
produce json
check res == %*js.mapIt(%*{"typjson": it})
let cps = [
(1, 1, "one-one"),
(1, 2, "one-two"),
(2, 1, "two-one")
]
suite "composite_pk_insert":
setup:
db.dropTable(sqlFile, "tb_composite_pk")
db.createTable(sqlFile, "tb_composite_pk")
test "insert":
for r in cps:
query:
insert tb_composite_pk(pk1 = ?r[0], pk2 = ?r[1], message = ?r[2])
check db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len
test "json":
for r in cps:
let v = %*{"pk1": r[0], "pk2": r[1], "message": r[2]}
query:
insert tb_composite_pk(pk1 = %v["pk1"], pk2 = %v["pk2"], message = %v["message"])
check db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len
suite "composite_pk":
db.dropTable(sqlFile, "tb_composite_pk")
db.createTable(sqlFile, "tb_composite_pk")
let insertSql = sql"insert into tb_composite_pk(pk1, pk2, message) values (?, ?, ?)"
for r in cps:
db.exec(insertSql, r[0], r[1], r[2])
doAssert db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len
test "query":
let res = query:
select tb_composite_pk(pk1, pk2, message)
check res == cps
test "where":
let res = query:
select tb_composite_pk(pk1, pk2, message)
where pk1 == ?cps[0][0]
check res == cps.filterIt(it[0] == cps[0][0])
test "json":
let res = query:
select tb_composite_pk(pk1, pk2, message)
produce json
check res == %*cps.mapIt(%*{"pk1": it[0], "pk2": it[1], "message": it[2]})
suite "nullable":
setup:
db.dropTable(sqlFile, "tb_nullable")
db.createTable(sqlFile, "tb_nullable")
query:
insert tb_nullable(id = 1, note = nil)
query:
insert tb_nullable(id = 2, note = "hello")
test "where_null":
let res = query:
select tb_nullable(id)
where note == nil
check res == @[1]
test "where_not_null":
let res = query:
select tb_nullable(id)
where note != null
check res == @[2]
test "update_to_null":
query:
update tb_nullable(note = null)
where id == 2
let res = query:
select tb_nullable(id)
where note == nil
check res == @[1, 2]
test "json_null":
let res = query:
select tb_nullable(id, note)
orderby id
produce json
check res[0]["note"].kind == JNull
check res[1]["note"].getStr == "hello"
suite "upsert":
setup:
db.dropTable(sqlFile, "tb_nullable")
db.createTable(sqlFile, "tb_nullable")
test "onconflict do nothing":
let first = "first value"
query:
insert tb_nullable(id = 1, note = ?first)
let ignored = "should not overwrite"
query:
insert tb_nullable(id = 1, note = ?ignored)
onconflict(id)
donothing()
let note = db.getValue(sql"select note from tb_nullable where id = 1")
check note == first
test "onconflict do update":
let first = "old note"
query:
insert tb_nullable(id = 2, note = ?first)
let replacement = "new note"
query:
insert tb_nullable(id = 2, note = ?replacement)
onconflict(id)
doupdate(note = ?replacement)
let note = db.getValue(sql"select note from tb_nullable where id = 2")
check note == replacement
test "onconflict do update where":
query:
insert tb_nullable(id = 3, note = "keep-me")
query:
insert tb_nullable(id = 3, note = "replace-me")
onconflict(id)
doupdate(note = "replace-me")
where note != "keep-me"
let note = db.getValue(sql"select note from tb_nullable where id = 3")
check note == "keep-me"
static:
doAssert not compiles(
(block:
query:
insert tb_nullable(id = 100, note = "x")
where id == 100
)
)
+166
View File
@@ -0,0 +1,166 @@
import unittest, os, sequtils
import db_connector/db_common
from db_connector/db_sqlite import open, exec, getValue
import ormin/db_utils
const usesLegacySqliteDropNames = defined(sqlite) and defined(orminLegacySqliteDropNames)
let
db {.global.} = open(":memory:", "", "", "")
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / "db_utils_case_quoted.sql")
let sqlContent = """
-- lower case, upper case, and quoted table names
create table lower_table (
id integer primary key
);
CREATE TABLE UPPER_TABLE (
id integer primary key
);
-- std sql quoted table name
create table "Quoted Table" (
id integer primary key
);
-- sqlite quoted table name
create table `Quoted Table2` (
id integer primary key
);
create table "UPPER_QUOTED" (
id integer primary key
);
create table "A""B" (
id integer primary key
);
"""
const staticSqlContent = staticLoad("db_utils_case_quoted.sql")
writeFile($sqlFile, sqlContent)
suite "db_utils: case and quoted names":
test "staticLoad returns typed compile-time SQL":
let loaded: DbSql = staticSqlContent
check $loaded == sqlContent
test "quoteDbIdentifier quotes and escapes identifiers":
check $quoteDbIdentifier("lower_table") == "lower_table"
check $quoteDbIdentifier("UPPER_TABLE") == "upper_table"
check $quoteDbIdentifier("\"Quoted Table\"") == "\"Quoted Table\""
check $quoteDbIdentifier("`Quoted Table2`") == "\"Quoted Table2\""
check $quoteDbIdentifier("'Quoted Table3'") == "\"Quoted Table3\""
check $quoteDbIdentifier("\"UPPER_TABLE\"") == "\"UPPER_TABLE\""
check $quoteDbIdentifier("\"A\"\"B\"") == "\"A\"\"B\""
check $quoteDbIdentifier("schema.table") == "schema.table"
check $quoteDbIdentifier("Schema.\"Mixed Table\"") == "schema.\"Mixed Table\""
check $quoteDbIdentifier("weird\"name") == "\"weird\"\"name\""
test "check tables names":
let pairs = tablePairs(sqlContent).toSeq()
check pairs.len == 6
check pairs[0][0] == ("lower_table")
check pairs[1][0] == ("upper_table")
check pairs[2][0] == ("quoted table")
check pairs[3][0] == ("quoted table2")
check pairs[4][0] == ("upper_quoted")
check pairs[5][0] == ("a\"b")
check pairs[0][1] == "create table lower_table(id integer primary key );"
check pairs[1][1] == "create table UPPER_TABLE(id integer primary key );"
check pairs[2][1] == "create table \"Quoted Table\"(id integer primary key );"
check pairs[4][1] == "create table \"UPPER_QUOTED\"(id integer primary key );"
check pairs[5][1] == "create table \"A\"\"B\"(id integer primary key );"
test "tablePairs parses compile-time SQL":
let pairs = tablePairs(staticSqlContent).toSeq()
check pairs.len == 6
check pairs[0][0] == "lower_table"
check pairs[1][0] == "upper_table"
check pairs[2][0] == "quoted table"
check pairs[3][0] == "quoted table2"
check pairs[4][0] == "upper_quoted"
check pairs[5][0] == "a\"b"
test "createTable creates all tables from SQL file":
db.createTable(sqlFile)
let countAll = db.getValue(sql"select count(*) from sqlite_master where type='table' and name in ('lower_table','UPPER_TABLE','Quoted Table')")
check countAll == "3"
test "createTable with specific lowercased name matches quoted":
# Use a new in-memory DB for isolation
let db2 = open(":memory:", "", "", "")
db2.createTable(sqlFile, "quoted table")
let countQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'Quoted Table'")
check countQuoted == "1"
test "createTable creates all tables from compile-time SQL":
let db2 = open(":memory:", "", "", "")
db2.createTable(staticSqlContent)
let countAll = db2.getValue(sql("select count(*) from sqlite_master where type='table' and name in ('lower_table','UPPER_TABLE','Quoted Table','Quoted Table2','UPPER_QUOTED','A\"B')"))
check countAll == "6"
test "createTable with specific lowercased name matches quoted from compile-time SQL":
let db2 = open(":memory:", "", "", "")
db2.createTable(staticSqlContent, "quoted table")
let countQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'Quoted Table'")
check countQuoted == "1"
test "dropTable handles quoted names from SQL file":
let db2 = open(":memory:", "", "", "")
db2.createTable(sqlFile)
db2.dropTable(sqlFile, "quoted table2")
let countQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'Quoted Table2'")
when usesLegacySqliteDropNames:
check countQuoted == "1"
else:
check countQuoted == "0"
db2.dropTable(sqlFile, "upper_quoted")
let countUpperQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'UPPER_QUOTED'")
when usesLegacySqliteDropNames:
check countUpperQuoted == "1"
else:
check countUpperQuoted == "0"
db2.dropTable(sqlFile, "a\"b")
let countEscapedQuoted = db2.getValue(sql("select count(*) from sqlite_master where type='table' and name = 'A\"B'"))
when usesLegacySqliteDropNames:
check countEscapedQuoted == "1"
else:
check countEscapedQuoted == "0"
test "dropTable removes tables from compile-time SQL":
let db2 = open(":memory:", "", "", "")
db2.createTable(staticSqlContent)
db2.dropTable(staticSqlContent, "quoted table2")
let countQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'Quoted Table2'")
when usesLegacySqliteDropNames:
check countQuoted == "1"
else:
check countQuoted == "0"
db2.dropTable(staticSqlContent, "upper_quoted")
let countUpperQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'UPPER_QUOTED'")
when usesLegacySqliteDropNames:
check countUpperQuoted == "1"
else:
check countUpperQuoted == "0"
db2.dropTable(staticSqlContent, "a\"b")
let countEscapedQuoted = db2.getValue(sql("select count(*) from sqlite_master where type='table' and name = 'A\"B'"))
when usesLegacySqliteDropNames:
check countEscapedQuoted == "1"
else:
check countEscapedQuoted == "0"
db2.dropTable(staticSqlContent)
let countAll = db2.getValue(sql("select count(*) from sqlite_master where type='table' and name in ('lower_table','UPPER_TABLE','Quoted Table','Quoted Table2','UPPER_QUOTED','A\"B')"))
when usesLegacySqliteDropNames:
check countAll == "3"
else:
check countAll == "0"
+822
View File
@@ -0,0 +1,822 @@
import unittest, strformat, sequtils, algorithm, sugar, json, tables, random
import os
import ormin
import ormin/db_utils
when NimVersion < "1.2.0": import ./compat
let testDir = currentSourcePath.parentDir()
when defined postgre:
when defined(macosx):
{.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
from db_connector/db_postgres import exec, getValue
const backend = DbBackend.postgre
importModel(backend, "forum_model_postgres")
const sqlFileName = "forum_model_postgres.sql"
let db {.global.} = open("localhost", "test", "test", "test_ormin")
else:
from db_connector/db_sqlite import exec, getValue
const backend = DbBackend.sqlite
importModel(backend, "forum_model_sqlite")
const sqlFileName = "forum_model_sqlite.sql"
var memoryPath = testDir & "/" & ":memory:"
let db {.global.} = open(memoryPath, "", "", "")
var sqlFilePath = Path(testDir & "/" & sqlFileName)
type
Person = tuple[id: int,
name: string,
password: string,
email: string,
salt: string,
status: string]
Thread = tuple[id: int,
name: string,
views: int]
Post = tuple[id: int,
author: int,
ip: string,
header: string,
content: string,
thread: int]
Antibot = tuple[id: int,
ip: string,
answer: string]
const
personcount = 5
threadcount = 5
postcount = 10
antibotcount = 5
var
persondata: seq[Person]
threaddata: seq[Thread]
postdata: seq[Post]
antibotdata: seq[Antibot]
suite &"Test ormin features of {backend}":
discard
suite "query":
db.dropTable(sqlFilePath)
db.createTable(sqlFilePath)
static:
doAssert compiles(block:
discard query:
select post(author)
crossjoin person(name)
)
doAssert compiles(block:
discard query:
select post(author)
outerjoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
leftjoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
leftouterjoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
rightjoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
rightouterjoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
fulljoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
fullouterjoin person(name) on author == id
)
# prepare data to insert
for i in 1..personcount:
persondata.add((id: i,
name: fmt"john{i}",
password: fmt"pass{i}",
email: fmt"john{i}@mail.com",
salt: fmt"abcd{i}",
status: fmt"ok{i}"))
for i in 1..threadcount:
threaddata.add((id: i,
name: fmt"thread{i}",
views: i))
for i in 1..postcount:
postdata.add((id: i,
author: sample({1..personcount}),
ip: "",
header: fmt"title{i}",
content: fmt"content{i}",
thread: sample({1..threadcount})))
for i in 1..antibotcount:
antibotdata.add((id: i,
ip: "",
answer: fmt"answer{i}"))
# insert data into database
let
insertperson = sql"insert into person (id, name, password, email, salt, status) values (?, ?, ?, ?, ?, ?)"
insertthread = sql"insert into thread (id, name, views) values (?, ?, ?)"
insertpost = sql"insert into post (id, author, ip, header, content, thread) values (?, ?, ?, ?, ?, ?)"
insertantibot = sql"insert into antibot (id, ip, answer) values (?, ?, ?)"
for p in persondata:
db.exec(insertperson, p.id, p.name, p.password, p.email, p.salt, p.status)
for t in threaddata:
db.exec(insertthread, t.id, t.name, t.views)
for p in postdata:
db.exec(insertpost, p.id, p.author, p.ip, p.header, p.content, p.thread)
for a in antibotdata:
db.exec(insertantibot, a.id, a.ip, a.answer)
# check data in database
let personexpected = db.getValue(sql"select count(*) from person")
assert personexpected == $personcount
let threadexpected = db.getValue(sql"select count(*) from thread")
assert threadexpected == $threadcount
let postexpected = db.getValue(sql"select count(*) from post")
assert postexpected == $postcount
let antibotexpected = db.getValue(sql"select count(*) from antibot")
assert antibotexpected == $antibotcount
test "table":
let res = query:
select person(id, name, password, email, salt, status)
check res == persondata
test "all_column":
let res = query:
select person(_)
check res.mapIt((it.id, it.name, it.password, it.email, it.salt, it.status)) == persondata
test "column_alias":
let res = query:
select person(id as personid, name as personname)
check res == persondata.mapIt((personid: it.id, personname: it.name))
test "arithmetic":
let res = query:
select person(id * 4 / 2 + 2 - 1 as id)
check res == persondata.mapIt(int(it.id * 4 / 2 + 2 - 1))
test "comparison":
let id = 1
let res = query:
select person(id, name, password, email, salt, status)
where id == ?id
check res == [persondata[id - 1]]
test "comparison_ne":
let id = 1
let res = query:
select person(id, name, password, email, salt, status)
where id != ?id
check res == persondata.filterIt(it.id != id)
test "comparison_ge":
let id = 3
let res = query:
select person(id, name, password, email, salt, status)
where id >= ?id
check res == persondata.filterIt(it.id >= id)
test "comparison_le":
let id = 3
let res = query:
select person(id, name, password, email, salt, status)
where id <= ?id
check res == persondata.filterIt(it.id <= id)
test "comparison_gt":
let id = 3
let res = query:
select person(id, name, password, email, salt, status)
where id > ?id
check res == persondata.filterIt(it.id > id)
test "comparison_lt":
let id = 3
let res = query:
select person(id, name, password, email, salt, status)
where id < ?id
check res == persondata.filterIt(it.id < id)
test "logical_not":
let id = 3
let res = query:
select person(id, name, password, email, salt, status)
where not (id > ?id)
check res == persondata.filterIt(it.id <= id)
test "logical_and":
let
id1 = 2
id2 = 4
let res = query:
select person(id, name, password, email, salt, status)
where id >= ?id1 and id <= ?id2
check res == persondata.filterIt(it.id >= id1 and it.id <= id2)
test "logical_or":
let
id1 = 2
id2 = 4
let res = query:
select person(id, name, password, email, salt, status)
where id == ?id1 or id == ?id2
check res == persondata.filterIt(it.id == id1 or it.id == id2)
test "logical_complex":
let
id1 = 2
id2 = 3
id3 = 4
let res = query:
select person(id, name, password, email, salt, status)
where id > ?id2 and (id == ?id1 or id == ?id3)
check res == persondata.filterIt(it.id == id3)
test "predicate_in":
let
id1 = 2
id2 = 4
let res = query:
select person(id, name, password, email, salt, status)
where id in ?id1 .. ?id2
check res == persondata.filterIt(it.id in id1..id2)
test "predicate_not_in":
let
id1 = 2
id2 = 4
let res = query:
select person(id, name, password, email, salt, status)
where id notin ?id1 .. ?id2
check res == persondata.filterIt(it.id < id1 or it.id > id2)
test "predicate_like":
let pattern = "john1%"
let res = query:
select person(id, name)
where name `like` ?pattern
check res == persondata.filterIt(it.name.startsWith("john1")).mapIt((it.id, it.name))
test "predicate_ilike":
let pattern = "JOHN1%"
let res = query:
select person(id, name)
where name `ilike` ?pattern
check res == persondata.filterIt(it.name.startsWith("john1")).mapIt((it.id, it.name))
test "predicate_not_like":
let pattern = "john1%"
let res = query:
select person(id, name)
where not (name `like` ?pattern)
check res == persondata.filterIt(not it.name.startsWith("john1")).mapIt((it.id, it.name))
test "distinct":
var res = query:
select `distinct` post(author)
res.sort()
let expected = postdata.mapIt(it.author).deduplicate().sortedByIt(it)
check res == expected
test "count_distinct":
let res = query:
select post(count(distinct author))
check res == @[postdata.mapIt(it.author).deduplicate().len]
test "window_row_number":
let res = query:
select post(author, id, over(row_number(), partitionby(author), orderby(id)) as rn)
orderby author, id
var expected: seq[(int, int, int)] = @[]
var counters = initCountTable[int]()
for p in postdata.sortedByIt((it.author, it.id)):
counters.inc(p.author)
expected.add((p.author, p.id, counters[p.author]))
check res == expected
test "window_running_sum":
let res = query:
select post(author, id, over(sum(id), partitionby(author), orderby(id)) as running)
orderby author, id
var expected: seq[(int, int, int)] = @[]
var sums = initTable[int, int]()
for p in postdata.sortedByIt((it.author, it.id)):
sums[p.author] = sums.getOrDefault(p.author) + p.id
expected.add((p.author, p.id, sums[p.author]))
check res == expected
test "limit":
let id = 1
let res = query:
select person(id, name, password, email, salt, status)
limit 1
static:
echo "LIMIT TEST: ", typeof(res)
check res == persondata[id - 1]
test "offset":
let res = query:
select person(id, name)
limit 2
offset 2
check res == persondata[2..3].mapIt((it.id, it.name))
test "match_assignment":
let id = 1
let (name, password, email, salt, status) = query:
select person(name, password, email, salt, status)
where id == ?id
limit 1
check:
name == persondata[id - 1].name
password == persondata[id - 1].password
email == persondata[id - 1].email
salt == persondata[id - 1].salt
status == persondata[id - 1].status
test "groupby":
let res = query:
select post(author, count(id))
groupby author
let counttable = postdata.mapIt(it.author).toCountTable()
for (author, c) in res:
check counttable[author] == c
test "groupby_aggregate":
let author = 3
let res = query:
select post(count(id))
where author == ?author
groupby author
let c = postdata.mapIt(it.author).toCountTable()[author]
check res == [c]
test "orderby":
let res = query:
select person(id, name)
orderby id
check res == persondata.mapIt((it.id, it.name)).sortedByIt(it[0])
test "orderby_asc":
let res = query:
select person(id, name)
orderby asc(id)
check res == persondata.mapIt((it.id, it.name)).sortedByIt(it[0])
test "orderby_desc":
let res = query:
select person(id, name)
orderby desc(id)
let expected = persondata.mapIt((it.id, it.name))
.sorted((x, y) => system.cmp(x[0], y[0]), Descending)
check res == expected
test "orderby_key_select":
let res = query:
select person(id as personid, name)
orderby personid
check res == persondata.mapIt((it.id, it.name)).sortedByIt(it[0])
test "orderby_key_not_select":
let res = query:
select person(name)
orderby id
check res == persondata.sortedByIt(it.id).mapIt(it.name)
test "orderby_mulitple":
# test fix #30 Incorrect handling of multiple sort keys in orderby
let res = query:
select post(author, id)
orderby author, desc(id)
check res == postdata.mapIt((it.author, it.id))
.sorted((x, y) => system.cmp(x[1], y[1]), Descending)
.sortedByIt(it[0])
test "having":
let id = 4
let res = query:
select post(author, count(_) as count)
groupby author
having author == ?id
let expected = collect(newSeq):
for p in postdata.mapIt(it.author).toCountTable().pairs():
if p[0] == id: p
check res == expected
test "having_aggregate":
let countvalue = 2
let res = query:
select post(author, count(id) as count)
groupby author
having count(id) >= ?countvalue
let expected = collect(newSeq):
for p in postdata.mapIt(it.author).toCountTable().pairs():
if p[1] >= countvalue: p
check res.sortedByIt(it[0]) == expected.sortedByIt(it[0])
test "having_complex":
let
authorid1 = 1
authorid2 = 3
countvalue = 2
let res = query:
select post(author, count(id) as count)
groupby author
having count(id) >= ?countvalue and (author == ?authorid1 or author == ?authorid2)
let expected = collect(newSeq):
for p in postdata.mapIt(it.author).toCountTable().pairs():
if p[0] in [authorid1, authorid2] and p[1] >= countvalue: p
check res == expected
test "subquery":
let res = query:
select post(id)
where author in
(select person(id))
check res.sortedByIt(it) == postdata.mapIt(it.id)
test "subquery_nest2":
let
personid1 = 1
personid2 = 2
expectedpost = postdata.filterIt(it.author in [personid1, personid2])
.mapIt(it.id)
let res = query:
select post(id)
where author in
(select person(id) where id == ?personid1 or id == ?personid2)
check res.sortedByIt(it) == expectedpost.sortedByIt(it)
test "subquery_nest3":
var res = query:
select thread(id)
where id in (select post(thread) where author in
(select person(id) where id in {1, 2}))
res.sort()
check res == postdata.filterIt(it.author in [1, 2])
.mapIt(it.thread)
.sortedByIt(it)
test "subquery_having":
# feature for having in subquery
let id = 4
let res = query:
select person(id)
where id in (
select post(author) groupby author having author == ?id)
check res == @[id]
test "exists":
let authorid = postdata[0].author
let res = query:
select person(id)
where exists(select post(id) where author == ?authorid)
check res.sortedByIt(it) == persondata.mapIt(it.id)
test "not_exists":
let missingAuthor = personcount + postcount
let res = query:
select person(id)
where not exists(select post(id) where author == ?missingAuthor)
check res.sortedByIt(it) == persondata.mapIt(it.id)
test "with_cte":
let res = query:
with recent(select post(id, author) where id <= 3)
select recent(author)
orderby id
check res == postdata.filterIt(it.id <= 3).sortedByIt(it.id).mapIt(it.author)
test "with_cte_chain":
let res = query:
with recent(select post(id, author) where id <= 3)
with authors(select recent(author))
select authors(author)
check res.sortedByIt(it) == postdata.filterIt(it.id <= 3).mapIt(it.author).sortedByIt(it)
test "with_cte_subquery":
let res = query:
with recent(select post(id, author) where id <= 3)
select person(id)
where id in (select recent(author))
check res.sortedByIt(it) == postdata.filterIt(it.id <= 3).mapIt(it.author).deduplicate().sortedByIt(it)
test "union":
var res = query:
select person(id) where id <= 2
union
select person(id) where id >= 4
res.sort()
check res == @[1, 2, 4, 5]
res = query:
union(
select person(id) where id <= 2,
select person(id) where id >= 4
)
res.sort()
check res == @[1, 2, 4, 5]
test "intersect":
var res = query:
select person(id) where id <= 3
intersect
select person(id) where id >= 2
res.sort()
check res == @[2, 3]
res = query:
intersect(
select person(id) where id <= 3,
select person(id) where id >= 2
)
res.sort()
check res == @[2, 3]
test "except":
var res = query:
select person(id) where id <= 4
`except`
select person(id) where id in {2, 3}
res.sort()
check res == @[1, 4]
res = query:
`except`(
select person(id) where id <= 4,
select person(id) where id in {2, 3}
)
res.sort()
check res == @[1, 4]
test "complex_query_subquery_having":
let
id1 = 2
id2 = 3
let res = query:
select thread(count(_))
where id in (
select post(thread) where (author == ?id1 or author == ?id2) and id in (
select post(min(id)) groupby thread having min(id) > 3))
limit 1
let threadinpost = postdata.mapIt(it.thread).deduplicate().sortedByIt(it)
var postminids: seq[int]
for id in threadinpost:
let group = collect(newSeq):
for p in postdata:
if p.thread == id: p.id
postminids.add(group.min())
let threadids = postdata.filterIt(it.author in [id1, id2] and
it.id in postminids.filterIt(it > 3))
.mapIt(it.thread)
check res == threadids.len()
test "auto_join":
let postid = 1
let res = query:
select post(author)
join person(name)
where id == ?postid
let (author, name) = res[0]
check name == persondata[author - 1].name
test "join_on":
# test fix #29 Cannot handle join on condition correctly
let postid = 1
let res = query:
select post(author)
join person(name) on author == id
where id == ?postid
let (author, name) = res[0]
check name == persondata[author - 1].name
test "leftjoin_on":
let postid = 1
let res = query:
select post(author)
leftjoin person(name) on author == id
where id == ?postid
let (author, name) = res[0]
check name == persondata[author - 1].name
test "leftouterjoin_on":
let postid = 1
let res = query:
select post(author)
leftouterjoin person(name) on author == id
where id == ?postid
let (author, name) = res[0]
check name == persondata[author - 1].name
test "casewhen":
# need more test, only number
# has problem with string or expression in condition
let res = query:
select person(name, (if id == 1: 10 elif id == 2: 20 else: 30) as pid)
check res == persondata.mapIt((
name: it.name,
pid: if it.id == 1: 10 elif it.id == 2: 20 else: 30))
test "produce_json":
# test fix #27 Error: type mismatch: got <typeof(nil)>
let threadjson = query:
select thread(id, name)
produce json
let expected = threaddata.map do (it: tuple) -> JsonNode:
result = newJObject()
result["id"] = %it.id
result["name"] = %it.name
check threadjson == %expected
test "produce_nim":
let threadnim = query:
select thread(id, name)
produce nim
check threadnim == threaddata.mapIt((it.id, it.name))
test "tryquery":
# unknow use, only not raise dbError?
let res = tryQuery:
select thread(id)
check res == threaddata.mapIt(it.id)
test "createproc":
createProc getAllThreadIds:
select thread(id)
check db.getAllThreadIds() == threaddata.mapIt(it.id)
test "createiter":
createIter allThreadIdsIter:
select thread(id)
let res = collect(newSeq):
for id in db.allThreadIdsIter:
id
check res.sortedByIt(it) == threaddata.mapIt(it.id)
test "update_concat_op":
let
id = 3
name = persondata[id - 1].name
appendstr = "updated"
nameupdated = name & appendstr
query:
update person(name = name & ?appendstr)
where id == ?id
let res = query:
select person(name)
where id == ?id
check res[0] == nameupdated
test "delete_one":
# test fix #14: delete not return value
let id = 1
query:
delete antibot
where id == ?id
let res = query:
select antibot(_)
where id == ?id
check res == []
test "delete_all":
query:
delete antibot
let res = query:
select antibot(_)
check res == []
test "insert_return_id":
# test fix #28 returning id fail under postgresql
let expectedid = 6
let id = query:
insert antibot(id = ?expectedid, ip = "", answer = "just insert")
returning id
check id == expectedid
test "insert_return_answer":
# test returning non-id parameter
let expectedanswer = "just insert another"
let answer = query:
insert antibot(id = 9, ip = "", answer = ?expectedanswer)
returning answer
check answer == expectedanswer
test "insert_return_id_auto":
# test returning id column
when defined(postgre):
# fix postgres sequence so next nextval returns 10
discard db.getValue(sql"select setval('antibot_id_seq', ?, ?)", 10, false)
let answer = query:
insert antibot(ip = "", answer = "just auto insert")
returning id
check answer == 10
test "insert_returning_uuid":
# test returning uuid column
let expecteduuid = "123e4567-e89b-12d3-a456-426614174000"
let uuid = query:
insert error(uuid = ?expecteduuid, message = "just insert")
returning uuid
check uuid == expecteduuid
test "where_json":
let
id = 1
p = %*{"id": id}
let res = query:
select person(id)
where id == %p["id"]
check res == [id]
test "insert_json":
let
id = 7
answer = "json answer"
a = %*{"answer": answer}
query:
insert antibot(id = ?id, ip = "", answer = %a["answer"])
let res = query:
select antibot(answer)
where id == ?id
check res[0] == answer
test "update_json":
let
id = 3
name = "json"
p = %*{"name": name}
query:
update person(name = %p["name"])
where id == ?id
let res = query:
select person(name)
where id == ?id
check res[0] == name
test "insert_rawsql":
let id = 8
query:
insert antibot(id = ?id, ip = "", answer = !!"'raw sql'",
created = !!"CURRENT_TIMESTAMP")
let res = query:
select antibot(_)
where id == ?id
check res[0].answer == "raw sql"
test "update_rawsql":
let id = 3
let res = query:
select person(name)
where id == ?id
query:
update person(name = !!"UPPER(name)")
where id == ?id
let res2 = query:
select person(name)
where id == ?id
check res[0] != res2[0]
check res[0].toUpper() == res2[0]
test "empty_string":
db.exec(insertthread, 6, "", 77)
createProc getAllThreads:
select thread(id, name, views)
where id >= 5
let rows = db.getAllThreads
check rows[1].id == 6
check rows[1].views == 77
check rows[1].name == ""
+29
View File
@@ -0,0 +1,29 @@
import unittest
import ormin/ormin_sqlite as orm_sqlite
from db_connector/db_sqlite import exec, getValue
import ormin
importModel(DbBackend.sqlite, "static_only_model", includeStatic = true)
let db {.global.} = orm_sqlite.open(":memory:", "", "", "")
suite "importModel includeStatic":
test "sqlSchema is generated and usable without a generated Nim model":
let schema: DbSql = sqlSchema
check ($schema).len > 0
db.createTable(sqlSchema)
db.exec(sql"insert into user_static (id, name) values (?, ?)", 1, "Ada")
let row = query:
select user_static(id, name)
where id == 1
limit 1
check row.id == 1
check row.name == "Ada"
check db.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'user_static'") == "1"
db.dropTable(sqlSchema, "user_static")
check db.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'user_static'") == "0"
+130
View File
@@ -0,0 +1,130 @@
import unittest, json, strutils, macros, times, os, sequtils
# Postgres connection handled through ormin_postgre backend
from db_connector/db_postgres import exec, getValue
import ormin
import ormin/ormin_postgre as ormin_postgre
# import db_connector/db_postgres as db_postgres
import ormin/db_utils
when defined(macosx):
{.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
importModel(DbBackend.postgre, "model_postgre")
let
db {.global.} = ormin_postgre.open("localhost", "test", "test", "test_ormin")
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / "model_postgre.sql")
suite "Test special database types and functions of postgre":
discard
let
dtStr1 = "2018-03-30 01:01:01"
dt1 = parse(dtStr1, "yyyy-MM-dd HH:mm:ss")
dtnStr1 = "2019-03-30 11:02:02+08"
dtn1 = parse(dtnStr1, "yyyy-MM-dd HH:mm:sszz")
dtzStr1 = "2020-03-30 09:03:03Z"
dtz1 = parse(dtzStr1, "yyyy-MM-dd HH:mm:sszz")
dtStr2 = "2018-03-30 01:01:01.123"
dt2 = parse(dtStr2, "yyyy-MM-dd HH:mm:ss\'.\'fff")
dtnStr2 = "2019-03-30 11:02:02.123+08"
dtn2 = parse(dtnStr2, "yyyy-MM-dd HH:mm:ss\'.\'fffzz")
dtzStr2 = "2020-03-30 09:03:03.123Z"
dtz2 = parse(dtzStr2, "yyyy-MM-dd HH:mm:ss\'.\'fffzz")
dtStr3 = "2018-03-30 01:01:01.123456"
dt3 = parse(dtStr3, "yyyy-MM-dd HH:mm:ss\'.\'ffffff")
dtnStr3 = "2019-03-30 11:02:02.123456+08"
dtn3 = parse(dtnStr3, "yyyy-MM-dd HH:mm:ss\'.\'ffffffzz")
dtzStr3 = "2020-03-30 09:03:03.123456Z"
dtz3 = parse(dtzStr3, "yyyy-MM-dd HH:mm:ss\'.\'ffffffzz")
dtjson1 = %*{"dt": dt1.format(jsonTimeFormat),
"dtn": dtn1.format(jsonTimeFormat),
"dtz": dtz1.format(jsonTimeFormat)}
dtjson2 = %*{"dt": dt2.format(jsonTimeFormat),
"dtn": dtn2.format(jsonTimeFormat),
"dtz": dtz2.format(jsonTimeFormat)}
dtjson3 = %*{"dt": dt3.format(jsonTimeFormat),
"dtn": dtn3.format(jsonTimeFormat),
"dtz": dtz3.format(jsonTimeFormat)}
let insertSql = sql"insert into tb_timestamp(dt, dtn, dtz) values (?, ?, ?)"
suite "timestamp_insert":
setup:
db.dropTable(sqlFile, "tb_timestamp")
db.createTable(sqlFile, "tb_timestamp")
test "insert":
query:
insert tb_timestamp(dt = ?dt1, dtn = ?dtn1, dtz = ?dtz1)
check db_postgres.getValue(db, sql"select count(*) from tb_timestamp") == "1"
test "json":
query:
insert tb_timestamp(dt = %dtjson1["dt"],
dtn = %dtjson1["dtn"],
dtz = %dtjson1["dtz"])
check db_postgres.getValue(db, sql"select count(*) from tb_timestamp") == "1"
suite "timestamp":
db.dropTable(sqlFile, "tb_timestamp")
db.createTable(sqlFile, "tb_timestamp")
db_postgres.exec(db, insertSql, dtStr1, dtnStr1, dtzStr1)
db_postgres.exec(db, insertSql, dtStr2, dtnStr2, dtzStr2)
db_postgres.exec(db, insertSql, dtStr3, dtnStr3, dtzStr3)
doAssert db_postgres.getValue(db, sql"select count(*) from tb_timestamp") == "3"
test "query":
let res = query:
select tb_timestamp(dt, dtn, dtz)
check res == [(dt1, dtn1, dtz1),
(dt2, dtn2, dtz2),
(dt3, dtn3, dtz3)]
test "where":
let res = query:
select tb_timestamp(dt)
where dt == ?dt1
check res == [dt1]
test "in":
let
duration = initDuration(hours = 1)
dtStart = dt1 - duration
dtEnd = dt1 + duration
res2 = query:
select tb_timestamp(dt)
where dt in ?dtStart .. ?dtEnd
check res2 == [dt1, dt2, dt3]
test "iter":
createIter iter:
select tb_timestamp(dt)
where dt == ?dt
var res: seq[DateTime]
for it in db.iter(dt1):
res.add(it)
check res == [dt1]
test "proc":
createProc aproc:
select tb_timestamp(dt)
where dt == ?dt
check db.aproc(dt1) == [dt1]
test "json":
let res = query:
select tb_timestamp(dt, dtn, dtz)
produce json
check res == %*[dtjson1, dtjson2, dtjson3]
test "json_where":
let res = query:
select tb_timestamp(dt, dtn, dtz)
where dt == %dtjson1["dt"]
produce json
check res[0] == dtjson1
+216
View File
@@ -0,0 +1,216 @@
import unittest, strformat, os, times, std/monotimes
import std/options
import ormin
import ormin/db_utils
import ormin/query_hooks
when defined(postgre):
when defined(macosx):
{.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
const backend = DbBackend.postgre
importModel(backend, "model_postgre")
const sqlFileName = "model_postgre.sql"
let db {.global.} = open("localhost", "test", "test", "test_ormin")
else:
const backend = DbBackend.sqlite
importModel(backend, "model_sqlite")
const sqlFileName = "model_sqlite.sql"
let db {.global.} = open("test.db", "", "", "")
let
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / sqlFileName)
type
CompositeRow = object
id: int
message: string
RefCompositeRow = ref object
id: int
message: string
BenchmarkCompositeRow = object
pk1: int
message: string
NullableNoteOptionRow = object
id: int
note: Option[string]
MessageSize = distinct int
HookedMessageRow = object
id: int
message: MessageSize
NullFallbackNote = distinct string
HookedNullableNoteRow = object
id: int
note: NullFallbackNote
const
benchmarkRowCount = 256
benchmarkWarmupIterations = 75
benchmarkIterations = 250
benchmarkRounds = 5
maxTypedQuerySlowdown = 1.20
proc fromQueryHook*(val: var MessageSize, value: string) =
val = MessageSize(value.len)
proc fromQueryHook*(val: var NullFallbackNote, value: DbValue[string]) =
if value.isNull:
val = NullFallbackNote("<missing>")
else:
val = NullFallbackNote("note:" & value.value)
proc loadBenchmarkRows() =
db.dropTable(sqlFile, "tb_composite_pk")
db.createTable(sqlFile, "tb_composite_pk")
for i in 1 .. benchmarkRowCount:
let message = &"message-{i}"
query:
insert tb_composite_pk(pk1 = ?i, pk2 = ?i, message = ?message)
proc benchmarkCurrentQuery(iterations: int): float =
var checksum = 0
let started = getMonoTime()
for _ in 0 ..< iterations:
let rows = query:
select tb_composite_pk(pk1, message)
orderby pk1
checksum += rows.len + rows[^1][0] + rows[^1][1].len
doAssert checksum > 0
result = (getMonoTime() - started).inNanoseconds.float / 1_000_000_000.0
proc benchmarkTypedQuery(iterations: int): float =
var checksum = 0
let started = getMonoTime()
for _ in 0 ..< iterations:
let rows = query(BenchmarkCompositeRow):
select tb_composite_pk(pk1, message)
orderby pk1
checksum += rows.len + rows[^1].pk1 + rows[^1].message.len
doAssert checksum > 0
result = (getMonoTime() - started).inNanoseconds.float / 1_000_000_000.0
suite &"query(T) mapping on {backend}":
setup:
db.dropTable(sqlFile, "tb_composite_pk")
db.createTable(sqlFile, "tb_composite_pk")
db.dropTable(sqlFile, "tb_nullable")
db.createTable(sqlFile, "tb_nullable")
query:
insert tb_composite_pk(pk1 = 1, pk2 = 1, message = "hello")
query:
insert tb_composite_pk(pk1 = 2, pk2 = 2, message = "world")
query:
insert tb_nullable(id = 1, note = nil)
query:
insert tb_nullable(id = 2, note = "hello")
test "maps selected rows to objects":
let rows = query(CompositeRow):
select tb_composite_pk(pk1 as id, message)
orderby pk1
check rows == @[
CompositeRow(id: 1, message: "hello"),
CompositeRow(id: 2, message: "world")
]
test "maps selected rows to ref objects":
let rows = query(RefCompositeRow):
select tb_composite_pk(pk1 as id, message)
orderby pk1
check rows.len == 2
check rows[0] != nil
check rows[0].id == 1
check rows[0].message == "hello"
check rows[1] != nil
check rows[1].id == 2
check rows[1].message == "world"
test "single-row query(T) returns a single object":
let row = query(CompositeRow):
select tb_composite_pk(pk1 as id, message)
where pk1 == 1 and pk2 == 1
limit 1
check row == CompositeRow(id: 1, message: "hello")
test "maps nullable column to Option":
let rows = query(NullableNoteOptionRow):
select tb_nullable(id, note)
orderby id
check rows[0].id == 1
check rows[0].note.isNone
check rows[1].id == 2
check rows[1].note.isSome
check rows[1].note.get == "hello"
test "maps object fields through user fromQueryHook overloads":
let rows = query(HookedMessageRow):
select tb_composite_pk(pk1 as id, message)
orderby pk1
check rows.len == 2
check rows[0].id == 1
check int(rows[0].message) == "hello".len
check rows[1].id == 2
check int(rows[1].message) == "world".len
test "maps scalar query(T) through user fromQueryHook overloads":
let values = query(MessageSize):
select tb_composite_pk(message)
orderby pk1
check values.len == 2
check int(values[0]) == "hello".len
check int(values[1]) == "world".len
test "allows user fromQueryHook overloads to handle NULL values":
let rows = query(HookedNullableNoteRow):
select tb_nullable(id, note)
orderby id
check rows.len == 2
check rows[0].id == 1
check string(rows[0].note) == "<missing>"
check rows[1].id == 2
check string(rows[1].note) == "note:hello"
test "sqlite benchmark for query and query(T)":
loadBenchmarkRows()
let untypedRows = query:
select tb_composite_pk(pk1, message)
orderby pk1
let typedRows = query(BenchmarkCompositeRow):
select tb_composite_pk(pk1, message)
orderby pk1
check untypedRows.len == typedRows.len
check typedRows[0] == BenchmarkCompositeRow(pk1: untypedRows[0][0], message: untypedRows[0][1])
check typedRows[^1] == BenchmarkCompositeRow(pk1: untypedRows[^1][0], message: untypedRows[^1][1])
discard benchmarkCurrentQuery(benchmarkWarmupIterations)
discard benchmarkTypedQuery(benchmarkWarmupIterations)
var currentBest = high(float)
var typedBest = high(float)
for _ in 0 ..< benchmarkRounds:
currentBest = min(currentBest, benchmarkCurrentQuery(benchmarkIterations))
typedBest = min(typedBest, benchmarkTypedQuery(benchmarkIterations))
let ratio = typedBest / currentBest
echo &"sqlite benchmark query={currentBest:.6f}s query(T)={typedBest:.6f}s ratio={ratio:.3f}x; 20% budget={(ratio <= maxTypedQuerySlowdown)}"
check currentBest > 0.0
check typedBest > 0.0
when defined(release):
check ratio <= maxTypedQuerySlowdown
+153
View File
@@ -0,0 +1,153 @@
import unittest, db_connector/sqlite3, json, times, sequtils
from db_connector/db_sqlite import exec, getValue
import ormin
import ormin/db_utils
importModel(DbBackend.sqlite, "model_sqlite")
let
db {.global.} = open(":memory:", "", "", "")
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / "model_sqlite.sql")
suite "Test special database types and functions of sqlite":
discard
jsonTimeFormat = "yyyy-MM-dd HH:mm:ss\'.\'fff"
let
dtStr1 = "2018-02-20 02:02:02"
dt1 = parse(dtStr1, "yyyy-MM-dd HH:mm:ss", utc())
dtStr2 = "2019-03-30 03:03:03.123"
dt2 = parse(dtStr2, "yyyy-MM-dd HH:mm:ss\'.\'fff", utc())
dtjson = %*{"dt1": dt1.format(jsonTimeFormat),
"dt2": dt2.format(jsonTimeFormat)}
let insertSql = sql"insert into tb_timestamp(dt1, dt2) values (?, ?)"
proc blobFromBytes(bytes: openArray[int]): seq[byte] =
result = newSeq[byte](bytes.len)
for i, b in bytes:
doAssert b >= 0 and b <= 255
result[i] = byte(b)
let blobFixtures = [
blobFromBytes(@[0, 1, 2, 3, 4, 5, 6, 7]),
blobFromBytes(@[255, 128, 64, 32, 16, 8, 4, 2, 1]),
blobFromBytes(@[ord('O'), ord('R'), ord('M'), ord('I'), ord('N'), 0, 1, 2])
]
suite "timestamp_insert":
setup:
db.dropTable(sqlFile, "tb_timestamp")
db.createTable(sqlFile, "tb_timestamp")
test "insert":
query:
insert tb_timestamp(dt1 = ?dt1, dt2 = ?dt2)
check db.getValue(sql"select count(*) from tb_timestamp") == "1"
test "json":
query:
insert tb_timestamp(dt1 = %dtjson["dt1"], dt2 = %dtjson["dt2"])
check db.getValue(sql"select count(*) from tb_timestamp") == "1"
suite "timestamp":
db.dropTable(sqlFile, "tb_timestamp")
db.createTable(sqlFile, "tb_timestamp")
db.exec(insertSql, dtStr1, dtStr2)
doAssert db.getValue(sql"select count(*) from tb_timestamp") == "1"
test "query":
let res = query:
select tb_timestamp(dt1, dt2)
check res == [(dt1, dt2)]
test "where":
let res = query:
select tb_timestamp(dt1, dt2)
where dt1 == ?dt1
check res == [(dt1, dt2)]
test "in":
let
duration = initDuration(hours = 1)
dtStart = dt1 - duration
dtEnd = dt1 + duration
res2 = query:
select tb_timestamp(dt1, dt2)
where dt1 in ?dtStart .. ?dtEnd
check res2 == [(dt1, dt2)]
test "iter":
createIter iter:
select tb_timestamp(dt1, dt2)
where dt1 == ?dt1
var res: seq[tuple[dt1: DateTime, dt2: DateTime]]
for it in db.iter(dt1):
res.add(it)
check res == [(dt1, dt2)]
test "proc":
createProc aproc:
select tb_timestamp(dt1, dt2)
where dt1 == ?dt1
check db.aproc(dt1) == [(dt1, dt2)]
test "json":
let res = query:
select tb_timestamp(dt1, dt2)
produce json
check res == %*[dtjson]
test "json_where":
let res = query:
select tb_timestamp(dt1, dt2)
where dt1 == %dtjson["dt1"]
produce json
check res == %*[dtjson]
suite "blob_insert":
setup:
db.dropTable(sqlFile, "tb_blob")
db.createTable(sqlFile, "tb_blob")
test "insert parameters":
for blob in blobFixtures:
query:
insert tb_blob(typblob = ?blob)
check db.getValue(sql"select count(*) from tb_blob") == $blobFixtures.len
suite "blob":
db.dropTable(sqlFile, "tb_blob")
db.createTable(sqlFile, "tb_blob")
for blob in blobFixtures:
query:
insert tb_blob(typblob = ?blob)
doAssert db.getValue(sql"select count(*) from tb_blob") == $blobFixtures.len
test "query":
let res = query:
select tb_blob(id, typblob)
check res.mapIt(it.typblob) == blobFixtures
test "where":
let target = blobFixtures[1]
let res = query:
select tb_blob(id, typblob)
where typblob == ?target
check res.len == 1
check res[0].typblob == target
createProc selectAllBlob:
select tb_blob(id, typblob)
test "createProc":
let res = db.selectAllBlob
check res.mapIt(it.typblob) == blobFixtures
test "createProc with empty blob in result":
db.exec(sql"insert into tb_blob(id, typblob) values (?, ?)", 9, "")
let res = db.selectAllBlob
check res[^1].typblob == blobFromBytes(@[])
+132
View File
@@ -0,0 +1,132 @@
import unittest, os, strformat
import ormin
import ormin/db_utils
when NimVersion < "1.2.0": import ./compat
let testDir = currentSourcePath.parentDir()
when defined postgre:
when defined(macosx):
{.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
from db_connector/db_postgres import exec, getValue
const backend = DbBackend.postgre
importModel(backend, "forum_model_postgres")
const sqlFileName = "forum_model_postgres.sql"
let db {.global.} = open("localhost", "test", "test", "test_ormin")
else:
from db_connector/db_sqlite import exec, getValue
const backend = DbBackend.sqlite
importModel(backend, "forum_model_sqlite")
const sqlFileName = "forum_model_sqlite.sql"
var memoryPath = testDir & "/" & ":memory:"
let db {.global.} = open(memoryPath, "", "", "")
var sqlFilePath = Path(testDir & "/" & sqlFileName)
# Fresh schema
db.dropTable(sqlFilePath)
db.createTable(sqlFilePath)
suite &"Transactions ({backend})":
test "commit on success":
transaction:
query:
insert person(id = ?(101), name = ?"john101", password = ?"p101", email = ?"john101@mail.com", salt = ?"s101", status = ?"ok")
check db.getValue(sql"select count(*) from person where id = 101") == "1"
test "rollback on error with manual try except":
# prepare one row
query:
insert person(id = ?(201), name = ?"john201", password = ?"p201", email = ?"john201@mail.com", salt = ?"s201", status = ?"ok")
# in transaction insert a new row and then violate PK
try:
transaction:
query:
insert person(id = ?(202), name = ?"john202", password = ?"p202", email = ?"john202@mail.com", salt = ?"s202", status = ?"ok")
# duplicate key error
query:
insert person(id = ?(201), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
check false # should not reach
except DbError as e:
discard
# both inserts inside the transaction should be rolled back
check db.getValue(sql"select count(*) from person where id = 202") == "0"
check db.getValue(sql"select count(*) from person where id = 201 and name = 'dup'") == "0"
test "rollback on error with else":
# prepare one row
var failed = false
query:
insert person(id = ?(501), name = ?"john501", password = ?"p501", email = ?"john501@mail.com", salt = ?"s501", status = ?"ok")
# in transaction insert a new row and then violate PK
transaction:
query:
insert person(id = ?(502), name = ?"john502", password = ?"p502", email = ?"john502@mail.com", salt = ?"s502", status = ?"ok")
# duplicate key error
query:
insert person(id = ?(501), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
check false # should not reach
else:
echo "do something else..."
failed = true
check failed
# both inserts inside the transaction should be rolled back
check db.getValue(sql"select count(*) from person where id = 502") == "0"
check db.getValue(sql"select count(*) from person where id = 501 and name = 'dup'") == "0"
test "commit normally with else":
# prepare one row
var failed = false
query:
insert person(id = ?(601), name = ?"john601", password = ?"p601", email = ?"john601@mail.com", salt = ?"s601", status = ?"ok")
# in transaction insert a new row and then violate PK
transaction:
query:
insert person(id = ?(602), name = ?"john602", password = ?"p602", email = ?"john602@mail.com", salt = ?"s602", status = ?"ok")
query:
insert person(id = ?(603), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
else:
failed = true
check not failed
# both inserts inside the transaction should be rolled back
check db.getValue(sql"select count(*) from person where id = 603") == "1"
check db.getValue(sql"select count(*) from person where id = 602") == "1"
check db.getValue(sql"select count(*) from person where id = 601") == "1"
test "transaction set false on DbError":
var failed = false
transaction:
query:
insert person(id = ?(301), name = ?"john301", password = ?"p301", email = ?"john301@mail.com", salt = ?"s301", status = ?"ok")
query:
insert person(id = ?(301), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
else:
failed = true
check failed
check db.getValue(sql"select count(*) from person where id = 301") == "0"
test "nested savepoints":
var failed = false
transaction:
query:
insert person(id = ?(401), name = ?"john401", password = ?"p401", email = ?"john401@mail.com", salt = ?"s401", status = ?"ok")
var innerOk = true
transaction:
query:
insert person(id = ?(402), name = ?"john402", password = ?"p402", email = ?"john402@mail.com", salt = ?"s402", status = ?"ok")
query:
insert person(id = ?(401), name = ?"dup401", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
else:
innerOk = false
check innerOk == false
# after inner rollback, we can still insert another row and commit outer
query:
insert person(id = ?(403), name = ?"john403", password = ?"p403", email = ?"john403@mail.com", salt = ?"s403", status = ?"ok")
else:
failed = true
check db.getValue(sql"select count(*) from person where id in (401,402,403)") == "2"
@@ -0,0 +1,38 @@
## Ormin -- ORM for Nim.
## (c) 2017 Andreas Rumpf
## MIT License.
import strutils, os, parseopt
import ../ormin/importer_core
proc writeHelp() =
echo """
ormin <schema.sql> --out:<file.nim> --db:postgre|sqlite|mysql
"""
proc writeVersion() = echo "v1.0"
var p = initOptParser()
var infile = ""
var outfile = ""
var target: ImportTarget
for kind, key, val in p.getopt():
case kind
of cmdArgument:
infile = key
of cmdLongOption, cmdShortOption:
case key
of "help", "h": writeHelp()
of "version", "v": writeVersion()
of "out", "o": outfile = val
of "db": target = parseEnum[ImportTarget](val)
else: discard
of cmdEnd: assert(false) # cannot happen
if infile == "":
# no filename has been given, so we show the help:
writeHelp()
else:
if outfile == "":
outfile = changeFileExt(infile, "nim")
writeFile(outfile, generateModelCode(readFile(infile), absolutePath(infile), target))
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
# Try default connection first, then fallback to -U postgres
run_psql_file() {
local db="$1"; shift
local file="$1"; shift
if ! psql -v ON_ERROR_STOP=1 -d "$db" -f "$file" "$@" >/dev/null 2>&1; then
psql -v ON_ERROR_STOP=1 -U postgres -d "$db" -f "$file" "$@"
fi
}
run_psql_cmd() {
local db="$1"; shift
local cmd="$1"; shift
if ! psql -v ON_ERROR_STOP=1 -d "$db" -c "$cmd" "$@" >/dev/null 2>&1; then
psql -v ON_ERROR_STOP=1 -U postgres -d "$db" -c "$cmd" "$@"
fi
}
# Create role 'test' if needed
run_psql_file postgres tools/setup_postgres_role.sql
# Create database 'test' if needed (must be outside DO block)
if ! psql -v ON_ERROR_STOP=1 -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname = 'test_ormin'" | grep -q 1; then
run_psql_cmd postgres "CREATE DATABASE test_ormin OWNER test"
fi
# Grant privileges on public schema
run_psql_cmd test_ormin "GRANT ALL PRIVILEGES ON SCHEMA public TO test"
echo "Postgres test DB/user ensured (role 'test', db 'test_ormin')."
@@ -0,0 +1,21 @@
DO
$$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'test') THEN
CREATE ROLE test LOGIN PASSWORD 'test';
END IF;
END
$$;
DO
$$
BEGIN
IF NOT EXISTS (SELECT FROM pg_database WHERE datname = 'test') THEN
CREATE DATABASE test OWNER test;
END IF;
END
$$;
\connect test
GRANT ALL PRIVILEGES ON SCHEMA public TO test;
@@ -0,0 +1,9 @@
DO
$$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'test') THEN
CREATE ROLE test LOGIN PASSWORD 'test';
END IF;
END
$$;