Skip to content

Commit

Permalink
feat: interpolate SQL tokens in sql.raw
Browse files Browse the repository at this point in the history
  • Loading branch information
gajus committed Apr 30, 2019
1 parent 87924ab commit 714da33
Show file tree
Hide file tree
Showing 20 changed files with 268 additions and 192 deletions.
12 changes: 6 additions & 6 deletions package.json
Expand Up @@ -28,18 +28,18 @@
},
"description": "A PostgreSQL client with strict types, detailed logging and assertions.",
"devDependencies": {
"@babel/cli": "^7.4.3",
"@babel/core": "^7.4.3",
"@babel/plugin-transform-flow-strip-types": "^7.4.0",
"@babel/preset-env": "^7.4.3",
"@babel/register": "^7.4.0",
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4",
"@babel/plugin-transform-flow-strip-types": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"@babel/register": "^7.4.4",
"ava": "^1.4.1",
"babel-plugin-istanbul": "^5.1.3",
"babel-plugin-transform-export-default-name": "^2.0.4",
"coveralls": "^3.0.3",
"eslint": "^5.16.0",
"eslint-config-canonical": "^17.0.1",
"flow-bin": "^0.97.0",
"flow-bin": "^0.98.0",
"flow-copy-source": "^2.0.3",
"gitdown": "^2.5.7",
"husky": "^2.1.0",
Expand Down
1 change: 1 addition & 0 deletions src/connectionMethods/query.js
Expand Up @@ -8,6 +8,7 @@ import type {
} from '../types';

const query: InternalQueryFunctionType<*> = async (connectionLogger, connection, clientConfiguration, rawSql, values, inheritedQueryId) => {
// $FlowFixMe
return executeQuery(
connectionLogger,
connection,
Expand Down
2 changes: 2 additions & 0 deletions src/connectionMethods/stream.js
Expand Up @@ -54,11 +54,13 @@ const stream: InternalStreamFunctionType = async (connectionLogger, connection,
}));

transformedStream.on('end', () => {
// $FlowFixMe
resolve({});
});

// Invoked if stream is destroyed using transformedStream.destroy().
transformedStream.on('close', () => {
// $FlowFixMe
resolve({});
});

Expand Down
22 changes: 22 additions & 0 deletions src/factories/createPrimitiveValueExpressions.js
@@ -0,0 +1,22 @@
// @flow

import {
UnexpectedStateError
} from '../errors';
import type {
PrimitiveValueExpressionType
} from '../types';

export default (values: $ReadOnlyArray<*>): $ReadOnlyArray<PrimitiveValueExpressionType> => {
const primitiveValueExpressions = [];

for (const value of values) {
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null) {
primitiveValueExpressions.push(value);
} else {
throw new UnexpectedStateError('Unexpected value expression.');
}
}

return primitiveValueExpressions;
};
1 change: 1 addition & 0 deletions src/factories/index.js
Expand Up @@ -2,5 +2,6 @@

export {default as createConnection} from './createConnection';
export {default as createPool} from './createPool';
export {default as createPrimitiveValueExpressions} from './createPrimitiveValueExpressions';
export {default as createSqlTokenSqlFragment} from './createSqlTokenSqlFragment';
export {default as createTypeParserPreset} from './createTypeParserPreset';
1 change: 1 addition & 0 deletions src/routines/executeQuery.js
Expand Up @@ -159,6 +159,7 @@ export default async (
connection.off('notice', noticeListener);
}

// $FlowFixMe
result.notices = notices;

