Source code for ax.storage.sqa_store.sqa_classes

#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

from datetime import datetime
from typing import Any, Dict, List, Optional

from ax.core.base_trial import TrialStatus
from ax.core.parameter import ParameterType
from ax.core.types import (
    ComparisonOp,
    TModelPredict,
    TModelPredictArm,
    TParameterization,
    TParamValue,
)
from ax.storage.sqa_store.db import (
    LONG_STRING_FIELD_LENGTH,
    LONGTEXT_BYTES,
    NAME_OR_TYPE_FIELD_LENGTH,
    Base,
)
from ax.storage.sqa_store.json import (
    JSONEncodedDict,
    JSONEncodedList,
    JSONEncodedObject,
    JSONEncodedTextDict,
)
from ax.storage.sqa_store.sqa_enum import IntEnum, StringEnum
from ax.storage.sqa_store.timestamp import IntTimestamp
from ax.storage.utils import DomainType, MetricIntent, ParameterConstraintType
from sqlalchemy import (
    BigInteger,
    Boolean,
    Column,
    Float,
    ForeignKey,
    Integer,
    String,
    Text,
)
from sqlalchemy.orm import backref, relationship


ONLY_ONE_FIELDS = ["experiment_id", "generator_run_id"]


ONLY_ONE_METRIC_FIELDS = ["scalarized_objective_id"]


