Skip to content

Commit 8c83c0b

Browse files
committed
napi metadata changes
1 parent 2c59157 commit 8c83c0b

7 files changed

Lines changed: 170 additions & 61 deletions

File tree

spannerlib/wrappers/spannerlib-nodejs-poc/napi/binding.gyp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,16 @@
3939
}],
4040
['OS=="linux"', {
4141
'ldflags': [
42-
'-Wl,-rpath,\'$$ORIGIN/../../../shared\''
42+
'-Wl,-rpath,$$ORIGIN'
4343
],
4444
'libraries': [
4545
'<(module_root_dir)/../../../shared/libspanner.so'
46+
],
47+
'copies': [
48+
{
49+
'destination': '<(PRODUCT_DIR)',
50+
'files': [ '<(module_root_dir)/../../../shared/libspanner.so' ]
51+
}
4652
]
4753
}]
4854
]

spannerlib/wrappers/spannerlib-nodejs-poc/napi/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"node": ">=20.0.0"
77
},
88
"scripts": {
9-
"build": "node-gyp rebuild && install_name_tool -change libspanner.so @loader_path/libspanner.so ./build/Release/spanner_napi.node",
9+
"build": "node-gyp rebuild",
10+
"postbuild": "node -e \"if (process.platform === 'darwin') require('child_process').execSync('install_name_tool -change libspanner.so @loader_path/libspanner.so ./build/Release/spanner_napi.node')\"",
1011
"test": "node test.js",
1112
"setup": "node setup_db.js"
1213
},

spannerlib/wrappers/spannerlib-nodejs-poc/napi/src/cpp/addon.cc

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,49 @@ Napi::Value NextWrapper(const Napi::CallbackInfo& info) {
247247
return env.Undefined();
248248
}
249249

250+
//
251+
// Worker 7: Metadata asynchronously
252+
//
253+
class MetadataWorker : public Napi::AsyncWorker {
254+
public:
255+
MetadataWorker(Napi::Function& callback, int64_t poolId, int64_t connId, int64_t rowsId)
256+
: AsyncWorker(callback), poolId_(poolId), connId_(connId), rowsId_(rowsId), result_({0, 0, 0, 0, nullptr}) {}
257+
258+
void Execute() override {
259+
result_ = ::Metadata(poolId_, connId_, rowsId_);
260+
}
261+
262+
void OnOK() override {
263+
Napi::Env env = Env();
264+
Napi::Object obj = Napi::Object::New(env);
265+
obj.Set("r0", Napi::Number::New(env, result_.r0));
266+
obj.Set("r1", Napi::Number::New(env, result_.r1));
267+
obj.Set("r2", Napi::Number::New(env, result_.r2));
268+
obj.Set("r3", Napi::Number::New(env, result_.r3));
269+
if (result_.r4 != nullptr && result_.r3 > 0) {
270+
obj.Set("r4", Napi::Buffer<uint8_t>::Copy(env, (uint8_t*)result_.r4, result_.r3));
271+
} else {
272+
obj.Set("r4", env.Null());
273+
}
274+
Callback().Call({env.Null(), obj});
275+
}
276+
private:
277+
int64_t poolId_, connId_, rowsId_;
278+
Metadata_return result_;
279+
};
280+
281+
Napi::Value MetadataWrapper(const Napi::CallbackInfo& info) {
282+
Napi::Env env = info.Env();
283+
int64_t pid = info[0].As<Napi::Number>().Int64Value();
284+
int64_t cid = info[1].As<Napi::Number>().Int64Value();
285+
int64_t rid = info[2].As<Napi::Number>().Int64Value();
286+
Napi::Function cb = info[3].As<Napi::Function>();
287+
288+
MetadataWorker* worker = new MetadataWorker(cb, pid, cid, rid);
289+
worker->Queue();
290+
return env.Undefined();
291+
}
292+
250293
// Memory Release (Synchronous as it is just freeing RAM via GC)
251294
Napi::Value NativeRelease(const Napi::CallbackInfo& info) {
252295
Napi::Env env = info.Env();
@@ -288,6 +331,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
288331
exports.Set("CloseRows", Napi::Function::New(env, CloseRowsWrapper));
289332
exports.Set("Release", Napi::Function::New(env, NativeRelease));
290333

334+
exports.Set("Metadata", Napi::Function::New(env, MetadataWrapper));
291335
return exports;
292336
}
293337

spannerlib/wrappers/spannerlib-nodejs-poc/napi/src/ffi/utils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
const addon = require('../../build/Release/spanner_napi.node');
22

