Skip to content

feat: add support for Node wrapper with TypeScript and dual-publishing (ESM/CJS)#798

Open
surbhigarg92 wants to merge 13 commits into
mainfrom
node-spannerwrapper
Open

feat: add support for Node wrapper with TypeScript and dual-publishing (ESM/CJS)#798
surbhigarg92 wants to merge 13 commits into
mainfrom
node-spannerwrapper

Conversation

@surbhigarg92
Copy link
Copy Markdown

@surbhigarg92 surbhigarg92 commented Apr 27, 2026

This PR introduces a Node.js wrapper (spannerlib-node) for the Spanner shared library. It uses TypeScript and Node-API to provide a dual-published package (ESM and CommonJS) .

Key Highlights

  1. Dual Publishing: Successfully ships targeted builds inside build/esm/ and build/cjs/ mapped through an exports map.
  2. executeSql Support: Implemented the core method to execute SQL statements against the database in Node wrapper. Transactions, sessions, and other administrative methods will be built out in a follow-up PR.
  3. Raw Protobuf Rows: rows.next() returns raw ListValue objects, matching Java and .NET behaviors.
  4. Automated Native Build: Added a custom shell script to handle Go binary compilation on the fly, chained directly into the standard npm run build lifecycle.

Current Limitations
Windows CI: The Windows CI currently fails on compile-time static linking. We have deferred Windows support and platform-specific packaging to a dedicated task after this merges.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new Node.js wrapper for the Spanner shared library, providing three implementation approaches (N-API, Koffi, and IPC) along with a benchmarking suite. The review identified several critical issues, including hardcoded paths in build scripts, synchronous execution of potentially blocking operations, missing memory management for pinned objects, and incomplete type handling in the result parser. I recommend addressing these portability and performance concerns before merging.

I am having trouble creating individual review comments. Click here to see my feedback.

spannerlib/grpc-server/build-protos.sh (31)

high

Hardcoded absolute path to the Java gRPC plugin. This will fail on any machine other than the author's. Consider using an environment variable or ensuring the plugin is available in the system PATH.

spannerlib/grpc-server/build-protos.sh (40)

high

Hardcoded absolute path to the C# gRPC plugin. This breaks the portability of the build script and will cause CI/CD pipelines to fail.

spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc (303-321)

high

CloseRowsWrapper is implemented synchronously. Closing rows in a database driver often involves network communication to signal the end of a stream or release server-side resources. Calling this on the Node.js main thread can block the event loop, leading to performance degradation. It should be implemented using Napi::AsyncWorker.

spannerlib/wrappers/spannerlib-node/src/ffi/utils.ts (15-43)

high

The invokeAsync function accepts constructor1 and constructor2 (which should be renamed to refInstance and pinManager) but never uses them to register the pinner ID (result.r0) with the memory manager. This will lead to memory leaks in the Go shared library because pinned objects are never released when JavaScript objects are garbage collected.

spannerlib/wrappers/spannerlib-node/src/lib/connection.ts (43)

high

The session ID is hardcoded to "poc/dummy". Cloud Spanner requires a valid session ID for executing SQL. This will cause errors when interacting with a real Spanner instance. The session should be dynamically obtained from the pool.

spannerlib/wrappers/spannerlib-node/src/lib/rows.ts (24-39)

medium

The parseRowToObject function only handles a subset of Spanner types. It is missing support for bytesValue, structValue, and listValue. This could lead to data loss or undefined values being returned without warning for those types.

spannerlib/wrappers/spannerlib-node/src/lib/rows.ts (72)

medium

Fetching rows one by one (numRows: 1) is highly inefficient as it incurs a network round-trip for every single row. Consider using a larger batch size or making it configurable to improve throughput.

spannerlib/wrappers/spannerlib-nodejs-poc/benchmark/benchmark.js (31)

medium

The database path is hardcoded to a specific testing instance. This should be configurable via environment variables to allow running benchmarks in different environments.

