Skip to content

feat(spanner-driver): integrate native spannerlib-node wrapper and implement type system - #9141

Open
surbhigarg92 wants to merge 1 commit into
mainfrom
spanner-driver-native-bridge
Open

feat(spanner-driver): integrate native spannerlib-node wrapper and implement type system#9141
surbhigarg92 wants to merge 1 commit into
mainfrom
spanner-driver-native-bridge

Conversation

@surbhigarg92

Copy link
Copy Markdown
Contributor

No description provided.

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

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.

Code Review

This pull request integrates the native CGO bridge (spannerlib-node) into the Spanner driver, enabling query execution, connection pooling, and a custom type parser system (TypeOverrides and Codec) compatible with node-postgres. Feedback focuses on preventing resource leaks by wrapping result set processing and connection creation in appropriate try...finally and try...catch blocks. Additionally, it is recommended to relax the numeric OID checks in TypeOverrides to support GoogleSQL string descriptors and to improve array element type inference when the first element is null.

Comment thread handwritten/spanner-driver/src/lib/client.ts Outdated
Comment thread handwritten/spanner-driver/src/lib/client.ts Outdated
Comment thread handwritten/spanner-driver/src/lib/pg/types.ts Outdated
Comment thread handwritten/spanner-driver/src/lib/pg/types.ts
Comment thread handwritten/spanner-driver/src/lib/pg/types.ts
Comment thread handwritten/spanner-driver/src/lib/codec.ts
@surbhigarg92
surbhigarg92 force-pushed the spanner-driver-native-bridge branch 6 times, most recently from 7a2a255 to 5b37e00 Compare August 13, 2026 15:57
@surbhigarg92 surbhigarg92 changed the title Spanner driver native bridge feat(spanner-driver): integrate native spannerlib-node wrapper and implement type system Aug 13, 2026
@surbhigarg92
surbhigarg92 force-pushed the spanner-driver-native-bridge branch from 5b37e00 to ae110fb Compare August 13, 2026 17:19
@surbhigarg92

Copy link
Copy Markdown
Contributor Author

/gemini review

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

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.

Code Review

This pull request integrates the native Go CGO bridge (spannerlib-node) into the Spanner Node.js driver, enabling native connection management, query execution, parameter serialization, and custom type parsing. The reviewer provided valuable feedback pointing out several critical issues, including a potential native resource leak during concurrent client teardown, a crash risk with invalid Date parameters, and a TypeError that breaks GoogleSQL queries. Additionally, the reviewer highlighted architectural concerns regarding nested native pools and memory buffering during streaming, a naive array parser that fails on commas, a regression in connect() compatibility, and a packaging issue with local dependency paths.

Comment thread handwritten/spanner-driver/src/lib/client.ts Outdated
Comment thread handwritten/spanner-driver/src/lib/codec.ts
Comment thread handwritten/spanner-driver/src/lib/pg/types.ts
Comment thread handwritten/spanner-driver/src/lib/client.ts
Comment thread handwritten/spanner-driver/package.json
Comment thread handwritten/spanner-driver/src/lib/client.ts
Comment thread handwritten/spanner-driver/src/lib/pg/types.ts
Comment thread handwritten/spanner-driver/src/lib/client.ts
@surbhigarg92
surbhigarg92 force-pushed the spanner-driver-native-bridge branch 2 times, most recently from e117502 to 7ffb333 Compare August 14, 2026 05:48
@surbhigarg92
surbhigarg92 marked this pull request as ready for review August 14, 2026 06:07
@surbhigarg92
surbhigarg92 requested a review from a team as a code owner August 14, 2026 06:07
@surbhigarg92
surbhigarg92 force-pushed the spanner-driver-native-bridge branch 5 times, most recently from edf2661 to 7cb6ac0 Compare August 14, 2026 10:29
…plement type system

- Integrate Client and Pool directly with native CGO/N-API bindings via spannerlib-node.
@surbhigarg92
surbhigarg92 force-pushed the spanner-driver-native-bridge branch from 7cb6ac0 to 1582dc1 Compare August 14, 2026 10:41
}

// -----------------------------------------------------------------------------
// Binary Format Parsers (matches pg-types/lib/binaryParsers.js)

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.

