diff --git a/cjs/src/connection.js b/cjs/src/connection.js index 6736d955..943aabeb 100644 --- a/cjs/src/connection.js +++ b/cjs/src/connection.js @@ -3,7 +3,7 @@ const tls = require('tls') const crypto = require('crypto') const Stream = require('stream') -const { stringify, handleValue, arrayParser, arraySerializer } = require('./types.js') +const { serialize, stringify, handleValue, arrayParser, arraySerializer } = require('./types.js') const { Errors } = require('./errors.js') const Result = require('./result.js') const Queue = require('./queue.js') @@ -180,7 +180,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose throw Errors.generic('MAX_PARAMETERS_EXCEEDED', 'Max number of parameters (65534) exceeded') return q.options.simple - ? b().Q().str(q.strings[0] + b.N).end() + ? b().Q().str(q.statement.string + b.N).end() : q.describeFirst ? Buffer.concat([describe(q), Flush]) : q.prepare @@ -912,7 +912,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose type = types[i] parameters[i] = x = type in options.serializers ? options.serializers[type](x) - : '' + x + : serialize(x) prev = b.i b.inc(4).str(x).i32(b.i - prev - 4, prev) diff --git a/cjs/src/types.js b/cjs/src/types.js index 1c8ae092..f9fca7cc 100644 --- a/cjs/src/types.js +++ b/cjs/src/types.js @@ -1,11 +1,19 @@ const { Query } = require('./query.js') const { Errors } = require('./errors.js') +const serialize = module.exports.serialize = function serialize(x) { + return typeof x === 'string' ? x : + x instanceof Date ? types.date.serialize(x) : + x instanceof Uint8Array ? types.bytea.serialize(x) : + (x === true || x === false) ? types.boolean.serialize(x) : + '' + x +} + const types = module.exports.types = { string: { to: 25, from: null, // defaults to string - serialize: x => '' + x + serialize }, number: { to: 0, @@ -66,10 +74,9 @@ const Builder = module.exports.Builder = class Builder extends NotTagged { build(before, parameters, types, options) { const keyword = builders.map(([x, fn]) => ({ fn, i: before.search(x) })).sort((a, b) => a.i - b.i).pop() - if (keyword.i === -1) - throw new Error('Could not infer helper mode') - - return keyword.fn(this.first, this.rest, parameters, types, options) + return keyword.i === -1 + ? escapeIdentifiers(this.first, options) + : keyword.fn(this.first, this.rest, parameters, types, options) } } @@ -137,7 +144,7 @@ function values(first, rest, parameters, types, options) { function select(first, rest, parameters, types, options) { typeof first === 'string' && (first = [first].concat(rest)) if (Array.isArray(first)) - return first.map(x => escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x)).join(',') + return escapeIdentifiers(first, options) let value const columns = rest.length ? rest.flat() : Object.keys(first) @@ -170,9 +177,7 @@ const builders = Object.entries({ insert(first, rest, parameters, types, options) { const columns = rest.length ? rest.flat() : Object.keys(Array.isArray(first) ? first[0] : first) - return '(' + columns.map(x => - escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) - ).join(',') + ')values' + + 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])) @@ -209,20 +214,18 @@ function typeHandlers(types) { }, { parsers: {}, serializers: {} }) } +function escapeIdentifiers(xs, { transform: { column } }) { + return xs.map(x => escapeIdentifier(column.to ? column.to(x) : x)).join(',') +} + const escapeIdentifier = module.exports.escapeIdentifier = function escape(str) { return '"' + str.replace(/"/g, '""').replace(/\./g, '"."') + '"' } const inferType = module.exports.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 - ) + return x instanceof Parameter + ? x.type + : 0 } const escapeBackslash = /\\/g diff --git a/cjs/tests/index.js b/cjs/tests/index.js index 985fb086..f9fd6056 100644 --- a/cjs/tests/index.js +++ b/cjs/tests/index.js @@ -89,16 +89,16 @@ t('String', async() => ) t('Boolean false', async() => - [false, (await sql`select ${ false } as x`)[0].x] + [false, (await sql`select ${ false }::bool as x`)[0].x] ) t('Boolean true', async() => - [true, (await sql`select ${ true } as x`)[0].x] + [true, (await sql`select ${ true }::bool as x`)[0].x] ) t('Date', async() => { const now = new Date() - return [0, now - (await sql`select ${ now } as x`)[0].x] + return [0, now - (await sql`select ${ now }::timestamptz as x`)[0].x] }) t('Json', async() => { @@ -134,7 +134,8 @@ t('Array of String', async() => 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()] + , iso = now.toISOString() + return [now.getTime(), (await sql`select ${ sql.array([iso, iso, iso]) }::timestamptz[] as x`)[0].x[2].getTime()] }) t('Nested array n2', async() => @@ -482,7 +483,7 @@ t('Point type array', async() => { }) 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])]) })` + await sql`insert into test (x) values (${ [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`] }) @@ -1194,10 +1195,7 @@ t('Multiple queries', async() => { }) t('Multiple statements', async() => - [2, await sql.unsafe(` - select 1 as x; - select 2 as a; - `).then(([, [x]]) => x.a)] + [2, await sql.unsafe(`select 1 as x;select 2 as a`).then(([, [x]]) => x.a)] ) t('throws correct error when authentication fails', async() => { @@ -1348,7 +1346,7 @@ t('Multiple Cursors', { timeout: 2 }, async() => { t('Cursor as async iterator', async() => { const order = [] - for await (const [x] of sql`select generate_series(1,2) as x;`.cursor()) { + 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') @@ -1359,7 +1357,7 @@ t('Cursor as async iterator', async() => { t('Cursor as async iterator with break', async() => { const order = [] - for await (const xs of sql`select generate_series(1,2) as x;`.cursor()) { + 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') @@ -1748,7 +1746,7 @@ t('Recreate prepared statements on transformAssignedExpr error', { timeout: 1 }, 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) + const error = await sql.begin(sql => sql`insert into test (x) values (${ false }::bool)`).catch(e => e) return [ error.code, '42804', diff --git a/deno/src/connection.js b/deno/src/connection.js index 2feac1bd..4024e498 100644 --- a/deno/src/connection.js +++ b/deno/src/connection.js @@ -7,7 +7,7 @@ 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 { serialize, stringify, handleValue, arrayParser, arraySerializer } from './types.js' import { Errors } from './errors.js' import Result from './result.js' import Queue from './queue.js' @@ -184,7 +184,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose throw Errors.generic('MAX_PARAMETERS_EXCEEDED', 'Max number of parameters (65534) exceeded') return q.options.simple - ? b().Q().str(q.strings[0] + b.N).end() + ? b().Q().str(q.statement.string + b.N).end() : q.describeFirst ? Buffer.concat([describe(q), Flush]) : q.prepare @@ -916,7 +916,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose type = types[i] parameters[i] = x = type in options.serializers ? options.serializers[type](x) - : '' + x + : serialize(x) prev = b.i b.inc(4).str(x).i32(b.i - prev - 4, prev) diff --git a/deno/src/types.js b/deno/src/types.js index c59d6224..c4b51626 100644 --- a/deno/src/types.js +++ b/deno/src/types.js @@ -2,11 +2,19 @@ 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 serialize = function serialize(x) { + return typeof x === 'string' ? x : + x instanceof Date ? types.date.serialize(x) : + x instanceof Uint8Array ? types.bytea.serialize(x) : + (x === true || x === false) ? types.boolean.serialize(x) : + '' + x +} + export const types = { string: { to: 25, from: null, // defaults to string - serialize: x => '' + x + serialize }, number: { to: 0, @@ -67,10 +75,9 @@ export class Builder extends NotTagged { build(before, parameters, types, options) { const keyword = builders.map(([x, fn]) => ({ fn, i: before.search(x) })).sort((a, b) => a.i - b.i).pop() - if (keyword.i === -1) - throw new Error('Could not infer helper mode') - - return keyword.fn(this.first, this.rest, parameters, types, options) + return keyword.i === -1 + ? escapeIdentifiers(this.first, options) + : keyword.fn(this.first, this.rest, parameters, types, options) } } @@ -138,7 +145,7 @@ function values(first, rest, parameters, types, options) { function select(first, rest, parameters, types, options) { typeof first === 'string' && (first = [first].concat(rest)) if (Array.isArray(first)) - return first.map(x => escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x)).join(',') + return escapeIdentifiers(first, options) let value const columns = rest.length ? rest.flat() : Object.keys(first) @@ -171,9 +178,7 @@ const builders = Object.entries({ insert(first, rest, parameters, types, options) { const columns = rest.length ? rest.flat() : Object.keys(Array.isArray(first) ? first[0] : first) - return '(' + columns.map(x => - escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) - ).join(',') + ')values' + + 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])) @@ -210,20 +215,18 @@ function typeHandlers(types) { }, { 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 - ) + return x instanceof Parameter + ? x.type + : 0 } const escapeBackslash = /\\/g diff --git a/deno/tests/index.js b/deno/tests/index.js index 688c002b..6b7b440b 100644 --- a/deno/tests/index.js +++ b/deno/tests/index.js @@ -91,16 +91,16 @@ t('String', async() => ) t('Boolean false', async() => - [false, (await sql`select ${ false } as x`)[0].x] + [false, (await sql`select ${ false }::bool as x`)[0].x] ) t('Boolean true', async() => - [true, (await sql`select ${ true } as x`)[0].x] + [true, (await sql`select ${ true }::bool as x`)[0].x] ) t('Date', async() => { const now = new Date() - return [0, now - (await sql`select ${ now } as x`)[0].x] + return [0, now - (await sql`select ${ now }::timestamptz as x`)[0].x] }) t('Json', async() => { @@ -136,7 +136,8 @@ t('Array of String', async() => 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()] + , iso = now.toISOString() + return [now.getTime(), (await sql`select ${ sql.array([iso, iso, iso]) }::timestamptz[] as x`)[0].x[2].getTime()] }) t('Nested array n2', async() => @@ -484,7 +485,7 @@ t('Point type array', async() => { }) 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])]) })` + await sql`insert into test (x) values (${ [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`] }) @@ -1196,10 +1197,7 @@ t('Multiple queries', async() => { }) t('Multiple statements', async() => - [2, await sql.unsafe(` - select 1 as x; - select 2 as a; - `).then(([, [x]]) => x.a)] + [2, await sql.unsafe(`select 1 as x;select 2 as a`).then(([, [x]]) => x.a)] ) t('throws correct error when authentication fails', async() => { @@ -1350,7 +1348,7 @@ t('Multiple Cursors', { timeout: 2 }, async() => { t('Cursor as async iterator', async() => { const order = [] - for await (const [x] of sql`select generate_series(1,2) as x;`.cursor()) { + 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') @@ -1361,7 +1359,7 @@ t('Cursor as async iterator', async() => { t('Cursor as async iterator with break', async() => { const order = [] - for await (const xs of sql`select generate_series(1,2) as x;`.cursor()) { + 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') @@ -1750,7 +1748,7 @@ t('Recreate prepared statements on transformAssignedExpr error', { timeout: 1 }, 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) + const error = await sql.begin(sql => sql`insert into test (x) values (${ false }::bool)`).catch(e => e) return [ error.code, '42804', diff --git a/src/connection.js b/src/connection.js index ca3c8cc8..58cd41a8 100644 --- a/src/connection.js +++ b/src/connection.js @@ -3,7 +3,7 @@ import tls from 'tls' import crypto from 'crypto' import Stream from 'stream' -import { stringify, handleValue, arrayParser, arraySerializer } from './types.js' +import { serialize, stringify, handleValue, arrayParser, arraySerializer } from './types.js' import { Errors } from './errors.js' import Result from './result.js' import Queue from './queue.js' @@ -912,7 +912,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose type = types[i] parameters[i] = x = type in options.serializers ? options.serializers[type](x) - : '' + x + : serialize(x) prev = b.i b.inc(4).str(x).i32(b.i - prev - 4, prev) diff --git a/src/types.js b/src/types.js index e4c1b680..eeb7237c 100644 --- a/src/types.js +++ b/src/types.js @@ -1,11 +1,19 @@ import { Query } from './query.js' import { Errors } from './errors.js' +export const serialize = function serialize(x) { + return typeof x === 'string' ? x : + x instanceof Date ? types.date.serialize(x) : + x instanceof Uint8Array ? types.bytea.serialize(x) : + (x === true || x === false) ? types.boolean.serialize(x) : + '' + x +} + export const types = { string: { to: 25, from: null, // defaults to string - serialize: x => '' + x + serialize }, number: { to: 0, @@ -215,15 +223,9 @@ export const escapeIdentifier = function escape(str) { } 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 - ) + return x instanceof Parameter + ? x.type + : 0 } const escapeBackslash = /\\/g diff --git a/tests/index.js b/tests/index.js index b990acbc..4a516f71 100644 --- a/tests/index.js +++ b/tests/index.js @@ -89,16 +89,16 @@ t('String', async() => ) t('Boolean false', async() => - [false, (await sql`select ${ false } as x`)[0].x] + [false, (await sql`select ${ false }::bool as x`)[0].x] ) t('Boolean true', async() => - [true, (await sql`select ${ true } as x`)[0].x] + [true, (await sql`select ${ true }::bool as x`)[0].x] ) t('Date', async() => { const now = new Date() - return [0, now - (await sql`select ${ now } as x`)[0].x] + return [0, now - (await sql`select ${ now }::timestamptz as x`)[0].x] }) t('Json', async() => { @@ -134,7 +134,8 @@ t('Array of String', async() => 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()] + , iso = now.toISOString() + return [now.getTime(), (await sql`select ${ sql.array([iso, iso, iso]) }::timestamptz[] as x`)[0].x[2].getTime()] }) t('Nested array n2', async() => @@ -482,7 +483,7 @@ t('Point type array', async() => { }) 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])]) })` + await sql`insert into test (x) values (${ [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`] }) @@ -1194,10 +1195,7 @@ t('Multiple queries', async() => { }) t('Multiple statements', async() => - [2, await sql.unsafe(` - select 1 as x; - select 2 as a; - `).then(([, [x]]) => x.a)] + [2, await sql.unsafe(`select 1 as x;select 2 as a`).then(([, [x]]) => x.a)] ) t('throws correct error when authentication fails', async() => { @@ -1348,7 +1346,7 @@ t('Multiple Cursors', { timeout: 2 }, async() => { t('Cursor as async iterator', async() => { const order = [] - for await (const [x] of sql`select generate_series(1,2) as x;`.cursor()) { + 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') @@ -1359,7 +1357,7 @@ t('Cursor as async iterator', async() => { t('Cursor as async iterator with break', async() => { const order = [] - for await (const xs of sql`select generate_series(1,2) as x;`.cursor()) { + 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') @@ -1748,7 +1746,7 @@ t('Recreate prepared statements on transformAssignedExpr error', { timeout: 1 }, 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) + const error = await sql.begin(sql => sql`insert into test (x) values (${ false }::bool)`).catch(e => e) return [ error.code, '42804',