const dbPath = process.env.SPANNER_DB_PATH || `projects/span-cloud-testing/instances/gargsurbhi-testing/databases/${dbName}`;

@surbhigarg92 surbhigarg92 force-pushed the node-spannerwrapper branch 2 times, most recently from 7044d92 to 16f434a Compare April 27, 2026 06:10
@surbhigarg92 surbhigarg92 changed the title Node spannerwrapper feat: add support for Node wrapper with TypeScript and dual-publishing (ESM/CJS) Apr 27, 2026
@surbhigarg92
Copy link
Copy Markdown
Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Node-API wrapper for the Spanner shared library, providing an object-oriented TypeScript interface for managing database pools, connections, and row iterators. The implementation includes a C++ bridge and a memory management system using a FinalizationRegistry. However, several critical issues were identified regarding memory management and performance. Specifically, memory leaks occur in Rows.next() and CloseRowsWrapper because native pinner IDs are not released, and the Pool, Connection, and Rows classes are not correctly registered with the cleanup registry. Additionally, CloseRowsWrapper should be implemented asynchronously to avoid blocking the Node.js event loop, and hardcoded session values in SQL execution must be addressed for production use.

Comment thread spannerlib/wrappers/spannerlib-node/src/lib/rows.ts
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/connection.ts
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/connection.ts
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/pool.ts
Comment thread spannerlib/wrappers/spannerlib-node/scripts/fix-extensions.cjs Outdated
Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/connection.ts
@surbhigarg92
Copy link
Copy Markdown
Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Node-API wrapper for the Spanner shared library, featuring a C++ addon to bridge Node.js and Go, along with TypeScript classes for managing pools, connections, and row iteration. The implementation includes automatic memory management via a FinalizationRegistry and comprehensive build scripts. Feedback highlights a critical race condition in the C++ addon where memory might be released before being copied, as well as potential issues with promises hanging due to insufficient argument validation. Other suggestions include improving type safety in the TypeScript layer and addressing hardcoded session identifiers.

Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc
Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc Outdated
Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/connection.ts
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/rows.ts
@surbhigarg92
Copy link
Copy Markdown
Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Node-API wrapper for the Spanner shared library, providing a high-performance bridge between Node.js and Go. It includes a C++ addon for handling native calls, TypeScript classes for managing pools, connections, and row iteration, and a memory management system using a FinalizationRegistry. The review identifies several critical memory management issues, specifically regarding the failure to release pinner IDs in error paths and after data copying in the C++ layer. Additionally, the FinalizationRegistry currently only releases pinner IDs without cleaning up the underlying Go objects. Other feedback suggests adding argument length checks in the C++ wrappers for better stability and replacing hardcoded session IDs with dynamic management.

Comment thread spannerlib/wrappers/spannerlib-node/src/ffi/utils.ts
Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/connection.ts
Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/spannerlib.ts Outdated
@surbhigarg92
Copy link
Copy Markdown
Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Node-API wrapper for the Spanner shared library, providing a bridge between Node.js and the Go-based library. Feedback focuses on critical memory management issues, including the incorrect use of FinalizationRegistry for short-lived identifiers and missing or redundant pinner ID releases in the native and TypeScript layers. Additionally, improvements are needed for C++ argument validation and the removal of hardcoded session identifiers to ensure compatibility with real Spanner environments.

Comment thread spannerlib/wrappers/spannerlib-node/src/lib/spannerlib.ts Outdated
Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc
Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/rows.ts Outdated
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/connection.ts
@surbhigarg92
Copy link
Copy Markdown
Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Node-API wrapper for the Spanner shared library, featuring C++ bindings and TypeScript abstractions for database management. The review identifies several high-severity issues, including native memory leaks in the C++ worker classes and an architectural flaw where Go objects are unpinned but not properly closed. Additional feedback suggests improving safety through argument validation, removing unused parameters, and avoiding tight coupling to internal package paths.

Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc
Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/spannerlib.ts Outdated
Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc
Comment thread spannerlib/wrappers/spannerlib-node/src/cpp/addon.cc
Comment thread spannerlib/wrappers/spannerlib-node/src/ffi/utils.ts
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/connection.ts
Comment thread spannerlib/wrappers/spannerlib-node/src/lib/connection.ts
@surbhigarg92
Copy link
Copy Markdown
Author