Technically, these will never be used in the Spanner driver. So if we keep them, that is only for code compatibility with node-pg. But I don't think any application code would ever use this, as these functions return the low-level PG wire-protocol binary representation of a value. That is not something I would expect an application to use. We can keep them here if that is better for compatibility, but we should preferably try to make sure that no-one tries to use them with the driver, as that would fail.


const defaultTypeParsers: Record<string, Record<number, TypeParser>> = {
text: textParsers,
binary: binaryParsers,

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.

I think that it would be better to let this just default to the textParsers. Anyone who tries to use this with this driver, will run into problems.

* @returns EncodedParam containing `valueProto` and `typeProto`.
*/
static encodeValue(val: unknown): EncodedParam {
if (val === null || val === undefined) {

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.

This should handle .toPostgres() first, as otherwise an array of .toPostgres() instances will be treated as an object:

  // 1. Unwrap custom objects implementing .toPostgres() first (handles both top-level and nested array items)
  if (
    typeof val === 'object' &&
    val !== null &&
    typeof (val as {toPostgres?: unknown}).toPostgres === 'function'
  ) {
    return Codec.encodeValue(
      (val as {toPostgres: () => unknown}).toPostgres(),
    );
  }

Test case:

it('should unwrap custom objects with .toPostgres() inside array parameters', () => {
  // Custom ORM / domain model wrappers
  const customId1 = { toPostgres: () => 101 };
  const customId2 = { toPostgres: () => 102 };
  // Pass array of custom objects to parameter $1
  const { fields } = Codec.encodeParams([[customId1, customId2]], 'pg');
  assert.deepStrictEqual(fields.p1, {
    listValue: {
      values: [
        { stringValue: '101' },
        { stringValue: '102' },
      ],
    },
  });
});

Comment on lines +151 to +152
const pool = await Pool.create(this.dsn);
this.nativePool = 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.

We should not create a new Pool for each new Client. A Pool creates an actual connection to Spanner (e.g. a gRPC channel pool), and each pool can create many Connection instances. A Connection instance is lightweight in SpannerLib. Pool is not a lightweight object.

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.

Compare to the implementation of the .NET driver here: https://github.com/googleapis/dotnet-spanner-entity-framework/blob/e4dd7124a6b60c70f0f38db3f7507d2bb087d629/spanner-ado-net/spanner-ado-net/SpannerConnection.cs#L371

Each Pool is shared across every connection that uses the same connection string (DSN). So we should introduce something similar here, where clients that are created like this create or pick a pool based on the DSN.

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.

We can do this in a follow-up PR, but we should put a TODO here in that case.

* @param callback - Optional Node callback function receiving `(err, result)`.
* @returns Executable `Query` instance implementing Thenable interface and EventEmitter.
*/
public query<R = Record<string, unknown>>(

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.

Can we split this function into smaller parts? It is getting very long and hard to read.

Comment on lines +292 to +297
if (v.numberValue !== undefined && v.numberValue !== null) {
return String(v.numberValue);
}
if (v.boolValue !== undefined && v.boolValue !== null) {
return v.boolValue ? 't' : 'f';
}

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.

This seems to 'emulate' the PostgreSQL wire-protocol. I don't think we need that (and it makes it less efficient). Now a boolean is converted from bool => string => bool. A number is converted from number => string => number.

return v.boolValue ? 't' : 'f';
}
if (v.structValue) {
return JSON.stringify(v.structValue);

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.

This will return something like this:

{
  "fields": {
    "user_id": { "stringValue": "100" },
    "score": { "numberValue": 98.5 }
  }
}

Is that really what we would want here?

// Streaming row events
await client.connect();

// Stream rows as they arrive from Spanner gRPC stream

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.

This comment is a bit misleading. We are indeed streaming the rows as they come from the Spanner gRPC stream. But the driver is also collecting all of them in memory, so if the query returns a large number of rows, then you will get an OOM.

: elementParser(item);
});
}
if (typeof source === 'string') {

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.

Spanner will never return this, so in that sense, we can remove everything from here in this function.


// Seed AllTypes table
await client.query('DELETE FROM AllTypes WHERE Id = 1');
await client.query(

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.

Can we also add rows here with:

  1. NULL values (to verify that NULL parameter values work correctly)
  2. Empty arrays (to verify that an empty array is correctly sent to Spanner)
  3. Arrays with NULL elements in the array. So for example [1, NULL, 2]

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.

Can we also add a test for using .toPostgres()

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.

2 participants