@@ -707,6 +707,15 @@ function _transform_single_time_series!(
707707 return
708708end
709709
710+ # Every requested feature key-value pair must be present in the existing metadata's
711+ # features. This is an intentionally exact match, stricter than the SQL LIKE-based
712+ # partial feature match in list_metadata, which can over-match on substrings
713+ # (e.g., a request for key=1 matches a stored key=10) and on LIKE wildcard
714+ # characters in string values.
715+ _features_contain (existing_features, requested_features) =
716+ all (kv -> get (existing_features, kv. first, nothing ) === kv. second,
717+ requested_features)
718+
710719"""
711720Check that all existing SingleTimeSeries can be converted to DeterministicSingleTimeSeries
712721with the given horizon and interval.
@@ -724,12 +733,38 @@ function _check_transform_single_time_series(
724733 resolution:: Union{Nothing, Dates.Period} ;
725734 skip_existing:: Bool = false ,
726735)
736+ store = data. time_series_manager. metadata_store
727737 items = list_metadata_with_owner_uuid (
728- data . time_series_manager . metadata_store ,
738+ store ,
729739 InfrastructureSystemsComponent;
730740 time_series_type = SingleTimeSeries,
731741 resolution = resolution,
732742 )
743+
744+ # The loop below runs once per SingleTimeSeries (potentially tens of thousands). To
745+ # avoid issuing one or more SQLite queries per iteration we build lookups up front:
746+ # - system forecast parameters depend only on (resolution, interval), so memoize them.
747+ # - fetch all existing Deterministic and DeterministicSingleTimeSeries metadata with
748+ # one query and index it by (owner_uuid, name, resolution). The per-series conflict
749+ # and skip_existing checks then run in memory against this lookup.
750+ forecast_params_cache =
751+ Dict {Tuple{Dates.Period, Dates.Period}, Union{Nothing, ForecastParameters}} ()
752+ existing_forecasts =
753+ Dict {Tuple{Base.UUID, String, String}, Vector{TimeSeriesMetadata}} ()
754+ for entry in list_metadata_with_owner_uuid (
755+ store,
756+ InfrastructureSystemsComponent;
757+ # Note: Deterministic matches both Deterministic and DeterministicSingleTimeSeries.
758+ time_series_type = Deterministic,
759+ )
760+ key = (
761+ entry. owner_uuid,
762+ get_name (entry. metadata),
763+ _serialize_period (get_resolution (entry. metadata)),
764+ )
765+ push! (get! (() -> TimeSeriesMetadata[], existing_forecasts, key), entry. metadata)
766+ end
767+
733768 components_with_params_and_metadata = NamedTuple[]
734769 for item in items
735770 params = _check_single_time_series_transformed_parameters (
@@ -738,61 +773,56 @@ function _check_transform_single_time_series(
738773 horizon,
739774 interval,
740775 )
741- system_params = get_forecast_parameters (
742- data. time_series_manager. metadata_store;
743- resolution = params. resolution,
744- interval = params. interval,
745- )
776+ system_params = get! (forecast_params_cache, (params. resolution, params. interval)) do
777+ get_forecast_parameters (
778+ store;
779+ resolution = params. resolution,
780+ interval = params. interval,
781+ )
782+ end
746783 check_params_compatibility (system_params, params)
747784 component = get_component (data, item. owner_uuid)
748785
786+ ts_name = get_name (item. metadata)
787+ ts_resolution = get_resolution (item. metadata)
788+ ts_features = get_features (item. metadata)
789+ existing = get (
790+ existing_forecasts,
791+ (item. owner_uuid, ts_name, _serialize_period (ts_resolution)),
792+ nothing ,
793+ )
794+
749795 # We do not allow a component to have both Deterministic and
750796 # DeterministicSingleTimeSeries with the same parameters.
751797 # The user might be calling this function because some components are missing
752798 # Deterministic forecasts. If other components already have Deterministic forecasts,
753799 # this check will fail.
754800 # transform_single_time_series! cannot be called at the component level.
755- # Note: has_metadata with Deterministic matches both Deterministic and
756- # DeterministicSingleTimeSeries. Use list_metadata and filter to check only for
757- # actual Deterministic forecasts.
758- ts_name = get_name (item. metadata)
759- ts_resolution = get_resolution (item. metadata)
760- ts_features = get_features (item. metadata)
761- ts_features_symbols = Dict {Symbol, Any} (Symbol (k) => v for (k, v) in ts_features)
762- existing_det = list_metadata (
763- data. time_series_manager. metadata_store,
764- component;
765- time_series_type = Deterministic,
766- name = ts_name,
767- resolution = ts_resolution,
768- ts_features_symbols... ,
769- )
770- if any (m -> get_time_series_type (m) === Deterministic, existing_det)
771- throw (
772- ConflictingInputsError (
773- " Cannot transform SingleTimeSeries to DeterministicSingleTimeSeries: " *
774- " A Deterministic forecast already exists for component $(summary (component)) " *
775- " with name='$ts_name ', resolution=$ts_resolution , and features=$ts_features " ,
776- ),
801+ if ! isnothing (existing)
802+ if any (
803+ m ->
804+ get_time_series_type (m) === Deterministic &&
805+ _features_contain (get_features (m), ts_features),
806+ existing,
777807 )
778- end
808+ throw (
809+ ConflictingInputsError (
810+ " Cannot transform SingleTimeSeries to DeterministicSingleTimeSeries: " *
811+ " A Deterministic forecast already exists for component $(summary (component)) " *
812+ " with name='$ts_name ', resolution=$ts_resolution , and features=$ts_features " ,
813+ ),
814+ )
815+ end
779816
780- # If skip_existing is true, skip SingleTimeSeries entries that already have a
781- # DeterministicSingleTimeSeries with the same name, resolution, features,
782- # horizon, and interval.
783- if skip_existing
784- existing = list_metadata (
785- data. time_series_manager. metadata_store,
786- component;
787- time_series_type = DeterministicSingleTimeSeries,
788- name = ts_name,
789- resolution = ts_resolution,
790- ts_features_symbols... ,
791- )
792- if any (
817+ # If skip_existing is true, skip SingleTimeSeries entries that already have a
818+ # DeterministicSingleTimeSeries with the same name, resolution, features,
819+ # horizon, and interval.
820+ if skip_existing && any (
793821 m ->
794- get_horizon (m) == params. horizon &&
795- get_interval (m) == params. interval,
822+ get_time_series_type (m) === DeterministicSingleTimeSeries &&
823+ get_horizon (m) == params. horizon &&
824+ get_interval (m) == params. interval &&
825+ _features_contain (get_features (m), ts_features),
796826 existing,
797827 )
798828 continue
0 commit comments