diff --git a/.gitignore b/.gitignore index d4f03a0df..14050d4e4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ system-test/*key.json .DS_Store package-lock.json __pycache__ +.vscode \ No newline at end of file diff --git a/src/database.ts b/src/database.ts index 970e6d8c2..c35f1557e 100644 --- a/src/database.ts +++ b/src/database.ts @@ -224,6 +224,13 @@ export interface GetIamPolicyOptions { gaxOptions?: CallOptions; } +/** + * @typedef {object} GetTransactionOptions + * * @property {boolean} [optimisticLock] The optimistic lock a + * {@link Transaction} should use while running. + */ +export type GetTransactionOptions = Omit; + export type CreateSessionCallback = ResourceCallback< Session, spannerClient.spanner.v1.ISession @@ -1989,16 +1996,36 @@ class Database extends common.GrpcServiceObject { * }); * ``` */ - getTransaction(): Promise<[Transaction]>; + getTransaction( + optionsOrCallback?: GetTransactionOptions + ): Promise<[Transaction]>; getTransaction(callback: GetTransactionCallback): void; getTransaction( + optionsOrCallback?: GetTransactionOptions | GetTransactionCallback, callback?: GetTransactionCallback ): void | Promise<[Transaction]> { + const cb = + typeof optionsOrCallback === 'function' + ? (optionsOrCallback as GetTransactionCallback) + : callback; + const options = + typeof optionsOrCallback === 'object' && optionsOrCallback + ? (optionsOrCallback as GetTransactionOptions) + : {}; this.pool_.getSession((err, session, transaction) => { + if (options.requestOptions) { + transaction!.requestOptions = Object.assign( + transaction!.requestOptions || {}, + options.requestOptions + ); + } + if (options.optimisticLock) { + transaction!.useOptimisticLock(); + } if (!err) { this._releaseOnEnd(session!, transaction!); } - callback!(err as grpc.ServiceError | null, transaction); + cb!(err as grpc.ServiceError | null, transaction); }); } diff --git a/system-test/spanner.ts b/system-test/spanner.ts index 8849f6e2b..041920a77 100644 --- a/system-test/spanner.ts +++ b/system-test/spanner.ts @@ -8894,6 +8894,24 @@ describe('Spanner', () => { mismatchedColumnError(done, DATABASE, googleSqlTable); }); + it('GOOGLE_STANDARD_SQL should use getTransaction for executing sql', async () => { + const transaction = ( + await DATABASE.getTransaction({optimisticLock: true}) + )[0]; + + try { + const [rows] = await transaction!.run('SELECT * FROM TxnTable'); + assert.strictEqual(rows.length, googleSqlRecords.length); + } catch (err) { + // flaky failures are acceptable here as long as the error is not due to a lock conflict + if ((err as grpc.ServiceError).code === grpc.status.ABORTED) { + assert.ok(err, 'Transaction is aborted'); + } + } finally { + transaction.end(); + } + }); + it('POSTGRESQL should throw an error for mismatched columns', function (done) { if (IS_EMULATOR_ENABLED) { this.skip(); diff --git a/test/spanner.ts b/test/spanner.ts index 38f1105d3..293b8bbd2 100644 --- a/test/spanner.ts +++ b/test/spanner.ts @@ -3286,6 +3286,29 @@ describe('Spanner with mock server', () => { }); }); + it('should use optimistic lock and transaction tag for getTransaction', async () => { + const database = newTestDatabase(); + const promise = await database.getTransaction({ + optimisticLock: true, + requestOptions: {transactionTag: 'transaction-tag'}, + }); + const transaction = promise[0]; + await transaction.run('SELECT 1').then(results => { + const request = spannerMock.getRequests().find(val => { + return (val as v1.ExecuteSqlRequest).sql; + }) as v1.ExecuteSqlRequest; + assert.ok(request, 'no ExecuteSqlRequest found'); + assert.strictEqual( + request.transaction!.begin!.readWrite!.readLockMode, + 'OPTIMISTIC' + ); + assert.strictEqual( + request.requestOptions?.transactionTag, + 'transaction-tag' + ); + }); + }); + it('should reuse a session for optimistic and pessimistic locks', async () => { const database = newTestDatabase({min: 1, max: 1}); let session1;