@@ -2253,6 +2253,168 @@ private void bindCost(Span span, Statement statement, String index) {
22532253 }
22542254 }
22552255
2256+ private static final String BULK_UPDATE = """
2257+ INSERT INTO spans (
2258+ id,
2259+ project_id,
2260+ workspace_id,
2261+ trace_id,
2262+ parent_span_id,
2263+ name,
2264+ type,
2265+ start_time,
2266+ end_time,
2267+ input,
2268+ output,
2269+ metadata,
2270+ model,
2271+ provider,
2272+ total_estimated_cost,
2273+ total_estimated_cost_version,
2274+ tags,
2275+ usage,
2276+ error_info,
2277+ created_at,
2278+ created_by,
2279+ last_updated_by,
2280+ truncation_threshold
2281+ )
2282+ SELECT
2283+ s.id,
2284+ s.project_id,
2285+ s.workspace_id,
2286+ s.trace_id,
2287+ s.parent_span_id,
2288+ <if(name)> :name <else> s.name <endif> as name,
2289+ <if(type)> :type <else> s.type <endif> as type,
2290+ s.start_time,
2291+ <if(end_time)> parseDateTime64BestEffort(:end_time, 9) <else> s.end_time <endif> as end_time,
2292+ <if(input)> :input <else> s.input <endif> as input,
2293+ <if(output)> :output <else> s.output <endif> as output,
2294+ <if(metadata)> :metadata <else> s.metadata <endif> as metadata,
2295+ <if(model)> :model <else> s.model <endif> as model,
2296+ <if(provider)> :provider <else> s.provider <endif> as provider,
2297+ <if(total_estimated_cost)> toDecimal128(:total_estimated_cost, 12) <else> s.total_estimated_cost <endif> as total_estimated_cost,
2298+ <if(total_estimated_cost_version)> :total_estimated_cost_version <else> s.total_estimated_cost_version <endif> as total_estimated_cost_version,
2299+ <if(tags)><if(merge_tags)>arrayConcat(s.tags, :tags)<else>:tags<endif><else>s.tags<endif> as tags,
2300+ <if(usage)> CAST((:usageKeys, :usageValues), 'Map(String, Int64)') <else> s.usage <endif> as usage,
2301+ <if(error_info)> :error_info <else> s.error_info <endif> as error_info,
2302+ s.created_at,
2303+ s.created_by,
2304+ :user_name as last_updated_by,
2305+ :truncation_threshold
2306+ FROM spans s
2307+ WHERE s.id IN :ids AND s.workspace_id = :workspace_id
2308+ ORDER BY (s.workspace_id, s.project_id, s.trace_id, s.parent_span_id, s.id) DESC, s.last_updated_at DESC
2309+ LIMIT 1 BY s.id;
2310+ """ ;
2311+
2312+ @ WithSpan
2313+ public Mono <Void > bulkUpdate (@ NonNull Set <UUID > ids , @ NonNull SpanUpdate update , boolean mergeTags ) {
2314+ Preconditions .checkArgument (!ids .isEmpty (), "ids must not be empty" );
2315+ log .info ("Bulk updating '{}' spans" , ids .size ());
2316+
2317+ var template = newBulkUpdateTemplate (update , BULK_UPDATE , mergeTags );
2318+ var query = template .render ();
2319+
2320+ return Mono .from (connectionFactory .create ())
2321+ .flatMapMany (connection -> {
2322+ var statement = connection .createStatement (query )
2323+ .bind ("ids" , ids );
2324+
2325+ bindBulkUpdateParams (update , statement );
2326+ TruncationUtils .bindTruncationThreshold (statement , "truncation_threshold" , configuration );
2327+
2328+ Segment segment = startSegment ("spans" , "Clickhouse" , "bulk_update" );
2329+
2330+ return makeFluxContextAware (bindUserNameAndWorkspaceContextToStream (statement ))
2331+ .doFinally (signalType -> endSegment (segment ));
2332+ })
2333+ .then ()
2334+ .doOnSuccess (__ -> log .info ("Completed bulk update for '{}' spans" , ids .size ()));
2335+ }
2336+
2337+ private ST newBulkUpdateTemplate (SpanUpdate spanUpdate , String sql , boolean mergeTags ) {
2338+ var template = new ST (sql );
2339+
2340+ if (StringUtils .isNotBlank (spanUpdate .name ())) {
2341+ template .add ("name" , spanUpdate .name ());
2342+ }
2343+ Optional .ofNullable (spanUpdate .type ())
2344+ .ifPresent (type -> template .add ("type" , type .toString ()));
2345+ Optional .ofNullable (spanUpdate .input ())
2346+ .ifPresent (input -> template .add ("input" , input .toString ()));
2347+ Optional .ofNullable (spanUpdate .output ())
2348+ .ifPresent (output -> template .add ("output" , output .toString ()));
2349+ Optional .ofNullable (spanUpdate .tags ())
2350+ .ifPresent (tags -> {
2351+ template .add ("tags" , tags .toString ());
2352+ template .add ("merge_tags" , mergeTags );
2353+ });
2354+ Optional .ofNullable (spanUpdate .metadata ())
2355+ .ifPresent (metadata -> template .add ("metadata" , metadata .toString ()));
2356+ if (StringUtils .isNotBlank (spanUpdate .model ())) {
2357+ template .add ("model" , spanUpdate .model ());
2358+ }
2359+ if (StringUtils .isNotBlank (spanUpdate .provider ())) {
2360+ template .add ("provider" , spanUpdate .provider ());
2361+ }
2362+ Optional .ofNullable (spanUpdate .endTime ())
2363+ .ifPresent (endTime -> template .add ("end_time" , endTime .toString ()));
2364+ Optional .ofNullable (spanUpdate .usage ())
2365+ .ifPresent (usage -> template .add ("usage" , usage .toString ()));
2366+ Optional .ofNullable (spanUpdate .errorInfo ())
2367+ .ifPresent (errorInfo -> template .add ("error_info" , JsonUtils .readTree (errorInfo ).toString ()));
2368+
2369+ if (spanUpdate .totalEstimatedCost () != null ) {
2370+ template .add ("total_estimated_cost" , "total_estimated_cost" );
2371+ template .add ("total_estimated_cost_version" , "total_estimated_cost_version" );
2372+ }
2373+ return template ;
2374+ }
2375+
2376+ private void bindBulkUpdateParams (SpanUpdate spanUpdate , Statement statement ) {
2377+ if (StringUtils .isNotBlank (spanUpdate .name ())) {
2378+ statement .bind ("name" , spanUpdate .name ());
2379+ }
2380+ Optional .ofNullable (spanUpdate .type ())
2381+ .ifPresent (type -> statement .bind ("type" , type .toString ()));
2382+ Optional .ofNullable (spanUpdate .input ())
2383+ .ifPresent (input -> statement .bind ("input" , input .toString ()));
2384+ Optional .ofNullable (spanUpdate .output ())
2385+ .ifPresent (output -> statement .bind ("output" , output .toString ()));
2386+ Optional .ofNullable (spanUpdate .tags ())
2387+ .ifPresent (tags -> statement .bind ("tags" , tags .toArray (String []::new )));
2388+ Optional .ofNullable (spanUpdate .usage ())
2389+ .ifPresent (usage -> {
2390+ var usageKeys = new ArrayList <String >();
2391+ var usageValues = new ArrayList <Integer >();
2392+ for (var entry : usage .entrySet ()) {
2393+ usageKeys .add (entry .getKey ());
2394+ usageValues .add (entry .getValue ());
2395+ }
2396+ statement .bind ("usageKeys" , usageKeys .toArray (String []::new ));
2397+ statement .bind ("usageValues" , usageValues .toArray (Integer []::new ));
2398+ });
2399+ Optional .ofNullable (spanUpdate .endTime ())
2400+ .ifPresent (endTime -> statement .bind ("end_time" , endTime .toString ()));
2401+ Optional .ofNullable (spanUpdate .metadata ())
2402+ .ifPresent (metadata -> statement .bind ("metadata" , metadata .toString ()));
2403+ if (StringUtils .isNotBlank (spanUpdate .model ())) {
2404+ statement .bind ("model" , spanUpdate .model ());
2405+ }
2406+ if (StringUtils .isNotBlank (spanUpdate .provider ())) {
2407+ statement .bind ("provider" , spanUpdate .provider ());
2408+ }
2409+ Optional .ofNullable (spanUpdate .errorInfo ())
2410+ .ifPresent (errorInfo -> statement .bind ("error_info" , JsonUtils .readTree (errorInfo ).toString ()));
2411+
2412+ if (spanUpdate .totalEstimatedCost () != null ) {
2413+ statement .bind ("total_estimated_cost" , spanUpdate .totalEstimatedCost ().toString ());
2414+ statement .bind ("total_estimated_cost_version" , "" );
2415+ }
2416+ }
2417+
22562418 private JsonNode getMetadataWithProvider (Row row , Set <SpanField > exclude , String provider ) {
22572419 // Parse base metadata from database
22582420 JsonNode baseMetadata = Optional
0 commit comments