[docs]class SQAParameter(Base): __tablename__: str = "parameter_v2" # pyre-fixme[8]: Attribute has type `DomainType`; used as `Column[typing.Any]`. domain_type: DomainType = Column(IntEnum(DomainType), nullable=False) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. experiment_id: Optional[int] = Column(Integer, ForeignKey("experiment_v2.id")) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. id: int = Column(Integer, primary_key=True) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. generator_run_id: Optional[int] = Column(Integer, ForeignKey("generator_run_v2.id")) # pyre-fixme[8]: Attribute has type `str`; used as `Column[str]`. name: str = Column(String(NAME_OR_TYPE_FIELD_LENGTH), nullable=False) # pyre-fixme[8]: Attribute has type `ParameterType`; used as `Column[typing.Any]`. parameter_type: ParameterType = Column(IntEnum(ParameterType), nullable=False) # pyre-fixme[8]: Attribute has type `Optional[bool]`; used as `Column[bool]`. is_fidelity: Optional[bool] = Column(Boolean) # pyre-fixme[8]: Attribute has type `Union[None, bool, float, int, str]`; used # as `Column[typing.Any]`. target_value: Optional[TParamValue] = Column(JSONEncodedObject) # Attributes for Range Parameters # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. digits: Optional[int] = Column(Integer) # pyre-fixme[8]: Attribute has type `Optional[bool]`; used as `Column[bool]`. log_scale: Optional[bool] = Column(Boolean) # pyre-fixme[8]: Attribute has type `Optional[float]`; used as # `Column[decimal.Decimal]`. lower: Optional[float] = Column(Float) # pyre-fixme[8]: Attribute has type `Optional[float]`; used as # `Column[decimal.Decimal]`. upper: Optional[float] = Column(Float) # Attributes for Choice Parameters # pyre-fixme[8]: Attribute has type `Optional[List[typing.Union[None, bool, # float, int, str]]]`; used as `Column[typing.Any]`. choice_values: Optional[List[TParamValue]] = Column(JSONEncodedList) # pyre-fixme[8]: Attribute has type `Optional[bool]`; used as `Column[bool]`. is_ordered: Optional[bool] = Column(Boolean) # pyre-fixme[8]: Attribute has type `Optional[bool]`; used as `Column[bool]`. is_task: Optional[bool] = Column(Boolean) # Attributes for Fixed Parameters # pyre-fixme[8]: Attribute has type `Union[None, bool, float, int, str]`; used # as `Column[typing.Any]`. fixed_value: Optional[TParamValue] = Column(JSONEncodedObject) immutable_fields = ["name"] unique_id = "name"
[docs]class SQAParameterConstraint(Base): __tablename__: str = "parameter_constraint_v2" # pyre-fixme[8]: Attribute has type `float`; used as `Column[decimal.Decimal]`. bound: float = Column(Float, nullable=False) # pyre-fixme[8]: Attribute has type `Dict[str, float]`; used as # `Column[typing.Any]`. constraint_dict: Dict[str, float] = Column(JSONEncodedDict, nullable=False) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. experiment_id: Optional[int] = Column(Integer, ForeignKey("experiment_v2.id")) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. id: int = Column(Integer, primary_key=True) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. generator_run_id: Optional[int] = Column(Integer, ForeignKey("generator_run_v2.id")) # pyre-fixme[8]: Attribute has type `IntEnum`; used as `Column[typing.Any]`. type: IntEnum = Column(IntEnum(ParameterConstraintType), nullable=False) # ParameterConstraints should never be updated; since they don't have # a field that can be used for a UID, if anything changes, # we should just throw them out and recreate them immutable_fields = ["type", "constraint_dict", "bound"]
[docs]class SQAMetric(Base): __tablename__: str = "metric_v2" # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. experiment_id: Optional[int] = Column(Integer, ForeignKey("experiment_v2.id")) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. generator_run_id: Optional[int] = Column(Integer, ForeignKey("generator_run_v2.id")) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. id: int = Column(Integer, primary_key=True) # pyre-fixme[8]: Attribute has type `Optional[bool]`; used as `Column[bool]`. lower_is_better: Optional[bool] = Column(Boolean) # pyre-fixme[8]: Attribute has type `MetricIntent`; used as `Column[typing.Any]`. intent: MetricIntent = Column(StringEnum(MetricIntent), nullable=False) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. metric_type: int = Column(Integer, nullable=False) # pyre-fixme[8]: Attribute has type `str`; used as `Column[str]`. name: str = Column(String(LONG_STRING_FIELD_LENGTH), nullable=False) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Any]]`; used as # `Column[typing.Any]`. properties: Optional[Dict[str, Any]] = Column(JSONEncodedTextDict, default={}) # Attributes for Objectives # pyre-fixme[8]: Attribute has type `Optional[bool]`; used as `Column[bool]`. minimize: Optional[bool] = Column(Boolean) # Attributes for Outcome Constraints # pyre-fixme[8]: Attribute has type `Optional[ComparisonOp]`; used as # `Column[typing.Any]`. op: Optional[ComparisonOp] = Column(IntEnum(ComparisonOp)) # pyre-fixme[8]: Attribute has type `Optional[float]`; used as # `Column[decimal.Decimal]`. bound: Optional[float] = Column(Float) # pyre-fixme[8]: Attribute has type `Optional[bool]`; used as `Column[bool]`. relative: Optional[bool] = Column(Boolean) # Multi-type Experiment attributes # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. trial_type: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. canonical_name: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH)) immutable_fields = ["name"] unique_id = "name" scalarized_objective_id = Column(Integer, ForeignKey("metric_v2.id")) # Relationship containing SQAMetric(s) only defined for the parent metric # of Multi/Scalarized Objective contains all children of the parent metric # join_depth argument: used for loading self-referential relationships # https://docs.sqlalchemy.org/en/13/orm/self_referential.html#configuring-self-referential-eager-loading scalarized_objective_children_metrics = relationship( "SQAMetric", cascade="all, delete-orphan", lazy="selectin", join_depth=5 ) # Attribute only defined for the children of Scalarized Objective # pyre-fixme[8]: Attribute has type `Optional[float]`; used as # `Column[decimal.Decimal]`. scalarized_objective_weight: Optional[float] = Column(Float)
[docs]class SQAArm(Base): __tablename__: str = "arm_v2" # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. generator_run_id: int = Column(Integer, ForeignKey("generator_run_v2.id")) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. id: int = Column(Integer, primary_key=True) # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. name: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `Dict[str, typing.Union[None, bool, float, # int, str]]`; used as `Column[typing.Any]`. parameters: TParameterization = Column(JSONEncodedTextDict, nullable=False) # pyre-fixme[8]: Attribute has type `float`; used as `Column[decimal.Decimal]`. weight: float = Column(Float, nullable=False, default=1.0) immutable_fields = ["parameters"] unique_id = "name"
[docs]class SQAAbandonedArm(Base): __tablename__: str = "abandoned_arm_v2" # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. abandoned_reason: Optional[str] = Column(String(LONG_STRING_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. id: int = Column(Integer, primary_key=True) # pyre-fixme[8]: Attribute has type `str`; used as `Column[str]`. name: str = Column(String(NAME_OR_TYPE_FIELD_LENGTH), nullable=False) # pyre-fixme[8]: Attribute has type `datetime`; used as `Column[typing.Any]`. time_abandoned: datetime = Column( IntTimestamp, nullable=False, default=datetime.now ) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. trial_id: int = Column(Integer, ForeignKey("trial_v2.id")) immutable_fields = ["name"] unique_id = "name"
[docs]class SQAGeneratorRun(Base): __tablename__: str = "generator_run_v2" # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. best_arm_name: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Union[None, bool, # float, int, str]]]`; used as `Column[typing.Any]`. best_arm_parameters: Optional[TParameterization] = Column(JSONEncodedTextDict) # pyre-fixme[8]: Attribute has type `Optional[typing.Tuple[Dict[str, float], # Optional[Dict[str, Dict[str, float]]]]]`; used as `Column[typing.Any]`. best_arm_predictions: Optional[TModelPredictArm] = Column(JSONEncodedList) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. generator_run_type: Optional[int] = Column(Integer) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. id: int = Column(Integer, primary_key=True) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. index: Optional[int] = Column(Integer) # pyre-fixme[8]: Attribute has type `Optional[typing.Tuple[Dict[str, # List[float]], Dict[str, Dict[str, List[float]]]]]`; used as # `Column[typing.Any]`. model_predictions: Optional[TModelPredict] = Column(JSONEncodedList) # pyre-fixme[8]: Attribute has type `datetime`; used as `Column[typing.Any]`. time_created: datetime = Column(IntTimestamp, nullable=False, default=datetime.now) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. trial_id: Optional[int] = Column(Integer, ForeignKey("trial_v2.id")) # pyre-fixme[8]: Attribute has type `Optional[float]`; used as # `Column[decimal.Decimal]`. weight: Optional[float] = Column(Float) # pyre-fixme[8]: Attribute has type `Optional[float]`; used as # `Column[decimal.Decimal]`. fit_time: Optional[float] = Column(Float) # pyre-fixme[8]: Attribute has type `Optional[float]`; used as # `Column[decimal.Decimal]`. gen_time: Optional[float] = Column(Float) # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. model_key: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Any]]`; used as # `Column[typing.Any]`. model_kwargs: Optional[Dict[str, Any]] = Column(JSONEncodedTextDict) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Any]]`; used as # `Column[typing.Any]`. bridge_kwargs: Optional[Dict[str, Any]] = Column(JSONEncodedTextDict) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Any]]`; used as # `Column[typing.Any]`. gen_metadata: Optional[Dict[str, Any]] = Column(JSONEncodedTextDict) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Any]]`; used as # `Column[typing.Any]`. model_state_after_gen: Optional[Dict[str, Any]] = Column(JSONEncodedTextDict) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. generation_strategy_id: Optional[int] = Column( Integer, ForeignKey("generation_strategy.id") ) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. generation_step_index: Optional[int] = Column(Integer) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Any]]`; used as # `Column[typing.Any]`. candidate_metadata_by_arm_signature: Optional[Dict[str, Any]] = Column( JSONEncodedTextDict ) # relationships # Use selectin loading for collections to prevent idle timeout errors # (https://docs.sqlalchemy.org/en/13/orm/loading_relationships.html#selectin-eager-loading) arms: List[SQAArm] = relationship( "SQAArm", cascade="all, delete-orphan", lazy="selectin", order_by=lambda: SQAArm.id, ) metrics: List[SQAMetric] = relationship( "SQAMetric", cascade="all, delete-orphan", lazy="selectin" ) parameters: List[SQAParameter] = relationship( "SQAParameter", cascade="all, delete-orphan", lazy="selectin" ) parameter_constraints: List[SQAParameterConstraint] = relationship( "SQAParameterConstraint", cascade="all, delete-orphan", lazy="selectin" ) ignore_during_update_fields = ["time_created"] unique_id = "index"
[docs]class SQARunner(Base): __tablename__: str = "runner" # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. id: int = Column(Integer, primary_key=True) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. experiment_id: Optional[int] = Column(Integer, ForeignKey("experiment_v2.id")) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Any]]`; used as # `Column[typing.Any]`. properties: Optional[Dict[str, Any]] = Column(JSONEncodedTextDict, default={}) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. runner_type: int = Column(Integer, nullable=False) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. trial_id: Optional[int] = Column(Integer, ForeignKey("trial_v2.id")) # Multi-type Experiment attributes # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. trial_type: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH))
[docs]class SQAData(Base): __tablename__: str = "data_v2" # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. id: int = Column(Integer, primary_key=True) # pyre-fixme[8]: Attribute has type `str`; used as `Column[str]`. data_json: str = Column(Text(LONGTEXT_BYTES), nullable=False) # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. description: Optional[str] = Column(String(LONG_STRING_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. experiment_id: int = Column(Integer, ForeignKey("experiment_v2.id")) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. time_created: int = Column(BigInteger, nullable=False) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. trial_index: Optional[int] = Column(Integer) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. generation_strategy_id: Optional[int] = Column( Integer, ForeignKey("generation_strategy.id") ) unique_id = "time_created"
[docs]class SQAGenerationStrategy(Base): __tablename__: str = "generation_strategy" # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. id: int = Column(Integer, primary_key=True) # pyre-fixme[8]: Attribute has type `str`; used as `Column[str]`. name: str = Column(String(NAME_OR_TYPE_FIELD_LENGTH), nullable=False) # pyre-fixme[8]: Attribute has type `List[Dict[str, typing.Any]]`; used as # `Column[typing.Any]`. steps: List[Dict[str, Any]] = Column(JSONEncodedList, nullable=False) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. curr_index: int = Column(Integer, nullable=False) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. experiment_id: Optional[int] = Column(Integer, ForeignKey("experiment_v2.id")) generator_runs: List[SQAGeneratorRun] = relationship( "SQAGeneratorRun", cascade="all, delete-orphan", lazy="selectin", order_by=lambda: SQAGeneratorRun.id, )
[docs]class SQATrial(Base): __tablename__: str = "trial_v2" # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. abandoned_reason: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. deployed_name: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. experiment_id: int = Column(Integer, ForeignKey("experiment_v2.id")) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. id: int = Column(Integer, primary_key=True) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. index: int = Column(Integer, index=True, nullable=False) # pyre-fixme[8]: Attribute has type `bool`; used as `Column[bool]`. is_batch: bool = Column("is_batched", Boolean, nullable=False, default=True) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. num_arms_created: int = Column(Integer, nullable=False, default=0) # pyre-fixme[8]: Attribute has type `Optional[bool]`; used as `Column[bool]`. optimize_for_power: Optional[bool] = Column(Boolean) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. ttl_seconds: Optional[int] = Column(Integer) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Any]]`; used as # `Column[typing.Any]`. run_metadata: Optional[Dict[str, Any]] = Column(JSONEncodedTextDict) # pyre-fixme[8]: Attribute has type `TrialStatus`; used as `Column[typing.Any]`. status: TrialStatus = Column( IntEnum(TrialStatus), nullable=False, default=TrialStatus.CANDIDATE ) # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. status_quo_name: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `Optional[datetime]`; used as # `Column[typing.Any]`. time_completed: Optional[datetime] = Column(IntTimestamp) # pyre-fixme[8]: Attribute has type `datetime`; used as `Column[typing.Any]`. time_created: datetime = Column(IntTimestamp, nullable=False) # pyre-fixme[8]: Attribute has type `Optional[datetime]`; used as # `Column[typing.Any]`. time_staged: Optional[datetime] = Column(IntTimestamp) # pyre-fixme[8]: Attribute has type `Optional[datetime]`; used as # `Column[typing.Any]`. time_run_started: Optional[datetime] = Column(IntTimestamp) # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. trial_type: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. generation_step_index: Optional[int] = Column(Integer) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Any]]`; used as # `Column[typing.Any]`. properties: Optional[Dict[str, Any]] = Column(JSONEncodedTextDict, default={}) # relationships # Trials and experiments are mutable, so the children relationships need # cascade="all, delete-orphan", which means if we remove or replace # a child, the old one will be deleted. # Use selectin loading for collections to prevent idle timeout errors # (https://docs.sqlalchemy.org/en/13/orm/loading_relationships.html#selectin-eager-loading) abandoned_arms: List[SQAAbandonedArm] = relationship( "SQAAbandonedArm", cascade="all, delete-orphan", lazy="selectin" ) generator_runs: List[SQAGeneratorRun] = relationship( "SQAGeneratorRun", cascade="all, delete-orphan", lazy="selectin" ) runner: SQARunner = relationship( "SQARunner", uselist=False, cascade="all, delete-orphan", lazy=False ) unique_id = "index" ignore_during_update_fields = ["time_created"] immutable_fields = ["is_batch"]
[docs]class SQAExperiment(Base): __tablename__: str = "experiment_v2" # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. description: Optional[str] = Column(String(LONG_STRING_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `Optional[int]`; used as `Column[int]`. experiment_type: Optional[int] = Column(Integer) # pyre-fixme[8]: Attribute has type `int`; used as `Column[int]`. id: int = Column(Integer, primary_key=True) # pyre-fixme[8]: Attribute has type `bool`; used as `Column[bool]`. is_test: bool = Column(Boolean, nullable=False, default=False) # pyre-fixme[8]: Attribute has type `str`; used as `Column[str]`. name: str = Column(String(NAME_OR_TYPE_FIELD_LENGTH), nullable=False) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Any]]`; used as # `Column[typing.Any]`. properties: Optional[Dict[str, Any]] = Column(JSONEncodedTextDict, default={}) # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. status_quo_name: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH)) # pyre-fixme[8]: Attribute has type `Optional[Dict[str, typing.Union[None, bool, # float, int, str]]]`; used as `Column[typing.Any]`. status_quo_parameters: Optional[TParameterization] = Column(JSONEncodedTextDict) # pyre-fixme[8]: Attribute has type `datetime`; used as `Column[typing.Any]`. time_created: datetime = Column(IntTimestamp, nullable=False) # pyre-fixme[8]: Attribute has type `Optional[str]`; used as `Column[str]`. default_trial_type: Optional[str] = Column(String(NAME_OR_TYPE_FIELD_LENGTH)) # relationships # Trials and experiments are mutable, so the children relationships need # cascade="all, delete-orphan", which means if we remove or replace # a child, the old one will be deleted. # Use selectin loading for collections to prevent idle timeout errors # (https://docs.sqlalchemy.org/en/13/orm/loading_relationships.html#selectin-eager-loading) data: List[SQAData] = relationship( "SQAData", cascade="all, delete-orphan", lazy="selectin" ) metrics: List[SQAMetric] = relationship( "SQAMetric", cascade="all, delete-orphan", lazy="selectin" ) parameters: List[SQAParameter] = relationship( "SQAParameter", cascade="all, delete-orphan", lazy="selectin" ) parameter_constraints: List[SQAParameterConstraint] = relationship( "SQAParameterConstraint", cascade="all, delete-orphan", lazy="selectin" ) runners: List[SQARunner] = relationship( "SQARunner", cascade="all, delete-orphan", lazy=False ) trials: List[SQATrial] = relationship( "SQATrial", cascade="all, delete-orphan", lazy="selectin" ) generation_strategy: Optional[SQAGenerationStrategy] = relationship( "SQAGenerationStrategy", backref=backref("experiment", lazy=True), uselist=False, lazy=True, ) immutable_fields = ["name"] ignore_during_update_fields = ["time_created"]