Skip to content

Commit 6f1118f

Browse files
committed
add statement level query tags support
1 parent 775e642 commit 6f1118f

File tree

6 files changed

+152
-2
lines changed

6 files changed

+152
-2
lines changed

lib/DBSQLSession.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import IOperation from './contracts/IOperation';
3131
import DBSQLOperation from './DBSQLOperation';
3232
import Status from './dto/Status';
3333
import InfoValue from './dto/InfoValue';
34-
import { definedOrError, LZ4, ProtocolVersion } from './utils';
34+
import { definedOrError, LZ4, ProtocolVersion, serializeQueryTags } from './utils';
3535
import CloseableCollection from './utils/CloseableCollection';
3636
import { LogLevel } from './contracts/IDBSQLLogger';
3737
import HiveDriverError from './errors/HiveDriverError';
@@ -227,6 +227,11 @@ export default class DBSQLSession implements IDBSQLSession {
227227
request.parameters = getQueryParameters(options.namedParameters, options.ordinalParameters);
228228
}
229229

230+
const serializedQueryTags = serializeQueryTags(options.queryTags);
231+
if (serializedQueryTags !== undefined) {
232+
request.confOverlay = { ...request.confOverlay, query_tags: serializedQueryTags };
233+
}
234+
230235
if (ProtocolVersion.supportsCloudFetch(this.serverProtocolVersion)) {
231236
request.canDownloadResult = options.useCloudFetch ?? clientConfig.useCloudFetch;
232237
}

lib/contracts/IDBSQLSession.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ export type ExecuteStatementOptions = {
2121
stagingAllowedLocalPath?: string | string[];
2222
namedParameters?: Record<string, DBSQLParameter | DBSQLParameterValue>;
2323
ordinalParameters?: Array<DBSQLParameter | DBSQLParameterValue>;
24+
/**
25+
* Per-statement query tags as key-value pairs. Serialized and passed via confOverlay
26+
* as "query_tags". Values may be null/undefined to include a key without a value.
27+
* These tags apply only to this statement and do not persist across queries.
28+
*/
29+
queryTags?: Record<string, string | null | undefined>;
2430
};
2531

2632
export type TypeInfoRequest = {

lib/utils/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,14 @@ import buildUserAgentString from './buildUserAgentString';
33
import formatProgress, { ProgressUpdateTransformer } from './formatProgress';
44
import LZ4 from './lz4';
55
import * as ProtocolVersion from './protocolVersion';
6+
import { serializeQueryTags } from './queryTags';
67

7-
export { definedOrError, buildUserAgentString, formatProgress, ProgressUpdateTransformer, LZ4, ProtocolVersion };
8+
export {
9+
definedOrError,
10+
buildUserAgentString,
11+
formatProgress,
12+
ProgressUpdateTransformer,
13+
LZ4,
14+
ProtocolVersion,
15+
serializeQueryTags,
16+
};

lib/utils/queryTags.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Serializes a query tags dictionary into a string for use in confOverlay.
3+
*
4+
* Format: comma-separated key:value pairs, e.g. "key1:value1,key2:value2"
5+
* - If a value is null or undefined, the key is included without a colon or value
6+
* - Special characters (backslash, colon, comma) in values are backslash-escaped
7+
* - Keys are not escaped
8+
*
9+
* @param queryTags - dictionary of query tag key-value pairs
10+
* @returns serialized string, or undefined if input is empty/null/undefined
11+
*/
12+
export function serializeQueryTags(
13+
queryTags: Record<string, string | null | undefined> | null | undefined,
14+
): string | undefined {
15+
if (queryTags == null) {
16+
return undefined;
17+
}
18+
19+
const keys = Object.keys(queryTags);
20+
if (keys.length === 0) {
21+
return undefined;
22+
}
23+
24+
return keys
25+
.map((key) => {
26+
const value = queryTags[key];
27+
if (value == null) {
28+
return key;
29+
}
30+
const escapedValue = value.replace(/[\\:,]/g, (c) => `\\${c}`);
31+
return `${key}:${escapedValue}`;
32+
})
33+
.join(',');
34+
}

tests/unit/DBSQLSession.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,44 @@ describe('DBSQLSession', () => {
259259
});
260260
});
261261

