Repository navigation
fix(bigtable): use ReadonlyMap for EncodedKeyMap implements clause - #9709
danieljbruce merged 4 commits into
Conversation
|
|
||
| getOrInsert( | ||
| key: string | bigint | Uint8Array | null, | ||
| defaultValue: SqlValue, | ||
| ): SqlValue { | ||
| if (this.has(key)) { | ||
| return this.get(key) as SqlValue; | ||
| } | ||
| this.set(key, defaultValue); | ||
| return defaultValue; | ||
| } | ||
|
|
||
| getOrInsertComputed( | ||
| key: string | bigint | Uint8Array | null, | ||
| callback: (key: string | bigint | Uint8Array | null) => SqlValue, | ||
| ): SqlValue { | ||
| if (typeof callback !== 'function') { | ||
| throw new TypeError('callback is not a function'); | ||
| } | ||
| if (this.has(key)) { | ||
| return this.get(key) as SqlValue; | ||
| } | ||
| const value = callback(key); | ||
| this.set(key, value); | ||
| return value; | ||
| } |
There was a problem hiding this comment.
[Generated] Implement TC39 Upsert getOrInsert and getOrInsertComputed methods on EncodedKeyMap (delegating to this.has, this.get, and this.set and validating callback is callable per the specification) so EncodedKeyMap implements Map<...> satisfies TypeScript 7.0's lib.esnext.collection.d.ts interface without triggering TS2420 when skipLibCheck is disabled.
| it('test getOrInsert and getOrInsertComputed', () => { | ||
| const map = new EncodedKeyMap([ | ||
| [Buffer.from('existingBuffer'), 'initialBufferValue'], | ||
| ['existingNullValue', null], | ||
| ]); | ||
|
|
||
| assert.strictEqual( | ||
| map.getOrInsert(Buffer.from('existingBuffer'), 'fallbackValue'), | ||
| 'initialBufferValue', | ||
| ); | ||
| assert.strictEqual( | ||
| map.getOrInsert('existingNullValue', 'fallbackForNull'), | ||
| null, | ||
| ); | ||
| assert.strictEqual( | ||
| map.getOrInsert(Buffer.from('newBuffer'), 'insertedBufferValue'), | ||
| 'insertedBufferValue', | ||
| ); | ||
| assert.strictEqual( | ||
| map.get(Buffer.from('newBuffer')), | ||
| 'insertedBufferValue', | ||
| ); | ||
|
|
||
| let callbackCount = 0; | ||
| assert.strictEqual( | ||
| map.getOrInsertComputed(Buffer.from('existingBuffer'), () => { | ||
| callbackCount++; | ||
| return 'computedFallback'; | ||
| }), | ||
| 'initialBufferValue', | ||
| ); | ||
| assert.strictEqual(callbackCount, 0); | ||
|
|
||
| const computedKey = Buffer.from('computedBuffer'); | ||
| assert.strictEqual( | ||
| map.getOrInsertComputed(computedKey, key => { | ||
| callbackCount++; | ||
| assert.strictEqual(key, computedKey); | ||
| return 'computedBufferValue'; | ||
| }), | ||
| 'computedBufferValue', | ||
| ); | ||
| assert.strictEqual(callbackCount, 1); | ||
| assert.strictEqual( | ||
| map.get(Buffer.from('computedBuffer')), | ||
| 'computedBufferValue', | ||
| ); | ||
|
|
||
| assert.throws( | ||
| () => | ||
| map.getOrInsertComputed( | ||
| 'existingNullValue', | ||
| null as unknown as ( | ||
| key: string | bigint | Uint8Array | null, | ||
| ) => SqlValue, | ||
| ), | ||
| TypeError, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
[Generated] Add unit tests for EncodedKeyMap#getOrInsert and EncodedKeyMap#getOrInsertComputed covering value-based Buffer key matching, preserved null values, lazy callback invocation, and TypeError enforcement when callback is not a function.
There was a problem hiding this comment.
Code Review
This pull request introduces getOrInsert and getOrInsertComputed methods to the EncodedKeyMap class, along with comprehensive unit tests. The feedback recommends optimizing both methods to perform a single lookup on the underlying map and call _parseBufferToMapKey only once, which avoids redundant lookups and reduces key encoding overhead.
| getOrInsert( | ||
| key: string | bigint | Uint8Array | null, | ||
| defaultValue: SqlValue, | ||
| ): SqlValue { | ||
| if (this.has(key)) { | ||
| return this.get(key) as SqlValue; | ||
| } | ||
| this.set(key, defaultValue); | ||
| return defaultValue; | ||
| } |
There was a problem hiding this comment.
The current implementation of getOrInsert performs multiple lookups on the underlying map and calls _parseBufferToMapKey multiple times (in has, get, and set). Since _parseBufferToMapKey can be expensive (especially for Uint8Array keys), we can optimize this by performing a single lookup on this.map_impl using the parsed key.
getOrInsert(
key: string | bigint | Uint8Array | null,
defaultValue: SqlValue,
): SqlValue {
const parsedKey = _parseBufferToMapKey(key);
const entry = this.map_impl.get(parsedKey);
if (entry !== undefined) {
return entry[1];
}
this.map_impl.set(parsedKey, [key, defaultValue]);
return defaultValue;
}| getOrInsertComputed( | ||
| key: string | bigint | Uint8Array | null, | ||
| callback: (key: string | bigint | Uint8Array | null) => SqlValue, | ||
| ): SqlValue { | ||
| if (typeof callback !== 'function') { | ||
| throw new TypeError('callback is not a function'); | ||
| } | ||
| if (this.has(key)) { | ||
| return this.get(key) as SqlValue; | ||
| } | ||
| const value = callback(key); | ||
| this.set(key, value); | ||
| return value; | ||
| } |
There was a problem hiding this comment.
Similarly to getOrInsert, getOrInsertComputed can be optimized to perform a single lookup on this.map_impl and call _parseBufferToMapKey only once, avoiding redundant lookups and key encoding overhead.
getOrInsertComputed(
key: string | bigint | Uint8Array | null,
callback: (key: string | bigint | Uint8Array | null) => SqlValue,
): SqlValue {
if (typeof callback !== 'function') {
throw new TypeError('callback is not a function');
}
const parsedKey = _parseBufferToMapKey(key);
const entry = this.map_impl.get(parsedKey);
if (entry !== undefined) {
return entry[1];
}
const value = callback(key);
this.map_impl.set(parsedKey, [key, value]);
return value;
}|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request adds the getOrInsert and getOrInsertComputed methods to the EncodedKeyMap class, along with comprehensive unit tests. The feedback recommends updating the getOrInsertComputed callback invocation to include the map instance itself as the second argument, aligning with the TC39 specification for Map.prototype.getOrInsertComputed.
| getOrInsertComputed( | ||
| key: string | bigint | Uint8Array | null, | ||
| callback: (key: string | bigint | Uint8Array | null) => SqlValue, | ||
| ): SqlValue { | ||
| if (typeof callback !== 'function') { | ||
| throw new TypeError('callback is not a function'); | ||
| } | ||
| const parsedKey = _parseBufferToMapKey(key); | ||
| const existing = this.map_impl.get(parsedKey); | ||
| if (existing !== undefined) { | ||
| return existing[1]; | ||
| } | ||
| const value = callback(key); | ||
| this.map_impl.set(parsedKey, [key, value]); | ||
| return value; | ||
| } |
There was a problem hiding this comment.
According to the TC39 Map.prototype.getOrInsertComputed specification (and the corresponding TypeScript lib.esnext.collection.d.ts definitions), the callback function should be invoked with two arguments: the key and the map instance itself (i.e., callback(key, map)). Currently, the callback is only invoked with the key argument, which violates the standard API contract and may cause issues if a caller expects the map instance as the second argument.
getOrInsertComputed(
key: string | bigint | Uint8Array | null,
callback: (
key: string | bigint | Uint8Array | null,
map: EncodedKeyMap
) => SqlValue,
): SqlValue {
if (typeof callback !== 'function') {
throw new TypeError('callback is not a function');
}
const parsedKey = _parseBufferToMapKey(key);
const existing = this.map_impl.get(parsedKey);
if (existing !== undefined) {
return existing[1];
}
const value = callback(key, this);
this.map_impl.set(parsedKey, [key, value]);
return value;
}…methods and fix lint
| | Struct | ||
| | Array<SqlValue> | ||
| | BigtableMap; | ||
| ExecuteQueryParameterValue | Struct | Array<SqlValue> | BigtableMap; |
There was a problem hiding this comment.
[Generated] Format export type SqlValue union onto a single line to satisfy Prettier (node ./bin/linter.mjs --strict) in presubmit/lint.
| export class EncodedKeyMap implements ReadonlyMap< | ||
| bigint | string | Uint8Array | null, | ||
| SqlValue | ||
| > { |
There was a problem hiding this comment.
[Generated] Change EncodedKeyMap from implements Map<...> to implements ReadonlyMap<bigint | string | Uint8Array | null, SqlValue> so emitted .d.ts declarations pass TypeScript 7.0 (--target esnext) without adding getOrInsert or getOrInsertComputed methods to EncodedKeyMap.
There was a problem hiding this comment.
This is helpful. Good to know this line was the problem.
| it('implements ReadonlyMap without adding extra methods', () => { | ||
| const map = new EncodedKeyMap([ | ||
| [Buffer.from('existingBuffer'), 'initialBufferValue'], | ||
| ['existingNullValue', null], | ||
| ]); | ||
| const readonlyMap: ReadonlyMap< | ||
| string | bigint | Uint8Array | null, | ||
| SqlValue | ||
| > = map; | ||
| const es2023Map: Map<string | bigint | Uint8Array | null, SqlValue> = map; | ||
| assert.strictEqual( | ||
| readonlyMap.get(Buffer.from('existingBuffer')), | ||
| 'initialBufferValue', | ||
| ); | ||
| assert.strictEqual(es2023Map.get('existingNullValue'), null); | ||
| assert.strictEqual('getOrInsert' in map, false); | ||
| assert.strictEqual('getOrInsertComputed' in map, false); | ||
| }); |
There was a problem hiding this comment.
[Generated] Verify EncodedKeyMap is assignable to ReadonlyMap and ES2023 Map and does not expose getOrInsert or getOrInsertComputed methods.
|
Tracking Buganizer issue: https://b.corp.google.com/issues/576741482 Updated to resolve |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request updates the EncodedKeyMap class to implement ReadonlyMap instead of Map to support newer target environments, and adds a corresponding test. The review feedback correctly identifies that asserting compatibility with the full Map interface in the test will cause a compilation error under --target esnext, and suggests using readonlyMap directly for assertions instead.
| const es2023Map: Map<string | bigint | Uint8Array | null, SqlValue> = map; | ||
| assert.strictEqual( | ||
| readonlyMap.get(Buffer.from('existingBuffer')), | ||
| 'initialBufferValue', | ||
| ); | ||
| assert.strictEqual(es2023Map.get('existingNullValue'), null); |
There was a problem hiding this comment.
Assigning map (which is an instance of EncodedKeyMap implementing ReadonlyMap) to a variable typed as Map will cause a compilation error under --target esnext (TypeScript 7.0+) because EncodedKeyMap does not implement the new Map methods (such as getOrInsert). Since the goal of this PR is to support --target esnext without adding these new methods, we should avoid asserting compatibility with the full Map interface in the tests. Instead, we can use readonlyMap directly for the assertions.
| const es2023Map: Map<string | bigint | Uint8Array | null, SqlValue> = map; | |
| assert.strictEqual( | |
| readonlyMap.get(Buffer.from('existingBuffer')), | |
| 'initialBufferValue', | |
| ); | |
| assert.strictEqual(es2023Map.get('existingNullValue'), null); | |
| assert.strictEqual( | |
| readonlyMap.get(Buffer.from('existingBuffer')), | |
| 'initialBufferValue', | |
| ); | |
| assert.strictEqual(readonlyMap.get('existingNullValue'), null); |
There was a problem hiding this comment.
Okay. Sounds like this is a testing issue we should take care of.
There was a problem hiding this comment.
Addressed in cf58149 — removed the es2023Map: Map<...> assignment and used readonlyMap directly so the test file itself also compiles cleanly under --target esnext.
|
System test failures are just flakey copy backup tests. We will solve them in https://b.corp.google.com/issues/577142125. |
🤖 I have created a release *beep* *boop* --- <details><summary>gapic-generator: 5.7.3</summary> ## [5.7.3](gapic-generator-v5.7.2...gapic-generator-v5.7.3) (2026-10-28) ### Bug Fixes * **core:** Log request/response payloads at DEBUG severity ([#9754](#9754)) ([bf46818](bf46818)) </details> <details><summary>google-gax: 6.14.0</summary> ## [6.14.0](google-gax-v6.13.0...google-gax-v6.14.0) (2026-10-28) ### Features * **gax:** Add observability logging flag ([#9656](#9656)) ([c0ab589](c0ab589)) ### Bug Fixes * **gax:** Suppress cancel destroy error in StreamArrayParser and add Bun certification matrix ([#9652](#9652)) ([a802909](a802909)) </details> <details><summary>gcp-metadata: 9.2.3</summary> ## [9.2.3](gcp-metadata-v9.2.2...gcp-metadata-v9.2.3) (2026-10-28) ### Bug Fixes * **core:** Log request/response payloads at DEBUG severity ([#9754](#9754)) ([bf46818](bf46818)) </details> <details><summary>google-auth-library: 11.3.2</summary> ## [11.3.2](google-auth-library-v11.3.1...google-auth-library-v11.3.2) (2026-10-28) ### Bug Fixes * **core:** Log request/response payloads at DEBUG severity ([#9754](#9754)) ([bf46818](bf46818)) * **google-auth-library:** Add credential deprecation and validation warnings ([#9720](#9720)) ([569678a](569678a)) </details> <details><summary>google-logging-utils: 2.1.1</summary> ## [2.1.1](google-logging-utils-v2.1.0...google-logging-utils-v2.1.1) (2026-10-28) ### Bug Fixes * **core:** Log request/response payloads at DEBUG severity ([#9754](#9754)) ([bf46818](bf46818)) * **logging-utils:** Export LogSeverity as value and treat 'true' as '*' alias ([#9718](#9718)) ([e8da4f0](e8da4f0)) </details> <details><summary>gapic-tools: 2.2.1</summary> ## [2.2.1](gapic-tools-v2.2.0...gapic-tools-v2.2.1) (2026-10-28) ### Bug Fixes * **gapic-tools:** Strip JS method bodies before pbts and parallelize minification ([#9737](#9737)) ([c7884b8](c7884b8)) </details> <details><summary>test-utils: 0.4.0</summary> ## [0.4.0](test-utils-v0.3.1...test-utils-v0.4.0) (2026-10-28) ### Features * **test-utils:** Add createProxyquire and proxyquire helper ([#9759](#9759)) ([763d1bd](763d1bd)) </details> <details><summary>bigtable: 7.5.1</summary> ## [7.5.1](bigtable-v7.5.0...bigtable-v7.5.1) (2026-10-28) ### Bug Fixes * **bigtable:** Use ReadonlyMap for EncodedKeyMap implements clause ([#9709](#9709)) ([f803bff](f803bff)) </details> <details><summary>pubsub: 6.3.2</summary> ## [6.3.2](pubsub-v6.3.1...pubsub-v6.3.2) (2026-10-28) ### Bug Fixes * **pubsub:** Reuse GoogleAuth instance and deprecate credential options ([#9721](#9721)) ([4c4ba34](4c4ba34)) </details> <details><summary>datamanager: 1.5.0</summary> ## [1.5.0](datamanager-v1.4.0...datamanager-v1.5.0) (2026-10-28) ### Features * **datamanager:** Update release_level to stable in .repo-metadata.json ([#9725](#9725)) ([6eb9340](6eb9340)), refs [#9685](#9685) </details> <details><summary>observability: 0.1.0</summary> ## 0.1.0 (2026-10-28) ### Features * **google/cloud/observability/v1:** Onboard a new library ([#9689](#9689)) ([5e53e2e](5e53e2e)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com>
Summary
Resolves TypeScript 7.0 (
--target esnext) typecheck errorTS2420(Class 'EncodedKeyMap' incorrectly implements interface 'Map<...>') in@google-cloud/bigtable(build/src/execute-query/values.d.ts) without adding new methods toEncodedKeyMap.EncodedKeyMap's class heritage clause fromimplements Map<bigint | string | Uint8Array | null, SqlValue>toimplements ReadonlyMap<bigint | string | Uint8Array | null, SqlValue>while keeping all existing methods (clear,delete,forEach,get,has,set,size,entries,keys,values,[Symbol.iterator], [Symbol.toStringTag]) unchanged.implements ReadonlyMap:getOrInsert/getOrInsertComputed) augmentsMapandWeakMapin TypeScript 7.0lib.esnext.collection.d.ts, notReadonlyMap.EncodedKeyMap's runtime API surface with new methods while preserving full structural compatibility with bothReadonlyMap(all TypeScript targets) andMap(ES2015–ES2025 targets)..github/workflows/breaking-change-check.yamlandpresubmit/lint.Stacked PR Chain
fix/bigtable-encoded-key-map-esnext-part-1->main)fix/bigtable-encoded-key-map-esnext-style-maintenance->fix/bigtable-encoded-key-map-esnext-part-1)Testing
handwritten/bigtable/build/test/base64keymap.jsand verified 7x (>= 99%confidence) on Node.js and Bun.pnpm --dir handwritten/bigtable run compile && node bin/run-test.cjs --proxyquire-shim --no-c8 handwritten/bigtable/build/test/base64keymap.js(andnpx --yes -p typescript@7.0.2 tsc --target esnext --module nodenext --moduleResolution nodenext --noEmit handwritten/bigtable/build/src/execute-query/values.d.ts) againstmainto see theTS2420failure, then re-run onHEADto confirm it passes.