/gemini review

@surbhigarg92 surbhigarg92 marked this pull request as ready for review April 29, 2026 13:54
@surbhigarg92 surbhigarg92 requested a review from a team as a code owner April 29, 2026 13:54
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Node-API wrapper for the Spanner shared library, providing a high-performance bridge between Node.js and the Go-based library. The implementation includes a C++ addon, TypeScript classes for managing database resources, and a comprehensive build and test suite. However, the review highlights several critical issues: multiple argument mismatches between the TypeScript FFI layer and the C++ wrapper functions will likely cause runtime crashes. Furthermore, native memory leaks were identified in the C++ workers due to unreleased pinner IDs, and the FinalizationRegistry logic for automatic resource cleanup is incorrectly implemented. Finally, the use of internal package paths for protobuf imports should be addressed to ensure long-term stability.

Comment on lines +45 to +51
const handled = await ffi.invokeAsync(
'CreatePool',
p,
spannerLib,
userAgentSuffix,
connectionString
);
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.

critical

There is a critical argument mismatch between the TypeScript call and the C++ wrapper. CreatePoolWrapper in addon.cc expects 2 string arguments (userAgent, connStr) followed by a callback. However, this call passes 4 arguments (p, spannerLib, userAgentSuffix, connectionString). This will cause the C++ side to attempt to cast the Pool instance (p) to a string, resulting in a crash or exception.

    const handled = await ffi.invokeAsync(
      'CreatePool',
      userAgentSuffix,
      connectionString
    );

Comment on lines +48 to +53
const handled = await ffi.invokeAsync(
'CreateConnection',
c,
spannerLib,
pool.oid
);
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.

critical

Argument mismatch: CreateConnectionWrapper in C++ expects 1 number argument (poolId) and a callback. The TypeScript code is passing 3 arguments (c, spannerLib, pool.oid). The first argument c (the Connection instance) will be incorrectly interpreted as the poolId number.

    const handled = await ffi.invokeAsync(
      'CreateConnection',
      pool.oid
    );

Comment on lines +84 to +91
const rowsResult = await ffi.invokeAsync(
'Execute',
null,
null,
this.pool.oid,
this.oid,
serializedPb
);
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.

critical

Argument mismatch: ExecuteWrapper in C++ expects 3 arguments (poolId, connId, payload) and a callback. The TypeScript code passes 5 arguments, including two leading null values. This will cause info[0] and info[1] to be null instead of the expected IDs, and info[2] to be a number instead of a Buffer, leading to a crash.

    const rowsResult = await ffi.invokeAsync(
      'Execute',
      this.pool.oid,
      this.oid,
      serializedPb
    );

Comment on lines +58 to +67
const handled = await ffi.invokeAsync(
'Next',
null,
null,
this.connection.pool.oid,
this.connection.oid,
this.oid,
1,
ENCODING_PROTOBUF
);
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.

critical

Argument mismatch: NextWrapper in C++ expects 5 arguments (poolId, connId, rowsId, numRows, encodeOtp) and a callback. The TypeScript code passes 7 arguments, including two leading null values. This will shift all arguments and cause type errors in the native layer.

    const handled = await ffi.invokeAsync(
      'Next',
      this.connection.pool.oid,
      this.connection.oid,
      this.oid,
      1,
      ENCODING_PROTOBUF
    );

Comment on lines +48 to +53
if (result_.r4 != nullptr && result_.r3 > 0) {
obj.Set("r4", Napi::Buffer<uint8_t>::Copy(env, (uint8_t*)result_.r4, result_.r3));
} else {
obj.Set("r4", env.Null());
}
Callback().Call({env.Null(), obj});
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.

