From ecd5ceb3b94151aca368e81e600ed2f92dbd6fbc Mon Sep 17 00:00:00 2001 From: Petri Lehtinen Date: Sat, 1 Aug 2020 12:59:08 +0300 Subject: [PATCH] Add sql.describe() --- README.md | 14 ++++++++++++++ lib/backend.js | 18 +++++++++++++++--- lib/connection.js | 5 ++++- lib/frontend.js | 15 +++++++++++++-- lib/index.js | 7 ++++++- tests/index.js | 30 ++++++++++++++++++++++++++++++ types/index.d.ts | 6 ++++++ 7 files changed, 88 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 21aef6db..8fd7efff 100644 --- a/README.md +++ b/README.md @@ -392,6 +392,20 @@ sql.file(path.join(__dirname, 'query.sql'), [], { ``` +## Describe `sql.describe(stmt) -> Promise` + +Describe the parameters and output columns of the given SQL statement. + +```js + +const { params, columns } = await sql.describe('select * from users where id = $1 and name like $2') + +``` + +The resulting `params` is an array of Postgres data type ids (OIDs) for parameters in order `$1`, `$2`, +etc. `columns` is an array of objects `{ name, type, parser }`, where `name` is the column name, `type` +is the type OID, and `parser` is a function used to parse the value to JavaScript. + ## Transactions diff --git a/lib/backend.js b/lib/backend.js index 14c3cd7f..a06219ef 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -155,7 +155,10 @@ function Backend({ : console.log(parseError(x)) // eslint-disable-line } - function NoData() { /* No handling needed */ } + function NoData() { + if (backend.query.describe) + backend.query.result.columns = [] + } function Authentication(x) { const type = x.readInt32BE(5) @@ -174,8 +177,17 @@ function Backend({ } /* c8 ignore next 3 */ - function ParameterDescription() { - backend.error = errors.notSupported('ParameterDescription') + function ParameterDescription(x) { + const length = x.readInt16BE(5) + let index = 7 + + backend.query.statement.params = Array(length) + + for (let i = 0; i < length; ++i) { + backend.query.statement.params[i] = x.readInt32BE(index) + index += 4 + } + backend.query.result.params = backend.query.statement.params } function RowDescription(x) { diff --git a/lib/connection.js b/lib/connection.js index d2aa9ec1..76ee9153 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -119,6 +119,7 @@ function Connection(options = {}) { query.args = args query.result = [] query.result.count = null + if (!query.describe) query.result.count = null idle_timeout && clearTimeout(timer) typeof options.debug === 'function' && options.debug(id, str, args) @@ -168,7 +169,9 @@ function Connection(options = {}) { query.statement = { name: sig ? 'p' + uid + statement_id++ : '', sig } return Buffer.concat([ frontend.Parse(query.statement.name, str, args), - bind(query, args) + query.describe + ? frontend.Describe(query.statement.name) + : bind(query, args) ]) } diff --git a/lib/frontend.js b/lib/frontend.js index fe8bd79d..9d5457a9 100644 --- a/lib/frontend.js +++ b/lib/frontend.js @@ -5,11 +5,14 @@ const { errors } = require('./errors.js') const N = String.fromCharCode(0) const empty = Buffer.alloc(0) +const flushSync = Buffer.concat([ + bytes.H().end(), + bytes.S().end() +]) const execute = Buffer.concat([ bytes.D().str('P').str(N).end(), bytes.E().str(N).i32(0).end(), - bytes.H().end(), - bytes.S().end() + flushSync, ]) const authNames = { @@ -39,6 +42,7 @@ module.exports = { auth, Bind, Parse, + Describe, Query, Close, Execute @@ -190,6 +194,13 @@ function Parse(name, str, args) { return bytes.end() } +function Describe(name) { + return Buffer.concat([ + bytes.D().str('S').str(name).str(N).end(), + flushSync, + ]) +} + function Execute(rows) { return Buffer.concat([ bytes.E().str(N).i32(rows).end(), diff --git a/lib/index.js b/lib/index.js index a3321961..9924a0c2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -275,7 +275,8 @@ function Postgres(a, b) { unsafe, array, file, - json + json, + describe, }) function notify(channel, payload) { @@ -316,6 +317,10 @@ function Postgres(a, b) { return promise } + function describe(stmt) { + return query({ raw: true, describe: true }, connection || getConnection(), stmt) + } + options.types && entries(options.types).forEach(([name, type]) => { sql.types[name] = (x) => ({ type: type.to, value: x }) }) diff --git a/tests/index.js b/tests/index.js index 9dddb2dc..9b83f5b0 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1101,3 +1101,33 @@ t('Catches query format errors', async() => [ 'wat', await sql.unsafe({ toString: () => { throw new Error('wat') } }).catch((e) => e.message) ]) + +t('Describe a statement', async() => { + await sql`create table tester (name text, age int)` + const r = await sql.describe('select name, age from tester where name like $1 and age > $2') + return [ + '25,23/name:25,age:23', + `${r.params.join(',')}/${r.columns.map(c => `${c.name}:${c.type}`).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.describe('select name, age from tester') + return [ + '0,2', + `${r.params.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.describe('insert into tester (name, age) values ($1, $2)') + return [ + '2,0', + `${r.params.length},${r.columns.length}`, + await sql`drop table tester` + ] +}) diff --git a/types/index.d.ts b/types/index.d.ts index 3e625f70..4da60b83 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -271,6 +271,11 @@ declare namespace postgres { columns: ColumnList; } + interface DescribeResult { + params: number[] + columns: ColumnList + } + type ExecutionResult = [] & ResultQueryMeta; type RowList = T & ResultQueryMeta; @@ -318,6 +323,7 @@ declare namespace postgres { PostgresError: typeof PostgresError; array(value: T): ArrayParameter; + describe(stmt: string): Promise begin(cb: (sql: TransactionSql) => T | Promise): Promise>; begin(options: string, cb: (sql: TransactionSql) => T | Promise): Promise>; end(options?: { timeout?: number }): Promise;