262+
describe('executeStatement with queryTags', () => {
263+
it('should set confOverlay with query_tags when queryTags are provided', async () => {
264+
const context = new ClientContextStub();
265+
const driver = sinon.spy(context.driver);
266+
const session = new DBSQLSession({ handle: sessionHandleStub, context });
267+
268+
await session.executeStatement('SELECT 1', { queryTags: { team: 'eng', app: 'etl' } });
269+
270+
expect(driver.executeStatement.callCount).to.eq(1);
271+
const req = driver.executeStatement.firstCall.args[0];
272+
expect(req.confOverlay).to.deep.include({ query_tags: 'team:eng,app:etl' });
273+
});
274+
275+
it('should not set confOverlay query_tags when queryTags is not provided', async () => {
276+
const context = new ClientContextStub();
277+
const driver = sinon.spy(context.driver);
278+
const session = new DBSQLSession({ handle: sessionHandleStub, context });
279+
280+
await session.executeStatement('SELECT 1');
281+
282+
expect(driver.executeStatement.callCount).to.eq(1);
283+
const req = driver.executeStatement.firstCall.args[0];
284+
expect(req.confOverlay?.query_tags).to.be.undefined;
285+
});
286+
287+
it('should not set confOverlay query_tags when queryTags is empty', async () => {
288+
const context = new ClientContextStub();
289+
const driver = sinon.spy(context.driver);
290+
const session = new DBSQLSession({ handle: sessionHandleStub, context });
291+
292+
await session.executeStatement('SELECT 1', { queryTags: {} });
293+
294+
expect(driver.executeStatement.callCount).to.eq(1);
295+
const req = driver.executeStatement.firstCall.args[0];
296+
expect(req.confOverlay?.query_tags).to.be.undefined;
297+
});
298+
});
299+
262300
describe('getTypeInfo', () => {
263301
it('should run operation', async () => {
264302
const session = new DBSQLSession({ handle: sessionHandleStub, context: new ClientContextStub() });

tests/unit/utils/queryTags.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { expect } from 'chai';
2+
import { serializeQueryTags } from '../../../lib/utils/queryTags';
3+
4+
describe('serializeQueryTags', () => {
5+
it('should return undefined for null input', () => {
6+
expect(serializeQueryTags(null)).to.be.undefined;
7+
});
8+
9+
it('should return undefined for undefined input', () => {
10+
expect(serializeQueryTags(undefined)).to.be.undefined;
11+
});
12+
13+
it('should return undefined for empty object', () => {
14+
expect(serializeQueryTags({})).to.be.undefined;
15+
});
16+
17+
it('should serialize a single tag', () => {
18+
expect(serializeQueryTags({ team: 'engineering' })).to.equal('team:engineering');
19+
});
20+
21+
it('should serialize multiple tags', () => {
22+
const result = serializeQueryTags({ team: 'engineering', app: 'etl' });
23+
expect(result).to.equal('team:engineering,app:etl');
24+
});
25+
26+
it('should omit colon for null value', () => {
27+
expect(serializeQueryTags({ team: null })).to.equal('team');
28+
});
29+
30+
it('should omit colon for undefined value', () => {
31+
expect(serializeQueryTags({ team: undefined })).to.equal('team');
32+
});
33+
34+
it('should mix null and non-null values', () => {
35+
const result = serializeQueryTags({ team: 'eng', flag: null, app: 'etl' });
36+
expect(result).to.equal('team:eng,flag,app:etl');
37+
});
38+
39+
it('should escape backslash in value', () => {
40+
expect(serializeQueryTags({ path: 'a\\b' })).to.equal('path:a\\\\b');
41+
});
42+
43+
it('should escape colon in value', () => {
44+
expect(serializeQueryTags({ url: 'http://host' })).to.equal('url:http\\://host');
45+
});
46+
47+
it('should escape comma in value', () => {
48+
expect(serializeQueryTags({ list: 'a,b' })).to.equal('list:a\\,b');
49+
});
50+
51+
it('should escape multiple special characters in value', () => {
52+
expect(serializeQueryTags({ val: 'a\\b:c,d' })).to.equal('val:a\\\\b\\:c\\,d');
53+
});
54+
55+
it('should not escape special characters in keys', () => {
56+
expect(serializeQueryTags({ 'key:name': 'value' })).to.equal('key:name:value');
57+
});
58+
});

0 commit comments

Comments
 (0)