high

The pinner ID (result_.r0) returned by the Go library for the response message is not being released in CreatePoolWorker. This will lead to a native memory leak as the Go runtime will keep the memory pinned indefinitely. It should be released immediately after the buffer is copied into the Node.js environment.

        if (result_.r4 != nullptr && result_.r3 > 0) {
            obj.Set("r4", Napi::Buffer<uint8_t>::Copy(env, (uint8_t*)result_.r4, result_.r3));
        } else {
            obj.Set("r4", env.Null());
        }
        // Release the pinner ID of the response message to prevent native leak!
        if (result_.r0 > 0) {
            ::Release(result_.r0);
        }
        Callback().Call({env.Null(), obj});

Comment on lines +136 to +142
if (result_.r4 != nullptr && result_.r3 > 0) {
obj.Set("r4", Napi::Buffer<uint8_t>::Copy(env, (uint8_t*)result_.r4, result_.r3));
} else {
obj.Set("r4", env.Null());
}
Callback().Call({env.Null(), obj});
}
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.

high

Similar to CreatePoolWorker, CreateConnectionWorker is missing the call to ::Release(result_.r0). This causes a native memory leak for every connection created.

        if (result_.r4 != nullptr && result_.r3 > 0) {
            obj.Set("r4", Napi::Buffer<uint8_t>::Copy(env, (uint8_t*)result_.r4, result_.r3));
        } else {
            obj.Set("r4", env.Null());
        }
        // Release the pinner ID of the response message to prevent native leak!
        if (result_.r0 > 0) {
            ::Release(result_.r0);
        }
        Callback().Call({env.Null(), obj});

Comment on lines +24 to +29
this.registry = new FinalizationRegistry((pinnerId: number) => {
if (pinnerId && pinnerId > 0) {
ffi.Release(pinnerId);
this.activePinners.delete(pinnerId);
}
});
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.

high

The FinalizationRegistry logic is fundamentally flawed. It is registering the pinnerId of the creation response message. As noted in other comments, this ID should be released immediately after the creation call finishes. Furthermore, releasing this pinner ID does NOT close the underlying Go object (Pool/Connection/Rows). To prevent native resource leaks, the registry should track the objectId and trigger the appropriate Close method (e.g., ClosePool), although this is complex because those methods are asynchronous and the registry callback is synchronous.

Comment thread spannerlib/wrappers/spannerlib-node/src/lib/connection.ts
@surbhigarg92
Copy link
Copy Markdown
Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a high-performance Node-API (N-API) wrapper for the Go-based Spanner shared library, providing core classes like Pool, Connection, and Rows along with memory management via a FinalizationRegistry. The review feedback highlights critical memory management and stability issues, specifically pointing out several potential double-release bugs of Go pinner IDs in the C++ worker classes and the SpannerLib registry, as well as premature object unpinning in the Execute worker. Additionally, the reviewer recommends implementing defensive null checks for nullable object IDs in Connection and Rows before invoking native methods to prevent crashes.

Comment on lines +227 to +231
// Release the pinner ID of the response buffer immediately after copy!
if (result_.r0 > 0) {
::Release(result_.r0);
}
Callback().Call({env.Null(), obj});
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.

critical

The Execute function returns a long-lived Rows object whose pinner ID (result_.r0) must be kept alive until the Rows object is closed or garbage collected. Releasing it immediately in ExecuteWorker::OnOK() unpins the Rows object in Go prematurely, which can lead to crashes or undefined behavior when Next is called. Additionally, since JS registers this pinner ID in spannerLib to be released later, releasing it here causes a double-release bug.

Suggested change
// Release the pinner ID of the response buffer immediately after copy!
if (result_.r0 > 0) {
::Release(result_.r0);
}
Callback().Call({env.Null(), obj});
Callback().Call({env.Null(), obj});