for (const interceptor of clientConfiguration.interceptors) {
Expand Down
9 changes: 5 additions & 4 deletions src/sqlFragmentFactories/createRawSqlSqlFragment.js
Expand Up @@ -5,14 +5,15 @@ import type {
SqlFragmentType
} from '../types';
import {
normalizePositionalParameterReferences,
normalizeNamedParameterReferences
interpolatePositionalParameterReferences,
interpolateNamedParameterReferences
} from '../utilities';

export default (token: RawSqlTokenType, greatestParameterPosition: number): SqlFragmentType => {
if (Array.isArray(token.values)) {
return normalizePositionalParameterReferences(token.sql, token.values, greatestParameterPosition);
return interpolatePositionalParameterReferences(token.sql, token.values, greatestParameterPosition);
} else {
return normalizeNamedParameterReferences(token.sql, token.values, greatestParameterPosition);
// $FlowFixMe
return interpolateNamedParameterReferences(token.sql, token.values, greatestParameterPosition);
}
};
3 changes: 2 additions & 1 deletion src/sqlFragmentFactories/createTupleListSqlFragment.js
Expand Up @@ -11,6 +11,7 @@ import {
isSqlToken
} from '../utilities';
import {
createPrimitiveValueExpressions,
createSqlTokenSqlFragment
} from '../factories';

Expand Down Expand Up @@ -58,6 +59,6 @@ export default (token: TupleListSqlTokenType, greatestParameterPosition: number)

return {
sql,
values
values: createPrimitiveValueExpressions(values)
};
};
3 changes: 2 additions & 1 deletion src/sqlFragmentFactories/createTupleSqlFragment.js
Expand Up @@ -11,6 +11,7 @@ import {
isSqlToken
} from '../utilities';
import {
createPrimitiveValueExpressions,
createSqlTokenSqlFragment
} from '../factories';

Expand Down Expand Up @@ -43,6 +44,6 @@ export default (token: TupleSqlTokenType, greatestParameterPosition: number): Sq

return {
sql,
values
values: createPrimitiveValueExpressions(values)
};
};
3 changes: 2 additions & 1 deletion src/sqlFragmentFactories/createValueListSqlFragment.js
Expand Up @@ -11,6 +11,7 @@ import {
isSqlToken
} from '../utilities';
import {
createPrimitiveValueExpressions,
createSqlTokenSqlFragment
} from '../factories';

Expand Down Expand Up @@ -41,6 +42,6 @@ export default (token: ValueListSqlTokenType, greatestParameterPosition: number)

