Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+260
View File
@@ -0,0 +1,260 @@
## Description
`pg` is a wrapper for the PostgreSQL client library. It provides access to a PostgreSQL
database server.
Before you can use this module, you must first have PostgreSQL installed on your system.
To do this, find your OS and perform the actions listed.
> **Note**
> These instructions are meant only as a convenience. If your OS is not listed
> or you need extra help, [go here](https://www.postgresql.org/download/).
### Fedora 31
```
sudo dnf install postgresql-server postgresql-contrib
sudo systemctl enable postgresql # to autostart on startup
sudo systemctl start postgresql
```
### Ubuntu/Debian
```
sudo apt install postgresql postgresql-client
sudo systemctl enable postgresql # to autostart on startup
sudo systemctl start postgresql
```
### MacOSX (Homebrew)
```
brew install postgresql
brew services start postgresql
```
On newer Homebrew setups the formula and service name may be versioned
instead, for example `postgresql@18`. Use the exact name reported by
`brew info postgresql`.
### MacOSX (MacPorts)
```
gem install pg -- --with-pg-config=/opt/local/lib/postgresql[version number]/bin/pg_config
```
## Installing libpq-dev or its equivalent for your OS
**Ubuntu/Debian**: `sudo apt install libpq-dev`
**Red Hat Linux (RHEL)**: `yum install postgresql-devel`
**OpenSuse**: `zypper in postgresql-devel`
**ArchLinux**: `pacman -S postgresql-libs`
**FreeBSD**: `pkg install postgresql18-client`
**OpenBSD**: `pkg_add postgresql-client`
**Windows**:
The directory structure of the `@VEXEROOT/thirdparty/pg` folder will look like the following
after following all instructions. Create the `pg` folder yourself if it does not exist yet.
```
@VEXEROOT/thirdparty/pg
├───libpq
│ libpq-fe.h
│ pg_config.h
│ postgres_ext.h
│
└───win64
└───msvc
libpq.lib
```
Installation instructions are as follows
```
Download the latest PostgreSQL version from the official website
(currently https://www.enterprisedb.com/downloads/postgres-postgresql-downloads)
In one of the steps in the installer, tick the following boxes:
[X] PostgreSQL Server
[ ] pgAdmin 4
[ ] Stack Builder
[X] Command Line Tools
We need PostgreSQL Server because it brings with it the C header files
needed for building programs that link to `libpq.dll`.
After finishing installation, add the folder `C:/Program Files/PostgreSQL/<version>/bin` to PATH.
Any program that wants to use postgres client functionality require these DLLs found in `/bin`:
- libcrypto-3-x64.dll
- libiconv-2.dll
- libintl-9.dll
- libpq.dll
- libssl-3-x64.dll
- libwinpthread-1.dll
If you want to compile with MSVC, you will need to copy `C:/Program Files/PostgreSQL/<version>/bin/libpq.lib`
into the `@VEXEROOT/thirdparty/pg/win64/msvc` directory.
Navigate to `C:/Program Files/PostgreSQL/<version>/include`. There you will find the files:
- `libpq-fe.h`
- `pg_config.h`
- `postgres_ext.h`
Copy the header files into `@VEXEROOT/thirdparty/pg/libpq`. You can now compile programs using the `db.pg` module.
---
After building an executable that uses `db.pg`, you may want to distribute it to others
who might not have the postgres DLLs installed on their machine. All you need to do is to
make sure a copy of all the required DLLs are in the same folder as your executable.
```
## Getting Started with [PostgreSQL](https://www.postgresqltutorial.com/postgresql-getting-started)
Read this section to learn how to install and connect to PostgreSQL
[*Windows*](https://www.postgresqltutorial.com/install-postgresql);
[*Linux*](https://www.postgresqltutorial.com/postgresql-getting-started/install-postgresql-linux);
[*macOS*](https://www.postgresqltutorial.com/postgresql-getting-started/install-postgresql-macos).
When you use `pg.connect(pg.Config{ ... })`, empty `Config` fields are omitted from the
generated libpq connection string. That lets libpq defaults, `PGPASSWORD`, and `.pgpass`
apply when you do not set those fields in code.
## Thread Safety & Connection Pool
`pg.connect()` returns a `&DB` that is safe to share across V threads. Internally
`DB` holds a pool of `Conn` objects (one libpq `PGconn*` each); every method on
`DB` transparently checks a `Conn` out of the pool for the duration of the call
and returns it when done. This matches Go's `database/sql.DB` model.
```v ignore
mut db := pg.connect(pg.Config{ ... })!
defer { db.close() or {} }
// Pool defaults: unlimited open conns, 2 idle conns kept warm, no lifetime cap.
// Tune them like Go:
db.set_max_open_conns(50)
db.set_max_idle_conns(10)
db.set_conn_max_lifetime(30 * time.minute)
```
For operations that must run on the **same physical connection** — LISTEN/NOTIFY,
session-scoped prepared statements, manual transactions — pin a conn with
`db.conn()` or open a transaction with `db.begin()`:
```v ignore
// Pinned connection: returned to the pool when conn.close() is called.
mut c := db.conn()!
defer { c.close() or {} }
c.listen('my_channel')!
// Transaction: the conn is pinned for the lifetime of the Tx and released on
// commit() or rollback().
mut tx := db.begin()!
tx.exec('UPDATE accounts SET balance = balance - 100 WHERE id = 1')!
tx.exec('UPDATE accounts SET balance = balance + 100 WHERE id = 2')!
tx.commit()!
```
## Using Parameterized Queries
Parameterized queries (exec_param, etc.) in V require the use of the following syntax: ($n).
The number following the $ specifies which parameter from the argument array to use.
```v ignore
db.exec_param_many('INSERT INTO users (username, password) VALUES ($1, $2)', ['tom', 'securePassword'])!
db.exec_param('SELECT * FROM users WHERE username = ($1) limit 1', 'tom')!
```
## Using LISTEN/NOTIFY
PostgreSQL's LISTEN/NOTIFY mechanism allows you to build event-driven applications. One
connection can send notifications on a channel, and all connections listening on that
channel will receive them.
### Basic Usage
LISTEN/NOTIFY is session-scoped, so you must pin a `Conn` from the pool —
calling `db.listen()` would only listen on whichever pooled conn happens to
serve that one call.
```v ignore
import db.pg
fn main() {
mut db := pg.connect(pg.Config{ user: 'postgres', password: 'password', dbname: 'mydb' })!
defer { db.close() or {} }
mut c := db.conn()!
defer { c.close() or {} }
// Start listening on a channel
c.listen('my_channel')!
// From another connection or session, send a notification
c.notify('my_channel', 'Hello, World!')!
// Process incoming data from the server
c.consume_input()!
// Check for notifications
if notification := c.get_notification() {
println('Received notification on channel: ${notification.channel}')
println('Payload: ${notification.payload}')
println('From server process: ${notification.pid}')
}
// Stop listening
c.unlisten('my_channel')!
// Or unlisten from all channels
c.unlisten_all()!
}
```
### Event Loop with Polling
For real-time applications, you can use the socket file descriptor with select/poll:
```v ignore
import db.pg
import time
fn main() {
mut db := pg.connect(pg.Config{ user: 'postgres', password: 'password', dbname: 'mydb' })!
defer { db.close() or {} }
mut c := db.conn()!
defer { c.close() or {} }
c.listen('events')!
// Get socket fd for polling (useful with select/epoll)
socket_fd := c.socket()
println('Socket FD: ${socket_fd}')
// Simple polling loop
for {
c.consume_input()!
for {
notification := c.get_notification() or { break }
println('Event: ${notification.channel} - ${notification.payload}')
}
time.sleep(100 * time.millisecond)
}
}
```
### Available Methods
- `listen(channel string)` - Register to receive notifications on a channel
- `unlisten(channel string)` - Unregister from a specific channel
- `unlisten_all()` - Unregister from all channels
- `notify(channel string, payload string)` - Send a notification (payload can be empty)
- `consume_input()` - Read pending data from server (call before get_notification)
- `get_notification()` - Returns the next pending notification, or none for no notifications.
- `socket()` - Returns the connection's socket file descriptor for use with select/poll
+16
View File
@@ -0,0 +1,16 @@
#if !defined(PG_VERSION_NUM)
#error VERROR_MESSAGE PG_VERSION_NUM is not defined. Please install the development headers for PostgreSQL, they are usually in a package named libpq-dev
#endif
#if PG_VERSION_NUM < 100000
#define CONNECTION_CHECK_WRITABLE 9
#endif
#if PG_VERSION_NUM < 100000
#define CONNECTION_CONSUME 10
#endif
#if PG_VERSION_NUM < 120000
#define CONNECTION_GSS_STARTUP 11
#endif
+207
View File
@@ -0,0 +1,207 @@
module pg
import io
import time
// DB is a thread-safe handle to a PostgreSQL database, backed by a pool of
// `Conn` objects. It mirrors Go's `database/sql.DB` design: methods on `DB`
// transparently acquire a conn for the call, then release it back to the
// pool. For operations that must run on the same physical connection
// (LISTEN/NOTIFY, session-scoped prepared statements, manual transactions),
// use `db.conn()` to pin a conn or `db.begin()` to start a transaction.
pub struct DB {
mut:
pool &Pool = unsafe { nil }
}
// connect creates a new pool and opens an initial connection to verify the
// config works. The returned `&DB` is safe to share between threads.
pub fn connect(config Config, pcfg PoolConfig) !&DB {
return connect_with_conninfo(config.conninfo()!, pcfg)!
}
// connect_with_conninfo is the conninfo-string variant of `connect`.
pub fn connect_with_conninfo(conninfo string, pcfg PoolConfig) !&DB {
mut db := &DB{
pool: new_pool(conninfo, pcfg)
}
// Fail fast if the conninfo is wrong, rather than at first query.
probe := db.pool.acquire()!
db.pool.release(probe)
return db
}
// close shuts down the pool and tears down all idle connections.
// In-flight conns will be closed when released.
pub fn (mut db DB) close() ! {
if isnil(db.pool) {
return
}
db.pool.close()
}
// stats returns a snapshot of the pool state.
pub fn (mut db DB) stats() PoolStats {
return db.pool.stats()
}
// set_max_open_conns caps the total number of open connections.
// A value of 0 means unlimited (the default, like Go).
pub fn (mut db DB) set_max_open_conns(n int) {
db.pool.set_max_open(n)
}
// set_max_idle_conns caps the number of idle connections kept warm.
pub fn (mut db DB) set_max_idle_conns(n int) {
db.pool.set_max_idle(n)
}
// set_conn_max_lifetime sets the maximum amount of time a conn may be reused.
// A value of zero means conns are reused indefinitely.
pub fn (mut db DB) set_conn_max_lifetime(d time.Duration) {
db.pool.set_conn_max_lifetime(d)
}
// conn checks a conn out of the pool. The caller is responsible for calling
// `conn.close()` when done; failing to do so leaks the conn. Use this when
// you need session-bound operations like LISTEN/NOTIFY.
pub fn (mut db DB) conn() !&Conn {
return db.pool.acquire()
}
// validate borrows a conn from the pool and checks it is alive.
pub fn (mut db DB) validate() !bool {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.validate()
}
// reset is a no-op kept for ORM compatibility.
pub fn (mut db DB) reset() ! {
}
// ---- exec/query helpers (acquire-use-release) ----
// exec runs `query` on a pooled conn and returns the rows.
pub fn (mut db DB) exec(query string) ![]Row {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec(query)
}
// exec_no_null runs `query` and returns rows with no nullable fields.
pub fn (mut db DB) exec_no_null(query string) ![]RowNoNull {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_no_null(query)
}
// exec_result runs `query` and returns a `Result` (rows + column index).
pub fn (mut db DB) exec_result(query string) !Result {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_result(query)
}
// exec_one runs `query` and returns its first row.
pub fn (mut db DB) exec_one(query string) !Row {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_one(query)
}
// exec_param_many runs `query` with the given parameters.
pub fn (mut db DB) exec_param_many(query string, params []string) ![]Row {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_param_many(query, params)
}
// exec_param_many_result runs `query` with parameters and returns a `Result`.
pub fn (mut db DB) exec_param_many_result(query string, params []string) !Result {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_param_many_result(query, params)
}
// exec_param runs `query` with a single `$1` parameter.
pub fn (mut db DB) exec_param(query string, param string) ![]Row {
return db.exec_param_many(query, [param])
}
// exec_param2 runs `query` with two parameters (`$1`, `$2`).
pub fn (mut db DB) exec_param2(query string, param string, param2 string) ![]Row {
return db.exec_param_many(query, [param, param2])
}
// q_int runs `query` and returns the first column of the first row as int.
pub fn (mut db DB) q_int(query string) !int {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.q_int(query)
}
// q_string runs `query` and returns the first column of the first row as string.
pub fn (mut db DB) q_string(query string) !string {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.q_string(query)
}
// q_strings runs `query` and returns the full row set (alias of `exec`).
pub fn (mut db DB) q_strings(query string) ![]Row {
return db.exec(query)
}
// copy_expert runs a COPY command on a pooled conn.
pub fn (mut db DB) copy_expert(query string, mut file io.ReaderWriter) !int {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.copy_expert(query, mut file)
}
// ---- prepared statements ----
//
// NOTE: prepared statements are session-scoped. Calling `prepare` on `DB`
// only registers the statement on the conn that happened to serve the call.
// Use `db.conn()` to pin a conn for prepare+exec_prepared cycles.
// prepare registers a prepared statement on a transient conn.
// For repeated use, pin a conn via `db.conn()`.
pub fn (mut db DB) prepare(name string, query string, num_params int) ! {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.prepare(name, query, num_params)
}
// exec_prepared runs a previously-prepared statement.
pub fn (mut db DB) exec_prepared(name string, params []string) ![]Row {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_prepared(name, params)
}
// exec_prepared_result runs a previously-prepared statement and returns a `Result`.
pub fn (mut db DB) exec_prepared_result(name string, params []string) !Result {
mut c := db.pool.acquire()!
defer { c.close() or {} }
return c.exec_prepared_result(name, params)
}
// ---- transactions ----
// begin starts a new transaction and returns a `Tx` that pins a conn from
// the pool. The conn is released when `Tx.commit()` or `Tx.rollback()` is
// called. The default isolation level is REPEATABLE READ (matching the old
// single-conn API); pass `PQTransactionParam{ transaction_level: ... }` to
// override.
pub fn (mut db DB) begin(param PQTransactionParam) !&Tx {
mut c := db.pool.acquire()!
c.begin_on_conn(param) or {
c.close() or {}
return err
}
return &Tx{
conn: c
}
}
+171
View File
@@ -0,0 +1,171 @@
module pg
pub enum Oid {
t_bool = 16
t_bytea = 17
t_char = 18
t_name = 19
t_int8 = 20
t_int2 = 21
t_int2vector = 22
t_int4 = 23
t_regproc = 24
t_text = 25
t_oid = 26
t_tid = 27
t_xid = 28
t_cid = 29
t_vector = 30
t_pg_ddl_command = 32
t_pg_type = 71
t_pg_attribute = 75
t_pg_proc = 81
t_pg_class = 83
t_json = 114
t_xml = 142
t__xml = 143
t_pg_node_tree = 194
t__json = 199
t_smgr = 210
t_index_am_handler = 325
t_point = 600
t_lseg = 601
t_path = 602
t_box = 603
t_polygon = 604
t_line = 628
t__line = 629
t_cidr = 650
t__cidr = 651
t_float4 = 700
t_float8 = 701
t_abstime = 702
t_reltime = 703
t_tinterval = 704
t_unknown = 705
t_circle = 718
t__circle = 719
t_money = 790
t__money = 791
t_macaddr = 829
t_inet = 869
t__bool = 1000
t__bytea = 1001
t__char = 1002
t__name = 1003
t__int2 = 1005
t__int2vector = 1006
t__int4 = 1007
t__regproc = 1008
t__text = 1009
t__tid = 1010
t__xid = 1011
t__cid = 1012
t__vector = 1013
t__bpchar = 1014
t__varchar = 1015
t__int8 = 1016
t__point = 1017
t__lseg = 1018
t__path = 1019
t__box = 1020
t__float4 = 1021
t__float8 = 1022
t__abstime = 1023
t__reltime = 1024
t__tinterval = 1025
t__polygon = 1027
t__ = 1028
t_aclitem = 1033
t__aclitem = 1034
t__macaddr = 1040
t__inet = 1041
t_bpchar = 1042
t_varchar = 1043
t_date = 1082
t_time = 1083
t_timestamp = 1114
t__timestamp = 1115
t__date = 1182
t__time = 1183
t_timestamptz = 1184
t__timestamptz = 1185
t_interval = 1186
t__interval = 1187
t__numeric = 1231
t_pg_database = 1248
t__cstring = 1263
t_timetz = 1266
t__timetz = 1270
t_bit = 1560
t__bit = 1561
t_varbit = 1562
t__varbit = 1563
t_numeric = 1700
t_refcursor = 1790
t__refcursor = 2201
t_regprocedure = 2202
t_regoper = 2203
t_regoperator = 2204
t_regclass = 2205
t_regtype = 2206
t__regprocedure = 2207
t__regoper = 2208
t__regoperator = 2209
t__regclass = 2210
t__regtype = 2211
t_record = 2249
t_cstring = 2275
t_any = 2276
t_anyarray = 2277
t_v = 2278
t_trigger = 2279
t_language_handler = 2280
t_internal = 2281
t_opaque = 2282
t_anyelement = 2283
t__record = 2287
t_anynonarray = 2776
t_pg_authid = 2842
t_pg_auth_members = 2843
t__txid_snapshot = 2949
t_uuid = 2950
t__uuid = 2951
t_txid_snapshot = 2970
t_fdw_handler = 3115
t_pg_lsn = 3220
t__pg_lsn = 3221
t_tsm_handler = 3310
t_anyenum = 3500
t_tsvector = 3614
t_tsquery = 3615
t_gtsvector = 3642
t__tsvector = 3643
t__gtsvector = 3644
t__tsquery = 3645
t_regconfig = 3734
t__regconfig = 3735
t_regdictionary = 3769
t__regdictionary = 3770
t_jsonb = 3802
t__jsonb = 3807
t_anyrange = 3831
t_event_trigger = 3838
t_int4range = 3904
t__int4range = 3905
t_numrange = 3906
t__numrange = 3907
t_tsrange = 3908
t__tsrange = 3909
t_tstzrange = 3910
t__tstzrange = 3911
t_daterange = 3912
t__daterange = 3913
t_int8range = 3926
t__int8range = 3927
t_pg_shseclabel = 4066
t_regnamespace = 4089
t__regnamespace = 4090
t_regrole = 4096
t__regrole = 4097
}
+537
View File
@@ -0,0 +1,537 @@
module pg
import orm
import time
import net.conv
// ---- ORM on Conn (single pinned connection) ----
// select is used internally by V's ORM for processing `SELECT ` queries.
pub fn (c &Conn) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
c.ensure_active()!
where_with_tenant := orm.apply_tenant_filter(config.table, where)
query := orm.orm_select_gen(config, '"', true, '$', 1, where_with_tenant)
rows := pg_stmt_worker(c, query, where_with_tenant, data)!
mut ret := [][]orm.Primitive{}
for row in rows {
mut row_data := []orm.Primitive{}
for i, val in row.vals {
row_data << val_to_primitive(val, config.types[i])!
}
ret << row_data
}
return ret
}
// insert is used internally by V's ORM for processing `INSERT ` queries.
pub fn (c &Conn) insert(table orm.Table, data orm.QueryData) ! {
c.ensure_active()!
query, converted_data :=
orm.orm_stmt_gen(.pg, table, '"', .insert, true, '$', 1, data, orm.QueryData{})
pg_stmt_worker(c, query, converted_data, orm.QueryData{})!
}
// update is used internally by V's ORM for processing `UPDATE ` queries.
pub fn (c &Conn) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
c.ensure_active()!
where_with_tenant := orm.apply_tenant_filter(table, where)
query, _ := orm.orm_stmt_gen(.default, table, '"', .update, true, '$', 1, data,
where_with_tenant)
pg_stmt_worker(c, query, data, where_with_tenant)!
}
// delete is used internally by V's ORM for processing `DELETE ` queries.
pub fn (c &Conn) delete(table orm.Table, where orm.QueryData) ! {
c.ensure_active()!
where_with_tenant := orm.apply_tenant_filter(table, where)
query, _ := orm.orm_stmt_gen(.default, table, '"', .delete, true, '$', 1, orm.QueryData{},
where_with_tenant)
pg_stmt_worker(c, query, orm.QueryData{}, where_with_tenant)!
}
// last_id is used internally by V's ORM for post-processing `INSERT ` queries.
pub fn (c &Conn) last_id() int {
query := 'SELECT LASTVAL();'
return c.q_int(query) or { 0 }
}
// create is used internally by V's ORM for processing table creation queries (DDL).
pub fn (c &Conn) create(table orm.Table, fields []orm.TableField) ! {
query := orm.orm_table_gen(.pg, table, '"', true, 0, fields, pg_type_from_v, false) or {
return err
}
stmts := query.split(';')
for stmt in stmts {
if stmt != '' {
c.exec(stmt + ';')!
}
}
}
// drop is used internally by V's ORM for processing table destroying queries (DDL).
pub fn (c &Conn) drop(table orm.Table) ! {
query := 'DROP TABLE "${table.name}";'
c.exec(query)!
}
// orm_begin starts a transaction on this conn.
pub fn (c &Conn) orm_begin() ! {
c.begin_on_conn()!
}
// orm_commit commits the transaction on this conn.
pub fn (c &Conn) orm_commit() ! {
c.commit()!
}
// orm_rollback rolls back the transaction on this conn.
pub fn (c &Conn) orm_rollback() ! {
c.rollback()!
}
// orm_savepoint creates a savepoint on this conn.
pub fn (c &Conn) orm_savepoint(name string) ! {
c.savepoint(name)!
}
// orm_rollback_to rolls back to a savepoint on this conn.
pub fn (c &Conn) orm_rollback_to(name string) ! {
c.rollback_to(name)!
}
// orm_release_savepoint releases a savepoint on this conn.
pub fn (c &Conn) orm_release_savepoint(name string) ! {
c.release_savepoint(name)!
}
// ---- ORM on DB (acquire-use-release per call) ----
// select acquires a conn from the pool and runs the ORM SELECT on it.
pub fn (mut db DB) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
return c.select(config, data, where)
}
// insert acquires a conn from the pool, runs the ORM INSERT on it, and
// stashes LASTVAL() captured on the same session for the calling thread.
// V's `sql db { insert ... }` macro emits a follow-up `db.last_id()` call;
// stashing here lets that read return the correct id even though the pool
// may hand out a different conn for the second call.
pub fn (mut db DB) insert(table orm.Table, data orm.QueryData) ! {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
c.insert(table, data)!
db.pool.stash_last_id(c.last_id())
}
// update acquires a conn from the pool and runs the ORM UPDATE on it.
pub fn (mut db DB) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
c.update(table, data, where)!
}
// delete acquires a conn from the pool and runs the ORM DELETE on it.
pub fn (mut db DB) delete(table orm.Table, where orm.QueryData) ! {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
c.delete(table, where)!
}
// create acquires a conn from the pool and runs CREATE TABLE on it.
pub fn (mut db DB) create(table orm.Table, fields []orm.TableField) ! {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
c.create(table, fields)!
}
// drop acquires a conn from the pool and runs DROP TABLE on it.
pub fn (mut db DB) drop(table orm.Table) ! {
mut c := db.pool.acquire()!
defer {
c.close() or {}
}
c.drop(table)!
}
// last_id returns the id stashed by this thread's most recent `DB.insert`
// (or 0 if there is none). LASTVAL() itself is session-scoped, so calling
// it on a freshly-checked-out pool conn would return the wrong value or 0;
// `DB.insert` captures it on the same conn that ran the INSERT and stashes
// it per-thread, which is what V's ORM macro expects.
pub fn (mut db DB) last_id() int {
return db.pool.take_last_id()
}
// ---- ORM on Tx (use the pinned conn) ----
// select runs the ORM SELECT on the pinned transaction conn.
pub fn (mut tx Tx) select(config orm.SelectConfig, data orm.QueryData, where orm.QueryData) ![][]orm.Primitive {
tx.ensure_active()!
return tx.conn.select(config, data, where)
}
// insert runs the ORM INSERT on the pinned transaction conn.
pub fn (mut tx Tx) insert(table orm.Table, data orm.QueryData) ! {
tx.ensure_active()!
tx.conn.insert(table, data)!
}
// update runs the ORM UPDATE on the pinned transaction conn.
pub fn (mut tx Tx) update(table orm.Table, data orm.QueryData, where orm.QueryData) ! {
tx.ensure_active()!
tx.conn.update(table, data, where)!
}
// delete runs the ORM DELETE on the pinned transaction conn.
pub fn (mut tx Tx) delete(table orm.Table, where orm.QueryData) ! {
tx.ensure_active()!
tx.conn.delete(table, where)!
}
// create runs CREATE TABLE on the pinned transaction conn.
pub fn (mut tx Tx) create(table orm.Table, fields []orm.TableField) ! {
tx.ensure_active()!
tx.conn.create(table, fields)!
}
// drop runs DROP TABLE on the pinned transaction conn.
pub fn (mut tx Tx) drop(table orm.Table) ! {
tx.ensure_active()!
tx.conn.drop(table)!
}
// last_id returns the last inserted id on the pinned conn.
pub fn (mut tx Tx) last_id() int {
if tx.done || isnil(tx.conn) {
return 0
}
return tx.conn.last_id()
}
// orm_begin is a no-op on Tx (begin already ran when the Tx was created).
// It exists so Tx satisfies `orm.TransactionalConnection` for nested savepoints.
pub fn (mut tx Tx) orm_begin() ! {
}
// orm_commit commits the Tx.
pub fn (mut tx Tx) orm_commit() ! {
tx.commit()!
}
// orm_rollback rolls back the Tx.
pub fn (mut tx Tx) orm_rollback() ! {
tx.rollback()!
}
// orm_savepoint creates a savepoint inside the Tx.
pub fn (mut tx Tx) orm_savepoint(name string) ! {
tx.savepoint(name)!
}
// orm_rollback_to rolls back to a savepoint inside the Tx.
pub fn (mut tx Tx) orm_rollback_to(name string) ! {
tx.rollback_to(name)!
}
// orm_release_savepoint releases a savepoint inside the Tx.
pub fn (mut tx Tx) orm_release_savepoint(name string) ! {
tx.release_savepoint(name)!
}
// ---- utils ----
fn pg_stmt_binder(mut types []u32, mut vals []&char, mut lens []int, mut formats []int, d orm.QueryData) {
for data in d.data {
pg_stmt_match(mut types, mut vals, mut lens, mut formats, data)
}
}
fn pg_stmt_match_array[T](mut types []u32, mut vals []&char, mut lens []int, mut formats []int, data []T) {
for element in data {
pg_stmt_match(mut types, mut vals, mut lens, mut formats, orm.Primitive(element))
}
}
fn pg_stmt_match(mut types []u32, mut vals []&char, mut lens []int, mut formats []int, data orm.Primitive) {
match data {
bool {
types << u32(Oid.t_bool)
vals << &char(&data)
lens << int(sizeof(bool))
formats << 1
}
u8 {
types << u32(Oid.t_char)
vals << &char(&data)
lens << int(sizeof(u8))
formats << 1
}
u16 {
types << u32(Oid.t_int2)
num := conv.hton16(data)
vals << &char(&num)
lens << int(sizeof(u16))
formats << 1
}
u32 {
types << u32(Oid.t_int4)
num := conv.hton32(data)
vals << &char(&num)
lens << int(sizeof(u32))
formats << 1
}
u64 {
types << u32(Oid.t_int8)
num := conv.hton64(data)
vals << &char(&num)
lens << int(sizeof(u64))
formats << 1
}
i8 {
types << u32(Oid.t_char)
vals << &char(&data)
lens << int(sizeof(i8))
formats << 1
}
i16 {
types << u32(Oid.t_int2)
num := conv.hton16(u16(data))
vals << &char(&num)
lens << int(sizeof(i16))
formats << 1
}
int {
types << u32(Oid.t_int4)
num := conv.hton32(u32(data))
vals << &char(&num)
lens << int(sizeof(int))
formats << 1
}
i64 {
types << u32(Oid.t_int8)
num := conv.hton64(u64(data))
vals << &char(&num)
lens << int(sizeof(i64))
formats << 1
}
f32 {
types << u32(Oid.t_float4)
num := conv.htonf32(f32(data))
vals << &char(&num)
lens << int(sizeof(f32))
formats << 1
}
f64 {
types << u32(Oid.t_float8)
num := conv.htonf64(f64(data))
vals << &char(&num)
lens << int(sizeof(f64))
formats << 1
}
string {
// If paramTypes is NULL, or any particular element in the array is zero,
// the server infers a data type for the parameter symbol in the same way
// it would do for an untyped literal string.
types << u32(0)
vals << &char(data.str)
lens << data.len
formats << 0
}
time.Time {
datetime := data.format_ss()
types << u32(0)
vals << &char(datetime.str)
lens << datetime.len
formats << 0
}
orm.InfixType {
pg_stmt_match(mut types, mut vals, mut lens, mut formats, data.right)
}
orm.Null {
types << u32(0) // we do not know col type, let server infer
vals << &char(unsafe { nil }) // NULL pointer indicates NULL
lens << int(0) // ignored
formats << 0 // ignored
}
[]orm.Primitive {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]bool {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]f32 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]f64 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]i16 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]i64 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]i8 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]int {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]string {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]time.Time {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]u16 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]u32 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]u64 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]u8 {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
[]orm.InfixType {
pg_stmt_match_array(mut types, mut vals, mut lens, mut formats, data)
}
}
}
fn pg_type_from_v(typ int) !string {
str := match typ {
orm.type_idx['i8'], orm.type_idx['i16'], orm.type_idx['u8'], orm.type_idx['u16'] {
'SMALLINT'
}
orm.type_idx['bool'] {
'BOOLEAN'
}
orm.type_idx['int'], orm.type_idx['u32'] {
'INT'
}
orm.time_ {
'TIMESTAMP'
}
orm.enum_ {
'BIGINT'
}
orm.type_idx['i64'], orm.type_idx['u64'] {
'BIGINT'
}
orm.float[0] {
'REAL'
}
orm.float[1] {
'DOUBLE PRECISION'
}
orm.type_string {
'TEXT'
}
orm.serial {
'SERIAL'
}
else {
''
}
}
if str == '' {
return error('Unknown type ${typ}')
}
return str
}
fn val_to_primitive(val ?string, typ int) !orm.Primitive {
if str := val {
match typ {
// bool
orm.type_idx['bool'] {
return orm.Primitive(str == 't')
}
// i8
orm.type_idx['i8'] {
return orm.Primitive(str.i8())
}
// i16
orm.type_idx['i16'] {
return orm.Primitive(str.i16())
}
// int
orm.type_idx['int'] {
return orm.Primitive(str.int())
}
// i64
orm.type_idx['i64'] {
return orm.Primitive(str.i64())
}
// u8
orm.type_idx['u8'] {
data := str.i8()
return orm.Primitive(*unsafe { &u8(&data) })
}
// u16
orm.type_idx['u16'] {
data := str.i16()
return orm.Primitive(*unsafe { &u16(&data) })
}
// u32
orm.type_idx['u32'] {
data := str.int()
return orm.Primitive(*unsafe { &u32(&data) })
}
// u64
orm.type_idx['u64'] {
data := str.i64()
return orm.Primitive(*unsafe { &u64(&data) })
}
// f32
orm.type_idx['f32'] {
return orm.Primitive(str.f32())
}
// f64
orm.type_idx['f64'] {
return orm.Primitive(str.f64())
}
orm.type_string {
return orm.Primitive(str)
}
orm.time_ {
if str.contains_any(' /:-') {
date_time_str := time.parse(str)!
return orm.Primitive(date_time_str)
}
timestamp := str.int()
return orm.Primitive(time.unix(timestamp))
}
orm.enum_ {
return orm.Primitive(str.i64())
}
else {}
}
return error('Unknown field type ${typ}')
} else {
return orm.Null{}
}
}
File diff suppressed because it is too large. Load diff
+45
View File
@@ -0,0 +1,45 @@
// vtest build: started_postgres?
module main
import db.pg
@[table: 'demo']
struct Demo {
id int @[primary; sql: serial]
number f64
number2 f32
}
fn test_float_field() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
conn := 'host=localhost user=postgres password=12345678' // insert own connection string
mut db := pg.connect_with_conninfo(conn)!
defer {
db.close() or {}
}
sql db {
create table Demo
}!
demo := Demo{0, 9.58815, 9.58815}
sql db {
insert demo into Demo
}!
rows := sql db {
select from Demo
}!
assert rows[0].number == 9.58815
assert rows[0].number2 == 9.58815
sql db {
drop table Demo
}!
println(rows)
}
+332
View File
@@ -0,0 +1,332 @@
// vtest build: started_postgres?
module main
import orm
import db.pg
import time
struct TestCustomSqlType {
id int @[primary; sql: serial]
custom string @[sql_type: 'TEXT']
custom1 string @[sql_type: 'VARCHAR(191)']
custom2 string @[sql_type: 'TIMESTAMP']
custom3 string @[sql_type: 'uuid']
}
struct TestCustomWrongSqlType {
id int @[primary; sql: serial]
custom string
custom1 string @[sql_type: 'VARCHAR']
custom2 string @[sql_type: 'money']
custom3 string @[sql_type: 'xml']
}
struct TestTimeType {
mut:
id int @[primary; sql: serial]
username string
created_at time.Time @[sql_type: 'TIMESTAMP']
updated_at string @[sql_type: 'TIMESTAMP']
deleted_at time.Time
}
struct TestDefaultAttribute {
id string @[default: 'gen_random_uuid()'; primary; sql_type: 'uuid']
name string
created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
}
struct TestInsertDefaultValues {
id int @[primary; sql: serial]
example string @[default: '']
}
@[comment: 'This is a table comment']
struct TestCommentAttribute {
id string @[primary; sql: serial]
name string @[comment: 'real user name']
created_at string @[default: 'CURRENT_TIMESTAMP'; sql_type: 'TIMESTAMP']
}
fn test_pg_orm() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(
host: 'localhost'
user: 'postgres'
password: '12345678'
dbname: 'postgres'
) or { panic(err) }
defer {
db.close() or {}
}
db.exec('create extension if not exists pgcrypto') or { panic(err) }
table := orm.Table{
name: 'Test'
}
db.drop(table) or {}
db.create(table, [
orm.TableField{
name: 'id'
typ: typeof[string]().idx
// is_time: false
default_val: ''
is_arr: false
attrs: [
VAttribute{
name: 'primary'
has_arg: false
arg: ''
kind: .plain
},
VAttribute{
name: 'sql'
has_arg: true
arg: 'serial'
kind: .plain
},
]
},
orm.TableField{
name: 'name'
typ: typeof[string]().idx
// is_time: false
default_val: ''
is_arr: false
attrs: []
},
orm.TableField{
name: 'age'
typ: typeof[i64]().idx
// is_time: false
default_val: ''
is_arr: false
attrs: []
},
]) or { panic(err) }
db.insert(table, orm.QueryData{
fields: ['name', 'age']
data: [orm.string_to_primitive('Louis'), orm.int_to_primitive(101)]
}) or { panic(err) }
res := db.select(orm.SelectConfig{
table: table
aggregate_kind: .none
has_where: true
has_order: false
order: ''
order_type: .asc
has_limit: false
primary: 'id'
has_offset: false
fields: ['id', 'name', 'age']
types: [typeof[int]().idx, typeof[string]().idx, typeof[i64]().idx]
}, orm.QueryData{}, orm.QueryData{
fields: ['name', 'age']
data: [orm.Primitive('Louis'), orm.Primitive(101)]
types: []
kinds: [.eq, .eq]
is_and: [true]
}) or { panic(err) }
id := res[0][0]
name := res[0][1]
age := res[0][2]
assert id is int
if id is int {
assert id == 1
}
assert name is string
if name is string {
assert name == 'Louis'
}
assert age is i64
if age is i64 {
assert age == 101
}
/** test orm sql type
* - verify if all type create by attribute sql_type has created
*/
sql db {
drop table TestCustomSqlType
} or {}
sql db {
create table TestCustomSqlType
}!
mut result_custom_sql := db.exec("
SELECT DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'testcustomsqltype'
ORDER BY ORDINAL_POSITION
") or {
println(err)
panic(err)
}
mut information_schema_data_types_results := []string{}
information_schema_custom_sql := ['integer', 'text', 'character varying',
'timestamp without time zone', 'uuid']
for data_type in result_custom_sql {
x := data_type.vals[0]
information_schema_data_types_results << x?
}
assert information_schema_data_types_results == information_schema_custom_sql
/** test_orm_time_type
* - test time.Time v type with sql_type: 'TIMESTAMP'
* - test string v type with sql_type: 'TIMESTAMP'
* - test time.Time v type without
*/
today := time.parse('2022-07-16 15:13:27') or {
println(err)
panic(err)
}
model := TestTimeType{
username: 'hitalo'
created_at: today
updated_at: today.str()
deleted_at: today
}
sql db {
create table TestTimeType
}!
sql db {
insert model into TestTimeType
}!
results := sql db {
select from TestTimeType where username == 'hitalo'
}!
sql db {
drop table TestTimeType
}!
assert results[0].username == model.username
assert results[0].created_at == model.created_at
assert results[0].updated_at == model.updated_at
assert results[0].deleted_at == model.deleted_at
/** test default attribute
*/
sql db {
create table TestDefaultAttribute
}!
mut result_defaults := db.exec("
SELECT column_default
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'testdefaultattribute'
ORDER BY ORDINAL_POSITION
") or {
println(err)
panic(err)
}
mut information_schema_defaults_results := []string{}
for defaults in result_defaults {
x := defaults.vals[0]
information_schema_defaults_results << x or { '' }
}
sql db {
drop table TestDefaultAttribute
}!
assert ['gen_random_uuid()', '', 'CURRENT_TIMESTAMP'] == information_schema_defaults_results
/** test inserting only default values
*/
sql db {
create table TestInsertDefaultValues
}!
model_default_values := TestInsertDefaultValues{
example: ''
}
sql db {
insert model_default_values into TestInsertDefaultValues
}!
inserted_default_values := sql db {
select from TestInsertDefaultValues
}!
sql db {
drop table TestInsertDefaultValues
}!
assert inserted_default_values.len == 1
assert inserted_default_values[0].example == ''
/** test comment attribute
*/
sql db {
create table TestCommentAttribute
}!
mut column_comments := db.exec("
SELECT
a.attname AS column_name,
col_description(a.attrelid, a.attnum) AS column_comment
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relname = 'testcommentattribute'
AND n.nspname = 'public'
AND a.attnum > 0
AND NOT a.attisdropped
ORDER BY a.attnum
") or {
println(err)
panic(err)
}
mut table_comment := db.exec("
SELECT
nspname AS schema_name,
relname AS table_name,
obj_description(pc.oid) AS table_comment
FROM pg_class pc
JOIN pg_namespace pn ON pn.oid = pc.relnamespace
WHERE pc.relkind = 'r' AND pc.relname = 'testcommentattribute'
ORDER BY schema_name, table_name
") or {
println(err)
panic(err)
}
sql db {
drop table TestCommentAttribute
}!
mut information_schema_column_comment_results := []string{}
for comment in column_comments {
x := comment.vals[1]
information_schema_column_comment_results << x or { '' }
}
assert information_schema_column_comment_results == ['', 'real user name', '']
mut information_schema_table_comment_result := []string{}
for comment in table_comment {
x := comment.vals[2]
information_schema_table_comment_result << x or { '' }
}
assert information_schema_table_comment_result == ['This is a table comment']
}
+57
View File
@@ -0,0 +1,57 @@
// vtest build: started_postgres?
module main
import db.pg
struct Info {
table_schema string
relname string
attname string
typename string
typealign string
typlen int
}
fn deref(val ?string) string {
return val or { panic('no value') }
}
fn row_mapper(res pg.Result, row pg.Row) !Info {
return Info{
table_schema: deref(row.vals[res.cols['table_schema']])
relname: deref(row.vals[res.cols['relname']])
attname: deref(row.vals[res.cols['attname']])
typename: deref(row.vals[res.cols['typename']])
typealign: deref(row.vals[res.cols['typealign']])
typlen: deref(row.vals[res.cols['typlen']]).int()
}
}
fn test_large_exec() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })!
defer {
db.close() or {}
}
result := db.exec_result('
SELECT ischema.table_schema, c.relname, a.attname, t.typname, t.typalign, t.typlen
FROM pg_class c
JOIN information_schema.tables ischema on ischema.table_name = c.relname
JOIN pg_attribute a ON (a.attrelid = c.oid)
JOIN pg_type t ON (t.oid = a.atttypid)
WHERE
a.attnum >= 0
')!
infos := result.as_structs(row_mapper)!
assert result.rows.len > 0 && infos.len == result.rows.len
// println(infos)
}
+164
View File
@@ -0,0 +1,164 @@
// vtest build: started_postgres?
module main
import db.pg
import orm
fn test_large_exec() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })!
defer {
db.close() or {}
}
assert db.validate()!
rows := db.exec('
SELECT ischema.table_schema, c.relname, a.attname, t.typname, t.typalign, t.typlen
FROM pg_class c
JOIN information_schema.tables ischema on ischema.table_name = c.relname
JOIN pg_attribute a ON (a.attrelid = c.oid)
JOIN pg_type t ON (t.oid = a.atttypid)
WHERE
a.attnum >= 0
')!
for row in rows {
// We just need to access the memory to ensure it's properly allocated
row.str()
}
}
fn test_prepared() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })!
defer {
db.close() or {}
}
// Prepared statements are session-scoped, so pin a single conn.
mut c := db.conn()!
defer {
c.close() or {}
}
c.prepare('test_prepared', 'SELECT NOW(), $1 AS NAME', 1) or { panic(err) }
result := c.exec_prepared('test_prepared', ['hello world']) or { panic(err) }
assert result.len == 1
}
fn test_transaction() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })!
defer {
db.close() or {}
}
db.exec('drop table if exists users')!
db.exec('create table if not exists users (
id SERIAL PRIMARY KEY,
username TEXT,
last_name TEXT NULL DEFAULT NULL
)')!
// orm.TransactionalConnection requires a pinned conn (DB is pool-backed
// and cannot guarantee BEGIN/COMMIT land on the same physical conn).
mut c := db.conn()!
mut tc := orm.TransactionalConnection(c)
mut otx := orm.begin(mut tc)!
otx.transaction[int](fn (mut tx orm.Tx) !int {
return 1
})!
otx.commit()!
c.close()!
mut tx := db.begin()!
tx.exec("insert into users (username) values ('jackson')")!
tx.savepoint('savepoint1')!
tx.exec("insert into users (username) values ('kitty')")!
tx.rollback_to('savepoint1')!
tx.exec("insert into users (username) values ('mars')")!
tx.commit()!
rows := db.exec('select * from users')!
for row in rows {
// We just need to access the memory to ensure it's properly allocated
dump(row.str())
}
}
fn test_listen_notify() {
$if !network ? {
eprintln('> Skipping test ${@FN}, since `-d network` is not passed.')
eprintln('> This test requires a working postgres server running on localhost.')
return
}
mut db := pg.connect(pg.Config{ user: 'postgres', password: '12345678', dbname: 'postgres' })!
defer {
db.close() or {}
}
// LISTEN/NOTIFY is session-scoped; pin a single conn for the test.
mut c := db.conn()!
defer {
c.close() or {}
}
// Test listen
c.listen('test_channel')!
// Test notify with payload
c.notify('test_channel', 'hello world')!
// Consume input to process the notification
c.consume_input()!
// Get the notification
if notification := c.get_notification() {
assert notification.channel == 'test_channel'
assert notification.payload == 'hello world'
assert notification.pid > 0
} else {
assert false, 'Expected a notification but got none'
}
// Test notify without payload
c.notify('test_channel', '')!
c.consume_input()!
if notification := c.get_notification() {
assert notification.channel == 'test_channel'
assert notification.payload == ''
} else {
assert false, 'Expected a notification but got none'
}
// Test that no more notifications are pending
assert c.get_notification() == none
// Test unlisten
c.unlisten('test_channel')!
// Test unlisten_all
c.listen('channel1')!
c.listen('channel2')!
c.unlisten_all()!
// Test socket (should return valid fd)
socket_fd := c.socket()
assert socket_fd >= 0
}
+375
View File
@@ -0,0 +1,375 @@
module pg
import sync
import time
// PoolStats reports the current state of a `DB`'s connection pool.
pub struct PoolStats {
pub:
max_open_connections int // configured limit (0 = unlimited)
open_connections int // total in-use + idle conns
in_use int // conns currently checked out
idle int // conns parked as idle
wait_count int // number of callers currently blocked on acquire
}
// PoolConfig configures pool behavior at construction time.
@[params]
pub struct PoolConfig {
pub:
max_open_conns int // 0 = unlimited
max_idle_conns int = 2 // 0 = keep no idle conns
conn_max_lifetime time.Duration // 0 = unlimited
}
// IdleSlot is the pool's internal representation of a pooled libpq handle.
// The pool tracks raw PGconn* metadata in idle/waiter channels; `acquire`
// allocates a fresh `&Conn` wrapping the slot and `release` extracts the
// metadata back. Keeping the wrapper separate from the slot means a stale
// `&Conn` kept by user code after `close()` cannot be revived even when
// the pool re-hands the same physical PGconn* to another caller.
struct IdleSlot {
handle voidptr
created_at time.Time
mut:
bad bool
}
@[heap]
struct Pool {
mut:
mu &sync.Mutex = unsafe { nil }
conninfo string
max_open int
max_idle int
max_lifetime time.Duration
idle []IdleSlot
open_count int
waiters []chan IdleSlot
closed bool
// last_ids stores the per-thread last-inserted id so DB.last_id() returns
// the value captured on the same pooled conn that ran the INSERT, instead
// of calling LASTVAL() on whatever conn the pool happens to hand out next
// (which is session-scoped and would return 0 or a wrong value).
last_ids_mu &sync.Mutex = unsafe { nil }
last_ids map[u64]i64
}
fn new_pool(conninfo string, cfg PoolConfig) &Pool {
mut p := &Pool{
mu: sync.new_mutex()
last_ids_mu: sync.new_mutex()
conninfo: conninfo
max_open: cfg.max_open_conns
max_idle: cfg.max_idle_conns
max_lifetime: cfg.conn_max_lifetime
}
if p.max_open < 0 {
p.max_open = 0
}
if p.max_idle < 0 {
p.max_idle = 0
}
return p
}
// wrap_slot builds the fresh `&Conn` that gets handed to a caller. Each
// acquire allocates a new wrapper so a previously-checked-out (and since
// released) `&Conn` reference can never be reused to reach the underlying
// PGconn*.
fn (p &Pool) wrap_slot(slot IdleSlot) &Conn {
return &Conn{
conn: slot.handle
pool: unsafe { p }
created_at: slot.created_at
bad: slot.bad
}
}
fn (mut p Pool) acquire() !&Conn {
for {
p.mu.lock()
if p.closed {
p.mu.unlock()
return error('pg: pool is closed')
}
// Pop newest idle slot (LIFO keeps the freshest connection warm)
for p.idle.len > 0 {
slot := p.idle.last()
p.idle.delete_last()
if slot_expired(slot, p.max_lifetime) || slot_bad(slot) {
physical_close_handle(slot.handle)
p.open_count--
continue
}
p.mu.unlock()
return p.wrap_slot(slot)
}
// Capacity available: open a new conn outside the lock
if p.max_open == 0 || p.open_count < p.max_open {
p.open_count++
conninfo := p.conninfo
p.mu.unlock()
slot := connect_slot(conninfo) or {
p.mu.lock()
p.open_count--
p.mu.unlock()
return err
}
// close() may have run while we were dialing outside the lock.
// Honor the close contract by tearing the fresh handle down
// instead of returning a live Conn after shutdown.
p.mu.lock()
if p.closed {
p.open_count--
p.mu.unlock()
physical_close_handle(slot.handle)
return error('pg: pool is closed')
}
p.mu.unlock()
return p.wrap_slot(slot)
}
// At capacity: wait for a release/close/cap-raise to signal us.
// Senders transfer slot ownership via this channel; a sentinel slot
// with a nil handle means "capacity changed, retry the acquire
// loop" (used by set_max_open when raising the limit).
waiter := chan IdleSlot{cap: 1}
p.waiters << waiter
p.mu.unlock()
slot := <-waiter or { return error('pg: pool was closed while waiting for connection') }
if isnil(slot.handle) {
continue
}
return p.wrap_slot(slot)
}
return error('pg: unreachable')
}
fn (mut p Pool) release(conn &Conn) {
if isnil(conn) {
return
}
mut c := unsafe { conn }
// Detach the wrapper from the physical handle before doing anything else.
// This is what makes a stale `&Conn` reference held by user code inert:
// any subsequent method call on it will see `c.conn == nil` and error.
// It is also the idempotency guard against double-close — a second
// release() finds nothing to return to the pool and is a no-op.
if isnil(c.conn) {
return
}
slot := IdleSlot{
handle: c.conn
created_at: c.created_at
bad: c.bad
}
c.conn = unsafe { nil }
c.pool = unsafe { nil }
p.mu.lock()
if p.closed {
// close() only decremented open_count for slots it found parked;
// in-use conns are accounted for here as they trickle back.
p.open_count--
p.mu.unlock()
physical_close_handle(slot.handle)
return
}
// Discard broken or expired conns
if slot_bad(slot) || slot_expired(slot, p.max_lifetime) {
physical_close_handle(slot.handle)
p.open_count--
// A capacity slot just opened; hand it to a waiter as a fresh conn
if p.waiters.len > 0 && (p.max_open == 0 || p.open_count < p.max_open) {
waiter := p.waiters[0]
p.waiters.delete(0)
p.open_count++
conninfo := p.conninfo
p.mu.unlock()
new_slot := connect_slot(conninfo) or {
p.mu.lock()
p.open_count--
// Capacity is now open but the dial just failed. Wake every
// other parked waiter too, otherwise they hang forever waiting
// for a release that will never come (e.g. max_open=1).
extras := p.waiters.clone()
p.waiters = []chan IdleSlot{}
p.mu.unlock()
waiter.close()
for w in extras {
w.close()
}
return
}
p.mu.lock()
if p.closed {
// Pool closed during the dial: drop the new conn and signal the waiter.
p.open_count--
p.mu.unlock()
physical_close_handle(new_slot.handle)
waiter.close()
return
}
waiter <- new_slot
p.mu.unlock()
return
}
p.mu.unlock()
return
}
// If a recent set_max_open() shrank the cap below open_count, this
// returning slot has to be retired even when a waiter is queued —
// otherwise the fast hand-off keeps open_count pinned above the new
// limit forever under steady traffic. Wake the waiter with a retry
// sentinel so it re-evaluates capacity under the new cap.
if p.max_open > 0 && p.open_count > p.max_open {
physical_close_handle(slot.handle)
p.open_count--
if p.waiters.len > 0 {
waiter := p.waiters[0]
p.waiters.delete(0)
waiter <- IdleSlot{
handle: unsafe { nil }
}
}
p.mu.unlock()
return
}
// Healthy conn: prefer handing it directly to a waiter. Send under the
// lock (cap:1 makes it non-blocking) so close() can't slip in and orphan
// the popped waiter with a live conn.
if p.waiters.len > 0 {
waiter := p.waiters[0]
p.waiters.delete(0)
waiter <- slot
p.mu.unlock()
return
}
// Park as idle, unless we'd exceed max_idle (0 = keep no idle conns)
// or we are over a recently-shrunk max_open and need to converge.
if p.idle.len >= p.max_idle || (p.max_open > 0 && p.open_count > p.max_open) {
physical_close_handle(slot.handle)
p.open_count--
p.mu.unlock()
return
}
p.idle << slot
p.mu.unlock()
}
fn (mut p Pool) close() {
p.mu.lock()
if p.closed {
p.mu.unlock()
return
}
p.closed = true
for slot in p.idle {
physical_close_handle(slot.handle)
}
idle_len := p.idle.len
p.idle = []IdleSlot{}
p.open_count -= idle_len
for waiter in p.waiters {
waiter.close()
}
p.waiters = []chan IdleSlot{}
p.mu.unlock()
}
fn (mut p Pool) stats() PoolStats {
p.mu.lock()
stats := PoolStats{
max_open_connections: p.max_open
open_connections: p.open_count
idle: p.idle.len
in_use: p.open_count - p.idle.len
wait_count: p.waiters.len
}
p.mu.unlock()
return stats
}
fn (mut p Pool) set_max_open(n int) {
mut nn := n
if nn < 0 {
nn = 0
}
p.mu.lock()
p.max_open = nn
// Shrink: drop idle slots until we are back under the new cap. Without
// this, acquire() pops idle slots before checking max_open, so two
// callers could still both succeed after set_max_open_conns(1) when two
// warm conns are parked. In-use conns can't be reclaimed here — they get
// discarded on release once open_count is over the cap.
if nn > 0 {
for p.idle.len > 0 && p.open_count > nn {
slot := p.idle.last()
p.idle.delete_last()
physical_close_handle(slot.handle)
p.open_count--
}
}
// Raise: wake parked waiters so they can retry the acquire loop and
// dial against the new headroom. Without this nudge they stay blocked
// until some other release happens, which may never come if all current
// conns are long-lived.
if p.waiters.len > 0 && (nn == 0 || p.open_count < nn) {
spare := if nn == 0 { p.waiters.len } else { nn - p.open_count }
mut n_wake := if spare < p.waiters.len { spare } else { p.waiters.len }
for n_wake > 0 {
waiter := p.waiters[0]
p.waiters.delete(0)
waiter <- IdleSlot{
handle: unsafe { nil }
}
n_wake--
}
}
p.mu.unlock()
}
fn (mut p Pool) set_max_idle(n int) {
mut nn := n
if nn < 0 {
nn = 0
}
p.mu.lock()
p.max_idle = nn
for p.idle.len > nn {
slot := p.idle.last()
p.idle.delete_last()
physical_close_handle(slot.handle)
p.open_count--
}
p.mu.unlock()
}
fn (mut p Pool) set_conn_max_lifetime(d time.Duration) {
p.mu.lock()
p.max_lifetime = d
p.mu.unlock()
}
// stash_last_id records `id` as the last-inserted id for the current thread.
// Called by `DB.insert` right after running INSERT on the pinned conn, so the
// next `DB.last_id()` call on the same thread returns this exact value rather
// than running LASTVAL() on a different pooled session.
fn (mut p Pool) stash_last_id(id int) {
tid := sync.thread_id()
p.last_ids_mu.lock()
p.last_ids[tid] = i64(id)
p.last_ids_mu.unlock()
}
// take_last_id returns the last-inserted id stashed by this thread's most
// recent `DB.insert`, or 0 if there is none. Consuming on read keeps the
// map bounded as threads come and go, and prevents a future thread that
// reuses this tid from observing a stale id before doing its own insert.
fn (mut p Pool) take_last_id() int {
tid := sync.thread_id()
p.last_ids_mu.lock()
id := p.last_ids[tid] or { i64(0) }
p.last_ids.delete(tid)
p.last_ids_mu.unlock()
return int(id)
}
+167
View File
@@ -0,0 +1,167 @@
module pg
import io
// Tx is a database transaction. It pins a single `Conn` from the pool for
// the lifetime of the transaction, so all queries run on the same physical
// connection. The pinned conn is returned to the pool when `commit()` or
// `rollback()` is called; failing to call either leaks the conn.
@[heap]
pub struct Tx {
mut:
conn &Conn = unsafe { nil }
done bool
}
fn (tx &Tx) ensure_active() ! {
if tx.done {
return error('pg: transaction is already finished')
}
if isnil(tx.conn) {
return error('pg: transaction has no connection')
}
}
fn (mut tx Tx) finish() {
if tx.done {
return
}
tx.done = true
if !isnil(tx.conn) {
mut c := tx.conn
c.close() or {}
tx.conn = unsafe { nil }
}
}
// commit commits the transaction and returns the pinned conn to the pool.
pub fn (mut tx Tx) commit() ! {
tx.ensure_active()!
defer {
tx.finish()
}
tx.conn.commit()!
}
// rollback rolls back the transaction and returns the pinned conn to the pool.
pub fn (mut tx Tx) rollback() ! {
tx.ensure_active()!
defer {
tx.finish()
}
tx.conn.rollback()!
}
// savepoint creates a savepoint named `name`.
pub fn (mut tx Tx) savepoint(name string) ! {
tx.ensure_active()!
tx.conn.savepoint(name)!
}
// rollback_to rolls the transaction back to the savepoint named `name`.
pub fn (mut tx Tx) rollback_to(name string) ! {
tx.ensure_active()!
tx.conn.rollback_to(name)!
}
// release_savepoint releases the savepoint named `name`.
pub fn (mut tx Tx) release_savepoint(name string) ! {
tx.ensure_active()!
tx.conn.release_savepoint(name)!
}
// raw returns the pinned conn for advanced use cases. The caller MUST NOT
// call `close()` on it; the conn is owned by the transaction.
pub fn (mut tx Tx) raw() !&Conn {
tx.ensure_active()!
return tx.conn
}
// ---- exec/query helpers ----
// exec runs `query` on the pinned conn.
pub fn (mut tx Tx) exec(query string) ![]Row {
tx.ensure_active()!
return tx.conn.exec(query)
}
// exec_no_null runs `query` and returns rows with no nullable fields.
pub fn (mut tx Tx) exec_no_null(query string) ![]RowNoNull {
tx.ensure_active()!
return tx.conn.exec_no_null(query)
}
// exec_result runs `query` and returns a `Result`.
pub fn (mut tx Tx) exec_result(query string) !Result {
tx.ensure_active()!
return tx.conn.exec_result(query)
}
// exec_one runs `query` and returns its first row.
pub fn (mut tx Tx) exec_one(query string) !Row {
tx.ensure_active()!
return tx.conn.exec_one(query)
}
// exec_param_many runs `query` with the given parameters.
pub fn (mut tx Tx) exec_param_many(query string, params []string) ![]Row {
tx.ensure_active()!
return tx.conn.exec_param_many(query, params)
}
// exec_param_many_result runs `query` with parameters and returns a `Result`.
pub fn (mut tx Tx) exec_param_many_result(query string, params []string) !Result {
tx.ensure_active()!
return tx.conn.exec_param_many_result(query, params)
}
// exec_param runs `query` with one `$1` parameter.
pub fn (mut tx Tx) exec_param(query string, param string) ![]Row {
return tx.exec_param_many(query, [param])
}
// exec_param2 runs `query` with two parameters.
pub fn (mut tx Tx) exec_param2(query string, param string, param2 string) ![]Row {
return tx.exec_param_many(query, [param, param2])
}
// q_int runs `query` and returns the first column of the first row as int.
pub fn (mut tx Tx) q_int(query string) !int {
tx.ensure_active()!
return tx.conn.q_int(query)
}
// q_string runs `query` and returns the first column of the first row as string.
pub fn (mut tx Tx) q_string(query string) !string {
tx.ensure_active()!
return tx.conn.q_string(query)
}
// q_strings runs `query` and returns the full row set (alias of `exec`).
pub fn (mut tx Tx) q_strings(query string) ![]Row {
return tx.exec(query)
}
// prepare registers a prepared statement on the pinned conn.
pub fn (mut tx Tx) prepare(name string, query string, num_params int) ! {
tx.ensure_active()!
return tx.conn.prepare(name, query, num_params)
}
// exec_prepared runs a previously-prepared statement on the pinned conn.
pub fn (mut tx Tx) exec_prepared(name string, params []string) ![]Row {
tx.ensure_active()!
return tx.conn.exec_prepared(name, params)
}
// exec_prepared_result runs a previously-prepared statement on the pinned conn.
pub fn (mut tx Tx) exec_prepared_result(name string, params []string) !Result {
tx.ensure_active()!
return tx.conn.exec_prepared_result(name, params)
}
// copy_expert runs a COPY command on the pinned conn.
pub fn (mut tx Tx) copy_expert(query string, mut file io.ReaderWriter) !int {
tx.ensure_active()!
return tx.conn.copy_expert(query, mut file)
}