Skip to content

Commit 1c08db8

Browse files
committed
Merge remote-tracking branch 'origin/main' into pr-363
# Conflicts: # CHANGELOG.md
2 parents cb264ab + f05f8a9 commit 1c08db8

20 files changed

Lines changed: 1192 additions & 7 deletions

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ node_modules
55
.nyc_output
66
coverage_e2e
77
coverage_unit
8+
coverage
89
.clinic
910

1011
dist

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
- Fix Azure AD OAuth for tenant-specific and single-tenant Entra apps, and correct the scope resource: use the Databricks Azure Login App ID (not the tenant GUID) as the OAuth scope; route OIDC discovery to `login.microsoftonline.com/${azureTenantId}/` when `azureTenantId` is provided (fallback `/organizations/` preserved).
66

7+
## 1.14.0
8+
9+
- Add statement-level query tag support (databricks/databricks-sql-nodejs#366 by @sreekanth-db)
10+
- Add AI coding agent detection to User-Agent header (databricks/databricks-sql-nodejs#333 by @vikrantpuppala)
11+
- Internal: telemetry infrastructure improvements — circuit breaker, feature flag cache, telemetry client management (off by default) (databricks/databricks-sql-nodejs#325, #326, #362)
12+
713
## 1.13.0
814

915
- Add token federation support with custom token providers (databricks/databricks-sql-nodejs#318, databricks/databricks-sql-nodejs#319, databricks/databricks-sql-nodejs#320 by @madhav-db)

examples/query_tags.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const { DBSQLClient } = require('..');
2+
3+
const client = new DBSQLClient();
4+
5+
const host = process.env.DATABRICKS_HOST;
6+
const path = process.env.DATABRICKS_HTTP_PATH;
7+
const token = process.env.DATABRICKS_TOKEN;
8+
9+
client
10+
.connect({ host, path, token })
11+
.then(async (client) => {
12+
// Session-level query tags: applied to every statement run on this session
13+
// (serialized into the session's QUERY_TAGS configuration).
14+
const session = await client.openSession({
15+
queryTags: {
16+
team: 'engineering',
17+
env: 'dev',
18+
driver: 'node',
19+
},
20+
});
21+
22+
// Statement A: inherits session-level tags only.
23+
const opA = await session.executeStatement('SELECT 1 AS inherits_session_tags');
24+
console.log(await opA.fetchAll());
25+
await opA.close();
26+
27+
// Statement B: statement-level query tags via executeStatement options.
28+
// These are passed via confOverlay as "query_tags" and apply ONLY to this statement.
29+
// Note: `env` here overrides the session-level `env: 'dev'` — for this statement
30+
// it will be `env: 'prod'`. Subsequent statements without statement-level tags
31+
// revert to the session-level values.
32+
const opB = await session.executeStatement('SELECT 2 AS has_statement_tags', {
33+
queryTags: {
34+
env: 'prod',
35+
request_id: 'abc-123',
36+
feature: 'reporting',
37+
},
38+
});
39+
console.log(await opB.fetchAll());
40+
await opB.close();
41+
42+
// Statement C: demonstrates escaping of special characters (`\`, `:`, `,`)
43+
// in tag values, plus null/undefined values which serialize as bare keys.
44+
const opC = await session.executeStatement('SELECT 3 AS escaped_and_null_tags', {
45+
queryTags: {
46+
path: 'C:\\users\\me',
47+
note: 'hello, world',
48+
flag: null,
49+
},
50+
});
51+
console.log(await opC.fetchAll());
52+
await opC.close();
53+
54+
await session.close();
55+
await client.close();
56+
})
57+
.catch((error) => {
58+
console.log(error);
59+
});

examples/session_params.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ client
1010
.connect({ host, path, token })
1111
.then(async (client) => {
1212
const session = await client.openSession({
13+
queryTags: {
14+
team: 'engineering',
15+
test: 'session-params',
16+
driver: 'node',
17+
},
1318
configuration: {
14-
QUERY_TAGS: 'team:engineering,test:session-params,driver:node',
1519
ansi_mode: 'false',
1620
},
1721
});

lib/DBSQLClient.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import HttpConnection from './connection/connections/HttpConnection';
1616
import IConnectionOptions from './connection/contracts/IConnectionOptions';
1717
import Status from './dto/Status';
1818
import HiveDriverError from './errors/HiveDriverError';
19-
import { buildUserAgentString, definedOrError } from './utils';
19+
import { buildUserAgentString, definedOrError, serializeQueryTags } from './utils';
2020
import PlainHttpAuthentication from './connection/auth/PlainHttpAuthentication';
2121
import DatabricksOAuth, { OAuthFlow } from './connection/auth/DatabricksOAuth';
2222
import {
@@ -298,6 +298,16 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I
298298
configuration['spark.sql.thriftserver.metadata.metricview.enabled'] = 'true';
299299
}
300300

301+
// Serialize queryTags dict and set in configuration; takes precedence over configuration.QUERY_TAGS
302+
if (request.queryTags !== undefined) {
303+
const serialized = serializeQueryTags(request.queryTags);
304+
if (serialized) {
305+
configuration.QUERY_TAGS = serialized;
306+
} else {
307+
delete configuration.QUERY_TAGS;
308+
}
309+
}
310+
301311
const response = await this.driver.openSession({
302312
client_protocol_i64: new Int64(TProtocolVersion.SPARK_CLI_SERVICE_PROTOCOL_V8),
303313
...getInitialNamespaceOptions(request.initialCatalog, request.initialSchema),

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/IDBSQLClient.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ export interface OpenSessionRequest {
6060
initialCatalog?: string;
6161
initialSchema?: string;
6262
configuration?: { [key: string]: string };
63+
/**
64+
* Session-level query tags as key-value pairs. Serialized and passed via session configuration
65+
* as "QUERY_TAGS". Values may be null/undefined to include a key without a value.
66+
* If both queryTags and configuration.QUERY_TAGS are specified, queryTags takes precedence.
67+
*/
68+
queryTags?: Record<string, string | null | undefined>;
6369
}
6470

6571
export default interface IDBSQLClient {

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/telemetry/TelemetryClient.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Copyright (c) 2025 Databricks Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import IClientContext from '../contracts/IClientContext';
18+
import { LogLevel } from '../contracts/IDBSQLLogger';
19+
20+
/**
21+
* Telemetry client for a specific host.
22+
* Managed by TelemetryClientProvider with reference counting.
23+
* One client instance is shared across all connections to the same host.
24+
*/
25+
class TelemetryClient {
26+
private closed: boolean = false;
27+
28+
constructor(private context: IClientContext, private host: string) {
29+
const logger = context.getLogger();
30+
logger.log(LogLevel.debug, `Created TelemetryClient for host: ${host}`);
31+
}
32+
33+
/**
34+
* Gets the host associated with this client.
35+
*/
36+
getHost(): string {
37+
return this.host;
38+
}
39+
40+
/**
41+
* Checks if the client has been closed.
42+
*/
43+
isClosed(): boolean {
44+
return this.closed;
45+
}
46+
47+
/**
48+
* Closes the telemetry client and releases resources.
49+
* Should only be called by TelemetryClientProvider when reference count reaches zero.
50+
*/
51+
close(): void {
52+
if (this.closed) {
53+
return;
54+
}
55+
try {
56+
this.context.getLogger().log(LogLevel.debug, `Closing TelemetryClient for host: ${this.host}`);
57+
} catch {
58+
// swallow
59+
} finally {
60+
this.closed = true;
61+
}
62+
}
63+
}
64+
65+
export default TelemetryClient;

0 commit comments

Comments
 (0)