return {
sql: placeholders.join(', '),
values
values: createPrimitiveValueExpressions(values)
};
};
2 changes: 1 addition & 1 deletion src/templateTags/sql.js
Expand Up @@ -131,7 +131,7 @@ sql.identifierList = (

sql.raw = (
rawSql: string,
values?: $ReadOnlyArray<PrimitiveValueExpressionType>
values?: $ReadOnlyArray<ValueExpressionType>
): RawSqlTokenType => {
return deepFreeze({
sql: rawSql,
Expand Down
6 changes: 3 additions & 3 deletions src/types.js
Expand Up @@ -219,10 +219,10 @@ export type QueryContextType = {|
+transactionId?: string
|};

export type PositionalParameterValuesType = $ReadOnlyArray<PrimitiveValueExpressionType>;
export type PositionalParameterValuesType = $ReadOnlyArray<ValueExpressionType>;

export type NamedParameterValuesType = {
[key: string]: PrimitiveValueExpressionType
[key: string]: ValueExpressionType
};

export type IdentifierTokenType = {|
Expand Down Expand Up @@ -361,7 +361,7 @@ export type SqlTaggedTemplateType = {|
) => IdentifierListTokenType,
raw: (
rawSql: string,
values?: $ReadOnlyArray<PrimitiveValueExpressionType>
values?: $ReadOnlyArray<ValueExpressionType>
) => RawSqlTokenType,
tuple: (
values: $ReadOnlyArray<ValueExpressionType>
Expand Down
4 changes: 2 additions & 2 deletions src/utilities/index.js
Expand Up @@ -5,9 +5,9 @@ export {default as createQueryId} from './createQueryId';
export {default as createUlid} from './createUlid';
export {default as deepFreeze} from './deepFreeze';
export {default as escapeIdentifier} from './escapeIdentifier';
export {default as interpolateNamedParameterReferences} from './interpolateNamedParameterReferences';
export {default as interpolatePositionalParameterReferences} from './interpolatePositionalParameterReferences';
export {default as isPrimitiveValueExpression} from './isPrimitiveValueExpression';
export {default as isSqlToken} from './isSqlToken';
export {default as mapTaggedTemplateLiteralInvocation} from './mapTaggedTemplateLiteralInvocation';
export {default as normalizeNamedParameterReferences} from './normalizeNamedParameterReferences';
export {default as normalizePositionalParameterReferences} from './normalizePositionalParameterReferences';
export {default as stripArrayNotation} from './stripArrayNotation';
66 changes: 66 additions & 0 deletions src/utilities/interpolateNamedParameterReferences.js
@@ -0,0 +1,66 @@
// @flow

import {
difference
} from 'lodash';
import Logger from '../Logger';
import {
UnexpectedStateError
} from '../errors';
import type {
NamedParameterValuesType
} from '../types';
import interpolatePositionalParameterReferences from './interpolatePositionalParameterReferences';

const log = Logger.child({
namespace: 'interpolateNamedParameterReferences'
});

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

/**
* @see https://github.com/mysqljs/sqlstring/blob/f946198800a8d7f198fcf98d8bb80620595d01ec/lib/SqlString.js#L73
*/
export default (
inputSql: string,
inputValues: NamedParameterValuesType = {},
greatestParameterPosition: number
) => {
const resultValues = [];
const parameterNames = Object.keys(inputValues);

for (const parameterName of parameterNames) {
const parameterValue = inputValues[parameterName];

resultValues.push(parameterValue);
}

const usedParamterNames = [];

const resultSql = inputSql.replace(namedPlaceholderRegex, (match, g1) => {
if (!parameterNames.includes(g1)) {
throw new UnexpectedStateError('Named parameter reference does not have a matching value.');
}

usedParamterNames.push(g1);

const parameterIndex = parameterNames.indexOf(g1) + 1;

return match.slice(0, -g1.length - 1) + '$' + parameterIndex;
});

const unusedParameterNames = difference(parameterNames, usedParamterNames);

if (unusedParameterNames.length > 0) {
log.warn({
unusedParameterNames
}, 'unused parameter names');

throw new UnexpectedStateError('Values object contains value(s) not present as named parameter references in the query.');
}

return interpolatePositionalParameterReferences(resultSql, resultValues, greatestParameterPosition);
};
60 changes: 60 additions & 0 deletions src/utilities/interpolatePositionalParameterReferences.js
@@ -0,0 +1,60 @@
// @flow

import {
UnexpectedStateError
} from '../errors';
import type {
PositionalParameterValuesType
} from '../types';
import {
createSqlTokenSqlFragment
} from '../factories';
import isSqlToken from './isSqlToken';

/**
* @see https://github.com/mysqljs/sqlstring/blob/f946198800a8d7f198fcf98d8bb80620595d01ec/lib/SqlString.js#L73
*/
export default (
inputSql: string,
inputValues: PositionalParameterValuesType = [],
greatestParameterPosition: number
) => {
const resultValues = [];

const bindingNames = (inputSql.match(/\$(\d+)/g) || [])
.map((match) => {
return parseInt(match.slice(1), 10);
})
.sort();

if (bindingNames[bindingNames.length - 1] > inputValues.length) {
throw new UnexpectedStateError('The greatest parameter position is greater than the number of parameter values.');
}

if (bindingNames.length > 0 && bindingNames[0] !== 1) {
throw new UnexpectedStateError('Parameter position must start at 1.');
}

const resultSql = inputSql.replace(/\$(\d+)/g, (match, g1) => {
const parameterPosition = parseInt(g1, 10);
const boundValue = inputValues[parameterPosition - 1];

if (isSqlToken(boundValue)) {
// $FlowFixMe
const sqlFragment = createSqlTokenSqlFragment(boundValue, resultValues.length + greatestParameterPosition);

resultValues.push(...sqlFragment.values);

return sqlFragment.sql;
} else {
resultValues.push(inputValues[parameterPosition - 1]);

return '$' + (resultValues.length + greatestParameterPosition);
}
});

return {
sql: resultSql,
values: resultValues
};
};
68 changes: 0 additions & 68 deletions src/utilities/normalizeNamedParameterReferences.js

This file was deleted.

0 comments on commit 714da33

Please sign in to comment.