feat(clients): add nim-allographer client with BaraDB driver support
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
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
- Pure Nim wire protocol client (async + sync, no C FFI) - Query Builder: fluent API, SQL generator, execution, transactions - Schema Builder: create/alter/drop tables, column operations - Connection pool with aging and timeout handling - ORM integration via compile-time DB_BARADB switch - Tests: test_open, test_query - Documentation: README.md, PLAN_BARADB.md
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
## index
|
||||
<!--ts-->
|
||||
* [index](#index)
|
||||
* [0.23.4](#0234)
|
||||
|
||||
<!-- Created by https://github.com/ekalinin/github-markdown-toc -->
|
||||
<!-- Added by: root, at: Mon Jul 17 07:46:18 UTC 2023 -->
|
||||
|
||||
<!--te-->
|
||||
|
||||
## 0.23.4
|
||||
```sh
|
||||
cd example
|
||||
DB_POSTGRES=true nim c -r -d:release --threads:off -d:danger --mm:orc benchmark
|
||||
```
|
||||
|
||||
query
|
||||
|num|time|
|
||||
|---|---|
|
||||
|1|0.938770656|
|
||||
|2|6.643012469|
|
||||
|3|6.493994112999999|
|
||||
|4|6.292020136999998|
|
||||
|5|6.618309597|
|
||||
|Avg|5.3972213944|
|
||||
|
||||
update
|
||||
|num|time|
|
||||
|---|---|
|
||||
|1|0.3599897400000001|
|
||||
|2|0.2508196720000004|
|
||||
|3|0.1847369050000012|
|
||||
|4|0.2267158040000012|
|
||||
|5|0.2636497700000007|
|
||||
|Avg|0.2571823782000007|
|
||||
@@ -0,0 +1,288 @@
|
||||
# PostgreSQL driver 効率改善レポート
|
||||
|
||||
MariaDB driver に対して実施した効率改善と同様の観点で、PostgreSQL driver(`src/allographer/query_builder/libs/postgres/` および `models/postgres/`)を調査した結果をまとめる。
|
||||
|
||||
---
|
||||
|
||||
## P0: 重大(即時修正推奨)
|
||||
|
||||
### 1. `exec` / `insertId` が DML のたびに `information_schema.columns` を問い合わせている
|
||||
|
||||
**ファイル:** `models/postgres/postgres_exec.nim` 196-211行, 214-229行
|
||||
|
||||
```nim
|
||||
proc exec(self:PostgresQuery, queryString:string) {.async.} =
|
||||
...
|
||||
let table = self.query["table"].getStr
|
||||
let columnGetQuery = &"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '{table}'"
|
||||
let (columns, _) = postgres_impl.query(..., columnGetQuery, ...).await
|
||||
postgres_impl.exec(..., queryString, ..., columns, ...).await
|
||||
```
|
||||
|
||||
**問題:**
|
||||
- INSERT / UPDATE / DELETE のたびに追加で1回 SELECT が発生し、DML 性能が実質2倍遅くなる
|
||||
- `'{table}'` の文字列補間は SQL インジェクションリスクがある(`insertId` にも同様の問題)
|
||||
|
||||
**改善案:**
|
||||
MariaDB で実施済みの `columnTypeCache: Table[string, seq[seq[string]]]` を `Connections` に追加し、テーブルごとに初回のみ取得・キャッシュする。SQL はパラメータ化クエリに変更する。
|
||||
|
||||
---
|
||||
|
||||
## P1: 中程度
|
||||
|
||||
### 2. `waiters` が `seq[Future[void]]` で先頭削除が O(n)
|
||||
|
||||
**ファイル:** `models/postgres/postgres_types.nim` 20行, `models/postgres/postgres_exec.nim` 28-35行
|
||||
|
||||
```nim
|
||||
# postgres_types.nim
|
||||
waiters*: seq[Future[void]]
|
||||
|
||||
# postgres_exec.nim
|
||||
proc wakeOnePoolWaiter(pools: Connections) =
|
||||
while pools.waiters.len > 0:
|
||||
let w = pools.waiters[0]
|
||||
pools.waiters.delete(0) # ← O(n) のシフト操作
|
||||
...
|
||||
```
|
||||
|
||||
**問題:** `seq` の先頭削除は全要素シフトを伴う O(n) 操作。waiter が多い高負荷時に性能劣化する。`removePoolWaiter` も同じくリニアスキャン+シフト。
|
||||
|
||||
**改善案:** `std/deques` の `Deque[Future[void]]` に変更。`popFirst()` が O(1)。MariaDB で実施済み。
|
||||
|
||||
### 3. `PgWaitState` が `ref object`(ヒープアロケーション)
|
||||
|
||||
**ファイル:** `libs/postgres/postgres_impl.nim` 14行
|
||||
|
||||
```nim
|
||||
type
|
||||
PgWaitState = ref object
|
||||
cancelled: bool
|
||||
```
|
||||
|
||||
**問題:** `waitPgReadable` / `waitPgWritable` が呼ばれるたびに `PgWaitState` のヒープアロケーションが発生する。
|
||||
|
||||
**改善案:** `object`(値型)に変更。async proc のクロージャ環境に格納されるため、値型でも正しく動作する。MariaDB で実施済み。
|
||||
|
||||
### 4. `waitPgReadable` と `waitPgWritable` がほぼ重複している
|
||||
|
||||
**ファイル:** `libs/postgres/postgres_impl.nim` 37-81行
|
||||
|
||||
```nim
|
||||
proc waitPgReadable(db: PPGconn, timeoutMs: int): Future[bool] {.async.} =
|
||||
...
|
||||
addRead(fd, readCb) # ← 違いはここだけ
|
||||
...
|
||||
|
||||
proc waitPgWritable(db: PPGconn, timeoutMs: int): Future[bool] {.async.} =
|
||||
...
|
||||
addWrite(fd, writeCb) # ← 違いはここだけ
|
||||
...
|
||||
```
|
||||
|
||||
**問題:** 2つの proc の差は `addRead` vs `addWrite` だけ。20行以上の完全な重複。
|
||||
|
||||
**改善案:** `waitPgIo(db, timeoutMs, forRead: bool)` のような統一 proc にまとめる。
|
||||
|
||||
### 5. `waitPgReadable` / `waitPgWritable` が `pqsocket` を二重に呼んでいる
|
||||
|
||||
**ファイル:** `libs/postgres/postgres_impl.nim` 37-58行
|
||||
|
||||
```nim
|
||||
proc waitPgReadable(db: PPGconn, timeoutMs: int): Future[bool] {.async.} =
|
||||
...
|
||||
ensurePgSocketRegistered(db) # 内部で pqsocket(db) を呼ぶ
|
||||
let sock = pqsocket(db) # ← 再度 pqsocket を呼んでいる
|
||||
if sock < 0:
|
||||
dbError(db)
|
||||
let fd = AsyncFD(cint(sock))
|
||||
...
|
||||
```
|
||||
|
||||
**問題:** `ensurePgSocketRegistered` 内で `pqsocket` を呼んで fd を得ているのに、その戻り値を使わず直後にもう一度 `pqsocket` を呼んでいる。FFI 呼び出しの無駄。
|
||||
|
||||
**改善案:** `ensurePgSocketRegistered` を `AsyncFD` を返す proc にする(MariaDB 版 `ensureMariadbSocketRegistered` と同じ設計)。
|
||||
|
||||
---
|
||||
|
||||
## P2: 軽微
|
||||
|
||||
### 6. `getTime().toUnix()` によるデッドライン計算(秒精度)
|
||||
|
||||
**ファイル:** `libs/postgres/postgres_impl.nim` 83-89行, 149-150行 他多数
|
||||
|
||||
```nim
|
||||
proc pgRemainingMs(deadline: int64): int =
|
||||
let leftSec = deadline - getTime().toUnix()
|
||||
...
|
||||
result = int(leftSec * 1000)
|
||||
|
||||
# deadline の生成(秒精度)
|
||||
let calledAt = getTime().toUnix()
|
||||
let deadline = calledAt + timeout.int64
|
||||
```
|
||||
|
||||
**問題:**
|
||||
- `getTime().toUnix()` は秒精度のため、ミリ秒単位のタイムアウト制御ができない
|
||||
- NTP 補正によるクロック巻き戻りの影響を受ける
|
||||
- `timeout = 1` のケースでは、0.9秒経過後に残り 0ms と判定される可能性がある
|
||||
|
||||
**改善案:** `std/monotimes` の `MonoTime` + `Duration` に統一する。MariaDB で実施済み。
|
||||
|
||||
### 7. `setColumnInfo` が行ごとに全カラム分の FFI 呼び出しを行っている
|
||||
|
||||
**ファイル:** `libs/postgres/postgres_lib.nim` 159-167行, `libs/postgres/postgres_impl.nim` 160-163行 他
|
||||
|
||||
```nim
|
||||
# postgres_lib.nim
|
||||
proc setColumnInfo*(res: PPGresult; dbRows: var DbRows; line, cols: int32) =
|
||||
var columns: DbColumns
|
||||
setLen(columns, cols)
|
||||
for col in 0'i32..cols-1:
|
||||
columns[col].name = $pqfname(res, col) # 行に依存しない
|
||||
columns[col].typ = getColumnType(res, line, col) # NULL判定のみ行依存
|
||||
columns[col].tableName = $(pqftable(res, col)) # 行に依存しない
|
||||
dbRows.add(columns)
|
||||
|
||||
# postgres_impl.nim(query* 内)
|
||||
for i in 0'i32 .. pqNtuples(pqresult) - 1:
|
||||
setRow(pqresult, row, i, cols)
|
||||
setColumnInfo(pqresult, dbRows, i, cols) # ← 毎行で全カラム FFI 呼び出し
|
||||
rows.add(row)
|
||||
```
|
||||
|
||||
**問題:** `pqfname`, `pqftable`, `pqftype` はカラムの OID・名前・テーブル名を返す関数で、行によって結果が変わらない。行に依存するのは `pqgetisnull(res, line, col)` による NULL 判定のみ。1000行×10カラムでは、カラム名・テーブル名だけで2万回の不要な FFI 呼び出し+文字列アロケーションが発生する。
|
||||
|
||||
**改善案:** カラムの基本情報(name, tableName, OID)はループ外で1回だけ取得し、行ごとには NULL 判定のみ行う。
|
||||
|
||||
```nim
|
||||
# 基本情報を1回だけ取得
|
||||
var baseColumns: DbColumns
|
||||
setLen(baseColumns, cols)
|
||||
for col in 0'i32..cols-1:
|
||||
baseColumns[col].name = $pqfname(res, col)
|
||||
baseColumns[col].typ = getBaseColumnType(res, col) # NULL判定なし
|
||||
baseColumns[col].tableName = $(pqftable(res, col))
|
||||
|
||||
# 行ごとにNULLだけ上書き
|
||||
for i in 0'i32 .. pqNtuples(pqresult) - 1:
|
||||
setRow(pqresult, row, i, cols)
|
||||
var rowColumns = baseColumns
|
||||
for col in 0'i32..cols-1:
|
||||
if pqgetisnull(res, i, col) == 1:
|
||||
rowColumns[col].typ = DbType(kind: dbNull, name: "null")
|
||||
dbRows.add(rowColumns)
|
||||
rows.add(row)
|
||||
```
|
||||
|
||||
### 8. `dbFormat` / `questionToDaller` の char-by-char 連結
|
||||
|
||||
**ファイル:** `libs/postgres/postgres_lib.nim` 203-216行, 219-229行
|
||||
|
||||
```nim
|
||||
proc dbFormat*(formatstr: string, args: varargs[string]): string =
|
||||
result = ""
|
||||
...
|
||||
for c in items(formatstr):
|
||||
if c == '?':
|
||||
add(result, dbQuote(args[a]))
|
||||
inc(a)
|
||||
else:
|
||||
add(result, c) # ← 1文字ずつ add
|
||||
|
||||
proc questionToDaller*(s:string):string =
|
||||
var i = 1
|
||||
for c in s:
|
||||
if c == '?':
|
||||
result.add(&"${i}")
|
||||
i += 1
|
||||
else:
|
||||
result.add(c) # ← 1文字ずつ add
|
||||
```
|
||||
|
||||
**問題:** `?` が出現するまでの連続部分を1文字ずつ `add` しており、長いクエリ文字列で非効率。
|
||||
|
||||
**改善案:** `?` 位置間のサブ文字列を一括 `add` する。MariaDB の `dbFormat` で実施済みの手法。
|
||||
|
||||
### 9. `query*` / `rawQuery*` / `execGetValue*` のパラメータ送信パターンが重複
|
||||
|
||||
**ファイル:** `libs/postgres/postgres_impl.nim` 134-166行, 193-225行, 228-260行
|
||||
|
||||
```nim
|
||||
# query*, execGetValue*, rawQuery* の冒頭がほぼ同一:
|
||||
let status =
|
||||
if pgParams.nParams > 0:
|
||||
pqsendQueryParams(db, query.cstring, pgParams.nParams, nil, pgParams.values, ...)
|
||||
else:
|
||||
pqsendQueryParams(db, query.cstring, pgParams.nParams, nil, nil, nil, nil, 0)
|
||||
defer:
|
||||
if pgParams.nParams > 0: pgParams.values.deallocCStringArray()
|
||||
if status != 1: dbError(db)
|
||||
```
|
||||
|
||||
**問題:** 4箇所にわたるクエリ送信ボイラープレート。保守コストが高い。
|
||||
|
||||
**改善案:** `pgSendQuery(db, pgParams)` のような共通 proc を抽出する。
|
||||
|
||||
### 10. `fromObjArray` の2つのオーバーロードが大部分重複
|
||||
|
||||
**ファイル:** `libs/postgres/postgres_lib.nim` 239-291行, 293-337行
|
||||
|
||||
**問題:** `columns` パラメータの有無のみが異なり、JSON→string 変換ロジックは同一。
|
||||
|
||||
**改善案:** 共通の内部 proc を抽出し、`columns` が空の場合はバイナリ判定をスキップする。
|
||||
|
||||
---
|
||||
|
||||
## P3: 微小
|
||||
|
||||
### 11. `dbQuote` の容量見積もりなし
|
||||
|
||||
**ファイル:** `libs/postgres/postgres_lib.nim` 182-201行
|
||||
|
||||
```nim
|
||||
proc dbQuote(s: string): string =
|
||||
if s == "null":
|
||||
return "NULL"
|
||||
result = "'"
|
||||
...
|
||||
```
|
||||
|
||||
**問題:** `newStringOfCap` による事前容量確保がない。特殊文字が多い文字列で再アロケーションが発生する。
|
||||
|
||||
**改善案:** `result = newStringOfCap(s.len + 2)` を先頭に追加。
|
||||
|
||||
---
|
||||
|
||||
## 改善優先度まとめ
|
||||
|
||||
| 優先度 | # | 問題 | 影響 | 修正難度 |
|
||||
|---|---|---|---|---|
|
||||
| **P0** | 1 | `exec`/`insertId` が毎回 `information_schema` を問い合わせ + SQL インジェクション | DML ごとに追加1クエリ | 中(キャッシュ + パラメータ化) |
|
||||
| **P1** | 2 | `waiters` の `seq.delete(0)` が O(n) | 高負荷時の性能劣化 | 低(`Deque` に変更) |
|
||||
| **P1** | 3 | `PgWaitState` が `ref object` | 毎回のヒープアロケーション | 低(`object` に変更) |
|
||||
| **P1** | 4 | `waitPgReadable`/`waitPgWritable` が重複 | 保守コスト | 低(統一) |
|
||||
| **P1** | 5 | `pqsocket` 二重呼び出し | 不要な FFI 呼び出し | 低 |
|
||||
| **P2** | 6 | `getTime().toUnix()` の秒精度デッドライン | タイムアウト精度 | 低(`MonoTime` 化) |
|
||||
| **P2** | 7 | `setColumnInfo` が行ごとに全カラム FFI 呼び出し | 行数×カラム数の不要な FFI + アロケーション | 中(構造変更) |
|
||||
| **P2** | 8 | `dbFormat`/`questionToDaller` の char-by-char | 長いクエリで微小な差 | 低 |
|
||||
| **P2** | 9 | クエリ送信ボイラープレート重複 | 保守コスト | 低 |
|
||||
| **P2** | 10 | `fromObjArray` の重複 | 保守コスト | 低 |
|
||||
| **P3** | 11 | `dbQuote` の容量見積もりなし | 微小 | 低 |
|
||||
|
||||
---
|
||||
|
||||
## MariaDB との比較
|
||||
|
||||
| 項目 | MariaDB(修正済み) | PostgreSQL(現状) |
|
||||
|---|---|---|
|
||||
| カラム型キャッシュ | `columnTypeCache` 導入済み | 毎回 `information_schema` を問い合わせ |
|
||||
| ブロッキング ping | 除去済み | `assert db.status == CONNECTION_OK`(FFI なし、問題小) |
|
||||
| `setColumnInfo` 位置 | ループ外に移動済み | ループ内で毎行呼び出し |
|
||||
| waiter データ構造 | `Deque` に変更済み | `seq`(先頭削除 O(n)) |
|
||||
| 待機状態オブジェクト | 値型(`object`)に変更済み | `ref object`(ヒープ割当) |
|
||||
| デッドライン | `MonoTime` に変更済み | `getTime().toUnix()`(秒精度) |
|
||||
| `dbFormat` | セグメント一括 add | char-by-char |
|
||||
| SQL インジェクション | プレースホルダ化済み | 文字列補間のまま |
|
||||
|
||||
**注意:** PostgreSQL の `assert db.status == CONNECTION_OK` は `pqStatus()` がローカルのフィールド読み取りであり、MariaDB の `mysql_ping()` のようなネットワークラウンドトリップは発生しない。そのため、PostgreSQL 側はこの assert の除去は不要。
|
||||
@@ -0,0 +1,773 @@
|
||||
Example: Query Builder for RDB
|
||||
===
|
||||
[back](../../README.md)
|
||||
|
||||
## index
|
||||
<!--ts-->
|
||||
* [Example: Query Builder for RDB](#example-query-builder-for-rdb)
|
||||
* [index](#index)
|
||||
* [Create Connection](#create-connection)
|
||||
* [SELECT](#select)
|
||||
* [return JsonNode](#return-jsonnode)
|
||||
* [return Object](#return-object)
|
||||
* [get](#get)
|
||||
* [first](#first)
|
||||
* [find](#find)
|
||||
* [columns](#columns)
|
||||
* [join](#join)
|
||||
* [where](#where)
|
||||
* [orWhere](#orwhere)
|
||||
* [whereBetween](#wherebetween)
|
||||
* [whereNotBetween](#wherenotbetween)
|
||||
* [whereIn](#wherein)
|
||||
* [whereNotIn](#wherenotin)
|
||||
* [whereNull](#wherenull)
|
||||
* [groupBy_having](#groupby_having)
|
||||
* [orderBy](#orderby)
|
||||
* [limit_offset](#limit_offset)
|
||||
* [paginate](#paginate)
|
||||
* [fastPaginate](#fastpaginate)
|
||||
* [INSERT](#insert)
|
||||
* [Return ID Insert](#return-id-insert)
|
||||
* [UPDATE](#update)
|
||||
* [DELETE](#delete)
|
||||
* [Plain Response](#plain-response)
|
||||
* [Raw SQL](#raw-sql)
|
||||
* [Prepared Statement](#prepared-statement)
|
||||
* [Aggregates](#aggregates)
|
||||
* [Transaction](#transaction)
|
||||
|
||||
<!-- Created by https://github.com/ekalinin/github-markdown-toc -->
|
||||
<!-- Added by: root, at: Mon Jul 17 07:46:13 UTC 2023 -->
|
||||
|
||||
<!--te-->
|
||||
---
|
||||
|
||||
## Create Connection
|
||||
```nim
|
||||
import allographer/connection
|
||||
|
||||
let maxConnections = 95
|
||||
let timeout = 30
|
||||
|
||||
# Using connection URL (Recommended)
|
||||
let rdb = dbOpen(PostgreSQL, "postgresql://user:password@localhost:5432/database", maxConnections, timeout)
|
||||
|
||||
# Optional pool aging controls
|
||||
# let rdb = dbOpen(
|
||||
# PostgreSQL,
|
||||
# "postgresql://user:password@localhost:5432/database",
|
||||
# maxConnections,
|
||||
# timeout,
|
||||
# maxConnectionLifetime = 300,
|
||||
# maxConnectionIdleTime = 300,
|
||||
# )
|
||||
|
||||
# Using positional arguments
|
||||
# let rdb = dbOpen(PostgreSQL, "database", "user", "password", "localhost", 5432, maxConnections, timeout)
|
||||
|
||||
# SQLite
|
||||
# let rdb = dbOpen(SQLite3, "/path/to/db.sqlite3", maxConnections, timeout)
|
||||
# let rdb = dbOpen(SQLite3, ":memory:", maxConnections, timeout)
|
||||
|
||||
# MySQL / MariaDB
|
||||
# let rdb = dbOpen(MySQL, "mysql://user:password@localhost:3306/database", maxConnections, timeout)
|
||||
# let rdb = dbOpen(MariaDB, "mariadb://user:password@localhost:3306/database", maxConnections, timeout)
|
||||
```
|
||||
|
||||
## SELECT
|
||||
[to index](#index)
|
||||
|
||||
When it returns following table
|
||||
|
||||
|id|float|char|datetime|null|is_admin|
|
||||
|---|---|---|---|---|---|
|
||||
|1|3.14|char|2019-01-01 12:00:00.1234||1|
|
||||
|
||||
### return JsonNode
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
echo rdb.table("test")
|
||||
.select("id", "float", "char", "datetime", "null", "is_admin")
|
||||
.get()
|
||||
.await
|
||||
```
|
||||
|
||||
```nim
|
||||
>> @[
|
||||
{
|
||||
"id": 1, # JInt
|
||||
"float": 3.14, # JFloat
|
||||
"char": "char", # JString
|
||||
"datetime": "2019-01-01 12:00:00.1234", # JString
|
||||
"null": null # JNull
|
||||
"is_admin": true # JBool
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### return Object
|
||||
If object is defined and set arg for `orm`, response will be an object as ORM
|
||||
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
type Typ = ref object
|
||||
id: int
|
||||
float: float
|
||||
char: string
|
||||
datetime: string
|
||||
null: string
|
||||
is_admin: bool
|
||||
|
||||
var rows = rdb.table("test")
|
||||
.select("id", "float", "char", "datetime", "null", "is_admin")
|
||||
.get()
|
||||
.orm(Typ)
|
||||
.await
|
||||
```
|
||||
|
||||
```nim
|
||||
echo rows[0].id
|
||||
>> 1 # int
|
||||
|
||||
echo rows[0].float
|
||||
>> 3.14 # float
|
||||
|
||||
echo rows[0].char
|
||||
>> "char" # string
|
||||
|
||||
echo rows[0].datetime
|
||||
>> "2019-01-01 12:00:00.1234" # string
|
||||
|
||||
echo rows[0].null
|
||||
>> "" # string
|
||||
|
||||
echo rows[0].is_admin
|
||||
>> true # bool
|
||||
```
|
||||
|
||||
If DB response is empty, `get` return empty seq, `find` and `first` return optional object.
|
||||
```nim
|
||||
let response = await rdb.table("test").get().orm(Typ).await
|
||||
assert response.len == 0
|
||||
|
||||
let response = await rdb.raw("select * from users").get().orm(Typ).await
|
||||
assert response.len == 0
|
||||
|
||||
let response = await rdb.table("test").find(1).orm(Typ).await
|
||||
assert response.type == Option[Typ]
|
||||
assert response.isSome == false
|
||||
|
||||
let response = await rdb.table("test").first().orm(Typ).await
|
||||
assert response.type == Option[Typ]
|
||||
assert response.isSome == false
|
||||
```
|
||||
|
||||
### get
|
||||
Retrieving all row from a table
|
||||
```nim
|
||||
let users = rdb.table("users").get().await
|
||||
for user in users:
|
||||
echo user["name"]
|
||||
```
|
||||
|
||||
### first
|
||||
Retrieving a single row from a table. This returns `Option[JsonNode]`
|
||||
```nim
|
||||
let user = rdb
|
||||
.table("users")
|
||||
.where("name", "=", "John")
|
||||
.first()
|
||||
.await
|
||||
if user.isSome:
|
||||
echo user.get["name"]
|
||||
```
|
||||
|
||||
### find
|
||||
Retrieve a single row by its primary key. This returns `Option[JsonNode]`
|
||||
```nim
|
||||
let user = rdb.table("users").find(1).await
|
||||
if user.isSome:
|
||||
echo user.get["name"]
|
||||
```
|
||||
|
||||
If the column name of a promary key is not "id", specify this in 2nd arg of `find`
|
||||
```nim
|
||||
let user = rdb.table("users").find(1, "user_id").await
|
||||
if user.isSome:
|
||||
echo user.get["name"]
|
||||
```
|
||||
|
||||
### columns
|
||||
Retrieve columns from table.
|
||||
```nim
|
||||
let columns = rdb.table("users").columns().await
|
||||
columns == @["id", "name", "email"]
|
||||
```
|
||||
|
||||
|
||||
### join
|
||||
```nim
|
||||
let users = rdb
|
||||
.table("users")
|
||||
.select("users.id", "contacts.phone", "orders.price")
|
||||
.join("contacts", "users.id", "=", "contacts.user_id")
|
||||
.join("orders", "users.id", "=", "orders.user_id")
|
||||
.get()
|
||||
.await
|
||||
```
|
||||
|
||||
### where
|
||||
```nim
|
||||
let users = rdb.table("users").where("age", ">", 25).get().await
|
||||
```
|
||||
|
||||
### orWhere
|
||||
```nim
|
||||
let users = rdb
|
||||
.table("users")
|
||||
.where("age", ">", 25)
|
||||
.orWhere("name", "=", "John")
|
||||
.get()
|
||||
.await
|
||||
```
|
||||
|
||||
### whereBetween
|
||||
```nim
|
||||
let users = rdb
|
||||
.table("users")
|
||||
.whereBetween("age", [25, 35])
|
||||
.get()
|
||||
.await
|
||||
```
|
||||
|
||||
### whereNotBetween
|
||||
```nim
|
||||
let users = rdb
|
||||
.table("users")
|
||||
.whereNotBetween("age", [25, 35])
|
||||
.get()
|
||||
.await
|
||||
```
|
||||
|
||||
### whereIn
|
||||
```nim
|
||||
let users = rdb
|
||||
.table("users")
|
||||
.whereIn("id", @[1, 2, 3])
|
||||
.get()
|
||||
.await
|
||||
```
|
||||
|
||||
### whereNotIn
|
||||
```nim
|
||||
let users = rdb
|
||||
.table("users")
|
||||
.whereNotIn("id", @[1, 2, 3])
|
||||
.get()
|
||||
.await
|
||||
```
|
||||
|
||||
### whereNull
|
||||
```nim
|
||||
let users = rdb
|
||||
.table("users")
|
||||
.whereNull("updated_at")
|
||||
.get()
|
||||
.await
|
||||
```
|
||||
|
||||
### groupBy_having
|
||||
```nim
|
||||
let users = rdb
|
||||
.table("users")
|
||||
.group_by("count")
|
||||
.having("count", ">", 100)
|
||||
.get()
|
||||
.await
|
||||
```
|
||||
|
||||
### orderBy
|
||||
```nim
|
||||
let users = rdb
|
||||
.table("users")
|
||||
.orderBy("name", Desc)
|
||||
.get()
|
||||
.await
|
||||
```
|
||||
2nd arg of `orderBy` is Enum. `Desc` or `Asc`
|
||||
|
||||
|
||||
### limit_offset
|
||||
```nim
|
||||
let users = rdb
|
||||
.table("users")
|
||||
.offset(10)
|
||||
.limit(5)
|
||||
.get()
|
||||
.await
|
||||
```
|
||||
|
||||
### paginate
|
||||
```nim
|
||||
rdb.table("users").delete(2).await
|
||||
let users = rdb
|
||||
.table("users")
|
||||
.select("id", "name")
|
||||
.paginate(3, 1)
|
||||
.await
|
||||
```
|
||||
arg1... Numer of items per page
|
||||
arg2... Numer of page(option)(1 is set by default)
|
||||
|
||||
```nim
|
||||
echo users
|
||||
>> {
|
||||
"count":3,
|
||||
"currentPage":[
|
||||
{"id":1,"name":"user1"},
|
||||
{"id":3,"name":"user3"},
|
||||
{"id":4,"name":"user4"}
|
||||
],
|
||||
"hasMorePages":true,
|
||||
"lastPage":3,
|
||||
"nextPage":2,
|
||||
"perPage":3,
|
||||
"previousPage":1,
|
||||
"total":9
|
||||
}
|
||||
```
|
||||
|
||||
|ATTRIBUTE|DESCRIPTION|
|
||||
|---|---|
|
||||
|count|number of results on the current page|
|
||||
|currentPage|results of current page|
|
||||
|hasMorePages|Returns `True` if there is more pages else `False`|
|
||||
|lastPage|The number of the last page|
|
||||
|nextPage|The number of the next page if it exists else equel to lastPage|
|
||||
|perPage|The number of results per page|
|
||||
|previousPage|The number of the previous page if it exists else 1|
|
||||
|total|The total number of results|
|
||||
|
||||
|
||||
### fastPaginate
|
||||
It run faster than `paginate()` because it doesn't use `offset`.
|
||||
|
||||
|sample URL|usage|result items|
|
||||
|---|---|---|
|
||||
|/users?items=5|`fastPaginate(5)`|1,2,3,4,5|
|
||||
|/users?items=5&since=6|`fastPaginateNext(5, 6)`|6,7,8,9,10|
|
||||
|/users?items=5&until=5|`fastPaginateBack(5, 5)`|1,2,3,4,5|
|
||||
|
||||
```nim
|
||||
proc fastPaginate(this:Rdb, display:int, key="id", order:Order=Asc): JsonNode
|
||||
```
|
||||
- display...Numer of items per page.
|
||||
- key...Name of a primary key column (option). default is `id`.
|
||||
- order...Asc or Desc (option). default is `Asc`.
|
||||
|
||||
```nim
|
||||
proc fastPaginateNext(this:Rdb, display:int, id:int, key="id", order:Order=Asc): JsonNode
|
||||
|
||||
proc fastPaginateBack(this:Rdb, display:int, id:int, key="id", order:Order=Asc): JsonNode
|
||||
```
|
||||
- display...Numer of items per page.
|
||||
- id...Value of primary key. It should be larger than 0.
|
||||
- key...Name of a primary key column (option). default is `id`.
|
||||
- order...Asc or Desc (option). default is `Asc`.
|
||||
|
||||
```nim
|
||||
var users = rdb.table("users").select("id", "name").fastPaginate(3).await
|
||||
|
||||
>> {
|
||||
"previousId":0,
|
||||
"hasPreviousId": false,
|
||||
"currentPage":[
|
||||
{"id":1,"name":"user1"},
|
||||
{"id":2,"name":"user2"},
|
||||
{"id":3,"name":"user3"},
|
||||
],
|
||||
"nextId":4,
|
||||
"hasNextId": true
|
||||
}
|
||||
```
|
||||
```nim
|
||||
users = rdb.table("users")
|
||||
.select("id", "name")
|
||||
.fastPaginateNext(3, users["nextId"].getInt)
|
||||
.await
|
||||
|
||||
>> {
|
||||
"previousId":4,
|
||||
"hasPreviousId": true,
|
||||
"currentPage":[
|
||||
{"id":5,"name":"user5"},
|
||||
{"id":6,"name":"user6"},
|
||||
{"id":7,"name":"user7"}
|
||||
],
|
||||
"nextId":8,
|
||||
"hasNextId": true
|
||||
}
|
||||
```
|
||||
```nim
|
||||
users = rdb.table("users")
|
||||
.select("id", "name")
|
||||
.fastPaginateBack(3, users["previousId"].getInt)
|
||||
.await
|
||||
|
||||
>> {
|
||||
"previousId":0,
|
||||
"hasPreviousId": false,
|
||||
"currentPage":[
|
||||
{"id":1,"name":"user1"},
|
||||
{"id":2,"name":"user2"},
|
||||
{"id":3,"name":"user3"},
|
||||
],
|
||||
"nextId":4,
|
||||
"hasNextId": true
|
||||
}
|
||||
```
|
||||
|
||||
order Desc
|
||||
```nim
|
||||
echo rdb.table("users")
|
||||
.select("id", "name")
|
||||
.fastPaginateNext(3, 5, order=Desc)
|
||||
.await
|
||||
|
||||
>> {
|
||||
"previousId":6,
|
||||
"hasPreviousId":true,
|
||||
"currentPage":[
|
||||
{"id":5,"name":"user5"},
|
||||
{"id":4,"name":"user4"},
|
||||
{"id":3,"name":"user3"}
|
||||
],
|
||||
"nextId":2,
|
||||
"hasNextId":true
|
||||
}
|
||||
```
|
||||
|
||||
paginate with `join` and `where`
|
||||
```nim
|
||||
echo rdb.table("users")
|
||||
.select("users.id", "users.name", "users.auth_id")
|
||||
.join("auth", "auth.id", "=", "users.auth_id")
|
||||
.where("auth.id", "=", 2)
|
||||
.fastPaginate(3, key="users.id")
|
||||
.await
|
||||
|
||||
>> {
|
||||
"previousId":0,
|
||||
"hasPreviousId":false,
|
||||
"currentPage":[
|
||||
{"id":4,"name":"user4","auth_id":2},
|
||||
{"id":6,"name":"user6","auth_id":2},
|
||||
{"id":8,"name":"user8","auth_id":2}
|
||||
],
|
||||
"nextId":8,
|
||||
"hasNextId":true
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## INSERT
|
||||
[to index](#index)
|
||||
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
rdb.table("users").insert(%*{
|
||||
"name": "John",
|
||||
"email": "John@gmail.com"
|
||||
})
|
||||
.await
|
||||
|
||||
>> INSERT INTO users (name, email) VALUES ("John", "John@gmail.com")
|
||||
```
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
rdb.table("users").insert(
|
||||
@[
|
||||
%*{"name": "John", "email": "John@gmail.com", "address": "London"},
|
||||
%*{"name": "Paul", "email": "Paul@gmail.com", "address": "London"},
|
||||
%*{"name": "George", "email": "George@gmail.com", "address": "London"},
|
||||
]
|
||||
)
|
||||
.await
|
||||
|
||||
>> INSERT INTO users (name, email, address) VALUES ("John", "John@gmail.com", "London"), ("Paul", "Paul@gmail.com", "London"), ("George", "George@gmail.com", "London")
|
||||
```
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
echo rdb.table("users").insert(
|
||||
@[
|
||||
%*{"name": "John", "email": "John@gmail.com", "address": "London"},
|
||||
%*{"name": "Paul", "email": "Paul@gmail.com", "address": "London"},
|
||||
%*{"name": "George", "birth_date": "1943-02-25", "address": "London"},
|
||||
]
|
||||
)
|
||||
.await
|
||||
|
||||
>> INSERT INTO users (name, email, address) VALUES ("John", "John@gmail.com", "London")
|
||||
>> INSERT INTO users (name, email, address) VALUES ("Paul", "Paul@gmail.com", "London")
|
||||
>> INSERT INTO users (name, birth_date, address) VALUES ("George", "1960-1-1", "London")
|
||||
```
|
||||
|
||||
### Return ID Insert
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
echo rdb.table("users").insertId(%*{
|
||||
"name": "John",
|
||||
"email": "John@gmail.com"
|
||||
})
|
||||
.await
|
||||
|
||||
>> INSERT INTO users (name, email) VALUES ("John", "John@gmail.com")
|
||||
>> 1 # ID of new row is return
|
||||
```
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
echo rdb.table("users").insertId(
|
||||
@[
|
||||
%*{"name": "John", "email": "John@gmail.com", "address": "London"},
|
||||
%*{"name": "Paul", "email": "Paul@gmail.com", "address": "London"},
|
||||
%*{"name": "George", "email": "George@gmail.com", "address": "London"},
|
||||
]
|
||||
)
|
||||
.await
|
||||
|
||||
>> INSERT INTO users (name, email, address) VALUES ("John", "John@gmail.com", "London"), ("Paul", "Paul@gmail.com", "London"), ("George", "George@gmail.com", "London")
|
||||
>> @[1, 2] # Seq of ID of new row is return
|
||||
```
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
echo rdb.table("users").insertId(
|
||||
@[
|
||||
%*{"name": "John", "email": "John@gmail.com", "address": "London"},
|
||||
%*{"name": "Paul", "email": "Paul@gmail.com", "address": "London"},
|
||||
%*{"name": "George", "birth_date": "1943-02-25", "address": "London"},
|
||||
]
|
||||
)
|
||||
.await
|
||||
|
||||
>> INSERT INTO users (name, email, address) VALUES ("John", "John@gmail.com", "London")
|
||||
>> INSERT INTO users (name, email, address) VALUES ("Paul", "Paul@gmail.com", "London")
|
||||
>> INSERT INTO users (name, birth_date, address) VALUES ("George", "1960-1-1", "London")
|
||||
>> @[1, 2, 3] # Seq of ID of new row is return
|
||||
```
|
||||
|
||||
|
||||
## UPDATE
|
||||
[to index](#index)
|
||||
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
rdb
|
||||
.table("users")
|
||||
.where("id", "=", 100)
|
||||
.update(%*{"name": "Mick", "address": "NY"})
|
||||
.await
|
||||
|
||||
>> UPDATE users SET name = "Mick", address = "NY" WHERE id = 100
|
||||
```
|
||||
|
||||
## DELETE
|
||||
[to index](#index)
|
||||
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
rdb
|
||||
.table("users")
|
||||
.delete(1)
|
||||
.await
|
||||
|
||||
>> DELETE FROM users WHERE id = 1
|
||||
```
|
||||
|
||||
If column name of primary key is not exactory "id", you can specify it's name.
|
||||
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
rdb
|
||||
.table("users")
|
||||
.delete(1, key="user_id")
|
||||
.await
|
||||
|
||||
>> DELETE FROM users WHERE user_id = 1
|
||||
```
|
||||
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
rdb
|
||||
.table("users")
|
||||
.where("address", "=", "London")
|
||||
.delete()
|
||||
.await
|
||||
|
||||
>> DELETE FROM users WHERE address = "London"
|
||||
```
|
||||
|
||||
## Plain Response
|
||||
[to index](#INDEX)
|
||||
|
||||
`Plain` response doesn't have it's column name but it run faster than `JsonNode` response
|
||||
|
||||
```nim
|
||||
echo rdb.table("users").get().await
|
||||
>> @[
|
||||
%*{"id": 1, "name": "user1", "email": "user1@gmail.com"},
|
||||
%*{"id": 2, "name": "user2", "email": "user2@gmail.com"},
|
||||
%*{"id": 3, "name": "user3", "email": "user3@gmail.com"}
|
||||
]
|
||||
|
||||
echo rdb.table("users").getPlain().await
|
||||
>> @[
|
||||
@["1", "user1", "user1@gmail.com"],
|
||||
@["2", "user2", "user2@gmail.com"],
|
||||
@["3", "user3", "user3@gmail.com"],
|
||||
]
|
||||
```
|
||||
|
||||
```nim
|
||||
echo rdb.table("users").find(1).await
|
||||
>> %*{"id": 1, "name": "user1", "email": "user1@gmail.com"}
|
||||
|
||||
echo rdb.table("users").findPlain(1).await
|
||||
>> @["1", "user1", "user1@gmail.com"]
|
||||
```
|
||||
|
||||
```nim
|
||||
echo rdb.table("users").first().await
|
||||
>> %*{"id": 1, "name": "user1", "email": "user1@gmail.com"}
|
||||
|
||||
echo rdb.table("users").firstPlain().await
|
||||
>> @["1", "user1", "user1@gmail.com"]
|
||||
```
|
||||
|
||||
## Raw SQL
|
||||
[to index](#INDEX)
|
||||
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
let sql = """
|
||||
SELECT ProductName
|
||||
FROM Product
|
||||
WHERE Id IN (SELECT ProductId
|
||||
FROM OrderItem
|
||||
WHERE Quantity > 100)
|
||||
"""
|
||||
echo rdb.raw(sql).get().await
|
||||
echo rdb.raw(sql).getPlain().await
|
||||
echo rdb.raw(sql).first().await
|
||||
echo rdb.raw(sql).firstPlain().await
|
||||
```
|
||||
```nim
|
||||
let sql = "UPDATE users SET name = ? where id = ?"
|
||||
rdb.raw(sql, "John", "1").exec().await
|
||||
```
|
||||
|
||||
## Prepared Statement
|
||||
[to index](#index)
|
||||
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
let selectStmt = rdb.prepare("""SELECT "id", "name" FROM "users" WHERE "id" = ?""")
|
||||
let updateStmt = rdb.prepare("""UPDATE "users" SET "name" = ? WHERE "id" = ?""")
|
||||
|
||||
let row = await selectStmt.first(@["1"])
|
||||
await updateStmt.exec(@["John", "1"])
|
||||
|
||||
# close() is logical close.
|
||||
await selectStmt.close()
|
||||
await updateStmt.close()
|
||||
```
|
||||
|
||||
SurrealDB では server-side prepare ではなく、`LET` を使った client-side template reuse として同じ API を提供します。
|
||||
|
||||
In PostgreSQL, a context API can be used to ensure multiple operations use the same connection.
|
||||
|
||||
```nim
|
||||
await rdb.withConn(
|
||||
proc(ctx: PostgresPreparedContext): Future[void] {.async.} =
|
||||
discard await selectStmt.first(ctx, @["1"])
|
||||
await updateStmt.exec(ctx, @["John", "1"])
|
||||
)
|
||||
```
|
||||
|
||||
In PostgreSQL, APIs to physically clear the prepared statement cache are also available.
|
||||
|
||||
```nim
|
||||
await rdb.flushStmt("""SELECT "id", "name" FROM "users" WHERE "id" = ?""")
|
||||
await rdb.clearStmtCache()
|
||||
```
|
||||
|
||||
## Aggregates
|
||||
[to index](#index)
|
||||
|
||||
Except of `count`, these functions return `Option` type.
|
||||
|
||||
```nim
|
||||
import allographer/query_builder
|
||||
|
||||
echo rdb.table("users").count().await
|
||||
>> 10 # int
|
||||
|
||||
let response = await rdb.table("users").max("name").await
|
||||
if response.isSome:
|
||||
echo response.get
|
||||
>> "user9" # string
|
||||
|
||||
let response = await rdb.table("users").max("id").await
|
||||
if response.isSome:
|
||||
echo response.get
|
||||
>> "10" # string
|
||||
|
||||
let response = await rdb.table("users").min("name").await
|
||||
if response.isSome:
|
||||
echo response.get
|
||||
>> "user1" # string
|
||||
|
||||
let response = await rdb.table("users").min("id").await
|
||||
if response.isSome:
|
||||
echo response.get
|
||||
>> "1" # string
|
||||
|
||||
let response = await rdb.table("users").avg("id").await
|
||||
if response.isSome:
|
||||
echo response.get
|
||||
>> 5.5 # float
|
||||
|
||||
let response = await rdb.table("users").sum("id").await
|
||||
if response.isSome:
|
||||
echo response.get
|
||||
>> 55.0 # float
|
||||
```
|
||||
|
||||
## Transaction
|
||||
[to index](#index)
|
||||
|
||||
```nim
|
||||
transaction:
|
||||
var user = rdb.table("users").select("id").where("name", "=", "user3").first().await
|
||||
if user.isSome:
|
||||
var id = user.get["id"].getInt()
|
||||
echo id
|
||||
user = rdb.table("users").select("name", "email").find(id).await
|
||||
if user.isSome:
|
||||
echo user.get
|
||||
```
|
||||
If all code in transaction block success, `COMMIT` is run otherwise `ROLLBACK`
|
||||
@@ -0,0 +1,248 @@
|
||||
Example: Schema Builder
|
||||
===
|
||||
[back](../README.md)
|
||||
|
||||
## index
|
||||
<!--ts-->
|
||||
- [Example: Schema Builder](#example-schema-builder)
|
||||
- [index](#index)
|
||||
- [Create table](#create-table)
|
||||
- [Alter Table](#alter-table)
|
||||
- [add column](#add-column)
|
||||
- [change column](#change-column)
|
||||
- [drop column](#drop-column)
|
||||
- [rename table](#rename-table)
|
||||
- [drop table](#drop-table)
|
||||
- [Migration history](#migration-history)
|
||||
- [seeder template](#seeder-template)
|
||||
- [integer](#integer)
|
||||
- [float](#float)
|
||||
- [char](#char)
|
||||
- [date](#date)
|
||||
- [others](#others)
|
||||
- [options](#options)
|
||||
- [Foreign Key Constraints](#foreign-key-constraints)
|
||||
|
||||
<!-- Created by https://github.com/ekalinin/github-markdown-toc -->
|
||||
<!-- Added by: root, at: Mon Jul 17 07:46:21 UTC 2023 -->
|
||||
|
||||
<!--te-->
|
||||
---
|
||||
|
||||
## Create table
|
||||
```nim
|
||||
import allographer/schema_builder
|
||||
import connections # or define rdb here
|
||||
|
||||
rdb.create(
|
||||
table("auth", [
|
||||
Column.increments("id"),
|
||||
Column.uuid("uuid"),
|
||||
Column.string("name"),
|
||||
Column.timestamps()
|
||||
]),
|
||||
table("users", [
|
||||
Column.increments("id"),
|
||||
Column.string("name"),
|
||||
Column.foreign("auth_id").reference("id").onTable("auth").onDelete(SET_NULL),
|
||||
Column.strForeign("uuid").reference("uuid").onTable("auth").onDelete(SET_NULL)
|
||||
])
|
||||
)
|
||||
```
|
||||
|
||||
## Alter Table
|
||||
### add column
|
||||
```nim
|
||||
rdb.alter(
|
||||
table("auth", [
|
||||
Column.string("email").add(),
|
||||
]),
|
||||
table("users", [
|
||||
Column.string("email").unique().default("").add(),
|
||||
Column.foreign("auth_id").reference("id").onTable("auth").onDelete(SET_NULL).add()
|
||||
])
|
||||
)
|
||||
```
|
||||
`>> ALTER TABLE "users" ADD COLUMN 'email' VARCHAR UNIQUE DEFAULT '' CHECK (length("email") <= 255)`
|
||||
|
||||
|
||||
### change column
|
||||
```nim
|
||||
rdb.alter(
|
||||
table("users", [
|
||||
Column.renameColumn("name", "new_name"),
|
||||
Column.char("name", 20).unique().default("").change()
|
||||
])
|
||||
)
|
||||
```
|
||||
- Create new table with new column definition
|
||||
- Rename table which you want to change column
|
||||
- Copy table data from old table to new table
|
||||
- Drop old table
|
||||
|
||||
|
||||
### drop column
|
||||
```nim
|
||||
rdb.alter(
|
||||
table("users", [
|
||||
Column.dropColumn("name")
|
||||
])
|
||||
)
|
||||
```
|
||||
|
||||
### rename table
|
||||
```nim
|
||||
rdb.alter(
|
||||
table("users").renameTo("new_users")
|
||||
)
|
||||
```
|
||||
`>> ALTER TABLE users RENAME TO new_users`
|
||||
|
||||
### drop table
|
||||
```nim
|
||||
rdb.drop(
|
||||
table("users")
|
||||
)
|
||||
```
|
||||
`>> DROP TABLE users`
|
||||
|
||||
## Migration history
|
||||
allographer generate `_allographer_migrations` table in your database. It has migration history data which have hash key generated a query.
|
||||
|
||||
|
||||
```nim
|
||||
# migrate.nim
|
||||
import std/asyncdispatch
|
||||
import std/json
|
||||
import allographer/schema_builder
|
||||
import connections
|
||||
|
||||
rdb.create(
|
||||
table("auth", [
|
||||
Column.increments("id"),
|
||||
Column.string("name")
|
||||
]),
|
||||
table("users", [
|
||||
Column.increments("id"),
|
||||
Column.string("name"),
|
||||
Column.foreign("auth_id").reference("id").onTable("auth").onDelete(SET_NULL)
|
||||
])
|
||||
)
|
||||
|
||||
seeder rdb, "auth":
|
||||
rdb.table("auth").insert(@[
|
||||
%*{"name": "admin"},
|
||||
%*{"name": "member"}
|
||||
])
|
||||
.waitFor
|
||||
|
||||
seeder rdb, "users":
|
||||
var data: seq[JsonNode]
|
||||
for i in 1..10:
|
||||
data.add(%*{
|
||||
"name": &"user{i}",
|
||||
"auth_id": if i mod 2 == 0: 1 else: 2
|
||||
})
|
||||
rdb.table("users").insert(data).waitFor
|
||||
```
|
||||
|
||||
```sh
|
||||
# first time
|
||||
nim c -r migrate
|
||||
>> run query and generate `_allographer_migrations` table
|
||||
# secound time
|
||||
nim c -r migrate
|
||||
>> nothing to do
|
||||
# reset existsing table and run migration again
|
||||
nim c -r migrate --reset
|
||||
or
|
||||
nim c -d:reset -r migrate
|
||||
>> drop table and create table
|
||||
```
|
||||
|
||||
### seeder template
|
||||
The `seeder` block allows the code in the block to work only when the table or specified column is empty.
|
||||
|
||||
```nim
|
||||
template seeder*(rdb:Rdb, tableName:string, body:untyped):untyped
|
||||
|
||||
template seeder*(rdb:Rdb, tableName, column:string, body:untyped):untyped
|
||||
```
|
||||
|
||||
|
||||
## integer
|
||||
|Command|Description|
|
||||
|---|---|
|
||||
|`increments("id")`|Auto-incrementing UNSIGNED INTEGER (primary key) equivalent column.|
|
||||
|`integer("votes")`|INTEGER equivalent column.|
|
||||
|`smallInteger("votes")`|SMALLINT equivalent column|
|
||||
|`mediumInteger("votes")`|MEDIUMINT equivalent column|
|
||||
|`bigInteger("votes")`|UNSIGNED BIGINT equivalent column.|
|
||||
|
||||
## float
|
||||
|Command|Description|
|
||||
|---|---|
|
||||
|`decimal("amount", 8, 2)`|DECIMAL equivalent column with a precision (total digits) and scale (decimal digits).|
|
||||
|`double("amount", 8, 2)`|DOUBLE equivalent column with a precision (total digits) and scale (decimal digits).|
|
||||
|`float("float")`|FLOAT equivalent column with a implicit precision (total digits) and scale (decimal digits).|
|
||||
|
||||
## char
|
||||
|Command|Description|
|
||||
|---|---|
|
||||
|`uuid("name")`|VARCHAR indexed and unique column.|
|
||||
|`char("name", 100)`|CHAR equivalent column with an optional length.|
|
||||
|`string("name")`|VARCHAR equivalent column.|
|
||||
|`string("name", 100)`|VARCHAR equivalent column with a optional length.|
|
||||
|`text("description")`|TEXT equivalent column.|
|
||||
|`mediumText("description")`|MEDIUMTEXT equivalent column.|
|
||||
|`longText("description")`|LONGTEXT equivalent column.|
|
||||
|
||||
## date
|
||||
|Command|Description|
|
||||
|---|---|
|
||||
|`date("created_at")`|DATE equivalent column.|
|
||||
|`datetime("created_at")`|DATETIME equivalent column.|
|
||||
|`time("sunrise")`|TIME equivalent column.|
|
||||
|`timestamp("added_on")`|TIMESTAMP equivalent column.|
|
||||
|`timestamps()`|Adds nullable `created_at` and `updated_at` TIMESTAMP equivalent columns.|
|
||||
|`softDelete()`|Adds a nullable `deleted_at` TIMESTAMP equivalent column for soft deletes.|
|
||||
|
||||
## others
|
||||
|Command|Description|
|
||||
|---|---|
|
||||
|`binary("data")`|BLOB equivalent column.|
|
||||
|`boolean("confirmed")`|BOOLEAN equivalent column.|
|
||||
|`enumField("level", ["easy", "hard"])`|ENUM equivalent column.|
|
||||
|`json("options")`|JSON equivalent column.|
|
||||
|
||||
## options
|
||||
|Command|Description|
|
||||
|---|---|
|
||||
|`.nullable()`|Designate that the column allows NULL values|
|
||||
|`.default(value)`|Declare a default value for a column|
|
||||
|`.unsigned()`|Set INTEGER to UNSIGNED|
|
||||
|`.unique()`|Adding an unique index|
|
||||
|`.index()`|Adding an index|
|
||||
|`.comment("value")`|Adding a comment to a column. Only for Postgres, Mariadb and Mysql|
|
||||
|
||||
## Foreign Key Constraints
|
||||
For example, let's define a `user_id` column on the table that references the `id` column on a `users` table:
|
||||
```nim
|
||||
Column.foreign("user_id")
|
||||
.reference("id")
|
||||
.onTable("users")
|
||||
.onDelete(SET_NULL)
|
||||
|
||||
Column.strForeign("uuid")
|
||||
.reference("uuid")
|
||||
.onTable("users")
|
||||
.onDelete(SET_NULL)
|
||||
|
||||
# use `onTable("users")`
|
||||
```
|
||||
|
||||
arg of `onDelete` is enum
|
||||
- RESTRICT
|
||||
- CASCADE
|
||||
- SET_NULL
|
||||
- NO_ACTION
|
||||
@@ -0,0 +1,222 @@
|
||||
# Surreal driver 効率改善レポート
|
||||
> [!WARNING]
|
||||
> SurrealDB support is planned for v3. The current implementation and proposed improvements are for reference only and will be addressed in v3.
|
||||
|
||||
`src/allographer/query_builder/libs/surreal/` と、その呼び出し元である `models/surreal/` を調査し、`documents/rdb/postgres_efficiency_report.md` と `320-allographerpostgresql-非同期待機改善-設計書.mdc` と同じ観点で、処理効率と待機制御の改善余地を整理した。
|
||||
|
||||
MariaDB / PostgreSQL 側で既に改善済みのポイントと比べると、Surreal 側は以下の 2 点が特に目立つ。
|
||||
|
||||
- 接続プール待機はすでに `Deque` + `MonoTime` ベースで改善済み
|
||||
- 一方で HTTP リクエスト自体の timeout 制御と、初期化時の重複 round-trip は未整理
|
||||
|
||||
---
|
||||
|
||||
## P0: 重大
|
||||
|
||||
### 1. `timeout` 引数が HTTP リクエスト経路で使われていない
|
||||
|
||||
**ファイル:** `src/allographer/query_builder/libs/surreal/surreal_impl.nim` 47-112行
|
||||
|
||||
```nim
|
||||
proc query*(db:SurrealConn, query: string, args: JsonNode, timeout:int):Future[JsonNode] {.async.} =
|
||||
let query = dbFormat(query, args)
|
||||
let resp = db.client.post(&"{db.host}:{db.port}/sql", query).await
|
||||
let body = resp.body().await.parseJson()
|
||||
```
|
||||
|
||||
**問題:**
|
||||
- `query` / `exec` / `info` の全経路で `timeout` が未使用
|
||||
- 遅い HTTP 応答やハングした接続があると、その接続スロットを無期限に占有する
|
||||
- `getFreeConn` 側は `MonoTime` で待機期限を持っていても、実リクエストが返らない限りコネクションは返却されず、スループットが崩れる
|
||||
- PostgreSQL 設計書で整理した「待機制御を driver 内で完結させる」という方針と比べると、Surreal だけ実処理 timeout が抜けている
|
||||
|
||||
**改善案:**
|
||||
- `post()` と `body()` を `withTimeout` で包み、`MonoTime` ベースの deadline へ統一する
|
||||
- もしくは `AsyncHttpClient` 側の timeout 設定を `dbOpen` で明示し、driver 引数 `timeout` と同期させる
|
||||
- エラー整形も共通化し、timeout 時は `DbError` へ寄せる
|
||||
|
||||
---
|
||||
|
||||
## P1: 中程度
|
||||
|
||||
### 2. `dbOpen` が接続数ぶん `/status` と `DEFINE NAMESPACE / DATABASE` を逐次実行している
|
||||
|
||||
**ファイル:** `src/allographer/query_builder/models/surreal/surreal_open.nim` 17-35行
|
||||
|
||||
```nim
|
||||
for i in 0..<maxConnections:
|
||||
let client = newAsyncHttpClient()
|
||||
...
|
||||
var resp = client.get(url).await
|
||||
...
|
||||
resp = client.post(url, &"DEFINE NAMESPACE `{namespace}`; USE NS `{namespace}`; DEFINE DATABASE `{database}`").await
|
||||
```
|
||||
|
||||
**問題:**
|
||||
- 初期化コストが `maxConnections` に比例して直線的に増える
|
||||
- 各接続ごとに `/status` と `DEFINE ...` を逐次実行しており、プール 10 本なら最低 20 回の HTTP round-trip が発生する
|
||||
- `DEFINE NAMESPACE` / `DEFINE DATABASE` はプール内の全接続で毎回やる必要が薄く、重複コストになりやすい
|
||||
- ループが直列なので、起動時間が RTT の影響を強く受ける
|
||||
|
||||
**改善案:**
|
||||
- namespace/database の bootstrap は 1 接続だけで先に済ませ、残りは接続確認だけにする
|
||||
- もしくは初期化を 1 本目の成功後に `allFutures` 系で並列化する
|
||||
- あわせて `Authorization` の Base64 生成や URL 生成もループ外へ寄せる
|
||||
|
||||
### 3. `dbFormat(queryString, args: JsonNode)` が中間文字列を多量に生成する
|
||||
|
||||
**ファイル:** `src/allographer/query_builder/libs/surreal/surreal_lib.nim` 77-118行
|
||||
|
||||
```nim
|
||||
var strArgs: seq[string]
|
||||
...
|
||||
strArgs.add(&"LET ${numToAlphabet(i)} = {$arg.getInt}; ")
|
||||
...
|
||||
result = strArgs.join() & queryPart
|
||||
```
|
||||
|
||||
**問題:**
|
||||
- 各引数ごとに `&` で新しい文字列を作る
|
||||
- `strArgs` に積んだ後で `join()`、最後に `& queryPart` でもう一度連結する
|
||||
- プレースホルダ数が多い bulk insert / update 系ほどアロケーションが増える
|
||||
- MariaDB / PostgreSQL の `dbFormat` 改善と違い、Surreal の `JsonNode` 経路はまだ単一パス化されていない
|
||||
|
||||
**改善案:**
|
||||
- `newStringOfCap` で見積もり確保し、`LET $a = ...; ` を `result` へ直接 `add` する
|
||||
- `strArgs: seq[string]` と `join()` を廃止し、最後に `queryPart` を `add` する
|
||||
- placeholder 名は毎回 `numToAlphabet` で再計算せず、ローカルキャッシュか直接 append する
|
||||
|
||||
### 4. `query` / `exec` / `info` の HTTP 実行ロジックが 6 箇所に重複している
|
||||
|
||||
**ファイル:** `src/allographer/query_builder/libs/surreal/surreal_impl.nim` 47-112行
|
||||
|
||||
```nim
|
||||
let query = dbFormat(query, args)
|
||||
let resp = db.client.post(&"{db.host}:{db.port}/sql", query).await
|
||||
let body = resp.body().await.parseJson()
|
||||
```
|
||||
|
||||
**問題:**
|
||||
- `seq[string]` / `JsonNode` の 2 系統で `query`, `exec`, `info` がほぼ同一実装
|
||||
- timeout 対応、HTTP status 判定、JSON parse、Surreal エラー整形を入れたいときに 6 箇所修正になる
|
||||
- 毎回 `&"{db.host}:{db.port}/sql"` を再生成しており、ホットパスに細かな再アロケーションが残る
|
||||
|
||||
**改善案:**
|
||||
- `runSql(db, sql: string): Future[JsonNode]` のような共通 proc を切り出す
|
||||
- `dbFormat` だけ呼び出し側オーバーロードで吸収し、HTTP 送信・JSON parse・エラー判定は 1 箇所へ寄せる
|
||||
- `SurrealConn` に `sqlUrl` を保持し、ホットパスの `strformat` をなくす
|
||||
|
||||
---
|
||||
|
||||
## P2: 軽微
|
||||
|
||||
### 5. `numToAlphabet` が placeholder ごとに前置連結と `toLower()` を行う
|
||||
|
||||
**ファイル:** `src/allographer/query_builder/libs/surreal/surreal_lib.nim` 44-55行
|
||||
|
||||
```nim
|
||||
result = ""
|
||||
while n > 0:
|
||||
...
|
||||
result = chr(int('A') + remainder) & result
|
||||
...
|
||||
return result.toLower()
|
||||
```
|
||||
|
||||
**問題:**
|
||||
- `& result` による前置連結で、文字数に応じた再アロケーションが発生する
|
||||
- 最後に `toLower()` でもう 1 回文字列を作る
|
||||
- 1 回のコストは小さいが、`dbFormat(JsonNode)` と `questionToDaller()` の両方から頻繁に呼ばれる
|
||||
|
||||
**改善案:**
|
||||
- 小さなバッファへ末尾追加して最後に reverse する
|
||||
- もしくは最初から `'a'` 基準で生成して `toLower()` をなくす
|
||||
|
||||
### 6. `questionToDaller()` が `numToAlphabet()` と組み合わさって placeholder 数ぶん文字列生成を行う
|
||||
|
||||
**ファイル:** `src/allographer/query_builder/libs/surreal/surreal_lib.nim` 58-74行
|
||||
|
||||
```nim
|
||||
if s[j] == '?':
|
||||
...
|
||||
result.add('$')
|
||||
result.add(numToAlphabet(i))
|
||||
```
|
||||
|
||||
**問題:**
|
||||
- 文字列本体はセグメント単位で `add` できており悪くないが、placeholder 名生成が都度アロケーションになる
|
||||
- `find()` 経路では `surreal_exec.nim` 366-369行で毎回 `questionToDaller` を通る
|
||||
|
||||
**改善案:**
|
||||
- `numToAlphabet` 改善とセットで最適化する
|
||||
- placeholder 数が少ないケースでは効果は小さいため、P1 の `dbFormat(JsonNode)` ほど優先度は高くない
|
||||
|
||||
### 7. `getAllRows()` が `JsonNode` 配列を `seq[JsonNode]` へ変換するため、結果件数ぶん追加走査が入る
|
||||
|
||||
**ファイル:** `src/allographer/query_builder/models/surreal/surreal_exec.nim` 121-138行, 184-201行
|
||||
|
||||
```nim
|
||||
let rows = surreal_impl.query(...).await
|
||||
...
|
||||
return rows.toSeq
|
||||
```
|
||||
|
||||
**問題:**
|
||||
- `surreal_impl.query()` はすでに `JsonNode` の配列を返している
|
||||
- `get()` / `insertId(seq)` のたびに `toSeq()` でもう一度全件を走査する
|
||||
- 大量行取得では余分なイテレーションと `seq` 生成が発生する
|
||||
|
||||
**改善案:**
|
||||
- 内部表現を `JsonNode` で統一して必要箇所だけ `seq` 化する
|
||||
- あるいは `surreal_impl.query()` 側で最終的に必要な `seq[JsonNode]` を 1 回だけ組み立てる
|
||||
|
||||
---
|
||||
|
||||
## すでに良い点
|
||||
|
||||
### 1. プール待機は `Deque` + 通知ベースで、PostgreSQL 改善後と同じ方向にそろっている
|
||||
|
||||
**ファイル:** `src/allographer/query_builder/models/surreal/surreal_exec.nim` 24-75行
|
||||
|
||||
- `waiters` は `Deque[Future[void]]`
|
||||
- `wakeOnePoolWaiter()` は `popFirst()` で O(1)
|
||||
- `sleepAsync(10)` ポーリングは使っていない
|
||||
|
||||
### 2. プール待機の deadline は `MonoTime` ベース
|
||||
|
||||
**ファイル:** `src/allographer/query_builder/models/surreal/surreal_exec.nim` 40-69行
|
||||
|
||||
- PostgreSQL 改善設計書で問題視していた wall clock 依存は入っていない
|
||||
- ただし「接続取得待ち」だけで、「HTTP 実行待ち」にはまだ広がっていない
|
||||
|
||||
---
|
||||
|
||||
## 改善優先度まとめ
|
||||
|
||||
| 優先度 | # | 問題 | 影響 | 修正難度 |
|
||||
|---|---|---|---|---|
|
||||
| **P0** | 1 | HTTP リクエストで `timeout` 未使用 | ハング時に接続が返却されず、プール全体が詰まる | 中 |
|
||||
| **P1** | 2 | `dbOpen` が接続数ぶん逐次 bootstrap | 起動時間が `maxConnections` に比例 | 低〜中 |
|
||||
| **P1** | 3 | `dbFormat(JsonNode)` の中間文字列多発 | bulk 系で CPU / アロケーション増 | 中 |
|
||||
| **P1** | 4 | HTTP 実行ロジック 6 箇所重複 | 最適化と timeout 対応の実装コスト増 | 低 |
|
||||
| **P2** | 5 | `numToAlphabet` の前置連結 + `toLower()` | placeholder 多いと微小劣化 | 低 |
|
||||
| **P2** | 6 | `questionToDaller()` の placeholder 名生成 | 微小 | 低 |
|
||||
| **P2** | 7 | `rows.toSeq()` の追加走査 | 大量行取得で余分な走査 | 低〜中 |
|
||||
|
||||
---
|
||||
|
||||
## PostgreSQL / MariaDB との比較
|
||||
|
||||
| 項目 | MariaDB / PostgreSQL | Surreal(現状) |
|
||||
|---|---|---|
|
||||
| プール待機 | `Deque` + 通知ベース | 同様に改善済み |
|
||||
| deadline 管理 | `MonoTime` 利用 | 接続取得待ちは利用、HTTP 実行待ちは未適用 |
|
||||
| 実リクエスト timeout | driver 内で明示設計 | `timeout` 引数が未使用 |
|
||||
| 初期化コスト | DB 接続確立中心 | `/status` + `DEFINE ...` を接続数ぶん HTTP 実行 |
|
||||
| フォーマット最適化 | `dbFormat` 改善済み箇所あり | `JsonNode` 経路は未最適化 |
|
||||
|
||||
## 次に着手するなら
|
||||
|
||||
1. `surreal_impl.nim` に共通 `runSql` ヘルパを作り、timeout・HTTP status・JSON parse・Surreal error 判定を 1 箇所へ寄せる
|
||||
2. `surreal_open.nim` の bootstrap を 1 回化し、残り接続は軽量に初期化する
|
||||
3. `surreal_lib.nim` の `dbFormat(JsonNode)` を単一バッファ書き込みへ変更する
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
## index
|
||||
<!--ts-->
|
||||
* [index](#index)
|
||||
* [JInt](#jint)
|
||||
* [JFloat](#jfloat)
|
||||
* [JBool](#jbool)
|
||||
|
||||
<!-- Created by https://github.com/ekalinin/github-markdown-toc -->
|
||||
<!-- Added by: root, at: Mon Jul 17 07:46:25 UTC 2023 -->
|
||||
|
||||
<!--te-->
|
||||
---
|
||||
|
||||
|allographer|sqlite|mysql|postgres|return type|
|
||||
|---|---|---|---|---|
|
||||
|increments|INTEGER|INT|INTEGER|JInt
|
||||
|integer|INTEGER|INT|INTEGER|JInt
|
||||
|smallInteger|INTEGER|SMALLINT|SMALLINT|JInt
|
||||
|mediumInteger|INTEGER|MEDIUMINT|INTEGER|JInt
|
||||
|bigInteger|INTEGER|BIGINT|BIGINT|JInt
|
||||
|decimal|NUMERIC|DECIMAL|NUMERIC|JFloat
|
||||
|double|NUMERIC|DOUBLE|NUMERIC|JFloat
|
||||
|float|DOUBLE|DOUBLE|NUMERIC|JFloat
|
||||
|char|VARCHAR|CHAR|CHAR|JString
|
||||
|string|VARCHAR|VARCHAR|VARCHAR|JString
|
||||
|uuid|VARCHAR|VARCHAR|VARCHAR|JString
|
||||
|text|TEXT|TEXT|TEXT|JString
|
||||
|mediumText|TEXT|MEDIUMTEXT|TEXT|JString
|
||||
|longText|TEXT|LONGTEXT|TEXT|JString
|
||||
|date|DATE|DATE|DATE|JString
|
||||
|datetime|DATETIME|DATETIME|TIMESTAMP|JString
|
||||
|time|TIME|TIME|TIME|JString
|
||||
|timestamp|DATETIME|DATETIME|TIMESTAMP|JString
|
||||
|timestamps|DATETIME|DATETIME|TIMESTAMP|JString
|
||||
|softDelete|DATETIME|DATETIME|TIMESTAMP|JString
|
||||
|binary|BLOB|BLOB|BYTEA|JString
|
||||
|boolean|TINYINT|TINYINT|BOOLEAN|JBool
|
||||
|enumField|VARCHAR|ENUM|CHARACTER|JString
|
||||
|json|TEXT|JSON|JSON|JObject / JArray *1|
|
||||
|
||||
*1: For SQLite, it returns `JString`. For other databases, it returns parsed `JsonNode` (`JObject` or `JArray`).
|
||||
|foreign|INTEGER|INT|INT|JInt
|
||||
|strForeign|VARCHAR|VARCHAR|VARCHAR|JString
|
||||
|
||||
|
||||
## JInt
|
||||
```
|
||||
["INTEGER", "INT", "SMALLINT", "MEDIUMINT", "BIGINT"]
|
||||
```
|
||||
|
||||
## JFloat
|
||||
```
|
||||
["NUMERIC", "DECIMAL", "DOUBLE"]
|
||||
```
|
||||
|
||||
## JBool
|
||||
```
|
||||
["TINYINT", "BOOLEAN"]
|
||||
```
|
||||
Reference in New Issue
Block a user