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

- 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:
2026-05-21 15:49:11 +03:00
parent 2d310a33a1
commit 6bfc5b3a3c
654 changed files with 109419 additions and 0 deletions
@@ -0,0 +1,28 @@
import ./env
import ./query_builder/libs/database_url
export DatabaseUrl, DatabaseUrlQuery, ParsedDatabaseUrl, asDatabaseUrl, parseDatabaseUrl, databaseName, sqliteDatabasePath, portOrDefault, requireDatabaseUrlScheme
when isExistsSqlite:
import ./query_builder/models/sqlite/sqlite_types; export SQLite3, SqliteConnections, SqlitePreparedContext
import ./query_builder/models/sqlite/sqlite_open; export sqlite_open
when isExistsPostgres:
import ./query_builder/models/postgres/postgres_types; export PostgreSQL, PostgresConnections, PostgresPreparedContext
import ./query_builder/models/postgres/postgres_open; export postgres_open
when isExistsMariadb:
import ./query_builder/models/mariadb/mariadb_types; export MariaDB, MariadbConnections, MariadbPreparedContext
import ./query_builder/models/mariadb/mariadb_open; export mariadb_open
when isExistsMysql:
import ./query_builder/models/mysql/mysql_types; export MySql, MysqlConnections, MysqlPreparedContext
import ./query_builder/models/mysql/mysql_open; export mysql_open
when isExistsSurrealdb:
import ./query_builder/models/surreal/surreal_types; export SurrealDB, SurrealConnections, SurrealPreparedContext
import ./query_builder/models/surreal/surreal_open; export surreal_open
when isExistsBaradb:
import ./query_builder/models/baradb/baradb_types; export Baradb, BaradbConnections, BaradbPreparedContext
import ./query_builder/models/baradb/baradb_open; export baradb_open
@@ -0,0 +1,27 @@
import std/os
import std/strutils
import std/streams
import std/parsecfg
for f in walkDir(getCurrentDir()):
if f.path.split("/")[^1] == ".env":
let path = getCurrentDir() / ".env"
var f = newFileStream(path, fmRead)
var p: CfgParser
open(p, f, path)
while true:
let e = next(p)
case e.kind
of cfgEof: break
of cfgKeyValuePair: putEnv(e.key, e.value)
else: discard
break
const
isExistsSqlite* = getEnv("DB_SQLITE", $false).parseBool
isExistsPostgres* = getEnv("DB_POSTGRES", $false).parseBool
isExistsMysql* = getEnv("DB_MYSQL", $false).parseBool
isExistsMariadb* = getEnv("DB_MARIADB", $false).parseBool
isExistsSurrealdb* = getEnv("DB_SURREAL", $false).parseBool
isExistsBaradb* = getEnv("DB_BARADB", $false).parseBool
@@ -0,0 +1,41 @@
import ./env
import ./query_builder/enums; export enums
import ./query_builder/error; export error
import ./query_builder/models/orm; export orm
when isExistsSqlite:
import ./query_builder/models/sqlite/sqlite_types; export SQLite3, SqliteConnections, SqlitePreparedContext, sqlite_types
import ./query_builder/models/sqlite/sqlite_query; export sqlite_query
import ./query_builder/models/sqlite/sqlite_exec; export sqlite_exec
import ./query_builder/models/sqlite/sqlite_transaction; export sqlite_transaction
when isExistsPostgres:
import ./query_builder/models/postgres/postgres_types; export postgres_types
import ./query_builder/models/postgres/postgres_query; export postgres_query
import ./query_builder/models/postgres/postgres_exec; export postgres_exec
import ./query_builder/models/postgres/poatgres_transaction; export poatgres_transaction
when isExistsMariadb:
import ./query_builder/models/mariadb/mariadb_types; export mariadb_types
import ./query_builder/models/mariadb/mariadb_query; export mariadb_query
import ./query_builder/models/mariadb/mariadb_exec; export mariadb_exec
import ./query_builder/models/mariadb/mariadb_transaction; export mariadb_transaction
when isExistsMysql:
import ./query_builder/models/mysql/mysql_types; export mysql_types
import ./query_builder/models/mysql/mysql_query; export mysql_query
import ./query_builder/models/mysql/mysql_exec; export mysql_exec
import ./query_builder/models/mysql/mysql_transaction; export mysql_transaction
when isExistsSurrealdb:
import ./query_builder/models/surreal/surreal_types; export surreal_types
import ./query_builder/models/surreal/surreal_query; export surreal_query
import ./query_builder/models/surreal/surreal_exec; export surreal_exec
when isExistsBaradb:
import ./query_builder/models/baradb/baradb_types; export baradb_types
import ./query_builder/models/baradb/baradb_query; export baradb_query
import ./query_builder/models/baradb/baradb_exec; export baradb_exec
import ./query_builder/models/baradb/baradb_transaction; export baradb_transaction
Binary file not shown.
@@ -0,0 +1,8 @@
type Order* = enum
Asc = "ASC"
Desc = "DESC"
type Collation* = enum
None = ""
Collate = "COLLATE"
Numeric = "NUMERIC"
@@ -0,0 +1,9 @@
type
DbError* = object of IOError ## exception that is raised if a database error occurs
proc dbError*(msg: string) {.noreturn, noinline.} =
## raises an DbError exception with message `msg`.
var e: ref DbError
new(e)
e.msg = msg
raise e
@@ -0,0 +1,706 @@
## BaraDB Client — Self-contained Nim client library
## No dependency on BaraDB server code.
## Communicates via the BaraDB Wire Protocol (binary, big-endian).
import std/asyncdispatch
import std/asyncnet
import std/net as netmod
import std/locks
import std/strutils
import std/endians
# === Wire Protocol (self-contained, no server dependency) ===
const
ProtocolMagic* = 0x42415241'u32
type
FieldKind* = enum
fkNull = 0x00
fkBool = 0x01
fkInt8 = 0x02
fkInt16 = 0x03
fkInt32 = 0x04
fkInt64 = 0x05
fkFloat32 = 0x06
fkFloat64 = 0x07
fkString = 0x08
fkBytes = 0x09
fkArray = 0x0A
fkObject = 0x0B
fkVector = 0x0C
fkJson = 0x0D
MsgKind* = enum
# Client messages
mkClientHandshake = 0x01
mkQuery = 0x02
mkQueryParams = 0x03
mkExecute = 0x04
mkBatch = 0x05
mkTransaction = 0x06
mkClose = 0x07
mkPing = 0x08
mkAuth = 0x09
# Server messages
mkServerHandshake = 0x80
mkReady = 0x81
mkData = 0x82
mkComplete = 0x83
mkError = 0x84
mkAuthChallenge = 0x85
mkAuthOk = 0x86
mkSchemaChange = 0x87
mkPong = 0x88
mkTransactionState = 0x89
ResultFormat* = enum
rfBinary = 0x00
rfJson = 0x01
rfText = 0x02
WireValue* = object
case kind*: FieldKind
of fkNull: discard
of fkBool: boolVal*: bool
of fkInt8: int8Val*: int8
of fkInt16: int16Val*: int16
of fkInt32: int32Val*: int32
of fkInt64: int64Val*: int64
of fkFloat32: float32Val*: float32
of fkFloat64: float64Val*: float64
of fkString: strVal*: string
of fkBytes: bytesVal*: seq[byte]
of fkArray: arrayVal*: seq[WireValue]
of fkObject: objVal*: seq[(string, WireValue)]
of fkVector: vecVal*: seq[float32]
of fkJson: jsonVal*: string
proc writeUint32(buf: var seq[byte], val: uint32) =
var bytes: array[4, byte]
bigEndian32(addr bytes, unsafeAddr val)
buf.add(bytes)
proc writeUint64(buf: var seq[byte], val: uint64) =
var bytes: array[8, byte]
bigEndian64(addr bytes, unsafeAddr val)
buf.add(bytes)
proc writeString(buf: var seq[byte], s: string) =
buf.writeUint32(uint32(s.len))
for ch in s:
buf.add(byte(ch))
proc readUint32(buf: openArray[byte], pos: var int): uint32 =
var bytes: array[4, byte]
for i in 0..3: bytes[i] = buf[pos + i]
bigEndian32(addr result, unsafeAddr bytes)
pos += 4
proc readUint64(buf: openArray[byte], pos: var int): uint64 =
var bytes: array[8, byte]
for i in 0..7: bytes[i] = buf[pos + i]
bigEndian64(addr result, unsafeAddr bytes)
pos += 8
proc readString(buf: openArray[byte], pos: var int): string =
let len = int(readUint32(buf, pos))
result = newString(len)
for i in 0..<len:
result[i] = char(buf[pos + i])
pos += len
proc toBytes(s: string): seq[byte] =
result = newSeq[byte](s.len)
for i, c in s:
result[i] = byte(c)
proc toString(s: seq[byte]): string =
result = newString(s.len)
for i, b in s:
result[i] = char(b)
proc serializeValue*(buf: var seq[byte], val: WireValue) =
buf.add(byte(val.kind))
case val.kind
of fkNull: discard
of fkBool: buf.add(if val.boolVal: 1'u8 else: 0'u8)
of fkInt8: buf.add(uint8(val.int8Val))
of fkInt16:
var bytes16: array[2, byte]
bigEndian16(addr bytes16, unsafeAddr val.int16Val)
buf.add(bytes16)
of fkInt32: buf.writeUint32(uint32(val.int32Val))
of fkInt64: buf.writeUint64(uint64(val.int64Val))
of fkFloat32:
var bytes32: array[4, byte]
copyMem(addr bytes32, unsafeAddr val.float32Val, 4)
buf.add(bytes32)
of fkFloat64:
var bytes: array[8, byte]
copyMem(addr bytes, unsafeAddr val.float64Val, 8)
buf.add(bytes)
of fkString: buf.writeString(val.strVal)
of fkBytes:
buf.writeUint32(uint32(val.bytesVal.len))
buf.add(val.bytesVal)
of fkArray:
buf.writeUint32(uint32(val.arrayVal.len))
for item in val.arrayVal:
buf.serializeValue(item)
of fkObject:
buf.writeUint32(uint32(val.objVal.len))
for (name, item) in val.objVal:
buf.writeString(name)
buf.serializeValue(item)
of fkVector:
buf.writeUint32(uint32(val.vecVal.len))
for f in val.vecVal:
var fb: array[4, byte]
copyMem(addr fb, unsafeAddr f, 4)
buf.add(fb)
of fkJson: buf.writeString(val.jsonVal)
proc deserializeValue*(buf: openArray[byte], pos: var int): WireValue =
let kind = FieldKind(buf[pos])
inc pos
case kind
of fkNull: result = WireValue(kind: fkNull)
of fkBool:
result = WireValue(kind: fkBool, boolVal: buf[pos] != 0)
inc pos
of fkInt8:
result = WireValue(kind: fkInt8, int8Val: cast[int8](buf[pos]))
inc pos
of fkInt16:
var bytes16: array[2, byte]
for i in 0..1: bytes16[i] = buf[pos + i]
var v16: int16
bigEndian16(addr v16, unsafeAddr bytes16)
result = WireValue(kind: fkInt16, int16Val: v16)
pos += 2
of fkInt32:
result = WireValue(kind: fkInt32, int32Val: int32(readUint32(buf, pos)))
of fkInt64:
result = WireValue(kind: fkInt64, int64Val: int64(readUint64(buf, pos)))
of fkFloat32:
var v32: float32
copyMem(addr v32, addr buf[pos], 4)
result = WireValue(kind: fkFloat32, float32Val: v32)
pos += 4
of fkFloat64:
var v: float64
copyMem(addr v, addr buf[pos], 8)
result = WireValue(kind: fkFloat64, float64Val: v)
pos += 8
of fkString:
result = WireValue(kind: fkString, strVal: readString(buf, pos))
of fkBytes:
let blen = int(readUint32(buf, pos))
var bval: seq[byte] = @[]
for i in 0..<blen:
bval.add(buf[pos + i])
result = WireValue(kind: fkBytes, bytesVal: bval)
pos += blen
of fkArray:
let count = int(readUint32(buf, pos))
var arr: seq[WireValue] = @[]
for i in 0..<count:
arr.add(deserializeValue(buf, pos))
result = WireValue(kind: fkArray, arrayVal: arr)
of fkObject:
let count = int(readUint32(buf, pos))
var obj: seq[(string, WireValue)] = @[]
for i in 0..<count:
let name = readString(buf, pos)
let val = deserializeValue(buf, pos)
obj.add((name, val))
result = WireValue(kind: fkObject, objVal: obj)
of fkVector:
let dim = int(readUint32(buf, pos))
var vec: seq[float32] = @[]
for i in 0..<dim:
var fv: float32
copyMem(addr fv, addr buf[pos], 4)
vec.add(fv)
pos += 4
result = WireValue(kind: fkVector, vecVal: vec)
of fkJson:
result = WireValue(kind: fkJson, jsonVal: readString(buf, pos))
proc buildMessage*(kind: MsgKind, requestId: uint32, payload: seq[byte]): seq[byte] =
result = @[]
result.writeUint32(uint32(kind))
result.writeUint32(uint32(payload.len))
result.writeUint32(requestId)
result.add(payload)
proc makeQueryMessage*(requestId: uint32, query: string): seq[byte] =
var payload: seq[byte] = @[]
payload.writeString(query)
payload.add(byte(rfBinary))
buildMessage(mkQuery, requestId, payload)
proc makeQueryParamsMessage*(requestId: uint32, query: string, params: seq[WireValue]): seq[byte] =
var payload: seq[byte] = @[]
payload.writeString(query)
payload.add(byte(rfBinary))
payload.writeUint32(uint32(params.len))
for p in params:
payload.serializeValue(p)
buildMessage(mkQueryParams, requestId, payload)
proc makeAuthMessage*(requestId: uint32, token: string): seq[byte] =
var payload: seq[byte] = @[]
payload.writeString(token)
buildMessage(mkAuth, requestId, payload)
# === Client Library ===
type
ClientConfig* = object
host*: string
port*: int
database*: string
username*: string
password*: string
timeoutMs*: int
maxRetries*: int
QueryResult* = object
columns*: seq[string]
columnTypes*: seq[string]
rows*: seq[seq[string]]
rowCount*: int
affectedRows*: int
executionTimeMs*: float64
BaraClient* = ref object
config: ClientConfig
socket: AsyncSocket
connected: bool
requestId: uint32
proc defaultConfig*(): ClientConfig =
ClientConfig(
host: "127.0.0.1", port: 9472, database: "default",
username: "admin", password: "", timeoutMs: 30000, maxRetries: 3,
)
proc newClient*(config: ClientConfig = defaultConfig()): BaraClient =
BaraClient(config: config, socket: newAsyncSocket(), connected: false, requestId: 0)
proc connect*(client: BaraClient) {.async.} =
await client.socket.connect(client.config.host, Port(client.config.port))
client.connected = true
proc nextId(client: BaraClient): uint32 =
inc client.requestId; client.requestId
proc close*(client: BaraClient) =
if client.connected:
try:
let msg = buildMessage(mkClose, client.nextId(), @[])
waitFor client.socket.send(toString(msg))
except: discard
client.socket.close()
client.connected = false
proc isConnected*(client: BaraClient): bool = client.connected
proc wireValueToString*(wv: WireValue): string =
case wv.kind
of fkNull: return ""
of fkBool: return if wv.boolVal: "true" else: "false"
of fkInt8: return $wv.int8Val
of fkInt16: return $wv.int16Val
of fkInt32: return $wv.int32Val
of fkInt64: return $wv.int64Val
of fkFloat32: return $wv.float32Val
of fkFloat64: return $wv.float64Val
of fkString: return wv.strVal
of fkBytes: return "<bytes:" & $wv.bytesVal.len & ">"
of fkArray: return "<array:" & $wv.arrayVal.len & ">"
of fkObject: return "<object:" & $wv.objVal.len & ">"
of fkVector: return "<vector:" & $wv.vecVal.len & ">"
of fkJson: return wv.jsonVal
proc readQueryResponse(client: BaraClient): Future[QueryResult] {.async.} =
let headerData = await client.socket.recv(12)
if headerData.len < 12:
raise newException(IOError, "Connection closed")
var pos = 0
let hdrData = toBytes(headerData)
let kind = MsgKind(readUint32(hdrData, pos))
let payloadLen = int(readUint32(hdrData, pos))
discard readUint32(hdrData, pos)
let payloadStr = await client.socket.recv(payloadLen)
var payload = toBytes(payloadStr)
result = QueryResult(columns: @[], rows: @[], rowCount: 0, affectedRows: 0)
if kind == mkReady:
return
if kind == mkError and payload.len >= 8:
var epos = 0
let code = readUint32(payload, epos)
let emsg = readString(payload, epos)
raise newException(IOError, "Error " & $code & ": " & emsg)
if kind == mkData:
var dpos = 0
let colCount = int(readUint32(payload, dpos))
var cols: seq[string] = @[]
for i in 0..<colCount:
cols.add(readString(payload, dpos))
result.columns = cols
var colTypes: seq[string] = @[]
for i in 0..<colCount:
colTypes.add($FieldKind(payload[dpos]))
inc dpos
result.columnTypes = colTypes
let rowCount = int(readUint32(payload, dpos))
for r in 0..<rowCount:
var row: seq[string] = @[]
for c in 0..<colCount:
let wv = deserializeValue(payload, dpos)
row.add(wireValueToString(wv))
result.rows.add(row)
result.rowCount = rowCount
# Read following mkComplete message
let compHeader = await client.socket.recv(12)
if compHeader.len >= 12:
var chPos = 0
let chData = toBytes(compHeader)
let compKind = MsgKind(readUint32(chData, chPos))
let compLen = int(readUint32(chData, chPos))
discard readUint32(chData, chPos)
let compPayloadStr = await client.socket.recv(compLen)
if compKind == mkComplete:
var cpPos = 0
result.affectedRows = int(readUint32(toBytes(compPayloadStr), cpPos))
return
if kind == mkComplete:
var rpos = 0
result.affectedRows = int(readUint32(payload, rpos))
return
proc query*(client: BaraClient, sql: string): Future[QueryResult] {.async.} =
if not client.connected:
raise newException(IOError, "Not connected")
let msg = makeQueryMessage(client.nextId(), sql)
let msgStr = toString(msg)
await client.socket.send(msgStr)
return await client.readQueryResponse()
proc query*(client: BaraClient, sql: string, params: seq[WireValue]): Future[QueryResult] {.async.} =
if not client.connected:
raise newException(IOError, "Not connected")
let msg = makeQueryParamsMessage(client.nextId(), sql, params)
let msgStr = toString(msg)
await client.socket.send(msgStr)
return await client.readQueryResponse()
proc exec*(client: BaraClient, sql: string): Future[int] {.async.} =
let qr = await client.query(sql)
return qr.affectedRows
proc auth*(client: BaraClient, token: string) {.async.} =
if not client.connected:
raise newException(IOError, "Not connected")
let msg = makeAuthMessage(client.nextId(), token)
let msgStr = toString(msg)
await client.socket.send(msgStr)
let headerData = await client.socket.recv(12)
if headerData.len < 12:
raise newException(IOError, "Connection closed")
var pos = 0
let hdrData = toBytes(headerData)
let kind = MsgKind(readUint32(hdrData, pos))
let payloadLen = int(readUint32(hdrData, pos))
discard readUint32(hdrData, pos)
if kind == mkAuthOk:
return
elif kind == mkError:
let payloadStr = await client.socket.recv(payloadLen)
var epos = 0
let emsg = readString(toBytes(payloadStr), epos)
raise newException(IOError, "Auth failed: " & emsg)
else:
raise newException(IOError, "Unexpected auth response: 0x" & toHex(uint32(kind), 2))
proc ping*(client: BaraClient): Future[bool] {.async.} =
if not client.connected:
return false
let msg = buildMessage(mkPing, client.nextId(), @[])
let msgStr = toString(msg)
await client.socket.send(msgStr)
let headerData = await client.socket.recv(12)
if headerData.len < 12:
return false
var pos = 0
let hdrData = toBytes(headerData)
let kind = MsgKind(readUint32(hdrData, pos))
return kind == mkPong
# === Fluent Query Builder ===
type
QueryBuilder* = ref object
client: BaraClient
selectCols: seq[string]
fromTable: string
whereClauses: seq[string]
joinClauses: seq[string]
groupByCols: seq[string]
havingClause: string
orderCols: seq[string]
orderDirs: seq[string]
limitVal: int
offsetVal: int
proc newQueryBuilder*(client: BaraClient): QueryBuilder =
QueryBuilder(client: client, limitVal: 0, offsetVal: 0)
proc select*(qb: QueryBuilder, cols: varargs[string]): QueryBuilder =
for c in cols: qb.selectCols.add(c)
return qb
proc `from`*(qb: QueryBuilder, table: string): QueryBuilder =
qb.fromTable = table
return qb
proc where*(qb: QueryBuilder, clause: string): QueryBuilder =
qb.whereClauses.add(clause)
return qb
proc join*(qb: QueryBuilder, table: string, on: string): QueryBuilder =
qb.joinClauses.add("JOIN " & table & " ON " & on)
return qb
proc leftJoin*(qb: QueryBuilder, table: string, on: string): QueryBuilder =
qb.joinClauses.add("LEFT JOIN " & table & " ON " & on)
return qb
proc groupBy*(qb: QueryBuilder, cols: varargs[string]): QueryBuilder =
for c in cols: qb.groupByCols.add(c)
return qb
proc having*(qb: QueryBuilder, clause: string): QueryBuilder =
qb.havingClause = clause
return qb
proc orderBy*(qb: QueryBuilder, col: string, dir: string = "ASC"): QueryBuilder =
qb.orderCols.add(col)
qb.orderDirs.add(dir)
return qb
proc limit*(qb: QueryBuilder, n: int): QueryBuilder =
qb.limitVal = n
return qb
proc offset*(qb: QueryBuilder, n: int): QueryBuilder =
qb.offsetVal = n
return qb
proc build*(qb: QueryBuilder): string =
result = "SELECT " & (if qb.selectCols.len > 0: qb.selectCols.join(", ") else: "*")
result &= " FROM " & qb.fromTable
for j in qb.joinClauses: result &= " " & j
if qb.whereClauses.len > 0: result &= " WHERE " & qb.whereClauses.join(" AND ")
if qb.groupByCols.len > 0: result &= " GROUP BY " & qb.groupByCols.join(", ")
if qb.havingClause.len > 0: result &= " HAVING " & qb.havingClause
if qb.orderCols.len > 0:
result &= " ORDER BY "
for i, col in qb.orderCols:
if i > 0: result &= ", "
result &= col & " " & qb.orderDirs[i]
if qb.limitVal > 0: result &= " LIMIT " & $qb.limitVal
if qb.offsetVal > 0: result &= " OFFSET " & $qb.offsetVal
proc exec*(qb: QueryBuilder): Future[QueryResult] {.async.} =
return await qb.client.query(qb.build())
# === Blocking Sync Client (production-grade, no waitFor) ===
type
SyncClient* = ref object
config: ClientConfig
socket: netmod.Socket
connected: bool
requestId: uint32
lock: Lock
proc newSyncClient*(config: ClientConfig = defaultConfig()): SyncClient =
result = SyncClient(config: config, connected: false, requestId: 0)
result.socket = netmod.newSocket()
initLock(result.lock)
proc recvExact(sock: netmod.Socket, size: int): string =
result = ""
while result.len < size:
let chunk = sock.recv(size - result.len)
if chunk.len == 0:
raise newException(IOError, "Connection closed")
result.add(chunk)
proc readQueryResponseBlocking(client: SyncClient): QueryResult =
let headerData = client.socket.recvExact(12)
var pos = 0
let hdrData = toBytes(headerData)
let kind = MsgKind(readUint32(hdrData, pos))
let payloadLen = int(readUint32(hdrData, pos))
discard readUint32(hdrData, pos)
let payloadStr = client.socket.recvExact(payloadLen)
var payload = toBytes(payloadStr)
result = QueryResult(columns: @[], rows: @[], rowCount: 0, affectedRows: 0)
if kind == mkReady:
return
if kind == mkError and payload.len >= 8:
var epos = 0
let code = readUint32(payload, epos)
let emsg = readString(payload, epos)
raise newException(IOError, "Error " & $code & ": " & emsg)
if kind == mkData:
var dpos = 0
let colCount = int(readUint32(payload, dpos))
var cols: seq[string] = @[]
for i in 0..<colCount:
cols.add(readString(payload, dpos))
result.columns = cols
var colTypes: seq[string] = @[]
for i in 0..<colCount:
colTypes.add($FieldKind(payload[dpos]))
inc dpos
result.columnTypes = colTypes
let rowCount = int(readUint32(payload, dpos))
for r in 0..<rowCount:
var row: seq[string] = @[]
for c in 0..<colCount:
let wv = deserializeValue(payload, dpos)
row.add(wireValueToString(wv))
result.rows.add(row)
result.rowCount = rowCount
# Read following mkComplete message
let compHeader = client.socket.recvExact(12)
var chPos = 0
let chData = toBytes(compHeader)
let compKind = MsgKind(readUint32(chData, chPos))
let compLen = int(readUint32(chData, chPos))
discard readUint32(chData, chPos)
let compPayloadStr = client.socket.recvExact(compLen)
if compKind == mkComplete:
var cpPos = 0
result.affectedRows = int(readUint32(toBytes(compPayloadStr), cpPos))
return
if kind == mkComplete:
var rpos = 0
result.affectedRows = int(readUint32(payload, rpos))
return
proc connect*(client: SyncClient) =
netmod.connect(client.socket, client.config.host, Port(client.config.port))
client.connected = true
proc close*(client: SyncClient) =
if client.connected:
try:
let msg = buildMessage(mkClose, 0, @[])
netmod.send(client.socket, toString(msg))
except: discard
netmod.close(client.socket)
client.connected = false
deinitLock(client.lock)
proc query*(client: SyncClient, sql: string): QueryResult =
acquire(client.lock)
try:
if not client.connected:
raise newException(IOError, "Not connected")
let msg = makeQueryMessage(0, sql)
netmod.send(client.socket, toString(msg))
return readQueryResponseBlocking(client)
finally:
release(client.lock)
proc query*(client: SyncClient, sql: string, params: seq[WireValue]): QueryResult =
acquire(client.lock)
try:
if not client.connected:
raise newException(IOError, "Not connected")
let msg = makeQueryParamsMessage(0, sql, params)
netmod.send(client.socket, toString(msg))
return readQueryResponseBlocking(client)
finally:
release(client.lock)
proc exec*(client: SyncClient, sql: string): int =
let qr = client.query(sql)
return qr.affectedRows
proc auth*(client: SyncClient, token: string) =
acquire(client.lock)
try:
if not client.connected:
raise newException(IOError, "Not connected")
let msg = makeAuthMessage(0, token)
netmod.send(client.socket, toString(msg))
let headerData = client.socket.recvExact(12)
var pos = 0
let hdrData = toBytes(headerData)
let kind = MsgKind(readUint32(hdrData, pos))
let payloadLen = int(readUint32(hdrData, pos))
discard readUint32(hdrData, pos)
if kind == mkAuthOk:
return
elif kind == mkError:
let payloadStr = client.socket.recvExact(payloadLen)
var epos = 0
let emsg = readString(toBytes(payloadStr), epos)
raise newException(IOError, "Auth failed: " & emsg)
else:
raise newException(IOError, "Unexpected auth response")
finally:
release(client.lock)
proc ping*(client: SyncClient): bool =
acquire(client.lock)
try:
if not client.connected:
return false
let msg = buildMessage(mkPing, 0, @[])
netmod.send(client.socket, toString(msg))
let headerData = client.socket.recvExact(12)
var pos = 0
let hdrData = toBytes(headerData)
let kind = MsgKind(readUint32(hdrData, pos))
return kind == mkPong
except:
return false
finally:
release(client.lock)
proc `$`*(qr: QueryResult): string =
if qr.columns.len == 0: return "(no results)"
result = ""
for i, col in qr.columns:
result &= col
if i < qr.columns.len - 1: result &= ", "
result &= "\n"
for row in qr.rows:
result &= row.join(", ") & "\n"
result &= "(" & $qr.rowCount & " rows)"
@@ -0,0 +1,101 @@
import std/strutils
import std/uri
type DatabaseUrl* = distinct string
type DatabaseUrlQuery* = tuple[key, value: string]
type ParsedDatabaseUrl* = object
raw*: string
scheme*: string
username*: string
password*: string
hostname*: string
port*: int
hasPort*: bool
path*: string
query*: seq[DatabaseUrlQuery]
proc asDatabaseUrl*(value: string): DatabaseUrl =
DatabaseUrl(value)
proc `$`*(value: DatabaseUrl): string =
string(value)
proc parseQueryPairs(query: string): seq[DatabaseUrlQuery] =
if query.len == 0:
return
for item in query.split('&'):
if item.len == 0:
continue
let eqIndex = item.find('=')
if eqIndex < 0:
result.add((decodeUrl(item), ""))
else:
result.add((decodeUrl(item[0..<eqIndex]), decodeUrl(item[eqIndex + 1 .. ^1])))
proc parseDatabaseUrl*(value: string): ParsedDatabaseUrl =
let parsed = parseUri(value)
result.raw = value
result.scheme = parsed.scheme.toLowerAscii()
result.username = decodeUrl(parsed.username)
result.password = decodeUrl(parsed.password)
result.hostname = decodeUrl(parsed.hostname)
result.path = decodeUrl(parsed.path)
result.query = parseQueryPairs(parsed.query)
if parsed.port.len > 0:
result.port = parsed.port.parseInt()
result.hasPort = true
proc parseDatabaseUrl*(value: DatabaseUrl): ParsedDatabaseUrl =
parseDatabaseUrl(string(value))
proc requireDatabaseUrlScheme*(value: ParsedDatabaseUrl; expectedSchemes: openArray[string];
driverName: string) =
if value.scheme.len == 0 or value.scheme notin expectedSchemes:
raise newException(
ValueError,
"Invalid URL format. Expected a " & driverName & " URL starting with '" &
expectedSchemes[0] & "://'."
)
proc stripLeadingSlash(value: string): string =
if value.len > 0 and value[0] == '/':
if value.len == 1:
return ""
return value[1..^1]
value
proc databaseName*(value: ParsedDatabaseUrl): string =
stripLeadingSlash(value.path)
proc sqliteDatabasePath*(value: ParsedDatabaseUrl): string =
if value.hostname.len == 0:
return value.path
let tail = stripLeadingSlash(value.path)
if tail.len == 0:
return value.hostname
result = value.hostname & "/" & tail
proc portOrDefault*(value: ParsedDatabaseUrl; defaultPort: int): int =
if value.hasPort:
value.port
else:
defaultPort
@@ -0,0 +1,558 @@
import std/asyncdispatch
import std/monotimes
import std/times
import std/json
import ../../error
import ../../models/database_types
import ../../models/mariadb/mariadb_types
import ../../prepared_param
import ./mariadb_rdb
import ./mariadb_lib
type
MariadbWaitState = object
cancelled: bool
proc mariadbTimeoutError(): ref DbError =
newException(DbError, "MariaDB query timeout")
proc makeDeadline(timeout: int): MonoTime =
if timeout > 0:
result = getMonoTime() + initDuration(seconds = timeout)
else:
result = getMonoTime()
proc remainingMs(deadline: MonoTime): int =
let left = (deadline - getMonoTime()).inMilliseconds
if left <= 0:
return 0
if left > int64(high(int)):
return high(int)
result = int(left)
if result < 1:
result = 1
proc ensureMariadbSocketRegistered(conn: PMySQL): AsyncFD =
let sock = mariadb_rdb.get_socket(conn)
if sock < 0:
dbError(conn)
result = AsyncFD(cint(sock))
let disp = getGlobalDispatcher()
if not disp.contains(result):
register(result)
proc waitMariadb(conn: PMySQL, waitStatus: cint, deadline: MonoTime): Future[cint] {.async.} =
if waitStatus == 0:
return 0
let totalRemaining = remainingMs(deadline)
if totalRemaining <= 0:
raise mariadbTimeoutError()
var stepTimeout = totalRemaining
if (waitStatus and MYSQL_WAIT_TIMEOUT) != 0:
let connectorTimeout = int(mariadb_rdb.get_timeout_value_ms(conn))
if connectorTimeout > 0 and connectorTimeout < stepTimeout:
stepTimeout = connectorTimeout
if stepTimeout < 1:
stepTimeout = 1
let shouldWaitRead = (waitStatus and (MYSQL_WAIT_READ or MYSQL_WAIT_EXCEPT)) != 0
let shouldWaitWrite = (waitStatus and MYSQL_WAIT_WRITE) != 0
if not shouldWaitRead and not shouldWaitWrite:
await sleepAsync(stepTimeout)
return cint(MYSQL_WAIT_TIMEOUT)
let fd = ensureMariadbSocketRegistered(conn)
var state = MariadbWaitState(cancelled: false)
var ioReady = newFuture[void]("waitMariadb")
var readyStatus = 0.cint
if shouldWaitRead:
proc readCb(f: AsyncFD): bool =
if state.cancelled:
return true
readyStatus = readyStatus or cint(MYSQL_WAIT_READ)
if (waitStatus and MYSQL_WAIT_EXCEPT) != 0:
readyStatus = readyStatus or cint(MYSQL_WAIT_EXCEPT)
if not ioReady.finished:
ioReady.complete()
return true
addRead(fd, readCb)
if shouldWaitWrite:
proc writeCb(f: AsyncFD): bool =
if state.cancelled:
return true
readyStatus = readyStatus or cint(MYSQL_WAIT_WRITE)
if not ioReady.finished:
ioReady.complete()
return true
addWrite(fd, writeCb)
let ok = await withTimeout(ioReady, stepTimeout)
if not ok:
state.cancelled = true
unregister(fd)
if (waitStatus and MYSQL_WAIT_TIMEOUT) != 0:
return cint(MYSQL_WAIT_TIMEOUT)
raise mariadbTimeoutError()
if readyStatus == 0:
if (waitStatus and MYSQL_WAIT_TIMEOUT) != 0:
return cint(MYSQL_WAIT_TIMEOUT)
return waitStatus and cint(MYSQL_WAIT_READ or MYSQL_WAIT_WRITE or MYSQL_WAIT_EXCEPT)
return readyStatus
proc runRealQuery(conn: PMySQL, q: string, deadline: MonoTime): Future[void] {.async.} =
var queryStatus = 0'i32
var waitStatus = mariadb_rdb.real_query_start(addr queryStatus, conn, q.cstring, culong(q.len))
while waitStatus != 0:
let ready = await waitMariadb(conn, waitStatus, deadline)
waitStatus = mariadb_rdb.real_query_cont(addr queryStatus, conn, ready)
if queryStatus != 0'i32:
dbError(conn)
proc runFetchRow(conn: PMySQL, sqlres: PRES, deadline: MonoTime): Future[mariadb_rdb.Row] {.async.} =
var row: mariadb_rdb.Row = nil
var waitStatus = mariadb_rdb.fetch_row_start(addr row, sqlres)
while waitStatus != 0:
let ready = await waitMariadb(conn, waitStatus, deadline)
waitStatus = mariadb_rdb.fetch_row_cont(addr row, sqlres, ready)
return row
proc runFreeResult(conn: PMySQL, sqlres: PRES, deadline: MonoTime): Future[void] {.async.} =
if sqlres.isNil:
return
var waitStatus = mariadb_rdb.free_result_start(sqlres)
while waitStatus != 0:
let ready = await waitMariadb(conn, waitStatus, deadline)
waitStatus = mariadb_rdb.free_result_cont(sqlres, ready)
proc rawExec(conn: PMySQL, query: string, args: MariadbParams, timeout: int) {.async.} =
let q = dbFormat(conn, query, args)
let deadline = makeDeadline(timeout)
await runRealQuery(conn, q, deadline)
proc runStmtPrepare(conn: PMySQL, stmt: PSTMT, sql: string, deadline: MonoTime): Future[void] {.async.} =
var ret = 0.cint
var waitStatus = stmt_prepare_start(addr ret, stmt, sql.cstring, culong(sql.len))
while waitStatus != 0:
let ready = await waitMariadb(conn, waitStatus, deadline)
waitStatus = stmt_prepare_cont(addr ret, stmt, ready)
if ret != 0:
raise newException(DbError, $stmt_error(stmt))
proc runStmtReset(conn: PMySQL, stmt: PSTMT, deadline: MonoTime): Future[void] {.async.} =
var ret = false
var waitStatus = stmt_reset_start(addr ret, stmt)
while waitStatus != 0:
let ready = await waitMariadb(conn, waitStatus, deadline)
waitStatus = stmt_reset_cont(addr ret, stmt, ready)
if ret:
raise newException(DbError, $stmt_error(stmt))
proc runStmtFreeResult(conn: PMySQL, stmt: PSTMT, deadline: MonoTime): Future[void] {.async.} =
var ret = false
var waitStatus = stmt_free_result_start(addr ret, stmt)
while waitStatus != 0:
let ready = await waitMariadb(conn, waitStatus, deadline)
waitStatus = stmt_free_result_cont(addr ret, stmt, ready)
if ret:
raise newException(DbError, $stmt_error(stmt))
proc runStmtExecute(conn: PMySQL, stmt: PSTMT, deadline: MonoTime): Future[void] {.async.} =
var ret = 0.cint
var waitStatus = stmt_execute_start(addr ret, stmt)
while waitStatus != 0:
let ready = await waitMariadb(conn, waitStatus, deadline)
waitStatus = stmt_execute_cont(addr ret, stmt, ready)
if ret != 0:
raise newException(DbError, $stmt_error(stmt))
proc runStmtStoreResult(conn: PMySQL, stmt: PSTMT, deadline: MonoTime): Future[void] {.async.} =
var ret = 0.cint
var waitStatus = stmt_store_result_start(addr ret, stmt)
while waitStatus != 0:
let ready = await waitMariadb(conn, waitStatus, deadline)
waitStatus = stmt_store_result_cont(addr ret, stmt, ready)
if ret != 0:
raise newException(DbError, $stmt_error(stmt))
proc runStmtFetch(conn: PMySQL, stmt: PSTMT, deadline: MonoTime): Future[cint] {.async.} =
var ret = 0.cint
var waitStatus = stmt_fetch_start(addr ret, stmt)
while waitStatus != 0:
let ready = await waitMariadb(conn, waitStatus, deadline)
waitStatus = stmt_fetch_cont(addr ret, stmt, ready)
return ret
proc prepareStmt*(conn: PMySQL, sql: string, timeout: int): Future[PSTMT] {.async.} =
assert(not conn.isNil, "Database not connected.")
result = stmt_init(conn)
if result.isNil:
dbError(conn)
let deadline = makeDeadline(timeout)
try:
await runStmtPrepare(conn, result, sql, deadline)
except CatchableError:
discard stmt_close(result)
raise
proc bindStmtParams(stmt: PSTMT, args: seq[PreparedParam]) =
if stmt_param_count(stmt) != args.len:
raise newException(DbError, "Prepared statement parameter count mismatch.")
if args.len == 0:
return
var binds = newSeq[BIND](args.len)
var values = newSeq[string](args.len)
var lengths = newSeq[culong](args.len)
var nullFlags = newSeq[my_bool](args.len)
var errorFlags = newSeq[my_bool](args.len)
for i, arg in args:
if arg.isNull:
nullFlags[i] = true
binds[i].buffer_type = TYPE_NULL
binds[i].is_null = addr nullFlags[i]
binds[i].error = addr errorFlags[i]
continue
values[i] = arg.value
lengths[i] = values[i].len.culong
binds[i].buffer_type = TYPE_STRING
if values[i].len > 0:
binds[i].buffer = cast[pointer](values[i].cstring)
else:
binds[i].buffer = nil
binds[i].buffer_length = values[i].len.culong
binds[i].length = addr lengths[i]
binds[i].is_null = addr nullFlags[i]
binds[i].error = addr errorFlags[i]
if stmt_bind_param(stmt, binds[0].addr):
raise newException(DbError, $stmt_error(stmt))
proc bindStmtResults(
stmt: PSTMT,
metadata: PRES,
resultBinds: MariadbResultBindCache
) =
let cols = int(num_fields(metadata))
if resultBinds.binds.len != cols:
resultBinds.binds = newSeq[BIND](cols)
resultBinds.buffers = newSeq[string](cols)
resultBinds.lengths = newSeq[culong](cols)
resultBinds.nullFlags = newSeq[my_bool](cols)
resultBinds.errorFlags = newSeq[my_bool](cols)
for i in 0 ..< cols:
let field = fetch_field_direct(metadata, cast[mariadb_rdb.cuint](i))
var bufferLen = int(field.len)
if bufferLen < 4096:
bufferLen = 4096
resultBinds.buffers[i] = newString(bufferLen)
else:
for i in 0 ..< cols:
resultBinds.lengths[i] = 0
resultBinds.nullFlags[i] = false
resultBinds.errorFlags[i] = false
for i in 0 ..< cols:
resultBinds.binds[i].buffer_type = TYPE_STRING
resultBinds.binds[i].buffer = if resultBinds.buffers[i].len > 0: cast[pointer](resultBinds.buffers[i].cstring) else: nil
resultBinds.binds[i].buffer_length = resultBinds.buffers[i].len.culong
resultBinds.binds[i].length = addr resultBinds.lengths[i]
resultBinds.binds[i].is_null = addr resultBinds.nullFlags[i]
resultBinds.binds[i].error = addr resultBinds.errorFlags[i]
if cols > 0 and stmt_bind_result(stmt, resultBinds.binds[0].addr):
raise newException(DbError, $stmt_error(stmt))
proc refetchTruncatedColumns(
stmt: PSTMT,
resultBinds: MariadbResultBindCache
) =
for i in 0 ..< resultBinds.binds.len:
if not resultBinds.errorFlags[i]:
continue
let needed = max(int(resultBinds.lengths[i]), resultBinds.buffers[i].len)
if needed <= 0:
continue
resultBinds.buffers[i] = newString(needed)
resultBinds.binds[i].buffer = cast[pointer](resultBinds.buffers[i].cstring)
resultBinds.binds[i].buffer_length = needed.culong
if stmt_fetch_column(stmt, resultBinds.binds[i].addr, cast[mariadb_rdb.cuint](i), 0) != 0:
raise newException(DbError, $stmt_error(stmt))
proc execPreparedStmt*(conn: PMySQL, stmt: PSTMT, args: seq[PreparedParam], timeout: int) {.async.} =
assert(not conn.isNil, "Database not connected.")
let deadline = makeDeadline(timeout)
await runStmtReset(conn, stmt, deadline)
await runStmtFreeResult(conn, stmt, deadline)
bindStmtParams(stmt, args)
await runStmtExecute(conn, stmt, deadline)
await runStmtFreeResult(conn, stmt, deadline)
proc queryPreparedStmt*(
conn: PMySQL,
stmt: PSTMT,
args: seq[PreparedParam],
timeout: int,
resultBinds: MariadbResultBindCache
): Future[(seq[database_types.Row], DbRows)] {.async.} =
assert(not conn.isNil, "Database not connected.")
let deadline = makeDeadline(timeout)
await runStmtReset(conn, stmt, deadline)
await runStmtFreeResult(conn, stmt, deadline)
bindStmtParams(stmt, args)
await runStmtExecute(conn, stmt, deadline)
var dbRows: DbRows
var rows = newSeq[seq[string]]()
let metadata = stmt_result_metadata(stmt)
if metadata.isNil:
await runStmtFreeResult(conn, stmt, deadline)
return (rows, dbRows)
defer:
free_result(metadata)
await runStmtFreeResult(conn, stmt, deadline)
await runStmtStoreResult(conn, stmt, deadline)
let cols = int(num_fields(metadata))
var baseColumns: DbColumns
setColumnInfo(baseColumns, metadata, cols)
bindStmtResults(stmt, metadata, resultBinds)
while true:
let fetchRes = await runStmtFetch(conn, stmt, deadline)
if fetchRes == 100:
break
if fetchRes notin {0, 101}:
raise newException(DbError, $stmt_error(stmt))
if fetchRes == 101:
refetchTruncatedColumns(stmt, resultBinds)
var rowColumns = baseColumns
var row = newSeq[string](cols)
for i in 0 ..< cols:
if resultBinds.nullFlags[i]:
rowColumns[i].typ.kind = dbNull
row[i] = ""
else:
let length = min(int(resultBinds.lengths[i]), resultBinds.buffers[i].len)
if length <= 0:
row[i] = ""
else:
row[i] = resultBinds.buffers[i][0 ..< length]
rows.add(row)
dbRows.add(rowColumns)
return (rows, dbRows)
proc closePreparedStmt*(stmt: PSTMT): void =
if stmt.isNil:
return
discard stmt_close(stmt)
proc jsonObjValuesToStrSeq(args: JsonNode): seq[string] =
result = newSeq[string](args.len)
var i = 0
for arg in args.items:
case arg["value"].kind
of JBool:
result[i] = if arg["value"].getBool: "1" else: "0"
of JInt:
result[i] = $arg["value"].getInt
of JFloat:
result[i] = $arg["value"].getFloat
of JArray, JObject:
result[i] = arg["value"].pretty()
of JNull:
result[i] = "null"
else:
result[i] = arg["value"].getStr()
inc i
proc jsonFlatToStrSeq(args: JsonNode): seq[string] =
result = newSeq[string](args.len)
var i = 0
for arg in args.items:
case arg.kind
of JBool:
result[i] = if arg.getBool: "1" else: "0"
of JInt:
result[i] = $arg.getInt
of JFloat:
result[i] = $arg.getFloat
of JArray, JObject:
result[i] = arg.pretty()
of JNull:
result[i] = "null"
else:
result[i] = arg.getStr()
inc i
proc query*(db: PMySQL, query: string, args: seq[string], timeout: int): Future[(seq[database_types.Row], DbRows)] {.async.} =
var dbRows: DbRows
var rows = newSeq[seq[string]]()
let q = dbFormat(query, args)
let deadline = makeDeadline(timeout)
await runRealQuery(db, q, deadline)
let sqlres = mariadb_rdb.use_result(db)
if sqlres.isNil:
return (rows, dbRows)
try:
let cols = int(mariadb_rdb.numFields(sqlres))
var baseColumns: DbColumns
setColumnInfo(baseColumns, sqlres, cols)
while true:
let row = await runFetchRow(db, sqlres, deadline)
if row == nil:
break
var baseRow = newSeq[string](cols)
var rowColumns = baseColumns
for i in 0 ..< cols:
if row[i].isNil:
rowColumns[i].typ.kind = dbNull
baseRow[i] = $row[i]
rows.add(baseRow)
dbRows.add(rowColumns)
finally:
await runFreeResult(db, sqlres, deadline)
return (rows, dbRows)
proc query*(db: PMySQL, query: string, args: JsonNode, timeout: int): Future[(seq[database_types.Row], DbRows)] {.async.} =
return query(db, query, jsonObjValuesToStrSeq(args), timeout).await
proc queryPlain*(db: PMySQL, query: string, args: seq[string], timeout: int): Future[seq[database_types.Row]] {.async.} =
let q = dbFormat(query, args)
let deadline = makeDeadline(timeout)
await runRealQuery(db, q, deadline)
var rows = newSeq[seq[string]]()
let sqlres = mariadb_rdb.use_result(db)
if sqlres.isNil:
return rows
try:
let cols = int(mariadb_rdb.numFields(sqlres))
while true:
let row = await runFetchRow(db, sqlres, deadline)
if row == nil:
break
var baseRow = newSeq[string](cols)
for i in 0 ..< cols:
baseRow[i] = $row[i]
rows.add(baseRow)
finally:
await runFreeResult(db, sqlres, deadline)
return rows
proc queryPlain*(db: PMySQL, query: string, args: JsonNode, timeout: int): Future[seq[database_types.Row]] {.async.} =
return queryPlain(db, query, jsonFlatToStrSeq(args), timeout).await
proc exec*(db: PMySQL, query: string, args: seq[string], timeout: int) {.async.} =
let q = dbFormat(query, args)
let deadline = makeDeadline(timeout)
await runRealQuery(db, q, deadline)
proc exec*(db: PMySQL, query: string, args: JsonNode, timeout: int) {.async.} =
exec(db, query, jsonFlatToStrSeq(args), timeout).await
proc exec*(db: PMySQL, query: string, args: JsonNode, columns: seq[seq[string]], timeout: int) {.async.} =
let params = MariadbParams.fromObj(args, columns)
await rawExec(db, query, params, timeout)
proc execGetValue*(db: PMySQL, query: string, args: JsonNode, columns: seq[seq[string]], timeout: int): Future[(seq[database_types.Row], DbRows)] {.async.} =
var dbRows: DbRows
var rows = newSeq[seq[string]]()
let params = MariadbParams.fromObj(args, columns)
let deadline = makeDeadline(timeout)
let q = dbFormat(db, query, params)
await runRealQuery(db, q, deadline)
let sqlres = mariadb_rdb.use_result(db)
if sqlres.isNil:
return (rows, dbRows)
try:
let cols = int(mariadb_rdb.numFields(sqlres))
var baseColumns: DbColumns
setColumnInfo(baseColumns, sqlres, cols)
while true:
let row = await runFetchRow(db, sqlres, deadline)
if row == nil:
break
var baseRow = newSeq[string](cols)
var rowColumns = baseColumns
for i in 0 ..< cols:
if row[i].isNil:
rowColumns[i].typ.kind = dbNull
baseRow[i] = $row[i]
rows.add(baseRow)
dbRows.add(rowColumns)
finally:
await runFreeResult(db, sqlres, deadline)
return (rows, dbRows)
proc rawQuery*(db: PMySQL, query: string, args: JsonNode, timeout: int): Future[(seq[database_types.Row], DbRows)] {.async.} =
return query(db, query, jsonFlatToStrSeq(args), timeout).await
proc getColumns*(db: PMySQL, query: string, args: seq[string], timeout: int): Future[seq[string]] {.async.} =
var columns: seq[string]
let q = dbFormat(query, args)
let deadline = makeDeadline(timeout)
await runRealQuery(db, q, deadline)
let sqlres = mariadb_rdb.use_result(db)
if sqlres.isNil:
return columns
try:
var dbColumns: DbColumns
let cols = int(mariadb_rdb.numFields(sqlres))
if cols <= 0:
return columns
setColumnInfo(dbColumns, sqlres, cols)
for column in dbColumns:
columns.add(column.name)
finally:
await runFreeResult(db, sqlres, deadline)
return columns
proc getColumnTypes*(db: PMySQL, database, table: string, timeout: int): Future[seq[database_types.Row]] {.async.} =
let sql = "SELECT `COLUMN_NAME`, `DATA_TYPE` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?"
return queryPlain(db, sql, @[database, table], timeout).await
@@ -0,0 +1,232 @@
import std/json
import ../../error
import ../../models/database_types
import ./mariadb_rdb
# type InstantRow* = object ## a handle that can be used to get a row's
# ## column text on demand
# row*: cstringArray
# len: int
proc dbError*(db: PMySQL) {.noreturn.} =
## raises a DbError exception.
var e: ref DbError
new(e)
e.msg = $db.error
raise e
# proc checkError*(db: PMySQL) =
# ## Raises a DbError exception.
# var message = $db.error
# if message.len > 0:
# raise newException(DbError, $message)
proc setTypeName(t: var DbType; f: PFIELD) =
t.name = $f.name
t.maxReprLen = Natural(f.max_length)
if (NOT_NULL_FLAG and f.flags) != 0: t.notNull = true
case f.ftype
of TYPE_DECIMAL:
t.kind = dbDecimal
of TYPE_TINY:
t.kind = dbInt
t.size = 1
of TYPE_SHORT:
t.kind = dbInt
t.size = 2
of TYPE_LONG:
t.kind = dbInt
t.size = 4
of TYPE_FLOAT:
t.kind = dbFloat
t.size = 4
of TYPE_DOUBLE:
t.kind = dbFloat
t.size = 8
of TYPE_NULL:
t.kind = dbNull
of TYPE_TIMESTAMP:
t.kind = dbTimestamp
of TYPE_LONGLONG:
t.kind = dbInt
t.size = 8
of TYPE_INT24:
t.kind = dbInt
t.size = 3
of TYPE_DATE:
t.kind = dbDate
of TYPE_TIME:
t.kind = dbTime
of TYPE_DATETIME:
t.kind = dbDatetime
of TYPE_YEAR:
t.kind = dbDate
of TYPE_NEWDATE:
t.kind = dbDate
of TYPE_VARCHAR, TYPE_VAR_STRING, TYPE_STRING:
t.kind = dbVarchar
of TYPE_BIT:
t.kind = dbBit
of TYPE_NEWDECIMAL:
t.kind = dbDecimal
of TYPE_ENUM: t.kind = dbEnum
of TYPE_SET: t.kind = dbSet
of TYPE_TINY_BLOB, TYPE_MEDIUM_BLOB, TYPE_LONG_BLOB,
TYPE_BLOB: t.kind = dbBlob
of TYPE_GEOMETRY:
t.kind = dbGeometry
proc setColumnInfo*(columns: var DbColumns; res: PRES; L: int) =
setLen(columns, L)
for i in 0..<L:
let fp = fetch_field_direct(res, cint(i))
setTypeName(columns[i].typ, fp)
columns[i].name = $fp.name
columns[i].tableName = $fp.table
columns[i].primaryKey = (fp.flags and PRI_KEY_FLAG) != 0
# proc newRow*(L: int): database_types.Row =
# newSeq(result, L)
# for i in 0..L-1: result[i] = ""
# proc properFreeResult*(sqlres: PRES, row: cstringArray) =
# if row != nil:
# while fetchRow(sqlres) != nil: discard
# freeResult(sqlres)
proc dbQuote(s: string): string =
## DB quotes the string.
if s == "null":
return "NULL"
result = newStringOfCap(s.len + 2)
result.add "'"
for c in items(s):
# see https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html#mysql-escaping
case c
of '\0': result.add "\\0"
of '\b': result.add "\\b"
of '\t': result.add "\\t"
of '\l': result.add "\\n"
of '\r': result.add "\\r"
of '\x1a': result.add "\\Z"
of '"': result.add "\\\""
of '\'': result.add "\\'"
of '\\': result.add "\\\\"
# of '_': result.add "\\_"
else: result.add c
add(result, '\'')
proc dbFormat*(formatstr: string, args: seq[string]): string =
result = newStringOfCap(formatstr.len + args.len * 8)
var a = 0
var segStart = 0
for i in 0 ..< formatstr.len:
if formatstr[i] == '?':
if i > segStart:
result.add(formatstr[segStart ..< i])
result.add(dbQuote(args[a]))
inc(a)
segStart = i + 1
if segStart < formatstr.len:
result.add(formatstr[segStart ..< formatstr.len])
# proc rawExec*(db:PMySQL, query:string, args: seq[string]) =
# var q = dbFormat(query, args)
# if realQuery(db, q.cstring, q.len) != 0'i32: dbError(db)
# iterator instantRows*(db: PMySQL; dbRows: var DbRows; query: string;
# args: seq[string]): InstantRow =
# ## Same as fastRows but returns a handle that can be used to get column text
# ## on demand using []. Returned handle is valid only within the iterator body.
# rawExec(db, query, args)
# var sqlres = mariadb_rdb.useResult(db)
# var dbColumns: DbColumns
# if sqlres != nil:
# let L = int(mariadb_rdb.numFields(sqlres))
# var row: cstringArray
# while true:
# setColumnInfo(dbColumns, sqlres, L)
# dbRows.add(dbColumns)
# for i in 0..L:
# row = mariadb_rdb.fetchRow(sqlres)
# if row == nil: break
# yield InstantRow(row: row, len: L)
# properFreeResult(sqlres, row)
# proc `[]`*(row: InstantRow, col: int): string {.inline.} =
# ## Returns text for given column of the row.
# $row.row[col]
# proc unsafeColumnAt*(row: InstantRow, index: int): cstring {.inline.} =
# ## Return cstring of given column of the row
# row.row[index]
# proc len*(row: InstantRow): int {.inline.} =
# ## Returns number of columns in the row.
# row.len
type MariadbParamValue* = object
value*: string
isBinary*:bool
type MariadbParams* = seq[MariadbParamValue]
proc fromObj*(_:type MariadbParams, args:JsonNode, columns:seq[seq[string]]):MariadbParams =
## args is JArray `[{"key":"id", "value": 1}, {"key": "name" "value": "alice"}]`
result = newSeq[MariadbParamValue](args.len)
var i = 0
for arg in args.items:
defer: i.inc()
case arg["value"].kind
of JBool:
let value = if arg["value"].getBool: "1" else: "0"
result[i] = MariadbParamValue(value:value, isBinary:false)
of JInt:
let value = $arg["value"].getInt
result[i] = MariadbParamValue(value:value, isBinary:false)
of JFloat:
let value = $arg["value"].getFloat
result[i] = MariadbParamValue(value:value, isBinary:false)
of JArray, JObject:
let value = arg["value"].pretty()
result[i] = MariadbParamValue(value:value, isBinary:false)
of JNull:
let value = "null"
result[i] = MariadbParamValue(value:value, isBinary:false)
else: # JString
let value = arg["value"].getStr
result[i] = MariadbParamValue(value:value, isBinary:false)
for column in columns:
let columnName = column[0]
let columnTyp = column[1]
if columnName == arg["key"].getStr:
defer:break
if columnTyp == "blob":
result[i].isBinary = true
proc dbFormat*(conn:PMySQL, query: string, args: MariadbParams): string =
result = newStringOfCap(query.len + args.len * 8)
var a = 0
var segStart = 0
for i in 0 ..< query.len:
if query[i] == '?':
if i > segStart:
result.add(query[segStart ..< i])
if args[a].isBinary:
let `from` = args[a].value
var to = newString(`from`.len * 2 + 1)
let escLen = mariadb_rdb.real_escape_string(conn, to.cstring, `from`.cstring, `from`.len)
to.setLen(escLen)
result.add("\'")
result.add(to)
result.add("\'")
else:
result.add(dbQuote(args[a].value))
inc(a)
segStart = i + 1
if segStart < query.len:
result.add(query[segStart ..< query.len])
@@ -0,0 +1,445 @@
import std/asyncdispatch
import std/times
import std/strutils
import std/strformat
import std/json
import ../../error
import ../../models/database_types
import ../../models/mysql/mysql_types
import ../../prepared_param
import ./mysql_rdb
import ./mysql_lib
proc rawExec(conn:PMySQL, query: string, args: seq[string]) =
assert conn.ping == 0
var stmt = mysql_rdb.stmt_init(conn)
if stmt.isNil:
mysql_rdb.close(conn)
dbError("mysql_stmt_init() failed")
var q = dbFormat(query, args)
if realQuery(conn, q.cstring, q.len) != 0'i32: dbError(conn)
proc rawExec(conn:PMySQL, query: string, args: MysqlParams) =
assert conn.ping == 0
var stmt = mysql_rdb.stmt_init(conn)
if stmt.isNil:
mysql_rdb.close(conn)
dbError("mysql_stmt_init() failed")
var q = dbFormat(conn, query, args)
if realQuery(conn, q.cstring, q.len) != 0'i32: dbError(conn)
proc prepareStmt*(conn: PMySQL, sql: string, timeout: int): Future[PSTMT] {.async.} =
assert(not conn.isNil, "Database not connected.")
await sleepAsync(0)
result = mysql_rdb.stmt_init(conn)
if result.isNil:
dbError(conn)
if mysql_rdb.stmt_prepare(result, sql.cstring, sql.len) != 0:
let errmsg = $mysql_rdb.stmt_error(result)
discard mysql_rdb.stmt_close(result)
raise newException(DbError, errmsg)
proc bindStmtParams(stmt: PSTMT, args: seq[PreparedParam]) =
if mysql_rdb.stmt_param_count(stmt) != args.len:
raise newException(DbError, "Prepared statement parameter count mismatch.")
if args.len == 0:
return
var binds = newSeq[BIND](args.len)
var values = newSeq[string](args.len)
var lengths = newSeq[culong](args.len)
var nullFlags = newSeq[my_bool](args.len)
var errorFlags = newSeq[my_bool](args.len)
for i, arg in args:
if arg.isNull:
nullFlags[i] = true
binds[i].buffer_type = TYPE_NULL
binds[i].is_null = addr nullFlags[i]
binds[i].error = addr errorFlags[i]
continue
values[i] = arg.value
lengths[i] = values[i].len.culong
binds[i].buffer_type = TYPE_STRING
if values[i].len > 0:
binds[i].buffer = cast[pointer](values[i].cstring)
else:
binds[i].buffer = nil
binds[i].buffer_length = values[i].len.culong
binds[i].length = addr lengths[i]
binds[i].is_null = addr nullFlags[i]
binds[i].error = addr errorFlags[i]
if mysql_rdb.stmt_bind_param(stmt, binds[0].addr):
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
proc bindStmtResults(
stmt: PSTMT,
metadata: PRES,
resultBinds: MysqlResultBindCache
) =
let cols = int(mysql_rdb.num_fields(metadata))
if resultBinds.binds.len != cols:
resultBinds.binds = newSeq[BIND](cols)
resultBinds.buffers = newSeq[string](cols)
resultBinds.lengths = newSeq[culong](cols)
resultBinds.nullFlags = newSeq[my_bool](cols)
resultBinds.errorFlags = newSeq[my_bool](cols)
for i in 0 ..< cols:
let field = mysql_rdb.fetch_field_direct(metadata, cast[mysql_rdb.cuint](i))
var bufferLen = int(field.len)
if bufferLen < 4096:
bufferLen = 4096
resultBinds.buffers[i] = newString(bufferLen)
else:
for i in 0 ..< cols:
resultBinds.lengths[i] = 0
resultBinds.nullFlags[i] = false
resultBinds.errorFlags[i] = false
for i in 0 ..< cols:
resultBinds.binds[i].buffer_type = TYPE_STRING
resultBinds.binds[i].buffer = if resultBinds.buffers[i].len > 0: cast[pointer](resultBinds.buffers[i].cstring) else: nil
resultBinds.binds[i].buffer_length = resultBinds.buffers[i].len.culong
resultBinds.binds[i].length = addr resultBinds.lengths[i]
resultBinds.binds[i].is_null = addr resultBinds.nullFlags[i]
resultBinds.binds[i].error = addr resultBinds.errorFlags[i]
if cols > 0 and mysql_rdb.stmt_bind_result(stmt, resultBinds.binds[0].addr):
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
proc refetchTruncatedColumns(
stmt: PSTMT,
resultBinds: MysqlResultBindCache
) =
for i in 0 ..< resultBinds.binds.len:
if not resultBinds.errorFlags[i]:
continue
let needed = max(int(resultBinds.lengths[i]), resultBinds.buffers[i].len)
if needed <= 0:
continue
resultBinds.buffers[i] = newString(needed)
resultBinds.binds[i].buffer = cast[pointer](resultBinds.buffers[i].cstring)
resultBinds.binds[i].buffer_length = needed.culong
if mysql_rdb.stmt_fetch_column(stmt, resultBinds.binds[i].addr, cast[mysql_rdb.cuint](i), 0) != 0:
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
proc execPreparedStmt*(conn: PMySQL, stmt: PSTMT, args: seq[PreparedParam], timeout: int) {.async.} =
assert(not conn.isNil, "Database not connected.")
await sleepAsync(0)
if mysql_rdb.stmt_reset(stmt):
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
if mysql_rdb.stmt_free_result(stmt):
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
bindStmtParams(stmt, args)
if mysql_rdb.stmt_execute(stmt) != 0:
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
if mysql_rdb.stmt_free_result(stmt):
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
proc queryPreparedStmt*(
conn: PMySQL,
stmt: PSTMT,
args: seq[PreparedParam],
timeout: int,
resultBinds: MysqlResultBindCache
): Future[(seq[database_types.Row], DbRows)] {.async.} =
assert(not conn.isNil, "Database not connected.")
await sleepAsync(0)
if mysql_rdb.stmt_reset(stmt):
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
if mysql_rdb.stmt_free_result(stmt):
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
bindStmtParams(stmt, args)
if mysql_rdb.stmt_execute(stmt) != 0:
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
var dbRows: DbRows
var rows = newSeq[seq[string]]()
let metadata = mysql_rdb.stmt_result_metadata(stmt)
if metadata.isNil:
if mysql_rdb.stmt_free_result(stmt):
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
return (rows, dbRows)
defer:
mysql_rdb.free_result(metadata)
if mysql_rdb.stmt_free_result(stmt):
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
if mysql_rdb.stmt_store_result(stmt) != 0:
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
let cols = int(mysql_rdb.num_fields(metadata))
var baseColumns: DbColumns
setColumnInfo(baseColumns, metadata, cols)
bindStmtResults(stmt, metadata, resultBinds)
while true:
let fetchRes = mysql_rdb.stmt_fetch(stmt)
if fetchRes == 100:
break
if fetchRes notin {0, 101}:
raise newException(DbError, $mysql_rdb.stmt_error(stmt))
if fetchRes == 101:
refetchTruncatedColumns(stmt, resultBinds)
var rowColumns = baseColumns
var row = newSeq[string](cols)
for i in 0 ..< cols:
if resultBinds.nullFlags[i]:
rowColumns[i].typ.kind = dbNull
row[i] = ""
else:
let length = min(int(resultBinds.lengths[i]), resultBinds.buffers[i].len)
if length <= 0:
row[i] = ""
else:
row[i] = resultBinds.buffers[i][0 ..< length]
rows.add(row)
dbRows.add(rowColumns)
return (rows, dbRows)
proc closePreparedStmt*(stmt: PSTMT) =
if stmt.isNil:
return
discard mysql_rdb.stmt_close(stmt)
proc query*(db:PMySQL, query: string, args: seq[string], timeout:int):Future[(seq[database_types.Row], DbRows)] {.async.} =
assert db.ping == 0
var dbRows: DbRows
var rows = newSeq[seq[string]]()
var lines = 0
rawExec(db, query, args)
var sqlres = mysql_rdb.useResult(db)
let calledAt = getTime().toUnix()
var dbColumns: DbColumns
let cols = int(mysql_rdb.numFields(sqlres))
while true:
if getTime().toUnix() >= calledAt + timeout:
return
await sleepAsync(0)
var row: mysql_rdb.Row
var baseRow = newSeq[string](cols)
setColumnInfo(dbColumns, sqlres, cols)
row = mysql_rdb.fetchRow(sqlres)
if row == nil: break
for i in 0..<cols:
if row[i].isNil:
dbColumns[i].typ.kind = dbNull
baseRow[i] = $row[i]
rows.add(baseRow)
dbRows.add(dbColumns)
lines.inc()
free_result(sqlres)
return (rows, dbRows)
proc query*(db:PMySQL, query: string, args: JsonNode, timeout:int):Future[(seq[database_types.Row], DbRows)] {.async.} =
var strArgs = newSeq[string](args.len)
var i = 0
for arg in args.items:
defer: i.inc()
case arg["value"].kind
of JBool:
strArgs[i] = if arg["value"].getBool: "1" else: "0"
of JInt:
strArgs[i] = $arg["value"].getInt
of JFloat:
strArgs[i] = $arg["value"].getFloat
of JArray, JObject:
strArgs[i] = arg["value"].pretty()
of JNull:
strArgs[i] = "null"
else: # JString
strArgs[i] = arg["value"].getStr()
return query(db, query, strArgs, timeout).await
proc queryPlain*(db:PMySQL, query: string, args: seq[string], timeout:int):Future[seq[database_types.Row]] {.async.} =
assert db.ping == 0
rawExec(db, query, args)
var rows = newSeq[seq[string]]()
var sqlres = mysql_rdb.useResult(db)
let calledAt = getTime().toUnix()
let cols = int(mysql_rdb.numFields(sqlres))
while true:
if getTime().toUnix() >= calledAt + timeout:
return
await sleepAsync(0)
var row: mysql_rdb.Row
var baseRow = newSeq[string](cols)
row = mysql_rdb.fetchRow(sqlres)
if row == nil: break
for i in 0..<cols:
baseRow[i] = $row[i]
rows.add(baseRow)
free_result(sqlres)
return rows
proc queryPlain*(db:PMySQL, query: string, args: JsonNode, timeout:int):Future[seq[database_types.Row]] {.async.} =
var strArgs = newSeq[string](args.len)
var i = 0
for arg in args.items:
defer: i.inc()
case arg.kind
of JBool:
strArgs[i] = if arg.getBool: "1" else: "0"
of JInt:
strArgs[i] = $arg.getInt
of JFloat:
strArgs[i] = $arg.getFloat
of JArray, JObject:
strArgs[i] = arg.pretty()
of JNull:
strArgs[i] = "null"
else: # JString
strArgs[i] = arg.getStr()
return queryPlain(db, query, strArgs, timeout).await
proc exec*(db:PMySQL, query: string, args: seq[string], timeout:int) {.async.} =
var q = dbFormat(query, args)
await sleepAsync(0)
if realQuery(db, q.cstring, q.len) != 0'i32: dbError(db)
proc exec*(db:PMySQL, query: string, args: JsonNode, timeout:int) {.async.} =
var strArgs = newSeq[string](args.len)
var i = 0
for arg in args.items:
defer: i.inc()
case arg.kind
of JBool:
strArgs[i] = if arg.getBool: "1" else: "0"
of JInt:
strArgs[i] = $arg.getInt
of JFloat:
strArgs[i] = $arg.getFloat
of JArray, JObject:
strArgs[i] = arg.pretty()
of JNull:
strArgs[i] = "null"
else: # JString
strArgs[i] = arg.getStr()
exec(db, query, strArgs, timeout).await
proc exec*(db:PMySQL, query: string, args: JsonNode, columns:seq[seq[string]], timeout:int) {.async.} =
## args is JArray `[{"key":"id", "value": 1}, {"key": "name" "value": "alice"}]`
assert db.ping == 0
let params = MysqlParams.fromObj(args, columns)
rawExec(db, query, params)
proc execGetValue*(db:PMySQL, query: string, args: JsonNode, columns:seq[seq[string]], timeout:int):Future[(seq[database_types.Row], DbRows)] {.async.} =
assert db.ping == 0
var dbRows: DbRows
var rows = newSeq[seq[string]]()
var lines = 0
let params = MysqlParams.fromObj(args, columns)
rawExec(db, query, params)
var sqlres = mysql_rdb.useResult(db)
let calledAt = getTime().toUnix()
var dbColumns: DbColumns
let cols = int(mysql_rdb.numFields(sqlres))
while true:
if getTime().toUnix() >= calledAt + timeout:
return
await sleepAsync(0)
var row: mysql_rdb.Row
var baseRow = newSeq[string](cols)
setColumnInfo(dbColumns, sqlres, cols)
row = mysql_rdb.fetchRow(sqlres)
if row == nil: break
for i in 0..<cols:
if row[i].isNil:
dbColumns[i].typ.kind = dbNull
baseRow[i] = $row[i]
rows.add(baseRow)
dbRows.add(dbColumns)
lines.inc()
free_result(sqlres)
return (rows, dbRows)
proc rawQuery*(db:PMySQL, query: string, args: JsonNode, timeout:int):Future[(seq[database_types.Row], DbRows)] {.async.} =
var strArgs = newSeq[string](args.len)
var i = 0
for arg in args.items:
defer: i.inc()
case arg.kind
of JBool:
strArgs[i] = if arg.getBool: "1" else: "0"
of JInt:
strArgs[i] = $arg.getInt
of JFloat:
strArgs[i] = $arg.getFloat
of JArray, JObject:
strArgs[i] = arg.pretty()
of JNull:
strArgs[i] = "null"
else: # JString
strArgs[i] = arg.getStr()
return query(db, query, strArgs, timeout).await
proc getColumns*(db:PMySQL, query: string, args: seq[string], timeout:int):Future[seq[string]] {.async.} =
assert db.ping == 0
var columns:seq[string]
rawExec(db, query, args)
var sqlres = mysql_rdb.useResult(db)
let calledAt = getTime().toUnix()
var dbColumns: DbColumns
let cols = int(mysql_rdb.numFields(sqlres))
while true:
if getTime().toUnix() >= calledAt + timeout:
return
await sleepAsync(0)
var row: mysql_rdb.Row
setColumnInfo(dbColumns, sqlres, cols)
for column in dbColumns:
columns.add(column.name)
row = mysql_rdb.fetchRow(sqlres)
break
free_result(sqlres)
return columns
proc getColumnTypes*(db:PMySQL, database, table:string, timeout:int):Future[seq[database_types.Row]] {.async.} =
assert db.ping == 0
let sql = &"SELECT `COLUMN_NAME`, `DATA_TYPE` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE TABLE_SCHEMA = '{database}' AND TABLE_NAME = '{table}'"
return queryPlain(db, sql, @[], timeout).await
@@ -0,0 +1,224 @@
import std/json
import ../../error
import ../../models/database_types
import ./mysql_rdb
# type InstantRow* = object ## a handle that can be used to get a row's
# ## column text on demand
# row*: cstringArray
# len: int
proc dbError*(db: PMySQL) {.noreturn.} =
## raises a DbError exception.
var e: ref DbError
new(e)
e.msg = $db.error
raise e
# proc checkError*(db: PMySQL) =
# ## Raises a DbError exception.
# var message = $db.error
# if message.len > 0:
# raise newException(DbError, $message)
proc setTypeName(t: var DbType; f: PFIELD) =
t.name = $f.name
t.maxReprLen = Natural(f.max_length)
if (NOT_NULL_FLAG and f.flags) != 0: t.notNull = true
case f.ftype
of TYPE_DECIMAL:
t.kind = dbDecimal
of TYPE_TINY:
t.kind = dbInt
t.size = 1
of TYPE_SHORT:
t.kind = dbInt
t.size = 2
of TYPE_LONG:
t.kind = dbInt
t.size = 4
of TYPE_FLOAT:
t.kind = dbFloat
t.size = 4
of TYPE_DOUBLE:
t.kind = dbFloat
t.size = 8
of TYPE_NULL:
t.kind = dbNull
of TYPE_TIMESTAMP:
t.kind = dbTimestamp
of TYPE_LONGLONG:
t.kind = dbInt
t.size = 8
of TYPE_INT24:
t.kind = dbInt
t.size = 3
of TYPE_DATE:
t.kind = dbDate
of TYPE_TIME:
t.kind = dbTime
of TYPE_DATETIME:
t.kind = dbDatetime
of TYPE_YEAR:
t.kind = dbDate
of TYPE_NEWDATE:
t.kind = dbDate
of TYPE_VARCHAR, TYPE_VAR_STRING, TYPE_STRING:
t.kind = dbVarchar
of TYPE_BIT:
t.kind = dbBit
of TYPE_NEWDECIMAL:
t.kind = dbDecimal
of TYPE_ENUM: t.kind = dbEnum
of TYPE_SET: t.kind = dbSet
of TYPE_TINY_BLOB, TYPE_MEDIUM_BLOB, TYPE_LONG_BLOB,
TYPE_BLOB: t.kind = dbBlob
of TYPE_GEOMETRY:
t.kind = dbGeometry
proc setColumnInfo*(columns: var DbColumns; res: PRES; L: int) =
setLen(columns, L)
for i in 0..<L:
let fp = fetch_field_direct(res, cint(i))
setTypeName(columns[i].typ, fp)
columns[i].name = $fp.name
columns[i].tableName = $fp.table
columns[i].primaryKey = (fp.flags and PRI_KEY_FLAG) != 0
# proc newRow*(L: int): database_types.Row =
# newSeq(result, L)
# for i in 0..L-1: result[i] = ""
# proc properFreeResult*(sqlres: PRES, row: cstringArray) =
# if row != nil:
# while fetchRow(sqlres) != nil: discard
# freeResult(sqlres)
proc dbQuote(s: string): string =
## DB quotes the string.
if s == "null":
return "NULL"
result = newStringOfCap(s.len + 2)
result.add "'"
for c in items(s):
# see https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html#mysql-escaping
case c
of '\0': result.add "\\0"
of '\b': result.add "\\b"
of '\t': result.add "\\t"
of '\l': result.add "\\n"
of '\r': result.add "\\r"
of '\x1a': result.add "\\Z"
of '"': result.add "\\\""
of '\'': result.add "\\'"
of '\\': result.add "\\\\"
# of '_': result.add "\\_"
else: result.add c
add(result, '\'')
proc dbFormat*(formatstr: string, args: seq[string]): string =
result = ""
var a = 0
for c in items(formatstr):
if c == '?':
add(result, dbQuote(args[a]))
inc(a)
else:
add(result, c)
# proc rawExec*(db:PMySQL, query:string, args: seq[string]) =
# var q = dbFormat(query, args)
# if realQuery(db, q.cstring, q.len) != 0'i32: dbError(db)
# iterator instantRows*(db: PMySQL; dbRows: var DbRows; query: string;
# args: seq[string]): InstantRow =
# ## Same as fastRows but returns a handle that can be used to get column text
# ## on demand using []. Returned handle is valid only within the iterator body.
# rawExec(db, query, args)
# var sqlres = mysql_rdb.useResult(db)
# var dbColumns: DbColumns
# if sqlres != nil:
# let L = int(mysql_rdb.numFields(sqlres))
# var row: cstringArray
# while true:
# setColumnInfo(dbColumns, sqlres, L)
# dbRows.add(dbColumns)
# for i in 0..L:
# row = mysql_rdb.fetchRow(sqlres)
# if row == nil: break
# yield InstantRow(row: row, len: L)
# properFreeResult(sqlres, row)
# proc `[]`*(row: InstantRow, col: int): string {.inline.} =
# ## Returns text for given column of the row.
# $row.row[col]
# proc unsafeColumnAt*(row: InstantRow, index: int): cstring {.inline.} =
# ## Return cstring of given column of the row
# row.row[index]
# proc len*(row: InstantRow): int {.inline.} =
# ## Returns number of columns in the row.
# row.len
type MysqlParamValue* = object
value*: string
isBinary*:bool
type MysqlParams* = seq[MysqlParamValue]
proc fromObj*(_:type MysqlParams, args:JsonNode, columns:seq[seq[string]]):MysqlParams =
## args is JArray `[{"key":"id", "value": 1}, {"key": "name" "value": "alice"}]`
result = newSeq[MysqlParamValue](args.len)
var i = 0
for arg in args.items:
defer: i.inc()
case arg["value"].kind
of JBool:
let value = if arg["value"].getBool: "1" else: "0"
result[i] = MysqlParamValue(value:value, isBinary:false)
of JInt:
let value = $arg["value"].getInt
result[i] = MysqlParamValue(value:value, isBinary:false)
of JFloat:
let value = $arg["value"].getFloat
result[i] = MysqlParamValue(value:value, isBinary:false)
of JArray, JObject:
let value = arg["value"].pretty()
result[i] = MysqlParamValue(value:value, isBinary:false)
of JNull:
let value = "null"
result[i] = MysqlParamValue(value:value, isBinary:false)
else: # JString
let value = arg["value"].getStr
result[i] = MysqlParamValue(value:value, isBinary:false)
for column in columns:
let columnName = column[0]
let columnTyp = column[1]
if columnName == arg["key"].getStr:
defer:break
if columnTyp == "blob":
result[i].isBinary = true
proc dbFormat*(conn:PMySQL, query: string, args: MysqlParams): string =
result = ""
var i = 0
for c in query:
if c == '?':
defer: i.inc()
if args[i].isBinary:
let `from` = args[i].value
var to = newString(`from`.len * 2 + 1)
let len = mysql_rdb.real_escape_string(conn, to.cstring, `from`.cstring, `from`.len)
to.setLen(len)
result.add("\'")
result.add(to)
result.add("\'")
else:
result.add(dbQuote(args[i].value))
else:
result.add(c)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,431 @@
## https://www.postgresql.jp/document/12/html/libpq-async.html
import std/asyncdispatch
import std/json
import std/monotimes
import std/strutils
import std/times
import ../../error
import ../../models/database_types
import ../../prepared_param
import ./postgres_rdb
import ./postgres_lib
type
PgWaitState = object
cancelled: bool
proc cancelQuery(db: PPGconn) {.raises: [DbError].} =
let cancel = pqGetCancel(db)
if cancel == nil:
raise newException(DbError, "PQgetCancel failed")
defer:
pqFreeCancel(cancel)
var errBuf = newStringOfCap(ERROR_MSG_LENGTH)
errBuf.setLen(ERROR_MSG_LENGTH)
if pqCancel(cancel, errBuf.cstring, int32(errBuf.len)) == 0:
raise newException(DbError, "PQcancel failed: " & $errBuf.cstring)
proc ensurePgSocketRegistered(db: PPGconn): AsyncFD =
let sock = pqsocket(db)
if sock < 0:
dbError(db)
result = AsyncFD(cint(sock))
let disp = getGlobalDispatcher()
if not disp.contains(result):
register(result)
proc waitPgIo(db: PPGconn, timeoutMs: int; forRead: bool): Future[bool] {.async.} =
if timeoutMs <= 0:
return false
let fd = ensurePgSocketRegistered(db)
var state = PgWaitState(cancelled: false)
var ioFut = newFuture[void]("waitPgIo")
if forRead:
proc readCb(f: AsyncFD): bool =
if state.cancelled:
return true
if not ioFut.finished:
ioFut.complete()
return true
addRead(fd, readCb)
else:
proc writeCb(f: AsyncFD): bool =
if state.cancelled:
return true
if not ioFut.finished:
ioFut.complete()
return true
addWrite(fd, writeCb)
let ok = await withTimeout(ioFut, timeoutMs)
if not ok:
state.cancelled = true
unregister(fd)
return ok
proc makePgDeadline(timeout: int): MonoTime =
let sec = if timeout > 0: timeout else: 0
getMonoTime() + initDuration(seconds = sec)
proc pgRemainingMs(deadline: MonoTime): int =
let left = (deadline - getMonoTime()).inMilliseconds
if left <= 0:
return 0
if left > int64(high(int)):
return high(int)
result = int(left)
if result < 1:
result = 1
proc pgSendQueryParams(db: PPGconn, query: string, pgParams: PGParams) {.raises: [DbError].} =
let status =
if pgParams.nParams > 0:
pqsendQueryParams(db, query.cstring, pgParams.nParams, nil, pgParams.values, pgParams.lengths[0].unsafeAddr, pgParams.formats[0].unsafeAddr, 0)
else:
pqsendQueryParams(db, query.cstring, pgParams.nParams, nil, nil, nil, nil, 0)
if status != 1:
dbError(db)
proc pgFlushOutgoing(db: PPGconn, deadline: MonoTime): Future[void] {.async.} =
while true:
let flushRes = pqflush(db)
if flushRes == 0:
return
if flushRes < 0:
dbError(db)
let ms = pgRemainingMs(deadline)
if ms <= 0:
cancelQuery(db)
raise newException(DbError, "PostgreSQL query timeout")
if not await waitPgIo(db, ms, false):
cancelQuery(db)
raise newException(DbError, "PostgreSQL query timeout")
proc pgAwaitReadyForGetResult(db: PPGconn, deadline: MonoTime): Future[void] {.async.} =
while true:
if pqconsumeInput(db) != 1:
dbError(db)
if pqisBusy(db) != 1:
return
let ms = pgRemainingMs(deadline)
if ms <= 0:
cancelQuery(db)
raise newException(DbError, "PostgreSQL query timeout")
if not await waitPgIo(db, ms, true):
cancelQuery(db)
raise newException(DbError, "PostgreSQL query timeout")
proc pgNextResult(db: PPGconn, deadline: MonoTime): Future[PPGresult] {.async.} =
await pgAwaitReadyForGetResult(db, deadline)
result = pqgetResult(db)
proc pgEnsureIdle(db: PPGconn, deadline: MonoTime): Future[void] {.async.} =
while true:
await pgAwaitReadyForGetResult(db, deadline)
let r = pqgetResult(db)
if r == nil:
db.checkError()
return
pqclear(r)
proc query*(db: PPGconn, query: string, args: JsonNode, timeout: int): Future[(seq[Row], DbRows)] {.async.} =
assert db.status == CONNECTION_OK
let pgParams = PGParams.fromObjArray(args)
defer:
if pgParams.nParams > 0: pgParams.values.deallocCStringArray()
pgSendQueryParams(db, query, pgParams)
var dbRows: DbRows
var rows = newSeq[Row]()
let deadline = makePgDeadline(timeout)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
let cols = pqnfields(pqresult)
var row = newRow(cols)
let base = buildBaseDbColumns(pqresult, cols)
for i in 0'i32 .. pqNtuples(pqresult) - 1:
setRow(pqresult, row, i, cols)
appendDbRowWithBaseColumns(pqresult, dbRows, i, cols, base)
rows.add(row)
pqclear(pqresult)
return (rows, dbRows)
proc exec*(db: PPGconn, query: string, args: JsonNode, columns: seq[Row], timeout: int) {.async.} =
assert db.status == CONNECTION_OK
let pgParams = PGParams.fromObjArray(args, columns)
defer:
if pgParams.nParams > 0: pgParams.values.deallocCStringArray()
pgSendQueryParams(db, query, pgParams)
let deadline = makePgDeadline(timeout)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
pqclear(pqresult)
proc execGetValue*(db: PPGconn, query: string, args: JsonNode, columns: seq[Row], timeout: int): Future[(seq[Row], DbRows)] {.async.} =
assert db.status == CONNECTION_OK
let pgParams = PGParams.fromObjArray(args, columns)
defer:
if pgParams.nParams > 0: pgParams.values.deallocCStringArray()
pgSendQueryParams(db, query, pgParams)
var dbRows: DbRows
var rows = newSeq[Row]()
let deadline = makePgDeadline(timeout)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
let cols = pqnfields(pqresult)
var row = newRow(cols)
let base = buildBaseDbColumns(pqresult, cols)
for i in 0'i32 .. pqNtuples(pqresult) - 1:
setRow(pqresult, row, i, cols)
appendDbRowWithBaseColumns(pqresult, dbRows, i, cols, base)
rows.add(row)
pqclear(pqresult)
return (rows, dbRows)
proc rawQuery*(db: PPGconn, query: string, args: JsonNode, timeout: int): Future[(seq[Row], DbRows)] {.async.} =
assert db.status == CONNECTION_OK
let pgParams = PGParams.fromArray(args)
defer:
if pgParams.nParams > 0: pgParams.values.deallocCStringArray()
pgSendQueryParams(db, query, pgParams)
var dbRows: DbRows
var rows = newSeq[Row]()
let deadline = makePgDeadline(timeout)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
let cols = pqnfields(pqresult)
var row = newRow(cols)
let base = buildBaseDbColumns(pqresult, cols)
for i in 0'i32 .. pqNtuples(pqresult) - 1:
setRow(pqresult, row, i, cols)
appendDbRowWithBaseColumns(pqresult, dbRows, i, cols, base)
rows.add(row)
pqclear(pqresult)
return (rows, dbRows)
proc rawExec*(db: PPGconn, query: string, args: JsonNode, timeout: int) {.async.} =
assert db.status == CONNECTION_OK
let pgParams = PGParams.fromArray(args)
defer:
if pgParams.nParams > 0: pgParams.values.deallocCStringArray()
pgSendQueryParams(db, query, pgParams)
let deadline = makePgDeadline(timeout)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
pqclear(pqresult)
# ==================================================
# Old functions
# ==================================================
proc query*(db: PPGconn, query: string, args: seq[string], timeout: int): Future[(seq[Row], DbRows)] {.async.} =
assert db.status == CONNECTION_OK
let status = pqsendQuery(db, dbFormat(query, args).cstring)
if status != 1: dbError(db)
var dbRows: DbRows
var rows = newSeq[Row]()
let deadline = makePgDeadline(timeout)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
let cols = pqnfields(pqresult)
var row = newRow(cols)
let base = buildBaseDbColumns(pqresult, cols)
for i in 0'i32 .. pqNtuples(pqresult) - 1:
setRow(pqresult, row, i, cols)
appendDbRowWithBaseColumns(pqresult, dbRows, i, cols, base)
rows.add(row)
pqclear(pqresult)
return (rows, dbRows)
proc queryPlain*(db: PPGconn, query: string, args: seq[string], timeout: int): Future[seq[Row]] {.async.} =
assert db.status == CONNECTION_OK
let status = pqsendQuery(db, dbFormat(query, args).cstring)
if status != 1: dbError(db)
var rows = newSeq[Row]()
let deadline = makePgDeadline(timeout)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
var cols = pqnfields(pqresult)
var row = newRow(cols)
for i in 0'i32 .. pqNtuples(pqresult) - 1:
setRow(pqresult, row, i, cols)
rows.add(row)
pqclear(pqresult)
return rows
proc exec*(db: PPGconn, query: string, args: seq[string], timeout: int) {.async.} =
assert db.status == CONNECTION_OK
let success = pqsendQuery(db, dbFormat(query, args).cstring)
if success != 1: dbError(db)
let deadline = makePgDeadline(timeout)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
pqclear(pqresult)
proc getColumns*(db: PPGconn, query: string, args: seq[string], timeout: int): Future[seq[string]] {.async.} =
assert db.status == CONNECTION_OK
let status = pqsendQuery(db, dbFormat(query, args).cstring)
if status != 1: dbError(db)
var dbRows: DbRows
let deadline = makePgDeadline(timeout)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
let cols = pqnfields(pqresult)
let base = buildBaseDbColumns(pqresult, cols)
appendDbRowWithBaseColumns(pqresult, dbRows, 0, cols, base)
pqclear(pqresult)
for column in dbRows[0]:
result.add(column.name)
proc prepare*(db: PPGconn, query: string, timeout: int, stmtName: string, nArgs: int): Future[void] {.async.} =
assert db.status == CONNECTION_OK
let success = pqsendPrepare(db, stmtName, questionToDaller(query).cstring, int32(nArgs), nil)
if success != 1: dbError(db)
let deadline = makePgDeadline(timeout)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
pqclear(pqresult)
proc deallocate*(db: PPGconn, stmtName: string, timeout: int): Future[void] {.async.} =
assert db.status == CONNECTION_OK
if stmtName.len == 0:
return
let success = pqsendQuery(db, ("DEALLOCATE " & stmtName).cstring)
if success != 1:
dbError(db)
let deadline = makePgDeadline(timeout)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
pqclear(pqresult)
proc allocPreparedCStringArray(args: seq[PreparedParam]): cstringArray =
result = cast[cstringArray](alloc0((args.len + 1) * sizeof(cstring)))
for i, arg in args:
if arg.isNull:
continue
let cstrLen = arg.value.len + 1
let cstr = cast[cstring](alloc0(cstrLen))
copyMem(cstr, arg.value.cstring, arg.value.len)
result[i] = cstr
proc freePreparedCStringArray(values: cstringArray, n: int) =
if values.isNil:
return
for i in 0 ..< n:
if values[i] != nil:
dealloc(values[i])
dealloc(values)
proc preparedQuery*(db: PPGconn, args: seq[PreparedParam], nArgs: int, timeout: int, stmtName: string): Future[(seq[Row], DbRows)] {.async.} =
assert db.status == CONNECTION_OK
let deadline = makePgDeadline(timeout)
await pgEnsureIdle(db, deadline)
let values = allocPreparedCStringArray(args)
defer:
freePreparedCStringArray(values, args.len)
let status = pqsendQueryPrepared(db, stmtName, int32(nArgs), values, nil, nil, 0)
if status != 1: dbError(db)
var dbRows: DbRows
var rows = newSeq[Row]()
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
let cols = pqnfields(pqresult)
var row = newRow(cols)
let base = buildBaseDbColumns(pqresult, cols)
for i in 0'i32 .. pqNtuples(pqresult) - 1:
setRow(pqresult, row, i, cols)
appendDbRowWithBaseColumns(pqresult, dbRows, i, cols, base)
rows.add(row)
pqclear(pqresult)
return (rows, dbRows)
proc preparedExec*(db: PPGconn, args: seq[PreparedParam], nArgs: int, timeout: int, stmtName: string) {.async.} =
assert db.status == CONNECTION_OK
let deadline = makePgDeadline(timeout)
await pgEnsureIdle(db, deadline)
let values = allocPreparedCStringArray(args)
defer:
freePreparedCStringArray(values, args.len)
let status = pqsendQueryPrepared(db, stmtName, int32(nArgs), values, nil, nil, 0)
if status != 1: dbError(db)
await pgFlushOutgoing(db, deadline)
while true:
let pqresult = await pgNextResult(db, deadline)
if pqresult == nil:
db.checkError()
break
pqclear(pqresult)
@@ -0,0 +1,364 @@
import std/strutils
import std/json
import ../../error
import ../../models/database_types
import ./postgres_rdb
proc dbError*(db: PPGconn) {.noreturn.} =
## raises a DbError exception.
var e: ref DbError
new(e)
e.msg = $pqErrorMessage(db)
raise e
proc checkError*(db: PPGconn) =
## Raises a DbError exception.
var message = pqErrorMessage(db)
if message.len > 0:
raise newException(DbError, $message)
proc getBaseColumnType(res: PPGresult, col: int32): DbType =
## OID → DbType(行に依存しない)。NULL セルは行ごとに `pqgetisnull` で上書きする。
let oid = pqftype(res, col)
case oid
of 16: return DbType(kind: DbTypeKind.dbBool, name: "bool")
of 17: return DbType(kind: DbTypeKind.dbBlob, name: "bytea")
of 21: return DbType(kind: DbTypeKind.dbInt, name: "int2", size: 2)
of 23: return DbType(kind: DbTypeKind.dbInt, name: "int4", size: 4)
of 20: return DbType(kind: DbTypeKind.dbInt, name: "int8", size: 8)
of 1560: return DbType(kind: DbTypeKind.dbBit, name: "bit")
of 1562: return DbType(kind: DbTypeKind.dbInt, name: "varbit")
of 18: return DbType(kind: DbTypeKind.dbFixedChar, name: "char")
of 19: return DbType(kind: DbTypeKind.dbFixedChar, name: "name")
of 1042: return DbType(kind: DbTypeKind.dbFixedChar, name: "bpchar")
of 25: return DbType(kind: DbTypeKind.dbVarchar, name: "text")
of 1043: return DbType(kind: DbTypeKind.dbVarChar, name: "varchar")
of 2275: return DbType(kind: DbTypeKind.dbVarchar, name: "cstring")
of 700: return DbType(kind: DbTypeKind.dbFloat, name: "float4")
of 701: return DbType(kind: DbTypeKind.dbFloat, name: "float8")
of 790: return DbType(kind: DbTypeKind.dbDecimal, name: "money")
of 1700: return DbType(kind: DbTypeKind.dbDecimal, name: "numeric")
of 704: return DbType(kind: DbTypeKind.dbTimeInterval, name: "tinterval")
of 702: return DbType(kind: DbTypeKind.dbTimestamp, name: "abstime")
of 703: return DbType(kind: DbTypeKind.dbTimeInterval, name: "reltime")
of 1082: return DbType(kind: DbTypeKind.dbDate, name: "date")
of 1083: return DbType(kind: DbTypeKind.dbTime, name: "time")
of 1114: return DbType(kind: DbTypeKind.dbTimestamp, name: "timestamp")
of 1184: return DbType(kind: DbTypeKind.dbTimestamp, name: "timestamptz")
of 1186: return DbType(kind: DbTypeKind.dbTimeInterval, name: "interval")
of 1266: return DbType(kind: DbTypeKind.dbTime, name: "timetz")
of 114: return DbType(kind: DbTypeKind.dbJson, name: "json")
of 142: return DbType(kind: DbTypeKind.dbXml, name: "xml")
of 3802: return DbType(kind: DbTypeKind.dbJson, name: "jsonb")
of 600: return DbType(kind: DbTypeKind.dbPoint, name: "point")
of 601: return DbType(kind: DbTypeKind.dbLseg, name: "lseg")
of 602: return DbType(kind: DbTypeKind.dbPath, name: "path")
of 603: return DbType(kind: DbTypeKind.dbBox, name: "box")
of 604: return DbType(kind: DbTypeKind.dbPolygon, name: "polygon")
of 628: return DbType(kind: DbTypeKind.dbLine, name: "line")
of 718: return DbType(kind: DbTypeKind.dbCircle, name: "circle")
of 650: return DbType(kind: DbTypeKind.dbInet, name: "cidr")
of 829: return DbType(kind: DbTypeKind.dbMacAddress, name: "macaddr")
of 869: return DbType(kind: DbTypeKind.dbInet, name: "inet")
of 2950: return DbType(kind: DbTypeKind.dbVarchar, name: "uuid")
of 3614: return DbType(kind: DbTypeKind.dbVarchar, name: "tsvector")
of 3615: return DbType(kind: DbTypeKind.dbVarchar, name: "tsquery")
of 2970: return DbType(kind: DbTypeKind.dbVarchar, name: "txid_snapshot")
of 27: return DbType(kind: DbTypeKind.dbComposite, name: "tid")
of 1790: return DbType(kind: DbTypeKind.dbComposite, name: "refcursor")
of 2249: return DbType(kind: DbTypeKind.dbComposite, name: "record")
of 3904: return DbType(kind: DbTypeKind.dbComposite, name: "int4range")
of 3906: return DbType(kind: DbTypeKind.dbComposite, name: "numrange")
of 3908: return DbType(kind: DbTypeKind.dbComposite, name: "tsrange")
of 3910: return DbType(kind: DbTypeKind.dbComposite, name: "tstzrange")
of 3912: return DbType(kind: DbTypeKind.dbComposite, name: "daterange")
of 3926: return DbType(kind: DbTypeKind.dbComposite, name: "int8range")
of 22: return DbType(kind: DbTypeKind.dbArray, name: "int2vector")
of 30: return DbType(kind: DbTypeKind.dbArray, name: "oidvector")
of 143: return DbType(kind: DbTypeKind.dbArray, name: "xml[]")
of 199: return DbType(kind: DbTypeKind.dbArray, name: "json[]")
of 629: return DbType(kind: DbTypeKind.dbArray, name: "line[]")
of 651: return DbType(kind: DbTypeKind.dbArray, name: "cidr[]")
of 719: return DbType(kind: DbTypeKind.dbArray, name: "circle[]")
of 791: return DbType(kind: DbTypeKind.dbArray, name: "money[]")
of 1000: return DbType(kind: DbTypeKind.dbArray, name: "bool[]")
of 1001: return DbType(kind: DbTypeKind.dbArray, name: "bytea[]")
of 1002: return DbType(kind: DbTypeKind.dbArray, name: "char[]")
of 1003: return DbType(kind: DbTypeKind.dbArray, name: "name[]")
of 1005: return DbType(kind: DbTypeKind.dbArray, name: "int2[]")
of 1006: return DbType(kind: DbTypeKind.dbArray, name: "int2vector[]")
of 1007: return DbType(kind: DbTypeKind.dbArray, name: "int4[]")
of 1008: return DbType(kind: DbTypeKind.dbArray, name: "regproc[]")
of 1009: return DbType(kind: DbTypeKind.dbArray, name: "text[]")
of 1028: return DbType(kind: DbTypeKind.dbArray, name: "oid[]")
of 1010: return DbType(kind: DbTypeKind.dbArray, name: "tid[]")
of 1011: return DbType(kind: DbTypeKind.dbArray, name: "xid[]")
of 1012: return DbType(kind: DbTypeKind.dbArray, name: "cid[]")
of 1013: return DbType(kind: DbTypeKind.dbArray, name: "oidvector[]")
of 1014: return DbType(kind: DbTypeKind.dbArray, name: "bpchar[]")
of 1015: return DbType(kind: DbTypeKind.dbArray, name: "varchar[]")
of 1016: return DbType(kind: DbTypeKind.dbArray, name: "int8[]")
of 1017: return DbType(kind: DbTypeKind.dbArray, name: "point[]")
of 1018: return DbType(kind: DbTypeKind.dbArray, name: "lseg[]")
of 1019: return DbType(kind: DbTypeKind.dbArray, name: "path[]")
of 1020: return DbType(kind: DbTypeKind.dbArray, name: "box[]")
of 1021: return DbType(kind: DbTypeKind.dbArray, name: "float4[]")
of 1022: return DbType(kind: DbTypeKind.dbArray, name: "float8[]")
of 1023: return DbType(kind: DbTypeKind.dbArray, name: "abstime[]")
of 1024: return DbType(kind: DbTypeKind.dbArray, name: "reltime[]")
of 1025: return DbType(kind: DbTypeKind.dbArray, name: "tinterval[]")
of 1027: return DbType(kind: DbTypeKind.dbArray, name: "polygon[]")
of 1040: return DbType(kind: DbTypeKind.dbArray, name: "macaddr[]")
of 1041: return DbType(kind: DbTypeKind.dbArray, name: "inet[]")
of 1263: return DbType(kind: DbTypeKind.dbArray, name: "cstring[]")
of 1115: return DbType(kind: DbTypeKind.dbArray, name: "timestamp[]")
of 1182: return DbType(kind: DbTypeKind.dbArray, name: "date[]")
of 1183: return DbType(kind: DbTypeKind.dbArray, name: "time[]")
of 1185: return DbType(kind: DbTypeKind.dbArray, name: "timestamptz[]")
of 1187: return DbType(kind: DbTypeKind.dbArray, name: "interval[]")
of 1231: return DbType(kind: DbTypeKind.dbArray, name: "numeric[]")
of 1270: return DbType(kind: DbTypeKind.dbArray, name: "timetz[]")
of 1561: return DbType(kind: DbTypeKind.dbArray, name: "bit[]")
of 1563: return DbType(kind: DbTypeKind.dbArray, name: "varbit[]")
of 2201: return DbType(kind: DbTypeKind.dbArray, name: "refcursor[]")
of 2951: return DbType(kind: DbTypeKind.dbArray, name: "uuid[]")
of 3643: return DbType(kind: DbTypeKind.dbArray, name: "tsvector[]")
of 3645: return DbType(kind: DbTypeKind.dbArray, name: "tsquery[]")
of 3807: return DbType(kind: DbTypeKind.dbArray, name: "jsonb[]")
of 2949: return DbType(kind: DbTypeKind.dbArray, name: "txid_snapshot[]")
of 3905: return DbType(kind: DbTypeKind.dbArray, name: "int4range[]")
of 3907: return DbType(kind: DbTypeKind.dbArray, name: "numrange[]")
of 3909: return DbType(kind: DbTypeKind.dbArray, name: "tsrange[]")
of 3911: return DbType(kind: DbTypeKind.dbArray, name: "tstzrange[]")
of 3913: return DbType(kind: DbTypeKind.dbArray, name: "daterange[]")
of 3927: return DbType(kind: DbTypeKind.dbArray, name: "int8range[]")
of 2287: return DbType(kind: DbTypeKind.dbArray, name: "record[]")
of 705: return DbType(kind: DbTypeKind.dbUnknown, name: "unknown")
else: return DbType(kind: DbTypeKind.dbUnknown, name: $oid) ## Query the system table pg_type to determine exactly which type is referenced.
proc buildBaseDbColumns*(res: PPGresult; cols: int32): DbColumns =
result = newSeqOfCap[DbColumn](cols.int)
setLen(result, cols)
for col in 0'i32 ..< cols:
result[col].name = $pqfname(res, col)
result[col].typ = getBaseColumnType(res, col)
result[col].tableName = $(pqftable(res, col))
proc appendDbRowWithBaseColumns*(res: PPGresult; dbRows: var DbRows; line, cols: int32; base: DbColumns) =
var columns = base
for col in 0'i32 ..< cols:
if pqgetisnull(res, line, col) == 1:
columns[col].typ = DbType(kind: dbNull, name: "null")
dbRows.add(columns)
proc setColumnInfo*(res: PPGresult; dbRows: var DbRows; line, cols: int32) =
let base = buildBaseDbColumns(res, cols)
appendDbRowWithBaseColumns(res, dbRows, line, cols, base)
proc newRow*(L: int): Row =
newSeq(result, L)
for i in 0..L-1: result[i] = ""
proc setRow*(res: PPGresult, r: var Row, line, cols: int32) =
for col in 0'i32..cols-1:
setLen(r[col], 0)
let x = pqgetvalue(res, line, col)
if x.isNil:
r[col] = ""
else:
add(r[col], x)
proc dbQuote(s: string): string =
# ## DB quotes the string.
# if s == "null":
# return "NULL"
# result = "'"
# for c in items(s):
# if c == '\'': add(result, "''")
# else: add(result, c)
# add(result, '\'')
## DB quotes the string.
if s == "null":
return "NULL"
result = newStringOfCap(s.len * 2 + 2)
result.add('\'')
for c in items(s):
case c
of '\'': add(result, "''")
of '\0': add(result, "\\0")
else: add(result, c)
add(result, '\'')
proc dbFormat*(formatstr: string, args: varargs[string]): string =
var a = 0
if args.len > 0 and not formatstr.contains("?"):
dbError("""parameter substitution expects "?" """)
if args.len == 0:
return formatstr
result = newStringOfCap(formatstr.len + args.len * 8)
var segStart = 0
for j in 0 ..< formatstr.len:
if formatstr[j] == '?':
if j > segStart:
result.add(formatstr[segStart ..< j])
result.add(dbQuote(args[a]))
inc(a)
segStart = j + 1
if segStart < formatstr.len:
result.add(formatstr[segStart ..< formatstr.len])
proc questionToDaller*(s:string):string =
## from `UPDATE user SET name = ?, email = ? WHERE id = ?`
##
## to `UPDATE user SET name = $1, email = $2 WHERE id = $3`
var i = 1
var segStart = 0
result = newStringOfCap(s.len + 8)
for j in 0 ..< s.len:
if s[j] == '?':
if j > segStart:
result.add(s[segStart ..< j])
result.add('$')
result.add($i)
inc(i)
segStart = j + 1
if segStart < s.len:
result.add(s[segStart ..< s.len])
type PGParams* = object
nParams*: int32
values*: cstringArray
lengths*: seq[int32]
formats*: seq[int32] # 0:text,1:binary
proc objArrayParamSeqs(args: JsonNode; columns: seq[Row]): tuple[values: seq[string], lengths: seq[int32], formats: seq[int32]] =
result.values = newSeq[string](args.len)
result.lengths = newSeq[int32](args.len)
result.formats = newSeq[int32](args.len)
var i = 0
for arg in args.items:
defer: i.inc()
case arg["value"].kind
of JBool:
result.values[i] = if arg["value"].getBool: "t" else: "f"
result.lengths[i] = 0
result.formats[i] = 0
of JInt:
result.values[i] = $arg["value"].getInt
result.lengths[i] = 0
result.formats[i] = 0
of JFloat:
result.values[i] = $arg["value"].getFloat
result.lengths[i] = 0
result.formats[i] = 0
of JNull:
result.values[i] = "NULL"
result.lengths[i] = 0
result.formats[i] = 0
of JObject, JArray:
result.values[i] = arg["value"].pretty
result.lengths[i] = 0
result.formats[i] = 0
of JString:
if columns.len > 0:
for column in columns:
if column[0] == arg["key"].getStr:
defer: break
let value = arg["value"].getStr
result.values[i] = value
result.lengths[i] = value.len.int32
if column[1] == "bytea":
result.formats[i] = 1
else:
result.formats[i] = 0
else:
let value = arg["value"].getStr
result.values[i] = value
result.lengths[i] = value.len.int32
result.formats[i] = 0
proc allocPgParamsFromSeqs(values: seq[string]; lengths, formats: seq[int32]; n: int): PGParams =
result.nParams = n.int32
result.lengths = lengths
result.formats = formats
result.values = allocCStringArray(values)
for j, row in values:
if row == "NULL":
result.values[j] = nil
proc fromObjArray*(_: type PGParams, args: JsonNode, columns: seq[Row]): PGParams =
if args.len == 0:
return
let t = objArrayParamSeqs(args, columns)
result = allocPgParamsFromSeqs(t.values, t.lengths, t.formats, args.len)
proc fromObjArray*(_: type PGParams, args: JsonNode): PGParams =
## `args` is JArray `[{"key": "bool", "value": true},{"key": "int", "value": 1}]`
if args.len == 0:
return
let t = objArrayParamSeqs(args, @[])
result = allocPgParamsFromSeqs(t.values, t.lengths, t.formats, args.len)
proc fromArray*(_:type PGParams, args: JsonNode):PGParams =
## `args` is JArray `{true, 1, 1.1, "alice"}`
if args.len == 0:
return
result.nParams = args.len.int32
var values = newSeq[string](args.len)
result.formats = newSeq[int32](args.len)
result.lengths = newSeq[int32](args.len)
var i = 0
for arg in args.items:
defer: i.inc()
case arg.kind
of JBool:
values[i] = if arg.getBool: "t" else: "f"
result.lengths[i] = 0
result.formats[i] = 0
of JInt:
values[i] = $arg.getInt
result.lengths[i] = 0
result.formats[i] = 0
of JFloat:
values[i] = $arg.getFloat
result.lengths[i] = 0
result.formats[i] = 0
of JNull:
values[i] = "NULL"
result.lengths[i] = 0
result.formats[i] = 0
of JObject, JArray:
values[i] = arg.pretty
result.lengths[i] = 0
result.formats[i] = 0
of JString:
let value = arg.getStr
values[i] = value
result.lengths[i] = value.len.int32
result.formats[i] = 0
result.values = allocCStringArray(values)
for i, row in values:
if row == "NULL":
result.values[i] = nil
@@ -0,0 +1,374 @@
when defined(nimHasStyleChecks):
{.push styleChecks: off.}
when defined(windows):
const
dllName = "libpq.dll"
elif defined(macosx):
const
dllName = "libpq.dylib"
else:
const
dllName = "libpq.so(.5|)"
type
POid* = ptr Oid
Oid* = int32
const
ERROR_MSG_LENGTH* = 4096
CMDSTATUS_LEN* = 40
type
SockAddr* = array[1..112, int8]
PGresAttDesc*{.pure, final.} = object
name*: cstring
adtid*: Oid
adtsize*: int
PPGresAttDesc* = ptr PGresAttDesc
PPPGresAttDesc* = ptr PPGresAttDesc
PGresAttValue*{.pure, final.} = object
length*: int32
value*: cstring
PPGresAttValue* = ptr PGresAttValue
PPPGresAttValue* = ptr PPGresAttValue
PExecStatusType* = ptr ExecStatusType
ExecStatusType* = enum
PGRES_EMPTY_QUERY = 0, PGRES_COMMAND_OK, PGRES_TUPLES_OK, PGRES_COPY_OUT,
PGRES_COPY_IN, PGRES_BAD_RESPONSE, PGRES_NONFATAL_ERROR, PGRES_FATAL_ERROR,
PGRES_COPY_BOTH, PGRES_SINGLE_TUPLE
PGlobjfuncs*{.pure, final.} = object
fn_lo_open*: Oid
fn_lo_close*: Oid
fn_lo_creat*: Oid
fn_lo_unlink*: Oid
fn_lo_lseek*: Oid
fn_lo_tell*: Oid
fn_lo_read*: Oid
fn_lo_write*: Oid
PPGlobjfuncs* = ptr PGlobjfuncs
PConnStatusType* = ptr ConnStatusType
ConnStatusType* = enum
CONNECTION_OK, CONNECTION_BAD, CONNECTION_STARTED, CONNECTION_MADE,
CONNECTION_AWAITING_RESPONSE, CONNECTION_AUTH_OK, CONNECTION_SETENV,
CONNECTION_SSL_STARTUP, CONNECTION_NEEDED, CONNECTION_CHECK_WRITABLE,
CONNECTION_CONSUME, CONNECTION_GSS_STARTUP, CONNECTION_CHECK_TARGET
PGconn*{.pure, final.} = object
pghost*: cstring
pgtty*: cstring
pgport*: cstring
pgoptions*: cstring
dbName*: cstring
status*: ConnStatusType
errorMessage*: array[0..(ERROR_MSG_LENGTH) - 1, char]
Pfin*: File
Pfout*: File
Pfdebug*: File
sock*: int32
laddr*: SockAddr
raddr*: SockAddr
salt*: array[0..(2) - 1, char]
asyncNotifyWaiting*: int32
notifyList*: pointer
pguser*: cstring
pgpass*: cstring
lobjfuncs*: PPGlobjfuncs
PPGconn* = ptr PGconn
PGresult*{.pure, final.} = object
ntups*: int32
numAttributes*: int32
attDescs*: PPGresAttDesc
tuples*: PPPGresAttValue
tupArrSize*: int32
resultStatus*: ExecStatusType
cmdStatus*: array[0..(CMDSTATUS_LEN) - 1, char]
binary*: int32
conn*: PPGconn
PGCancel*{.pure, final.} = object
PPGcancel* = ptr PGCancel
PPGresult* = ptr PGresult
PPostgresPollingStatusType* = ptr PostgresPollingStatusType
PostgresPollingStatusType* = enum
PGRES_POLLING_FAILED = 0, PGRES_POLLING_READING, PGRES_POLLING_WRITING,
PGRES_POLLING_OK, PGRES_POLLING_ACTIVE
PPGTransactionStatusType* = ptr PGTransactionStatusType
PGTransactionStatusType* = enum
PQTRANS_IDLE, PQTRANS_ACTIVE, PQTRANS_INTRANS, PQTRANS_INERROR,
PQTRANS_UNKNOWN
PPGVerbosity* = ptr PGVerbosity
PGVerbosity* = enum
PQERRORS_TERSE, PQERRORS_DEFAULT, PQERRORS_VERBOSE, PQERRORS_SQLSTATE
PPGNotify* = ptr pgNotify
pgNotify*{.pure, final.} = object
relname*: cstring
be_pid*: int32
extra*: cstring
PQnoticeReceiver* = proc (arg: pointer, res: PPGresult){.cdecl.}
PQnoticeProcessor* = proc (arg: pointer, message: cstring){.cdecl.}
Ppqbool* = ptr pqbool
pqbool* = char
PPQprintOpt* = ptr PQprintOpt
PQprintOpt*{.pure, final.} = object
header*: pqbool
align*: pqbool
standard*: pqbool
html3*: pqbool
expanded*: pqbool
pager*: pqbool
fieldSep*: cstring
tableOpt*: cstring
caption*: cstring
fieldName*: ptr cstring
PPQconninfoOption* = ptr PQconninfoOption
PQconninfoOption*{.pure, final.} = object
keyword*: cstring
envvar*: cstring
compiled*: cstring
val*: cstring
label*: cstring
dispchar*: cstring
dispsize*: int32
PPQArgBlock* = ptr PQArgBlock
PQArgBlock*{.pure, final.} = object
length*: int32
isint*: int32
p*: pointer
proc pqinitOpenSSL*(do_ssl: int32, do_crypto: int32) {.cdecl, dynlib: dllName,
importc: "PQinitOpenSSL".}
proc pqconnectStart*(conninfo: cstring): PPGconn{.cdecl, dynlib: dllName,
importc: "PQconnectStart".}
proc pqconnectPoll*(conn: PPGconn): PostgresPollingStatusType{.cdecl,
dynlib: dllName, importc: "PQconnectPoll".}
proc pqconnectdb*(conninfo: cstring): PPGconn{.cdecl, dynlib: dllName,
importc: "PQconnectdb".}
proc pqsetdbLogin*(pghost: cstring, pgport: cstring, pgoptions: cstring,
pgtty: cstring, dbName: cstring, login: cstring, pwd: cstring): PPGconn{.
cdecl, dynlib: dllName, importc: "PQsetdbLogin".}
proc pqsetdb*(M_PGHOST, M_PGPORT, M_PGOPT, M_PGTTY, M_DBNAME: cstring): PPGconn
proc pqfinish*(conn: PPGconn){.cdecl, dynlib: dllName, importc: "PQfinish".}
proc pqconndefaults*(): PPQconninfoOption{.cdecl, dynlib: dllName,
importc: "PQconndefaults".}
proc pqconninfoFree*(connOptions: PPQconninfoOption){.cdecl, dynlib: dllName,
importc: "PQconninfoFree".}
proc pqresetStart*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQresetStart".}
proc pqresetPoll*(conn: PPGconn): PostgresPollingStatusType{.cdecl,
dynlib: dllName, importc: "PQresetPoll".}
proc pqreset*(conn: PPGconn){.cdecl, dynlib: dllName, importc: "PQreset".}
# add these procs pqGetCancel, pqFreeCancel and pqCancel.
# https://www.postgresql.org/docs/12/libpq-cancel.html
proc pqGetCancel*(conn: PPGconn): PPGcancel{.cdecl, dynlib: dllName, importc: "PQgetCancel".}
proc pqFreeCancel*(cancel: PPGcancel){.cdecl, dynlib: dllName, importc: "PQfreeCancel".}
proc pqCancel*(cancel: PPGcancel, errbuf: cstring, errbufsize: int32):int32{.cdecl, dynlib: dllName, importc: "PQcancel".}
proc pqrequestCancel*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQrequestCancel".}
proc pqdb*(conn: PPGconn): cstring{.cdecl, dynlib: dllName, importc: "PQdb".}
proc pquser*(conn: PPGconn): cstring{.cdecl, dynlib: dllName, importc: "PQuser".}
proc pqpass*(conn: PPGconn): cstring{.cdecl, dynlib: dllName, importc: "PQpass".}
proc pqhost*(conn: PPGconn): cstring{.cdecl, dynlib: dllName, importc: "PQhost".}
proc pqport*(conn: PPGconn): cstring{.cdecl, dynlib: dllName, importc: "PQport".}
proc pqtty*(conn: PPGconn): cstring{.cdecl, dynlib: dllName, importc: "PQtty".}
proc pqoptions*(conn: PPGconn): cstring{.cdecl, dynlib: dllName,
importc: "PQoptions".}
proc pqstatus*(conn: PPGconn): ConnStatusType{.cdecl, dynlib: dllName,
importc: "PQstatus".}
proc pqtransactionStatus*(conn: PPGconn): PGTransactionStatusType{.cdecl,
dynlib: dllName, importc: "PQtransactionStatus".}
proc pqparameterStatus*(conn: PPGconn, paramName: cstring): cstring{.cdecl,
dynlib: dllName, importc: "PQparameterStatus".}
proc pqserverVersion*(conn: PPGconn): int32{.cdecl,
dynlib: dllName, importc: "PQserverVersion".}
proc pqprotocolVersion*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQprotocolVersion".}
proc pqerrorMessage*(conn: PPGconn): cstring{.cdecl, dynlib: dllName,
importc: "PQerrorMessage".}
proc pqsocket*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQsocket".}
proc pqbackendPID*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQbackendPID".}
proc pqconnectionNeedsPassword*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQconnectionNeedsPassword".}
proc pqconnectionUsedPassword*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQconnectionUsedPassword".}
proc pqclientEncoding*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQclientEncoding".}
proc pqsetClientEncoding*(conn: PPGconn, encoding: cstring): int32{.cdecl,
dynlib: dllName, importc: "PQsetClientEncoding".}
when defined(USE_SSL):
# Get the SSL structure associated with a connection
proc pqgetssl*(conn: PPGconn): PSSL{.cdecl, dynlib: dllName,
importc: "PQgetssl".}
proc pqsetErrorVerbosity*(conn: PPGconn, verbosity: PGVerbosity): PGVerbosity{.
cdecl, dynlib: dllName, importc: "PQsetErrorVerbosity".}
proc pqtrace*(conn: PPGconn, debug_port: File){.cdecl, dynlib: dllName,
importc: "PQtrace".}
proc pquntrace*(conn: PPGconn){.cdecl, dynlib: dllName, importc: "PQuntrace".}
proc pqsetNoticeReceiver*(conn: PPGconn, theProc: PQnoticeReceiver, arg: pointer): PQnoticeReceiver{.
cdecl, dynlib: dllName, importc: "PQsetNoticeReceiver".}
proc pqsetNoticeProcessor*(conn: PPGconn, theProc: PQnoticeProcessor,
arg: pointer): PQnoticeProcessor{.cdecl,
dynlib: dllName, importc: "PQsetNoticeProcessor".}
proc pqexec*(conn: PPGconn, query: cstring): PPGresult{.cdecl, dynlib: dllName,
importc: "PQexec".}
proc pqexecParams*(conn: PPGconn, command: cstring, nParams: int32,
paramTypes: POid, paramValues: cstringArray,
paramLengths, paramFormats: ptr int32, resultFormat: int32): PPGresult{.
cdecl, dynlib: dllName, importc: "PQexecParams".}
proc pqprepare*(conn: PPGconn, stmtName, query: cstring, nParams: int32,
paramTypes: POid): PPGresult{.cdecl, dynlib: dllName, importc: "PQprepare".}
proc pqexecPrepared*(conn: PPGconn, stmtName: cstring, nParams: int32,
paramValues: cstringArray,
paramLengths, paramFormats: ptr int32, resultFormat: int32): PPGresult{.
cdecl, dynlib: dllName, importc: "PQexecPrepared".}
proc pqsendQuery*(conn: PPGconn, query: cstring): int32{.cdecl, dynlib: dllName,
importc: "PQsendQuery".}
## See also https://www.postgresql.org/docs/current/libpq-async.html
proc pqsendQueryParams*(conn: PPGconn, command: cstring, nParams: int32,
paramTypes: POid, paramValues: cstringArray,
paramLengths, paramFormats: ptr int32,
resultFormat: int32): int32{.cdecl, dynlib: dllName,
importc: "PQsendQueryParams".}
proc pqsendPrepare*(conn: PPGconn, stmtName: cstring, query: cstring,
nParams: int32, paramTypes: POid): int32{.cdecl, dynlib: dllName,
importc: "PQsendPrepare".}
proc pqsendQueryPrepared*(conn: PPGconn, stmtName: cstring, nParams: int32,
paramValues: cstringArray,
paramLengths, paramFormats: ptr int32,
resultFormat: int32): int32{.cdecl, dynlib: dllName,
importc: "PQsendQueryPrepared".}
proc pqSetSingleRowMode*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQsetSingleRowMode".}
## See also https://www.postgresql.org/docs/current/libpq-single-row-mode.html
proc pqgetResult*(conn: PPGconn): PPGresult{.cdecl, dynlib: dllName,
importc: "PQgetResult".}
proc pqisBusy*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQisBusy".}
proc pqconsumeInput*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQconsumeInput".}
proc pqnotifies*(conn: PPGconn): PPGNotify{.cdecl, dynlib: dllName,
importc: "PQnotifies".}
proc pqputCopyData*(conn: PPGconn, buffer: cstring, nbytes: int32): int32{.
cdecl, dynlib: dllName, importc: "PQputCopyData".}
proc pqputCopyEnd*(conn: PPGconn, errormsg: cstring): int32{.cdecl,
dynlib: dllName, importc: "PQputCopyEnd".}
proc pqgetCopyData*(conn: PPGconn, buffer: cstringArray, async: int32): int32{.
cdecl, dynlib: dllName, importc: "PQgetCopyData".}
proc pqgetline*(conn: PPGconn, str: cstring, len: int32): int32{.cdecl,
dynlib: dllName, importc: "PQgetline".}
proc pqputline*(conn: PPGconn, str: cstring): int32{.cdecl, dynlib: dllName,
importc: "PQputline".}
proc pqgetlineAsync*(conn: PPGconn, buffer: cstring, bufsize: int32): int32{.
cdecl, dynlib: dllName, importc: "PQgetlineAsync".}
proc pqputnbytes*(conn: PPGconn, buffer: cstring, nbytes: int32): int32{.cdecl,
dynlib: dllName, importc: "PQputnbytes".}
proc pqendcopy*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQendcopy".}
proc pqsetnonblocking*(conn: PPGconn, arg: int32): int32{.cdecl,
dynlib: dllName, importc: "PQsetnonblocking".}
proc pqisnonblocking*(conn: PPGconn): int32{.cdecl, dynlib: dllName,
importc: "PQisnonblocking".}
proc pqflush*(conn: PPGconn): int32{.cdecl, dynlib: dllName, importc: "PQflush".}
proc pqfn*(conn: PPGconn, fnid: int32, result_buf, result_len: ptr int32,
result_is_int: int32, args: PPQArgBlock, nargs: int32): PPGresult{.
cdecl, dynlib: dllName, importc: "PQfn".}
proc pqresultStatus*(res: PPGresult): ExecStatusType{.cdecl, dynlib: dllName,
importc: "PQresultStatus".}
proc pqresStatus*(status: ExecStatusType): cstring{.cdecl, dynlib: dllName,
importc: "PQresStatus".}
proc pqresultErrorMessage*(res: PPGresult): cstring{.cdecl, dynlib: dllName,
importc: "PQresultErrorMessage".}
proc pqresultErrorField*(res: PPGresult, fieldcode: int32): cstring{.cdecl,
dynlib: dllName, importc: "PQresultErrorField".}
proc pqntuples*(res: PPGresult): int32{.cdecl, dynlib: dllName,
importc: "PQntuples".}
proc pqnfields*(res: PPGresult): int32{.cdecl, dynlib: dllName,
importc: "PQnfields".}
proc pqbinaryTuples*(res: PPGresult): int32{.cdecl, dynlib: dllName,
importc: "PQbinaryTuples".}
proc pqfname*(res: PPGresult, field_num: int32): cstring{.cdecl,
dynlib: dllName, importc: "PQfname".}
proc pqfnumber*(res: PPGresult, field_name: cstring): int32{.cdecl,
dynlib: dllName, importc: "PQfnumber".}
proc pqftable*(res: PPGresult, field_num: int32): Oid{.cdecl, dynlib: dllName,
importc: "PQftable".}
proc pqftablecol*(res: PPGresult, field_num: int32): int32{.cdecl,
dynlib: dllName, importc: "PQftablecol".}
proc pqfformat*(res: PPGresult, field_num: int32): int32{.cdecl,
dynlib: dllName, importc: "PQfformat".}
proc pqftype*(res: PPGresult, field_num: int32): Oid{.cdecl, dynlib: dllName,
importc: "PQftype".}
proc pqfsize*(res: PPGresult, field_num: int32): int32{.cdecl, dynlib: dllName,
importc: "PQfsize".}
proc pqfmod*(res: PPGresult, field_num: int32): int32{.cdecl, dynlib: dllName,
importc: "PQfmod".}
proc pqcmdStatus*(res: PPGresult): cstring{.cdecl, dynlib: dllName,
importc: "PQcmdStatus".}
proc pqoidStatus*(res: PPGresult): cstring{.cdecl, dynlib: dllName,
importc: "PQoidStatus".}
proc pqoidValue*(res: PPGresult): Oid{.cdecl, dynlib: dllName,
importc: "PQoidValue".}
proc pqcmdTuples*(res: PPGresult): cstring{.cdecl, dynlib: dllName,
importc: "PQcmdTuples".}
proc pqgetvalue*(res: PPGresult, tup_num: int32, field_num: int32): cstring{.
cdecl, dynlib: dllName, importc: "PQgetvalue".}
proc pqgetlength*(res: PPGresult, tup_num: int32, field_num: int32): int32{.
cdecl, dynlib: dllName, importc: "PQgetlength".}
proc pqgetisnull*(res: PPGresult, tup_num: int32, field_num: int32): int32{.
cdecl, dynlib: dllName, importc: "PQgetisnull".}
proc pqclear*(res: PPGresult){.cdecl, dynlib: dllName, importc: "PQclear".}
proc pqfreemem*(p: pointer){.cdecl, dynlib: dllName, importc: "PQfreemem".}
proc pqmakeEmptyPGresult*(conn: PPGconn, status: ExecStatusType): PPGresult{.
cdecl, dynlib: dllName, importc: "PQmakeEmptyPGresult".}
proc pqescapeString*(till, `from`: cstring, len: int): int{.cdecl,
dynlib: dllName, importc: "PQescapeString".}
proc pqescapeBytea*(bintext: cstring, binlen: int, bytealen: var int): cstring{.
cdecl, dynlib: dllName, importc: "PQescapeBytea".}
proc pqunescapeBytea*(strtext: cstring, retbuflen: var int): cstring{.cdecl,
dynlib: dllName, importc: "PQunescapeBytea".}
proc pqprint*(fout: File, res: PPGresult, ps: PPQprintOpt){.cdecl,
dynlib: dllName, importc: "PQprint".}
proc pqdisplayTuples*(res: PPGresult, fp: File, fillAlign: int32,
fieldSep: cstring, printHeader: int32, quiet: int32){.
cdecl, dynlib: dllName, importc: "PQdisplayTuples".}
proc pqprintTuples*(res: PPGresult, fout: File, printAttName: int32,
terseOutput: int32, width: int32){.cdecl, dynlib: dllName,
importc: "PQprintTuples".}
proc lo_open*(conn: PPGconn, lobjId: Oid, mode: int32): int32{.cdecl,
dynlib: dllName, importc: "lo_open".}
proc lo_close*(conn: PPGconn, fd: int32): int32{.cdecl, dynlib: dllName,
importc: "lo_close".}
proc lo_read*(conn: PPGconn, fd: int32, buf: cstring, length: int): int32{.
cdecl, dynlib: dllName, importc: "lo_read".}
proc lo_write*(conn: PPGconn, fd: int32, buf: cstring, length: int): int32{.
cdecl, dynlib: dllName, importc: "lo_write".}
proc lo_lseek*(conn: PPGconn, fd: int32, offset: int32, whence: int32): int32{.
cdecl, dynlib: dllName, importc: "lo_lseek".}
proc lo_creat*(conn: PPGconn, mode: int32): Oid{.cdecl, dynlib: dllName,
importc: "lo_creat".}
proc lo_tell*(conn: PPGconn, fd: int32): int32{.cdecl, dynlib: dllName,
importc: "lo_tell".}
proc lo_unlink*(conn: PPGconn, lobjId: Oid): int32{.cdecl, dynlib: dllName,
importc: "lo_unlink".}
proc lo_import*(conn: PPGconn, filename: cstring): Oid{.cdecl, dynlib: dllName,
importc: "lo_import".}
proc lo_export*(conn: PPGconn, lobjId: Oid, filename: cstring): int32{.cdecl,
dynlib: dllName, importc: "lo_export".}
proc pqmblen*(s: cstring, encoding: int32): int32{.cdecl, dynlib: dllName,
importc: "PQmblen".}
proc pqenv2encoding*(): int32{.cdecl, dynlib: dllName, importc: "PQenv2encoding".}
proc pqsetdb(M_PGHOST, M_PGPORT, M_PGOPT, M_PGTTY, M_DBNAME: cstring): PPGconn =
result = pqsetdbLogin(M_PGHOST, M_PGPORT, M_PGOPT, M_PGTTY, M_DBNAME, "", "")
when defined(nimHasStyleChecks):
{.pop.}
@@ -0,0 +1,263 @@
import std/asyncdispatch
import std/strutils
import std/json
import ../../models/database_types
import ../../prepared_param
import ./sqlite_rdb
import ./sqlite_lib
proc query*(db:PSqlite3, query:string, args:seq[string], timeout:int):Future[(seq[Row], DbRows)] {.async.} =
assert(not db.isNil, "Database not connected.")
sleepAsync(0).await
var dbRows: DbRows
var rows = newSeq[seq[string]]()
for row in db.instantRows(dbRows, query, args):
var columns = newSeq[string](row.len)
for i in 0..row.len()-1:
columns[i] = row[i]
rows.add(columns)
return (rows, dbRows)
proc queryPlain*(db:PSqlite3, query:string, args:seq[string], timeout:int):Future[seq[Row]] {.async.} =
assert(not db.isNil, "Database not connected.")
sleepAsync(0).await
var rows = newSeq[seq[string]]()
for row in db.instantRowsPlain(query, args):
var columns = newSeq[string](row.len)
for i in 0..row.len()-1:
columns[i] = row[i]
rows.add(columns)
return rows
proc getColumnTypes*(db:PSqlite3, query: string):Future[seq[(string, string)]] {.async.} =
sleepAsync(0).await
var dbRows: DbRows
var columns = newSeq[(string, string)]()
for row in db.instantRows(dbRows, query, newSeq[string]()):
columns.add((row[1], row[2]))
return columns
proc exec*(db:PSqlite3, query: string, args: JsonNode, columns:seq[(string, string)], timeout:int) {.async.} =
## args is `JArray`
assert(not db.isNil, "Database not connected.")
sleepAsync(0).await
# var q = dbFormat(query, strArges)
var stmt: PStmt
var res:bool
if prepare_v2(db, query.cstring, query.len.cint, stmt, nil) == SQLITE_OK:
var argCount = 1
for arg in args.items:
defer: argCount.inc()
case arg["value"].kind
of JBool:
let value = arg["value"].getBool
let insertVal = if value:1 else: 0
discard bind_int64(stmt, argCount.int32, insertVal)
of JInt:
let value = arg["value"].getInt
discard bind_int64(stmt, argCount.int32, value)
of JFloat:
let value = arg["value"].getFloat
discard bind_double(stmt, argCount.int32, value.float64)
of JNull:
discard bind_null(stmt, argCount.int32)
of JObject, JArray:
let value = arg["value"].pretty
discard bind_text(stmt, argCount.int32, value.cstring, value.len.int32, SQLITE_TRANSIENT)
of JString:
for column in columns:
let columnName = column[0]
let columnTyp = column[1]
if columnName == arg["key"].getStr:
defer: break
if columnTyp == "BLOB":
let value = arg["value"].getStr
var blobVal:seq[byte]
for c in value:
blobVal.add(c.byte)
discard bind_blob(stmt, argCount.int32, blobVal[0].unsafeAddr, blobVal.len.int32, SQLITE_TRANSIENT)
else:
let value = arg["value"].getStr
discard bind_text(stmt, argCount.int32, value.cstring, value.len.int32, SQLITE_TRANSIENT)
let x = step(stmt)
if x in {SQLITE_DONE, SQLITE_ROW}:
res = finalize(stmt) == SQLITE_OK
else:
discard finalize(stmt)
res = false
if not res:
dbError(db)
proc exec*(db:PSqlite3, query: string, args: JsonNode, timeout:int) {.async.} =
## used for rdb.raw().exec()
## args are `JArray`
assert(not db.isNil, "Database not connected.")
sleepAsync(0).await
var stmt: PStmt
var res:bool
if prepare_v2(db, query.cstring, query.len.cint, stmt, nil) == SQLITE_OK:
var argCount = 1
for arg in args.items:
defer: argCount.inc()
case arg.kind
of JBool:
let value = arg.getBool
let insertVal = if value:1 else: 0
discard bind_int64(stmt, argCount.int32, insertVal)
of JInt:
let value = arg.getInt
discard bind_int64(stmt, argCount.int32, value)
of JFloat:
let value = arg.getFloat
discard bind_double(stmt, argCount.int32, value.float64)
of JNull:
discard bind_null(stmt, argCount.int32)
of JObject, JArray:
let value = arg.pretty
discard bind_text(stmt, argCount.int32, value.cstring, value.len.int32, SQLITE_TRANSIENT)
of JString:
let value = arg.getStr
discard bind_text(stmt, argCount.int32, value.cstring, value.len.int32, SQLITE_TRANSIENT)
let x = step(stmt)
if x in {SQLITE_DONE, SQLITE_ROW}:
res = finalize(stmt) == SQLITE_OK
else:
discard finalize(stmt)
res = false
if not res:
dbError(db)
proc exec*(db:PSqlite3, query: string, args: seq[string], timeout:int) {.async.} =
## Not used anymore
assert(not db.isNil, "Database not connected.")
sleepAsync(0).await
var q = dbFormat(query, args)
var stmt: PStmt
var res:bool
if prepare_v2(db, q.cstring, q.len.cint, stmt, nil) == SQLITE_OK:
let x = step(stmt)
if x in {SQLITE_DONE, SQLITE_ROW}:
res = finalize(stmt) == SQLITE_OK
else:
discard finalize(stmt)
res = false
if not res:
dbError(db)
proc getColumns*(db:PSqlite3, query:string, args:seq[string], timeout:int):Future[seq[string]] {.async.} =
assert(not db.isNil, "Database not connected.")
var dbRows: DbRows
return db.getColumns(dbRows, query, args)
proc prepare*(db:PSqlite3, query:string, timeout:int):Future[PStmt] {.async.} =
if prepare_v2(db, query, query.len.cint, result, nil) != SQLITE_OK:
discard finalize(result)
dbError(db)
proc bindPreparedParams(db: PSqlite3, stmt: PStmt, args: seq[PreparedParam]) =
if reset(stmt) != SQLITE_OK:
dbError(db)
if clear_bindings(stmt) != SQLITE_OK:
dbError(db)
for i, arg in args:
let paramIdx = i.int32 + 1
if arg.isNull:
if bind_null(stmt, paramIdx) != SQLITE_OK:
dbError(db)
else:
if bind_text(stmt, paramIdx, arg.value.cstring, arg.value.len.int32, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
proc preparedQueryReuse*(db: PSqlite3, stmt: PStmt, args: seq[PreparedParam], timeout: int,
cachedColumns: DbColumns): Future[(seq[Row], DbRows)] {.async.} =
assert(not db.isNil, "Database not connected.")
sleepAsync(0).await
bindPreparedParams(db, stmt, args)
defer:
discard clear_bindings(stmt)
var dbRows: DbRows
var rows = newSeq[seq[string]]()
while true:
let stepRes = step(stmt)
if stepRes == SQLITE_ROW:
var columns = cachedColumns
setColumnsRuntimeTypes(columns, stmt)
dbRows.add(columns)
var row = newSeq[string](int(column_count(stmt)))
for i in 0 ..< row.len:
let text = column_text(stmt, i.int32)
if text.isNil:
row[i] = ""
else:
row[i] = $text
rows.add(row)
continue
if stepRes == SQLITE_DONE:
break
dbError(db)
return (rows, dbRows)
proc preparedExecReuse*(db: PSqlite3, stmt: PStmt, args: seq[PreparedParam], timeout: int) {.async.} =
assert(not db.isNil, "Database not connected.")
sleepAsync(0).await
bindPreparedParams(db, stmt, args)
defer:
discard clear_bindings(stmt)
var stepRes = step(stmt)
while stepRes == SQLITE_ROW:
stepRes = step(stmt)
if stepRes != SQLITE_DONE:
dbError(db)
proc preparedQuery*(db:PSqlite3, args:seq[string] = @[], sqliteStmt:PStmt):Future[(seq[Row], DbRows)] {.async.} =
# bind params
for i, row in args:
sqliteStmt.bindParam(i+1, row)
# run query
assert(not db.isNil, "Database not connected.")
sleepAsync(0).await
var dbRows: DbRows
var rows = newSeq[seq[string]]()
for row in db.instantRows(dbRows, sqliteStmt):
await sleepAsync(0)
var columns = newSeq[string](row.len)
for i in 0..row.len()-1:
columns[i] = row[i]
rows.add(columns)
return (rows, dbRows)
proc preparedExec*(db:PSqlite3, args:seq[string] = @[], sqliteStmt:PStmt) {.async.} =
# bind params
for i, row in args:
sqliteStmt.bindParam(i+1, row)
# run query
sleepAsync(0).await
let x = step(sqliteStmt)
var res:bool
if x in {SQLITE_DONE, SQLITE_ROW}:
res = true
else:
res = false
discard finalize(sqliteStmt)
if not res:
dbError(db)
@@ -0,0 +1,200 @@
import std/macros
import ../../error
import ../../models/database_types
import ./sqlite_rdb
proc dbQuote(s: string): string =
## Escapes the `'` (single quote) char to `''`.
## Because single quote is used for defining `VARCHAR` in SQL.
runnableExamples:
doAssert dbQuote("'") == "''''"
doAssert dbQuote("A Foobar's pen.") == "'A Foobar''s pen.'"
if s == "null":
return "NULL"
result = newStringOfCap(s.len * 2 + 2)
result.add('\'')
for c in items(s):
if c == '\'': add(result, "''")
else: add(result, c)
add(result, '\'')
proc sqliteQuoteIdent*(name: string): string =
## `PRAGMA` 等で使う SQLite の二重引用符識別子(内部の `"` は `""` にエスケープ)。
result = newStringOfCap(name.len + 2)
result.add('"')
for c in name:
if c == '"':
result.add("\"\"")
else:
result.add(c)
result.add('"')
proc dbError*(db: PSqlite3) {.noreturn.} =
## Raises a `DbError` exception.
##
## **Examples:**
##
## .. code-block:: Nim
##
## let db = open("mytest.db", "", "", "")
## if not db.tryExec(sql"SELECT * FROM not_exist_table"):
## dbError(db)
## db.close()
var e: ref DbError
new(e)
e.msg = $errmsg(db)
raise e
proc dbFormat*(formatstr: string, args: varargs[string]): string =
var a = 0
result = newStringOfCap(formatstr.len + args.len * 8)
var segStart = 0
for j in 0 ..< formatstr.len:
if formatstr[j] == '?':
if j > segStart:
result.add(formatstr[segStart ..< j])
result.add(dbQuote(args[a]))
inc(a)
segStart = j + 1
if segStart < formatstr.len:
result.add(formatstr[segStart ..< formatstr.len])
proc setupQuery(db: PSqlite3, query: string, args: varargs[string]): PStmt =
assert(not db.isNil, "Database not connected.")
var q = dbFormat(query, args)
if prepare_v2(db, q.cstring, q.len.cint, result, nil) != SQLITE_OK: dbError(db)
proc toTypeKind(t: var DbType; x: int32) =
case x
of SQLITE_INTEGER:
t.kind = dbInt
t.size = 8
of SQLITE_FLOAT:
t.kind = dbFloat
t.size = 8
of SQLITE_BLOB: t.kind = dbBlob
of SQLITE_NULL: t.kind = dbNull
of SQLITE_TEXT: t.kind = dbVarchar
else: t.kind = dbUnknown
proc setColumnsStaticMeta*(columns: var DbColumns; x: PStmt) =
## ステップ前でも列名・宣言型・テーブル名は取得できる(行に依存しない)。
let L = column_count(x)
setLen(columns, L.int)
for i in 0'i32 ..< L:
columns[i].name = $column_name(x, i)
columns[i].typ.name = $column_decltype(x, i)
columns[i].tableName = $column_table_name(x, i)
proc setColumnsRuntimeTypes*(columns: var DbColumns; x: PStmt) =
## 行ごとに変わりうるのは `column_type` のみ。
let L = column_count(x)
for i in 0'i32 ..< L:
toTypeKind(columns[i].typ, column_type(x, i))
iterator instantRows*(db: PSqlite3; dbRows: var DbRows; query: string, args: seq[string]): InstantRow
{.tags: [ReadDbEffect].} =
## Similar to `instantRows iterator <#instantRows.i,DbConn,SqlQuery,varargs[string,]>`_,
## but sets information about columns to `columns`.
##
## **Examples:**
##
## .. code-block:: Nim
##
## let db = open("mytest.db", "", "", "")
##
## # Records of my_table:
## # | id | name |
## # |----|----------|
## # | 1 | item#1 |
## # | 2 | item#2 |
##
## var columns: DbColumns
## for row in db.instantRows(columns, sql"SELECT * FROM my_table"):
## discard
## echo columns[0]
##
## # Output:
## # (name: "id", tableName: "my_table", typ: (kind: dbNull,
## # notNull: false, name: "INTEGER", size: 0, maxReprLen: 0, precision: 0,
## # scale: 0, min: 0, max: 0, validValues: @[]), primaryKey: false,
## # foreignKey: false)
##
## db.close()
var stmt = setupQuery(db, query, args)
try:
var columns: DbColumns
setColumnsStaticMeta(columns, stmt)
while step(stmt) == SQLITE_ROW:
setColumnsRuntimeTypes(columns, stmt)
dbRows.add(columns)
yield stmt
finally:
if finalize(stmt) != SQLITE_OK: dbError(db)
iterator instantRowsPlain*(db: PSqlite3; query: string, args: seq[string]): InstantRow
{.tags: [ReadDbEffect].} =
var stmt = setupQuery(db, query, args)
try:
while step(stmt) == SQLITE_ROW:
yield stmt
finally:
if finalize(stmt) != SQLITE_OK: dbError(db)
iterator instantRows*(db: PSqlite3, dbRows: var DbRows, sqliteStmt: PStmt): InstantRow
{.tags: [ReadDbEffect,WriteDbEffect].} =
var sqliteStmt = sqliteStmt
try:
var columns: DbColumns
setColumnsStaticMeta(columns, sqliteStmt)
while step(sqliteStmt) == SQLITE_ROW:
setColumnsRuntimeTypes(columns, sqliteStmt)
dbRows.add(columns)
yield sqliteStmt
finally:
if finalize(sqliteStmt) != SQLITE_OK: dbError(db)
proc getColumns*(db: PSqlite3; dbRows: var DbRows; query: string, args: seq[string]):seq[string] =
var stmt = setupQuery(db, query, args)
try:
var i:int32 = 0
while true:
let name = column_name(stmt, i)
if name.len == 0: break
result.add($name)
i.inc()
finally:
if finalize(stmt) != SQLITE_OK: dbError(db)
return result
proc len*(row: InstantRow): int32 {.inline.} =
## Returns number of columns in a row.
##
## See also:
## * `instantRows iterator <#instantRows.i,DbConn,SqlQuery,varargs[string,]>`_
## example code
column_count(row)
proc `[]`*(row: InstantRow, col: int32): string {.inline.} =
## Returns text for given column of the row.
##
## See also:
## * `instantRows iterator <#instantRows.i,DbConn,SqlQuery,varargs[string,]>`_
## example code
$column_text(row, col)
template dbBindParamError*(paramIdx: int, val: varargs[untyped]) =
## Raises a `DbError` exception.
var e: ref DbError
new(e)
e.msg = "error binding param in position " & $paramIdx
raise e
proc bindParam*(ps: PStmt, paramIdx: int, val: string, copy = true) =
## Binds a string to the specified paramIndex.
## if copy is true then SQLite makes its own private copy of the data immediately
if bind_text(ps, paramIdx.int32, val.cstring, val.len.int32, if copy: SQLITE_TRANSIENT else: SQLITE_STATIC) != SQLITE_OK:
dbBindParamError(paramIdx, val)
@@ -0,0 +1,384 @@
when defined(nimHasStyleChecks):
{.push styleChecks: off.}
when defined(windows):
when defined(nimOldDlls):
const Lib = "sqlite3.dll"
elif defined(cpu64):
const Lib = "sqlite3_64.dll"
else:
const Lib = "sqlite3_32.dll"
elif defined(macosx):
const
Lib = "libsqlite3(|.0).dylib"
else:
const
Lib = "libsqlite3.so(|.0)"
when defined(staticSqlite):
{.pragma: mylib.}
{.compile("sqlite3.c", "-O3").}
else:
{.pragma: mylib, dynlib: Lib.}
const
SQLITE_INTEGER* = 1
SQLITE_FLOAT* = 2
SQLITE_BLOB* = 4
SQLITE_NULL* = 5
SQLITE_TEXT* = 3
SQLITE_UTF8* = 1
SQLITE_UTF16LE* = 2
SQLITE_UTF16BE* = 3 # Use native byte order
SQLITE_UTF16* = 4 # sqlite3_create_function only
SQLITE_ANY* = 5 #sqlite_exec return values
SQLITE_OK* = 0
SQLITE_ERROR* = 1 # SQL error or missing database
SQLITE_INTERNAL* = 2 # An internal logic error in SQLite
SQLITE_PERM* = 3 # Access permission denied
SQLITE_ABORT* = 4 # Callback routine requested an abort
SQLITE_BUSY* = 5 # The database file is locked
SQLITE_LOCKED* = 6 # A table in the database is locked
SQLITE_NOMEM* = 7 # A malloc() failed
SQLITE_READONLY* = 8 # Attempt to write a readonly database
SQLITE_INTERRUPT* = 9 # Operation terminated by sqlite3_interrupt()
SQLITE_IOERR* = 10 # Some kind of disk I/O error occurred
SQLITE_CORRUPT* = 11 # The database disk image is malformed
SQLITE_NOTFOUND* = 12 # (Internal Only) Table or record not found
SQLITE_FULL* = 13 # Insertion failed because database is full
SQLITE_CANTOPEN* = 14 # Unable to open the database file
SQLITE_PROTOCOL* = 15 # Database lock protocol error
SQLITE_EMPTY* = 16 # Database is empty
SQLITE_SCHEMA* = 17 # The database schema changed
SQLITE_TOOBIG* = 18 # Too much data for one row of a table
SQLITE_CONSTRAINT* = 19 # Abort due to constraint violation
SQLITE_MISMATCH* = 20 # Data type mismatch
SQLITE_MISUSE* = 21 # Library used incorrectly
SQLITE_NOLFS* = 22 # Uses OS features not supported on host
SQLITE_AUTH* = 23 # Authorization denied
SQLITE_FORMAT* = 24 # Auxiliary database format error
SQLITE_RANGE* = 25 # 2nd parameter to sqlite3_bind out of range
SQLITE_NOTADB* = 26 # File opened that is not a database file
SQLITE_ROW* = 100 # sqlite3_step() has another row ready
SQLITE_DONE* = 101 # sqlite3_step() has finished executing
SQLITE_COPY* = 0
SQLITE_CREATE_INDEX* = 1
SQLITE_CREATE_TABLE* = 2
SQLITE_CREATE_TEMP_INDEX* = 3
SQLITE_CREATE_TEMP_TABLE* = 4
SQLITE_CREATE_TEMP_TRIGGER* = 5
SQLITE_CREATE_TEMP_VIEW* = 6
SQLITE_CREATE_TRIGGER* = 7
SQLITE_CREATE_VIEW* = 8
SQLITE_DELETE* = 9
SQLITE_DROP_INDEX* = 10
SQLITE_DROP_TABLE* = 11
SQLITE_DROP_TEMP_INDEX* = 12
SQLITE_DROP_TEMP_TABLE* = 13
SQLITE_DROP_TEMP_TRIGGER* = 14
SQLITE_DROP_TEMP_VIEW* = 15
SQLITE_DROP_TRIGGER* = 16
SQLITE_DROP_VIEW* = 17
SQLITE_INSERT* = 18
SQLITE_PRAGMA* = 19
SQLITE_READ* = 20
SQLITE_SELECT* = 21
SQLITE_TRANSACTION* = 22
SQLITE_UPDATE* = 23
SQLITE_ATTACH* = 24
SQLITE_DETACH* = 25
SQLITE_ALTER_TABLE* = 26
SQLITE_REINDEX* = 27
SQLITE_DENY* = 1
SQLITE_IGNORE* = 2 # Original from sqlite3.h:
#define SQLITE_STATIC ((void(*)(void *))0)
#define SQLITE_TRANSIENT ((void(*)(void *))-1)
SQLITE_DETERMINISTIC* = 0x800
type
Sqlite3 {.pure, final.} = object
PSqlite3* = ptr Sqlite3
PPSqlite3* = ptr PSqlite3
Sqlite3_Backup {.pure, final.} = object
PSqlite3_Backup* = ptr Sqlite3_Backup
PPSqlite3_Backup* = ptr PSqlite3_Backup
Context{.pure, final.} = object
Pcontext* = ptr Context
TStmt{.pure, final.} = object
PStmt* = ptr TStmt
InstantRow* = PStmt ## A handle that can be used to get a row's column
Value{.pure, final.} = object
PValue* = ptr Value
PValueArg* = array[0..127, PValue]
Callback* = proc (para1: pointer, para2: int32, para3,
para4: cstringArray): int32{.cdecl.}
Tbind_destructor_func* = proc (para1: pointer){.cdecl, tags: [], gcsafe.}
Create_function_step_func* = proc (para1: Pcontext, para2: int32,
para3: PValueArg){.cdecl.}
Create_function_func_func* = proc (para1: Pcontext, para2: int32,
para3: PValueArg){.cdecl.}
Create_function_final_func* = proc (para1: Pcontext){.cdecl.}
Result_func* = proc (para1: pointer){.cdecl.}
Create_collation_func* = proc (para1: pointer, para2: int32, para3: pointer,
para4: int32, para5: pointer): int32{.cdecl.}
Collation_needed_func* = proc (para1: pointer, para2: PSqlite3, eTextRep: int32,
para4: cstring){.cdecl.}
const
SQLITE_STATIC* = nil
SQLITE_TRANSIENT* = cast[Tbind_destructor_func](-1)
proc close*(para1: PSqlite3): int32{.cdecl, mylib, importc: "sqlite3_close".}
proc exec*(para1: PSqlite3, sql: cstring, para3: Callback, para4: pointer,
errmsg: var cstring): int32{.cdecl, mylib,
importc: "sqlite3_exec".}
proc last_insert_rowid*(para1: PSqlite3): int64{.cdecl, mylib,
importc: "sqlite3_last_insert_rowid".}
proc changes*(para1: PSqlite3): int32{.cdecl, mylib, importc: "sqlite3_changes".}
proc total_changes*(para1: PSqlite3): int32{.cdecl, mylib,
importc: "sqlite3_total_changes".}
proc interrupt*(para1: PSqlite3){.cdecl, mylib, importc: "sqlite3_interrupt".}
proc complete*(sql: cstring): int32{.cdecl, mylib,
importc: "sqlite3_complete".}
proc complete16*(sql: pointer): int32{.cdecl, mylib,
importc: "sqlite3_complete16".}
proc busy_handler*(para1: PSqlite3,
para2: proc (para1: pointer, para2: int32): int32{.cdecl.},
para3: pointer): int32{.cdecl, mylib,
importc: "sqlite3_busy_handler".}
proc busy_timeout*(para1: PSqlite3, ms: int32): int32{.cdecl, mylib,
importc: "sqlite3_busy_timeout".}
proc get_table*(para1: PSqlite3, sql: cstring, resultp: var cstringArray,
nrow, ncolumn: var cint, errmsg: ptr cstring): int32{.cdecl,
mylib, importc: "sqlite3_get_table".}
proc free_table*(result: cstringArray){.cdecl, mylib,
importc: "sqlite3_free_table".}
# Todo: see how translate sqlite3_mprintf, sqlite3_vmprintf, sqlite3_snprintf
# function sqlite3_mprintf(_para1:Pchar; args:array of const):Pchar;cdecl; external Sqlite3Lib name 'sqlite3_mprintf';
proc mprintf*(para1: cstring): cstring{.cdecl, varargs, mylib,
importc: "sqlite3_mprintf".}
#function sqlite3_vmprintf(_para1:Pchar; _para2:va_list):Pchar;cdecl; external Sqlite3Lib name 'sqlite3_vmprintf';
proc free*(z: cstring){.cdecl, mylib, importc: "sqlite3_free".}
#function sqlite3_snprintf(_para1:longint; _para2:Pchar; _para3:Pchar; args:array of const):Pchar;cdecl; external Sqlite3Lib name 'sqlite3_snprintf';
proc snprintf*(para1: int32, para2: cstring, para3: cstring): cstring{.cdecl,
mylib, varargs, importc: "sqlite3_snprintf".}
proc set_authorizer*(para1: PSqlite3, xAuth: proc (para1: pointer, para2: int32,
para3: cstring, para4: cstring, para5: cstring, para6: cstring): int32{.
cdecl.}, pUserData: pointer): int32{.cdecl, mylib,
importc: "sqlite3_set_authorizer".}
proc trace*(para1: PSqlite3, xTrace: proc (para1: pointer, para2: cstring){.cdecl.},
para3: pointer): pointer{.cdecl, mylib,
importc: "sqlite3_trace".}
proc progress_handler*(para1: PSqlite3, para2: int32,
para3: proc (para1: pointer): int32{.cdecl.},
para4: pointer){.cdecl, mylib,
importc: "sqlite3_progress_handler".}
proc commit_hook*(para1: PSqlite3, para2: proc (para1: pointer): int32{.cdecl.},
para3: pointer): pointer{.cdecl, mylib,
importc: "sqlite3_commit_hook".}
proc open*(filename: cstring, ppDb: var PSqlite3): int32{.cdecl, mylib,
importc: "sqlite3_open".}
proc open16*(filename: pointer, ppDb: var PSqlite3): int32{.cdecl, mylib,
importc: "sqlite3_open16".}
proc errcode*(db: PSqlite3): int32{.cdecl, mylib, importc: "sqlite3_errcode".}
proc errmsg*(para1: PSqlite3): cstring{.cdecl, mylib, importc: "sqlite3_errmsg".}
proc errmsg16*(para1: PSqlite3): pointer{.cdecl, mylib,
importc: "sqlite3_errmsg16".}
proc prepare*(db: PSqlite3, zSql: cstring, nBytes: int32, ppStmt: var PStmt,
pzTail: ptr cstring): int32{.cdecl, mylib,
importc: "sqlite3_prepare".}
proc prepare_v2*(db: PSqlite3, zSql: cstring, nByte: cint, ppStmt: var PStmt,
pzTail: ptr cstring): cint {.
importc: "sqlite3_prepare_v2", cdecl, mylib.}
proc prepare16*(db: PSqlite3, zSql: pointer, nBytes: int32, ppStmt: var PStmt,
pzTail: var pointer): int32{.cdecl, mylib,
importc: "sqlite3_prepare16".}
proc bind_blob*(para1: PStmt, para2: int32, para3: pointer, n: int32,
para5: Tbind_destructor_func): int32{.cdecl, mylib,
importc: "sqlite3_bind_blob".}
proc bind_double*(para1: PStmt, para2: int32, para3: float64): int32{.cdecl,
mylib, importc: "sqlite3_bind_double".}
proc bind_int*(para1: PStmt, para2: int32, para3: int32): int32{.cdecl,
mylib, importc: "sqlite3_bind_int".}
proc bind_int64*(para1: PStmt, para2: int32, para3: int64): int32{.cdecl,
mylib, importc: "sqlite3_bind_int64".}
proc bind_null*(para1: PStmt, para2: int32): int32{.cdecl, mylib,
importc: "sqlite3_bind_null".}
proc bind_text*(para1: PStmt, para2: int32, para3: cstring, n: int32,
para5: Tbind_destructor_func): int32{.cdecl, mylib,
importc: "sqlite3_bind_text".}
proc bind_text16*(para1: PStmt, para2: int32, para3: pointer, para4: int32,
para5: Tbind_destructor_func): int32{.cdecl, mylib,
importc: "sqlite3_bind_text16".}
#function sqlite3_bind_value(_para1:Psqlite3_stmt; _para2:longint; _para3:Psqlite3_value):longint;cdecl; external Sqlite3Lib name 'sqlite3_bind_value';
#These overloaded functions were introduced to allow the use of SQLITE_STATIC and SQLITE_TRANSIENT
#It's the c world man ;-)
proc bind_blob*(para1: PStmt, para2: int32, para3: pointer, n: int32,
para5: int32): int32{.cdecl, mylib,
importc: "sqlite3_bind_blob".}
proc bind_text*(para1: PStmt, para2: int32, para3: cstring, n: int32,
para5: int32): int32{.cdecl, mylib,
importc: "sqlite3_bind_text".}
proc bind_text16*(para1: PStmt, para2: int32, para3: pointer, para4: int32,
para5: int32): int32{.cdecl, mylib,
importc: "sqlite3_bind_text16".}
proc bind_parameter_count*(para1: PStmt): int32{.cdecl, mylib,
importc: "sqlite3_bind_parameter_count".}
proc bind_parameter_name*(para1: PStmt, para2: int32): cstring{.cdecl,
mylib, importc: "sqlite3_bind_parameter_name".}
proc bind_parameter_index*(para1: PStmt, zName: cstring): int32{.cdecl,
mylib, importc: "sqlite3_bind_parameter_index".}
proc clear_bindings*(para1: PStmt): int32 {.cdecl,
mylib, importc: "sqlite3_clear_bindings".}
proc column_count*(PStmt: PStmt): int32{.cdecl, mylib,
importc: "sqlite3_column_count".}
proc column_name*(para1: PStmt, para2: int32): cstring{.cdecl, mylib,
importc: "sqlite3_column_name".}
proc column_table_name*(para1: PStmt; para2: int32): cstring{.cdecl, mylib,
importc: "sqlite3_column_table_name".}
proc column_name16*(para1: PStmt, para2: int32): pointer{.cdecl, mylib,
importc: "sqlite3_column_name16".}
proc column_decltype*(para1: PStmt, i: int32): cstring{.cdecl, mylib,
importc: "sqlite3_column_decltype".}
proc column_decltype16*(para1: PStmt, para2: int32): pointer{.cdecl,
mylib, importc: "sqlite3_column_decltype16".}
proc step*(para1: PStmt): int32{.cdecl, mylib, importc: "sqlite3_step".}
proc data_count*(PStmt: PStmt): int32{.cdecl, mylib,
importc: "sqlite3_data_count".}
proc column_blob*(para1: PStmt, iCol: int32): pointer{.cdecl, mylib,
importc: "sqlite3_column_blob".}
proc column_bytes*(para1: PStmt, iCol: int32): int32{.cdecl, mylib,
importc: "sqlite3_column_bytes".}
proc column_bytes16*(para1: PStmt, iCol: int32): int32{.cdecl, mylib,
importc: "sqlite3_column_bytes16".}
proc column_double*(para1: PStmt, iCol: int32): float64{.cdecl, mylib,
importc: "sqlite3_column_double".}
proc column_int*(para1: PStmt, iCol: int32): int32{.cdecl, mylib,
importc: "sqlite3_column_int".}
proc column_int64*(para1: PStmt, iCol: int32): int64{.cdecl, mylib,
importc: "sqlite3_column_int64".}
proc column_text*(para1: PStmt, iCol: int32): cstring{.cdecl, mylib,
importc: "sqlite3_column_text".}
proc column_text16*(para1: PStmt, iCol: int32): pointer{.cdecl, mylib,
importc: "sqlite3_column_text16".}
proc column_type*(para1: PStmt, iCol: int32): int32{.cdecl, mylib,
importc: "sqlite3_column_type".}
proc finalize*(PStmt: PStmt): int32{.cdecl, mylib,
importc: "sqlite3_finalize".}
proc reset*(PStmt: PStmt): int32{.cdecl, mylib, importc: "sqlite3_reset".}
proc create_function*(para1: PSqlite3, zFunctionName: cstring, nArg: int32,
eTextRep: int32, para5: pointer,
xFunc: Create_function_func_func,
xStep: Create_function_step_func,
xFinal: Create_function_final_func): int32{.cdecl,
mylib, importc: "sqlite3_create_function".}
proc create_function16*(para1: PSqlite3, zFunctionName: pointer, nArg: int32,
eTextRep: int32, para5: pointer,
xFunc: Create_function_func_func,
xStep: Create_function_step_func,
xFinal: Create_function_final_func): int32{.cdecl,
mylib, importc: "sqlite3_create_function16".}
proc aggregate_count*(para1: Pcontext): int32{.cdecl, mylib,
importc: "sqlite3_aggregate_count".}
proc value_blob*(para1: PValue): pointer{.cdecl, mylib,
importc: "sqlite3_value_blob".}
proc value_bytes*(para1: PValue): int32{.cdecl, mylib,
importc: "sqlite3_value_bytes".}
proc value_bytes16*(para1: PValue): int32{.cdecl, mylib,
importc: "sqlite3_value_bytes16".}
proc value_double*(para1: PValue): float64{.cdecl, mylib,
importc: "sqlite3_value_double".}
proc value_int*(para1: PValue): int32{.cdecl, mylib,
importc: "sqlite3_value_int".}
proc value_int64*(para1: PValue): int64{.cdecl, mylib,
importc: "sqlite3_value_int64".}
proc value_text*(para1: PValue): cstring{.cdecl, mylib,
importc: "sqlite3_value_text".}
proc value_text16*(para1: PValue): pointer{.cdecl, mylib,
importc: "sqlite3_value_text16".}
proc value_text16le*(para1: PValue): pointer{.cdecl, mylib,
importc: "sqlite3_value_text16le".}
proc value_text16be*(para1: PValue): pointer{.cdecl, mylib,
importc: "sqlite3_value_text16be".}
proc value_type*(para1: PValue): int32{.cdecl, mylib,
importc: "sqlite3_value_type".}
proc aggregate_context*(para1: Pcontext, nBytes: int32): pointer{.cdecl,
mylib, importc: "sqlite3_aggregate_context".}
proc user_data*(para1: Pcontext): pointer{.cdecl, mylib,
importc: "sqlite3_user_data".}
proc get_auxdata*(para1: Pcontext, para2: int32): pointer{.cdecl, mylib,
importc: "sqlite3_get_auxdata".}
proc set_auxdata*(para1: Pcontext, para2: int32, para3: pointer,
para4: proc (para1: pointer){.cdecl.}){.cdecl, mylib,
importc: "sqlite3_set_auxdata".}
proc result_blob*(para1: Pcontext, para2: pointer, para3: int32,
para4: Result_func){.cdecl, mylib,
importc: "sqlite3_result_blob".}
proc result_double*(para1: Pcontext, para2: float64){.cdecl, mylib,
importc: "sqlite3_result_double".}
proc result_error*(para1: Pcontext, para2: cstring, para3: int32){.cdecl,
mylib, importc: "sqlite3_result_error".}
proc result_error16*(para1: Pcontext, para2: pointer, para3: int32){.cdecl,
mylib, importc: "sqlite3_result_error16".}
proc result_int*(para1: Pcontext, para2: int32){.cdecl, mylib,
importc: "sqlite3_result_int".}
proc result_int64*(para1: Pcontext, para2: int64){.cdecl, mylib,
importc: "sqlite3_result_int64".}
proc result_null*(para1: Pcontext){.cdecl, mylib,
importc: "sqlite3_result_null".}
proc result_text*(para1: Pcontext, para2: cstring, para3: int32,
para4: Result_func){.cdecl, mylib,
importc: "sqlite3_result_text".}
proc result_text16*(para1: Pcontext, para2: pointer, para3: int32,
para4: Result_func){.cdecl, mylib,
importc: "sqlite3_result_text16".}
proc result_text16le*(para1: Pcontext, para2: pointer, para3: int32,
para4: Result_func){.cdecl, mylib,
importc: "sqlite3_result_text16le".}
proc result_text16be*(para1: Pcontext, para2: pointer, para3: int32,
para4: Result_func){.cdecl, mylib,
importc: "sqlite3_result_text16be".}
proc result_value*(para1: Pcontext, para2: PValue){.cdecl, mylib,
importc: "sqlite3_result_value".}
proc create_collation*(para1: PSqlite3, zName: cstring, eTextRep: int32,
para4: pointer, xCompare: Create_collation_func): int32{.
cdecl, mylib, importc: "sqlite3_create_collation".}
proc create_collation16*(para1: PSqlite3, zName: cstring, eTextRep: int32,
para4: pointer, xCompare: Create_collation_func): int32{.
cdecl, mylib, importc: "sqlite3_create_collation16".}
proc collation_needed*(para1: PSqlite3, para2: pointer, para3: Collation_needed_func): int32{.
cdecl, mylib, importc: "sqlite3_collation_needed".}
proc collation_needed16*(para1: PSqlite3, para2: pointer, para3: Collation_needed_func): int32{.
cdecl, mylib, importc: "sqlite3_collation_needed16".}
proc libversion*(): cstring{.cdecl, mylib, importc: "sqlite3_libversion".}
#Alias for allowing better code portability (win32 is not working with external variables)
proc version*(): cstring{.cdecl, mylib, importc: "sqlite3_libversion".}
# Not published functions
proc libversion_number*(): int32{.cdecl, mylib,
importc: "sqlite3_libversion_number".}
proc backup_init*(pDest: PSqlite3, zDestName: cstring, pSource: PSqlite3, zSourceName: cstring): PSqlite3_Backup {.
cdecl, mylib, importc: "sqlite3_backup_init".}
proc backup_step*(pBackup: PSqlite3_Backup, nPage: int32): int32 {.cdecl, mylib, importc: "sqlite3_backup_step".}
proc backup_finish*(pBackup: PSqlite3_Backup): int32 {.cdecl, mylib, importc: "sqlite3_backup_finish".}
proc backup_pagecount*(pBackup: PSqlite3_Backup): int32 {.cdecl, mylib, importc: "sqlite3_backup_pagecount".}
proc backup_remaining*(pBackup: PSqlite3_Backup): int32 {.cdecl, mylib, importc: "sqlite3_backup_remaining".}
proc sqlite3_sleep*(t: int64): int64 {.cdecl, mylib, importc: "sqlite3_sleep".}
#function sqlite3_key(db:Psqlite3; pKey:pointer; nKey:longint):longint;cdecl; external Sqlite3Lib name 'sqlite3_key';
#function sqlite3_rekey(db:Psqlite3; pKey:pointer; nKey:longint):longint;cdecl; external Sqlite3Lib name 'sqlite3_rekey';
#function sqlite3_sleep(_para1:longint):longint;cdecl; external Sqlite3Lib name 'sqlite3_sleep';
#function sqlite3_expired(_para1:Psqlite3_stmt):longint;cdecl; external Sqlite3Lib name 'sqlite3_expired';
#function sqlite3_global_recover:longint;cdecl; external Sqlite3Lib name 'sqlite3_global_recover';
# implementation
when defined(nimHasStyleChecks):
{.pop.}
@@ -0,0 +1,148 @@
import std/asyncdispatch
import std/httpclient
import std/strformat
import std/strutils
import std/json
import ../../error
import ./surreal_rdb
import ./surreal_lib
type SurrealImpl* = ref object
proc surrealTimeoutMs(timeout: int): int =
if timeout <= 0:
return 0
if timeout > high(int) div 1000:
return high(int)
result = timeout * 1000
proc awaitWithTimeout[T](fut: Future[T], timeoutMs: int, timeoutMsg: string): Future[T] {.async.} =
if timeoutMs <= 0:
return await fut
let ok = await withTimeout(fut, timeoutMs)
if not ok:
dbError(timeoutMsg)
result = fut.read
proc failSurrealBody(body: JsonNode) =
if body.kind == JObject:
if body.hasKey("code") and body["code"].kind == JInt and body["code"].getInt == 400:
if body.hasKey("information") and body["information"].kind == JString:
dbError(body["information"].getStr())
if body.hasKey("detail") and not body.hasKey("result") and body["detail"].kind == JString:
dbError(body["detail"].getStr())
if body.kind == JArray:
for row in body.items:
if row.kind != JObject:
continue
if row.hasKey("status") and row["status"].kind == JString and row["status"].getStr() == "ERR":
if row.hasKey("detail") and row["detail"].kind == JString:
dbError(row["detail"].getStr())
if row.hasKey("detail") and not row.hasKey("result") and row["detail"].kind == JString:
dbError(row["detail"].getStr())
proc runSurrealSql(db: SurrealConn, sql: string, timeout: int): Future[JsonNode] {.async.} =
let timeoutMs = surrealTimeoutMs(timeout)
let url = &"{db.host}:{db.port}/sql"
let response = await awaitWithTimeout(
db.client.post(url, sql),
timeoutMs,
&"SurrealDb request timed out after {timeoutMs} ms"
)
let bodyText = await awaitWithTimeout(response.body(), timeoutMs,
&"SurrealDb response body timed out after {timeoutMs} ms")
var body: JsonNode
try:
body = bodyText.parseJson()
except CatchableError:
dbError(&"SurrealDb returned non-JSON response: {bodyText}")
failSurrealBody(body)
if response.status != $Http200:
dbError(&"SurrealDb request failed with HTTP status {response.status}")
return body
# proc open*(_:type SurrealImpl, namespace="", database="",user="", password="",
# host="", port:int32 = 0, maxConnections=1, timeout=30):Future[SurrealConnections] {.async.} =
# var pools = newSeq[SurrealConn](maxConnections)
# for i in 0..<maxConnections:
# let client = newAsyncHttpClient()
# var headers = newHttpHeaders(true)
# headers["NS"] = namespace
# headers["DB"] = database
# headers["Accept"] = "application/json"
# headers["Authorization"] = "Basic " & base64.encode(user & ":" & password)
# client.headers = headers
# var url = &"{host}:{port}/status"
# var resp = client.get(url).await
# if(resp.status != $Http200):
# dbError(&"Cannot connect to SurrealDb {host}:{port}")
# url = &"{host}:{port}/sql"
# resp = client.post(url, &"DEFINE NAMESPACE `{namespace}`; USE NS `{namespace}`; DEFINE DATABASE `{database}`").await
# if(resp.status != $Http200):
# dbError(&"Cannot connect to SurrealDb {host}:{port}")
# pools[i] = SurrealConn(
# conn: client,
# host:host,
# port:port,
# isBusy: false,
# createdAt: getTime().toUnix(),
# )
# return SurrealConnections(
# pools: pools,
# timeout: timeout
# )
proc query*(db:SurrealConn, query: string, args: seq[string], timeout:int):Future[JsonNode] {.async.} =
## return JArray
assert(not db.client.isNil, "Database not connected.")
let body = await runSurrealSql(db, dbFormat(query, args), timeout)
if body.kind != JArray or body.len == 0:
dbError("SurrealDb returned empty result set")
return body[^1]["result"]
proc query*(db:SurrealConn, query: string, args: JsonNode, timeout:int):Future[JsonNode] {.async.} =
## return JArray
assert(not db.client.isNil, "Database not connected.")
let body = await runSurrealSql(db, dbFormat(query, args), timeout)
if body.kind != JArray or body.len == 0:
dbError("SurrealDb returned empty result set")
return body[^1]["result"]
proc exec*(db:SurrealConn, query: string, args: seq[string], timeout:int) {.async.} =
assert(not db.client.isNil, "Database not connected.")
let body = await runSurrealSql(db, dbFormat(query, args), timeout)
if body[^1]["status"].getStr() == "ERR":
dbError(body[^1]["result"].getStr())
proc exec*(db:SurrealConn, query: string, args: JsonNode, timeout:int) {.async.} =
assert(not db.client.isNil, "Database not connected.")
let body = await runSurrealSql(db, dbFormat(query, args), timeout)
if body[^1]["status"].getStr() == "ERR":
dbError(body[^1]["result"].getStr())
proc info*(db:SurrealConn, query: string, args: seq[string], timeout:int):Future[JsonNode] {.async.} =
assert(not db.client.isNil, "Database not connected.")
return await runSurrealSql(db, dbFormat(query, args), timeout)
proc info*(db:SurrealConn, query: string, args: JsonNode, timeout:int):Future[JsonNode] {.async.} =
assert(not db.client.isNil, "Database not connected.")
return await runSurrealSql(db, dbFormat(query, args), timeout)
@@ -0,0 +1,197 @@
import std/strutils
import std/json
proc looksLikeRecordId(s: string): bool =
let colonPos = s.find(':')
if colonPos <= 0 or colonPos >= s.high:
return false
if s.find(':', colonPos + 1) >= 0:
return false
if s.contains(' ') or s.contains('"') or s.contains('\''):
return false
if not (s[0].isAlphaAscii or s[0] == '_'):
return false
for ch in s[1 ..< colonPos]:
if not (ch.isAlphaNumeric or ch == '_'):
return false
for ch in s[colonPos + 1 .. ^1]:
if ch.isSpaceAscii:
return false
return true
proc looksLikeDateTimeValue(s: string): bool =
if s.len < 10:
return false
for idx in [0, 1, 2, 3, 5, 6, 8, 9]:
if idx >= s.len or not s[idx].isDigit:
return false
if s[4] != '-' or s[7] != '-':
return false
if s.len == 10:
return true
if s.len < 19:
return false
if s[10] notin {'T', ' '}:
return false
for idx in [11, 12, 14, 15, 17, 18]:
if idx >= s.len or not s[idx].isDigit:
return false
if s[13] != ':' or s[16] != ':':
return false
if s.len == 19:
return true
return s[19] in {'.', 'Z', '+', '-'}
proc dbQuote(s:string):string =
## DB quotes the string.
if s == "null":
return "NULL"
if looksLikeRecordId(s):
return s
result = newStringOfCap(s.len + 2)
result.add "'"
for c in items(s):
# see https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html#mysql-escaping
case c
of '\0': result.add "\\0"
of '\b': result.add "\\b"
of '\t': result.add "\\t"
of '\l': result.add "\\n"
of '\r': result.add "\\r"
of '\x1a': result.add "\\Z"
of '"': result.add "\\\""
of '\'': result.add "\\'"
of '\\': result.add "\\\\"
of '_': result.add "\\_"
else: result.add c
add(result, '\'')
proc appendAlphabet(result: var string, n: int) =
var n = n
if n <= 0:
return
var chars: seq[char] = @[]
while n > 0:
n.dec()
chars.add(chr(ord('a') + (n mod 26)))
n = n div 26
for i in countdown(chars.high, 0):
result.add(chars[i])
proc dbFormat*(formatstr: string, args: varargs[string]): string =
var a = 0
result = newStringOfCap(formatstr.len + args.len * 8)
var segStart = 0
for j in 0 ..< formatstr.len:
if formatstr[j] == '?':
if j > segStart:
result.add(formatstr[segStart ..< j])
result.add(dbQuote(args[a]))
inc(a)
segStart = j + 1
if segStart < formatstr.len:
result.add(formatstr[segStart ..< formatstr.len])
proc numToAlphabet*(n:int):string =
## 1 => "a", 26 => "z", 27 => "aa", 28 => "ab", 52 => "az", 53 => "ba"
result = newStringOfCap(8)
appendAlphabet(result, n)
proc questionToDaller*(s: string): string =
## from `UPDATE user SET name = ?, email = ? WHERE id = ?`
##
## to `UPDATE user SET name = $a, email = $b WHERE id = $c`
var i = 1
var segStart = 0
result = newStringOfCap(s.len + 8)
for j in 0 ..< s.len:
if s[j] == '?':
if j > segStart:
result.add(s[segStart ..< j])
result.add('$')
appendAlphabet(result, i)
inc(i)
segStart = j + 1
if segStart < s.len:
result.add(s[segStart ..< s.len])
proc appendJsonLetClause(result: var string, idx: int, arg: JsonNode, quoteString: bool) =
result.add("LET $")
appendAlphabet(result, idx)
result.add(" = ")
case arg.kind
of JBool:
result.add($arg.getBool)
of JInt:
result.add($arg.getInt)
of JFloat:
result.add($arg.getFloat)
of JString:
let val = arg.getStr().replace("\"", "\\\"")
if looksLikeRecordId(val):
result.add(val)
elif looksLikeDateTimeValue(val):
result.add("<datetime>\"")
result.add(val)
result.add("\"")
elif quoteString:
result.add('"')
result.add(val)
result.add('"')
else:
result.add(val)
of JNull:
result.add("NONE")
of JArray, JObject:
result.add($arg)
result.add("; ")
proc dbFormatPrepared*(normalizedQueryString: string, args: JsonNode): string =
## `normalizedQueryString` should already have `?` converted to SurrealQL
## style placeholders.
if args.isNil:
result = newStringOfCap(normalizedQueryString.len)
result.add(normalizedQueryString)
return
if args.kind == JNull:
result = newStringOfCap(normalizedQueryString.len)
result.add(normalizedQueryString)
return
result = newStringOfCap(normalizedQueryString.len + max(args.len, 1) * 24)
if args.kind == JArray and args.len > 0:
var i = 1
for arg in args.items:
appendJsonLetClause(result, i, arg, true)
inc(i)
elif args.kind == JObject and args.len > 0:
var i = 1
for (_, arg) in args.pairs:
appendJsonLetClause(result, i, arg, false)
inc(i)
result.add(normalizedQueryString)
proc dbFormat*(queryString: string, args: JsonNode): string =
result = dbFormatPrepared(queryString.questionToDaller(), args)
@@ -0,0 +1,6 @@
import std/httpclient
type SurrealConn* = object
client*: AsyncHttpClient
host*:string
port*:int32
@@ -0,0 +1,58 @@
import std/os
import std/parsecfg
import std/terminal
import std/logging
import std/macros
import std/strformat
type
LogSetting* = ref object
shouldDisplayLog*: bool
shouldOutputLogFile*: bool
logDir*: string
proc logger*(self:LogSetting, output: auto) =
# let msg = $output & " " & $args
let msg = $output
# console log
if self.shouldDisplayLog:
let logger = newConsoleLogger()
logger.log(lvlDebug, msg)
# file log
if self.shouldOutputLogFile:
# info $output & $args
let path = self.logDir & "/log.log"
createDir(parentDir(path))
let logger = newRollingFileLogger(path, mode=fmAppend, fmtStr=verboseFmtStr)
defer: logger.file.close()
logger.log(lvlDebug, msg)
flushFile(logger.file)
proc echoErrorMsg*(self:LogSetting, msg:string, args:seq[string] = @[]) =
let msg = msg & " " & $args
# console log
if self.shouldDisplayLog:
styledWriteLine(stdout, fgRed, bgDefault, msg, resetStyle)
# file log
if self.shouldOutputLogFile:
let path = self.logDir & "/error.log"
createDir(parentDir(path))
let logger = newRollingFileLogger(path, mode=fmAppend, fmtStr=verboseFmtStr)
defer: logger.file.close()
logger.log(lvlError, msg)
flushFile(logger.file)
proc echoWarningMsg*(self:LogSetting, msg:string) =
# console log
if self.shouldDisplayLog:
styledWriteLine(stdout, fgYellow, bgDefault, msg, resetStyle)
# file log
if self.shouldOutputLogFile:
let path = self.logDir & "/error.log"
createDir(parentDir(path))
let logger = newRollingFileLogger(path, mode=fmAppend, fmtStr=verboseFmtStr)
defer: logger.file.close()
logger.log(lvlError, msg)
flushFile(logger.file)
@@ -0,0 +1,716 @@
import std/asyncdispatch
import std/deques
import std/json
import std/monotimes
import std/options
import std/strformat
import std/strutils
import std/sequtils
import std/tables
import std/times
import ../../error
import ../../libs/baradb/baradb_client
import ../../log
import ../database_types
import ./query/baradb_builder
import ./baradb_types
# ================================================================================
# connection pool
# ================================================================================
proc removePoolWaiter(pools: Connections, w: Future[void]) =
var kept = initDeque[Future[void]]()
while pools.waiters.len > 0:
let x = pools.waiters.popFirst()
if x != w:
kept.addLast(x)
pools.waiters = move(kept)
proc wakeOnePoolWaiter(pools: Connections) =
while pools.waiters.len > 0:
let w = pools.waiters.popFirst()
if w.finished:
continue
w.complete()
break
proc poolRemainingMs(deadline: MonoTime): int =
let left = (deadline - getMonoTime()).inMilliseconds
if left <= 0:
return 0
if left > int64(high(int)):
return high(int)
result = int(left)
if result < 1:
result = 1
proc nowUnix(): int64 =
getTime().toUnix()
proc hasConnExpired(self: Connections, conn: Connection): bool =
let now = nowUnix()
if self.maxConnectionLifetime > 0 and now - conn.createdAt >= self.maxConnectionLifetime.int64:
return true
if self.maxConnectionIdleTime > 0 and now - conn.lastUsedAt >= self.maxConnectionIdleTime.int64:
return true
return false
proc openBaradbConn(self: Connections): BaraClient =
let config = ClientConfig(
host: self.host,
port: self.port,
database: self.database,
username: self.user,
password: self.password,
timeoutMs: self.timeout * 1000,
maxRetries: 3,
)
let client = newClient(config)
waitFor client.connect()
return client
proc refreshConn(self: Connections, connI: int): bool =
if connI < 0 or connI >= self.conns.len:
return false
let client = openBaradbConn(self)
let oldConn = self.conns[connI].client
if not oldConn.isNil:
oldConn.close()
self.conns[connI].client = client
self.conns[connI].createdAt = nowUnix()
self.conns[connI].lastUsedAt = self.conns[connI].createdAt
return true
proc getFreeConn(self: BaradbConnections | BaradbQuery | RawBaradbQuery): Future[int] {.async.} =
let deadline = getMonoTime() + initDuration(seconds = self.pools.timeout)
while true:
for i in 0 ..< self.pools.conns.len:
if not self.pools.conns[i].isBusy:
self.pools.conns[i].isBusy = true
if self.pools.hasConnExpired(self.pools.conns[i]):
try:
discard self.pools.refreshConn(i)
except CatchableError:
discard
when defined(check_pool):
echo "=== getFreeConn ", i
return i
if getMonoTime() >= deadline:
return errorConnectionNum
let w = newFuture[void]("getFreeConn.poolWait")
self.pools.waiters.addLast(w)
var ms = poolRemainingMs(deadline)
if ms < 1:
ms = 1
let ok = await withTimeout(w, ms)
if not ok:
removePoolWaiter(self.pools, w)
return errorConnectionNum
proc returnConn(self: BaradbConnections | BaradbQuery | RawBaradbQuery, i: int) {.async.} =
if i != errorConnectionNum:
self.pools.conns[i].isBusy = false
self.pools.conns[i].lastUsedAt = nowUnix()
wakeOnePoolWaiter(self.pools)
proc raisePoolTimeout(self: BaradbConnections | BaradbQuery | RawBaradbQuery) {.noreturn.} =
raise newException(DbError, "Timed out while waiting for a free Baradb connection")
# ================================================================================
# SQL formatting helpers
# ================================================================================
proc escapeSqlValue(val: JsonNode): string =
case val.kind
of JNull:
return "NULL"
of JBool:
return if val.getBool: "TRUE" else: "FALSE"
of JInt:
return $val.getInt
of JFloat:
return $val.getFloat
of JString:
let s = val.getStr
return "'" & s.replace("'", "''") & "'"
else:
return "'" & val.pretty.replace("'", "''") & "'"
proc formatSql*(sql: string, args: seq[JsonNode]): string =
result = sql
for arg in args:
let pos = result.find("?")
if pos < 0:
break
result = result[0..<pos] & escapeSqlValue(arg) & result[pos+1..^1]
proc formatSql*(sql: string, args: JsonNode): string =
var arr: seq[JsonNode]
for arg in args.items:
arr.add(arg["value"])
return formatSql(sql, arr)
# ================================================================================
# toJson
# ================================================================================
proc toJson*(resultSet: QueryResult): seq[JsonNode] =
var response_table = newSeq[JsonNode](resultSet.rowCount)
for r in 0 ..< resultSet.rowCount:
var response_row = newJObject()
for c in 0 ..< resultSet.columns.len:
let key = resultSet.columns[c]
let val = resultSet.rows[r][c]
if val.len == 0:
response_row[key] = newJNull()
elif val == "NULL" or val == "null":
response_row[key] = newJNull()
elif val == "t" or val == "true" or val == "f" or val == "false":
response_row[key] = newJBool(val == "t" or val == "true")
else:
try:
response_row[key] = newJInt(val.parseInt)
except ValueError:
try:
response_row[key] = newJFloat(val.parseFloat)
except ValueError:
response_row[key] = newJString(val)
response_table[r] = response_row
return response_table
# ================================================================================
# private exec
# ================================================================================
proc getAllRows(self: BaradbQuery, queryString: string): Future[seq[JsonNode]] {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
let sql = formatSql(queryString, self.placeHolder)
let qr = await self.pools.conns[connI].client.query(sql)
if qr.rowCount == 0:
self.log.echoErrorMsg(queryString)
return newSeq[JsonNode](0)
return toJson(qr)
proc getAllRowsPlain(self: BaradbQuery, queryString: string, args: JsonNode): Future[seq[seq[string]]] {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
let sql = formatSql(queryString, args)
let qr = await self.pools.conns[connI].client.query(sql)
return qr.rows
proc getRow(self: BaradbQuery, queryString: string): Future[Option[JsonNode]] {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
let sql = formatSql(queryString, self.placeHolder)
let qr = await self.pools.conns[connI].client.query(sql)
if qr.rowCount == 0:
self.log.echoErrorMsg(queryString)
return none(JsonNode)
return toJson(qr)[0].some()
proc getRowPlain(self: BaradbQuery, queryString: string, args: JsonNode): Future[seq[string]] {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
let sql = formatSql(queryString, args)
let qr = await self.pools.conns[connI].client.query(sql)
return qr.rows[0]
proc getAllRows(self: RawBaradbQuery, queryString: string): Future[seq[JsonNode]] {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
var arr: seq[JsonNode]
for arg in self.placeHolder.items:
arr.add(arg)
let sql = formatSql(queryString, arr)
let qr = await self.pools.conns[connI].client.query(sql)
if qr.rowCount == 0:
self.log.echoErrorMsg(queryString)
return newSeq[JsonNode](0)
return toJson(qr)
proc getAllRowsPlain(self: RawBaradbQuery, queryString: string, args: JsonNode): Future[seq[seq[string]]] {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
var arr: seq[JsonNode]
for arg in args.items:
arr.add(arg)
let sql = formatSql(queryString, arr)
let qr = await self.pools.conns[connI].client.query(sql)
return qr.rows
proc getRow(self: RawBaradbQuery, queryString: string): Future[Option[JsonNode]] {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
var arr: seq[JsonNode]
for arg in self.placeHolder.items:
arr.add(arg)
let sql = formatSql(queryString, arr)
let qr = await self.pools.conns[connI].client.query(sql)
if qr.rowCount == 0:
self.log.echoErrorMsg(queryString)
return none(JsonNode)
return toJson(qr)[0].some()
proc getRowPlain(self: RawBaradbQuery, queryString: string, args: JsonNode): Future[seq[string]] {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
var arr: seq[JsonNode]
for arg in args.items:
arr.add(arg)
let sql = formatSql(queryString, arr)
let qr = await self.pools.conns[connI].client.query(sql)
return qr.rows[0]
proc exec(self: BaradbQuery, queryString: string) {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
let sql = formatSql(queryString, self.placeHolder)
discard await self.pools.conns[connI].client.exec(sql)
proc exec(self: RawBaradbQuery, queryString: string, args: JsonNode) {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
var arr: seq[JsonNode]
for arg in args.items:
arr.add(arg)
let sql = formatSql(queryString, arr)
discard await self.pools.conns[connI].client.exec(sql)
proc insertId(self: BaradbQuery, queryString: string, key: string): Future[string] {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
let sql = formatSql(queryString, self.placeHolder)
discard await self.pools.conns[connI].client.exec(sql)
let table = self.query["table"].getStr
let idSql = &"SELECT MAX(\"{key}\") FROM \"{table}\""
let qr = await self.pools.conns[connI].client.query(idSql)
if qr.rowCount > 0 and qr.rows[0].len > 0:
return qr.rows[0][0]
return ""
proc getColumns(self: BaradbQuery, queryString: string, args = newJArray()): Future[seq[string]] {.async.} =
var connI = self.transactionConn
if not self.isInTransaction:
connI = getFreeConn(self).await
defer:
if not self.isInTransaction:
self.returnConn(connI).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
var arr: seq[JsonNode]
for arg in args.items:
arr.add(arg["value"])
let sql = formatSql(queryString, arr)
let qr = await self.pools.conns[connI].client.query(sql)
if qr.rowCount > 0:
return qr.rows[0]
return @[]
proc transactionStart(self: BaradbConnections) {.async.} =
let connI = getFreeConn(self).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
self.isInTransaction = true
self.transactionConn = connI
discard await self.pools.conns[connI].client.exec("BEGIN")
proc transactionEnd(self: BaradbConnections, query: string) {.async.} =
defer:
self.returnConn(self.transactionConn).await
self.transactionConn = 0
self.isInTransaction = false
discard await self.pools.conns[self.transactionConn].client.exec(query)
# ================================================================================
# public exec
# ================================================================================
# ==================== return json ====================
proc get*(self: BaradbQuery): Future[seq[JsonNode]] {.async.} =
let sql = self.selectBuilder()
try:
self.log.logger(sql)
return await self.getAllRows(sql)
except CatchableError:
self.log.echoErrorMsg(sql)
self.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc first*(self: BaradbQuery): Future[Option[JsonNode]] {.async.} =
let sql = self.selectFirstBuilder()
try:
self.log.logger(sql)
return await self.getRow(sql)
except CatchableError:
self.log.echoErrorMsg(sql)
self.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc find*(self: BaradbQuery, id: string, key = "id"): Future[Option[JsonNode]] {.async.} =
self.placeHolder.add(%*{"key": key, "value": id})
let sql = self.selectFindBuilder(key)
try:
self.log.logger(sql)
return await self.getRow(sql)
except CatchableError:
self.log.echoErrorMsg(sql)
self.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc find*(self: BaradbQuery, id: int, key = "id"): Future[Option[JsonNode]] {.async.} =
return await self.find($id, key)
# ==================== return string ====================
proc getPlain*(self: BaradbQuery): Future[seq[seq[string]]] {.async.} =
let sql = self.selectBuilder()
try:
self.log.logger(sql)
return await self.getAllRowsPlain(sql, self.placeHolder)
except CatchableError:
self.log.echoErrorMsg(sql)
self.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc firstPlain*(self: BaradbQuery): Future[seq[string]] {.async.} =
let sql = self.selectFirstBuilder()
try:
self.log.logger(sql)
return await self.getRowPlain(sql, self.placeHolder)
except CatchableError:
self.log.echoErrorMsg(sql)
self.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc findPlain*(self: BaradbQuery, id: string, key = "id"): Future[seq[string]] {.async.} =
self.placeHolder.add(%*{"key": key, "value": id})
let sql = self.selectFindBuilder(key)
try:
self.log.logger(sql)
return await self.getRowPlain(sql, self.placeHolder)
except CatchableError:
self.log.echoErrorMsg(sql)
self.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc findPlain*(self: BaradbQuery, id: int, key = "id"): Future[seq[string]] {.async.} =
return await self.findPlain($id, key)
# ==================== insert JsonNode ====================
proc insert*(self: BaradbQuery, items: JsonNode) {.async.} =
let sql = self.insertValueBuilder(items)
self.log.logger(sql)
await self.exec(sql)
proc insert*(self: BaradbQuery, items: seq[JsonNode]) {.async.} =
let sql = self.insertValuesBuilder(items)
self.log.logger(sql)
await self.exec(sql)
proc insertId*(self: BaradbQuery, items: JsonNode, key = "id"): Future[string] {.async.} =
let sql = self.insertValueBuilder(items)
self.log.logger(sql)
return await self.insertId(sql, key)
proc insertId*(self: BaradbQuery, items: seq[JsonNode], key = "id"): Future[seq[string]] {.async.} =
result = newSeq[string](items.len)
for i, item in items:
let sql = self.insertValueBuilder(item)
self.log.logger(sql)
result[i] = await self.insertId(sql, key)
self.placeHolder = newJArray()
# ==================== insert Object ====================
proc insert*[T](self: BaradbQuery, items: T) {.async.} =
let sql = self.insertValueBuilder(%items)
self.log.logger(sql)
await self.exec(sql)
proc insert*[T](self: BaradbQuery, items: seq[T]) {.async.} =
let items = items.mapIt(%it)
let sql = self.insertValuesBuilder(items)
self.log.logger(sql)
await self.exec(sql)
proc insertId*[T](self: BaradbQuery, items: T, key = "id"): Future[string] {.async.} =
let sql = self.insertValueBuilder(%items)
self.log.logger(sql)
return await self.insertId(sql, key)
proc insertId*[T](self: BaradbQuery, items: seq[T], key = "id"): Future[seq[string]] {.async.} =
result = newSeq[string](items.len)
for i, item in items:
let sql = self.insertValueBuilder(%item)
self.log.logger(sql)
result[i] = await self.insertId(sql, key)
self.placeHolder = newJArray()
# ==================== update ====================
proc update*(self: BaradbQuery, items: JsonNode) {.async.} =
let sql = self.updateBuilder(items)
self.log.logger(sql)
await self.exec(sql)
proc update*[T](self: BaradbQuery, items: T) {.async.} =
let sql = self.updateBuilder(%items)
self.log.logger(sql)
await self.exec(sql)
proc delete*(self: BaradbQuery) {.async.} =
let sql = self.deleteBuilder()
self.log.logger(sql)
await self.exec(sql)
proc delete*(self: BaradbQuery, id: int, key = "id") {.async.} =
let sql = self.deleteByIdBuilder(id, key)
self.log.logger(sql)
self.placeHolder.add(%*{"key": key, "value": id})
await self.exec(sql)
proc columns*(self: BaradbQuery): Future[seq[string]] {.async.} =
let sql = self.columnBuilder()
self.log.logger(sql)
return await self.getColumns(sql, self.placeHolder)
proc count*(self: BaradbQuery): Future[int] {.async.} =
let sql = self.countBuilder()
self.log.logger(sql)
let response = await self.getRow(sql)
if response.isSome:
return response.get["aggregate"].getStr().parseInt()
else:
return 0
proc min*(self: BaradbQuery, column: string): Future[Option[string]] {.async.} =
let sql = self.minBuilder(column)
self.log.logger(sql)
let response = await self.getRow(sql)
if response.isSome:
case response.get["aggregate"].kind
of JInt:
return some($(response.get["aggregate"].getInt))
of JFloat:
return some($(response.get["aggregate"].getFloat))
else:
return some(response.get["aggregate"].getStr)
else:
return none(string)
proc max*(self: BaradbQuery, column: string): Future[Option[string]] {.async.} =
let sql = self.maxBuilder(column)
self.log.logger(sql)
let response = await self.getRow(sql)
if response.isSome:
case response.get["aggregate"].kind
of JInt:
return some($(response.get["aggregate"].getInt))
of JFloat:
return some($(response.get["aggregate"].getFloat))
else:
return some(response.get["aggregate"].getStr)
else:
return none(string)
proc avg*(self: BaradbQuery, column: string): Future[Option[float]] {.async.} =
let sql = self.avgBuilder(column)
self.log.logger(sql)
let response = await self.getRow(sql)
if response.isSome:
return response.get["aggregate"].getStr().parseFloat.some
else:
return none(float)
proc sum*(self: BaradbQuery, column: string): Future[Option[float]] {.async.} =
let sql = self.sumBuilder(column)
self.log.logger(sql)
let response = await self.getRow(sql)
if response.isSome:
return response.get["aggregate"].getStr.parseFloat.some
else:
return none(float)
proc begin*(self: BaradbConnections) {.async.} =
self.log.logger("BEGIN")
await self.transactionStart()
proc rollback*(self: BaradbConnections) {.async.} =
self.log.logger("ROLLBACK")
await self.transactionEnd("ROLLBACK")
proc commit*(self: BaradbConnections) {.async.} =
self.log.logger("COMMIT")
await self.transactionEnd("COMMIT")
# raw queries
proc get*(self: RawBaradbQuery): Future[seq[JsonNode]] {.async.} =
self.log.logger(self.queryString)
return await self.getAllRows(self.queryString)
proc getPlain*(self: RawBaradbQuery): Future[seq[seq[string]]] {.async.} =
self.log.logger(self.queryString)
return await self.getAllRowsPlain(self.queryString, self.placeHolder)
proc exec*(self: RawBaradbQuery) {.async.} =
self.log.logger(self.queryString)
await self.exec(self.queryString, self.placeHolder)
proc first*(self: RawBaradbQuery): Future[Option[JsonNode]] {.async.} =
self.log.logger(self.queryString)
return await self.getRow(self.queryString)
proc firstPlain*(self: RawBaradbQuery): Future[seq[string]] {.async.} =
self.log.logger(self.queryString)
return await self.getRowPlain(self.queryString, self.placeHolder)
# seeder templates
template seeder*(rdb: BaradbConnections, tableName: string, body: untyped): untyped =
block:
if waitFor rdb.table(tableName).count() == 0:
body
template seeder*(rdb: BaradbConnections, tableName, column: string, body: untyped): untyped =
block:
if waitFor rdb.select(column).table(tableName).count() == 0:
body
@@ -0,0 +1,52 @@
import std/asyncdispatch
import std/deques
import std/json
import std/tables
import std/times
import ../../libs/baradb/baradb_client
import ../../log
import ./baradb_types
proc dbOpen*(_: type Baradb, database: string, user: string, password: string,
host: string, port: int, maxConnections = 1, timeout = 30,
shouldDisplayLog = false, shouldOutputLogFile = false, logDir = "",
maxConnectionLifetime = DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime = DEFAULT_CONN_MAX_IDLE_SECONDS): BaradbConnections =
var conns = newSeq[Connection](maxConnections)
for i in 0..<maxConnections:
let config = ClientConfig(
host: host,
port: port,
database: database,
username: user,
password: password,
timeoutMs: timeout * 1000,
maxRetries: 3,
)
let client = newClient(config)
waitFor client.connect()
conns[i] = Connection(
client: client,
isBusy: false,
createdAt: getTime().toUnix(),
lastUsedAt: getTime().toUnix(),
)
let pools = Connections(
conns: conns,
timeout: timeout,
maxConnectionLifetime: maxConnectionLifetime,
maxConnectionIdleTime: maxConnectionIdleTime,
database: database,
user: user,
password: password,
host: host,
port: port,
waiters: initDeque[Future[void]](),
columnTypeCache: initTable[string, seq[seq[string]]](),
preparedCache: initTable[string, BaradbPreparedEntry](),
)
result = BaradbConnections(
pools: pools,
log: LogSetting(shouldDisplayLog: shouldDisplayLog, shouldOutputLogFile: shouldOutputLogFile, logDir: logDir)
)
@@ -0,0 +1,386 @@
import std/asyncdispatch
import std/json
import std/options
import std/strutils
import std/times
import ../../enums
import ./baradb_types
# ================================================================================
# query
# ================================================================================
proc select*(self: BaradbConnections, columnsArg: varargs[string]): BaradbQuery =
let query = newJObject()
if columnsArg.len == 0 or columnsArg[0] == "*":
query["select"] = %["*"]
else:
query["select"] = %columnsArg
let baradbQuery = BaradbQuery(
log: self.log,
pools: self.pools,
query: query,
queryString: "",
placeHolder: newJArray(),
isInTransaction: self.isInTransaction,
transactionConn: self.transactionConn,
)
return baradbQuery
proc table*(self: BaradbConnections, tableArg: string): BaradbQuery =
let query = newJObject()
query["table"] = %tableArg
let baradbQuery = BaradbQuery(
log: self.log,
pools: self.pools,
query: query,
queryString: "",
placeHolder: newJArray(),
isInTransaction: self.isInTransaction,
transactionConn: self.transactionConn,
)
return baradbQuery
proc table*(self: BaradbQuery, tableArg: string): BaradbQuery =
self.query["table"] = %tableArg
return self
proc `distinct`*(self: BaradbQuery): BaradbQuery =
self.query["distinct"] = %true
return self
# ============================== Conditions ==============================
proc join*(self: BaradbQuery, table: string, column1: string, symbol: string,
column2: string): BaradbQuery =
if self.query.hasKey("join") == false:
self.query["join"] = %*[{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
}]
else:
self.query["join"].add(%*{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
})
return self
proc leftJoin*(self: BaradbQuery, table: string, column1: string, symbol: string,
column2: string): BaradbQuery =
if self.query.hasKey("left_join") == false:
self.query["left_join"] = %*[{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
}]
else:
self.query["left_join"].add(%*{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
})
return self
const whereSymbols = ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "LIKE","%LIKE","LIKE%","%LIKE%"]
const whereSymbolsError = """Arg position 3 is only allowed of ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "LIKE","%LIKE","LIKE%","%LIKE%"]"""
proc where*(self: BaradbQuery, column: string, symbol: string,
value: string|int|float): BaradbQuery =
if not whereSymbols.contains(symbol):
raise newException(CatchableError, whereSymbolsError)
self.placeHolder.add(%*{"key": column, "value": value})
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["where"].add(%*{
"column": column,
"symbol": symbol,
"value": "?"
})
return self
proc where*(self: BaradbQuery, column: string, symbol: string,
value: bool): BaradbQuery =
if not whereSymbols.contains(symbol):
raise newException(CatchableError, whereSymbolsError)
self.placeHolder.add(%*{"key": column, "value": value})
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["where"].add(%*{
"column": column,
"symbol": symbol,
"value": "?"
})
return self
proc where*(self: BaradbQuery, column: string, symbol: string, value: nil.type): BaradbQuery =
if not whereSymbols.contains(symbol):
raise newException(CatchableError, whereSymbolsError)
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["where"].add(%*{
"column": column,
"symbol": symbol,
"value": "null"
})
return self
proc orWhere*(self: BaradbQuery, column: string, symbol: string,
value: string|int|float|bool): BaradbQuery =
if not whereSymbols.contains(symbol):
raise newException(CatchableError, whereSymbolsError)
self.placeHolder.add(%*{"key": column, "value": value})
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["or_where"].add(%*{
"column": column,
"symbol": symbol,
"value": "?"
})
return self
proc orWhere*(self: BaradbQuery, column: string, symbol: string, value: nil.type): BaradbQuery =
if not whereSymbols.contains(symbol):
raise newException(CatchableError, whereSymbolsError)
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["or_where"].add(%*{
"column": column,
"symbol": symbol,
"value": "null"
})
return self
proc whereBetween*(self: BaradbQuery, column: string, width: array[2, int|float]): BaradbQuery =
if self.query.hasKey("where_between") == false:
self.query["where_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_between"].add(%*{
"column": column,
"width": width
})
return self
proc whereBetween*(self: BaradbQuery, column: string, width: array[2, string]): BaradbQuery =
if self.query.hasKey("where_between_string") == false:
self.query["where_between_string"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_between_string"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotBetween*(self: BaradbQuery, column: string, width: array[2, int|float]): BaradbQuery =
if self.query.hasKey("where_not_between") == false:
self.query["where_not_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_between"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotBetween*(self: BaradbQuery, column: string, width: array[2, string]): BaradbQuery =
if self.query.hasKey("where_not_between_string") == false:
self.query["where_not_between_string"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_between_string"].add(%*{
"column": column,
"width": width
})
return self
proc whereIn*(self: BaradbQuery, column: string, width: seq[int|float|string]): BaradbQuery =
if self.query.hasKey("where_in") == false:
self.query["where_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotIn*(self: BaradbQuery, column: string, width: seq[int|float|string]): BaradbQuery =
if self.query.hasKey("where_not_in") == false:
self.query["where_not_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNull*(self: BaradbQuery, column: string): BaradbQuery =
if self.query.hasKey("where_null") == false:
self.query["where_null"] = %*[{
"column": column
}]
else:
self.query["where_null"].add(%*{
"column": column
})
return self
proc groupBy*(self: BaradbQuery, column: string): BaradbQuery =
if self.query.hasKey("group_by") == false:
self.query["group_by"] = %*[{"column": column}]
else:
self.query["group_by"].add(%*{"column": column})
return self
proc having*(self: BaradbQuery, column: string, symbol: string,
value: string|int|float|bool): BaradbQuery =
if not whereSymbols.contains(symbol):
raise newException(CatchableError, whereSymbolsError)
self.placeHolder.add(%*{"key": column, "value": value})
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["having"].add(%*{
"column": column,
"symbol": symbol,
"value": "?"
})
return self
proc having*(self: BaradbQuery, column: string, symbol: string, value: nil.type): BaradbQuery =
if not whereSymbols.contains(symbol):
raise newException(CatchableError, whereSymbolsError)
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["having"].add(%*{
"column": column,
"symbol": symbol,
"value": "null"
})
return self
proc orderBy*(self: BaradbQuery, column: string, order: Order): BaradbQuery =
if self.query.hasKey("order_by") == false:
self.query["order_by"] = %*[{
"column": column,
"order": $order
}]
else:
self.query["order_by"].add(%*{
"column": column,
"order": $order
})
return self
proc limit*(self: BaradbQuery, num: int): BaradbQuery =
self.query["limit"] = %num
return self
proc offset*(self: BaradbQuery, num: int): BaradbQuery =
self.query["offset"] = %num
return self
proc raw*(self: BaradbConnections, sql: string, arges = newJArray()): RawBaradbQuery =
let rawQueryRdb = RawBaradbQuery(
log: self.log,
pools: self.pools,
query: newJObject(),
queryString: sql,
placeHolder: arges,
isInTransaction: false,
transactionConn: 0
)
return rawQueryRdb
@@ -0,0 +1,42 @@
import std/macros
import std/strutils
import std/strformat
import ../../models/baradb/baradb_types
when NimMajor == 1:
macro rdbTransaction(rdb: BaradbConnections, callback: untyped): untyped =
var callbackStr = callback.repr
callbackStr.removePrefix
callbackStr = callbackStr.indent(4)
callbackStr = fmt"""
block:
{rdb.repr}.begin().await
try:
{callbackStr}
{rdb.repr}.commit().await
except:
{rdb.repr}.rollback().await
"""
let body = callbackStr.parseStmt()
return body
else:
macro rdbTransaction(rdb: BaradbConnections, callback: untyped): untyped =
var callbackStr = callback.repr
callbackStr.removePrefix
callbackStr = callbackStr.indent(4)
callbackStr = fmt"""
block:
{rdb.repr}.begin().await
try:
{callbackStr}
{rdb.repr}.commit().await
except DbError, CatchableError:
{rdb.repr}.rollback().await
"""
let body = callbackStr.parseStmt()
return body
template transaction*(rdb: BaradbConnections, callback: untyped) =
rdbTransaction(rdb, callback)
@@ -0,0 +1,90 @@
import std/asyncdispatch
import std/deques
import std/json
import std/tables
import ../../log
import ../../libs/baradb/baradb_client
type Baradb* = object
const DEFAULT_CONN_MAX_LIFETIME_SECONDS* = 300
const DEFAULT_CONN_MAX_IDLE_SECONDS* = 300
type Connection* = ref object
client*: BaraClient
isBusy*: bool
createdAt*: int64
lastUsedAt*: int64
type BaradbPreparedEntry* = ref object
sql*: string
nArgs*: int
refCount*: int
lastUsedAt*: int64
type Connections* = ref object
conns*: seq[Connection]
timeout*: int
maxConnectionLifetime*: int
maxConnectionIdleTime*: int
database*: string
user*: string
password*: string
host*: string
port*: int
waiters*: Deque[Future[void]]
columnTypeCache*: Table[string, seq[seq[string]]]
preparedCache*: Table[string, BaradbPreparedEntry]
type BaradbConnections* = ref object
log*: LogSetting
pools*: Connections
isInTransaction*: bool
transactionConn*: int
type BaradbPreparedContext* = ref object
owner*: BaradbConnections
connI*: int
type BaradbPreparedStatement* = ref object
owner*: BaradbConnections
entry*: BaradbPreparedEntry
sql*: string
nArgs*: int
isClosed*: bool
type BaradbQuery* = ref object
log*: LogSetting
pools*: Connections
query*: JsonNode
queryString*: string
placeHolder*: JsonNode
isInTransaction*: bool
transactionConn*: int
type RawBaradbQuery* = ref object
log*: LogSetting
pools*: Connections
query*: JsonNode
queryString*: string
placeHolder*: JsonNode
isInTransaction*: bool
transactionConn*: int
proc `$`*(self: BaradbConnections | BaradbQuery | RawBaradbQuery): string =
return "Baradb"
proc isConnected*(self: BaradbConnections | BaradbQuery | RawBaradbQuery): bool =
return self.pools.conns.len > 0
@@ -0,0 +1,237 @@
import std/json
import ../baradb_types
import ./baradb_generator
# ==================== SELECT ====================
proc selectBuilder*(self: BaradbQuery): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc selectFirstBuilder*(self: BaradbQuery): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.selectFirstSql()
.offsetSql()
.queryString
proc selectFindBuilder*(self: BaradbQuery, key: string): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.selectByIdSql(key)
.queryString
# ==================== INSERT ====================
proc insertValueBuilder*(self: BaradbQuery, items: JsonNode): string =
return self
.insertSql()
.insertValueSql(items)
.queryString
proc insertValuesBuilder*(self: BaradbQuery, rows: openArray[JsonNode]): string =
return self
.insertSql()
.insertValuesSql(rows)
.queryString
# ==================== UPDATE ====================
proc updateBuilder*(self: BaradbQuery, items: JsonNode): string =
return self
.updateSql()
.updateValuesSql(items)
.whereSql()
.orWhereSql()
.limitSql()
.offsetSql()
.queryString
# ==================== DELETE ====================
proc deleteBuilder*(self: BaradbQuery): string =
return self
.deleteSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.limitSql()
.offsetSql()
.queryString
proc deleteByIdBuilder*(self: BaradbQuery, id: int, key: string): string =
return self
.deleteSql()
.fromSql()
.deleteByIdSql(id, key)
.queryString
# ==================== Aggregates ====================
proc countBuilder*(self: BaradbQuery): string =
return self
.selectCountSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc maxBuilder*(self: BaradbQuery, column: string): string =
return self
.selectMaxSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc minBuilder*(self: BaradbQuery, column: string): string =
return self
.selectMinSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc avgBuilder*(self: BaradbQuery, column: string): string =
return self
.selectAvgSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc sumBuilder*(self: BaradbQuery, column: string): string =
return self
.selectSumSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc columnBuilder*(self: BaradbQuery): string =
return self
.selectSql()
.fromSql()
.selectFirstSql()
.queryString
@@ -0,0 +1,397 @@
import std/json
import std/strformat
import std/strutils
import ../baradb_types
proc quote(input:string):string =
var tmp = newSeq[string]()
for segment in input.split("."):
if segment.contains(" as "):
let parts = segment.split(" as ", maxsplit = 1)
let expression = parts[0]
let alias = parts[1]
if expression.contains("("):
let funcStart = expression.find('(')
let funcEnd = expression.find(')', funcStart)
let funcName = expression[0 ..< funcStart]
let columnName = expression[funcStart + 1 ..< funcEnd]
tmp.add(&"{funcName}(`{columnName}`) as `{alias}`")
else:
tmp.add(&"`{expression}` as `{alias}`")
elif segment.contains("("):
tmp.add(segment)
else:
tmp.add(&"`{segment}`")
return tmp.join(".")
# ==================== SELECT ====================
proc selectSql*(self: BaradbQuery): BaradbQuery =
var queryString = ""
if self.query.hasKey("distinct"):
queryString.add("SELECT DISTINCT")
else:
queryString.add("SELECT")
if self.query.hasKey("select"):
for i, item in self.query["select"].getElems():
if i > 0: queryString.add(",")
var column = item.getStr()
if column != "*": column = quote(column)
queryString.add(&" {column}")
else:
queryString.add(" *")
self.queryString = queryString
return self
proc fromSql*(self: BaradbQuery): BaradbQuery =
let table = self.query["table"].getStr()
self.queryString.add(&" FROM `{table}`")
return self
proc selectFirstSql*(self: BaradbQuery): BaradbQuery =
self.queryString.add(" LIMIT 1")
return self
proc selectByIdSql*(self: BaradbQuery, key: string): BaradbQuery =
let key = key.quote()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {key} = ? LIMIT 1")
else:
self.queryString.add(&" WHERE {key} = ? LIMIT 1")
return self
proc joinSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("join"):
for row in self.query["join"]:
let table = row["table"].getStr().quote()
let column1 = row["column1"].getStr().quote()
let symbol = row["symbol"].getStr()
let column2 = row["column2"].getStr().quote()
self.queryString.add(&" INNER JOIN {table} ON {column1} {symbol} {column2}")
return self
proc leftJoinSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("left_join"):
for row in self.query["left_join"]:
let table = row["table"].getStr().quote()
let column1 = row["column1"].getStr().quote()
let symbol = row["symbol"].getStr()
let column2 = row["column2"].getStr().quote()
self.queryString.add(&" LEFT JOIN {table} ON {column1} {symbol} {column2}")
return self
proc whereSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("where"):
for i, row in self.query["where"].getElems():
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
if i == 0:
self.queryString.add(&" WHERE {column} {symbol} ?")
else:
self.queryString.add(&" AND {column} {symbol} ?")
return self
proc orWhereSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("or_where"):
for row in self.query["or_where"]:
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
if self.queryString.contains("WHERE"):
self.queryString.add(&" OR {column} {symbol} ?")
else:
self.queryString.add(&" WHERE {column} {symbol} ?")
return self
proc whereBetweenSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("where_between"):
for row in self.query["where_between"]:
let column = row["column"].getStr().quote()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} BETWEEN ? AND ?")
else:
self.queryString.add(&" WHERE {column} BETWEEN ? AND ?")
return self
proc whereBetweenStringSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("where_between_string"):
for row in self.query["where_between_string"]:
let column = row["column"].getStr().quote()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} BETWEEN ? AND ?")
else:
self.queryString.add(&" WHERE {column} BETWEEN ? AND ?")
return self
proc whereNotBetweenSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("where_not_between"):
for row in self.query["where_not_between"]:
let column = row["column"].getStr().quote()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} NOT BETWEEN ? AND ?")
else:
self.queryString.add(&" WHERE {column} NOT BETWEEN ? AND ?")
return self
proc whereNotBetweenStringSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("where_not_between_string"):
for row in self.query["where_not_between_string"]:
let column = row["column"].getStr().quote()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} NOT BETWEEN ? AND ?")
else:
self.queryString.add(&" WHERE {column} NOT BETWEEN ? AND ?")
return self
proc whereInSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("where_in"):
var widthString = ""
for row in self.query["where_in"]:
let column = row["column"].getStr().quote()
for i, val in row["width"].getElems():
if i > 0: widthString.add(", ")
widthString.add("?")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} IN ({widthString})")
else:
self.queryString.add(&" WHERE {column} IN ({widthString})")
return self
proc whereNotInSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("where_not_in"):
var widthString = ""
for row in self.query["where_not_in"]:
let column = row["column"].getStr().quote()
for i, val in row["width"].getElems():
if i > 0: widthString.add(", ")
widthString.add("?")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} NOT IN ({widthString})")
else:
self.queryString.add(&" WHERE {column} NOT IN ({widthString})")
return self
proc whereNullSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("where_null"):
for row in self.query["where_null"]:
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} {symbol} null")
else:
self.queryString.add(&" WHERE {column} {symbol} null")
return self
proc groupBySql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("group_by"):
for row in self.query["group_by"]:
let column = row["column"].getStr().quote()
if self.queryString.contains("GROUP BY"):
self.queryString.add(&", {column}")
else:
self.queryString.add(&" GROUP BY {column}")
return self
proc havingSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("having"):
for i, row in self.query["having"].getElems():
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
if i == 0:
self.queryString.add(&" HAVING {column} {symbol} ?")
else:
self.queryString.add(&" AND {column} {symbol} ?")
return self
proc orderBySql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("order_by"):
for row in self.query["order_by"]:
let column = row["column"].getStr().quote()
let order = row["order"].getStr()
if self.queryString.contains("ORDER BY"):
self.queryString.add(&", {column} {order}")
else:
self.queryString.add(&" ORDER BY {column} {order}")
return self
proc limitSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("limit"):
let num = self.query["limit"].getInt()
self.queryString.add(&" LIMIT {num}")
return self
proc offsetSql*(self: BaradbQuery): BaradbQuery =
if self.query.hasKey("offset"):
let num = self.query["offset"].getInt()
self.queryString.add(&" OFFSET {num}")
return self
# ==================== INSERT ====================
proc insertSql*(self: BaradbQuery): BaradbQuery =
let table = self.query["table"].getStr()
self.queryString = &"INSERT INTO `{table}`"
return self
proc insertValueSql*(self: BaradbQuery, items: JsonNode): BaradbQuery =
var columns = ""
var values = ""
var i = 0
for key, val in items.pairs:
defer: i += 1
if i > 0:
columns.add(", ")
values.add(", ")
columns.add(&"`{key}`")
self.placeHolder.add(%*{"key": key, "value": val})
values.add("?")
self.queryString.add(&" ({columns}) VALUES ({values})")
return self
proc insertValuesSql*(self: BaradbQuery, rows: openArray[JsonNode]): BaradbQuery =
var columns = ""
var i = 0
for key, value in rows[0]:
defer: i += 1
if i > 0: columns.add(", ")
columns.add(&"`{key}`")
var values = ""
var valuesCount = 0
for items in rows:
var valueCount = 0
var value = ""
for key, val in items.pairs:
defer: valueCount += 1
if valueCount > 0: value.add(", ")
self.placeHolder.add(%*{"key": key, "value": val})
value.add("?")
if valuesCount > 0: values.add(", ")
valuesCount += 1
values.add(&"({value})")
self.queryString.add(&" ({columns}) VALUES {values}")
return self
# ==================== UPDATE ====================
proc updateSql*(self: BaradbQuery): BaradbQuery =
var queryString = ""
queryString.add("UPDATE")
var table = self.query["table"].getStr()
queryString.add(&" `{table}` SET")
self.queryString = queryString
return self
proc updateValuesSql*(self: BaradbQuery, items: JsonNode): BaradbQuery =
var value = ""
let placeHolder = newJArray()
var i = 0
for key, val in items.pairs:
defer: i += 1
if i > 0: value.add(",")
value.add(&" `{key}` = ?")
placeHolder.add(%*{"key": key, "value": val})
for row in self.placeHolder.items:
placeHolder.add(row)
self.placeHolder = placeHolder
self.queryString.add(value)
return self
# ==================== DELETE ====================
proc deleteSql*(self: BaradbQuery): BaradbQuery =
self.queryString = "DELETE"
return self
proc deleteByIdSql*(self: BaradbQuery, id: int, key: string): BaradbQuery =
self.queryString.add(&" WHERE `{key}` = ?")
return self
# ==================== Aggregates ====================
proc selectCountSql*(self: BaradbQuery): BaradbQuery =
let queryString =
if self.query.hasKey("select"):
let column = self.query["select"][0].getStr
&"`{column}`"
else:
"*"
self.queryString = &"SELECT count({queryString}) as aggregate"
return self
proc selectMaxSql*(self: BaradbQuery, column: string): BaradbQuery =
self.queryString = &"SELECT max(`{column}`) as aggregate"
return self
proc selectMinSql*(self: BaradbQuery, column: string): BaradbQuery =
self.queryString = &"SELECT min(`{column}`) as aggregate"
return self
proc selectAvgSql*(self: BaradbQuery, column: string): BaradbQuery =
self.queryString = &"SELECT avg(`{column}`) as aggregate"
return self
proc selectSumSql*(self: BaradbQuery, column: string): BaradbQuery =
self.queryString = &"SELECT sum(`{column}`) as aggregate"
return self
@@ -0,0 +1,71 @@
type
Row* = seq[string]
DbTypeKind* = enum ## a superset of datatypes that might be supported.
dbUnknown, ## unknown datatype
dbSerial, ## datatype used for primary auto-increment keys
dbNull, ## datatype used for the NULL value
dbBit, ## bit datatype
dbBool, ## boolean datatype
dbBlob, ## blob datatype
dbFixedChar, ## string of fixed length
dbVarchar, ## string datatype
dbJson, ## JSON datatype
dbXml, ## XML datatype
dbInt, ## some integer type
dbUInt, ## some unsigned integer type
dbDecimal, ## decimal numbers (fixed-point number)
dbFloat, ## some floating point type
dbDate, ## a year-month-day description
dbTime, ## HH:MM:SS information
dbDatetime, ## year-month-day and HH:MM:SS information,
## plus optional time or timezone information
dbTimestamp, ## Timestamp values are stored as the number of seconds
## since the epoch ('1970-01-01 00:00:00' UTC).
dbTimeInterval, ## an interval [a,b] of times
dbEnum, ## some enum
dbSet, ## set of enum values
dbArray, ## an array of values
dbComposite, ## composite type (record, struct, etc)
dbUrl, ## a URL
dbUuid, ## a UUID
dbInet, ## an IP address
dbMacAddress, ## a MAC address
dbGeometry, ## some geometric type
dbPoint, ## Point on a plane (x,y)
dbLine, ## Infinite line ((x1,y1),(x2,y2))
dbLseg, ## Finite line segment ((x1,y1),(x2,y2))
dbBox, ## Rectangular box ((x1,y1),(x2,y2))
dbPath, ## Closed or open path (similar to polygon) ((x1,y1),...)
dbPolygon, ## Polygon (similar to closed path) ((x1,y1),...)
dbCircle, ## Circle <(x,y),r> (center point and radius)
dbUser1, ## user definable datatype 1 (for unknown extensions)
dbUser2, ## user definable datatype 2 (for unknown extensions)
dbUser3, ## user definable datatype 3 (for unknown extensions)
dbUser4, ## user definable datatype 4 (for unknown extensions)
dbUser5 ## user definable datatype 5 (for unknown extensions)
DbType* = object ## describes a database type
kind*: DbTypeKind ## the kind of the described type
notNull*: bool ## does the type contain NULL?
name*: string ## the name of the type
size*: Natural ## the size of the datatype; 0 if of variable size
maxReprLen*: Natural ## maximal length required for the representation
precision*, scale*: Natural ## precision and scale of the number
min*, max*: BiggestInt ## the minimum and maximum of allowed values
validValues*: seq[string] ## valid values of an enum or a set
DbColumn* = object ## information about a database column
name*: string ## name of the column
tableName*: string ## name of the table the column belongs to (optional)
typ*: DbType ## type of the column
primaryKey*: bool ## is this a primary key?
foreignKey*: bool ## is this a foreign key?
DbColumns* = seq[DbColumn]
DbRows* = seq[DbColumns]
DbEffect* = object of IOEffect ## effect that denotes a database operation
ReadDbEffect* = object of DbEffect ## effect that denotes a read operation
WriteDbEffect* = object of DbEffect ## effect that denotes a write operation
const errorConnectionNum* = 99999
@@ -0,0 +1,90 @@
import std/asyncdispatch
import std/deques
import std/tables
import std/times
import ../../libs/database_url
import ../../libs/mariadb/mariadb_rdb
import ../../error
import ../../log
import ./mariadb_types
proc dbOpen*(_: type MariaDB, database: string, user: string, password: string,
host: string, port: int, maxConnections=1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir="",
maxConnectionLifetime=DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime=DEFAULT_CONN_MAX_IDLE_SECONDS): MariadbConnections =
var conns = newSeq[Connection](maxConnections)
for i in 0..<maxConnections:
let conn = mariadb_rdb.init(nil)
if conn == nil:
mariadb_rdb.close(conn)
dbError("mariadb_rdb.init() failed")
if mariadb_rdb.options(conn, MYSQL_OPT_NONBLOCK, nil) != 0:
let errmsg = $mariadb_rdb.error(conn)
mariadb_rdb.close(conn)
dbError(errmsg)
if mariadb_rdb.real_connect(conn, host, user, password, database, port.int32, nil, 0) == nil:
var errmsg = $mariadb_rdb.error(conn)
mariadb_rdb.close(conn)
dbError(errmsg)
conns[i] = Connection(
conn: conn,
isBusy: false,
createdAt: getTime().toUnix(),
lastUsedAt: getTime().toUnix()
)
let pools = Connections(
conns: conns,
timeout: timeout,
maxConnectionLifetime: maxConnectionLifetime,
maxConnectionIdleTime: maxConnectionIdleTime,
info: ConnectionInfo(
database: database,
user: user,
password: password,
host: host,
port: port
),
waiters: initDeque[Future[void]](),
columnTypeCache: initTable[string, seq[seq[string]]](),
preparedCache: initTable[string, MariadbPreparedEntry](),
)
result = MariadbConnections(
pools: pools,
info: pools.info,
log: LogSetting(shouldDisplayLog:shouldDisplayLog, shouldOutputLogFile:shouldOutputLogFile, logDir:logDir)
)
proc dbOpen*(_:type MariaDB, url: string, maxConnections: int = 1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir="",
maxConnectionLifetime=DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime=DEFAULT_CONN_MAX_IDLE_SECONDS): MariadbConnections =
return dbOpen(MariaDB, asDatabaseUrl(url), maxConnections, timeout, shouldDisplayLog, shouldOutputLogFile, logDir, maxConnectionLifetime, maxConnectionIdleTime)
proc dbOpen*(_:type MariaDB, databaseUrl: DatabaseUrl, maxConnections=1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir="",
maxConnectionLifetime=DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime=DEFAULT_CONN_MAX_IDLE_SECONDS): MariadbConnections =
let parsed = parseDatabaseUrl(databaseUrl)
requireDatabaseUrlScheme(parsed, ["mariadb"], "MariaDB")
let database = databaseName(parsed)
let port = portOrDefault(parsed, 3306)
return dbOpen(
MariaDB,
database,
parsed.username,
parsed.password,
parsed.hostname,
port,
maxConnections,
timeout,
shouldDisplayLog,
shouldOutputLogFile,
logDir,
maxConnectionLifetime,
maxConnectionIdleTime
)
@@ -0,0 +1,439 @@
import std/asyncdispatch
import std/json
import std/options
import std/strutils
import std/times
import ../../libs/mariadb/mariadb_impl
import ../../enums
import ./mariadb_types
proc select*(self:MariadbConnections, columnsArg:varargs[string]):MariadbQuery =
let query = newJObject()
if columnsArg.len == 0:
query["select"] = %["*"]
else:
query["select"] = %columnsArg
let MariadbQuery = MariadbQuery(
log: self.log,
pools: self.pools,
info: self.info,
query: query,
queryString: "",
placeHolder: newJArray(),
isInTransaction: self.isInTransaction,
transactionConn: self.transactionConn,
)
return MariadbQuery
proc table*(self:MariadbConnections, tableArg: string): MariadbQuery =
let query = newJObject()
query["table"] = %tableArg
let MariadbQuery = MariadbQuery(
log: self.log,
pools: self.pools,
query: query,
queryString: "",
placeHolder: newJArray(),
isInTransaction: self.isInTransaction,
transactionConn: self.transactionConn,
)
return MariadbQuery
proc table*(self:MariadbQuery, tableArg: string): MariadbQuery =
self.query["table"] = %tableArg
return self
proc `distinct`*(self: MariadbQuery): MariadbQuery =
self.query["distinct"] = %true
return self
# ============================== Conditions ==============================
proc join*(self: MariadbQuery, table: string, column1: string, symbol: string,
column2: string): MariadbQuery =
if self.query.hasKey("join") == false:
self.query["join"] = %*[{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
}]
else:
self.query["join"].add(%*{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
})
return self
proc leftJoin*(self: MariadbQuery, table: string, column1: string, symbol: string,
column2: string): MariadbQuery =
if self.query.hasKey("left_join") == false:
self.query["left_join"] = %*[{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
}]
else:
self.query["left_join"].add(%*{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
})
return self
const whereSymbols = ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "LIKE","%LIKE","LIKE%","%LIKE%"]
const whereSymbolsError = """Arg position 3 is only allowed of ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "LIKE","%LIKE","LIKE%","%LIKE%"]"""
proc where*(self: MariadbQuery, column: string, symbol: string,
value: string|int|float): MariadbQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key": column, "value": value})
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc where*(self: MariadbQuery, column: string, symbol: string,
value: bool): MariadbQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc where*(self: MariadbQuery, column: string, symbol: string, value: nil.type): MariadbQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("where_null") == false:
self.query["where_null"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["where_null"].add(
%*{
"column": column,
"symbol": symbol,
"value": "null"
}
)
return self
proc orWhere*(self: MariadbQuery, column: string, symbol: string,
value: string|int|float|bool): MariadbQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["or_where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc orWhere*(self: MariadbQuery, column: string, symbol: string, value: nil.type): MariadbQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["or_where"].add(%*{
"column": column,
"symbol": symbol,
"value": "null"
})
return self
proc whereBetween*(self:MariadbQuery, column:string, width:array[2, int|float]): MariadbQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_between") == false:
self.query["where_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_between"].add(%*{
"column": column,
"width": width
})
return self
proc whereBetween*(self:MariadbQuery, column:string, width:array[2, string]): MariadbQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_between_string") == false:
self.query["where_between_string"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_between_string"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotBetween*(self:MariadbQuery, column:string, width:array[2, int|float]): MariadbQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_not_between") == false:
self.query["where_not_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_between"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotBetween*(self:MariadbQuery, column:string, width:array[2, string]): MariadbQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_not_between_string") == false:
self.query["where_not_between_string"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_between_string"].add(%*{
"column": column,
"width": width
})
return self
proc whereIn*(self:MariadbQuery, column:string, width:seq[int|float|string]): MariadbQuery =
for row in width:
self.placeHolder.add(%*{"key": column, "value": row})
if self.query.hasKey("where_in") == false:
self.query["where_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotIn*(self:MariadbQuery, column:string, width:seq[int|float|string]): MariadbQuery =
for row in width:
self.placeHolder.add(%*{"key": column, "value": row})
if self.query.hasKey("where_not_in") == false:
self.query["where_not_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNull*(self:MariadbQuery, column:string): MariadbQuery =
if self.query.hasKey("where_null") == false:
self.query["where_null"] = %*[{
"column": column,
"symbol": "is",
}]
else:
self.query["where_null"].add(%*{
"column": column,
"symbol": "is",
})
return self
proc groupBy*(self:MariadbQuery, column:string): MariadbQuery =
if self.query.hasKey("group_by") == false:
self.query["group_by"] = %*[{"column": column}]
else:
self.query["group_by"].add(%*{"column": column})
return self
proc having*(self: MariadbQuery, column: string, symbol: string,
value: string|int|float|bool): MariadbQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["having"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc having*(self: MariadbQuery, column: string, symbol: string, value: nil.type): MariadbQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["having"].add(
%*{
"column": column,
"symbol": symbol,
"value": "null"
}
)
return self
proc orderBy*(self:MariadbQuery, column:string, order:Order): MariadbQuery =
if self.query.hasKey("order_by") == false:
self.query["order_by"] = %*[{
"column": column,
"order": $order
}]
else:
self.query["order_by"].add(%*{
"column": column,
"order": $order
})
return self
proc limit*(self: MariadbQuery, num: int): MariadbQuery =
self.query["limit"] = %num
return self
proc offset*(self: MariadbQuery, num: int): MariadbQuery =
self.query["offset"] = %num
return self
proc raw*(self:MariadbConnections, sql:string, arges=newJArray()): RawMariadbQuery =
## arges is `JArray` `[true, 1, 1.1, "str"]`
##
## can't use BLOB data.
let rawQueryRdb = RawMariadbQuery(
log: self.log,
pools: self.pools,
info: self.info,
query: newJObject(),
queryString: sql.strip(),
placeHolder: arges,
isInTransaction: false,
transactionConn: 0
)
return rawQueryRdb
@@ -0,0 +1,42 @@
import std/macros
import std/strutils
import std/strformat
import ../../models/mariadb/mariadb_types
when NimMajor == 1:
macro rdbTransaction(rdb:MariadbConnections, callback: untyped):untyped =
var callbackStr = callback.repr
callbackStr.removePrefix
callbackStr = callbackStr.indent(4)
callbackStr = fmt"""
block:
{rdb.repr}.begin().await
try:
{callbackStr}
{rdb.repr}.commit().await
except DbError:
{rdb.repr}.rollback().await
"""
let body = callbackStr.parseStmt()
return body
else:
macro rdbTransaction(rdb:MariadbConnections, callback: untyped):untyped =
var callbackStr = callback.repr
callbackStr.removePrefix
callbackStr = callbackStr.indent(4)
callbackStr = fmt"""
block:
{rdb.repr}.begin().await
try:
{callbackStr}
{rdb.repr}.commit().await
except CatchableError:
{rdb.repr}.rollback().await
"""
let body = callbackStr.parseStmt()
return body
template transaction*(rdb:MariadbConnections, callback: untyped) =
rdbTransaction(rdb, callback)
@@ -0,0 +1,115 @@
import std/asyncdispatch
import std/deques
import std/json
import std/tables
import ../../log
import ../../libs/mariadb/mariadb_rdb
type MariaDB* = object
const DEFAULT_CONN_MAX_LIFETIME_SECONDS* = 300
const DEFAULT_CONN_MAX_IDLE_SECONDS* = 300
type ConnectionInfo* = object
database*:string
user*:string
password*:string
host*:string
port*:int
type Connection* = object
conn*: PMySQL
isBusy*: bool
createdAt*: int64
lastUsedAt*: int64
type MariadbPreparedEntry* = ref object
sql*: string
nArgs*: int
stmts*: seq[PSTMT]
refCount*: int
lastUsedAt*: int64
type Connections* = ref object
conns*: seq[Connection]
timeout*:int
maxConnectionLifetime*: int
maxConnectionIdleTime*: int
info*: ConnectionInfo
waiters*: Deque[Future[void]]
columnTypeCache*: Table[string, seq[seq[string]]]
preparedCache*: Table[string, MariadbPreparedEntry]
## created by `let rdb = dbOpen(MySQL, "localhost", 3306)`
type MariadbConnections* = ref object
log*: LogSetting
pools*:Connections
info*:ConnectionInfo
# for transaction
isInTransaction*: bool
transactionConn*: int
type MariadbPreparedContext* = ref object
owner*: MariadbConnections
connI*: int
## created by `rdb.select("columnName")` or `rdb.table("tableName")`
type MariadbQuery* = ref object
log*: LogSetting
pools*:Connections
info*:ConnectionInfo
query*: JsonNode
queryString*: string
placeHolder*: JsonNode # [{"key":"user", "value":"user1"}]
# for transaction
isInTransaction*: bool
transactionConn*: int
type RawMariadbQuery* = ref object
log*: LogSetting
pools*:Connections
info*:ConnectionInfo
query*: JsonNode
queryString*: string
placeHolder*: JsonNode # ["user1", "user1@example.com"]
# for transaction
isInTransaction*: bool
transactionConn*: int
type MariadbResultBindCache* = ref object
binds*: seq[BIND]
buffers*: seq[string]
lengths*: seq[culong]
nullFlags*: seq[my_bool]
errorFlags*: seq[my_bool]
type MariadbPreparedStatement* = ref object
owner*: MariadbConnections
info*: ConnectionInfo
entry*: MariadbPreparedEntry
sql*: string
nArgs*: int
isClosed*: bool
resultBindCache*: seq[MariadbResultBindCache]
proc `$`*(self:MariadbConnections|MariadbQuery|RawMariadbQuery):string =
return "MariaDB"
proc isConnected*(self:MariadbConnections|MariadbQuery|RawMariadbQuery):bool =
return self.pools.conns.len > 0
@@ -0,0 +1,228 @@
import std/json
import ../mariadb_types
import ./mariadb_generator
# ==================== SELECT ====================
proc selectBuilder*(self: MariadbQuery): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc selectFirstBuilder*(self: MariadbQuery): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.selectFirstSql()
.offsetSql()
.queryString
proc selectFindBuilder*(self: MariadbQuery, key: string): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.selectByIdSql(key)
.queryString
# ==================== INSERT ====================
proc insertValueBuilder*(self: MariadbQuery, items: JsonNode): string =
return self
.insertSql()
.insertValueSql(items)
.queryString
proc insertValuesBuilder*(self: MariadbQuery, rows: openArray[JsonNode]): string =
return self
.insertSql()
.insertValuesSql(rows)
.queryString
# ==================== UPDATE ====================
proc updateBuilder*(self: MariadbQuery, items: JsonNode): string =
return self
.updateSql()
.updateValuesSql(items)
.whereSql()
.orWhereSql()
.limitSql()
.offsetSql()
.queryString
# ==================== DELETE ====================
proc deleteBuilder*(self: MariadbQuery): string =
return self
.deleteSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.limitSql()
.offsetSql()
.queryString
proc deleteByIdBuilder*(self: MariadbQuery, id: int, key: string): string =
return self
.deleteSql()
.fromSql()
.deleteByIdSql(id, key)
.queryString
# ==================== Aggregates ====================
proc countBuilder*(self:MariadbQuery): string =
return self
.selectCountSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc maxBuilder*(self:MariadbQuery, column:string): string =
return self
.selectMaxSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc minBuilder*(self:MariadbQuery, column:string): string =
return self
.selectMinSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc avgBuilder*(self:MariadbQuery, column:string): string =
return self
.selectAvgSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc sumBuilder*(self:MariadbQuery, column:string): string =
return self
.selectSumSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc columnBuilder*(self:MariadbQuery): string =
return self
.selectSql()
.fromSql()
.selectFirstSql()
.queryString
@@ -0,0 +1,429 @@
import std/json
import std/strformat
import std/strutils
# import ../../database_types
import ../mariadb_types
proc quote(input:string):string =
## "User.id as userId" => ```User```.```id``` as ```userId```
##
## "COUNT(id) as count" => ```COUNT(id)``` as ```count```
##
## "MAX(id) as maxId" => ```MAX(id)``` as ```maxId```
##
var tmp = newSeq[string]()
for segment in input.split("."):
if segment.contains(" as "):
# Split once on ' as ' to separate the expression and its alias
let parts = segment.split(" as ", maxsplit = 1)
let expression = parts[0]
let alias = parts[1]
if expression.contains("("):
# Extract function name and column name using index-based slicing
let funcStart = expression.find('(')
let funcEnd = expression.find(')', funcStart)
let funcName = expression[0 ..< funcStart]
let columnName = expression[funcStart + 1 ..< funcEnd]
tmp.add(&"{funcName}(`{columnName}`) as `{alias}`")
else:
# Quote the expression and alias
tmp.add(&"`{expression}` as `{alias}`")
elif segment.contains("("):
# Leave functions without alias as is
tmp.add(segment)
else:
# Quote standalone identifiers
tmp.add(&"`{segment}`")
return tmp.join(".")
# ==================== SELECT ====================
proc selectSql*(self: MariadbQuery): MariadbQuery =
var queryString = ""
if self.query.hasKey("distinct"):
queryString.add("SELECT DISTINCT")
else:
queryString.add("SELECT")
if self.query.hasKey("select"):
for i, item in self.query["select"].getElems():
if i > 0: queryString.add(",")
var column = item.getStr()
if column != "*": column = quote(column)
queryString.add(&" {column}")
else:
queryString.add(" *")
self.queryString = queryString
return self
proc fromSql*(self: MariadbQuery): MariadbQuery =
let table = self.query["table"].getStr()
self.queryString.add(&" FROM `{table}`")
return self
proc selectFirstSql*(self: MariadbQuery): MariadbQuery =
self.queryString.add(" LIMIT 1")
return self
proc selectByIdSql*(self: MariadbQuery, key:string): MariadbQuery =
let key = key.quote()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {key} = ? LIMIT 1")
else:
self.queryString.add(&" WHERE {key} = ? LIMIT 1")
return self
proc joinSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("join"):
for row in self.query["join"]:
let table = row["table"].getStr().quote()
let column1 = row["column1"].getStr().quote()
let symbol = row["symbol"].getStr()
let column2 = row["column2"].getStr().quote()
self.queryString.add(&" INNER JOIN {table} ON {column1} {symbol} {column2}")
return self
proc leftJoinSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("left_join"):
for row in self.query["left_join"]:
let table = row["table"].getStr().quote()
let column1 = row["column1"].getStr().quote()
let symbol = row["symbol"].getStr()
let column2 = row["column2"].getStr().quote()
self.queryString.add(&" LEFT JOIN {table} ON {column1} {symbol} {column2}")
return self
proc whereSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("where"):
for i, row in self.query["where"].getElems():
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
# let value = row["value"].getStr()
if i == 0:
self.queryString.add(&" WHERE {column} {symbol} ?")
else:
self.queryString.add(&" AND {column} {symbol} ?")
return self
proc orWhereSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("or_where"):
for row in self.query["or_where"]:
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
# let value = row["value"].getStr()
if self.queryString.contains("WHERE"):
self.queryString.add(&" OR {column} {symbol} ?")
else:
self.queryString.add(&" WHERE {column} {symbol} ?")
return self
proc whereBetweenSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("where_between"):
for row in self.query["where_between"]:
let column = row["column"].getStr()
# let start = row["width"][0].getFloat()
# let stop = row["width"][1].getFloat()
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND `{column}` BETWEEN {start} AND {stop}")
self.queryString.add(&" AND `{column}` BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE `{column}` BETWEEN {start} AND {stop}")
self.queryString.add(&" WHERE `{column}` BETWEEN ? AND ?")
return self
proc whereBetweenStringSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("where_between_string"):
for row in self.query["where_between_string"]:
let column = row["column"].getStr()
# let start = row["width"][0].getStr
# let stop = row["width"][1].getStr
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND `{column}` BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" AND `{column}` BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE `{column}` BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" WHERE `{column}` BETWEEN ? AND ?")
return self
proc whereNotBetweenSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("where_not_between"):
for row in self.query["where_not_between"]:
let column = row["column"].getStr().quote()
# let start = row["width"][0].getFloat()
# let stop = row["width"][1].getFloat()
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND `{column}` NOT BETWEEN {start} AND {stop}")
self.queryString.add(&" AND {column} NOT BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE `{column}` NOT BETWEEN {start} AND {stop}")
self.queryString.add(&" WHERE {column} NOT BETWEEN ? AND ?")
return self
proc whereNotBetweenStringSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("where_not_between_string"):
for row in self.query["where_not_between_string"]:
let column = row["column"].getStr().quote()
# let start = row["width"][0].getStr
# let stop = row["width"][1].getStr
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND `{column}` NOT BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" AND {column} NOT BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE `{column}` NOT BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" WHERE {column} NOT BETWEEN ? AND ?")
return self
proc whereInSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("where_in"):
var widthString = ""
for row in self.query["where_in"]:
let column = row["column"].getStr().quote()
for i, val in row["width"].getElems():
if i > 0: widthString.add(", ")
widthString.add("?")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} IN ({widthString})")
else:
self.queryString.add(&" WHERE {column} IN ({widthString})")
return self
proc whereNotInSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("where_not_in"):
var widthString = ""
for row in self.query["where_not_in"]:
let column = row["column"].getStr().quote()
for i, val in row["width"].getElems():
if i > 0: widthString.add(", ")
widthString.add("?")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} NOT IN ({widthString})")
else:
self.queryString.add(&" WHERE {column} NOT IN ({widthString})")
return self
proc whereNullSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("where_null"):
for row in self.query["where_null"]:
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} {symbol} NULL")
else:
self.queryString.add(&" WHERE {column} {symbol} NULL")
return self
proc groupBySql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("group_by"):
for row in self.query["group_by"]:
let column = row["column"].getStr().quote()
if self.queryString.contains("GROUP BY"):
self.queryString.add(&", {column}")
else:
self.queryString.add(&" GROUP BY {column}")
return self
proc havingSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("having"):
for i, row in self.query["having"].getElems():
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
# let value = row["value"].getStr()
if i == 0:
self.queryString.add(&" HAVING {column} {symbol} ?")
else:
self.queryString.add(&" AND {column} {symbol} ?")
return self
proc orderBySql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("order_by"):
for row in self.query["order_by"]:
let column = row["column"].getStr().quote()
let order = row["order"].getStr()
if self.queryString.contains("ORDER BY"):
self.queryString.add(&", {column} {order}")
else:
self.queryString.add(&" ORDER BY {column} {order}")
return self
proc limitSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("limit"):
let num = self.query["limit"].getInt()
self.queryString.add(&" LIMIT {num}")
return self
proc offsetSql*(self: MariadbQuery): MariadbQuery =
if self.query.hasKey("offset"):
let num = self.query["offset"].getInt()
self.queryString.add(&" OFFSET {num}")
return self
# ==================== INSERT ====================
proc insertSql*(self: MariadbQuery): MariadbQuery =
let table = self.query["table"].getStr()
self.queryString = &"INSERT INTO `{table}`"
return self
proc insertValueSql*(self: MariadbQuery, items: JsonNode): MariadbQuery =
var columns = ""
var values = ""
var i = 0
for key, val in items.pairs:
defer: i += 1
if i > 0:
columns.add(", ")
values.add(", ")
# If column name contains Upper letter, column name is covered by double quote
columns.add(&"`{key}`")
self.placeHolder.add(%*{"key":key, "value":val})
values.add("?")
self.queryString.add(&" ({columns}) VALUES ({values})")
return self
proc insertValuesSql*(self: MariadbQuery, rows: openArray[JsonNode]): MariadbQuery =
var columns = ""
var i = 0
for key, value in rows[0]:
defer: i += 1
if i > 0: columns.add(", ")
# If column name contains Upper letter, column name is covered by double quote
columns.add(&"`{key}`")
var values = ""
var valuesCount = 0
for items in rows:
var valueCount = 0
var value = ""
for key, val in items.pairs:
defer: valueCount += 1
if valueCount > 0: value.add(", ")
self.placeHolder.add(%*{"key":key, "value":val})
value.add("?")
if valuesCount > 0: values.add(", ")
valuesCount += 1
values.add(&"({value})")
self.queryString.add(&" ({columns}) VALUES {values}")
return self
# ==================== UPDATE ====================
proc updateSql*(self: MariadbQuery): MariadbQuery =
var queryString = ""
queryString.add("UPDATE")
var table = self.query["table"].getStr()
queryString.add(&" `{table}` SET")
self.queryString = queryString
return self
proc updateValuesSql*(self: MariadbQuery, items:JsonNode): MariadbQuery =
var value = ""
let placeHolder = newJArray()
var i = 0
for key, val in items.pairs:
defer: i += 1
if i > 0: value.add(",")
value.add(&" `{key}` = ?")
placeHolder.add(%*{"key":key, "value":val})
for row in self.placeHolder.items:
placeHolder.add(row)
self.placeHolder = placeHolder
self.queryString.add(value)
return self
# ==================== DELETE ====================
proc deleteSql*(self: MariadbQuery): MariadbQuery =
self.queryString = "DELETE"
return self
proc deleteByIdSql*(self: MariadbQuery, id: int, key: string): MariadbQuery =
self.queryString.add(&" WHERE `{key}` = ?")
return self
# ==================== Aggregates ====================
proc selectCountSql*(self: MariadbQuery): MariadbQuery =
let queryString =
if self.query.hasKey("select"):
let column = self.query["select"][0].getStr
&"`{column}`"
else:
"*"
self.queryString = &"SELECT count({queryString}) as aggregate"
return self
proc selectMaxSql*(self: MariadbQuery, column:string): MariadbQuery =
self.queryString = &"SELECT max(`{column}`) as aggregate"
return self
proc selectMinSql*(self: MariadbQuery, column:string): MariadbQuery =
self.queryString = &"SELECT min(`{column}`) as aggregate"
return self
proc selectAvgSql*(self: MariadbQuery, column:string): MariadbQuery =
self.queryString = &"SELECT avg(`{column}`) as aggregate"
return self
proc selectSumSql*(self: MariadbQuery, column:string): MariadbQuery =
self.queryString = &"SELECT sum(`{column}`) as aggregate"
return self
@@ -0,0 +1,89 @@
import std/tables
import std/times
import ../../libs/database_url
import ../../libs/mysql/mysql_rdb
import ../../error
import ../../log
import ./mysql_types
proc dbOpen*(_:type MySQL, database: string, user: string, password: string,
host: string, port: int, maxConnections: int = 1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir="",
maxConnectionLifetime=DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime=DEFAULT_CONN_MAX_IDLE_SECONDS): MysqlConnections =
var conns = newSeq[Connection](maxConnections)
for i in 0..<maxConnections:
let conn = mysql_rdb.init(nil)
if conn == nil:
mysql_rdb.close(conn)
dbError("mysql_rdb.init() failed")
if mysql_rdb.real_connect(conn, host, user, password, database, port.int32, nil, 0) == nil:
var errmsg = $mysql_rdb.error(conn)
mysql_rdb.close(conn)
let info = {
"database": database,
"user": user,
"password": password,
"host": host,
"port": $port
}
dbError(errmsg & $info)
conns[i] = Connection(
conn: conn,
isBusy: false,
createdAt: getTime().toUnix(),
lastUsedAt: getTime().toUnix()
)
let pools = Connections(
conns: conns,
timeout: timeout,
maxConnectionLifetime: maxConnectionLifetime,
maxConnectionIdleTime: maxConnectionIdleTime,
info: ConnectionInfo(
database: database,
user: user,
password: password,
host: host,
port: port
),
preparedCache: initTable[string, MysqlPreparedEntry](),
)
result = MysqlConnections(
pools: pools,
info: pools.info,
log: LogSetting(shouldDisplayLog:shouldDisplayLog, shouldOutputLogFile:shouldOutputLogFile, logDir:logDir)
)
proc dbOpen*(_:type MySQL, url: string, maxConnections=1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir="",
maxConnectionLifetime=DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime=DEFAULT_CONN_MAX_IDLE_SECONDS): MysqlConnections =
return dbOpen(MySQL, asDatabaseUrl(url), maxConnections, timeout, shouldDisplayLog, shouldOutputLogFile, logDir, maxConnectionLifetime, maxConnectionIdleTime)
proc dbOpen*(_:type MySQL, databaseUrl: DatabaseUrl, maxConnections=1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir="",
maxConnectionLifetime=DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime=DEFAULT_CONN_MAX_IDLE_SECONDS): MysqlConnections =
let parsed = parseDatabaseUrl(databaseUrl)
requireDatabaseUrlScheme(parsed, ["mysql"], "MySQL")
let database = databaseName(parsed)
let port = portOrDefault(parsed, 3306)
return dbOpen(
MySQL,
database,
parsed.username,
parsed.password,
parsed.hostname,
port,
maxConnections,
timeout,
shouldDisplayLog,
shouldOutputLogFile,
logDir,
maxConnectionLifetime,
maxConnectionIdleTime
)
@@ -0,0 +1,438 @@
import std/asyncdispatch
import std/json
import std/options
import std/strutils
import std/times
import ../../libs/mysql/mysql_impl
import ../../enums
import ./mysql_types
proc select*(self:MysqlConnections, columnsArg:varargs[string]):MysqlQuery =
let query = newJObject()
if columnsArg.len == 0:
query["select"] = %["*"]
else:
query["select"] = %columnsArg
let mysqlQuery = MysqlQuery(
log: self.log,
pools: self.pools,
query: query,
queryString: "",
placeHolder: newJArray(),
isInTransaction: self.isInTransaction,
transactionConn: self.transactionConn,
)
return mysqlQuery
proc table*(self:MysqlConnections, tableArg: string): MysqlQuery =
let query = newJObject()
query["table"] = %tableArg
let mysqlQuery = MysqlQuery(
log: self.log,
pools: self.pools,
query: query,
queryString: "",
placeHolder: newJArray(),
isInTransaction: self.isInTransaction,
transactionConn: self.transactionConn,
)
return mysqlQuery
proc table*(self:MysqlQuery, tableArg: string): MysqlQuery =
self.query["table"] = %tableArg
return self
proc `distinct`*(self: MysqlQuery): MysqlQuery =
self.query["distinct"] = %true
return self
# ============================== Conditions ==============================
proc join*(self: MysqlQuery, table: string, column1: string, symbol: string,
column2: string): MysqlQuery =
if self.query.hasKey("join") == false:
self.query["join"] = %*[{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
}]
else:
self.query["join"].add(%*{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
})
return self
proc leftJoin*(self: MysqlQuery, table: string, column1: string, symbol: string,
column2: string): MysqlQuery =
if self.query.hasKey("left_join") == false:
self.query["left_join"] = %*[{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
}]
else:
self.query["left_join"].add(%*{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
})
return self
const whereSymbols = ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "LIKE","%LIKE","LIKE%","%LIKE%"]
const whereSymbolsError = """Arg position 3 is only allowed of ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "LIKE","%LIKE","LIKE%","%LIKE%"]"""
proc where*(self: MysqlQuery, column: string, symbol: string,
value: string|int|float): MysqlQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key": column, "value": value})
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc where*(self: MysqlQuery, column: string, symbol: string,
value: bool): MysqlQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc where*(self: MysqlQuery, column: string, symbol: string, value: nil.type): MysqlQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("where_null") == false:
self.query["where_null"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["where_null"].add(
%*{
"column": column,
"symbol": symbol,
"value": "null"
}
)
return self
proc orWhere*(self: MysqlQuery, column: string, symbol: string,
value: string|int|float|bool): MysqlQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["or_where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc orWhere*(self: MysqlQuery, column: string, symbol: string, value: nil.type): MysqlQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["or_where"].add(%*{
"column": column,
"symbol": symbol,
"value": "null"
})
return self
proc whereBetween*(self:MysqlQuery, column:string, width:array[2, int|float]): MysqlQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_between") == false:
self.query["where_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_between"].add(%*{
"column": column,
"width": width
})
return self
proc whereBetween*(self:MysqlQuery, column:string, width:array[2, string]): MysqlQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_between_string") == false:
self.query["where_between_string"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_between_string"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotBetween*(self:MysqlQuery, column:string, width:array[2, int|float]): MysqlQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_not_between") == false:
self.query["where_not_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_between"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotBetween*(self:MysqlQuery, column:string, width:array[2, string]): MysqlQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_not_between_string") == false:
self.query["where_not_between_string"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_between_string"].add(%*{
"column": column,
"width": width
})
return self
proc whereIn*(self:MysqlQuery, column:string, width:seq[int|float|string]): MysqlQuery =
for row in width:
self.placeHolder.add(%*{"key": column, "value": row})
if self.query.hasKey("where_in") == false:
self.query["where_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotIn*(self:MysqlQuery, column:string, width:seq[int|float|string]): MysqlQuery =
for row in width:
self.placeHolder.add(%*{"key": column, "value": row})
if self.query.hasKey("where_not_in") == false:
self.query["where_not_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNull*(self:MysqlQuery, column:string): MysqlQuery =
if self.query.hasKey("where_null") == false:
self.query["where_null"] = %*[{
"column": column,
"symbol": "is",
}]
else:
self.query["where_null"].add(%*{
"column": column,
"symbol": "is",
})
return self
proc groupBy*(self:MysqlQuery, column:string): MysqlQuery =
if self.query.hasKey("group_by") == false:
self.query["group_by"] = %*[{"column": column}]
else:
self.query["group_by"].add(%*{"column": column})
return self
proc having*(self: MysqlQuery, column: string, symbol: string,
value: string|int|float|bool): MysqlQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["having"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc having*(self: MysqlQuery, column: string, symbol: string, value: nil.type): MysqlQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["having"].add(
%*{
"column": column,
"symbol": symbol,
"value": "null"
}
)
return self
proc orderBy*(self:MysqlQuery, column:string, order:Order): MysqlQuery =
if self.query.hasKey("order_by") == false:
self.query["order_by"] = %*[{
"column": column,
"order": $order
}]
else:
self.query["order_by"].add(%*{
"column": column,
"order": $order
})
return self
proc limit*(self: MysqlQuery, num: int): MysqlQuery =
self.query["limit"] = %num
return self
proc offset*(self: MysqlQuery, num: int): MysqlQuery =
self.query["offset"] = %num
return self
proc raw*(self:MysqlConnections, sql:string, arges=newJArray()): RawMysqlQuery =
## arges is `JArray` `[true, 1, 1.1, "str"]`
##
## can't use BLOB data.
let rawQueryRdb = RawMysqlQuery(
log: self.log,
pools: self.pools,
info: self.info,
query: newJObject(),
queryString: sql.strip(),
placeHolder: arges,
isInTransaction: false,
transactionConn: 0
)
return rawQueryRdb
@@ -0,0 +1,42 @@
import std/macros
import std/strutils
import std/strformat
import ../../models/mysql/mysql_types
when NimMajor == 1:
macro rdbTransaction(rdb:MysqlConnections, callback: untyped):untyped =
var callbackStr = callback.repr
callbackStr.removePrefix
callbackStr = callbackStr.indent(4)
callbackStr = fmt"""
block:
{rdb.repr}.begin().await
try:
{callbackStr}
{rdb.repr}.commit().await
except DbError:
{rdb.repr}.rollback().await
"""
let body = callbackStr.parseStmt()
return body
else:
macro rdbTransaction(rdb:MysqlConnections, callback: untyped):untyped =
var callbackStr = callback.repr
callbackStr.removePrefix
callbackStr = callbackStr.indent(4)
callbackStr = fmt"""
block:
{rdb.repr}.begin().await
try:
{callbackStr}
{rdb.repr}.commit().await
except CatchableError:
{rdb.repr}.rollback().await
"""
let body = callbackStr.parseStmt()
return body
template transaction*(rdb:MysqlConnections, callback: untyped) =
rdbTransaction(rdb, callback)
@@ -0,0 +1,109 @@
import std/tables
import std/json
import ../../log
import ../../libs/mysql/mysql_rdb
type MySQL* = object
const DEFAULT_CONN_MAX_LIFETIME_SECONDS* = 300
const DEFAULT_CONN_MAX_IDLE_SECONDS* = 300
type ConnectionInfo* = object
database*:string
user*:string
password*:string
host*:string
port*:int
type Connection* = object
conn*: PMySQL
isBusy*: bool
createdAt*: int64
lastUsedAt*: int64
type MysqlPreparedEntry* = ref object
sql*: string
nArgs*: int
stmts*: seq[PSTMT]
refCount*: int
lastUsedAt*: int64
type Connections* = ref object
conns*: seq[Connection]
timeout*:int
maxConnectionLifetime*: int
maxConnectionIdleTime*: int
info*: ConnectionInfo
preparedCache*: Table[string, MysqlPreparedEntry]
## created by `let rdb = dbOpen(MySQL, "localhost", 3306)`
type MysqlConnections* = ref object
log*: LogSetting
pools*:Connections
info*:ConnectionInfo
# for transaction
isInTransaction*: bool
transactionConn*: int
type MysqlPreparedContext* = ref object
owner*: MysqlConnections
connI*: int
## created by `rdb.select("columnName")` or `rdb.table("tableName")`
type MysqlQuery* = ref object
log*: LogSetting
pools*:Connections
info*:ConnectionInfo
query*: JsonNode
queryString*: string
placeHolder*: JsonNode # [{"key":"user", "value":"user1"}]
# for transaction
isInTransaction*: bool
transactionConn*: int
type RawMysqlQuery* = ref object
log*: LogSetting
pools*:Connections
info*:ConnectionInfo
query*: JsonNode
queryString*: string
placeHolder*: JsonNode # ["user1", "user1@example.com"]
# for transaction
isInTransaction*: bool
transactionConn*: int
type MysqlResultBindCache* = ref object
binds*: seq[BIND]
buffers*: seq[string]
lengths*: seq[culong]
nullFlags*: seq[my_bool]
errorFlags*: seq[my_bool]
type MysqlPreparedStatement* = ref object
owner*: MysqlConnections
info*: ConnectionInfo
entry*: MysqlPreparedEntry
sql*: string
nArgs*: int
isClosed*: bool
resultBindCache*: seq[MysqlResultBindCache]
proc `$`*(self:MysqlConnections|MysqlQuery|RawMysqlQuery):string =
return "MySQL"
proc isConnected*(self:MysqlConnections|MysqlQuery|RawMysqlQuery):bool =
return self.pools.conns.len > 0
@@ -0,0 +1,228 @@
import std/json
import ../mysql_types
import ./mysql_generator
# ==================== SELECT ====================
proc selectBuilder*(self: MysqlQuery): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc selectFirstBuilder*(self: MysqlQuery): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.selectFirstSql()
.offsetSql()
.queryString
proc selectFindBuilder*(self: MysqlQuery, key: string): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.selectByIdSql(key)
.queryString
# ==================== INSERT ====================
proc insertValueBuilder*(self: MysqlQuery, items: JsonNode): string =
return self
.insertSql()
.insertValueSql(items)
.queryString
proc insertValuesBuilder*(self: MysqlQuery, rows: openArray[JsonNode]): string =
return self
.insertSql()
.insertValuesSql(rows)
.queryString
# ==================== UPDATE ====================
proc updateBuilder*(self: MysqlQuery, items: JsonNode): string =
return self
.updateSql()
.updateValuesSql(items)
.whereSql()
.orWhereSql()
.limitSql()
.offsetSql()
.queryString
# ==================== DELETE ====================
proc deleteBuilder*(self: MysqlQuery): string =
return self
.deleteSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.limitSql()
.offsetSql()
.queryString
proc deleteByIdBuilder*(self: MysqlQuery, id: int, key: string): string =
return self
.deleteSql()
.fromSql()
.deleteByIdSql(id, key)
.queryString
# ==================== Aggregates ====================
proc countBuilder*(self:MysqlQuery): string =
return self
.selectCountSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc maxBuilder*(self:MysqlQuery, column:string): string =
return self
.selectMaxSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc minBuilder*(self:MysqlQuery, column:string): string =
return self
.selectMinSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc avgBuilder*(self:MysqlQuery, column:string): string =
return self
.selectAvgSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc sumBuilder*(self:MysqlQuery, column:string): string =
return self
.selectSumSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc columnBuilder*(self:MysqlQuery): string =
return self
.selectSql()
.fromSql()
.selectFirstSql()
.queryString
@@ -0,0 +1,430 @@
import std/json
import std/strformat
import std/strutils
# import ../../database_types
import ../mysql_types
proc quote(input:string):string =
## "User.id as userId" => ```User```.```id``` as ```userId```
var tmp = newSeq[string]()
for segment in input.split("."):
if segment.contains(" as "):
# Split once on ' as ' to separate the expression and its alias
let parts = segment.split(" as ", maxsplit = 1)
let expression = parts[0]
let alias = parts[1]
if expression.contains("("):
# Extract function name and column name using index-based slicing
let funcStart = expression.find('(')
let funcEnd = expression.find(')', funcStart)
let funcName = expression[0 ..< funcStart]
let columnName = expression[funcStart + 1 ..< funcEnd]
tmp.add(&"{funcName}(`{columnName}`) as `{alias}`")
else:
# Quote the expression and alias
tmp.add(&"`{expression}` as `{alias}`")
elif segment.contains("("):
# Leave functions without alias as is
tmp.add(segment)
else:
# Quote standalone identifiers
tmp.add(&"`{segment}`")
return tmp.join(".")
# ==================== SELECT ====================
proc selectSql*(self: MysqlQuery): MysqlQuery =
var queryString = ""
if self.query.hasKey("distinct"):
queryString.add("SELECT DISTINCT")
else:
queryString.add("SELECT")
if self.query.hasKey("select"):
for i, item in self.query["select"].getElems():
if i > 0: queryString.add(",")
var column = item.getStr()
# if column.contains("as"):
# let original = column.split("as")[0].strip()
# let renamed = column.split("as")[1].strip()
# queryString.add(&" `{original}` as `{renamed}`")
# else:
# queryString.add(&" `{column}`")
column = quote(column)
queryString.add(&" {column}")
else:
queryString.add(" *")
self.queryString = queryString
return self
proc fromSql*(self: MysqlQuery): MysqlQuery =
let table = self.query["table"].getStr().quote()
self.queryString.add(&" FROM {table}")
return self
proc selectFirstSql*(self: MysqlQuery): MysqlQuery =
self.queryString.add(" LIMIT 1")
return self
proc selectByIdSql*(self: MysqlQuery, key:string): MysqlQuery =
let key = key.quote()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {key} = ? LIMIT 1")
else:
self.queryString.add(&" WHERE {key} = ? LIMIT 1")
return self
proc joinSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("join"):
for row in self.query["join"]:
let table = row["table"].getStr().quote()
let column1 = row["column1"].getStr().quote()
let symbol = row["symbol"].getStr()
let column2 = row["column2"].getStr().quote()
self.queryString.add(&" INNER JOIN {table} ON {column1} {symbol} {column2}")
return self
proc leftJoinSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("left_join"):
for row in self.query["left_join"]:
let table = row["table"].getStr().quote()
let column1 = row["column1"].getStr().quote()
let symbol = row["symbol"].getStr()
let column2 = row["column2"].getStr().quote()
self.queryString.add(&" LEFT JOIN {table} ON {column1} {symbol} {column2}")
return self
proc whereSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("where"):
for i, row in self.query["where"].getElems():
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
# let value = row["value"].getStr()
if i == 0:
self.queryString.add(&" WHERE {column} {symbol} ?")
else:
self.queryString.add(&" AND {column} {symbol} ?")
return self
proc orWhereSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("or_where"):
for row in self.query["or_where"]:
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
# let value = row["value"].getStr()
if self.queryString.contains("WHERE"):
self.queryString.add(&" OR {column} {symbol} ?")
else:
self.queryString.add(&" WHERE {column} {symbol} ?")
return self
proc whereBetweenSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("where_between"):
for row in self.query["where_between"]:
let column = row["column"].getStr()
# let start = row["width"][0].getFloat()
# let stop = row["width"][1].getFloat()
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND `{column}` BETWEEN {start} AND {stop}")
self.queryString.add(&" AND `{column}` BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE `{column}` BETWEEN {start} AND {stop}")
self.queryString.add(&" WHERE `{column}` BETWEEN ? AND ?")
return self
proc whereBetweenStringSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("where_between_string"):
for row in self.query["where_between_string"]:
let column = row["column"].getStr()
# let start = row["width"][0].getStr
# let stop = row["width"][1].getStr
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND `{column}` BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" AND `{column}` BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE `{column}` BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" WHERE `{column}` BETWEEN ? AND ?")
return self
proc whereNotBetweenSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("where_not_between"):
for row in self.query["where_not_between"]:
let column = row["column"].getStr().quote()
# let start = row["width"][0].getFloat()
# let stop = row["width"][1].getFloat()
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND `{column}` NOT BETWEEN {start} AND {stop}")
self.queryString.add(&" AND {column} NOT BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE `{column}` NOT BETWEEN {start} AND {stop}")
self.queryString.add(&" WHERE {column} NOT BETWEEN ? AND ?")
return self
proc whereNotBetweenStringSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("where_not_between_string"):
for row in self.query["where_not_between_string"]:
let column = row["column"].getStr().quote()
# let start = row["width"][0].getStr
# let stop = row["width"][1].getStr
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND `{column}` NOT BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" AND {column} NOT BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE `{column}` NOT BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" WHERE {column} NOT BETWEEN ? AND ?")
return self
proc whereInSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("where_in"):
var widthString = ""
for row in self.query["where_in"]:
let column = row["column"].getStr().quote()
for i, val in row["width"].getElems():
if i > 0: widthString.add(", ")
widthString.add("?")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} IN ({widthString})")
else:
self.queryString.add(&" WHERE {column} IN ({widthString})")
return self
proc whereNotInSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("where_not_in"):
var widthString = ""
for row in self.query["where_not_in"]:
let column = row["column"].getStr().quote()
for i, val in row["width"].getElems():
if i > 0: widthString.add(", ")
widthString.add("?")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} NOT IN ({widthString})")
else:
self.queryString.add(&" WHERE {column} NOT IN ({widthString})")
return self
proc whereNullSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("where_null"):
for row in self.query["where_null"]:
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} {symbol} NULL")
else:
self.queryString.add(&" WHERE {column} {symbol} NULL")
return self
proc groupBySql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("group_by"):
for row in self.query["group_by"]:
let column = row["column"].getStr().quote()
if self.queryString.contains("GROUP BY"):
self.queryString.add(&", {column}")
else:
self.queryString.add(&" GROUP BY {column}")
return self
proc havingSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("having"):
for i, row in self.query["having"].getElems():
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
# let value = row["value"].getStr()
if i == 0:
self.queryString.add(&" HAVING {column} {symbol} ?")
else:
self.queryString.add(&" AND {column} {symbol} ?")
return self
proc orderBySql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("order_by"):
for row in self.query["order_by"]:
let column = row["column"].getStr().quote()
let order = row["order"].getStr()
if self.queryString.contains("ORDER BY"):
self.queryString.add(&", {column} {order}")
else:
self.queryString.add(&" ORDER BY {column} {order}")
return self
proc limitSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("limit"):
let num = self.query["limit"].getInt()
self.queryString.add(&" LIMIT {num}")
return self
proc offsetSql*(self: MysqlQuery): MysqlQuery =
if self.query.hasKey("offset"):
let num = self.query["offset"].getInt()
self.queryString.add(&" OFFSET {num}")
return self
# ==================== INSERT ====================
proc insertSql*(self: MysqlQuery): MysqlQuery =
let table = self.query["table"].getStr()
self.queryString = &"INSERT INTO `{table}`"
return self
proc insertValueSql*(self: MysqlQuery, items: JsonNode): MysqlQuery =
var columns = ""
var values = ""
var i = 0
for key, val in items.pairs:
defer: i += 1
if i > 0:
columns.add(", ")
values.add(", ")
# If column name contains Upper letter, column name is covered by double quote
columns.add(&"`{key}`")
self.placeHolder.add(%*{"key":key, "value":val})
values.add("?")
self.queryString.add(&" ({columns}) VALUES ({values})")
return self
proc insertValuesSql*(self: MysqlQuery, rows: openArray[JsonNode]): MysqlQuery =
var columns = ""
var i = 0
for key, value in rows[0]:
defer: i += 1
if i > 0: columns.add(", ")
# If column name contains Upper letter, column name is covered by double quote
columns.add(&"`{key}`")
var values = ""
var valuesCount = 0
for items in rows:
var valueCount = 0
var value = ""
for key, val in items.pairs:
defer: valueCount += 1
if valueCount > 0: value.add(", ")
self.placeHolder.add(%*{"key":key, "value":val})
value.add("?")
if valuesCount > 0: values.add(", ")
valuesCount += 1
values.add(&"({value})")
self.queryString.add(&" ({columns}) VALUES {values}")
return self
# ==================== UPDATE ====================
proc updateSql*(self: MysqlQuery): MysqlQuery =
var queryString = ""
queryString.add("UPDATE")
var table = self.query["table"].getStr()
queryString.add(&" `{table}` SET")
self.queryString = queryString
return self
proc updateValuesSql*(self: MysqlQuery, items:JsonNode): MysqlQuery =
var value = ""
let placeHolder = newJArray()
var i = 0
for key, val in items.pairs:
defer: i += 1
if i > 0: value.add(",")
value.add(&" `{key}` = ?")
placeHolder.add(%*{"key":key, "value":val})
for row in self.placeHolder.items:
placeHolder.add(row)
self.placeHolder = placeHolder
self.queryString.add(value)
return self
# ==================== DELETE ====================
proc deleteSql*(self: MysqlQuery): MysqlQuery =
self.queryString = "DELETE"
return self
proc deleteByIdSql*(self: MysqlQuery, id: int, key: string): MysqlQuery =
self.queryString.add(&" WHERE `{key}` = ?")
return self
# ==================== Aggregates ====================
proc selectCountSql*(self: MysqlQuery): MysqlQuery =
let queryString =
if self.query.hasKey("select"):
let column = self.query["select"][0].getStr
&"`{column}`"
else:
"*"
self.queryString = &"SELECT count({queryString}) as aggregate"
return self
proc selectMaxSql*(self: MysqlQuery, column:string): MysqlQuery =
self.queryString = &"SELECT max(`{column}`) as aggregate"
return self
proc selectMinSql*(self: MysqlQuery, column:string): MysqlQuery =
self.queryString = &"SELECT min(`{column}`) as aggregate"
return self
proc selectAvgSql*(self: MysqlQuery, column:string): MysqlQuery =
self.queryString = &"SELECT avg(`{column}`) as aggregate"
return self
proc selectSumSql*(self: MysqlQuery, column:string): MysqlQuery =
self.queryString = &"SELECT sum(`{column}`) as aggregate"
return self
@@ -0,0 +1,18 @@
import std/asyncdispatch
import std/options
import std/json
proc orm*[T](response:Future[seq[JsonNode]], typ:typedesc[T]):Future[seq[T]] {.async.} =
let response = response.await
var result = newSeq[T](response.len)
for i, res in response:
result[i] = res.to(typ)
return result
proc orm*[T](response:Future[Option[JsonNode]], typ:typedesc[T]):Future[Option[T]] {.async.} =
let response = response.await
if response.isSome:
return response.get.to(typ).some()
else:
return none(T)
@@ -0,0 +1,42 @@
import std/macros
import std/strutils
import std/strformat
import ../../models/postgres/postgres_types
when NimMajor == 1:
macro rdbTransaction(rdb:PostgresConnections, callback: untyped):untyped =
var callbackStr = callback.repr
callbackStr.removePrefix
callbackStr = callbackStr.indent(4)
callbackStr = fmt"""
block:
{rdb.repr}.begin().await
try:
{callbackStr}
{rdb.repr}.commit().await
except DbError:
{rdb.repr}.rollback().await
"""
let body = callbackStr.parseStmt()
return body
else:
macro rdbTransaction(rdb:PostgresConnections, callback: untyped):untyped =
var callbackStr = callback.repr
callbackStr.removePrefix
callbackStr = callbackStr.indent(4)
callbackStr = fmt"""
block:
{rdb.repr}.begin().await
try:
{callbackStr}
{rdb.repr}.commit().await
except CatchableError:
{rdb.repr}.rollback().await
"""
let body = callbackStr.parseStmt()
return body
template transaction*(rdb:PostgresConnections, callback: untyped) =
rdbTransaction(rdb, callback)
@@ -0,0 +1,82 @@
import std/asyncdispatch
import std/deques
import std/tables
import std/times
import ../database_types
import ../../error
import ../../libs/database_url
import ../../libs/postgres/postgres_rdb
import ../../libs/postgres/postgres_lib
import ../../log
import ./postgres_types
proc dbOpen*(_:type PostgreSQL, database: string, user: string, password: string,
host: string, port: int, maxConnections=1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir="",
maxConnectionLifetime=DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime=DEFAULT_CONN_MAX_IDLE_SECONDS): PostgresConnections =
var conns = newSeq[Connection](maxConnections)
for i in 0..<maxConnections:
let conn = postgres_rdb.pqsetdbLogin(host, port.`$`.cstring, nil, nil, database, user, password)
if pqStatus(conn) != CONNECTION_OK: dbError(conn)
if pqsetnonblocking(conn, 1'i32) != 0'i32:
dbError(conn)
if pqisnonblocking(conn) != 1'i32:
raise newException(DbError, "PostgreSQL connection could not be set to non-blocking mode")
conns[i] = Connection(
conn: conn,
isBusy: false,
createdAt: getTime().toUnix(),
lastUsedAt: getTime().toUnix(),
)
let pools = Connections(
conns: conns,
timeout: timeout,
maxConnectionLifetime: maxConnectionLifetime,
maxConnectionIdleTime: maxConnectionIdleTime,
database: database,
user: user,
password: password,
host: host,
port: port,
waiters: initDeque[Future[void]](),
columnTypeCache: initTable[string, seq[Row]](),
preparedCache: initTable[string, PostgresPreparedEntry](),
)
result = PostgresConnections(
pools: pools,
log: LogSetting(shouldDisplayLog:shouldDisplayLog, shouldOutputLogFile:shouldOutputLogFile, logDir:logDir)
)
proc dbOpen*(_:type PostgreSQL, url: string, maxConnections: int = 1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir="",
maxConnectionLifetime=DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime=DEFAULT_CONN_MAX_IDLE_SECONDS): PostgresConnections =
return dbOpen(PostgreSQL, asDatabaseUrl(url), maxConnections, timeout, shouldDisplayLog, shouldOutputLogFile, logDir, maxConnectionLifetime, maxConnectionIdleTime)
proc dbOpen*(_:type PostgreSQL, databaseUrl: DatabaseUrl, maxConnections: int = 1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir="",
maxConnectionLifetime=DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime=DEFAULT_CONN_MAX_IDLE_SECONDS): PostgresConnections =
let parsed = parseDatabaseUrl(databaseUrl)
requireDatabaseUrlScheme(parsed, ["postgresql", "postgres"], "PostgreSQL")
let database = databaseName(parsed)
let port = portOrDefault(parsed, 5432)
return dbOpen(
PostgreSQL,
database,
parsed.username,
parsed.password,
parsed.hostname,
port,
maxConnections,
timeout,
shouldDisplayLog,
shouldOutputLogFile,
logDir,
maxConnectionLifetime,
maxConnectionIdleTime
)
@@ -0,0 +1,441 @@
import std/asyncdispatch
import std/json
import std/options
import std/strutils
import std/times
import ../../libs/postgres/postgres_impl
import ../../enums
import ./postgres_types
# ================================================================================
# query
# ================================================================================
proc select*(self:PostgresConnections, columnsArg:varargs[string]):PostgresQuery =
let query = newJObject()
if columnsArg.len == 0 or columnsArg[0] == "*":
query["select"] = %["*"]
else:
query["select"] = %columnsArg
let postgresQuery = PostgresQuery(
log: self.log,
pools: self.pools,
query: query,
queryString: "",
placeHolder: newJArray(),
isInTransaction: self.isInTransaction,
transactionConn: self.transactionConn,
)
return postgresQuery
proc table*(self:PostgresConnections, tableArg: string): PostgresQuery =
let query = newJObject()
query["table"] = %tableArg
let postgresQuery = PostgresQuery(
log: self.log,
pools: self.pools,
query: query,
queryString: "",
placeHolder: newJArray(),
isInTransaction: self.isInTransaction,
transactionConn: self.transactionConn,
)
return postgresQuery
proc table*(self:PostgresQuery, tableArg: string): PostgresQuery =
self.query["table"] = %tableArg
return self
proc `distinct`*(self: PostgresQuery): PostgresQuery =
self.query["distinct"] = %true
return self
# ============================== Conditions ==============================
proc join*(self: PostgresQuery, table: string, column1: string, symbol: string,
column2: string): PostgresQuery =
if self.query.hasKey("join") == false:
self.query["join"] = %*[{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
}]
else:
self.query["join"].add(%*{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
})
return self
proc leftJoin*(self: PostgresQuery, table: string, column1: string, symbol: string,
column2: string): PostgresQuery =
if self.query.hasKey("left_join") == false:
self.query["left_join"] = %*[{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
}]
else:
self.query["left_join"].add(%*{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
})
return self
const whereSymbols = ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "LIKE","%LIKE","LIKE%","%LIKE%"]
const whereSymbolsError = """Arg position 3 is only allowed of ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "LIKE","%LIKE","LIKE%","%LIKE%"]"""
proc where*(self: PostgresQuery, column: string, symbol: string,
value: string|int|float): PostgresQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key": column, "value": value})
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc where*(self: PostgresQuery, column: string, symbol: string,
value: bool): PostgresQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc where*(self: PostgresQuery, column: string, symbol: string, value: nil.type): PostgresQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("where_null") == false:
self.query["where_null"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["where_null"].add(
%*{
"column": column,
"symbol": symbol,
"value": "null"
}
)
return self
proc orWhere*(self: PostgresQuery, column: string, symbol: string,
value: string|int|float|bool): PostgresQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["or_where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc orWhere*(self: PostgresQuery, column: string, symbol: string, value: nil.type): PostgresQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["or_where"].add(%*{
"column": column,
"symbol": symbol,
"value": "null"
})
return self
proc whereBetween*(self:PostgresQuery, column:string, width:array[2, int|float]): PostgresQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_between") == false:
self.query["where_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_between"].add(%*{
"column": column,
"width": width
})
return self
proc whereBetween*(self:PostgresQuery, column:string, width:array[2, string]): PostgresQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_between_string") == false:
self.query["where_between_string"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_between_string"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotBetween*(self:PostgresQuery, column:string, width:array[2, int|float]): PostgresQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_not_between") == false:
self.query["where_not_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_between"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotBetween*(self:PostgresQuery, column:string, width:array[2, string]): PostgresQuery =
self.placeHolder.add(%*{"key": column, "value": width[0]})
self.placeHolder.add(%*{"key": column, "value": width[1]})
if self.query.hasKey("where_not_between_string") == false:
self.query["where_not_between_string"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_between_string"].add(%*{
"column": column,
"width": width
})
return self
proc whereIn*(self:PostgresQuery, column:string, width:seq[int|float|string]): PostgresQuery =
for row in width:
self.placeHolder.add(%*{"key": column, "value": row})
if self.query.hasKey("where_in") == false:
self.query["where_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotIn*(self:PostgresQuery, column:string, width:seq[int|float|string]): PostgresQuery =
for row in width:
self.placeHolder.add(%*{"key": column, "value": row})
if self.query.hasKey("where_not_in") == false:
self.query["where_not_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNull*(self:PostgresQuery, column:string): PostgresQuery =
if self.query.hasKey("where_null") == false:
self.query["where_null"] = %*[{
"column": column,
"symbol": "is",
}]
else:
self.query["where_null"].add(%*{
"column": column,
"symbol": "is",
})
return self
proc groupBy*(self:PostgresQuery, column:string): PostgresQuery =
if self.query.hasKey("group_by") == false:
self.query["group_by"] = %*[{"column": column}]
else:
self.query["group_by"].add(%*{"column": column})
return self
proc having*(self: PostgresQuery, column: string, symbol: string,
value: string|int|float|bool): PostgresQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["having"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc having*(self: PostgresQuery, column: string, symbol: string, value: nil.type): PostgresQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["having"].add(
%*{
"column": column,
"symbol": symbol,
"value": "null"
}
)
return self
proc orderBy*(self:PostgresQuery, column:string, order:Order): PostgresQuery =
if self.query.hasKey("order_by") == false:
self.query["order_by"] = %*[{
"column": column,
"order": $order
}]
else:
self.query["order_by"].add(%*{
"column": column,
"order": $order
})
return self
proc limit*(self: PostgresQuery, num: int): PostgresQuery =
self.query["limit"] = %num
return self
proc offset*(self: PostgresQuery, num: int): PostgresQuery =
self.query["offset"] = %num
return self
proc raw*(self:PostgresConnections, sql:string, arges=newJArray()): RawPostgresQuery =
## arges is `JArray`
##
## can't use BLOB data.
let rawQueryRdb = RawPostgresQuery(
log: self.log,
pools: self.pools,
query: newJObject(),
queryString: sql,
placeHolder: arges,
isInTransaction: false,
transactionConn: 0
)
return rawQueryRdb
@@ -0,0 +1,101 @@
import std/asyncdispatch
import std/deques
import std/json
import std/tables
import ../database_types
import ../../log
import ../../libs/postgres/postgres_rdb
type PostgreSQL* = object
const DEFAULT_CONN_MAX_LIFETIME_SECONDS* = 300
const DEFAULT_CONN_MAX_IDLE_SECONDS* = 300
type Connection* = ref object
conn*: PPGconn
isBusy*: bool
createdAt*: int64
lastUsedAt*: int64
type PostgresPreparedEntry* = ref object
sql*: string
nArgs*: int
stmtBaseName*: string
stmtNames*: seq[string]
refCount*: int
lastUsedAt*: int64
type Connections* = ref object
conns*: seq[Connection]
timeout*: int
maxConnectionLifetime*: int
maxConnectionIdleTime*: int
database*: string
user*: string
password*: string
host*: string
port*: int
## `getFreeConn` が接続を待つときに積む Future。`returnConn` が先頭から 1 件だけ完了させる。
waiters*: Deque[Future[void]]
## `exec` / `insertId` 用。テーブルごとに information_schema 相当の列型を初回のみ取得して保持する。
columnTypeCache*: Table[string, seq[Row]]
## SQL 単位の prepared statement cache。物理 prepare 状態は conn ごとに保持する。
preparedCache*: Table[string, PostgresPreparedEntry]
## created by `let rdb = dbOpen(PostgreSQL, "localhost", 5432)`
type PostgresConnections* = ref object
log*: LogSetting
pools*:Connections
# for transaction
isInTransaction*: bool
transactionConn*: int
## created by `rdb.select("columnName")` or `rdb.table("tableName")`
type PostgresQuery* = ref object
log*: LogSetting
pools*:Connections
query*: JsonNode # JObject
queryString*: string
placeHolder*: JsonNode # JArray [{"key":"user", "value":"user1"}]
# for transaction
isInTransaction*: bool
transactionConn*: int
type RawPostgresQuery* = ref object
log*: LogSetting
pools*:Connections
query*: JsonNode
queryString*: string
placeHolder*: JsonNode # JArray ["user1", "user1@example.com"]
# for transaction
isInTransaction*: bool
transactionConn*: int
type PostgresPreparedContext* = ref object
owner*: PostgresConnections
connI*: int
type PostgresPreparedStatement* = ref object
owner*: PostgresConnections
entry*: PostgresPreparedEntry
sql*: string
nArgs*: int
isClosed*: bool
proc `$`*(self:PostgresConnections|PostgresQuery|RawPostgresQuery):string =
return "PostgreSQL"
proc isConnected*(self:PostgresConnections|PostgresQuery|RawPostgresQuery):bool =
return self.pools.conns.len > 0
@@ -0,0 +1,228 @@
import std/json
import ../postgres_types
import ./postgres_generator
# ==================== SELECT ====================
proc selectBuilder*(self: PostgresQuery): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc selectFirstBuilder*(self: PostgresQuery): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.selectFirstSql()
.offsetSql()
.queryString
proc selectFindBuilder*(self: PostgresQuery, key: string): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.selectByIdSql(key)
.queryString
# ==================== INSERT ====================
proc insertValueBuilder*(self: PostgresQuery, items: JsonNode): string =
return self
.insertSql()
.insertValueSql(items)
.queryString
proc insertValuesBuilder*(self: PostgresQuery, rows: openArray[JsonNode]): string =
return self
.insertSql()
.insertValuesSql(rows)
.queryString
# ==================== UPDATE ====================
proc updateBuilder*(self: PostgresQuery, items: JsonNode): string =
return self
.updateSql()
.updateValuesSql(items)
.whereSql()
.orWhereSql()
.limitSql()
.offsetSql()
.queryString
# ==================== DELETE ====================
proc deleteBuilder*(self: PostgresQuery): string =
return self
.deleteSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.limitSql()
.offsetSql()
.queryString
proc deleteByIdBuilder*(self: PostgresQuery, id: int, key: string): string =
return self
.deleteSql()
.fromSql()
.deleteByIdSql(id, key)
.queryString
# ==================== Aggregates ====================
proc countBuilder*(self:PostgresQuery): string =
return self
.selectCountSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc maxBuilder*(self:PostgresQuery, column:string): string =
return self
.selectMaxSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc minBuilder*(self:PostgresQuery, column:string): string =
return self
.selectMinSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc avgBuilder*(self:PostgresQuery, column:string): string =
return self
.selectAvgSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc sumBuilder*(self:PostgresQuery, column:string): string =
return self
.selectSumSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc columnBuilder*(self:PostgresQuery): string =
return self
.selectSql()
.fromSql()
.selectFirstSql()
.queryString
@@ -0,0 +1,429 @@
import std/json
import std/strformat
import std/strutils
import ../postgres_types
proc quote(input:string):string =
## `User.id as userId` => `"User"."id" as "userId"`
##
## `COUNT(id) as count` => `COUNT("id") as "count"`
##
## `MAX(id) as maxId` => `MAX("id") as "maxId"`
##
var tmp = newSeq[string]()
for segment in input.split("."):
if segment.contains(" as "):
# Split once on ' as ' to separate the expression and its alias
let parts = segment.split(" as ", maxsplit = 1)
let expression = parts[0]
let alias = parts[1]
if expression.contains("("):
# Extract function name and column name using index-based slicing
let funcStart = expression.find('(')
let funcEnd = expression.find(')', funcStart)
let funcName = expression[0 ..< funcStart]
let columnName = expression[funcStart + 1 ..< funcEnd]
tmp.add(&"{funcName}(\"{columnName}\") as \"{alias}\"")
else:
# Quote the expression and alias
tmp.add(&"\"{expression}\" as \"{alias}\"")
elif segment.contains("("):
# Leave functions without alias as is
tmp.add(segment)
else:
# Quote standalone identifiers
tmp.add(&"\"{segment}\"")
return tmp.join(".")
# ==================== SELECT ====================
proc selectSql*(self: PostgresQuery): PostgresQuery =
var queryString = ""
if self.query.hasKey("distinct"):
queryString.add("SELECT DISTINCT")
else:
queryString.add("SELECT")
if self.query.hasKey("select"):
for i, item in self.query["select"].getElems():
if i > 0: queryString.add(",")
var column = item.getStr()
if column != "*": column = quote(column)
queryString.add(&" {column}")
else:
queryString.add(" *")
self.queryString = queryString
return self
proc fromSql*(self: PostgresQuery): PostgresQuery =
let table = self.query["table"].getStr()
self.queryString.add(&" FROM \"{table}\"")
return self
proc selectFirstSql*(self: PostgresQuery): PostgresQuery =
self.queryString.add(" LIMIT 1")
return self
proc selectByIdSql*(self: PostgresQuery, key:string): PostgresQuery =
let key = key.quote()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {key} = ? LIMIT 1")
else:
self.queryString.add(&" WHERE {key} = ? LIMIT 1")
return self
proc joinSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("join"):
for row in self.query["join"]:
let table = row["table"].getStr().quote()
let column1 = row["column1"].getStr().quote()
let symbol = row["symbol"].getStr()
let column2 = row["column2"].getStr().quote()
self.queryString.add(&" INNER JOIN {table} ON {column1} {symbol} {column2}")
return self
proc leftJoinSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("left_join"):
for row in self.query["left_join"]:
let table = row["table"].getStr().quote()
let column1 = row["column1"].getStr().quote()
let symbol = row["symbol"].getStr()
let column2 = row["column2"].getStr().quote()
self.queryString.add(&" LEFT JOIN {table} ON {column1} {symbol} {column2}")
return self
proc whereSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("where"):
for i, row in self.query["where"].getElems():
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
# let value = row["value"].getStr()
if i == 0:
self.queryString.add(&" WHERE {column} {symbol} ?")
else:
self.queryString.add(&" AND {column} {symbol} ?")
return self
proc orWhereSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("or_where"):
for row in self.query["or_where"]:
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
# let value = row["value"].getStr()
if self.queryString.contains("WHERE"):
self.queryString.add(&" OR {column} {symbol} ?")
else:
self.queryString.add(&" WHERE {column} {symbol} ?")
return self
proc whereBetweenSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("where_between"):
for row in self.query["where_between"]:
let column = row["column"].getStr()
# let start = row["width"][0].getFloat()
# let stop = row["width"][1].getFloat()
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND \"{column}\" BETWEEN {start} AND {stop}")
self.queryString.add(&" AND \"{column}\" BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE \"{column}\" BETWEEN {start} AND {stop}")
self.queryString.add(&" WHERE \"{column}\" BETWEEN ? AND ?")
return self
proc whereBetweenStringSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("where_between_string"):
for row in self.query["where_between_string"]:
let column = row["column"].getStr()
# let start = row["width"][0].getStr
# let stop = row["width"][1].getStr
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND \"{column}\" BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" AND \"{column}\" BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE \"{column}\" BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" WHERE \"{column}\" BETWEEN ? AND ?")
return self
proc whereNotBetweenSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("where_not_between"):
for row in self.query["where_not_between"]:
let column = row["column"].getStr().quote()
# let start = row["width"][0].getFloat()
# let stop = row["width"][1].getFloat()
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND \"{column}\" NOT BETWEEN {start} AND {stop}")
self.queryString.add(&" AND {column} NOT BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE \"{column}\" NOT BETWEEN {start} AND {stop}")
self.queryString.add(&" WHERE {column} NOT BETWEEN ? AND ?")
return self
proc whereNotBetweenStringSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("where_not_between_string"):
for row in self.query["where_not_between_string"]:
let column = row["column"].getStr().quote()
# let start = row["width"][0].getStr
# let stop = row["width"][1].getStr
if self.queryString.contains("WHERE"):
# self.queryString.add(&" AND \"{column}\" NOT BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" AND {column} NOT BETWEEN ? AND ?")
else:
# self.queryString.add(&" WHERE \"{column}\" NOT BETWEEN '{start}' AND '{stop}'")
self.queryString.add(&" WHERE {column} NOT BETWEEN ? AND ?")
return self
proc whereInSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("where_in"):
var widthString = ""
for row in self.query["where_in"]:
let column = row["column"].getStr().quote()
for i, val in row["width"].getElems():
if i > 0: widthString.add(", ")
widthString.add("?")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} IN ({widthString})")
else:
self.queryString.add(&" WHERE {column} IN ({widthString})")
return self
proc whereNotInSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("where_not_in"):
var widthString = ""
for row in self.query["where_not_in"]:
let column = row["column"].getStr().quote()
for i, val in row["width"].getElems():
if i > 0: widthString.add(", ")
widthString.add("?")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} NOT IN ({widthString})")
else:
self.queryString.add(&" WHERE {column} NOT IN ({widthString})")
return self
proc whereNullSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("where_null"):
for row in self.query["where_null"]:
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} {symbol} null")
else:
self.queryString.add(&" WHERE {column} {symbol} null")
return self
proc groupBySql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("group_by"):
for row in self.query["group_by"]:
let column = row["column"].getStr().quote()
if self.queryString.contains("GROUP BY"):
self.queryString.add(&", {column}")
else:
self.queryString.add(&" GROUP BY {column}")
return self
proc havingSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("having"):
for i, row in self.query["having"].getElems():
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
# let value = row["value"].getStr()
if i == 0:
self.queryString.add(&" HAVING {column} {symbol} ?")
else:
self.queryString.add(&" AND {column} {symbol} ?")
return self
proc orderBySql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("order_by"):
for row in self.query["order_by"]:
let column = row["column"].getStr().quote()
let order = row["order"].getStr()
if self.queryString.contains("ORDER BY"):
self.queryString.add(&", {column} {order}")
else:
self.queryString.add(&" ORDER BY {column} {order}")
return self
proc limitSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("limit"):
let num = self.query["limit"].getInt()
self.queryString.add(&" LIMIT {num}")
return self
proc offsetSql*(self: PostgresQuery): PostgresQuery =
if self.query.hasKey("offset"):
let num = self.query["offset"].getInt()
self.queryString.add(&" OFFSET {num}")
return self
# ==================== INSERT ====================
proc insertSql*(self: PostgresQuery): PostgresQuery =
let table = self.query["table"].getStr()
self.queryString = &"INSERT INTO \"{table}\""
return self
proc insertValueSql*(self: PostgresQuery, items: JsonNode): PostgresQuery =
var columns = ""
var values = ""
var i = 0
for key, val in items.pairs:
defer: i += 1
if i > 0:
columns.add(", ")
values.add(", ")
# If column name contains Upper letter, column name is covered by double quote
columns.add(&"\"{key}\"")
self.placeHolder.add(%*{"key":key, "value":val})
values.add("?")
self.queryString.add(&" ({columns}) VALUES ({values})")
return self
proc insertValuesSql*(self: PostgresQuery, rows: openArray[JsonNode]): PostgresQuery =
var columns = ""
var i = 0
for key, value in rows[0]:
defer: i += 1
if i > 0: columns.add(", ")
# If column name contains Upper letter, column name is covered by double quote
columns.add(&"\"{key}\"")
var values = ""
var valuesCount = 0
for items in rows:
var valueCount = 0
var value = ""
for key, val in items.pairs:
defer: valueCount += 1
if valueCount > 0: value.add(", ")
self.placeHolder.add(%*{"key":key, "value":val})
value.add("?")
if valuesCount > 0: values.add(", ")
valuesCount += 1
values.add(&"({value})")
self.queryString.add(&" ({columns}) VALUES {values}")
return self
# ==================== UPDATE ====================
proc updateSql*(self: PostgresQuery): PostgresQuery =
var queryString = ""
queryString.add("UPDATE")
var table = self.query["table"].getStr()
queryString.add(&" \"{table}\" SET")
self.queryString = queryString
return self
proc updateValuesSql*(self: PostgresQuery, items:JsonNode): PostgresQuery =
var value = ""
let placeHolder = newJArray()
var i = 0
for key, val in items.pairs:
defer: i += 1
if i > 0: value.add(",")
value.add(&" \"{key}\" = ?")
placeHolder.add(%*{"key":key, "value":val})
for row in self.placeHolder.items:
placeHolder.add(row)
self.placeHolder = placeHolder
self.queryString.add(value)
return self
# ==================== DELETE ====================
proc deleteSql*(self: PostgresQuery): PostgresQuery =
self.queryString = "DELETE"
return self
proc deleteByIdSql*(self: PostgresQuery, id: int, key: string): PostgresQuery =
self.queryString.add(&" WHERE \"{key}\" = ?")
return self
# ==================== Aggregates ====================
proc selectCountSql*(self: PostgresQuery): PostgresQuery =
let queryString =
if self.query.hasKey("select"):
let column = self.query["select"][0].getStr
&"\"{column}\""
else:
"*"
self.queryString = &"SELECT count({queryString}) as aggregate"
return self
proc selectMaxSql*(self: PostgresQuery, column:string): PostgresQuery =
self.queryString = &"SELECT max(\"{column}\") as aggregate"
return self
proc selectMinSql*(self: PostgresQuery, column:string): PostgresQuery =
self.queryString = &"SELECT min(\"{column}\") as aggregate"
return self
proc selectAvgSql*(self: PostgresQuery, column:string): PostgresQuery =
self.queryString = &"SELECT avg(\"{column}\") as aggregate"
return self
proc selectSumSql*(self: PostgresQuery, column:string): PostgresQuery =
self.queryString = &"SELECT sum(\"{column}\") as aggregate"
return self
@@ -0,0 +1,229 @@
import std/json
import ../sqlite_types
import ./sqlite_generator
# ==================== SELECT ====================
proc selectBuilder*(self: SqliteQuery): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc selectFirstBuilder*(self: SqliteQuery): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.selectFirstSql()
.offsetSql()
.queryString
proc selectFindBuilder*(self: SqliteQuery, key: string): string =
return self
.selectSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereBetweenStringSql()
.whereNotBetweenSql()
.whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.selectByIdSql(key)
.queryString
# ==================== INSERT ====================
proc insertValueBuilder*(self: SqliteQuery, items: JsonNode): string =
## items is JOject
return self
.insertSql()
.insertValueSql(items)
.queryString
proc insertValuesBuilder*(self: SqliteQuery, rows: openArray[JsonNode]): string =
return self
.insertSql()
.insertValuesSql(rows)
.queryString
# ==================== UPDATE ====================
proc updateBuilder*(self: SqliteQuery, items: JsonNode): string =
return self
.updateSql()
.updateValuesSql(items)
.whereSql()
.orWhereSql()
.limitSql()
.offsetSql()
.queryString
# ==================== DELETE ====================
proc deleteBuilder*(self: SqliteQuery): string =
return self
.deleteSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.limitSql()
.offsetSql()
.queryString
proc deleteByIdBuilder*(self: SqliteQuery, id: int, key: string): string =
return self
.deleteSql()
.fromSql()
.deleteByIdSql(id, key)
.queryString
# ==================== Aggregates ====================
proc countBuilder*(self:SqliteQuery): string =
return self
.selectCountSql()
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc maxBuilder*(self:SqliteQuery, column:string): string =
return self
.selectMaxSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc minBuilder*(self:SqliteQuery, column:string): string =
return self
.selectMinSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc avgBuilder*(self:SqliteQuery, column:string): string =
return self
.selectAvgSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc sumBuilder*(self:SqliteQuery, column:string): string =
return self
.selectSumSql(column)
.fromSql()
.joinSql()
.leftJoinSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
.havingSql()
.orderBySql()
.limitSql()
.offsetSql()
.queryString
proc columnBuilder*(self:SqliteQuery): string =
return self
.selectSql()
.fromSql()
.selectFirstSql()
.queryString
@@ -0,0 +1,431 @@
import std/json
import std/strformat
import std/strutils
# import ../../database_types
import ../sqlite_types
proc quote(input:string):string =
## `User.id as userId` => `"User"."id" as "userId"`
##
## `COUNT(id) as count` => `COUNT("id") as "count"`
##
## `MAX(id) as maxId` => `MAX("id") as "maxId"`
##
var tmp = newSeq[string]()
for segment in input.split("."):
if segment.contains(" as "):
# Split once on ' as ' to separate the expression and its alias
let parts = segment.split(" as ", maxsplit = 1)
let expression = parts[0]
let alias = parts[1]
if expression.contains("("):
# Extract function name and column name using index-based slicing
let funcStart = expression.find('(')
let funcEnd = expression.find(')', funcStart)
let funcName = expression[0 ..< funcStart]
let columnName = expression[funcStart + 1 ..< funcEnd]
tmp.add(&"{funcName}(\"{columnName}\") as \"{alias}\"")
else:
# Quote the expression and alias
tmp.add(&"\"{expression}\" as \"{alias}\"")
elif segment.contains("("):
# Leave functions without alias as is
tmp.add(segment)
else:
# Quote standalone identifiers
tmp.add(&"\"{segment}\"")
return tmp.join(".")
# ==================== SELECT ====================
proc selectSql*(self: SqliteQuery): SqliteQuery =
var queryString = ""
if self.query.hasKey("distinct"):
queryString.add("SELECT DISTINCT")
else:
queryString.add("SELECT")
if self.query.hasKey("select"):
for i, item in self.query["select"].getElems():
if i > 0: queryString.add(",")
var column = item.getStr()
if column != "*": column = quote(column)
queryString.add(&" {column}")
else:
queryString.add(" *")
self.queryString = queryString
return self
proc fromSql*(self: SqliteQuery): SqliteQuery =
let table = self.query["table"].getStr()
self.queryString.add(&" FROM \"{table}\"")
return self
proc selectFirstSql*(self: SqliteQuery): SqliteQuery =
self.queryString.add(" LIMIT 1")
return self
proc selectByIdSql*(self: SqliteQuery, key:string): SqliteQuery =
let key = key.quote()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {key} = ? LIMIT 1")
else:
self.queryString.add(&" WHERE {key} = ? LIMIT 1")
return self
proc joinSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("join"):
for row in self.query["join"]:
let table = row["table"].getStr().quote()
let column1 = row["column1"].getStr().quote()
let symbol = row["symbol"].getStr()
let column2 = row["column2"].getStr().quote()
self.queryString.add(&" INNER JOIN {table} ON {column1} {symbol} {column2}")
return self
proc leftJoinSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("left_join"):
for row in self.query["left_join"]:
let table = row["table"].getStr().quote()
let column1 = row["column1"].getStr().quote()
let symbol = row["symbol"].getStr()
let column2 = row["column2"].getStr().quote()
self.queryString.add(&" LEFT JOIN {table} ON {column1} {symbol} {column2}")
return self
proc whereSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("where"):
for i, row in self.query["where"].getElems():
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
let value = row["value"].getStr()
if i == 0:
self.queryString.add(&" WHERE {column} {symbol} {value}")
else:
self.queryString.add(&" AND {column} {symbol} {value}")
return self
proc orWhereSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("or_where"):
for row in self.query["or_where"]:
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
let value = row["value"].getStr()
if self.queryString.contains("WHERE"):
self.queryString.add(&" OR {column} {symbol} {value}")
else:
self.queryString.add(&" WHERE {column} {symbol} {value}")
return self
proc whereBetweenSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("where_between"):
for row in self.query["where_between"]:
let column = row["column"].getStr().quote()
let start = row["width"][0].getFloat()
let stop = row["width"][1].getFloat()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} BETWEEN {start} AND {stop}")
else:
self.queryString.add(&" WHERE {column} BETWEEN {start} AND {stop}")
return self
proc whereBetweenStringSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("where_between_string"):
for row in self.query["where_between_string"]:
let column = row["column"].getStr().quote()
let start = row["width"][0].getStr()
let stop = row["width"][1].getStr()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} BETWEEN \"{start}\" AND \"{stop}\"")
else:
self.queryString.add(&" WHERE {column} BETWEEN \"{start}\" AND \"{stop}\"")
return self
proc whereNotBetweenSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("where_not_between"):
for row in self.query["where_not_between"]:
let column = row["column"].getStr().quote()
let start = row["width"][0].getFloat()
let stop = row["width"][1].getFloat()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} NOT BETWEEN {start} AND {stop}")
else:
self.queryString.add(&" WHERE {column} NOT BETWEEN {start} AND {stop}")
return self
proc whereNotBetweenStringSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("where_not_between_string"):
for row in self.query["where_not_between_string"]:
let column = row["column"].getStr().quote()
let start = row["width"][0].getStr()
let stop = row["width"][1].getStr()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} NOT BETWEEN \"{start}\" AND \"{stop}\"")
else:
self.queryString.add(&" WHERE {column} NOT BETWEEN \"{start}\" AND \"{stop}\"")
return self
proc whereInSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("where_in"):
var widthString = ""
for row in self.query["where_in"]:
let column = row["column"].getStr().quote()
for i, val in row["width"].getElems():
if i > 0: widthString.add(", ")
if val.kind == JInt:
widthString.add($(val.getInt()))
elif val.kind == JFloat:
widthString.add($(val.getFloat()))
elif val.kind == JString:
widthString.add(&"\"{val.getStr()}\"")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} IN ({widthString})")
else:
self.queryString.add(&" WHERE {column} IN ({widthString})")
return self
proc whereNotInSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("where_not_in"):
var widthString = ""
for row in self.query["where_not_in"]:
let column = row["column"].getStr().quote()
for i, val in row["width"].getElems():
if i > 0: widthString.add(", ")
if val.kind == JInt:
widthString.add($(val.getInt()))
elif val.kind == JFloat:
widthString.add($(val.getFloat()))
elif val.kind == JString:
widthString.add(&"\"{val.getStr()}\"")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} NOT IN ({widthString})")
else:
self.queryString.add(&" WHERE {column} NOT IN ({widthString})")
return self
proc whereNullSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("where_null"):
for row in self.query["where_null"]:
let column = row["column"].getStr().quote()
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {column} is null")
else:
self.queryString.add(&" WHERE {column} is null")
return self
proc groupBySql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("group_by"):
for row in self.query["group_by"]:
let column = row["column"].getStr().quote()
if self.queryString.contains("GROUP BY"):
self.queryString.add(&", {column}")
else:
self.queryString.add(&" GROUP BY {column}")
return self
proc havingSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("having"):
for i, row in self.query["having"].getElems():
let column = row["column"].getStr().quote()
let symbol = row["symbol"].getStr()
let value = row["value"].getStr()
if i == 0:
self.queryString.add(&" HAVING {column} {symbol} {value}")
else:
self.queryString.add(&" AND {column} {symbol} {value}")
return self
proc orderBySql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("order_by"):
for row in self.query["order_by"]:
let column = row["column"].getStr().quote()
let order = row["order"].getStr()
if self.queryString.contains("ORDER BY"):
self.queryString.add(&", {column} {order}")
else:
self.queryString.add(&" ORDER BY {column} {order}")
return self
proc limitSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("limit"):
let num = self.query["limit"].getInt()
self.queryString.add(&" LIMIT {num}")
return self
proc offsetSql*(self: SqliteQuery): SqliteQuery =
if self.query.hasKey("offset"):
let num = self.query["offset"].getInt()
self.queryString.add(&" OFFSET {num}")
return self
# ==================== INSERT ====================
proc insertSql*(self: SqliteQuery): SqliteQuery =
let table = self.query["table"].getStr()
self.queryString = &"INSERT INTO \"{table}\""
return self
proc insertValueSql*(self: SqliteQuery, items: JsonNode): SqliteQuery =
## items is JObject
var columns = ""
var values = ""
var i = 0
for key, val in items.pairs:
if i > 0:
columns.add(", ")
values.add(", ")
i += 1
# If column name contains Upper letter, column name is covered by double quote
columns.add(&"\"{key}\"")
self.placeHolder.add(%*{"key":key, "value":val})
values.add("?")
self.queryString.add(&" ({columns}) VALUES ({values})")
return self
proc insertValuesSql*(self: SqliteQuery, rows: openArray[JsonNode]): SqliteQuery =
var columns = ""
var i = 0
for key, value in rows[0]:
defer: i += 1
if i > 0: columns.add(", ")
# If column name contains Upper letter, column name is covered by double quote
columns.add(&"\"{key}\"")
var values = ""
var valuesCount = 0
for items in rows:
var valueCount = 0
var value = ""
for key, val in items.pairs:
defer: valueCount += 1
if valueCount > 0: value.add(", ")
self.placeHolder.add(%*{"key":key, "value":val})
value.add("?")
if valuesCount > 0: values.add(", ")
valuesCount += 1
values.add(&"({value})")
self.queryString.add(&" ({columns}) VALUES {values}")
return self
# ==================== UPDATE ====================
proc updateSql*(self: SqliteQuery): SqliteQuery =
var queryString = ""
queryString.add("UPDATE")
var table = self.query["table"].getStr()
queryString.add(&" \"{table}\" SET")
self.queryString = queryString
return self
proc updateValuesSql*(self: SqliteQuery, items:JsonNode): SqliteQuery =
var value = ""
let placeHolder = newJArray()
var i = 0
for key, val in items.pairs:
defer: i.inc()
if i > 0: value.add(",")
value.add(&" \"{key}\" = ?")
placeHolder.add(%*{"key":key, "value":val})
for row in self.placeHolder.items:
placeHolder.add(row)
self.placeHolder = placeHolder
self.queryString.add(value)
return self
# ==================== DELETE ====================
proc deleteSql*(self: SqliteQuery): SqliteQuery =
self.queryString = "DELETE"
return self
proc deleteByIdSql*(self: SqliteQuery, id: int, key: string): SqliteQuery =
self.queryString.add(&" WHERE \"{key}\" = ?")
return self
# ==================== Aggregates ====================
proc selectCountSql*(self: SqliteQuery): SqliteQuery =
let queryString =
if self.query.hasKey("select"):
let column = self.query["select"][0].getStr
&"{column}"
else:
"*"
self.queryString = &"SELECT count({queryString}) as aggregate"
return self
proc selectMaxSql*(self: SqliteQuery, column:string): SqliteQuery =
self.queryString = &"SELECT max({column}) as aggregate"
return self
proc selectMinSql*(self: SqliteQuery, column:string): SqliteQuery =
self.queryString = &"SELECT min({column}) as aggregate"
return self
proc selectAvgSql*(self: SqliteQuery, column:string): SqliteQuery =
self.queryString = &"SELECT avg({column}) as aggregate"
return self
proc selectSumSql*(self: SqliteQuery, column:string): SqliteQuery =
self.queryString = &"SELECT sum({column}) as aggregate"
return self
@@ -0,0 +1,63 @@
import std/asyncdispatch
import std/deques
import std/strutils
import std/tables
import std/times
import ../../libs/database_url
import ../../libs/sqlite/sqlite_rdb
import ../../log
import ./sqlite_types
proc openSqlite(database: string; maxConnections: int; timeout: int;
shouldDisplayLog: bool; shouldOutputLogFile: bool; logDir: string;
maxConnectionLifetime: int = DEFAULT_CONN_MAX_LIFETIME_SECONDS;
maxConnectionIdleTime: int = DEFAULT_CONN_MAX_IDLE_SECONDS): SqliteConnections =
var conns = newSeq[Connection](maxConnections)
for i in 0..<maxConnections:
var db: PSqlite3
discard sqlite_rdb.open(database, db)
conns[i] = Connection(
conn: db,
isBusy: false,
createdAt: getTime().toUnix(),
lastUsedAt: getTime().toUnix(),
)
let pools = Connections(
conns: conns,
timeout: timeout,
maxConnectionLifetime: maxConnectionLifetime,
maxConnectionIdleTime: maxConnectionIdleTime,
database: database,
waiters: initDeque[Future[void]](),
columnTypeCache: initTable[string, seq[(string, string)]](),
preparedCache: initTable[string, SqlitePreparedEntry](),
)
result = SqliteConnections(
pools: pools,
log: LogSetting(shouldDisplayLog:shouldDisplayLog, shouldOutputLogFile:shouldOutputLogFile, logDir:logDir)
)
proc dbOpen*(_:type SQLite3, database: string = "",
maxConnections: int = 1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir="",
maxConnectionLifetime=DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime=DEFAULT_CONN_MAX_IDLE_SECONDS): SqliteConnections =
if database.len > 0 and database.contains("://"):
let parsed = parseDatabaseUrl(asDatabaseUrl(database))
requireDatabaseUrlScheme(parsed, ["sqlite"], "SQLite")
return openSqlite(sqliteDatabasePath(parsed), maxConnections, timeout, shouldDisplayLog, shouldOutputLogFile, logDir, maxConnectionLifetime, maxConnectionIdleTime)
return openSqlite(database, maxConnections, timeout, shouldDisplayLog, shouldOutputLogFile, logDir, maxConnectionLifetime, maxConnectionIdleTime)
proc dbOpen*(_:type SQLite3, databaseUrl: DatabaseUrl,
maxConnections: int = 1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir="",
maxConnectionLifetime=DEFAULT_CONN_MAX_LIFETIME_SECONDS,
maxConnectionIdleTime=DEFAULT_CONN_MAX_IDLE_SECONDS): SqliteConnections =
let parsed = parseDatabaseUrl(databaseUrl)
requireDatabaseUrlScheme(parsed, ["sqlite"], "SQLite")
let database = sqliteDatabasePath(parsed)
return openSqlite(database, maxConnections, timeout, shouldDisplayLog, shouldOutputLogFile, logDir, maxConnectionLifetime, maxConnectionIdleTime)
@@ -0,0 +1,411 @@
import std/asyncdispatch
import std/json
import std/options
import std/strutils
import std/times
import ../../libs/sqlite/sqlite_impl
import ../../enums
import ./sqlite_types
proc select*(self:SqliteConnections, columnsArg:varargs[string]):SqliteQuery =
let query = newJObject()
if columnsArg.len == 0:
query["select"] = %["*"]
else:
query["select"] = %columnsArg
let sqliteQuery = SqliteQuery(
log: self.log,
pools: self.pools,
query: query,
queryString: "",
placeHolder: newJArray(),
isInTransaction: self.isInTransaction,
transactionConn: self.transactionConn,
)
return sqliteQuery
proc table*(self:SqliteConnections, tableArg: string): SqliteQuery =
let query = newJObject()
query["table"] = %tableArg
let sqliteQuery = SqliteQuery(
log: self.log,
pools: self.pools,
query: query,
queryString: "",
placeHolder: newJArray(),
isInTransaction: self.isInTransaction,
transactionConn: self.transactionConn,
)
return sqliteQuery
proc table*(self:SqliteQuery, tableArg: string): SqliteQuery =
self.query["table"] = %tableArg
return self
proc `distinct`*(self: SqliteQuery): SqliteQuery =
self.query["distinct"] = %true
return self
# ============================== Conditions ==============================
proc join*(self: SqliteQuery, table: string, column1: string, symbol: string,
column2: string): SqliteQuery =
if self.query.hasKey("join") == false:
self.query["join"] = %*[{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
}]
else:
self.query["join"].add(%*{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
})
return self
proc leftJoin*(self: SqliteQuery, table: string, column1: string, symbol: string,
column2: string): SqliteQuery =
if self.query.hasKey("left_join") == false:
self.query["left_join"] = %*[{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
}]
else:
self.query["left_join"].add(%*{
"table": table,
"column1": column1,
"symbol": symbol,
"column2": column2
})
return self
const whereSymbols = ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "LIKE","%LIKE","LIKE%","%LIKE%"]
const whereSymbolsError = """Arg position 3 is only allowed of ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "LIKE","%LIKE","LIKE%","%LIKE%"]"""
proc where*(self: SqliteQuery, column: string, symbol: string,
value: string|int|float): SqliteQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key": column, "value": value})
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc where*(self: SqliteQuery, column: string, symbol: string,
value: bool): SqliteQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc where*(self: SqliteQuery, column: string, symbol: string, value: nil.type): SqliteQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "null"
}
)
return self
proc orWhere*(self: SqliteQuery, column: string, symbol: string,
value: string|int|float|bool): SqliteQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["or_where"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc orWhere*(self: SqliteQuery, column: string, symbol: string, value: nil.type): SqliteQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["or_where"].add(%*{
"column": column,
"symbol": symbol,
"value": "null"
})
return self
proc whereBetween*(self:SqliteQuery, column:string, width:array[2, int|float]): SqliteQuery =
if self.query.hasKey("where_between") == false:
self.query["where_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_between"].add(%*{
"column": column,
"width": width
})
return self
proc whereBetween*(self:SqliteQuery, column:string, width:array[2, string]): SqliteQuery =
if self.query.hasKey("where_between_string") == false:
self.query["where_between_string"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_between_string"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotBetween*(self:SqliteQuery, column:string, width:array[2, int|float]): SqliteQuery =
if self.query.hasKey("where_not_between") == false:
self.query["where_not_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_between"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotBetween*(self:SqliteQuery, column:string, width:array[2, string]): SqliteQuery =
if self.query.hasKey("where_not_between_string") == false:
self.query["where_not_between_string"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_between_string"].add(%*{
"column": column,
"width": width
})
return self
proc whereIn*(self:SqliteQuery, column:string, width:seq[int|float|string]): SqliteQuery =
if self.query.hasKey("where_in") == false:
self.query["where_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotIn*(self:SqliteQuery, column:string, width:seq[int|float|string]): SqliteQuery =
if self.query.hasKey("where_not_in") == false:
self.query["where_not_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNull*(self:SqliteQuery, column:string): SqliteQuery =
if self.query.hasKey("where_null") == false:
self.query["where_null"] = %*[{
"column": column
}]
else:
self.query["where_null"].add(%*{
"column": column
})
return self
proc groupBy*(self:SqliteQuery, column:string): SqliteQuery =
if self.query.hasKey("group_by") == false:
self.query["group_by"] = %*[{"column": column}]
else:
self.query["group_by"].add(%*{"column": column})
return self
proc having*(self: SqliteQuery, column: string, symbol: string,
value: string|int|float|bool): SqliteQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
self.placeHolder.add(%*{"key":column, "value":value})
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": "?"
}]
else:
self.query["having"].add(
%*{
"column": column,
"symbol": symbol,
"value": "?"
}
)
return self
proc having*(self: SqliteQuery, column: string, symbol: string, value: nil.type): SqliteQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": "null"
}]
else:
self.query["having"].add(
%*{
"column": column,
"symbol": symbol,
"value": "null"
}
)
return self
proc orderBy*(self:SqliteQuery, column:string, order:Order): SqliteQuery =
if self.query.hasKey("order_by") == false:
self.query["order_by"] = %*[{
"column": column,
"order": $order
}]
else:
self.query["order_by"].add(%*{
"column": column,
"order": $order
})
return self
proc limit*(self: SqliteQuery, num: int): SqliteQuery =
self.query["limit"] = %num
return self
proc offset*(self: SqliteQuery, num: int): SqliteQuery =
self.query["offset"] = %num
return self
proc raw*(self:SqliteConnections, sql:string, arges=newJArray()): RawSqliteQuery =
## arges is `JArray`
##
## can't use BLOB data.
let rawQueryRdb = RawSqliteQuery(
log: self.log,
pools: self.pools,
query: newJObject(),
queryString: sql,
placeHolder: arges,
isInTransaction: false,
transactionConn: 0
)
return rawQueryRdb
@@ -0,0 +1,42 @@
import std/macros
import std/strutils
import std/strformat
import ../../models/sqlite/sqlite_types
when NimMajor == 1:
macro rdbTransaction(rdb:SqliteConnections, callback: untyped):untyped =
var callbackStr = callback.repr
callbackStr.removePrefix
callbackStr = callbackStr.indent(4)
callbackStr = fmt"""
block:
{rdb.repr}.begin().await
try:
{callbackStr}
{rdb.repr}.commit().await
except:
{rdb.repr}.rollback().await
"""
let body = callbackStr.parseStmt()
return body
else:
macro rdbTransaction(rdb:SqliteConnections, callback: untyped):untyped =
var callbackStr = callback.repr
callbackStr.removePrefix
callbackStr = callbackStr.indent(4)
callbackStr = fmt"""
block:
{rdb.repr}.begin().await
try:
{callbackStr}
{rdb.repr}.commit().await
except DbError, CatchableError:
{rdb.repr}.rollback().await
"""
let body = callbackStr.parseStmt()
return body
template transaction*(rdb:SqliteConnections, callback: untyped) =
rdbTransaction(rdb, callback)
@@ -0,0 +1,93 @@
import std/asyncdispatch
import std/deques
import std/json
import std/tables
import ../database_types
import ../../log
import ../../libs/sqlite/sqlite_rdb
type SQLite3* = object
const DEFAULT_CONN_MAX_LIFETIME_SECONDS* = 300
const DEFAULT_CONN_MAX_IDLE_SECONDS* = 300
type Connection* = ref object
conn*: PSqlite3
isBusy*: bool
createdAt*: int64
lastUsedAt*: int64
type SqlitePreparedEntry* = ref object
sql*: string
nArgs*: int
stmts*: seq[PStmt]
refCount*: int
lastUsedAt*: int64
type Connections* = ref object
conns*: seq[Connection]
timeout*: int
maxConnectionLifetime*: int
maxConnectionIdleTime*: int
database*: string
## `getFreeConn` が接続を待つときに積む Future。`returnConn` が先頭から 1 件だけ完了させる。
waiters*: Deque[Future[void]]
## `exec` / `insertId` 用。テーブルごとに PRAGMA table_info の結果を初回のみ保持する。
columnTypeCache*: Table[string, seq[(string, string)]]
preparedCache*: Table[string, SqlitePreparedEntry]
## created by `let rdb = dbOpen(SQLite3, "/path/to/sqlite.db")`
type SqliteConnections* = ref object
log*: LogSetting
pools*:Connections
# for transaction
isInTransaction*: bool
transactionConn*: int
type SqlitePreparedContext* = ref object
owner*: SqliteConnections
connI*: int
## created by `rdb.select("columnName")` or `rdb.table("tableName")`
type SqliteQuery* = ref object
log*: LogSetting
pools*:Connections
query*: JsonNode # JObject
queryString*: string
placeHolder*: JsonNode # JArray [{"key":"user", "value":"user1"}]
# for transaction
isInTransaction*: bool
transactionConn*: int
type RawSqliteQuery* = ref object
log*: LogSetting
pools*:Connections
query*: JsonNode # JObject
queryString*: string
placeHolder*: JsonNode # JArray ["user1", "user1@example.com"]
# for transaction
isInTransaction*: bool
transactionConn*: int
type SqlitePreparedStatement* = ref object
owner*: SqliteConnections
entry*: SqlitePreparedEntry
sql*: string
isClosed*: bool
cachedColumns*: DbColumns
hasCachedColumns*: bool
proc `$`*(self:SqliteConnections|SqliteQuery|RawSqliteQuery):string =
return "SQLite"
@@ -0,0 +1,197 @@
import std/json
import ../surreal_types
import ./surreal_generator
# ==================== SELECT ====================
proc selectBuilder*(self: SurrealQuery): string =
return self
.selectSql()
.fromSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
# .whereBetweenStringSql()
.whereNotBetweenSql()
# .whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
#.havingSql()
.orderBySql()
.limitSql()
.startSql()
.fetchSql()
.parallelSql()
.queryString
proc selectFirstBuilder*(self: SurrealQuery): string =
return self
.selectSql()
.fromSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
# .whereBetweenStringSql()
.whereNotBetweenSql()
# .whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
#.havingSql()
.orderBySql()
.selectFirstSql() # LIMIT
.startSql()
.fetchSql()
.parallelSql()
.queryString
proc selectFindBuilder*(self: SurrealQuery, id:SurrealId, key: string): string =
return self
.selectSql()
.fromSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
# .whereBetweenStringSql()
.whereNotBetweenSql()
# .whereNotBetweenStringSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.selectByIdSql(id, key) # LIMIT
.fetchSql()
.parallelSql()
.queryString
# ==================== INSERT ====================
proc insertValueBuilder*(self: SurrealQuery, items: JsonNode): string =
return self
.insertSql()
.insertValueSql(items)
.parallelSql()
.queryString
proc insertValuesBuilder*(self: SurrealQuery, items: openArray[JsonNode]): string =
return self
.insertSql()
.insertValuesSql(items)
.parallelSql()
.queryString
# ==================== UPDATE ====================
proc updateBuilder*(self:SurrealQuery, items:JsonNode):string =
return self
.updateSql()
.updateValuesSql(items)
.whereSql()
.orWhereSql()
.limitSql()
.startSql()
.parallelSql()
.queryString
proc updateMergeBuilder*(self:SurrealQuery, id:string, items:JsonNode):string =
return self
.updateMergeSql(id, items)
# .whereSql()
# .orWhereSql()
# .limitSql()
# .startSql()
.parallelSql()
.queryString
# ==================== DELETE ====================
proc deleteBuilder*(self: SurrealQuery): string =
return self
.deleteSql()
.fromSql()
.whereSql()
.orWhereSql()
.limitSql()
.startSql()
.queryString
proc deleteByIdBuilder*(self: SurrealQuery, id: string): string =
return self
.deleteSql()
.deleteByIdSql(id)
.queryString
# ==================== Aggregates ====================
proc countBuilder*(self:SurrealQuery): string =
let sql = self
.selectCountSql()
.fromSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
#.havingSql()
.orderBySql()
.limitSql()
.startSql()
.fetchSql()
.parallelSql()
.queryString
return sql & " GROUP ALL"
proc selectAvgBuilder*(self:SurrealQuery, column:string): string =
return self
.selectAvgSql(column)
.fromSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
#.havingSql()
.orderBySql()
.limitSql()
.startSql()
.fetchSql()
.parallelSql()
.groupAllSql()
.queryString
proc selectSumBuilder*(self:SurrealQuery, column:string): string =
return self
.selectSumSql(column)
.fromSql()
.whereSql()
.orWhereSql()
.whereBetweenSql()
.whereNotBetweenSql()
.whereInSql()
.whereNotInSql()
.whereNullSql()
.groupBySql()
#.havingSql()
.orderBySql()
.limitSql()
.startSql()
.fetchSql()
.parallelSql()
.groupAllSql()
.queryString
@@ -0,0 +1,391 @@
import std/json
import std/strformat
import std/strutils
# import ../databases/surreal_lib
import ../surreal_types
# proc quoteTable*(input:var string) =
# input = &"`{input}`"
proc quote*(input:string):string =
## "User.id as userId" => `User`.`id` as `userId`
##
## "count(id) as count" => count(`id`) as `count`
##
## "math::max(id) as maxId" => math::max(`id`) as `maxId`
##
var tmp = newSeq[string]()
for segment in input.split("."):
if segment.contains(" as "):
# Split once on ' as ' to separate the expression and its alias
let parts = segment.split(" as ", maxsplit = 1)
let expression = parts[0]
let alias = parts[1]
if expression.contains("("):
# Extract function name and column name using index-based slicing
let funcStart = expression.find('(')
let funcEnd = expression.find(')', funcStart)
let funcName = expression[0 ..< funcStart]
let columnName = expression[funcStart + 1 ..< funcEnd]
if columnName.len > 0:
tmp.add(&"{funcName}(`{columnName}`) as `{alias}`")
else:
tmp.add(&"{funcName}() as `{alias}`")
else:
# Quote the expression and alias
tmp.add(&"`{expression}` as `{alias}`")
elif segment.contains("("):
# Leave functions without alias as is
tmp.add(segment)
else:
# Quote standalone identifiers
tmp.add(&"`{segment}`")
return tmp.join(".")
# ==================== SELECT ====================
proc selectSql*(self: SurrealQuery): SurrealQuery =
var queryString = "SELECT"
if self.query.hasKey("select"):
for i, item in self.query["select"].getElems():
if i > 0: queryString.add(",")
var column = item.getStr()
column = quote(column)
queryString.add(&" {column}")
else:
queryString.add(" *")
self.queryString = queryString
return self
proc fromSql*(self: SurrealQuery): SurrealQuery =
var table = self.query["table"].getStr()
table = quote(table)
self.queryString.add(&" FROM {table}")
return self
proc maxFromSql*(self:SurrealQuery, column:string):SurrealQuery =
var table = self.query["table"].getStr()
table = quote(table)
self.queryString.add(&" FROM array::max({column})")
return self
proc selectFirstSql*(self:SurrealQuery): SurrealQuery =
self.queryString.add(" LIMIT 1")
return self
proc selectByIdSql*(self:SurrealQuery, id:SurrealId, key:string): SurrealQuery =
var key = key
key = quote(key)
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND {key} = ? LIMIT 1")
self.placeHolder.add(%id.rawId)
else:
self.queryString.add(&" WHERE {key} = ? LIMIT 1")
self.placeHolder.add(%id.rawId)
return self
proc whereSql*(self: SurrealQuery): SurrealQuery =
if self.query.hasKey("where"):
for i, row in self.query["where"].getElems():
var column = row["column"].getStr()
column = quote(column)
var symbol = row["symbol"].getStr()
var value = row["value"]
if i == 0:
self.queryString.add(&" WHERE {column} {symbol} ?")
self.placeHolder.add(value)
else:
self.queryString.add(&" AND {column} {symbol} ?")
self.placeHolder.add(value)
return self
proc orWhereSql*(self: SurrealQuery): SurrealQuery =
if self.query.hasKey("or_where"):
for row in self.query["or_where"]:
var column = row["column"].getStr()
column = quote(column)
var symbol = row["symbol"].getStr()
var value = row["value"]
if self.queryString.contains("WHERE"):
self.queryString.add(&" OR {column} {symbol} ?")
self.placeHolder.add(value)
else:
self.queryString.add(&" WHERE {column} {symbol} ?")
self.placeHolder.add(value)
return self
proc whereBetweenSql*(self: SurrealQuery): SurrealQuery =
if self.query.hasKey("where_between"):
for row in self.query["where_between"]:
var column = row["column"].getStr()
column = quote(column)
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND ? <= {column} AND {column} <= ?")
else:
self.queryString.add(&" WHERE ? <= {column} AND {column} <= ?")
self.placeHolder.add(row["width"][0])
self.placeHolder.add(row["width"][1])
return self
proc whereNotBetweenSql*(self: SurrealQuery): SurrealQuery =
if self.query.hasKey("where_not_between"):
for row in self.query["where_not_between"]:
var column = row["column"].getStr()
column = quote(column)
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND ? > {column} AND {column} < ?")
else:
self.queryString.add(&" WHERE ? > {column} AND {column} < ?")
self.placeHolder.add(row["width"][0])
self.placeHolder.add(row["width"][1])
return self
proc whereInSql*(self: SurrealQuery): SurrealQuery =
if self.query.hasKey("where_in"):
for row in self.query["where_in"]:
var column = row["column"].getStr()
column = quote(column)
var values = ""
var i = 0
for row in row["width"].items:
defer: i.inc()
if i > 0: values.add(", ")
values.add("?")
self.placeHolder.add(row)
values.add("")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND [{values}] CONTAINS {column}")
else:
self.queryString.add(&" WHERE [{values}] CONTAINS {column}")
return self
proc whereNotInSql*(self: SurrealQuery): SurrealQuery =
if self.query.hasKey("where_not_in"):
for row in self.query["where_not_in"]:
var column = row["column"].getStr()
column = quote(column)
var values = ""
var i = 0
for row in row["width"].items:
defer: i.inc()
if i > 0: values.add(", ")
values.add("?")
self.placeHolder.add(row)
values.add("")
if self.queryString.contains("WHERE"):
self.queryString.add(&" AND [{values}] CONTAINSNOT {column}")
else:
self.queryString.add(&" WHERE [{values}] CONTAINSNOT {column}")
return self
proc whereNullSql*(self: SurrealQuery): SurrealQuery =
if self.query.hasKey("where_null"):
for row in self.query["where_null"]:
var column = row["column"].getStr()
column = quote(column)
if self.queryString.contains("WHERE"):
self.queryString.add(&" OR {column} IS NONE")
else:
self.queryString.add(&" WHERE {column} IS NONE")
return self
proc groupBySql*(self:SurrealQuery): SurrealQuery =
if self.query.hasKey("group_by"):
for row in self.query["group_by"]:
var column = row["column"].getStr()
column = quote(column)
if self.queryString.contains("GROUP BY"):
self.queryString.add(&", {column}")
else:
self.queryString.add(&" GROUP BY {column}")
return self
proc orderBySql*(self:SurrealQuery): SurrealQuery =
if self.query.hasKey("order_by"):
for row in self.query["order_by"]:
var column = row["column"].getStr()
column = quote(column)
let collation = row["collation"].getStr()
let order = row["order"].getStr()
if self.queryString.contains("ORDER BY"):
self.queryString.add(&", {column} {collation} {order}")
else:
self.queryString.add(&" ORDER BY {column} {collation} {order}")
return self
proc limitSql*(self: SurrealQuery): SurrealQuery =
if self.query.hasKey("limit"):
var num = self.query["limit"].getInt()
self.queryString.add(&" LIMIT {num}")
return self
proc startSql*(self: SurrealQuery): SurrealQuery =
if self.query.hasKey("start"):
var num = self.query["start"].getInt()
self.queryString.add(&" START {num}")
return self
proc fetchSql*(self:SurrealQuery):SurrealQuery =
if self.query.hasKey("fetch"):
self.queryString.add(" FETCH")
for i, item in self.query["fetch"].getElems():
if i > 0: self.queryString.add(",")
var column = item.getStr()
column = quote(column)
self.queryString.add(&" {column}")
return self
proc parallelSql*(self:SurrealQuery):SurrealQuery =
if self.query.hasKey("parallel") and self.query["parallel"].getBool():
self.queryString.add(" PARALLEL")
return self
# ==================== INSERT ====================
proc insertSql*(self: SurrealQuery): SurrealQuery =
var table = self.query["table"].getStr()
table = quote(table)
self.queryString = &"INSERT INTO {table}"
return self
proc insertValueSql*(self: SurrealQuery, items: JsonNode): SurrealQuery =
self.queryString.add(" {")
var i = 0
for (key, item) in items.pairs:
defer: i.inc()
if i > 0: self.queryString.add(", ")
self.queryString.add(&"{key}: ?")
self.placeHolder.add(item)
self.queryString.add("}")
return self
proc insertValuesSql*(self: SurrealQuery, rows: openArray[JsonNode]): SurrealQuery =
self.queryString.add(" [")
for i, row in rows:
if i > 0: self.queryString.add(", ")
var j = 0
self.queryString.add("{")
for (key, item) in row.pairs:
defer: j.inc()
if j > 0: self.queryString.add(", ")
self.queryString.add(&"{key}: ?")
self.placeHolder.add(item)
self.queryString.add("}")
self.queryString.add("]")
return self
# ==================== UPDATE ====================
proc updateSql*(self: SurrealQuery): SurrealQuery =
var queryString = ""
queryString.add("UPDATE")
var table = self.query["table"].getStr()
table = quote(table)
queryString.add(&" {table} SET ")
self.queryString = queryString
return self
proc updateValuesSql*(self: SurrealQuery, items:JsonNode): SurrealQuery =
var i = 0
for key, val in items.pairs:
defer: i.inc()
if i > 0: self.queryString.add(", ")
self.queryString.add(&"{key} = ?")
self.placeHolder.add(val)
return self
proc updateMergeSql*(self: SurrealQuery, id:string, items:JsonNode):SurrealQuery =
self.queryString = &"UPDATE {id} MERGE {items}"
return self
# ==================== DELETE ====================
proc deleteSql*(self: SurrealQuery): SurrealQuery =
self.queryString = "DELETE"
return self
proc deleteByIdSql*(self: SurrealQuery, id: string): SurrealQuery =
self.queryString.add(&" ?")
self.placeHolder.add(%id)
return self
# ==================== Aggregates ====================
proc selectCountSql*(self: SurrealQuery): SurrealQuery =
var queryString =
if self.query.hasKey("select"):
var column = self.query["select"][0].getStr
column = quote(column)
&"{column}"
else:
""
self.queryString = &"SELECT count({queryString}) AS total"
return self
proc selectAvgSql*(self: SurrealQuery, column:string): SurrealQuery =
var column = column
column = quote(column)
self.queryString = &"SELECT math::mean({column}) AS avg"
return self
proc selectSumSql*(self: SurrealQuery, column:string): SurrealQuery =
var column = column
column = quote(column)
self.queryString = &"SELECT math::sum({column}) AS sum"
return self
proc groupAllSql*(self: SurrealQuery):SurrealQuery =
self.queryString.add(" GROUP ALL")
return self
@@ -0,0 +1,982 @@
import std/asyncdispatch
import std/deques
import std/json
import std/monotimes
import std/options
import std/sequtils
import std/strformat
import std/strutils
import std/tables
import std/times
import ../../error
import ../../libs/surreal/surreal_lib
import ../../libs/surreal/surreal_impl
import ../../log
import ../../enums
import ../database_types
import ../../prepared_param
import ./query/surreal_builder
import ./surreal_types
import ./surreal_query
# ================================================================================
# connection
# ================================================================================
proc removePoolWaiter(pools: Connections, w: Future[void]) =
var kept = initDeque[Future[void]]()
while pools.waiters.len > 0:
let x = pools.waiters.popFirst()
if x != w:
kept.addLast(x)
pools.waiters = move(kept)
proc wakeOnePoolWaiter(pools: Connections) =
while pools.waiters.len > 0:
let w = pools.waiters.popFirst()
if w.finished:
continue
w.complete()
break
proc surrealPoolRemainingMs(deadline: MonoTime): int =
let left = (deadline - getMonoTime()).inMilliseconds
if left <= 0:
return 0
if left > int64(high(int)):
return high(int)
result = int(left)
if result < 1:
result = 1
proc getFreeConn(self: SurrealConnections | SurrealQuery | RawSurrealQuery): Future[int] {.async.} =
let deadline = getMonoTime() + initDuration(seconds = self.pools.timeout)
while true:
for i in 0 ..< self.pools.conns.len:
if not self.pools.conns[i].isBusy:
self.pools.conns[i].isBusy = true
when defined(check_pool):
echo "=== getFreeConn ", i
return i
if getMonoTime() >= deadline:
return errorConnectionNum
let w = newFuture[void]("getFreeConn.poolWait")
self.pools.waiters.addLast(w)
var ms = surrealPoolRemainingMs(deadline)
if ms < 1:
ms = 1
let ok = await withTimeout(w, ms)
if not ok:
removePoolWaiter(self.pools, w)
return errorConnectionNum
proc returnConn(self: SurrealConnections | SurrealQuery | RawSurrealQuery, i: int) {.async.} =
if i != errorConnectionNum:
self.pools.conns[i].isBusy = false
wakeOnePoolWaiter(self.pools)
proc raisePoolTimeout(self: SurrealConnections | SurrealQuery | RawSurrealQuery | SurrealPreparedStatement) {.noreturn.} =
raise newException(DbError, "Timed out while waiting for a free SurrealDB connection")
proc touchStmtEntry(entry: SurrealPreparedEntry) =
entry.lastUsedAt = getTime().toUnix()
proc mustBeOpen(self: SurrealPreparedStatement) =
if self.isNil or self.owner.isNil or self.isClosed:
raise newException(DbError, "SurrealDB prepared statement is already closed")
proc hasPreparedEntry(cache: Table[string, SurrealPreparedEntry], sql: string): bool =
for key in cache.keys:
if key == sql:
return true
return false
proc getStmtEntry(self: SurrealConnections, sql: string): SurrealPreparedEntry =
if hasPreparedEntry(self.pools.preparedCache, sql):
return self.pools.preparedCache[sql]
let entry = SurrealPreparedEntry(
sql: sql,
normalizedSql: sql.questionToDaller(),
nArgs: countQuestionMarks(sql),
refCount: 0,
lastUsedAt: getTime().toUnix(),
)
self.pools.preparedCache[sql] = entry
return entry
proc prepare*(self: SurrealConnections, sql: string): SurrealPreparedStatement =
let entry = self.getStmtEntry(sql)
entry.refCount += 1
touchStmtEntry(entry)
new(result)
result.owner = self
result.entry = entry
result.sql = sql
result.nArgs = entry.nArgs
result.isClosed = false
proc withConn*(
self: SurrealConnections,
body: proc (ctx: SurrealPreparedContext): Future[void]
) {.async.} =
let connI = getFreeConn(self).await
if connI == errorConnectionNum:
raisePoolTimeout(self)
defer:
self.returnConn(connI).await
let ctx = SurrealPreparedContext(owner: self, connI: connI)
await body(ctx)
proc verifyCtx(self: SurrealPreparedStatement, ctx: SurrealPreparedContext) =
self.mustBeOpen()
if ctx.isNil:
raise newException(DbError, "SurrealDB prepared context is nil")
if ctx.owner.isNil or ctx.owner != self.owner:
raise newException(DbError, "SurrealDB prepared context owner mismatch")
if ctx.connI < 0 or ctx.connI >= self.owner.pools.conns.len:
raise newException(DbError, "SurrealDB prepared context has invalid connection index")
proc toPreparedArgsJson(args: seq[string]): JsonNode =
result = newJArray()
for arg in args:
if arg == "NULL" or arg == "null":
result.add(newJNull())
else:
result.add(%arg)
proc buildPreparedSql(self: SurrealPreparedStatement, args: JsonNode): string =
self.mustBeOpen()
touchStmtEntry(self.entry)
result = dbFormatPrepared(self.entry.normalizedSql, args)
proc getPreparedRowsOnConn(
self: SurrealPreparedStatement,
connI: int,
args: JsonNode
): Future[seq[JsonNode]] {.async.} =
self.mustBeOpen()
if connI < 0 or connI >= self.owner.pools.conns.len:
raise newException(DbError, "SurrealDB prepared statement received an invalid connection index")
let sql = self.buildPreparedSql(args)
let rows = surreal_impl.query(
self.owner.pools.conns[connI].conn,
sql,
newJArray(),
self.owner.pools.timeout
).await
if rows.kind != JArray or rows.len == 0:
return newSeq[JsonNode](0)
return rows.getElems()
proc getPreparedRowOnConn(
self: SurrealPreparedStatement,
connI: int,
args: JsonNode
): Future[Option[JsonNode]] {.async.} =
let rows = await self.getPreparedRowsOnConn(connI, args)
if rows.len == 0:
return none(JsonNode)
return rows[0].some
proc execPreparedOnConn(
self: SurrealPreparedStatement,
connI: int,
args: JsonNode
) {.async.} =
self.mustBeOpen()
if connI < 0 or connI >= self.owner.pools.conns.len:
raise newException(DbError, "SurrealDB prepared statement received an invalid connection index")
let sql = self.buildPreparedSql(args)
await surreal_impl.exec(
self.owner.pools.conns[connI].conn,
sql,
newJArray(),
self.owner.pools.timeout
)
proc getPreparedRows(self: SurrealPreparedStatement, args: JsonNode): Future[seq[JsonNode]] {.async.} =
let connI = await getFreeConn(self.owner)
if connI == errorConnectionNum:
raisePoolTimeout(self)
defer:
await self.owner.returnConn(connI)
return await self.getPreparedRowsOnConn(connI, args)
proc getPreparedRows(self: SurrealPreparedStatement, ctx: SurrealPreparedContext, args: JsonNode): Future[seq[JsonNode]] {.async.} =
self.verifyCtx(ctx)
return await self.getPreparedRowsOnConn(ctx.connI, args)
proc getPreparedRow(self: SurrealPreparedStatement, args: JsonNode): Future[Option[JsonNode]] {.async.} =
let rows = await self.getPreparedRows(args)
if rows.len == 0:
return none(JsonNode)
return rows[0].some
proc getPreparedRow(self: SurrealPreparedStatement, ctx: SurrealPreparedContext, args: JsonNode): Future[Option[JsonNode]] {.async.} =
self.verifyCtx(ctx)
return await self.getPreparedRowOnConn(ctx.connI, args)
proc execPrepared(self: SurrealPreparedStatement, args: JsonNode) {.async.} =
let connI = await getFreeConn(self.owner)
if connI == errorConnectionNum:
raisePoolTimeout(self)
defer:
await self.owner.returnConn(connI)
await self.execPreparedOnConn(connI, args)
proc execPrepared(self: SurrealPreparedStatement, ctx: SurrealPreparedContext, args: JsonNode) {.async.} =
self.verifyCtx(ctx)
await self.execPreparedOnConn(ctx.connI, args)
# ================================================================================
# toJson
# ================================================================================
# proc toJson(results:openArray[seq[string]], dbRows:DbRows):seq[JsonNode] =
# var response_table = newSeq[JsonNode](results.len)
# for index, rows in results.pairs:
# var response_row = newJObject()
# for i, row in rows:
# let key = dbRows[index][i].name
# let typ = dbRows[index][i].typ.kind
# # let kindName = dbRows[index][i].typ.name
# # let size = dbRows[index][i].typ.size
# if typ == dbNull:
# response_row[key] = newJNull()
# elif [dbInt, dbUInt].contains(typ):
# response_row[key] = newJInt(row.parseInt)
# elif [dbDecimal, dbFloat].contains(typ):
# response_row[key] = newJFloat(row.parseFloat)
# elif [dbBool].contains(typ):
# if row == "f":
# response_row[key] = newJBool(false)
# elif row == "t":
# response_row[key] = newJBool(true)
# elif [dbJson].contains(typ):
# response_row[key] = row.parseJson
# elif [dbFixedChar, dbVarchar].contains(typ):
# if row == "NULL":
# response_row[key] = newJNull()
# else:
# response_row[key] = newJString(row)
# else:
# response_row[key] = newJString(row)
# response_table[index] = response_row
# return response_table
# ================================================================================
# private exec
# ================================================================================
proc getAllRows(self:SurrealQuery, queryString:string):Future[seq[JsonNode]] {.async.} =
var connI = getFreeConn(self).await
defer:
self.returnConn(connI).await
if connI == errorConnectionNum:
return
let rows = surreal_impl.query(
self.pools.conns[connI].conn,
queryString,
self.placeHolder,
self.pools.timeout
).await
if rows.len == 0:
self.log.echoErrorMsg(queryString)
return newSeq[JsonNode](0)
return rows.getElems() # seq[JsonNode]
proc getRow(self:SurrealQuery, queryString:string):Future[Option[JsonNode]] {.async.} =
var connI = getFreeConn(self).await
defer:
self.returnConn(connI).await
if connI == errorConnectionNum:
return
let rows = surreal_impl.query(self.pools.conns[connI].conn, queryString, self.placeHolder, self.pools.timeout).await
if rows.len == 0:
return none(JsonNode)
else:
return rows[0].some
proc exec(self:SurrealQuery, queryString:string) {.async.} =
## args is `JObject`
var connI = getFreeConn(self).await
defer:
self.returnConn(connI).await
if connI == errorConnectionNum:
return
surreal_impl.exec(self.pools.conns[connI].conn, queryString, self.placeHolder, self.pools.timeout).await
# proc insertId(self:SurrealQuery, queryString:string, key:string):Future[string] {.async.} =
# var connI = self.transactionConn
# if not self.isInTransaction:
# connI = getFreeConn(self).await
# defer:
# if not self.isInTransaction:
# self.returnConn(connI).await
# if connI == errorConnectionNum:
# return
# let table = self.query["table"].getStr
# let columnGetQuery = &"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '{table}'"
# let (columns, _) = surreal_impl.query(self.pools.conns[connI].conn, columnGetQuery, newJArray(), self.pools.timeout).await
# let (rows, _) = surreal_impl.execGetValue(self.pools.conns[connI].conn, queryString, self.placeHolder, columns, self.pools.timeout).await
# return rows[0][0]
proc getAllRows(self:RawSurrealQuery, queryString:string):Future[seq[JsonNode]] {.async.} =
var connI = getFreeConn(self).await
defer:
self.returnConn(connI).await
if connI == errorConnectionNum:
return
let rows = surreal_impl.query(
self.pools.conns[connI].conn,
queryString,
self.placeHolder,
self.pools.timeout
).await
if rows.len == 0:
self.log.echoErrorMsg(queryString)
return newSeq[JsonNode](0)
return rows.getElems()
# proc getAllRowsPlain(self:RawSurrealQuery, queryString:string, args:JsonNode):Future[seq[seq[string]]] {.async.} =
# var connI = self.transactionConn
# if not self.isInTransaction:
# connI = getFreeConn(self).await
# defer:
# if not self.isInTransaction:
# self.returnConn(connI).await
# if connI == errorConnectionNum:
# return
# let queryString = queryString.questionToDaller()
# let (rows, _) = surreal_impl.rawQuery(
# self.pools.conns[connI].conn,
# queryString,
# self.placeHolder,
# self.pools.timeout
# ).await
# return rows
proc getRow(self:RawSurrealQuery, queryString:string):Future[Option[JsonNode]] {.async.} =
var connI = getFreeConn(self).await
defer:
self.returnConn(connI).await
if connI == errorConnectionNum:
return
let rows = surreal_impl.query(self.pools.conns[connI].conn, queryString, self.placeHolder, self.pools.timeout).await
if rows.len == 0:
return none(JsonNode)
else:
return rows[^1].some
# proc getRowPlain(self:RawSurrealQuery, queryString:string, args:JsonNode):Future[seq[string]] {.async.} =
# var connI = self.transactionConn
# if not self.isInTransaction:
# connI = getFreeConn(self).await
# defer:
# if not self.isInTransaction:
# self.returnConn(connI).await
# if connI == errorConnectionNum:
# return
# let queryString = queryString.questionToDaller()
# let (rows, _) = surreal_impl.rawQuery(
# self.pools.conns[connI].conn,
# queryString,
# self.placeHolder,
# self.pools.timeout
# ).await
# return rows[0]
proc exec(self:RawSurrealQuery, queryString:string) {.async.} =
let connI = getFreeConn(self).await
defer:
self.returnConn(connI).await
if connI == errorConnectionNum:
return
surreal_impl.exec(
self.pools.conns[connI].conn,
queryString,
self.placeHolder,
self.pools.timeout
).await
proc info(self:RawSurrealQuery, queryString:string):Future[JsonNode] {.async.} =
let connI = getFreeConn(self).await
defer:
self.returnConn(connI).await
if connI == errorConnectionNum:
return
return surreal_impl.info(
self.pools.conns[connI].conn,
queryString,
self.placeHolder,
self.pools.timeout
).await
proc column(self:SurrealQuery, queryString:string):Future[JsonNode] {.async.} =
var connI = getFreeConn(self).await
defer:
self.returnConn(connI).await
if connI == errorConnectionNum:
return
var strArgs:seq[string]
for arg in self.placeHolder.items:
case arg["value"].kind
of JBool:
if arg["value"].getBool:
strArgs.add("1")
else:
strArgs.add("0")
of JInt:
strArgs.add($arg["value"].getInt)
of JFloat:
strArgs.add($arg["value"].getFloat)
of JString:
strArgs.add($arg["value"].getStr)
of JNull:
strArgs.add("NULL")
else:
strArgs.add(arg["value"].pretty)
return surreal_impl.info(self.pools.conns[connI].conn, queryString, strArgs, self.pools.timeout).await
# ================================================================================
# prepared statement cache
# ================================================================================
proc close*(self: SurrealPreparedStatement) {.async.} =
if self.isNil or self.isClosed:
return
self.isClosed = true
if not self.entry.isNil and self.entry.refCount > 0:
self.entry.refCount -= 1
touchStmtEntry(self.entry)
proc removePreparedEntry(self: SurrealConnections, sql: string) =
if not self.pools.preparedCache.hasKey(sql):
return
self.pools.preparedCache.del(sql)
proc flushStmt*(self: SurrealConnections, stmt: SurrealPreparedStatement) {.async.} =
if stmt.isNil:
return
let sql = stmt.sql
await stmt.close()
self.removePreparedEntry(sql)
proc clearStmtCache*(self: SurrealConnections) {.async.} =
let keys = toSeq(self.pools.preparedCache.keys)
for sql in keys:
self.removePreparedEntry(sql)
# ================================================================================
# public prepared exec
# ================================================================================
proc get*(self: SurrealPreparedStatement, args: seq[string]): Future[seq[JsonNode]] {.async.} =
try:
self.owner.log.logger(self.sql)
return await self.getPreparedRows(toPreparedArgsJson(args))
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc get*(self: SurrealPreparedStatement, ctx: SurrealPreparedContext, args: seq[string]): Future[seq[JsonNode]] {.async.} =
try:
self.owner.log.logger(self.sql)
return await self.getPreparedRows(ctx, toPreparedArgsJson(args))
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc get*(self: SurrealPreparedStatement, args: JsonNode): Future[seq[JsonNode]] {.async.} =
try:
self.owner.log.logger(self.sql)
return await self.getPreparedRows(args)
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc get*(self: SurrealPreparedStatement, ctx: SurrealPreparedContext, args: JsonNode): Future[seq[JsonNode]] {.async.} =
try:
self.owner.log.logger(self.sql)
return await self.getPreparedRows(ctx, args)
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc first*(self: SurrealPreparedStatement, args: seq[string]): Future[Option[JsonNode]] {.async.} =
try:
self.owner.log.logger(self.sql)
return await self.getPreparedRow(toPreparedArgsJson(args))
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc first*(self: SurrealPreparedStatement, ctx: SurrealPreparedContext, args: seq[string]): Future[Option[JsonNode]] {.async.} =
try:
self.owner.log.logger(self.sql)
return await self.getPreparedRow(ctx, toPreparedArgsJson(args))
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc first*(self: SurrealPreparedStatement, args: JsonNode): Future[Option[JsonNode]] {.async.} =
try:
self.owner.log.logger(self.sql)
return await self.getPreparedRow(args)
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc first*(self: SurrealPreparedStatement, ctx: SurrealPreparedContext, args: JsonNode): Future[Option[JsonNode]] {.async.} =
try:
self.owner.log.logger(self.sql)
return await self.getPreparedRow(ctx, args)
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc exec*(self: SurrealPreparedStatement, args: seq[string]) {.async.} =
try:
self.owner.log.logger(self.sql)
await self.execPrepared(toPreparedArgsJson(args))
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc exec*(self: SurrealPreparedStatement, ctx: SurrealPreparedContext, args: seq[string]) {.async.} =
try:
self.owner.log.logger(self.sql)
await self.execPrepared(ctx, toPreparedArgsJson(args))
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc exec*(self: SurrealPreparedStatement, args: JsonNode) {.async.} =
try:
self.owner.log.logger(self.sql)
await self.execPrepared(args)
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
proc exec*(self: SurrealPreparedStatement, ctx: SurrealPreparedContext, args: JsonNode) {.async.} =
try:
self.owner.log.logger(self.sql)
await self.execPrepared(ctx, args)
except CatchableError:
self.owner.log.echoErrorMsg(self.sql)
self.owner.log.echoErrorMsg(getCurrentExceptionMsg())
raise getCurrentException()
# proc transactionStart(self:SurrealConnections) {.async.} =
# let connI = getFreeConn(self).await
# if connI == errorConnectionNum:
# return
# self.isInTransaction = true
# self.transactionConn = connI
# surreal_impl.exec(self.pools.conns[connI].conn, "BEGIN", newJArray(), newSeq[Row](), self.pools.timeout).await
# proc transactionEnd(self:SurrealConnections, query:string) {.async.} =
# defer:
# self.returnConn(self.transactionConn).await
# self.transactionConn = 0
# self.isInTransaction = false
# surreal_impl.exec(self.pools[self.transactionConn].conn, query, newJArray(), newSeq[Row](), self.pools.timeout).await
# ================================================================================
# public exec
# ================================================================================
proc get*(self: SurrealQuery):Future[seq[JsonNode]] {.async.} =
## https://surrealdb.com/docs/surrealql/statements/select
let sql = self.selectBuilder()
try:
self.log.logger(sql)
return self.getAllRows(sql).await
except CatchableError:
self.log.echoErrorMsg(sql)
self.log.echoErrorMsg( getCurrentExceptionMsg() )
return newSeq[JsonNode]()
proc first*(self: SurrealQuery):Future[Option[JsonNode]] {.async.} =
var sql = self.selectFirstBuilder()
try:
self.log.logger(sql)
return self.getRow(sql).await
except CatchableError:
self.log.echoErrorMsg(sql)
self.log.echoErrorMsg( getCurrentExceptionMsg() )
return none(JsonNode)
proc find*(self: SurrealQuery, id:SurrealId, key="id"):Future[Option[JsonNode]] {.async.} =
var sql = self.selectFindBuilder(id, key)
sql = questionToDaller(sql)
try:
self.log.logger(sql)
return self.getRow(sql).await
except CatchableError:
self.log.echoErrorMsg(sql)
self.log.echoErrorMsg( getCurrentExceptionMsg() )
raise getCurrentException()
# ==================== insert JsonNode ====================
proc insert*(self:SurrealQuery, items:JsonNode) {.async.} =
## https://surrealdb.com/docs/surrealql/statements/insert
let sql = self.insertValueBuilder(items)
self.log.logger(sql)
self.exec(sql).await
proc insert*(self:SurrealQuery, items:seq[JsonNode]) {.async.} =
## https://surrealdb.com/docs/surrealql/statements/insert
var sql = self.insertValuesBuilder(items)
self.log.logger(sql)
self.exec(sql).await
proc insertId*(self:SurrealQuery, items:JsonNode, key="id"):Future[SurrealId] {.async.} =
## https://surrealdb.com/docs/surrealql/statements/insert
var sql = self.insertValueBuilder(items) & " RETURN AFTER"
self.log.logger(sql)
let res = self.getRow(sql).await
return SurrealId.new(res.get()[key].getStr)
proc insertId*(self: SurrealQuery, items: seq[JsonNode], key="id"):Future[seq[SurrealId]] {.async.} =
result = newSeq[SurrealId](items.len)
var sql = self.insertValuesBuilder(items) & " RETURN AFTER"
self.log.logger(sql)
let res = self.getAllRows(sql).await
var i = 0
for row in res.items:
defer: i.inc()
result[i] = SurrealId.new(row[key].getStr)
# ==================== insert Object ====================
proc insert*[T](self:SurrealQuery, items:T) {.async.} =
## https://surrealdb.com/docs/surrealql/statements/insert
let sql = self.insertValueBuilder(%items)
self.log.logger(sql)
self.exec(sql).await
proc insert*[T](self:SurrealQuery, items:seq[T]) {.async.} =
## https://surrealdb.com/docs/surrealql/statements/insert
var jsonItems = newSeq[JsonNode](items.len)
for i, item in items:
jsonItems[i] = %item
var sql = self.insertValuesBuilder(jsonItems)
self.log.logger(sql)
self.exec(sql).await
proc insertId*[T](self:SurrealQuery, items:T, key="id"):Future[SurrealId] {.async.} =
## https://surrealdb.com/docs/surrealql/statements/insert
var sql = self.insertValueBuilder(%items) & " RETURN AFTER"
self.log.logger(sql)
let res = self.getRow(sql).await
return SurrealId.new(res.get()[key].getStr)
proc insertId*[T](self: SurrealQuery, items: seq[T], key="id"):Future[seq[SurrealId]] {.async.} =
result = newSeq[SurrealId](items.len)
var jsonItems = newSeq[JsonNode](items.len)
for i, item in items:
jsonItems[i] = %item
var sql = self.insertValuesBuilder(jsonItems) & " RETURN AFTER"
self.log.logger(sql)
let res = self.getAllRows(sql).await
var i = 0
for row in res.items:
defer: i.inc()
result[i] = SurrealId.new(row[key].getStr)
proc update*(self: SurrealQuery, items: JsonNode){.async.} =
## https://surrealdb.com/docs/surrealql/statements/update
var sql = self.updateBuilder(items)
self.log.logger(sql)
self.exec(sql).await
proc update*(self:SurrealConnections, id:SurrealId, items:JsonNode) {.async.} =
## https://surrealdb.com/docs/surrealql/statements/update
let surrealQuery = SurrealQuery.new(
self.log,
self.pools,
newJObject()
)
let sql = surrealQuery.updateMergeBuilder(id.rawid, items)
surrealQuery.log.logger(sql)
surrealQuery.exec(sql).await
proc update*[T](self: SurrealQuery, items: T){.async.} =
## https://surrealdb.com/docs/surrealql/statements/update
var sql = self.updateBuilder(%items)
self.log.logger(sql)
self.exec(sql).await
proc update*[T](self:SurrealConnections, id:SurrealId, items:T) {.async.} =
## https://surrealdb.com/docs/surrealql/statements/update
let surrealQuery = SurrealQuery.new(
self.log,
self.pools,
newJObject()
)
let sql = surrealQuery.updateMergeBuilder(id.rawid, %items)
surrealQuery.log.logger(sql)
surrealQuery.exec(sql).await
proc delete*(self: SurrealQuery){.async.} =
## https://surrealdb.com/docs/surrealql/statements/delete
let sql = self.deleteBuilder()
self.log.logger(sql)
self.exec(sql).await
proc delete*(self: SurrealQuery, id: SurrealId){.async.} =
## https://surrealdb.com/docs/surrealql/statements/delete
let sql = self.deleteByIdBuilder(id.rawId)
self.log.logger(sql)
self.exec(sql).await
proc columns*(self: SurrealQuery):Future[seq[string]] {.async.} =
let tableName = self.query["table"].getStr
let sql = &"INFO FOR TABLE `{tableName}`"
try:
self.log.logger(sql)
let resp = self.column(sql).await
var columns:seq[string]
for (key, value) in resp[0]["result"]["fields"].pairs:
columns.add(key)
return columns
except CatchableError:
self.log.echoErrorMsg(sql)
self.log.echoErrorMsg( getCurrentExceptionMsg() )
return @[]
proc count*(self:SurrealQuery):Future[int] {.async.} =
let sql = self.countBuilder()
self.log.logger(sql)
let response = self.getRow(sql).await
if response.isSome:
return response.get["total"].getInt()
else:
return 0
proc min*(self:SurrealQuery, column:string, collaction:Collation=None):Future[string] {.async.} =
## = `ORDER BY {column} {collaction} ASC LIMIT 1`
let self = self.orderBy(column, collaction, Asc).limit(1)
let sql = self.selectFirstBuilder()
self.log.logger(sql)
let response = self.getRow(sql).await
if response.isSome():
let column = if column.contains("."): column.split(".")[^1] else: column
let value = response.get()[column]
case value.kind
of JString:
return $value.getStr()
else:
return $value
else:
return ""
proc max*(self:SurrealQuery, column:string, collaction:Collation=None):Future[string]{.async.} =
## = `ORDER BY {column} {collaction} DESC LIMIT 1`
let self = self.orderBy(column, collaction, Desc).limit(1)
let sql = self.selectFirstBuilder()
self.log.logger(sql)
let response = self.getRow(sql).await
if response.isSome():
let column = if column.contains("."): column.split(".")[^1] else: column
let value = response.get()[column]
case value.kind
of JString:
return $value.getStr()
else:
return $value
else:
return ""
proc avg*(self:SurrealQuery, column:string):Future[float]{.async.} =
var sql = self.selectAvgBuilder(column)
self.log.logger(sql)
let response = await self.getRow(sql)
if response.isSome:
let value = response.get["avg"]
case value.kind
of JInt:
return value.getInt.float
of JFloat:
return value.getFloat()
of JString:
return value.getStr().parseFloat()
else:
return 0.0
else:
return 0.0
proc sum*(self:SurrealQuery, column:string):Future[float]{.async.} =
var sql = self.selectSumBuilder(column)
self.log.logger(sql)
let response = await self.getRow(sql)
if response.isSome:
return response.get["sum"].getFloat()
else:
return 0.0
proc get*(self: RawSurrealQuery):Future[seq[JsonNode]] {.async.} =
## It is only used with raw()
self.log.logger(self.queryString)
return self.getAllRows(self.queryString).await
proc exec*(self: RawSurrealQuery) {.async.} =
## It is only used with raw()
##
## https://surrealdb.com/docs/integration/http#sql
##
## https://surrealdb.com/docs/surrealql
try:
self.log.logger(self.queryString)
self.exec(self.queryString).await
except CatchableError:
self.log.echoErrorMsg(self.queryString)
self.log.echoErrorMsg( getCurrentExceptionMsg() )
proc info*(self: RawSurrealQuery):Future[JsonNode] {.async.} =
## Get all response.
##
## https://surrealdb.com/docs/integration/http#sql
##
## https://surrealdb.com/docs/surrealql
try:
self.log.logger(self.queryString)
return self.info(self.queryString).await
except CatchableError:
self.log.echoErrorMsg(self.queryString)
self.log.echoErrorMsg( getCurrentExceptionMsg() )
proc first*(self: RawSurrealQuery):Future[Option[JsonNode]] {.async.} =
## It is only used with raw()
self.log.logger(self.queryString)
return self.getRow(self.queryString).await
template seeder*(rdb:SurrealConnections, tableName:string, body:untyped):untyped =
## The `seeder` block allows the code in the block to work only when the table is empty.
block:
if rdb.table(tableName).count().waitFor == 0:
body
template seeder*(rdb:SurrealConnections, tableName, column:string, body:untyped):untyped =
## The `seeder` block allows the code in the block to work only when the table or specified column is empty.
block:
if rdb.table(tableName).select(column).count().waitFor == 0:
body
@@ -0,0 +1,85 @@
import std/asyncdispatch
import std/deques
import std/httpclient
import std/httpcore
import std/strformat
import std/base64
import std/tables
import std/times
import ../../libs/surreal/surreal_rdb
import ../../error
import ../../log
import ./surreal_types
proc surrealTimeoutMs(timeout: int): int =
if timeout <= 0:
return 0
if timeout > high(int) div 1000:
return high(int)
result = timeout * 1000
proc awaitWithTimeout[T](fut: Future[T], timeoutMs: int, timeoutMsg: string): Future[T] {.async.} =
if timeoutMs <= 0:
return await fut
let ok = await withTimeout(fut, timeoutMs)
if not ok:
dbError(timeoutMsg)
result = fut.read
proc buildSurrealHeaders(namespace, database, authHeader: string): HttpHeaders =
result = newHttpHeaders(true)
result["Surreal-NS"] = namespace
result["Surreal-DB"] = database
result["Accept"] = "application/json"
result["Authorization"] = authHeader
proc dbOpen*(_:type SurrealDB, namespace:string = "", database: string = "", user: string = "", password: string = "",
host: string = "", port: int = 0, maxConnections: int = 1, timeout=30,
shouldDisplayLog=false, shouldOutputLogFile=false, logDir=""): Future[SurrealConnections] {.async.} =
let timeoutMs = surrealTimeoutMs(timeout)
let statusUrl = &"{host}:{port}/status"
let sqlUrl = &"{host}:{port}/sql"
let bootstrapSql = &"USE NS `{namespace}` DB `{database}`"
let authHeader = "Basic " & base64.encode(user & ":" & password)
var conns = newSeq[Connection](maxConnections)
for i in 0..<maxConnections:
let client = newAsyncHttpClient()
client.headers = buildSurrealHeaders(namespace, database, authHeader)
let statusResp = await awaitWithTimeout(client.get(statusUrl), timeoutMs,
&"Cannot connect to SurrealDb {host}:{port} (status request timed out)")
if statusResp.status != $Http200:
dbError(&"Cannot connect to SurrealDb {host}:{port}")
if i == 0:
let bootstrapResp = await awaitWithTimeout(client.post(sqlUrl, bootstrapSql), timeoutMs,
&"Cannot connect to SurrealDb {host}:{port} (bootstrap request timed out)")
if bootstrapResp.status != $Http200:
dbError(&"Cannot connect to SurrealDb {host}:{port}")
let conn = SurrealConn(
client: client,
host: host,
port: port.int32
)
conns[i] = Connection(
conn: conn,
isBusy: false,
createdAt: getTime().toUnix(),
)
let pools = Connections(
conns: conns,
timeout: timeout,
waiters: initDeque[Future[void]](),
preparedCache: initTable[string, SurrealPreparedEntry](),
)
return SurrealConnections(
pools: pools,
log: LogSetting(shouldDisplayLog:shouldDisplayLog, shouldOutputLogFile:shouldOutputLogFile, logDir:logDir)
)
@@ -0,0 +1,452 @@
import std/asyncdispatch
import std/json
import std/options
import std/strutils
import std/times
import ../../libs/surreal/surreal_impl
import ../../enums
import ./surreal_types
proc select*(self:SurrealConnections, columnsArg:varargs[string]):SurrealQuery =
let query = newJObject()
if columnsArg.len == 0:
query["select"] = %["*"]
else:
query["select"] = %columnsArg
let surrealQuery = SurrealQuery.new(
self.log,
self.pools,
query
)
return surrealQuery
proc table*(self:SurrealConnections, tableArg: string): SurrealQuery =
let query = newJObject()
query["table"] = %tableArg
let surrealQuery = SurrealQuery.new(
self.log,
self.pools,
query,
)
return surrealQuery
proc table*(self:SurrealQuery, tableArg: string): SurrealQuery =
self.query["table"] = %tableArg
return self
proc `distinct`*(self: SurrealQuery): SurrealQuery =
self.query["distinct"] = %true
return self
# # ============================== Conditions ==============================
# proc join*(self: SurrealQuery, table: string, column1: string, symbol: string,
# column2: string): SurrealQuery =
# if self.query.hasKey("join") == false:
# self.query["join"] = %*[{
# "table": table,
# "column1": column1,
# "symbol": symbol,
# "column2": column2
# }]
# else:
# self.query["join"].add(%*{
# "table": table,
# "column1": column1,
# "symbol": symbol,
# "column2": column2
# })
# return self
# proc leftJoin*(self: SurrealQuery, table: string, column1: string, symbol: string,
# column2: string): SurrealQuery =
# if self.query.hasKey("left_join") == false:
# self.query["left_join"] = %*[{
# "table": table,
# "column1": column1,
# "symbol": symbol,
# "column2": column2
# }]
# else:
# self.query["left_join"].add(%*{
# "table": table,
# "column1": column1,
# "symbol": symbol,
# "column2": column2
# })
# return self
## https://surrealdb.com/docs/surrealql/operators
const whereSymbols = ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "CONTAINS", "CONTAINSNOT", "@@"]
const whereSymbolsError = """Arg position 3 is only allowed of ["is", "is not", "=", "!=", "<", "<=", ">=", ">", "<>", "CONTAINS", "CONTAINSNOT", "@@"]"""
proc where*(self: SurrealQuery, column: string, symbol: string,
value: bool|int|float|string|SurrealId): SurrealQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("where") == false:
self.query["where"] = %*[{
"column": column,
"symbol": symbol,
"value": value
}]
else:
self.query["where"].add(
%*{
"column": column,
"symbol": symbol,
"value": value
}
)
return self
proc where*(self: SurrealQuery, column: string, symbol: string, value: nil.type): SurrealQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("where_null") == false:
self.query["where_null"] = %*[{
"column": column,
"symbol": symbol,
"value": value
}]
else:
self.query["where_null"].add(
%*{
"column": column,
"symbol": symbol,
"value": value
}
)
return self
proc orWhere*(self: SurrealQuery, column: string, symbol: string,
value: bool|int|float|string|SurrealId): SurrealQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": value
}]
else:
self.query["or_where"].add(
%*{
"column": column,
"symbol": symbol,
"value": value
}
)
return self
proc orWhere*(self: SurrealQuery, column: string, symbol: string, value: nil.type): SurrealQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("or_where") == false:
self.query["or_where"] = %*[{
"column": column,
"symbol": symbol,
"value": nil
}]
else:
self.query["or_where"].add(%*{
"column": column,
"symbol": symbol,
"value": nil
})
return self
proc whereBetween*(self:SurrealQuery, column:string, width:array[2, int|float]): SurrealQuery =
## `rdb.table("user").whereBetween("index", [1, 3]).get()`
##
## `SELECT * FROM user WHERE 1 <= index AND index <= 3`
##
## https://surrealdb.com/docs/surrealql/operators#lessthanorequal
if self.query.hasKey("where_between") == false:
self.query["where_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_between"].add(%*{
"column": column,
"width": width
})
return self
# proc whereBetween*(self:SurrealQuery, column:string, width:array[2, string]): SurrealQuery =
# ## rdb.table("user").whereBetween("name", ["user1", "user3"]).get()
# ##
# ## `SELECT * FROM user WHERE "user1" <= index <= "user3`
# ##
# ## https://surrealdb.com/docs/surrealql/operators#lessthanorequal
# if self.query.hasKey("where_between_string") == false:
# self.query["where_between_string"] = %*[{
# "column": column,
# "width": width
# }]
# else:
# self.query["where_between_string"].add(%*{
# "column": column,
# "width": width
# })
# return self
proc whereNotBetween*(self:SurrealQuery, column:string, width:array[2, int|float]): SurrealQuery =
## `rdb.table("user").whereNotBetween("index", [1, 3]).get()`
##
## `SELECT * FROM user WHERE index > 1 AND 3 < index`
##
## https://surrealdb.com/docs/surrealql/operators#greaterthan
if self.query.hasKey("where_not_between") == false:
self.query["where_not_between"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_between"].add(%*{
"column": column,
"width": width
})
return self
# proc whereNotBetween*(self:SurrealQuery, column:string, width:array[2, string]): SurrealQuery =
# self.placeHolder.add(%*{"key": column, "value": width[0]})
# self.placeHolder.add(%*{"key": column, "value": width[1]})
# if self.query.hasKey("where_not_between_string") == false:
# self.query["where_not_between_string"] = %*[{
# "column": column,
# "width": width
# }]
# else:
# self.query["where_not_between_string"].add(%*{
# "column": column,
# "width": width
# })
# return self
proc whereIn*(self:SurrealQuery, column:string, width:seq[int|float|string]): SurrealQuery =
## `rdb.table("user").whereIn("index", [2, 3]).get()`
##
## `SELECT * FROM user WHERE [2, 3] CONTAINS index`
##
## https://surrealdb.com/docs/surrealql/operators#contains
if self.query.hasKey("where_in") == false:
self.query["where_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNotIn*(self:SurrealQuery, column:string, width:seq[int|float|string]): SurrealQuery =
## `rdb.table("user").whereNotIn("index", [2, 3]).get()`
##
## `SELECT * FROM user WHERE [2, 3] CONTAINSNOT index`
##
## https://surrealdb.com/docs/surrealql/operators#contains-not
if self.query.hasKey("where_not_in") == false:
self.query["where_not_in"] = %*[{
"column": column,
"width": width
}]
else:
self.query["where_not_in"].add(%*{
"column": column,
"width": width
})
return self
proc whereNull*(self:SurrealQuery, column:string): SurrealQuery =
## https://surrealdb.com/docs/surrealql/operators#equal
if self.query.hasKey("where_null") == false:
self.query["where_null"] = %*[{
"column": column,
"symbol": "is",
}]
else:
self.query["where_null"].add(%*{
"column": column,
"symbol": "is",
})
return self
proc groupBy*(self:SurrealQuery, column:string): SurrealQuery =
if self.query.hasKey("group_by") == false:
self.query["group_by"] = %*[{"column": column}]
else:
self.query["group_by"].add(%*{"column": column})
return self
proc having*(self: SurrealQuery, column: string, symbol: string,
value: bool|int|float|string): SurrealQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": value
}]
else:
self.query["having"].add(
%*{
"column": column,
"symbol": symbol,
"value": value
}
)
return self
proc having*(self: SurrealQuery, column: string, symbol: string, value: nil.type): SurrealQuery =
if not whereSymbols.contains(symbol):
raise newException(
CatchableError,
whereSymbolsError
)
if self.query.hasKey("having") == false:
self.query["having"] = %*[{
"column": column,
"symbol": symbol,
"value": nil
}]
else:
self.query["having"].add(
%*{
"column": column,
"symbol": symbol,
"value": nil
}
)
return self
proc fetch*(self:SurrealQuery, columnsArg: varargs[string]):SurrealQuery =
self.query["fetch"] = %columnsArg
return self
proc orderBy*(self:SurrealQuery, column:string, order:Order): SurrealQuery =
if self.query.hasKey("order_by") == false:
self.query["order_by"] = %*[{
"column": column,
"collation": "",
"order": $order
}]
else:
self.query["order_by"].add(%*{
"column": column,
"collation": "",
"order": $order
})
return self
proc orderBy*(self:SurrealQuery, column:string, collation:Collation, order:Order): SurrealQuery =
if self.query.hasKey("order_by") == false:
self.query["order_by"] = %*[{
"column": column,
"collation": $collation,
"order": $order
}]
else:
self.query["order_by"].add(%*{
"column": column,
"collation": $collation,
"order": $order
})
return self
proc limit*(self: SurrealQuery, num: int): SurrealQuery =
self.query["limit"] = %num
return self
proc start*(self: SurrealQuery, num: int): SurrealQuery =
self.query["start"] = %num
return self
proc parallel*(self: SurrealQuery): SurrealQuery =
self.query["parallel"] = %true
return self
# proc offset*(self: SurrealQuery, num: int): SurrealQuery =
# self.query["offset"] = %num
# return self
# proc inTransaction*(self:SurrealQuery, connI:int) =
# ## Only used in transation block
# self.isInTransaction = true
# self.transactionConn = connI
# proc freeTransactionConn*(self:SurrealQuery, connI:int) =
# ## Only used in transation block
# self.isInTransaction = false
proc raw*(self:SurrealConnections, sql:string, arges=newJArray()): RawSurrealQuery =
## arges is `JArray`
##
## can't use BLOB data.
let rawQueryRdb = RawSurrealQuery(
log: self.log,
pools: self.pools,
query: newJObject(),
queryString: sql.strip(),
placeHolder: arges,
)
return rawQueryRdb
@@ -0,0 +1,135 @@
import std/asyncdispatch
import std/deques
import std/json
import std/httpclient
import std/tables
import ../../log
import ../../libs/surreal/surreal_rdb
import ../../error
type SurrealDB* = object
type Connection* = object
conn*:SurrealConn
isBusy*:bool
createdAt*:int64
type SurrealPreparedEntry* = ref object
sql*: string
normalizedSql*: string
nArgs*: int
refCount*: int
lastUsedAt*: int64
type Connections* = ref object
conns*: seq[Connection]
timeout*: int
## `getFreeConn` が接続を待つときに積む Future。`returnConn` が先頭から 1 件だけ完了させる。
waiters*: Deque[Future[void]]
preparedCache*: Table[string, SurrealPreparedEntry]
## created by `let rdb = dbOpen(SurrealDB, "ns", "database", "user", "pass", "http://surreal", 8000)`
type SurrealConnections* = ref object
log*: LogSetting
pools*:Connections
type SurrealPreparedContext* = ref object
owner*: SurrealConnections
connI*: int
type SurrealPreparedStatement* = ref object
owner*: SurrealConnections
entry*: SurrealPreparedEntry
sql*: string
nArgs*: int
isClosed*: bool
type SurrealQuery* = ref object
## created by `rdb.select("columnName")` or `rdb.table("tableName")`
log*: LogSetting
pools*:Connections
query*: JsonNode
queryString*: string
# placeHolder*: JsonNode # JArray [{"key":"user", "value":"user1"}]
placeHolder*: JsonNode # JArray [true, 1, 1.1, "str"]
proc new*(_:type SurrealQuery, log:LogSetting, pools:Connections, query:JsonNode):SurrealQuery =
return SurrealQuery(
log:log,
pools:pools,
query:query,
queryString:"",
placeHolder: newJArray()
)
type RawSurrealQuery* = ref object
log*: LogSetting
pools*:Connections
query*: JsonNode
queryString*: string
placeHolder*: JsonNode # JArray ["user1", "user1@example.com"]
proc `$`*(self:SurrealConnections|SurrealQuery|RawSurrealQuery):string =
return "SurrealDB"
type SurrealId* = object
table*:string
id:string
proc new*(_:type SurrealId):SurrealId =
## create empty surreal id
return SurrealId(table:"", id:"")
proc new*(_:type SurrealId, table, id:string):SurrealId =
## .. code-block:: Nim
## let rawId = "user:z7cr4mz474h4ab7rcd6d"
## let table = "user"
## let id = "z7cr4mz474h4ab7rcd6d"
## let surrealId = SureealId.new(table, id)
if table.len == 0:
dbError("table cannot be empty")
if id.len == 0:
dbError("id cannot be empty")
return SurrealId(table:table, id:id)
proc new*(_:type SurrealId, rawId:string):SurrealId =
## .. code-block:: Nim
## let rawId = "user:z7cr4mz474h4ab7rcd6d"
## let surrealId = SureealId.new(rawId)
if rawId.len == 0:
dbError("rawId cannot be empty")
let split = rawId.split(":")
let table = split[0]
let id = split[1]
return SurrealId(table:table, id:id)
proc rawId*(self:SurrealId):string =
## .. code-block:: Nim
## let rawId = "user:z7cr4mz474h4ab7rcd6d"
## let surrealId = SureealId.new(rawId)
## surrealId.rawId() == "user:z7cr4mz474h4ab7rcd6d"
return self.table & ":" & self.id
proc `$`*(self:SurrealId):string =
return self.id
proc `%`*(self:SurrealId):JsonNode =
return %(self.rawId())
@@ -0,0 +1,115 @@
import std/json
import std/options
import std/macros
type PreparedParam* = object
value*: string
isNull*: bool
proc nullPreparedParam*(): PreparedParam =
result.isNull = true
proc toPreparedParam*(v: string): PreparedParam =
result.value = v
proc toPreparedParam*(v: cstring): PreparedParam =
if v.isNil:
return nullPreparedParam()
result.value = $v
proc toPreparedParam*(v: bool): PreparedParam =
result.value = if v: "1" else: "0"
proc toPreparedParam*[T: SomeInteger](v: T): PreparedParam =
result.value = $v
proc toPreparedParam*[T: SomeFloat](v: T): PreparedParam =
result.value = $v
proc toPreparedParam*(v: JsonNode): PreparedParam =
if v.isNil or v.kind == JNull:
return nullPreparedParam()
case v.kind
of JBool:
result.value = if v.getBool: "1" else: "0"
of JInt:
result.value = $v.getInt
of JFloat:
result.value = $v.getFloat
of JString:
result.value = v.getStr
of JArray, JObject:
result.value = v.pretty
of JNull:
discard
proc toPreparedParam*[T](v: Option[T]): PreparedParam =
if v.isSome:
return toPreparedParam(v.get)
return nullPreparedParam()
proc toPreparedParam*[T](v: T): PreparedParam =
result.value = $v
proc toPreparedParams*(args: seq[string]): seq[PreparedParam] =
result = newSeq[PreparedParam](args.len)
for i, arg in args:
if arg == "NULL" or arg == "null":
result[i] = nullPreparedParam()
else:
result[i] = toPreparedParam(arg)
proc toPreparedParams*(args: JsonNode): seq[PreparedParam] =
if args.isNil or args.kind == JNull:
return
if args.kind == JArray:
result = newSeq[PreparedParam](args.len)
for i in 0 ..< args.len:
result[i] = toPreparedParam(args[i])
return
result = @[toPreparedParam(args)]
proc preparedText*(param: PreparedParam): string =
if param.isNull:
return "NULL"
return param.value
proc preparedTextSeq*(args: openArray[PreparedParam]): seq[string] =
result = newSeq[string](args.len)
for i, arg in args:
result[i] = arg.preparedText
proc countQuestionMarks*(s: string): int =
for ch in s:
if ch == '?':
inc result
proc buildPreparedArgsExpr*(args: NimNode): NimNode =
let toPreparedParamSym = bindSym("toPreparedParam")
let nullPreparedParamSym = bindSym("nullPreparedParam")
var arr = nnkBracket.newTree()
for arg in args:
if arg.kind == nnkNilLit:
arr.add(newCall(nullPreparedParamSym))
else:
arr.add(newCall(toPreparedParamSym, arg))
result = newTree(nnkPrefix, ident("@"), arr)
@@ -0,0 +1,28 @@
query_builder/models/xx/xx_exec.nim
```nim
# public exec
# return json
get() / first() / find() / insert() / insertId() / update() / delete()
# return string
getPlain() / firstPlain() / findPlain()
# aggregate
count() / min() / max() / avg() / sum()
```
```nim
# private exec
getAllRows() / getRow() / exec() / insertId()
getAllRowsPlain() / getRowPlain()
```
query_builder/libs/xx/xx_impl.nim
```nim
query() / exec() / execGetValue()
rawQuery() / rawExec()
```
---
@@ -0,0 +1,48 @@
import ./env
import ./schema_builder/enums; export enums
import ./schema_builder/models/table; export table
import ./schema_builder/models/column; export column
when isExistsSqlite:
import ./query_builder/models/sqlite/sqlite_types; export SQLite3, SqliteConnections
import ./schema_builder/usecases/sqlite/create as sqlite_create; export sqlite_create
import ./schema_builder/usecases/sqlite/alter as sqlite_alter; export sqlite_alter
import ./schema_builder/usecases/sqlite/drop as sqlite_drop; export sqlite_drop
import ./schema_builder/usecases/sqlite/create_schema as sqlite_create_schema; export sqlite_create_schema
when isExistsPostgres:
import ./query_builder/models/postgres/postgres_types; export PostgreSQL, PostgresConnections
import ./schema_builder/usecases/postgres/create as postgres_create; export postgres_create
import ./schema_builder/usecases/postgres/alter as postgres_alter; export postgres_alter
import ./schema_builder/usecases/postgres/drop as postgres_drop; export postgres_drop
import ./schema_builder/usecases/postgres/create_schema as postgres_create_schema; export postgres_create_schema
when isExistsMariadb:
import ./query_builder/models/mariadb/mariadb_types; export MariaDB, MariadbConnections
import ./schema_builder/usecases/mariadb/create as mariadb_create; export mariadb_create
import ./schema_builder/usecases/mariadb/alter as mariadb_alter; export mariadb_alter
import ./schema_builder/usecases/mariadb/drop as mariadb_drop; export mariadb_drop
import ./schema_builder/usecases/mariadb/create_schema as mariadb_create_schema; export mariadb_create_schema
when isExistsMysql:
import ./query_builder/models/mysql/mysql_types; export MySql, MysqlConnections
import ./schema_builder/usecases/mysql/create as mysql_create; export mysql_create
import ./schema_builder/usecases/mysql/alter as mysql_alter; export mysql_alter
import ./schema_builder/usecases/mysql/drop as mysql_drop; export mysql_drop
import ./schema_builder/usecases/mysql/create_schema as mysql_create_schema; export mysql_create_schema
when isExistsSurrealdb:
import ./query_builder/models/surreal/surreal_types; export SurrealDB, SurrealConnections
import ./schema_builder/usecases/surreal/create as surreal_create; export surreal_create
import ./schema_builder/usecases/surreal/alter as surreal_alter; export surreal_alter
import ./schema_builder/usecases/surreal/drop as surreal_drop; export surreal_drop
import ./schema_builder/usecases/surreal/create_schema as surreal_create_schema; export surreal_create_schema
when isExistsBaradb:
import ./query_builder/models/baradb/baradb_types; export Baradb, BaradbConnections
import ./schema_builder/usecases/baradb/create as baradb_create; export baradb_create
import ./schema_builder/usecases/baradb/alter as baradb_alter; export baradb_alter
import ./schema_builder/usecases/baradb/drop as baradb_drop; export baradb_drop
import ./schema_builder/usecases/baradb/create_schema as baradb_create_schema; export baradb_create_schema
Binary file not shown.
@@ -0,0 +1,62 @@
type RdbTypeKind* = enum
# int
rdbIncrements = "rdbIncrements"
rdbInteger = "rdbInteger"
rdbSmallInteger = "rdbSmallInteger"
rdbMediumInteger = "rdbMediumInteger"
rdbBigInteger = "rdbBigInteger"
# float
rdbDecimal = "rdbDecimal"
rdbDouble = "rdbDouble"
rdbFloat = "rdbFloat"
# char
rdbUuid = "rdbUuid"
rdbChar = "rdbChar"
rdbString = "rdbString"
# text
rdbText = "rdbText"
rdbMediumText = "rdbMediumText"
rdbLongText = "rdbLongText"
# date
rdbDate = "rdbDate"
rdbDatetime = "rdbDatetime"
rdbTime = "rdbTime"
rdbTimestamp = "rdbTimestamp"
rdbTimestamps = "rdbTimestamps"
rdbSoftDelete = "rdbSoftDelete"
# others
rdbBinary = "rdbBinary"
rdbBoolean = "rdbBoolean"
rdbEnumField = "rdbEnumField"
rdbJson = "rdbJson"
rdbForeign = "rdbForeign"
rdbStrForeign = "rdbStrForeign"
type ForeignOnDelete* = enum
RESTRICT
CASCADE
SET_NULL
NO_ACTION
type DefaultDateTime* = enum
None
Current
CurrentOnUpdate
type UsecaseType* = enum
Create
Alter
Drop
type TableMigrationType* = enum
CreateTable
ChangeTable
RenameTable
DropTable
type ColumnMigrationType* = enum
AddColumn
ChangeColumn
RenameColumn
DropColumn
@@ -0,0 +1,421 @@
import std/json
import ../enums
type Column* = ref object
name*: string
typ*: RdbTypeKind
isIndex*: bool
isNullable*: bool
isUnsigned*: bool
isUnique*: bool
isAutoIncrement*:bool
isDefault*: bool
isUpdatedAt*: bool
defaultBool*: bool
defaultInt*: int
defaultFloat*: float
defaultString*: string
defaultJson*: JsonNode
defaultDatetime*: DefaultDateTime
foreignOnDelete*: ForeignOnDelete
commentContent*:string
isCommentNull*:bool
info*: JsonNode
checksum*:string
# alter table
previousName*:string
migrationType*:ColumnMigrationType
usecaseType*:UsecaseType
proc new(_:type Column):Column =
return Column(
defaultJson: newJNull(),
info: newJNull(),
)
proc toSchema*(self:Column):JsonNode =
return %*{
"name": self.name,
"typ": self.typ,
"isIndex": self.isIndex,
"isNullable": self.isNullable,
"isUnsigned": self.isUnsigned,
"isUnique": self.isUnique,
"isAutoIncrement": self.isAutoIncrement,
"isDefault": self.isDefault,
"isUpdatedAt": self.isUpdatedAt,
"defaultBool": self.defaultBool,
"defaultInt": self.defaultInt,
"defaultFloat": self.defaultFloat,
"defaultString": self.defaultString,
"defaultJson": self.defaultJson,
"foreignOnDelete": self.foreignOnDelete,
"comment": self.commentContent,
"isCommentNull": self.isCommentNull,
"info": self.info,
"previousName":self.previousName,
"migrationType":self.migrationType,
"usecaseType": self.usecaseType,
}
# =============================================================================
# int
# =============================================================================
proc increments*(_:type Column, name:string): Column =
let column = Column.new()
column.name = name
column.typ = rdbIncrements
return column
proc integer*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbInteger
return column
proc smallInteger*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbSmallInteger
return column
proc mediumInteger*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbMediumInteger
return column
proc bigInteger*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbBigInteger
return column
# =============================================================================
# float
# =============================================================================
proc decimal*(_:type Column, name:string, maximum:int, digit:int): Column =
let column = Column.new()
column.name = name
column.typ = rdbDecimal
column.info = %*{
"maximum": maximum,
"digit": digit
}
return column
proc double*(_:type Column, name:string, maximum:int, digit:int):Column =
let column = Column.new()
column.name = name
column.typ = rdbDouble
column.info = %*{
"maximum": maximum,
"digit": digit
}
return column
proc float*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbFloat
return column
# =============================================================================
# char
# =============================================================================
proc char*(_:type Column, name:string, maxLength:int): Column =
let column = Column.new()
column.name = name
column.typ = rdbChar
column.info = %*{
"maxLength": maxLength
}
return column
proc string*(_:type Column, name:string, length=256):Column =
let column = Column.new()
column.name = name
column.typ = rdbString
column.info = %*{"maxLength": length}
return column
proc uuid*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbUuid
column.isUnique = true
column.info = %*{"maxLength": 256}
return column
proc text*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbText
return column
proc mediumText*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbMediumText
return column
proc longText*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbLongText
return column
# =============================================================================
# date
# =============================================================================
proc date*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbDate
return column
proc datetime*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbDatetime
return column
proc time*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbTime
return column
proc timestamp*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbTimestamp
return column
proc timestamps*(_:type Column):Column =
let column = Column.new()
column.typ = rdbTimestamps
column.isUpdatedAt = true
return column
proc softDelete*(_:type Column):Column =
let column = Column.new()
column.typ = rdbSoftDelete
return column
# =============================================================================
# others
# =============================================================================
proc binary*(_:type Column, name:string): Column =
let column = Column.new()
column.name = name
column.typ = rdbBinary
return column
proc boolean*(_:type Column, name:string): Column =
let column = Column.new()
column.name = name
column.typ = rdbBoolean
return column
proc enumField*(_:type Column, name:string, options:openArray[string]):Column =
let column = Column.new()
column.name = name
column.typ = rdbEnumField
column.info = %*{
"options": options
}
return column
proc json*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.typ = rdbJson
return column
# =============================================================================
# Foreign
# =============================================================================
proc foreign*(_:type Column, name:string):Column =
let column = Column.new()
column.name = name
column.previousName = name
column.typ = rdbForeign
column.isIndex = false
return column
proc strForeign*(_:type Column, name:string, length=256):Column =
let column = Column.new()
column.name = name
column.previousName = name
column.typ = rdbStrForeign
column.isIndex = false
column.info = %*{"maxLength": length}
return column
proc reference*(self:Column, column:string):Column =
if self.info.kind == JNull:
self.info = %*{"column": column}
else:
self.info["column"] = %column
return self
proc onTable*(self:Column, table:string):Column =
if self.info.kind == JNull:
self.info = %*{"table": table}
else:
self.info["table"] = %table
return self
when NimMajor == 1:
proc on*(self:Column, table:string):Column {.deprecated: "Consider using onTable instead".} =
self.info["table"] = %*table
return self
proc onDelete*(self:Column, kind:ForeignOnDelete):Column =
self.foreignOnDelete = kind
return self
# =============================================================================
# options
# =============================================================================
proc autoIncrement*(c: Column): Column =
c.isAutoIncrement = true
c.isDefault = false
c.isUnique = true
return c
proc default*(c: Column, value:bool): Column =
if not c.isAutoIncrement:
c.isDefault = true
c.defaultBool = value
return c
proc default*(c: Column, value:int): Column =
if not c.isAutoIncrement:
c.isDefault = true
c.defaultInt = value
return c
proc default*(c: Column, value:float): Column =
if not c.isAutoIncrement:
c.isDefault = true
c.defaultFloat = value
return c
proc default*(c: Column, value:string): Column =
if not c.isAutoIncrement:
c.isDefault = true
c.defaultString = value
return c
proc default*(c: Column, value:JsonNode): Column =
if not c.isAutoIncrement:
c.isDefault = true
c.defaultJson = value
return c
proc default*(c: Column, value:DefaultDateTime): Column =
if not c.isAutoIncrement:
c.isDefault = true
c.defaultDatetime = value
return c
proc default*(c: Column):Column =
if not c.isAutoIncrement:
c.isDefault = true
return c
proc index*(c: Column):Column =
if not c.isUnique:
c.isIndex = true
return c
proc nullable*(c: Column): Column =
c.isNullable = true
return c
proc unique*(c: Column): Column =
c.isUnique = true
c.isIndex = false
return c
proc unsigned*(c: Column): Column =
c.isUnsigned = true
return c
proc comment*(c: Column, value:string): Column =
c.commentContent = value
return c
proc comment*(c: Column, arg:type nil): Column =
c.isCommentNull = true
return c
# =============================================================================
# alter table
# =============================================================================
proc add*(c: Column): Column =
c.migrationType = AddColumn
return c
proc change*(c: Column): Column =
c.migrationType = ChangeColumn
return c
proc renameColumn*(_:type Column, src, dest:string): Column =
let column = Column.new()
column.name = dest
column.previousName = src
column.migrationType = RenameColumn
return column
proc dropColumn*(_:type Column, name:string): Column =
let column = Column.new()
column.name = name
column.migrationType = DropColumn
return column
@@ -0,0 +1,72 @@
import std/json
import std/strutils
import std/sequtils
import ../enums
from ./column import Column, toSchema
type Table* = ref object
name*: string
columns*: seq[Column]
primary*: seq[string]
query*:seq[string]
commentContent*:string
checksum*:string
previousName*:string
migrationType*: TableMigrationType
usecaseType*:UsecaseType
proc table*(name:string):Table =
return Table(
name: name,
query: newSeq[string](),
migrationType: CreateTable,
)
proc table*(name:string, columns:openArray[Column], comment=""):Table =
return Table(
name: name,
commentContent: comment,
columns: @columns,
query: newSeq[string](),
migrationType: CreateTable,
)
proc table*(name:string, columns:seq[Column], primary:seq[string] = @[], comment=""):Table =
return Table(
name: name,
commentContent: comment,
columns: @columns,
primary: @primary,
query: newSeq[string](),
migrationType: CreateTable,
)
proc toSchema*(self:Table):JsonNode =
let columns = self.columns.map(
proc(column:Column):JsonNode =
return column.toSchema()
)
return %*{
"name":self.name,
"comment": self.commentContent,
"columns": columns,
"primary": self.primary,
"previousName": self.previousName,
"migrationType": self.migrationType,
"usecaseType": self.usecaseType,
}
proc smallName*(self:Table):string =
## TableName -> tablename
return self.name.toLowerAscii()
proc renameTo*(self:Table, name:string):Table =
let previousName = self.name
self.name = name
self.previousName = previousName
self.migrationType = RenameTable
return self
@@ -0,0 +1,8 @@
import std/strformat
import ../../models/table
import ../../models/column
import ./baradb_query_type
proc addColumn*(self: BaradbSchema, isReset: bool) =
let sql = &"ALTER TABLE `{self.table.name}` ADD COLUMN `{self.column.name}` {self.column.typ}"
discard waitFor self.rdb.raw(sql).exec()
@@ -0,0 +1,14 @@
import ../../../query_builder/models/baradb/baradb_types
import ../../models/table
import ../../models/column
type BaradbSchema* = ref object
rdb*: BaradbConnections
table*: Table
column*: Column
proc new*(_: type BaradbSchema, rdb: BaradbConnections, table: Table): BaradbSchema =
return BaradbSchema(rdb: rdb, table: table)
proc new*(_: type BaradbSchema, rdb: BaradbConnections, table: Table, column: Column): BaradbSchema =
return BaradbSchema(rdb: rdb, table: table, column: column)
@@ -0,0 +1,8 @@
import std/strformat
import ../../models/table
import ../../models/column
import ./baradb_query_type
proc changeColumn*(self: BaradbSchema, isReset: bool) =
let sql = &"ALTER TABLE `{self.table.name}` ALTER COLUMN `{self.column.name}` TYPE {self.column.typ}"
discard waitFor self.rdb.raw(sql).exec()
@@ -0,0 +1,12 @@
import std/strformat
import ../../models/table
import ./baradb_query_type
proc createMigrationTable*(self: BaradbSchema) =
let sql = """CREATE TABLE IF NOT EXISTS "schema_migrations" (
"id" SERIAL PRIMARY KEY,
"name" VARCHAR(255) NOT NULL,
"checksum" VARCHAR(64) NOT NULL,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)"""
discard waitFor self.rdb.raw(sql).exec()
@@ -0,0 +1,16 @@
import std/strformat
import std/sequtils
import ../../models/table
import ../../models/column
import ./baradb_query_type
proc createTable*(self: BaradbSchema, isReset: bool) =
var query = ""
for i, column in self.table.columns:
if query.len > 0: query.add(", ")
query.add(&"`{column.name}` {column.typ}")
if self.table.primary.len > 0:
let primary = self.table.primary.map(proc(row: string): string = &"`{row}`").join(", ")
query.add(&", PRIMARY KEY({primary})")
let sql = &"CREATE TABLE IF NOT EXISTS `{self.table.name}` ({query})"
discard waitFor self.rdb.raw(sql).exec()
@@ -0,0 +1,8 @@
import std/strformat
import ../../models/table
import ../../models/column
import ./baradb_query_type
proc dropColumn*(self: BaradbSchema, isReset: bool) =
let sql = &"ALTER TABLE `{self.table.name}` DROP COLUMN `{self.column.name}`"
discard waitFor self.rdb.raw(sql).exec()
@@ -0,0 +1,7 @@
import std/strformat
import ../../models/table
import ./baradb_query_type
proc dropTable*(self: BaradbSchema, isReset: bool) =
let sql = &"DROP TABLE `{self.table.name}`"
discard waitFor self.rdb.raw(sql).exec()
@@ -0,0 +1,8 @@
import std/strformat
import ../../models/table
import ../../models/column
import ./baradb_query_type
proc renameColumn*(self: BaradbSchema, isReset: bool) =
let sql = &"ALTER TABLE `{self.table.name}` RENAME COLUMN `{self.column.name}` TO `{self.column.changeTo}`"
discard waitFor self.rdb.raw(sql).exec()
@@ -0,0 +1,7 @@
import std/strformat
import ../../models/table
import ./baradb_query_type
proc renameTable*(self: BaradbSchema, isReset: bool) =
let sql = &"ALTER TABLE `{self.table.name}` RENAME TO `{self.table.changeTo}`"
discard waitFor self.rdb.raw(sql).exec()
@@ -0,0 +1,7 @@
import std/strformat
import ../../models/table
import ./baradb_query_type
proc resetTable*(self: BaradbSchema) =
let sql = &"DELETE FROM `{self.table.name}`"
discard waitFor self.rdb.raw(sql).exec()
@@ -0,0 +1,12 @@
import std/strformat
import ../../../query_builder/models/baradb/baradb_types
import ../../models/table
proc execThenSaveHistory*(rdb: BaradbConnections, name: string, queries: seq[string], checksum: string) =
for sql in queries:
discard waitFor rdb.raw(sql).exec()
let hist = &"INSERT INTO `schema_migrations` (`name`, `checksum`) VALUES ('{name}', '{checksum}')"
discard waitFor rdb.raw(hist).exec()
proc shouldRun*(rdb: BaradbConnections, table: Table, checksum: string, isReset: bool): bool =
return true
@@ -0,0 +1,8 @@
import std/strformat
import ../../models/table
import ../../models/column
proc addColumnString*(table: Table, column: Column): string =
result = &"`{column.name}` {column.typ}"
if not column.isNullable:
result.add(" NOT NULL")
@@ -0,0 +1,6 @@
import std/strformat
import ../../models/table
import ../../models/column
proc changeColumnString*(table: Table, column: Column): string =
result = &"`{column.name}` {column.typ}"
@@ -0,0 +1,24 @@
import std/strformat
import ../../models/table
import ../../models/column
proc createColumnString*(table: Table, column: Column): string =
result = &"`{column.name}` {column.typ}"
if not column.isNullable:
result.add(" NOT NULL")
if column.isUnique:
result.add(" UNIQUE")
if column.isDefault:
result.add(&" DEFAULT {column.default}")
proc createForeignString*(table: Table, column: Column): string =
result = &"FOREIGN KEY (`{column.name}`) REFERENCES `{column.info}`"
proc createIndexString*(table: Table, column: Column): string =
result = &"CREATE INDEX `{table.name}_{column.name}_idx` ON `{table.name}` (`{column.name}`)"
proc createUpdatedAtString*(table: Table, column: Column): string =
result = ""
proc createCommentColumn*(table: Table, column: Column): string =
result = ""
@@ -0,0 +1,21 @@
when NimMajor >= 2:
import checksums/sha1
else:
import std/sha1
import std/json
import ../../models/table
import ../../models/column
import ./schema_utils
import ./mariadb_query_type
import ./sub/add_column_query
proc addColumn*(self:MariadbSchema, isReset:bool) =
let queries = addColumnString(self.table, self.column)
let schema = $self.column.toSchema()
let checksum = $schema.secureHash()
if shouldRun(self.rdb, self.table, checksum, isReset):
execThenSaveHistory(self.rdb, self.table.name, queries, checksum)
@@ -0,0 +1,31 @@
when NimMajor >= 2:
import checksums/sha1
else:
import std/sha1
import std/strformat
import std/json
import ../../models/table
import ../../models/column
import ./schema_utils
import ./mariadb_query_type
import ./sub/change_column_query
proc changeColumn*(self:MariadbSchema, isReset:bool) =
let schema = $self.column.toSchema()
let checksum = $schema.secureHash()
var queries:seq[string]
queries.add(&"ALTER TABLE `{self.table.name}` DROP INDEX IF EXISTS `{self.column.name}`") # unique
queries.add(&"ALTER TABLE `{self.table.name}` DROP INDEX IF EXISTS `{self.table.name}_{self.column.name}_index`")
queries.add(
changeColumnString(self.table, self.column)
)
if self.column.isIndex:
let indexQuery = addIndexString(self.column, self.table)
queries.add(indexQuery)
if shouldRun(self.rdb, self.table, checksum, isReset):
execThenSaveHistory(self.rdb, self.table.name, queries, checksum)
@@ -0,0 +1,38 @@
import std/strformat
import ../../enums
import ../../models/table
import ../../models/column
import ./schema_utils
import ./mariadb_query_type
import ./sub/create_column_query
proc createMigrationTable*(self: MariadbSchema) =
var queries:seq[string] = @[]
var query = ""
var foreignQuery = ""
var indexQuery:seq[string] = @[]
for i, column in self.table.columns:
if query.len > 0: query.add(", ")
query.add(createColumnString(self.table, column))
if column.typ == rdbForeign or column.typ == rdbStrForeign:
if foreignQuery.len > 0: foreignQuery.add(", ")
foreignQuery.add(createForeignKey(self.table, column))
if column.isIndex:
indexQuery.add(createIndexString(self.table, column))
if foreignQuery.len > 0:
queries.add(
&"CREATE TABLE IF NOT EXISTS `{self.table.name}` ({query}, {foreignQuery})"
)
else:
queries.add(
&"CREATE TABLE IF NOT EXISTS `{self.table.name}` ({query})"
)
if indexQuery.len > 0:
queries.add(indexQuery)
exec(self.rdb, queries)
@@ -0,0 +1,64 @@
when NimMajor >= 2:
import checksums/sha1
else:
import std/sha1
import std/strformat
import std/strutils
import std/sequtils
import std/json
import ../../enums
import ../../models/table
import ../../models/column
import ./schema_utils
import ./mariadb_query_type
import ./sub/create_column_query
proc createTable*(self: MariadbSchema, isReset:bool) =
var queries:seq[string] = @[]
var query = ""
var foreignQuery = ""
var indexQuery:seq[string] = @[]
for i, column in self.table.columns:
if query.len > 0: query.add(", ")
query.add(createColumnString(self.table, column))
if column.typ == rdbForeign or column.typ == rdbStrForeign:
if foreignQuery.len > 0: foreignQuery.add(", ")
foreignQuery.add(createForeignKey(self.table, column))
if column.isIndex:
indexQuery.add(createIndexString(self.table, column))
if self.table.primary.len > 0:
let primary = self.table.primary.map(
proc(row:string):string =
return &"`{row}`"
)
.join(", ")
query.add(&", PRIMARY KEY({primary})")
if foreignQuery.len > 0:
queries.add(
&"CREATE TABLE IF NOT EXISTS `{self.table.name}` ({query}, {foreignQuery})"
)
else:
queries.add(
&"CREATE TABLE IF NOT EXISTS `{self.table.name}` ({query})"
)
if self.table.commentContent.len > 0:
queries[^1].add(
&" COMMENT = '{self.table.commentContent}'"
)
if indexQuery.len > 0:
queries.add(indexQuery)
let schema = $self.table.toSchema()
let checksum = $schema.secureHash()
if shouldRun(self.rdb, self.table, checksum, isReset):
execThenSaveHistory(self.rdb, self.table.name, queries, checksum)
@@ -0,0 +1,19 @@
when NimMajor >= 2:
import checksums/sha1
else:
import std/sha1
import std/strformat
import std/json
import ../../models/table
import ../../models/column
import ./schema_utils
import ./mariadb_query_type
proc dropColumn*(self:MariadbSchema, isReset:bool) =
let query = &"ALTER TABLE `{self.table.name}` DROP COLUMN `{self.column.name}`"
let schema = $self.column.toSchema()
let checksum = $schema.secureHash()
if shouldRun(self.rdb, self.table, checksum, isReset):
execThenSaveHistory(self.rdb, self.table.name, query, checksum)
@@ -0,0 +1,18 @@
when NimMajor >= 2:
import checksums/sha1
else:
import std/sha1
import std/strformat
import std/json
import ../../models/table
import ./schema_utils
import ./mariadb_query_type
proc dropTable*(self:MariadbSchema, isReset:bool) =
let query = &"DROP TABLE IF EXISTS `{self.table.name}`"
let schema = $self.table.toSchema()
let checksum = $schema.secureHash()
if shouldRun(self.rdb, self.table, checksum, isReset):
execThenSaveHistory(self.rdb, self.table.name, query, checksum)
@@ -0,0 +1,15 @@
import ../../../query_builder/models/mariadb/mariadb_types
import ../../models/table
import ../../models/column
type MariadbSchema* = ref object
rdb*:MariadbConnections
table*:Table
column*:Column
proc new*(_:type MariadbSchema, rdb:MariadbConnections, table:Table):MariadbSchema =
return MariadbSchema(rdb:rdb, table:table)
proc new*(_:type MariadbSchema, rdb:MariadbConnections, table:Table, column:Column):MariadbSchema =
return MariadbSchema(rdb:rdb, table:table, column:column)
@@ -0,0 +1,19 @@
when NimMajor >= 2:
import checksums/sha1
else:
import std/sha1
import std/json
import std/strformat
import ../../models/table
import ../../models/column
import ./schema_utils
import ./mariadb_query_type
proc renameColumn*(self:MariadbSchema, isReset:bool) =
let query = &"ALTER TABLE `{self.table.name}` RENAME COLUMN `{self.column.previousName}` TO `{self.column.name}`"
let schema = $self.column.toSchema()
let checksum = $schema.secureHash()
if shouldRun(self.rdb, self.table, checksum, isReset):
execThenSaveHistory(self.rdb, self.table.name, query, checksum)
@@ -0,0 +1,19 @@
when NimMajor >= 2:
import checksums/sha1
else:
import std/sha1
import std/strformat
import std/json
import ../../models/table
import ./mariadb_query_type
import ./schema_utils
proc renameTable*(self:MariadbSchema, isReset:bool) =
let query = &"RENAME TABLE `{self.table.previousName}` TO `{self.table.name}`"
let schema = $self.table.toSchema()
let checksum = $schema.secureHash()
if shouldRun(self.rdb, self.table, checksum, isReset):
execThenSaveHistory(self.rdb, self.table.name, query, checksum)
@@ -0,0 +1,13 @@
import std/asyncdispatch
import std/strformat
import ../../../query_builder/models/mariadb/mariadb_query
import ../../../query_builder/models/mariadb/mariadb_exec
import ../../models/table
import ./mariadb_query_type
proc resetMigrationTable*(self:MariadbSchema) =
self.rdb.table("_allographer_migrations").where("name", "=", self.table.name).delete.waitFor
proc resetTable*(self:MariadbSchema) =
self.rdb.raw(&"DROP TABLE IF EXISTS `{self.table.name}`").exec.waitFor

Some files were not shown because too many files have changed in this diff Show More