@@ -1213,6 +1213,132 @@ TEST_F(LogicalPlannerTest, ClickHouseExportWithExplicitEndpoint) {
12131213 EXPECT_TRUE (has_clickhouse_export);
12141214}
12151215
1216+ constexpr char kClickHouseExportQuery [] = R"pxl(
1217+ import px
1218+
1219+ # Test ClickHouse export using endpoint config
1220+ df = px.DataFrame('http_events', start_time='-10m')
1221+ df = df[['time_', 'req_path', 'resp_status', 'resp_latency_ns']]
1222+ px.export(df, px.otel.ClickHouseRows(table='http_events'))
1223+ )pxl" ;
1224+
1225+ TEST_F (LogicalPlannerTest, ClickHouseExportWithEndpointConfig) {
1226+ auto planner = LogicalPlanner::Create (info_).ConsumeValueOrDie ();
1227+
1228+ // Create a planner state with an OTel endpoint config containing ClickHouse DSN
1229+ auto state = testutils::CreateTwoPEMsOneKelvinPlannerState (testutils::kHttpEventsSchema );
1230+
1231+ // Set up the endpoint config with ClickHouse DSN in the URL field
1232+ auto * endpoint_config = state.mutable_otel_endpoint_config ();
1233+ endpoint_config->
set_url (
" clickhouse_user:[email protected] :9000/pixie_db" );
1234+ endpoint_config->set_insecure (true );
1235+ endpoint_config->set_timeout (10 );
1236+
1237+ auto plan_or_s = planner->Plan (MakeQueryRequest (state, kClickHouseExportQuery ));
1238+ EXPECT_OK (plan_or_s);
1239+ auto plan = plan_or_s.ConsumeValueOrDie ();
1240+ EXPECT_OK (plan->ToProto ());
1241+
1242+ // Verify the plan contains ClickHouse export sink operators with correct config
1243+ auto plan_pb = plan->ToProto ().ConsumeValueOrDie ();
1244+ bool has_clickhouse_export = false ;
1245+
1246+ for (const auto & [address, agent_plan] : plan_pb.qb_address_to_plan ()) {
1247+ for (const auto & planFragment : agent_plan.nodes ()) {
1248+ for (const auto & planNode : planFragment.nodes ()) {
1249+ if (planNode.op ().op_type () == planpb::OperatorType::CLICKHOUSE_EXPORT_SINK_OPERATOR) {
1250+ const auto & clickhouse_sink_op = planNode.op ().clickhouse_sink_op ();
1251+
1252+ // Verify table name
1253+ EXPECT_EQ (clickhouse_sink_op.table_name (), " http_events" );
1254+
1255+ // Verify the DSN was parsed correctly into ClickHouseConfig
1256+ const auto & config = clickhouse_sink_op.clickhouse_config ();
1257+ EXPECT_EQ (config.username (), " clickhouse_user" );
1258+ EXPECT_EQ (config.password (), " clickhouse_pass" );
1259+ EXPECT_EQ (config.host (), " clickhouse.example.com" );
1260+ EXPECT_EQ (config.port (), 9000 );
1261+ EXPECT_EQ (config.database (), " pixie_db" );
1262+
1263+ // Verify column mappings were created
1264+ EXPECT_GT (clickhouse_sink_op.column_mappings_size (), 0 );
1265+
1266+ has_clickhouse_export = true ;
1267+ break ;
1268+ }
1269+ }
1270+ if (has_clickhouse_export) break ;
1271+ }
1272+ if (has_clickhouse_export) break ;
1273+ }
1274+
1275+ EXPECT_TRUE (has_clickhouse_export);
1276+ }
1277+
1278+ constexpr char kClickHouseExportWithExplicitEndpointQuery [] = R"pxl(
1279+ import px
1280+
1281+ # Test ClickHouse export with explicit endpoint config
1282+ df = px.DataFrame('http_events', start_time='-10m')
1283+ df = df[['time_', 'req_path', 'resp_status']]
1284+
1285+ endpoint = px.otel.Endpoint(
1286+ url="explicit_user:explicit_pass@explicit-host:9001/explicit_db",
1287+ insecure=False,
1288+ timeout=20
1289+ )
1290+
1291+ px.export(df, px.otel.ClickHouseRows(table='custom_table', endpoint=endpoint))
1292+ )pxl" ;
1293+
1294+ TEST_F (LogicalPlannerTest, ClickHouseExportWithExplicitEndpoint) {
1295+ auto planner = LogicalPlanner::Create (info_).ConsumeValueOrDie ();
1296+
1297+ // Create a planner state with a default endpoint config
1298+ auto state = testutils::CreateTwoPEMsOneKelvinPlannerState (testutils::kHttpEventsSchema );
1299+
1300+ // Set up a default endpoint config (should be overridden by explicit endpoint)
1301+ auto * endpoint_config = state.mutable_otel_endpoint_config ();
1302+ endpoint_config->set_url (" default_user:default_pass@default-host:9000/default_db" );
1303+
1304+ auto plan_or_s = planner->Plan (MakeQueryRequest (state, kClickHouseExportWithExplicitEndpointQuery ));
1305+ EXPECT_OK (plan_or_s);
1306+ auto plan = plan_or_s.ConsumeValueOrDie ();
1307+ EXPECT_OK (plan->ToProto ());
1308+
1309+ // Verify the plan uses the explicit endpoint config, not the default
1310+ auto plan_pb = plan->ToProto ().ConsumeValueOrDie ();
1311+ bool has_clickhouse_export = false ;
1312+
1313+ for (const auto & [address, agent_plan] : plan_pb.qb_address_to_plan ()) {
1314+ for (const auto & planFragment : agent_plan.nodes ()) {
1315+ for (const auto & planNode : planFragment.nodes ()) {
1316+ if (planNode.op ().op_type () == planpb::OperatorType::CLICKHOUSE_EXPORT_SINK_OPERATOR) {
1317+ const auto & clickhouse_sink_op = planNode.op ().clickhouse_sink_op ();
1318+
1319+ // Verify table name
1320+ EXPECT_EQ (clickhouse_sink_op.table_name (), " custom_table" );
1321+
1322+ // Verify the explicit endpoint was used, not the default
1323+ const auto & config = clickhouse_sink_op.clickhouse_config ();
1324+ EXPECT_EQ (config.username (), " explicit_user" );
1325+ EXPECT_EQ (config.password (), " explicit_pass" );
1326+ EXPECT_EQ (config.host (), " explicit-host" );
1327+ EXPECT_EQ (config.port (), 9001 );
1328+ EXPECT_EQ (config.database (), " explicit_db" );
1329+
1330+ has_clickhouse_export = true ;
1331+ break ;
1332+ }
1333+ }
1334+ if (has_clickhouse_export) break ;
1335+ }
1336+ if (has_clickhouse_export) break ;
1337+ }
1338+
1339+ EXPECT_TRUE (has_clickhouse_export);
1340+ }
1341+
12161342} // namespace planner
12171343} // namespace carnot
12181344} // namespace px
0 commit comments