void OnOK() override {
Napi::Env env = Env();
Napi::Object obj = Napi::Object::New(env);
obj.Set("r0", Napi::Number::New(env, result_.r0));
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.

high

Since NextWorker already releases result_.r0 in C++ to prevent native leaks of the temporary response buffer, passing result_.r0 to JS can lead to a double-release bug in utils.ts if the operation fails (as utils.ts will attempt to release it again). Setting r0 to 0 in the returned object prevents this double-release.

Suggested change
obj.Set("r0", Napi::Number::New(env, result_.r0));
obj.Set("r0", Napi::Number::New(env, 0));

void OnOK() override {
Napi::Env env = Env();
Napi::Object obj = Napi::Object::New(env);
obj.Set("r0", Napi::Number::New(env, result_.r0));
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.

high

Since MetadataWorker already releases result_.r0 in C++ to prevent native leaks of the temporary response buffer, passing result_.r0 to JS can lead to a double-release bug in utils.ts if the operation fails. Setting r0 to 0 in the returned object prevents this double-release.

Suggested change
obj.Set("r0", Napi::Number::New(env, result_.r0));
obj.Set("r0", Napi::Number::New(env, 0));

void OnOK() override {
Napi::Env env = Env();
Napi::Object obj = Napi::Object::New(env);
obj.Set("r0", Napi::Number::New(env, result_.r0));
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.

high

Since CloseRowsWorker already releases result_.r0 in C++ to prevent native leaks of the temporary response buffer, passing result_.r0 to JS can lead to a double-release bug in utils.ts if the operation fails. Setting r0 to 0 in the returned object prevents this double-release.

Suggested change
obj.Set("r0", Napi::Number::New(env, result_.r0));
obj.Set("r0", Napi::Number::New(env, 0));

Comment on lines +24 to +29
this.registry = new FinalizationRegistry((pinnerId: number) => {
if (pinnerId && pinnerId > 0) {
ffi.Release(pinnerId);
this.activePinners.delete(pinnerId);
}
});
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.

high

If releaseAll() is called (e.g., during cleanup or shutdown), all active pinners are released and cleared from activePinners. However, the JS objects themselves are not unregistered from the FinalizationRegistry. When those JS objects are eventually garbage collected, the registry callback will run and attempt to release the pinnerId again, causing a double-release. Checking this.activePinners.has(pinnerId) in the callback prevents this.

Suggested change
this.registry = new FinalizationRegistry((pinnerId: number) => {
if (pinnerId && pinnerId > 0) {
ffi.Release(pinnerId);
this.activePinners.delete(pinnerId);
}
});
this.registry = new FinalizationRegistry((pinnerId: number) => {
if (pinnerId && pinnerId > 0 && this.activePinners.has(pinnerId)) {
ffi.Release(pinnerId);
this.activePinners.delete(pinnerId);
}
});

Comment on lines +74 to +76
async executeSql(sqlString: string): Promise<Rows> {
if (this.closed) throw new Error('Connection is already closed');
if (!this.pool) throw new Error('Connection is not bound to a Pool');
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.

medium

To enforce defensive programming, we should check if this.oid or this.pool.oid are null before passing them to the native wrapper, as they are nullable in TypeScript and passing null can cause crashes or type errors in the native layer.

Suggested change
async executeSql(sqlString: string): Promise<Rows> {
if (this.closed) throw new Error('Connection is already closed');
if (!this.pool) throw new Error('Connection is not bound to a Pool');
async executeSql(sqlString: string): Promise<Rows> {
if (this.closed) throw new Error('Connection is already closed');
if (!this.pool) throw new Error('Connection is not bound to a Pool');
if (this.oid === null) throw new Error('Connection is not initialized');
if (this.pool.oid === null) throw new Error('Pool is not initialized');

Comment on lines +44 to +46
static async create(pool: Pool): Promise<Connection> {
const c = new Connection();
c.pool = pool;
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.

medium

To enforce defensive programming, we should check if pool.oid is null before attempting to create a connection, as passing null to the native wrapper can cause crashes or type errors.

  static async create(pool: Pool): Promise<Connection> {
    if (pool.oid === null) {
      throw new Error('Cannot create connection: Pool is not initialized');
    }
    const c = new Connection();
    c.pool = pool;

Comment on lines +51 to +56
async next(): Promise<unknown> {
if (this.closed) throw new Error('Rows are already closed');

if (!this.connection.pool) {
throw new Error('Connection must be bound to a Pool to fetch rows');
}
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.

medium

To enforce defensive programming, we should check if this.connection.oid or this.connection.pool.oid are null before passing them to the native wrapper, as they are nullable in TypeScript and passing null can cause crashes or type errors.

  async next(): Promise<unknown> {
    if (this.closed) throw new Error('Rows are already closed');

    if (!this.connection.pool) {
      throw new Error('Connection must be bound to a Pool to fetch rows');
    }
    if (this.connection.oid === null) {
      throw new Error('Connection is not initialized');
    }
    if (this.connection.pool.oid === null) {
      throw new Error('Pool is not initialized');
    }

@surbhigarg92
Copy link
Copy Markdown
Author

/gemini review

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a high-performance Node-API (N-API) wrapper for the Go-based Spanner shared library, featuring automatic native memory management via a FinalizationRegistry. The review feedback highlights critical issues in the C++ addon where native resources are released prematurely on worker threads, potentially causing crashes or losing error messages during pool and connection closures. Additionally, the reviewer recommends removing redundant release calls in the FFI utility layer, adding defensive null checks for object IDs in Connection and Rows to prevent passing invalid handles, and addressing the misleading, currently no-op cleanup() export.

Comment on lines +87 to +100
void Execute() override {
result_ = ClosePool(poolId_);
// Release the pinner ID of the response message to prevent native leak!
if (result_.r0 > 0) {
::Release(result_.r0);
}
}

void OnOK() override {
Napi::Env env = Env();
Napi::Object obj = Napi::Object::New(env);
obj.Set("r1", Napi::Number::New(env, result_.r1));
Callback().Call({env.Null(), obj});
}
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.

high

In ClosePoolWorker, the pinner ID (result_.r0) is released in Execute() (which runs on a worker thread) before OnOK() runs. If ClosePool fails, the error message in result_.r4 is pinned by result_.r0. Releasing it early makes it unsafe to access result_.r4 in OnOK(). Furthermore, OnOK() currently does not copy result_.r4 or set r0/r3/r4 on the returned object, meaning any error message returned by Go is lost in the JS layer.

To fix this, do not release result_.r0 in Execute(). Instead, copy result_.r4 to a JS Buffer in OnOK() (just like CreatePoolWorker does) and release result_.r0 at the end of OnOK() to prevent native memory leaks and ensure error messages are correctly propagated.

    void Execute() override {
        result_ = ClosePool(poolId_);
    }

    void OnOK() override {
        Napi::Env env = Env();
        Napi::Object obj = Napi::Object::New(env);
        obj.Set("r0", Napi::Number::New(env, result_.r0));
        obj.Set("r1", Napi::Number::New(env, result_.r1));
        obj.Set("r2", Napi::Number::New(env, result_.r2));
        obj.Set("r3", Napi::Number::New(env, result_.r3));
        if (result_.r4 != nullptr && result_.r3 > 0) {
            obj.Set("r4", Napi::Buffer<uint8_t>::Copy(env, (uint8_t*)result_.r4, result_.r3));
        } else {
            obj.Set("r4", env.Null());
        }
        if (result_.r0 > 0) {
            ::Release(result_.r0);
        }
        Callback().Call({env.Null(), obj});
    }

Comment on lines +175 to +188
void Execute() override {
result_ = CloseConnection(poolId_, connId_);
// Release the pinner ID of the response message to prevent native leak!
if (result_.r0 > 0) {
::Release(result_.r0);
}
}

void OnOK() override {
Napi::Env env = Env();
Napi::Object obj = Napi::Object::New(env);
obj.Set("r1", Napi::Number::New(env, result_.r1));
Callback().Call({env.Null(), obj});
}
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.

high

Similarly to ClosePoolWorker, CloseConnectionWorker releases result_.r0 in Execute() and does not populate r0/r3/r4 in OnOK(). This causes any error messages from CloseConnection to be lost in the JS layer.

To fix this, defer the release of result_.r0 to OnOK() and copy result_.r4 to the returned JS object.

    void Execute() override {
        result_ = CloseConnection(poolId_, connId_);
    }

    void OnOK() override {
        Napi::Env env = Env();
        Napi::Object obj = Napi::Object::New(env);
        obj.Set("r0", Napi::Number::New(env, result_.r0));
        obj.Set("r1", Napi::Number::New(env, result_.r1));
        obj.Set("r2", Napi::Number::New(env, result_.r2));
        obj.Set("r3", Napi::Number::New(env, result_.r3));
        if (result_.r4 != nullptr && result_.r3 > 0) {
            obj.Set("r4", Napi::Buffer<uint8_t>::Copy(env, (uint8_t*)result_.r4, result_.r3));
        } else {
            obj.Set("r4", env.Null());
        }
        if (result_.r0 > 0) {
            ::Release(result_.r0);
        }
        Callback().Call({env.Null(), obj});
    }

Comment on lines +48 to +52
if (result.r1 !== 0) {
if (result.r0 && result.r0 > 0) {
ffi.Release(result.r0);
}
if (result.r4 && result.r3 > 0) {
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.

medium

The result.r0 pinner ID is already released in the C++ layer (addon.cc) immediately after copying the buffer in OnOK(). Calling ffi.Release(result.r0) here is redundant and results in an unnecessary native call. This block can be safely removed.

Suggested change
if (result.r1 !== 0) {
if (result.r0 && result.r0 > 0) {
ffi.Release(result.r0);
}
if (result.r4 && result.r3 > 0) {
if (result.r1 !== 0) {
if (result.r4 && result.r3 > 0) {

Comment on lines +73 to +74
if (this.closed) throw new Error('Connection is already closed');
if (!this.pool) throw new Error('Connection is not bound to a Pool');
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.

medium

To enforce defensive programming and prevent potential runtime errors or crashes in the native addon, we should explicitly check that both this.pool.oid and this.oid are not null before executing the SQL statement.

Suggested change
if (this.closed) throw new Error('Connection is already closed');
if (!this.pool) throw new Error('Connection is not bound to a Pool');
if (this.closed) throw new Error('Connection is already closed');
if (!this.pool || this.pool.oid === null || this.oid === null) {
throw new Error('Connection is not bound to an active Pool');
}

Comment on lines +55 to +59
if (this.closed) throw new Error('Rows are already closed');

if (!this.connection.pool) {
throw new Error('Connection must be bound to a Pool to fetch rows');
}
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.

medium

Defensively check that this.connection.pool.oid and this.connection.oid are not null before calling ffi.invokeAsync to prevent passing invalid handles to the native layer.

Suggested change
if (this.closed) throw new Error('Rows are already closed');
if (!this.connection.pool) {
throw new Error('Connection must be bound to a Pool to fetch rows');
}
if (this.closed) throw new Error('Rows are already closed');
if (!this.connection.pool || this.connection.pool.oid === null || this.connection.oid === null) {
throw new Error('Connection must be bound to an active Pool to fetch rows');
}

Comment on lines +53 to +55
releaseAll(): void {
// No-op as active pinners are now released immediately in addon.cc
}
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.

medium

releaseAll() is currently a no-op, but it is exported as cleanup() in index.ts with documentation claiming it 'Releases all pinned resources and handles managed by the library... to prevent native memory leaks.' This is misleading because it does not actually close any active pools, connections, or rows. We should either implement tracking of active resources (e.g., using a Set of WeakRefs) to actually close them in releaseAll(), or remove the misleading cleanup() export entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant