Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.
Merged
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
4 changes: 2 additions & 2 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
google as spannerClient,
} from '../protos/protos';
import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel;
import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import {
CreateDatabaseCallback,
CreateDatabaseOptions,
Expand Down Expand Up @@ -265,8 +266,6 @@ export interface GetIamPolicyOptions {

/**
* @typedef {object} GetTransactionOptions
* * @property {boolean} [optimisticLock] The optimistic lock a
* {@link Transaction} should use while running.
*/
export type GetTransactionOptions = Omit<RunTransactionOptions, 'timeout'>;

Expand Down Expand Up @@ -325,6 +324,7 @@ export interface RestoreOptions {
}

export interface WriteAtLeastOnceOptions extends CallOptions {
readLockMode?: ReadLockMode;
isolationLevel?: IsolationLevel;
}

Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
} from 'google-gax';
import {google, google as instanceAdmin} from '../protos/protos';
import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel;
import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import {
PagedOptions,
PagedResponse,
Expand Down Expand Up @@ -159,7 +160,10 @@ export interface SpannerOptions extends GrpcClientOptions {
sslCreds?: grpc.ChannelCredentials;
routeToLeaderEnabled?: boolean;
directedReadOptions?: google.spanner.v1.IDirectedReadOptions | null;
defaultTransactionOptions?: Pick<RunTransactionOptions, 'isolationLevel'>;
defaultTransactionOptions?: Pick<
RunTransactionOptions,
'isolationLevel' | 'readLockMode'
>;
observabilityOptions?: ObservabilityOptions;
disableBuiltInMetrics?: boolean;
interceptors?: any[];
Expand Down Expand Up @@ -432,6 +436,7 @@ class Spanner extends GrpcService {
? options.defaultTransactionOptions
: {
isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED,
};
delete options.defaultTransactionOptions;

Expand Down
8 changes: 8 additions & 0 deletions src/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
} from './instrument';
import {google} from '../protos/protos';
import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel;
import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;

export type Key = string | string[];

Expand All @@ -56,6 +57,7 @@ interface MutateRowsOptions extends CommitOptions {
requestOptions?: Omit<IRequestOptions, 'requestTag'>;
excludeTxnFromChangeStreams?: boolean;
isolationLevel?: IsolationLevel;
readLockMode?: ReadLockMode;
}

export type DeleteRowsCallback = CommitCallback;
Expand Down Expand Up @@ -1110,11 +1112,17 @@ class Table {
? options.isolationLevel
: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED;

const readLockMode =
'readLockMode' in options
? options.readLockMode
: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED;

this.database.runTransaction(
Comment thread
skuruppu marked this conversation as resolved.
{
requestOptions: requestOptions,
excludeTxnFromChangeStreams: excludeTxnFromChangeStreams,
isolationLevel: isolationLevel,
readLockMode: readLockMode,
},
(err, transaction) => {
if (err) {
Expand Down
6 changes: 6 additions & 0 deletions src/transaction-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {Database} from './database';
import {google} from '../protos/protos';
import IRequestOptions = google.spanner.v1.IRequestOptions;
import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel;
import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;

// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonProtos = require('../protos/protos.json');
Expand All @@ -45,9 +46,13 @@ const RetryInfo = Root.fromJSON(jsonProtos).lookup('google.rpc.RetryInfo');
export interface RunTransactionOptions {
timeout?: number;
requestOptions?: Pick<IRequestOptions, 'transactionTag'>;
/**
* @deprecated Use readLockMode instead.
*/
optimisticLock?: boolean;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this is. breaking change, even though this property might not be used. We can mark this deprecated instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea. I added it back with a deprecated annotation. But should I also be adding back the logic in src/transaction.ts to actually set the read lock mode based on this? Or just leave it as a no-op if someone sets it?

Copy link
Copy Markdown
Contributor

@surbhigarg92 surbhigarg92 Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving no-op should be fine, we already verified that there are no users for this feature. Just in-case if someone is using , maybe in their testing etc, in that case it should not break their application.

WDYT ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that should be fine. Given that this was something prepared for preview years ago and never released, it should be safe.

excludeTxnFromChangeStreams?: boolean;
isolationLevel?: IsolationLevel;
readLockMode?: ReadLockMode;
}

/**
Expand Down Expand Up @@ -130,6 +135,7 @@ export abstract class Runner<T> {
const defaults = {
timeout: 3600000,
isolationLevel: IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
readLockMode: ReadLockMode.READ_LOCK_MODE_UNSPECIFIED,
};

this.options = Object.assign(defaults, options);
Expand Down
17 changes: 10 additions & 7 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2946,6 +2946,8 @@ export class Transaction extends Dml {
* (when any needed locks are acquired). The validation process succeeds only
* if there are no conflicting committed transactions (that committed
* mutations to the read data at a commit timestamp after the read timestamp).
*
* @deprecated Set readLockMode through setReadWriteTransactionOptions instead.
*/
useOptimisticLock(): void {
this._options.readWrite!.readLockMode = ReadLockMode.OPTIMISTIC;
Expand All @@ -2964,12 +2966,6 @@ export class Transaction extends Dml {
}

setReadWriteTransactionOptions(options: RunTransactionOptions) {
/**
* Set optimistic concurrency control for the transaction.
*/
if (options?.optimisticLock) {
this._options.readWrite!.readLockMode = ReadLockMode.OPTIMISTIC;
}
/**
* Set option excludeTxnFromChangeStreams=true to exclude read/write transactions
* from being tracked in change streams.
Expand All @@ -2978,11 +2974,18 @@ export class Transaction extends Dml {
this._options.excludeTxnFromChangeStreams = true;
}
/**
* Set isolation level .
* Set isolation level.
*/
this._options.isolationLevel = options?.isolationLevel
? options?.isolationLevel
: this._getSpanner().defaultTransactionOptions.isolationLevel;

/**
* Set read lock mode.
*/
this._options.readWrite!.readLockMode = options?.readLockMode
? options?.readLockMode
: this._getSpanner().defaultTransactionOptions.readLockMode;
}
}

Expand Down
3 changes: 2 additions & 1 deletion system-test/spanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {google} from '../protos/protos';
import CreateDatabaseMetadata = google.spanner.admin.database.v1.CreateDatabaseMetadata;
import CreateBackupMetadata = google.spanner.admin.database.v1.CreateBackupMetadata;
import CreateInstanceConfigMetadata = google.spanner.admin.instance.v1.CreateInstanceConfigMetadata;
import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
const singer = require('../test/data/singer');
const music = singer.examples.spanner.music;
import {util} from 'protobufjs';
Expand Down Expand Up @@ -9152,7 +9153,7 @@ describe('Spanner', () => {

it('GOOGLE_STANDARD_SQL should use getTransaction for executing sql', async () => {
const transaction = (
await DATABASE.getTransaction({optimisticLock: true})
await DATABASE.getTransaction({readLockMode: ReadLockMode.OPTIMISTIC})
)[0];

try {
Expand Down
22 changes: 22 additions & 0 deletions test/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3402,6 +3402,17 @@ describe('Database', () => {
assert.strictEqual(options, fakeOptions);
});

it('should optionally accept runner `option` readLockMode', async () => {
const fakeOptions = {
readLockMode: ReadLockMode.PESSIMISTIC,
};

await database.runTransaction(fakeOptions, assert.ifError);

const options = fakeTransactionRunner.calledWith_[3];
assert.strictEqual(options, fakeOptions);
});

it('should release the session when finished', done => {
const releaseStub = (
sandbox.stub(fakeSessionFactory, 'release') as sinon.SinonStub
Expand Down Expand Up @@ -3519,6 +3530,17 @@ describe('Database', () => {
assert.strictEqual(options, fakeOptions);
});

it('should optionally accept runner `option` readLockMode', async () => {
const fakeOptions = {
readLockMode: ReadLockMode.PESSIMISTIC,
};

await database.runTransactionAsync(fakeOptions, assert.ifError);

const options = fakeAsyncTransactionRunner.calledWith_[3];
assert.strictEqual(options, fakeOptions);
});

it('should return the runners resolved value', async () => {
const fakeValue = {};

Expand Down
2 changes: 2 additions & 0 deletions test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
import {CLOUD_RESOURCE_HEADER, AFE_SERVER_TIMING_HEADER} from '../src/common';
import {MetricsTracerFactory} from '../src/metrics/metrics-tracer-factory';
import IsolationLevel = protos.google.spanner.v1.TransactionOptions.IsolationLevel;
import ReadLockMode = protos.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
const singer = require('./data/singer');
const music = singer.examples.spanner.music;

Expand Down Expand Up @@ -357,6 +358,7 @@ describe('Spanner', () => {
const fakeDefaultTxnOptions = {
defaultTransactionOptions: {
isolationLevel: IsolationLevel.REPEATABLE_READ,
readLockMode: ReadLockMode.PESSIMISTIC,
},
};

Expand Down
82 changes: 49 additions & 33 deletions test/spanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import Priority = google.spanner.v1.RequestOptions.Priority;
import TypeCode = google.spanner.v1.TypeCode;
import NullValue = google.protobuf.NullValue;
import IsolationLevel = google.spanner.v1.TransactionOptions.IsolationLevel;
import ReadLockMode = google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode;
import {SessionFactory} from '../src/session-factory';
import {MultiplexedSession} from '../src/multiplexed-session';
import {WriteAtLeastOnceOptions} from '../src/database';
Expand Down Expand Up @@ -601,7 +602,7 @@ describe('Spanner with mock server', () => {
const database = newTestDatabase();
await database.runTransactionAsync(
{
optimisticLock: true,
readLockMode: ReadLockMode.OPTIMISTIC,
requestOptions: {transactionTag: 'transaction-tag'},
},
async tx => {
Expand Down Expand Up @@ -5000,7 +5001,7 @@ describe('Spanner with mock server', () => {
const database = newTestDatabase();
await database.runTransactionAsync(
{
optimisticLock: true,
readLockMode: ReadLockMode.OPTIMISTIC,
},
async tx => {
await tx!.run(selectSql);
Expand Down Expand Up @@ -5105,7 +5106,7 @@ describe('Spanner with mock server', () => {
const database = newTestDatabase();
await database.runTransactionAsync(
{
optimisticLock: true,
readLockMode: ReadLockMode.OPTIMISTIC,
excludeTxnFromChangeStreams: true,
},
async tx => {
Expand Down Expand Up @@ -5167,22 +5168,27 @@ describe('Spanner with mock server', () => {

it('should use optimistic lock for runTransaction', done => {
const database = newTestDatabase();
database.runTransaction({optimisticLock: true}, async (err, tx) => {
assert.ifError(err);
await tx!.run(selectSql);
await tx!.commit();
await database.close();
database.runTransaction(
{
readLockMode: ReadLockMode.OPTIMISTIC,
},
async (err, tx) => {
assert.ifError(err);
await tx!.run(selectSql);
await tx!.commit();
await database.close();

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',
);
done();
});
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',
);
done();
},
);
});

it('should use exclude transaction from change stream for runTransaction', done => {
Expand Down Expand Up @@ -5234,7 +5240,7 @@ 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,
readLockMode: ReadLockMode.OPTIMISTIC,
requestOptions: {transactionTag: 'transaction-tag'},
});
const transaction = promise[0];
Expand Down Expand Up @@ -5275,11 +5281,16 @@ describe('Spanner with mock server', () => {
const database = newTestDatabase({min: 1, max: 1});
let session1;
let session2;
await database.runTransactionAsync({optimisticLock: true}, async tx => {
session1 = tx!.session.id;
await tx!.run(selectSql);
await tx.commit();
});
await database.runTransactionAsync(
{
readLockMode: ReadLockMode.OPTIMISTIC,
},
async tx => {
session1 = tx!.session.id;
await tx!.run(selectSql);
await tx.commit();
},
);
spannerMock.resetRequests();
await database.runTransactionAsync(async tx => {
session2 = tx!.session.id;
Expand Down Expand Up @@ -5435,15 +5446,20 @@ describe('Spanner with mock server', () => {
it('should use beginTransaction on retry with optimistic lock', async () => {
const database = newTestDatabase();
let attempts = 0;
await database.runTransactionAsync({optimisticLock: true}, async tx => {
await tx!.run(selectSql);
if (!attempts) {
spannerMock.abortTransaction(tx);
}
attempts++;
await tx!.run(insertSql);
await tx.commit();
});
await database.runTransactionAsync(
{
readLockMode: ReadLockMode.OPTIMISTIC,
},
async tx => {
await tx!.run(selectSql);
if (!attempts) {
spannerMock.abortTransaction(tx);
}
attempts++;
await tx!.run(insertSql);
await tx.commit();
},
);
await database.close();

const beginTxnRequest = spannerMock
Expand Down
Loading
Loading