|
1 |
| -# async_postgres |
2 |
| -PostgreSQL binary module for Garry's Mod |
| 1 | + |
| 2 | + |
| 3 | +Simple PostgreSQL connector module for Garry's Mod |
| 4 | +using [libpq] with asynchronousity in mind. |
| 5 | + |
| 6 | +Besides binary module you'll probably need to add [`async_postgres.lua`][lua module] to your project. |
| 7 | + |
| 8 | +## Features |
| 9 | +* Fully asynchronous, yet allows to wait a query to finish |
| 10 | +* Provides full simplified [libpq] interface |
| 11 | +* Simple, robust and efficient |
| 12 | +* Flexible [lua module] which extends functionality |
| 13 | +* [Type friendly][LuaLS] [lua module] with documentatio |
| 14 | + |
| 15 | +## Installation |
| 16 | +1. Go to [releases](https://github.com/Pika-Software/gmsv_async_postgres/releases) |
| 17 | +2. Download `async_postgres.lua` and `gmsv_async_postgres_xxx.dll` files |
| 18 | +> [!NOTE] |
| 19 | +> If are unsure which binary to download, you can run this command inside console of your server |
| 20 | +> ```lua |
| 21 | +> lua_run print("gmsv_async_postgres_" .. (system.IsWindows() and "win" or system.IsOSX() and "osx" or "linux") .. (jit.arch == "x64" and "64" or not system.IsLinux() and "32" or "") .. ".dll") |
| 22 | +> ``` |
| 23 | +3. Put `gmsv_async_postgres_xxx.dll` inside `garrysmod/lua/bin/` folder (if folder does not exists, create it) |
| 24 | +4. Put `async_postgres.lua` inside `garrysmod/lua/autorun/server/` or inside your project folder |
| 25 | +5. Profit 🎉 |
| 26 | +
|
| 27 | +## Caveats |
| 28 | +* when `queryParams` is used and parameters is `string`, then string will be sent as bytes!<br> |
| 29 | + You'll need to convert numbers **exclipitly** to `number` type, otherwise |
| 30 | + PostgreSQL will interpent parameter as binary integer, and will return error |
| 31 | + or unexpected results may happend. |
| 32 | +
|
| 33 | +* Result rows are returned as strings, you'll need to convert them to numbers if needed. |
| 34 | +* You'll need to use `Client:unescapeBytea(...)` to convert bytea data to string from reuslt. |
| 35 | +
|
| 36 | +## Usage |
| 37 | +`async_postgres.Client` usage example |
| 38 | +```lua |
| 39 | +-- Lua module will require the binary module automatically, and will provide Client and Pool classes |
| 40 | +include("async_postgres.lua") |
| 41 | +
|
| 42 | +-- See https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING for connection string format |
| 43 | +local client = async_postgres.Client("postgresql://postgres:postgres@localhost") |
| 44 | +
|
| 45 | +-- Do not forget to connect to the server |
| 46 | +client:connect(function(ok, err) |
| 47 | + assert(ok, err) |
| 48 | + print("Connected to " .. client:host() .. ":" .. client:port()) |
| 49 | +end) |
| 50 | +
|
| 51 | +-- PostgreSQL can only process one query at a time, |
| 52 | +-- but async_postgres.Client has internal queue for queries |
| 53 | +-- so you can queue up as many queries as you want |
| 54 | +-- and they will be executed one by one when possible |
| 55 | +-- |
| 56 | +-- For example this query will be executed after the connection is established |
| 57 | +client:query("select now()", function(ok, res) |
| 58 | + assert(ok, res) |
| 59 | + print("Current time is " .. res.rows[1].now) |
| 60 | +end) |
| 61 | +
|
| 62 | +-- You can also pass parameters to the query without the need of using client:escape(...) |
| 63 | +client:queryParams("select $1 as a, $2 as b", { 5, 10 }, function(ok, res) |
| 64 | + assert(ok, res) |
| 65 | + PrintTable(res.rows) -- will output [1] = { ["a"] = "5", ["b"] = "10" } |
| 66 | +end) |
| 67 | +
|
| 68 | +client:prepare("test", "select $1 as value", function(ok, err) |
| 69 | + assert(ok, err) |
| 70 | +end) |
| 71 | +
|
| 72 | +client:queryPrepared("test", { "foobar" }, function(ok, res) |
| 73 | + assert(ok, res) |
| 74 | + print("Value is: " .. res.rows[1].value) -- will output "Value is foobar" |
| 75 | +end) |
| 76 | +
|
| 77 | +-- You can also wait for all queries to finish |
| 78 | +while client:isBusy() do |
| 79 | + -- Will wait for the next query/reset to finish |
| 80 | + client:wait() |
| 81 | +end |
| 82 | +
|
| 83 | +-- And :close() will close the connection to the server |
| 84 | +client:close() -- passing true will wait for all queries to finish, but we did waiting in the loop above |
| 85 | +``` |
| 86 | +
|
| 87 | +`async_postgres.Pool` usage example |
| 88 | +```lua |
| 89 | +local pool = async_postgres.Pool("postgresql://postgres:postgres@localhost") |
| 90 | + |
| 91 | +-- You can make same queries as with Client |
| 92 | +-- but with Pool you don't need to worry about connection |
| 93 | +-- Pool will manage connections for you |
| 94 | +pool:query("select now()", function(ok, res) |
| 95 | + assert(ok, res) |
| 96 | + print("Current time is " .. res.rows[1].now) |
| 97 | +end) |
| 98 | + |
| 99 | +-- With Pool you also can create transactions |
| 100 | +-- callback will be called in coroutine with ctx |
| 101 | +-- which has query methods that don't need callback |
| 102 | +-- and will return results directly |
| 103 | +pool:transaction(function(ctx) |
| 104 | + -- you can also use ctx.client for methods like :db() if you want |
| 105 | + |
| 106 | + local res = ctx:query("select now()") |
| 107 | + print("Time in transaction is: " .. res.rows[1].now) |
| 108 | + |
| 109 | + local res = ctx:query("select $1 as value", { "barfoo" }) |
| 110 | + print("Value in transaction is: " .. res.rows[1].value) -- will output "Value in transaction is barfoo" |
| 111 | + |
| 112 | + -- If error happens in transaction, it will be rolled back |
| 113 | + error("welp something went wrong :p) |
| 114 | + |
| 115 | + -- if no error happens, transaction will be commited |
| 116 | +end) |
| 117 | + |
| 118 | +-- Or you can use :connect() to create your own transactions or for smth else |
| 119 | +-- :connect() will acquire first available connected Client from the Pool |
| 120 | +-- and you'll need to call client:release() when you're done |
| 121 | +pool:connect(function(client) |
| 122 | + client:query("select now()", function(ok, res) |
| 123 | + client:release() -- don't forget to release the client! |
| 124 | + |
| 125 | + assert(ok, res) |
| 126 | + print("Current time is " .. res.rows[1].now) |
| 127 | + end) |
| 128 | +end) |
| 129 | +``` |
| 130 | + |
| 131 | +## Reference |
| 132 | +### Enums |
| 133 | +- `async_postgres.VERSION`: string, e.g., "1.0.0" |
| 134 | +- `async_postgres.BRANCH`: string, e.g., "main" |
| 135 | +- `async_postgres.URL`: string |
| 136 | +- `async_postgres.PQ_VERSION`: number, e.g., 160004 |
| 137 | +- `async_postgres.LUA_API_VERSION`: number, e.g., 1 |
| 138 | +- `async_postgres.CONNECTION_OK`: number |
| 139 | +- `async_postgres.CONNECTION_BAD`: number |
| 140 | +- `async_postgres.PQTRANS_IDLE`: number |
| 141 | +- `async_postgres.PQTRANS_ACTIVE`: number |
| 142 | +- `async_postgres.PQTRANS_INTRANS`: number |
| 143 | +- `async_postgres.PQTRANS_INERROR`: number |
| 144 | +- `async_postgres.PQTRANS_UNKNOWN`: number |
| 145 | +- `async_postgres.PQERRORS_TERSE`: number |
| 146 | +- `async_postgres.PQERRORS_DEFAULT`: number |
| 147 | +- `async_postgres.PQERRORS_VERBOSE`: number |
| 148 | +- `async_postgres.PQERRORS_SQLSTATE`: number |
| 149 | +- `async_postgres.PQSHOW_CONTEXT_NEVER`: number |
| 150 | +- `async_postgres.PQSHOW_CONTEXT_ERRORS`: number |
| 151 | +- `async_postgres.PQSHOW_CONTEXT_ALWAYS`: number |
| 152 | + |
| 153 | +### `async_postgres.Client` Class |
| 154 | +- `async_postgres.Client(conninfo)`: Creates a new client instance |
| 155 | + |
| 156 | +#### Methods |
| 157 | +- `Client:connect(callback)`: Connects to the database (or reconnects if was connected) |
| 158 | +- `Client:reset(callback)`: Reconnects to the database |
| 159 | +- `Client:query(query, callback)`: Sends a query to the server |
| 160 | +- `Client:queryParams(query, params, callback)`: Sends a query with parameters to the server |
| 161 | +- `Client:prepare(name, query, callback)`: Creates a prepared statement |
| 162 | +- `Client:queryPrepared(name, params, callback)`: Executes a prepared statement |
| 163 | +- `Client:describePrepared(name, callback)`: Describes a prepared statement |
| 164 | +- `Client:describePortal(name, callback)`: Describes a portal |
| 165 | +- `Client:close(wait)`: Closes the connection to the database |
| 166 | +- `Client:pendingQueries()`: Returns the number of queued queries (excludes currently executing query) |
| 167 | +- `Client:db()`: Returns the database name |
| 168 | +- `Client:user()`: Returns the user name |
| 169 | +- `Client:pass()`: Returns the password |
| 170 | +- `Client:host()`: Returns the host name |
| 171 | +- `Client:hostaddr()`: Returns the server IP address |
| 172 | +- `Client:port()`: Returns the port |
| 173 | +- `Client:transactionStatus()`: Returns the transaction status (see `PQTRANS` enums) |
| 174 | +- `Client:parameterStatus(paramName)`: Looks up a current parameter setting |
| 175 | +- `Client:protocolVersion()`: Interrogates the frontend/backend protocol being used |
| 176 | +- `Client:serverVersion()`: Returns the server version as integer |
| 177 | +- `Client:errorMessage()`: Returns last error message |
| 178 | +- `Client:backendPID()`: Returns the backend process ID |
| 179 | +- `Client:sslInUse()`: Returns true if SSL is used |
| 180 | +- `Client:sslAttribute(name)`: Returns SSL-related information |
| 181 | +- `Client:encryptPassword(user, password, algorithm)`: Prepares the encrypted form of a password |
| 182 | +- `Client:escape(str)`: Escapes a string for use within an SQL command |
| 183 | +- `Client:escapeIdentifier(str)`: Escapes a string for use as an SQL identifier |
| 184 | +- `Client:escapeBytea(str)`: Escapes binary data for use within an SQL command |
| 185 | +- `Client:unescapeBytea(str)`: Converts an escaped bytea data into binary data |
| 186 | +- `Client:release(suppress)`: Releases the client back to the pool |
| 187 | + |
| 188 | +#### Events |
| 189 | +- `Client:onNotify(channel, payload, backendPID)`: Called when a NOTIFY message is received |
| 190 | +- `Client:onNotice(message, errdata)`: Called when the server sends a notice/warning message during a query |
| 191 | +- `Client:onError(message)`: Called whenever an error occurs inside connect/query callback |
| 192 | +- `Client:onEnd()`: Called whenever connection to the server is lost/closed |
| 193 | + |
| 194 | +### `async_postgres.Pool` Class |
| 195 | +- `async_postgres.Pool(conninfo)`: Creates a new pool instance |
| 196 | + |
| 197 | +#### Methods |
| 198 | +- `Pool:connect(callback)`: Acquires a client from the pool |
| 199 | +- `Pool:query(query, callback)`: Sends a query to the server |
| 200 | +- `Pool:queryParams(query, params, callback)`: Sends a query with parameters to the server |
| 201 | +- `Pool:prepare(name, query, callback)`: Creates a prepared statement |
| 202 | +- `Pool:queryPrepared(name, params, callback)`: Executes a prepared statement |
| 203 | +- `Pool:describePrepared(name, callback)`: Describes a prepared statement |
| 204 | +- `Pool:describePortal(name, callback)`: Describes a portal |
| 205 | +- `Pool:transaction(callback)`: Begins a transaction and runs the callback with a transaction context |
| 206 | + |
| 207 | +#### Events |
| 208 | +- `Pool:onConnect(client)`: Called when a new client connection is established |
| 209 | +- `Pool:onAcquire(client)`: Called when a client is acquired |
| 210 | +- `Pool:onError(message, client)`: Called when an error occurs |
| 211 | +- `Pool:onRelease(client)`: Called when a client is released back to the pool |
| 212 | + |
| 213 | +### I need more documentation! |
| 214 | +Please check [`async_postgres.lua`][lua module] for full interface documentation. |
| 215 | + |
| 216 | +And you also can check [libpq documentation][libpq] for more information on method behavior. |
| 217 | + |
| 218 | +## Credit |
| 219 | +* [goobie-mysql](https://github.com/Srlion/goobie-mysql) for inspiring with many good ideas |
| 220 | +* @unknown-gd for being a good friend |
| 221 | + |
| 222 | + |
| 223 | +[libpq]: https://www.postgresql.org/docs/16/libpq.html |
| 224 | +[lua module]: ./async_postgres.lua |
| 225 | +[LuaLS]: https://luals.github.io/ |
0 commit comments