Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
@@ -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']
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user