From 6ba756c965f554eddbad6855e10aa93b5642318b Mon Sep 17 00:00:00 2001 From: Jobians Techie <88005779+Jobians@users.noreply.github.com> Date: Thu, 12 Jun 2025 23:18:42 +0100 Subject: [PATCH 1/3] Fix PGAPPNAME env access by prioritizing connection.application_name Use connection.application_name if provided before falling back to PGAPPNAME env var to avoid unnecessary env access errors. --- deno/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno/src/index.js b/deno/src/index.js index 98a82345..aa7a920f 100644 --- a/deno/src/index.js +++ b/deno/src/index.js @@ -482,8 +482,8 @@ function parseOptions(a, b) { {} ), connection : { - application_name: env.PGAPPNAME || 'postgres.js', ...o.connection, + application_name: o.connection?.application_name ?? env.PGAPPNAME ?? 'postgres.js', ...Object.entries(query).reduce((acc, [k, v]) => (k in defaults || (acc[k] = v), acc), {}) }, types : o.types || {}, From 606f88d2b010193f0c66676dea9e6359d2b2e51c Mon Sep 17 00:00:00 2001 From: Jobians Date: Wed, 18 Jun 2025 18:57:14 +0100 Subject: [PATCH 2/3] refactor: replace std@0.132.0 node imports with node: built-ins --- deno/mod.js.bak | 2 + deno/polyfills.js | 4 +- deno/polyfills.js.bak | 189 +++ deno/src/bytes.js | 2 +- deno/src/bytes.js.bak | 79 ++ deno/src/connection.js | 6 +- deno/src/connection.js.bak | 1045 ++++++++++++++ deno/src/errors.js.bak | 53 + deno/src/index.js | 6 +- deno/src/index.js.bak | 567 ++++++++ deno/src/large.js | 2 +- deno/src/large.js.bak | 70 + deno/src/query.js.bak | 173 +++ deno/src/queue.js.bak | 31 + deno/src/result.js.bak | 16 + deno/src/subscribe.js | 2 +- deno/src/subscribe.js.bak | 278 ++++ deno/src/types.js | 2 +- deno/src/types.js.bak | 368 +++++ deno/tests/bootstrap.js | 2 +- deno/tests/bootstrap.js.bak | 34 + deno/tests/index.js | 8 +- deno/tests/index.js.bak | 2620 +++++++++++++++++++++++++++++++++++ deno/tests/test.js | 4 +- deno/tests/test.js.bak | 88 ++ deno/types/index.d.ts | 6 +- deno/types/index.d.ts.bak | 732 ++++++++++ 27 files changed, 6367 insertions(+), 22 deletions(-) create mode 100644 deno/mod.js.bak create mode 100644 deno/polyfills.js.bak create mode 100644 deno/src/bytes.js.bak create mode 100644 deno/src/connection.js.bak create mode 100644 deno/src/errors.js.bak create mode 100644 deno/src/index.js.bak create mode 100644 deno/src/large.js.bak create mode 100644 deno/src/query.js.bak create mode 100644 deno/src/queue.js.bak create mode 100644 deno/src/result.js.bak create mode 100644 deno/src/subscribe.js.bak create mode 100644 deno/src/types.js.bak create mode 100644 deno/tests/bootstrap.js.bak create mode 100644 deno/tests/index.js.bak create mode 100644 deno/tests/test.js.bak create mode 100644 deno/types/index.d.ts.bak diff --git a/deno/mod.js.bak b/deno/mod.js.bak new file mode 100644 index 00000000..7cbf18c3 --- /dev/null +++ b/deno/mod.js.bak @@ -0,0 +1,2 @@ +// @deno-types="./types/index.d.ts" +export { default } from './src/index.js' diff --git a/deno/polyfills.js b/deno/polyfills.js index 71ee694d..eb8c36a9 100644 --- a/deno/polyfills.js +++ b/deno/polyfills.js @@ -1,7 +1,7 @@ /* global Deno */ -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' -import { isIP } from 'https://deno.land/std@0.132.0/node/net.ts' +import { Buffer } from 'node:buffer' +import { isIP } from 'node:net' const events = () => ({ data: [], error: [], drain: [], connect: [], secureConnect: [], close: [] }) diff --git a/deno/polyfills.js.bak b/deno/polyfills.js.bak new file mode 100644 index 00000000..71ee694d --- /dev/null +++ b/deno/polyfills.js.bak @@ -0,0 +1,189 @@ +/* global Deno */ + +import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' +import { isIP } from 'https://deno.land/std@0.132.0/node/net.ts' + +const events = () => ({ data: [], error: [], drain: [], connect: [], secureConnect: [], close: [] }) + +class Socket { + constructor() { + return createSocket() + } +} + +function createSocket() { + let paused + , resume + , keepAlive + + const socket = { + error, + success, + readyState: 'open', + setKeepAlive: x => { + keepAlive = x + socket.raw && socket.raw.setKeepAlive && socket.raw.setKeepAlive(x) + }, + connect: (port, hostname) => { + socket.raw = null + socket.readyState = 'connecting' + typeof port === 'string' + ? Deno.connect({ transport: 'unix', path: socket.path = port }).then(success, error) + : Deno.connect({ transport: 'tcp', port: socket.port = port, hostname: socket.hostname = hostname || 'localhost' }).then(success, error) // eslint-disable-line + return socket + }, + pause: () => { + paused = new Promise(r => resume = r) + }, + resume: () => { + resume && resume() + paused = null + }, + isPaused: () => !!paused, + removeAllListeners: () => socket.events = events(), + events: events(), + raw: null, + on: (x, fn) => socket.events[x].push(fn), + once: (x, fn) => { + if (x === 'data') + socket.break = true + const e = socket.events[x] + e.push(once) + once.once = fn + function once(...args) { + fn(...args) + e.indexOf(once) > -1 && e.splice(e.indexOf(once), 1) + } + }, + removeListener: (x, fn) => { + socket.events[x] = socket.events[x].filter(x => x !== fn && x.once !== fn) + }, + write: (x, cb) => { + socket.raw.write(x).then(l => { + l < x.length + ? socket.write(x.slice(l), cb) + : (cb && cb(null)) + }).catch(err => { + cb && cb() + call(socket.events.error, err) + }) + return false + }, + destroy: () => close(), + end: (x) => { + x && socket.write(x) + close() + } + } + + return socket + + async function success(raw) { + if (socket.readyState !== 'connecting') + return raw.close() + + const encrypted = socket.encrypted + socket.raw = raw + keepAlive != null && raw.setKeepAlive && raw.setKeepAlive(keepAlive) + socket.readyState = 'open' + socket.encrypted + ? call(socket.events.secureConnect) + : call(socket.events.connect) + + const b = new Uint8Array(1024) + let result + + try { + while ((result = socket.readyState === 'open' && await raw.read(b))) { + call(socket.events.data, Buffer.from(b.subarray(0, result))) + if (!encrypted && socket.break && (socket.break = false, b[0] === 83)) + return socket.break = false + paused && await paused + } + } catch (e) { + if (e instanceof Deno.errors.BadResource === false) + error(e) + } + + if (!socket.encrypted || encrypted) + closed() + } + + function close() { + try { + socket.raw && socket.raw.close() + } catch (e) { + if (e instanceof Deno.errors.BadResource === false) + call(socket.events.error, e) + } + } + + function closed() { + if (socket.readyState === 'closed') + return + + socket.break = socket.encrypted = false + socket.readyState = 'closed' + call(socket.events.close) + } + + function error(err) { + call(socket.events.error, err) + socket.raw + ? close() + : closed() + } + + function call(xs, x) { + xs.slice().forEach(fn => fn(x)) + } +} + +export const net = { + isIP, + createServer() { + const server = { + address() { + return { port: 9876 } + }, + async listen() { + server.raw = Deno.listen({ port: 9876, transport: 'tcp' }) + for await (const conn of server.raw) + setTimeout(() => conn.close(), 500) + }, + close() { + server.raw.close() + } + } + return server + }, + Socket +} + +export const tls = { + connect({ socket, ...options }) { + socket.encrypted = true + socket.readyState = 'connecting' + Deno.startTls(socket.raw, { hostname: socket.hostname, ...options }) + .then(socket.success, socket.error) + socket.raw = null + return socket + } +} + +let ids = 1 +const tasks = new Set() +export const setImmediate = fn => { + const id = ids++ + tasks.add(id) + queueMicrotask(() => { + if (tasks.has(id)) { + fn() + tasks.delete(id) + } + }) + return id +} + +export const clearImmediate = id => tasks.delete(id) + diff --git a/deno/src/bytes.js b/deno/src/bytes.js index fe9359db..48b6f983 100644 --- a/deno/src/bytes.js +++ b/deno/src/bytes.js @@ -1,4 +1,4 @@ -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' +import { Buffer } from 'node:buffer' const size = 256 let buffer = Buffer.allocUnsafe(size) diff --git a/deno/src/bytes.js.bak b/deno/src/bytes.js.bak new file mode 100644 index 00000000..fe9359db --- /dev/null +++ b/deno/src/bytes.js.bak @@ -0,0 +1,79 @@ +import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' +const size = 256 +let buffer = Buffer.allocUnsafe(size) + +const messages = 'BCcDdEFfHPpQSX'.split('').reduce((acc, x) => { + const v = x.charCodeAt(0) + acc[x] = () => { + buffer[0] = v + b.i = 5 + return b + } + return acc +}, {}) + +const b = Object.assign(reset, messages, { + N: String.fromCharCode(0), + i: 0, + inc(x) { + b.i += x + return b + }, + str(x) { + const length = Buffer.byteLength(x) + fit(length) + b.i += buffer.write(x, b.i, length, 'utf8') + return b + }, + i16(x) { + fit(2) + buffer.writeUInt16BE(x, b.i) + b.i += 2 + return b + }, + i32(x, i) { + if (i || i === 0) { + buffer.writeUInt32BE(x, i) + return b + } + fit(4) + buffer.writeUInt32BE(x, b.i) + b.i += 4 + return b + }, + z(x) { + fit(x) + buffer.fill(0, b.i, b.i + x) + b.i += x + return b + }, + raw(x) { + buffer = Buffer.concat([buffer.subarray(0, b.i), x]) + b.i = buffer.length + return b + }, + end(at = 1) { + buffer.writeUInt32BE(b.i - at, at) + const out = buffer.subarray(0, b.i) + b.i = 0 + buffer = Buffer.allocUnsafe(size) + return out + } +}) + +export default b + +function fit(x) { + if (buffer.length - b.i < x) { + const prev = buffer + , length = prev.length + + buffer = Buffer.allocUnsafe(length + (length >> 1) + x) + prev.copy(buffer) + } +} + +function reset() { + b.i = 0 + return b +} diff --git a/deno/src/connection.js b/deno/src/connection.js index a3f43c48..53723d02 100644 --- a/deno/src/connection.js +++ b/deno/src/connection.js @@ -1,10 +1,10 @@ import { HmacSha256 } from 'https://deno.land/std@0.132.0/hash/sha256.ts' -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' +import { Buffer } from 'node:buffer' import { setImmediate, clearImmediate } from '../polyfills.js' import { net } from '../polyfills.js' import { tls } from '../polyfills.js' -import crypto from 'https://deno.land/std@0.132.0/node/crypto.ts' -import Stream from 'https://deno.land/std@0.132.0/node/stream.ts' +import crypto from 'node:crypto' +import Stream from 'node:stream' import { stringify, handleValue, arrayParser, arraySerializer } from './types.js' diff --git a/deno/src/connection.js.bak b/deno/src/connection.js.bak new file mode 100644 index 00000000..a3f43c48 --- /dev/null +++ b/deno/src/connection.js.bak @@ -0,0 +1,1045 @@ +import { HmacSha256 } from 'https://deno.land/std@0.132.0/hash/sha256.ts' +import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' +import { setImmediate, clearImmediate } from '../polyfills.js' +import { net } from '../polyfills.js' +import { tls } from '../polyfills.js' +import crypto from 'https://deno.land/std@0.132.0/node/crypto.ts' +import Stream from 'https://deno.land/std@0.132.0/node/stream.ts' + + +import { stringify, handleValue, arrayParser, arraySerializer } from './types.js' +import { Errors } from './errors.js' +import Result from './result.js' +import Queue from './queue.js' +import { Query, CLOSE } from './query.js' +import b from './bytes.js' + +export default Connection + +let uid = 1 + +const Sync = b().S().end() + , Flush = b().H().end() + , SSLRequest = b().i32(8).i32(80877103).end(8) + , ExecuteUnnamed = Buffer.concat([b().E().str(b.N).i32(0).end(), Sync]) + , DescribeUnnamed = b().D().str('S').str(b.N).end() + , noop = () => { /* noop */ } + +const retryRoutines = new Set([ + 'FetchPreparedStatement', + 'RevalidateCachedQuery', + 'transformAssignedExpr' +]) + +const errorFields = { + 83 : 'severity_local', // S + 86 : 'severity', // V + 67 : 'code', // C + 77 : 'message', // M + 68 : 'detail', // D + 72 : 'hint', // H + 80 : 'position', // P + 112 : 'internal_position', // p + 113 : 'internal_query', // q + 87 : 'where', // W + 115 : 'schema_name', // s + 116 : 'table_name', // t + 99 : 'column_name', // c + 100 : 'data type_name', // d + 110 : 'constraint_name', // n + 70 : 'file', // F + 76 : 'line', // L + 82 : 'routine' // R +} + +function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose = noop } = {}) { + const { + ssl, + max, + user, + host, + port, + database, + parsers, + transform, + onnotice, + onnotify, + onparameter, + max_pipeline, + keep_alive, + backoff, + target_session_attrs + } = options + + const sent = Queue() + , id = uid++ + , backend = { pid: null, secret: null } + , idleTimer = timer(end, options.idle_timeout) + , lifeTimer = timer(end, options.max_lifetime) + , connectTimer = timer(connectTimedOut, options.connect_timeout) + + let socket = null + , cancelMessage + , result = new Result() + , incoming = Buffer.alloc(0) + , needsTypes = options.fetch_types + , backendParameters = {} + , statements = {} + , statementId = Math.random().toString(36).slice(2) + , statementCount = 1 + , closedDate = 0 + , remaining = 0 + , hostIndex = 0 + , retries = 0 + , length = 0 + , delay = 0 + , rows = 0 + , serverSignature = null + , nextWriteTimer = null + , terminated = false + , incomings = null + , results = null + , initial = null + , ending = null + , stream = null + , chunk = null + , ended = null + , nonce = null + , query = null + , final = null + + const connection = { + queue: queues.closed, + idleTimer, + connect(query) { + initial = query + reconnect() + }, + terminate, + execute, + cancel, + end, + count: 0, + id + } + + queues.closed && queues.closed.push(connection) + + return connection + + async function createSocket() { + let x + try { + x = options.socket + ? (await Promise.resolve(options.socket(options))) + : new net.Socket() + } catch (e) { + error(e) + return + } + x.on('error', error) + x.on('close', closed) + x.on('drain', drain) + return x + } + + async function cancel({ pid, secret }, resolve, reject) { + try { + cancelMessage = b().i32(16).i32(80877102).i32(pid).i32(secret).end(16) + await connect() + socket.once('error', reject) + socket.once('close', resolve) + } catch (error) { + reject(error) + } + } + + function execute(q) { + if (terminated) + return queryError(q, Errors.connection('CONNECTION_DESTROYED', options)) + + if (q.cancelled) + return + + try { + q.state = backend + query + ? sent.push(q) + : (query = q, query.active = true) + + build(q) + return write(toBuffer(q)) + && !q.describeFirst + && !q.cursorFn + && sent.length < max_pipeline + && (!q.options.onexecute || q.options.onexecute(connection)) + } catch (error) { + sent.length === 0 && write(Sync) + errored(error) + return true + } + } + + function toBuffer(q) { + if (q.parameters.length >= 65534) + throw Errors.generic('MAX_PARAMETERS_EXCEEDED', 'Max number of parameters (65534) exceeded') + + return q.options.simple + ? b().Q().str(q.statement.string + b.N).end() + : q.describeFirst + ? Buffer.concat([describe(q), Flush]) + : q.prepare + ? q.prepared + ? prepared(q) + : Buffer.concat([describe(q), prepared(q)]) + : unnamed(q) + } + + function describe(q) { + return Buffer.concat([ + Parse(q.statement.string, q.parameters, q.statement.types, q.statement.name), + Describe('S', q.statement.name) + ]) + } + + function prepared(q) { + return Buffer.concat([ + Bind(q.parameters, q.statement.types, q.statement.name, q.cursorName), + q.cursorFn + ? Execute('', q.cursorRows) + : ExecuteUnnamed + ]) + } + + function unnamed(q) { + return Buffer.concat([ + Parse(q.statement.string, q.parameters, q.statement.types), + DescribeUnnamed, + prepared(q) + ]) + } + + function build(q) { + const parameters = [] + , types = [] + + const string = stringify(q, q.strings[0], q.args[0], parameters, types, options) + + !q.tagged && q.args.forEach(x => handleValue(x, parameters, types, options)) + + q.prepare = options.prepare && ('prepare' in q.options ? q.options.prepare : true) + q.string = string + q.signature = q.prepare && types + string + q.onlyDescribe && (delete statements[q.signature]) + q.parameters = q.parameters || parameters + q.prepared = q.prepare && q.signature in statements + q.describeFirst = q.onlyDescribe || (parameters.length && !q.prepared) + q.statement = q.prepared + ? statements[q.signature] + : { string, types, name: q.prepare ? statementId + statementCount++ : '' } + + typeof options.debug === 'function' && options.debug(id, string, parameters, types) + } + + function write(x, fn) { + chunk = chunk ? Buffer.concat([chunk, x]) : Buffer.from(x) + if (fn || chunk.length >= 1024) + return nextWrite(fn) + nextWriteTimer === null && (nextWriteTimer = setImmediate(nextWrite)) + return true + } + + function nextWrite(fn) { + const x = socket.write(chunk, fn) + nextWriteTimer !== null && clearImmediate(nextWriteTimer) + chunk = nextWriteTimer = null + return x + } + + function connectTimedOut() { + errored(Errors.connection('CONNECT_TIMEOUT', options, socket)) + socket.destroy() + } + + async function secure() { + write(SSLRequest) + const canSSL = await new Promise(r => socket.once('data', x => r(x[0] === 83))) // S + + if (!canSSL && ssl === 'prefer') + return connected() + + socket.removeAllListeners() + socket = tls.connect({ + socket, + servername: net.isIP(socket.host) ? undefined : socket.host, + ...(ssl === 'require' || ssl === 'allow' || ssl === 'prefer' + ? { rejectUnauthorized: false } + : ssl === 'verify-full' + ? {} + : typeof ssl === 'object' + ? ssl + : {} + ) + }) + socket.on('secureConnect', connected) + socket.on('error', error) + socket.on('close', closed) + socket.on('drain', drain) + } + + /* c8 ignore next 3 */ + function drain() { + !query && onopen(connection) + } + + function data(x) { + if (incomings) { + incomings.push(x) + remaining -= x.length + if (remaining > 0) + return + } + + incoming = incomings + ? Buffer.concat(incomings, length - remaining) + : incoming.length === 0 + ? x + : Buffer.concat([incoming, x], incoming.length + x.length) + + while (incoming.length > 4) { + length = incoming.readUInt32BE(1) + if (length >= incoming.length) { + remaining = length - incoming.length + incomings = [incoming] + break + } + + try { + handle(incoming.subarray(0, length + 1)) + } catch (e) { + query && (query.cursorFn || query.describeFirst) && write(Sync) + errored(e) + } + incoming = incoming.subarray(length + 1) + remaining = 0 + incomings = null + } + } + + async function connect() { + terminated = false + backendParameters = {} + socket || (socket = await createSocket()) + + if (!socket) + return + + connectTimer.start() + + if (options.socket) + return ssl ? secure() : connected() + + socket.on('connect', ssl ? secure : connected) + + if (options.path) + return socket.connect(options.path) + + socket.ssl = ssl + socket.connect(port[hostIndex], host[hostIndex]) + socket.host = host[hostIndex] + socket.port = port[hostIndex] + + hostIndex = (hostIndex + 1) % port.length + } + + function reconnect() { + setTimeout(connect, closedDate ? closedDate + delay - performance.now() : 0) + } + + function connected() { + try { + statements = {} + needsTypes = options.fetch_types + statementId = Math.random().toString(36).slice(2) + statementCount = 1 + lifeTimer.start() + socket.on('data', data) + keep_alive && socket.setKeepAlive && socket.setKeepAlive(true) + const s = StartupMessage() + write(s) + } catch (err) { + error(err) + } + } + + function error(err) { + if (connection.queue === queues.connecting && options.host[retries + 1]) + return + + errored(err) + while (sent.length) + queryError(sent.shift(), err) + } + + function errored(err) { + stream && (stream.destroy(err), stream = null) + query && queryError(query, err) + initial && (queryError(initial, err), initial = null) + } + + function queryError(query, err) { + if (query.reserve) + return query.reject(err) + + if (!err || typeof err !== 'object') + err = new Error(err) + + 'query' in err || 'parameters' in err || Object.defineProperties(err, { + stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug }, + query: { value: query.string, enumerable: options.debug }, + parameters: { value: query.parameters, enumerable: options.debug }, + args: { value: query.args, enumerable: options.debug }, + types: { value: query.statement && query.statement.types, enumerable: options.debug } + }) + query.reject(err) + } + + function end() { + return ending || ( + !connection.reserved && onend(connection), + !connection.reserved && !initial && !query && sent.length === 0 + ? (terminate(), new Promise(r => socket && socket.readyState !== 'closed' ? socket.once('close', r) : r())) + : ending = new Promise(r => ended = r) + ) + } + + function terminate() { + terminated = true + if (stream || query || initial || sent.length) + error(Errors.connection('CONNECTION_DESTROYED', options)) + + clearImmediate(nextWriteTimer) + if (socket) { + socket.removeListener('data', data) + socket.removeListener('connect', connected) + socket.readyState === 'open' && socket.end(b().X().end()) + } + ended && (ended(), ending = ended = null) + } + + async function closed(hadError) { + incoming = Buffer.alloc(0) + remaining = 0 + incomings = null + clearImmediate(nextWriteTimer) + socket.removeListener('data', data) + socket.removeListener('connect', connected) + idleTimer.cancel() + lifeTimer.cancel() + connectTimer.cancel() + + socket.removeAllListeners() + socket = null + + if (initial) + return reconnect() + + !hadError && (query || sent.length) && error(Errors.connection('CONNECTION_CLOSED', options, socket)) + closedDate = performance.now() + hadError && options.shared.retries++ + delay = (typeof backoff === 'function' ? backoff(options.shared.retries) : backoff) * 1000 + onclose(connection, Errors.connection('CONNECTION_CLOSED', options, socket)) + } + + /* Handlers */ + function handle(xs, x = xs[0]) { + ( + x === 68 ? DataRow : // D + x === 100 ? CopyData : // d + x === 65 ? NotificationResponse : // A + x === 83 ? ParameterStatus : // S + x === 90 ? ReadyForQuery : // Z + x === 67 ? CommandComplete : // C + x === 50 ? BindComplete : // 2 + x === 49 ? ParseComplete : // 1 + x === 116 ? ParameterDescription : // t + x === 84 ? RowDescription : // T + x === 82 ? Authentication : // R + x === 110 ? NoData : // n + x === 75 ? BackendKeyData : // K + x === 69 ? ErrorResponse : // E + x === 115 ? PortalSuspended : // s + x === 51 ? CloseComplete : // 3 + x === 71 ? CopyInResponse : // G + x === 78 ? NoticeResponse : // N + x === 72 ? CopyOutResponse : // H + x === 99 ? CopyDone : // c + x === 73 ? EmptyQueryResponse : // I + x === 86 ? FunctionCallResponse : // V + x === 118 ? NegotiateProtocolVersion : // v + x === 87 ? CopyBothResponse : // W + /* c8 ignore next */ + UnknownMessage + )(xs) + } + + function DataRow(x) { + let index = 7 + let length + let column + let value + + const row = query.isRaw ? new Array(query.statement.columns.length) : {} + for (let i = 0; i < query.statement.columns.length; i++) { + column = query.statement.columns[i] + length = x.readInt32BE(index) + index += 4 + + value = length === -1 + ? null + : query.isRaw === true + ? x.subarray(index, index += length) + : column.parser === undefined + ? x.toString('utf8', index, index += length) + : column.parser.array === true + ? column.parser(x.toString('utf8', index + 1, index += length)) + : column.parser(x.toString('utf8', index, index += length)) + + query.isRaw + ? (row[i] = query.isRaw === true + ? value + : transform.value.from ? transform.value.from(value, column) : value) + : (row[column.name] = transform.value.from ? transform.value.from(value, column) : value) + } + + query.forEachFn + ? query.forEachFn(transform.row.from ? transform.row.from(row) : row, result) + : (result[rows++] = transform.row.from ? transform.row.from(row) : row) + } + + function ParameterStatus(x) { + const [k, v] = x.toString('utf8', 5, x.length - 1).split(b.N) + backendParameters[k] = v + if (options.parameters[k] !== v) { + options.parameters[k] = v + onparameter && onparameter(k, v) + } + } + + function ReadyForQuery(x) { + query && query.options.simple && query.resolve(results || result) + query = results = null + result = new Result() + connectTimer.cancel() + + if (initial) { + if (target_session_attrs) { + if (!backendParameters.in_hot_standby || !backendParameters.default_transaction_read_only) + return fetchState() + else if (tryNext(target_session_attrs, backendParameters)) + return terminate() + } + + if (needsTypes) { + initial.reserve && (initial = null) + return fetchArrayTypes() + } + + initial && !initial.reserve && execute(initial) + options.shared.retries = retries = 0 + initial = null + return + } + + while (sent.length && (query = sent.shift()) && (query.active = true, query.cancelled)) + Connection(options).cancel(query.state, query.cancelled.resolve, query.cancelled.reject) + + if (query) + return // Consider opening if able and sent.length < 50 + + connection.reserved + ? !connection.reserved.release && x[5] === 73 // I + ? ending + ? terminate() + : (connection.reserved = null, onopen(connection)) + : connection.reserved() + : ending + ? terminate() + : onopen(connection) + } + + function CommandComplete(x) { + rows = 0 + + for (let i = x.length - 1; i > 0; i--) { + if (x[i] === 32 && x[i + 1] < 58 && result.count === null) + result.count = +x.toString('utf8', i + 1, x.length - 1) + if (x[i - 1] >= 65) { + result.command = x.toString('utf8', 5, i) + result.state = backend + break + } + } + + final && (final(), final = null) + + if (result.command === 'BEGIN' && max !== 1 && !connection.reserved) + return errored(Errors.generic('UNSAFE_TRANSACTION', 'Only use sql.begin, sql.reserved or max: 1')) + + if (query.options.simple) + return BindComplete() + + if (query.cursorFn) { + result.count && query.cursorFn(result) + write(Sync) + } + + query.resolve(result) + } + + function ParseComplete() { + query.parsing = false + } + + function BindComplete() { + !result.statement && (result.statement = query.statement) + result.columns = query.statement.columns + } + + function ParameterDescription(x) { + const length = x.readUInt16BE(5) + + for (let i = 0; i < length; ++i) + !query.statement.types[i] && (query.statement.types[i] = x.readUInt32BE(7 + i * 4)) + + query.prepare && (statements[query.signature] = query.statement) + query.describeFirst && !query.onlyDescribe && (write(prepared(query)), query.describeFirst = false) + } + + function RowDescription(x) { + if (result.command) { + results = results || [result] + results.push(result = new Result()) + result.count = null + query.statement.columns = null + } + + const length = x.readUInt16BE(5) + let index = 7 + let start + + query.statement.columns = Array(length) + + for (let i = 0; i < length; ++i) { + start = index + while (x[index++] !== 0); + const table = x.readUInt32BE(index) + const number = x.readUInt16BE(index + 4) + const type = x.readUInt32BE(index + 6) + query.statement.columns[i] = { + name: transform.column.from + ? transform.column.from(x.toString('utf8', start, index - 1)) + : x.toString('utf8', start, index - 1), + parser: parsers[type], + table, + number, + type + } + index += 18 + } + + result.statement = query.statement + if (query.onlyDescribe) + return (query.resolve(query.statement), write(Sync)) + } + + async function Authentication(x, type = x.readUInt32BE(5)) { + ( + type === 3 ? AuthenticationCleartextPassword : + type === 5 ? AuthenticationMD5Password : + type === 10 ? SASL : + type === 11 ? SASLContinue : + type === 12 ? SASLFinal : + type !== 0 ? UnknownAuth : + noop + )(x, type) + } + + /* c8 ignore next 5 */ + async function AuthenticationCleartextPassword() { + const payload = await Pass() + write( + b().p().str(payload).z(1).end() + ) + } + + async function AuthenticationMD5Password(x) { + const payload = 'md5' + ( + await md5( + Buffer.concat([ + Buffer.from(await md5((await Pass()) + user)), + x.subarray(9) + ]) + ) + ) + write( + b().p().str(payload).z(1).end() + ) + } + + async function SASL() { + nonce = (await crypto.randomBytes(18)).toString('base64') + b().p().str('SCRAM-SHA-256' + b.N) + const i = b.i + write(b.inc(4).str('n,,n=*,r=' + nonce).i32(b.i - i - 4, i).end()) + } + + async function SASLContinue(x) { + const res = x.toString('utf8', 9).split(',').reduce((acc, x) => (acc[x[0]] = x.slice(2), acc), {}) + + const saltedPassword = await crypto.pbkdf2Sync( + await Pass(), + Buffer.from(res.s, 'base64'), + parseInt(res.i), 32, + 'sha256' + ) + + const clientKey = await hmac(saltedPassword, 'Client Key') + + const auth = 'n=*,r=' + nonce + ',' + + 'r=' + res.r + ',s=' + res.s + ',i=' + res.i + + ',c=biws,r=' + res.r + + serverSignature = (await hmac(await hmac(saltedPassword, 'Server Key'), auth)).toString('base64') + + const payload = 'c=biws,r=' + res.r + ',p=' + xor( + clientKey, Buffer.from(await hmac(await sha256(clientKey), auth)) + ).toString('base64') + + write( + b().p().str(payload).end() + ) + } + + function SASLFinal(x) { + if (x.toString('utf8', 9).split(b.N, 1)[0].slice(2) === serverSignature) + return + /* c8 ignore next 5 */ + errored(Errors.generic('SASL_SIGNATURE_MISMATCH', 'The server did not return the correct signature')) + socket.destroy() + } + + function Pass() { + return Promise.resolve(typeof options.pass === 'function' + ? options.pass() + : options.pass + ) + } + + function NoData() { + result.statement = query.statement + result.statement.columns = [] + if (query.onlyDescribe) + return (query.resolve(query.statement), write(Sync)) + } + + function BackendKeyData(x) { + backend.pid = x.readUInt32BE(5) + backend.secret = x.readUInt32BE(9) + } + + async function fetchArrayTypes() { + needsTypes = false + const types = await new Query([` + select b.oid, b.typarray + from pg_catalog.pg_type a + left join pg_catalog.pg_type b on b.oid = a.typelem + where a.typcategory = 'A' + group by b.oid, b.typarray + order by b.oid + `], [], execute) + types.forEach(({ oid, typarray }) => addArrayType(oid, typarray)) + } + + function addArrayType(oid, typarray) { + if (!!options.parsers[typarray] && !!options.serializers[typarray]) return + const parser = options.parsers[oid] + options.shared.typeArrayMap[oid] = typarray + options.parsers[typarray] = (xs) => arrayParser(xs, parser, typarray) + options.parsers[typarray].array = true + options.serializers[typarray] = (xs) => arraySerializer(xs, options.serializers[oid], options, typarray) + } + + function tryNext(x, xs) { + return ( + (x === 'read-write' && xs.default_transaction_read_only === 'on') || + (x === 'read-only' && xs.default_transaction_read_only === 'off') || + (x === 'primary' && xs.in_hot_standby === 'on') || + (x === 'standby' && xs.in_hot_standby === 'off') || + (x === 'prefer-standby' && xs.in_hot_standby === 'off' && options.host[retries]) + ) + } + + function fetchState() { + const query = new Query([` + show transaction_read_only; + select pg_catalog.pg_is_in_recovery() + `], [], execute, null, { simple: true }) + query.resolve = ([[a], [b]]) => { + backendParameters.default_transaction_read_only = a.transaction_read_only + backendParameters.in_hot_standby = b.pg_is_in_recovery ? 'on' : 'off' + } + query.execute() + } + + function ErrorResponse(x) { + query && (query.cursorFn || query.describeFirst) && write(Sync) + const error = Errors.postgres(parseError(x)) + query && query.retried + ? errored(query.retried) + : query && query.prepared && retryRoutines.has(error.routine) + ? retry(query, error) + : errored(error) + } + + function retry(q, error) { + delete statements[q.signature] + q.retried = error + execute(q) + } + + function NotificationResponse(x) { + if (!onnotify) + return + + let index = 9 + while (x[index++] !== 0); + onnotify( + x.toString('utf8', 9, index - 1), + x.toString('utf8', index, x.length - 1) + ) + } + + async function PortalSuspended() { + try { + const x = await Promise.resolve(query.cursorFn(result)) + rows = 0 + x === CLOSE + ? write(Close(query.portal)) + : (result = new Result(), write(Execute('', query.cursorRows))) + } catch (err) { + write(Sync) + query.reject(err) + } + } + + function CloseComplete() { + result.count && query.cursorFn(result) + query.resolve(result) + } + + function CopyInResponse() { + stream = new Stream.Writable({ + autoDestroy: true, + write(chunk, encoding, callback) { + socket.write(b().d().raw(chunk).end(), callback) + }, + destroy(error, callback) { + callback(error) + socket.write(b().f().str(error + b.N).end()) + stream = null + }, + final(callback) { + socket.write(b().c().end()) + final = callback + } + }) + query.resolve(stream) + } + + function CopyOutResponse() { + stream = new Stream.Readable({ + read() { socket.resume() } + }) + query.resolve(stream) + } + + /* c8 ignore next 3 */ + function CopyBothResponse() { + stream = new Stream.Duplex({ + autoDestroy: true, + read() { socket.resume() }, + /* c8 ignore next 11 */ + write(chunk, encoding, callback) { + socket.write(b().d().raw(chunk).end(), callback) + }, + destroy(error, callback) { + callback(error) + socket.write(b().f().str(error + b.N).end()) + stream = null + }, + final(callback) { + socket.write(b().c().end()) + final = callback + } + }) + query.resolve(stream) + } + + function CopyData(x) { + stream && (stream.push(x.subarray(5)) || socket.pause()) + } + + function CopyDone() { + stream && stream.push(null) + stream = null + } + + function NoticeResponse(x) { + onnotice + ? onnotice(parseError(x)) + : console.log(parseError(x)) // eslint-disable-line + + } + + /* c8 ignore next 3 */ + function EmptyQueryResponse() { + /* noop */ + } + + /* c8 ignore next 3 */ + function FunctionCallResponse() { + errored(Errors.notSupported('FunctionCallResponse')) + } + + /* c8 ignore next 3 */ + function NegotiateProtocolVersion() { + errored(Errors.notSupported('NegotiateProtocolVersion')) + } + + /* c8 ignore next 3 */ + function UnknownMessage(x) { + console.error('Postgres.js : Unknown Message:', x[0]) // eslint-disable-line + } + + /* c8 ignore next 3 */ + function UnknownAuth(x, type) { + console.error('Postgres.js : Unknown Auth:', type) // eslint-disable-line + } + + /* Messages */ + function Bind(parameters, types, statement = '', portal = '') { + let prev + , type + + b().B().str(portal + b.N).str(statement + b.N).i16(0).i16(parameters.length) + + parameters.forEach((x, i) => { + if (x === null) + return b.i32(0xFFFFFFFF) + + type = types[i] + parameters[i] = x = type in options.serializers + ? options.serializers[type](x) + : '' + x + + prev = b.i + b.inc(4).str(x).i32(b.i - prev - 4, prev) + }) + + b.i16(0) + + return b.end() + } + + function Parse(str, parameters, types, name = '') { + b().P().str(name + b.N).str(str + b.N).i16(parameters.length) + parameters.forEach((x, i) => b.i32(types[i] || 0)) + return b.end() + } + + function Describe(x, name = '') { + return b().D().str(x).str(name + b.N).end() + } + + function Execute(portal = '', rows = 0) { + return Buffer.concat([ + b().E().str(portal + b.N).i32(rows).end(), + Flush + ]) + } + + function Close(portal = '') { + return Buffer.concat([ + b().C().str('P').str(portal + b.N).end(), + b().S().end() + ]) + } + + function StartupMessage() { + return cancelMessage || b().inc(4).i16(3).z(2).str( + Object.entries(Object.assign({ + user, + database, + client_encoding: 'UTF8' + }, + options.connection + )).filter(([, v]) => v).map(([k, v]) => k + b.N + v).join(b.N) + ).z(2).end(0) + } + +} + +function parseError(x) { + const error = {} + let start = 5 + for (let i = 5; i < x.length - 1; i++) { + if (x[i] === 0) { + error[errorFields[x[start]]] = x.toString('utf8', start + 1, i) + start = i + 1 + } + } + return error +} + +function md5(x) { + return crypto.createHash('md5').update(x).digest('hex') +} + +function hmac(key, x) { + return Buffer.from(new HmacSha256(key).update(x).digest()) +} + +function sha256(x) { + return crypto.createHash('sha256').update(x).digest() +} + +function xor(a, b) { + const length = Math.max(a.length, b.length) + const buffer = Buffer.allocUnsafe(length) + for (let i = 0; i < length; i++) + buffer[i] = a[i] ^ b[i] + return buffer +} + +function timer(fn, seconds) { + seconds = typeof seconds === 'function' ? seconds() : seconds + if (!seconds) + return { cancel: noop, start: noop } + + let timer + return { + cancel() { + timer && (clearTimeout(timer), timer = null) + }, + start() { + timer && clearTimeout(timer) + timer = setTimeout(done, seconds * 1000, arguments) + } + } + + function done(args) { + fn.apply(null, args) + timer = null + } +} diff --git a/deno/src/errors.js.bak b/deno/src/errors.js.bak new file mode 100644 index 00000000..0ff83c42 --- /dev/null +++ b/deno/src/errors.js.bak @@ -0,0 +1,53 @@ +export class PostgresError extends Error { + constructor(x) { + super(x.message) + this.name = this.constructor.name + Object.assign(this, x) + } +} + +export const Errors = { + connection, + postgres, + generic, + notSupported +} + +function connection(x, options, socket) { + const { host, port } = socket || options + const error = Object.assign( + new Error(('write ' + x + ' ' + (options.path || (host + ':' + port)))), + { + code: x, + errno: x, + address: options.path || host + }, options.path ? {} : { port: port } + ) + Error.captureStackTrace(error, connection) + return error +} + +function postgres(x) { + const error = new PostgresError(x) + Error.captureStackTrace(error, postgres) + return error +} + +function generic(code, message) { + const error = Object.assign(new Error(code + ': ' + message), { code }) + Error.captureStackTrace(error, generic) + return error +} + +/* c8 ignore next 10 */ +function notSupported(x) { + const error = Object.assign( + new Error(x + ' (B) is not supported'), + { + code: 'MESSAGE_NOT_SUPPORTED', + name: x + } + ) + Error.captureStackTrace(error, notSupported) + return error +} diff --git a/deno/src/index.js b/deno/src/index.js index aa7a920f..119e7e6d 100644 --- a/deno/src/index.js +++ b/deno/src/index.js @@ -1,6 +1,6 @@ -import process from 'https://deno.land/std@0.132.0/node/process.ts' -import os from 'https://deno.land/std@0.132.0/node/os.ts' -import fs from 'https://deno.land/std@0.132.0/node/fs.ts' +import process from 'node:process' +import os from 'node:os' +import fs from 'node:fs' import { mergeUserTypes, diff --git a/deno/src/index.js.bak b/deno/src/index.js.bak new file mode 100644 index 00000000..aa7a920f --- /dev/null +++ b/deno/src/index.js.bak @@ -0,0 +1,567 @@ +import process from 'https://deno.land/std@0.132.0/node/process.ts' +import os from 'https://deno.land/std@0.132.0/node/os.ts' +import fs from 'https://deno.land/std@0.132.0/node/fs.ts' + +import { + mergeUserTypes, + inferType, + Parameter, + Identifier, + Builder, + toPascal, + pascal, + toCamel, + camel, + toKebab, + kebab, + fromPascal, + fromCamel, + fromKebab +} from './types.js' + +import Connection from './connection.js' +import { Query, CLOSE } from './query.js' +import Queue from './queue.js' +import { Errors, PostgresError } from './errors.js' +import Subscribe from './subscribe.js' +import largeObject from './large.js' + +Object.assign(Postgres, { + PostgresError, + toPascal, + pascal, + toCamel, + camel, + toKebab, + kebab, + fromPascal, + fromCamel, + fromKebab, + BigInt: { + to: 20, + from: [20], + parse: x => BigInt(x), // eslint-disable-line + serialize: x => x.toString() + } +}) + +export default Postgres + +function Postgres(a, b) { + const options = parseOptions(a, b) + , subscribe = options.no_subscribe || Subscribe(Postgres, { ...options }) + + let ending = false + + const queries = Queue() + , connecting = Queue() + , reserved = Queue() + , closed = Queue() + , ended = Queue() + , open = Queue() + , busy = Queue() + , full = Queue() + , queues = { connecting, reserved, closed, ended, open, busy, full } + + const connections = [...Array(options.max)].map(() => Connection(options, queues, { onopen, onend, onclose })) + + const sql = Sql(handler) + + Object.assign(sql, { + get parameters() { return options.parameters }, + largeObject: largeObject.bind(null, sql), + subscribe, + CLOSE, + END: CLOSE, + PostgresError, + options, + reserve, + listen, + begin, + close, + end + }) + + return sql + + function Sql(handler) { + handler.debug = options.debug + + Object.entries(options.types).reduce((acc, [name, type]) => { + acc[name] = (x) => new Parameter(x, type.to) + return acc + }, typed) + + Object.assign(sql, { + types: typed, + typed, + unsafe, + notify, + array, + json, + file + }) + + return sql + + function typed(value, type) { + return new Parameter(value, type) + } + + function sql(strings, ...args) { + const query = strings && Array.isArray(strings.raw) + ? new Query(strings, args, handler, cancel) + : typeof strings === 'string' && !args.length + ? new Identifier(options.transform.column.to ? options.transform.column.to(strings) : strings) + : new Builder(strings, args) + return query + } + + function unsafe(string, args = [], options = {}) { + arguments.length === 2 && !Array.isArray(args) && (options = args, args = []) + const query = new Query([string], args, handler, cancel, { + prepare: false, + ...options, + simple: 'simple' in options ? options.simple : args.length === 0 + }) + return query + } + + function file(path, args = [], options = {}) { + arguments.length === 2 && !Array.isArray(args) && (options = args, args = []) + const query = new Query([], args, (query) => { + fs.readFile(path, 'utf8', (err, string) => { + if (err) + return query.reject(err) + + query.strings = [string] + handler(query) + }) + }, cancel, { + ...options, + simple: 'simple' in options ? options.simple : args.length === 0 + }) + return query + } + } + + async function listen(name, fn, onlisten) { + const listener = { fn, onlisten } + + const sql = listen.sql || (listen.sql = Postgres({ + ...options, + max: 1, + idle_timeout: null, + max_lifetime: null, + fetch_types: false, + onclose() { + Object.entries(listen.channels).forEach(([name, { listeners }]) => { + delete listen.channels[name] + Promise.all(listeners.map(l => listen(name, l.fn, l.onlisten).catch(() => { /* noop */ }))) + }) + }, + onnotify(c, x) { + c in listen.channels && listen.channels[c].listeners.forEach(l => l.fn(x)) + } + })) + + const channels = listen.channels || (listen.channels = {}) + , exists = name in channels + + if (exists) { + channels[name].listeners.push(listener) + const result = await channels[name].result + listener.onlisten && listener.onlisten() + return { state: result.state, unlisten } + } + + channels[name] = { result: sql`listen ${ + sql.unsafe('"' + name.replace(/"/g, '""') + '"') + }`, listeners: [listener] } + const result = await channels[name].result + listener.onlisten && listener.onlisten() + return { state: result.state, unlisten } + + async function unlisten() { + if (name in channels === false) + return + + channels[name].listeners = channels[name].listeners.filter(x => x !== listener) + if (channels[name].listeners.length) + return + + delete channels[name] + return sql`unlisten ${ + sql.unsafe('"' + name.replace(/"/g, '""') + '"') + }` + } + } + + async function notify(channel, payload) { + return await sql`select pg_notify(${ channel }, ${ '' + payload })` + } + + async function reserve() { + const queue = Queue() + const c = open.length + ? open.shift() + : await new Promise((resolve, reject) => { + const query = { reserve: resolve, reject } + queries.push(query) + closed.length && connect(closed.shift(), query) + }) + + move(c, reserved) + c.reserved = () => queue.length + ? c.execute(queue.shift()) + : move(c, reserved) + c.reserved.release = true + + const sql = Sql(handler) + sql.release = () => { + c.reserved = null + onopen(c) + } + + return sql + + function handler(q) { + c.queue === full + ? queue.push(q) + : c.execute(q) || move(c, full) + } + } + + async function begin(options, fn) { + !fn && (fn = options, options = '') + const queries = Queue() + let savepoints = 0 + , connection + , prepare = null + + try { + await sql.unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), [], { onexecute }).execute() + return await Promise.race([ + scope(connection, fn), + new Promise((_, reject) => connection.onclose = reject) + ]) + } catch (error) { + throw error + } + + async function scope(c, fn, name) { + const sql = Sql(handler) + sql.savepoint = savepoint + sql.prepare = x => prepare = x.replace(/[^a-z0-9$-_. ]/gi) + let uncaughtError + , result + + name && await sql`savepoint ${ sql(name) }` + try { + result = await new Promise((resolve, reject) => { + const x = fn(sql) + Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve, reject) + }) + + if (uncaughtError) + throw uncaughtError + } catch (e) { + await (name + ? sql`rollback to ${ sql(name) }` + : sql`rollback` + ) + throw e instanceof PostgresError && e.code === '25P02' && uncaughtError || e + } + + if (!name) { + prepare + ? await sql`prepare transaction '${ sql.unsafe(prepare) }'` + : await sql`commit` + } + + return result + + function savepoint(name, fn) { + if (name && Array.isArray(name.raw)) + return savepoint(sql => sql.apply(sql, arguments)) + + arguments.length === 1 && (fn = name, name = null) + return scope(c, fn, 's' + savepoints++ + (name ? '_' + name : '')) + } + + function handler(q) { + q.catch(e => uncaughtError || (uncaughtError = e)) + c.queue === full + ? queries.push(q) + : c.execute(q) || move(c, full) + } + } + + function onexecute(c) { + connection = c + move(c, reserved) + c.reserved = () => queries.length + ? c.execute(queries.shift()) + : move(c, reserved) + } + } + + function move(c, queue) { + c.queue.remove(c) + queue.push(c) + c.queue = queue + queue === open + ? c.idleTimer.start() + : c.idleTimer.cancel() + return c + } + + function json(x) { + return new Parameter(x, 3802) + } + + function array(x, type) { + if (!Array.isArray(x)) + return array(Array.from(arguments)) + + return new Parameter(x, type || (x.length ? inferType(x) || 25 : 0), options.shared.typeArrayMap) + } + + function handler(query) { + if (ending) + return query.reject(Errors.connection('CONNECTION_ENDED', options, options)) + + if (open.length) + return go(open.shift(), query) + + if (closed.length) + return connect(closed.shift(), query) + + busy.length + ? go(busy.shift(), query) + : queries.push(query) + } + + function go(c, query) { + return c.execute(query) + ? move(c, busy) + : move(c, full) + } + + function cancel(query) { + return new Promise((resolve, reject) => { + query.state + ? query.active + ? Connection(options).cancel(query.state, resolve, reject) + : query.cancelled = { resolve, reject } + : ( + queries.remove(query), + query.cancelled = true, + query.reject(Errors.generic('57014', 'canceling statement due to user request')), + resolve() + ) + }) + } + + async function end({ timeout = null } = {}) { + if (ending) + return ending + + await 1 + let timer + return ending = Promise.race([ + new Promise(r => timeout !== null && (timer = setTimeout(destroy, timeout * 1000, r))), + Promise.all(connections.map(c => c.end()).concat( + listen.sql ? listen.sql.end({ timeout: 0 }) : [], + subscribe.sql ? subscribe.sql.end({ timeout: 0 }) : [] + )) + ]).then(() => clearTimeout(timer)) + } + + async function close() { + await Promise.all(connections.map(c => c.end())) + } + + async function destroy(resolve) { + await Promise.all(connections.map(c => c.terminate())) + while (queries.length) + queries.shift().reject(Errors.connection('CONNECTION_DESTROYED', options)) + resolve() + } + + function connect(c, query) { + move(c, connecting) + c.connect(query) + return c + } + + function onend(c) { + move(c, ended) + } + + function onopen(c) { + if (queries.length === 0) + return move(c, open) + + let max = Math.ceil(queries.length / (connecting.length + 1)) + , ready = true + + while (ready && queries.length && max-- > 0) { + const query = queries.shift() + if (query.reserve) + return query.reserve(c) + + ready = c.execute(query) + } + + ready + ? move(c, busy) + : move(c, full) + } + + function onclose(c, e) { + move(c, closed) + c.reserved = null + c.onclose && (c.onclose(e), c.onclose = null) + options.onclose && options.onclose(c.id) + queries.length && connect(c, queries.shift()) + } +} + +function parseOptions(a, b) { + if (a && a.shared) + return a + + const env = process.env // eslint-disable-line + , o = (!a || typeof a === 'string' ? b : a) || {} + , { url, multihost } = parseUrl(a) + , query = [...url.searchParams].reduce((a, [b, c]) => (a[b] = c, a), {}) + , host = o.hostname || o.host || multihost || url.hostname || env.PGHOST || 'localhost' + , port = o.port || url.port || env.PGPORT || 5432 + , user = o.user || o.username || url.username || env.PGUSERNAME || env.PGUSER || osUsername() + + o.no_prepare && (o.prepare = false) + query.sslmode && (query.ssl = query.sslmode, delete query.sslmode) + 'timeout' in o && (console.log('The timeout option is deprecated, use idle_timeout instead'), o.idle_timeout = o.timeout) // eslint-disable-line + query.sslrootcert === 'system' && (query.ssl = 'verify-full') + + const ints = ['idle_timeout', 'connect_timeout', 'max_lifetime', 'max_pipeline', 'backoff', 'keep_alive'] + const defaults = { + max : 10, + ssl : false, + idle_timeout : null, + connect_timeout : 30, + max_lifetime : max_lifetime, + max_pipeline : 100, + backoff : backoff, + keep_alive : 60, + prepare : true, + debug : false, + fetch_types : true, + publications : 'alltables', + target_session_attrs: null + } + + return { + host : Array.isArray(host) ? host : host.split(',').map(x => x.split(':')[0]), + port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(x.split(':')[1] || port)), + path : o.path || host.indexOf('/') > -1 && host + '/.s.PGSQL.' + port, + database : o.database || o.db || (url.pathname || '').slice(1) || env.PGDATABASE || user, + user : user, + pass : o.pass || o.password || url.password || env.PGPASSWORD || '', + ...Object.entries(defaults).reduce( + (acc, [k, d]) => { + const value = k in o ? o[k] : k in query + ? (query[k] === 'disable' || query[k] === 'false' ? false : query[k]) + : env['PG' + k.toUpperCase()] || d + acc[k] = typeof value === 'string' && ints.includes(k) + ? +value + : value + return acc + }, + {} + ), + connection : { + ...o.connection, + application_name: o.connection?.application_name ?? env.PGAPPNAME ?? 'postgres.js', + ...Object.entries(query).reduce((acc, [k, v]) => (k in defaults || (acc[k] = v), acc), {}) + }, + types : o.types || {}, + target_session_attrs: tsa(o, url, env), + onnotice : o.onnotice, + onnotify : o.onnotify, + onclose : o.onclose, + onparameter : o.onparameter, + socket : o.socket, + transform : parseTransform(o.transform || { undefined: undefined }), + parameters : {}, + shared : { retries: 0, typeArrayMap: {} }, + ...mergeUserTypes(o.types) + } +} + +function tsa(o, url, env) { + const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS + if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x)) + return x + + throw new Error('target_session_attrs ' + x + ' is not supported') +} + +function backoff(retries) { + return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20) +} + +function max_lifetime() { + return 60 * (30 + Math.random() * 30) +} + +function parseTransform(x) { + return { + undefined: x.undefined, + column: { + from: typeof x.column === 'function' ? x.column : x.column && x.column.from, + to: x.column && x.column.to + }, + value: { + from: typeof x.value === 'function' ? x.value : x.value && x.value.from, + to: x.value && x.value.to + }, + row: { + from: typeof x.row === 'function' ? x.row : x.row && x.row.from, + to: x.row && x.row.to + } + } +} + +function parseUrl(url) { + if (!url || typeof url !== 'string') + return { url: { searchParams: new Map() } } + + let host = url + host = host.slice(host.indexOf('://') + 3).split(/[?/]/)[0] + host = decodeURIComponent(host.slice(host.indexOf('@') + 1)) + + const urlObj = new URL(url.replace(host, host.split(',')[0])) + + return { + url: { + username: decodeURIComponent(urlObj.username), + password: decodeURIComponent(urlObj.password), + host: urlObj.host, + hostname: urlObj.hostname, + port: urlObj.port, + pathname: urlObj.pathname, + searchParams: urlObj.searchParams + }, + multihost: host.indexOf(',') > -1 && host + } +} + +function osUsername() { + try { + return os.userInfo().username // eslint-disable-line + } catch (_) { + return process.env.USERNAME || process.env.USER || process.env.LOGNAME // eslint-disable-line + } +} diff --git a/deno/src/large.js b/deno/src/large.js index 1b9f42d2..8ae150dd 100644 --- a/deno/src/large.js +++ b/deno/src/large.js @@ -1,4 +1,4 @@ -import Stream from 'https://deno.land/std@0.132.0/node/stream.ts' +import Stream from 'node:stream' export default function largeObject(sql, oid, mode = 0x00020000 | 0x00040000) { return new Promise(async(resolve, reject) => { diff --git a/deno/src/large.js.bak b/deno/src/large.js.bak new file mode 100644 index 00000000..1b9f42d2 --- /dev/null +++ b/deno/src/large.js.bak @@ -0,0 +1,70 @@ +import Stream from 'https://deno.land/std@0.132.0/node/stream.ts' + +export default function largeObject(sql, oid, mode = 0x00020000 | 0x00040000) { + return new Promise(async(resolve, reject) => { + await sql.begin(async sql => { + let finish + !oid && ([{ oid }] = await sql`select lo_creat(-1) as oid`) + const [{ fd }] = await sql`select lo_open(${ oid }, ${ mode }) as fd` + + const lo = { + writable, + readable, + close : () => sql`select lo_close(${ fd })`.then(finish), + tell : () => sql`select lo_tell64(${ fd })`, + read : (x) => sql`select loread(${ fd }, ${ x }) as data`, + write : (x) => sql`select lowrite(${ fd }, ${ x })`, + truncate : (x) => sql`select lo_truncate64(${ fd }, ${ x })`, + seek : (x, whence = 0) => sql`select lo_lseek64(${ fd }, ${ x }, ${ whence })`, + size : () => sql` + select + lo_lseek64(${ fd }, location, 0) as position, + seek.size + from ( + select + lo_lseek64($1, 0, 2) as size, + tell.location + from (select lo_tell64($1) as location) tell + ) seek + ` + } + + resolve(lo) + + return new Promise(async r => finish = r) + + async function readable({ + highWaterMark = 2048 * 8, + start = 0, + end = Infinity + } = {}) { + let max = end - start + start && await lo.seek(start) + return new Stream.Readable({ + highWaterMark, + async read(size) { + const l = size > max ? size - max : size + max -= size + const [{ data }] = await lo.read(l) + this.push(data) + if (data.length < size) + this.push(null) + } + }) + } + + async function writable({ + highWaterMark = 2048 * 8, + start = 0 + } = {}) { + start && await lo.seek(start) + return new Stream.Writable({ + highWaterMark, + write(chunk, encoding, callback) { + lo.write(chunk).then(() => callback(), callback) + } + }) + } + }).catch(reject) + }) +} diff --git a/deno/src/query.js.bak b/deno/src/query.js.bak new file mode 100644 index 00000000..0d44a15c --- /dev/null +++ b/deno/src/query.js.bak @@ -0,0 +1,173 @@ +const originCache = new Map() + , originStackCache = new Map() + , originError = Symbol('OriginError') + +export const CLOSE = {} +export class Query extends Promise { + constructor(strings, args, handler, canceller, options = {}) { + let resolve + , reject + + super((a, b) => { + resolve = a + reject = b + }) + + this.tagged = Array.isArray(strings.raw) + this.strings = strings + this.args = args + this.handler = handler + this.canceller = canceller + this.options = options + + this.state = null + this.statement = null + + this.resolve = x => (this.active = false, resolve(x)) + this.reject = x => (this.active = false, reject(x)) + + this.active = false + this.cancelled = null + this.executed = false + this.signature = '' + + this[originError] = this.handler.debug + ? new Error() + : this.tagged && cachedError(this.strings) + } + + get origin() { + return (this.handler.debug + ? this[originError].stack + : this.tagged && originStackCache.has(this.strings) + ? originStackCache.get(this.strings) + : originStackCache.set(this.strings, this[originError].stack).get(this.strings) + ) || '' + } + + static get [Symbol.species]() { + return Promise + } + + cancel() { + return this.canceller && (this.canceller(this), this.canceller = null) + } + + simple() { + this.options.simple = true + this.options.prepare = false + return this + } + + async readable() { + this.simple() + this.streaming = true + return this + } + + async writable() { + this.simple() + this.streaming = true + return this + } + + cursor(rows = 1, fn) { + this.options.simple = false + if (typeof rows === 'function') { + fn = rows + rows = 1 + } + + this.cursorRows = rows + + if (typeof fn === 'function') + return (this.cursorFn = fn, this) + + let prev + return { + [Symbol.asyncIterator]: () => ({ + next: () => { + if (this.executed && !this.active) + return { done: true } + + prev && prev() + const promise = new Promise((resolve, reject) => { + this.cursorFn = value => { + resolve({ value, done: false }) + return new Promise(r => prev = r) + } + this.resolve = () => (this.active = false, resolve({ done: true })) + this.reject = x => (this.active = false, reject(x)) + }) + this.execute() + return promise + }, + return() { + prev && prev(CLOSE) + return { done: true } + } + }) + } + } + + describe() { + this.options.simple = false + this.onlyDescribe = this.options.prepare = true + return this + } + + stream() { + throw new Error('.stream has been renamed to .forEach') + } + + forEach(fn) { + this.forEachFn = fn + this.handle() + return this + } + + raw() { + this.isRaw = true + return this + } + + values() { + this.isRaw = 'values' + return this + } + + async handle() { + !this.executed && (this.executed = true) && await 1 && this.handler(this) + } + + execute() { + this.handle() + return this + } + + then() { + this.handle() + return super.then.apply(this, arguments) + } + + catch() { + this.handle() + return super.catch.apply(this, arguments) + } + + finally() { + this.handle() + return super.finally.apply(this, arguments) + } +} + +function cachedError(xs) { + if (originCache.has(xs)) + return originCache.get(xs) + + const x = Error.stackTraceLimit + Error.stackTraceLimit = 4 + originCache.set(xs, new Error()) + Error.stackTraceLimit = x + return originCache.get(xs) +} diff --git a/deno/src/queue.js.bak b/deno/src/queue.js.bak new file mode 100644 index 00000000..c4ef9716 --- /dev/null +++ b/deno/src/queue.js.bak @@ -0,0 +1,31 @@ +export default Queue + +function Queue(initial = []) { + let xs = initial.slice() + let index = 0 + + return { + get length() { + return xs.length - index + }, + remove: (x) => { + const index = xs.indexOf(x) + return index === -1 + ? null + : (xs.splice(index, 1), x) + }, + push: (x) => (xs.push(x), x), + shift: () => { + const out = xs[index++] + + if (index === xs.length) { + index = 0 + xs = [] + } else { + xs[index - 1] = undefined + } + + return out + } + } +} diff --git a/deno/src/result.js.bak b/deno/src/result.js.bak new file mode 100644 index 00000000..31014284 --- /dev/null +++ b/deno/src/result.js.bak @@ -0,0 +1,16 @@ +export default class Result extends Array { + constructor() { + super() + Object.defineProperties(this, { + count: { value: null, writable: true }, + state: { value: null, writable: true }, + command: { value: null, writable: true }, + columns: { value: null, writable: true }, + statement: { value: null, writable: true } + }) + } + + static get [Symbol.species]() { + return Array + } +} diff --git a/deno/src/subscribe.js b/deno/src/subscribe.js index b20efb96..8716100e 100644 --- a/deno/src/subscribe.js +++ b/deno/src/subscribe.js @@ -1,4 +1,4 @@ -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' +import { Buffer } from 'node:buffer' const noop = () => { /* noop */ } export default function Subscribe(postgres, options) { diff --git a/deno/src/subscribe.js.bak b/deno/src/subscribe.js.bak new file mode 100644 index 00000000..b20efb96 --- /dev/null +++ b/deno/src/subscribe.js.bak @@ -0,0 +1,278 @@ +import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' +const noop = () => { /* noop */ } + +export default function Subscribe(postgres, options) { + const subscribers = new Map() + , slot = 'postgresjs_' + Math.random().toString(36).slice(2) + , state = {} + + let connection + , stream + , ended = false + + const sql = subscribe.sql = postgres({ + ...options, + transform: { column: {}, value: {}, row: {} }, + max: 1, + fetch_types: false, + idle_timeout: null, + max_lifetime: null, + connection: { + ...options.connection, + replication: 'database' + }, + onclose: async function() { + if (ended) + return + stream = null + state.pid = state.secret = undefined + connected(await init(sql, slot, options.publications)) + subscribers.forEach(event => event.forEach(({ onsubscribe }) => onsubscribe())) + }, + no_subscribe: true + }) + + const end = sql.end + , close = sql.close + + sql.end = async() => { + ended = true + stream && (await new Promise(r => (stream.once('close', r), stream.end()))) + return end() + } + + sql.close = async() => { + stream && (await new Promise(r => (stream.once('close', r), stream.end()))) + return close() + } + + return subscribe + + async function subscribe(event, fn, onsubscribe = noop, onerror = noop) { + event = parseEvent(event) + + if (!connection) + connection = init(sql, slot, options.publications) + + const subscriber = { fn, onsubscribe } + const fns = subscribers.has(event) + ? subscribers.get(event).add(subscriber) + : subscribers.set(event, new Set([subscriber])).get(event) + + const unsubscribe = () => { + fns.delete(subscriber) + fns.size === 0 && subscribers.delete(event) + } + + return connection.then(x => { + connected(x) + onsubscribe() + stream && stream.on('error', onerror) + return { unsubscribe, state, sql } + }) + } + + function connected(x) { + stream = x.stream + state.pid = x.state.pid + state.secret = x.state.secret + } + + async function init(sql, slot, publications) { + if (!publications) + throw new Error('Missing publication names') + + const xs = await sql.unsafe( + `CREATE_REPLICATION_SLOT ${ slot } TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT` + ) + + const [x] = xs + + const stream = await sql.unsafe( + `START_REPLICATION SLOT ${ slot } LOGICAL ${ + x.consistent_point + } (proto_version '1', publication_names '${ publications }')` + ).writable() + + const state = { + lsn: Buffer.concat(x.consistent_point.split('/').map(x => Buffer.from(('00000000' + x).slice(-8), 'hex'))) + } + + stream.on('data', data) + stream.on('error', error) + stream.on('close', sql.close) + + return { stream, state: xs.state } + + function error(e) { + console.error('Unexpected error during logical streaming - reconnecting', e) // eslint-disable-line + } + + function data(x) { + if (x[0] === 0x77) { + parse(x.subarray(25), state, sql.options.parsers, handle, options.transform) + } else if (x[0] === 0x6b && x[17]) { + state.lsn = x.subarray(1, 9) + pong() + } + } + + function handle(a, b) { + const path = b.relation.schema + '.' + b.relation.table + call('*', a, b) + call('*:' + path, a, b) + b.relation.keys.length && call('*:' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) + call(b.command, a, b) + call(b.command + ':' + path, a, b) + b.relation.keys.length && call(b.command + ':' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) + } + + function pong() { + const x = Buffer.alloc(34) + x[0] = 'r'.charCodeAt(0) + x.fill(state.lsn, 1) + x.writeBigInt64BE(BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000), 25) + stream.write(x) + } + } + + function call(x, a, b) { + subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => fn(a, b, x)) + } +} + +function Time(x) { + return new Date(Date.UTC(2000, 0, 1) + Number(x / BigInt(1000))) +} + +function parse(x, state, parsers, handle, transform) { + const char = (acc, [k, v]) => (acc[k.charCodeAt(0)] = v, acc) + + Object.entries({ + R: x => { // Relation + let i = 1 + const r = state[x.readUInt32BE(i)] = { + schema: x.toString('utf8', i += 4, i = x.indexOf(0, i)) || 'pg_catalog', + table: x.toString('utf8', i + 1, i = x.indexOf(0, i + 1)), + columns: Array(x.readUInt16BE(i += 2)), + keys: [] + } + i += 2 + + let columnIndex = 0 + , column + + while (i < x.length) { + column = r.columns[columnIndex++] = { + key: x[i++], + name: transform.column.from + ? transform.column.from(x.toString('utf8', i, i = x.indexOf(0, i))) + : x.toString('utf8', i, i = x.indexOf(0, i)), + type: x.readUInt32BE(i += 1), + parser: parsers[x.readUInt32BE(i)], + atttypmod: x.readUInt32BE(i += 4) + } + + column.key && r.keys.push(column) + i += 4 + } + }, + Y: () => { /* noop */ }, // Type + O: () => { /* noop */ }, // Origin + B: x => { // Begin + state.date = Time(x.readBigInt64BE(9)) + state.lsn = x.subarray(1, 9) + }, + I: x => { // Insert + let i = 1 + const relation = state[x.readUInt32BE(i)] + const { row } = tuples(x, relation.columns, i += 7, transform) + + handle(row, { + command: 'insert', + relation + }) + }, + D: x => { // Delete + let i = 1 + const relation = state[x.readUInt32BE(i)] + i += 4 + const key = x[i] === 75 + handle(key || x[i] === 79 + ? tuples(x, relation.columns, i += 3, transform).row + : null + , { + command: 'delete', + relation, + key + }) + }, + U: x => { // Update + let i = 1 + const relation = state[x.readUInt32BE(i)] + i += 4 + const key = x[i] === 75 + const xs = key || x[i] === 79 + ? tuples(x, relation.columns, i += 3, transform) + : null + + xs && (i = xs.i) + + const { row } = tuples(x, relation.columns, i + 3, transform) + + handle(row, { + command: 'update', + relation, + key, + old: xs && xs.row + }) + }, + T: () => { /* noop */ }, // Truncate, + C: () => { /* noop */ } // Commit + }).reduce(char, {})[x[0]](x) +} + +function tuples(x, columns, xi, transform) { + let type + , column + , value + + const row = transform.raw ? new Array(columns.length) : {} + for (let i = 0; i < columns.length; i++) { + type = x[xi++] + column = columns[i] + value = type === 110 // n + ? null + : type === 117 // u + ? undefined + : column.parser === undefined + ? x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi)) + : column.parser.array === true + ? column.parser(x.toString('utf8', xi + 5, xi += 4 + x.readUInt32BE(xi))) + : column.parser(x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi))) + + transform.raw + ? (row[i] = transform.raw === true + ? value + : transform.value.from ? transform.value.from(value, column) : value) + : (row[column.name] = transform.value.from + ? transform.value.from(value, column) + : value + ) + } + + return { i: xi, row: transform.row.from ? transform.row.from(row) : row } +} + +function parseEvent(x) { + const xs = x.match(/^(\*|insert|update|delete)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || [] + + if (!xs) + throw new Error('Malformed subscribe pattern: ' + x) + + const [, command, path, key] = xs + + return (command || '*') + + (path ? ':' + (path.indexOf('.') === -1 ? 'public.' + path : path) : '') + + (key ? '=' + key : '') +} diff --git a/deno/src/types.js b/deno/src/types.js index ea0da6a2..aa2ead29 100644 --- a/deno/src/types.js +++ b/deno/src/types.js @@ -1,4 +1,4 @@ -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' +import { Buffer } from 'node:buffer' import { Query } from './query.js' import { Errors } from './errors.js' diff --git a/deno/src/types.js.bak b/deno/src/types.js.bak new file mode 100644 index 00000000..ea0da6a2 --- /dev/null +++ b/deno/src/types.js.bak @@ -0,0 +1,368 @@ +import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' +import { Query } from './query.js' +import { Errors } from './errors.js' + +export const types = { + string: { + to: 25, + from: null, // defaults to string + serialize: x => '' + x + }, + number: { + to: 0, + from: [21, 23, 26, 700, 701], + serialize: x => '' + x, + parse: x => +x + }, + json: { + to: 114, + from: [114, 3802], + serialize: x => JSON.stringify(x), + parse: x => JSON.parse(x) + }, + boolean: { + to: 16, + from: 16, + serialize: x => x === true ? 't' : 'f', + parse: x => x === 't' + }, + date: { + to: 1184, + from: [1082, 1114, 1184], + serialize: x => (x instanceof Date ? x : new Date(x)).toISOString(), + parse: x => new Date(x) + }, + bytea: { + to: 17, + from: 17, + serialize: x => '\\x' + Buffer.from(x).toString('hex'), + parse: x => Buffer.from(x.slice(2), 'hex') + } +} + +class NotTagged { then() { notTagged() } catch() { notTagged() } finally() { notTagged() }} + +export class Identifier extends NotTagged { + constructor(value) { + super() + this.value = escapeIdentifier(value) + } +} + +export class Parameter extends NotTagged { + constructor(value, type, array) { + super() + this.value = value + this.type = type + this.array = array + } +} + +export class Builder extends NotTagged { + constructor(first, rest) { + super() + this.first = first + this.rest = rest + } + + build(before, parameters, types, options) { + const keyword = builders.map(([x, fn]) => ({ fn, i: before.search(x) })).sort((a, b) => a.i - b.i).pop() + return keyword.i === -1 + ? escapeIdentifiers(this.first, options) + : keyword.fn(this.first, this.rest, parameters, types, options) + } +} + +export function handleValue(x, parameters, types, options) { + let value = x instanceof Parameter ? x.value : x + if (value === undefined) { + x instanceof Parameter + ? x.value = options.transform.undefined + : value = x = options.transform.undefined + + if (value === undefined) + throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') + } + + return '$' + (types.push( + x instanceof Parameter + ? (parameters.push(x.value), x.array + ? x.array[x.type || inferType(x.value)] || x.type || firstIsString(x.value) + : x.type + ) + : (parameters.push(x), inferType(x)) + )) +} + +const defaultHandlers = typeHandlers(types) + +export function stringify(q, string, value, parameters, types, options) { // eslint-disable-line + for (let i = 1; i < q.strings.length; i++) { + string += (stringifyValue(string, value, parameters, types, options)) + q.strings[i] + value = q.args[i] + } + + return string +} + +function stringifyValue(string, value, parameters, types, o) { + return ( + value instanceof Builder ? value.build(string, parameters, types, o) : + value instanceof Query ? fragment(value, parameters, types, o) : + value instanceof Identifier ? value.value : + value && value[0] instanceof Query ? value.reduce((acc, x) => acc + ' ' + fragment(x, parameters, types, o), '') : + handleValue(value, parameters, types, o) + ) +} + +function fragment(q, parameters, types, options) { + q.fragment = true + return stringify(q, q.strings[0], q.args[0], parameters, types, options) +} + +function valuesBuilder(first, parameters, types, columns, options) { + return first.map(row => + '(' + columns.map(column => + stringifyValue('values', row[column], parameters, types, options) + ).join(',') + ')' + ).join(',') +} + +function values(first, rest, parameters, types, options) { + const multi = Array.isArray(first[0]) + const columns = rest.length ? rest.flat() : Object.keys(multi ? first[0] : first) + return valuesBuilder(multi ? first : [first], parameters, types, columns, options) +} + +function select(first, rest, parameters, types, options) { + typeof first === 'string' && (first = [first].concat(rest)) + if (Array.isArray(first)) + return escapeIdentifiers(first, options) + + let value + const columns = rest.length ? rest.flat() : Object.keys(first) + return columns.map(x => { + value = first[x] + return ( + value instanceof Query ? fragment(value, parameters, types, options) : + value instanceof Identifier ? value.value : + handleValue(value, parameters, types, options) + ) + ' as ' + escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) + }).join(',') +} + +const builders = Object.entries({ + values, + in: (...xs) => { + const x = values(...xs) + return x === '()' ? '(null)' : x + }, + select, + as: select, + returning: select, + '\\(': select, + + update(first, rest, parameters, types, options) { + return (rest.length ? rest.flat() : Object.keys(first)).map(x => + escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) + + '=' + stringifyValue('values', first[x], parameters, types, options) + ) + }, + + insert(first, rest, parameters, types, options) { + const columns = rest.length ? rest.flat() : Object.keys(Array.isArray(first) ? first[0] : first) + return '(' + escapeIdentifiers(columns, options) + ')values' + + valuesBuilder(Array.isArray(first) ? first : [first], parameters, types, columns, options) + } +}).map(([x, fn]) => ([new RegExp('((?:^|[\\s(])' + x + '(?:$|[\\s(]))(?![\\s\\S]*\\1)', 'i'), fn])) + +function notTagged() { + throw Errors.generic('NOT_TAGGED_CALL', 'Query not called as a tagged template literal') +} + +export const serializers = defaultHandlers.serializers +export const parsers = defaultHandlers.parsers + +export const END = {} + +function firstIsString(x) { + if (Array.isArray(x)) + return firstIsString(x[0]) + return typeof x === 'string' ? 1009 : 0 +} + +export const mergeUserTypes = function(types) { + const user = typeHandlers(types || {}) + return { + serializers: Object.assign({}, serializers, user.serializers), + parsers: Object.assign({}, parsers, user.parsers) + } +} + +function typeHandlers(types) { + return Object.keys(types).reduce((acc, k) => { + types[k].from && [].concat(types[k].from).forEach(x => acc.parsers[x] = types[k].parse) + if (types[k].serialize) { + acc.serializers[types[k].to] = types[k].serialize + types[k].from && [].concat(types[k].from).forEach(x => acc.serializers[x] = types[k].serialize) + } + return acc + }, { parsers: {}, serializers: {} }) +} + +function escapeIdentifiers(xs, { transform: { column } }) { + return xs.map(x => escapeIdentifier(column.to ? column.to(x) : x)).join(',') +} + +export const escapeIdentifier = function escape(str) { + return '"' + str.replace(/"/g, '""').replace(/\./g, '"."') + '"' +} + +export const inferType = function inferType(x) { + return ( + x instanceof Parameter ? x.type : + x instanceof Date ? 1184 : + x instanceof Uint8Array ? 17 : + (x === true || x === false) ? 16 : + typeof x === 'bigint' ? 20 : + Array.isArray(x) ? inferType(x[0]) : + 0 + ) +} + +const escapeBackslash = /\\/g +const escapeQuote = /"/g + +function arrayEscape(x) { + return x + .replace(escapeBackslash, '\\\\') + .replace(escapeQuote, '\\"') +} + +export const arraySerializer = function arraySerializer(xs, serializer, options, typarray) { + if (Array.isArray(xs) === false) + return xs + + if (!xs.length) + return '{}' + + const first = xs[0] + // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter + const delimiter = typarray === 1020 ? ';' : ',' + + if (Array.isArray(first) && !first.type) + return '{' + xs.map(x => arraySerializer(x, serializer, options, typarray)).join(delimiter) + '}' + + return '{' + xs.map(x => { + if (x === undefined) { + x = options.transform.undefined + if (x === undefined) + throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') + } + + return x === null + ? 'null' + : '"' + arrayEscape(serializer ? serializer(x.type ? x.value : x) : '' + x) + '"' + }).join(delimiter) + '}' +} + +const arrayParserState = { + i: 0, + char: null, + str: '', + quoted: false, + last: 0 +} + +export const arrayParser = function arrayParser(x, parser, typarray) { + arrayParserState.i = arrayParserState.last = 0 + return arrayParserLoop(arrayParserState, x, parser, typarray) +} + +function arrayParserLoop(s, x, parser, typarray) { + const xs = [] + // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter + const delimiter = typarray === 1020 ? ';' : ',' + for (; s.i < x.length; s.i++) { + s.char = x[s.i] + if (s.quoted) { + if (s.char === '\\') { + s.str += x[++s.i] + } else if (s.char === '"') { + xs.push(parser ? parser(s.str) : s.str) + s.str = '' + s.quoted = x[s.i + 1] === '"' + s.last = s.i + 2 + } else { + s.str += s.char + } + } else if (s.char === '"') { + s.quoted = true + } else if (s.char === '{') { + s.last = ++s.i + xs.push(arrayParserLoop(s, x, parser, typarray)) + } else if (s.char === '}') { + s.quoted = false + s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) + s.last = s.i + 1 + break + } else if (s.char === delimiter && s.p !== '}' && s.p !== '"') { + xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) + s.last = s.i + 1 + } + s.p = s.char + } + s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i + 1)) : x.slice(s.last, s.i + 1)) + return xs +} + +export const toCamel = x => { + let str = x[0] + for (let i = 1; i < x.length; i++) + str += x[i] === '_' ? x[++i].toUpperCase() : x[i] + return str +} + +export const toPascal = x => { + let str = x[0].toUpperCase() + for (let i = 1; i < x.length; i++) + str += x[i] === '_' ? x[++i].toUpperCase() : x[i] + return str +} + +export const toKebab = x => x.replace(/_/g, '-') + +export const fromCamel = x => x.replace(/([A-Z])/g, '_$1').toLowerCase() +export const fromPascal = x => (x.slice(0, 1) + x.slice(1).replace(/([A-Z])/g, '_$1')).toLowerCase() +export const fromKebab = x => x.replace(/-/g, '_') + +function createJsonTransform(fn) { + return function jsonTransform(x, column) { + return typeof x === 'object' && x !== null && (column.type === 114 || column.type === 3802) + ? Array.isArray(x) + ? x.map(x => jsonTransform(x, column)) + : Object.entries(x).reduce((acc, [k, v]) => Object.assign(acc, { [fn(k)]: jsonTransform(v, column) }), {}) + : x + } +} + +toCamel.column = { from: toCamel } +toCamel.value = { from: createJsonTransform(toCamel) } +fromCamel.column = { to: fromCamel } + +export const camel = { ...toCamel } +camel.column.to = fromCamel + +toPascal.column = { from: toPascal } +toPascal.value = { from: createJsonTransform(toPascal) } +fromPascal.column = { to: fromPascal } + +export const pascal = { ...toPascal } +pascal.column.to = fromPascal + +toKebab.column = { from: toKebab } +toKebab.value = { from: createJsonTransform(toKebab) } +fromKebab.column = { to: fromKebab } + +export const kebab = { ...toKebab } +kebab.column.to = fromKebab diff --git a/deno/tests/bootstrap.js b/deno/tests/bootstrap.js index da416896..c75ddc40 100644 --- a/deno/tests/bootstrap.js +++ b/deno/tests/bootstrap.js @@ -1,4 +1,4 @@ -import { spawn } from 'https://deno.land/std@0.132.0/node/child_process.ts' +import { spawn } from 'node:child_process' await exec('dropdb', ['postgres_js_test']) diff --git a/deno/tests/bootstrap.js.bak b/deno/tests/bootstrap.js.bak new file mode 100644 index 00000000..da416896 --- /dev/null +++ b/deno/tests/bootstrap.js.bak @@ -0,0 +1,34 @@ +import { spawn } from 'https://deno.land/std@0.132.0/node/child_process.ts' + +await exec('dropdb', ['postgres_js_test']) + +await exec('psql', ['-c', 'alter system set ssl=on']) +await exec('psql', ['-c', 'drop user postgres_js_test']) +await exec('psql', ['-c', 'create user postgres_js_test']) +await exec('psql', ['-c', 'alter system set password_encryption=md5']) +await exec('psql', ['-c', 'select pg_reload_conf()']) +await exec('psql', ['-c', 'drop user if exists postgres_js_test_md5']) +await exec('psql', ['-c', 'create user postgres_js_test_md5 with password \'postgres_js_test_md5\'']) +await exec('psql', ['-c', 'alter system set password_encryption=\'scram-sha-256\'']) +await exec('psql', ['-c', 'select pg_reload_conf()']) +await exec('psql', ['-c', 'drop user if exists postgres_js_test_scram']) +await exec('psql', ['-c', 'create user postgres_js_test_scram with password \'postgres_js_test_scram\'']) + +await exec('createdb', ['postgres_js_test']) +await exec('psql', ['-c', 'grant all on database postgres_js_test to postgres_js_test']) +await exec('psql', ['-c', 'alter database postgres_js_test owner to postgres_js_test']) + +function ignore(cmd, args) { + const { stderr } = spawnSync(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) + if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) + throw stderr +} + +export async function exec(cmd, args) { // eslint-disable-line + let stderr = '' + const cp = await spawn(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) // eslint-disable-line + cp.stderr.on('data', x => stderr += x) + await new Promise(x => cp.on('exit', x)) + if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) + throw new Error(stderr) +} diff --git a/deno/tests/index.js b/deno/tests/index.js index adedf1e0..32c2a0df 100644 --- a/deno/tests/index.js +++ b/deno/tests/index.js @@ -1,11 +1,11 @@ -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' -import process from 'https://deno.land/std@0.132.0/node/process.ts' +import { Buffer } from 'node:buffer' +import process from 'node:process' import { exec } from './bootstrap.js' import { t, nt, ot } from './test.js' // eslint-disable-line import { net } from '../polyfills.js' -import fs from 'https://deno.land/std@0.132.0/node/fs.ts' -import crypto from 'https://deno.land/std@0.132.0/node/crypto.ts' +import fs from 'node:fs' +import crypto from 'node:crypto' import postgres from '../src/index.js' const delay = ms => new Promise(r => setTimeout(r, ms)) diff --git a/deno/tests/index.js.bak b/deno/tests/index.js.bak new file mode 100644 index 00000000..adedf1e0 --- /dev/null +++ b/deno/tests/index.js.bak @@ -0,0 +1,2620 @@ +import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' +import process from 'https://deno.land/std@0.132.0/node/process.ts' +import { exec } from './bootstrap.js' + +import { t, nt, ot } from './test.js' // eslint-disable-line +import { net } from '../polyfills.js' +import fs from 'https://deno.land/std@0.132.0/node/fs.ts' +import crypto from 'https://deno.land/std@0.132.0/node/crypto.ts' + +import postgres from '../src/index.js' +const delay = ms => new Promise(r => setTimeout(r, ms)) + +const rel = x => new URL(x, import.meta.url) +const idle_timeout = 1 + +const login = { + user: 'postgres_js_test' +} + +const login_md5 = { + user: 'postgres_js_test_md5', + pass: 'postgres_js_test_md5' +} + +const login_scram = { + user: 'postgres_js_test_scram', + pass: 'postgres_js_test_scram' +} + +const options = { + db: 'postgres_js_test', + user: login.user, + pass: login.pass, + idle_timeout, + connect_timeout: 1, + max: 1 +} + +const sql = postgres(options) + +t('Connects with no options', async() => { + const sql = postgres({ max: 1 }) + + const result = (await sql`select 1 as x`)[0].x + await sql.end() + + return [1, result] +}) + +t('Uses default database without slash', async() => { + const sql = postgres('postgres://localhost') + return [sql.options.user, sql.options.database] +}) + +t('Uses default database with slash', async() => { + const sql = postgres('postgres://localhost/') + return [sql.options.user, sql.options.database] +}) + +t('Result is array', async() => + [true, Array.isArray(await sql`select 1`)] +) + +t('Result has count', async() => + [1, (await sql`select 1`).count] +) + +t('Result has command', async() => + ['SELECT', (await sql`select 1`).command] +) + +t('Create table', async() => + ['CREATE TABLE', (await sql`create table test(int int)`).command, await sql`drop table test`] +) + +t('Drop table', { timeout: 2 }, async() => { + await sql`create table test(int int)` + return ['DROP TABLE', (await sql`drop table test`).command] +}) + +t('null', async() => + [null, (await sql`select ${ null } as x`)[0].x] +) + +t('Integer', async() => + ['1', (await sql`select ${ 1 } as x`)[0].x] +) + +t('String', async() => + ['hello', (await sql`select ${ 'hello' } as x`)[0].x] +) + +t('Boolean false', async() => + [false, (await sql`select ${ false } as x`)[0].x] +) + +t('Boolean true', async() => + [true, (await sql`select ${ true } as x`)[0].x] +) + +t('Date', async() => { + const now = new Date() + return [0, now - (await sql`select ${ now } as x`)[0].x] +}) + +t('Json', async() => { + const x = (await sql`select ${ sql.json({ a: 'hello', b: 42 }) } as x`)[0].x + return ['hello,42', [x.a, x.b].join()] +}) + +t('implicit json', async() => { + const x = (await sql`select ${ { a: 'hello', b: 42 } }::json as x`)[0].x + return ['hello,42', [x.a, x.b].join()] +}) + +t('implicit jsonb', async() => { + const x = (await sql`select ${ { a: 'hello', b: 42 } }::jsonb as x`)[0].x + return ['hello,42', [x.a, x.b].join()] +}) + +t('Empty array', async() => + [true, Array.isArray((await sql`select ${ sql.array([], 1009) } as x`)[0].x)] +) + +t('String array', async() => + ['123', (await sql`select ${ '{1,2,3}' }::int[] as x`)[0].x.join('')] +) + +t('Array of Integer', async() => + ['3', (await sql`select ${ sql.array([1, 2, 3]) } as x`)[0].x[2]] +) + +t('Array of String', async() => + ['c', (await sql`select ${ sql.array(['a', 'b', 'c']) } as x`)[0].x[2]] +) + +t('Array of Date', async() => { + const now = new Date() + return [now.getTime(), (await sql`select ${ sql.array([now, now, now]) } as x`)[0].x[2].getTime()] +}) + +t('Array of Box', async() => [ + '(3,4),(1,2);(6,7),(4,5)', + (await sql`select ${ '{(1,2),(3,4);(4,5),(6,7)}' }::box[] as x`)[0].x.join(';') +]) + +t('Nested array n2', async() => + ['4', (await sql`select ${ sql.array([[1, 2], [3, 4]]) } as x`)[0].x[1][1]] +) + +t('Nested array n3', async() => + ['6', (await sql`select ${ sql.array([[[1, 2]], [[3, 4]], [[5, 6]]]) } as x`)[0].x[2][0][1]] +) + +t('Escape in arrays', async() => + ['Hello "you",c:\\windows', (await sql`select ${ sql.array(['Hello "you"', 'c:\\windows']) } as x`)[0].x.join(',')] +) + +t('Escapes', async() => { + return ['hej"hej', Object.keys((await sql`select 1 as ${ sql('hej"hej') }`)[0])[0]] +}) + +t('null for int', async() => { + await sql`create table test (x int)` + return [1, (await sql`insert into test values(${ null })`).count, await sql`drop table test`] +}) + +t('Throws on illegal transactions', async() => { + const sql = postgres({ ...options, max: 2, fetch_types: false }) + const error = await sql`begin`.catch(e => e) + return [ + error.code, + 'UNSAFE_TRANSACTION' + ] +}) + +t('Transaction throws', async() => { + await sql`create table test (a int)` + return ['22P02', await sql.begin(async sql => { + await sql`insert into test values(1)` + await sql`insert into test values('hej')` + }).catch(x => x.code), await sql`drop table test`] +}) + +t('Transaction rolls back', async() => { + await sql`create table test (a int)` + await sql.begin(async sql => { + await sql`insert into test values(1)` + await sql`insert into test values('hej')` + }).catch(() => { /* ignore */ }) + return [0, (await sql`select a from test`).count, await sql`drop table test`] +}) + +t('Transaction throws on uncaught savepoint', async() => { + await sql`create table test (a int)` + + return ['fail', (await sql.begin(async sql => { + await sql`insert into test values(1)` + await sql.savepoint(async sql => { + await sql`insert into test values(2)` + throw new Error('fail') + }) + }).catch((err) => err.message)), await sql`drop table test`] +}) + +t('Transaction throws on uncaught named savepoint', async() => { + await sql`create table test (a int)` + + return ['fail', (await sql.begin(async sql => { + await sql`insert into test values(1)` + await sql.savepoit('watpoint', async sql => { + await sql`insert into test values(2)` + throw new Error('fail') + }) + }).catch(() => 'fail')), await sql`drop table test`] +}) + +t('Transaction succeeds on caught savepoint', async() => { + await sql`create table test (a int)` + await sql.begin(async sql => { + await sql`insert into test values(1)` + await sql.savepoint(async sql => { + await sql`insert into test values(2)` + throw new Error('please rollback') + }).catch(() => { /* ignore */ }) + await sql`insert into test values(3)` + }) + + return ['2', (await sql`select count(1) from test`)[0].count, await sql`drop table test`] +}) + +t('Savepoint returns Result', async() => { + let result + await sql.begin(async sql => { + result = await sql.savepoint(sql => + sql`select 1 as x` + ) + }) + + return [1, result[0].x] +}) + +t('Prepared transaction', async() => { + await sql`create table test (a int)` + + await sql.begin(async sql => { + await sql`insert into test values(1)` + await sql.prepare('tx1') + }) + + await sql`commit prepared 'tx1'` + + return ['1', (await sql`select count(1) from test`)[0].count, await sql`drop table test`] +}) + +t('Transaction requests are executed implicitly', async() => { + const sql = postgres({ debug: true, idle_timeout: 1, fetch_types: false }) + return [ + 'testing', + (await sql.begin(sql => [ + sql`select set_config('postgres_js.test', 'testing', true)`, + sql`select current_setting('postgres_js.test') as x` + ]))[1][0].x + ] +}) + +t('Uncaught transaction request errors bubbles to transaction', async() => [ + '42703', + (await sql.begin(sql => [ + sql`select wat`, + sql`select current_setting('postgres_js.test') as x, ${ 1 } as a` + ]).catch(e => e.code)) +]) + +t('Fragments in transactions', async() => [ + true, + (await sql.begin(sql => sql`select true as x where ${ sql`1=1` }`))[0].x +]) + +t('Transaction rejects with rethrown error', async() => [ + 'WAT', + await sql.begin(async sql => { + try { + await sql`select exception` + } catch (ex) { + throw new Error('WAT') + } + }).catch(e => e.message) +]) + +t('Parallel transactions', async() => { + await sql`create table test (a int)` + return ['11', (await Promise.all([ + sql.begin(sql => sql`select 1`), + sql.begin(sql => sql`select 1`) + ])).map(x => x.count).join(''), await sql`drop table test`] +}) + +t('Many transactions at beginning of connection', async() => { + const sql = postgres(options) + const xs = await Promise.all(Array.from({ length: 100 }, () => sql.begin(sql => sql`select 1`))) + return [100, xs.length] +}) + +t('Transactions array', async() => { + await sql`create table test (a int)` + + return ['11', (await sql.begin(sql => [ + sql`select 1`.then(x => x), + sql`select 1` + ])).map(x => x.count).join(''), await sql`drop table test`] +}) + +t('Transaction waits', async() => { + await sql`create table test (a int)` + await sql.begin(async sql => { + await sql`insert into test values(1)` + await sql.savepoint(async sql => { + await sql`insert into test values(2)` + throw new Error('please rollback') + }).catch(() => { /* ignore */ }) + await sql`insert into test values(3)` + }) + + return ['11', (await Promise.all([ + sql.begin(sql => sql`select 1`), + sql.begin(sql => sql`select 1`) + ])).map(x => x.count).join(''), await sql`drop table test`] +}) + +t('Helpers in Transaction', async() => { + return ['1', (await sql.begin(async sql => + await sql`select ${ sql({ x: 1 }) }` + ))[0].x] +}) + +t('Undefined values throws', async() => { + let error + + await sql` + select ${ undefined } as x + `.catch(x => error = x.code) + + return ['UNDEFINED_VALUE', error] +}) + +t('Transform undefined', async() => { + const sql = postgres({ ...options, transform: { undefined: null } }) + return [null, (await sql`select ${ undefined } as x`)[0].x] +}) + +t('Transform undefined in array', async() => { + const sql = postgres({ ...options, transform: { undefined: null } }) + return [null, (await sql`select * from (values ${ sql([undefined, undefined]) }) as x(x, y)`)[0].y] +}) + +t('Null sets to null', async() => + [null, (await sql`select ${ null } as x`)[0].x] +) + +t('Throw syntax error', async() => + ['42601', (await sql`wat 1`.catch(x => x)).code] +) + +t('Connect using uri', async() => + [true, await new Promise((resolve, reject) => { + const sql = postgres('postgres://' + login.user + ':' + (login.pass || '') + '@localhost:5432/' + options.db, { + idle_timeout + }) + sql`select 1`.then(() => resolve(true), reject) + })] +) + +t('Options from uri with special characters in user and pass', async() => { + const opt = postgres({ user: 'öla', pass: 'pass^word' }).options + return [[opt.user, opt.pass].toString(), 'öla,pass^word'] +}) + +t('Fail with proper error on no host', async() => + ['ECONNREFUSED', (await new Promise((resolve, reject) => { + const sql = postgres('postgres://localhost:33333/' + options.db, { + idle_timeout + }) + sql`select 1`.then(reject, resolve) + })).code] +) + +t('Connect using SSL', async() => + [true, (await new Promise((resolve, reject) => { + postgres({ + ssl: { rejectUnauthorized: false }, + idle_timeout + })`select 1`.then(() => resolve(true), reject) + }))] +) + +t('Connect using SSL require', async() => + [true, (await new Promise((resolve, reject) => { + postgres({ + ssl: 'require', + idle_timeout + })`select 1`.then(() => resolve(true), reject) + }))] +) + +t('Connect using SSL prefer', async() => { + await exec('psql', ['-c', 'alter system set ssl=off']) + await exec('psql', ['-c', 'select pg_reload_conf()']) + + const sql = postgres({ + ssl: 'prefer', + idle_timeout + }) + + return [ + 1, (await sql`select 1 as x`)[0].x, + await exec('psql', ['-c', 'alter system set ssl=on']), + await exec('psql', ['-c', 'select pg_reload_conf()']) + ] +}) + +t('Reconnect using SSL', { timeout: 2 }, async() => { + const sql = postgres({ + ssl: 'require', + idle_timeout: 0.1 + }) + + await sql`select 1` + await delay(200) + + return [1, (await sql`select 1 as x`)[0].x] +}) + +t('Proper handling of non object Errors', async() => { + const sql = postgres({ socket: () => { throw 'wat' } }) // eslint-disable-line + + return [ + 'wat', await sql`select 1 as x`.catch(e => e.message) + ] +}) + +t('Proper handling of null Errors', async() => { + const sql = postgres({ socket: () => { throw null } }) // eslint-disable-line + + return [ + 'null', await sql`select 1 as x`.catch(e => e.message) + ] +}) + +t('Ensure reserve on connection throws proper error', async() => { + const sql = postgres({ socket: () => { throw 'wat' }, idle_timeout }) // eslint-disable-line + + return [ + 'wat', await sql.reserve().catch(e => e) + ] +}) + +t('Login without password', async() => { + return [true, (await postgres({ ...options, ...login })`select true as x`)[0].x] +}) + +t('Login using MD5', async() => { + return [true, (await postgres({ ...options, ...login_md5 })`select true as x`)[0].x] +}) + +t('Login using scram-sha-256', async() => { + return [true, (await postgres({ ...options, ...login_scram })`select true as x`)[0].x] +}) + +t('Parallel connections using scram-sha-256', { + timeout: 2 +}, async() => { + const sql = postgres({ ...options, ...login_scram }) + return [true, (await Promise.all([ + sql`select true as x, pg_sleep(0.01)`, + sql`select true as x, pg_sleep(0.01)`, + sql`select true as x, pg_sleep(0.01)` + ]))[0][0].x] +}) + +t('Support dynamic password function', async() => { + return [true, (await postgres({ + ...options, + ...login_scram, + pass: () => 'postgres_js_test_scram' + })`select true as x`)[0].x] +}) + +t('Support dynamic async password function', async() => { + return [true, (await postgres({ + ...options, + ...login_scram, + pass: () => Promise.resolve('postgres_js_test_scram') + })`select true as x`)[0].x] +}) + +t('Point type', async() => { + const sql = postgres({ + ...options, + types: { + point: { + to: 600, + from: [600], + serialize: ([x, y]) => '(' + x + ',' + y + ')', + parse: (x) => x.slice(1, -1).split(',').map(x => +x) + } + } + }) + + await sql`create table test (x point)` + await sql`insert into test (x) values (${ sql.types.point([10, 20]) })` + return [20, (await sql`select x from test`)[0].x[1], await sql`drop table test`] +}) + +t('Point type array', async() => { + const sql = postgres({ + ...options, + types: { + point: { + to: 600, + from: [600], + serialize: ([x, y]) => '(' + x + ',' + y + ')', + parse: (x) => x.slice(1, -1).split(',').map(x => +x) + } + } + }) + + await sql`create table test (x point[])` + await sql`insert into test (x) values (${ sql.array([sql.types.point([10, 20]), sql.types.point([20, 30])]) })` + return [30, (await sql`select x from test`)[0].x[1][1], await sql`drop table test`] +}) + +t('sql file', async() => + [1, (await sql.file(rel('select.sql')))[0].x] +) + +t('sql file has forEach', async() => { + let result + await sql + .file(rel('select.sql'), { cache: false }) + .forEach(({ x }) => result = x) + + return [1, result] +}) + +t('sql file throws', async() => + ['ENOENT', (await sql.file(rel('selectomondo.sql')).catch(x => x.code))] +) + +t('sql file cached', async() => { + await sql.file(rel('select.sql')) + await delay(20) + + return [1, (await sql.file(rel('select.sql')))[0].x] +}) + +t('Parameters in file', async() => { + const result = await sql.file( + rel('select-param.sql'), + ['hello'] + ) + return ['hello', result[0].x] +}) + +t('Connection ended promise', async() => { + const sql = postgres(options) + + await sql.end() + + return [undefined, await sql.end()] +}) + +t('Connection ended timeout', async() => { + const sql = postgres(options) + + await sql.end({ timeout: 10 }) + + return [undefined, await sql.end()] +}) + +t('Connection ended error', async() => { + const sql = postgres(options) + await sql.end() + return ['CONNECTION_ENDED', (await sql``.catch(x => x.code))] +}) + +t('Connection end does not cancel query', async() => { + const sql = postgres(options) + + const promise = sql`select 1 as x`.execute() + + await sql.end() + + return [1, (await promise)[0].x] +}) + +t('Connection destroyed', async() => { + const sql = postgres(options) + process.nextTick(() => sql.end({ timeout: 0 })) + return ['CONNECTION_DESTROYED', await sql``.catch(x => x.code)] +}) + +t('Connection destroyed with query before', async() => { + const sql = postgres(options) + , error = sql`select pg_sleep(0.2)`.catch(err => err.code) + + sql.end({ timeout: 0 }) + return ['CONNECTION_DESTROYED', await error] +}) + +t('transform column', async() => { + const sql = postgres({ + ...options, + transform: { column: x => x.split('').reverse().join('') } + }) + + await sql`create table test (hello_world int)` + await sql`insert into test values (1)` + return ['dlrow_olleh', Object.keys((await sql`select * from test`)[0])[0], await sql`drop table test`] +}) + +t('column toPascal', async() => { + const sql = postgres({ + ...options, + transform: { column: postgres.toPascal } + }) + + await sql`create table test (hello_world int)` + await sql`insert into test values (1)` + return ['HelloWorld', Object.keys((await sql`select * from test`)[0])[0], await sql`drop table test`] +}) + +t('column toCamel', async() => { + const sql = postgres({ + ...options, + transform: { column: postgres.toCamel } + }) + + await sql`create table test (hello_world int)` + await sql`insert into test values (1)` + return ['helloWorld', Object.keys((await sql`select * from test`)[0])[0], await sql`drop table test`] +}) + +t('column toKebab', async() => { + const sql = postgres({ + ...options, + transform: { column: postgres.toKebab } + }) + + await sql`create table test (hello_world int)` + await sql`insert into test values (1)` + return ['hello-world', Object.keys((await sql`select * from test`)[0])[0], await sql`drop table test`] +}) + +t('Transform nested json in arrays', async() => { + const sql = postgres({ + ...options, + transform: postgres.camel + }) + return ['aBcD', (await sql`select '[{"a_b":1},{"c_d":2}]'::jsonb as x`)[0].x.map(Object.keys).join('')] +}) + +t('Transform deeply nested json object in arrays', async() => { + const sql = postgres({ + ...options, + transform: postgres.camel + }) + return [ + 'childObj_deeplyNestedObj_grandchildObj', + (await sql` + select '[{"nested_obj": {"child_obj": 2, "deeply_nested_obj": {"grandchild_obj": 3}}}]'::jsonb as x + `)[0].x.map(x => { + let result + for (const key in x) + result = [...Object.keys(x[key]), ...Object.keys(x[key].deeplyNestedObj)] + return result + })[0] + .join('_') + ] +}) + +t('Transform deeply nested json array in arrays', async() => { + const sql = postgres({ + ...options, + transform: postgres.camel + }) + return [ + 'childArray_deeplyNestedArray_grandchildArray', + (await sql` + select '[{"nested_array": [{"child_array": 2, "deeply_nested_array": [{"grandchild_array":3}]}]}]'::jsonb AS x + `)[0].x.map((x) => { + let result + for (const key in x) + result = [...Object.keys(x[key][0]), ...Object.keys(x[key][0].deeplyNestedArray[0])] + return result + })[0] + .join('_') + ] +}) + +t('Bypass transform for json primitive', async() => { + const sql = postgres({ + ...options, + transform: postgres.camel + }) + + const x = ( + await sql`select 'null'::json as a, 'false'::json as b, '"a"'::json as c, '1'::json as d` + )[0] + + return [ + JSON.stringify({ a: null, b: false, c: 'a', d: 1 }), + JSON.stringify(x) + ] +}) + +t('Bypass transform for jsonb primitive', async() => { + const sql = postgres({ + ...options, + transform: postgres.camel + }) + + const x = ( + await sql`select 'null'::jsonb as a, 'false'::jsonb as b, '"a"'::jsonb as c, '1'::jsonb as d` + )[0] + + return [ + JSON.stringify({ a: null, b: false, c: 'a', d: 1 }), + JSON.stringify(x) + ] +}) + +t('unsafe', async() => { + await sql`create table test (x int)` + return [1, (await sql.unsafe('insert into test values ($1) returning *', [1]))[0].x, await sql`drop table test`] +}) + +t('unsafe simple', async() => { + return [1, (await sql.unsafe('select 1 as x'))[0].x] +}) + +t('unsafe simple includes columns', async() => { + return ['x', (await sql.unsafe('select 1 as x').values()).columns[0].name] +}) + +t('unsafe describe', async() => { + const q = 'insert into test values (1)' + await sql`create table test(a int unique)` + await sql.unsafe(q).describe() + const x = await sql.unsafe(q).describe() + return [ + q, + x.string, + await sql`drop table test` + ] +}) + +t('simple query using unsafe with multiple statements', async() => { + return [ + '1,2', + (await sql.unsafe('select 1 as x;select 2 as x')).map(x => x[0].x).join() + ] +}) + +t('simple query using simple() with multiple statements', async() => { + return [ + '1,2', + (await sql`select 1 as x;select 2 as x`.simple()).map(x => x[0].x).join() + ] +}) + +t('listen and notify', async() => { + const sql = postgres(options) + const channel = 'hello' + const result = await new Promise(async r => { + await sql.listen(channel, r) + sql.notify(channel, 'works') + }) + + return [ + 'works', + result, + sql.end() + ] +}) + +t('double listen', async() => { + const sql = postgres(options) + , channel = 'hello' + + let count = 0 + + await new Promise((resolve, reject) => + sql.listen(channel, resolve) + .then(() => sql.notify(channel, 'world')) + .catch(reject) + ).then(() => count++) + + await new Promise((resolve, reject) => + sql.listen(channel, resolve) + .then(() => sql.notify(channel, 'world')) + .catch(reject) + ).then(() => count++) + + // for coverage + sql.listen('weee', () => { /* noop */ }).then(sql.end) + + return [2, count] +}) + +t('multiple listeners work after a reconnect', async() => { + const sql = postgres(options) + , xs = [] + + const s1 = await sql.listen('test', x => xs.push('1', x)) + await sql.listen('test', x => xs.push('2', x)) + await sql.notify('test', 'a') + await delay(50) + await sql`select pg_terminate_backend(${ s1.state.pid })` + await delay(200) + await sql.notify('test', 'b') + await delay(50) + sql.end() + + return ['1a2a1b2b', xs.join('')] +}) + +t('listen and notify with weird name', async() => { + const sql = postgres(options) + const channel = 'wat-;.ø.§' + const result = await new Promise(async r => { + const { unlisten } = await sql.listen(channel, r) + sql.notify(channel, 'works') + await delay(50) + await unlisten() + }) + + return [ + 'works', + result, + sql.end() + ] +}) + +t('listen and notify with upper case', async() => { + const sql = postgres(options) + const channel = 'withUpperChar' + const result = await new Promise(async r => { + await sql.listen(channel, r) + sql.notify(channel, 'works') + }) + + return [ + 'works', + result, + sql.end() + ] +}) + +t('listen reconnects', { timeout: 2 }, async() => { + const sql = postgres(options) + , resolvers = {} + , a = new Promise(r => resolvers.a = r) + , b = new Promise(r => resolvers.b = r) + + let connects = 0 + + const { state: { pid } } = await sql.listen( + 'test', + x => x in resolvers && resolvers[x](), + () => connects++ + ) + await sql.notify('test', 'a') + await a + await sql`select pg_terminate_backend(${ pid })` + await delay(100) + await sql.notify('test', 'b') + await b + sql.end() + return [connects, 2] +}) + +t('listen result reports correct connection state after reconnection', async() => { + const sql = postgres(options) + , xs = [] + + const result = await sql.listen('test', x => xs.push(x)) + const initialPid = result.state.pid + await sql.notify('test', 'a') + await sql`select pg_terminate_backend(${ initialPid })` + await delay(50) + sql.end() + + return [result.state.pid !== initialPid, true] +}) + +t('unlisten removes subscription', async() => { + const sql = postgres(options) + , xs = [] + + const { unlisten } = await sql.listen('test', x => xs.push(x)) + await sql.notify('test', 'a') + await delay(50) + await unlisten() + await sql.notify('test', 'b') + await delay(50) + sql.end() + + return ['a', xs.join('')] +}) + +t('listen after unlisten', async() => { + const sql = postgres(options) + , xs = [] + + const { unlisten } = await sql.listen('test', x => xs.push(x)) + await sql.notify('test', 'a') + await delay(50) + await unlisten() + await sql.notify('test', 'b') + await delay(50) + await sql.listen('test', x => xs.push(x)) + await sql.notify('test', 'c') + await delay(50) + sql.end() + + return ['ac', xs.join('')] +}) + +t('multiple listeners and unlisten one', async() => { + const sql = postgres(options) + , xs = [] + + await sql.listen('test', x => xs.push('1', x)) + const s2 = await sql.listen('test', x => xs.push('2', x)) + await sql.notify('test', 'a') + await delay(50) + await s2.unlisten() + await sql.notify('test', 'b') + await delay(50) + sql.end() + + return ['1a2a1b', xs.join('')] +}) + +t('responds with server parameters (application_name)', async() => + ['postgres.js', await new Promise((resolve, reject) => postgres({ + ...options, + onparameter: (k, v) => k === 'application_name' && resolve(v) + })`select 1`.catch(reject))] +) + +t('has server parameters', async() => { + return ['postgres.js', (await sql`select 1`.then(() => sql.parameters.application_name))] +}) + +t('big query body', { timeout: 2 }, async() => { + await sql`create table test (x int)` + return [50000, (await sql`insert into test ${ + sql([...Array(50000).keys()].map(x => ({ x }))) + }`).count, await sql`drop table test`] +}) + +t('Throws if more than 65534 parameters', async() => { + await sql`create table test (x int)` + return ['MAX_PARAMETERS_EXCEEDED', (await sql`insert into test ${ + sql([...Array(65535).keys()].map(x => ({ x }))) + }`.catch(e => e.code)), await sql`drop table test`] +}) + +t('let postgres do implicit cast of unknown types', async() => { + await sql`create table test (x timestamp with time zone)` + const [{ x }] = await sql`insert into test values (${ new Date().toISOString() }) returning *` + return [true, x instanceof Date, await sql`drop table test`] +}) + +t('only allows one statement', async() => + ['42601', await sql`select 1; select 2`.catch(e => e.code)] +) + +t('await sql() throws not tagged error', async() => { + let error + try { + await sql('select 1') + } catch (e) { + error = e.code + } + return ['NOT_TAGGED_CALL', error] +}) + +t('sql().then throws not tagged error', async() => { + let error + try { + sql('select 1').then(() => { /* noop */ }) + } catch (e) { + error = e.code + } + return ['NOT_TAGGED_CALL', error] +}) + +t('sql().catch throws not tagged error', async() => { + let error + try { + await sql('select 1') + } catch (e) { + error = e.code + } + return ['NOT_TAGGED_CALL', error] +}) + +t('sql().finally throws not tagged error', async() => { + let error + try { + sql('select 1').finally(() => { /* noop */ }) + } catch (e) { + error = e.code + } + return ['NOT_TAGGED_CALL', error] +}) + +t('little bobby tables', async() => { + const name = 'Robert\'); DROP TABLE students;--' + + await sql`create table students (name text, age int)` + await sql`insert into students (name) values (${ name })` + + return [ + name, (await sql`select name from students`)[0].name, + await sql`drop table students` + ] +}) + +t('Connection errors are caught using begin()', { + timeout: 2 +}, async() => { + let error + try { + const sql = postgres({ host: 'localhost', port: 1 }) + + await sql.begin(async(sql) => { + await sql`insert into test (label, value) values (${1}, ${2})` + }) + } catch (err) { + error = err + } + + return [ + true, + error.code === 'ECONNREFUSED' || + error.message === 'Connection refused (os error 61)' + ] +}) + +t('dynamic table name', async() => { + await sql`create table test(a int)` + return [ + 0, (await sql`select * from ${ sql('test') }`).count, + await sql`drop table test` + ] +}) + +t('dynamic schema name', async() => { + await sql`create table test(a int)` + return [ + 0, (await sql`select * from ${ sql('public') }.test`).count, + await sql`drop table test` + ] +}) + +t('dynamic schema and table name', async() => { + await sql`create table test(a int)` + return [ + 0, (await sql`select * from ${ sql('public.test') }`).count, + await sql`drop table test` + ] +}) + +t('dynamic column name', async() => { + return ['!not_valid', Object.keys((await sql`select 1 as ${ sql('!not_valid') }`)[0])[0]] +}) + +t('dynamic select as', async() => { + return ['2', (await sql`select ${ sql({ a: 1, b: 2 }) }`)[0].b] +}) + +t('dynamic select as pluck', async() => { + return [undefined, (await sql`select ${ sql({ a: 1, b: 2 }, 'a') }`)[0].b] +}) + +t('dynamic insert', async() => { + await sql`create table test (a int, b text)` + const x = { a: 42, b: 'the answer' } + + return ['the answer', (await sql`insert into test ${ sql(x) } returning *`)[0].b, await sql`drop table test`] +}) + +t('dynamic insert pluck', async() => { + await sql`create table test (a int, b text)` + const x = { a: 42, b: 'the answer' } + + return [null, (await sql`insert into test ${ sql(x, 'a') } returning *`)[0].b, await sql`drop table test`] +}) + +t('dynamic in with empty array', async() => { + await sql`create table test (a int)` + await sql`insert into test values (1)` + return [ + (await sql`select * from test where null in ${ sql([]) }`).count, + 0, + await sql`drop table test` + ] +}) + +t('dynamic in after insert', async() => { + await sql`create table test (a int, b text)` + const [{ x }] = await sql` + with x as ( + insert into test values (1, 'hej') + returning * + ) + select 1 in ${ sql([1, 2, 3]) } as x from x + ` + return [ + true, x, + await sql`drop table test` + ] +}) + +t('array insert', async() => { + await sql`create table test (a int, b int)` + return [2, (await sql`insert into test (a, b) values ${ sql([1, 2]) } returning *`)[0].b, await sql`drop table test`] +}) + +t('where parameters in()', async() => { + await sql`create table test (x text)` + await sql`insert into test values ('a')` + return [ + (await sql`select * from test where x in ${ sql(['a', 'b', 'c']) }`)[0].x, + 'a', + await sql`drop table test` + ] +}) + +t('where parameters in() values before', async() => { + return [2, (await sql` + with rows as ( + select * from (values (1), (2), (3), (4)) as x(a) + ) + select * from rows where a in ${ sql([3, 4]) } + `).count] +}) + +t('dynamic multi row insert', async() => { + await sql`create table test (a int, b text)` + const x = { a: 42, b: 'the answer' } + + return [ + 'the answer', + (await sql`insert into test ${ sql([x, x]) } returning *`)[1].b, await sql`drop table test` + ] +}) + +t('dynamic update', async() => { + await sql`create table test (a int, b text)` + await sql`insert into test (a, b) values (17, 'wrong')` + + return [ + 'the answer', + (await sql`update test set ${ sql({ a: 42, b: 'the answer' }) } returning *`)[0].b, await sql`drop table test` + ] +}) + +t('dynamic update pluck', async() => { + await sql`create table test (a int, b text)` + await sql`insert into test (a, b) values (17, 'wrong')` + + return [ + 'wrong', + (await sql`update test set ${ sql({ a: 42, b: 'the answer' }, 'a') } returning *`)[0].b, await sql`drop table test` + ] +}) + +t('dynamic select array', async() => { + await sql`create table test (a int, b text)` + await sql`insert into test (a, b) values (42, 'yay')` + return ['yay', (await sql`select ${ sql(['a', 'b']) } from test`)[0].b, await sql`drop table test`] +}) + +t('dynamic returning array', async() => { + await sql`create table test (a int, b text)` + return [ + 'yay', + (await sql`insert into test (a, b) values (42, 'yay') returning ${ sql(['a', 'b']) }`)[0].b, + await sql`drop table test` + ] +}) + +t('dynamic select args', async() => { + await sql`create table test (a int, b text)` + await sql`insert into test (a, b) values (42, 'yay')` + return ['yay', (await sql`select ${ sql('a', 'b') } from test`)[0].b, await sql`drop table test`] +}) + +t('dynamic values single row', async() => { + const [{ b }] = await sql` + select * from (values ${ sql(['a', 'b', 'c']) }) as x(a, b, c) + ` + + return ['b', b] +}) + +t('dynamic values multi row', async() => { + const [, { b }] = await sql` + select * from (values ${ sql([['a', 'b', 'c'], ['a', 'b', 'c']]) }) as x(a, b, c) + ` + + return ['b', b] +}) + +t('connection parameters', async() => { + const sql = postgres({ + ...options, + connection: { + 'some.var': 'yay' + } + }) + + return ['yay', (await sql`select current_setting('some.var') as x`)[0].x] +}) + +t('Multiple queries', async() => { + const sql = postgres(options) + + return [4, (await Promise.all([ + sql`select 1`, + sql`select 2`, + sql`select 3`, + sql`select 4` + ])).length] +}) + +t('Multiple statements', async() => + [2, await sql.unsafe(` + select 1 as x; + select 2 as a; + `).then(([, [x]]) => x.a)] +) + +t('throws correct error when authentication fails', async() => { + const sql = postgres({ + ...options, + ...login_md5, + pass: 'wrong' + }) + return ['28P01', await sql`select 1`.catch(e => e.code)] +}) + +t('notice', async() => { + let notice + const log = console.log // eslint-disable-line + console.log = function(x) { // eslint-disable-line + notice = x + } + + const sql = postgres(options) + + await sql`create table if not exists users()` + await sql`create table if not exists users()` + + console.log = log // eslint-disable-line + + return ['NOTICE', notice.severity] +}) + +t('notice hook', async() => { + let notice + const sql = postgres({ + ...options, + onnotice: x => notice = x + }) + + await sql`create table if not exists users()` + await sql`create table if not exists users()` + + return ['NOTICE', notice.severity] +}) + +t('bytea serializes and parses', async() => { + const buf = Buffer.from('wat') + + await sql`create table test (x bytea)` + await sql`insert into test values (${ buf })` + + return [ + buf.toString(), + (await sql`select x from test`)[0].x.toString(), + await sql`drop table test` + ] +}) + +t('forEach', async() => { + let result + await sql`select 1 as x`.forEach(({ x }) => result = x) + return [1, result] +}) + +t('forEach returns empty array', async() => { + return [0, (await sql`select 1 as x`.forEach(() => { /* noop */ })).length] +}) + +t('Cursor', async() => { + const order = [] + await sql`select 1 as x union select 2 as x`.cursor(async([x]) => { + order.push(x.x + 'a') + await delay(100) + order.push(x.x + 'b') + }) + return ['1a1b2a2b', order.join('')] +}) + +t('Unsafe cursor', async() => { + const order = [] + await sql.unsafe('select 1 as x union select 2 as x').cursor(async([x]) => { + order.push(x.x + 'a') + await delay(100) + order.push(x.x + 'b') + }) + return ['1a1b2a2b', order.join('')] +}) + +t('Cursor custom n', async() => { + const order = [] + await sql`select * from generate_series(1,20)`.cursor(10, async(x) => { + order.push(x.length) + }) + return ['10,10', order.join(',')] +}) + +t('Cursor custom with rest n', async() => { + const order = [] + await sql`select * from generate_series(1,20)`.cursor(11, async(x) => { + order.push(x.length) + }) + return ['11,9', order.join(',')] +}) + +t('Cursor custom with less results than batch size', async() => { + const order = [] + await sql`select * from generate_series(1,20)`.cursor(21, async(x) => { + order.push(x.length) + }) + return ['20', order.join(',')] +}) + +t('Cursor cancel', async() => { + let result + await sql`select * from generate_series(1,10) as x`.cursor(async([{ x }]) => { + result = x + return sql.CLOSE + }) + return [1, result] +}) + +t('Cursor throw', async() => { + const order = [] + await sql`select 1 as x union select 2 as x`.cursor(async([x]) => { + order.push(x.x + 'a') + await delay(100) + throw new Error('watty') + }).catch(() => order.push('err')) + return ['1aerr', order.join('')] +}) + +t('Cursor error', async() => [ + '42601', + await sql`wat`.cursor(() => { /* noop */ }).catch((err) => err.code) +]) + +t('Multiple Cursors', { timeout: 2 }, async() => { + const result = [] + await sql.begin(async sql => [ + await sql`select 1 as cursor, x from generate_series(1,4) as x`.cursor(async([row]) => { + result.push(row.x) + await new Promise(r => setTimeout(r, 20)) + }), + await sql`select 2 as cursor, x from generate_series(101,104) as x`.cursor(async([row]) => { + result.push(row.x) + await new Promise(r => setTimeout(r, 10)) + }) + ]) + + return ['1,2,3,4,101,102,103,104', result.join(',')] +}) + +t('Cursor as async iterator', async() => { + const order = [] + for await (const [x] of sql`select generate_series(1,2) as x;`.cursor()) { + order.push(x.x + 'a') + await delay(10) + order.push(x.x + 'b') + } + + return ['1a1b2a2b', order.join('')] +}) + +t('Cursor as async iterator with break', async() => { + const order = [] + for await (const xs of sql`select generate_series(1,2) as x;`.cursor()) { + order.push(xs[0].x + 'a') + await delay(10) + order.push(xs[0].x + 'b') + break + } + + return ['1a1b', order.join('')] +}) + +t('Async Iterator Unsafe cursor', async() => { + const order = [] + for await (const [x] of sql.unsafe('select 1 as x union select 2 as x').cursor()) { + order.push(x.x + 'a') + await delay(10) + order.push(x.x + 'b') + } + return ['1a1b2a2b', order.join('')] +}) + +t('Async Iterator Cursor custom n', async() => { + const order = [] + for await (const x of sql`select * from generate_series(1,20)`.cursor(10)) + order.push(x.length) + + return ['10,10', order.join(',')] +}) + +t('Async Iterator Cursor custom with rest n', async() => { + const order = [] + for await (const x of sql`select * from generate_series(1,20)`.cursor(11)) + order.push(x.length) + + return ['11,9', order.join(',')] +}) + +t('Async Iterator Cursor custom with less results than batch size', async() => { + const order = [] + for await (const x of sql`select * from generate_series(1,20)`.cursor(21)) + order.push(x.length) + return ['20', order.join(',')] +}) + +t('Transform row', async() => { + const sql = postgres({ + ...options, + transform: { row: () => 1 } + }) + + return [1, (await sql`select 'wat'`)[0]] +}) + +t('Transform row forEach', async() => { + let result + const sql = postgres({ + ...options, + transform: { row: () => 1 } + }) + + await sql`select 1`.forEach(x => result = x) + + return [1, result] +}) + +t('Transform value', async() => { + const sql = postgres({ + ...options, + transform: { value: () => 1 } + }) + + return [1, (await sql`select 'wat' as x`)[0].x] +}) + +t('Transform columns from', async() => { + const sql = postgres({ + ...options, + transform: postgres.fromCamel + }) + await sql`create table test (a_test int, b_test text)` + await sql`insert into test ${ sql([{ aTest: 1, bTest: 1 }]) }` + await sql`update test set ${ sql({ aTest: 2, bTest: 2 }) }` + return [ + 2, + (await sql`select ${ sql('aTest', 'bTest') } from test`)[0].a_test, + await sql`drop table test` + ] +}) + +t('Transform columns to', async() => { + const sql = postgres({ + ...options, + transform: postgres.toCamel + }) + await sql`create table test (a_test int, b_test text)` + await sql`insert into test ${ sql([{ a_test: 1, b_test: 1 }]) }` + await sql`update test set ${ sql({ a_test: 2, b_test: 2 }) }` + return [ + 2, + (await sql`select a_test, b_test from test`)[0].aTest, + await sql`drop table test` + ] +}) + +t('Transform columns from and to', async() => { + const sql = postgres({ + ...options, + transform: postgres.camel + }) + await sql`create table test (a_test int, b_test text)` + await sql`insert into test ${ sql([{ aTest: 1, bTest: 1 }]) }` + await sql`update test set ${ sql({ aTest: 2, bTest: 2 }) }` + return [ + 2, + (await sql`select ${ sql('aTest', 'bTest') } from test`)[0].aTest, + await sql`drop table test` + ] +}) + +t('Transform columns from and to (legacy)', async() => { + const sql = postgres({ + ...options, + transform: { + column: { + to: postgres.fromCamel, + from: postgres.toCamel + } + } + }) + await sql`create table test (a_test int, b_test text)` + await sql`insert into test ${ sql([{ aTest: 1, bTest: 1 }]) }` + await sql`update test set ${ sql({ aTest: 2, bTest: 2 }) }` + return [ + 2, + (await sql`select ${ sql('aTest', 'bTest') } from test`)[0].aTest, + await sql`drop table test` + ] +}) + +t('Unix socket', async() => { + const sql = postgres({ + ...options, + host: process.env.PGSOCKET || '/tmp' // eslint-disable-line + }) + + return [1, (await sql`select 1 as x`)[0].x] +}) + +t('Big result', async() => { + return [100000, (await sql`select * from generate_series(1, 100000)`).count] +}) + +t('Debug', async() => { + let result + const sql = postgres({ + ...options, + debug: (connection_id, str) => result = str + }) + + await sql`select 1` + + return ['select 1', result] +}) + +t('bigint is returned as String', async() => [ + 'string', + typeof (await sql`select 9223372036854777 as x`)[0].x +]) + +t('int is returned as Number', async() => [ + 'number', + typeof (await sql`select 123 as x`)[0].x +]) + +t('numeric is returned as string', async() => [ + 'string', + typeof (await sql`select 1.2 as x`)[0].x +]) + +t('Async stack trace', async() => { + const sql = postgres({ ...options, debug: false }) + return [ + parseInt(new Error().stack.split('\n')[1].match(':([0-9]+):')[1]) + 1, + parseInt(await sql`error`.catch(x => x.stack.split('\n').pop().match(':([0-9]+):')[1])) + ] +}) + +t('Debug has long async stack trace', async() => { + const sql = postgres({ ...options, debug: true }) + + return [ + 'watyo', + await yo().catch(x => x.stack.match(/wat|yo/g).join('')) + ] + + function yo() { + return wat() + } + + function wat() { + return sql`error` + } +}) + +t('Error contains query string', async() => [ + 'selec 1', + (await sql`selec 1`.catch(err => err.query)) +]) + +t('Error contains query serialized parameters', async() => [ + 1, + (await sql`selec ${ 1 }`.catch(err => err.parameters[0])) +]) + +t('Error contains query raw parameters', async() => [ + 1, + (await sql`selec ${ 1 }`.catch(err => err.args[0])) +]) + +t('Query and parameters on errorare not enumerable if debug is not set', async() => { + const sql = postgres({ ...options, debug: false }) + + return [ + false, + (await sql`selec ${ 1 }`.catch(err => err.propertyIsEnumerable('parameters') || err.propertyIsEnumerable('query'))) + ] +}) + +t('Query and parameters are enumerable if debug is set', async() => { + const sql = postgres({ ...options, debug: true }) + + return [ + true, + (await sql`selec ${ 1 }`.catch(err => err.propertyIsEnumerable('parameters') && err.propertyIsEnumerable('query'))) + ] +}) + +t('connect_timeout', { timeout: 20 }, async() => { + const connect_timeout = 0.2 + const server = net.createServer() + server.listen() + const sql = postgres({ port: server.address().port, host: '127.0.0.1', connect_timeout }) + const start = Date.now() + let end + await sql`select 1`.catch((e) => { + if (e.code !== 'CONNECT_TIMEOUT') + throw e + end = Date.now() + }) + server.close() + return [connect_timeout, Math.floor((end - start) / 100) / 10] +}) + +t('connect_timeout throws proper error', async() => [ + 'CONNECT_TIMEOUT', + await postgres({ + ...options, + ...login_scram, + connect_timeout: 0.001 + })`select 1`.catch(e => e.code) +]) + +t('connect_timeout error message includes host:port', { timeout: 20 }, async() => { + const connect_timeout = 0.2 + const server = net.createServer() + server.listen() + const sql = postgres({ port: server.address().port, host: '127.0.0.1', connect_timeout }) + const port = server.address().port + let err + await sql`select 1`.catch((e) => { + if (e.code !== 'CONNECT_TIMEOUT') + throw e + err = e.message + }) + server.close() + return [['write CONNECT_TIMEOUT 127.0.0.1:', port].join(''), err] +}) + +t('requests works after single connect_timeout', async() => { + let first = true + + const sql = postgres({ + ...options, + ...login_scram, + connect_timeout: { valueOf() { return first ? (first = false, 0.0001) : 1 } } + }) + + return [ + 'CONNECT_TIMEOUT,,1', + [ + await sql`select 1 as x`.then(() => 'success', x => x.code), + await delay(10), + (await sql`select 1 as x`)[0].x + ].join(',') + ] +}) + +t('Postgres errors are of type PostgresError', async() => + [true, (await sql`bad keyword`.catch(e => e)) instanceof sql.PostgresError] +) + +t('Result has columns spec', async() => + ['x', (await sql`select 1 as x`).columns[0].name] +) + +t('forEach has result as second argument', async() => { + let x + await sql`select 1 as x`.forEach((_, result) => x = result) + return ['x', x.columns[0].name] +}) + +t('Result as arrays', async() => { + const sql = postgres({ + ...options, + transform: { + row: x => Object.values(x) + } + }) + + return ['1,2', (await sql`select 1 as a, 2 as b`)[0].join(',')] +}) + +t('Insert empty array', async() => { + await sql`create table tester (ints int[])` + return [ + Array.isArray((await sql`insert into tester (ints) values (${ sql.array([]) }) returning *`)[0].ints), + true, + await sql`drop table tester` + ] +}) + +t('Insert array in sql()', async() => { + await sql`create table tester (ints int[])` + return [ + Array.isArray((await sql`insert into tester ${ sql({ ints: sql.array([]) }) } returning *`)[0].ints), + true, + await sql`drop table tester` + ] +}) + +t('Automatically creates prepared statements', async() => { + const sql = postgres(options) + const result = await sql`select * from pg_prepared_statements` + return [true, result.some(x => x.name = result.statement.name)] +}) + +t('no_prepare: true disables prepared statements (deprecated)', async() => { + const sql = postgres({ ...options, no_prepare: true }) + const result = await sql`select * from pg_prepared_statements` + return [false, result.some(x => x.name = result.statement.name)] +}) + +t('prepare: false disables prepared statements', async() => { + const sql = postgres({ ...options, prepare: false }) + const result = await sql`select * from pg_prepared_statements` + return [false, result.some(x => x.name = result.statement.name)] +}) + +t('prepare: true enables prepared statements', async() => { + const sql = postgres({ ...options, prepare: true }) + const result = await sql`select * from pg_prepared_statements` + return [true, result.some(x => x.name = result.statement.name)] +}) + +t('prepares unsafe query when "prepare" option is true', async() => { + const sql = postgres({ ...options, prepare: true }) + const result = await sql.unsafe('select * from pg_prepared_statements where name <> $1', ['bla'], { prepare: true }) + return [true, result.some(x => x.name = result.statement.name)] +}) + +t('does not prepare unsafe query by default', async() => { + const sql = postgres({ ...options, prepare: true }) + const result = await sql.unsafe('select * from pg_prepared_statements where name <> $1', ['bla']) + return [false, result.some(x => x.name = result.statement.name)] +}) + +t('Recreate prepared statements on transformAssignedExpr error', { timeout: 1 }, async() => { + const insert = () => sql`insert into test (name) values (${ '1' }) returning name` + await sql`create table test (name text)` + await insert() + await sql`alter table test alter column name type int using name::integer` + return [ + 1, + (await insert())[0].name, + await sql`drop table test` + ] +}) + +t('Throws correct error when retrying in transactions', async() => { + await sql`create table test(x int)` + const error = await sql.begin(sql => sql`insert into test (x) values (${ false })`).catch(e => e) + return [ + error.code, + '42804', + sql`drop table test` + ] +}) + +t('Recreate prepared statements on RevalidateCachedQuery error', async() => { + const select = () => sql`select name from test` + await sql`create table test (name text)` + await sql`insert into test values ('1')` + await select() + await sql`alter table test alter column name type int using name::integer` + return [ + 1, + (await select())[0].name, + await sql`drop table test` + ] +}) + +t('Properly throws routine error on not prepared statements', async() => { + await sql`create table x (x text[])` + const { routine } = await sql.unsafe(` + insert into x(x) values (('a', 'b')) + `).catch(e => e) + + return ['transformAssignedExpr', routine, await sql`drop table x`] +}) + +t('Properly throws routine error on not prepared statements in transaction', async() => { + const { routine } = await sql.begin(sql => [ + sql`create table x (x text[])`, + sql`insert into x(x) values (('a', 'b'))` + ]).catch(e => e) + + return ['transformAssignedExpr', routine] +}) + +t('Properly throws routine error on not prepared statements using file', async() => { + const { routine } = await sql.unsafe(` + create table x (x text[]); + insert into x(x) values (('a', 'b')); + `, { prepare: true }).catch(e => e) + + return ['transformAssignedExpr', routine] +}) + +t('Catches connection config errors', async() => { + const sql = postgres({ ...options, user: { toString: () => { throw new Error('wat') } }, database: 'prut' }) + + return [ + 'wat', + await sql`select 1`.catch((e) => e.message) + ] +}) + +t('Catches connection config errors with end', async() => { + const sql = postgres({ ...options, user: { toString: () => { throw new Error('wat') } }, database: 'prut' }) + + return [ + 'wat', + await sql`select 1`.catch((e) => e.message), + await sql.end() + ] +}) + +t('Catches query format errors', async() => [ + 'wat', + await sql.unsafe({ toString: () => { throw new Error('wat') } }).catch((e) => e.message) +]) + +t('Multiple hosts', { + timeout: 1 +}, async() => { + const s1 = postgres({ idle_timeout }) + , s2 = postgres({ idle_timeout, port: 5433 }) + , sql = postgres('postgres://localhost:5432,localhost:5433', { idle_timeout, max: 1 }) + , result = [] + + const id1 = (await s1`select system_identifier as x from pg_control_system()`)[0].x + const id2 = (await s2`select system_identifier as x from pg_control_system()`)[0].x + + const x1 = await sql`select 1` + result.push((await sql`select system_identifier as x from pg_control_system()`)[0].x) + await s1`select pg_terminate_backend(${ x1.state.pid }::int)` + await delay(50) + + const x2 = await sql`select 1` + result.push((await sql`select system_identifier as x from pg_control_system()`)[0].x) + await s2`select pg_terminate_backend(${ x2.state.pid }::int)` + await delay(50) + + result.push((await sql`select system_identifier as x from pg_control_system()`)[0].x) + + return [[id1, id2, id1].join(','), result.join(',')] +}) + +t('Escaping supports schemas and tables', async() => { + await sql`create schema a` + await sql`create table a.b (c int)` + await sql`insert into a.b (c) values (1)` + return [ + 1, + (await sql`select ${ sql('a.b.c') } from a.b`)[0].c, + await sql`drop table a.b`, + await sql`drop schema a` + ] +}) + +t('Raw method returns rows as arrays', async() => { + const [x] = await sql`select 1`.raw() + return [ + Array.isArray(x), + true + ] +}) + +t('Raw method returns values unparsed as Buffer', async() => { + const [[x]] = await sql`select 1`.raw() + return [ + x instanceof Uint8Array, + true + ] +}) + +t('Array returns rows as arrays of columns', async() => { + return [(await sql`select 1`.values())[0][0], 1] +}) + +t('Copy read', async() => { + const result = [] + + await sql`create table test (x int)` + await sql`insert into test select * from generate_series(1,10)` + const readable = await sql`copy test to stdout`.readable() + readable.on('data', x => result.push(x)) + await new Promise(r => readable.on('end', r)) + + return [ + result.length, + 10, + await sql`drop table test` + ] +}) + +t('Copy write', { timeout: 2 }, async() => { + await sql`create table test (x int)` + const writable = await sql`copy test from stdin`.writable() + + writable.write('1\n') + writable.write('1\n') + writable.end() + + await new Promise(r => writable.on('finish', r)) + + return [ + (await sql`select 1 from test`).length, + 2, + await sql`drop table test` + ] +}) + +t('Copy write as first', async() => { + await sql`create table test (x int)` + const first = postgres(options) + const writable = await first`COPY test FROM STDIN WITH(FORMAT csv, HEADER false, DELIMITER ',')`.writable() + writable.write('1\n') + writable.write('1\n') + writable.end() + + await new Promise(r => writable.on('finish', r)) + + return [ + (await sql`select 1 from test`).length, + 2, + await sql`drop table test` + ] +}) + +t('Copy from file', async() => { + await sql`create table test (x int, y int, z int)` + await new Promise(async r => fs + .createReadStream(rel('copy.csv')) + .pipe(await sql`copy test from stdin`.writable()) + .on('finish', r) + ) + + return [ + JSON.stringify(await sql`select * from test`), + '[{"x":1,"y":2,"z":3},{"x":4,"y":5,"z":6}]', + await sql`drop table test` + ] +}) + +t('Copy from works in transaction', async() => { + await sql`create table test(x int)` + const xs = await sql.begin(async sql => { + (await sql`copy test from stdin`.writable()).end('1\n2') + await delay(20) + return sql`select 1 from test` + }) + + return [ + xs.length, + 2, + await sql`drop table test` + ] +}) + +t('Copy from abort', async() => { + const sql = postgres(options) + const readable = fs.createReadStream(rel('copy.csv')) + + await sql`create table test (x int, y int, z int)` + await sql`TRUNCATE TABLE test` + + const writable = await sql`COPY test FROM STDIN`.writable() + + let aborted + + readable + .pipe(writable) + .on('error', (err) => aborted = err) + + writable.destroy(new Error('abort')) + await sql.end() + + return [ + 'abort', + aborted.message, + await postgres(options)`drop table test` + ] +}) + +t('multiple queries before connect', async() => { + const sql = postgres({ ...options, max: 2 }) + const xs = await Promise.all([ + sql`select 1 as x`, + sql`select 2 as x`, + sql`select 3 as x`, + sql`select 4 as x` + ]) + + return [ + '1,2,3,4', + xs.map(x => x[0].x).join() + ] +}) + +t('subscribe', { timeout: 2 }, async() => { + const sql = postgres({ + database: 'postgres_js_test', + publications: 'alltables' + }) + + await sql.unsafe('create publication alltables for all tables') + + const result = [] + + const { unsubscribe } = await sql.subscribe('*', (row, { command, old }) => { + result.push(command, row.name, row.id, old && old.name, old && old.id) + }) + + await sql` + create table test ( + id serial primary key, + name text + ) + ` + + await sql`alter table test replica identity default` + await sql`insert into test (name) values ('Murray')` + await sql`update test set name = 'Rothbard'` + await sql`update test set id = 2` + await sql`delete from test` + await sql`alter table test replica identity full` + await sql`insert into test (name) values ('Murray')` + await sql`update test set name = 'Rothbard'` + await sql`delete from test` + await delay(10) + await unsubscribe() + await sql`insert into test (name) values ('Oh noes')` + await delay(10) + return [ + 'insert,Murray,1,,,update,Rothbard,1,,,update,Rothbard,2,,1,delete,,2,,,insert,Murray,2,,,update,Rothbard,2,Murray,2,delete,Rothbard,2,,', // eslint-disable-line + result.join(','), + await sql`drop table test`, + await sql`drop publication alltables`, + await sql.end() + ] +}) + +t('subscribe with transform', { timeout: 2 }, async() => { + const sql = postgres({ + transform: { + column: { + from: postgres.toCamel, + to: postgres.fromCamel + } + }, + database: 'postgres_js_test', + publications: 'alltables' + }) + + await sql.unsafe('create publication alltables for all tables') + + const result = [] + + const { unsubscribe } = await sql.subscribe('*', (row, { command, old }) => + result.push(command, row.nameInCamel || row.id, old && old.nameInCamel) + ) + + await sql` + create table test ( + id serial primary key, + name_in_camel text + ) + ` + + await sql`insert into test (name_in_camel) values ('Murray')` + await sql`update test set name_in_camel = 'Rothbard'` + await sql`delete from test` + await sql`alter table test replica identity full` + await sql`insert into test (name_in_camel) values ('Murray')` + await sql`update test set name_in_camel = 'Rothbard'` + await sql`delete from test` + await delay(10) + await unsubscribe() + await sql`insert into test (name_in_camel) values ('Oh noes')` + await delay(10) + return [ + 'insert,Murray,,update,Rothbard,,delete,1,,insert,Murray,,update,Rothbard,Murray,delete,Rothbard,', + result.join(','), + await sql`drop table test`, + await sql`drop publication alltables`, + await sql.end() + ] +}) + +t('subscribe reconnects and calls onsubscribe', { timeout: 4 }, async() => { + const sql = postgres({ + database: 'postgres_js_test', + publications: 'alltables', + fetch_types: false + }) + + await sql.unsafe('create publication alltables for all tables') + + const result = [] + let onsubscribes = 0 + + const { unsubscribe, sql: subscribeSql } = await sql.subscribe( + '*', + (row, { command, old }) => result.push(command, row.name || row.id, old && old.name), + () => onsubscribes++ + ) + + await sql` + create table test ( + id serial primary key, + name text + ) + ` + + await sql`insert into test (name) values ('Murray')` + await delay(10) + await subscribeSql.close() + await delay(500) + await sql`delete from test` + await delay(100) + await unsubscribe() + return [ + '2insert,Murray,,delete,1,', + onsubscribes + result.join(','), + await sql`drop table test`, + await sql`drop publication alltables`, + await sql.end() + ] +}) + +t('Execute', async() => { + const result = await new Promise((resolve) => { + const sql = postgres({ ...options, fetch_types: false, debug:(id, query) => resolve(query) }) + sql`select 1`.execute() + }) + + return [result, 'select 1'] +}) + +t('Cancel running query', async() => { + const query = sql`select pg_sleep(2)` + setTimeout(() => query.cancel(), 500) + const error = await query.catch(x => x) + return ['57014', error.code] +}) + +t('Cancel piped query', { timeout: 5 }, async() => { + await sql`select 1` + const last = sql`select pg_sleep(1)`.execute() + const query = sql`select pg_sleep(2) as dig` + setTimeout(() => query.cancel(), 500) + const error = await query.catch(x => x) + await last + return ['57014', error.code] +}) + +t('Cancel queued query', async() => { + const query = sql`select pg_sleep(2) as nej` + const tx = sql.begin(sql => ( + query.cancel(), + sql`select pg_sleep(0.5) as hej, 'hejsa'` + )) + const error = await query.catch(x => x) + await tx + return ['57014', error.code] +}) + +t('Fragments', async() => [ + 1, + (await sql` + ${ sql`select` } 1 as x + `)[0].x +]) + +t('Result becomes array', async() => [ + true, + (await sql`select 1`).slice() instanceof Array +]) + +t('Describe', async() => { + const type = (await sql`select ${ 1 }::int as x`.describe()).types[0] + return [23, type] +}) + +t('Describe a statement', async() => { + await sql`create table tester (name text, age int)` + const r = await sql`select name, age from tester where name like $1 and age > $2`.describe() + return [ + '25,23/name:25,age:23', + `${ r.types.join(',') }/${ r.columns.map(c => `${c.name}:${c.type}`).join(',') }`, + await sql`drop table tester` + ] +}) + +t('Include table oid and column number in column details', async() => { + await sql`create table tester (name text, age int)` + const r = await sql`select name, age from tester where name like $1 and age > $2`.describe() + const [{ oid }] = await sql`select oid from pg_class where relname = 'tester'` + + return [ + `table:${oid},number:1|table:${oid},number:2`, + `${ r.columns.map(c => `table:${c.table},number:${c.number}`).join('|') }`, + await sql`drop table tester` + ] +}) + +t('Describe a statement without parameters', async() => { + await sql`create table tester (name text, age int)` + const r = await sql`select name, age from tester`.describe() + return [ + '0,2', + `${ r.types.length },${ r.columns.length }`, + await sql`drop table tester` + ] +}) + +t('Describe a statement without columns', async() => { + await sql`create table tester (name text, age int)` + const r = await sql`insert into tester (name, age) values ($1, $2)`.describe() + return [ + '2,0', + `${ r.types.length },${ r.columns.length }`, + await sql`drop table tester` + ] +}) + +t('Large object', async() => { + const file = rel('index.js') + , md5 = crypto.createHash('md5').update(fs.readFileSync(file)).digest('hex') + + const lo = await sql.largeObject() + await new Promise(async r => fs.createReadStream(file).pipe(await lo.writable()).on('finish', r)) + await lo.seek(0) + + const out = crypto.createHash('md5') + await new Promise(r => lo.readable().then(x => x.on('data', x => out.update(x)).on('end', r))) + + return [ + md5, + out.digest('hex'), + await lo.close() + ] +}) + +t('Catches type serialize errors', async() => { + const sql = postgres({ + idle_timeout, + types: { + text: { + from: 25, + to: 25, + parse: x => x, + serialize: () => { throw new Error('watSerialize') } + } + } + }) + + return [ + 'watSerialize', + (await sql`select ${ 'wat' }`.catch(e => e.message)) + ] +}) + +t('Catches type parse errors', async() => { + const sql = postgres({ + idle_timeout, + types: { + text: { + from: 25, + to: 25, + parse: () => { throw new Error('watParse') }, + serialize: x => x + } + } + }) + + return [ + 'watParse', + (await sql`select 'wat'`.catch(e => e.message)) + ] +}) + +t('Catches type serialize errors in transactions', async() => { + const sql = postgres({ + idle_timeout, + types: { + text: { + from: 25, + to: 25, + parse: x => x, + serialize: () => { throw new Error('watSerialize') } + } + } + }) + + return [ + 'watSerialize', + (await sql.begin(sql => ( + sql`select 1`, + sql`select ${ 'wat' }` + )).catch(e => e.message)) + ] +}) + +t('Catches type parse errors in transactions', async() => { + const sql = postgres({ + idle_timeout, + types: { + text: { + from: 25, + to: 25, + parse: () => { throw new Error('watParse') }, + serialize: x => x + } + } + }) + + return [ + 'watParse', + (await sql.begin(sql => ( + sql`select 1`, + sql`select 'wat'` + )).catch(e => e.message)) + ] +}) + +t('Prevent premature end of connection in transaction', async() => { + const sql = postgres({ max_lifetime: 0.01, idle_timeout }) + const result = await sql.begin(async sql => { + await sql`select 1` + await delay(20) + await sql`select 1` + return 'yay' + }) + + + return [ + 'yay', + result + ] +}) + +t('Ensure reconnect after max_lifetime with transactions', { timeout: 5 }, async() => { + const sql = postgres({ + max_lifetime: 0.01, + idle_timeout, + max: 1 + }) + + let x = 0 + while (x++ < 10) await sql.begin(sql => sql`select 1 as x`) + + return [true, true] +}) + + +t('Ensure transactions throw if connection is closed dwhile there is no query', async() => { + const sql = postgres(options) + const x = await sql.begin(async() => { + setTimeout(() => sql.end({ timeout: 0 }), 10) + await new Promise(r => setTimeout(r, 200)) + return sql`select 1` + }).catch(x => x) + return ['CONNECTION_CLOSED', x.code] +}) + +t('Custom socket', {}, async() => { + let result + const sql = postgres({ + socket: () => new Promise((resolve, reject) => { + const socket = new net.Socket() + socket.connect(5432) + socket.once('data', x => result = x[0]) + socket.on('error', reject) + socket.on('connect', () => resolve(socket)) + }), + idle_timeout + }) + + await sql`select 1` + + return [ + result, + 82 + ] +}) + +t('Ensure drain only dequeues if ready', async() => { + const sql = postgres(options) + + const res = await Promise.all([ + sql.unsafe('SELECT 0+$1 --' + '.'.repeat(100000), [1]), + sql.unsafe('SELECT 0+$1+$2+$3', [1, 2, 3]) + ]) + + return [res.length, 2] +}) + +t('Supports fragments as dynamic parameters', async() => { + await sql`create table test (a int, b bool)` + await sql`insert into test values(1, true)` + await sql`insert into test ${ + sql({ + a: 2, + b: sql`exists(select 1 from test where b = ${ true })` + }) + }` + + return [ + '1,t2,t', + (await sql`select * from test`.raw()).join(''), + await sql`drop table test` + ] +}) + +t('Supports nested fragments with parameters', async() => { + await sql`create table test ${ + sql`(${ sql('a') } ${ sql`int` })` + }` + await sql`insert into test values(1)` + return [ + 1, + (await sql`select a from test`)[0].a, + await sql`drop table test` + ] +}) + +t('Supports multiple nested fragments with parameters', async() => { + const [{ b }] = await sql`select * ${ + sql`from ${ + sql`(values (2, ${ 1 }::int)) as x(${ sql(['a', 'b']) })` + }` + }` + return [ + 1, + b + ] +}) + +t('Supports arrays of fragments', async() => { + const [{ x }] = await sql` + ${ [sql`select`, sql`1`, sql`as`, sql`x`] } + ` + + return [ + 1, + x + ] +}) + +t('Does not try rollback when commit errors', async() => { + let notice = null + const sql = postgres({ ...options, onnotice: x => notice = x }) + await sql`create table test(x int constraint test_constraint unique deferrable initially deferred)` + + await sql.begin('isolation level serializable', async sql => { + await sql`insert into test values(1)` + await sql`insert into test values(1)` + }).catch(e => e) + + return [ + notice, + null, + await sql`drop table test` + ] +}) + +t('Last keyword used even with duplicate keywords', async() => { + await sql`create table test (x int)` + await sql`insert into test values(1)` + const [{ x }] = await sql` + select + 1 in (1) as x + from test + where x in ${ sql([1, 2]) } + ` + + return [x, true, await sql`drop table test`] +}) + +t('Insert array with null', async() => { + await sql`create table test (x int[])` + await sql`insert into test ${ sql({ x: [1, null, 3] }) }` + return [ + 1, + (await sql`select x from test`)[0].x[0], + await sql`drop table test` + ] +}) + +t('Insert array with undefined throws', async() => { + await sql`create table test (x int[])` + return [ + 'UNDEFINED_VALUE', + await sql`insert into test ${ sql({ x: [1, undefined, 3] }) }`.catch(e => e.code), + await sql`drop table test` + ] +}) + +t('Insert array with undefined transform', async() => { + const sql = postgres({ ...options, transform: { undefined: null } }) + await sql`create table test (x int[])` + await sql`insert into test ${ sql({ x: [1, undefined, 3] }) }` + return [ + 1, + (await sql`select x from test`)[0].x[0], + await sql`drop table test` + ] +}) + +t('concurrent cursors', async() => { + const xs = [] + + await Promise.all([...Array(7)].map((x, i) => [ + sql`select ${ i }::int as a, generate_series(1, 2) as x`.cursor(([x]) => xs.push(x.a + x.x)) + ]).flat()) + + return ['12233445566778', xs.join('')] +}) + +t('concurrent cursors multiple connections', async() => { + const sql = postgres({ ...options, max: 2 }) + const xs = [] + + await Promise.all([...Array(7)].map((x, i) => [ + sql`select ${ i }::int as a, generate_series(1, 2) as x`.cursor(([x]) => xs.push(x.a + x.x)) + ]).flat()) + + return ['12233445566778', xs.sort().join('')] +}) + +t('reserve connection', async() => { + const reserved = await sql.reserve() + + setTimeout(() => reserved.release(), 510) + + const xs = await Promise.all([ + reserved`select 1 as x`.then(([{ x }]) => ({ time: Date.now(), x })), + sql`select 2 as x`.then(([{ x }]) => ({ time: Date.now(), x })), + reserved`select 3 as x`.then(([{ x }]) => ({ time: Date.now(), x })) + ]) + + if (xs[1].time - xs[2].time < 500) + throw new Error('Wrong time') + + return [ + '123', + xs.map(x => x.x).join('') + ] +}) + +t('arrays in reserved connection', async() => { + const reserved = await sql.reserve() + const [{ x }] = await reserved`select array[1, 2, 3] as x` + reserved.release() + + return [ + '123', + x.join('') + ] +}) + +t('Ensure reserve on query throws proper error', async() => { + const sql = postgres({ idle_timeout }) // eslint-disable-line + const reserved = await sql.reserve() + const [{ x }] = await reserved`select 'wat' as x` + + return [ + 'wat', x, reserved.release() + ] +}) + +;globalThis.addEventListener("unload", () => Deno.exit(process.exitCode)) \ No newline at end of file diff --git a/deno/tests/test.js b/deno/tests/test.js index f61a253f..343eedf3 100644 --- a/deno/tests/test.js +++ b/deno/tests/test.js @@ -1,7 +1,7 @@ -import process from 'https://deno.land/std@0.132.0/node/process.ts' +import process from 'node:process' /* eslint no-console: 0 */ -import util from 'https://deno.land/std@0.132.0/node/util.ts' +import util from 'node:util' let done = 0 let only = false diff --git a/deno/tests/test.js.bak b/deno/tests/test.js.bak new file mode 100644 index 00000000..f61a253f --- /dev/null +++ b/deno/tests/test.js.bak @@ -0,0 +1,88 @@ +import process from 'https://deno.land/std@0.132.0/node/process.ts' +/* eslint no-console: 0 */ + +import util from 'https://deno.land/std@0.132.0/node/util.ts' + +let done = 0 +let only = false +let ignored = 0 +let failed = false +let promise = Promise.resolve() +const tests = {} + , ignore = {} + +export const nt = () => ignored++ +export const ot = (...rest) => (only = true, test(true, ...rest)) +export const t = (...rest) => test(false, ...rest) +t.timeout = 5 + +async function test(o, name, options, fn) { + typeof options !== 'object' && (fn = options, options = {}) + const line = new Error().stack.split('\n')[3].match(':([0-9]+):')[1] + + await 1 + + if (only && !o) + return + + tests[line] = { fn, line, name } + promise = promise.then(() => Promise.race([ + new Promise((resolve, reject) => + fn.timer = setTimeout(() => reject('Timed out'), (options.timeout || t.timeout) * 1000) + ), + failed + ? (ignored++, ignore) + : fn() + ])) + .then(async x => { + clearTimeout(fn.timer) + if (x === ignore) + return + + if (!Array.isArray(x)) + throw new Error('Test should return result array') + + const [expected, got] = await Promise.all(x) + if (expected !== got) { + failed = true + throw new Error(util.inspect(expected) + ' != ' + util.inspect(got)) + } + + tests[line].succeeded = true + process.stdout.write('✅') + }) + .catch(err => { + tests[line].failed = failed = true + tests[line].error = err instanceof Error ? err : new Error(util.inspect(err)) + }) + .then(() => { + ++done === Object.keys(tests).length && exit() + }) +} + +function exit() { + let success = true + Object.values(tests).every((x) => { + if (x.succeeded) + return true + + success = false + x.cleanup + ? console.error('⛔️', x.name + ' at line', x.line, 'cleanup failed', '\n', util.inspect(x.cleanup)) + : console.error('⛔️', x.name + ' at line', x.line, x.failed + ? 'failed' + : 'never finished', x.error ? '\n' + util.inspect(x.error) : '' + ) + }) + + only + ? console.error('⚠️', 'Not all tests were run') + : ignored + ? console.error('⚠️', ignored, 'ignored test' + (ignored === 1 ? '' : 's', '\n')) + : success + ? console.log('🎉') + : console.error('⚠️', 'Not good') + + !process.exitCode && (!success || only || ignored) && (process.exitCode = 1) +} + diff --git a/deno/types/index.d.ts b/deno/types/index.d.ts index 44a07af0..c124b257 100644 --- a/deno/types/index.d.ts +++ b/deno/types/index.d.ts @@ -1,6 +1,6 @@ -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' -import process from 'https://deno.land/std@0.132.0/node/process.ts' -import { Readable, Writable } from 'https://deno.land/std@0.132.0/node/stream.ts' +import { Buffer } from 'node:buffer' +import process from 'node:process' +import { Readable, Writable } from 'node:stream' /** * Establish a connection to a PostgreSQL server. diff --git a/deno/types/index.d.ts.bak b/deno/types/index.d.ts.bak new file mode 100644 index 00000000..44a07af0 --- /dev/null +++ b/deno/types/index.d.ts.bak @@ -0,0 +1,732 @@ +import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' +import process from 'https://deno.land/std@0.132.0/node/process.ts' +import { Readable, Writable } from 'https://deno.land/std@0.132.0/node/stream.ts' + +/** + * Establish a connection to a PostgreSQL server. + * @param options Connection options - default to the same as psql + * @returns An utility function to make queries to the server + */ +declare function postgres = {}>(options?: postgres.Options | undefined): postgres.Sql extends T ? {} : { [type in keyof T]: T[type] extends { + serialize: (value: infer R) => any, + parse: (raw: any) => infer R +} ? R : never }> +/** + * Establish a connection to a PostgreSQL server. + * @param url Connection string used for authentication + * @param options Connection options - default to the same as psql + * @returns An utility function to make queries to the server + */ +declare function postgres = {}>(url: string, options?: postgres.Options | undefined): postgres.Sql extends T ? {} : { [type in keyof T]: T[type] extends { + serialize: (value: infer R) => any, + parse: (raw: any) => infer R +} ? R : never }> + +/** + * Connection options of Postgres. + */ +interface BaseOptions> { + /** Postgres ip address[s] or domain name[s] */ + host: string | string[] | undefined; + /** Postgres server[s] port[s] */ + port: number | number[] | undefined; + /** unix socket path (usually '/tmp') */ + path: string | undefined; + /** + * Name of database to connect to + * @default process.env['PGDATABASE'] || options.user + */ + database: string; + /** + * Username of database user + * @default process.env['PGUSERNAME'] || process.env['PGUSER'] || require('os').userInfo().username + */ + user: string; + /** + * How to deal with ssl (can be a tls.connect option object) + * @default false + */ + ssl: 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object; + /** + * Max number of connections + * @default 10 + */ + max: number; + /** + * Idle connection timeout in seconds + * @default process.env['PGIDLE_TIMEOUT'] + */ + idle_timeout: number | undefined; + /** + * Connect timeout in seconds + * @default process.env['PGCONNECT_TIMEOUT'] + */ + connect_timeout: number; + /** Array of custom types; see more in the README */ + types: T; + /** + * Enables prepare mode. + * @default true + */ + prepare: boolean; + /** + * Called when a notice is received + * @default console.log + */ + onnotice: (notice: postgres.Notice) => void; + /** (key; value) when a server param change */ + onparameter: (key: string, value: any) => void; + /** Is called with (connection; query; parameters) */ + debug: boolean | ((connection: number, query: string, parameters: any[], paramTypes: any[]) => void); + /** Transform hooks */ + transform: { + /** Transforms outcoming undefined values */ + undefined?: any + + /** Transforms incoming and outgoing column names */ + column?: ((column: string) => string) | { + /** Transform function for column names in result rows */ + from?: ((column: string) => string) | undefined; + /** Transform function for column names in interpolated values passed to tagged template literal */ + to?: ((column: string) => string) | undefined; + } | undefined; + /** Transforms incoming and outgoing row values */ + value?: ((value: any) => any) | { + /** Transform function for values in result rows */ + from?: ((value: unknown, column: postgres.Column) => any) | undefined; + // to?: ((value: unknown) => any) | undefined; // unused + } | undefined; + /** Transforms entire rows */ + row?: ((row: postgres.Row) => any) | { + /** Transform function for entire result rows */ + from?: ((row: postgres.Row) => any) | undefined; + // to?: ((row: postgres.Row) => any) | undefined; // unused + } | undefined; + }; + /** Connection parameters */ + connection: Partial; + /** + * Use 'read-write' with multiple hosts to ensure only connecting to primary + * @default process.env['PGTARGETSESSIONATTRS'] + */ + target_session_attrs: undefined | 'read-write' | 'read-only' | 'primary' | 'standby' | 'prefer-standby'; + /** + * Automatically fetches types on connect + * @default true + */ + fetch_types: boolean; + /** + * Publications to subscribe to (only relevant when calling `sql.subscribe()`) + * @default 'alltables' + */ + publications: string + onclose: (connId: number) => void; + backoff: boolean | ((attemptNum: number) => number); + max_lifetime: number | null; + keep_alive: number | null; +} + + +declare const PRIVATE: unique symbol; + +declare class NotAPromise { + private [PRIVATE]: never; // prevent user-side interface implementation + + /** + * @deprecated This object isn't an SQL query, and therefore not a Promise; use the tagged template string syntax instead: ```await sql\`...\`;``` + * @throws NOT_TAGGED_CALL + */ + private then(): never; + /** + * @deprecated This object isn't an SQL query, and therefore not a Promise; use the tagged template string syntax instead: ```await sql\`...\`;``` + * @throws NOT_TAGGED_CALL + */ + private catch(): never; + /** + * @deprecated This object isn't an SQL query, and therefore not a Promise; use the tagged template string syntax instead: ```await sql\`...\`;``` + * @throws NOT_TAGGED_CALL + */ + private finally(): never; +} + +type UnwrapPromiseArray = T extends any[] ? { + [k in keyof T]: T[k] extends Promise ? R : T[k] +} : T; + +type Keys = string + +type SerializableObject = + number extends K['length'] ? {} : + Partial<(Record | undefined> & Record)> + +type First = + // Tagged template string call + T extends TemplateStringsArray ? TemplateStringsArray : + // Identifiers helper + T extends string ? string : + // Dynamic values helper (depth 2) + T extends readonly any[][] ? readonly postgres.EscapableArray[] : + // Insert/update helper (depth 2) + T extends readonly (object & infer R)[] ? (R extends postgres.SerializableParameter ? readonly postgres.SerializableParameter[] : readonly SerializableObject[]) : + // Dynamic values/ANY helper (depth 1) + T extends readonly any[] ? (readonly postgres.SerializableParameter[]) : + // Insert/update helper (depth 1) + T extends object ? SerializableObject : + // Unexpected type + never + +type Rest = + T extends TemplateStringsArray ? never : // force fallback to the tagged template function overload + T extends string ? readonly string[] : + T extends readonly any[][] ? readonly [] : + T extends readonly (object & infer R)[] ? ( + readonly (Keys & keyof R)[] // sql(data, "prop", "prop2") syntax + | + [readonly (Keys & keyof R)[]] // sql(data, ["prop", "prop2"]) syntax + ) : + T extends readonly any[] ? readonly [] : + T extends object ? ( + readonly (Keys & keyof T)[] // sql(data, "prop", "prop2") syntax + | + [readonly (Keys & keyof T)[]] // sql(data, ["prop", "prop2"]) syntax + ) : + any + +type Return = + [T] extends [TemplateStringsArray] ? + [unknown] extends [T] ? postgres.Helper : // ensure no `PendingQuery` with `any` types + [TemplateStringsArray] extends [T] ? postgres.PendingQuery : + postgres.Helper : + postgres.Helper + +declare namespace postgres { + class PostgresError extends Error { + name: 'PostgresError'; + severity_local: string; + severity: string; + code: string; + position: string; + file: string; + line: string; + routine: string; + + detail?: string | undefined; + hint?: string | undefined; + internal_position?: string | undefined; + internal_query?: string | undefined; + where?: string | undefined; + schema_name?: string | undefined; + table_name?: string | undefined; + column_name?: string | undefined; + data?: string | undefined; + type_name?: string | undefined; + constraint_name?: string | undefined; + + /** Only set when debug is enabled */ + query: string; + /** Only set when debug is enabled */ + parameters: any[]; + } + + /** + * Convert a snake_case string to PascalCase. + * @param str The string from snake_case to convert + * @returns The new string in PascalCase + */ + function toPascal(str: string): string; + namespace toPascal { + namespace column { function from(str: string): string; } + namespace value { function from(str: unknown, column: Column): string } + } + /** + * Convert a PascalCase string to snake_case. + * @param str The string from snake_case to convert + * @returns The new string in snake_case + */ + function fromPascal(str: string): string; + namespace fromPascal { + namespace column { function to(str: string): string } + } + /** + * Convert snake_case to and from PascalCase. + */ + namespace pascal { + namespace column { + function from(str: string): string; + function to(str: string): string; + } + namespace value { function from(str: unknown, column: Column): string } + } + /** + * Convert a snake_case string to camelCase. + * @param str The string from snake_case to convert + * @returns The new string in camelCase + */ + function toCamel(str: string): string; + namespace toCamel { + namespace column { function from(str: string): string; } + namespace value { function from(str: unknown, column: Column): string } + } + /** + * Convert a camelCase string to snake_case. + * @param str The string from snake_case to convert + * @returns The new string in snake_case + */ + function fromCamel(str: string): string; + namespace fromCamel { + namespace column { function to(str: string): string } + } + /** + * Convert snake_case to and from camelCase. + */ + namespace camel { + namespace column { + function from(str: string): string; + function to(str: string): string; + } + namespace value { function from(str: unknown, column: Column): string } + } + /** + * Convert a snake_case string to kebab-case. + * @param str The string from snake_case to convert + * @returns The new string in kebab-case + */ + function toKebab(str: string): string; + namespace toKebab { + namespace column { function from(str: string): string; } + namespace value { function from(str: unknown, column: Column): string } + } + /** + * Convert a kebab-case string to snake_case. + * @param str The string from snake_case to convert + * @returns The new string in snake_case + */ + function fromKebab(str: string): string; + namespace fromKebab { + namespace column { function to(str: string): string } + } + /** + * Convert snake_case to and from kebab-case. + */ + namespace kebab { + namespace column { + function from(str: string): string; + function to(str: string): string; + } + namespace value { function from(str: unknown, column: Column): string } + } + + const BigInt: PostgresType; + + interface PostgresType { + to: number; + from: number[]; + serialize: (value: T) => unknown; + parse: (raw: any) => T; + } + + interface ConnectionParameters { + /** + * Default application_name + * @default 'postgres.js' + */ + application_name: string; + default_transaction_isolation: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable', + default_transaction_read_only: boolean, + default_transaction_deferrable: boolean, + statement_timeout: number, + lock_timeout: number, + idle_in_transaction_session_timeout: number, + idle_session_timeout: number, + DateStyle: string, + IntervalStyle: string, + TimeZone: string, + /** Other connection parameters */ + [name: string]: string | number | boolean; + } + + interface Options> extends Partial> { + /** @inheritdoc */ + host?: string | undefined; + /** @inheritdoc */ + port?: number | undefined; + /** @inheritdoc */ + path?: string | undefined; + /** Password of database user (an alias for `password`) */ + pass?: Options['password'] | undefined; + /** + * Password of database user + * @default process.env['PGPASSWORD'] + */ + password?: string | (() => string | Promise) | undefined; + /** Name of database to connect to (an alias for `database`) */ + db?: Options['database'] | undefined; + /** Username of database user (an alias for `user`) */ + username?: Options['user'] | undefined; + /** Postgres ip address or domain name (an alias for `host`) */ + hostname?: Options['host'] | undefined; + /** + * Disable prepared mode + * @deprecated use "prepare" option instead + */ + no_prepare?: boolean | undefined; + /** + * Idle connection timeout in seconds + * @deprecated use "idle_timeout" option instead + */ + timeout?: Options['idle_timeout'] | undefined; + } + + interface ParsedOptions = {}> extends BaseOptions<{ [name in keyof T]: PostgresType }> { + /** @inheritdoc */ + host: string[]; + /** @inheritdoc */ + port: number[]; + /** @inheritdoc */ + pass: null; + /** @inheritdoc */ + transform: Transform; + serializers: Record unknown>; + parsers: Record unknown>; + } + + interface Transform { + /** Transforms outcoming undefined values */ + undefined: any + + column: { + /** Transform function for column names in result rows */ + from: ((column: string) => string) | undefined; + /** Transform function for column names in interpolated values passed to tagged template literal */ + to: ((column: string) => string) | undefined; + }; + value: { + /** Transform function for values in result rows */ + from: ((value: any, column?: Column) => any) | undefined; + /** Transform function for interpolated values passed to tagged template literal */ + to: undefined; // (value: any) => any + }; + row: { + /** Transform function for entire result rows */ + from: ((row: postgres.Row) => any) | undefined; + to: undefined; // (row: postgres.Row) => any + }; + } + + interface Notice { + [field: string]: string; + } + + interface Parameter extends NotAPromise { + /** + * PostgreSQL OID of the type + */ + type: number; + /** + * Serialized value + */ + value: string | null; + /** + * Raw value to serialize + */ + raw: T | null; + } + + interface ArrayParameter extends Parameter { + array: true; + } + + interface ConnectionError extends globalThis.Error { + code: + | 'CONNECTION_DESTROYED' + | 'CONNECT_TIMEOUT' + | 'CONNECTION_CLOSED' + | 'CONNECTION_ENDED'; + errno: this['code']; + address: string; + port?: number | undefined; + } + + interface NotSupportedError extends globalThis.Error { + code: 'MESSAGE_NOT_SUPPORTED'; + name: string; + } + + interface GenericError extends globalThis.Error { + code: + | '57014' // canceling statement due to user request + | 'NOT_TAGGED_CALL' + | 'UNDEFINED_VALUE' + | 'MAX_PARAMETERS_EXCEEDED' + | 'SASL_SIGNATURE_MISMATCH' + | 'UNSAFE_TRANSACTION'; + message: string; + } + + interface AuthNotImplementedError extends globalThis.Error { + code: 'AUTH_TYPE_NOT_IMPLEMENTED'; + type: number | string; + message: string; + } + + type Error = never + | PostgresError + | ConnectionError + | NotSupportedError + | GenericError + | AuthNotImplementedError; + + interface ColumnInfo { + key: number; + name: string; + type: number; + parser?(raw: string): unknown; + atttypmod: number; + } + + interface RelationInfo { + schema: string; + table: string; + columns: ColumnInfo[]; + keys: ColumnInfo[]; + } + + type ReplicationEvent = + | { command: 'insert', relation: RelationInfo } + | { command: 'delete', relation: RelationInfo, key: boolean } + | { command: 'update', relation: RelationInfo, key: boolean, old: Row | null }; + + interface SubscriptionHandle { + unsubscribe(): void; + } + + interface LargeObject { + writable(options?: { + highWaterMark?: number | undefined, + start?: number | undefined + } | undefined): Promise; + readable(options?: { + highWaterMark?: number | undefined, + start?: number | undefined, + end?: number | undefined + } | undefined): Promise; + + close(): Promise; + tell(): Promise; + read(size: number): Promise; + write(buffer: Uint8Array): Promise<[{ data: Uint8Array }]>; + truncate(size: number): Promise; + seek(offset: number, whence?: number | undefined): Promise; + size(): Promise<[{ position: bigint, size: bigint }]>; + } + + type EscapableArray = (string | number)[] + + type Serializable = never + | null + | boolean + | number + | string + | Date + | Uint8Array; + + type SerializableParameter = never + | T + | Serializable + | Helper + | Parameter + | ArrayParameter + | readonly SerializableParameter[]; + + type JSONValue = // using a dedicated type to detect symbols, bigints, and other non serializable types + | null + | string + | number + | boolean + | Date // serialized as `string` + | readonly JSONValue[] + | { toJSON(): any } // `toJSON` called by `JSON.stringify`; not typing the return type, types definition is strict enough anyway + | { + readonly [prop: string | number]: + | undefined + | JSONValue + | ((...args: any) => any) // serialized as `undefined` + }; + + interface Row { + [column: string]: any; + } + + type MaybeRow = Row | undefined; + + interface Column { + name: T; + type: number; + table: number; + number: number; + parser?: ((raw: string) => unknown) | undefined; + } + + type ColumnList = (T extends string ? Column : never)[]; + + interface State { + status: string; + pid: number; + secret: number; + } + + interface Statement { + /** statement unique name */ + name: string; + /** sql query */ + string: string; + /** parameters types */ + types: number[]; + columns: ColumnList; + } + + interface ResultMeta { + count: T; // For tuples + command: string; + statement: Statement; + state: State; + } + + interface ResultQueryMeta extends ResultMeta { + columns: ColumnList; + } + + type ExecutionResult = [] & ResultQueryMeta>; + type ValuesRowList = T[number][keyof T[number]][][] & ResultQueryMeta; + type RawRowList = Buffer[][] & Iterable & ResultQueryMeta; + type RowList = T & Iterable> & ResultQueryMeta; + + interface PendingQueryModifiers { + simple(): this; + readable(): Promise; + writable(): Promise; + + execute(): this; + cancel(): void; + + /** + * @deprecated `.stream` has been renamed to `.forEach` + * @throws + */ + stream(cb: (row: NonNullable, result: ExecutionResult) => void): never; + forEach(cb: (row: NonNullable, result: ExecutionResult) => void): Promise>; + + cursor(rows?: number | undefined): AsyncIterable[]>; + cursor(cb: (row: [NonNullable]) => void): Promise>; + cursor(rows: number, cb: (rows: NonNullable[]) => void): Promise>; + } + + interface PendingDescribeQuery extends Promise { + } + + interface PendingValuesQuery extends Promise>, PendingQueryModifiers { + describe(): PendingDescribeQuery; + } + + interface PendingRawQuery extends Promise>, PendingQueryModifiers { + } + + interface PendingQuery extends Promise>, PendingQueryModifiers { + describe(): PendingDescribeQuery; + values(): PendingValuesQuery; + raw(): PendingRawQuery; + } + + interface PendingRequest extends Promise<[] & ResultMeta> { } + + interface ListenRequest extends Promise { } + interface ListenMeta extends ResultMeta { + unlisten(): Promise + } + + interface Helper extends NotAPromise { + first: T; + rest: U; + } + + type Fragment = PendingQuery + + type ParameterOrJSON = + | SerializableParameter + | JSONValue + + type ParameterOrFragment = + | SerializableParameter + | Fragment + | Fragment[] + + interface Sql = {}> { + /** + * Query helper + * @param first Define how the helper behave + * @param rest Other optional arguments, depending on the helper type + * @returns An helper object usable as tagged template parameter in sql queries + */ + >(first: T & First, ...rest: K): Return; + + /** + * Execute the SQL query passed as a template string. Can only be used as template string tag. + * @param template The template generated from the template string + * @param parameters Interpoled values of the template string + * @returns A promise resolving to the result of your query + */ + (template: TemplateStringsArray, ...parameters: readonly (ParameterOrFragment)[]): PendingQuery; + + CLOSE: {}; + END: this['CLOSE']; + PostgresError: typeof PostgresError; + + options: ParsedOptions; + parameters: ConnectionParameters; + types: this['typed']; + typed: ((value: T, oid: number) => Parameter) & { + [name in keyof TTypes]: (value: TTypes[name]) => postgres.Parameter + }; + + unsafe)[]>(query: string, parameters?: (ParameterOrJSON)[] | undefined, queryOptions?: UnsafeQueryOptions | undefined): PendingQuery; + end(options?: { timeout?: number | undefined } | undefined): Promise; + + listen(channel: string, onnotify: (value: string) => void, onlisten?: (() => void) | undefined): ListenRequest; + notify(channel: string, payload: string): PendingRequest; + + subscribe(event: string, cb: (row: Row | null, info: ReplicationEvent) => void, onsubscribe?: (() => void), onerror?: (() => any)): Promise; + + largeObject(oid?: number | undefined, /** @default 0x00020000 | 0x00040000 */ mode?: number | undefined): Promise; + + begin(cb: (sql: TransactionSql) => T | Promise): Promise>; + begin(options: string, cb: (sql: TransactionSql) => T | Promise): Promise>; + + array[] = SerializableParameter[]>(value: T, type?: number | undefined): ArrayParameter; + file(path: string | Buffer | URL | number, options?: { cache?: boolean | undefined } | undefined): PendingQuery; + file(path: string | Buffer | URL | number, args: (ParameterOrJSON)[], options?: { cache?: boolean | undefined } | undefined): PendingQuery; + json(value: JSONValue): Parameter; + + reserve(): Promise> + } + + interface UnsafeQueryOptions { + /** + * When executes query as prepared statement. + * @default false + */ + prepare?: boolean | undefined; + } + + interface TransactionSql = {}> extends Sql { + savepoint(cb: (sql: TransactionSql) => T | Promise): Promise>; + savepoint(name: string, cb: (sql: TransactionSql) => T | Promise): Promise>; + + prepare(name: string): Promise>; + } + + interface ReservedSql = {}> extends Sql { + release(): void; + } +} + +export = postgres; From acd14f1bd9f04db6d371ba8dcdade44cc2c880c7 Mon Sep 17 00:00:00 2001 From: Jobians Date: Wed, 18 Jun 2025 19:02:47 +0100 Subject: [PATCH 3/3] chore: remove sed backup files --- deno/mod.js.bak | 2 - deno/polyfills.js.bak | 189 --- deno/src/bytes.js.bak | 79 -- deno/src/connection.js.bak | 1045 -------------- deno/src/errors.js.bak | 53 - deno/src/index.js.bak | 567 -------- deno/src/large.js.bak | 70 - deno/src/query.js.bak | 173 --- deno/src/queue.js.bak | 31 - deno/src/result.js.bak | 16 - deno/src/subscribe.js.bak | 278 ---- deno/src/types.js.bak | 368 ----- deno/tests/bootstrap.js.bak | 34 - deno/tests/index.js.bak | 2620 ----------------------------------- deno/tests/test.js.bak | 88 -- deno/types/index.d.ts.bak | 732 ---------- 16 files changed, 6345 deletions(-) delete mode 100644 deno/mod.js.bak delete mode 100644 deno/polyfills.js.bak delete mode 100644 deno/src/bytes.js.bak delete mode 100644 deno/src/connection.js.bak delete mode 100644 deno/src/errors.js.bak delete mode 100644 deno/src/index.js.bak delete mode 100644 deno/src/large.js.bak delete mode 100644 deno/src/query.js.bak delete mode 100644 deno/src/queue.js.bak delete mode 100644 deno/src/result.js.bak delete mode 100644 deno/src/subscribe.js.bak delete mode 100644 deno/src/types.js.bak delete mode 100644 deno/tests/bootstrap.js.bak delete mode 100644 deno/tests/index.js.bak delete mode 100644 deno/tests/test.js.bak delete mode 100644 deno/types/index.d.ts.bak diff --git a/deno/mod.js.bak b/deno/mod.js.bak deleted file mode 100644 index 7cbf18c3..00000000 --- a/deno/mod.js.bak +++ /dev/null @@ -1,2 +0,0 @@ -// @deno-types="./types/index.d.ts" -export { default } from './src/index.js' diff --git a/deno/polyfills.js.bak b/deno/polyfills.js.bak deleted file mode 100644 index 71ee694d..00000000 --- a/deno/polyfills.js.bak +++ /dev/null @@ -1,189 +0,0 @@ -/* global Deno */ - -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' -import { isIP } from 'https://deno.land/std@0.132.0/node/net.ts' - -const events = () => ({ data: [], error: [], drain: [], connect: [], secureConnect: [], close: [] }) - -class Socket { - constructor() { - return createSocket() - } -} - -function createSocket() { - let paused - , resume - , keepAlive - - const socket = { - error, - success, - readyState: 'open', - setKeepAlive: x => { - keepAlive = x - socket.raw && socket.raw.setKeepAlive && socket.raw.setKeepAlive(x) - }, - connect: (port, hostname) => { - socket.raw = null - socket.readyState = 'connecting' - typeof port === 'string' - ? Deno.connect({ transport: 'unix', path: socket.path = port }).then(success, error) - : Deno.connect({ transport: 'tcp', port: socket.port = port, hostname: socket.hostname = hostname || 'localhost' }).then(success, error) // eslint-disable-line - return socket - }, - pause: () => { - paused = new Promise(r => resume = r) - }, - resume: () => { - resume && resume() - paused = null - }, - isPaused: () => !!paused, - removeAllListeners: () => socket.events = events(), - events: events(), - raw: null, - on: (x, fn) => socket.events[x].push(fn), - once: (x, fn) => { - if (x === 'data') - socket.break = true - const e = socket.events[x] - e.push(once) - once.once = fn - function once(...args) { - fn(...args) - e.indexOf(once) > -1 && e.splice(e.indexOf(once), 1) - } - }, - removeListener: (x, fn) => { - socket.events[x] = socket.events[x].filter(x => x !== fn && x.once !== fn) - }, - write: (x, cb) => { - socket.raw.write(x).then(l => { - l < x.length - ? socket.write(x.slice(l), cb) - : (cb && cb(null)) - }).catch(err => { - cb && cb() - call(socket.events.error, err) - }) - return false - }, - destroy: () => close(), - end: (x) => { - x && socket.write(x) - close() - } - } - - return socket - - async function success(raw) { - if (socket.readyState !== 'connecting') - return raw.close() - - const encrypted = socket.encrypted - socket.raw = raw - keepAlive != null && raw.setKeepAlive && raw.setKeepAlive(keepAlive) - socket.readyState = 'open' - socket.encrypted - ? call(socket.events.secureConnect) - : call(socket.events.connect) - - const b = new Uint8Array(1024) - let result - - try { - while ((result = socket.readyState === 'open' && await raw.read(b))) { - call(socket.events.data, Buffer.from(b.subarray(0, result))) - if (!encrypted && socket.break && (socket.break = false, b[0] === 83)) - return socket.break = false - paused && await paused - } - } catch (e) { - if (e instanceof Deno.errors.BadResource === false) - error(e) - } - - if (!socket.encrypted || encrypted) - closed() - } - - function close() { - try { - socket.raw && socket.raw.close() - } catch (e) { - if (e instanceof Deno.errors.BadResource === false) - call(socket.events.error, e) - } - } - - function closed() { - if (socket.readyState === 'closed') - return - - socket.break = socket.encrypted = false - socket.readyState = 'closed' - call(socket.events.close) - } - - function error(err) { - call(socket.events.error, err) - socket.raw - ? close() - : closed() - } - - function call(xs, x) { - xs.slice().forEach(fn => fn(x)) - } -} - -export const net = { - isIP, - createServer() { - const server = { - address() { - return { port: 9876 } - }, - async listen() { - server.raw = Deno.listen({ port: 9876, transport: 'tcp' }) - for await (const conn of server.raw) - setTimeout(() => conn.close(), 500) - }, - close() { - server.raw.close() - } - } - return server - }, - Socket -} - -export const tls = { - connect({ socket, ...options }) { - socket.encrypted = true - socket.readyState = 'connecting' - Deno.startTls(socket.raw, { hostname: socket.hostname, ...options }) - .then(socket.success, socket.error) - socket.raw = null - return socket - } -} - -let ids = 1 -const tasks = new Set() -export const setImmediate = fn => { - const id = ids++ - tasks.add(id) - queueMicrotask(() => { - if (tasks.has(id)) { - fn() - tasks.delete(id) - } - }) - return id -} - -export const clearImmediate = id => tasks.delete(id) - diff --git a/deno/src/bytes.js.bak b/deno/src/bytes.js.bak deleted file mode 100644 index fe9359db..00000000 --- a/deno/src/bytes.js.bak +++ /dev/null @@ -1,79 +0,0 @@ -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' -const size = 256 -let buffer = Buffer.allocUnsafe(size) - -const messages = 'BCcDdEFfHPpQSX'.split('').reduce((acc, x) => { - const v = x.charCodeAt(0) - acc[x] = () => { - buffer[0] = v - b.i = 5 - return b - } - return acc -}, {}) - -const b = Object.assign(reset, messages, { - N: String.fromCharCode(0), - i: 0, - inc(x) { - b.i += x - return b - }, - str(x) { - const length = Buffer.byteLength(x) - fit(length) - b.i += buffer.write(x, b.i, length, 'utf8') - return b - }, - i16(x) { - fit(2) - buffer.writeUInt16BE(x, b.i) - b.i += 2 - return b - }, - i32(x, i) { - if (i || i === 0) { - buffer.writeUInt32BE(x, i) - return b - } - fit(4) - buffer.writeUInt32BE(x, b.i) - b.i += 4 - return b - }, - z(x) { - fit(x) - buffer.fill(0, b.i, b.i + x) - b.i += x - return b - }, - raw(x) { - buffer = Buffer.concat([buffer.subarray(0, b.i), x]) - b.i = buffer.length - return b - }, - end(at = 1) { - buffer.writeUInt32BE(b.i - at, at) - const out = buffer.subarray(0, b.i) - b.i = 0 - buffer = Buffer.allocUnsafe(size) - return out - } -}) - -export default b - -function fit(x) { - if (buffer.length - b.i < x) { - const prev = buffer - , length = prev.length - - buffer = Buffer.allocUnsafe(length + (length >> 1) + x) - prev.copy(buffer) - } -} - -function reset() { - b.i = 0 - return b -} diff --git a/deno/src/connection.js.bak b/deno/src/connection.js.bak deleted file mode 100644 index a3f43c48..00000000 --- a/deno/src/connection.js.bak +++ /dev/null @@ -1,1045 +0,0 @@ -import { HmacSha256 } from 'https://deno.land/std@0.132.0/hash/sha256.ts' -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' -import { setImmediate, clearImmediate } from '../polyfills.js' -import { net } from '../polyfills.js' -import { tls } from '../polyfills.js' -import crypto from 'https://deno.land/std@0.132.0/node/crypto.ts' -import Stream from 'https://deno.land/std@0.132.0/node/stream.ts' - - -import { stringify, handleValue, arrayParser, arraySerializer } from './types.js' -import { Errors } from './errors.js' -import Result from './result.js' -import Queue from './queue.js' -import { Query, CLOSE } from './query.js' -import b from './bytes.js' - -export default Connection - -let uid = 1 - -const Sync = b().S().end() - , Flush = b().H().end() - , SSLRequest = b().i32(8).i32(80877103).end(8) - , ExecuteUnnamed = Buffer.concat([b().E().str(b.N).i32(0).end(), Sync]) - , DescribeUnnamed = b().D().str('S').str(b.N).end() - , noop = () => { /* noop */ } - -const retryRoutines = new Set([ - 'FetchPreparedStatement', - 'RevalidateCachedQuery', - 'transformAssignedExpr' -]) - -const errorFields = { - 83 : 'severity_local', // S - 86 : 'severity', // V - 67 : 'code', // C - 77 : 'message', // M - 68 : 'detail', // D - 72 : 'hint', // H - 80 : 'position', // P - 112 : 'internal_position', // p - 113 : 'internal_query', // q - 87 : 'where', // W - 115 : 'schema_name', // s - 116 : 'table_name', // t - 99 : 'column_name', // c - 100 : 'data type_name', // d - 110 : 'constraint_name', // n - 70 : 'file', // F - 76 : 'line', // L - 82 : 'routine' // R -} - -function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose = noop } = {}) { - const { - ssl, - max, - user, - host, - port, - database, - parsers, - transform, - onnotice, - onnotify, - onparameter, - max_pipeline, - keep_alive, - backoff, - target_session_attrs - } = options - - const sent = Queue() - , id = uid++ - , backend = { pid: null, secret: null } - , idleTimer = timer(end, options.idle_timeout) - , lifeTimer = timer(end, options.max_lifetime) - , connectTimer = timer(connectTimedOut, options.connect_timeout) - - let socket = null - , cancelMessage - , result = new Result() - , incoming = Buffer.alloc(0) - , needsTypes = options.fetch_types - , backendParameters = {} - , statements = {} - , statementId = Math.random().toString(36).slice(2) - , statementCount = 1 - , closedDate = 0 - , remaining = 0 - , hostIndex = 0 - , retries = 0 - , length = 0 - , delay = 0 - , rows = 0 - , serverSignature = null - , nextWriteTimer = null - , terminated = false - , incomings = null - , results = null - , initial = null - , ending = null - , stream = null - , chunk = null - , ended = null - , nonce = null - , query = null - , final = null - - const connection = { - queue: queues.closed, - idleTimer, - connect(query) { - initial = query - reconnect() - }, - terminate, - execute, - cancel, - end, - count: 0, - id - } - - queues.closed && queues.closed.push(connection) - - return connection - - async function createSocket() { - let x - try { - x = options.socket - ? (await Promise.resolve(options.socket(options))) - : new net.Socket() - } catch (e) { - error(e) - return - } - x.on('error', error) - x.on('close', closed) - x.on('drain', drain) - return x - } - - async function cancel({ pid, secret }, resolve, reject) { - try { - cancelMessage = b().i32(16).i32(80877102).i32(pid).i32(secret).end(16) - await connect() - socket.once('error', reject) - socket.once('close', resolve) - } catch (error) { - reject(error) - } - } - - function execute(q) { - if (terminated) - return queryError(q, Errors.connection('CONNECTION_DESTROYED', options)) - - if (q.cancelled) - return - - try { - q.state = backend - query - ? sent.push(q) - : (query = q, query.active = true) - - build(q) - return write(toBuffer(q)) - && !q.describeFirst - && !q.cursorFn - && sent.length < max_pipeline - && (!q.options.onexecute || q.options.onexecute(connection)) - } catch (error) { - sent.length === 0 && write(Sync) - errored(error) - return true - } - } - - function toBuffer(q) { - if (q.parameters.length >= 65534) - throw Errors.generic('MAX_PARAMETERS_EXCEEDED', 'Max number of parameters (65534) exceeded') - - return q.options.simple - ? b().Q().str(q.statement.string + b.N).end() - : q.describeFirst - ? Buffer.concat([describe(q), Flush]) - : q.prepare - ? q.prepared - ? prepared(q) - : Buffer.concat([describe(q), prepared(q)]) - : unnamed(q) - } - - function describe(q) { - return Buffer.concat([ - Parse(q.statement.string, q.parameters, q.statement.types, q.statement.name), - Describe('S', q.statement.name) - ]) - } - - function prepared(q) { - return Buffer.concat([ - Bind(q.parameters, q.statement.types, q.statement.name, q.cursorName), - q.cursorFn - ? Execute('', q.cursorRows) - : ExecuteUnnamed - ]) - } - - function unnamed(q) { - return Buffer.concat([ - Parse(q.statement.string, q.parameters, q.statement.types), - DescribeUnnamed, - prepared(q) - ]) - } - - function build(q) { - const parameters = [] - , types = [] - - const string = stringify(q, q.strings[0], q.args[0], parameters, types, options) - - !q.tagged && q.args.forEach(x => handleValue(x, parameters, types, options)) - - q.prepare = options.prepare && ('prepare' in q.options ? q.options.prepare : true) - q.string = string - q.signature = q.prepare && types + string - q.onlyDescribe && (delete statements[q.signature]) - q.parameters = q.parameters || parameters - q.prepared = q.prepare && q.signature in statements - q.describeFirst = q.onlyDescribe || (parameters.length && !q.prepared) - q.statement = q.prepared - ? statements[q.signature] - : { string, types, name: q.prepare ? statementId + statementCount++ : '' } - - typeof options.debug === 'function' && options.debug(id, string, parameters, types) - } - - function write(x, fn) { - chunk = chunk ? Buffer.concat([chunk, x]) : Buffer.from(x) - if (fn || chunk.length >= 1024) - return nextWrite(fn) - nextWriteTimer === null && (nextWriteTimer = setImmediate(nextWrite)) - return true - } - - function nextWrite(fn) { - const x = socket.write(chunk, fn) - nextWriteTimer !== null && clearImmediate(nextWriteTimer) - chunk = nextWriteTimer = null - return x - } - - function connectTimedOut() { - errored(Errors.connection('CONNECT_TIMEOUT', options, socket)) - socket.destroy() - } - - async function secure() { - write(SSLRequest) - const canSSL = await new Promise(r => socket.once('data', x => r(x[0] === 83))) // S - - if (!canSSL && ssl === 'prefer') - return connected() - - socket.removeAllListeners() - socket = tls.connect({ - socket, - servername: net.isIP(socket.host) ? undefined : socket.host, - ...(ssl === 'require' || ssl === 'allow' || ssl === 'prefer' - ? { rejectUnauthorized: false } - : ssl === 'verify-full' - ? {} - : typeof ssl === 'object' - ? ssl - : {} - ) - }) - socket.on('secureConnect', connected) - socket.on('error', error) - socket.on('close', closed) - socket.on('drain', drain) - } - - /* c8 ignore next 3 */ - function drain() { - !query && onopen(connection) - } - - function data(x) { - if (incomings) { - incomings.push(x) - remaining -= x.length - if (remaining > 0) - return - } - - incoming = incomings - ? Buffer.concat(incomings, length - remaining) - : incoming.length === 0 - ? x - : Buffer.concat([incoming, x], incoming.length + x.length) - - while (incoming.length > 4) { - length = incoming.readUInt32BE(1) - if (length >= incoming.length) { - remaining = length - incoming.length - incomings = [incoming] - break - } - - try { - handle(incoming.subarray(0, length + 1)) - } catch (e) { - query && (query.cursorFn || query.describeFirst) && write(Sync) - errored(e) - } - incoming = incoming.subarray(length + 1) - remaining = 0 - incomings = null - } - } - - async function connect() { - terminated = false - backendParameters = {} - socket || (socket = await createSocket()) - - if (!socket) - return - - connectTimer.start() - - if (options.socket) - return ssl ? secure() : connected() - - socket.on('connect', ssl ? secure : connected) - - if (options.path) - return socket.connect(options.path) - - socket.ssl = ssl - socket.connect(port[hostIndex], host[hostIndex]) - socket.host = host[hostIndex] - socket.port = port[hostIndex] - - hostIndex = (hostIndex + 1) % port.length - } - - function reconnect() { - setTimeout(connect, closedDate ? closedDate + delay - performance.now() : 0) - } - - function connected() { - try { - statements = {} - needsTypes = options.fetch_types - statementId = Math.random().toString(36).slice(2) - statementCount = 1 - lifeTimer.start() - socket.on('data', data) - keep_alive && socket.setKeepAlive && socket.setKeepAlive(true) - const s = StartupMessage() - write(s) - } catch (err) { - error(err) - } - } - - function error(err) { - if (connection.queue === queues.connecting && options.host[retries + 1]) - return - - errored(err) - while (sent.length) - queryError(sent.shift(), err) - } - - function errored(err) { - stream && (stream.destroy(err), stream = null) - query && queryError(query, err) - initial && (queryError(initial, err), initial = null) - } - - function queryError(query, err) { - if (query.reserve) - return query.reject(err) - - if (!err || typeof err !== 'object') - err = new Error(err) - - 'query' in err || 'parameters' in err || Object.defineProperties(err, { - stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug }, - query: { value: query.string, enumerable: options.debug }, - parameters: { value: query.parameters, enumerable: options.debug }, - args: { value: query.args, enumerable: options.debug }, - types: { value: query.statement && query.statement.types, enumerable: options.debug } - }) - query.reject(err) - } - - function end() { - return ending || ( - !connection.reserved && onend(connection), - !connection.reserved && !initial && !query && sent.length === 0 - ? (terminate(), new Promise(r => socket && socket.readyState !== 'closed' ? socket.once('close', r) : r())) - : ending = new Promise(r => ended = r) - ) - } - - function terminate() { - terminated = true - if (stream || query || initial || sent.length) - error(Errors.connection('CONNECTION_DESTROYED', options)) - - clearImmediate(nextWriteTimer) - if (socket) { - socket.removeListener('data', data) - socket.removeListener('connect', connected) - socket.readyState === 'open' && socket.end(b().X().end()) - } - ended && (ended(), ending = ended = null) - } - - async function closed(hadError) { - incoming = Buffer.alloc(0) - remaining = 0 - incomings = null - clearImmediate(nextWriteTimer) - socket.removeListener('data', data) - socket.removeListener('connect', connected) - idleTimer.cancel() - lifeTimer.cancel() - connectTimer.cancel() - - socket.removeAllListeners() - socket = null - - if (initial) - return reconnect() - - !hadError && (query || sent.length) && error(Errors.connection('CONNECTION_CLOSED', options, socket)) - closedDate = performance.now() - hadError && options.shared.retries++ - delay = (typeof backoff === 'function' ? backoff(options.shared.retries) : backoff) * 1000 - onclose(connection, Errors.connection('CONNECTION_CLOSED', options, socket)) - } - - /* Handlers */ - function handle(xs, x = xs[0]) { - ( - x === 68 ? DataRow : // D - x === 100 ? CopyData : // d - x === 65 ? NotificationResponse : // A - x === 83 ? ParameterStatus : // S - x === 90 ? ReadyForQuery : // Z - x === 67 ? CommandComplete : // C - x === 50 ? BindComplete : // 2 - x === 49 ? ParseComplete : // 1 - x === 116 ? ParameterDescription : // t - x === 84 ? RowDescription : // T - x === 82 ? Authentication : // R - x === 110 ? NoData : // n - x === 75 ? BackendKeyData : // K - x === 69 ? ErrorResponse : // E - x === 115 ? PortalSuspended : // s - x === 51 ? CloseComplete : // 3 - x === 71 ? CopyInResponse : // G - x === 78 ? NoticeResponse : // N - x === 72 ? CopyOutResponse : // H - x === 99 ? CopyDone : // c - x === 73 ? EmptyQueryResponse : // I - x === 86 ? FunctionCallResponse : // V - x === 118 ? NegotiateProtocolVersion : // v - x === 87 ? CopyBothResponse : // W - /* c8 ignore next */ - UnknownMessage - )(xs) - } - - function DataRow(x) { - let index = 7 - let length - let column - let value - - const row = query.isRaw ? new Array(query.statement.columns.length) : {} - for (let i = 0; i < query.statement.columns.length; i++) { - column = query.statement.columns[i] - length = x.readInt32BE(index) - index += 4 - - value = length === -1 - ? null - : query.isRaw === true - ? x.subarray(index, index += length) - : column.parser === undefined - ? x.toString('utf8', index, index += length) - : column.parser.array === true - ? column.parser(x.toString('utf8', index + 1, index += length)) - : column.parser(x.toString('utf8', index, index += length)) - - query.isRaw - ? (row[i] = query.isRaw === true - ? value - : transform.value.from ? transform.value.from(value, column) : value) - : (row[column.name] = transform.value.from ? transform.value.from(value, column) : value) - } - - query.forEachFn - ? query.forEachFn(transform.row.from ? transform.row.from(row) : row, result) - : (result[rows++] = transform.row.from ? transform.row.from(row) : row) - } - - function ParameterStatus(x) { - const [k, v] = x.toString('utf8', 5, x.length - 1).split(b.N) - backendParameters[k] = v - if (options.parameters[k] !== v) { - options.parameters[k] = v - onparameter && onparameter(k, v) - } - } - - function ReadyForQuery(x) { - query && query.options.simple && query.resolve(results || result) - query = results = null - result = new Result() - connectTimer.cancel() - - if (initial) { - if (target_session_attrs) { - if (!backendParameters.in_hot_standby || !backendParameters.default_transaction_read_only) - return fetchState() - else if (tryNext(target_session_attrs, backendParameters)) - return terminate() - } - - if (needsTypes) { - initial.reserve && (initial = null) - return fetchArrayTypes() - } - - initial && !initial.reserve && execute(initial) - options.shared.retries = retries = 0 - initial = null - return - } - - while (sent.length && (query = sent.shift()) && (query.active = true, query.cancelled)) - Connection(options).cancel(query.state, query.cancelled.resolve, query.cancelled.reject) - - if (query) - return // Consider opening if able and sent.length < 50 - - connection.reserved - ? !connection.reserved.release && x[5] === 73 // I - ? ending - ? terminate() - : (connection.reserved = null, onopen(connection)) - : connection.reserved() - : ending - ? terminate() - : onopen(connection) - } - - function CommandComplete(x) { - rows = 0 - - for (let i = x.length - 1; i > 0; i--) { - if (x[i] === 32 && x[i + 1] < 58 && result.count === null) - result.count = +x.toString('utf8', i + 1, x.length - 1) - if (x[i - 1] >= 65) { - result.command = x.toString('utf8', 5, i) - result.state = backend - break - } - } - - final && (final(), final = null) - - if (result.command === 'BEGIN' && max !== 1 && !connection.reserved) - return errored(Errors.generic('UNSAFE_TRANSACTION', 'Only use sql.begin, sql.reserved or max: 1')) - - if (query.options.simple) - return BindComplete() - - if (query.cursorFn) { - result.count && query.cursorFn(result) - write(Sync) - } - - query.resolve(result) - } - - function ParseComplete() { - query.parsing = false - } - - function BindComplete() { - !result.statement && (result.statement = query.statement) - result.columns = query.statement.columns - } - - function ParameterDescription(x) { - const length = x.readUInt16BE(5) - - for (let i = 0; i < length; ++i) - !query.statement.types[i] && (query.statement.types[i] = x.readUInt32BE(7 + i * 4)) - - query.prepare && (statements[query.signature] = query.statement) - query.describeFirst && !query.onlyDescribe && (write(prepared(query)), query.describeFirst = false) - } - - function RowDescription(x) { - if (result.command) { - results = results || [result] - results.push(result = new Result()) - result.count = null - query.statement.columns = null - } - - const length = x.readUInt16BE(5) - let index = 7 - let start - - query.statement.columns = Array(length) - - for (let i = 0; i < length; ++i) { - start = index - while (x[index++] !== 0); - const table = x.readUInt32BE(index) - const number = x.readUInt16BE(index + 4) - const type = x.readUInt32BE(index + 6) - query.statement.columns[i] = { - name: transform.column.from - ? transform.column.from(x.toString('utf8', start, index - 1)) - : x.toString('utf8', start, index - 1), - parser: parsers[type], - table, - number, - type - } - index += 18 - } - - result.statement = query.statement - if (query.onlyDescribe) - return (query.resolve(query.statement), write(Sync)) - } - - async function Authentication(x, type = x.readUInt32BE(5)) { - ( - type === 3 ? AuthenticationCleartextPassword : - type === 5 ? AuthenticationMD5Password : - type === 10 ? SASL : - type === 11 ? SASLContinue : - type === 12 ? SASLFinal : - type !== 0 ? UnknownAuth : - noop - )(x, type) - } - - /* c8 ignore next 5 */ - async function AuthenticationCleartextPassword() { - const payload = await Pass() - write( - b().p().str(payload).z(1).end() - ) - } - - async function AuthenticationMD5Password(x) { - const payload = 'md5' + ( - await md5( - Buffer.concat([ - Buffer.from(await md5((await Pass()) + user)), - x.subarray(9) - ]) - ) - ) - write( - b().p().str(payload).z(1).end() - ) - } - - async function SASL() { - nonce = (await crypto.randomBytes(18)).toString('base64') - b().p().str('SCRAM-SHA-256' + b.N) - const i = b.i - write(b.inc(4).str('n,,n=*,r=' + nonce).i32(b.i - i - 4, i).end()) - } - - async function SASLContinue(x) { - const res = x.toString('utf8', 9).split(',').reduce((acc, x) => (acc[x[0]] = x.slice(2), acc), {}) - - const saltedPassword = await crypto.pbkdf2Sync( - await Pass(), - Buffer.from(res.s, 'base64'), - parseInt(res.i), 32, - 'sha256' - ) - - const clientKey = await hmac(saltedPassword, 'Client Key') - - const auth = 'n=*,r=' + nonce + ',' - + 'r=' + res.r + ',s=' + res.s + ',i=' + res.i - + ',c=biws,r=' + res.r - - serverSignature = (await hmac(await hmac(saltedPassword, 'Server Key'), auth)).toString('base64') - - const payload = 'c=biws,r=' + res.r + ',p=' + xor( - clientKey, Buffer.from(await hmac(await sha256(clientKey), auth)) - ).toString('base64') - - write( - b().p().str(payload).end() - ) - } - - function SASLFinal(x) { - if (x.toString('utf8', 9).split(b.N, 1)[0].slice(2) === serverSignature) - return - /* c8 ignore next 5 */ - errored(Errors.generic('SASL_SIGNATURE_MISMATCH', 'The server did not return the correct signature')) - socket.destroy() - } - - function Pass() { - return Promise.resolve(typeof options.pass === 'function' - ? options.pass() - : options.pass - ) - } - - function NoData() { - result.statement = query.statement - result.statement.columns = [] - if (query.onlyDescribe) - return (query.resolve(query.statement), write(Sync)) - } - - function BackendKeyData(x) { - backend.pid = x.readUInt32BE(5) - backend.secret = x.readUInt32BE(9) - } - - async function fetchArrayTypes() { - needsTypes = false - const types = await new Query([` - select b.oid, b.typarray - from pg_catalog.pg_type a - left join pg_catalog.pg_type b on b.oid = a.typelem - where a.typcategory = 'A' - group by b.oid, b.typarray - order by b.oid - `], [], execute) - types.forEach(({ oid, typarray }) => addArrayType(oid, typarray)) - } - - function addArrayType(oid, typarray) { - if (!!options.parsers[typarray] && !!options.serializers[typarray]) return - const parser = options.parsers[oid] - options.shared.typeArrayMap[oid] = typarray - options.parsers[typarray] = (xs) => arrayParser(xs, parser, typarray) - options.parsers[typarray].array = true - options.serializers[typarray] = (xs) => arraySerializer(xs, options.serializers[oid], options, typarray) - } - - function tryNext(x, xs) { - return ( - (x === 'read-write' && xs.default_transaction_read_only === 'on') || - (x === 'read-only' && xs.default_transaction_read_only === 'off') || - (x === 'primary' && xs.in_hot_standby === 'on') || - (x === 'standby' && xs.in_hot_standby === 'off') || - (x === 'prefer-standby' && xs.in_hot_standby === 'off' && options.host[retries]) - ) - } - - function fetchState() { - const query = new Query([` - show transaction_read_only; - select pg_catalog.pg_is_in_recovery() - `], [], execute, null, { simple: true }) - query.resolve = ([[a], [b]]) => { - backendParameters.default_transaction_read_only = a.transaction_read_only - backendParameters.in_hot_standby = b.pg_is_in_recovery ? 'on' : 'off' - } - query.execute() - } - - function ErrorResponse(x) { - query && (query.cursorFn || query.describeFirst) && write(Sync) - const error = Errors.postgres(parseError(x)) - query && query.retried - ? errored(query.retried) - : query && query.prepared && retryRoutines.has(error.routine) - ? retry(query, error) - : errored(error) - } - - function retry(q, error) { - delete statements[q.signature] - q.retried = error - execute(q) - } - - function NotificationResponse(x) { - if (!onnotify) - return - - let index = 9 - while (x[index++] !== 0); - onnotify( - x.toString('utf8', 9, index - 1), - x.toString('utf8', index, x.length - 1) - ) - } - - async function PortalSuspended() { - try { - const x = await Promise.resolve(query.cursorFn(result)) - rows = 0 - x === CLOSE - ? write(Close(query.portal)) - : (result = new Result(), write(Execute('', query.cursorRows))) - } catch (err) { - write(Sync) - query.reject(err) - } - } - - function CloseComplete() { - result.count && query.cursorFn(result) - query.resolve(result) - } - - function CopyInResponse() { - stream = new Stream.Writable({ - autoDestroy: true, - write(chunk, encoding, callback) { - socket.write(b().d().raw(chunk).end(), callback) - }, - destroy(error, callback) { - callback(error) - socket.write(b().f().str(error + b.N).end()) - stream = null - }, - final(callback) { - socket.write(b().c().end()) - final = callback - } - }) - query.resolve(stream) - } - - function CopyOutResponse() { - stream = new Stream.Readable({ - read() { socket.resume() } - }) - query.resolve(stream) - } - - /* c8 ignore next 3 */ - function CopyBothResponse() { - stream = new Stream.Duplex({ - autoDestroy: true, - read() { socket.resume() }, - /* c8 ignore next 11 */ - write(chunk, encoding, callback) { - socket.write(b().d().raw(chunk).end(), callback) - }, - destroy(error, callback) { - callback(error) - socket.write(b().f().str(error + b.N).end()) - stream = null - }, - final(callback) { - socket.write(b().c().end()) - final = callback - } - }) - query.resolve(stream) - } - - function CopyData(x) { - stream && (stream.push(x.subarray(5)) || socket.pause()) - } - - function CopyDone() { - stream && stream.push(null) - stream = null - } - - function NoticeResponse(x) { - onnotice - ? onnotice(parseError(x)) - : console.log(parseError(x)) // eslint-disable-line - - } - - /* c8 ignore next 3 */ - function EmptyQueryResponse() { - /* noop */ - } - - /* c8 ignore next 3 */ - function FunctionCallResponse() { - errored(Errors.notSupported('FunctionCallResponse')) - } - - /* c8 ignore next 3 */ - function NegotiateProtocolVersion() { - errored(Errors.notSupported('NegotiateProtocolVersion')) - } - - /* c8 ignore next 3 */ - function UnknownMessage(x) { - console.error('Postgres.js : Unknown Message:', x[0]) // eslint-disable-line - } - - /* c8 ignore next 3 */ - function UnknownAuth(x, type) { - console.error('Postgres.js : Unknown Auth:', type) // eslint-disable-line - } - - /* Messages */ - function Bind(parameters, types, statement = '', portal = '') { - let prev - , type - - b().B().str(portal + b.N).str(statement + b.N).i16(0).i16(parameters.length) - - parameters.forEach((x, i) => { - if (x === null) - return b.i32(0xFFFFFFFF) - - type = types[i] - parameters[i] = x = type in options.serializers - ? options.serializers[type](x) - : '' + x - - prev = b.i - b.inc(4).str(x).i32(b.i - prev - 4, prev) - }) - - b.i16(0) - - return b.end() - } - - function Parse(str, parameters, types, name = '') { - b().P().str(name + b.N).str(str + b.N).i16(parameters.length) - parameters.forEach((x, i) => b.i32(types[i] || 0)) - return b.end() - } - - function Describe(x, name = '') { - return b().D().str(x).str(name + b.N).end() - } - - function Execute(portal = '', rows = 0) { - return Buffer.concat([ - b().E().str(portal + b.N).i32(rows).end(), - Flush - ]) - } - - function Close(portal = '') { - return Buffer.concat([ - b().C().str('P').str(portal + b.N).end(), - b().S().end() - ]) - } - - function StartupMessage() { - return cancelMessage || b().inc(4).i16(3).z(2).str( - Object.entries(Object.assign({ - user, - database, - client_encoding: 'UTF8' - }, - options.connection - )).filter(([, v]) => v).map(([k, v]) => k + b.N + v).join(b.N) - ).z(2).end(0) - } - -} - -function parseError(x) { - const error = {} - let start = 5 - for (let i = 5; i < x.length - 1; i++) { - if (x[i] === 0) { - error[errorFields[x[start]]] = x.toString('utf8', start + 1, i) - start = i + 1 - } - } - return error -} - -function md5(x) { - return crypto.createHash('md5').update(x).digest('hex') -} - -function hmac(key, x) { - return Buffer.from(new HmacSha256(key).update(x).digest()) -} - -function sha256(x) { - return crypto.createHash('sha256').update(x).digest() -} - -function xor(a, b) { - const length = Math.max(a.length, b.length) - const buffer = Buffer.allocUnsafe(length) - for (let i = 0; i < length; i++) - buffer[i] = a[i] ^ b[i] - return buffer -} - -function timer(fn, seconds) { - seconds = typeof seconds === 'function' ? seconds() : seconds - if (!seconds) - return { cancel: noop, start: noop } - - let timer - return { - cancel() { - timer && (clearTimeout(timer), timer = null) - }, - start() { - timer && clearTimeout(timer) - timer = setTimeout(done, seconds * 1000, arguments) - } - } - - function done(args) { - fn.apply(null, args) - timer = null - } -} diff --git a/deno/src/errors.js.bak b/deno/src/errors.js.bak deleted file mode 100644 index 0ff83c42..00000000 --- a/deno/src/errors.js.bak +++ /dev/null @@ -1,53 +0,0 @@ -export class PostgresError extends Error { - constructor(x) { - super(x.message) - this.name = this.constructor.name - Object.assign(this, x) - } -} - -export const Errors = { - connection, - postgres, - generic, - notSupported -} - -function connection(x, options, socket) { - const { host, port } = socket || options - const error = Object.assign( - new Error(('write ' + x + ' ' + (options.path || (host + ':' + port)))), - { - code: x, - errno: x, - address: options.path || host - }, options.path ? {} : { port: port } - ) - Error.captureStackTrace(error, connection) - return error -} - -function postgres(x) { - const error = new PostgresError(x) - Error.captureStackTrace(error, postgres) - return error -} - -function generic(code, message) { - const error = Object.assign(new Error(code + ': ' + message), { code }) - Error.captureStackTrace(error, generic) - return error -} - -/* c8 ignore next 10 */ -function notSupported(x) { - const error = Object.assign( - new Error(x + ' (B) is not supported'), - { - code: 'MESSAGE_NOT_SUPPORTED', - name: x - } - ) - Error.captureStackTrace(error, notSupported) - return error -} diff --git a/deno/src/index.js.bak b/deno/src/index.js.bak deleted file mode 100644 index aa7a920f..00000000 --- a/deno/src/index.js.bak +++ /dev/null @@ -1,567 +0,0 @@ -import process from 'https://deno.land/std@0.132.0/node/process.ts' -import os from 'https://deno.land/std@0.132.0/node/os.ts' -import fs from 'https://deno.land/std@0.132.0/node/fs.ts' - -import { - mergeUserTypes, - inferType, - Parameter, - Identifier, - Builder, - toPascal, - pascal, - toCamel, - camel, - toKebab, - kebab, - fromPascal, - fromCamel, - fromKebab -} from './types.js' - -import Connection from './connection.js' -import { Query, CLOSE } from './query.js' -import Queue from './queue.js' -import { Errors, PostgresError } from './errors.js' -import Subscribe from './subscribe.js' -import largeObject from './large.js' - -Object.assign(Postgres, { - PostgresError, - toPascal, - pascal, - toCamel, - camel, - toKebab, - kebab, - fromPascal, - fromCamel, - fromKebab, - BigInt: { - to: 20, - from: [20], - parse: x => BigInt(x), // eslint-disable-line - serialize: x => x.toString() - } -}) - -export default Postgres - -function Postgres(a, b) { - const options = parseOptions(a, b) - , subscribe = options.no_subscribe || Subscribe(Postgres, { ...options }) - - let ending = false - - const queries = Queue() - , connecting = Queue() - , reserved = Queue() - , closed = Queue() - , ended = Queue() - , open = Queue() - , busy = Queue() - , full = Queue() - , queues = { connecting, reserved, closed, ended, open, busy, full } - - const connections = [...Array(options.max)].map(() => Connection(options, queues, { onopen, onend, onclose })) - - const sql = Sql(handler) - - Object.assign(sql, { - get parameters() { return options.parameters }, - largeObject: largeObject.bind(null, sql), - subscribe, - CLOSE, - END: CLOSE, - PostgresError, - options, - reserve, - listen, - begin, - close, - end - }) - - return sql - - function Sql(handler) { - handler.debug = options.debug - - Object.entries(options.types).reduce((acc, [name, type]) => { - acc[name] = (x) => new Parameter(x, type.to) - return acc - }, typed) - - Object.assign(sql, { - types: typed, - typed, - unsafe, - notify, - array, - json, - file - }) - - return sql - - function typed(value, type) { - return new Parameter(value, type) - } - - function sql(strings, ...args) { - const query = strings && Array.isArray(strings.raw) - ? new Query(strings, args, handler, cancel) - : typeof strings === 'string' && !args.length - ? new Identifier(options.transform.column.to ? options.transform.column.to(strings) : strings) - : new Builder(strings, args) - return query - } - - function unsafe(string, args = [], options = {}) { - arguments.length === 2 && !Array.isArray(args) && (options = args, args = []) - const query = new Query([string], args, handler, cancel, { - prepare: false, - ...options, - simple: 'simple' in options ? options.simple : args.length === 0 - }) - return query - } - - function file(path, args = [], options = {}) { - arguments.length === 2 && !Array.isArray(args) && (options = args, args = []) - const query = new Query([], args, (query) => { - fs.readFile(path, 'utf8', (err, string) => { - if (err) - return query.reject(err) - - query.strings = [string] - handler(query) - }) - }, cancel, { - ...options, - simple: 'simple' in options ? options.simple : args.length === 0 - }) - return query - } - } - - async function listen(name, fn, onlisten) { - const listener = { fn, onlisten } - - const sql = listen.sql || (listen.sql = Postgres({ - ...options, - max: 1, - idle_timeout: null, - max_lifetime: null, - fetch_types: false, - onclose() { - Object.entries(listen.channels).forEach(([name, { listeners }]) => { - delete listen.channels[name] - Promise.all(listeners.map(l => listen(name, l.fn, l.onlisten).catch(() => { /* noop */ }))) - }) - }, - onnotify(c, x) { - c in listen.channels && listen.channels[c].listeners.forEach(l => l.fn(x)) - } - })) - - const channels = listen.channels || (listen.channels = {}) - , exists = name in channels - - if (exists) { - channels[name].listeners.push(listener) - const result = await channels[name].result - listener.onlisten && listener.onlisten() - return { state: result.state, unlisten } - } - - channels[name] = { result: sql`listen ${ - sql.unsafe('"' + name.replace(/"/g, '""') + '"') - }`, listeners: [listener] } - const result = await channels[name].result - listener.onlisten && listener.onlisten() - return { state: result.state, unlisten } - - async function unlisten() { - if (name in channels === false) - return - - channels[name].listeners = channels[name].listeners.filter(x => x !== listener) - if (channels[name].listeners.length) - return - - delete channels[name] - return sql`unlisten ${ - sql.unsafe('"' + name.replace(/"/g, '""') + '"') - }` - } - } - - async function notify(channel, payload) { - return await sql`select pg_notify(${ channel }, ${ '' + payload })` - } - - async function reserve() { - const queue = Queue() - const c = open.length - ? open.shift() - : await new Promise((resolve, reject) => { - const query = { reserve: resolve, reject } - queries.push(query) - closed.length && connect(closed.shift(), query) - }) - - move(c, reserved) - c.reserved = () => queue.length - ? c.execute(queue.shift()) - : move(c, reserved) - c.reserved.release = true - - const sql = Sql(handler) - sql.release = () => { - c.reserved = null - onopen(c) - } - - return sql - - function handler(q) { - c.queue === full - ? queue.push(q) - : c.execute(q) || move(c, full) - } - } - - async function begin(options, fn) { - !fn && (fn = options, options = '') - const queries = Queue() - let savepoints = 0 - , connection - , prepare = null - - try { - await sql.unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), [], { onexecute }).execute() - return await Promise.race([ - scope(connection, fn), - new Promise((_, reject) => connection.onclose = reject) - ]) - } catch (error) { - throw error - } - - async function scope(c, fn, name) { - const sql = Sql(handler) - sql.savepoint = savepoint - sql.prepare = x => prepare = x.replace(/[^a-z0-9$-_. ]/gi) - let uncaughtError - , result - - name && await sql`savepoint ${ sql(name) }` - try { - result = await new Promise((resolve, reject) => { - const x = fn(sql) - Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve, reject) - }) - - if (uncaughtError) - throw uncaughtError - } catch (e) { - await (name - ? sql`rollback to ${ sql(name) }` - : sql`rollback` - ) - throw e instanceof PostgresError && e.code === '25P02' && uncaughtError || e - } - - if (!name) { - prepare - ? await sql`prepare transaction '${ sql.unsafe(prepare) }'` - : await sql`commit` - } - - return result - - function savepoint(name, fn) { - if (name && Array.isArray(name.raw)) - return savepoint(sql => sql.apply(sql, arguments)) - - arguments.length === 1 && (fn = name, name = null) - return scope(c, fn, 's' + savepoints++ + (name ? '_' + name : '')) - } - - function handler(q) { - q.catch(e => uncaughtError || (uncaughtError = e)) - c.queue === full - ? queries.push(q) - : c.execute(q) || move(c, full) - } - } - - function onexecute(c) { - connection = c - move(c, reserved) - c.reserved = () => queries.length - ? c.execute(queries.shift()) - : move(c, reserved) - } - } - - function move(c, queue) { - c.queue.remove(c) - queue.push(c) - c.queue = queue - queue === open - ? c.idleTimer.start() - : c.idleTimer.cancel() - return c - } - - function json(x) { - return new Parameter(x, 3802) - } - - function array(x, type) { - if (!Array.isArray(x)) - return array(Array.from(arguments)) - - return new Parameter(x, type || (x.length ? inferType(x) || 25 : 0), options.shared.typeArrayMap) - } - - function handler(query) { - if (ending) - return query.reject(Errors.connection('CONNECTION_ENDED', options, options)) - - if (open.length) - return go(open.shift(), query) - - if (closed.length) - return connect(closed.shift(), query) - - busy.length - ? go(busy.shift(), query) - : queries.push(query) - } - - function go(c, query) { - return c.execute(query) - ? move(c, busy) - : move(c, full) - } - - function cancel(query) { - return new Promise((resolve, reject) => { - query.state - ? query.active - ? Connection(options).cancel(query.state, resolve, reject) - : query.cancelled = { resolve, reject } - : ( - queries.remove(query), - query.cancelled = true, - query.reject(Errors.generic('57014', 'canceling statement due to user request')), - resolve() - ) - }) - } - - async function end({ timeout = null } = {}) { - if (ending) - return ending - - await 1 - let timer - return ending = Promise.race([ - new Promise(r => timeout !== null && (timer = setTimeout(destroy, timeout * 1000, r))), - Promise.all(connections.map(c => c.end()).concat( - listen.sql ? listen.sql.end({ timeout: 0 }) : [], - subscribe.sql ? subscribe.sql.end({ timeout: 0 }) : [] - )) - ]).then(() => clearTimeout(timer)) - } - - async function close() { - await Promise.all(connections.map(c => c.end())) - } - - async function destroy(resolve) { - await Promise.all(connections.map(c => c.terminate())) - while (queries.length) - queries.shift().reject(Errors.connection('CONNECTION_DESTROYED', options)) - resolve() - } - - function connect(c, query) { - move(c, connecting) - c.connect(query) - return c - } - - function onend(c) { - move(c, ended) - } - - function onopen(c) { - if (queries.length === 0) - return move(c, open) - - let max = Math.ceil(queries.length / (connecting.length + 1)) - , ready = true - - while (ready && queries.length && max-- > 0) { - const query = queries.shift() - if (query.reserve) - return query.reserve(c) - - ready = c.execute(query) - } - - ready - ? move(c, busy) - : move(c, full) - } - - function onclose(c, e) { - move(c, closed) - c.reserved = null - c.onclose && (c.onclose(e), c.onclose = null) - options.onclose && options.onclose(c.id) - queries.length && connect(c, queries.shift()) - } -} - -function parseOptions(a, b) { - if (a && a.shared) - return a - - const env = process.env // eslint-disable-line - , o = (!a || typeof a === 'string' ? b : a) || {} - , { url, multihost } = parseUrl(a) - , query = [...url.searchParams].reduce((a, [b, c]) => (a[b] = c, a), {}) - , host = o.hostname || o.host || multihost || url.hostname || env.PGHOST || 'localhost' - , port = o.port || url.port || env.PGPORT || 5432 - , user = o.user || o.username || url.username || env.PGUSERNAME || env.PGUSER || osUsername() - - o.no_prepare && (o.prepare = false) - query.sslmode && (query.ssl = query.sslmode, delete query.sslmode) - 'timeout' in o && (console.log('The timeout option is deprecated, use idle_timeout instead'), o.idle_timeout = o.timeout) // eslint-disable-line - query.sslrootcert === 'system' && (query.ssl = 'verify-full') - - const ints = ['idle_timeout', 'connect_timeout', 'max_lifetime', 'max_pipeline', 'backoff', 'keep_alive'] - const defaults = { - max : 10, - ssl : false, - idle_timeout : null, - connect_timeout : 30, - max_lifetime : max_lifetime, - max_pipeline : 100, - backoff : backoff, - keep_alive : 60, - prepare : true, - debug : false, - fetch_types : true, - publications : 'alltables', - target_session_attrs: null - } - - return { - host : Array.isArray(host) ? host : host.split(',').map(x => x.split(':')[0]), - port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(x.split(':')[1] || port)), - path : o.path || host.indexOf('/') > -1 && host + '/.s.PGSQL.' + port, - database : o.database || o.db || (url.pathname || '').slice(1) || env.PGDATABASE || user, - user : user, - pass : o.pass || o.password || url.password || env.PGPASSWORD || '', - ...Object.entries(defaults).reduce( - (acc, [k, d]) => { - const value = k in o ? o[k] : k in query - ? (query[k] === 'disable' || query[k] === 'false' ? false : query[k]) - : env['PG' + k.toUpperCase()] || d - acc[k] = typeof value === 'string' && ints.includes(k) - ? +value - : value - return acc - }, - {} - ), - connection : { - ...o.connection, - application_name: o.connection?.application_name ?? env.PGAPPNAME ?? 'postgres.js', - ...Object.entries(query).reduce((acc, [k, v]) => (k in defaults || (acc[k] = v), acc), {}) - }, - types : o.types || {}, - target_session_attrs: tsa(o, url, env), - onnotice : o.onnotice, - onnotify : o.onnotify, - onclose : o.onclose, - onparameter : o.onparameter, - socket : o.socket, - transform : parseTransform(o.transform || { undefined: undefined }), - parameters : {}, - shared : { retries: 0, typeArrayMap: {} }, - ...mergeUserTypes(o.types) - } -} - -function tsa(o, url, env) { - const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS - if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x)) - return x - - throw new Error('target_session_attrs ' + x + ' is not supported') -} - -function backoff(retries) { - return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20) -} - -function max_lifetime() { - return 60 * (30 + Math.random() * 30) -} - -function parseTransform(x) { - return { - undefined: x.undefined, - column: { - from: typeof x.column === 'function' ? x.column : x.column && x.column.from, - to: x.column && x.column.to - }, - value: { - from: typeof x.value === 'function' ? x.value : x.value && x.value.from, - to: x.value && x.value.to - }, - row: { - from: typeof x.row === 'function' ? x.row : x.row && x.row.from, - to: x.row && x.row.to - } - } -} - -function parseUrl(url) { - if (!url || typeof url !== 'string') - return { url: { searchParams: new Map() } } - - let host = url - host = host.slice(host.indexOf('://') + 3).split(/[?/]/)[0] - host = decodeURIComponent(host.slice(host.indexOf('@') + 1)) - - const urlObj = new URL(url.replace(host, host.split(',')[0])) - - return { - url: { - username: decodeURIComponent(urlObj.username), - password: decodeURIComponent(urlObj.password), - host: urlObj.host, - hostname: urlObj.hostname, - port: urlObj.port, - pathname: urlObj.pathname, - searchParams: urlObj.searchParams - }, - multihost: host.indexOf(',') > -1 && host - } -} - -function osUsername() { - try { - return os.userInfo().username // eslint-disable-line - } catch (_) { - return process.env.USERNAME || process.env.USER || process.env.LOGNAME // eslint-disable-line - } -} diff --git a/deno/src/large.js.bak b/deno/src/large.js.bak deleted file mode 100644 index 1b9f42d2..00000000 --- a/deno/src/large.js.bak +++ /dev/null @@ -1,70 +0,0 @@ -import Stream from 'https://deno.land/std@0.132.0/node/stream.ts' - -export default function largeObject(sql, oid, mode = 0x00020000 | 0x00040000) { - return new Promise(async(resolve, reject) => { - await sql.begin(async sql => { - let finish - !oid && ([{ oid }] = await sql`select lo_creat(-1) as oid`) - const [{ fd }] = await sql`select lo_open(${ oid }, ${ mode }) as fd` - - const lo = { - writable, - readable, - close : () => sql`select lo_close(${ fd })`.then(finish), - tell : () => sql`select lo_tell64(${ fd })`, - read : (x) => sql`select loread(${ fd }, ${ x }) as data`, - write : (x) => sql`select lowrite(${ fd }, ${ x })`, - truncate : (x) => sql`select lo_truncate64(${ fd }, ${ x })`, - seek : (x, whence = 0) => sql`select lo_lseek64(${ fd }, ${ x }, ${ whence })`, - size : () => sql` - select - lo_lseek64(${ fd }, location, 0) as position, - seek.size - from ( - select - lo_lseek64($1, 0, 2) as size, - tell.location - from (select lo_tell64($1) as location) tell - ) seek - ` - } - - resolve(lo) - - return new Promise(async r => finish = r) - - async function readable({ - highWaterMark = 2048 * 8, - start = 0, - end = Infinity - } = {}) { - let max = end - start - start && await lo.seek(start) - return new Stream.Readable({ - highWaterMark, - async read(size) { - const l = size > max ? size - max : size - max -= size - const [{ data }] = await lo.read(l) - this.push(data) - if (data.length < size) - this.push(null) - } - }) - } - - async function writable({ - highWaterMark = 2048 * 8, - start = 0 - } = {}) { - start && await lo.seek(start) - return new Stream.Writable({ - highWaterMark, - write(chunk, encoding, callback) { - lo.write(chunk).then(() => callback(), callback) - } - }) - } - }).catch(reject) - }) -} diff --git a/deno/src/query.js.bak b/deno/src/query.js.bak deleted file mode 100644 index 0d44a15c..00000000 --- a/deno/src/query.js.bak +++ /dev/null @@ -1,173 +0,0 @@ -const originCache = new Map() - , originStackCache = new Map() - , originError = Symbol('OriginError') - -export const CLOSE = {} -export class Query extends Promise { - constructor(strings, args, handler, canceller, options = {}) { - let resolve - , reject - - super((a, b) => { - resolve = a - reject = b - }) - - this.tagged = Array.isArray(strings.raw) - this.strings = strings - this.args = args - this.handler = handler - this.canceller = canceller - this.options = options - - this.state = null - this.statement = null - - this.resolve = x => (this.active = false, resolve(x)) - this.reject = x => (this.active = false, reject(x)) - - this.active = false - this.cancelled = null - this.executed = false - this.signature = '' - - this[originError] = this.handler.debug - ? new Error() - : this.tagged && cachedError(this.strings) - } - - get origin() { - return (this.handler.debug - ? this[originError].stack - : this.tagged && originStackCache.has(this.strings) - ? originStackCache.get(this.strings) - : originStackCache.set(this.strings, this[originError].stack).get(this.strings) - ) || '' - } - - static get [Symbol.species]() { - return Promise - } - - cancel() { - return this.canceller && (this.canceller(this), this.canceller = null) - } - - simple() { - this.options.simple = true - this.options.prepare = false - return this - } - - async readable() { - this.simple() - this.streaming = true - return this - } - - async writable() { - this.simple() - this.streaming = true - return this - } - - cursor(rows = 1, fn) { - this.options.simple = false - if (typeof rows === 'function') { - fn = rows - rows = 1 - } - - this.cursorRows = rows - - if (typeof fn === 'function') - return (this.cursorFn = fn, this) - - let prev - return { - [Symbol.asyncIterator]: () => ({ - next: () => { - if (this.executed && !this.active) - return { done: true } - - prev && prev() - const promise = new Promise((resolve, reject) => { - this.cursorFn = value => { - resolve({ value, done: false }) - return new Promise(r => prev = r) - } - this.resolve = () => (this.active = false, resolve({ done: true })) - this.reject = x => (this.active = false, reject(x)) - }) - this.execute() - return promise - }, - return() { - prev && prev(CLOSE) - return { done: true } - } - }) - } - } - - describe() { - this.options.simple = false - this.onlyDescribe = this.options.prepare = true - return this - } - - stream() { - throw new Error('.stream has been renamed to .forEach') - } - - forEach(fn) { - this.forEachFn = fn - this.handle() - return this - } - - raw() { - this.isRaw = true - return this - } - - values() { - this.isRaw = 'values' - return this - } - - async handle() { - !this.executed && (this.executed = true) && await 1 && this.handler(this) - } - - execute() { - this.handle() - return this - } - - then() { - this.handle() - return super.then.apply(this, arguments) - } - - catch() { - this.handle() - return super.catch.apply(this, arguments) - } - - finally() { - this.handle() - return super.finally.apply(this, arguments) - } -} - -function cachedError(xs) { - if (originCache.has(xs)) - return originCache.get(xs) - - const x = Error.stackTraceLimit - Error.stackTraceLimit = 4 - originCache.set(xs, new Error()) - Error.stackTraceLimit = x - return originCache.get(xs) -} diff --git a/deno/src/queue.js.bak b/deno/src/queue.js.bak deleted file mode 100644 index c4ef9716..00000000 --- a/deno/src/queue.js.bak +++ /dev/null @@ -1,31 +0,0 @@ -export default Queue - -function Queue(initial = []) { - let xs = initial.slice() - let index = 0 - - return { - get length() { - return xs.length - index - }, - remove: (x) => { - const index = xs.indexOf(x) - return index === -1 - ? null - : (xs.splice(index, 1), x) - }, - push: (x) => (xs.push(x), x), - shift: () => { - const out = xs[index++] - - if (index === xs.length) { - index = 0 - xs = [] - } else { - xs[index - 1] = undefined - } - - return out - } - } -} diff --git a/deno/src/result.js.bak b/deno/src/result.js.bak deleted file mode 100644 index 31014284..00000000 --- a/deno/src/result.js.bak +++ /dev/null @@ -1,16 +0,0 @@ -export default class Result extends Array { - constructor() { - super() - Object.defineProperties(this, { - count: { value: null, writable: true }, - state: { value: null, writable: true }, - command: { value: null, writable: true }, - columns: { value: null, writable: true }, - statement: { value: null, writable: true } - }) - } - - static get [Symbol.species]() { - return Array - } -} diff --git a/deno/src/subscribe.js.bak b/deno/src/subscribe.js.bak deleted file mode 100644 index b20efb96..00000000 --- a/deno/src/subscribe.js.bak +++ /dev/null @@ -1,278 +0,0 @@ -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' -const noop = () => { /* noop */ } - -export default function Subscribe(postgres, options) { - const subscribers = new Map() - , slot = 'postgresjs_' + Math.random().toString(36).slice(2) - , state = {} - - let connection - , stream - , ended = false - - const sql = subscribe.sql = postgres({ - ...options, - transform: { column: {}, value: {}, row: {} }, - max: 1, - fetch_types: false, - idle_timeout: null, - max_lifetime: null, - connection: { - ...options.connection, - replication: 'database' - }, - onclose: async function() { - if (ended) - return - stream = null - state.pid = state.secret = undefined - connected(await init(sql, slot, options.publications)) - subscribers.forEach(event => event.forEach(({ onsubscribe }) => onsubscribe())) - }, - no_subscribe: true - }) - - const end = sql.end - , close = sql.close - - sql.end = async() => { - ended = true - stream && (await new Promise(r => (stream.once('close', r), stream.end()))) - return end() - } - - sql.close = async() => { - stream && (await new Promise(r => (stream.once('close', r), stream.end()))) - return close() - } - - return subscribe - - async function subscribe(event, fn, onsubscribe = noop, onerror = noop) { - event = parseEvent(event) - - if (!connection) - connection = init(sql, slot, options.publications) - - const subscriber = { fn, onsubscribe } - const fns = subscribers.has(event) - ? subscribers.get(event).add(subscriber) - : subscribers.set(event, new Set([subscriber])).get(event) - - const unsubscribe = () => { - fns.delete(subscriber) - fns.size === 0 && subscribers.delete(event) - } - - return connection.then(x => { - connected(x) - onsubscribe() - stream && stream.on('error', onerror) - return { unsubscribe, state, sql } - }) - } - - function connected(x) { - stream = x.stream - state.pid = x.state.pid - state.secret = x.state.secret - } - - async function init(sql, slot, publications) { - if (!publications) - throw new Error('Missing publication names') - - const xs = await sql.unsafe( - `CREATE_REPLICATION_SLOT ${ slot } TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT` - ) - - const [x] = xs - - const stream = await sql.unsafe( - `START_REPLICATION SLOT ${ slot } LOGICAL ${ - x.consistent_point - } (proto_version '1', publication_names '${ publications }')` - ).writable() - - const state = { - lsn: Buffer.concat(x.consistent_point.split('/').map(x => Buffer.from(('00000000' + x).slice(-8), 'hex'))) - } - - stream.on('data', data) - stream.on('error', error) - stream.on('close', sql.close) - - return { stream, state: xs.state } - - function error(e) { - console.error('Unexpected error during logical streaming - reconnecting', e) // eslint-disable-line - } - - function data(x) { - if (x[0] === 0x77) { - parse(x.subarray(25), state, sql.options.parsers, handle, options.transform) - } else if (x[0] === 0x6b && x[17]) { - state.lsn = x.subarray(1, 9) - pong() - } - } - - function handle(a, b) { - const path = b.relation.schema + '.' + b.relation.table - call('*', a, b) - call('*:' + path, a, b) - b.relation.keys.length && call('*:' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) - call(b.command, a, b) - call(b.command + ':' + path, a, b) - b.relation.keys.length && call(b.command + ':' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) - } - - function pong() { - const x = Buffer.alloc(34) - x[0] = 'r'.charCodeAt(0) - x.fill(state.lsn, 1) - x.writeBigInt64BE(BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000), 25) - stream.write(x) - } - } - - function call(x, a, b) { - subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => fn(a, b, x)) - } -} - -function Time(x) { - return new Date(Date.UTC(2000, 0, 1) + Number(x / BigInt(1000))) -} - -function parse(x, state, parsers, handle, transform) { - const char = (acc, [k, v]) => (acc[k.charCodeAt(0)] = v, acc) - - Object.entries({ - R: x => { // Relation - let i = 1 - const r = state[x.readUInt32BE(i)] = { - schema: x.toString('utf8', i += 4, i = x.indexOf(0, i)) || 'pg_catalog', - table: x.toString('utf8', i + 1, i = x.indexOf(0, i + 1)), - columns: Array(x.readUInt16BE(i += 2)), - keys: [] - } - i += 2 - - let columnIndex = 0 - , column - - while (i < x.length) { - column = r.columns[columnIndex++] = { - key: x[i++], - name: transform.column.from - ? transform.column.from(x.toString('utf8', i, i = x.indexOf(0, i))) - : x.toString('utf8', i, i = x.indexOf(0, i)), - type: x.readUInt32BE(i += 1), - parser: parsers[x.readUInt32BE(i)], - atttypmod: x.readUInt32BE(i += 4) - } - - column.key && r.keys.push(column) - i += 4 - } - }, - Y: () => { /* noop */ }, // Type - O: () => { /* noop */ }, // Origin - B: x => { // Begin - state.date = Time(x.readBigInt64BE(9)) - state.lsn = x.subarray(1, 9) - }, - I: x => { // Insert - let i = 1 - const relation = state[x.readUInt32BE(i)] - const { row } = tuples(x, relation.columns, i += 7, transform) - - handle(row, { - command: 'insert', - relation - }) - }, - D: x => { // Delete - let i = 1 - const relation = state[x.readUInt32BE(i)] - i += 4 - const key = x[i] === 75 - handle(key || x[i] === 79 - ? tuples(x, relation.columns, i += 3, transform).row - : null - , { - command: 'delete', - relation, - key - }) - }, - U: x => { // Update - let i = 1 - const relation = state[x.readUInt32BE(i)] - i += 4 - const key = x[i] === 75 - const xs = key || x[i] === 79 - ? tuples(x, relation.columns, i += 3, transform) - : null - - xs && (i = xs.i) - - const { row } = tuples(x, relation.columns, i + 3, transform) - - handle(row, { - command: 'update', - relation, - key, - old: xs && xs.row - }) - }, - T: () => { /* noop */ }, // Truncate, - C: () => { /* noop */ } // Commit - }).reduce(char, {})[x[0]](x) -} - -function tuples(x, columns, xi, transform) { - let type - , column - , value - - const row = transform.raw ? new Array(columns.length) : {} - for (let i = 0; i < columns.length; i++) { - type = x[xi++] - column = columns[i] - value = type === 110 // n - ? null - : type === 117 // u - ? undefined - : column.parser === undefined - ? x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi)) - : column.parser.array === true - ? column.parser(x.toString('utf8', xi + 5, xi += 4 + x.readUInt32BE(xi))) - : column.parser(x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi))) - - transform.raw - ? (row[i] = transform.raw === true - ? value - : transform.value.from ? transform.value.from(value, column) : value) - : (row[column.name] = transform.value.from - ? transform.value.from(value, column) - : value - ) - } - - return { i: xi, row: transform.row.from ? transform.row.from(row) : row } -} - -function parseEvent(x) { - const xs = x.match(/^(\*|insert|update|delete)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || [] - - if (!xs) - throw new Error('Malformed subscribe pattern: ' + x) - - const [, command, path, key] = xs - - return (command || '*') - + (path ? ':' + (path.indexOf('.') === -1 ? 'public.' + path : path) : '') - + (key ? '=' + key : '') -} diff --git a/deno/src/types.js.bak b/deno/src/types.js.bak deleted file mode 100644 index ea0da6a2..00000000 --- a/deno/src/types.js.bak +++ /dev/null @@ -1,368 +0,0 @@ -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' -import { Query } from './query.js' -import { Errors } from './errors.js' - -export const types = { - string: { - to: 25, - from: null, // defaults to string - serialize: x => '' + x - }, - number: { - to: 0, - from: [21, 23, 26, 700, 701], - serialize: x => '' + x, - parse: x => +x - }, - json: { - to: 114, - from: [114, 3802], - serialize: x => JSON.stringify(x), - parse: x => JSON.parse(x) - }, - boolean: { - to: 16, - from: 16, - serialize: x => x === true ? 't' : 'f', - parse: x => x === 't' - }, - date: { - to: 1184, - from: [1082, 1114, 1184], - serialize: x => (x instanceof Date ? x : new Date(x)).toISOString(), - parse: x => new Date(x) - }, - bytea: { - to: 17, - from: 17, - serialize: x => '\\x' + Buffer.from(x).toString('hex'), - parse: x => Buffer.from(x.slice(2), 'hex') - } -} - -class NotTagged { then() { notTagged() } catch() { notTagged() } finally() { notTagged() }} - -export class Identifier extends NotTagged { - constructor(value) { - super() - this.value = escapeIdentifier(value) - } -} - -export class Parameter extends NotTagged { - constructor(value, type, array) { - super() - this.value = value - this.type = type - this.array = array - } -} - -export class Builder extends NotTagged { - constructor(first, rest) { - super() - this.first = first - this.rest = rest - } - - build(before, parameters, types, options) { - const keyword = builders.map(([x, fn]) => ({ fn, i: before.search(x) })).sort((a, b) => a.i - b.i).pop() - return keyword.i === -1 - ? escapeIdentifiers(this.first, options) - : keyword.fn(this.first, this.rest, parameters, types, options) - } -} - -export function handleValue(x, parameters, types, options) { - let value = x instanceof Parameter ? x.value : x - if (value === undefined) { - x instanceof Parameter - ? x.value = options.transform.undefined - : value = x = options.transform.undefined - - if (value === undefined) - throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') - } - - return '$' + (types.push( - x instanceof Parameter - ? (parameters.push(x.value), x.array - ? x.array[x.type || inferType(x.value)] || x.type || firstIsString(x.value) - : x.type - ) - : (parameters.push(x), inferType(x)) - )) -} - -const defaultHandlers = typeHandlers(types) - -export function stringify(q, string, value, parameters, types, options) { // eslint-disable-line - for (let i = 1; i < q.strings.length; i++) { - string += (stringifyValue(string, value, parameters, types, options)) + q.strings[i] - value = q.args[i] - } - - return string -} - -function stringifyValue(string, value, parameters, types, o) { - return ( - value instanceof Builder ? value.build(string, parameters, types, o) : - value instanceof Query ? fragment(value, parameters, types, o) : - value instanceof Identifier ? value.value : - value && value[0] instanceof Query ? value.reduce((acc, x) => acc + ' ' + fragment(x, parameters, types, o), '') : - handleValue(value, parameters, types, o) - ) -} - -function fragment(q, parameters, types, options) { - q.fragment = true - return stringify(q, q.strings[0], q.args[0], parameters, types, options) -} - -function valuesBuilder(first, parameters, types, columns, options) { - return first.map(row => - '(' + columns.map(column => - stringifyValue('values', row[column], parameters, types, options) - ).join(',') + ')' - ).join(',') -} - -function values(first, rest, parameters, types, options) { - const multi = Array.isArray(first[0]) - const columns = rest.length ? rest.flat() : Object.keys(multi ? first[0] : first) - return valuesBuilder(multi ? first : [first], parameters, types, columns, options) -} - -function select(first, rest, parameters, types, options) { - typeof first === 'string' && (first = [first].concat(rest)) - if (Array.isArray(first)) - return escapeIdentifiers(first, options) - - let value - const columns = rest.length ? rest.flat() : Object.keys(first) - return columns.map(x => { - value = first[x] - return ( - value instanceof Query ? fragment(value, parameters, types, options) : - value instanceof Identifier ? value.value : - handleValue(value, parameters, types, options) - ) + ' as ' + escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) - }).join(',') -} - -const builders = Object.entries({ - values, - in: (...xs) => { - const x = values(...xs) - return x === '()' ? '(null)' : x - }, - select, - as: select, - returning: select, - '\\(': select, - - update(first, rest, parameters, types, options) { - return (rest.length ? rest.flat() : Object.keys(first)).map(x => - escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) + - '=' + stringifyValue('values', first[x], parameters, types, options) - ) - }, - - insert(first, rest, parameters, types, options) { - const columns = rest.length ? rest.flat() : Object.keys(Array.isArray(first) ? first[0] : first) - return '(' + escapeIdentifiers(columns, options) + ')values' + - valuesBuilder(Array.isArray(first) ? first : [first], parameters, types, columns, options) - } -}).map(([x, fn]) => ([new RegExp('((?:^|[\\s(])' + x + '(?:$|[\\s(]))(?![\\s\\S]*\\1)', 'i'), fn])) - -function notTagged() { - throw Errors.generic('NOT_TAGGED_CALL', 'Query not called as a tagged template literal') -} - -export const serializers = defaultHandlers.serializers -export const parsers = defaultHandlers.parsers - -export const END = {} - -function firstIsString(x) { - if (Array.isArray(x)) - return firstIsString(x[0]) - return typeof x === 'string' ? 1009 : 0 -} - -export const mergeUserTypes = function(types) { - const user = typeHandlers(types || {}) - return { - serializers: Object.assign({}, serializers, user.serializers), - parsers: Object.assign({}, parsers, user.parsers) - } -} - -function typeHandlers(types) { - return Object.keys(types).reduce((acc, k) => { - types[k].from && [].concat(types[k].from).forEach(x => acc.parsers[x] = types[k].parse) - if (types[k].serialize) { - acc.serializers[types[k].to] = types[k].serialize - types[k].from && [].concat(types[k].from).forEach(x => acc.serializers[x] = types[k].serialize) - } - return acc - }, { parsers: {}, serializers: {} }) -} - -function escapeIdentifiers(xs, { transform: { column } }) { - return xs.map(x => escapeIdentifier(column.to ? column.to(x) : x)).join(',') -} - -export const escapeIdentifier = function escape(str) { - return '"' + str.replace(/"/g, '""').replace(/\./g, '"."') + '"' -} - -export const inferType = function inferType(x) { - return ( - x instanceof Parameter ? x.type : - x instanceof Date ? 1184 : - x instanceof Uint8Array ? 17 : - (x === true || x === false) ? 16 : - typeof x === 'bigint' ? 20 : - Array.isArray(x) ? inferType(x[0]) : - 0 - ) -} - -const escapeBackslash = /\\/g -const escapeQuote = /"/g - -function arrayEscape(x) { - return x - .replace(escapeBackslash, '\\\\') - .replace(escapeQuote, '\\"') -} - -export const arraySerializer = function arraySerializer(xs, serializer, options, typarray) { - if (Array.isArray(xs) === false) - return xs - - if (!xs.length) - return '{}' - - const first = xs[0] - // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter - const delimiter = typarray === 1020 ? ';' : ',' - - if (Array.isArray(first) && !first.type) - return '{' + xs.map(x => arraySerializer(x, serializer, options, typarray)).join(delimiter) + '}' - - return '{' + xs.map(x => { - if (x === undefined) { - x = options.transform.undefined - if (x === undefined) - throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') - } - - return x === null - ? 'null' - : '"' + arrayEscape(serializer ? serializer(x.type ? x.value : x) : '' + x) + '"' - }).join(delimiter) + '}' -} - -const arrayParserState = { - i: 0, - char: null, - str: '', - quoted: false, - last: 0 -} - -export const arrayParser = function arrayParser(x, parser, typarray) { - arrayParserState.i = arrayParserState.last = 0 - return arrayParserLoop(arrayParserState, x, parser, typarray) -} - -function arrayParserLoop(s, x, parser, typarray) { - const xs = [] - // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter - const delimiter = typarray === 1020 ? ';' : ',' - for (; s.i < x.length; s.i++) { - s.char = x[s.i] - if (s.quoted) { - if (s.char === '\\') { - s.str += x[++s.i] - } else if (s.char === '"') { - xs.push(parser ? parser(s.str) : s.str) - s.str = '' - s.quoted = x[s.i + 1] === '"' - s.last = s.i + 2 - } else { - s.str += s.char - } - } else if (s.char === '"') { - s.quoted = true - } else if (s.char === '{') { - s.last = ++s.i - xs.push(arrayParserLoop(s, x, parser, typarray)) - } else if (s.char === '}') { - s.quoted = false - s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) - s.last = s.i + 1 - break - } else if (s.char === delimiter && s.p !== '}' && s.p !== '"') { - xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) - s.last = s.i + 1 - } - s.p = s.char - } - s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i + 1)) : x.slice(s.last, s.i + 1)) - return xs -} - -export const toCamel = x => { - let str = x[0] - for (let i = 1; i < x.length; i++) - str += x[i] === '_' ? x[++i].toUpperCase() : x[i] - return str -} - -export const toPascal = x => { - let str = x[0].toUpperCase() - for (let i = 1; i < x.length; i++) - str += x[i] === '_' ? x[++i].toUpperCase() : x[i] - return str -} - -export const toKebab = x => x.replace(/_/g, '-') - -export const fromCamel = x => x.replace(/([A-Z])/g, '_$1').toLowerCase() -export const fromPascal = x => (x.slice(0, 1) + x.slice(1).replace(/([A-Z])/g, '_$1')).toLowerCase() -export const fromKebab = x => x.replace(/-/g, '_') - -function createJsonTransform(fn) { - return function jsonTransform(x, column) { - return typeof x === 'object' && x !== null && (column.type === 114 || column.type === 3802) - ? Array.isArray(x) - ? x.map(x => jsonTransform(x, column)) - : Object.entries(x).reduce((acc, [k, v]) => Object.assign(acc, { [fn(k)]: jsonTransform(v, column) }), {}) - : x - } -} - -toCamel.column = { from: toCamel } -toCamel.value = { from: createJsonTransform(toCamel) } -fromCamel.column = { to: fromCamel } - -export const camel = { ...toCamel } -camel.column.to = fromCamel - -toPascal.column = { from: toPascal } -toPascal.value = { from: createJsonTransform(toPascal) } -fromPascal.column = { to: fromPascal } - -export const pascal = { ...toPascal } -pascal.column.to = fromPascal - -toKebab.column = { from: toKebab } -toKebab.value = { from: createJsonTransform(toKebab) } -fromKebab.column = { to: fromKebab } - -export const kebab = { ...toKebab } -kebab.column.to = fromKebab diff --git a/deno/tests/bootstrap.js.bak b/deno/tests/bootstrap.js.bak deleted file mode 100644 index da416896..00000000 --- a/deno/tests/bootstrap.js.bak +++ /dev/null @@ -1,34 +0,0 @@ -import { spawn } from 'https://deno.land/std@0.132.0/node/child_process.ts' - -await exec('dropdb', ['postgres_js_test']) - -await exec('psql', ['-c', 'alter system set ssl=on']) -await exec('psql', ['-c', 'drop user postgres_js_test']) -await exec('psql', ['-c', 'create user postgres_js_test']) -await exec('psql', ['-c', 'alter system set password_encryption=md5']) -await exec('psql', ['-c', 'select pg_reload_conf()']) -await exec('psql', ['-c', 'drop user if exists postgres_js_test_md5']) -await exec('psql', ['-c', 'create user postgres_js_test_md5 with password \'postgres_js_test_md5\'']) -await exec('psql', ['-c', 'alter system set password_encryption=\'scram-sha-256\'']) -await exec('psql', ['-c', 'select pg_reload_conf()']) -await exec('psql', ['-c', 'drop user if exists postgres_js_test_scram']) -await exec('psql', ['-c', 'create user postgres_js_test_scram with password \'postgres_js_test_scram\'']) - -await exec('createdb', ['postgres_js_test']) -await exec('psql', ['-c', 'grant all on database postgres_js_test to postgres_js_test']) -await exec('psql', ['-c', 'alter database postgres_js_test owner to postgres_js_test']) - -function ignore(cmd, args) { - const { stderr } = spawnSync(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) - if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) - throw stderr -} - -export async function exec(cmd, args) { // eslint-disable-line - let stderr = '' - const cp = await spawn(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) // eslint-disable-line - cp.stderr.on('data', x => stderr += x) - await new Promise(x => cp.on('exit', x)) - if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) - throw new Error(stderr) -} diff --git a/deno/tests/index.js.bak b/deno/tests/index.js.bak deleted file mode 100644 index adedf1e0..00000000 --- a/deno/tests/index.js.bak +++ /dev/null @@ -1,2620 +0,0 @@ -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' -import process from 'https://deno.land/std@0.132.0/node/process.ts' -import { exec } from './bootstrap.js' - -import { t, nt, ot } from './test.js' // eslint-disable-line -import { net } from '../polyfills.js' -import fs from 'https://deno.land/std@0.132.0/node/fs.ts' -import crypto from 'https://deno.land/std@0.132.0/node/crypto.ts' - -import postgres from '../src/index.js' -const delay = ms => new Promise(r => setTimeout(r, ms)) - -const rel = x => new URL(x, import.meta.url) -const idle_timeout = 1 - -const login = { - user: 'postgres_js_test' -} - -const login_md5 = { - user: 'postgres_js_test_md5', - pass: 'postgres_js_test_md5' -} - -const login_scram = { - user: 'postgres_js_test_scram', - pass: 'postgres_js_test_scram' -} - -const options = { - db: 'postgres_js_test', - user: login.user, - pass: login.pass, - idle_timeout, - connect_timeout: 1, - max: 1 -} - -const sql = postgres(options) - -t('Connects with no options', async() => { - const sql = postgres({ max: 1 }) - - const result = (await sql`select 1 as x`)[0].x - await sql.end() - - return [1, result] -}) - -t('Uses default database without slash', async() => { - const sql = postgres('postgres://localhost') - return [sql.options.user, sql.options.database] -}) - -t('Uses default database with slash', async() => { - const sql = postgres('postgres://localhost/') - return [sql.options.user, sql.options.database] -}) - -t('Result is array', async() => - [true, Array.isArray(await sql`select 1`)] -) - -t('Result has count', async() => - [1, (await sql`select 1`).count] -) - -t('Result has command', async() => - ['SELECT', (await sql`select 1`).command] -) - -t('Create table', async() => - ['CREATE TABLE', (await sql`create table test(int int)`).command, await sql`drop table test`] -) - -t('Drop table', { timeout: 2 }, async() => { - await sql`create table test(int int)` - return ['DROP TABLE', (await sql`drop table test`).command] -}) - -t('null', async() => - [null, (await sql`select ${ null } as x`)[0].x] -) - -t('Integer', async() => - ['1', (await sql`select ${ 1 } as x`)[0].x] -) - -t('String', async() => - ['hello', (await sql`select ${ 'hello' } as x`)[0].x] -) - -t('Boolean false', async() => - [false, (await sql`select ${ false } as x`)[0].x] -) - -t('Boolean true', async() => - [true, (await sql`select ${ true } as x`)[0].x] -) - -t('Date', async() => { - const now = new Date() - return [0, now - (await sql`select ${ now } as x`)[0].x] -}) - -t('Json', async() => { - const x = (await sql`select ${ sql.json({ a: 'hello', b: 42 }) } as x`)[0].x - return ['hello,42', [x.a, x.b].join()] -}) - -t('implicit json', async() => { - const x = (await sql`select ${ { a: 'hello', b: 42 } }::json as x`)[0].x - return ['hello,42', [x.a, x.b].join()] -}) - -t('implicit jsonb', async() => { - const x = (await sql`select ${ { a: 'hello', b: 42 } }::jsonb as x`)[0].x - return ['hello,42', [x.a, x.b].join()] -}) - -t('Empty array', async() => - [true, Array.isArray((await sql`select ${ sql.array([], 1009) } as x`)[0].x)] -) - -t('String array', async() => - ['123', (await sql`select ${ '{1,2,3}' }::int[] as x`)[0].x.join('')] -) - -t('Array of Integer', async() => - ['3', (await sql`select ${ sql.array([1, 2, 3]) } as x`)[0].x[2]] -) - -t('Array of String', async() => - ['c', (await sql`select ${ sql.array(['a', 'b', 'c']) } as x`)[0].x[2]] -) - -t('Array of Date', async() => { - const now = new Date() - return [now.getTime(), (await sql`select ${ sql.array([now, now, now]) } as x`)[0].x[2].getTime()] -}) - -t('Array of Box', async() => [ - '(3,4),(1,2);(6,7),(4,5)', - (await sql`select ${ '{(1,2),(3,4);(4,5),(6,7)}' }::box[] as x`)[0].x.join(';') -]) - -t('Nested array n2', async() => - ['4', (await sql`select ${ sql.array([[1, 2], [3, 4]]) } as x`)[0].x[1][1]] -) - -t('Nested array n3', async() => - ['6', (await sql`select ${ sql.array([[[1, 2]], [[3, 4]], [[5, 6]]]) } as x`)[0].x[2][0][1]] -) - -t('Escape in arrays', async() => - ['Hello "you",c:\\windows', (await sql`select ${ sql.array(['Hello "you"', 'c:\\windows']) } as x`)[0].x.join(',')] -) - -t('Escapes', async() => { - return ['hej"hej', Object.keys((await sql`select 1 as ${ sql('hej"hej') }`)[0])[0]] -}) - -t('null for int', async() => { - await sql`create table test (x int)` - return [1, (await sql`insert into test values(${ null })`).count, await sql`drop table test`] -}) - -t('Throws on illegal transactions', async() => { - const sql = postgres({ ...options, max: 2, fetch_types: false }) - const error = await sql`begin`.catch(e => e) - return [ - error.code, - 'UNSAFE_TRANSACTION' - ] -}) - -t('Transaction throws', async() => { - await sql`create table test (a int)` - return ['22P02', await sql.begin(async sql => { - await sql`insert into test values(1)` - await sql`insert into test values('hej')` - }).catch(x => x.code), await sql`drop table test`] -}) - -t('Transaction rolls back', async() => { - await sql`create table test (a int)` - await sql.begin(async sql => { - await sql`insert into test values(1)` - await sql`insert into test values('hej')` - }).catch(() => { /* ignore */ }) - return [0, (await sql`select a from test`).count, await sql`drop table test`] -}) - -t('Transaction throws on uncaught savepoint', async() => { - await sql`create table test (a int)` - - return ['fail', (await sql.begin(async sql => { - await sql`insert into test values(1)` - await sql.savepoint(async sql => { - await sql`insert into test values(2)` - throw new Error('fail') - }) - }).catch((err) => err.message)), await sql`drop table test`] -}) - -t('Transaction throws on uncaught named savepoint', async() => { - await sql`create table test (a int)` - - return ['fail', (await sql.begin(async sql => { - await sql`insert into test values(1)` - await sql.savepoit('watpoint', async sql => { - await sql`insert into test values(2)` - throw new Error('fail') - }) - }).catch(() => 'fail')), await sql`drop table test`] -}) - -t('Transaction succeeds on caught savepoint', async() => { - await sql`create table test (a int)` - await sql.begin(async sql => { - await sql`insert into test values(1)` - await sql.savepoint(async sql => { - await sql`insert into test values(2)` - throw new Error('please rollback') - }).catch(() => { /* ignore */ }) - await sql`insert into test values(3)` - }) - - return ['2', (await sql`select count(1) from test`)[0].count, await sql`drop table test`] -}) - -t('Savepoint returns Result', async() => { - let result - await sql.begin(async sql => { - result = await sql.savepoint(sql => - sql`select 1 as x` - ) - }) - - return [1, result[0].x] -}) - -t('Prepared transaction', async() => { - await sql`create table test (a int)` - - await sql.begin(async sql => { - await sql`insert into test values(1)` - await sql.prepare('tx1') - }) - - await sql`commit prepared 'tx1'` - - return ['1', (await sql`select count(1) from test`)[0].count, await sql`drop table test`] -}) - -t('Transaction requests are executed implicitly', async() => { - const sql = postgres({ debug: true, idle_timeout: 1, fetch_types: false }) - return [ - 'testing', - (await sql.begin(sql => [ - sql`select set_config('postgres_js.test', 'testing', true)`, - sql`select current_setting('postgres_js.test') as x` - ]))[1][0].x - ] -}) - -t('Uncaught transaction request errors bubbles to transaction', async() => [ - '42703', - (await sql.begin(sql => [ - sql`select wat`, - sql`select current_setting('postgres_js.test') as x, ${ 1 } as a` - ]).catch(e => e.code)) -]) - -t('Fragments in transactions', async() => [ - true, - (await sql.begin(sql => sql`select true as x where ${ sql`1=1` }`))[0].x -]) - -t('Transaction rejects with rethrown error', async() => [ - 'WAT', - await sql.begin(async sql => { - try { - await sql`select exception` - } catch (ex) { - throw new Error('WAT') - } - }).catch(e => e.message) -]) - -t('Parallel transactions', async() => { - await sql`create table test (a int)` - return ['11', (await Promise.all([ - sql.begin(sql => sql`select 1`), - sql.begin(sql => sql`select 1`) - ])).map(x => x.count).join(''), await sql`drop table test`] -}) - -t('Many transactions at beginning of connection', async() => { - const sql = postgres(options) - const xs = await Promise.all(Array.from({ length: 100 }, () => sql.begin(sql => sql`select 1`))) - return [100, xs.length] -}) - -t('Transactions array', async() => { - await sql`create table test (a int)` - - return ['11', (await sql.begin(sql => [ - sql`select 1`.then(x => x), - sql`select 1` - ])).map(x => x.count).join(''), await sql`drop table test`] -}) - -t('Transaction waits', async() => { - await sql`create table test (a int)` - await sql.begin(async sql => { - await sql`insert into test values(1)` - await sql.savepoint(async sql => { - await sql`insert into test values(2)` - throw new Error('please rollback') - }).catch(() => { /* ignore */ }) - await sql`insert into test values(3)` - }) - - return ['11', (await Promise.all([ - sql.begin(sql => sql`select 1`), - sql.begin(sql => sql`select 1`) - ])).map(x => x.count).join(''), await sql`drop table test`] -}) - -t('Helpers in Transaction', async() => { - return ['1', (await sql.begin(async sql => - await sql`select ${ sql({ x: 1 }) }` - ))[0].x] -}) - -t('Undefined values throws', async() => { - let error - - await sql` - select ${ undefined } as x - `.catch(x => error = x.code) - - return ['UNDEFINED_VALUE', error] -}) - -t('Transform undefined', async() => { - const sql = postgres({ ...options, transform: { undefined: null } }) - return [null, (await sql`select ${ undefined } as x`)[0].x] -}) - -t('Transform undefined in array', async() => { - const sql = postgres({ ...options, transform: { undefined: null } }) - return [null, (await sql`select * from (values ${ sql([undefined, undefined]) }) as x(x, y)`)[0].y] -}) - -t('Null sets to null', async() => - [null, (await sql`select ${ null } as x`)[0].x] -) - -t('Throw syntax error', async() => - ['42601', (await sql`wat 1`.catch(x => x)).code] -) - -t('Connect using uri', async() => - [true, await new Promise((resolve, reject) => { - const sql = postgres('postgres://' + login.user + ':' + (login.pass || '') + '@localhost:5432/' + options.db, { - idle_timeout - }) - sql`select 1`.then(() => resolve(true), reject) - })] -) - -t('Options from uri with special characters in user and pass', async() => { - const opt = postgres({ user: 'öla', pass: 'pass^word' }).options - return [[opt.user, opt.pass].toString(), 'öla,pass^word'] -}) - -t('Fail with proper error on no host', async() => - ['ECONNREFUSED', (await new Promise((resolve, reject) => { - const sql = postgres('postgres://localhost:33333/' + options.db, { - idle_timeout - }) - sql`select 1`.then(reject, resolve) - })).code] -) - -t('Connect using SSL', async() => - [true, (await new Promise((resolve, reject) => { - postgres({ - ssl: { rejectUnauthorized: false }, - idle_timeout - })`select 1`.then(() => resolve(true), reject) - }))] -) - -t('Connect using SSL require', async() => - [true, (await new Promise((resolve, reject) => { - postgres({ - ssl: 'require', - idle_timeout - })`select 1`.then(() => resolve(true), reject) - }))] -) - -t('Connect using SSL prefer', async() => { - await exec('psql', ['-c', 'alter system set ssl=off']) - await exec('psql', ['-c', 'select pg_reload_conf()']) - - const sql = postgres({ - ssl: 'prefer', - idle_timeout - }) - - return [ - 1, (await sql`select 1 as x`)[0].x, - await exec('psql', ['-c', 'alter system set ssl=on']), - await exec('psql', ['-c', 'select pg_reload_conf()']) - ] -}) - -t('Reconnect using SSL', { timeout: 2 }, async() => { - const sql = postgres({ - ssl: 'require', - idle_timeout: 0.1 - }) - - await sql`select 1` - await delay(200) - - return [1, (await sql`select 1 as x`)[0].x] -}) - -t('Proper handling of non object Errors', async() => { - const sql = postgres({ socket: () => { throw 'wat' } }) // eslint-disable-line - - return [ - 'wat', await sql`select 1 as x`.catch(e => e.message) - ] -}) - -t('Proper handling of null Errors', async() => { - const sql = postgres({ socket: () => { throw null } }) // eslint-disable-line - - return [ - 'null', await sql`select 1 as x`.catch(e => e.message) - ] -}) - -t('Ensure reserve on connection throws proper error', async() => { - const sql = postgres({ socket: () => { throw 'wat' }, idle_timeout }) // eslint-disable-line - - return [ - 'wat', await sql.reserve().catch(e => e) - ] -}) - -t('Login without password', async() => { - return [true, (await postgres({ ...options, ...login })`select true as x`)[0].x] -}) - -t('Login using MD5', async() => { - return [true, (await postgres({ ...options, ...login_md5 })`select true as x`)[0].x] -}) - -t('Login using scram-sha-256', async() => { - return [true, (await postgres({ ...options, ...login_scram })`select true as x`)[0].x] -}) - -t('Parallel connections using scram-sha-256', { - timeout: 2 -}, async() => { - const sql = postgres({ ...options, ...login_scram }) - return [true, (await Promise.all([ - sql`select true as x, pg_sleep(0.01)`, - sql`select true as x, pg_sleep(0.01)`, - sql`select true as x, pg_sleep(0.01)` - ]))[0][0].x] -}) - -t('Support dynamic password function', async() => { - return [true, (await postgres({ - ...options, - ...login_scram, - pass: () => 'postgres_js_test_scram' - })`select true as x`)[0].x] -}) - -t('Support dynamic async password function', async() => { - return [true, (await postgres({ - ...options, - ...login_scram, - pass: () => Promise.resolve('postgres_js_test_scram') - })`select true as x`)[0].x] -}) - -t('Point type', async() => { - const sql = postgres({ - ...options, - types: { - point: { - to: 600, - from: [600], - serialize: ([x, y]) => '(' + x + ',' + y + ')', - parse: (x) => x.slice(1, -1).split(',').map(x => +x) - } - } - }) - - await sql`create table test (x point)` - await sql`insert into test (x) values (${ sql.types.point([10, 20]) })` - return [20, (await sql`select x from test`)[0].x[1], await sql`drop table test`] -}) - -t('Point type array', async() => { - const sql = postgres({ - ...options, - types: { - point: { - to: 600, - from: [600], - serialize: ([x, y]) => '(' + x + ',' + y + ')', - parse: (x) => x.slice(1, -1).split(',').map(x => +x) - } - } - }) - - await sql`create table test (x point[])` - await sql`insert into test (x) values (${ sql.array([sql.types.point([10, 20]), sql.types.point([20, 30])]) })` - return [30, (await sql`select x from test`)[0].x[1][1], await sql`drop table test`] -}) - -t('sql file', async() => - [1, (await sql.file(rel('select.sql')))[0].x] -) - -t('sql file has forEach', async() => { - let result - await sql - .file(rel('select.sql'), { cache: false }) - .forEach(({ x }) => result = x) - - return [1, result] -}) - -t('sql file throws', async() => - ['ENOENT', (await sql.file(rel('selectomondo.sql')).catch(x => x.code))] -) - -t('sql file cached', async() => { - await sql.file(rel('select.sql')) - await delay(20) - - return [1, (await sql.file(rel('select.sql')))[0].x] -}) - -t('Parameters in file', async() => { - const result = await sql.file( - rel('select-param.sql'), - ['hello'] - ) - return ['hello', result[0].x] -}) - -t('Connection ended promise', async() => { - const sql = postgres(options) - - await sql.end() - - return [undefined, await sql.end()] -}) - -t('Connection ended timeout', async() => { - const sql = postgres(options) - - await sql.end({ timeout: 10 }) - - return [undefined, await sql.end()] -}) - -t('Connection ended error', async() => { - const sql = postgres(options) - await sql.end() - return ['CONNECTION_ENDED', (await sql``.catch(x => x.code))] -}) - -t('Connection end does not cancel query', async() => { - const sql = postgres(options) - - const promise = sql`select 1 as x`.execute() - - await sql.end() - - return [1, (await promise)[0].x] -}) - -t('Connection destroyed', async() => { - const sql = postgres(options) - process.nextTick(() => sql.end({ timeout: 0 })) - return ['CONNECTION_DESTROYED', await sql``.catch(x => x.code)] -}) - -t('Connection destroyed with query before', async() => { - const sql = postgres(options) - , error = sql`select pg_sleep(0.2)`.catch(err => err.code) - - sql.end({ timeout: 0 }) - return ['CONNECTION_DESTROYED', await error] -}) - -t('transform column', async() => { - const sql = postgres({ - ...options, - transform: { column: x => x.split('').reverse().join('') } - }) - - await sql`create table test (hello_world int)` - await sql`insert into test values (1)` - return ['dlrow_olleh', Object.keys((await sql`select * from test`)[0])[0], await sql`drop table test`] -}) - -t('column toPascal', async() => { - const sql = postgres({ - ...options, - transform: { column: postgres.toPascal } - }) - - await sql`create table test (hello_world int)` - await sql`insert into test values (1)` - return ['HelloWorld', Object.keys((await sql`select * from test`)[0])[0], await sql`drop table test`] -}) - -t('column toCamel', async() => { - const sql = postgres({ - ...options, - transform: { column: postgres.toCamel } - }) - - await sql`create table test (hello_world int)` - await sql`insert into test values (1)` - return ['helloWorld', Object.keys((await sql`select * from test`)[0])[0], await sql`drop table test`] -}) - -t('column toKebab', async() => { - const sql = postgres({ - ...options, - transform: { column: postgres.toKebab } - }) - - await sql`create table test (hello_world int)` - await sql`insert into test values (1)` - return ['hello-world', Object.keys((await sql`select * from test`)[0])[0], await sql`drop table test`] -}) - -t('Transform nested json in arrays', async() => { - const sql = postgres({ - ...options, - transform: postgres.camel - }) - return ['aBcD', (await sql`select '[{"a_b":1},{"c_d":2}]'::jsonb as x`)[0].x.map(Object.keys).join('')] -}) - -t('Transform deeply nested json object in arrays', async() => { - const sql = postgres({ - ...options, - transform: postgres.camel - }) - return [ - 'childObj_deeplyNestedObj_grandchildObj', - (await sql` - select '[{"nested_obj": {"child_obj": 2, "deeply_nested_obj": {"grandchild_obj": 3}}}]'::jsonb as x - `)[0].x.map(x => { - let result - for (const key in x) - result = [...Object.keys(x[key]), ...Object.keys(x[key].deeplyNestedObj)] - return result - })[0] - .join('_') - ] -}) - -t('Transform deeply nested json array in arrays', async() => { - const sql = postgres({ - ...options, - transform: postgres.camel - }) - return [ - 'childArray_deeplyNestedArray_grandchildArray', - (await sql` - select '[{"nested_array": [{"child_array": 2, "deeply_nested_array": [{"grandchild_array":3}]}]}]'::jsonb AS x - `)[0].x.map((x) => { - let result - for (const key in x) - result = [...Object.keys(x[key][0]), ...Object.keys(x[key][0].deeplyNestedArray[0])] - return result - })[0] - .join('_') - ] -}) - -t('Bypass transform for json primitive', async() => { - const sql = postgres({ - ...options, - transform: postgres.camel - }) - - const x = ( - await sql`select 'null'::json as a, 'false'::json as b, '"a"'::json as c, '1'::json as d` - )[0] - - return [ - JSON.stringify({ a: null, b: false, c: 'a', d: 1 }), - JSON.stringify(x) - ] -}) - -t('Bypass transform for jsonb primitive', async() => { - const sql = postgres({ - ...options, - transform: postgres.camel - }) - - const x = ( - await sql`select 'null'::jsonb as a, 'false'::jsonb as b, '"a"'::jsonb as c, '1'::jsonb as d` - )[0] - - return [ - JSON.stringify({ a: null, b: false, c: 'a', d: 1 }), - JSON.stringify(x) - ] -}) - -t('unsafe', async() => { - await sql`create table test (x int)` - return [1, (await sql.unsafe('insert into test values ($1) returning *', [1]))[0].x, await sql`drop table test`] -}) - -t('unsafe simple', async() => { - return [1, (await sql.unsafe('select 1 as x'))[0].x] -}) - -t('unsafe simple includes columns', async() => { - return ['x', (await sql.unsafe('select 1 as x').values()).columns[0].name] -}) - -t('unsafe describe', async() => { - const q = 'insert into test values (1)' - await sql`create table test(a int unique)` - await sql.unsafe(q).describe() - const x = await sql.unsafe(q).describe() - return [ - q, - x.string, - await sql`drop table test` - ] -}) - -t('simple query using unsafe with multiple statements', async() => { - return [ - '1,2', - (await sql.unsafe('select 1 as x;select 2 as x')).map(x => x[0].x).join() - ] -}) - -t('simple query using simple() with multiple statements', async() => { - return [ - '1,2', - (await sql`select 1 as x;select 2 as x`.simple()).map(x => x[0].x).join() - ] -}) - -t('listen and notify', async() => { - const sql = postgres(options) - const channel = 'hello' - const result = await new Promise(async r => { - await sql.listen(channel, r) - sql.notify(channel, 'works') - }) - - return [ - 'works', - result, - sql.end() - ] -}) - -t('double listen', async() => { - const sql = postgres(options) - , channel = 'hello' - - let count = 0 - - await new Promise((resolve, reject) => - sql.listen(channel, resolve) - .then(() => sql.notify(channel, 'world')) - .catch(reject) - ).then(() => count++) - - await new Promise((resolve, reject) => - sql.listen(channel, resolve) - .then(() => sql.notify(channel, 'world')) - .catch(reject) - ).then(() => count++) - - // for coverage - sql.listen('weee', () => { /* noop */ }).then(sql.end) - - return [2, count] -}) - -t('multiple listeners work after a reconnect', async() => { - const sql = postgres(options) - , xs = [] - - const s1 = await sql.listen('test', x => xs.push('1', x)) - await sql.listen('test', x => xs.push('2', x)) - await sql.notify('test', 'a') - await delay(50) - await sql`select pg_terminate_backend(${ s1.state.pid })` - await delay(200) - await sql.notify('test', 'b') - await delay(50) - sql.end() - - return ['1a2a1b2b', xs.join('')] -}) - -t('listen and notify with weird name', async() => { - const sql = postgres(options) - const channel = 'wat-;.ø.§' - const result = await new Promise(async r => { - const { unlisten } = await sql.listen(channel, r) - sql.notify(channel, 'works') - await delay(50) - await unlisten() - }) - - return [ - 'works', - result, - sql.end() - ] -}) - -t('listen and notify with upper case', async() => { - const sql = postgres(options) - const channel = 'withUpperChar' - const result = await new Promise(async r => { - await sql.listen(channel, r) - sql.notify(channel, 'works') - }) - - return [ - 'works', - result, - sql.end() - ] -}) - -t('listen reconnects', { timeout: 2 }, async() => { - const sql = postgres(options) - , resolvers = {} - , a = new Promise(r => resolvers.a = r) - , b = new Promise(r => resolvers.b = r) - - let connects = 0 - - const { state: { pid } } = await sql.listen( - 'test', - x => x in resolvers && resolvers[x](), - () => connects++ - ) - await sql.notify('test', 'a') - await a - await sql`select pg_terminate_backend(${ pid })` - await delay(100) - await sql.notify('test', 'b') - await b - sql.end() - return [connects, 2] -}) - -t('listen result reports correct connection state after reconnection', async() => { - const sql = postgres(options) - , xs = [] - - const result = await sql.listen('test', x => xs.push(x)) - const initialPid = result.state.pid - await sql.notify('test', 'a') - await sql`select pg_terminate_backend(${ initialPid })` - await delay(50) - sql.end() - - return [result.state.pid !== initialPid, true] -}) - -t('unlisten removes subscription', async() => { - const sql = postgres(options) - , xs = [] - - const { unlisten } = await sql.listen('test', x => xs.push(x)) - await sql.notify('test', 'a') - await delay(50) - await unlisten() - await sql.notify('test', 'b') - await delay(50) - sql.end() - - return ['a', xs.join('')] -}) - -t('listen after unlisten', async() => { - const sql = postgres(options) - , xs = [] - - const { unlisten } = await sql.listen('test', x => xs.push(x)) - await sql.notify('test', 'a') - await delay(50) - await unlisten() - await sql.notify('test', 'b') - await delay(50) - await sql.listen('test', x => xs.push(x)) - await sql.notify('test', 'c') - await delay(50) - sql.end() - - return ['ac', xs.join('')] -}) - -t('multiple listeners and unlisten one', async() => { - const sql = postgres(options) - , xs = [] - - await sql.listen('test', x => xs.push('1', x)) - const s2 = await sql.listen('test', x => xs.push('2', x)) - await sql.notify('test', 'a') - await delay(50) - await s2.unlisten() - await sql.notify('test', 'b') - await delay(50) - sql.end() - - return ['1a2a1b', xs.join('')] -}) - -t('responds with server parameters (application_name)', async() => - ['postgres.js', await new Promise((resolve, reject) => postgres({ - ...options, - onparameter: (k, v) => k === 'application_name' && resolve(v) - })`select 1`.catch(reject))] -) - -t('has server parameters', async() => { - return ['postgres.js', (await sql`select 1`.then(() => sql.parameters.application_name))] -}) - -t('big query body', { timeout: 2 }, async() => { - await sql`create table test (x int)` - return [50000, (await sql`insert into test ${ - sql([...Array(50000).keys()].map(x => ({ x }))) - }`).count, await sql`drop table test`] -}) - -t('Throws if more than 65534 parameters', async() => { - await sql`create table test (x int)` - return ['MAX_PARAMETERS_EXCEEDED', (await sql`insert into test ${ - sql([...Array(65535).keys()].map(x => ({ x }))) - }`.catch(e => e.code)), await sql`drop table test`] -}) - -t('let postgres do implicit cast of unknown types', async() => { - await sql`create table test (x timestamp with time zone)` - const [{ x }] = await sql`insert into test values (${ new Date().toISOString() }) returning *` - return [true, x instanceof Date, await sql`drop table test`] -}) - -t('only allows one statement', async() => - ['42601', await sql`select 1; select 2`.catch(e => e.code)] -) - -t('await sql() throws not tagged error', async() => { - let error - try { - await sql('select 1') - } catch (e) { - error = e.code - } - return ['NOT_TAGGED_CALL', error] -}) - -t('sql().then throws not tagged error', async() => { - let error - try { - sql('select 1').then(() => { /* noop */ }) - } catch (e) { - error = e.code - } - return ['NOT_TAGGED_CALL', error] -}) - -t('sql().catch throws not tagged error', async() => { - let error - try { - await sql('select 1') - } catch (e) { - error = e.code - } - return ['NOT_TAGGED_CALL', error] -}) - -t('sql().finally throws not tagged error', async() => { - let error - try { - sql('select 1').finally(() => { /* noop */ }) - } catch (e) { - error = e.code - } - return ['NOT_TAGGED_CALL', error] -}) - -t('little bobby tables', async() => { - const name = 'Robert\'); DROP TABLE students;--' - - await sql`create table students (name text, age int)` - await sql`insert into students (name) values (${ name })` - - return [ - name, (await sql`select name from students`)[0].name, - await sql`drop table students` - ] -}) - -t('Connection errors are caught using begin()', { - timeout: 2 -}, async() => { - let error - try { - const sql = postgres({ host: 'localhost', port: 1 }) - - await sql.begin(async(sql) => { - await sql`insert into test (label, value) values (${1}, ${2})` - }) - } catch (err) { - error = err - } - - return [ - true, - error.code === 'ECONNREFUSED' || - error.message === 'Connection refused (os error 61)' - ] -}) - -t('dynamic table name', async() => { - await sql`create table test(a int)` - return [ - 0, (await sql`select * from ${ sql('test') }`).count, - await sql`drop table test` - ] -}) - -t('dynamic schema name', async() => { - await sql`create table test(a int)` - return [ - 0, (await sql`select * from ${ sql('public') }.test`).count, - await sql`drop table test` - ] -}) - -t('dynamic schema and table name', async() => { - await sql`create table test(a int)` - return [ - 0, (await sql`select * from ${ sql('public.test') }`).count, - await sql`drop table test` - ] -}) - -t('dynamic column name', async() => { - return ['!not_valid', Object.keys((await sql`select 1 as ${ sql('!not_valid') }`)[0])[0]] -}) - -t('dynamic select as', async() => { - return ['2', (await sql`select ${ sql({ a: 1, b: 2 }) }`)[0].b] -}) - -t('dynamic select as pluck', async() => { - return [undefined, (await sql`select ${ sql({ a: 1, b: 2 }, 'a') }`)[0].b] -}) - -t('dynamic insert', async() => { - await sql`create table test (a int, b text)` - const x = { a: 42, b: 'the answer' } - - return ['the answer', (await sql`insert into test ${ sql(x) } returning *`)[0].b, await sql`drop table test`] -}) - -t('dynamic insert pluck', async() => { - await sql`create table test (a int, b text)` - const x = { a: 42, b: 'the answer' } - - return [null, (await sql`insert into test ${ sql(x, 'a') } returning *`)[0].b, await sql`drop table test`] -}) - -t('dynamic in with empty array', async() => { - await sql`create table test (a int)` - await sql`insert into test values (1)` - return [ - (await sql`select * from test where null in ${ sql([]) }`).count, - 0, - await sql`drop table test` - ] -}) - -t('dynamic in after insert', async() => { - await sql`create table test (a int, b text)` - const [{ x }] = await sql` - with x as ( - insert into test values (1, 'hej') - returning * - ) - select 1 in ${ sql([1, 2, 3]) } as x from x - ` - return [ - true, x, - await sql`drop table test` - ] -}) - -t('array insert', async() => { - await sql`create table test (a int, b int)` - return [2, (await sql`insert into test (a, b) values ${ sql([1, 2]) } returning *`)[0].b, await sql`drop table test`] -}) - -t('where parameters in()', async() => { - await sql`create table test (x text)` - await sql`insert into test values ('a')` - return [ - (await sql`select * from test where x in ${ sql(['a', 'b', 'c']) }`)[0].x, - 'a', - await sql`drop table test` - ] -}) - -t('where parameters in() values before', async() => { - return [2, (await sql` - with rows as ( - select * from (values (1), (2), (3), (4)) as x(a) - ) - select * from rows where a in ${ sql([3, 4]) } - `).count] -}) - -t('dynamic multi row insert', async() => { - await sql`create table test (a int, b text)` - const x = { a: 42, b: 'the answer' } - - return [ - 'the answer', - (await sql`insert into test ${ sql([x, x]) } returning *`)[1].b, await sql`drop table test` - ] -}) - -t('dynamic update', async() => { - await sql`create table test (a int, b text)` - await sql`insert into test (a, b) values (17, 'wrong')` - - return [ - 'the answer', - (await sql`update test set ${ sql({ a: 42, b: 'the answer' }) } returning *`)[0].b, await sql`drop table test` - ] -}) - -t('dynamic update pluck', async() => { - await sql`create table test (a int, b text)` - await sql`insert into test (a, b) values (17, 'wrong')` - - return [ - 'wrong', - (await sql`update test set ${ sql({ a: 42, b: 'the answer' }, 'a') } returning *`)[0].b, await sql`drop table test` - ] -}) - -t('dynamic select array', async() => { - await sql`create table test (a int, b text)` - await sql`insert into test (a, b) values (42, 'yay')` - return ['yay', (await sql`select ${ sql(['a', 'b']) } from test`)[0].b, await sql`drop table test`] -}) - -t('dynamic returning array', async() => { - await sql`create table test (a int, b text)` - return [ - 'yay', - (await sql`insert into test (a, b) values (42, 'yay') returning ${ sql(['a', 'b']) }`)[0].b, - await sql`drop table test` - ] -}) - -t('dynamic select args', async() => { - await sql`create table test (a int, b text)` - await sql`insert into test (a, b) values (42, 'yay')` - return ['yay', (await sql`select ${ sql('a', 'b') } from test`)[0].b, await sql`drop table test`] -}) - -t('dynamic values single row', async() => { - const [{ b }] = await sql` - select * from (values ${ sql(['a', 'b', 'c']) }) as x(a, b, c) - ` - - return ['b', b] -}) - -t('dynamic values multi row', async() => { - const [, { b }] = await sql` - select * from (values ${ sql([['a', 'b', 'c'], ['a', 'b', 'c']]) }) as x(a, b, c) - ` - - return ['b', b] -}) - -t('connection parameters', async() => { - const sql = postgres({ - ...options, - connection: { - 'some.var': 'yay' - } - }) - - return ['yay', (await sql`select current_setting('some.var') as x`)[0].x] -}) - -t('Multiple queries', async() => { - const sql = postgres(options) - - return [4, (await Promise.all([ - sql`select 1`, - sql`select 2`, - sql`select 3`, - sql`select 4` - ])).length] -}) - -t('Multiple statements', async() => - [2, await sql.unsafe(` - select 1 as x; - select 2 as a; - `).then(([, [x]]) => x.a)] -) - -t('throws correct error when authentication fails', async() => { - const sql = postgres({ - ...options, - ...login_md5, - pass: 'wrong' - }) - return ['28P01', await sql`select 1`.catch(e => e.code)] -}) - -t('notice', async() => { - let notice - const log = console.log // eslint-disable-line - console.log = function(x) { // eslint-disable-line - notice = x - } - - const sql = postgres(options) - - await sql`create table if not exists users()` - await sql`create table if not exists users()` - - console.log = log // eslint-disable-line - - return ['NOTICE', notice.severity] -}) - -t('notice hook', async() => { - let notice - const sql = postgres({ - ...options, - onnotice: x => notice = x - }) - - await sql`create table if not exists users()` - await sql`create table if not exists users()` - - return ['NOTICE', notice.severity] -}) - -t('bytea serializes and parses', async() => { - const buf = Buffer.from('wat') - - await sql`create table test (x bytea)` - await sql`insert into test values (${ buf })` - - return [ - buf.toString(), - (await sql`select x from test`)[0].x.toString(), - await sql`drop table test` - ] -}) - -t('forEach', async() => { - let result - await sql`select 1 as x`.forEach(({ x }) => result = x) - return [1, result] -}) - -t('forEach returns empty array', async() => { - return [0, (await sql`select 1 as x`.forEach(() => { /* noop */ })).length] -}) - -t('Cursor', async() => { - const order = [] - await sql`select 1 as x union select 2 as x`.cursor(async([x]) => { - order.push(x.x + 'a') - await delay(100) - order.push(x.x + 'b') - }) - return ['1a1b2a2b', order.join('')] -}) - -t('Unsafe cursor', async() => { - const order = [] - await sql.unsafe('select 1 as x union select 2 as x').cursor(async([x]) => { - order.push(x.x + 'a') - await delay(100) - order.push(x.x + 'b') - }) - return ['1a1b2a2b', order.join('')] -}) - -t('Cursor custom n', async() => { - const order = [] - await sql`select * from generate_series(1,20)`.cursor(10, async(x) => { - order.push(x.length) - }) - return ['10,10', order.join(',')] -}) - -t('Cursor custom with rest n', async() => { - const order = [] - await sql`select * from generate_series(1,20)`.cursor(11, async(x) => { - order.push(x.length) - }) - return ['11,9', order.join(',')] -}) - -t('Cursor custom with less results than batch size', async() => { - const order = [] - await sql`select * from generate_series(1,20)`.cursor(21, async(x) => { - order.push(x.length) - }) - return ['20', order.join(',')] -}) - -t('Cursor cancel', async() => { - let result - await sql`select * from generate_series(1,10) as x`.cursor(async([{ x }]) => { - result = x - return sql.CLOSE - }) - return [1, result] -}) - -t('Cursor throw', async() => { - const order = [] - await sql`select 1 as x union select 2 as x`.cursor(async([x]) => { - order.push(x.x + 'a') - await delay(100) - throw new Error('watty') - }).catch(() => order.push('err')) - return ['1aerr', order.join('')] -}) - -t('Cursor error', async() => [ - '42601', - await sql`wat`.cursor(() => { /* noop */ }).catch((err) => err.code) -]) - -t('Multiple Cursors', { timeout: 2 }, async() => { - const result = [] - await sql.begin(async sql => [ - await sql`select 1 as cursor, x from generate_series(1,4) as x`.cursor(async([row]) => { - result.push(row.x) - await new Promise(r => setTimeout(r, 20)) - }), - await sql`select 2 as cursor, x from generate_series(101,104) as x`.cursor(async([row]) => { - result.push(row.x) - await new Promise(r => setTimeout(r, 10)) - }) - ]) - - return ['1,2,3,4,101,102,103,104', result.join(',')] -}) - -t('Cursor as async iterator', async() => { - const order = [] - for await (const [x] of sql`select generate_series(1,2) as x;`.cursor()) { - order.push(x.x + 'a') - await delay(10) - order.push(x.x + 'b') - } - - return ['1a1b2a2b', order.join('')] -}) - -t('Cursor as async iterator with break', async() => { - const order = [] - for await (const xs of sql`select generate_series(1,2) as x;`.cursor()) { - order.push(xs[0].x + 'a') - await delay(10) - order.push(xs[0].x + 'b') - break - } - - return ['1a1b', order.join('')] -}) - -t('Async Iterator Unsafe cursor', async() => { - const order = [] - for await (const [x] of sql.unsafe('select 1 as x union select 2 as x').cursor()) { - order.push(x.x + 'a') - await delay(10) - order.push(x.x + 'b') - } - return ['1a1b2a2b', order.join('')] -}) - -t('Async Iterator Cursor custom n', async() => { - const order = [] - for await (const x of sql`select * from generate_series(1,20)`.cursor(10)) - order.push(x.length) - - return ['10,10', order.join(',')] -}) - -t('Async Iterator Cursor custom with rest n', async() => { - const order = [] - for await (const x of sql`select * from generate_series(1,20)`.cursor(11)) - order.push(x.length) - - return ['11,9', order.join(',')] -}) - -t('Async Iterator Cursor custom with less results than batch size', async() => { - const order = [] - for await (const x of sql`select * from generate_series(1,20)`.cursor(21)) - order.push(x.length) - return ['20', order.join(',')] -}) - -t('Transform row', async() => { - const sql = postgres({ - ...options, - transform: { row: () => 1 } - }) - - return [1, (await sql`select 'wat'`)[0]] -}) - -t('Transform row forEach', async() => { - let result - const sql = postgres({ - ...options, - transform: { row: () => 1 } - }) - - await sql`select 1`.forEach(x => result = x) - - return [1, result] -}) - -t('Transform value', async() => { - const sql = postgres({ - ...options, - transform: { value: () => 1 } - }) - - return [1, (await sql`select 'wat' as x`)[0].x] -}) - -t('Transform columns from', async() => { - const sql = postgres({ - ...options, - transform: postgres.fromCamel - }) - await sql`create table test (a_test int, b_test text)` - await sql`insert into test ${ sql([{ aTest: 1, bTest: 1 }]) }` - await sql`update test set ${ sql({ aTest: 2, bTest: 2 }) }` - return [ - 2, - (await sql`select ${ sql('aTest', 'bTest') } from test`)[0].a_test, - await sql`drop table test` - ] -}) - -t('Transform columns to', async() => { - const sql = postgres({ - ...options, - transform: postgres.toCamel - }) - await sql`create table test (a_test int, b_test text)` - await sql`insert into test ${ sql([{ a_test: 1, b_test: 1 }]) }` - await sql`update test set ${ sql({ a_test: 2, b_test: 2 }) }` - return [ - 2, - (await sql`select a_test, b_test from test`)[0].aTest, - await sql`drop table test` - ] -}) - -t('Transform columns from and to', async() => { - const sql = postgres({ - ...options, - transform: postgres.camel - }) - await sql`create table test (a_test int, b_test text)` - await sql`insert into test ${ sql([{ aTest: 1, bTest: 1 }]) }` - await sql`update test set ${ sql({ aTest: 2, bTest: 2 }) }` - return [ - 2, - (await sql`select ${ sql('aTest', 'bTest') } from test`)[0].aTest, - await sql`drop table test` - ] -}) - -t('Transform columns from and to (legacy)', async() => { - const sql = postgres({ - ...options, - transform: { - column: { - to: postgres.fromCamel, - from: postgres.toCamel - } - } - }) - await sql`create table test (a_test int, b_test text)` - await sql`insert into test ${ sql([{ aTest: 1, bTest: 1 }]) }` - await sql`update test set ${ sql({ aTest: 2, bTest: 2 }) }` - return [ - 2, - (await sql`select ${ sql('aTest', 'bTest') } from test`)[0].aTest, - await sql`drop table test` - ] -}) - -t('Unix socket', async() => { - const sql = postgres({ - ...options, - host: process.env.PGSOCKET || '/tmp' // eslint-disable-line - }) - - return [1, (await sql`select 1 as x`)[0].x] -}) - -t('Big result', async() => { - return [100000, (await sql`select * from generate_series(1, 100000)`).count] -}) - -t('Debug', async() => { - let result - const sql = postgres({ - ...options, - debug: (connection_id, str) => result = str - }) - - await sql`select 1` - - return ['select 1', result] -}) - -t('bigint is returned as String', async() => [ - 'string', - typeof (await sql`select 9223372036854777 as x`)[0].x -]) - -t('int is returned as Number', async() => [ - 'number', - typeof (await sql`select 123 as x`)[0].x -]) - -t('numeric is returned as string', async() => [ - 'string', - typeof (await sql`select 1.2 as x`)[0].x -]) - -t('Async stack trace', async() => { - const sql = postgres({ ...options, debug: false }) - return [ - parseInt(new Error().stack.split('\n')[1].match(':([0-9]+):')[1]) + 1, - parseInt(await sql`error`.catch(x => x.stack.split('\n').pop().match(':([0-9]+):')[1])) - ] -}) - -t('Debug has long async stack trace', async() => { - const sql = postgres({ ...options, debug: true }) - - return [ - 'watyo', - await yo().catch(x => x.stack.match(/wat|yo/g).join('')) - ] - - function yo() { - return wat() - } - - function wat() { - return sql`error` - } -}) - -t('Error contains query string', async() => [ - 'selec 1', - (await sql`selec 1`.catch(err => err.query)) -]) - -t('Error contains query serialized parameters', async() => [ - 1, - (await sql`selec ${ 1 }`.catch(err => err.parameters[0])) -]) - -t('Error contains query raw parameters', async() => [ - 1, - (await sql`selec ${ 1 }`.catch(err => err.args[0])) -]) - -t('Query and parameters on errorare not enumerable if debug is not set', async() => { - const sql = postgres({ ...options, debug: false }) - - return [ - false, - (await sql`selec ${ 1 }`.catch(err => err.propertyIsEnumerable('parameters') || err.propertyIsEnumerable('query'))) - ] -}) - -t('Query and parameters are enumerable if debug is set', async() => { - const sql = postgres({ ...options, debug: true }) - - return [ - true, - (await sql`selec ${ 1 }`.catch(err => err.propertyIsEnumerable('parameters') && err.propertyIsEnumerable('query'))) - ] -}) - -t('connect_timeout', { timeout: 20 }, async() => { - const connect_timeout = 0.2 - const server = net.createServer() - server.listen() - const sql = postgres({ port: server.address().port, host: '127.0.0.1', connect_timeout }) - const start = Date.now() - let end - await sql`select 1`.catch((e) => { - if (e.code !== 'CONNECT_TIMEOUT') - throw e - end = Date.now() - }) - server.close() - return [connect_timeout, Math.floor((end - start) / 100) / 10] -}) - -t('connect_timeout throws proper error', async() => [ - 'CONNECT_TIMEOUT', - await postgres({ - ...options, - ...login_scram, - connect_timeout: 0.001 - })`select 1`.catch(e => e.code) -]) - -t('connect_timeout error message includes host:port', { timeout: 20 }, async() => { - const connect_timeout = 0.2 - const server = net.createServer() - server.listen() - const sql = postgres({ port: server.address().port, host: '127.0.0.1', connect_timeout }) - const port = server.address().port - let err - await sql`select 1`.catch((e) => { - if (e.code !== 'CONNECT_TIMEOUT') - throw e - err = e.message - }) - server.close() - return [['write CONNECT_TIMEOUT 127.0.0.1:', port].join(''), err] -}) - -t('requests works after single connect_timeout', async() => { - let first = true - - const sql = postgres({ - ...options, - ...login_scram, - connect_timeout: { valueOf() { return first ? (first = false, 0.0001) : 1 } } - }) - - return [ - 'CONNECT_TIMEOUT,,1', - [ - await sql`select 1 as x`.then(() => 'success', x => x.code), - await delay(10), - (await sql`select 1 as x`)[0].x - ].join(',') - ] -}) - -t('Postgres errors are of type PostgresError', async() => - [true, (await sql`bad keyword`.catch(e => e)) instanceof sql.PostgresError] -) - -t('Result has columns spec', async() => - ['x', (await sql`select 1 as x`).columns[0].name] -) - -t('forEach has result as second argument', async() => { - let x - await sql`select 1 as x`.forEach((_, result) => x = result) - return ['x', x.columns[0].name] -}) - -t('Result as arrays', async() => { - const sql = postgres({ - ...options, - transform: { - row: x => Object.values(x) - } - }) - - return ['1,2', (await sql`select 1 as a, 2 as b`)[0].join(',')] -}) - -t('Insert empty array', async() => { - await sql`create table tester (ints int[])` - return [ - Array.isArray((await sql`insert into tester (ints) values (${ sql.array([]) }) returning *`)[0].ints), - true, - await sql`drop table tester` - ] -}) - -t('Insert array in sql()', async() => { - await sql`create table tester (ints int[])` - return [ - Array.isArray((await sql`insert into tester ${ sql({ ints: sql.array([]) }) } returning *`)[0].ints), - true, - await sql`drop table tester` - ] -}) - -t('Automatically creates prepared statements', async() => { - const sql = postgres(options) - const result = await sql`select * from pg_prepared_statements` - return [true, result.some(x => x.name = result.statement.name)] -}) - -t('no_prepare: true disables prepared statements (deprecated)', async() => { - const sql = postgres({ ...options, no_prepare: true }) - const result = await sql`select * from pg_prepared_statements` - return [false, result.some(x => x.name = result.statement.name)] -}) - -t('prepare: false disables prepared statements', async() => { - const sql = postgres({ ...options, prepare: false }) - const result = await sql`select * from pg_prepared_statements` - return [false, result.some(x => x.name = result.statement.name)] -}) - -t('prepare: true enables prepared statements', async() => { - const sql = postgres({ ...options, prepare: true }) - const result = await sql`select * from pg_prepared_statements` - return [true, result.some(x => x.name = result.statement.name)] -}) - -t('prepares unsafe query when "prepare" option is true', async() => { - const sql = postgres({ ...options, prepare: true }) - const result = await sql.unsafe('select * from pg_prepared_statements where name <> $1', ['bla'], { prepare: true }) - return [true, result.some(x => x.name = result.statement.name)] -}) - -t('does not prepare unsafe query by default', async() => { - const sql = postgres({ ...options, prepare: true }) - const result = await sql.unsafe('select * from pg_prepared_statements where name <> $1', ['bla']) - return [false, result.some(x => x.name = result.statement.name)] -}) - -t('Recreate prepared statements on transformAssignedExpr error', { timeout: 1 }, async() => { - const insert = () => sql`insert into test (name) values (${ '1' }) returning name` - await sql`create table test (name text)` - await insert() - await sql`alter table test alter column name type int using name::integer` - return [ - 1, - (await insert())[0].name, - await sql`drop table test` - ] -}) - -t('Throws correct error when retrying in transactions', async() => { - await sql`create table test(x int)` - const error = await sql.begin(sql => sql`insert into test (x) values (${ false })`).catch(e => e) - return [ - error.code, - '42804', - sql`drop table test` - ] -}) - -t('Recreate prepared statements on RevalidateCachedQuery error', async() => { - const select = () => sql`select name from test` - await sql`create table test (name text)` - await sql`insert into test values ('1')` - await select() - await sql`alter table test alter column name type int using name::integer` - return [ - 1, - (await select())[0].name, - await sql`drop table test` - ] -}) - -t('Properly throws routine error on not prepared statements', async() => { - await sql`create table x (x text[])` - const { routine } = await sql.unsafe(` - insert into x(x) values (('a', 'b')) - `).catch(e => e) - - return ['transformAssignedExpr', routine, await sql`drop table x`] -}) - -t('Properly throws routine error on not prepared statements in transaction', async() => { - const { routine } = await sql.begin(sql => [ - sql`create table x (x text[])`, - sql`insert into x(x) values (('a', 'b'))` - ]).catch(e => e) - - return ['transformAssignedExpr', routine] -}) - -t('Properly throws routine error on not prepared statements using file', async() => { - const { routine } = await sql.unsafe(` - create table x (x text[]); - insert into x(x) values (('a', 'b')); - `, { prepare: true }).catch(e => e) - - return ['transformAssignedExpr', routine] -}) - -t('Catches connection config errors', async() => { - const sql = postgres({ ...options, user: { toString: () => { throw new Error('wat') } }, database: 'prut' }) - - return [ - 'wat', - await sql`select 1`.catch((e) => e.message) - ] -}) - -t('Catches connection config errors with end', async() => { - const sql = postgres({ ...options, user: { toString: () => { throw new Error('wat') } }, database: 'prut' }) - - return [ - 'wat', - await sql`select 1`.catch((e) => e.message), - await sql.end() - ] -}) - -t('Catches query format errors', async() => [ - 'wat', - await sql.unsafe({ toString: () => { throw new Error('wat') } }).catch((e) => e.message) -]) - -t('Multiple hosts', { - timeout: 1 -}, async() => { - const s1 = postgres({ idle_timeout }) - , s2 = postgres({ idle_timeout, port: 5433 }) - , sql = postgres('postgres://localhost:5432,localhost:5433', { idle_timeout, max: 1 }) - , result = [] - - const id1 = (await s1`select system_identifier as x from pg_control_system()`)[0].x - const id2 = (await s2`select system_identifier as x from pg_control_system()`)[0].x - - const x1 = await sql`select 1` - result.push((await sql`select system_identifier as x from pg_control_system()`)[0].x) - await s1`select pg_terminate_backend(${ x1.state.pid }::int)` - await delay(50) - - const x2 = await sql`select 1` - result.push((await sql`select system_identifier as x from pg_control_system()`)[0].x) - await s2`select pg_terminate_backend(${ x2.state.pid }::int)` - await delay(50) - - result.push((await sql`select system_identifier as x from pg_control_system()`)[0].x) - - return [[id1, id2, id1].join(','), result.join(',')] -}) - -t('Escaping supports schemas and tables', async() => { - await sql`create schema a` - await sql`create table a.b (c int)` - await sql`insert into a.b (c) values (1)` - return [ - 1, - (await sql`select ${ sql('a.b.c') } from a.b`)[0].c, - await sql`drop table a.b`, - await sql`drop schema a` - ] -}) - -t('Raw method returns rows as arrays', async() => { - const [x] = await sql`select 1`.raw() - return [ - Array.isArray(x), - true - ] -}) - -t('Raw method returns values unparsed as Buffer', async() => { - const [[x]] = await sql`select 1`.raw() - return [ - x instanceof Uint8Array, - true - ] -}) - -t('Array returns rows as arrays of columns', async() => { - return [(await sql`select 1`.values())[0][0], 1] -}) - -t('Copy read', async() => { - const result = [] - - await sql`create table test (x int)` - await sql`insert into test select * from generate_series(1,10)` - const readable = await sql`copy test to stdout`.readable() - readable.on('data', x => result.push(x)) - await new Promise(r => readable.on('end', r)) - - return [ - result.length, - 10, - await sql`drop table test` - ] -}) - -t('Copy write', { timeout: 2 }, async() => { - await sql`create table test (x int)` - const writable = await sql`copy test from stdin`.writable() - - writable.write('1\n') - writable.write('1\n') - writable.end() - - await new Promise(r => writable.on('finish', r)) - - return [ - (await sql`select 1 from test`).length, - 2, - await sql`drop table test` - ] -}) - -t('Copy write as first', async() => { - await sql`create table test (x int)` - const first = postgres(options) - const writable = await first`COPY test FROM STDIN WITH(FORMAT csv, HEADER false, DELIMITER ',')`.writable() - writable.write('1\n') - writable.write('1\n') - writable.end() - - await new Promise(r => writable.on('finish', r)) - - return [ - (await sql`select 1 from test`).length, - 2, - await sql`drop table test` - ] -}) - -t('Copy from file', async() => { - await sql`create table test (x int, y int, z int)` - await new Promise(async r => fs - .createReadStream(rel('copy.csv')) - .pipe(await sql`copy test from stdin`.writable()) - .on('finish', r) - ) - - return [ - JSON.stringify(await sql`select * from test`), - '[{"x":1,"y":2,"z":3},{"x":4,"y":5,"z":6}]', - await sql`drop table test` - ] -}) - -t('Copy from works in transaction', async() => { - await sql`create table test(x int)` - const xs = await sql.begin(async sql => { - (await sql`copy test from stdin`.writable()).end('1\n2') - await delay(20) - return sql`select 1 from test` - }) - - return [ - xs.length, - 2, - await sql`drop table test` - ] -}) - -t('Copy from abort', async() => { - const sql = postgres(options) - const readable = fs.createReadStream(rel('copy.csv')) - - await sql`create table test (x int, y int, z int)` - await sql`TRUNCATE TABLE test` - - const writable = await sql`COPY test FROM STDIN`.writable() - - let aborted - - readable - .pipe(writable) - .on('error', (err) => aborted = err) - - writable.destroy(new Error('abort')) - await sql.end() - - return [ - 'abort', - aborted.message, - await postgres(options)`drop table test` - ] -}) - -t('multiple queries before connect', async() => { - const sql = postgres({ ...options, max: 2 }) - const xs = await Promise.all([ - sql`select 1 as x`, - sql`select 2 as x`, - sql`select 3 as x`, - sql`select 4 as x` - ]) - - return [ - '1,2,3,4', - xs.map(x => x[0].x).join() - ] -}) - -t('subscribe', { timeout: 2 }, async() => { - const sql = postgres({ - database: 'postgres_js_test', - publications: 'alltables' - }) - - await sql.unsafe('create publication alltables for all tables') - - const result = [] - - const { unsubscribe } = await sql.subscribe('*', (row, { command, old }) => { - result.push(command, row.name, row.id, old && old.name, old && old.id) - }) - - await sql` - create table test ( - id serial primary key, - name text - ) - ` - - await sql`alter table test replica identity default` - await sql`insert into test (name) values ('Murray')` - await sql`update test set name = 'Rothbard'` - await sql`update test set id = 2` - await sql`delete from test` - await sql`alter table test replica identity full` - await sql`insert into test (name) values ('Murray')` - await sql`update test set name = 'Rothbard'` - await sql`delete from test` - await delay(10) - await unsubscribe() - await sql`insert into test (name) values ('Oh noes')` - await delay(10) - return [ - 'insert,Murray,1,,,update,Rothbard,1,,,update,Rothbard,2,,1,delete,,2,,,insert,Murray,2,,,update,Rothbard,2,Murray,2,delete,Rothbard,2,,', // eslint-disable-line - result.join(','), - await sql`drop table test`, - await sql`drop publication alltables`, - await sql.end() - ] -}) - -t('subscribe with transform', { timeout: 2 }, async() => { - const sql = postgres({ - transform: { - column: { - from: postgres.toCamel, - to: postgres.fromCamel - } - }, - database: 'postgres_js_test', - publications: 'alltables' - }) - - await sql.unsafe('create publication alltables for all tables') - - const result = [] - - const { unsubscribe } = await sql.subscribe('*', (row, { command, old }) => - result.push(command, row.nameInCamel || row.id, old && old.nameInCamel) - ) - - await sql` - create table test ( - id serial primary key, - name_in_camel text - ) - ` - - await sql`insert into test (name_in_camel) values ('Murray')` - await sql`update test set name_in_camel = 'Rothbard'` - await sql`delete from test` - await sql`alter table test replica identity full` - await sql`insert into test (name_in_camel) values ('Murray')` - await sql`update test set name_in_camel = 'Rothbard'` - await sql`delete from test` - await delay(10) - await unsubscribe() - await sql`insert into test (name_in_camel) values ('Oh noes')` - await delay(10) - return [ - 'insert,Murray,,update,Rothbard,,delete,1,,insert,Murray,,update,Rothbard,Murray,delete,Rothbard,', - result.join(','), - await sql`drop table test`, - await sql`drop publication alltables`, - await sql.end() - ] -}) - -t('subscribe reconnects and calls onsubscribe', { timeout: 4 }, async() => { - const sql = postgres({ - database: 'postgres_js_test', - publications: 'alltables', - fetch_types: false - }) - - await sql.unsafe('create publication alltables for all tables') - - const result = [] - let onsubscribes = 0 - - const { unsubscribe, sql: subscribeSql } = await sql.subscribe( - '*', - (row, { command, old }) => result.push(command, row.name || row.id, old && old.name), - () => onsubscribes++ - ) - - await sql` - create table test ( - id serial primary key, - name text - ) - ` - - await sql`insert into test (name) values ('Murray')` - await delay(10) - await subscribeSql.close() - await delay(500) - await sql`delete from test` - await delay(100) - await unsubscribe() - return [ - '2insert,Murray,,delete,1,', - onsubscribes + result.join(','), - await sql`drop table test`, - await sql`drop publication alltables`, - await sql.end() - ] -}) - -t('Execute', async() => { - const result = await new Promise((resolve) => { - const sql = postgres({ ...options, fetch_types: false, debug:(id, query) => resolve(query) }) - sql`select 1`.execute() - }) - - return [result, 'select 1'] -}) - -t('Cancel running query', async() => { - const query = sql`select pg_sleep(2)` - setTimeout(() => query.cancel(), 500) - const error = await query.catch(x => x) - return ['57014', error.code] -}) - -t('Cancel piped query', { timeout: 5 }, async() => { - await sql`select 1` - const last = sql`select pg_sleep(1)`.execute() - const query = sql`select pg_sleep(2) as dig` - setTimeout(() => query.cancel(), 500) - const error = await query.catch(x => x) - await last - return ['57014', error.code] -}) - -t('Cancel queued query', async() => { - const query = sql`select pg_sleep(2) as nej` - const tx = sql.begin(sql => ( - query.cancel(), - sql`select pg_sleep(0.5) as hej, 'hejsa'` - )) - const error = await query.catch(x => x) - await tx - return ['57014', error.code] -}) - -t('Fragments', async() => [ - 1, - (await sql` - ${ sql`select` } 1 as x - `)[0].x -]) - -t('Result becomes array', async() => [ - true, - (await sql`select 1`).slice() instanceof Array -]) - -t('Describe', async() => { - const type = (await sql`select ${ 1 }::int as x`.describe()).types[0] - return [23, type] -}) - -t('Describe a statement', async() => { - await sql`create table tester (name text, age int)` - const r = await sql`select name, age from tester where name like $1 and age > $2`.describe() - return [ - '25,23/name:25,age:23', - `${ r.types.join(',') }/${ r.columns.map(c => `${c.name}:${c.type}`).join(',') }`, - await sql`drop table tester` - ] -}) - -t('Include table oid and column number in column details', async() => { - await sql`create table tester (name text, age int)` - const r = await sql`select name, age from tester where name like $1 and age > $2`.describe() - const [{ oid }] = await sql`select oid from pg_class where relname = 'tester'` - - return [ - `table:${oid},number:1|table:${oid},number:2`, - `${ r.columns.map(c => `table:${c.table},number:${c.number}`).join('|') }`, - await sql`drop table tester` - ] -}) - -t('Describe a statement without parameters', async() => { - await sql`create table tester (name text, age int)` - const r = await sql`select name, age from tester`.describe() - return [ - '0,2', - `${ r.types.length },${ r.columns.length }`, - await sql`drop table tester` - ] -}) - -t('Describe a statement without columns', async() => { - await sql`create table tester (name text, age int)` - const r = await sql`insert into tester (name, age) values ($1, $2)`.describe() - return [ - '2,0', - `${ r.types.length },${ r.columns.length }`, - await sql`drop table tester` - ] -}) - -t('Large object', async() => { - const file = rel('index.js') - , md5 = crypto.createHash('md5').update(fs.readFileSync(file)).digest('hex') - - const lo = await sql.largeObject() - await new Promise(async r => fs.createReadStream(file).pipe(await lo.writable()).on('finish', r)) - await lo.seek(0) - - const out = crypto.createHash('md5') - await new Promise(r => lo.readable().then(x => x.on('data', x => out.update(x)).on('end', r))) - - return [ - md5, - out.digest('hex'), - await lo.close() - ] -}) - -t('Catches type serialize errors', async() => { - const sql = postgres({ - idle_timeout, - types: { - text: { - from: 25, - to: 25, - parse: x => x, - serialize: () => { throw new Error('watSerialize') } - } - } - }) - - return [ - 'watSerialize', - (await sql`select ${ 'wat' }`.catch(e => e.message)) - ] -}) - -t('Catches type parse errors', async() => { - const sql = postgres({ - idle_timeout, - types: { - text: { - from: 25, - to: 25, - parse: () => { throw new Error('watParse') }, - serialize: x => x - } - } - }) - - return [ - 'watParse', - (await sql`select 'wat'`.catch(e => e.message)) - ] -}) - -t('Catches type serialize errors in transactions', async() => { - const sql = postgres({ - idle_timeout, - types: { - text: { - from: 25, - to: 25, - parse: x => x, - serialize: () => { throw new Error('watSerialize') } - } - } - }) - - return [ - 'watSerialize', - (await sql.begin(sql => ( - sql`select 1`, - sql`select ${ 'wat' }` - )).catch(e => e.message)) - ] -}) - -t('Catches type parse errors in transactions', async() => { - const sql = postgres({ - idle_timeout, - types: { - text: { - from: 25, - to: 25, - parse: () => { throw new Error('watParse') }, - serialize: x => x - } - } - }) - - return [ - 'watParse', - (await sql.begin(sql => ( - sql`select 1`, - sql`select 'wat'` - )).catch(e => e.message)) - ] -}) - -t('Prevent premature end of connection in transaction', async() => { - const sql = postgres({ max_lifetime: 0.01, idle_timeout }) - const result = await sql.begin(async sql => { - await sql`select 1` - await delay(20) - await sql`select 1` - return 'yay' - }) - - - return [ - 'yay', - result - ] -}) - -t('Ensure reconnect after max_lifetime with transactions', { timeout: 5 }, async() => { - const sql = postgres({ - max_lifetime: 0.01, - idle_timeout, - max: 1 - }) - - let x = 0 - while (x++ < 10) await sql.begin(sql => sql`select 1 as x`) - - return [true, true] -}) - - -t('Ensure transactions throw if connection is closed dwhile there is no query', async() => { - const sql = postgres(options) - const x = await sql.begin(async() => { - setTimeout(() => sql.end({ timeout: 0 }), 10) - await new Promise(r => setTimeout(r, 200)) - return sql`select 1` - }).catch(x => x) - return ['CONNECTION_CLOSED', x.code] -}) - -t('Custom socket', {}, async() => { - let result - const sql = postgres({ - socket: () => new Promise((resolve, reject) => { - const socket = new net.Socket() - socket.connect(5432) - socket.once('data', x => result = x[0]) - socket.on('error', reject) - socket.on('connect', () => resolve(socket)) - }), - idle_timeout - }) - - await sql`select 1` - - return [ - result, - 82 - ] -}) - -t('Ensure drain only dequeues if ready', async() => { - const sql = postgres(options) - - const res = await Promise.all([ - sql.unsafe('SELECT 0+$1 --' + '.'.repeat(100000), [1]), - sql.unsafe('SELECT 0+$1+$2+$3', [1, 2, 3]) - ]) - - return [res.length, 2] -}) - -t('Supports fragments as dynamic parameters', async() => { - await sql`create table test (a int, b bool)` - await sql`insert into test values(1, true)` - await sql`insert into test ${ - sql({ - a: 2, - b: sql`exists(select 1 from test where b = ${ true })` - }) - }` - - return [ - '1,t2,t', - (await sql`select * from test`.raw()).join(''), - await sql`drop table test` - ] -}) - -t('Supports nested fragments with parameters', async() => { - await sql`create table test ${ - sql`(${ sql('a') } ${ sql`int` })` - }` - await sql`insert into test values(1)` - return [ - 1, - (await sql`select a from test`)[0].a, - await sql`drop table test` - ] -}) - -t('Supports multiple nested fragments with parameters', async() => { - const [{ b }] = await sql`select * ${ - sql`from ${ - sql`(values (2, ${ 1 }::int)) as x(${ sql(['a', 'b']) })` - }` - }` - return [ - 1, - b - ] -}) - -t('Supports arrays of fragments', async() => { - const [{ x }] = await sql` - ${ [sql`select`, sql`1`, sql`as`, sql`x`] } - ` - - return [ - 1, - x - ] -}) - -t('Does not try rollback when commit errors', async() => { - let notice = null - const sql = postgres({ ...options, onnotice: x => notice = x }) - await sql`create table test(x int constraint test_constraint unique deferrable initially deferred)` - - await sql.begin('isolation level serializable', async sql => { - await sql`insert into test values(1)` - await sql`insert into test values(1)` - }).catch(e => e) - - return [ - notice, - null, - await sql`drop table test` - ] -}) - -t('Last keyword used even with duplicate keywords', async() => { - await sql`create table test (x int)` - await sql`insert into test values(1)` - const [{ x }] = await sql` - select - 1 in (1) as x - from test - where x in ${ sql([1, 2]) } - ` - - return [x, true, await sql`drop table test`] -}) - -t('Insert array with null', async() => { - await sql`create table test (x int[])` - await sql`insert into test ${ sql({ x: [1, null, 3] }) }` - return [ - 1, - (await sql`select x from test`)[0].x[0], - await sql`drop table test` - ] -}) - -t('Insert array with undefined throws', async() => { - await sql`create table test (x int[])` - return [ - 'UNDEFINED_VALUE', - await sql`insert into test ${ sql({ x: [1, undefined, 3] }) }`.catch(e => e.code), - await sql`drop table test` - ] -}) - -t('Insert array with undefined transform', async() => { - const sql = postgres({ ...options, transform: { undefined: null } }) - await sql`create table test (x int[])` - await sql`insert into test ${ sql({ x: [1, undefined, 3] }) }` - return [ - 1, - (await sql`select x from test`)[0].x[0], - await sql`drop table test` - ] -}) - -t('concurrent cursors', async() => { - const xs = [] - - await Promise.all([...Array(7)].map((x, i) => [ - sql`select ${ i }::int as a, generate_series(1, 2) as x`.cursor(([x]) => xs.push(x.a + x.x)) - ]).flat()) - - return ['12233445566778', xs.join('')] -}) - -t('concurrent cursors multiple connections', async() => { - const sql = postgres({ ...options, max: 2 }) - const xs = [] - - await Promise.all([...Array(7)].map((x, i) => [ - sql`select ${ i }::int as a, generate_series(1, 2) as x`.cursor(([x]) => xs.push(x.a + x.x)) - ]).flat()) - - return ['12233445566778', xs.sort().join('')] -}) - -t('reserve connection', async() => { - const reserved = await sql.reserve() - - setTimeout(() => reserved.release(), 510) - - const xs = await Promise.all([ - reserved`select 1 as x`.then(([{ x }]) => ({ time: Date.now(), x })), - sql`select 2 as x`.then(([{ x }]) => ({ time: Date.now(), x })), - reserved`select 3 as x`.then(([{ x }]) => ({ time: Date.now(), x })) - ]) - - if (xs[1].time - xs[2].time < 500) - throw new Error('Wrong time') - - return [ - '123', - xs.map(x => x.x).join('') - ] -}) - -t('arrays in reserved connection', async() => { - const reserved = await sql.reserve() - const [{ x }] = await reserved`select array[1, 2, 3] as x` - reserved.release() - - return [ - '123', - x.join('') - ] -}) - -t('Ensure reserve on query throws proper error', async() => { - const sql = postgres({ idle_timeout }) // eslint-disable-line - const reserved = await sql.reserve() - const [{ x }] = await reserved`select 'wat' as x` - - return [ - 'wat', x, reserved.release() - ] -}) - -;globalThis.addEventListener("unload", () => Deno.exit(process.exitCode)) \ No newline at end of file diff --git a/deno/tests/test.js.bak b/deno/tests/test.js.bak deleted file mode 100644 index f61a253f..00000000 --- a/deno/tests/test.js.bak +++ /dev/null @@ -1,88 +0,0 @@ -import process from 'https://deno.land/std@0.132.0/node/process.ts' -/* eslint no-console: 0 */ - -import util from 'https://deno.land/std@0.132.0/node/util.ts' - -let done = 0 -let only = false -let ignored = 0 -let failed = false -let promise = Promise.resolve() -const tests = {} - , ignore = {} - -export const nt = () => ignored++ -export const ot = (...rest) => (only = true, test(true, ...rest)) -export const t = (...rest) => test(false, ...rest) -t.timeout = 5 - -async function test(o, name, options, fn) { - typeof options !== 'object' && (fn = options, options = {}) - const line = new Error().stack.split('\n')[3].match(':([0-9]+):')[1] - - await 1 - - if (only && !o) - return - - tests[line] = { fn, line, name } - promise = promise.then(() => Promise.race([ - new Promise((resolve, reject) => - fn.timer = setTimeout(() => reject('Timed out'), (options.timeout || t.timeout) * 1000) - ), - failed - ? (ignored++, ignore) - : fn() - ])) - .then(async x => { - clearTimeout(fn.timer) - if (x === ignore) - return - - if (!Array.isArray(x)) - throw new Error('Test should return result array') - - const [expected, got] = await Promise.all(x) - if (expected !== got) { - failed = true - throw new Error(util.inspect(expected) + ' != ' + util.inspect(got)) - } - - tests[line].succeeded = true - process.stdout.write('✅') - }) - .catch(err => { - tests[line].failed = failed = true - tests[line].error = err instanceof Error ? err : new Error(util.inspect(err)) - }) - .then(() => { - ++done === Object.keys(tests).length && exit() - }) -} - -function exit() { - let success = true - Object.values(tests).every((x) => { - if (x.succeeded) - return true - - success = false - x.cleanup - ? console.error('⛔️', x.name + ' at line', x.line, 'cleanup failed', '\n', util.inspect(x.cleanup)) - : console.error('⛔️', x.name + ' at line', x.line, x.failed - ? 'failed' - : 'never finished', x.error ? '\n' + util.inspect(x.error) : '' - ) - }) - - only - ? console.error('⚠️', 'Not all tests were run') - : ignored - ? console.error('⚠️', ignored, 'ignored test' + (ignored === 1 ? '' : 's', '\n')) - : success - ? console.log('🎉') - : console.error('⚠️', 'Not good') - - !process.exitCode && (!success || only || ignored) && (process.exitCode = 1) -} - diff --git a/deno/types/index.d.ts.bak b/deno/types/index.d.ts.bak deleted file mode 100644 index 44a07af0..00000000 --- a/deno/types/index.d.ts.bak +++ /dev/null @@ -1,732 +0,0 @@ -import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' -import process from 'https://deno.land/std@0.132.0/node/process.ts' -import { Readable, Writable } from 'https://deno.land/std@0.132.0/node/stream.ts' - -/** - * Establish a connection to a PostgreSQL server. - * @param options Connection options - default to the same as psql - * @returns An utility function to make queries to the server - */ -declare function postgres = {}>(options?: postgres.Options | undefined): postgres.Sql extends T ? {} : { [type in keyof T]: T[type] extends { - serialize: (value: infer R) => any, - parse: (raw: any) => infer R -} ? R : never }> -/** - * Establish a connection to a PostgreSQL server. - * @param url Connection string used for authentication - * @param options Connection options - default to the same as psql - * @returns An utility function to make queries to the server - */ -declare function postgres = {}>(url: string, options?: postgres.Options | undefined): postgres.Sql extends T ? {} : { [type in keyof T]: T[type] extends { - serialize: (value: infer R) => any, - parse: (raw: any) => infer R -} ? R : never }> - -/** - * Connection options of Postgres. - */ -interface BaseOptions> { - /** Postgres ip address[s] or domain name[s] */ - host: string | string[] | undefined; - /** Postgres server[s] port[s] */ - port: number | number[] | undefined; - /** unix socket path (usually '/tmp') */ - path: string | undefined; - /** - * Name of database to connect to - * @default process.env['PGDATABASE'] || options.user - */ - database: string; - /** - * Username of database user - * @default process.env['PGUSERNAME'] || process.env['PGUSER'] || require('os').userInfo().username - */ - user: string; - /** - * How to deal with ssl (can be a tls.connect option object) - * @default false - */ - ssl: 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object; - /** - * Max number of connections - * @default 10 - */ - max: number; - /** - * Idle connection timeout in seconds - * @default process.env['PGIDLE_TIMEOUT'] - */ - idle_timeout: number | undefined; - /** - * Connect timeout in seconds - * @default process.env['PGCONNECT_TIMEOUT'] - */ - connect_timeout: number; - /** Array of custom types; see more in the README */ - types: T; - /** - * Enables prepare mode. - * @default true - */ - prepare: boolean; - /** - * Called when a notice is received - * @default console.log - */ - onnotice: (notice: postgres.Notice) => void; - /** (key; value) when a server param change */ - onparameter: (key: string, value: any) => void; - /** Is called with (connection; query; parameters) */ - debug: boolean | ((connection: number, query: string, parameters: any[], paramTypes: any[]) => void); - /** Transform hooks */ - transform: { - /** Transforms outcoming undefined values */ - undefined?: any - - /** Transforms incoming and outgoing column names */ - column?: ((column: string) => string) | { - /** Transform function for column names in result rows */ - from?: ((column: string) => string) | undefined; - /** Transform function for column names in interpolated values passed to tagged template literal */ - to?: ((column: string) => string) | undefined; - } | undefined; - /** Transforms incoming and outgoing row values */ - value?: ((value: any) => any) | { - /** Transform function for values in result rows */ - from?: ((value: unknown, column: postgres.Column) => any) | undefined; - // to?: ((value: unknown) => any) | undefined; // unused - } | undefined; - /** Transforms entire rows */ - row?: ((row: postgres.Row) => any) | { - /** Transform function for entire result rows */ - from?: ((row: postgres.Row) => any) | undefined; - // to?: ((row: postgres.Row) => any) | undefined; // unused - } | undefined; - }; - /** Connection parameters */ - connection: Partial; - /** - * Use 'read-write' with multiple hosts to ensure only connecting to primary - * @default process.env['PGTARGETSESSIONATTRS'] - */ - target_session_attrs: undefined | 'read-write' | 'read-only' | 'primary' | 'standby' | 'prefer-standby'; - /** - * Automatically fetches types on connect - * @default true - */ - fetch_types: boolean; - /** - * Publications to subscribe to (only relevant when calling `sql.subscribe()`) - * @default 'alltables' - */ - publications: string - onclose: (connId: number) => void; - backoff: boolean | ((attemptNum: number) => number); - max_lifetime: number | null; - keep_alive: number | null; -} - - -declare const PRIVATE: unique symbol; - -declare class NotAPromise { - private [PRIVATE]: never; // prevent user-side interface implementation - - /** - * @deprecated This object isn't an SQL query, and therefore not a Promise; use the tagged template string syntax instead: ```await sql\`...\`;``` - * @throws NOT_TAGGED_CALL - */ - private then(): never; - /** - * @deprecated This object isn't an SQL query, and therefore not a Promise; use the tagged template string syntax instead: ```await sql\`...\`;``` - * @throws NOT_TAGGED_CALL - */ - private catch(): never; - /** - * @deprecated This object isn't an SQL query, and therefore not a Promise; use the tagged template string syntax instead: ```await sql\`...\`;``` - * @throws NOT_TAGGED_CALL - */ - private finally(): never; -} - -type UnwrapPromiseArray = T extends any[] ? { - [k in keyof T]: T[k] extends Promise ? R : T[k] -} : T; - -type Keys = string - -type SerializableObject = - number extends K['length'] ? {} : - Partial<(Record | undefined> & Record)> - -type First = - // Tagged template string call - T extends TemplateStringsArray ? TemplateStringsArray : - // Identifiers helper - T extends string ? string : - // Dynamic values helper (depth 2) - T extends readonly any[][] ? readonly postgres.EscapableArray[] : - // Insert/update helper (depth 2) - T extends readonly (object & infer R)[] ? (R extends postgres.SerializableParameter ? readonly postgres.SerializableParameter[] : readonly SerializableObject[]) : - // Dynamic values/ANY helper (depth 1) - T extends readonly any[] ? (readonly postgres.SerializableParameter[]) : - // Insert/update helper (depth 1) - T extends object ? SerializableObject : - // Unexpected type - never - -type Rest = - T extends TemplateStringsArray ? never : // force fallback to the tagged template function overload - T extends string ? readonly string[] : - T extends readonly any[][] ? readonly [] : - T extends readonly (object & infer R)[] ? ( - readonly (Keys & keyof R)[] // sql(data, "prop", "prop2") syntax - | - [readonly (Keys & keyof R)[]] // sql(data, ["prop", "prop2"]) syntax - ) : - T extends readonly any[] ? readonly [] : - T extends object ? ( - readonly (Keys & keyof T)[] // sql(data, "prop", "prop2") syntax - | - [readonly (Keys & keyof T)[]] // sql(data, ["prop", "prop2"]) syntax - ) : - any - -type Return = - [T] extends [TemplateStringsArray] ? - [unknown] extends [T] ? postgres.Helper : // ensure no `PendingQuery` with `any` types - [TemplateStringsArray] extends [T] ? postgres.PendingQuery : - postgres.Helper : - postgres.Helper - -declare namespace postgres { - class PostgresError extends Error { - name: 'PostgresError'; - severity_local: string; - severity: string; - code: string; - position: string; - file: string; - line: string; - routine: string; - - detail?: string | undefined; - hint?: string | undefined; - internal_position?: string | undefined; - internal_query?: string | undefined; - where?: string | undefined; - schema_name?: string | undefined; - table_name?: string | undefined; - column_name?: string | undefined; - data?: string | undefined; - type_name?: string | undefined; - constraint_name?: string | undefined; - - /** Only set when debug is enabled */ - query: string; - /** Only set when debug is enabled */ - parameters: any[]; - } - - /** - * Convert a snake_case string to PascalCase. - * @param str The string from snake_case to convert - * @returns The new string in PascalCase - */ - function toPascal(str: string): string; - namespace toPascal { - namespace column { function from(str: string): string; } - namespace value { function from(str: unknown, column: Column): string } - } - /** - * Convert a PascalCase string to snake_case. - * @param str The string from snake_case to convert - * @returns The new string in snake_case - */ - function fromPascal(str: string): string; - namespace fromPascal { - namespace column { function to(str: string): string } - } - /** - * Convert snake_case to and from PascalCase. - */ - namespace pascal { - namespace column { - function from(str: string): string; - function to(str: string): string; - } - namespace value { function from(str: unknown, column: Column): string } - } - /** - * Convert a snake_case string to camelCase. - * @param str The string from snake_case to convert - * @returns The new string in camelCase - */ - function toCamel(str: string): string; - namespace toCamel { - namespace column { function from(str: string): string; } - namespace value { function from(str: unknown, column: Column): string } - } - /** - * Convert a camelCase string to snake_case. - * @param str The string from snake_case to convert - * @returns The new string in snake_case - */ - function fromCamel(str: string): string; - namespace fromCamel { - namespace column { function to(str: string): string } - } - /** - * Convert snake_case to and from camelCase. - */ - namespace camel { - namespace column { - function from(str: string): string; - function to(str: string): string; - } - namespace value { function from(str: unknown, column: Column): string } - } - /** - * Convert a snake_case string to kebab-case. - * @param str The string from snake_case to convert - * @returns The new string in kebab-case - */ - function toKebab(str: string): string; - namespace toKebab { - namespace column { function from(str: string): string; } - namespace value { function from(str: unknown, column: Column): string } - } - /** - * Convert a kebab-case string to snake_case. - * @param str The string from snake_case to convert - * @returns The new string in snake_case - */ - function fromKebab(str: string): string; - namespace fromKebab { - namespace column { function to(str: string): string } - } - /** - * Convert snake_case to and from kebab-case. - */ - namespace kebab { - namespace column { - function from(str: string): string; - function to(str: string): string; - } - namespace value { function from(str: unknown, column: Column): string } - } - - const BigInt: PostgresType; - - interface PostgresType { - to: number; - from: number[]; - serialize: (value: T) => unknown; - parse: (raw: any) => T; - } - - interface ConnectionParameters { - /** - * Default application_name - * @default 'postgres.js' - */ - application_name: string; - default_transaction_isolation: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable', - default_transaction_read_only: boolean, - default_transaction_deferrable: boolean, - statement_timeout: number, - lock_timeout: number, - idle_in_transaction_session_timeout: number, - idle_session_timeout: number, - DateStyle: string, - IntervalStyle: string, - TimeZone: string, - /** Other connection parameters */ - [name: string]: string | number | boolean; - } - - interface Options> extends Partial> { - /** @inheritdoc */ - host?: string | undefined; - /** @inheritdoc */ - port?: number | undefined; - /** @inheritdoc */ - path?: string | undefined; - /** Password of database user (an alias for `password`) */ - pass?: Options['password'] | undefined; - /** - * Password of database user - * @default process.env['PGPASSWORD'] - */ - password?: string | (() => string | Promise) | undefined; - /** Name of database to connect to (an alias for `database`) */ - db?: Options['database'] | undefined; - /** Username of database user (an alias for `user`) */ - username?: Options['user'] | undefined; - /** Postgres ip address or domain name (an alias for `host`) */ - hostname?: Options['host'] | undefined; - /** - * Disable prepared mode - * @deprecated use "prepare" option instead - */ - no_prepare?: boolean | undefined; - /** - * Idle connection timeout in seconds - * @deprecated use "idle_timeout" option instead - */ - timeout?: Options['idle_timeout'] | undefined; - } - - interface ParsedOptions = {}> extends BaseOptions<{ [name in keyof T]: PostgresType }> { - /** @inheritdoc */ - host: string[]; - /** @inheritdoc */ - port: number[]; - /** @inheritdoc */ - pass: null; - /** @inheritdoc */ - transform: Transform; - serializers: Record unknown>; - parsers: Record unknown>; - } - - interface Transform { - /** Transforms outcoming undefined values */ - undefined: any - - column: { - /** Transform function for column names in result rows */ - from: ((column: string) => string) | undefined; - /** Transform function for column names in interpolated values passed to tagged template literal */ - to: ((column: string) => string) | undefined; - }; - value: { - /** Transform function for values in result rows */ - from: ((value: any, column?: Column) => any) | undefined; - /** Transform function for interpolated values passed to tagged template literal */ - to: undefined; // (value: any) => any - }; - row: { - /** Transform function for entire result rows */ - from: ((row: postgres.Row) => any) | undefined; - to: undefined; // (row: postgres.Row) => any - }; - } - - interface Notice { - [field: string]: string; - } - - interface Parameter extends NotAPromise { - /** - * PostgreSQL OID of the type - */ - type: number; - /** - * Serialized value - */ - value: string | null; - /** - * Raw value to serialize - */ - raw: T | null; - } - - interface ArrayParameter extends Parameter { - array: true; - } - - interface ConnectionError extends globalThis.Error { - code: - | 'CONNECTION_DESTROYED' - | 'CONNECT_TIMEOUT' - | 'CONNECTION_CLOSED' - | 'CONNECTION_ENDED'; - errno: this['code']; - address: string; - port?: number | undefined; - } - - interface NotSupportedError extends globalThis.Error { - code: 'MESSAGE_NOT_SUPPORTED'; - name: string; - } - - interface GenericError extends globalThis.Error { - code: - | '57014' // canceling statement due to user request - | 'NOT_TAGGED_CALL' - | 'UNDEFINED_VALUE' - | 'MAX_PARAMETERS_EXCEEDED' - | 'SASL_SIGNATURE_MISMATCH' - | 'UNSAFE_TRANSACTION'; - message: string; - } - - interface AuthNotImplementedError extends globalThis.Error { - code: 'AUTH_TYPE_NOT_IMPLEMENTED'; - type: number | string; - message: string; - } - - type Error = never - | PostgresError - | ConnectionError - | NotSupportedError - | GenericError - | AuthNotImplementedError; - - interface ColumnInfo { - key: number; - name: string; - type: number; - parser?(raw: string): unknown; - atttypmod: number; - } - - interface RelationInfo { - schema: string; - table: string; - columns: ColumnInfo[]; - keys: ColumnInfo[]; - } - - type ReplicationEvent = - | { command: 'insert', relation: RelationInfo } - | { command: 'delete', relation: RelationInfo, key: boolean } - | { command: 'update', relation: RelationInfo, key: boolean, old: Row | null }; - - interface SubscriptionHandle { - unsubscribe(): void; - } - - interface LargeObject { - writable(options?: { - highWaterMark?: number | undefined, - start?: number | undefined - } | undefined): Promise; - readable(options?: { - highWaterMark?: number | undefined, - start?: number | undefined, - end?: number | undefined - } | undefined): Promise; - - close(): Promise; - tell(): Promise; - read(size: number): Promise; - write(buffer: Uint8Array): Promise<[{ data: Uint8Array }]>; - truncate(size: number): Promise; - seek(offset: number, whence?: number | undefined): Promise; - size(): Promise<[{ position: bigint, size: bigint }]>; - } - - type EscapableArray = (string | number)[] - - type Serializable = never - | null - | boolean - | number - | string - | Date - | Uint8Array; - - type SerializableParameter = never - | T - | Serializable - | Helper - | Parameter - | ArrayParameter - | readonly SerializableParameter[]; - - type JSONValue = // using a dedicated type to detect symbols, bigints, and other non serializable types - | null - | string - | number - | boolean - | Date // serialized as `string` - | readonly JSONValue[] - | { toJSON(): any } // `toJSON` called by `JSON.stringify`; not typing the return type, types definition is strict enough anyway - | { - readonly [prop: string | number]: - | undefined - | JSONValue - | ((...args: any) => any) // serialized as `undefined` - }; - - interface Row { - [column: string]: any; - } - - type MaybeRow = Row | undefined; - - interface Column { - name: T; - type: number; - table: number; - number: number; - parser?: ((raw: string) => unknown) | undefined; - } - - type ColumnList = (T extends string ? Column : never)[]; - - interface State { - status: string; - pid: number; - secret: number; - } - - interface Statement { - /** statement unique name */ - name: string; - /** sql query */ - string: string; - /** parameters types */ - types: number[]; - columns: ColumnList; - } - - interface ResultMeta { - count: T; // For tuples - command: string; - statement: Statement; - state: State; - } - - interface ResultQueryMeta extends ResultMeta { - columns: ColumnList; - } - - type ExecutionResult = [] & ResultQueryMeta>; - type ValuesRowList = T[number][keyof T[number]][][] & ResultQueryMeta; - type RawRowList = Buffer[][] & Iterable & ResultQueryMeta; - type RowList = T & Iterable> & ResultQueryMeta; - - interface PendingQueryModifiers { - simple(): this; - readable(): Promise; - writable(): Promise; - - execute(): this; - cancel(): void; - - /** - * @deprecated `.stream` has been renamed to `.forEach` - * @throws - */ - stream(cb: (row: NonNullable, result: ExecutionResult) => void): never; - forEach(cb: (row: NonNullable, result: ExecutionResult) => void): Promise>; - - cursor(rows?: number | undefined): AsyncIterable[]>; - cursor(cb: (row: [NonNullable]) => void): Promise>; - cursor(rows: number, cb: (rows: NonNullable[]) => void): Promise>; - } - - interface PendingDescribeQuery extends Promise { - } - - interface PendingValuesQuery extends Promise>, PendingQueryModifiers { - describe(): PendingDescribeQuery; - } - - interface PendingRawQuery extends Promise>, PendingQueryModifiers { - } - - interface PendingQuery extends Promise>, PendingQueryModifiers { - describe(): PendingDescribeQuery; - values(): PendingValuesQuery; - raw(): PendingRawQuery; - } - - interface PendingRequest extends Promise<[] & ResultMeta> { } - - interface ListenRequest extends Promise { } - interface ListenMeta extends ResultMeta { - unlisten(): Promise - } - - interface Helper extends NotAPromise { - first: T; - rest: U; - } - - type Fragment = PendingQuery - - type ParameterOrJSON = - | SerializableParameter - | JSONValue - - type ParameterOrFragment = - | SerializableParameter - | Fragment - | Fragment[] - - interface Sql = {}> { - /** - * Query helper - * @param first Define how the helper behave - * @param rest Other optional arguments, depending on the helper type - * @returns An helper object usable as tagged template parameter in sql queries - */ - >(first: T & First, ...rest: K): Return; - - /** - * Execute the SQL query passed as a template string. Can only be used as template string tag. - * @param template The template generated from the template string - * @param parameters Interpoled values of the template string - * @returns A promise resolving to the result of your query - */ - (template: TemplateStringsArray, ...parameters: readonly (ParameterOrFragment)[]): PendingQuery; - - CLOSE: {}; - END: this['CLOSE']; - PostgresError: typeof PostgresError; - - options: ParsedOptions; - parameters: ConnectionParameters; - types: this['typed']; - typed: ((value: T, oid: number) => Parameter) & { - [name in keyof TTypes]: (value: TTypes[name]) => postgres.Parameter - }; - - unsafe)[]>(query: string, parameters?: (ParameterOrJSON)[] | undefined, queryOptions?: UnsafeQueryOptions | undefined): PendingQuery; - end(options?: { timeout?: number | undefined } | undefined): Promise; - - listen(channel: string, onnotify: (value: string) => void, onlisten?: (() => void) | undefined): ListenRequest; - notify(channel: string, payload: string): PendingRequest; - - subscribe(event: string, cb: (row: Row | null, info: ReplicationEvent) => void, onsubscribe?: (() => void), onerror?: (() => any)): Promise; - - largeObject(oid?: number | undefined, /** @default 0x00020000 | 0x00040000 */ mode?: number | undefined): Promise; - - begin(cb: (sql: TransactionSql) => T | Promise): Promise>; - begin(options: string, cb: (sql: TransactionSql) => T | Promise): Promise>; - - array[] = SerializableParameter[]>(value: T, type?: number | undefined): ArrayParameter; - file(path: string | Buffer | URL | number, options?: { cache?: boolean | undefined } | undefined): PendingQuery; - file(path: string | Buffer | URL | number, args: (ParameterOrJSON)[], options?: { cache?: boolean | undefined } | undefined): PendingQuery; - json(value: JSONValue): Parameter; - - reserve(): Promise> - } - - interface UnsafeQueryOptions { - /** - * When executes query as prepared statement. - * @default false - */ - prepare?: boolean | undefined; - } - - interface TransactionSql = {}> extends Sql { - savepoint(cb: (sql: TransactionSql) => T | Promise): Promise>; - savepoint(name: string, cb: (sql: TransactionSql) => T | Promise): Promise>; - - prepare(name: string): Promise>; - } - - interface ReservedSql = {}> extends Sql { - release(): void; - } -} - -export = postgres;