feat(spanner): support setting read lock mode by skuruppu · Pull Request #2388 · googleapis/nodejs-spanner · GitHub
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
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?

@surbhigarg92 surbhigarg92 Aug 22, 2025

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.

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
Loading
Loading