Skip to content

Commit 8da5457

Browse files
author
Jade Wang
committed
fix(csharp): emit correct metadata telemetry for statement-level metadata commands
Statement-level metadata commands (getcatalogs, gettables, getcolumns, etc.) executed via DatabricksStatement.ExecuteQuery were incorrectly tagged as StatementType.Query/OperationType.ExecuteStatement. This fix correctly emits StatementType.Metadata with the appropriate OperationType (ListCatalogs, ListTables, ListColumns, etc.), aligning with the connection-level GetObjects telemetry. The two paths remain distinguishable via sql_statement_id (populated for statement path, empty for GetObjects path). Co-authored-by: Isaac
1 parent 19ae1c0 commit 8da5457

File tree

3 files changed

+322
-2
lines changed

3 files changed

+322
-2
lines changed

csharp/src/DatabricksStatement.cs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,41 @@ public DatabricksStatement(DatabricksConnection connection)
118118
return ctx;
119119
}
120120

121+
/// <summary>
122+
/// Maps a metadata SQL command to the corresponding telemetry operation type.
123+
/// Returns null if the command is not a recognized metadata command.
124+
/// </summary>
125+
internal static OperationType? GetMetadataOperationType(string? sqlQuery)
126+
{
127+
return sqlQuery?.ToLowerInvariant() switch
128+
{
129+
"getcatalogs" => OperationType.ListCatalogs,
130+
"getschemas" => OperationType.ListSchemas,
131+
"gettables" => OperationType.ListTables,
132+
"getcolumns" or "getcolumnsextended" => OperationType.ListColumns,
133+
"gettabletypes" => OperationType.ListTableTypes,
134+
"getprimarykeys" => OperationType.ListPrimaryKeys,
135+
"getcrossreference" => OperationType.ListCrossReferences,
136+
_ => null
137+
};
138+
}
139+
140+
private StatementTelemetryContext? CreateMetadataTelemetryContext()
141+
{
142+
var session = ((DatabricksConnection)Connection).TelemetrySession;
143+
if (session?.TelemetryClient == null) return null;
144+
145+
var operationType = GetMetadataOperationType(SqlQuery) ?? OperationType.Unspecified;
146+
147+
var ctx = new StatementTelemetryContext(session);
148+
ctx.OperationType = operationType;
149+
ctx.StatementType = Telemetry.Proto.Statement.Types.Type.Metadata;
150+
ctx.ResultFormat = ExecutionResultFormat.InlineArrow;
151+
ctx.IsCompressed = false;
152+
ctx.IsInternalCall = IsInternalCall;
153+
return ctx;
154+
}
155+
121156
private void RecordSuccess(StatementTelemetryContext ctx)
122157
{
123158
ctx.RecordFirstBatchReady();
@@ -136,7 +171,9 @@ private void RecordError(StatementTelemetryContext ctx, Exception ex)
136171

137172
public override QueryResult ExecuteQuery()
138173
{
139-
var ctx = CreateTelemetryContext(Telemetry.Proto.Statement.Types.Type.Query);
174+
var ctx = IsMetadataCommand
175+
? CreateMetadataTelemetryContext()
176+
: CreateTelemetryContext(Telemetry.Proto.Statement.Types.Type.Query);
140177
if (ctx == null) return base.ExecuteQuery();
141178

142179
try
@@ -159,7 +196,9 @@ public override QueryResult ExecuteQuery()
159196

160197
public override async ValueTask<QueryResult> ExecuteQueryAsync()
161198
{
162-
var ctx = CreateTelemetryContext(Telemetry.Proto.Statement.Types.Type.Query);
199+
var ctx = IsMetadataCommand
200+
? CreateMetadataTelemetryContext()
201+
: CreateTelemetryContext(Telemetry.Proto.Statement.Types.Type.Query);
163202
if (ctx == null) return await base.ExecuteQueryAsync();
164203

165204
try
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/*
2+
* Copyright (c) 2025 ADBC Drivers 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+
using System;
18+
using System.Collections.Generic;
19+
using System.Linq;
20+
using System.Threading.Tasks;
21+
using AdbcDrivers.Databricks.Telemetry;
22+
using AdbcDrivers.HiveServer2;
23+
using Apache.Arrow.Adbc;
24+
using Apache.Arrow.Adbc.Tests;
25+
using Xunit;
26+
using Xunit.Abstractions;
27+
using OperationType = AdbcDrivers.Databricks.Telemetry.Proto.Operation.Types.Type;
28+
using StatementType = AdbcDrivers.Databricks.Telemetry.Proto.Statement.Types.Type;
29+
30+
namespace AdbcDrivers.Databricks.Tests.E2E.Telemetry
31+
{
32+
/// <summary>
33+
/// E2E tests for statement-level metadata command telemetry.
34+
/// Validates that metadata commands executed via DatabricksStatement.ExecuteQuery
35+
/// (e.g., SqlQuery = "getcatalogs") emit telemetry with correct StatementType.Metadata
36+
/// and the appropriate OperationType, rather than StatementType.Query/OperationType.ExecuteStatement.
37+
/// </summary>
38+
public class StatementMetadataTelemetryTests : TestBase<DatabricksTestConfiguration, DatabricksTestEnvironment>
39+
{
40+
// Filters to scope metadata queries and avoid MaxMessageSize errors
41+
private const string TestCatalog = "main";
42+
private const string TestSchema = "adbc_testing";
43+
private const string TestTable = "all_column_types";
44+
45+
public StatementMetadataTelemetryTests(ITestOutputHelper? outputHelper)
46+
: base(outputHelper, new DatabricksTestEnvironment.Factory())
47+
{
48+
Skip.IfNot(Utils.CanExecuteTestConfig(TestConfigVariable));
49+
}
50+
51+
[SkippableFact]
52+
public async Task Telemetry_StatementGetCatalogs_EmitsMetadataWithListCatalogs()
53+
{
54+
await AssertStatementMetadataTelemetry(
55+
command: "getcatalogs",
56+
expectedOperationType: OperationType.ListCatalogs);
57+
}
58+
59+
[SkippableFact]
60+
public async Task Telemetry_StatementGetSchemas_EmitsMetadataWithListSchemas()
61+
{
62+
await AssertStatementMetadataTelemetry(
63+
command: "getschemas",
64+
expectedOperationType: OperationType.ListSchemas,
65+
options: new Dictionary<string, string>
66+
{
67+
[ApacheParameters.CatalogName] = TestCatalog,
68+
});
69+
}
70+
71+
[SkippableFact]
72+
public async Task Telemetry_StatementGetTables_EmitsMetadataWithListTables()
73+
{
74+
await AssertStatementMetadataTelemetry(
75+
command: "gettables",
76+
expectedOperationType: OperationType.ListTables,
77+
options: new Dictionary<string, string>
78+
{
79+
[ApacheParameters.CatalogName] = TestCatalog,
80+
[ApacheParameters.SchemaName] = TestSchema,
81+
});
82+
}
83+
84+
[SkippableFact]
85+
public async Task Telemetry_StatementGetColumns_EmitsMetadataWithListColumns()
86+
{
87+
await AssertStatementMetadataTelemetry(
88+
command: "getcolumns",
89+
expectedOperationType: OperationType.ListColumns,
90+
options: new Dictionary<string, string>
91+
{
92+
[ApacheParameters.CatalogName] = TestCatalog,
93+
[ApacheParameters.SchemaName] = TestSchema,
94+
[ApacheParameters.TableName] = TestTable,
95+
});
96+
}
97+
98+
[SkippableFact]
99+
public async Task Telemetry_StatementMetadata_AllCommands_EmitCorrectOperationType()
100+
{
101+
CapturingTelemetryExporter exporter = null!;
102+
AdbcConnection? connection = null;
103+
104+
try
105+
{
106+
var properties = TestEnvironment.GetDriverParameters(TestConfiguration);
107+
(connection, exporter) = TelemetryTestHelpers.CreateConnectionWithCapturingTelemetry(properties);
108+
109+
var commandMappings = new (string Command, OperationType ExpectedOp, Dictionary<string, string>? Options)[]
110+
{
111+
("getcatalogs", OperationType.ListCatalogs, null),
112+
("getschemas", OperationType.ListSchemas, new Dictionary<string, string>
113+
{
114+
[ApacheParameters.CatalogName] = TestCatalog,
115+
}),
116+
("gettables", OperationType.ListTables, new Dictionary<string, string>
117+
{
118+
[ApacheParameters.CatalogName] = TestCatalog,
119+
[ApacheParameters.SchemaName] = TestSchema,
120+
}),
121+
("getcolumns", OperationType.ListColumns, new Dictionary<string, string>
122+
{
123+
[ApacheParameters.CatalogName] = TestCatalog,
124+
[ApacheParameters.SchemaName] = TestSchema,
125+
[ApacheParameters.TableName] = TestTable,
126+
}),
127+
};
128+
129+
foreach (var mapping in commandMappings)
130+
{
131+
exporter.Reset();
132+
133+
// Explicit using block so statement is disposed (and telemetry emitted) before we check
134+
using (var statement = connection.CreateStatement())
135+
{
136+
statement.SetOption(ApacheParameters.IsMetadataCommand, "true");
137+
statement.SqlQuery = mapping.Command;
138+
139+
if (mapping.Options != null)
140+
{
141+
foreach (var opt in mapping.Options)
142+
{
143+
statement.SetOption(opt.Key, opt.Value);
144+
}
145+
}
146+
147+
var result = statement.ExecuteQuery();
148+
result.Stream?.Dispose();
149+
}
150+
151+
// Flush telemetry after statement disposal
152+
if (connection is DatabricksConnection dbConn && dbConn.TelemetrySession?.TelemetryClient != null)
153+
{
154+
await dbConn.TelemetrySession.TelemetryClient.FlushAsync(default);
155+
}
156+
157+
var logs = await TelemetryTestHelpers.WaitForTelemetryEvents(exporter, expectedCount: 1, timeoutMs: 5000);
158+
Assert.NotEmpty(logs);
159+
160+
var log = TelemetryTestHelpers.FindLog(logs, proto =>
161+
proto.SqlOperation?.OperationDetail?.OperationType == mapping.ExpectedOp);
162+
163+
Assert.NotNull(log);
164+
165+
var protoLog = TelemetryTestHelpers.GetProtoLog(log);
166+
Assert.Equal(StatementType.Metadata, protoLog.SqlOperation.StatementType);
167+
Assert.Equal(mapping.ExpectedOp, protoLog.SqlOperation.OperationDetail.OperationType);
168+
}
169+
}
170+
finally
171+
{
172+
connection?.Dispose();
173+
TelemetryTestHelpers.ClearExporterOverride();
174+
}
175+
}
176+
177+
/// <summary>
178+
/// Helper method to test a single statement-level metadata command emits the correct telemetry.
179+
/// </summary>
180+
private async Task AssertStatementMetadataTelemetry(
181+
string command,
182+
OperationType expectedOperationType,
183+
Dictionary<string, string>? options = null)
184+
{
185+
CapturingTelemetryExporter exporter = null!;
186+
AdbcConnection? connection = null;
187+
188+
try
189+
{
190+
var properties = TestEnvironment.GetDriverParameters(TestConfiguration);
191+
(connection, exporter) = TelemetryTestHelpers.CreateConnectionWithCapturingTelemetry(properties);
192+
193+
// Execute metadata command via statement path
194+
// Explicit using block so statement is disposed (and telemetry emitted) before we check
195+
using (var statement = connection.CreateStatement())
196+
{
197+
statement.SetOption(ApacheParameters.IsMetadataCommand, "true");
198+
statement.SqlQuery = command;
199+
200+
if (options != null)
201+
{
202+
foreach (var opt in options)
203+
{
204+
statement.SetOption(opt.Key, opt.Value);
205+
}
206+
}
207+
208+
var result = statement.ExecuteQuery();
209+
result.Stream?.Dispose();
210+
}
211+
212+
// Flush telemetry after statement disposal
213+
if (connection is DatabricksConnection dbConn && dbConn.TelemetrySession?.TelemetryClient != null)
214+
{
215+
await dbConn.TelemetrySession.TelemetryClient.FlushAsync(default);
216+
}
217+
218+
// Wait for telemetry events
219+
var logs = await TelemetryTestHelpers.WaitForTelemetryEvents(exporter, expectedCount: 1, timeoutMs: 5000);
220+
221+
Assert.NotEmpty(logs);
222+
223+
// Find the metadata telemetry log with correct operation type
224+
var log = TelemetryTestHelpers.FindLog(logs, proto =>
225+
proto.SqlOperation?.OperationDetail?.OperationType == expectedOperationType);
226+
227+
Assert.NotNull(log);
228+
229+
var protoLog = TelemetryTestHelpers.GetProtoLog(log);
230+
231+
// Verify statement type is METADATA (not QUERY)
232+
Assert.Equal(StatementType.Metadata, protoLog.SqlOperation.StatementType);
233+
234+
// Verify operation type matches the metadata command
235+
Assert.Equal(expectedOperationType, protoLog.SqlOperation.OperationDetail.OperationType);
236+
237+
// Verify basic session-level telemetry fields are populated
238+
TelemetryTestHelpers.AssertSessionFieldsPopulated(protoLog);
239+
}
240+
finally
241+
{
242+
connection?.Dispose();
243+
TelemetryTestHelpers.ClearExporterOverride();
244+
}
245+
}
246+
}
247+
}

csharp/test/Unit/DatabricksStatementUnitTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using AdbcDrivers.HiveServer2.Spark;
2121
using AdbcDrivers.Databricks;
2222
using Xunit;
23+
using OperationType = AdbcDrivers.Databricks.Telemetry.Proto.Operation.Types.Type;
2324

2425
namespace AdbcDrivers.Databricks.Tests.Unit
2526
{
@@ -126,5 +127,38 @@ public void CreateStatement_ConfOverlayInitiallyNull()
126127
var confOverlay = GetConfOverlay(statement);
127128
Assert.Null(confOverlay);
128129
}
130+
131+
[Theory]
132+
[InlineData("getcatalogs", OperationType.ListCatalogs)]
133+
[InlineData("getschemas", OperationType.ListSchemas)]
134+
[InlineData("gettables", OperationType.ListTables)]
135+
[InlineData("getcolumns", OperationType.ListColumns)]
136+
[InlineData("getcolumnsextended", OperationType.ListColumns)]
137+
[InlineData("gettabletypes", OperationType.ListTableTypes)]
138+
[InlineData("getprimarykeys", OperationType.ListPrimaryKeys)]
139+
[InlineData("getcrossreference", OperationType.ListCrossReferences)]
140+
public void GetMetadataOperationType_ReturnsCorrectType(string command, OperationType expected)
141+
{
142+
Assert.Equal(expected, DatabricksStatement.GetMetadataOperationType(command));
143+
}
144+
145+
[Theory]
146+
[InlineData(null)]
147+
[InlineData("")]
148+
[InlineData("SELECT 1")]
149+
[InlineData("unknown_command")]
150+
public void GetMetadataOperationType_ReturnsNull_ForNonMetadataCommands(string? command)
151+
{
152+
Assert.Null(DatabricksStatement.GetMetadataOperationType(command));
153+
}
154+
155+
[Theory]
156+
[InlineData("GETCATALOGS")]
157+
[InlineData("GetCatalogs")]
158+
[InlineData("GetTables")]
159+
public void GetMetadataOperationType_IsCaseInsensitive(string command)
160+
{
161+
Assert.NotNull(DatabricksStatement.GetMetadataOperationType(command));
162+
}
129163
}
130164
}

0 commit comments

Comments
 (0)