3+
const ENCODING_JSON = 0;
4+
const ENCODING_PROTOBUF = 1;
5+
36
/**
47
* Normalizes C++ responses to look structurally identical to what Koffi did.
58
* Koffi returned: { r0: int, r1: int, r2: int, r3: int, r4: pointer/buffer }
@@ -43,6 +46,8 @@ function invokeAsync(funcName, constructor1, constructor2, ...args) {
4346
const Release = addon.Release;
4447

4548
module.exports = {
49+
ENCODING_JSON,
50+
ENCODING_PROTOBUF,
4651
invokeAsync,
4752
Release
4853
};

spannerlib/wrappers/spannerlib-nodejs-poc/napi/src/lib/connection.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,37 @@ class Connection {
5656
const ExecuteSqlRequestProto = google.spanner.v1.ExecuteSqlRequest;
5757
const serializedPb = ExecuteSqlRequestProto.encode(requestObj).finish();
5858

59-
// 2. Transmit the standard protobuf binary buffer over CGO FFI seamlessly
60-
const handled = await invokeAsync(
61-
Execute,
62-
null, // Rows gets constructed afterward
59+
// 1. Execute the SQL to get a Rows identifier
60+
const rowsResult = await invokeAsync(
61+
"Execute",
62+
null,
6363
null,
6464
this.pool.oid,
6565
this.oid,
6666
serializedPb
6767
);
68+
const rowsId = rowsResult.objectId;
6869

69-
return new Rows(this, handled.objectId);
70+
// 2. Fetch the metadata to get column names
71+
const metadataResult = await invokeAsync(
72+
"Metadata",
73+
null,
74+
null,
75+
this.pool.oid,
76+
this.oid,
77+
rowsId
78+
);
79+
80+
// 3. Decode the metadata protobuf
81+
const ResultSetMetadataProto = google.spanner.v1.ResultSetMetadata;
82+
const metadata = ResultSetMetadataProto.decode(metadataResult.protobufBytes);
83+
const columnInfo = metadata.rowType.fields.map(field => ({
84+
name: field.name,
85+
typeCode: field.type.code
86+
}));
87+
88+
// 4. Return a new Rows object, now equipped with the column info
89+
return new Rows(this, rowsId, columnInfo);
7090
}
7191

7292
/**

spannerlib/wrappers/spannerlib-nodejs-poc/napi/src/lib/rows.js

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,96 @@
1-
const { CloseRows, Next } = require('../ffi/bindings.js');
2-
const { invokeAsync } = require('../ffi/utils.js');
1+
const { invokeAsync, ENCODING_PROTOBUF } = require('../ffi/utils.js');
32
const { spannerLib } = require('./spannerlib.js');
43
const { google } = require('@google-cloud/spanner/build/protos/protos.js');
4+
const ListValue = google.protobuf.ListValue;
5+
6+
/**
7+
* Parses a binary `ListValue` protobuf message into a key-value row object.
8+
* @param {Buffer} buffer The binary buffer from the Go library.
9+
* @param {Array<{name: string, typeCode: number}>} columnInfo An array of objects with column names and types.
10+
* @returns {object|null}
11+
*/
12+
function parseRowToObject(buffer, columnInfo) {
13+
if (!buffer || buffer.length === 0) {
14+
return null; // End of result set
15+
}
16+
17+
const listValue = ListValue.decode(buffer);
18+
const rowObject = {};
19+
const values = listValue.values;
20+
21+
columnInfo.forEach((column, index) => {
22+
const value = values[index];
23+
const columnName = column.name;
24+
let parsedValue;
25+
26+
// The decoded `value` object has a 'kind' oneof field.
27+
// We check which property is set to get the primitive value.
28+
switch (value.kind) {
29+
case 'nullValue':
30+
parsedValue = null;
31+
break;
32+
case 'numberValue':
33+
parsedValue = value.numberValue;
34+
break;
35+
case 'stringValue':
36+
parsedValue = value.stringValue;
37+
break;
38+
case 'boolValue':
39+
parsedValue = value.boolValue;
40+
break;
41+
default:
42+
parsedValue = undefined;
43+
}
44+
rowObject[columnName] = parsedValue;
45+
});
46+
47+
return rowObject;
48+
}
549

