Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

named parameters in SQL queries #126

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 25 additions & 14 deletions packages/node-firebird-driver/src/lib/impl/attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ import {
Events,
FetchOptions,
PrepareOptions,
TransactionOptions
TransactionOptions,
Parameters
} from '..';
import { parseParams } from './parser';

const adjustPrepareOptions = (prepareOptions?: PrepareOptions, parameters?: Parameters) =>
parameters && !Array.isArray(parameters) && !prepareOptions ? { namedParams: true } : prepareOptions;

/** AbstractAttachment implementation. */
export abstract class AbstractAttachment implements Attachment {
Expand Down Expand Up @@ -78,31 +82,31 @@ export abstract class AbstractAttachment implements Attachment {
}

/** Executes a statement that has no result set. */
async execute(transaction: AbstractTransaction, sqlStmt: string, parameters?: any[],
async execute(transaction: AbstractTransaction, sqlStmt: string, parameters?: Parameters,
options?: {
prepareOptions?: PrepareOptions,
executeOptions?: ExecuteOptions
}): Promise<void> {
this.check();

const statement = await this.prepare(transaction, sqlStmt, options && options.prepareOptions);
const statement = await this.prepare(transaction, sqlStmt, adjustPrepareOptions(options?.prepareOptions, parameters));
try {
return await statement.execute(transaction, parameters, options && options.executeOptions);
return await statement.execute(transaction, parameters, options?.executeOptions);
}
finally {
await statement.dispose();
}
}

/** Executes a statement that returns a single record. */
async executeSingleton(transaction: AbstractTransaction, sqlStmt: string, parameters?: Array<any>,
async executeSingleton(transaction: AbstractTransaction, sqlStmt: string, parameters?: Parameters,
options?: {
prepareOptions?: PrepareOptions,
executeOptions?: ExecuteOptions
}): Promise<Array<any>> {
this.check();

const statement = await this.prepare(transaction, sqlStmt, options && options.prepareOptions);
const statement = await this.prepare(transaction, sqlStmt, adjustPrepareOptions(options?.prepareOptions, parameters));
try {
return await statement.executeSingleton(transaction, parameters, options && options.executeOptions);
}
Expand All @@ -112,14 +116,14 @@ export abstract class AbstractAttachment implements Attachment {
}

/** Executes a statement that returns a single record in object form. */
async executeSingletonAsObject<T extends object>(transaction: AbstractTransaction, sqlStmt: string, parameters?: any[],
async executeSingletonAsObject<T extends object>(transaction: AbstractTransaction, sqlStmt: string, parameters?: Parameters,
options?: {
prepareOptions?: PrepareOptions,
executeOptions?: ExecuteOptions
}): Promise<T> {
this.check();

const statement = await this.prepare(transaction, sqlStmt, options && options.prepareOptions);
const statement = await this.prepare(transaction, sqlStmt, adjustPrepareOptions(options?.prepareOptions, parameters));
try {
return await statement.executeSingletonAsObject(transaction, parameters, options && options.executeOptions);
}
Expand All @@ -129,7 +133,7 @@ export abstract class AbstractAttachment implements Attachment {
}

/** Executes a statement that returns a single record. */
async executeReturning(transaction: AbstractTransaction, sqlStmt: string, parameters?: Array<any>,
async executeReturning(transaction: AbstractTransaction, sqlStmt: string, parameters?: Parameters,
options?: {
prepareOptions?: PrepareOptions,
executeOptions?: ExecuteOptions
Expand All @@ -138,7 +142,7 @@ export abstract class AbstractAttachment implements Attachment {
}

/** Executes a statement that returns a single record in object form. */
async executeReturningAsObject<T extends object>(transaction: AbstractTransaction, sqlStmt: string, parameters?: any[],
async executeReturningAsObject<T extends object>(transaction: AbstractTransaction, sqlStmt: string, parameters?: Parameters,
options?: {
prepareOptions?: PrepareOptions,
executeOptions?: ExecuteOptions
Expand All @@ -147,16 +151,16 @@ export abstract class AbstractAttachment implements Attachment {
}

/** Executes a statement that has result set. */
async executeQuery(transaction: AbstractTransaction, sqlStmt: string, parameters?: any[],
async executeQuery(transaction: AbstractTransaction, sqlStmt: string, parameters?: Parameters,
options?: {
prepareOptions?: PrepareOptions,
executeOptions?: ExecuteQueryOptions
}): Promise<AbstractResultSet> {
this.check();

const statement = await this.prepare(transaction, sqlStmt, options && options.prepareOptions);
const statement = await this.prepare(transaction, sqlStmt, adjustPrepareOptions(options?.prepareOptions, parameters));
try {
const resultSet = await statement.executeQuery(transaction, parameters, options && options.executeOptions);
const resultSet = await statement.executeQuery(transaction, parameters, options?.executeOptions);
resultSet.diposeStatementOnClose = true;
return resultSet;
}
Expand Down Expand Up @@ -206,8 +210,15 @@ export abstract class AbstractAttachment implements Attachment {
async prepare(transaction: AbstractTransaction, sqlStmt: string, options?: PrepareOptions): Promise<AbstractStatement> {
this.check();

const statement = await this.internalPrepare(transaction, sqlStmt,
const parsed = options?.namedParams ? parseParams(sqlStmt) : undefined;

const statement = await this.internalPrepare(transaction, parsed ? parsed.sqlStmt : sqlStmt,
options || this.defaultPrepareOptions || this.client!.defaultPrepareOptions);

if (parsed?.paramNames) {
statement.paramNames = parsed.paramNames;
}

this.statements.add(statement);
return statement;
}
Expand Down
116 changes: 116 additions & 0 deletions packages/node-firebird-driver/src/lib/impl/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
type State = 'DEFAULT' | 'QUOTE' | 'COMMENT' | 'LINECOMMENT';

const isValidParamName = (ch: string) => {
const CODE_A = 'a'.charCodeAt(0);
const CODE_Z = 'z'.charCodeAt(0);
const CODE_A_UPPER = 'A'.charCodeAt(0);
const CODE_Z_UPPER = 'Z'.charCodeAt(0);
const CODE_0 = '0'.charCodeAt(0);
const CODE_9 = '9'.charCodeAt(0);
const c = ch.charCodeAt(0);
return (c >= CODE_A && c <= CODE_Z)
|| (c >= CODE_A_UPPER && c <= CODE_Z_UPPER)
|| (c >= CODE_0 && c <= CODE_9)
|| (ch === '_');
};

export interface IParseResult {
sqlStmt: string;
paramNames?: string[];
};

export const parseParams = (sqlStmt: string): IParseResult => {
let i = 0;
let state: State = 'DEFAULT';
let quoteChar = '';
const params: [number, number][] = [];

while (i < sqlStmt.length - 1) {
// Get the current token and a look-ahead
const c = sqlStmt[i];
const nc = sqlStmt[i + 1];

// Now act based on the current state
switch (state) {
case 'DEFAULT':
switch (c) {
case '"':
case '\'':
quoteChar = c;
state = 'QUOTE';
break;

case ':': {
const paramStart = i + 1;
for (i = paramStart; i < sqlStmt.length && isValidParamName(sqlStmt[i]); i++);
if (i === paramStart) {
throw new Error(`SQL syntax error. No param name found at position ${i}.`);
}
params.push([paramStart, i]);
i--;
break;
}

case '/':
if (nc === '*') {
i++;
state = 'COMMENT';
}
break;

case '-':
if (nc === '-') {
i++;
state = 'LINECOMMENT';
}
break;
}
break;

case 'COMMENT':
if (c === '*' && nc === '/') {
i++;
state = 'DEFAULT';
}
break;

case 'LINECOMMENT':
if (nc === '\n' || nc === '\r') {
i++;
state = 'DEFAULT';
}
break;

case 'QUOTE':
if (c === quoteChar) {
if (nc === quoteChar) {
i++;
} else {
state = 'DEFAULT';
}
}
break;
}

i++;
}

if (state !== 'DEFAULT' && state !== 'LINECOMMENT') {
throw new Error('SQL syntax error');
}

const paramNames = params.map( ([start, end]) => sqlStmt.slice(start, end) );

return paramNames.length ?
{
sqlStmt: params
.map( ([start], idx) => sqlStmt.slice(idx ? params[idx - 1][1] : 0, start - 1) )
.concat(sqlStmt.slice(params[params.length - 1][1]))
.join('?'),
paramNames
}
:
{
sqlStmt
};
};
31 changes: 21 additions & 10 deletions packages/node-firebird-driver/src/lib/impl/statement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import {
ExecuteOptions,
ExecuteQueryOptions,
FetchOptions,
Statement
Statement,
Parameters,
NamedParameters
} from '..';


/** AbstractStatement implementation. */
export abstract class AbstractStatement implements Statement {
resultSet?: AbstractResultSet;
paramNames?: string[];

abstract getExecPathText(): Promise<string | undefined>;

Expand All @@ -34,6 +37,14 @@ export abstract class AbstractStatement implements Statement {
protected constructor(public attachment?: AbstractAttachment) {
}

protected namedParameters2Array(namedParameters: NamedParameters): any[] {
return this.paramNames ? this.paramNames.map( p => namedParameters[p] ?? null ) : [];
}

protected adjustParameters(parameters?: Parameters): (any[] | undefined) {
return Array.isArray(parameters) ? parameters : parameters ? this.namedParameters2Array(parameters) : undefined;
}

/** Disposes this statement's resources. */
async dispose(): Promise<void> {
this.check();
Expand All @@ -56,25 +67,25 @@ export abstract class AbstractStatement implements Statement {
}

/** Executes a prepared statement that has no result set. */
async execute(transaction: AbstractTransaction, parameters?: any[], options?: ExecuteOptions): Promise<void> {
async execute(transaction: AbstractTransaction, parameters?: Parameters, options?: ExecuteOptions): Promise<void> {
this.check();

//// TODO: check opened resultSet.
await this.internalExecute(transaction, parameters,
await this.internalExecute(transaction, this.adjustParameters(parameters),
options || this.attachment!.defaultExecuteOptions || this.attachment!.client!.defaultExecuteOptions);
}

/** Executes a statement that returns a single record as [col1, col2, ..., colN]. */
async executeSingleton(transaction: AbstractTransaction, parameters?: any[], options?: ExecuteOptions): Promise<any[]> {
async executeSingleton(transaction: AbstractTransaction, parameters?: Parameters, options?: ExecuteOptions): Promise<any[]> {
this.check();

//// TODO: check opened resultSet.
return await this.internalExecute(transaction, parameters,
return await this.internalExecute(transaction, this.adjustParameters(parameters),
options || this.attachment!.defaultExecuteOptions || this.attachment!.client!.defaultExecuteOptions);
}

/** Executes a statement that returns a single record as an object. */
async executeSingletonAsObject<T extends object>(transaction: AbstractTransaction, parameters?: any[],
async executeSingletonAsObject<T extends object>(transaction: AbstractTransaction, parameters?: Parameters,
options?: ExecuteOptions): Promise<T> {
this.check();

Expand All @@ -93,23 +104,23 @@ export abstract class AbstractStatement implements Statement {
}

/** Executes a statement that returns a single record as [col1, col2, ..., colN]. */
async executeReturning(transaction: AbstractTransaction, parameters?: any[], options?: ExecuteOptions): Promise<any[]> {
async executeReturning(transaction: AbstractTransaction, parameters?: Parameters, options?: ExecuteOptions): Promise<any[]> {
return await this.executeSingleton(transaction, parameters, options);
}

/** Executes a statement that returns a single record as an object. */
async executeReturningAsObject<T extends object>(transaction: AbstractTransaction, parameters?: any[],
async executeReturningAsObject<T extends object>(transaction: AbstractTransaction, parameters?: Parameters,
options?: ExecuteOptions): Promise<T> {
return await this.executeSingletonAsObject<T>(transaction, parameters, options);
}

/** Executes a prepared statement that has result set. */
async executeQuery(transaction: AbstractTransaction, parameters?: any[], options?: ExecuteQueryOptions):
async executeQuery(transaction: AbstractTransaction, parameters?: Parameters, options?: ExecuteQueryOptions):
Promise<AbstractResultSet> {
this.check();

//// TODO: check opened resultSet.
const resultSet = await this.internalExecuteQuery(transaction, parameters,
const resultSet = await this.internalExecuteQuery(transaction, this.adjustParameters(parameters),
options || this.attachment!.defaultExecuteQueryOptions || this.attachment!.client!.defaultExecuteQueryOptions);
this.resultSet = resultSet;
return resultSet;
Expand Down
Loading