Files
2026-07-13 13:22:34 +08:00

3778 lines
126 KiB
Python

import json
import uuid
from typing import Any
import sqlalchemy as sa
from sqlalchemy import (
JSON,
BigInteger,
Boolean,
CheckConstraint,
Column,
Computed,
Float,
ForeignKey,
ForeignKeyConstraint,
Index,
Integer,
LargeBinary,
PrimaryKeyConstraint,
String,
Text,
UnicodeText,
UniqueConstraint,
)
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.inspection import inspect
from sqlalchemy.orm import backref, relationship, validates
from mlflow.entities import (
Assessment,
AssessmentError,
AssessmentSource,
Dataset,
DatasetRecord,
DatasetRecordSource,
EvaluationDataset,
Expectation,
Experiment,
ExperimentTag,
FallbackConfig,
FallbackStrategy,
Feedback,
GatewayEndpoint,
GatewayEndpointBinding,
GatewayEndpointModelMapping,
GatewayEndpointTag,
GatewayModelDefinition,
GatewayResourceType,
GatewaySecretInfo,
InputTag,
Issue,
IssueReference,
IssueSeverity,
IssueStatus,
Metric,
Param,
RoutingStrategy,
Run,
RunData,
RunInfo,
RunStatus,
RunTag,
SourceType,
TraceInfo,
ViewType,
)
from mlflow.entities.dataset_record import DATASET_RECORD_WRAPPED_OUTPUT_KEY
from mlflow.entities.gateway_budget_policy import (
BudgetAction,
BudgetDuration,
BudgetDurationUnit,
BudgetTargetScope,
BudgetUnit,
GatewayBudgetPolicy,
)
from mlflow.entities.gateway_guardrail import (
GatewayGuardrail,
GatewayGuardrailConfig,
GuardrailAction,
GuardrailStage,
)
from mlflow.entities.lifecycle_stage import LifecycleStage
from mlflow.entities.logged_model import LoggedModel
from mlflow.entities.logged_model_parameter import LoggedModelParameter
from mlflow.entities.logged_model_status import LoggedModelStatus
from mlflow.entities.logged_model_tag import LoggedModelTag
from mlflow.entities.trace_location import TraceLocation
from mlflow.entities.trace_state import TraceState
from mlflow.exceptions import MlflowException
from mlflow.genai.scorers.online.entities import OnlineScoringConfig
from mlflow.store.db.base_sql_model import Base
from mlflow.tracing.utils import generate_assessment_id
from mlflow.utils.mlflow_tags import MLFLOW_USER, _get_run_name_from_tags
from mlflow.utils.time import get_current_time_millis
from mlflow.utils.workspace_utils import DEFAULT_WORKSPACE_NAME
SourceTypes = [
SourceType.to_string(SourceType.NOTEBOOK),
SourceType.to_string(SourceType.JOB),
SourceType.to_string(SourceType.LOCAL),
SourceType.to_string(SourceType.UNKNOWN),
SourceType.to_string(SourceType.PROJECT),
]
RunStatusTypes = [
RunStatus.to_string(RunStatus.SCHEDULED),
RunStatus.to_string(RunStatus.FAILED),
RunStatus.to_string(RunStatus.FINISHED),
RunStatus.to_string(RunStatus.RUNNING),
RunStatus.to_string(RunStatus.KILLED),
]
# Create MutableJSON type for tracking mutations in JSON columns
MutableJSON = MutableDict.as_mutable(JSON)
class SqlExperiment(Base):
"""
DB model for :py:class:`mlflow.entities.Experiment`. These are recorded in ``experiment`` table.
"""
__tablename__ = "experiments"
experiment_id = Column(Integer, autoincrement=True)
"""
Experiment ID: `Integer`. *Primary Key* for ``experiment`` table.
"""
name = Column(String(256), nullable=False)
"""
Experiment name: `String` (limit 256 characters). Unique *within a workspace* (enforced by
the ``workspace`` + ``name`` constraint) and *Non null* in the table schema.
"""
workspace = Column(
String(63),
nullable=False,
default=DEFAULT_WORKSPACE_NAME,
server_default=sa.text(f"'{DEFAULT_WORKSPACE_NAME}'"),
)
"""
Workspace identifier for this experiment: `String` (limit 63 characters). Defaults to
``'default'`` when not explicitly provided.
"""
artifact_location = Column(String(256), nullable=True)
"""
Default artifact location for this experiment: `String` (limit 256 characters). Defined as
*Non null* in table schema.
"""
lifecycle_stage = Column(String(32), default=LifecycleStage.ACTIVE)
"""
Lifecycle Stage of experiment: `String` (limit 32 characters).
Can be either ``active`` (default) or ``deleted``.
"""
creation_time = Column(BigInteger(), default=get_current_time_millis)
"""
Creation time of experiment: `BigInteger`.
"""
last_update_time = Column(BigInteger(), default=get_current_time_millis)
"""
Last Update time of experiment: `BigInteger`.
"""
__table_args__ = (
CheckConstraint(
lifecycle_stage.in_(LifecycleStage.view_type_to_stages(ViewType.ALL)),
name="experiments_lifecycle_stage",
),
PrimaryKeyConstraint("experiment_id", name="experiment_pk"),
UniqueConstraint("workspace", "name", name="uq_experiments_workspace_name"),
)
def __repr__(self):
return f"<SqlExperiment ({self.experiment_id}, {self.name})>"
def to_mlflow_entity(self, effective_trace_archival_retention: str | None = None):
"""
Convert DB model to corresponding MLflow entity.
Returns:
:py:class:`mlflow.entities.Experiment`.
"""
return Experiment(
experiment_id=str(self.experiment_id),
name=self.name,
artifact_location=self.artifact_location,
lifecycle_stage=self.lifecycle_stage,
tags=[t.to_mlflow_entity() for t in self.tags],
creation_time=self.creation_time,
last_update_time=self.last_update_time,
workspace=self.workspace,
effective_trace_archival_retention=effective_trace_archival_retention,
)
class SqlRun(Base):
"""
DB model for :py:class:`mlflow.entities.Run`. These are recorded in ``runs`` table.
"""
__tablename__ = "runs"
run_uuid = Column(String(32), nullable=False)
"""
Run UUID: `String` (limit 32 characters). *Primary Key* for ``runs`` table.
"""
name = Column(String(250))
"""
Run name: `String` (limit 250 characters).
"""
source_type = Column(String(20), default=SourceType.to_string(SourceType.LOCAL))
"""
Source Type: `String` (limit 20 characters). Can be one of ``NOTEBOOK``, ``JOB``, ``PROJECT``,
``LOCAL`` (default), or ``UNKNOWN``.
"""
source_name = Column(String(500))
"""
Name of source recording the run: `String` (limit 500 characters).
"""
entry_point_name = Column(String(50))
"""
Entry-point name that launched the run run: `String` (limit 50 characters).
"""
user_id = Column(String(256), nullable=True, default=None)
"""
User ID: `String` (limit 256 characters). Defaults to ``null``.
"""
status = Column(String(20), default=RunStatus.to_string(RunStatus.SCHEDULED))
"""
Run Status: `String` (limit 20 characters). Can be one of ``RUNNING``, ``SCHEDULED`` (default),
``FINISHED``, ``FAILED``.
"""
start_time = Column(BigInteger, default=get_current_time_millis)
"""
Run start time: `BigInteger`. Defaults to current system time.
"""
end_time = Column(BigInteger, nullable=True, default=None)
"""
Run end time: `BigInteger`.
"""
deleted_time = Column(BigInteger, nullable=True, default=None)
"""
Run deleted time: `BigInteger`. Timestamp of when run is deleted, defaults to none.
"""
source_version = Column(String(50))
"""
Source version: `String` (limit 50 characters).
"""
lifecycle_stage = Column(String(20), default=LifecycleStage.ACTIVE)
"""
Lifecycle Stage of run: `String` (limit 32 characters).
Can be either ``active`` (default) or ``deleted``.
"""
artifact_uri = Column(String(200), default=None)
"""
Default artifact location for this run: `String` (limit 200 characters).
"""
experiment_id = Column(Integer, ForeignKey("experiments.experiment_id"))
"""
Experiment ID to which this run belongs to: *Foreign Key* into ``experiment`` table.
"""
experiment = relationship("SqlExperiment", backref=backref("runs", cascade="all"))
"""
SQLAlchemy relationship (many:one) with :py:class:`mlflow.store.dbmodels.models.SqlExperiment`.
"""
__table_args__ = (
CheckConstraint(source_type.in_(SourceTypes), name="source_type"),
CheckConstraint(status.in_(RunStatusTypes), name="status"),
CheckConstraint(
lifecycle_stage.in_(LifecycleStage.view_type_to_stages(ViewType.ALL)),
name="runs_lifecycle_stage",
),
PrimaryKeyConstraint("run_uuid", name="run_pk"),
)
@staticmethod
def get_attribute_name(mlflow_attribute_name):
"""
Resolves an MLflow attribute name to a `SqlRun` attribute name.
"""
# Currently, MLflow Search attributes defined in `SearchUtils.VALID_SEARCH_ATTRIBUTE_KEYS`
# share the same names as their corresponding `SqlRun` attributes. Therefore, this function
# returns the same attribute name
return {"run_name": "name", "run_id": "run_uuid"}.get(
mlflow_attribute_name, mlflow_attribute_name
)
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
mlflow.entities.Run: Description of the return value.
"""
run_info = RunInfo(
run_id=self.run_uuid,
run_name=self.name,
experiment_id=str(self.experiment_id),
user_id=self.user_id,
status=self.status,
start_time=self.start_time,
end_time=self.end_time,
lifecycle_stage=self.lifecycle_stage,
artifact_uri=self.artifact_uri,
)
tags = [t.to_mlflow_entity() for t in self.tags]
run_data = RunData(
metrics=[m.to_mlflow_entity() for m in self.latest_metrics],
params=[p.to_mlflow_entity() for p in self.params],
tags=tags,
)
if not run_info.run_name:
if run_name := _get_run_name_from_tags(tags):
run_info._set_run_name(run_name)
return Run(run_info=run_info, run_data=run_data)
class SqlExperimentTag(Base):
"""
DB model for :py:class:`mlflow.entities.RunTag`.
These are recorded in ``experiment_tags`` table.
"""
__tablename__ = "experiment_tags"
key = Column(String(250))
"""
Tag key: `String` (limit 250 characters). *Primary Key* for ``tags`` table.
"""
value = Column(String(5000), nullable=True)
"""
Value associated with tag: `String` (limit 5000 characters). Could be *null*.
"""
experiment_id = Column(Integer, ForeignKey("experiments.experiment_id"))
"""
Experiment ID to which this tag belongs: *Foreign Key* into ``experiments`` table.
"""
experiment = relationship("SqlExperiment", backref=backref("tags", cascade="all"))
"""
SQLAlchemy relationship (many:one) with :py:class:`mlflow.store.dbmodels.models.SqlExperiment`.
"""
__table_args__ = (PrimaryKeyConstraint("key", "experiment_id", name="experiment_tag_pk"),)
def __repr__(self):
return f"<SqlExperimentTag({self.key}, {self.value})>"
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
mlflow.entities.RunTag: Description of the return value.
"""
return ExperimentTag(key=self.key, value=self.value)
class SqlTag(Base):
"""
DB model for :py:class:`mlflow.entities.RunTag`. These are recorded in ``tags`` table.
"""
__tablename__ = "tags"
__table_args__ = (
PrimaryKeyConstraint("key", "run_uuid", name="tag_pk"),
Index(f"index_{__tablename__}_run_uuid", "run_uuid"),
)
key = Column(String(250))
"""
Tag key: `String` (limit 250 characters). *Primary Key* for ``tags`` table.
"""
value = Column(String(8000), nullable=True)
"""
Value associated with tag: `String` (limit 8000 characters). Could be *null*.
"""
run_uuid = Column(String(32), ForeignKey("runs.run_uuid"))
"""
Run UUID to which this tag belongs to: *Foreign Key* into ``runs`` table.
"""
run = relationship("SqlRun", backref=backref("tags", cascade="all"))
"""
SQLAlchemy relationship (many:one) with :py:class:`mlflow.store.dbmodels.models.SqlRun`.
"""
def __repr__(self):
return f"<SqlRunTag({self.key}, {self.value})>"
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
:py:class:`mlflow.entities.RunTag`.
"""
return RunTag(key=self.key, value=self.value)
class SqlMetric(Base):
__tablename__ = "metrics"
__table_args__ = (
PrimaryKeyConstraint(
"key", "timestamp", "step", "run_uuid", "value", "is_nan", name="metric_pk"
),
Index(f"index_{__tablename__}_run_uuid", "run_uuid"),
Index(f"index_{__tablename__}_run_uuid_key_step", "run_uuid", "key", "step"),
)
key = Column(String(250))
"""
Metric key: `String` (limit 250 characters). Part of *Primary Key* for ``metrics`` table.
"""
value = Column(sa.types.Float(precision=53), nullable=False)
"""
Metric value: `Float`. Defined as *Non-null* in schema.
"""
timestamp = Column(BigInteger, default=get_current_time_millis)
"""
Timestamp recorded for this metric entry: `BigInteger`. Part of *Primary Key* for
``metrics`` table.
"""
step = Column(BigInteger, default=0, nullable=False)
"""
Step recorded for this metric entry: `BigInteger`.
"""
is_nan = Column(Boolean(create_constraint=True), nullable=False, default=False)
"""
True if the value is in fact NaN.
"""
run_uuid = Column(String(32), ForeignKey("runs.run_uuid"))
"""
Run UUID to which this metric belongs to: Part of *Primary Key* for ``metrics`` table.
*Foreign Key* into ``runs`` table.
"""
run = relationship("SqlRun", backref=backref("metrics", cascade="all"))
"""
SQLAlchemy relationship (many:one) with :py:class:`mlflow.store.dbmodels.models.SqlRun`.
"""
def __repr__(self):
return f"<SqlMetric({self.key}, {self.value}, {self.timestamp}, {self.step})>"
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
mlflow.entities.Metric: Description of the return value.
"""
return Metric(
key=self.key,
value=self.value if not self.is_nan else float("nan"),
timestamp=self.timestamp,
step=self.step,
)
class SqlLatestMetric(Base):
__tablename__ = "latest_metrics"
__table_args__ = (
PrimaryKeyConstraint("key", "run_uuid", name="latest_metric_pk"),
Index(f"index_{__tablename__}_run_uuid", "run_uuid"),
)
key = Column(String(250))
"""
Metric key: `String` (limit 250 characters). Part of *Primary Key* for ``latest_metrics`` table.
"""
value = Column(sa.types.Float(precision=53), nullable=False)
"""
Metric value: `Float`. Defined as *Non-null* in schema.
"""
timestamp = Column(BigInteger, default=get_current_time_millis)
"""
Timestamp recorded for this metric entry: `BigInteger`. Part of *Primary Key* for
``latest_metrics`` table.
"""
step = Column(BigInteger, default=0, nullable=False)
"""
Step recorded for this metric entry: `BigInteger`.
"""
is_nan = Column(Boolean(create_constraint=True), nullable=False, default=False)
"""
True if the value is in fact NaN.
"""
run_uuid = Column(String(32), ForeignKey("runs.run_uuid"))
"""
Run UUID to which this metric belongs to: Part of *Primary Key* for ``latest_metrics`` table.
*Foreign Key* into ``runs`` table.
"""
run = relationship("SqlRun", backref=backref("latest_metrics", cascade="all"))
"""
SQLAlchemy relationship (many:one) with :py:class:`mlflow.store.dbmodels.models.SqlRun`.
"""
def __repr__(self):
return f"<SqlLatestMetric({self.key}, {self.value}, {self.timestamp}, {self.step})>"
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
mlflow.entities.Metric: Description of the return value.
"""
return Metric(
key=self.key,
value=self.value if not self.is_nan else float("nan"),
timestamp=self.timestamp,
step=self.step,
)
class SqlParam(Base):
__tablename__ = "params"
__table_args__ = (
PrimaryKeyConstraint("key", "run_uuid", name="param_pk"),
Index(f"index_{__tablename__}_run_uuid", "run_uuid"),
)
key = Column(String(250))
"""
Param key: `String` (limit 250 characters). Part of *Primary Key* for ``params`` table.
"""
value = Column(String(8000), nullable=False)
"""
Param value: `String` (limit 8000 characters). Defined as *Non-null* in schema.
"""
run_uuid = Column(String(32), ForeignKey("runs.run_uuid"))
"""
Run UUID to which this metric belongs to: Part of *Primary Key* for ``params`` table.
*Foreign Key* into ``runs`` table.
"""
run = relationship("SqlRun", backref=backref("params", cascade="all"))
"""
SQLAlchemy relationship (many:one) with :py:class:`mlflow.store.dbmodels.models.SqlRun`.
"""
def __repr__(self):
return f"<SqlParam({self.key}, {self.value})>"
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
mlflow.entities.Param: Description of the return value.
"""
return Param(key=self.key, value=self.value)
class SqlDataset(Base):
__tablename__ = "datasets"
__table_args__ = (
PrimaryKeyConstraint("experiment_id", "name", "digest", name="dataset_pk"),
Index(f"index_{__tablename__}_dataset_uuid", "dataset_uuid"),
Index(
f"index_{__tablename__}_experiment_id_dataset_source_type",
"experiment_id",
"dataset_source_type",
),
)
dataset_uuid = Column(String(36), nullable=False)
"""
Dataset UUID: `String` (limit 36 characters). Defined as *Non-null* in schema.
Part of *Primary Key* for ``datasets`` table.
"""
experiment_id = Column(Integer, ForeignKey("experiments.experiment_id", ondelete="CASCADE"))
"""
Experiment ID to which this dataset belongs: *Foreign Key* into ``experiments`` table.
"""
name = Column(String(500), nullable=False)
"""
Param name: `String` (limit 500 characters). Defined as *Non-null* in schema.
Part of *Primary Key* for ``datasets`` table.
"""
digest = Column(String(36), nullable=False)
"""
Param digest: `String` (limit 500 characters). Defined as *Non-null* in schema.
Part of *Primary Key* for ``datasets`` table.
"""
dataset_source_type = Column(String(36), nullable=False)
"""
Param dataset_source_type: `String` (limit 36 characters). Defined as *Non-null* in schema.
"""
dataset_source = Column(UnicodeText, nullable=False)
"""
Param dataset_source: `UnicodeText`. Defined as *Non-null* in schema.
"""
dataset_schema = Column(UnicodeText, nullable=True)
"""
Param dataset_schema: `UnicodeText`.
"""
dataset_profile = Column(UnicodeText, nullable=True)
"""
Param dataset_profile: `UnicodeText`.
"""
def __repr__(self):
return "<SqlDataset ({}, {}, {}, {}, {}, {}, {}, {})>".format(
self.dataset_uuid,
self.experiment_id,
self.name,
self.digest,
self.dataset_source_type,
self.dataset_source,
self.dataset_schema,
self.dataset_profile,
)
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
mlflow.entities.Dataset.
"""
return Dataset(
name=self.name,
digest=self.digest,
source_type=self.dataset_source_type,
source=self.dataset_source,
schema=self.dataset_schema,
profile=self.dataset_profile,
)
class SqlInput(Base):
__tablename__ = "inputs"
__table_args__ = (
PrimaryKeyConstraint(
"source_type", "source_id", "destination_type", "destination_id", name="inputs_pk"
),
Index(f"index_{__tablename__}_input_uuid", "input_uuid"),
Index(
f"index_{__tablename__}_destination_type_destination_id_source_type",
"destination_type",
"destination_id",
"source_type",
),
)
input_uuid = Column(String(36), nullable=False)
"""
Input UUID: `String` (limit 36 characters). Defined as *Non-null* in schema.
"""
source_type = Column(String(36), nullable=False)
"""
Source type: `String` (limit 36 characters). Defined as *Non-null* in schema.
Part of *Primary Key* for ``inputs`` table.
"""
source_id = Column(String(36), nullable=False)
"""
Source Id: `String` (limit 36 characters). Defined as *Non-null* in schema.
Part of *Primary Key* for ``inputs`` table.
"""
destination_type = Column(String(36), nullable=False)
"""
Destination type: `String` (limit 36 characters). Defined as *Non-null* in schema.
Part of *Primary Key* for ``inputs`` table.
"""
destination_id = Column(String(36), nullable=False)
"""
Destination Id: `String` (limit 36 characters). Defined as *Non-null* in schema.
Part of *Primary Key* for ``inputs`` table.
"""
step = Column(BigInteger, nullable=False, server_default="0")
def __repr__(self):
return "<SqlInput ({}, {}, {}, {}, {})>".format(
self.input_uuid,
self.source_type,
self.source_id,
self.destination_type,
self.destination_id,
)
class SqlInputTag(Base):
__tablename__ = "input_tags"
__table_args__ = (PrimaryKeyConstraint("input_uuid", "name", name="input_tags_pk"),)
input_uuid = Column(String(36), ForeignKey("inputs.input_uuid"), nullable=False)
"""
Input UUID: `String` (limit 36 characters). Defined as *Non-null* in schema.
*Foreign Key* into ``inputs`` table. Part of *Primary Key* for ``input_tags`` table.
"""
name = Column(String(255), nullable=False)
"""
Param name: `String` (limit 255 characters). Defined as *Non-null* in schema.
Part of *Primary Key* for ``input_tags`` table.
"""
value = Column(String(500), nullable=False)
"""
Param value: `String` (limit 500 characters). Defined as *Non-null* in schema.
Part of *Primary Key* for ``input_tags`` table.
"""
def __repr__(self):
return f"<SqlInputTag ({self.input_uuid}, {self.name}, {self.value})>"
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
mlflow.entities.InputTag: Description of the return value.
"""
return InputTag(key=self.name, value=self.value)
#######################################################################################
# Below are Tracing models. We may refactor them to be in a separate module in the future.
#######################################################################################
class SqlTraceInfo(Base):
__tablename__ = "trace_info"
request_id = Column(String(50), nullable=False)
"""
Trace ID: `String` (limit 50 characters). *Primary Key* for ``trace_info`` table.
Named as "trace_id" in V3 format.
"""
experiment_id = Column(Integer, ForeignKey("experiments.experiment_id"), nullable=False)
"""
Experiment ID to which this trace belongs: *Foreign Key* into ``experiments`` table.
"""
experiment = relationship(
"SqlExperiment",
backref=backref("trace_infos", cascade="all, delete-orphan"),
)
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.dbmodels.models.SqlExperiment`. The ``delete-orphan``
cascade ensures that ``session.delete(experiment)`` (used by
``_hard_delete_experiment`` and ``mlflow gc``) emits ``DELETE`` statements
for all trace_info rows before deleting the parent experiment row.
"""
timestamp_ms = Column(BigInteger, nullable=False)
"""
Start time of the trace, in milliseconds. Named as "request_time" in V3 format.
"""
execution_time_ms = Column(BigInteger, nullable=True)
"""
Duration of the trace, in milliseconds. Could be *null* if the trace is still in progress
or not ended correctly for some reason. Named as "execution_duration" in V3 format.
"""
status = Column(String(50), nullable=False)
"""
State of the trace. The values are defined in
:py:class:`mlflow.entities.trace_status.TraceStatus` enum but we don't enforce
constraint at DB level. Named as "state" in V3 format.
"""
client_request_id = Column(String(50), nullable=True)
"""
Client request ID: `String` (limit 50 characters). Could be *null*. Newly added in V3 format.
"""
request_preview = Column(String(1000), nullable=True)
"""
Request preview: `String` (limit 1000 characters). Could be *null*. Newly added in V3 format.
"""
response_preview = Column(String(1000), nullable=True)
"""
Response preview: `String` (limit 1000 characters). Could be *null*. Newly added in V3 format.
"""
db_payload_generation = Column(Integer, nullable=False, server_default="0")
"""
DB-backed trace payload generation used for concurrency coordination.
Defaults to 0.
"""
__table_args__ = (
PrimaryKeyConstraint("request_id", name="trace_info_pk"),
# The most frequent query will be get all traces in an experiment sorted by timestamp desc,
# which is the default view in the UI. Also every search query should have experiment_id(s)
# in the where clause.
Index(f"index_{__tablename__}_experiment_id_timestamp_ms", "experiment_id", "timestamp_ms"),
)
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
:py:class:`mlflow.entities.TraceInfo` object.
"""
return TraceInfo(
trace_id=self.request_id,
trace_location=TraceLocation.from_experiment_id(str(self.experiment_id)),
request_time=self.timestamp_ms,
execution_duration=self.execution_time_ms,
state=TraceState(self.status),
tags={t.key: t.value for t in self.tags},
trace_metadata={m.key: m.value for m in self.request_metadata},
client_request_id=self.client_request_id,
request_preview=self.request_preview,
response_preview=self.response_preview,
assessments=[a.to_mlflow_entity() for a in self.assessments],
)
class SqlTraceTag(Base):
__tablename__ = "trace_tags"
key = Column(String(250))
"""
Tag key: `String` (limit 250 characters).
"""
value = Column(String(8000), nullable=True)
"""
Value associated with tag: `String` (limit 250 characters). Could be *null*.
"""
request_id = Column(
String(50), ForeignKey("trace_info.request_id", ondelete="CASCADE"), nullable=False
)
"""
Request ID to which this tag belongs: *Foreign Key* into ``trace_info`` table.
"""
trace_info = relationship("SqlTraceInfo", backref=backref("tags", cascade="all"))
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.dbmodels.models.SqlTraceInfo`.
"""
# Key is unique within a request_id
__table_args__ = (
PrimaryKeyConstraint("request_id", "key", name="trace_tag_pk"),
Index(f"index_{__tablename__}_request_id"),
)
class SqlTraceMetadata(Base):
__tablename__ = "trace_request_metadata"
key = Column(String(250))
"""
Metadata key: `String` (limit 250 characters).
"""
value = Column(String(8000), nullable=True)
"""
Value associated with metadata: `String` (limit 250 characters). Could be *null*.
"""
request_id = Column(
String(50), ForeignKey("trace_info.request_id", ondelete="CASCADE"), nullable=False
)
"""
Request ID to which this metadata belongs: *Foreign Key* into ``trace_info`` table.
**Corresponding to the "trace_id" in V3 format.**
"""
trace_info = relationship("SqlTraceInfo", backref=backref("request_metadata", cascade="all"))
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.dbmodels.models.SqlTraceInfo`.
"""
# Key is unique within a request_id
__table_args__ = (
PrimaryKeyConstraint("request_id", "key", name="trace_request_metadata_pk"),
Index(f"index_{__tablename__}_request_id"),
)
class SqlTraceMetrics(Base):
__tablename__ = "trace_metrics"
request_id = Column(
String(50), ForeignKey("trace_info.request_id", ondelete="CASCADE"), nullable=False
)
"""
Request ID to which this metric belongs: *Foreign Key* into ``trace_info`` table.
**Corresponding to the "trace_id" in V3 format.**
"""
key = Column(String(250), nullable=False)
"""
Metric key: `String` (limit 250 characters). Examples: "input_tokens", "output_tokens",
"total_tokens", "cost", etc.
"""
value = Column(sa.types.Float(precision=53), nullable=True)
"""
Metric value: `Float`. Could be *null* if not available. Supports both integer values
(e.g., token counts) and decimal values (e.g., API costs).
"""
trace_info = relationship(
"SqlTraceInfo",
backref=backref("metrics", cascade="all, delete-orphan", passive_deletes=True),
)
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.dbmodels.models.SqlTraceInfo`.
"""
# Composite primary key: (request_id, key)
__table_args__ = (
PrimaryKeyConstraint("request_id", "key", name="trace_metrics_pk"),
Index(f"index_{__tablename__}_request_id", "request_id"),
)
class SqlSpanMetrics(Base):
__tablename__ = "span_metrics"
trace_id = Column(String(50), nullable=False)
"""
Trace ID: `String` (limit 50 characters). Part of composite foreign key to spans table.
"""
span_id = Column(String(50), nullable=False)
"""
Span ID: `String` (limit 50 characters). Part of composite foreign key to spans table.
"""
key = Column(String(250), nullable=False)
"""
Metric key: `String` (limit 250 characters).
"""
value = Column(sa.types.Float(precision=53), nullable=True)
"""
Metric value: `Float`. Could be *null* if not available.
"""
span = relationship(
"SqlSpan",
backref=backref("metrics", cascade="all, delete-orphan", passive_deletes=True),
)
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.dbmodels.models.SqlSpan`.
"""
# Composite primary key: (trace_id, span_id, key)
__table_args__ = (
PrimaryKeyConstraint("trace_id", "span_id", "key", name="span_metrics_pk"),
ForeignKeyConstraint(
["trace_id", "span_id"],
["spans.trace_id", "spans.span_id"],
name="fk_span_metrics_span",
ondelete="CASCADE",
),
Index("index_span_metrics_trace_id_span_id", "trace_id", "span_id"),
)
class SqlAssessments(Base):
__tablename__ = "assessments"
assessment_id = Column(String(50), nullable=False)
"""
Assessment ID: `String` (limit 50 characters). *Primary Key* for ``assessments`` table.
"""
trace_id = Column(
String(50), ForeignKey("trace_info.request_id", ondelete="CASCADE"), nullable=False
)
"""
Trace ID that a given assessment belongs to. *Foreign Key* into ``trace_info`` table.
"""
name = Column(String(250), nullable=False)
"""
Assessment Name: `String` (limit of 250 characters).
"""
assessment_type = Column(String(50), nullable=False)
"""
Assessment type: `String` (limit 50 characters). Either "feedback", "expectation", or "issue".
"""
value = Column(Text, nullable=False)
"""
The assessment's value data stored as JSON: `Text` for the actual value content.
"""
error = Column(Text, nullable=True)
"""
AssessmentError stored as JSON: `Text` for error information (feedback only).
"""
created_timestamp = Column(BigInteger, nullable=False)
"""
The assessment's creation timestamp: `BigInteger`.
"""
last_updated_timestamp = Column(BigInteger, nullable=False)
"""
The update time of an assessment if the assessment has been updated: `BigInteger`.
"""
source_type = Column(String(50), nullable=False)
"""
Assessment source type: `String` (limit 50 characters). e.g., "HUMAN", "CODE", "LLM_JUDGE".
"""
source_id = Column(String(250), nullable=True)
"""
Assessment source ID: `String` (limit 250 characters). e.g., "evaluator@company.com".
"""
run_id = Column(String(32), nullable=True)
"""
Run ID associated with the assessment if generated due to a run event:
`String` (limit of 32 characters).
"""
span_id = Column(String(50), nullable=True)
"""
Span ID if the assessment is applied to a Span within a Trace:
`String` (limit of 50 characters).
"""
rationale = Column(Text, nullable=True)
"""
Justification for the assessment: `Text` for longer explanations.
"""
overrides = Column(String(50), nullable=True)
"""
Overridden assessment_id if an assessment is intended to update and replace an existing
assessment: `String` (limit of 50 characters).
"""
valid = Column(Boolean, nullable=False, default=True)
"""
Indicator for whether an assessment has been marked as invalid: `Boolean`. Defaults to True.
"""
assessment_metadata = Column(Text, nullable=True)
"""
Assessment metadata stored as JSON: `Text` for complex metadata structures.
"""
trace_info = relationship("SqlTraceInfo", backref=backref("assessments", cascade="all"))
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.dbmodels.models.SqlTraceInfo`.
"""
__table_args__ = (
PrimaryKeyConstraint("assessment_id", name="assessments_pkey"),
Index(f"index_{__tablename__}_trace_id_created_timestamp", "trace_id", "created_timestamp"),
Index(f"index_{__tablename__}_run_id_created_timestamp", "run_id", "created_timestamp"),
Index(f"index_{__tablename__}_last_updated_timestamp", "last_updated_timestamp"),
Index(f"index_{__tablename__}_assessment_type", "assessment_type"),
)
def to_mlflow_entity(self) -> Assessment:
"""Convert SqlAssessments to Assessment object."""
value_str = self.value
error_str = self.error
assessment_metadata_str = self.assessment_metadata
assessment_type_value = self.assessment_type
parsed_value = json.loads(value_str)
parsed_error = None
if error_str is not None:
error_dict = json.loads(error_str)
parsed_error = AssessmentError.from_dictionary(error_dict)
parsed_metadata = None
if assessment_metadata_str is not None:
parsed_metadata = json.loads(assessment_metadata_str)
source = AssessmentSource(source_type=self.source_type, source_id=self.source_id)
if assessment_type_value == "feedback":
assessment = Feedback(
name=self.name,
value=parsed_value,
error=parsed_error,
source=source,
trace_id=self.trace_id,
rationale=self.rationale,
metadata=parsed_metadata,
span_id=self.span_id,
create_time_ms=self.created_timestamp,
last_update_time_ms=self.last_updated_timestamp,
overrides=self.overrides,
valid=self.valid,
)
elif assessment_type_value == "expectation":
assessment = Expectation(
name=self.name,
value=parsed_value,
source=source,
trace_id=self.trace_id,
metadata=parsed_metadata,
span_id=self.span_id,
create_time_ms=self.created_timestamp,
last_update_time_ms=self.last_updated_timestamp,
)
assessment.overrides = self.overrides
assessment.valid = self.valid
elif assessment_type_value == "issue":
assessment = IssueReference(
issue_id=self.name,
issue_name=parsed_value.get("issue_name"),
source=source,
trace_id=self.trace_id,
run_id=self.run_id,
rationale=self.rationale,
metadata=parsed_metadata,
span_id=self.span_id,
create_time_ms=self.created_timestamp,
last_update_time_ms=self.last_updated_timestamp,
)
assessment.overrides = self.overrides
assessment.valid = self.valid
else:
raise ValueError(f"Unknown assessment type: {assessment_type_value}")
assessment.run_id = self.run_id
assessment.assessment_id = self.assessment_id
return assessment
@classmethod
def from_mlflow_entity(cls, assessment: Assessment):
if assessment.assessment_id is None:
assessment.assessment_id = generate_assessment_id()
current_timestamp = get_current_time_millis()
if assessment.feedback is not None:
assessment_type = "feedback"
value_json = json.dumps(assessment.feedback.value)
error_json = (
json.dumps(assessment.feedback.error.to_dictionary())
if assessment.feedback.error
else None
)
elif assessment.expectation is not None:
assessment_type = "expectation"
value_json = json.dumps(assessment.expectation.value)
error_json = None
elif assessment.issue is not None:
assessment_type = "issue"
value_json = json.dumps(assessment.issue.to_dictionary())
error_json = None
else:
raise MlflowException.invalid_parameter_value(
"Assessment must have either feedback, expectation, or issue value"
)
metadata_json = json.dumps(assessment.metadata) if assessment.metadata else None
return SqlAssessments(
assessment_id=assessment.assessment_id,
trace_id=assessment.trace_id,
name=assessment.name,
assessment_type=assessment_type,
value=value_json,
error=error_json,
created_timestamp=assessment.create_time_ms or current_timestamp,
last_updated_timestamp=assessment.last_update_time_ms or current_timestamp,
source_type=assessment.source.source_type,
source_id=assessment.source.source_id,
run_id=assessment.run_id,
span_id=assessment.span_id,
rationale=assessment.rationale,
overrides=assessment.overrides,
valid=True,
assessment_metadata=metadata_json,
)
def __repr__(self):
return f"<SqlAssessments({self.assessment_id}, {self.name}, {self.assessment_type})>"
class SqlIssue(Base):
__tablename__ = "issues"
issue_id = Column(String(36), nullable=False)
"""
Issue ID: `String` (limit 36 characters). *Primary Key* for ``issues`` table.
Format: "iss-<uuid>".
"""
experiment_id = Column(
Integer, ForeignKey("experiments.experiment_id", ondelete="CASCADE"), nullable=False
)
"""
Experiment ID: `Integer`. *Foreign Key* into ``experiments`` table. Required.
"""
name = Column(String(250), nullable=False)
"""
Issue name/title: `String` (limit 250 characters).
"""
description = Column(Text, nullable=False)
"""
Detailed description of the issue: `Text`.
"""
status = Column(String(50), nullable=False)
"""
Issue status: `String` (limit 50 characters).
"""
severity = Column(String(50), nullable=True)
"""
Severity level: `String` (limit 50 characters). Optional indicator of issue severity.
"""
root_causes = Column(Text, nullable=True)
"""
Root causes analysis stored as JSON array: `Text`. Nullable if root causes are not yet
determined.
"""
source_run_id = Column(
String(32), ForeignKey("runs.run_uuid", ondelete="SET NULL"), nullable=True
)
"""
Source run ID that discovered this issue: `String` (limit 32 characters).
*Foreign Key* into ``runs`` table. Nullable for manually created issues.
When the source run is deleted, this field is set to NULL.
"""
categories = Column(Text, nullable=True)
"""
Categories stored as JSON array: `Text`. Nullable if categories are not yet
determined.
"""
created_timestamp = Column(BigInteger, nullable=False)
"""
Creation timestamp: `BigInteger` in milliseconds.
"""
last_updated_timestamp = Column(BigInteger, nullable=False)
"""
Last update timestamp: `BigInteger` in milliseconds.
"""
created_by = Column(String(255), nullable=True)
"""
Creator identifier: `String` (limit 255 characters). Optional.
"""
run = relationship("SqlRun", foreign_keys=[source_run_id], backref=backref("issues"))
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.tracking.dbmodels.models.SqlRun`.
"""
__table_args__ = (
PrimaryKeyConstraint("issue_id", name="issues_pk"),
Index(f"index_{__tablename__}_experiment_id", "experiment_id"),
Index(f"index_{__tablename__}_source_run_id", "source_run_id"),
Index(f"index_{__tablename__}_status", "status"),
Index(f"index_{__tablename__}_created_by", "created_by"),
)
def __repr__(self):
return f"<SqlIssue({self.issue_id}, {self.name}, {self.status})>"
def to_mlflow_entity(self, trace_count: int | None = None) -> Issue:
"""
Convert DB model to corresponding MLflow entity.
Args:
trace_count: Optional trace count to include in the Issue entity.
Returns:
:py:class:`mlflow.entities.Issue` object.
"""
return Issue(
issue_id=self.issue_id,
experiment_id=str(self.experiment_id),
name=self.name,
description=self.description,
status=IssueStatus(self.status),
severity=IssueSeverity(self.severity) if self.severity else None,
root_causes=json.loads(self.root_causes) if self.root_causes else None,
source_run_id=self.source_run_id,
categories=json.loads(self.categories) if self.categories else None,
created_timestamp=self.created_timestamp,
last_updated_timestamp=self.last_updated_timestamp,
created_by=self.created_by,
trace_count=trace_count,
)
class SqlLoggedModel(Base):
__tablename__ = "logged_models"
model_id = Column(String(36), nullable=False)
"""
Model ID: `String` (limit 36 characters). *Primary Key* for ``logged_models`` table.
"""
experiment_id = Column(Integer, nullable=False)
"""
Experiment ID to which this model belongs: *Foreign Key* into ``experiments`` table.
"""
name = Column(String(500), nullable=False)
"""
Model name: `String` (limit 500 characters).
"""
artifact_location = Column(String(1000), nullable=False)
"""
Artifact location: `String` (limit 1000 characters).
"""
creation_timestamp_ms = Column(BigInteger, nullable=False)
"""
Creation timestamp: `BigInteger`.
"""
last_updated_timestamp_ms = Column(BigInteger, nullable=False)
"""
Last updated timestamp: `BigInteger`.
"""
status = Column(Integer, nullable=False)
"""
Status: `Integer`.
"""
lifecycle_stage = Column(String(32), default=LifecycleStage.ACTIVE)
"""
Lifecycle Stage of model: `String` (limit 32 characters).
"""
model_type = Column(String(500), nullable=True)
"""
Model type: `String` (limit 500 characters).
"""
source_run_id = Column(String(32), nullable=True)
"""
Source run ID: `String` (limit 32 characters).
"""
status_message = Column(String(1000), nullable=True)
"""
Status message: `String` (limit 1000 characters).
"""
experiment = relationship("SqlExperiment", backref=backref("logged_models", cascade="all"))
tags = relationship("SqlLoggedModelTag", backref="logged_model", cascade="all")
params = relationship("SqlLoggedModelParam", backref="logged_model", cascade="all")
metrics = relationship("SqlLoggedModelMetric", backref="logged_model", cascade="all")
__table_args__ = (
PrimaryKeyConstraint("model_id", name="logged_models_pk"),
CheckConstraint(
lifecycle_stage.in_(LifecycleStage.view_type_to_stages(ViewType.ALL)),
name="logged_models_lifecycle_stage_check",
),
ForeignKeyConstraint(
["experiment_id"],
["experiments.experiment_id"],
ondelete="CASCADE",
name="fk_logged_models_experiment_id",
),
)
def to_mlflow_entity(self) -> LoggedModel:
return LoggedModel(
model_id=self.model_id,
experiment_id=str(self.experiment_id),
name=self.name,
artifact_location=self.artifact_location,
creation_timestamp=self.creation_timestamp_ms,
last_updated_timestamp=self.last_updated_timestamp_ms,
status=LoggedModelStatus.from_int(self.status),
model_type=self.model_type,
source_run_id=self.source_run_id,
status_message=self.status_message,
tags={t.tag_key: t.tag_value for t in self.tags} if self.tags else None,
params={p.param_key: p.param_value for p in self.params} if self.params else None,
metrics=[m.to_mlflow_entity() for m in self.metrics] if self.metrics else None,
)
ALIASES = {
"creation_time": "creation_timestamp_ms",
"creation_timestamp": "creation_timestamp_ms",
"last_updated_timestamp": "last_updated_timestamp_ms",
}
@staticmethod
def is_numeric(s: str) -> bool:
return SqlLoggedModel.ALIASES.get(s, s) in {
"creation_timestamp_ms",
"last_updated_timestamp_ms",
}
class SqlLoggedModelMetric(Base):
__tablename__ = "logged_model_metrics"
model_id = Column(String(36), nullable=False)
"""
Model ID: `String` (limit 36 characters).
"""
metric_name = Column(String(500), nullable=False)
"""
Metric name: `String` (limit 500 characters).
"""
metric_timestamp_ms = Column(BigInteger, nullable=False)
"""
Metric timestamp: `BigInteger`.
"""
metric_step = Column(BigInteger, nullable=False)
"""
Metric step: `BigInteger`.
"""
metric_value = Column(sa.types.Float(precision=53), nullable=True)
"""
Metric value: `Float`.
"""
experiment_id = Column(Integer, nullable=False)
"""
Experiment ID: `Integer`.
"""
run_id = Column(String(32), nullable=False)
"""
Run ID: `String` (limit 32 characters).
"""
dataset_uuid = Column(String(36), nullable=True)
"""
Dataset UUID: `String` (limit 36 characters).
"""
dataset_name = Column(String(500), nullable=True)
"""
Dataset name: `String` (limit 500 characters).
"""
dataset_digest = Column(String(36), nullable=True)
"""
Dataset digest: `String` (limit 36 characters).
"""
__table_args__ = (
PrimaryKeyConstraint(
"model_id",
"metric_name",
"metric_timestamp_ms",
"metric_step",
"run_id",
name="logged_model_metrics_pk",
),
ForeignKeyConstraint(
["model_id"],
["logged_models.model_id"],
ondelete="CASCADE",
name="fk_logged_model_metrics_model_id",
),
ForeignKeyConstraint(
["experiment_id"],
["experiments.experiment_id"],
name="fk_logged_model_metrics_experiment_id",
),
ForeignKeyConstraint(
["run_id"],
["runs.run_uuid"],
ondelete="CASCADE",
name="fk_logged_model_metrics_run_id",
),
Index("index_logged_model_metrics_model_id", "model_id"),
)
def to_mlflow_entity(self) -> Metric:
return Metric(
key=self.metric_name,
value=self.metric_value,
timestamp=self.metric_timestamp_ms,
step=self.metric_step,
run_id=self.run_id,
dataset_name=self.dataset_name,
dataset_digest=self.dataset_digest,
model_id=self.model_id,
)
class SqlLoggedModelParam(Base):
__tablename__ = "logged_model_params"
model_id = Column(String(36), nullable=False)
"""
Model ID: `String` (limit 36 characters).
"""
experiment_id = Column(Integer, nullable=False)
"""
Experiment ID: `Integer`.
"""
param_key = Column(String(255), nullable=False)
"""
Param key: `String` (limit 255 characters).
"""
param_value = Column(Text(), nullable=False)
"""
Param value: `Text`.
"""
__table_args__ = (
PrimaryKeyConstraint(
"model_id",
"param_key",
name="logged_model_params_pk",
),
ForeignKeyConstraint(
["model_id"],
["logged_models.model_id"],
name="fk_logged_model_params_model_id",
ondelete="CASCADE",
),
ForeignKeyConstraint(
["experiment_id"],
["experiments.experiment_id"],
name="fk_logged_model_params_experiment_id",
),
)
def to_mlflow_entity(self) -> LoggedModelParameter:
return LoggedModelParameter(key=self.param_key, value=self.param_value)
class SqlLoggedModelTag(Base):
__tablename__ = "logged_model_tags"
model_id = Column(String(36), nullable=False)
"""
Model ID: `String` (limit 36 characters).
"""
experiment_id = Column(Integer, nullable=False)
"""
Experiment ID: `Integer`.
"""
tag_key = Column(String(255), nullable=False)
"""
Tag key: `String` (limit 255 characters).
"""
tag_value = Column(Text(), nullable=False)
"""
Tag value: `Text`.
"""
__table_args__ = (
PrimaryKeyConstraint(
"model_id",
"tag_key",
name="logged_model_tags_pk",
),
ForeignKeyConstraint(
["model_id"],
["logged_models.model_id"],
name="fk_logged_model_tags_model_id",
ondelete="CASCADE",
),
ForeignKeyConstraint(
["experiment_id"],
["experiments.experiment_id"],
name="fk_logged_model_tags_experiment_id",
),
)
def to_mlflow_entity(self) -> LoggedModelTag:
return LoggedModelTag(key=self.tag_key, value=self.tag_value)
class SqlEvaluationDataset(Base):
"""
DB model for evaluation datasets.
"""
__tablename__ = "evaluation_datasets"
dataset_id = Column(String(36), primary_key=True)
"""
Dataset ID: `String` (limit 36 characters).
*Primary Key* for ``evaluation_datasets`` table.
"""
workspace = Column(
String(63),
nullable=False,
default=DEFAULT_WORKSPACE_NAME,
server_default=sa.text(f"'{DEFAULT_WORKSPACE_NAME}'"),
)
"""
Workspace name that scopes this dataset.
"""
name = Column(String(255), nullable=False)
"""
Dataset name: `String` (limit 255 characters). *Non null* in table schema.
"""
schema = Column(Text, nullable=True)
"""
Schema information: `Text`.
"""
profile = Column(Text, nullable=True)
"""
Profile information: `Text`.
"""
digest = Column(String(64), nullable=True)
"""
Dataset digest: `String` (limit 64 characters).
"""
created_time = Column(BigInteger, default=get_current_time_millis)
"""
Creation time: `BigInteger`.
"""
last_update_time = Column(BigInteger, default=get_current_time_millis)
"""
Last update time: `BigInteger`.
"""
created_by = Column(String(255), nullable=True)
"""
Creator user ID: `String` (limit 255 characters).
"""
last_updated_by = Column(String(255), nullable=True)
"""
Last updater user ID: `String` (limit 255 characters).
"""
records = relationship(
"SqlEvaluationDatasetRecord", back_populates="dataset", cascade="all, delete-orphan"
)
tags = relationship(
"SqlEvaluationDatasetTag",
cascade="all, delete-orphan",
lazy="selectin",
)
__table_args__ = (
PrimaryKeyConstraint("dataset_id", name="evaluation_datasets_pk"),
Index("index_evaluation_datasets_name", "name"),
Index("index_evaluation_datasets_created_time", "created_time"),
Index("idx_evaluation_datasets_workspace", "workspace"),
)
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
:py:class:`mlflow.entities.EvaluationDataset`.
"""
records = None
# NB: Using SQLAlchemy's inspect module to determine if the field is loaded
# or not as calling .records on the EvaluationDataset object will trigger
# lazy-loading of the records.
state = inspect(self)
if "records" in state.dict:
records = [record.to_mlflow_entity() for record in self.records]
# Convert tags from relationship to dict
# Since we use lazy="selectin", tags are always loaded
# Return empty dict if no tags exist
tags_dict = {tag.key: tag.value for tag in self.tags}
dataset = EvaluationDataset(
dataset_id=self.dataset_id,
name=self.name,
tags=tags_dict,
schema=self.schema,
profile=self.profile,
digest=self.digest,
created_time=self.created_time,
last_update_time=self.last_update_time,
created_by=self.created_by,
last_updated_by=self.last_updated_by,
# experiment_ids will be loaded lazily when accessed
)
if records is not None:
dataset._records = records
return dataset
@classmethod
def from_mlflow_entity(cls, dataset: EvaluationDataset):
"""
Create SqlEvaluationDataset from EvaluationDataset entity.
Args:
dataset: EvaluationDataset entity
Returns:
SqlEvaluationDataset instance
"""
# Note: tags are not set here - they are handled as
# SqlEvaluationDatasetTag objects
return cls(
dataset_id=dataset.dataset_id,
name=dataset.name,
schema=dataset.schema,
profile=dataset.profile,
digest=dataset.digest,
created_time=dataset.created_time or get_current_time_millis(),
last_update_time=dataset.last_update_time or get_current_time_millis(),
created_by=dataset.created_by,
last_updated_by=dataset.last_updated_by,
)
class SqlEvaluationDatasetTag(Base):
"""
DB model for evaluation dataset tags.
"""
__tablename__ = "evaluation_dataset_tags"
dataset_id = Column(
String(36),
ForeignKey("evaluation_datasets.dataset_id", ondelete="CASCADE"),
primary_key=True,
)
"""
Dataset ID: `String` (limit 36 characters). Foreign key to evaluation_datasets.
*Primary Key* for ``evaluation_dataset_tags`` table.
"""
key = Column(String(255), primary_key=True)
"""
Tag key: `String` (limit 255 characters).
*Primary Key* for ``evaluation_dataset_tags`` table.
"""
value = Column(String(5000), nullable=True)
"""
Tag value: `String` (limit 5000 characters).
"""
__table_args__ = (
PrimaryKeyConstraint("dataset_id", "key", name="evaluation_dataset_tags_pk"),
ForeignKeyConstraint(
["dataset_id"],
["evaluation_datasets.dataset_id"],
name="fk_evaluation_dataset_tags_dataset_id",
ondelete="CASCADE",
),
Index("index_evaluation_dataset_tags_dataset_id", "dataset_id"),
)
class SqlEvaluationDatasetRecord(Base):
"""
DB model for evaluation dataset records.
"""
__tablename__ = "evaluation_dataset_records"
RECORD_ID_PREFIX = "dr-"
dataset_record_id = Column(String(36), primary_key=True)
"""
Dataset record ID: `String` (limit 36 characters).
*Primary Key* for ``evaluation_dataset_records`` table.
"""
dataset_id = Column(
String(36), ForeignKey("evaluation_datasets.dataset_id", ondelete="CASCADE"), nullable=False
)
"""
Dataset ID: `String` (limit 36 characters). Foreign key to evaluation_datasets.
"""
inputs = Column(MutableJSON, nullable=False)
"""
Inputs JSON: `JSON`. *Non null* in table schema.
"""
outputs = Column(MutableJSON, nullable=True)
"""
Outputs JSON: `JSON`.
"""
expectations = Column(MutableJSON, nullable=True)
"""
Expectations JSON: `JSON`.
"""
tags = Column(MutableJSON, nullable=True)
"""
Tags JSON: `JSON`.
"""
source = Column(MutableJSON, nullable=True)
"""
Source JSON: `JSON`.
"""
source_id = Column(String(36), nullable=True)
"""
Source ID for lookups: `String` (limit 36 characters).
"""
source_type = Column(String(255), nullable=True)
"""
Source type: `Text`.
"""
created_time = Column(BigInteger, default=get_current_time_millis)
"""
Creation time: `BigInteger`.
"""
last_update_time = Column(BigInteger, default=get_current_time_millis)
"""
Last update time: `BigInteger`.
"""
created_by = Column(String(255), nullable=True)
"""
Creator user ID: `String` (limit 255 characters).
"""
last_updated_by = Column(String(255), nullable=True)
"""
Last updater user ID: `String` (limit 255 characters).
"""
input_hash = Column(String(64), nullable=False)
"""
Hash of inputs for deduplication: `String` (limit 64 characters).
"""
dataset = relationship("SqlEvaluationDataset", back_populates="records")
__table_args__ = (
PrimaryKeyConstraint("dataset_record_id", name="evaluation_dataset_records_pk"),
Index("index_evaluation_dataset_records_dataset_id", "dataset_id"),
UniqueConstraint("dataset_id", "input_hash", name="unique_dataset_input"),
ForeignKeyConstraint(
["dataset_id"],
["evaluation_datasets.dataset_id"],
name="fk_evaluation_dataset_records_dataset_id",
ondelete="CASCADE",
),
)
def __init__(self, **kwargs):
"""Initialize a new dataset record with auto-generated ID if not provided."""
if "dataset_record_id" not in kwargs:
kwargs["dataset_record_id"] = self.generate_record_id()
super().__init__(**kwargs)
@staticmethod
def generate_record_id() -> str:
"""
Generate a unique ID for dataset records.
Returns:
A unique record ID with the format "dr-<uuid_hex>".
"""
return f"{SqlEvaluationDatasetRecord.RECORD_ID_PREFIX}{uuid.uuid4().hex}"
def to_mlflow_entity(self):
inputs = self.inputs
expectations = self.expectations
tags = self.tags
outputs = self.outputs.get(DATASET_RECORD_WRAPPED_OUTPUT_KEY) if self.outputs else None
source = None
if self.source:
source = DatasetRecordSource.from_dict(self.source)
return DatasetRecord(
dataset_record_id=self.dataset_record_id,
dataset_id=self.dataset_id,
inputs=inputs,
outputs=outputs,
expectations=expectations,
tags=tags,
source=source,
source_id=self.source_id,
created_time=self.created_time,
last_update_time=self.last_update_time,
created_by=self.created_by,
last_updated_by=self.last_updated_by,
)
@classmethod
def from_mlflow_entity(cls, record: DatasetRecord, input_hash: str):
"""
Create SqlEvaluationDatasetRecord from DatasetRecord entity.
Args:
record: DatasetRecord entity
input_hash: SHA256 hash of inputs for deduplication
Returns:
SqlEvaluationDatasetRecord instance
"""
source_dict = None
if record.source:
source_dict = record.source.to_dict()
outputs = (
{DATASET_RECORD_WRAPPED_OUTPUT_KEY: record.outputs}
if record.outputs is not None
else None
)
kwargs = {
"dataset_id": record.dataset_id,
"inputs": record.inputs,
"outputs": outputs,
"expectations": record.expectations,
"tags": record.tags,
"source": source_dict,
"source_id": record.source_id,
"source_type": record.source.source_type if record.source else None,
"created_time": record.created_time or get_current_time_millis(),
"last_update_time": record.last_update_time or get_current_time_millis(),
"created_by": record.created_by,
"last_updated_by": record.last_updated_by,
"input_hash": input_hash,
}
if record.dataset_record_id:
kwargs["dataset_record_id"] = record.dataset_record_id
return cls(**kwargs)
def merge(self, new_record_dict: dict[str, Any]) -> None:
"""
Merge new record data into this existing record.
Updates outputs, expectations and tags by merging new values with existing ones.
Preserves created_time and created_by from the original record.
Args:
new_record_dict: Dictionary containing new record data with optional
'outputs', 'expectations' and 'tags' fields to merge.
"""
if "outputs" in new_record_dict:
new_outputs = new_record_dict["outputs"]
self.outputs = (
{DATASET_RECORD_WRAPPED_OUTPUT_KEY: new_outputs}
if new_outputs is not None
else None
)
if new_expectations := new_record_dict.get("expectations"):
if self.expectations is None:
self.expectations = {}
self.expectations.update(new_expectations)
if new_tags := new_record_dict.get("tags"):
if self.tags is None:
self.tags = {}
self.tags.update(new_tags)
self.last_update_time = get_current_time_millis()
# Update last_updated_by if mlflow.user tag is present
# Otherwise keep the existing last_updated_by (don't change it to None)
if new_tags and MLFLOW_USER in new_tags:
self.last_updated_by = new_tags[MLFLOW_USER]
class SqlSpan(Base):
__tablename__ = "spans"
trace_id = Column(
String(50), ForeignKey("trace_info.request_id", ondelete="CASCADE"), nullable=False
)
"""
Trace ID: `String` (limit 50 characters). Part of composite primary key.
Foreign key to trace_info table.
"""
experiment_id = Column(Integer, ForeignKey("experiments.experiment_id"), nullable=False)
"""
Experiment ID: `Integer`. Foreign key to experiments table.
"""
span_id = Column(String(50), nullable=False)
"""
Span ID: `String` (limit 50 characters). Part of composite primary key.
"""
parent_span_id = Column(String(50), nullable=True)
"""
Parent span ID: `String` (limit 50 characters). Can be null for root spans.
"""
name = Column(Text, nullable=True)
"""
Span name: `Text`. Can be null.
"""
type = Column(String(500), nullable=True)
"""
Span type: `String` (limit 500 characters). Can be null.
Uses String instead of Text to support MSSQL indexes.
Limited to 500 chars to stay within MySQL's max index key length.
"""
status = Column(String(50), nullable=False)
"""
Span status: `String` (limit 50 characters).
"""
start_time_unix_nano = Column(BigInteger, nullable=False)
"""
Start time in nanoseconds since Unix epoch: `BigInteger`.
"""
end_time_unix_nano = Column(BigInteger, nullable=True)
"""
End time in nanoseconds since Unix epoch: `BigInteger`. Can be null if span is in progress.
"""
duration_ns = Column(
BigInteger,
Computed("end_time_unix_nano - start_time_unix_nano", persisted=True),
nullable=True,
)
"""
Duration in nanoseconds: `BigInteger`. Computed from end_time - start_time.
Stored as a persisted/stored generated column for efficient filtering.
Will be NULL for in-progress spans (where end_time is NULL).
"""
content = Column(Text, nullable=False)
"""
Full span content as JSON: `Text`.
Uses LONGTEXT in MySQL to support large spans (up to 4GB).
"""
dimension_attributes = Column(MutableJSON, nullable=True)
"""
Dimension attributes JSON: `JSON`. Optional field for storing reserved span attributes for
efficient querying or metrics aggregation.
"""
trace_info = relationship("SqlTraceInfo", backref=backref("spans", cascade="all"))
"""
SQLAlchemy relationship (many:one) with :py:class:`mlflow.store.dbmodels.models.SqlTraceInfo`.
"""
__table_args__ = (
PrimaryKeyConstraint("trace_id", "span_id", name="spans_pk"),
Index("index_spans_experiment_id", "experiment_id"),
# Two indexes needed to support both filter patterns efficiently:
Index(
"index_spans_experiment_id_status_type", "experiment_id", "status", "type"
), # For status-only and status+type filters
Index(
"index_spans_experiment_id_type_status", "experiment_id", "type", "status"
), # For type-only and type+status filters
Index("index_spans_experiment_id_duration", "experiment_id", "duration_ns"),
)
class SqlEntityAssociation(Base):
"""
DB model for entity associations.
"""
__tablename__ = "entity_associations"
ASSOCIATION_ID_PREFIX = "a-"
association_id = Column(String(36), nullable=False)
"""
Association ID: `String` (limit 36 characters).
"""
source_type = Column(String(36), nullable=False)
"""
Source entity type: `String` (limit 36 characters).
"""
source_id = Column(String(36), nullable=False)
"""
Source entity ID: `String` (limit 36 characters).
"""
destination_type = Column(String(36), nullable=False)
"""
Destination entity type: `String` (limit 36 characters).
"""
destination_id = Column(String(36), nullable=False)
"""
Destination entity ID: `String` (limit 36 characters).
"""
created_time = Column(BigInteger, default=get_current_time_millis)
"""
Creation time: `BigInteger`.
"""
__table_args__ = (
PrimaryKeyConstraint(
"source_type",
"source_id",
"destination_type",
"destination_id",
name="entity_associations_pk",
),
Index("index_entity_associations_association_id", "association_id"),
Index(
"index_entity_associations_reverse_lookup",
"destination_type",
"destination_id",
"source_type",
"source_id",
),
)
def __init__(self, **kwargs):
"""Initialize a new entity association with auto-generated ID if not provided."""
if "association_id" not in kwargs:
kwargs["association_id"] = self.generate_association_id()
super().__init__(**kwargs)
@staticmethod
def generate_association_id() -> str:
"""
Generate a unique ID for entity associations.
Returns:
A unique association ID with the format "a-<uuid_hex>".
"""
return f"{SqlEntityAssociation.ASSOCIATION_ID_PREFIX}{uuid.uuid4().hex}"
class SqlScorer(Base):
"""
DB model for storing scorer information. These are recorded in ``scorers`` table.
"""
__tablename__ = "scorers"
experiment_id = Column(
Integer, ForeignKey("experiments.experiment_id", ondelete="CASCADE"), nullable=False
)
"""
Experiment ID to which this scorer belongs: *Foreign Key* into ``experiments`` table.
"""
scorer_name = Column(String(256), nullable=False)
"""
Scorer name: `String` (limit 256 characters). Part of *Primary Key* for ``scorers`` table.
"""
scorer_id = Column(String(36), nullable=False)
"""
Scorer ID: `String` (limit 36 characters). Unique identifier for the scorer.
"""
experiment = relationship("SqlExperiment", backref=backref("scorers", cascade="all"))
"""
SQLAlchemy relationship (many:one) with :py:class:`mlflow.store.dbmodels.models.SqlExperiment`.
"""
__table_args__ = (
PrimaryKeyConstraint("scorer_id", name="scorer_pk"),
Index(
f"index_{__tablename__}_experiment_id_scorer_name",
"experiment_id",
"scorer_name",
unique=True,
),
)
def __repr__(self):
return f"<SqlScorer ({self.experiment_id}, {self.scorer_name}, {self.scorer_id})>"
class SqlScorerVersion(Base):
"""
DB model for storing scorer version information. These are recorded in
``scorer_versions`` table.
"""
__tablename__ = "scorer_versions"
scorer_id = Column(
String(36), ForeignKey("scorers.scorer_id", ondelete="CASCADE"), nullable=False
)
"""
Scorer ID: `String` (limit 36 characters). *Foreign Key* into ``scorers`` table.
"""
scorer_version = Column(Integer, nullable=False)
"""
Scorer version: `Integer`. Part of *Primary Key* for ``scorer_versions`` table.
"""
serialized_scorer = Column(Text, nullable=False)
"""
Serialized scorer data: `Text`. Contains the serialized scorer object.
"""
creation_time = Column(BigInteger(), default=get_current_time_millis)
"""
Creation time of scorer version: `BigInteger`. Automatically set to current time when created.
"""
# Relationship to the parent scorer
scorer = relationship("SqlScorer", backref=backref("scorer_versions", cascade="all"))
"""
SQLAlchemy relationship (many:one) with :py:class:`mlflow.store.dbmodels.models.SqlScorer`.
"""
__table_args__ = (
PrimaryKeyConstraint("scorer_id", "scorer_version", name="scorer_version_pk"),
Index(f"index_{__tablename__}_scorer_id", "scorer_id"),
)
def __repr__(self):
return f"<SqlScorerVersion ({self.scorer_id}, {self.scorer_version})>"
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
mlflow.entities.ScorerVersion.
"""
from mlflow.entities.scorer import ScorerVersion
return ScorerVersion(
experiment_id=str(self.scorer.experiment_id),
scorer_name=self.scorer.scorer_name,
scorer_version=self.scorer_version,
serialized_scorer=self.serialized_scorer,
creation_time=self.creation_time,
scorer_id=self.scorer_id,
)
class SqlOnlineScoringConfig(Base):
"""
DB model for storing online scoring configuration. These are recorded in
``online_scoring_configs`` table.
"""
__tablename__ = "online_scoring_configs"
online_scoring_config_id = Column(String(36), nullable=False)
"""
Online Scoring Config ID: `String` (limit 36 characters). *Primary Key* for
``online_scoring_configs`` table.
"""
scorer_id = Column(
String(36), ForeignKey("scorers.scorer_id", ondelete="CASCADE"), nullable=False
)
"""
Scorer ID: `String` (limit 36 characters). *Foreign Key* into ``scorers`` table.
"""
sample_rate = Column(sa.types.Float(precision=53), nullable=False)
"""
Sample rate for online scoring: `Float` (double precision).
Value between 0 and 1 representing the fraction of traces to sample.
"""
experiment_id = Column(Integer, ForeignKey("experiments.experiment_id"), nullable=False)
"""
Experiment ID: `Integer`. *Foreign Key* into ``experiments`` table.
"""
filter_string = Column(Text, nullable=True)
"""
Filter string for online scoring: `Text`. Optional filter expression to select traces.
"""
# Relationship to the parent scorer
scorer = relationship("SqlScorer", backref=backref("online_configs", cascade="all"))
"""
SQLAlchemy relationship (many:one) with :py:class:`mlflow.store.dbmodels.models.SqlScorer`.
"""
__table_args__ = (
PrimaryKeyConstraint("online_scoring_config_id", name="online_scoring_config_pk"),
)
def __repr__(self):
return (
f"<SqlOnlineScoringConfig ({self.online_scoring_config_id}, {self.scorer_id}, "
f"{self.sample_rate}, {self.experiment_id}, {self.filter_string})>"
)
def to_mlflow_entity(self) -> OnlineScoringConfig:
"""
Convert this SqlOnlineScoringConfig to an OnlineScoringConfig entity.
Returns:
OnlineScoringConfig: The entity representation of this online config.
"""
return OnlineScoringConfig(
online_scoring_config_id=self.online_scoring_config_id,
scorer_id=self.scorer_id,
sample_rate=self.sample_rate,
experiment_id=str(self.experiment_id),
filter_string=self.filter_string,
)
class SqlJob(Base):
"""
DB model for Job entities. These are recorded in the ``jobs`` table.
"""
__tablename__ = "jobs"
id = Column(String(36), nullable=False)
"""
Job ID: `String` (limit 36 characters). *Primary Key* for ``jobs`` table.
"""
creation_time = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Creation timestamp: `BigInteger`.
"""
job_name = Column(String(500), nullable=False)
"""
Job name: `String` (limit 500 characters).
"""
params = Column(Text, nullable=False)
"""
Job parameters: `Text`.
"""
workspace = Column(
String(63),
nullable=False,
default=DEFAULT_WORKSPACE_NAME,
server_default=sa.text(f"'{DEFAULT_WORKSPACE_NAME}'"),
)
"""
Workspace identifier for this job: `String` (limit 63 characters). Defaults to ``'default'``.
"""
timeout = Column(sa.types.Float(precision=53), nullable=True)
"""
Job execution timeout in seconds: `Float`
"""
status = Column(Integer, nullable=False)
"""
Job status: `Integer`.
"""
result = Column(Text, nullable=True)
"""
Job result: `Text`.
"""
retry_count = Column(Integer, default=0, nullable=False)
"""
Job retry count: `Integer`
"""
last_update_time = Column(BigInteger(), default=get_current_time_millis, nullable=False)
"""
Last Update time of experiment: `BigInteger`.
"""
status_details = Column(MutableJSON, nullable=True)
"""
Job status details: `JSON`.
Stores additional job status details.
"""
__table_args__ = (
PrimaryKeyConstraint("id", name="jobs_pk"),
Index(
"index_jobs_name_status_creation_time",
"job_name",
"workspace",
"status",
"creation_time",
),
)
def __repr__(self):
return f"<SqlJob ({self.id}, {self.job_name}, {self.status})>"
def to_mlflow_entity(self):
"""
Convert DB model to corresponding MLflow entity.
Returns:
mlflow.entities._job.Job.
"""
from mlflow.entities._job import Job
from mlflow.entities._job_status import JobStatus
return Job(
job_id=self.id,
creation_time=self.creation_time,
job_name=self.job_name,
params=self.params,
timeout=self.timeout,
status=JobStatus.from_int(self.status),
result=self.result,
retry_count=self.retry_count,
last_update_time=self.last_update_time,
workspace=self.workspace,
status_details=self.status_details,
)
class SqlGatewaySecret(Base):
"""
DB model for secrets. These are recorded in the ``secrets`` table.
Stores encrypted credentials used by MLflow resources (e.g., LLM provider API keys).
"""
__tablename__ = "secrets"
secret_id = Column(String(36), nullable=False)
"""
Secret ID: `String` (limit 36 characters). *Primary Key* for ``secrets`` table.
NB: IMMUTABLE. This field is used as part of the AAD (Additional Authenticated Data) during
AES-GCM encryption. If modified, decryption will fail with authentication error. See
mlflow/utils/crypto.py:_create_aad() for details.
"""
secret_name = Column(String(255), nullable=False)
"""
Secret name: `String` (limit 255 characters). User-provided name for the secret.
Defined as *Unique* in table schema to prevent confusing selection of secrets in the UI.
NB: IMMUTABLE. This field is used as part of the AAD (Additional Authenticated Data) during
AES-GCM encryption. If modified, decryption will fail with authentication error. To "rename"
a secret, create a new secret with the desired name and delete the old one. See
mlflow/utils/crypto.py:_create_aad() for details.
"""
encrypted_value = Column(LargeBinary, nullable=False)
"""
Encrypted secret data: `LargeBinary`. Combined nonce (12 bytes) + AES-GCM ciphertext +
tag (16 bytes). The secret value is encrypted using envelope encryption with a DEK, and
the nonce is prepended for storage. AAD (Additional Authenticated Data) from secret_id
and secret_name is included during encryption to prevent ciphertext substitution attacks.
"""
wrapped_dek = Column(LargeBinary, nullable=False)
"""
Wrapped data encryption key: `LargeBinary`. DEK encrypted by KEK.
The DEK is a randomly generated 256-bit AES key used to encrypt the secret value.
"""
kek_version = Column(Integer, nullable=False, default=1)
"""
KEK version: `Integer`. Indicates which KEK version was used to wrap the DEK.
Used for KEK rotation - allows multiple KEK versions to coexist during migration.
"""
masked_value = Column(String(500), nullable=False)
"""
Masked secret value: `String` (limit 500 characters). JSON-serialized dict showing partial
secret values for identification. Format: ``{"key": "prefix...suffix"}``, e.g.,
``{"api_key": "sk-...xyz123"}`` or ``{"aws_access_key_id": "AKI...1234", ...}``.
Helps users identify secrets without exposing the full values.
"""
provider = Column(String(64), nullable=True)
"""
Provider identifier: `String` (limit 64 characters). Optional.
E.g., "anthropic", "openai", "cohere", "vertex_ai", "bedrock", "databricks".
"""
auth_config = Column(Text, nullable=True)
"""
Provider authentication config: `Text` (JSON string). Non-sensitive metadata for
provider configuration like region, project_id, endpoint URL. Useful for UI display
and disambiguation. Not encrypted since it contains no secrets.
For multi-auth providers, includes "auth_mode" key (e.g., "access_keys", "iam_role").
"""
description = Column(Text, nullable=True)
"""
Secret description: `Text`. Optional user-provided description for the API key.
"""
created_by = Column(String(255), nullable=True)
"""
Creator user ID: `String` (limit 255 characters).
"""
created_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Creation timestamp: `BigInteger`.
"""
last_updated_by = Column(String(255), nullable=True)
"""
Last updater user ID: `String` (limit 255 characters).
"""
last_updated_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Last update timestamp: `BigInteger`.
"""
workspace = Column(
String(63),
nullable=False,
default=DEFAULT_WORKSPACE_NAME,
server_default=sa.text(f"'{DEFAULT_WORKSPACE_NAME}'"),
)
"""
Workspace: `String` (limit 63 characters). Workspace scope for logical isolation.
"""
__table_args__ = (
PrimaryKeyConstraint("secret_id", name="secrets_pk"),
UniqueConstraint("workspace", "secret_name", name="uq_secrets_workspace_secret_name"),
Index("idx_secrets_workspace", "workspace"),
)
def __repr__(self):
return f"<SqlGatewaySecret ({self.secret_id}, {self.secret_name})>"
def to_mlflow_entity(self):
try:
masked_value = json.loads(self.masked_value)
except (json.JSONDecodeError, TypeError):
masked_value = {"value": "***"}
return GatewaySecretInfo(
secret_id=self.secret_id,
secret_name=self.secret_name,
masked_values=masked_value,
created_at=self.created_at,
last_updated_at=self.last_updated_at,
provider=self.provider,
auth_config=json.loads(self.auth_config) if self.auth_config else None,
workspace=self.workspace,
created_by=self.created_by,
last_updated_by=self.last_updated_by,
)
class SqlGatewayEndpoint(Base):
"""
DB model for endpoints. These are recorded in ``endpoints`` table.
Represents LLM gateway endpoints that route requests to configured models.
"""
__tablename__ = "endpoints"
endpoint_id = Column(String(36), nullable=False)
"""
Endpoint ID: `String` (limit 36 characters). *Primary Key* for ``endpoints`` table.
"""
name = Column(String(255), nullable=True)
"""
Endpoint name: `String` (limit 255 characters). User-provided name for the endpoint.
Defined as *Unique* in table schema.
"""
created_by = Column(String(255), nullable=True)
"""
Creator user ID: `String` (limit 255 characters).
"""
created_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Creation timestamp: `BigInteger`.
"""
last_updated_by = Column(String(255), nullable=True)
"""
Last updater user ID: `String` (limit 255 characters).
"""
last_updated_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Last update timestamp: `BigInteger`.
"""
routing_strategy = Column(String(64), nullable=True)
"""
Routing strategy: `String` (limit 64 characters). E.g., "FALLBACK".
"""
fallback_config_json = Column(Text, nullable=True)
"""
Fallback configuration as JSON: `Text`. Stores FallbackConfig proto as JSON.
Example: {"strategy": "SEQUENTIAL", "max_attempts": 3, "model_definition_ids": ["d-1", "d-2"]}
"""
experiment_id = Column(
Integer, ForeignKey("experiments.experiment_id", ondelete="SET NULL"), nullable=True
)
"""
Experiment ID: `Integer`. *Foreign Key* into ``experiments`` table.
ID of the MLflow experiment where traces for this endpoint are logged.
Uses SET NULL on delete - if the experiment is deleted, this becomes NULL.
"""
usage_tracking = Column(Boolean, nullable=False, default=True)
"""
Usage tracking: `Boolean`. Whether usage tracking is enabled for this endpoint.
When true, traces will be logged for endpoint invocations.
"""
workspace = Column(
String(63),
nullable=False,
default=DEFAULT_WORKSPACE_NAME,
server_default=sa.text(f"'{DEFAULT_WORKSPACE_NAME}'"),
)
"""
Workspace: `String` (limit 63 characters). Workspace scope for logical isolation.
"""
__table_args__ = (
PrimaryKeyConstraint("endpoint_id", name="endpoints_pk"),
UniqueConstraint("workspace", "name", name="uq_endpoints_workspace_name"),
Index("idx_endpoints_workspace", "workspace"),
)
def __repr__(self):
return f"<SqlGatewayEndpoint ({self.endpoint_id}, {self.name})>"
def to_mlflow_entity(self):
fallback_config = None
model_mappings = [m.to_mlflow_entity() for m in self.model_mappings]
if self.fallback_config_json:
try:
fallback_config_dict = json.loads(self.fallback_config_json)
fallback_config = FallbackConfig(
strategy=FallbackStrategy(fallback_config_dict.get("strategy"))
if fallback_config_dict.get("strategy")
else None,
max_attempts=fallback_config_dict.get("max_attempts"),
)
except (json.JSONDecodeError, TypeError):
pass
routing_strategy = RoutingStrategy(self.routing_strategy) if self.routing_strategy else None
return GatewayEndpoint(
endpoint_id=self.endpoint_id,
name=self.name,
model_mappings=model_mappings,
tags=[tag.to_mlflow_entity() for tag in self.tags],
created_at=self.created_at,
last_updated_at=self.last_updated_at,
created_by=self.created_by,
last_updated_by=self.last_updated_by,
routing_strategy=routing_strategy,
fallback_config=fallback_config,
experiment_id=str(self.experiment_id) if self.experiment_id is not None else None,
usage_tracking=self.usage_tracking,
workspace=self.workspace,
)
class SqlGatewayModelDefinition(Base):
"""
DB model for model definitions. These are recorded in ``model_definitions`` table.
Represents reusable LLM model configurations that can be shared across multiple endpoints.
"""
__tablename__ = "model_definitions"
model_definition_id = Column(String(36), nullable=False)
"""
Model Definition ID: `String` (limit 36 characters).
*Primary Key* for ``model_definitions`` table.
"""
name = Column(String(255), nullable=False)
"""
Model definition name: `String` (limit 255 characters). User-provided name for identification.
Defined as *Unique* in table schema.
"""
secret_id = Column(
String(36), ForeignKey("secrets.secret_id", ondelete="SET NULL"), nullable=True
)
"""
Secret ID: `String` (limit 36 characters). *Foreign Key* into ``secrets`` table.
References the API key/credentials for this model. Nullable to allow orphaned
model definitions when secrets are deleted.
"""
provider = Column(String(64), nullable=False)
"""
Provider identifier: `String` (limit 64 characters).
E.g., "anthropic", "openai", "cohere", "vertex_ai", "bedrock", "databricks".
"""
model_name = Column(String(256), nullable=False)
"""
Model name: `String` (limit 256 characters). Provider-specific model identifier.
E.g., "claude-3-5-sonnet-20241022", "gpt-4o", "command-r-plus".
"""
created_by = Column(String(255), nullable=True)
"""
Creator user ID: `String` (limit 255 characters).
"""
created_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Creation timestamp: `BigInteger`.
"""
last_updated_by = Column(String(255), nullable=True)
"""
Last updater user ID: `String` (limit 255 characters).
"""
last_updated_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Last update timestamp: `BigInteger`.
"""
workspace = Column(
String(63),
nullable=False,
default=DEFAULT_WORKSPACE_NAME,
server_default=sa.text(f"'{DEFAULT_WORKSPACE_NAME}'"),
)
"""
Workspace: `String` (limit 63 characters). Workspace scope for logical isolation.
"""
secret = relationship("SqlGatewaySecret")
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.tracking.dbmodels.models.SqlGatewaySecret`.
"""
__table_args__ = (
PrimaryKeyConstraint("model_definition_id", name="model_definitions_pk"),
UniqueConstraint("workspace", "name", name="uq_model_definitions_workspace_name"),
Index("index_model_definitions_secret_id", "secret_id"),
Index("index_model_definitions_provider", "provider"),
Index("idx_model_definitions_workspace", "workspace"),
)
def __repr__(self):
return f"<SqlGatewayModelDefinition ({self.model_definition_id}, {self.name})>"
def to_mlflow_entity(self):
return GatewayModelDefinition(
model_definition_id=self.model_definition_id,
name=self.name,
secret_id=self.secret_id,
secret_name=self.secret.secret_name if self.secret else None,
provider=self.provider,
model_name=self.model_name,
created_at=self.created_at,
last_updated_at=self.last_updated_at,
created_by=self.created_by,
last_updated_by=self.last_updated_by,
workspace=self.workspace,
)
class SqlGatewayEndpointModelMapping(Base):
"""
DB model for endpoint-model mappings. These are recorded in ``endpoint_model_mappings`` table.
Junction table linking endpoints to model definitions (supports multi-model routing).
"""
__tablename__ = "endpoint_model_mappings"
mapping_id = Column(String(36), nullable=False)
"""
Mapping ID: `String` (limit 36 characters). *Primary Key* for ``endpoint_model_mappings`` table.
"""
endpoint_id = Column(
String(36), ForeignKey("endpoints.endpoint_id", ondelete="CASCADE"), nullable=False
)
"""
Endpoint ID: `String` (limit 36 characters). *Foreign Key* into ``endpoints`` table.
Cascades on delete - removing an endpoint removes all its model mappings.
"""
model_definition_id = Column(
String(36),
ForeignKey("model_definitions.model_definition_id"),
nullable=False,
)
"""
Model Definition ID: `String` (limit 36 characters).
*Foreign Key* into ``model_definitions`` table.
Prevents deletion of a model definition that is in use (default FK behavior).
"""
weight = Column(Float, default=1.0, nullable=False)
"""
Routing weight: `Float`. Used for traffic distribution when endpoint has multiple models.
Default is 1.0.
"""
linkage_type = Column(String(64), default="PRIMARY", nullable=False)
"""
Linkage type: `String` (limit 64 characters). Specifies whether this is a PRIMARY or
FALLBACK linkage. Default is PRIMARY.
"""
fallback_order = Column(Integer, nullable=True)
"""
Fallback order: `Integer`. Specifies the order for fallback attempts.
NULL for PRIMARY linkages. Lower values are tried first.
"""
created_by = Column(String(255), nullable=True)
"""
Creator user ID: `String` (limit 255 characters).
"""
created_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Creation timestamp: `BigInteger`.
"""
endpoint = relationship("SqlGatewayEndpoint", backref=backref("model_mappings", cascade="all"))
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.tracking.dbmodels.models.SqlGatewayEndpoint`.
"""
model_definition = relationship("SqlGatewayModelDefinition")
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.tracking.dbmodels.models.SqlGatewayModelDefinition`.
"""
__table_args__ = (
PrimaryKeyConstraint("mapping_id", name="endpoint_model_mappings_pk"),
Index("index_endpoint_model_mappings_endpoint_id", "endpoint_id"),
Index("index_endpoint_model_mappings_model_definition_id", "model_definition_id"),
Index(
"unique_endpoint_model_linkage_mapping",
"endpoint_id",
"model_definition_id",
"linkage_type",
unique=True,
),
)
def __repr__(self):
return (
f"<SqlGatewayEndpointModelMapping ({self.mapping_id}, "
f"endpoint={self.endpoint_id}, model={self.model_definition_id})>"
)
def to_mlflow_entity(self):
from mlflow.entities.gateway_endpoint import GatewayModelLinkageType
model_def = None
if self.model_definition:
model_def = self.model_definition.to_mlflow_entity()
return GatewayEndpointModelMapping(
mapping_id=self.mapping_id,
endpoint_id=self.endpoint_id,
model_definition_id=self.model_definition_id,
model_definition=model_def,
weight=self.weight,
linkage_type=GatewayModelLinkageType(self.linkage_type),
fallback_order=self.fallback_order,
created_at=self.created_at,
created_by=self.created_by,
)
class SqlGatewayEndpointBinding(Base):
"""
DB model for endpoint bindings. These are recorded in ``endpoint_bindings`` table.
Tracks which resources are bound to which endpoints (e.g., model configurations, experiments).
"""
__tablename__ = "endpoint_bindings"
endpoint_id = Column(
String(36), ForeignKey("endpoints.endpoint_id", ondelete="CASCADE"), nullable=False
)
"""
Endpoint ID: `String` (limit 36 characters). *Foreign Key* into ``endpoints`` table.
Cascades on delete. Part of composite primary key.
"""
resource_type = Column(String(50), nullable=False)
"""
Resource type: `String` (limit 50 characters). Type of resource bound to the endpoint.
E.g., "endpoint_model", "experiment", "registered_model". Part of composite primary key.
"""
resource_id = Column(String(255), nullable=False)
"""
Resource ID: `String` (limit 255 characters). ID of the specific resource instance.
Part of composite primary key.
"""
created_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Creation timestamp: `BigInteger`.
"""
created_by = Column(String(255), nullable=True)
"""
Creator user ID: `String` (limit 255 characters).
"""
last_updated_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Last update timestamp: `BigInteger`.
"""
last_updated_by = Column(String(255), nullable=True)
"""
Last updater user ID: `String` (limit 255 characters).
"""
display_name = Column(String(255), nullable=True)
"""
Human-readable display name: `String` (limit 255 characters).
E.g., scorer name for display in the UI.
"""
endpoint = relationship("SqlGatewayEndpoint", backref=backref("bindings", cascade="all"))
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.tracking.dbmodels.models.SqlGatewayEndpoint`.
"""
__table_args__ = (
PrimaryKeyConstraint(
"endpoint_id", "resource_type", "resource_id", name="endpoint_bindings_pk"
),
)
def __repr__(self):
return (
f"<SqlGatewayEndpointBinding "
f"({self.endpoint_id}, {self.resource_type}, {self.resource_id})>"
)
def to_mlflow_entity(self):
return GatewayEndpointBinding(
endpoint_id=self.endpoint_id,
resource_type=GatewayResourceType(self.resource_type),
resource_id=self.resource_id,
created_at=self.created_at,
last_updated_at=self.last_updated_at,
created_by=self.created_by,
last_updated_by=self.last_updated_by,
display_name=self.display_name,
)
class SqlGatewayEndpointTag(Base):
"""
DB model for endpoint tags. These are recorded in ``endpoint_tags`` table.
Tags are key-value pairs associated with endpoints for categorization and filtering.
"""
__tablename__ = "endpoint_tags"
key = Column(String(250), nullable=False)
"""
Tag key: `String` (limit 250 characters). Part of composite *Primary Key*.
"""
value = Column(String(5000), nullable=True)
"""
Value associated with tag: `String` (limit 5000 characters). Could be *null*.
"""
endpoint_id = Column(
String(36), ForeignKey("endpoints.endpoint_id", ondelete="CASCADE"), nullable=False
)
"""
Endpoint ID to which this tag belongs: *Foreign Key* into ``endpoints`` table.
Part of composite *Primary Key*. Cascades on delete.
"""
endpoint = relationship("SqlGatewayEndpoint", backref=backref("tags", cascade="all"))
"""
SQLAlchemy relationship (many:one) with
:py:class:`mlflow.store.tracking.dbmodels.models.SqlGatewayEndpoint`.
"""
__table_args__ = (
PrimaryKeyConstraint("key", "endpoint_id", name="endpoint_tag_pk"),
Index("index_endpoint_tags_endpoint_id", "endpoint_id"),
)
def __repr__(self):
return f"<SqlGatewayEndpointTag({self.key}, {self.value})>"
def to_mlflow_entity(self):
return GatewayEndpointTag(key=self.key, value=self.value)
class SqlGatewayBudgetPolicy(Base):
"""
DB model for budget policies. These are recorded in ``budget_policies`` table.
Represents cost-based budget limits for the AI Gateway with fixed time windows.
"""
__tablename__ = "budget_policies"
budget_policy_id = Column(String(36), nullable=False)
"""
Budget policy ID: `String` (limit 36 characters). *Primary Key*.
"""
budget_unit = Column(String(32), nullable=False)
"""
Budget measurement unit: `String` (USD).
"""
budget_amount = Column(Float, nullable=False)
"""
Budget limit amount: `Float`.
"""
duration_unit = Column(String(32), nullable=False)
"""
Duration unit for the fixed window: `String` (MINUTES, HOURS, DAYS, WEEKS, MONTHS).
"""
duration_value = Column(Integer, nullable=False)
"""
Duration value: `Integer`. Length of the window in units of duration_type.
"""
target_scope = Column(String(32), nullable=False)
"""
Target scope: `String` (GLOBAL, WORKSPACE).
"""
budget_action = Column(String(32), nullable=False)
"""
Action when budget exceeded: `String` (ALERT, REJECT).
"""
created_by = Column(String(255), nullable=True)
"""
Creator user ID: `String` (limit 255 characters).
"""
created_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Creation timestamp: `BigInteger`.
"""
last_updated_by = Column(String(255), nullable=True)
"""
Last updater user ID: `String` (limit 255 characters).
"""
last_updated_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Last update timestamp: `BigInteger`.
"""
workspace = Column(
String(63),
nullable=False,
default=DEFAULT_WORKSPACE_NAME,
server_default=sa.text(f"'{DEFAULT_WORKSPACE_NAME}'"),
)
"""
Workspace: `String` (limit 63 characters). Workspace scope for logical isolation.
"""
__table_args__ = (
PrimaryKeyConstraint("budget_policy_id", name="budget_policies_pk"),
Index("idx_budget_policies_workspace", "workspace"),
)
def __repr__(self):
return f"<SqlGatewayBudgetPolicy ({self.budget_policy_id})>"
def to_mlflow_entity(self):
return GatewayBudgetPolicy(
budget_policy_id=self.budget_policy_id,
budget_unit=BudgetUnit(self.budget_unit),
budget_amount=self.budget_amount,
duration=BudgetDuration(
unit=BudgetDurationUnit(self.duration_unit),
value=self.duration_value,
),
target_scope=BudgetTargetScope(self.target_scope),
budget_action=BudgetAction(self.budget_action),
created_at=self.created_at,
last_updated_at=self.last_updated_at,
created_by=self.created_by,
last_updated_by=self.last_updated_by,
workspace=self.workspace,
)
class SqlGatewayGuardrail(Base):
"""
DB model for guardrails. These are recorded in ``guardrails`` table.
A guardrail wraps a scorer with a stage (BEFORE/AFTER) and action (VALIDATION/SANITIZATION).
"""
__tablename__ = "guardrails"
guardrail_id = Column(String(36), nullable=False)
"""
Guardrail ID: `String` (limit 36 characters). *Primary Key*.
"""
name = Column(String(255), nullable=False)
"""
Human-readable guardrail name: `String` (limit 255 characters).
"""
scorer_id = Column(String(36), nullable=False)
"""
Scorer ID referencing the MLflow scorer: `String`.
"""
scorer_version = Column(Integer, nullable=False)
"""
Scorer version: `Integer`.
"""
scorer_version_ref = relationship(
"SqlScorerVersion",
foreign_keys=[scorer_id, scorer_version],
primaryjoin=(
"and_(SqlGatewayGuardrail.scorer_id == SqlScorerVersion.scorer_id, "
"SqlGatewayGuardrail.scorer_version == SqlScorerVersion.scorer_version)"
),
viewonly=True,
lazy="joined",
)
stage = Column(String(32), nullable=False)
"""
Guardrail stage: `String` (BEFORE, AFTER).
"""
action = Column(String(32), nullable=False)
"""
Guardrail action: `String` (VALIDATION, SANITIZATION).
"""
action_endpoint_id = Column(String(36), nullable=True)
"""
Optional endpoint ID for sanitization LLM: `String`. Used when action is SANITIZATION.
"""
created_by = Column(String(255), nullable=True)
"""
Creator user ID: `String` (limit 255 characters).
"""
created_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Creation timestamp: `BigInteger`.
"""
last_updated_by = Column(String(255), nullable=True)
"""
Last updater user ID: `String` (limit 255 characters).
"""
last_updated_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Last update timestamp: `BigInteger`.
"""
workspace = Column(
String(63),
nullable=False,
default=DEFAULT_WORKSPACE_NAME,
server_default=sa.text(f"'{DEFAULT_WORKSPACE_NAME}'"),
)
"""
Workspace: `String` (limit 63 characters). Workspace scope for logical isolation.
"""
action_endpoint = relationship(
"SqlGatewayEndpoint",
foreign_keys=[action_endpoint_id],
viewonly=True,
lazy="joined",
)
configs = relationship(
"SqlGatewayGuardrailConfig",
backref="guardrail",
cascade="all, delete-orphan",
)
__table_args__ = (
PrimaryKeyConstraint("guardrail_id", name="guardrails_pk"),
ForeignKeyConstraint(
["scorer_id", "scorer_version"],
["scorer_versions.scorer_id", "scorer_versions.scorer_version"],
name="fk_guardrails_scorer_version",
),
ForeignKeyConstraint(
["action_endpoint_id"],
["endpoints.endpoint_id"],
name="fk_guardrails_action_endpoint_id",
ondelete="SET NULL",
),
Index("idx_guardrails_workspace", "workspace"),
Index("idx_guardrails_scorer", "scorer_id", "scorer_version"),
)
def __repr__(self):
return f"<SqlGatewayGuardrail ({self.guardrail_id})>"
def to_mlflow_entity(self):
return GatewayGuardrail(
guardrail_id=self.guardrail_id,
name=self.name,
scorer=self.scorer_version_ref.to_mlflow_entity(),
stage=GuardrailStage(self.stage),
action=GuardrailAction(self.action),
action_endpoint_name=(self.action_endpoint.name if self.action_endpoint else None),
created_at=self.created_at,
last_updated_at=self.last_updated_at,
created_by=self.created_by,
last_updated_by=self.last_updated_by,
workspace=self.workspace,
)
class SqlGatewayGuardrailConfig(Base):
"""
DB model for guardrail-endpoint associations. These are recorded in
``guardrail_configs`` table. Each row links a guardrail to an endpoint
with an execution order.
"""
__tablename__ = "guardrail_configs"
endpoint_id = Column(String(36), nullable=False)
"""
Endpoint ID: `String` (limit 36 characters). *Composite Primary Key*.
"""
guardrail_id = Column(String(36), nullable=False)
"""
Guardrail ID: `String` (limit 36 characters). *Composite Primary Key*.
"""
execution_order = Column(Integer, nullable=True)
"""
Execution order: `Integer`. Lower values run first. NULL if unspecified.
Not unique in the DB, and uniqueness is guaranteed by the application logic.
"""
created_by = Column(String(255), nullable=True)
"""
Creator user ID: `String` (limit 255 characters).
"""
created_at = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Creation timestamp: `BigInteger`.
"""
workspace = Column(
String(63),
nullable=False,
default=DEFAULT_WORKSPACE_NAME,
server_default=sa.text(f"'{DEFAULT_WORKSPACE_NAME}'"),
)
"""
Workspace: `String` (limit 63 characters). Workspace scope for logical isolation.
"""
__table_args__ = (
PrimaryKeyConstraint("endpoint_id", "guardrail_id", name="guardrail_configs_pk"),
ForeignKeyConstraint(
["endpoint_id"],
["endpoints.endpoint_id"],
name="fk_guardrail_configs_endpoint_id",
ondelete="CASCADE",
),
ForeignKeyConstraint(
["guardrail_id"],
["guardrails.guardrail_id"],
name="fk_guardrail_configs_guardrail_id",
ondelete="CASCADE",
),
Index("idx_guardrail_configs_endpoint_id", "endpoint_id"),
Index("idx_guardrail_configs_guardrail_id", "guardrail_id"),
)
def __repr__(self):
return (
f"<SqlGatewayGuardrailConfig "
f"(endpoint={self.endpoint_id}, guardrail={self.guardrail_id})>"
)
def to_mlflow_entity(self):
return GatewayGuardrailConfig(
endpoint_id=self.endpoint_id,
guardrail_id=self.guardrail_id,
execution_order=self.execution_order,
created_at=self.created_at,
guardrail=self.guardrail.to_mlflow_entity() if self.guardrail else None,
created_by=self.created_by,
workspace=self.workspace,
)
class SqlLabelSchema(Base):
"""
DB model for label schemas.
Schemas are experiment-scoped UI rendering hints; they do not gate
or validate assessment writes. See
``mlflow/genai/label_schemas/label_schemas.py`` for the entity
dataclass and ``mlflow/genai/label_schemas/validation.py`` for the
server-side validation rules.
The schema inherits its workspace from the parent experiment (the
workspace-aware store filters via a join to ``experiments``), so there
is no denormalized ``workspace`` column on this table.
"""
__tablename__ = "label_schemas"
LABEL_SCHEMA_ID_PREFIX = "ls-"
schema_id = Column(String(36), primary_key=True)
"""
Label schema ID: ``String`` (limit 36 characters). *Primary Key* for
``label_schemas`` table.
"""
experiment_id = Column(
Integer,
ForeignKey("experiments.experiment_id", ondelete="CASCADE"),
nullable=False,
)
"""
Experiment ID the schema belongs to. *Foreign Key* into ``experiments``.
Cascade-deletes when the parent experiment is deleted.
"""
name = Column(String(250), nullable=False)
"""
Schema name: ``String`` (limit 250 characters, matching the assessment
key/name limit used elsewhere in the tracking store). Free text shown
to reviewers as the label prompt and used as the assessment key. Unique
within ``experiment_id``.
"""
type = Column(String(16), nullable=False)
"""
Schema type: ``String`` (limit 16). One of ``'feedback'`` or
``'expectation'``. Immutable after create (enforced at update time
by the validation module).
"""
instruction = Column(Text, nullable=True)
"""
Optional detailed instructions: ``Text`` (≤ 1000 chars enforced by
validation, but stored as ``Text`` for flexibility).
"""
enable_comment = Column(Boolean, nullable=False, default=False, server_default="0")
"""
Whether the reviewer widget renders a free-form comment input alongside
the schema-typed value. UI-only hint; not consulted server-side.
"""
input_type = Column(String(32), nullable=False)
"""
Discriminator for the input config payload. One of ``'pass_fail'``,
``'categorical'``, ``'numeric'``, ``'text'`` for tracking-store schemas. The
remaining Databricks-routed types (``'categorical_list'``,
``'text_list'``) are not accepted by the server.
"""
input_config = Column(Text, nullable=False)
"""
JSON payload carrying input-type-specific fields. Shape depends on
``input_type``; see :py:func:`_input_to_dict` / :py:func:`_input_from_dict`
in this module for the round-trip.
"""
created_by = Column(String(255), nullable=True)
"""
User who created the schema.
"""
created_time = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Creation time in milliseconds.
"""
last_update_time = Column(BigInteger, default=get_current_time_millis, nullable=False)
"""
Last update time in milliseconds.
"""
is_default = Column(Boolean, nullable=False, default=False, server_default="0")
"""
Whether this is the experiment's protected default question: server-seeded,
undeletable, and uneditable. At most one row per experiment is ``True``.
"""
__table_args__ = (
PrimaryKeyConstraint("schema_id", name="label_schemas_pk"),
UniqueConstraint("experiment_id", "name", name="uq_label_schemas_exp_name"),
Index("index_label_schemas_experiment_id", "experiment_id"),
)
def to_mlflow_entity(self):
"""Convert DB model to corresponding MLflow entity.
Returns:
:py:class:`mlflow.genai.label_schemas.label_schemas.LabelSchema`.
"""
# Imported here to avoid a circular import at module load time:
# `mlflow.genai.label_schemas.label_schemas` transitively imports
# entities that import this module.
from mlflow.genai.label_schemas.label_schemas import LabelSchema, LabelSchemaType
return LabelSchema(
name=self.name,
type=LabelSchemaType(self.type),
input=_input_from_dict(self.input_type, json.loads(self.input_config)),
instruction=self.instruction,
enable_comment=self.enable_comment,
schema_id=self.schema_id,
experiment_id=str(self.experiment_id),
created_by=self.created_by,
created_at=self.created_time,
updated_at=self.last_update_time,
is_default=self.is_default,
)
@classmethod
def from_mlflow_entity(cls, schema):
"""Create a ``SqlLabelSchema`` from a LabelSchema entity.
The ``experiment_id`` is converted from a string to an int for the
underlying FK; the entity carries it as a string.
Args:
schema: :py:class:`mlflow.genai.label_schemas.label_schemas.LabelSchema`.
"""
input_type, input_config = _input_to_dict(schema.input)
now = get_current_time_millis()
return cls(
schema_id=schema.schema_id,
experiment_id=int(schema.experiment_id),
name=schema.name,
type=str(schema.type),
instruction=schema.instruction,
enable_comment=schema.enable_comment,
input_type=input_type,
input_config=input_config,
created_by=schema.created_by,
created_time=schema.created_at or now,
last_update_time=schema.updated_at or now,
is_default=schema.is_default,
)
class SqlReviewQueue(Base):
"""
DB model for review queues.
A review queue is a named bundle of attached items, questions
(label schemas), and assigned users, scoped to an experiment and
keyed on ``(experiment_id, name)``. See
``mlflow/genai/review_queues/review_queues.py`` for the entity
dataclasses and ``validation.py`` for the validation rules.
The queue inherits its workspace from the parent experiment (the
workspace-aware store filters via a join to ``experiments``, exactly
like ``label_schemas``), so there is no denormalized ``workspace``
column. The three child tables (``review_queue_users``,
``review_queue_items``, ``review_queue_label_schemas``) inherit it
transitively through this table.
"""
__tablename__ = "review_queues"
QUEUE_ID_PREFIX = "rq-"
queue_id = Column(String(36), primary_key=True)
"""
Queue ID: ``String`` (limit 36 characters). *Primary Key* for
``review_queues`` table.
"""
experiment_id = Column(
Integer,
ForeignKey("experiments.experiment_id", ondelete="CASCADE"),
nullable=False,
)
"""
Experiment the queue belongs to. *Foreign Key* into ``experiments``.
Cascade-deletes when the parent experiment is hard-deleted.
"""
name = Column(String(250), nullable=False)
"""
Queue name: ``String`` (limit 250, matching ``label_schemas.name``).
For a user queue this equals the (normalized) user identifier; for a
custom queue it is an arbitrary display name, stored case-preserved.
``'default'`` (the no-auth default user queue) is reserved
case-insensitively (any casing of ``'default'``) and rejected for
custom queues.
"""
name_key = Column(String(250), nullable=False)
"""
Case-folded (lowercased) form of ``name``, carrying the uniqueness
guarantee. Names are unique within ``experiment_id`` case-insensitively,
so ``Foo`` and ``foo`` can't coexist (and a custom queue can't collide
with a user queue's normalized name). ``name`` keeps the display casing;
this column is the identity key. Kept equal to ``name.lower()`` by the
``@validates("name")`` hook, which derives it whenever ``name`` is assigned.
"""
queue_type = Column(String(16), nullable=False)
"""
Queue flavor: ``'user'`` or ``'custom'``. ``String`` (limit 16).
"""
created_by = Column(String(255), nullable=True)
"""
User who created the queue.
"""
creation_time_ms = Column(BigInteger, nullable=False, default=get_current_time_millis)
"""
Queue creation time in milliseconds since epoch.
"""
last_update_time_ms = Column(BigInteger, nullable=False, default=get_current_time_millis)
"""
Time of the most recent change to the queue's own configuration (its
assigned users / attached schemas) in milliseconds since epoch. It does
NOT track attach/detach or per-item status churn in
``review_queue_items`` — those carry their own timestamps — so a "last
activity" view must consult the child rows, not just this field.
"""
__table_args__ = (
PrimaryKeyConstraint("queue_id", name="review_queues_pk"),
UniqueConstraint("experiment_id", "name_key", name="uq_review_queues_experiment_name_key"),
Index("index_review_queues_experiment_id", "experiment_id"),
)
@validates("name")
def _derive_name_key(self, _key, value):
# Keep `name_key` in lockstep with `name` from one place. Uses Python's
# Unicode-aware `.lower()` (the same casefold the rest of the store uses),
# so the key stays consistent across every dialect -- unlike a SQL
# `LOWER()` CHECK, which is ASCII-only on SQLite. This fires on ORM
# attribute assignment (constructor kwargs and `queue.name = ...`); it does
# NOT fire for Core / bulk updates, which this store never uses on `name`.
# `name` is non-nullable and always a validated string, so no None guard.
self.name_key = value.lower()
return value
def __repr__(self):
return (
f"<SqlReviewQueue (id={self.queue_id}, experiment_id={self.experiment_id}, "
f"name={self.name}, type={self.queue_type})>"
)
def to_mlflow_entity(self, *, users=None, schema_ids=None):
"""Convert DB model to corresponding MLflow entity.
``users`` / ``schema_ids`` are the queue's association sets,
loaded separately by the store and passed in (there are no ORM
relationships, so lazy-loading them here is impossible by design).
Returns:
:py:class:`mlflow.genai.review_queues.ReviewQueue`.
"""
# Lazy import: importing `mlflow.genai.review_queues` triggers the
# `mlflow.genai` package init, which can pull this module back in;
# deferring the import avoids that cycle at module load time.
from mlflow.genai.review_queues import ReviewQueue, ReviewQueueType
return ReviewQueue(
queue_id=self.queue_id,
experiment_id=str(self.experiment_id),
name=self.name,
queue_type=ReviewQueueType(self.queue_type),
created_by=self.created_by,
creation_time_ms=self.creation_time_ms,
last_update_time_ms=self.last_update_time_ms,
users=list(users) if users is not None else [],
schema_ids=list(schema_ids) if schema_ids is not None else [],
)
class SqlReviewQueueUser(Base):
"""
DB model for the assigned-user set of a review queue.
One row per ``(queue_id, user)``. The assigned users are a *pool*:
any one of them may work the queue's items. A user queue has exactly
one row (``user == queue.name``); a custom queue has 0..N.
"""
__tablename__ = "review_queue_users"
queue_id = Column(
String(36),
ForeignKey("review_queues.queue_id", ondelete="CASCADE"),
nullable=False,
)
"""
Queue this assignment belongs to. *Foreign Key* into ``review_queues``.
"""
user_id = Column(String(250), nullable=False)
"""
Assigned user identifier (normalized lowercase). ``VARCHAR(250)`` to
mirror ``SqlAssessments.source_id`` so an assigned user can never be
too long to also appear as an assessment ``source_id``. Named
``user_id`` (not ``user``) because ``user`` is a reserved word in
several SQL dialects.
"""
__table_args__ = (
PrimaryKeyConstraint("queue_id", "user_id", name="review_queue_users_pk"),
Index("index_review_queue_users_user_id", "user_id"),
)
def __repr__(self):
return f"<SqlReviewQueueUser (queue_id={self.queue_id}, user_id={self.user_id})>"
class SqlReviewQueueItem(Base):
"""
DB model for an item attached to a review queue + its shared-pool
workflow status.
One row per ``(queue_id, item_id)``. ``status`` is per-``(queue,
item)`` (NOT per-user): an item is addressed when **any** assigned
user completes/declines it, and ``completed_by`` records who. There is
no ``in_progress`` state; status only changes on an explicit reviewer
action, never as a side effect of writing an assessment.
"""
__tablename__ = "review_queue_items"
queue_id = Column(
String(36),
ForeignKey("review_queues.queue_id", ondelete="CASCADE"),
nullable=False,
)
"""
Queue this item is attached to. *Foreign Key* into ``review_queues``.
"""
item_type = Column(String(16), nullable=False)
"""
What kind of object is attached: ``String`` (limit 16). v1 ships
``'trace'`` only; ``'session'`` / ``'span'`` are reserved.
"""
item_id = Column(String(50), nullable=False)
"""
The attached object's id — a trace id today. ``String`` (limit 50).
"""
status = Column(String(16), nullable=False)
"""
Shared-pool workflow status: ``'pending'``, ``'complete'``, or
``'declined'``. ``String`` (limit 16).
"""
completed_by = Column(String(250), nullable=True)
"""
Who completed or declined this item; ``NULL`` while ``pending``.
Same shape as ``review_queue_users.user_id``. Cleared on reopen.
"""
completed_time_ms = Column(BigInteger, nullable=True)
"""
Time the item reached a terminal status in milliseconds since epoch;
``NULL`` while ``pending``. Cleared on reopen.
"""
creation_time_ms = Column(BigInteger, nullable=False, default=get_current_time_millis)
"""
Time the item was attached to the queue in milliseconds since epoch.
"""
last_update_time_ms = Column(BigInteger, nullable=False, default=get_current_time_millis)
"""
Time of the most recent status change in milliseconds since epoch.
Equals ``creation_time_ms`` for an item that is still ``pending``.
"""
__table_args__ = (
PrimaryKeyConstraint("queue_id", "item_id", name="review_queue_items_pk"),
# "Show me this queue's <status> items" — the queue view's status tabs.
Index("index_review_queue_items_queue_id_status", "queue_id", "status"),
# "Which queues is this item in?" — the per-item review widget.
Index("index_review_queue_items_item_id", "item_id"),
)
def __repr__(self):
return (
f"<SqlReviewQueueItem (queue_id={self.queue_id}, item_id={self.item_id}, "
f"status={self.status})>"
)
def to_mlflow_entity(self):
"""Convert DB model to corresponding MLflow entity.
Returns:
:py:class:`mlflow.genai.review_queues.ReviewQueueItem`.
"""
from mlflow.genai.review_queues import ReviewItemType, ReviewQueueItem, ReviewStatus
return ReviewQueueItem(
queue_id=self.queue_id,
item_type=ReviewItemType(self.item_type),
item_id=self.item_id,
status=ReviewStatus(self.status),
creation_time_ms=self.creation_time_ms,
last_update_time_ms=self.last_update_time_ms,
completed_by=self.completed_by,
completed_time_ms=self.completed_time_ms,
)
class SqlReviewQueueLabelSchema(Base):
"""
DB model for the questions (label schemas) attached to a *custom*
review queue.
One row per ``(queue_id, schema_id)``. **User queues store no rows
here** — they resolve to all of the experiment's label schemas at read
time.
"""
__tablename__ = "review_queue_label_schemas"
queue_id = Column(
String(36),
ForeignKey("review_queues.queue_id", ondelete="CASCADE"),
nullable=False,
)
"""
Queue this question belongs to. *Foreign Key* into ``review_queues``.
"""
schema_id = Column(String(36), nullable=False)
"""
The attached label schema's id. Validated against ``label_schemas`` at
write time but intentionally NOT a DB foreign key: a second cascading FK
here (to ``label_schemas``) would converge with the ``queue_id`` ->
``review_queues`` -> ``experiments`` cascade on a single experiment
delete, which MSSQL rejects as a multiple-cascade-path. The reference is
therefore soft (like an assessment's ``name`` -> schema link): a row may
point at a since-deleted schema. The store read path returns the stored ids
as-is (no pruning); orphans are harmless because callers resolve a queue's
schema ids against the experiment's live label schemas, so a missing one is
simply not surfaced. A periodic sweep to physically prune orphans is deferred.
"""
__table_args__ = (
PrimaryKeyConstraint("queue_id", "schema_id", name="review_queue_label_schemas_pk"),
Index("index_review_queue_label_schemas_schema_id", "schema_id"),
)
def __repr__(self):
return f"<SqlReviewQueueLabelSchema (queue_id={self.queue_id}, schema_id={self.schema_id})>"
def _input_to_dict(input_obj) -> tuple[str, str]:
"""Serialize a LabelSchema input dataclass to (discriminator, JSON).
Returns a ``(input_type, input_config)`` pair suitable for direct
insertion into the ``input_type`` and ``input_config`` columns on
``SqlLabelSchema``.
Raises:
ValueError: if ``input_obj`` is not one of the OSS-supported input types.
"""
from mlflow.genai.label_schemas.label_schemas import (
InputCategorical,
InputNumeric,
InputPassFail,
InputText,
)
if isinstance(input_obj, InputPassFail):
config = {
"positive_label": input_obj.positive_label,
"negative_label": input_obj.negative_label,
}
return "pass_fail", json.dumps(config)
if isinstance(input_obj, InputCategorical):
config = {
"options": input_obj.options,
"multi_select": input_obj.multi_select,
}
return "categorical", json.dumps(config)
if isinstance(input_obj, InputNumeric):
config = {
"min_value": input_obj.min_value,
"max_value": input_obj.max_value,
}
return "numeric", json.dumps(config)
if isinstance(input_obj, InputText):
config = {"max_length": input_obj.max_length}
return "text", json.dumps(config)
raise ValueError(
f"Cannot persist label schema input of type {type(input_obj).__name__!r}; "
"OSS-supported types are InputPassFail, InputCategorical, InputNumeric, InputText."
)
def _input_from_dict(input_type: str, config: dict[str, Any]):
"""Reconstruct a LabelSchema input dataclass from a discriminator + dict."""
from mlflow.genai.label_schemas.label_schemas import (
InputCategorical,
InputNumeric,
InputPassFail,
InputText,
)
match input_type:
case "pass_fail":
return InputPassFail(
positive_label=config["positive_label"],
negative_label=config["negative_label"],
)
case "categorical":
return InputCategorical(
options=config["options"],
multi_select=config.get("multi_select", False),
)
case "text":
return InputText(max_length=config.get("max_length"))
case "numeric":
return InputNumeric(
min_value=config.get("min_value"),
max_value=config.get("max_value"),
)
case _:
raise ValueError(
f"Unknown label schema input_type {input_type!r}; expected one of "
"'pass_fail', 'categorical', 'numeric', 'text'."
)