650
class Rows {
751
/**
8-
* @param {import('./connection.js').Connection} conn
9-
* @param {Number} objectId - The OID identifying these rows inside the Go Driver
52+
* @param {import('./connection.js').Connection} connection
53+
* @param {Number} oid
54+
* @param {Array<{name: string, typeCode: number}>} columnInfo
1055
*/
11-
constructor(conn, objectId) {
12-
this.conn = conn;
13-
14-
/**
15-
* The Object ID (OID).
16-
* Used to execute operations like `.next()` against THIS specific ResultSet inside Go.
17-
* @type {Number|null}
18-
*/
19-
this.oid = objectId;
20-
56+
constructor(connection, oid, columnInfo) {
57+
this.connection = connection;
58+
this.oid = oid;
59+
this.pinnerId = null;
2160
this.closed = false;
22-
23-
// FinalizationRegistry could optionally be mapped to this via
24-
// spannerLib.register(this, pinnerId_from_execute)
25-
// For the POC, we won't fully map the Rows Pinner unless needed
26-
// by the Next() function iterator.
61+
this.columnInfo = columnInfo; // Store column names and types
2762
}
2863

2964
/**
30-
* Iterates to the next result chunk.
31-
* In a full implementation, it would call `Next(poolId, connId, rowsId, ...)` natively.
65+
* Fetches the next row from the result set.
66+
* @returns {Promise<object|null>} A promise that resolves to a row object, or null if there are no more rows.
3267
*/
3368
async next() {
34-
if (this.closed) throw new Error("Rows object is already closed");
35-
36-
// Go Signature: Next(poolId, connId, rowsId, numRows, encodeRowOption)
37-
// We pass 1 for numRows, and 0 for encodeRowOption as per POC defaults
38-
const handled = await invokeAsync(Next, null, null, this.conn.pool.oid, this.conn.oid, this.oid, 1, 0);
39-
40-
// Handle EOF case (The chunk buffer is perfectly empty or contains no message)
41-
if (!handled.protobufBytes || handled.protobufBytes.length === 0) {
42-
return null; // Signals end of rows to the caller
43-
}
44-
45-
// The returned message contains a google.protobuf.ListValue according to the spec!
46-
// We natively unpack those bytes matching standard Protobuf conventions.
47-
const listValueProto = google.protobuf.ListValue;
48-
const decodedList = listValueProto.decode(handled.protobufBytes);
69+
if (this.closed) throw new Error("Rows are already closed");
4970

50-
// This converts the complex generic Protobuf ListValue deeply into native Javascript!
51-
const jsonRecord = listValueProto.toObject(decodedList, {
52-
longs: String, // Ensure Int64 types from Spanner decode as Strings instead of mangled JS doubles
53-
enums: String,
54-
bytes: String,
55-
});
71+
const handled = await invokeAsync(
72+
"Next",
73+
null,
74+
null,
75+
this.connection.pool.oid,
76+
this.connection.oid,
77+
this.oid,
78+
1, // Fetch one row at a time
79+
ENCODING_PROTOBUF
80+
);
5681

57-
return jsonRecord.values || [];
82+
// The result for `Next` is a binary `ListValue` protobuf message.
83+
return parseRowToObject(handled.protobufBytes, this.columnInfo);
5884
}
5985

60-
/**
61-
* Closes the rows object safely, waiting on the network.
62-
*/
6386
async close() {
6487
if (!this.closed) {
6588
this.closed = true;
66-
// Native FFI execution in the background
67-
await invokeAsync(CloseRows, null, spannerLib, this.conn.pool.oid, this.conn.oid, this.oid);
89+
try {
90+
await invokeAsync("CloseRows", this, spannerLib, this.connection.pool.oid, this.connection.oid, this.oid);
91+
} finally {
92+
spannerLib.unregister(this, this.pinnerId);
93+
}
6894
}
6995
}
7096
}

spannerlib/wrappers/spannerlib-nodejs-poc/napi/test.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,19 @@ async function runTest() {
3434
rows = await connection.executeSql(sqlQuery);
3535
console.log(`Executed SQL successfully in ${(performance.now() - startTime).toFixed(3)}ms (Rows OID: ${rows.oid})`);
3636

37-
console.log("\n[JS-App] 4. Fetching ResultSet Native Types (Int, String, Float, Bool)...");
38-
let nextRow;
39-
while ((nextRow = await rows.next()) !== null) {
40-
// nextRow is an array of Value protobuf objects (Google Protobuf Structs)
41-
console.log(" - Fetched native row chunk ->");
42-
console.log(" " + JSON.stringify(nextRow));
37+
console.log("\n[JS-App] 4. Fetching ResultSet as Objects...");
38+
let row;
39+
const results = [];
40+
while ((row = await rows.next()) !== null) {
41+
results.push(row);
42+
}
43+
44+
console.log(" - Fetched rows ->");
45+
console.log(JSON.stringify(results, null, 2));
46+
47+
// Example of accessing a property on the first row
48+
if (results.length > 0) {
49+
console.log(`\nExample access: results[0].FirstName is "${results[0].FirstName}"`);
4350
}
4451

4552
} catch (err) {

0 commit comments

Comments
 (0)