Skip to content

Commit

Permalink
feat: add named placeholders
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus committed Apr 14, 2017
1 parent a78129b commit 110eed7
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 82 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A PostgreSQL client with strict types and assertions.
* [Value placeholders](#value-placeholders)
* [A value set](#a-value-set)
* [Multiple value sets](#multiple-value-sets)
* [Named placeholders](#named-placeholders)
* [Convenience methods](#convenience-methods)
* [`any`](#any)
* [`insert`](#insert)
Expand Down Expand Up @@ -108,6 +109,24 @@ SELECT ($1, $2, $3), ($4, $5, $6)

```

### Named placeholders

A `:[a-zA-Z]` regex is used to match named placeholders.

```js
await connection.query('SELECT :foo', {
foo: 'FOO'
});

```

Produces:

```sql
SELECT $1

```

## Convenience methods

### `any`
Expand Down
30 changes: 24 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import {
} from 'pg-connection-string';
import createDebug from 'debug';
import prettyHrtime from 'pretty-hrtime';
import arrayFlatten from 'array-flatten';
import {
DataIntegrityError,
DuplicateEntryError,
NotFoundError
} from './errors';
import {
normalizeValuePlaceholders
normalizeAnonymousValuePlaceholders,
normalizeNamedValuePlaceholders
} from './utilities';
import type {
DatabaseConfigurationType,
Expand Down Expand Up @@ -45,17 +45,35 @@ types.setTypeParser(20, (value) => {

const debug = createDebug('mightyql');

export const query: InternalQueryType = async (connection, sql, values = []) => {
export const query: InternalQueryType = async (connection, sql, values) => {
debug('query input', sql, values);

try {
const start = process.hrtime();

const normalizedQuery = normalizeValuePlaceholders(sql, values);
let result;

debug('normlized query', normalizedQuery);
if (Array.isArray(values)) {
const {
sql: normalizedSql,
values: normalizedValues
} = normalizeAnonymousValuePlaceholders(sql, values);

const result = await connection.query(normalizedQuery, arrayFlatten(values));
debug('normlized SQL', normalizedSql);

result = await connection.query(normalizedSql, normalizedValues);
} else if (values) {
const {
sql: normalizedSql,
values: normalizedValues
} = normalizeNamedValuePlaceholders(sql, values);

debug('normlized SQL', normalizedSql);

result = await connection.query(normalizedSql, normalizedValues);
} else {
result = await connection.query(sql);
}

const end = process.hrtime(start);

Expand Down
20 changes: 16 additions & 4 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,29 @@ export type QueryResultRowType = {
[key: string]: string | number
};

export type NormalizedQueryType = {|
+sql: string,
+values: $ReadOnlyArray<*>
|};

type QueryPrimitiveValueType = string | number | null;

export type DatabaseQueryValuesType =
// eslint-disable-next-line
$ReadOnlyArray<
// eslint-disable-next-line flowtype/generic-spacing
export type AnonymouseValuePlaceholderValuesType = $ReadOnlyArray<

// INSERT ... VALUES ? => INSERT ... VALUES (1, 2, 3); [[1, 2, 3]]
// INSERT ... VALUES ? => INSERT ... VALUES (1), (2), (3); [[[1], [2], [3]]]
$ReadOnlyArray<QueryPrimitiveValueType | $ReadOnlyArray<QueryPrimitiveValueType>> |
QueryPrimitiveValueType
>;
>;

export type NamedValuePlaceholderValuesType = {
+[key: string]: string | number | null
};

export type DatabaseQueryValuesType =
AnonymouseValuePlaceholderValuesType |
NamedValuePlaceholderValuesType;

// eslint-disable-next-line flowtype/no-weak-types
export type InternalQueryType = (connection: InternalDatabaseConnectionType, sql: string, values?: DatabaseQueryValuesType) => Promise<any>;
Expand Down
3 changes: 2 additions & 1 deletion src/utilities/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @flow

export {default as normalizeValuePlaceholders} from './normalizeValuePlaceholders';
export {default as normalizeAnonymousValuePlaceholders} from './normalizeAnonymousValuePlaceholders';
export {default as normalizeNamedValuePlaceholders} from './normalizeNamedValuePlaceholders';
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
// @flow

import arrayFlatten from 'array-flatten';
import type {
DatabaseQueryValuesType
AnonymouseValuePlaceholderValuesType,
NormalizedQueryType
} from '../types';

const placeholdersRegex = /\?/g;
const anonymousePlaceholdersRegex = /\?/g;

/**
* @see https://github.com/mysqljs/sqlstring/blob/f946198800a8d7f198fcf98d8bb80620595d01ec/lib/SqlString.js#L73
*/
export default (
sql: string,
values: DatabaseQueryValuesType = []
): string => {
values: AnonymouseValuePlaceholderValuesType = []
): NormalizedQueryType => {
let chunkIndex = 0;
let result = '';
let match;
let valueIndex = 0;
let placeholderIndex = 0;

// eslint-disable-next-line no-cond-assign
while (match = placeholdersRegex.exec(sql)) {
while (match = anonymousePlaceholdersRegex.exec(sql)) {
if (!values.hasOwnProperty(valueIndex)) {
throw new Error('Value placeholder is missing a value.');
}
Expand All @@ -30,7 +32,7 @@ export default (
valueIndex++;

result += sql.slice(chunkIndex, match.index);
chunkIndex = placeholdersRegex.lastIndex;
chunkIndex = anonymousePlaceholdersRegex.lastIndex;

// SELECT ?, [[[1,1],[1,1]]]; SELECT ($1, $2), ($3, $4)
if (Array.isArray(value) && Array.isArray(value[0])) {
Expand Down Expand Up @@ -75,12 +77,13 @@ export default (
}

if (chunkIndex === 0) {
return sql;
result = sql;
} else if (chunkIndex < sql.length) {
result += sql.slice(chunkIndex);
}

if (chunkIndex < sql.length) {
return result + sql.slice(chunkIndex);
}

return result;
return {
sql: result,
values: arrayFlatten(values)
};
};
59 changes: 59 additions & 0 deletions src/utilities/normalizeNamedValuePlaceholders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// @flow

import type {
NamedValuePlaceholderValuesType,
NormalizedQueryType
} from '../types';

/**
* @see https://regex101.com/r/KrEe8i/1
*/
const namedPlaceholderRegex = /[\s,(]:([a-zA-Z]+)/g;

/**
* @see https://github.com/mysqljs/sqlstring/blob/f946198800a8d7f198fcf98d8bb80620595d01ec/lib/SqlString.js#L73
*/
export default (
sql: string,
values: NamedValuePlaceholderValuesType = {}
): NormalizedQueryType => {
let chunkIndex = 0;
let result = '';
let match;
let placeholderIndex = 0;

const normalizedValues = [];

// eslint-disable-next-line no-cond-assign
while (match = namedPlaceholderRegex.exec(sql)) {
const matchIndex = match.index + 1;
const matchName = match[1];

if (!values.hasOwnProperty(matchName)) {
throw new Error('Value placeholder is missing a value.');
}

const value = values[matchName];

normalizedValues.push(value);

result += sql.slice(chunkIndex, matchIndex);

chunkIndex = namedPlaceholderRegex.lastIndex;

++placeholderIndex;

result += '$' + placeholderIndex;
}

if (chunkIndex === 0) {
result = sql;
} else if (chunkIndex < sql.length) {
result += sql.slice(chunkIndex);
}

return {
sql: result,
values: normalizedValues
};
};
90 changes: 90 additions & 0 deletions test/utilities/normalizeAnonymousValuePlaceholders.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// @flow

import test from 'ava';
import {
normalizeAnonymousValuePlaceholders
} from '../../src/utilities';

test('does not error when placeholders are absent', (t) => {
const {
sql,
values
} = normalizeAnonymousValuePlaceholders('SELECT 1');

t.true(sql === 'SELECT 1');
t.deepEqual(values, []);
});

test('interpolates a value placeholder', (t) => {
const {
sql,
values
} = normalizeAnonymousValuePlaceholders('SELECT ?', [
'foo'
]);

t.true(sql === 'SELECT $1');
t.deepEqual(values, [
'foo'
]);
});

test('interpolates multiple value placeholders', (t) => {
const {
sql,
values
} = normalizeAnonymousValuePlaceholders('SELECT ?, ?', [
'foo',
'bar'
]);

t.true(sql === 'SELECT $1, $2');
t.deepEqual(values, [
'foo',
'bar'
]);
});

test('interpolates a value set', (t) => {
const {
sql,
values
} = normalizeAnonymousValuePlaceholders('SELECT ?', [
[
'foo',
'bar'
]
]);

t.true(sql === 'SELECT ($1, $2)');
t.deepEqual(values, [
'foo',
'bar'
]);
});

test('interpolates a list of value sets', (t) => {
const {
sql,
values
} = normalizeAnonymousValuePlaceholders('SELECT ?', [
[
[
'foo',
'bar'
],
[
'baz',
'qux'
]
]
]);

t.true(sql === 'SELECT ($1, $2), ($3, $4)');
t.deepEqual(values, [
'foo',
'bar',
'baz',
'qux'
]);
});
Loading

0 comments on commit 110eed7

Please sign in to comment.