diff --git a/packages/node-firebird-driver/src/lib/impl/attachment.ts b/packages/node-firebird-driver/src/lib/impl/attachment.ts index 36006f4..4912639 100644 --- a/packages/node-firebird-driver/src/lib/impl/attachment.ts +++ b/packages/node-firebird-driver/src/lib/impl/attachment.ts @@ -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 { @@ -78,16 +82,16 @@ 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 { 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(); @@ -95,14 +99,14 @@ export abstract class AbstractAttachment implements Attachment { } /** Executes a statement that returns a single record. */ - async executeSingleton(transaction: AbstractTransaction, sqlStmt: string, parameters?: Array, + async executeSingleton(transaction: AbstractTransaction, sqlStmt: string, parameters?: Parameters, options?: { prepareOptions?: PrepareOptions, executeOptions?: ExecuteOptions }): Promise> { 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); } @@ -112,14 +116,14 @@ export abstract class AbstractAttachment implements Attachment { } /** Executes a statement that returns a single record in object form. */ - async executeSingletonAsObject(transaction: AbstractTransaction, sqlStmt: string, parameters?: any[], + async executeSingletonAsObject(transaction: AbstractTransaction, sqlStmt: string, parameters?: Parameters, options?: { prepareOptions?: PrepareOptions, executeOptions?: ExecuteOptions }): Promise { 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); } @@ -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, + async executeReturning(transaction: AbstractTransaction, sqlStmt: string, parameters?: Parameters, options?: { prepareOptions?: PrepareOptions, executeOptions?: ExecuteOptions @@ -138,7 +142,7 @@ export abstract class AbstractAttachment implements Attachment { } /** Executes a statement that returns a single record in object form. */ - async executeReturningAsObject(transaction: AbstractTransaction, sqlStmt: string, parameters?: any[], + async executeReturningAsObject(transaction: AbstractTransaction, sqlStmt: string, parameters?: Parameters, options?: { prepareOptions?: PrepareOptions, executeOptions?: ExecuteOptions @@ -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 { 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; } @@ -206,8 +210,15 @@ export abstract class AbstractAttachment implements Attachment { async prepare(transaction: AbstractTransaction, sqlStmt: string, options?: PrepareOptions): Promise { 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; } diff --git a/packages/node-firebird-driver/src/lib/impl/parser.ts b/packages/node-firebird-driver/src/lib/impl/parser.ts new file mode 100644 index 0000000..b00fa2d --- /dev/null +++ b/packages/node-firebird-driver/src/lib/impl/parser.ts @@ -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 + }; +}; \ No newline at end of file diff --git a/packages/node-firebird-driver/src/lib/impl/statement.ts b/packages/node-firebird-driver/src/lib/impl/statement.ts index 0a982f8..d813791 100644 --- a/packages/node-firebird-driver/src/lib/impl/statement.ts +++ b/packages/node-firebird-driver/src/lib/impl/statement.ts @@ -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; @@ -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 { this.check(); @@ -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 { + async execute(transaction: AbstractTransaction, parameters?: Parameters, options?: ExecuteOptions): Promise { 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 { + async executeSingleton(transaction: AbstractTransaction, parameters?: Parameters, options?: ExecuteOptions): Promise { 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(transaction: AbstractTransaction, parameters?: any[], + async executeSingletonAsObject(transaction: AbstractTransaction, parameters?: Parameters, options?: ExecuteOptions): Promise { this.check(); @@ -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 { + async executeReturning(transaction: AbstractTransaction, parameters?: Parameters, options?: ExecuteOptions): Promise { return await this.executeSingleton(transaction, parameters, options); } /** Executes a statement that returns a single record as an object. */ - async executeReturningAsObject(transaction: AbstractTransaction, parameters?: any[], + async executeReturningAsObject(transaction: AbstractTransaction, parameters?: Parameters, options?: ExecuteOptions): Promise { return await this.executeSingletonAsObject(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 { 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; diff --git a/packages/node-firebird-driver/src/lib/index.ts b/packages/node-firebird-driver/src/lib/index.ts index e7bb8e6..c0e9162 100644 --- a/packages/node-firebird-driver/src/lib/index.ts +++ b/packages/node-firebird-driver/src/lib/index.ts @@ -74,6 +74,7 @@ export interface TransactionOptions { /** PrepareOptions interface. */ export interface PrepareOptions { + namedParams?: boolean; } /** ExecuteOptions interface. */ @@ -117,21 +118,21 @@ export interface Attachment { }): Promise; /** Executes a statement that has no result set. */ - execute(transaction: Transaction, sqlStmt: string, parameters?: any[], + execute(transaction: Transaction, sqlStmt: string, parameters?: Parameters, options?: { prepareOptions?: PrepareOptions, executeOptions?: ExecuteOptions }): Promise; /** Executes a statement that returns a single record as [col1, col2, ..., colN]. */ - executeSingleton(transaction: Transaction, sqlStmt: string, parameters?: any[], + executeSingleton(transaction: Transaction, sqlStmt: string, parameters?: Parameters, options?: { prepareOptions?: PrepareOptions, executeOptions?: ExecuteOptions }): Promise; /** Executes a statement that returns a single record as an object. */ - executeSingletonAsObject(transaction: Transaction, sqlStmt: string, parameters?: any[], + executeSingletonAsObject(transaction: Transaction, sqlStmt: string, parameters?: Parameters, options?: { prepareOptions?: PrepareOptions, executeOptions?: ExecuteOptions @@ -141,7 +142,7 @@ export interface Attachment { * Executes a statement that returns a single record as [col1, col2, ..., colN]. * @deprecated since version 2.4.0 and will be removed in next major version. Replaced by executeSingleton. */ - executeReturning(transaction: Transaction, sqlStmt: string, parameters?: any[], + executeReturning(transaction: Transaction, sqlStmt: string, parameters?: Parameters, options?: { prepareOptions?: PrepareOptions, executeOptions?: ExecuteOptions @@ -151,14 +152,14 @@ export interface Attachment { * Executes a statement that returns a single record as an object. * @deprecated since version 2.4.0 and will be removed in next major version. Replaced by executeSingletonAsObject. */ - executeReturningAsObject(transaction: Transaction, sqlStmt: string, parameters?: any[], + executeReturningAsObject(transaction: Transaction, sqlStmt: string, parameters?: Parameters, options?: { prepareOptions?: PrepareOptions, executeOptions?: ExecuteOptions }): Promise; /** Executes a statement that has result set. */ - executeQuery(transaction: Transaction, sqlStmt: string, parameters?: any[], + executeQuery(transaction: Transaction, sqlStmt: string, parameters?: Parameters, options?: { prepareOptions?: PrepareOptions, executeOptions?: ExecuteQueryOptions @@ -203,6 +204,12 @@ export interface Transaction { readonly isValid: boolean; } +export interface NamedParameters { + [name: string]: any; +} + +export type Parameters = any[] | NamedParameters; + /** Statement interface. */ export interface Statement { /** Disposes this statement's resources. */ @@ -212,29 +219,29 @@ export interface Statement { executeTransaction(transaction: Transaction): Promise; /** Executes a prepared statement that has no result set. */ - execute(transaction: Transaction, parameters?: any[], options?: ExecuteOptions): Promise; + execute(transaction: Transaction, parameters?: Parameters, options?: ExecuteOptions): Promise; /** Executes a statement that returns a single record as [col1, col2, ..., colN]. */ - executeSingleton(transaction: Transaction, parameters?: any[], executeOptions?: ExecuteOptions): Promise; + executeSingleton(transaction: Transaction, parameters?: Parameters, executeOptions?: ExecuteOptions): Promise; /** Executes a statement that returns a single record as an object. */ - executeSingletonAsObject(transaction: Transaction, parameters?: any[], executeOptions?: ExecuteOptions): Promise; + executeSingletonAsObject(transaction: Transaction, parameters?: Parameters, executeOptions?: ExecuteOptions): Promise; /** * Executes a statement that returns a single record as [col1, col2, ..., colN]. * @deprecated since version 2.4.0 and will be removed in next major version. Replaced by executeSingleton. */ - executeReturning(transaction: Transaction, parameters?: any[], executeOptions?: ExecuteOptions): Promise; + executeReturning(transaction: Transaction, parameters?: Parameters, executeOptions?: ExecuteOptions): Promise; /** * Executes a statement that returns a single record as an object. * @deprecated since version 2.4.0 and will be removed in next major version. Replaced by executeSingletonAsObject. */ - executeReturningAsObject(transaction: Transaction, parameters?: any[], + executeReturningAsObject(transaction: Transaction, parameters?: Parameters, options?: ExecuteOptions): Promise; /** Executes a prepared statement that has result set. */ - executeQuery(transaction: Transaction, parameters?: any[], options?: ExecuteQueryOptions): Promise; + executeQuery(transaction: Transaction, parameters?: Parameters, options?: ExecuteQueryOptions): Promise; /** * Set cursor name of a SELECT ... FOR UPDATE statement. diff --git a/packages/node-firebird-driver/src/test/parser.test.ts b/packages/node-firebird-driver/src/test/parser.test.ts new file mode 100644 index 0000000..4932ffa --- /dev/null +++ b/packages/node-firebird-driver/src/test/parser.test.ts @@ -0,0 +1,32 @@ +import { parseParams } from '../lib/impl/parser'; + +describe('Parameters parser tests', () => { + test('no params in sql', () => { + [ + 'SELECT id FROM some_table WHERE id > 0', + '', + '\n', + '\n\n', + 'SELECT \':this_is_not_param\' FROM some_table WHERE id > 0', + 'SELECT /* :this_is_not_param */ FROM some_table WHERE id > 0', + 'SELECT id FROM some_table WHERE id > 0 --:this_is_not_param' + ].forEach( q => { + expect(parseParams(q).paramNames).toBeUndefined(); + expect(parseParams(q).sqlStmt).toEqual(q); + }); + }); + + test('named params', () => { + let p = parseParams('SELECT id FROM some_table WHERE id = :id'); + expect(p.paramNames).toEqual(['id']); + expect(p.sqlStmt).toEqual('SELECT id FROM some_table WHERE id = ?'); + + p = parseParams('SELECT :id FROM some_table WHERE id = :id'); + expect(p.paramNames).toEqual(['id', 'id']); + expect(p.sqlStmt).toEqual('SELECT ? FROM some_table WHERE id = ?'); + + p = parseParams('SELECT :name FROM some_table WHERE id > :start_id AND id < :end_id'); + expect(p.paramNames).toEqual(['name', 'start_id', 'end_id']); + expect(p.sqlStmt).toEqual('SELECT ? FROM some_table WHERE id > ? AND id < ?'); + }); +}); \ No newline at end of file diff --git a/packages/node-firebird-driver/src/test/tests.ts b/packages/node-firebird-driver/src/test/tests.ts index f516ece..ef35f2e 100644 --- a/packages/node-firebird-driver/src/test/tests.ts +++ b/packages/node-firebird-driver/src/test/tests.ts @@ -355,6 +355,70 @@ export function runCommonTests(client: Client) { }); }); + describe('Attachment with named params', () => { + test('#execute()', async () => { + const attachment = await client.createDatabase(getTempFile('Attachment-execute-np.fdb')); + const transaction = await attachment.startTransaction(); + + await attachment.execute(transaction, 'create table t1 (n1 integer)'); + await transaction.commitRetaining(); + + await attachment.execute(transaction, 'insert into t1 (n1) values (:v)', { v: 1 }); + + await transaction.commit(); + await attachment.dropDatabase(); + }); + + test('#executeQuery()', async () => { + const attachment = await client.createDatabase(getTempFile('Attachment-executeQuery-np.fdb')); + const transaction = await attachment.startTransaction(); + + await attachment.execute(transaction, 'create table t1 (n1 integer)'); + await transaction.commitRetaining(); + + await attachment.execute(transaction, 'insert into t1 (n1) values (:val)', { val: 17 }); + + const resultSet = await attachment.executeQuery(transaction, 'select n1 from t1'); + expect(resultSet.isValid).toBeTruthy(); + expect((await resultSet.fetchAsObject<{ N1: number }>())[0].N1).toEqual(17); + await resultSet.close(); + expect(resultSet.isValid).toBeFalsy(); + + await transaction.commit(); + await attachment.dropDatabase(); + }); + + test('#executeSingleton()', async () => { + const attachment = await client.createDatabase(getTempFile('Attachment-executeSingleton-np.fdb')); + const transaction = await attachment.startTransaction(); + + await attachment.execute(transaction, 'create table t1 (n1 integer)'); + await transaction.commitRetaining(); + + const result = await attachment.executeSingleton(transaction, 'insert into t1 values (:n) returning n1', { n: 222 }); + expect(result.length).toBe(1); + expect(result[0]).toBe(222); + + await transaction.commit(); + await attachment.dropDatabase(); + }); + + test('#executeSingletonAsObject()', async () => { + const attachment = await client.createDatabase(getTempFile('Attachment-executeSingletonAsObject-np.fdb')); + const transaction = await attachment.startTransaction(); + + await attachment.execute(transaction, 'create table t1 (n1 integer)'); + await transaction.commitRetaining(); + + const output = await attachment.executeSingletonAsObject<{ N1: number }>(transaction, + 'insert into t1 values (:v) returning n1', { v: 11 }); + expect(output.N1).toBe(11); + + await transaction.commit(); + await attachment.dropDatabase(); + }); + }); + describe('Transaction', () => { test('#commit()', async () => { const attachment = await client.createDatabase(getTempFile('Transaction-commit.fdb')); @@ -539,6 +603,94 @@ export function runCommonTests(client: Client) { }); }); + describe('Statement with named params', () => { + test('#execute()', async () => { + const attachment = await client.createDatabase(getTempFile('Statement-execute-np.fdb')); + const transaction = await attachment.startTransaction(); + + const statement1 = await attachment.prepare(transaction, 'create table t1 (n1 integer)'); + await statement1.execute(transaction); + await statement1.dispose(); + await transaction.commitRetaining(); + + const statement2 = await attachment.prepare(transaction, 'insert into t1 (n1) values (:n)', { namedParams: true }); + await statement2.execute(transaction, [1]); + await statement2.execute(transaction, [null]); + await statement2.execute(transaction, [10]); + await statement2.execute(transaction, [100]); + await statement2.execute(transaction, { n: 1 }); + await statement2.execute(transaction, { n: null }); + await statement2.execute(transaction, { n: 10 }); + await statement2.execute(transaction, { n: 100 }); + expect(statement2.isValid).toBeTruthy(); + await statement2.dispose(); + expect(statement2.isValid).toBeFalsy(); + + const rs = await attachment.executeQuery(transaction, + `select sum(n1) || ', ' || count(n1) || ', ' || count(*) ret from t1`); + const ret = await rs.fetchAsObject<{ RET: string }>(); + await rs.close(); + + expect(ret[0].RET).toStrictEqual('222, 6, 8'); + + await transaction.commit(); + await attachment.dropDatabase(); + }); + + test('#execute() multiple params', async () => { + const attachment = await client.createDatabase(getTempFile('Statement-execute-np.fdb')); + const transaction = await attachment.startTransaction(); + + const statement1 = await attachment.prepare(transaction, + 'create table t1 (n1 integer, n2 double precision, s varchar(60), t timestamp)'); + await statement1.execute(transaction); + await statement1.dispose(); + await transaction.commitRetaining(); + + const someDate = new Date(); + + const statement2 = await attachment.prepare(transaction, + 'insert into t1 (n1, n2, s, t) values (:n, :n, :str, :date)', { namedParams: true }); + await statement2.execute(transaction, { n: 200, str: 'this is a string', date: someDate }); + expect(statement2.isValid).toBeTruthy(); + await statement2.dispose(); + expect(statement2.isValid).toBeFalsy(); + + const rs = await attachment.executeQuery(transaction, + `select * from t1`); + const ret = await rs.fetchAsObject<{ N1: number; N2: number; S: string; T: Date }>(); + await rs.close(); + + expect(ret[0].N1).toStrictEqual(200); + expect(ret[0].N2).toStrictEqual(200); + expect(ret[0].S).toStrictEqual('this is a string'); + expect(ret[0].T.getTime()).toStrictEqual(someDate.getTime()); + + await transaction.commit(); + await attachment.dropDatabase(); + }); + + test('#executeSingleton()', async () => { + const attachment = await client.createDatabase(getTempFile('Attachment-executeSingleton-np.fdb')); + const transaction = await attachment.startTransaction(); + + await attachment.execute(transaction, 'create table t1 (n1 integer)'); + await transaction.commitRetaining(); + + const statement = await attachment.prepare(transaction, 'insert into t1 values (:value) returning n1, n1 * 2', { namedParams: true }); + + const result = await statement.executeSingleton(transaction, { value: 11 }); + expect(result.length).toBe(2); + expect(result[0]).toBe(11); + expect(result[1]).toBe(11 * 2); + + await statement.dispose(); + + await transaction.commit(); + await attachment.dropDatabase(); + }); + }); + describe('ResultSet', () => { test('#fetch()', async () => { const attachment = await client.createDatabase(getTempFile('ResultSet-fetch.fdb'));