Skip to content

WIP: Database migration CI check#7

Closed
mprahl wants to merge 1 commit into
opendatahub-io:masterfrom
mprahl:workspaces-db-model
Closed

WIP: Database migration CI check#7
mprahl wants to merge 1 commit into
opendatahub-io:masterfrom
mprahl:workspaces-db-model

Conversation

@mprahl

@mprahl mprahl commented Nov 18, 2025

Copy link
Copy Markdown

Summary by CodeRabbit

  • New Features

    • OpenTelemetry tracing improvements (LangChain/LangGraph) and Vercel AI span translation; server-side trace telemetry emitted.
    • Multi-workspace support across Tracking & Model Registry and new scorer permission APIs.
  • Removals

    • Diviner model flavor, examples, and related tests/docs removed.
  • Documentation

    • PyTorch/TensorFlow/Prophet/SparkML guides reorganized and consolidated.
  • Bug Fixes

    • Improved preview comment rendering for long change lists.
  • Chores

    • Bumped default Python patch versions; adjusted Click compatibility and UI support/reporting link.

@github-actions

Copy link
Copy Markdown

@mprahl Thank you for the contribution! Could you fix the following issue(s)?

⚠ Invalid PR template

This PR does not appear to have been filed using the MLflow PR template. Please copy the PR template from here and fill it out.

@coderabbitai

coderabbitai Bot commented Nov 18, 2025

Copy link
Copy Markdown

Walkthrough

Adds workspace multi‑tenancy (DB schema, ORM, migration, tests), removes the Diviner flavor and docs/tests, introduces scorer permissions and telemetry for received traces, adds Vercel AI OTEL translator, repo-aware Python inference, a subprocess lint rule, many test/CI updates, and tracing GetTrace endpoints and store changes. (50 words)

Changes

Cohort / File(s) Summary
Workspace Multi‑tenancy & Migration
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py, mlflow/store/workspace/dbmodels/models.py, mlflow/store/workspace/dbmodels/__init__.py, mlflow/store/model_registry/dbmodels/models.py, mlflow/store/tracking/dbmodels/models.py, mlflow/store/model_registry/sqlalchemy_store.py, mlflow/store/model_registry/file_store.py, mlflow/store/tracking/__init__.py, mlflow/utils/workspace_utils.py, tests/db/*, tests/resources/db/*, tests/store/*
Adds workspace column to multiple tables, composite PK/FK changes, new workspaces table, DEFAULT_WORKSPACE_NAME constant, dialect-aware Alembic upgrade/downgrade, ORM/store updates, and extensive migration/schema tests and SQL fixtures.
Diviner Removal
mlflow/diviner/__init__.py, mlflow/__init__.py, mlflow/models/__init__.py, docs/api_reference/source/python_api/mlflow.diviner.rst, docs/docs/classic-ml/model/index.mdx, examples/diviner/*, requirements/extra-ml-requirements.txt, tests/diviner/*, tests/examples/test_examples.py, tests/pyfunc/docker/test_docker_flavors.py
Removes mlflow.diviner module and API surface, deletes Diviner docs, examples, requirements, and related tests/fixtures; removes Diviner from public exports and test resource lists.
Scorer Permissions & Auth
mlflow/server/auth/__init__.py, mlflow/server/auth/client.py, mlflow/server/auth/routes.py, mlflow/server/auth/db/models.py, mlflow/server/auth/entities.py, mlflow/server/auth/sqlalchemy_store.py, mlflow/server/auth/db/migrations/versions/0965eb92f5f0_add_scorer_permissions.py, tests/server/auth/*
Adds ScorerPermission entity, DB model and migration, REST routes, store methods, client APIs, permission validators/handlers, and test coverage for scorer permission CRUD and access control.
Telemetry & OTEL Tracing Enhancements
mlflow/server/otel_api.py, mlflow/telemetry/events.py, mlflow/tracing/otel/translation/vercel_ai.py, mlflow/tracing/otel/translation/__init__.py, mlflow/tracing/otel/translation/*, tests/tracing/*, mlflow/tracking/request_header/default_request_header_provider.py
Emits TracesReceivedByServerEvent with TraceSource detection (via user-agent), adds TraceSource/TracesReceivedByServerEvent, introduces VercelAITranslator and sanitize_attributes, updates client user-agent prefix, and adds related tests.
Tracing GetTrace API and Store Support
mlflow/server/handlers.py, mlflow/store/tracking/abstract_store.py, mlflow/store/tracking/sqlalchemy_store.py, mlflow/store/tracking/rest_store.py, mlflow/store/tracking/databricks_rest_store.py, tests/tracking/*, tests/store/tracking/*
Adds GetTrace handler/endpoint and store-level get_trace/batch_get_traces support, optional allow_partial semantics, helpers to assemble traces from stored spans, and tests for REST and handler behavior.
Vercel AI Translator Tests
tests/tracing/otel/test_vercel_ai_translator.py, tests/tracing/otel/test_span_translation.py
Adds unit tests for VercelAITranslator and extends OTEL translation tests (sanitize_attributes, Vercel span kinds).
Repository‑aware Package Versioning
dev/set_matrix.py, mlflow/ml-package-versions.yml, mlflow/ml_package_versions.py
Adds repo URL to PackageInfo, infers requires-python from repo pyproject.toml for DEV versions, propagates repo_url through get_python_version/infer_python_version, and populates many package repo entries.
Clint Linter Rule
dev/clint/src/clint/rules/subprocess_check_call.py, dev/clint/src/clint/rules/__init__.py, dev/clint/src/clint/linter.py, dev/clint/tests/rules/test_subprocess_check_call.py
Adds SubprocessCheckCall rule detecting subprocess.run(..., check=True), exports it in rules package, and integrates unit tests.
Subprocess API Modernization
dev/create_release_tag.py, tests/conftest.py, tests/tracing/conftest.py, tests/pyfunc/docker/conftest.py, tests/utils/test_request_utils.py, tests/mcp/test_mcp.py
Replaces subprocess.run(..., check=True) with subprocess.check_call(...) and updates uses to sys.executable where appropriate in tests/util scripts.
Python Version Bumps & Test Resources
.github/actions/setup-python/action.yml, tests/resources/example_mlflow_1x_sklearn_model/*, tests/resources/example_virtualenv_*/*, various test entrypoints
Updates Python patch versions in CI/actions and test resource files (e.g., 3.10.16 → 3.10.19, 3.11.* updates) and adjusts test assertions to match.
Docs Restructuring & Redirects
docs/docs/classic-ml/*, docs/docusaurus.config.ts, docs/sidebarsClassicML.ts, docs/sidebars.ts, docs/api_reference/api_inventory.txt, docs/docs/genai/*
Reworks multiple Classic-ML pages to component-driven layouts, consolidates redirects, removes Diviner docs, updates GenAI tracing docs to emphasize OpenTelemetry, and adjusts sidebar entries.
Semantic Kernel Compatibility
mlflow/semantic_kernel/tracing_utils.py, tests/semantic_kernel/*
Makes streaming decorator imports optional (try/except), avoiding import errors when decorators absent; relaxes some related tests.
MCP / Bedrock / Gateway
mlflow/gateway/providers/bedrock.py, tests/gateway/providers/test_bedrock.py, mlflow/mcp/server.py
Adds anthropic_version field to Bedrock payloads and tightens Click default handling to avoid emitting UNSET sentinel defaults in schema.
CLI & Server Defaults / Tracking Store Selection
mlflow/cli/__init__.py, mlflow/tracking/_tracking_service/utils.py, mlflow/utils/server_cli_utils.py, mlflow/store/tracking/__init__.py
Changes server/gc CLI --backend-store-uri defaults to None, introduces runtime fallback to computed default tracking URI, adds DEFAULT_TRACKING_URI constant, and adds logic to prefer existing mlruns data when choosing default.
Build / Dependency Constraints & CI
pyproject.toml, requirements/constraints.txt, .github/workflows/master.yml
Removes explicit Click 8.3.0 exclusion from constraints, updates CI workflow (e.g., docker images --digests), and removes pip cache action in a job.
Test Adjustments & Misc Fixes
many tests/*, libs/*, mlflow/R/*
Broad test updates: relaxed assertions, mocking helper switches to with_mocked_bindings, added --env-manager uv flags in R CLI tests, docker image digests in compose, and other expectation/fixture adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant Client as OTLP client
    participant API as mlflow OTEL API
    participant Store as Tracking Store
    participant Telemetry as Telemetry Recorder

    Client->>API: POST /v1/traces (with User-Agent)
    API->>API: process spans -> detect completed traces
    API->>Store: persist spans (batch)
    API->>Telemetry: _record_event(TracesReceivedByServerEvent(source, count))
    Telemetry-->>API: acknowledged
    API-->>Client: 200 OK
Loading
sequenceDiagram
    actor User
    participant CLI as mlflow server CLI
    participant Utils as tracking utils
    participant FS as local mlruns
    participant DB as DEFAULT_TRACKING_URI

    User->>CLI: start server (no backend-store-uri)
    CLI->>Utils: _get_default_tracking_uri()
    Utils->>FS: check mlruns/0/meta.yaml
    alt mlruns exists with data
        FS-->>Utils: found
        Utils-->>CLI: use file-based mlruns
    else
        Utils-->>CLI: use DEFAULT_TRACKING_URI (sqlite:///mlflow.db)
    end
    CLI-->>User: server started with chosen backend_store_uri
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Areas needing extra attention:

  • Workspace migration script, ORM composite PK/FK changes, and downgrade conflict handling across dialects.
  • Model registry/tracking ORM/store updates and their integration tests.
  • Tracing GetTrace API + store trace assembly, allow_partial semantics, and related tests.
  • Scorer permissions end-to-end flow (DB migration, REST, store, client, tests).
  • VercelAI translator JSON unpacking and sanitize_attributes edge cases.
  • Large-scale test and CI updates: Python version bumps, subprocess API replacements, and ensuring no lingering Diviner references.

Poem

🐰 I hopped through schemas, ate a bug,

Workspaces planted snug and snug.
Diviner waved a quiet bye,
Docs tiled up beneath the sky.
Check-calls thump — the tests comply!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is completely empty; no explanation of changes, rationale, testing approach, or documentation updates were provided by the author. Add a comprehensive description following the repository template, including: what changes are proposed, how the PR is tested, whether documentation updates are needed, and appropriate release notes and component classifications.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.80% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'WIP: Database migration CI check' is vague and uses non-descriptive terms ('WIP', 'CI check') that don't convey meaningful information about the changeset's main purpose. Replace vague wording with a clear, specific title that describes the primary change (e.g., 'Add workspace columns and migrations to support multi-tenant database schema' or 'Implement database workspace support with schema migrations').
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mprahl mprahl force-pushed the workspaces-db-model branch from 85861c2 to 10278ca Compare November 18, 2025 21:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (27)
.github/workflows/preview-comment.js (1)

132-151: LGTM! The conditional rendering improves UX.

The implementation correctly branches on the number of changed pages, providing a collapsible section for longer lists and a simpler display for shorter ones. The logic is sound and the markdown/HTML formatting is appropriate for GitHub.

Consider extracting the threshold value as a named constant for better maintainability:

+const MAX_PAGES_BEFORE_COLLAPSE = 5;
+
 function getCommentTemplate({
   commitSha,
   workflowRunLink,
   docsWorkflowRunUrl,
   mainMessage,
   changedPages,
 }) {
   let changedPagesSection = "";

   if (changedPages && changedPages.length > 0) {
     const pageLinks = changedPages.map(({ link, status }) => `- ${link} (${status})`).join("\n");

     // Only collapse if there are more than 5 changed pages
-    if (changedPages.length > 5) {
+    if (changedPages.length > MAX_PAGES_BEFORE_COLLAPSE) {
       changedPagesSection = `

 <details>
 <summary>Changed Pages (${changedPages.length})</summary>

 ${pageLinks}

 </details>
 `;
     } else {
       changedPagesSection = `

 **Changed Pages (${changedPages.length})**

 ${pageLinks}
 `;
     }
   }
tests/gateway/providers/test_bedrock.py (1)

108-168: Anthropic Bedrock fixtures correctly assert anthropic_version, consider de‑duplicating the literal

The added anthropic_version: "bedrock-2023-05-31" fields in the three Anthropic fixtures line up with the adapter behavior and make the _request expectations precise. To avoid future drift between tests and AmazonBedrockAnthropicAdapter, consider extracting this version into a shared constant in mlflow/gateway/providers/bedrock.py and referencing it here instead of repeating the string literal in multiple places.

mlflow/gateway/providers/bedrock.py (2)

19-26: Chat adapter now stamps anthropic_version; consider using a shared constant / defaulting

Adding payload["anthropic_version"] = "bedrock-2023-05-31" in chat_to_model makes the Bedrock Anthropic chat payloads explicit and consistent with completions. To reduce duplication (and keep tests in sync), you might want to promote this string to a module-level constant and reference it here. If you ever plan to allow callers to choose a different version, using payload.setdefault("anthropic_version", <CONST>) would avoid overwriting a caller-specified value.


29-43: Completions adapter anthropic_version addition aligns with tests; same constant/default considerations

The new payload["anthropic_version"] = "bedrock-2023-05-31" in completions_to_model, alongside the existing stop_sequences and max_tokens_to_sample normalization, matches what tests/gateway/providers/test_bedrock.py now expects for Anthropic models. As above, consider extracting the version into a single constant used by both adapter methods and the tests, and optionally switching to setdefault if preserving a user-provided version becomes important later.

mlflow/tracking/fluent.py (1)

407-410: Error message improved but could be clearer.

The change from "active run ID" to "active experiment ID" is a good improvement. However, the phrase "environment run ID" at the end might still be confusing since the actual comparison is between two experiment IDs (the active experiment ID set via set_experiment() vs. the experiment ID of the run specified by the MLFLOW_RUN_ID environment variable).

Consider rewording for even better clarity:

-        f"Cannot start run with ID {existing_run_id} because active experiment ID "
-        "does not match environment run ID. Make sure --experiment-name "
+        f"Cannot start run with ID {existing_run_id} because active experiment ID "
+        "does not match the experiment ID of the run from environment. Make sure --experiment-name "
tests/crewai/test_crewai_autolog.py (2)

118-118: Consider the trade-off of using ANY for tasks_output.

Replacing the explicit task output structure with ANY makes the test more version-tolerant but significantly reduces precision. This could mask regressions in task output format.

If the task output structure varies legitimately across CrewAI versions, consider version-conditional assertions instead of a blanket ANY.


268-268: Significant reduction in test assertion precision.

Multiple span output assertions have been relaxed from checking exact dictionary structures to simply verifying is not None. While this improves test stability across CrewAI versions, it substantially reduces test coverage.

Consider:

  • Documenting why exact assertions aren't feasible across versions
  • Implementing version-conditional assertions for critical output fields where possible
  • Adding complementary integration tests that validate end-to-end output quality

This pattern appears intentional for version tolerance, but be aware it may not catch output structure regressions.

Also applies to: 396-396, 463-463, 473-473, 515-515, 594-594, 718-718, 807-807, 886-886

tests/semantic_kernel/test_semantic_kernel_autolog.py (5)

103-114: LLM span name assertion now supports both naming schemes

Allowing spans[3].name to be either "chat.completions gpt-4o-mini" or "chat gpt-4o-mini" keeps this test stable across instrumentation variants while still tightly checking the actual LLM span.

To avoid repeating this tuple across the file, consider extracting a small constant and using it here and in the other tests that assert the same names, e.g.:

CHAT_SPAN_NAME_CANDIDATES = ("chat.completions gpt-4o-mini", "chat gpt-4o-mini")
-    assert spans[3].name in ("chat.completions gpt-4o-mini", "chat gpt-4o-mini")
+    assert spans[3].name in CHAT_SPAN_NAME_CANDIDATES

319-331: Active-span test shares the same chat span name tuple

This assertion mirrors the earlier one in test_sk_invoke_simple, again allowing both "chat.completions gpt-4o-mini" and "chat gpt-4o-mini".

If you introduce a shared CHAT_SPAN_NAME_CANDIDATES constant as suggested earlier, it would be nice to use it here too to keep these tests in sync:

-    assert spans[4].name in ("chat.completions gpt-4o-mini", "chat gpt-4o-mini")
+    assert spans[4].name in CHAT_SPAN_NAME_CANDIDATES

376-420: Substring-based span_name_pattern works but could become ambiguous later

Switching the parametrization to "chat" / "text" and then matching with:

for span in trace.data.spans:
    if span_name_pattern in span.name:
        target_span = span
        break

makes the test tolerant to changes in the full span name, which is good. The only caveat is future ambiguity if additional spans’ names also contain "chat" or "text".

To future‑proof this a bit, you could optionally narrow candidates using span.span_type (or SpanAttributeKey.SPAN_TYPE) so you always pick the LLM/chat span even if multiple names match, e.g.:

from mlflow.entities import SpanType

for span in trace.data.spans:
    if span_name_pattern == "chat" and span.span_type == SpanType.CHAT_MODEL:
        target_span = span
        break
    if span_name_pattern == "text" and span.span_type == SpanType.LLM:
        target_span = span
        break

Not required right now, but would reduce the chance of flakiness as instrumentation evolves.


432-435: Selecting the chat span via "chat" in s.name is fine; consider using span_type

Using:

child_span = next(s for s in traces[0].data.spans if "chat" in s.name)

is a pragmatic way to handle multiple possible chat span names here, and it’s safe given the current span set. To make it more robust and self‑documenting, you could instead key off SpanType.CHAT_MODEL (imported at the top) which directly represents the semantic role:

-    child_span = next(s for s in traces[0].data.spans if "chat" in s.name)
+    child_span = next(
+        s for s in traces[0].data.spans if s.span_type == SpanType.CHAT_MODEL
+    )

This aligns the test with how spans are typed in mlflow.entities.span.SpanType and avoids relying on name substrings.


468-478: Kernel function-object chat span assertion duplicates allowed name tuple

As in the other tests, accepting both "chat.completions gpt-4o-mini" and "chat gpt-4o-mini" is a good way to accommodate instrumentation differences while still asserting on the correct child span.

If you decide to introduce a shared CHAT_SPAN_NAME_CANDIDATES constant, this assertion can be simplified too:

-    assert chat_span.name in ("chat.completions gpt-4o-mini", "chat gpt-4o-mini")
+    assert chat_span.name in CHAT_SPAN_NAME_CANDIDATES
docs/docs/classic-ml/traditional-ml/prophet/index.mdx (2)

47-98: Add installation and setup instructions.

The documentation jumps directly to code examples without explaining how to install Prophet or enable the MLflow Prophet integration. Consider adding a setup section before "Basic Model Logging" that covers installation steps and any required configuration.

Suggested addition (before line 47):

## Installation

Before using MLflow with Prophet, install both libraries:

\`\`\`bash
pip install prophet mlflow
\`\`\`

Ensure you have the MLflow Prophet flavor available:

\`\`\`bash
pip install mlflow[prophet]
\`\`\`

152-153: Add missing closing section.

The code block starting at line 152 (Optuna import section) appears to have an extra blank line (line 153). While minor, ensure this doesn't break the code example rendering.

This is a nitpick: consider removing the extra blank line at line 153 for cleaner code formatting:

 import optuna
 

-
 def objective(trial, df):
dev/set_matrix.py (2)

368-370: Consider using a proper TOML parser.

The regex-based TOML parsing is fragile and may fail on valid TOML files with:

  • Multiline strings
  • Escaped quotes in values
  • Comments within strings
  • Alternative quote styles

Consider using the tomli library (standard in Python 3.11+) or toml:

+import tomli
+
 def _requires_python_from_repo(repo_url: str) -> str | None:
     ...
     try:
         resp = requests.get(raw_url, timeout=10)
         resp.raise_for_status()
     except requests.HTTPError as e:
         if e.response.status_code == 404:
             print(f"  pyproject.toml not found at {raw_url}", file=sys.stderr)
             return None
         raise
 
-    if match := re.search(r'requires-python\s*=\s*["\']([^"\']+)["\']', resp.text):
-        print(f"  Found requires-python: {match.group(1)}", file=sys.stderr)
-        return match.group(1)
+    try:
+        data = tomli.loads(resp.text)
+        if requires_python := data.get("project", {}).get("requires-python"):
+            print(f"  Found requires-python: {requires_python}", file=sys.stderr)
+            return requires_python
+    except tomli.TOMLDecodeError:
+        print("  Failed to parse pyproject.toml", file=sys.stderr)
+        return None
 
     print("  requires-python field not found in pyproject.toml", file=sys.stderr)
     return None

359-366: Note: Limited exception handling for network errors.

The function only catches HTTPError. Other exceptions like Timeout, ConnectionError, or RequestException will propagate uncaught. This may be intentional for CI failure-fast behavior, but consider whether graceful degradation (returning None) would be more appropriate.

If graceful degradation is preferred:

     try:
         resp = requests.get(raw_url, timeout=10)
         resp.raise_for_status()
-    except requests.HTTPError as e:
+    except requests.RequestException as e:
-        if e.response.status_code == 404:
+        if isinstance(e, requests.HTTPError) and e.response.status_code == 404:
             print(f"  pyproject.toml not found at {raw_url}", file=sys.stderr)
-            return None
+        else:
+            print(f"  Network error fetching pyproject.toml: {e}", file=sys.stderr)
-        raise
+        return None
mlflow/ml-package-versions.yml (1)

1-1067: New repo URLs for dev installs are consistent with existing install_dev commands

Adding package_info.repo for these packages (both non‑GenAI and GenAI) aligns with the existing install_dev commands and uses the same GitHub org/repo paths and subdirectories. That should make any tooling that surfaces source locations or links much more useful with minimal risk.

One thing to keep in mind: using /tree/HEAD means these links always track the default branch, which is great for discoverability but less reproducible if upstream restructures directories. If you ever need stronger determinism for CI tooling that consumes repo, consider pinning to a specific branch, tag, or commit instead.

docs/docs/classic-ml/traditional-ml/sparkml/index.mdx (1)

162-166: Consider adding a version requirement note.

The array_to_vector function was introduced in Spark 3.1. Consider adding a brief note about the Spark version requirement for this example.

Add a note similar to the datasource autologging section:

 ```python
 from mlflow.models import infer_signature
 from pyspark.ml.functions import array_to_vector

+# Note: array_to_vector requires Spark 3.1+
 vector_data = spark.createDataFrame(
docs/docusaurus.config.ts (1)

800-806: Consider consolidating TensorFlow redirects.

For consistency with the PyTorch redirect pattern (lines 757-764), consider combining these two TensorFlow redirect entries into a single entry with both from-paths.

Apply this diff to consolidate the redirects:

-          {
-            to: '/ml/deep-learning/tensorflow',
-            from: ['/deep-learning/tensorflow/guide'],
-          },
-          {
-            to: '/ml/deep-learning/tensorflow',
-            from: ['/deep-learning/tensorflow/quickstart/quickstart_tensorflow'],
-          },
+          {
+            to: '/ml/deep-learning/tensorflow',
+            from: [
+              '/deep-learning/tensorflow/guide',
+              '/ml/deep-learning/tensorflow/guide',
+              '/deep-learning/tensorflow/quickstart/quickstart_tensorflow',
+              '/ml/deep-learning/tensorflow/quickstart/quickstart-tensorflow',
+            ],
+          },
tests/tracing/otel/test_span_translation.py (1)

19-75: Vercel span-kind translation coverage looks correct

The added cases for VercelAITranslator (ai.generateTextSpanType.LLM, ai.toolCallSpanType.TOOL) align with SPAN_KIND_TO_MLFLOW_TYPE and follow the existing JSON‑serialized pattern in this test. Looks good as-is; you could later add an embedding case if you want full SPAN_KIND coverage, but it’s not required here.

mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)

20-223: Upgrade path and dialect-specific unique/FK handling looks solid

The upgrade logic correctly:

  • Normalizes unique detection across dialects (including MSSQL via _fetch_mssql_unique_metadata) and safely drops pre‑existing uniqueness on name before introducing workspace-aware keys.
  • Handles SQLite vs non‑SQLite by pre‑dropping FKs only on non‑SQLite and letting _with_batch take care of FK drops on SQLite, avoiding double-drops.
  • Adds workspace with a consistent server_default 'default' and updates PKs/FKs to include workspace, matching the intended multi-tenant schema.

One optional improvement would be to hoist the "default" workspace name into a module-level constant and reuse it for both the column defaults and the seed insert to avoid drift if the default workspace name ever changes.

tests/db/test_schema.py (1)

2-107: Dialect-aware unique-constraint reattachment is appropriate for schema dumps

The new _reattach_missing_unique_constraints + _get_unique_constraints logic cleanly normalizes unique-constraint reflection quirks across MySQL and MSSQL before generating schema strings. Limiting this to a small, explicit set of constraint names and guarding with duplicates_index for MySQL keeps the behavior safe and targeted, and the warning on missing columns is a good diagnostic. Overall this is a solid way to stabilize dump_schema across dialects.

mlflow/tracing/otel/translation/vercel_ai.py (1)

12-32: Translator behavior looks correct; consider a small note on JSON depth handling

The span kind/type mapping and chat vs non‑chat branching in get_input_value / get_output_value look consistent with the Vercel AI schema and the base OtelSchemaTranslator contract. The _unpack_attributes_with_prefix and tools handling should give you a clean, JSON‑encoded aggregate of ai.prompt.* / ai.response.* attributes for chat spans, while deferring to the base logic for non‑chat spans.

One minor point: _safe_load_json only recurses while the intermediate value is still a string; once it becomes a dict/list, it returns it as‑is. That’s fine for handling double‑encoded JSON strings (your main goal) but it won’t walk nested dicts/lists to decode any inner stringified JSON. If you ever need deeper normalization of nested structures, this is the place to extend, but it’s not required for correctness as written.

Also applies to: 34-48, 50-62, 64-88

mlflow/store/model_registry/dbmodels/models.py (2)

1-1: Simplify server_default for workspace columns

For string defaults you can rely on SQLAlchemy’s own quoting/escaping and drop the sa.text(f"'{DEFAULT_WORKSPACE_NAME}'") wrapper, which also lets you avoid the extra sqlalchemy as sa import.

-    workspace = Column(
-        String(63),
-        nullable=False,
-        server_default=sa.text(f"'{DEFAULT_WORKSPACE_NAME}'"),
-    )
+    workspace = Column(
+        String(63),
+        nullable=False,
+        server_default=DEFAULT_WORKSPACE_NAME,
+    )

(Same pattern applies to the other workspace columns in this file.)

Also applies to: 42-46, 87-91, 156-160, 193-197, 232-236, 296-300


173-180: Consider reordering tag PK columns to match likely access patterns

Both tag tables currently use PKs prefixed by (workspace, key, ...); given the pre-workspace keys were effectively (name[, version], key), it’s probably more efficient and intuitive to keep (workspace, name[, version], key) so the index prefix matches typical lookups by model/version identity.

-        PrimaryKeyConstraint("workspace", "key", "name", name="registered_model_tag_pk"),
+        PrimaryKeyConstraint("workspace", "name", "key", name="registered_model_tag_pk"),
-        PrimaryKeyConstraint("workspace", "key", "name", "version", name="model_version_tag_pk"),
+        PrimaryKeyConstraint("workspace", "name", "version", "key", name="model_version_tag_pk"),

If you make this change, remember to update the migrations and test schemas (e.g., sqlite.sql) to match.

Also applies to: 207-214

tests/db/schemas/sqlite.sql (2)

106-110: Optional: add FKs from workspace columns to workspaces

Right now workspaces is a standalone catalog; if you want stronger integrity guarantees, consider adding foreign keys from columns like experiments.workspace and registered_models.workspace to workspaces(name) to prevent dangling workspace names. If intentional, no change needed.


218-221: Revisit PK column order for tag tables in sqlite schema

The PKs for registered_model_tags and model_version_tags are (workspace, key, name[, version]). To better preserve the original (name[, version], key) ordering and index prefix, consider switching to (workspace, name[, version], key), and keep this in sync with the ORM definitions.

-	CONSTRAINT registered_model_tag_pk PRIMARY KEY (workspace, key, name),
+	CONSTRAINT registered_model_tag_pk PRIMARY KEY (workspace, name, key),
-	CONSTRAINT model_version_tag_pk PRIMARY KEY (workspace, key, name, version),
+	CONSTRAINT model_version_tag_pk PRIMARY KEY (workspace, name, version, key),

Also applies to: 372-375

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64e138e and 10278ca.

⛔ Files ignored due to path filters (3)
  • docs/static/images/traditional-ml-ui.gif is excluded by !**/*.gif
  • mlflow/server/js/nohup.out is excluded by !**/*.out
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (86)
  • .github/actions/setup-python/action.yml (1 hunks)
  • .github/workflows/master.yml (0 hunks)
  • .github/workflows/preview-comment.js (2 hunks)
  • dev/clint/src/clint/linter.py (1 hunks)
  • dev/clint/src/clint/rules/__init__.py (2 hunks)
  • dev/clint/src/clint/rules/subprocess_check_call.py (1 hunks)
  • dev/clint/tests/rules/test_subprocess_check_call.py (1 hunks)
  • dev/create_release_tag.py (1 hunks)
  • dev/set_matrix.py (5 hunks)
  • docs/api_reference/api_inventory.txt (0 hunks)
  • docs/api_reference/source/python_api/mlflow.diviner.rst (0 hunks)
  • docs/docs/classic-ml/deep-learning/pytorch/guide/index.mdx (0 hunks)
  • docs/docs/classic-ml/deep-learning/pytorch/index.mdx (0 hunks)
  • docs/docs/classic-ml/deep-learning/pytorch/quickstart/quickstart-pytorch.ipynb (0 hunks)
  • docs/docs/classic-ml/model/index.mdx (0 hunks)
  • docs/docs/classic-ml/traditional-ml/index.mdx (2 hunks)
  • docs/docs/classic-ml/traditional-ml/prophet/guide/index.mdx (0 hunks)
  • docs/docs/classic-ml/traditional-ml/prophet/index.mdx (2 hunks)
  • docs/docs/classic-ml/traditional-ml/sparkml/guide/index.mdx (0 hunks)
  • docs/docs/classic-ml/traditional-ml/sparkml/index.mdx (1 hunks)
  • docs/docs/classic-ml/tutorials-and-examples/index.mdx (0 hunks)
  • docs/docs/genai/tracing/integrations/listing/langchain.mdx (6 hunks)
  • docs/docs/genai/tracing/integrations/listing/langgraph.mdx (6 hunks)
  • docs/docusaurus.config.ts (3 hunks)
  • docs/sidebarsClassicML.ts (2 hunks)
  • examples/diviner/MLproject (0 hunks)
  • examples/diviner/python_env.yaml (0 hunks)
  • examples/diviner/train.py (0 hunks)
  • mlflow/__init__.py (0 hunks)
  • mlflow/diviner/__init__.py (0 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/gateway/providers/bedrock.py (2 hunks)
  • mlflow/mcp/server.py (1 hunks)
  • mlflow/ml-package-versions.yml (30 hunks)
  • mlflow/ml_package_versions.py (1 hunks)
  • mlflow/models/__init__.py (0 hunks)
  • mlflow/semantic_kernel/tracing_utils.py (1 hunks)
  • mlflow/server/js/src/common/constants.tsx (1 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/telemetry/constant.py (0 hunks)
  • mlflow/tracing/otel/translation/__init__.py (2 hunks)
  • mlflow/tracing/otel/translation/vercel_ai.py (1 hunks)
  • mlflow/tracking/fluent.py (1 hunks)
  • mlflow/types/utils.py (0 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • pyproject.toml (0 hunks)
  • requirements/constraints.txt (0 hunks)
  • requirements/extra-ml-requirements.txt (0 hunks)
  • tests/check_mlflow_lazily_imports_ml_packages.py (0 hunks)
  • tests/conftest.py (2 hunks)
  • tests/crewai/test_crewai_autolog.py (16 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/schemas/mssql.sql (6 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
  • tests/diviner/test_diviner_model_export.py (0 hunks)
  • tests/examples/test_examples.py (0 hunks)
  • tests/gateway/providers/test_bedrock.py (3 hunks)
  • tests/mcp/test_mcp.py (2 hunks)
  • tests/pyfunc/docker/conftest.py (1 hunks)
  • tests/pyfunc/docker/test_docker_flavors.py (0 hunks)
  • tests/resources/example_mlflow_1x_sklearn_model/MLmodel (1 hunks)
  • tests/resources/example_mlflow_1x_sklearn_model/conda.yaml (1 hunks)
  • tests/resources/example_mlflow_1x_sklearn_model/python_env.yaml (1 hunks)
  • tests/resources/example_virtualenv_conda_project/conda.yaml (1 hunks)
  • tests/resources/example_virtualenv_conda_project/entrypoint.py (1 hunks)
  • tests/resources/example_virtualenv_no_python_env/entrypoint.py (1 hunks)
  • tests/resources/example_virtualenv_no_python_env/python_env.yaml (1 hunks)
  • tests/resources/example_virtualenv_project/entrypoint.py (1 hunks)
  • tests/resources/example_virtualenv_project/python_env.yaml (1 hunks)
  • tests/semantic_kernel/test_semantic_kernel_autolog.py (7 hunks)
  • tests/sklearn/test_sklearn_model_export.py (2 hunks)
  • tests/tracing/conftest.py (1 hunks)
  • tests/tracing/otel/test_span_translation.py (2 hunks)
  • tests/tracing/otel/test_vercel_ai_translator.py (1 hunks)
  • tests/tracking/fluent/test_fluent.py (1 hunks)
  • tests/utils/test_request_utils.py (1 hunks)
  • tests/webhooks/test_e2e.py (1 hunks)
💤 Files with no reviewable changes (25)
  • .github/workflows/master.yml
  • mlflow/types/utils.py
  • examples/diviner/MLproject
  • docs/docs/classic-ml/deep-learning/pytorch/index.mdx
  • mlflow/models/init.py
  • mlflow/telemetry/constant.py
  • requirements/extra-ml-requirements.txt
  • docs/api_reference/api_inventory.txt
  • docs/docs/classic-ml/tutorials-and-examples/index.mdx
  • examples/diviner/train.py
  • docs/api_reference/source/python_api/mlflow.diviner.rst
  • docs/docs/classic-ml/traditional-ml/prophet/guide/index.mdx
  • pyproject.toml
  • docs/docs/classic-ml/traditional-ml/sparkml/guide/index.mdx
  • tests/diviner/test_diviner_model_export.py
  • docs/docs/classic-ml/model/index.mdx
  • tests/examples/test_examples.py
  • docs/docs/classic-ml/deep-learning/pytorch/guide/index.mdx
  • mlflow/init.py
  • docs/docs/classic-ml/deep-learning/pytorch/quickstart/quickstart-pytorch.ipynb
  • tests/pyfunc/docker/test_docker_flavors.py
  • tests/check_mlflow_lazily_imports_ml_packages.py
  • mlflow/diviner/init.py
  • requirements/constraints.txt
  • examples/diviner/python_env.yaml
🧰 Additional context used
🧬 Code graph analysis (18)
dev/clint/src/clint/rules/__init__.py (1)
dev/clint/src/clint/rules/subprocess_check_call.py (1)
  • SubprocessCheckCall (7-43)
mlflow/store/workspace/dbmodels/__init__.py (1)
mlflow/store/workspace/dbmodels/models.py (1)
  • SqlWorkspace (10-22)
mlflow/semantic_kernel/tracing_utils.py (1)
mlflow/entities/span.py (1)
  • SpanType (44-63)
mlflow/tracing/otel/translation/__init__.py (1)
mlflow/tracing/otel/translation/vercel_ai.py (1)
  • VercelAITranslator (8-88)
mlflow/entities/__init__.py (1)
mlflow/entities/workspace.py (1)
  • Workspace (9-13)
mlflow/tracing/otel/translation/vercel_ai.py (2)
mlflow/entities/span.py (1)
  • SpanType (44-63)
mlflow/tracing/otel/translation/base.py (1)
  • OtelSchemaTranslator (15-171)
dev/clint/src/clint/rules/subprocess_check_call.py (2)
dev/clint/src/clint/resolver.py (1)
  • Resolver (6-89)
dev/clint/src/clint/rules/base.py (1)
  • Rule (11-31)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
tests/db/test_schema.py (1)
  • _get_unique_constraints (90-107)
tests/tracking/fluent/test_fluent.py (1)
mlflow/exceptions.py (1)
  • MlflowException (67-113)
tests/tracing/otel/test_span_translation.py (2)
mlflow/tracing/otel/translation/vercel_ai.py (1)
  • VercelAITranslator (8-88)
mlflow/entities/span.py (1)
  • SpanType (44-63)
tests/db/test_schema.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
  • _get_unique_constraints (144-159)
tests/tracing/otel/test_vercel_ai_translator.py (1)
mlflow/tracing/otel/translation/__init__.py (1)
  • translate_span_when_storing (35-71)
tests/semantic_kernel/test_semantic_kernel_autolog.py (2)
mlflow/entities/span.py (9)
  • name (131-133)
  • name (796-797)
  • parent_id (146-150)
  • parent_id (812-813)
  • span_id (126-128)
  • span_id (792-793)
  • span_type (168-170)
  • SpanType (44-63)
  • get_attribute (215-225)
tests/semantic_kernel/resources.py (2)
  • _create_and_invoke_text_completion (95-108)
  • _create_and_invoke_chat_completion_direct (123-142)
tests/crewai/test_crewai_autolog.py (2)
dev/set_matrix.py (1)
  • Version (57-79)
mlflow/entities/span.py (1)
  • outputs (163-165)
mlflow/store/workspace/dbmodels/models.py (1)
mlflow/entities/workspace.py (1)
  • Workspace (9-13)
tests/db/test_workspace_migration.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)
  • upgrade (138-421)
  • downgrade (424-678)
dev/clint/src/clint/linter.py (1)
dev/clint/src/clint/rules/subprocess_check_call.py (2)
  • SubprocessCheckCall (7-43)
  • check (15-43)
dev/clint/tests/rules/test_subprocess_check_call.py (3)
dev/clint/src/clint/linter.py (3)
  • Position (52-59)
  • Range (62-95)
  • lint_file (918-959)
dev/clint/src/clint/rules/subprocess_check_call.py (1)
  • SubprocessCheckCall (7-43)
dev/clint/tests/rules/conftest.py (1)
  • index_path (8-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (30)
  • GitHub Check: windows (4)
  • GitHub Check: windows (2)
  • GitHub Check: windows (3)
  • GitHub Check: windows (1)
  • GitHub Check: pyfunc (3)
  • GitHub Check: pyfunc (4)
  • GitHub Check: models (2)
  • GitHub Check: pyfunc (2)
  • GitHub Check: models (1)
  • GitHub Check: pyfunc (1)
  • GitHub Check: evaluate (2)
  • GitHub Check: database
  • GitHub Check: evaluate (1)
  • GitHub Check: java
  • GitHub Check: genai
  • GitHub Check: flavors
  • GitHub Check: python (2)
  • GitHub Check: python (1)
  • GitHub Check: python (3)
  • GitHub Check: python-skinny
  • GitHub Check: build (tracing)
  • GitHub Check: build (dev)
  • GitHub Check: build (skinny)
  • GitHub Check: r
  • GitHub Check: build
  • GitHub Check: core
  • GitHub Check: js (ubuntu-latest, --testPathPattern)
  • GitHub Check: js (windows-latest, --testPathIgnorePatterns)
  • GitHub Check: js (ubuntu-latest, --testPathIgnorePatterns)
  • GitHub Check: js (windows-latest, --testPathPattern)

Comment thread .github/actions/setup-python/action.yml
Comment on lines +82 to 98
# Cross-validation
cv_results = cross_validation(
model, initial="730 days", period="180 days", horizon="365 days"
model,
initial="730 days",
period="180 days",
horizon="365 days",
)

# Log performance metrics
metrics = performance_metrics(cv_results)
avg_metrics = metrics[["mse", "rmse", "mae", "mape"]].mean().to_dict()
mlflow.log_metrics(avg_metrics)
mlflow.log_metrics(metrics[["mse", "rmse", "mae", "mape"]].mean().to_dict())

# Log the model
# Log model
mlflow.prophet.log_model(
pr_model=model, name="prophet_model", input_example=df[["ds"]].head()
)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Clarify required dependencies for code examples.

The Basic Model Logging example imports cross_validation and performance_metrics from prophet.diagnostics and uses mlflow.prophet.log_model(). Ensure the documentation mentions these required packages and that the metric column names ("mse", "rmse", "mae", "mape") are accurate for the current Prophet version.


🌐 Web query:

What are the output column names from Prophet's performance_metrics function in recent versions?

💡 Result:

Typical output columns from Prophet's performance_metrics (recent versions) are:

  • horizon
  • mse
  • rmse
  • mae
  • mape
  • mdape
  • smape
  • coverage

(Which metrics appear can depend on the package/language and the metrics argument; by default the set includes mse, rmse, mae, mape, mdape, smape, and coverage.) [1][2][3]


Need to verify the remaining APIs used in the code example.

Code example APIs verified as correct; add dependency clarification to documentation.

The code correctly uses cross_validation from prophet.diagnostics with parameters initial, period, and horizon. The performance_metrics utility computes mean squared error (MSE), root mean squared error (RMSE), mean absolute error (MAE), mean absolute percent error (MAPE) along with additional metrics, so the four metric names selected are valid. mlflow.prophet.log_model() correctly accepts pr_model and input_example parameters.

The example works as written. To improve clarity, explicitly mention that users need the prophet and mlflow packages for this example (the basic Prophet import example also implicitly requires pandas).

🤖 Prompt for AI Agents
In docs/docs/classic-ml/traditional-ml/prophet/index.mdx around lines 82–98, the
code example works but lacks a dependency note; add a short sentence before or
after the code block stating that the example requires the prophet and mlflow
Python packages (and pandas for DataFrame handling), e.g., mentioning pip
install prophet mlflow pandas or equivalent, so readers know which libraries to
install before running the snippet.

@@ -1,205 +1,276 @@
import Link from "@docusaurus/Link";
import { CardGroup, PageCard } from "@site/src/components/Card";
import { APILink } from "@site/src/components/APILink";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Verify if APILink import is used.

The APILink component is imported but doesn't appear to be used in the document body.

Run the following script to check if this import is used:


🏁 Script executed:

#!/bin/bash
# Check if APILink is used in the SparkML index file
rg -n "APILink" docs/docs/classic-ml/traditional-ml/sparkml/index.mdx

Length of output: 57


Remove unused APILink import.

The APILink component is imported on line 1 but is not used anywhere in the document. Remove the unused import.

🤖 Prompt for AI Agents
In docs/docs/classic-ml/traditional-ml/sparkml/index.mdx around line 1, the
APILink component is imported but never used; remove the unused import line
`import { APILink } from "@site/src/components/APILink";` from the top of the
file to clean up the code.

Comment on lines +230 to +240
```typescript
import { OpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";

const model = new OpenAI("gpt-4o-mini");
const prompt = PromptTemplate.fromTemplate("What is a good name for a company that makes {product}?");
const chain = prompt.pipe({ llm: model });

const res = await chain.invoke({ product: "colorful socks" });
console.log({ res });
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Incorrect TypeScript v0 pipe syntax—likely to fail when users reproduce the example.

Line 236 uses prompt.pipe({ llm: model }), but the correct LangChain.js syntax (v0 and v1) is prompt.pipe(model). The object-wrapped syntax does not match documented patterns and will cause errors at runtime.

Apply this diff to fix the chain definition:

-const chain = prompt.pipe({ llm: model });
+const chain = prompt.pipe(model);

Additionally, Line 234 uses a positional argument for the model instantiation. Ensure this matches the OpenAI constructor API for LangChain 0.x. If the current syntax is valid, consider adding a brief comment for clarity.


🌐 Web query:

LangChain.js pipe syntax prompt.pipe model chainable

💡 Result:

Short answer: LangChain.js uses the Runnable/LCEL pipe API so prompts, models, parsers and other Runables are chainable with .pipe(). The output of each runnable becomes the input of the next, and the resulting RunnableSequence can be invoked, streamed, or further chained.

Example (typical pattern):

  • create a prompt runnable
  • pipe it into a chat model runnable
  • pipe into an output parser (optional)
  • invoke/stream the final chain

Code:
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";

const prompt = ChatPromptTemplate.fromTemplate("Tell me a joke about {topic}");
const model = new ChatOpenAI({});
const chain = prompt.pipe(model).pipe(new StringOutputParser());
const result = await chain.invoke({ topic: "bears" });

Notes:

  • You can bind model kwargs with .bind() before piping (e.g., stop sequences or function schemas). [1][3]
  • Chains support streaming (.stream()) and further composition (RunnablePick, RunnableLambda, RunnablePassthrough, etc.). [1][2]
  • This is part of LangChain’s Expression Language / Runnable API; docs show examples across integrations. [1][4]

Sources:
[1] LangChain docs — “How to chain runnables / The pipe method”.
[2] LangChain docs — “Get started / Prompt + LLM” examples.
[3] LangChain docs — “Prompt + LLM / Attaching kwargs with .bind()”.
[4] @langchain/core API examples (pipe + stream).


🌐 Web query:

LangChain OpenAI constructor positional arguments v0

💡 Result:

Short answer: In LangChain v0 the OpenAI/ChatOpenAI constructors expect keyword arguments (e.g., model/model_name, temperature, max_tokens, api_key/base_url/organization, timeout, max_retries, stream_options, etc.) rather than requiring a fixed set of positional parameters. Example instantiation:

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.0, max_retries=2, api_key="...")

Docs / reference for the constructor parameters: [1], [2].

Sources:
[1] LangChain reference — ChatOpenAI / OpenAI constructor params.
[2] LangChain v0 API docs — OpenAI llm constructor details.


🌐 Web query:

LangChain.js pipe object parameter llm model syntax

💡 Result:

Short answer — pipe accepts a "runnable-like" (an LLM/ChatModel instance or other runnable). Create the LLM with the model name in its constructor, then pass that instance to .pipe. To set per-call kwargs (stop, functions, etc.) use .bind(...) on the model runnable.

Examples:

  • Basic (string LLM / chat model):
    const model = new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0.2 });
    const prompt = PromptTemplate.fromTemplate("Tell me a joke about {topic}");
    const chain = prompt.pipe(model);
    const out = await chain.invoke({ topic: "cats" });

  • Attach call-time options via bind:
    const chain = prompt.pipe(model.bind({ stop: ["\n"], functions: myFunctionSchema }));

  • Use RunnableSequence.from for explicit sequences:
    const chain = RunnableSequence.from([prompt, model, outputParser]);

References: LangChain.js prompt+LLM examples and API docs. [1][2][3]

Sources
[1] Prompt + LLM examples (LangChain docs).
[2] Quickstart / model constructor (LangChain docs).
[3] Runnable / pipe API reference (LangChain.js API).


🌐 Web query:

@langchain/openai OpenAI constructor TypeScript JavaScript parameters

💡 Result:

Short answer — constructor signatures and main params

  • Signatures:

    • new OpenAI(fields?: Partial & BaseLLMParams & { configuration?: ClientOptions & LegacyOpenAIInput }) → OpenAI. (v02.api.js.langchain.com)
    • (older/alternate docs) new OpenAI(fields?: Partial & BaseLLMParams & { configuration?: ClientOptions }) → OpenAI. (v03.api.js.langchain.com)
  • Common fields you can pass (in the fields object)

  • configuration (ClientOptions) — passed either as fields.configuration or as a second optional arg; accepts official OpenAI SDK ClientOptions such as:

    • baseURL, defaultHeaders (or custom headers), other ClientOptions accepted by openai SDK. Useful for proxies/custom endpoints or Azure. (v02.api.js.langchain.com)

Example (JS/TS):

  • new OpenAI({ model: "gpt-3.5-turbo", temperature: 0.7, apiKey: process.env.OPENAI_API_KEY, configuration: { baseURL: "https://proxy.example.com", defaultHeaders: { Authorization: "Bearer X" } } }); (docs.langchain.com)

If you want, I can paste the exact TypeScript types (OpenAIInput, BaseLLMParams, ClientOptions) from the repo/docs.

Citations:


Fix two syntax errors in the TypeScript v0 LangChain example that will cause runtime failures.

Line 236 uses incorrect pipe syntax: prompt.pipe({ llm: model }). The correct LangChain.js pattern is prompt.pipe(model) where the LLM instance is passed directly to pipe.

Line 234 also has a syntax error: new OpenAI("gpt-4o-mini"). The OpenAI constructor requires an object parameter with fields like model, temperature, and other configuration options, not a positional string argument.

Apply these diffs:

-const model = new OpenAI("gpt-4o-mini");
+const model = new OpenAI({ model: "gpt-4o-mini" });
const prompt = PromptTemplate.fromTemplate("What is a good name for a company that makes {product}?");
-const chain = prompt.pipe({ llm: model });
+const chain = prompt.pipe(model);
🤖 Prompt for AI Agents
In docs/docs/genai/tracing/integrations/listing/langchain.mdx around lines 230
to 240, the TypeScript v0 LangChain example has two syntax errors: the OpenAI
constructor is called with a positional string and should be called with an
options object (e.g., new OpenAI({ model: "gpt-4o-mini", ... })) and the pipe
call incorrectly passes an object; change prompt.pipe({ llm: model }) to
prompt.pipe(model) so the LLM instance is passed directly. Ensure any additional
OpenAI options (temperature, apiKey, etc.) are provided in the options object as
needed.

Comment thread mlflow/mcp/server.py
Comment on lines +44 to +49
if p.default is not None and (
# In click >= 8.3.0, the default value is set to `Sentinel.UNSET` when no default is
# provided. Skip setting the default in this case.
# See https://github.com/pallets/click/pull/3030 for more details.
not isinstance(p.default, str) and repr(p.default) != "Sentinel.UNSET"
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Critical logic error: legitimate string defaults are excluded.

The condition not isinstance(p.default, str) and repr(p.default) != "Sentinel.UNSET" will exclude all string defaults from the JSON schema because if p.default is a string, not isinstance(p.default, str) evaluates to False, making the entire expression False.

Since Sentinel.UNSET is a sentinel object (not a string), string defaults are always legitimate and should be included in the schema. The correct logic should include defaults that are either strings or non-sentinel values.

Apply this diff to fix the logic:

-        if p.default is not None and (
-            # In click >= 8.3.0, the default value is set to `Sentinel.UNSET` when no default is
-            # provided. Skip setting the default in this case.
-            # See https://github.com/pallets/click/pull/3030 for more details.
-            not isinstance(p.default, str) and repr(p.default) != "Sentinel.UNSET"
-        ):
+        if p.default is not None and (
+            # In click >= 8.3.0, the default value is set to `Sentinel.UNSET` when no default is
+            # provided. Skip setting the default in this case.
+            # See https://github.com/pallets/click/pull/3030 for more details.
+            isinstance(p.default, str) or repr(p.default) != "Sentinel.UNSET"
+        ):
             schema["default"] = p.default

Verify whether any Click commands in traces_cli or scorers_cli have string defaults that would be affected:


🏁 Script executed:

#!/bin/bash
# Description: Search for Click options/arguments with string defaults in CLI commands

# Search for default string values in Click decorators
rg -n -A 2 -B 2 '@click\.(option|argument)' mlflow/cli/traces/commands.py mlflow/cli/scorers/commands.py | rg -C 2 'default\s*=\s*["\']'

Length of output: 68


Let me regenerate the verification script with corrected syntax:


🏁 Script executed:

#!/bin/bash
# Search for Click options/arguments with string defaults in mlflow/cli
rg -n -B 2 -A 2 "default\s*=\s*['\"]" mlflow/cli

Length of output: 1604


🏁 Script executed:

#!/bin/bash
# Also check the structure of the CLI directory
fd -e py . mlflow/cli | head -20

Length of output: 115


Let me check if the MCP server actually processes these CLI commands with string defaults:


🏁 Script executed:

#!/bin/bash
# Search for imports of traces or scorers CLI in mcp/server.py
rg -n "traces|scorers" mlflow/mcp/server.py

Length of output: 277


🏁 Script executed:

#!/bin/bash
# Check what functions/modules are imported from mlflow.cli in mcp/server.py
rg -n -A 5 "from mlflow.cli|import.*cli" mlflow/mcp/server.py

Length of output: 379


Critical logic error confirmed: legitimate string defaults are excluded from MCP tool schemas.

The condition not isinstance(p.default, str) and repr(p.default) != "Sentinel.UNSET" excludes all string defaults because if p.default is a string, not isinstance(p.default, str) is False, making the entire expression False.

This affects real commands: mlflow/cli/traces.py and mlflow/cli/scorers.py both have string defaults ("table", "main", "local") that are processed by cmd_to_function_tool() at lines 121-122 of mlflow/mcp/server.py. These defaults will be omitted from the generated MCP tool schemas.

Apply this diff:

-        if p.default is not None and (
-            # In click >= 8.3.0, the default value is set to `Sentinel.UNSET` when no default is
-            # provided. Skip setting the default in this case.
-            # See https://github.com/pallets/click/pull/3030 for more details.
-            not isinstance(p.default, str) and repr(p.default) != "Sentinel.UNSET"
-        ):
+        if p.default is not None and (
+            # In click >= 8.3.0, the default value is set to `Sentinel.UNSET` when no default is
+            # provided. Skip setting the default in this case.
+            # See https://github.com/pallets/click/pull/3030 for more details.
+            isinstance(p.default, str) or repr(p.default) != "Sentinel.UNSET"
+        ):
             schema["default"] = p.default
🤖 Prompt for AI Agents
In mlflow/mcp/server.py around lines 44-49, the conditional that checks whether
to include parameter defaults wrongly excludes legitimate string defaults due to
the `not isinstance(p.default, str) and` clause; remove that clause so the check
only skips Click's Sentinel.UNSET. Change the condition to: if p.default is not
None and repr(p.default) != "Sentinel.UNSET": so string defaults like "table",
"main", "local" are preserved in generated MCP tool schemas.

Comment on lines +30 to +39
def llm():
# NB: CrewAI >= 1.0.0 introduced native LLM connectors that don't rely on LiteLLM. To use
# consistent mock between 1.x and 0.x, we opt-in to use LiteLLM for 1.x.
if _IS_CREWAI_V1:
from crewai import LLM

return LLM(model="openai/gpt-4o-mini", is_litellm=True)
else:
return "openai/gpt-4o-mini"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify CrewAI v1 LLM API compatibility.

The version-aware LLM helper correctly differentiates between CrewAI v1 and earlier versions. However, ensure the is_litellm=True parameter is the correct way to opt into LiteLLM for CrewAI v1+.


🌐 Web query:

CrewAI version 1 LLM class is_litellm parameter documentation

💡 Result:

Short answer: There is no public, formal “is_litellm” parameter documented for CrewAI v1’s LLM class — it appears to be an internal flag injected when CrewAI calls litellm and is not part of the documented public API. Users have reported it showing up in litellm requests (and causing errors with some providers such as Groq). Workarounds: strip the key from litellm calls (monkey‑patch litellm.completion) or implement a custom BaseLLM to avoid the litellm path. References: community report showing the Groq error and a sample monkey‑patch, plus CrewAI docs on custom LLMs and LLM connections. [1][2][3]

Sources

  • Community report and example monkey‑patch for removing "is_litellm". [1]
  • CrewAI docs: Custom LLM implementation (BaseLLM). [2]
  • CrewAI docs: Connect to any LLM / LiteLLM integration. [3]

Do not use undocumented is_litellm parameter; implement custom BaseLLM instead.

The is_litellm=True parameter is not part of CrewAI v1's documented public API and is an internal flag. It's known to cause errors with certain providers (e.g., Groq). Per CrewAI docs, use a custom BaseLLM implementation or the recommended LiteLLM integration approach for reliable, supported behavior.

🤖 Prompt for AI Agents
In tests/crewai/test_crewai_autolog.py around lines 30 to 39, replace the
undocumented use of the internal is_litellm=True flag with a small custom
BaseLLM subclass that implements the minimal public BaseLLM API and delegates to
the desired model ("openai/gpt-4o-mini") or integrates with LiteLLM via the
documented extension points; remove the is_litellm parameter entirely,
instantiate and return your custom BaseLLM implementation when _IS_CREWAI_V1 is
true, and for older versions keep returning the string model name so tests use a
consistent, supported mock.

@mprahl mprahl force-pushed the workspaces-db-model branch from 10278ca to b933f52 Compare November 18, 2025 23:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
tests/db/test_schema.py (1)

2-2: Dialect‑aware unique‑constraint reattachment looks correct; consider sharing helper logic

The _reattach_missing_unique_constraints() / _get_unique_constraints() combination correctly compensates for MySQL and MSSQL reflection quirks and is scoped to the expected constraint name set, so it shouldn’t disturb other tables. The call from dump_schema() is also in the right place (after reflection, before DDL generation). One improvement you might consider is de‑duplicating this reflection logic with the similar helpers in the Alembic migration module so future constraint additions stay in sync.

Also applies to: 8-16, 32-32, 46-88, 90-107

tests/db/check_migration.py (1)

52-61: Workspace backfill checks are solid; table-name list could be centralized

The added WORKSPACE_TABLES plus the SELECT DISTINCT workspace ... assertions in post_migration() give good coverage that all affected tables are fully backfilled to the "default" workspace. To avoid drift with future workspace-enabled tables, you might later derive this set from a shared constant (e.g., the migration module or ORM models) instead of hard-coding the names here.

Also applies to: 69-96, 129-132

mlflow/store/model_registry/sqlalchemy_store.py (1)

64-64: Registry now writes workspace correctly; future multi-workspace support will need query changes

The changes to use DEFAULT_WORKSPACE_NAME when creating registered models, model versions, tags, and aliases (including the session.merge(...) upserts) are necessary and sufficient to satisfy the new composite PK/FK schemas while you still operate in a single default workspace. All write paths now populate the workspace column consistently.

Read/lookup paths (_get_registered_model*, _get_model_version*, tag and alias getters, search queries) still filter only on name / version, which is fine as long as the only workspace in play is the default one. Once multi-workspace behavior is surfaced at the store or API level, you’ll need to thread workspace context through these queries and joins to avoid ambiguity across workspaces.

Also applies to: 172-207, 669-707, 730-821, 1186-1212, 1237-1270

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 10278ca and b933f52.

📒 Files selected for processing (17)
  • docs/api_reference/api_inventory.txt (2 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/model_registry/sqlalchemy_store.py (6 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/schemas/mssql.sql (6 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • mlflow/entities/init.py
  • mlflow/utils/workspace_utils.py
  • mlflow/entities/workspace.py
  • mlflow/store/workspace/dbmodels/init.py
  • tests/db/schemas/mysql.sql
🧰 Additional context used
🧬 Code graph analysis (7)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
tests/db/test_schema.py (1)
  • _get_unique_constraints (90-107)
tests/db/check_migration.py (1)
mlflow/store/model_registry/sqlalchemy_store.py (1)
  • create_registered_model (172-214)
mlflow/store/workspace/dbmodels/models.py (1)
mlflow/entities/workspace.py (1)
  • Workspace (9-13)
mlflow/store/model_registry/sqlalchemy_store.py (1)
mlflow/store/model_registry/dbmodels/models.py (3)
  • SqlRegisteredModelTag (155-190)
  • SqlModelVersionTag (193-230)
  • SqlRegisteredModelAlias (233-268)
tests/db/test_schema.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
  • _get_unique_constraints (144-159)
tests/db/test_workspace_migration.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)
  • upgrade (138-421)
  • downgrade (424-678)
mlflow/store/model_registry/dbmodels/models.py (1)
mlflow/entities/model_registry/registered_model.py (4)
  • name (44-46)
  • name (49-50)
  • description (69-71)
  • description (74-75)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (34)
  • GitHub Check: lint
  • GitHub Check: typescript-sdk (20)
  • GitHub Check: typescript-sdk (24)
  • GitHub Check: typescript-sdk (22)
  • GitHub Check: js (ubuntu-latest, --testPathPattern)
  • GitHub Check: js (windows-latest, --testPathPattern)
  • GitHub Check: js (windows-latest, --testPathIgnorePatterns)
  • GitHub Check: js (ubuntu-latest, --testPathIgnorePatterns)
  • GitHub Check: windows (3)
  • GitHub Check: pyfunc (3)
  • GitHub Check: windows (4)
  • GitHub Check: pyfunc (4)
  • GitHub Check: database
  • GitHub Check: pyfunc (2)
  • GitHub Check: pyfunc (1)
  • GitHub Check: windows (1)
  • GitHub Check: windows (2)
  • GitHub Check: java
  • GitHub Check: python (3)
  • GitHub Check: python-skinny
  • GitHub Check: evaluate (1)
  • GitHub Check: python (2)
  • GitHub Check: evaluate (2)
  • GitHub Check: python (1)
  • GitHub Check: flavors
  • GitHub Check: models (1)
  • GitHub Check: models (2)
  • GitHub Check: genai
  • GitHub Check: core
  • GitHub Check: build (tracing)
  • GitHub Check: build (skinny)
  • GitHub Check: build (dev)
  • GitHub Check: r
  • GitHub Check: build
🔇 Additional comments (23)
docs/api_reference/api_inventory.txt (1)

531-531: Workspace entries correctly added to API inventory

Both mlflow.entities.Workspace and mlflow.entities.workspace.Workspace are listed consistently with other entities; this keeps the public surface in sync with the new model.

Also applies to: 631-631

tests/db/schemas/mssql.sql (1)

29-31: MSSQL schema workspace changes align with the migration

The added workspace columns, composite PKs/FKs, uq_experiments_workspace_name, and the new workspaces table all match the shapes introduced in the Alembic migration and ORM models. This should keep test_schema_is_up_to_date consistent for MSSQL after running the migration.

Also applies to: 41-44, 85-87, 100-101, 105-109, 195-198, 205-208, 215-218, 364-367

mlflow/store/workspace/dbmodels/models.py (1)

1-22: SqlWorkspace ORM mapping matches the new catalog table

The SqlWorkspace model correctly mirrors the workspaces table (PK on name, optional description) and cleanly converts to the Workspace entity via to_mlflow_entity. Looks good.

tests/db/schemas/postgresql.sql (1)

29-31: PostgreSQL schema workspace DDL matches the migration design

Workspace columns, composite primary keys / foreign keys, uq_experiments_workspace_name, and the workspaces table definition are all consistent with the Alembic upgrade logic and ORM models, so the reflected schema should continue to match this file.

Also applies to: 41-44, 86-88, 101-102, 106-110, 198-201, 208-211, 218-221, 370-373

mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)

20-25: Upgrade path and MSSQL unique-introspection helpers look sound

The migration’s helper stack (_NAMING_CONVENTION, _WORKSPACE_TABLES, _SQLITE_LEGACY_FKS, _fetch_mssql_unique_metadata, and the _get_unique_constraints / _get_unique_indexes / _detect_unique_on_name / _collect_foreign_keys trio) is thoughtfully put together: it handles the lack of get_unique_constraints() on MSSQL, the MySQL “unique-as-index” pattern, and unnamed SQLite FKs without overreaching into other dialects. The upgrade steps then cleanly:

  • Drop pre-existing uniqueness on name for experiments / registered_models,
  • Add non-null workspace with default to the 8 tables,
  • Rebuild PKs/FKs to use (workspace, …) keys, and
  • Create the required workspace indexes and the seeded workspaces catalog.

Given the amount of dialect-specific introspection, it’s worth running this migration against all four supported backends to confirm inspector behavior matches assumptions.

Also applies to: 26-61, 64-134, 136-238


424-678: Downgrade safety checks are careful; evaluation_datasets conflict check is slightly conservative

The downgrade logic is well-structured: it (1) reflects tables once and caches them, (2) asserts there are no conflicting rows when collapsing workspaces back to a single global space, (3) bulk-updates all workspace columns to "default", (4) tears down workspace-specific FKs/PKs/indexes/columns in a dialect-aware way (including batched SQLite ops), and (5) restores pre-workspace PKs/FKs plus a uq_experiments_name constraint before dropping workspaces. The only mildly conservative bit is treating collisions on evaluation_datasets.name as a hard blocker even though that table’s PK never changes, but that only affects downgrade scenarios and errs on the side of safety, so it’s acceptable.

Please sanity-check downgrade by applying upgrade + downgrade on a test DB with some synthetic data across the impacted tables for all dialects you care about.

mlflow/store/tracking/dbmodels/models.py (4)

64-64: LGTM: Workspace column properly configured.

The workspace column is correctly defined with:

  • Appropriate length (63 characters)
  • Non-nullable constraint with proper defaults
  • Server-side default for database-level consistency

Also applies to: 103-111


98-102: LGTM: Uniqueness correctly scoped to workspace.

The unique constraint has been properly updated from a single-column constraint on name to a composite constraint on (workspace, name), ensuring experiment names are unique per workspace.

Also applies to: 137-137


1315-1323: LGTM: Workspace support added consistently.

The workspace column and index for SqlEvaluationDataset follow the same pattern as SqlExperiment, maintaining consistency across the codebase.

Also applies to: 1379-1379


143-158: Verify workspace handling in entity layer.

The to_mlflow_entity() methods don't propagate the workspace field to the returned Experiment and EvaluationDataset entities. Please confirm this is intentional and that workspace context is managed at the store or API layer rather than in the entity objects themselves.

Also applies to: 1382-1419

tests/db/test_workspace_migration.py (5)

10-21: LGTM: Test setup properly isolated.

The database preparation creates a fresh SQLite database for each test using tmp_path, ensuring test isolation. Migration to PREVIOUS_REVISION correctly establishes the pre-workspace state.


24-232: LGTM: Pre-migration seeding correctly uses legacy schema.

The deliberate use of raw SQL to seed data without workspace columns is the right approach for testing the migration. The comment clearly explains this design decision.


242-579: LGTM: Post-migration helpers properly workspace-aware.

The helper functions correctly require workspace parameters and use the post-migration schema, providing good test utilities for scenarios after the migration is applied.


590-719: LGTM: Comprehensive upgrade validation.

The upgrade test thoroughly validates:

  • Workspace column presence and defaults
  • Data backfill to 'default' workspace
  • Updated primary keys and constraints
  • New indexes

This provides strong confidence in the migration's correctness.


722-941: LGTM: Thorough downgrade and conflict testing.

The downgrade validation ensures schema correctly reverts to pre-workspace state. The parameterized conflict detection tests comprehensively cover different resource types that could have name collisions when merging workspaces, with proper error message validation.

tests/db/schemas/sqlite.sql (4)

29-29: LGTM: Workspaces catalog and dataset scoping properly defined.

The new workspaces table provides a central catalog, and the workspace column in evaluation_datasets follows the established pattern.

Also applies to: 106-110


41-44: LGTM: Experiment uniqueness correctly scoped.

The unique constraint change from name alone to (workspace, name) enables workspace-scoped experiment names.


86-87: LGTM: Model registry tables consistently workspace-scoped.

All model registry tables have been properly updated with:

  • Workspace columns with appropriate defaults
  • Composite primary keys with workspace as the leading column
  • Foreign keys correctly referencing the new composite keys
  • Appropriate CASCADE options for referential integrity

Also applies to: 198-200, 208-210, 218-220, 372-374


101-102: Verify webhook primary key design.

The webhooks table adds a workspace column but retains webhook_id as the sole primary key (unlike registered_models which uses a composite PK). Confirm this is intentional—likely because webhook_id is globally unique across all workspaces.

mlflow/store/model_registry/dbmodels/models.py (4)

1-1: LGTM: Workspace support properly integrated into SqlRegisteredModel.

The workspace column and composite primary key changes are consistent with the tracking models, establishing workspace-scoped registered models.

Also applies to: 36-36, 42-57


88-93: LGTM: Model version properly linked to workspace-scoped registered models.

The foreign key constraint correctly references the composite key (workspace, name) in registered_models with onupdate="cascade" to maintain referential integrity.

Also applies to: 127-133


196-223: LGTM: Model version tags properly scoped and linked.

The workspace column, composite foreign key, and bidirectional relationship are all correctly configured. The relationship provides convenient navigation from model versions to their tags.


158-183: LGTM: Remaining models consistently updated with workspace support.

All models follow the established pattern with workspace columns, appropriate indexes, and correctly configured foreign key relationships. The webhook design (workspace column with non-composite PK) is consistent with the schema.

Also applies to: 236-261, 301-321

@mprahl mprahl force-pushed the workspaces-db-model branch from b933f52 to f8322c0 Compare November 19, 2025 00:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
tests/db/test_schema.py (1)

2-16: Dialect-specific reattachment of unique constraints looks solid; keep the mapping in sync over time

The _reattach_missing_unique_constraints / _get_unique_constraints pairing is a reasonable way to normalize reflection quirks (MySQL unique-as-index, MSSQL lacking get_unique_constraints) before generating schema strings. Using duplicates_index to gate MySQL recreation and warning on missing columns are good guardrails.

One thing to watch: _DIALECT_REFLECTED_UNIQUE_CONSTRAINTS is currently hard-coded for uq_experiments_workspace_name only. If future migrations introduce additional unique constraints that suffer from the same reflection issues on MySQL or MSSQL, this map will need to be extended to keep dump_schema output aligned with the real schema.

Also applies to: 32-43, 46-88, 90-107

mlflow/store/tracking/dbmodels/models.py (1)

64-65: Tracking models’ workspace columns match the migration; entity mapping can stay “default-only” for now

The additions of workspace to SqlExperiment (with uq_experiments_workspace_name) and to SqlEvaluationDataset (plus idx_evaluation_datasets_workspace) line up with the Alembic migration and the updated Postgres schema: same length, non-nullability, and 'default' server default via DEFAULT_WORKSPACE_NAME. That gives you a consistent workspace dimension in the tracking schema.

Right now to_mlflow_entity / from_mlflow_entity for these models ignore workspace, which keeps the runtime effectively “single default workspace” even though the schema is multi-tenant-ready. That’s fine if the intention of this PR is to get the database into a workspace-capable shape first, but once workspace becomes part of the user-visible API you’ll likely want to thread it through the corresponding entities and conversions.

Also applies to: 94-111, 131-138, 1309-1323, 1375-1380

tests/db/test_workspace_migration.py (1)

1-21: Workspace migration tests give good end-to-end coverage of upgrade, downgrade, and conflict scenarios

The SQLite-based harness that seeds a pre-workspace schema, upgrades to REVISION, and then exercises both upgrade and downgrade paths looks solid:

  • _seed_pre_workspace_entities correctly targets the legacy (no-workspace) layout, ensuring the migration itself is responsible for backfilling.
  • Post-upgrade assertions validate:
    • workspace columns (non-null + 'default' default),
    • data backfill for experiments, registry objects, and evaluation datasets,
    • new PK shapes and FKs, and
    • the expected workspace indexes.
  • Post-downgrade assertions confirm:
    • removal of workspace columns and the workspaces table,
    • restoration of legacy PK/FK shapes, and
    • that data survive a downgrade unchanged.
  • The conflict tests neatly mirror _assert_no_workspace_conflicts and verify that both the downgrade guards and the underlying grouping queries behave as intended.

The only minor note is that _get_workspace_default currently assumes fairly simple default representations, which is fine for SQLite in this test but might need tightening if you ever extend these checks to other dialects.

Also applies to: 24-239, 242-247, 530-587, 590-718, 722-805, 807-859, 861-895, 898-941

mlflow/store/model_registry/sqlalchemy_store.py (1)

64-65: Registry store now writes workspace-aware rows while remaining effectively “default workspace only”

Plumbing DEFAULT_WORKSPACE_NAME into:

  • SqlRegisteredModel / SqlRegisteredModelTag creation,
  • SqlModelVersion / SqlModelVersionTag creation,
  • set_registered_model_tag, set_model_version_tag, and set_registered_model_alias,

keeps all registry writes compatible with the new composite primary keys and non-null workspace columns in the ORM models.

At the same time, lookup paths like _get_registered_model, _get_registered_model_tag, _get_model_version_tag, and _get_registered_model_alias still filter only on name (and version/key), not workspace, so the effective behavior remains “single default workspace” for now. That’s a reasonable interim state for a schema-first migration; when you later introduce true multi-workspace behavior, those queries and the tag/alias search joins will need to be extended to include a workspace dimension to avoid ambiguity.

Also applies to: 172-206, 670-685, 686-707, 730-821, 1186-1212, 1237-1245, 1247-1270

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b933f52 and f8322c0.

📒 Files selected for processing (18)
  • docs/api_reference/api_inventory.txt (2 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/model_registry/sqlalchemy_store.py (6 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/schemas/mssql.sql (6 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
  • tests/resources/db/latest_schema.sql (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • mlflow/entities/workspace.py
  • tests/db/check_migration.py
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py
  • mlflow/entities/init.py
  • mlflow/utils/workspace_utils.py
  • mlflow/store/workspace/dbmodels/models.py
🧰 Additional context used
🧬 Code graph analysis (5)
tests/db/test_workspace_migration.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)
  • upgrade (138-615)
  • downgrade (618-873)
mlflow/store/workspace/dbmodels/__init__.py (1)
mlflow/store/workspace/dbmodels/models.py (1)
  • SqlWorkspace (10-22)
tests/db/test_schema.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
  • _get_unique_constraints (144-159)
mlflow/store/model_registry/dbmodels/models.py (1)
mlflow/entities/model_registry/registered_model.py (4)
  • name (44-46)
  • name (49-50)
  • description (69-71)
  • description (74-75)
mlflow/store/model_registry/sqlalchemy_store.py (1)
mlflow/store/model_registry/dbmodels/models.py (3)
  • SqlRegisteredModelTag (155-190)
  • SqlModelVersionTag (193-230)
  • SqlRegisteredModelAlias (233-268)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (34)
  • GitHub Check: js (windows-latest, --testPathIgnorePatterns)
  • GitHub Check: js (ubuntu-latest, --testPathPattern)
  • GitHub Check: js (ubuntu-latest, --testPathIgnorePatterns)
  • GitHub Check: js (windows-latest, --testPathPattern)
  • GitHub Check: typescript-sdk (24)
  • GitHub Check: typescript-sdk (20)
  • GitHub Check: flavors
  • GitHub Check: typescript-sdk (22)
  • GitHub Check: core
  • GitHub Check: lint-macos
  • GitHub Check: windows (4)
  • GitHub Check: windows (2)
  • GitHub Check: models (1)
  • GitHub Check: models (2)
  • GitHub Check: pyfunc (4)
  • GitHub Check: pyfunc (3)
  • GitHub Check: database
  • GitHub Check: genai
  • GitHub Check: windows (3)
  • GitHub Check: java
  • GitHub Check: evaluate (2)
  • GitHub Check: windows (1)
  • GitHub Check: pyfunc (2)
  • GitHub Check: evaluate (1)
  • GitHub Check: pyfunc (1)
  • GitHub Check: python (3)
  • GitHub Check: build (tracing)
  • GitHub Check: build (skinny)
  • GitHub Check: r
  • GitHub Check: python (1)
  • GitHub Check: build (dev)
  • GitHub Check: python (2)
  • GitHub Check: python-skinny
  • GitHub Check: build
🔇 Additional comments (15)
mlflow/store/workspace/dbmodels/__init__.py (1)

1-3: Expose SqlWorkspace via package root looks correct

Re-exporting SqlWorkspace from mlflow.store.workspace.dbmodels via __all__ matches existing dbmodels patterns and provides the expected import surface for callers.

docs/api_reference/api_inventory.txt (1)

531-531: Workspace entity entries align with existing inventory conventions

Listing both mlflow.entities.Workspace and mlflow.entities.workspace.Workspace is consistent with how other entities are represented here (top-level alias + module-qualified class). No further changes needed.

Also applies to: 631-631

tests/db/schemas/postgresql.sql (1)

19-31: PostgreSQL schema updates correctly reflect workspace-scoped registry layout

The added workspace columns, updated composite primary keys, and adjusted foreign keys (e.g., model_versionsregistered_models, tag/alias tables) are consistent with the workspace-aware migration and ORM models. The new workspaces catalog table also matches the expected shape. I don’t see schema-level issues here.

Also applies to: 34-45, 81-88, 91-103, 106-110, 184-201, 204-211, 214-221, 365-373

tests/db/schemas/mysql.sql (4)

29-29: LGTM: Workspace column additions are consistent.

The workspace columns are uniformly defined across all affected tables with VARCHAR(63) DEFAULT 'default' NOT NULL, which ensures backward compatibility with existing data.

Also applies to: 41-41, 86-86, 101-101, 197-197, 207-207, 217-217, 372-372


106-110: LGTM: Workspaces catalog table.

The new workspaces table properly establishes the foundation for multi-tenancy with a simple structure (name as PK, optional description).


43-44: LGTM: Workspace-scoped experiment name uniqueness.

The unique constraint uq_experiments_workspace_name on (workspace, name) correctly ensures experiments are unique within each workspace while allowing the same name across different workspaces.


87-87: Review the cascade constraint asymmetry and confirm design intent.

Your review comment correctly identifies an inconsistent cascade pattern in the schema. The verification confirms:

  • model_versions, registered_model_tags, and model_version_tags use ON UPDATE CASCADE only (no ON DELETE CASCADE)
  • registered_model_aliases uses ON DELETE CASCADE ON UPDATE CASCADE
  • No tests validate cascade behavior for these constraints
  • No documentation explains why the design differs

The asymmetry between registered_model_aliases and the other workspace-aware model tables warrants clarification. Confirm whether:

  1. Deletion of a registered_model should cascade to model_versions, registered_model_tags, and model_version_tags (matching business requirements)
  2. The inconsistency with registered_model_aliases is intentional, or if model_versions should also include ON DELETE CASCADE
  3. Application code handles cascading deletes manually where foreign keys restrict them
tests/db/schemas/sqlite.sql (1)

29-29: LGTM: SQLite schema consistent with MySQL.

The workspace additions, composite keys, and foreign key constraints in the SQLite schema are consistent with the MySQL schema, ensuring cross-database compatibility.

Also applies to: 41-41, 86-86, 101-101, 106-110, 198-200, 208-210, 218-220, 372-374

tests/resources/db/latest_schema.sql (1)

29-29: LGTM: Latest schema reference is consistent.

The latest schema file correctly reflects the workspace-aware multi-tenancy changes and matches the dialect-specific schema files (MySQL, SQLite, MSSQL).

Also applies to: 41-44, 86-87, 101-101, 106-110, 198-200, 208-210, 218-220, 372-374

tests/db/schemas/mssql.sql (1)

29-29: LGTM: MSSQL schema with appropriate dialect-specific syntax.

The MSSQL schema correctly uses:

  • COLLATE "SQL_Latin1_General_CP1_CI_AS" for VARCHAR columns
  • Parentheses around DEFAULT values: DEFAULT ('default')
  • Workspace-aware composite keys and foreign key constraints

The workspace additions are structurally consistent with other dialects.

Also applies to: 41-41, 85-86, 100-100, 105-109, 195-197, 205-207, 215-217, 364-366

mlflow/store/model_registry/dbmodels/models.py (5)

57-57: LGTM: Composite primary keys correctly defined.

All workspace-aware tables have properly updated their primary keys to include workspace as the first component of composite keys:

  • SqlRegisteredModel: (workspace, name)
  • SqlModelVersion: (workspace, name, version)
  • SqlRegisteredModelTag: (workspace, key, name)
  • SqlModelVersionTag: (workspace, key, name, version)
  • SqlRegisteredModelAlias: (workspace, name, alias)

Also applies to: 132-132, 182-183, 217-218, 260-260


126-133: LGTM: Foreign key constraints properly reference workspace-qualified keys.

The ForeignKeyConstraint definitions correctly reference composite keys that include workspace:

  • SqlModelVersionregistered_models(workspace, name) with ON UPDATE CASCADE
  • SqlRegisteredModelTagregistered_models(workspace, name) with ON UPDATE CASCADE
  • SqlModelVersionTagmodel_versions(workspace, name, version) with ON UPDATE CASCADE
  • SqlRegisteredModelAliasregistered_models(workspace, name) with ON DELETE CASCADE and ON UPDATE CASCADE

This ensures referential integrity across the workspace-scoped multi-tenancy model.

Also applies to: 176-183, 211-218, 252-261


36-36: No issues found. The import and constant usage are correct.

Verification confirms that DEFAULT_WORKSPACE_NAME = "default", which matches the expected value in the SQL schema. The import at line 36 is correct, and its usage in server_default parameters throughout the SQLAlchemy models aligns with the schema definitions.


65-82: The review comment is incorrect and should be dismissed.

The workspace field in SqlRegisteredModel is an internal database/storage concern for multi-tenancy and is deliberately not included in the RegisteredModel entity or API contract. This is confirmed by:

  1. The protobuf schema (mlflow/protos/model_registry.proto line 382+) explicitly excludes workspace from the RegisteredModel message definition
  2. The entity's from_proto() and to_proto() methods make no reference to workspace
  3. Similar multi-tenant models like SqlRegisteredModelTag and SqlRegisteredModelAlias also exclude workspace from their entity/proto representations

Workspace is an architectural boundary: it exists at the backend storage layer to support multi-tenancy but does not cross into the entity or API layers. Adding it to RegisteredModel would require modifying the proto schema and could break the API contract.

Likely an incorrect or invalid review comment.


317-322: Review comment is incorrect - workspace is not enforced via FK to workspaces table in this codebase.

The review's assumption that "other workspace-aware tables" have FK constraints to the workspaces table is false. Investigation shows SqlRegisteredModelTag, SqlModelVersion, and other workspace-aware tables in model_registry all store workspace as plain String(63) columns without FK constraints. This is the established pattern: workspace functions as a multi-tenant identifier string, not a foreign key reference. No table in mlflow/store/model_registry has a FK to the workspaces table.

However, there is a separate schema issue: the workspace column exists in the SqlWebhook model definition but is missing from the migration file (1a0cddfcaa16), which could cause migration failures or schema sync problems.

Likely an incorrect or invalid review comment.

@mprahl mprahl force-pushed the workspaces-db-model branch from f8322c0 to 0446441 Compare November 19, 2025 00:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mlflow/store/model_registry/dbmodels/models.py (1)

271-295: Critical: Fallback key generation causes data loss on restart.

Line 284 generates a random encryption key if MLFLOW_WEBHOOK_SECRET_ENCRYPTION_KEY is not set. This key is ephemeral and not persisted, meaning:

  • Each application restart generates a different key
  • Previously encrypted secrets become permanently unreadable
  • Webhook secrets are effectively lost

This creates a critical data integrity issue in production.

Apply this diff to fail fast when the encryption key is missing:

     def __init__(self):
         super().__init__()
-        # Get encryption key from environment variable or generate one
-        # In production, this should come from a secure key management service
-        encryption_key = MLFLOW_WEBHOOK_SECRET_ENCRYPTION_KEY.get() or Fernet.generate_key()
+        # Get encryption key from environment variable
+        # In production, this should come from a secure key management service
+        encryption_key = MLFLOW_WEBHOOK_SECRET_ENCRYPTION_KEY.get()
+        if encryption_key is None:
+            raise ValueError(
+                "MLFLOW_WEBHOOK_SECRET_ENCRYPTION_KEY must be set to encrypt webhook secrets"
+            )
         self.cipher = Fernet(encryption_key)
🧹 Nitpick comments (14)
mlflow/utils/workspace_utils.py (1)

1-5: DEFAULT_WORKSPACE_NAME helper looks good; consider centralizing usage in tests.

The constant and export are clean and match the rest of the PR’s workspace defaults. As a follow‑up, consider importing and using DEFAULT_WORKSPACE_NAME in places like tests/db/check_migration.py (rather than hard‑coding "default") to reduce drift if the default ever changes.

tests/db/schemas/postgresql.sql (1)

19-31: Workspace-aware DDL looks coherent; consider optional FK to workspaces.

The PostgreSQL schema updates form a consistent workspace model:

  • All relevant tables get workspace VARCHAR(63) DEFAULT 'default'::character varying NOT NULL.
  • Experiments have uq_experiments_workspace_name for (workspace, name).
  • Registered models, versions, aliases, and tags move to composite PKs/FKs including workspace.
  • A dedicated workspaces table is added with name as PK.

This all lines up with the multi‑tenancy changes elsewhere in the PR and should work fine as-is.

If, in the future, you want stronger integrity guarantees, you could optionally add FOREIGN KEY (workspace) REFERENCES workspaces (name) to the workspace-backed tables to enforce that all workspace values are registered, but that’s a design/compat decision and not required for this change.

Also applies to: 34-45, 81-88, 91-103, 106-110, 184-201, 204-211, 214-221, 365-373

mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)

64-130: MSSQL unique-metadata helper looks solid; keep it aligned with schema tests

The _fetch_mssql_unique_metadata() helper plus the _get_unique_constraints() / _get_unique_indexes() wrappers give you a robust MSSQL fallback, mirroring what tests/db/test_schema.py does via inspector/indexes. The catalog queries and the returned shape ({"name", "column_names"}) look correct.

The only thing to watch is long‑term drift: if you ever add new unique constraints or change naming conventions for MSSQL, this helper and the schema tests (_get_unique_constraints in tests/db/test_schema.py) must be updated together so migration detection on name keeps working consistently.


618-751: Downgrade conflict detection is thorough; keep specs in sync with schema evolution

The _assert_no_workspace_conflicts() + conflict_specs + legacy_*_defs setup in downgrade() is a good safety net to prevent lossy merges when dropping the workspace dimension, and the messages align with what the tests expect.

Going forward, any new workspace‑scoped tables or changes to key uniqueness (e.g., adding a unique on (workspace, something_else)) will need matching updates in:

  • conflict_specs
  • _WORKSPACE_TABLES
  • legacy_pk_defs / legacy_fk_defs

to avoid silent mismatches during downgrade. It could be worth adding a brief comment near these lists stating that they must be kept in lockstep with the “latest schema” SQL files and workspace‑migration tests.

mlflow/store/tracking/dbmodels/models.py (2)

92-138: Experiment workspace column and uniqueness look consistent with the migration

The SqlExperiment.workspace column plus UniqueConstraint("workspace", "name", name="uq_experiments_workspace_name") line up with the new migration and schema snapshots. This preserves existing behavior for a single default workspace while giving room to scope experiment names per workspace.

Note that the migration hard‑codes 'default' as the server default while the ORM uses DEFAULT_WORKSPACE_NAME; if you ever change that constant, you’ll need a follow‑up migration rather than just tweaking this model, otherwise existing rows and new inserts could diverge.


1303-1380: Evaluation dataset workspace support is wired at the DB level but not surfaced in entities

Adding workspace with default/server_default and the idx_evaluation_datasets_workspace index matches the migration and dialect schemas and should give good performance for workspace‑scoped queries.

Right now SqlEvaluationDataset.to_mlflow_entity() / from_mlflow_entity() don’t expose or accept workspace, so everything implicitly lives in the default workspace. That’s fine for this step, but once you introduce a workspace‑aware public API, you’ll need to thread workspace through these mappers (and probably the EvaluationDataset entity) to avoid silent misrouting of data.

tests/db/test_workspace_migration.py (3)

235-240: _get_workspace_default is fine for SQLite; keep scope limited

_get_workspace_default() is tailored to how SQLite reports column defaults (string expressions) and works for the engine created in _prepare_database. If you ever extend these tests to run against other dialects, you’ll likely need a more dialect‑aware default parser (or to reuse SQLAlchemy’s compiled default inspection helpers) to avoid brittle string handling.


590-659: Upgrade test gives strong coverage; consider also asserting on webhooks.workspace

test_workspace_migration_upgrade_adds_columns_and_backfills() does a nice job verifying:

  • workspace columns exist and are non‑nullable,
  • legacy rows are backfilled to "default",
  • PKs/UKs/FKs and indexes match the new schema,
  • the workspaces table is created and seeded.

Since the migration also adds a non‑nullable workspace to webhooks and creates idx_webhooks_workspace, it’d be a small but useful extension to:

  • include "webhooks" in the assert_workspace_column(...) loop, and
  • assert that has_index("webhooks", "idx_webhooks_workspace", ["workspace"]) is true.

Not critical, but it would fully exercise all the workspace‑aware tables in the upgrade path.


807-941: Downgrade conflict tests align well with the downgrade safety checks

The downgrade conflict tests (test_workspace_migration_downgrade_detects_conflicts and test_workspace_migration_conflict_detection_queries) mirror the logic in _assert_no_workspace_conflicts and _fetch_conflicts, and the expected fragments match the resource_description strings in the migration.

This is exactly the kind of safety net you want before allowing a downgrade that merges workspaces; as you add more workspace‑scoped tables or adjust key uniqueness, remember to add corresponding _setup_*_conflict helpers and param cases here.

mlflow/store/model_registry/sqlalchemy_store.py (4)

172-207: Creating registry objects in the default workspace is correct but implicit

Using workspace=DEFAULT_WORKSPACE_NAME when creating SqlRegisteredModel and its tags ensures new rows satisfy the composite PK/constraints introduced in the models and migrations, and keeps behavior equivalent to the pre‑workspace world (everything in a single logical tenant).

This is a good transitional step; just keep in mind that if/when you support non‑default workspaces through this store, you’ll probably want to make workspace an explicit parameter rather than relying on a hard‑coded default here.


670-707: Tag helpers should become workspace‑aware before multi‑workspace support goes live

set_registered_model_tag() now merges a SqlRegisteredModelTag with workspace=DEFAULT_WORKSPACE_NAME, but the corresponding getter _get_registered_model_tag() still filters only on (name, key) and assumes a single row.

That’s fine as long as every registered model lives in the default workspace, but once you allow the same model name in multiple workspaces you’ll need to:

  • include workspace in the query predicates (or in a uniqueness guarantee), and
  • decide how workspace is passed through the public API.

Otherwise you’ll either hit the “Found N tags” INVALID_STATE branch or mix tags across workspaces.


1186-1212: Model version tag helpers have the same single‑workspace assumption

set_model_version_tag() now persists tags with workspace=DEFAULT_WORKSPACE_NAME, but _get_model_version_tag() still queries only by (name, version, key). Similar to the registered‑model tag case, this implicitly assumes there is at most one workspace in play for a given (name, version).

Before multi‑workspace usage of the registry store, it would be safer either to:

  • add SqlModelVersionTag.workspace == DEFAULT_WORKSPACE_NAME to the query, or
  • make workspace an explicit parameter on the public tag APIs.

That will avoid ambiguous results or cross‑workspace interference later.


1247-1270: Registered model aliases also need a workspace story

set_registered_model_alias() now merges a SqlRegisteredModelAlias with workspace=DEFAULT_WORKSPACE_NAME, but _get_registered_model_alias() still filters by (name, alias) only. As with tags, this is fine while there’s only the default workspace, but it’s ambiguous as soon as you allow the same alias/name pair in multiple workspaces.

Consider either:

  • constraining this store to the default workspace and documenting that assumption, or
  • threading workspace into alias lookups (and making it part of the public API) so that PK and query conditions stay aligned.
mlflow/store/model_registry/dbmodels/models.py (1)

42-57: Consider adding an index on workspace for query performance.

While the composite primary key on (workspace, name) exists, queries that filter by workspace alone (e.g., listing all models in a workspace) would benefit from a dedicated index.

Apply this diff to add the index:

-    __table_args__ = (PrimaryKeyConstraint("workspace", "name", name="registered_model_pk"),)
+    __table_args__ = (
+        PrimaryKeyConstraint("workspace", "name", name="registered_model_pk"),
+        Index("idx_registered_models_workspace", "workspace"),
+    )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8322c0 and 0446441.

📒 Files selected for processing (18)
  • docs/api_reference/api_inventory.txt (2 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/model_registry/sqlalchemy_store.py (6 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/schemas/mssql.sql (6 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
  • tests/resources/db/latest_schema.sql (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • mlflow/entities/workspace.py
  • mlflow/store/workspace/dbmodels/init.py
  • mlflow/store/workspace/dbmodels/models.py
  • docs/api_reference/api_inventory.txt
🧰 Additional context used
🧬 Code graph analysis (6)
mlflow/entities/__init__.py (1)
mlflow/entities/workspace.py (1)
  • Workspace (9-13)
tests/db/test_schema.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
  • _get_unique_constraints (144-159)
tests/db/test_workspace_migration.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)
  • upgrade (138-615)
  • downgrade (618-901)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
tests/db/test_schema.py (1)
  • _get_unique_constraints (90-107)
mlflow/store/model_registry/sqlalchemy_store.py (1)
mlflow/store/model_registry/dbmodels/models.py (3)
  • SqlRegisteredModelTag (155-190)
  • SqlModelVersionTag (193-230)
  • SqlRegisteredModelAlias (233-268)
tests/db/check_migration.py (1)
mlflow/store/model_registry/sqlalchemy_store.py (1)
  • create_registered_model (172-214)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (34)
  • GitHub Check: pyfunc (1)
  • GitHub Check: pyfunc (4)
  • GitHub Check: pyfunc (2)
  • GitHub Check: pyfunc (3)
  • GitHub Check: evaluate (2)
  • GitHub Check: evaluate (1)
  • GitHub Check: genai
  • GitHub Check: models (2)
  • GitHub Check: windows (2)
  • GitHub Check: windows (3)
  • GitHub Check: flavors
  • GitHub Check: models (1)
  • GitHub Check: windows (1)
  • GitHub Check: windows (4)
  • GitHub Check: python (3)
  • GitHub Check: python (2)
  • GitHub Check: python (1)
  • GitHub Check: java
  • GitHub Check: database
  • GitHub Check: python-skinny
  • GitHub Check: typescript-sdk (22)
  • GitHub Check: typescript-sdk (24)
  • GitHub Check: typescript-sdk (20)
  • GitHub Check: build
  • GitHub Check: r
  • GitHub Check: js (ubuntu-latest, --testPathIgnorePatterns)
  • GitHub Check: js (windows-latest, --testPathPattern)
  • GitHub Check: js (windows-latest, --testPathIgnorePatterns)
  • GitHub Check: js (ubuntu-latest, --testPathPattern)
  • GitHub Check: build (tracing)
  • GitHub Check: build (skinny)
  • GitHub Check: build (dev)
  • GitHub Check: lint
  • GitHub Check: core
🔇 Additional comments (16)
mlflow/entities/__init__.py (1)

65-65: Workspace re-export is consistent with existing entities API.

Importing Workspace and adding it to __all__ nicely aligns with the other entity exports and enables from mlflow.entities import Workspace as expected. No issues from this change.

Also applies to: 129-129

tests/db/test_schema.py (1)

2-3: Implementation logic is sound; verify test coverage on mysql/mssql in CI.

The code changes implement proper dialect-specific handling:

  • _DIALECT_REFLECTED_UNIQUE_CONSTRAINTS correctly limits reattachment to uq_experiments_workspace_name on mysql/mssql.
  • _get_unique_constraints() properly handles NotImplementedError from inspector.get_unique_constraints() and falls back to filtering get_indexes() for unique constraints.
  • MySQL-specific guard (line 67) checks duplicates_index to distinguish between unique constraints and unique indexes.
  • Guards against missing columns (lines 75-79) and duplicate constraints (line 73).
  • Constraint name matches the model definition.

Existing tests in test_workspace_migration.py (lines 692–695, 778–780) already validate inspector.get_unique_constraints("experiments") behavior on mysql/mssql dialects, confirming the inspector returns the expected constraint metadata.

However, test_schema_is_up_to_date() is not parameterized by dialect in the current codebase—it runs only on the default database connection. To fully verify the requested concern, manually confirm in CI that either:

  • test_schema_is_up_to_date executes on mysql/mssql database containers and successfully restores the constraint, or
  • Add dialect parameterization to the test to ensure all three dialects are covered.
tests/db/schemas/mysql.sql (2)

19-45: MySQL schema matches workspace changes; confirm index expectations

The MySQL DDL for evaluation_datasets, experiments (including uq_experiments_workspace_name), registered_models, and the model‑registry tables reflects the new workspace columns, composite PKs, and workspace‑qualified FKs introduced in the migration.

The migration also creates several workspace indexes (idx_experiments_workspace, idx_registered_models_workspace, idx_evaluation_datasets_workspace, idx_webhooks_workspace). If your check_migration / schema‑parity tests assert on indexes using this file as ground truth, it would be good to ensure those indexes are represented here as well (either inline via KEY clauses or as CREATE INDEX statements), so the snapshot matches a migrated database.


81-88: Workspace‑qualified PK/FKs for registry tables look consistent

For registered_models, model_versions, registered_model_aliases, registered_model_tags, and model_version_tags, the MySQL schema now:

  • adds workspace VARCHAR(63) DEFAULT 'default' NOT NULL,
  • promotes (workspace, name[ , version ]) to the primary key, and
  • updates FKs to reference registered_models / model_versions via their workspace‑qualified keys.

This lines up with the ORM models and the Alembic migration. No issues spotted here; just the same note as above about keeping any index definitions in sync if schema tests rely on them.

Also applies to: 183-200, 203-220, 367-375

tests/db/schemas/sqlite.sql (2)

19-45: SQLite schema’s workspace additions and constraints align with the migration

The SQLite schema correctly introduces workspace on evaluation_datasets and experiments, with:

  • workspace VARCHAR(63) DEFAULT 'default' NOT NULL, and
  • uq_experiments_workspace_name enforcing per‑workspace uniqueness of experiment names.

This matches the ORM definition and the upgrade migration behavior.


81-88: Registry workspace PK/FKs in SQLite look correct; consider index coverage in snapshots

For the registry tables, the schema updates:

  • add non‑nullable workspace with default 'default',
  • switch PKs to (workspace, name) / (workspace, name, version) / (workspace, key, name[ , version ]), and
  • wire FKs through those workspace‑qualified keys using the same constraint names as the migration.

That’s exactly what the downgrade/upgrade tests expect. As with MySQL, if your schema‑parity tests ever start asserting on indexes, you may want to ensure the workspace indexes created by the migration are also reflected in the SQLite snapshot.

Also applies to: 184-201, 204-221, 367-375

tests/resources/db/latest_schema.sql (2)

19-45: “Latest schema” snapshot matches the new workspace-aware logical schema

The generic latest_schema.sql correctly reflects:

  • workspace on evaluation_datasets and experiments,
  • the uq_experiments_workspace_name unique constraint, and
  • the non‑nullable default 'default' behavior.

This keeps the logical schema in sync with what the migration produces and what the ORM models express.


81-88: Registry workspace keys and workspaces catalog are consistent across snapshots

The additions of:

  • workspace‑qualified PKs/FKs for registered_models, model_versions, registered_model_aliases, registered_model_tags, and model_version_tags, and
  • the workspaces table with workspaces_pk on name

closely mirror the dialect‑specific schemas and the Alembic migration. This looks internally consistent and should be a reliable target for schema‑parity checks.

As with the dialect schemas, if your tests ever verify indexes as part of “latest schema” parity (e.g., idx_experiments_workspace), be sure to add those here too to avoid divergence between migrated DBs and this snapshot.

Also applies to: 184-201, 204-221, 367-375, 106-111

tests/db/schemas/mssql.sql (2)

19-45: MSSQL workspace columns and experiment uniqueness match the migration logic

For MSSQL, the additions of:

  • workspace VARCHAR(63) ... DEFAULT ('default') NOT NULL on evaluation_datasets and experiments, and
  • uq_experiments_workspace_name unique on (workspace, name)

look correct and compatible with the catalog queries in _fetch_mssql_unique_metadata() that detect unique constraints on name. This should keep the migration’s constraint‑detection logic and the expected schema in sync.


80-87: MSSQL registry workspace PK/FKs and workspaces table are consistent

The MSSQL schema updates:

  • add workspace with default 'default' across registered_models, model_versions, registered_model_aliases, registered_model_tags, and model_version_tags,
  • convert PKs to workspace‑qualified forms (e.g., (workspace, name, version)), and
  • update FKs to reference the new composite keys with constraint names used in the migration (e.g., fk_model_versions_registered_models, fk_model_version_tags_model_versions),

plus the new workspaces table with workspaces_pk on name. This matches both the Alembic migration and the ORM definitions and should behave correctly under the MSSQL‑specific migration paths.

Also applies to: 181-198, 201-218, 359-367, 105-109

mlflow/store/model_registry/dbmodels/models.py (6)

1-1: LGTM!

The new imports are correctly used throughout the file to support workspace multi-tenancy.

Also applies to: 36-36


220-223: LGTM!

The explicit model_version relationship improves code clarity and enables bidirectional navigation between model versions and their tags.


252-261: LGTM!

The foreign key constraint correctly includes both onupdate="cascade" and ondelete="cascade", ensuring proper referential integrity at the database level. This is the correct pattern that should be applied to other tables as well.


301-321: Verify the primary key strategy for workspace isolation.

Unlike other tables in this file, SqlWebhook uses a single-column primary key (webhook_id) rather than a composite key including workspace. This means:

  • webhook_id must be globally unique across all workspaces
  • The design pattern is inconsistent with SqlRegisteredModel, SqlModelVersion, etc.

If webhook_id is generated as a UUID or similar globally-unique identifier, this is acceptable. However, if there's any possibility of workspace-scoped collision, or if consistency with the rest of the schema is preferred, consider making the primary key (workspace, webhook_id).

Please verify whether webhook_id is guaranteed to be globally unique or whether the primary key should be updated to (workspace, webhook_id) for consistency.


344-367: SqlWebhookEvent design depends on webhook_id uniqueness.

SqlWebhookEvent does not have a workspace column and references webhooks.webhook_id directly. This design is acceptable if webhook_id is globally unique across workspaces. However, if the primary key of SqlWebhook is changed to a composite key (workspace, webhook_id), then SqlWebhookEvent would need:

  • A workspace column
  • A composite foreign key constraint on (workspace, webhook_id)

This is directly related to the design decision for SqlWebhook flagged above.


65-82: Code is correct as-is; no changes needed.

The RegisteredModel entity class does not have a workspace parameter in its __init__ method, and this is intentional. Workspace is a database-level tenancy concern managed by the ORM models (e.g., SqlRegisteredModel), while the entity layer represents domain objects independent of infrastructure details. This pattern is consistent across all entity conversions in the codebase.

Comment on lines +126 to +133
__table_args__ = (
ForeignKeyConstraint(
["workspace", "name"],
["registered_models.workspace", "registered_models.name"],
onupdate="cascade",
),
PrimaryKeyConstraint("workspace", "name", "version", name="model_version_pk"),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add ondelete="cascade" to the foreign key constraint for consistency.

The ForeignKeyConstraint includes onupdate="cascade" but lacks ondelete="cascade". While the ORM-level relationship (line 123) specifies cascade="all", having database-level cascade deletion ensures referential integrity even for direct SQL operations.

Apply this diff:

     __table_args__ = (
         ForeignKeyConstraint(
             ["workspace", "name"],
             ["registered_models.workspace", "registered_models.name"],
             onupdate="cascade",
+            ondelete="cascade",
         ),
         PrimaryKeyConstraint("workspace", "name", "version", name="model_version_pk"),
     )
🤖 Prompt for AI Agents
In mlflow/store/model_registry/dbmodels/models.py around lines 126 to 133, the
ForeignKeyConstraint sets onupdate="cascade" but omits ondelete="cascade";
update the ForeignKeyConstraint to include ondelete="cascade" so the DB enforces
cascading deletes too (keep onupdate="cascade" as-is) and ensure the constraint
signature remains valid with both options specified.

Comment on lines +176 to +183
__table_args__ = (
ForeignKeyConstraint(
["workspace", "name"],
["registered_models.workspace", "registered_models.name"],
onupdate="cascade",
),
PrimaryKeyConstraint("workspace", "key", "name", name="registered_model_tag_pk"),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add ondelete="cascade" to maintain consistency.

Similar to SqlModelVersion, this foreign key constraint should include ondelete="cascade" for database-level referential integrity.

Apply this diff:

     __table_args__ = (
         ForeignKeyConstraint(
             ["workspace", "name"],
             ["registered_models.workspace", "registered_models.name"],
             onupdate="cascade",
+            ondelete="cascade",
         ),
         PrimaryKeyConstraint("workspace", "key", "name", name="registered_model_tag_pk"),
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
__table_args__ = (
ForeignKeyConstraint(
["workspace", "name"],
["registered_models.workspace", "registered_models.name"],
onupdate="cascade",
),
PrimaryKeyConstraint("workspace", "key", "name", name="registered_model_tag_pk"),
)
__table_args__ = (
ForeignKeyConstraint(
["workspace", "name"],
["registered_models.workspace", "registered_models.name"],
onupdate="cascade",
ondelete="cascade",
),
PrimaryKeyConstraint("workspace", "key", "name", name="registered_model_tag_pk"),
)
🤖 Prompt for AI Agents
In mlflow/store/model_registry/dbmodels/models.py around lines 176 to 183, the
ForeignKeyConstraint for (workspace, name) referencing registered_models lacks
ondelete="cascade"; update that ForeignKeyConstraint to include
ondelete="cascade" (matching SqlModelVersion) so deletions of registered_models
cascade to this table, leaving the PrimaryKeyConstraint unchanged.

Comment on lines +174 to +218
__table_args__ = (
PrimaryKeyConstraint("key", "name", "version", name="model_version_tag_pk"),
ForeignKeyConstraint(
("name", "version"),
("model_versions.name", "model_versions.version"),
["workspace", "name", "version"],
["model_versions.workspace", "model_versions.name", "model_versions.version"],
onupdate="cascade",
),
PrimaryKeyConstraint("workspace", "key", "name", "version", name="model_version_tag_pk"),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add ondelete="cascade" for referential integrity.

The foreign key constraint should include ondelete="cascade" to ensure tags are removed when the parent model version is deleted, maintaining consistency with the ORM-level cascade behavior.

Apply this diff:

     __table_args__ = (
         ForeignKeyConstraint(
             ["workspace", "name", "version"],
             ["model_versions.workspace", "model_versions.name", "model_versions.version"],
             onupdate="cascade",
+            ondelete="cascade",
         ),
         PrimaryKeyConstraint("workspace", "key", "name", "version", name="model_version_tag_pk"),
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
__table_args__ = (
PrimaryKeyConstraint("key", "name", "version", name="model_version_tag_pk"),
ForeignKeyConstraint(
("name", "version"),
("model_versions.name", "model_versions.version"),
["workspace", "name", "version"],
["model_versions.workspace", "model_versions.name", "model_versions.version"],
onupdate="cascade",
),
PrimaryKeyConstraint("workspace", "key", "name", "version", name="model_version_tag_pk"),
)
__table_args__ = (
ForeignKeyConstraint(
["workspace", "name", "version"],
["model_versions.workspace", "model_versions.name", "model_versions.version"],
onupdate="cascade",
ondelete="cascade",
),
PrimaryKeyConstraint("workspace", "key", "name", "version", name="model_version_tag_pk"),
)
🤖 Prompt for AI Agents
In mlflow/store/model_registry/dbmodels/models.py around lines 211 to 218, the
ForeignKeyConstraint linking tags to model_versions lacks ondelete="cascade", so
database-level deletions of a model_version won't cascade to its tags; update
the ForeignKeyConstraint to include ondelete="cascade" (i.e., add
ondelete="cascade" alongside the existing onupdate="cascade") so that DB-level
referential integrity matches the ORM cascade behavior and tags are removed when
their parent model_version is deleted.

Comment thread tests/db/check_migration.py
@mprahl mprahl force-pushed the workspaces-db-model branch from 0446441 to 72bfda2 Compare November 19, 2025 01:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (3)
mlflow/store/model_registry/dbmodels/models.py (3)

126-133: Past review comment not yet addressed: Missing ondelete="cascade" in ForeignKeyConstraint.

The past review comment correctly identified that this foreign key constraint lacks ondelete="cascade". While the ORM relationship (line 123) has cascade="all", database-level cascade ensures referential integrity for direct SQL operations.

Apply this diff:

     __table_args__ = (
         ForeignKeyConstraint(
             ["workspace", "name"],
             ["registered_models.workspace", "registered_models.name"],
             onupdate="cascade",
+            ondelete="cascade",
         ),
         PrimaryKeyConstraint("workspace", "name", "version", name="model_version_pk"),
     )

Based on learnings from past review comments.


176-183: Past review comment not yet addressed: Missing ondelete="cascade" in ForeignKeyConstraint.

The past review comment correctly identified this missing constraint. SqlRegisteredModelAlias (lines 252-261) shows the correct pattern with both onupdate="cascade" and ondelete="cascade".

Apply this diff:

     __table_args__ = (
         ForeignKeyConstraint(
             ["workspace", "name"],
             ["registered_models.workspace", "registered_models.name"],
             onupdate="cascade",
+            ondelete="cascade",
         ),
         PrimaryKeyConstraint("workspace", "key", "name", name="registered_model_tag_pk"),
     )

Based on learnings from past review comments.


211-218: Past review comment not yet addressed: Missing ondelete="cascade" in ForeignKeyConstraint.

The past review comment correctly identified this issue. Tags should be removed when the parent model version is deleted.

Apply this diff:

     __table_args__ = (
         ForeignKeyConstraint(
             ["workspace", "name", "version"],
             ["model_versions.workspace", "model_versions.name", "model_versions.version"],
             onupdate="cascade",
+            ondelete="cascade",
         ),
         PrimaryKeyConstraint("workspace", "key", "name", "version", name="model_version_tag_pk"),
     )

Based on learnings from past review comments.

🧹 Nitpick comments (4)
mlflow/store/tracking/dbmodels/models.py (1)

1315-1323: Clarify workspace handling for evaluation datasets and consider composite index

The added workspace column plus a dedicated workspace index are a good starting point for workspace‑scoped evaluation datasets, and the default/server_default pattern mirrors the experiment change nicely.

A couple of follow‑ups to consider:

  1. Entity visibility: SqlEvaluationDataset.to_mlflow_entity() / from_mlflow_entity() currently ignore workspace. If evaluation datasets are meant to be workspace‑aware at the entity/API level, we should thread workspace through there; otherwise, it’s implicitly a store‑only dimension, similar to the current experiment behavior.

  2. Index/uniqueness shape: If typical queries look like WHERE workspace = :ws AND name = :name (or include digest), a composite index such as (workspace, name) could be more efficient than separate indexes on workspace and name. And if we logically expect a dataset name (or name+digest) to be unique per workspace, a corresponding UniqueConstraint would help enforce that invariant and avoid race conditions.

None of this blocks the current change but is worth aligning with expected access patterns and invariants.

Also applies to: 1375-1380

mlflow/entities/workspace.py (1)

8-13: Consider adding validation for the workspace name.

The Workspace dataclass lacks validation for the name field. Based on the database schema (VARCHAR(63)), the name should be validated for:

  • Non-empty value
  • Maximum length of 63 characters
  • Potentially valid characters/format

Consider adding a __post_init__ method for validation:

 @dataclass(frozen=True, slots=True)
 class Workspace:
     """Minimal metadata describing a workspace."""
 
     name: str
     description: str | None = None
+
+    def __post_init__(self):
+        if not self.name or not self.name.strip():
+            raise ValueError("Workspace name cannot be empty")
+        if len(self.name) > 63:
+            raise ValueError("Workspace name cannot exceed 63 characters")
tests/db/test_schema.py (1)

90-107: Potential code duplication with migration logic.

The _get_unique_constraints helper closely resembles the logic in the migration file at mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (lines 141-156). Both handle the same MSSQL fallback and index-based constraint detection.

Consider extracting this shared logic to a common utility module (e.g., mlflow/store/db/utils.py) to avoid duplication and ensure consistent behavior across migration and test code.

mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)

133-134: TODO comment about removing server_default - track this technical debt.

The comment indicates that server_default should be removed once the tracking and model registry stores properly set the workspace. This is important to ensure data integrity and prevent silent fallback to 'default' workspace.

Please create a follow-up task or issue to:

  1. Update the stores to explicitly set workspace values
  2. Remove the server_default from the migration
  3. Verify all code paths properly handle workspace assignment

Would you like me to open an issue to track this work?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0446441 and 72bfda2.

📒 Files selected for processing (18)
  • docs/api_reference/api_inventory.txt (2 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/model_registry/sqlalchemy_store.py (6 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/schemas/mssql.sql (6 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
  • tests/resources/db/latest_schema.sql (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • mlflow/utils/workspace_utils.py
  • tests/db/check_migration.py
  • mlflow/entities/init.py
  • mlflow/store/workspace/dbmodels/init.py
  • docs/api_reference/api_inventory.txt
  • mlflow/store/workspace/dbmodels/models.py
🧰 Additional context used
🧬 Code graph analysis (4)
tests/db/test_workspace_migration.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)
  • upgrade (135-612)
  • downgrade (615-945)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
tests/db/test_schema.py (1)
  • _get_unique_constraints (90-107)
tests/db/test_schema.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
  • _get_unique_constraints (141-156)
mlflow/store/model_registry/sqlalchemy_store.py (1)
mlflow/store/model_registry/dbmodels/models.py (3)
  • SqlRegisteredModelTag (155-190)
  • SqlModelVersionTag (193-230)
  • SqlRegisteredModelAlias (233-268)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (34)
  • GitHub Check: flavors
  • GitHub Check: evaluate (2)
  • GitHub Check: models (1)
  • GitHub Check: genai
  • GitHub Check: evaluate (1)
  • GitHub Check: models (2)
  • GitHub Check: java
  • GitHub Check: pyfunc (4)
  • GitHub Check: windows (1)
  • GitHub Check: windows (2)
  • GitHub Check: windows (4)
  • GitHub Check: windows (3)
  • GitHub Check: pyfunc (3)
  • GitHub Check: database
  • GitHub Check: pyfunc (1)
  • GitHub Check: pyfunc (2)
  • GitHub Check: python (1)
  • GitHub Check: python (2)
  • GitHub Check: python-skinny
  • GitHub Check: python (3)
  • GitHub Check: typescript-sdk (22)
  • GitHub Check: typescript-sdk (24)
  • GitHub Check: typescript-sdk (20)
  • GitHub Check: build (skinny)
  • GitHub Check: build (tracing)
  • GitHub Check: build (dev)
  • GitHub Check: js (windows-latest, --testPathPattern)
  • GitHub Check: js (ubuntu-latest, --testPathPattern)
  • GitHub Check: core
  • GitHub Check: js (ubuntu-latest, --testPathIgnorePatterns)
  • GitHub Check: js (windows-latest, --testPathIgnorePatterns)
  • GitHub Check: lint-macos
  • GitHub Check: build
  • GitHub Check: r
🔇 Additional comments (12)
mlflow/store/tracking/dbmodels/models.py (2)

64-64: Confirm DEFAULT_WORKSPACE_NAME import doesn’t create circular dependencies

Importing DEFAULT_WORKSPACE_NAME here is reasonable, but please double-check that mlflow.utils.workspace_utils does not, directly or indirectly, import mlflow.store.tracking.dbmodels.models to avoid circular import issues.


98-111: Workspace‑scoped experiments look consistent; verify entity & migration alignment

The new workspace column plus UniqueConstraint("workspace", "name", name="uq_experiments_workspace_name") is a clean way to move from globally unique names to workspace‑scoped experiments. The combination of default + server_default on workspace should also make migrations/backfills for existing rows straightforward.

Two things to confirm:

  1. Experiment entity surface: SqlExperiment.to_mlflow_entity() still doesn’t expose workspace. If the Experiment entity class (or surrounding APIs) is intended to carry workspace information, we’ll need to plumb workspace through here; if workspace is intentionally a store‑only dimension, this is fine but worth making explicit.

  2. Migration/constraint naming: Make sure the Alembic (or equivalent) migration that adds workspace and drops the old global unique constraint uses the same constraint name uq_experiments_workspace_name, so upgrade/downgrade paths and CI checks don’t drift from the model definition.

Also applies to: 131-138

tests/db/schemas/postgresql.sql (1)

29-29: LGTM! Workspace schema changes are well-structured.

The workspace column additions and constraint updates are consistent across all affected tables:

  • Proper NOT NULL constraints with sensible defaults
  • Composite primary keys correctly include workspace
  • Foreign key constraints properly reference workspace-qualified parent keys
  • Appropriate CASCADE behavior for updates and deletes

Also applies to: 41-41, 86-86, 101-101, 106-110, 198-200, 208-210, 218-220, 370-372

mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)

626-646: LGTM! Conflict detection prevents data loss during downgrade.

The _assert_no_workspace_conflicts function properly validates that downgrading (which merges all workspaces into 'default') won't create duplicate keys. The error messages are clear and actionable.


206-412: I need to gather more information about the actual test coverage for SQLite migrations.Now let me search more specifically for test files related to this migration:

Now let me search the codebase for actual test files related to this migration:

Based on my verification through documentation and code analysis, I've gathered sufficient information. The original review comment asks to verify SQLite batch operations but the requested verification commands had limited utility. Let me provide the rewritten review comment:

SQLite batch operations appear correctly implemented but verification incomplete.

The migration properly uses batch operations with constraint handling, which is the standard pattern for SQLite. With batch operations set to "auto", SQLite will recreate tables when operations other than add_column(), create_index(), or drop_index() are present, and this migration performs multiple constraint modifications requiring recreation.

However, the initial verification search (rg -n "dialect.*sqlite" tests/db/test_workspace_migration.py) found no results, and USE_EXTERNAL_DB indicates tests prefer external databases over SQLite. Without access to comprehensive SQLite-specific test coverage verification, manual testing of this migration against SQLite is recommended to confirm all constraint transitions (PK/FK drops, recreations, and the new workspace column additions) execute successfully.

tests/db/test_workspace_migration.py (3)

160-342: Excellent use of raw SQL for pre-migration seeding.

Using raw SQL to insert pre-migration data (without workspace columns) ensures the migration is truly responsible for adding and backfilling these fields. This is a best practice for testing migrations.


704-829: Comprehensive upgrade test coverage.

The test verifies:

  • Workspace column presence and defaults
  • Data backfilling to 'default' workspace
  • Primary key updates
  • Foreign key relationships
  • Unique constraints
  • Index creation
  • Workspace catalog seeding

795-806: MSSQL constraint reflection workaround is appropriate.

The test correctly handles MSSQL's NotImplementedError for get_unique_constraints by catching the exception and setting unique_experiments = None. This aligns with the migration's MSSQL-specific handling.

mlflow/store/model_registry/sqlalchemy_store.py (1)

198-198: Workspace is hardcoded to DEFAULT_WORKSPACE_NAME.

All entity creation operations use DEFAULT_WORKSPACE_NAME as a hardcoded value. This is appropriate for the current phase of the migration but indicates that multi-workspace support is not yet implemented at the API level.

Verify that:

  1. There's a plan to add workspace parameter to the public API methods
  2. The hardcoded workspace aligns with the migration's server_default behavior
  3. Future PRs will enable dynamic workspace selection

This is noted in the migration file's TODO comment about removing server_default once stores support workspaces properly.

Based on learnings

Also applies to: 204-204, 704-704, 813-813, 819-819, 1206-1206, 1268-1268

tests/db/schemas/mysql.sql (1)

29-29: LGTM! MySQL schema matches PostgreSQL structure.

The workspace changes are consistent across dialects:

  • Same column definitions (VARCHAR(63), DEFAULT 'default', NOT NULL)
  • Same constraint structure (composite PKs, workspace-qualified FKs)
  • Proper MySQL-specific syntax (_utf8mb4 charset, CHECK constraint format)

Also applies to: 41-44, 86-87, 106-110, 198-200, 208-210, 218-220, 372-374

tests/db/schemas/sqlite.sql (1)

29-29: LGTM! SQLite schema is consistent with other dialects.

The workspace schema changes match PostgreSQL and MySQL:

  • Identical column definitions and defaults
  • Same composite key structure
  • SQLite-appropriate syntax for constraints

All three dialect schemas (PostgreSQL, MySQL, SQLite) are now consistent with the workspace multi-tenancy model.

Also applies to: 41-43, 86-87, 106-110, 199-200, 209-210, 219-220, 373-374

mlflow/store/model_registry/dbmodels/models.py (1)

252-261: LGTM: Correct FK constraint with both onupdate and ondelete cascade.

This SqlRegisteredModelAlias FK constraint correctly includes both onupdate="cascade" and ondelete="cascade", ensuring proper referential integrity at the database level. This is the pattern that should be followed by the other models.

Comment on lines +42 to +47
workspace = Column(
String(63),
nullable=False,
default=DEFAULT_WORKSPACE_NAME,
server_default=sa.text(f"'{DEFAULT_WORKSPACE_NAME}'"),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Critical: Add foreign key constraints from workspace columns to workspaces table.

All workspace columns across SqlRegisteredModel, SqlModelVersion, SqlRegisteredModelTag, SqlModelVersionTag, SqlRegisteredModelAlias, and SqlWebhook lack foreign key constraints to a workspaces table. This mirrors the schema file issues and allows invalid workspace values.

Verify that a SqlWorkspace model exists and add FK constraints:

If SqlWorkspace exists, add FK constraints. For example, in SqlRegisteredModel.__table_args__:

__table_args__ = (
    ForeignKeyConstraint(
        ["workspace"],
        ["workspaces.name"],
        name="fk_registered_models_workspace",
    ),
    PrimaryKeyConstraint("workspace", "name", name="registered_model_pk"),
)

Apply similar changes to SqlWebhook and any other root-level models with workspace columns. Child models like SqlModelVersion inherit workspace validation through their composite FKs to parent tables.

Also applies to: 88-93, 158-163, 196-201, 236-241, 301-306


🏁 Script executed:

#!/bin/bash
# Search for SqlWorkspace model definition
ast-grep --pattern 'class SqlWorkspace($$$)'

# Search for workspace-related models and imports
rg -n "SqlWorkspace|class.*Workspace.*Base" --type=py

Length of output: 1377


I found that SqlWorkspace exists. Now I need to verify whether FK constraints have already been added to the workspace columns in the models mentioned in the review comment.

Now I need to directly inspect the models file to check if FK constraints have been added:


🏁 Script executed:

#!/bin/bash
# Extract workspace column and __table_args__ definitions from all relevant models
# Check SqlRegisteredModel, SqlModelVersion, SqlRegisteredModelTag, SqlModelVersionTag, 
# SqlRegisteredModelAlias, and SqlWebhook

cat mlflow/store/model_registry/dbmodels/models.py | head -350 | tail -100

echo "---"

# Search for __table_args__ definitions containing ForeignKeyConstraint
rg -n "ForeignKeyConstraint|__table_args__" mlflow/store/model_registry/dbmodels/models.py -A 5 -B 2

Length of output: 6137


Critical: FK constraints to workspaces table are missing from SqlRegisteredModel and SqlWebhook.

Verification confirms that SqlWorkspace exists (mlflow/store/workspace/dbmodels/models.py:10) with the workspaces table keyed on "name". However, FK constraints are absent from root-level models:

  • SqlRegisteredModel (line 57): __table_args__ contains only PrimaryKeyConstraint, no FK to workspaces table
  • SqlWebhook (lines 317-322): __table_args__ contains only PrimaryKeyConstraint and Indexes, no FK to workspaces table

While child models have composite FKs to their parents, these do not enforce that workspace values exist in the workspaces table. Add direct FK constraints in:

  1. SqlRegisteredModel.table_args (line 57): Add constraint ForeignKeyConstraint(["workspace"], ["workspaces.name"], name="fk_registered_models_workspace")
  2. SqlWebhook.table_args (lines 317-322): Add constraint ForeignKeyConstraint(["workspace"], ["workspaces.name"], name="fk_webhooks_workspace")

Child models' workspace validation will cascade through their composite FKs to parents once root models enforce workspace existence.

🤖 Prompt for AI Agents
In mlflow/store/model_registry/dbmodels/models.py around lines 42–47 and
specifically update SqlRegisteredModel.__table_args__ at ~line 57 and
SqlWebhook.__table_args__ at ~lines 317–322: add a ForeignKeyConstraint on the
"workspace" column referencing workspaces.name for each root model (name them
fk_registered_models_workspace and fk_webhooks_workspace respectively) so the
root models enforce that workspace values exist in the workspaces table; ensure
you insert the ForeignKeyConstraint entries into the existing __table_args__
tuple alongside the current PrimaryKeyConstraint/Indexes without removing them.

Comment on lines +105 to +109
CREATE TABLE workspaces (
name VARCHAR(63) COLLATE "SQL_Latin1_General_CP1_CI_AS" NOT NULL,
description VARCHAR COLLATE "SQL_Latin1_General_CP1_CI_AS",
CONSTRAINT workspaces_pk PRIMARY KEY (name)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Add foreign key constraints from workspace columns to the workspaces table.

Same issue as in latest_schema.sql: the MSSQL schema lacks foreign key constraints from workspace columns to the workspaces catalog table. This allows invalid workspace references.

Add FK constraints for MSSQL dialect:

-- Add after evaluation_datasets table definition
ALTER TABLE evaluation_datasets ADD CONSTRAINT fk_evaluation_datasets_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);

-- Add after experiments table definition  
ALTER TABLE experiments ADD CONSTRAINT fk_experiments_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);

-- Add after registered_models table definition
ALTER TABLE registered_models ADD CONSTRAINT fk_registered_models_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);

-- Add after webhooks table definition
ALTER TABLE webhooks ADD CONSTRAINT fk_webhooks_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);
🤖 Prompt for AI Agents
In tests/db/schemas/mssql.sql around lines 105 to 109 the workspaces table is
defined but there are no foreign key constraints from other tables' workspace
columns to workspaces(name); add ALTER TABLE statements after each relevant
table definition (evaluation_datasets, experiments, registered_models, webhooks)
to create FK constraints (e.g. fk_evaluation_datasets_workspace,
fk_experiments_workspace, fk_registered_models_workspace, fk_webhooks_workspace)
that reference workspaces(name) so workspace columns cannot contain invalid
values.

Comment on lines +196 to 198
CONSTRAINT model_version_pk PRIMARY KEY (workspace, name, version),
CONSTRAINT fk_model_versions_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON UPDATE CASCADE
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major: Inconsistent ON DELETE CASCADE in MSSQL schema.

The MSSQL schema has the same inconsistent cascade behavior as latest_schema.sql:

  • registered_model_aliases (line 207) has ON DELETE CASCADE
  • model_versions (line 197), registered_model_tags (line 217), and model_version_tags (line 366) lack ON DELETE CASCADE

Apply the same fix as in latest_schema.sql:

 	workspace VARCHAR(63) COLLATE "SQL_Latin1_General_CP1_CI_AS" DEFAULT ('default') NOT NULL,
 	CONSTRAINT model_version_pk PRIMARY KEY (workspace, name, version),
-	CONSTRAINT fk_model_versions_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON UPDATE CASCADE
+	CONSTRAINT fk_model_versions_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON DELETE CASCADE ON UPDATE CASCADE
 )

 ...

 	workspace VARCHAR(63) COLLATE "SQL_Latin1_General_CP1_CI_AS" DEFAULT ('default') NOT NULL,
 	CONSTRAINT registered_model_tag_pk PRIMARY KEY (workspace, key, name),
-	CONSTRAINT fk_registered_model_tags_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON UPDATE CASCADE
+	CONSTRAINT fk_registered_model_tags_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON DELETE CASCADE ON UPDATE CASCADE
 )

 ...

 	workspace VARCHAR(63) COLLATE "SQL_Latin1_General_CP1_CI_AS" DEFAULT ('default') NOT NULL,
 	CONSTRAINT model_version_tag_pk PRIMARY KEY (workspace, key, name, version),
-	CONSTRAINT fk_model_version_tags_model_versions FOREIGN KEY(workspace, name, version) REFERENCES model_versions (workspace, name, version) ON UPDATE CASCADE
+	CONSTRAINT fk_model_version_tags_model_versions FOREIGN KEY(workspace, name, version) REFERENCES model_versions (workspace, name, version) ON DELETE CASCADE ON UPDATE CASCADE
 )

Also applies to: 206-208, 216-218, 365-367

🤖 Prompt for AI Agents
In tests/db/schemas/mssql.sql around lines 196-198 (and also update 206-208,
216-218, 365-367), the FOREIGN KEY constraints for model_versions,
registered_model_tags, and model_version_tags are missing ON DELETE CASCADE
causing inconsistent cascade behavior vs registered_model_aliases; modify each
referenced FOREIGN KEY ... REFERENCES ... clauses to append ON DELETE CASCADE
(matching latest_schema.sql) so deletions of registered_models or model_versions
cascade to their dependent rows.

Comment on lines +106 to +110
CREATE TABLE workspaces (
name VARCHAR(63) NOT NULL,
description TEXT,
CONSTRAINT workspaces_pk PRIMARY KEY (name)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Add foreign key constraints from workspace columns to the workspaces table.

The new workspaces catalog table is defined, but none of the tables with workspace columns have foreign key constraints referencing it. This allows insertion of invalid workspace values (e.g., workspace='nonexistent') that don't exist in the workspaces table, defeating the purpose of the catalog.

Add FK constraints for these root tables:

  • evaluation_datasets.workspace → workspaces.name
  • experiments.workspace → workspaces.name
  • registered_models.workspace → workspaces.name
  • webhooks.workspace → workspaces.name

Child tables (model_versions, registered_model_aliases, etc.) inherit workspace validation through their composite FK constraints to parent tables.

-- Add after evaluation_datasets table definition
ALTER TABLE evaluation_datasets ADD CONSTRAINT fk_evaluation_datasets_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);

-- Add after experiments table definition
ALTER TABLE experiments ADD CONSTRAINT fk_experiments_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);

-- Add after registered_models table definition
ALTER TABLE registered_models ADD CONSTRAINT fk_registered_models_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);

-- Add after webhooks table definition
ALTER TABLE webhooks ADD CONSTRAINT fk_webhooks_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);
🤖 Prompt for AI Agents
In tests/resources/db/latest_schema.sql around lines 106 to 110, the new
workspaces table is created but other root tables with a workspace column lack
foreign key constraints, allowing invalid workspace values; add ALTER TABLE
statements after the respective table definitions to add FK constraints: ALTER
TABLE evaluation_datasets ADD CONSTRAINT fk_evaluation_datasets_workspace
FOREIGN KEY(workspace) REFERENCES workspaces(name); ALTER TABLE experiments ADD
CONSTRAINT fk_experiments_workspace FOREIGN KEY(workspace) REFERENCES
workspaces(name); ALTER TABLE registered_models ADD CONSTRAINT
fk_registered_models_workspace FOREIGN KEY(workspace) REFERENCES
workspaces(name); ALTER TABLE webhooks ADD CONSTRAINT fk_webhooks_workspace
FOREIGN KEY(workspace) REFERENCES workspaces(name).

Comment on lines +199 to 201
CONSTRAINT model_version_pk PRIMARY KEY (workspace, name, version),
CONSTRAINT fk_model_versions_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON UPDATE CASCADE
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major: Inconsistent ON DELETE CASCADE behavior in foreign key constraints.

The foreign key constraints have inconsistent cascade deletion behavior:

  • Line 210: registered_model_aliases has both ON DELETE CASCADE ON UPDATE CASCADE
  • Line 200: model_versions has only ON UPDATE CASCADE (missing ON DELETE)
  • Line 220: registered_model_tags has only ON UPDATE CASCADE (missing ON DELETE)
  • Line 374: model_version_tags has only ON UPDATE CASCADE (missing ON DELETE)

This inconsistency means deleting a registered_model would cascade to aliases but leave orphaned tags and model versions, requiring manual cleanup. Either:

  1. Add ON DELETE CASCADE to all these constraints for automatic cleanup, or
  2. Document why some relationships should preserve child records on parent deletion (though this seems unlikely for model metadata)

Apply this diff for consistency:

 	workspace VARCHAR(63) DEFAULT 'default' NOT NULL,
 	CONSTRAINT model_version_pk PRIMARY KEY (workspace, name, version),
-	CONSTRAINT fk_model_versions_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON UPDATE CASCADE
+	CONSTRAINT fk_model_versions_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON DELETE CASCADE ON UPDATE CASCADE
 )

 ...

 	workspace VARCHAR(63) DEFAULT 'default' NOT NULL,
 	CONSTRAINT registered_model_tag_pk PRIMARY KEY (workspace, key, name),
-	CONSTRAINT fk_registered_model_tags_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON UPDATE CASCADE
+	CONSTRAINT fk_registered_model_tags_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON DELETE CASCADE ON UPDATE CASCADE
 )

 ...

 	workspace VARCHAR(63) DEFAULT 'default' NOT NULL,
 	CONSTRAINT model_version_tag_pk PRIMARY KEY (workspace, key, name, version),
-	CONSTRAINT fk_model_version_tags_model_versions FOREIGN KEY(workspace, name, version) REFERENCES model_versions (workspace, name, version) ON UPDATE CASCADE
+	CONSTRAINT fk_model_version_tags_model_versions FOREIGN KEY(workspace, name, version) REFERENCES model_versions (workspace, name, version) ON DELETE CASCADE ON UPDATE CASCADE
 )

Also applies to: 209-211, 219-221, 373-375

🤖 Prompt for AI Agents
In tests/resources/db/latest_schema.sql around lines 199-201 (and also update
the related FK definitions at 209-211, 219-221, and 373-375), the foreign key
constraints lack ON DELETE CASCADE causing inconsistent cascade behavior; update
each referenced FOREIGN KEY(...) REFERENCES ... ON UPDATE CASCADE to include ON
DELETE CASCADE as well so that deleting a registered_models row will
automatically delete related model_versions, registered_model_tags,
model_version_tags (and any other child tables listed), ensuring consistent
cleanup across these relationships.

@mprahl mprahl force-pushed the workspaces-db-model branch from 72bfda2 to fea3308 Compare November 19, 2025 02:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mlflow/store/model_registry/sqlalchemy_store.py (1)

226-231: Critical: Query must filter by workspace to prevent ambiguity.

The query filters by name only (line 229) but the new schema has a composite primary key (workspace, name). Without filtering by workspace, this query can return multiple registered models with the same name from different workspaces, causing the error at lines 237-241.

Add workspace filtering to the query:

     query_options = cls._get_eager_registered_model_query_options() if eager else []
     rms = (
         session.query(SqlRegisteredModel)
         .options(*query_options)
         .filter(SqlRegisteredModel.name == name)
+        .filter(SqlRegisteredModel.workspace == DEFAULT_WORKSPACE_NAME)
         .all()
     )

Note: If future multi-tenant support requires passing workspace as a parameter, update the method signature accordingly.

♻️ Duplicate comments (9)
tests/resources/db/latest_schema.sql (4)

29-43: Foreign key constraint to workspaces table is missing.

The evaluation_datasets and experiments tables now have workspace columns but lack foreign key constraints to the workspaces table. This allows invalid workspace values to be inserted.


86-87: Foreign key constraint to workspaces table is missing.

The registered_models table now has a workspace column but lacks a foreign key constraint to the workspaces table.


101-101: Foreign key constraint to workspaces table is missing.

The webhooks table now has a workspace column but lacks a foreign key constraint to the workspaces table.


198-201: Inconsistent ON DELETE CASCADE behavior in foreign key constraints.

The foreign key constraints have inconsistent cascade deletion behavior across model registry tables. This can lead to orphaned records when registered models are deleted.

tests/db/check_migration.py (1)

129-132: Workspace validation will fail for empty tables; use shared constant.

The post-migration assertions have two issues:

  1. For empty tables, set(df["workspace"]) == {"default"} becomes set() == {"default"}False, causing spurious failures
  2. The hardcoded "default" string should use the shared constant
mlflow/store/model_registry/sqlalchemy_store.py (4)

702-706: Verify: Hardcoded workspace limits multi-tenancy.

Similar to model creation, the workspace parameter is hardcoded to DEFAULT_WORKSPACE_NAME when setting tags. This prevents workspace-scoped tag management.

Verify whether the workspace should be derived from the parent registered model:

#!/bin/bash
# Check if workspace should be obtained from the registered model
rg -n "set_registered_model_tag.*workspace" --type=py -A5 -B5

813-813: Verify: Hardcoded workspace limits multi-tenancy.

Model version creation also hardcodes workspace to DEFAULT_WORKSPACE_NAME (lines 813, 819). Since model versions reference registered models via composite FK (workspace, name), the workspace must match the parent model's workspace for referential integrity.

Verify that workspace is correctly inherited from the parent registered model:

#!/bin/bash
# Check if workspace should be obtained from sql_registered_model
ast-grep --pattern 'sql_registered_model = $$$
$$$
model_version = SqlModelVersion($$$)'

Also applies to: 819-819


1204-1212: Verify: Hardcoded workspace limits multi-tenancy.

Model version tag creation hardcodes workspace to DEFAULT_WORKSPACE_NAME. Similar to model versions, tags must match their parent model version's workspace for FK integrity.


1266-1270: Verify: Hardcoded workspace limits multi-tenancy.

Alias creation also hardcodes workspace to DEFAULT_WORKSPACE_NAME. The workspace must match the parent registered model for FK integrity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72bfda2 and fea3308.

📒 Files selected for processing (18)
  • docs/api_reference/api_inventory.txt (2 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/model_registry/sqlalchemy_store.py (6 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/schemas/mssql.sql (6 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
  • tests/resources/db/latest_schema.sql (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • mlflow/entities/init.py
  • mlflow/utils/workspace_utils.py
  • mlflow/store/workspace/dbmodels/init.py
  • docs/api_reference/api_inventory.txt
🧰 Additional context used
🧬 Code graph analysis (7)
tests/db/check_migration.py (1)
mlflow/store/model_registry/sqlalchemy_store.py (1)
  • create_registered_model (172-214)
tests/db/test_schema.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
  • _get_unique_constraints (141-156)
tests/db/test_workspace_migration.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)
  • upgrade (135-612)
  • downgrade (615-910)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
tests/db/test_schema.py (1)
  • _get_unique_constraints (90-107)
mlflow/store/model_registry/sqlalchemy_store.py (1)
mlflow/store/model_registry/dbmodels/models.py (3)
  • SqlRegisteredModelTag (155-190)
  • SqlModelVersionTag (193-230)
  • SqlRegisteredModelAlias (233-268)
mlflow/store/workspace/dbmodels/models.py (1)
mlflow/entities/workspace.py (1)
  • Workspace (9-13)
mlflow/store/model_registry/dbmodels/models.py (1)
mlflow/entities/model_registry/registered_model.py (4)
  • name (44-46)
  • name (49-50)
  • description (69-71)
  • description (74-75)
🪛 GitHub Actions: Lint
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py

[error] 684-684: F841 Local variable 'fk_constraints' is assigned to but never used. Remove assignment to unused variable.


[error] 691-691: F841 Local variable 'pk_constraints' is assigned to but never used. Remove assignment to unused variable.


[error] 699-699: F841 Local variable 'indexes_to_drop' is assigned to but never used. Remove assignment to unused variable.


[error] 707-707: F841 Local variable 'legacy_pk_defs' is assigned to but never used. Remove assignment to unused variable.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (33)
  • GitHub Check: windows (4)
  • GitHub Check: windows (1)
  • GitHub Check: pyfunc (2)
  • GitHub Check: genai
  • GitHub Check: java
  • GitHub Check: windows (2)
  • GitHub Check: windows (3)
  • GitHub Check: pyfunc (4)
  • GitHub Check: database
  • GitHub Check: pyfunc (3)
  • GitHub Check: pyfunc (1)
  • GitHub Check: evaluate (2)
  • GitHub Check: evaluate (1)
  • GitHub Check: models (2)
  • GitHub Check: models (1)
  • GitHub Check: flavors
  • GitHub Check: python (1)
  • GitHub Check: python (2)
  • GitHub Check: python (3)
  • GitHub Check: core
  • GitHub Check: build (tracing)
  • GitHub Check: python-skinny
  • GitHub Check: r
  • GitHub Check: build (skinny)
  • GitHub Check: build (dev)
  • GitHub Check: typescript-sdk (22)
  • GitHub Check: js (windows-latest, --testPathIgnorePatterns)
  • GitHub Check: js (windows-latest, --testPathPattern)
  • GitHub Check: js (ubuntu-latest, --testPathIgnorePatterns)
  • GitHub Check: js (ubuntu-latest, --testPathPattern)
  • GitHub Check: typescript-sdk (24)
  • GitHub Check: typescript-sdk (20)
  • GitHub Check: build
🔇 Additional comments (23)
mlflow/store/tracking/dbmodels/models.py (5)

64-64: LGTM! Import added for workspace support.

The import of DEFAULT_WORKSPACE_NAME is necessary for the workspace column defaults defined below.


98-111: LGTM! Workspace column and constraint implementation.

The changes properly implement workspace-scoped experiments:

  • Name column is now nullable=False without the unique constraint (moved to composite)
  • Workspace column with proper defaults ensures backward compatibility
  • Documentation clearly explains the purpose

137-137: LGTM! Composite unique constraint ensures per-workspace name uniqueness.

The UniqueConstraint on (workspace, name) correctly replaces the previous simple unique constraint on name alone, enabling multi-tenant workspace isolation.


1315-1323: LGTM! Workspace column added to SqlEvaluationDataset.

The workspace column definition matches the pattern used for SqlExperiment, with proper defaults for legacy compatibility.


1379-1379: LGTM! Index added for workspace filtering.

The index on the workspace column will optimize workspace-scoped queries for evaluation datasets.

tests/db/check_migration.py (2)

52-61: LGTM! Workspace tables constant defined.

The WORKSPACE_TABLES set correctly identifies all tables that should have workspace columns after migration.


93-95: LGTM! Additional resources created for comprehensive workspace testing.

Creating extra experiments and registered models ensures the workspace backfill is properly tested across multiple resources.

mlflow/entities/workspace.py (1)

1-13: LGTM! Clean and efficient Workspace entity definition.

The dataclass is well-designed:

  • frozen=True ensures immutability
  • slots=True optimizes memory usage
  • Minimal fields (name and optional description) keep the interface simple
mlflow/store/workspace/dbmodels/models.py (1)

10-22: LGTM! SqlWorkspace ORM model correctly defined.

The implementation properly:

  • Defines the workspaces table structure
  • Sets name as the primary key
  • Provides conversion to the Workspace entity
tests/db/test_schema.py (4)

2-16: LGTM! Proper setup for dialect-aware constraint handling.

The imports and dialect mapping are well-organized to handle database-specific unique constraint reflection behavior.


36-36: LGTM! Integration of missing constraint reattachment.

Calling _reattach_missing_unique_constraints before dumping the schema ensures the in-memory metadata correctly represents all unique constraints across dialects.


46-87: LGTM! Robust dialect-aware constraint reattachment logic.

The function properly handles:

  • Dialect-specific constraint reflection differences
  • MySQL's unique indexes vs. unique constraints
  • MSSQL's lack of get_unique_constraints implementation
  • Logging of skipped constraints due to missing columns

90-107: LGTM! Fallback logic for unique constraint retrieval.

The helper function provides a unified interface for getting unique constraints, with proper fallback to indexes for dialects that don't expose constraints directly.

mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)

135-613: LGTM! Comprehensive upgrade logic with proper dialect handling.

The upgrade function properly:

  • Detects and handles dialect-specific behaviors (SQLite, MSSQL, MySQL)
  • Adds workspace columns with appropriate defaults
  • Updates primary keys and foreign keys to include workspace
  • Creates necessary indexes
  • Seeds the default workspace

626-646: LGTM! Robust conflict detection prevents data loss on downgrade.

The _assert_no_workspace_conflicts function properly:

  • Detects duplicate resources that would be created when merging workspaces
  • Provides clear error messages with examples
  • Aborts the downgrade to prevent data corruption
tests/db/test_workspace_migration.py (7)

47-55: LGTM! Workspace tables constant for test validation.

The _WORKSPACE_TABLES tuple correctly identifies all tables that receive workspace columns, enabling systematic validation across the test suite.


103-114: LGTM! MSSQL-specific identity insert handling.

The context manager properly handles MSSQL's IDENTITY_INSERT requirement for explicit ID insertion during testing.


160-341: LGTM! Comprehensive legacy data seeding for migration testing.

The _seed_pre_workspace_entities function properly:

  • Uses raw SQL matching the pre-migration schema (no workspace columns)
  • Seeds a representative set of entities across all workspace-backed tables
  • Ensures the migration is fully responsible for adding/backfilling workspace columns

704-829: LGTM! Thorough upgrade validation.

The test comprehensively validates:

  • Workspace columns are added with correct defaults
  • Legacy data is backfilled with "default" workspace
  • Primary keys and foreign keys are updated to include workspace
  • Required indexes are created
  • Workspaces table is created and seeded

832-922: LGTM! Downgrade validation ensures clean rollback.

The test verifies:

  • Workspace columns are removed
  • Workspaces table is dropped
  • Primary keys and foreign keys revert to legacy structure
  • Data from multiple workspaces is merged into default

979-1013: LGTM! Conflict detection prevents data loss.

The parameterized test ensures the downgrade properly detects and aborts when merging workspaces would create duplicate resources (experiments, models, datasets).


1016-1059: LGTM! Granular conflict query validation.

The test validates that the conflict detection queries work correctly for child tables (model_versions, tags, aliases) that require checking multiple columns.

mlflow/store/model_registry/sqlalchemy_store.py (1)

198-198: Hardcoded workspace confirmed—blocks multi-tenancy.

The workspace parameter is hardcoded to DEFAULT_WORKSPACE_NAME at line 198 (model creation) and line 204 (tag creation). The create_registered_model method signature includes no workspace parameter, so callers cannot specify a target workspace. All registered models will be created in the "default" workspace, preventing actual workspace isolation despite the database schema supporting it.

Also applies to: 204-204

@mprahl mprahl force-pushed the workspaces-db-model branch from fea3308 to bd0a834 Compare November 19, 2025 02:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (6)
mlflow/store/model_registry/dbmodels/models.py (2)

126-133: Consider adding ondelete="cascade" to composite FKs for versions and tags

The composite foreign keys:

  • SqlModelVersion.__table_args__ (["workspace", "name"]registered_models)
  • SqlRegisteredModelTag.__table_args__ (["workspace", "name"]registered_models)
  • SqlModelVersionTag.__table_args__ (["workspace", "name", "version"]model_versions)

all specify onupdate="cascade" but no ondelete, whereas SqlRegisteredModelAlias does use ondelete="cascade". Without DB‑level cascading, deleting a registered model or model version directly at the SQL layer can leave dangling versions/tags until the application cleans them up.

For consistency with SqlRegisteredModelAlias and to harden referential integrity, it’s worth considering ondelete="cascade" on these FKs as well.

Also applies to: 176-183, 211-218


42-49: Add FKs from workspace to the workspaces catalog for registered models and webhooks

The new workspace columns on SqlRegisteredModel and SqlWebhook are not backed by ForeignKeyConstraints to the workspaces table (see workspaces.name in tests/db/schemas/postgresql.sql), so the DB will happily accept non‑existent or mistyped workspace names even though you now maintain a catalog.

Consider adding explicit FK constraints, for example:

  • SqlRegisteredModel.__table_args__: FK ["workspace"] -> ["workspaces.name"]
  • SqlWebhook.__table_args__: FK ["workspace"] -> ["workspaces.name"]

to enforce integrity at the database level in addition to any application‑level checks.

Also applies to: 57-58, 301-306, 317-322

tests/resources/db/latest_schema.sql (2)

106-110: Critical: Missing foreign key constraints to workspaces table.

Past reviews have already identified this critical issue. The workspaces catalog table exists but root tables with workspace columns lack FK constraints to enforce referential integrity.


199-201: Major: Inconsistent ON DELETE CASCADE in foreign keys.

Past reviews have already flagged the inconsistent cascade deletion behavior. The registered_model_aliases constraint includes ON DELETE CASCADE but model_versions, registered_model_tags, and model_version_tags do not, creating orphaned records on deletion.

Also applies to: 209-211, 219-221, 373-375

tests/db/schemas/mssql.sql (2)

105-109: Critical: MSSQL schema missing foreign key constraints to workspaces table.

Past reviews have already identified this critical issue for the MSSQL schema. The workspaces table is defined but workspace columns lack FK constraints to enforce referential integrity.


196-198: Major: MSSQL schema has inconsistent ON DELETE CASCADE.

Past reviews have already flagged the inconsistent cascade deletion behavior in the MSSQL schema, where registered_model_aliases cascades on delete but model_versions and related tables do not.

Also applies to: 206-208, 216-218, 365-367

🧹 Nitpick comments (2)
mlflow/store/model_registry/dbmodels/models.py (1)

271-295: Tighten webhook secret encryption ergonomics (key source and column length)

The EncryptedString type works, but two edge cases are worth considering:

  • If MLFLOW_WEBHOOK_SECRET_ENCRYPTION_KEY is unset, you generate a fresh random key per process. Any secrets written in one process become undecryptable after a restart, which can be surprising even in non‑prod environments. You might prefer to fail fast (or log loudly) when the env var is missing outside tests/dev.
  • impl = String(1000) was fine for plaintext, but Fernet ciphertext is longer than the input. Very long secrets (near 1000 chars) risk truncation and decryption failure. Bumping the underlying length (or switching to an unbounded Text type for DB storage) would make this safer.

Neither is a blocker, but adjusting these now will avoid hard‑to‑debug webhook issues later.

Also applies to: 312-312

tests/db/test_schema.py (1)

90-107: Minor: _get_unique_constraints’s dialect argument is unused and logic duplicates migration helper

_get_unique_constraints currently ignores its dialect parameter and re‑implements logic that’s very similar to the helper in 1b5f0d9ad7c1_add_workspace_columns_and_catalog.py. As a small cleanup, you could either:

  • drop the unused dialect argument here, or
  • refactor both callers to share a single helper (so any future dialect fixes only need to be made in one place).

This is purely a maintainability nit; behavior is fine as‑is.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fea3308 and bd0a834.

📒 Files selected for processing (18)
  • docs/api_reference/api_inventory.txt (2 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/model_registry/sqlalchemy_store.py (6 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/schemas/mssql.sql (13 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
  • tests/resources/db/latest_schema.sql (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • mlflow/entities/workspace.py
  • mlflow/store/workspace/dbmodels/models.py
  • tests/db/check_migration.py
  • mlflow/store/model_registry/sqlalchemy_store.py
  • docs/api_reference/api_inventory.txt
  • mlflow/entities/init.py
  • tests/db/test_workspace_migration.py
  • mlflow/store/workspace/dbmodels/init.py
🧰 Additional context used
🧬 Code graph analysis (3)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
tests/db/test_schema.py (1)
  • _get_unique_constraints (90-107)
tests/db/test_schema.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
  • _get_unique_constraints (141-155)
mlflow/store/model_registry/dbmodels/models.py (1)
mlflow/entities/model_registry/registered_model.py (4)
  • name (44-46)
  • name (49-50)
  • description (69-71)
  • description (74-75)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (35)
  • GitHub Check: pyfunc (2)
  • GitHub Check: pyfunc (1)
  • GitHub Check: pyfunc (4)
  • GitHub Check: pyfunc (3)
  • GitHub Check: windows (4)
  • GitHub Check: windows (3)
  • GitHub Check: windows (2)
  • GitHub Check: windows (1)
  • GitHub Check: database
  • GitHub Check: python (3)
  • GitHub Check: python (2)
  • GitHub Check: python (1)
  • GitHub Check: python-skinny
  • GitHub Check: genai
  • GitHub Check: evaluate (1)
  • GitHub Check: evaluate (2)
  • GitHub Check: models (2)
  • GitHub Check: models (1)
  • GitHub Check: java
  • GitHub Check: flavors
  • GitHub Check: js (windows-latest, --testPathIgnorePatterns)
  • GitHub Check: js (windows-latest, --testPathPattern)
  • GitHub Check: js (ubuntu-latest, --testPathPattern)
  • GitHub Check: js (ubuntu-latest, --testPathIgnorePatterns)
  • GitHub Check: typescript-sdk (22)
  • GitHub Check: typescript-sdk (24)
  • GitHub Check: typescript-sdk (20)
  • GitHub Check: protect
  • GitHub Check: build (skinny)
  • GitHub Check: build (dev)
  • GitHub Check: r
  • GitHub Check: build (tracing)
  • GitHub Check: lint-macos
  • GitHub Check: build
  • GitHub Check: core
🔇 Additional comments (6)
mlflow/utils/workspace_utils.py (1)

1-5: DEFAULT_WORKSPACE_NAME helper looks good and keeps workspace defaults centralized

The module-level constant and explicit __all__ export are clear and align with how the rest of the PR uses workspace defaults.

mlflow/store/tracking/dbmodels/models.py (1)

64-64: Workspace scoping for experiments and evaluation datasets looks consistent; double‑check callers that assume globally unique names

  • SqlExperiment now enforces uniqueness on (workspace, name) via uq_experiments_workspace_name and adds a non‑nullable workspace column with a default of "default".
  • SqlEvaluationDataset similarly gains a non‑nullable workspace with default plus a workspace index.

This lines up with the new Postgres schema and the multi‑workspace model, but it does relax the old invariant that experiment names are globally unique. Please confirm higher‑level code paths (e.g. “get by name” lookups, migrations, and uniqueness checks) have been updated to supply or infer the correct workspace rather than assuming a single global namespace.

Also applies to: 98-111, 131-138, 1315-1323, 1375-1380

tests/db/schemas/postgresql.sql (1)

19-31: Postgres schema matches the new workspace‑aware models; keep constraint names in sync with tests

The updated Postgres DDL for:

  • experiments (workspace column + uq_experiments_workspace_name),
  • registered_models / model_versions / registered_model_aliases / registered_model_tags / model_version_tags,
  • evaluation_datasets and webhooks,
  • and the new workspaces table,

is consistent with the corresponding SQLAlchemy models and composite keys in this PR. Note that tests/db/test_schema.py now hard‑codes "uq_experiments_workspace_name" for dialect‑specific reflection fixes, so if you ever rename that constraint you’ll need to update both this schema file and the _DIALECT_REFLECTED_UNIQUE_CONSTRAINTS mapping together.

Also applies to: 34-45, 81-88, 91-103, 106-110, 184-201, 204-221, 365-373

tests/db/test_schema.py (1)

2-3: Dialect‑aware reattachment of missing UniqueConstraints is sensible; please validate on MySQL and MSSQL

The _reattach_missing_unique_constraints + _get_unique_constraints flow is a pragmatic way to ensure uq_experiments_workspace_name is present in reflected metadata for MySQL and MSSQL before generating the schema string. Using inspector APIs first and falling back to get_indexes when needed should handle the major dialect quirks.

Given how finicky SQLAlchemy’s dialects can be here, it’s worth explicitly verifying on real MySQL and MSSQL instances that:

  • uq_experiments_workspace_name is reattached exactly once,
  • no spurious extra UniqueConstraints are added, and
  • dump_schema output now matches the updated schemas/*.sql across all supported DBs.

Also applies to: 8-16, 32-37, 46-88, 90-107

mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)

133-134: Note: server_default is temporary scaffolding.

The comment indicates that the server_default='default' is temporary until tracking and model registry stores properly set workspace values. Once store-level support is complete, the server_default should be removed to ensure explicit workspace assignment.

Track this as a follow-up task to remove the server_default once stores are updated.


621-641: LGTM: Conflict detection prevents unsafe downgrade.

The _assert_no_workspace_conflicts function correctly prevents downgrade when merging workspaces would create duplicate resources. The error message provides clear guidance with specific conflicting values (up to 5 examples), making it actionable for operators.

Comment on lines +229 to +405
with _with_batch("experiments") as batch_op:
if experiments_unique_constraint:
batch_op.drop_constraint(experiments_unique_constraint, type_="unique")
elif experiments_unique_index:
batch_op.drop_index(experiments_unique_index)
batch_op.add_column(
sa.Column(
"workspace",
sa.String(length=63),
nullable=False,
server_default=sa.text("'default'"),
)
)
batch_op.create_unique_constraint(
"uq_experiments_workspace_name",
["workspace", "name"],
)

with _with_batch("registered_models") as batch_op:
if registered_models_unique_constraint:
batch_op.drop_constraint(registered_models_unique_constraint, type_="unique")
elif registered_models_unique_index:
batch_op.drop_index(registered_models_unique_index)
batch_op.add_column(
sa.Column(
"workspace",
sa.String(length=63),
nullable=False,
server_default=sa.text("'default'"),
)
)
batch_op.drop_constraint("registered_model_pk", type_="primary")
batch_op.create_primary_key("registered_model_pk", ["workspace", "name"])

with _with_batch("model_versions") as batch_op:
batch_op.add_column(
sa.Column(
"workspace",
sa.String(length=63),
nullable=False,
server_default=sa.text("'default'"),
)
)
for fk_name in fk_model_versions:
batch_op.drop_constraint(fk_name, type_="foreignkey")
batch_op.drop_constraint("model_version_pk", type_="primary")
batch_op.create_primary_key("model_version_pk", ["workspace", "name", "version"])
batch_op.create_foreign_key(
"fk_model_versions_registered_models",
"registered_models",
["workspace", "name"],
["workspace", "name"],
onupdate="CASCADE",
)

with _with_batch("registered_model_tags") as batch_op:
batch_op.add_column(
sa.Column(
"workspace",
sa.String(length=63),
nullable=False,
server_default=sa.text("'default'"),
)
)
for fk_name in fk_registered_model_tags:
batch_op.drop_constraint(fk_name, type_="foreignkey")
batch_op.drop_constraint("registered_model_tag_pk", type_="primary")
batch_op.create_primary_key("registered_model_tag_pk", ["workspace", "key", "name"])
batch_op.create_foreign_key(
"fk_registered_model_tags_registered_models",
"registered_models",
["workspace", "name"],
["workspace", "name"],
onupdate="CASCADE",
)

with _with_batch("model_version_tags") as batch_op:
batch_op.add_column(
sa.Column(
"workspace",
sa.String(length=63),
nullable=False,
server_default=sa.text("'default'"),
)
)
for fk_name in fk_model_version_tags:
batch_op.drop_constraint(fk_name, type_="foreignkey")
batch_op.drop_constraint("model_version_tag_pk", type_="primary")
batch_op.create_primary_key(
"model_version_tag_pk",
["workspace", "key", "name", "version"],
)
batch_op.create_foreign_key(
"fk_model_version_tags_model_versions",
"model_versions",
["workspace", "name", "version"],
["workspace", "name", "version"],
onupdate="CASCADE",
)

with _with_batch("registered_model_aliases") as batch_op:
batch_op.add_column(
sa.Column(
"workspace",
sa.String(length=63),
nullable=False,
server_default=sa.text("'default'"),
)
)
for fk_name in fk_registered_model_aliases:
batch_op.drop_constraint(fk_name, type_="foreignkey")
batch_op.drop_constraint("registered_model_alias_pk", type_="primary")
batch_op.create_primary_key(
"registered_model_alias_pk",
["workspace", "name", "alias"],
)
batch_op.create_foreign_key(
"fk_registered_model_aliases_registered_models",
"registered_models",
["workspace", "name"],
["workspace", "name"],
onupdate="CASCADE",
ondelete="CASCADE",
)

with _with_batch("evaluation_datasets") as batch_op:
batch_op.add_column(
sa.Column(
"workspace",
sa.String(length=63),
nullable=False,
server_default=sa.text("'default'"),
)
)

with _with_batch("webhooks") as batch_op:
batch_op.add_column(
sa.Column(
"workspace",
sa.String(length=63),
nullable=False,
server_default=sa.text("'default'"),
)
)

op.create_index("idx_experiments_workspace", "experiments", ["workspace"])
op.create_index("idx_registered_models_workspace", "registered_models", ["workspace"])
op.create_index(
"idx_experiments_workspace_creation_time",
"experiments",
["workspace", "creation_time"],
unique=False,
)
op.create_index("idx_evaluation_datasets_workspace", "evaluation_datasets", ["workspace"])
op.create_index("idx_webhooks_workspace", "webhooks", ["workspace"])

op.create_table(
"workspaces",
sa.Column("name", sa.String(length=63), nullable=False),
sa.Column("description", sa.Text(), nullable=True),
sa.PrimaryKeyConstraint("name", name="workspaces_pk"),
)

metadata = sa.MetaData()
workspaces_table = sa.Table(
"workspaces",
metadata,
sa.Column("name", sa.String(length=63)),
sa.Column("description", sa.Text()),
)

conn.execute(
workspaces_table.insert().values(
name="default",
description="Default workspace for legacy resources",
)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Add foreign key constraints from workspace columns to the workspaces table.

The migration creates the workspaces catalog table and adds workspace columns to eight tables, but does not establish foreign key constraints from those workspace columns to workspaces.name. This allows insertion of invalid workspace values (e.g., workspace='nonexistent') that don't exist in the workspaces table.

Root tables that need FK constraints:

  • evaluation_datasets.workspace → workspaces.name
  • experiments.workspace → workspaces.name
  • registered_models.workspace → workspaces.name
  • webhooks.workspace → workspaces.name

Child tables (model_versions, registered_model_aliases, registered_model_tags, model_version_tags) inherit workspace validation through their composite FK constraints to parent tables, so they don't need direct FKs to workspaces.

For SQLite (batch operations), add the FK constraints within the batch contexts:

         with _with_batch("experiments") as batch_op:
             if experiments_unique_constraint:
                 batch_op.drop_constraint(experiments_unique_constraint, type_="unique")
             elif experiments_unique_index:
                 batch_op.drop_index(experiments_unique_index)
             batch_op.add_column(
                 sa.Column(
                     "workspace",
                     sa.String(length=63),
                     nullable=False,
                     server_default=sa.text("'default'"),
                 )
             )
             batch_op.create_unique_constraint(
                 "uq_experiments_workspace_name",
                 ["workspace", "name"],
             )
+            batch_op.create_foreign_key(
+                "fk_experiments_workspace",
+                "workspaces",
+                ["workspace"],
+                ["name"],
+            )

Apply similar changes for registered_models, evaluation_datasets, and webhooks batch operations, then add the corresponding non-batch FK constraints for non-SQLite dialects after line 574.

🤖 Prompt for AI Agents
In
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py
around lines 229-405, add foreign key constraints from the newly added workspace
columns to workspaces.name for the root tables that currently lack them: inside
the SQLite batch contexts for experiments, registered_models,
evaluation_datasets, and webhooks use batch_op.create_foreign_key(...) to
reference workspaces(name) (e.g., fk_experiments_workspaces -> "workspaces",
["workspace"], ["name"]); do not add direct workspace FKs for child tables
(model_versions, registered_model_aliases, registered_model_tags,
model_version_tags) because they are validated via their composite FKs to parent
tables; additionally, for non-SQLite dialects add corresponding
op.create_foreign_key(...) statements after line 574 so experiments,
registered_models, evaluation_datasets, and webhooks enforce workspace ->
workspaces.name at the non-batch level as well.

Comment on lines +276 to +282
batch_op.create_foreign_key(
"fk_model_versions_registered_models",
"registered_models",
["workspace", "name"],
["workspace", "name"],
onupdate="CASCADE",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major: Inconsistent ON DELETE CASCADE behavior creates orphaned records.

Foreign key constraints have inconsistent cascade deletion behavior:

  • Line 345-351 (SQLite) & 555-562 (non-SQLite): registered_model_aliases has ON DELETE CASCADE
  • Lines 276-282 (SQLite) & 483-490 (non-SQLite): model_versions has only ON UPDATE CASCADE (missing ON DELETE)
  • Lines 297-303 (SQLite) & 507-514 (non-SQLite): registered_model_tags has only ON UPDATE CASCADE (missing ON DELETE)
  • Lines 321-327 (SQLite) & 531-538 (non-SQLite): model_version_tags has only ON UPDATE CASCADE (missing ON DELETE)

When a registered_model is deleted, aliases cascade correctly but model_versions, registered_model_tags, and model_version_tags remain orphaned, requiring manual cleanup.

Add ON DELETE CASCADE to these foreign keys:

             batch_op.create_foreign_key(
                 "fk_model_versions_registered_models",
                 "registered_models",
                 ["workspace", "name"],
                 ["workspace", "name"],
                 onupdate="CASCADE",
+                ondelete="CASCADE",
             )

Apply to all four locations: SQLite batch operations for model_versions (line 276-282), registered_model_tags (297-303), model_version_tags (321-327), and the corresponding non-SQLite operations (lines 483-490, 507-514, 531-538).

Also applies to: 321-327, 483-490, 531-538

🤖 Prompt for AI Agents
In
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py
around lines 276-282, 297-303, 321-327 (SQLite batch_op blocks) and their
non-SQLite counterparts around lines 483-490, 507-514, 531-538, the foreign key
definitions currently only set ON UPDATE CASCADE; add ON DELETE CASCADE to each
of these foreign_key calls so that deletions of registered_models cascade to
model_versions, registered_model_tags, and model_version_tags; update the
batch_op.create_foreign_key(...) calls in each of the six locations to include
ondelete="CASCADE" (in addition to the existing onupdate="CASCADE") to ensure
consistent cascade delete behavior across SQLite and non-SQLite blocks.

Comment on lines +106 to +110
CREATE TABLE workspaces (
name VARCHAR(63) NOT NULL,
description TEXT,
PRIMARY KEY (name)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Schema should include foreign key constraints to workspaces table.

The workspaces table is defined but the schema lacks foreign key constraints from workspace columns to workspaces.name. This should match the corrected migration that adds these FK constraints.

Expected FK constraints:

  • evaluation_datasets.workspace → workspaces.name
  • experiments.workspace → workspaces.name
  • registered_models.workspace → workspaces.name
  • webhooks.workspace → workspaces.name

This schema file should reflect the corrected migration once FK constraints are added to the migration file.

🤖 Prompt for AI Agents
In tests/db/schemas/mysql.sql around lines 106 to 110, the schema lacks the
foreign key relationships from workspace columns in other tables to
workspaces.name; update the schema to add foreign key constraints so that
evaluation_datasets.workspace, experiments.workspace,
registered_models.workspace, and webhooks.workspace all reference
workspaces(name). Ensure the referenced column types and lengths match
(VARCHAR(63)), add explicit CONSTRAINT names for clarity, and include
appropriate ON UPDATE/ON DELETE behavior (e.g., ON DELETE CASCADE or RESTRICT
consistent with the migration) so the SQL mirrors the corrected migration.

Comment on lines +197 to +199
workspace VARCHAR(63) DEFAULT 'default' NOT NULL,
PRIMARY KEY (workspace, name, version),
CONSTRAINT fk_model_versions_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON UPDATE CASCADE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major: Schema has inconsistent ON DELETE CASCADE behavior.

The foreign key constraints show inconsistent cascade deletion:

  • Line 209: registered_model_aliases has ON DELETE CASCADE ON UPDATE CASCADE
  • Line 199: model_versions lacks ON DELETE CASCADE
  • Line 219: registered_model_tags lacks ON DELETE CASCADE
  • Line 374: model_version_tags lacks ON DELETE CASCADE

Once the migration is corrected to include ON DELETE CASCADE on all these constraints, regenerate this schema file to reflect the consistent behavior.

Also applies to: 207-209, 217-219, 372-374

🤖 Prompt for AI Agents
In tests/db/schemas/mysql.sql around lines 197-199 (and also ensure changes at
207-209, 217-219, 372-374), the foreign key constraints referencing
registered_models and model_versions are missing ON DELETE CASCADE causing
inconsistent cascade behavior; update each FOREIGN KEY clause to include ON
DELETE CASCADE ON UPDATE CASCADE (or at minimum ON DELETE CASCADE) where
appropriate, then run the migration/regeneration process to produce an updated
schema file so the file reflects the corrected constraints consistently across
all listed lines.

Comment on lines +106 to +110
CREATE TABLE workspaces (
name VARCHAR(63) NOT NULL,
description TEXT,
CONSTRAINT workspaces_pk PRIMARY KEY (name)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: SQLite schema missing foreign key constraints to workspaces table.

The SQLite schema lacks foreign key constraints from workspace columns to workspaces.name, allowing invalid workspace references. This should be corrected once the migration adds these FK constraints.

Expected FK constraints for SQLite:

  • evaluation_datasets.workspace → workspaces.name
  • experiments.workspace → workspaces.name
  • registered_models.workspace → workspaces.name
  • webhooks.workspace → workspaces.name
🤖 Prompt for AI Agents
In tests/db/schemas/sqlite.sql around lines 106 to 110, the workspaces table
exists but other tables referencing a workspace lack foreign key constraints to
workspaces.name; update the SQLite schema by adding FOREIGN KEY constraints from
evaluation_datasets.workspace, experiments.workspace,
registered_models.workspace, and webhooks.workspace to workspaces(name) (either
inline in each CREATE TABLE definition or via ALTER TABLE ADD CONSTRAINT
statements) and ensure PRAGMA foreign_keys=ON is considered for tests so invalid
workspace references are rejected.

Comment on lines +198 to +200
workspace VARCHAR(63) DEFAULT 'default' NOT NULL,
CONSTRAINT model_version_pk PRIMARY KEY (workspace, name, version),
CONSTRAINT fk_model_versions_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON UPDATE CASCADE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major: SQLite schema has inconsistent ON DELETE CASCADE.

The SQLite schema shows the same inconsistent cascade deletion behavior as other dialects:

  • Line 210: registered_model_aliases includes ON DELETE CASCADE
  • Lines 200, 220, 374: model_versions, registered_model_tags, model_version_tags lack ON DELETE CASCADE

This schema should be regenerated after the migration is corrected to include consistent ON DELETE CASCADE behavior across all model registry foreign keys.

Also applies to: 208-210, 218-220, 372-374

🤖 Prompt for AI Agents
In tests/db/schemas/sqlite.sql around lines 198-200 (and also update 208-210,
218-220, 372-374), the model registry foreign keys for model_versions,
registered_model_tags and model_version_tags are missing ON DELETE CASCADE;
update the migration that generated this schema so those FOREIGN KEY clauses
include ON DELETE CASCADE for consistency with registered_model_aliases, then
regenerate the sqlite.sql schema file so the referenced lines include the ON
DELETE CASCADE option on the appropriate foreign keys (ensure references to
registered_models and model_versions use ON DELETE CASCADE and commit the
regenerated file).

@mprahl mprahl force-pushed the workspaces-db-model branch from bd0a834 to b8fb77e Compare November 19, 2025 03:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/db/schemas/postgresql.sql (1)

19-31: Missing FKs from workspace columns to workspaces table (PostgreSQL schema).

The new workspaces catalog exists, but evaluation_datasets.workspace, experiments.workspace, registered_models.workspace, and webhooks.workspace are not constrained to workspaces(name). This allows invalid workspace values and breaks the catalog invariant.

Add FOREIGN KEY(workspace) REFERENCES workspaces(name) constraints (either inline with reordered table creation or via ALTER TABLE statements) for these root tables and regenerate this schema from the corrected migration.

Also applies to: 34-45, 81-88, 91-103, 106-110

mlflow/store/model_registry/dbmodels/models.py (1)

271-296: Address encryption key persistence and increase column size to prevent data loss and truncation errors.

The EncryptedString implementation has two confirmed issues:

  1. Key regeneration on process restart: The __init__ method instantiates the cipher once at column-definition time (when the module is first imported). When a process restarts and MLFLOW_WEBHOOK_SECRET_ENCRYPTION_KEY is unset, __init__ generates a new random key. Previously encrypted secrets become undecryptable on subsequent process starts, causing silent data loss until a decrypt attempt fails. This requires the environment variable to be set in production; the fallback should not be the default behavior.

  2. Insufficient column capacity: Fernet adds approximately 100 characters of overhead (57 bytes binary + base64 encoding) to encrypted output. For a 900-character plaintext secret, the ciphertext exceeds 1100 characters, truncating against the String(1000) column. The test only validates small secrets ("my_secret" ≈ 100 chars encrypted), masking this boundary condition.

Recommended fixes:

  • Require MLFLOW_WEBHOOK_SECRET_ENCRYPTION_KEY to be explicitly set; remove the or Fernet.generate_key() fallback (or gate it behind an explicit test-mode flag).
  • Increase the column to Text or a larger String (e.g., String(2000)) to safely accommodate encrypted tokens.
♻️ Duplicate comments (12)
tests/db/check_migration.py (1)

52-61: Use the shared default workspace constant instead of hard-coding "default"

WORKSPACE_TABLES and the post‑migration assertions assume "default" as the only workspace value:

for table in WORKSPACE_TABLES:
    df = pd.read_sql(sa.text(f"SELECT DISTINCT workspace FROM {table}"), conn)
    assert not df["workspace"].isna().any()
    assert set(df["workspace"]) == {"default"}

To keep this test aligned with the rest of the codebase and future‑proof against changes to the default workspace name, consider importing and using the shared constant (e.g. DEFAULT_WORKSPACE_NAME from mlflow.utils.workspace_utils) instead of the string literal here.

Also applies to: 161-164

tests/db/schemas/postgresql.sql (1)

184-201: Align model registry FKs with alias behavior by adding ON DELETE CASCADE.

model_versions, registered_model_tags, and model_version_tags FKs only specify ON UPDATE CASCADE, whereas registered_model_aliases already uses ON DELETE CASCADE ON UPDATE CASCADE. This inconsistency means deleting a registered model can leave orphaned versions and tags unless all deletions are carefully ordered in application code.

Consider adding ON DELETE CASCADE to:

  • fk_model_versions_registered_models
  • fk_registered_model_tags_registered_models
  • fk_model_version_tags_model_versions

and then regenerating this PostgreSQL schema to match.

Also applies to: 214-221, 365-373

tests/db/schemas/mysql.sql (2)

19-31: Still missing FKs from workspace columns to workspaces (MySQL schema).

evaluation_datasets.workspace, experiments.workspace, registered_models.workspace, and webhooks.workspace are not constrained to workspaces.name, so invalid workspace values are still possible.

Update the migration to add these FKs and regenerate this MySQL schema so it includes FOREIGN KEY(workspace) REFERENCES workspaces(name) for the four root tables.

Also applies to: 34-45, 81-88, 91-103, 106-110


183-200: Model registry FKs remain inconsistent w.r.t ON DELETE CASCADE (MySQL).

model_versions, registered_model_tags, and model_version_tags FKs only have ON UPDATE CASCADE, while registered_model_aliases includes ON DELETE CASCADE ON UPDATE CASCADE. This can leave orphan child rows when a registered model or model version is deleted.

For consistency and easier cleanup, consider adding ON DELETE CASCADE to these FKs in the migration and re‑exporting this schema.

Also applies to: 213-220, 367-375

tests/resources/db/latest_schema.sql (2)

19-31: Root tables are not constrained to existing workspaces in latest_schema.sql.

The workspaces catalog exists, but evaluation_datasets.workspace, experiments.workspace, registered_models.workspace, and webhooks.workspace are not declared as FKs to workspaces(name). This schema snapshot still permits invalid workspace values.

Once the migration adds these FK constraints, please regenerate latest_schema.sql so it reflects them.

Also applies to: 34-45, 81-88, 91-103, 106-110


184-201: Cascade behavior for model registry FKs is still inconsistent in latest_schema.sql.

Here too, model_versions, registered_model_tags, and model_version_tags FKs only specify ON UPDATE CASCADE, while registered_model_aliases uses ON DELETE CASCADE ON UPDATE CASCADE. This makes deletion semantics depend on table, increasing the chance of orphaned metadata.

After deciding the desired behavior (likely full cascade from registered_models/model_versions to all children), update the migration and regenerate this schema with consistent ON DELETE CASCADE on the affected FKs.

Also applies to: 204-221, 367-375

tests/db/schemas/sqlite.sql (2)

19-31: SQLite schema still lacks FKs from workspace columns to workspaces.

evaluation_datasets.workspace, experiments.workspace, registered_models.workspace, and webhooks.workspace are unconstrained, so rows can reference non‑existent workspaces even though a workspaces catalog exists.

Once the migration enforces these relationships, regenerate sqlite.sql so each of these tables has a FOREIGN KEY(workspace) REFERENCES workspaces(name) (and ensure tests enable PRAGMA foreign_keys=ON).

Also applies to: 34-45, 81-88, 91-103, 106-110


184-201: Inconsistent ON DELETE CASCADE on SQLite model registry FKs.

As with other dialects, model_versions, registered_model_tags, and model_version_tags only have ON UPDATE CASCADE, whereas registered_model_aliases also has ON DELETE CASCADE. This inconsistency complicates cleanup when deleting registered models or model versions.

Consider updating the migration so these FKs include ON DELETE CASCADE, then regenerate the SQLite schema to match.

Also applies to: 204-221, 367-375

mlflow/store/model_registry/dbmodels/models.py (2)

42-47: Add FK constraints from workspace to the workspaces table in ORM models.

SqlRegisteredModel.workspace and SqlWebhook.workspace are plain columns with no ForeignKeyConstraint to the workspaces table, so the ORM does not enforce that their workspace values exist in the catalog, mirroring the schema issues.

Consider extending __table_args__ for both models to include FKs, e.g.:

 class SqlRegisteredModel(Base):
@@
-    __table_args__ = (PrimaryKeyConstraint("workspace", "name", name="registered_model_pk"),)
+    __table_args__ = (
+        ForeignKeyConstraint(
+            ["workspace"],
+            ["workspaces.name"],
+            name="fk_registered_models_workspace",
+        ),
+        PrimaryKeyConstraint("workspace", "name", name="registered_model_pk"),
+    )
@@
 class SqlWebhook(Base):
@@
-    __table_args__ = (
-        PrimaryKeyConstraint("webhook_id", name="webhook_pk"),
-        Index("idx_webhooks_status", "status"),
-        Index("idx_webhooks_name", "name"),
-        Index("idx_webhooks_workspace", "workspace"),
-    )
+    __table_args__ = (
+        ForeignKeyConstraint(
+            ["workspace"],
+            ["workspaces.name"],
+            name="fk_webhooks_workspace",
+        ),
+        PrimaryKeyConstraint("webhook_id", name="webhook_pk"),
+        Index("idx_webhooks_status", "status"),
+        Index("idx_webhooks_name", "name"),
+        Index("idx_webhooks_workspace", "workspace"),
+    )

This keeps child models validating workspace via their composite FKs to these roots.

Also applies to: 57-57, 301-306, 317-322


126-133: Add ondelete="cascade" to model registry composite FKs for DB‑level integrity.

SqlModelVersion, SqlRegisteredModelTag, and SqlModelVersionTag define composite ForeignKeyConstraints with onupdate="cascade" only. Given that SqlRegisteredModelAlias already uses ondelete="cascade", and ORM relationships use cascade="all", omitting DB‑level delete cascades can leave orphans when rows are deleted via SQL or if cascades are misordered in application code.

Align these constraints with alias by adding ondelete="cascade", for example:

 ForeignKeyConstraint(
     ["workspace", "name"],
     ["registered_models.workspace", "registered_models.name"],
-    onupdate="cascade",
+    onupdate="cascade",
+    ondelete="cascade",
 ),
@@
 ForeignKeyConstraint(
     ["workspace", "name"],
     ["registered_models.workspace", "registered_models.name"],
-    onupdate="cascade",
+    onupdate="cascade",
+    ondelete="cascade",
 ),
@@
 ForeignKeyConstraint(
     ["workspace", "name", "version"],
     ["model_versions.workspace", "model_versions.name", "model_versions.version"],
-    onupdate="cascade",
+    onupdate="cascade",
+    ondelete="cascade",
 ),

and then ensure the migrations and schema snapshots are regenerated accordingly.

Also applies to: 176-183, 211-218

tests/db/schemas/mssql.sql (2)

105-109: [DUPLICATE] Add foreign key constraints from workspace columns to the workspaces table.

This issue was flagged in the previous review. The MSSQL schema still lacks foreign key constraints from workspace columns to the workspaces(name) table across multiple tables (evaluation_datasets, experiments, registered_models, webhooks, model_versions, registered_model_aliases, registered_model_tags, model_version_tags). Without these constraints, invalid workspace references are possible.

Please reference the prior review comment for the suggested FK constraint additions. The same fix applies here.

Also applies to: 29-29, 41-41, 85-85, 100-100, 195-195, 205-205, 215-215, 364-364


197-197: [DUPLICATE] Inconsistent ON DELETE CASCADE in MSSQL schema.

This issue was flagged in the previous review. Three FK constraints (lines 197, 217, 366) lack ON DELETE CASCADE while fk_registered_model_aliases_registered_models (line 207) has it, causing inconsistent cascade behavior:

  • Line 197: fk_model_versions_registered_models — missing ON DELETE CASCADE
  • Line 217: fk_registered_model_tags_registered_models — missing ON DELETE CASCADE
  • Line 366: fk_model_version_tags_model_versions — missing ON DELETE CASCADE

Please reference the prior review comment for the suggested diffs to add ON DELETE CASCADE to these constraints.

Also applies to: 217-217, 366-366

🧹 Nitpick comments (2)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)

288-301: Confirm intended cascade semantics for registry FK relationships

model_versions, registered_model_tags, and model_version_tags are now workspace-scoped and re-wired to registered_models / model_versions with onupdate="CASCADE" only, while registered_model_aliases adds both onupdate="CASCADE" and ondelete="CASCADE". This means a direct delete of a registered model will DB‑cascade aliases but not versions/tags, which may or may not match your lifecycle expectations and ORM cascade configuration.

Please double‑check that:

  • ORM‑level cascades or business logic fully clean up dependent rows when a registered model is deleted, and
  • you either explicitly rely on no DB‑level ON DELETE CASCADE for versions/tags (to prevent accidental hard deletes) or, if you do want DB‑level cascading, mirror the alias pattern and add ondelete="CASCADE" here plus the corresponding downgrade changes.

Also applies to: 302-315, 316-331, 405-419, 420-437, 439-456

tests/db/test_schema.py (1)

2-3: Schema reflection fix for MySQL/MSSQL unique constraints looks correct

The combination of _DIALECT_REFLECTED_UNIQUE_CONSTRAINTS, _reattach_missing_unique_constraints, and _get_unique_constraints gives a clear, dialect-aware way to ensure uq_experiments_workspace_name is present in reflected metadata before dump_schema() compares DDL. The filtering by constraint name and by existing column sets, plus the warning on missing columns, keeps the behavior safe and debuggable.

The only minor nit is that _get_unique_constraints takes a dialect parameter that is not currently used; if you don’t plan to branch on it later, you could drop or rename it to _ for clarity, but it’s harmless as-is.

Also applies to: 8-16, 32-37, 46-88, 90-107

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd0a834 and b8fb77e.

📒 Files selected for processing (18)
  • docs/api_reference/api_inventory.txt (2 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/model_registry/sqlalchemy_store.py (6 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/schemas/mssql.sql (13 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
  • tests/resources/db/latest_schema.sql (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • mlflow/store/workspace/dbmodels/init.py
  • mlflow/utils/workspace_utils.py
  • mlflow/entities/workspace.py
  • docs/api_reference/api_inventory.txt
  • mlflow/entities/init.py
  • tests/db/test_workspace_migration.py
  • mlflow/store/workspace/dbmodels/models.py
🧰 Additional context used
🧬 Code graph analysis (5)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
tests/db/test_schema.py (1)
  • _get_unique_constraints (90-107)
mlflow/store/model_registry/sqlalchemy_store.py (1)
mlflow/store/model_registry/dbmodels/models.py (3)
  • SqlRegisteredModelTag (155-190)
  • SqlModelVersionTag (193-230)
  • SqlRegisteredModelAlias (233-268)
tests/db/check_migration.py (1)
mlflow/store/model_registry/sqlalchemy_store.py (4)
  • create_model_version (730-838)
  • set_registered_model_alias (1247-1270)
  • create_registered_model (172-214)
  • create_webhook (1335-1367)
tests/db/test_schema.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
  • _get_unique_constraints (150-164)
mlflow/store/model_registry/dbmodels/models.py (1)
mlflow/entities/model_registry/registered_model.py (4)
  • name (44-46)
  • name (49-50)
  • description (69-71)
  • description (74-75)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (35)
  • GitHub Check: models (1)
  • GitHub Check: windows (4)
  • GitHub Check: windows (3)
  • GitHub Check: models (2)
  • GitHub Check: genai
  • GitHub Check: windows (1)
  • GitHub Check: flavors
  • GitHub Check: windows (2)
  • GitHub Check: java
  • GitHub Check: pyfunc (1)
  • GitHub Check: pyfunc (4)
  • GitHub Check: pyfunc (2)
  • GitHub Check: pyfunc (3)
  • GitHub Check: evaluate (1)
  • GitHub Check: evaluate (2)
  • GitHub Check: database
  • GitHub Check: python (1)
  • GitHub Check: python (2)
  • GitHub Check: python-skinny
  • GitHub Check: python (3)
  • GitHub Check: js (ubuntu-latest, --testPathIgnorePatterns)
  • GitHub Check: js (windows-latest, --testPathIgnorePatterns)
  • GitHub Check: js (windows-latest, --testPathPattern)
  • GitHub Check: js (ubuntu-latest, --testPathPattern)
  • GitHub Check: build (dev)
  • GitHub Check: build (tracing)
  • GitHub Check: build (skinny)
  • GitHub Check: lint-macos
  • GitHub Check: protect
  • GitHub Check: build
  • GitHub Check: core
  • GitHub Check: r
  • GitHub Check: typescript-sdk (24)
  • GitHub Check: typescript-sdk (22)
  • GitHub Check: typescript-sdk (20)
🔇 Additional comments (5)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)

491-552: Downgrade safety and conflict checks look solid

The downgrade path validates per-table conflict specs before collapsing workspaces, rewrites all resources back to the "default" workspace, then cleanly restores legacy PKs/FKs and drops workspace columns and indexes (including MSSQL default-constraint cleanup). This gives a reversible story with clear failure modes if multiple workspaces coexist.

No changes requested here.

Also applies to: 555-587, 626-641, 645-666, 679-686, 688-721

tests/db/check_migration.py (1)

69-80: Good coverage of all workspace-backed tables in pre-migration data

The extended log_everything() now touches all entries in WORKSPACE_TABLES (model version + alias, extra experiment/model, webhook, and a direct evaluation_datasets row), so the post‑migration workspace assertions will exercise real backfill behavior instead of failing on empty tables. This is a much more robust CI signal for the migration.

Also applies to: 85-107, 109-127

mlflow/store/tracking/dbmodels/models.py (1)

64-65: Workspace columns and constraints for tracking models align with the migration

The additions of:

  • workspace on SqlExperiment with UniqueConstraint("workspace", "name", name="uq_experiments_workspace_name"), and
  • workspace plus Index("idx_evaluation_datasets_workspace", "workspace") on SqlEvaluationDataset

are consistent with the Alembic migration’s DDL and the workspace backfill strategy. Using DEFAULT_WORKSPACE_NAME with both default and server_default gives a clean path for new rows while the migration seeds legacy rows.

It’s also reasonable that the entity mappers don’t surface workspace yet, keeping behavior effectively single‑workspace until higher layers are ready to pass it through.

Also applies to: 87-112, 131-138, 1315-1323, 1375-1380

mlflow/store/model_registry/sqlalchemy_store.py (1)

64-65: Consistent default-workspace wiring for registry entities

The registry store now consistently sets workspace=DEFAULT_WORKSPACE_NAME when:

  • creating SqlRegisteredModel and its SqlRegisteredModelTags,
  • creating SqlModelVersion and its SqlModelVersionTags, and
  • creating/upserting SqlRegisteredModelAlias and tags via the set_* APIs.

Given that all reads and tag/alias lookups still filter by name (and version) only, constraining writes to the default workspace avoids ambiguity for now and keeps existing behavior intact. When you later introduce user-visible multi-workspace support, this will be the natural place to thread an explicit workspace parameter through and extend the query filters accordingly.

Also applies to: 192-199, 200-206, 687-706, 800-814, 815-821, 1202-1212, 1264-1270

tests/db/schemas/mssql.sql (1)

42-43: Schema design for workspace scoping looks good.

The workspace columns are consistently defined, composite primary keys correctly include workspace where needed, and the UNIQUE constraint on (workspace, name) for experiments appropriately prevents duplicates within workspaces. The collation and default values are uniform.

Also applies to: 86-86, 196-196, 206-206, 216-216, 365-365

@mprahl mprahl force-pushed the workspaces-db-model branch from b8fb77e to 3fb5a9e Compare November 19, 2025 03:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (16)
tests/db/check_migration.py (1)

52-61: Consider de-duplicating workspace table metadata and avoiding hard-coded "default"

WORKSPACE_TABLES here and in the migration/tests are hand-maintained lists, and the post-migration assertions hard-code "default" as the only valid workspace. To reduce drift when the workspace model evolves, consider:

  • Either sharing a single source of truth for the workspace table set (or at least adding a brief comment tying this constant to the migration’s _WORKSPACE_TABLES), and
  • Importing and using DEFAULT_WORKSPACE_NAME instead of the string "default" in the post_migration checks.

This keeps the CI check aligned with the runtime defaults as workspaces mature.

Also applies to: 161-164

mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)

219-252: Add foreign keys from workspace columns to the workspaces catalog to enforce referential integrity

The migration introduces a workspaces table and adds non-null workspace columns to 8 tables, but no FK ties those columns back to workspaces.name. That means rows can be inserted with arbitrary workspace values that don’t exist in the catalog, undermining the integrity of the new tenant boundary.

At minimum, the “root” tables should validate their workspace against the catalog:

  • experiments.workspace → workspaces.name
  • registered_models.workspace → workspaces.name
  • evaluation_datasets.workspace → workspaces.name
  • webhooks.workspace → workspaces.name

Child tables (model_versions, registered_model_aliases, registered_model_tags, model_version_tags) already route validation through composite FKs to these parents, so they don’t need direct FKs to workspaces.

Concretely, you can:

  • In the SQLite batches for these four tables, add batch_op.create_foreign_key(...) calls after the workspace column is added and before exiting each batch.
  • In the non-SQLite path, add corresponding op.create_foreign_key(...) definitions after the workspace columns and primary keys are created and before _create_workspace_indexes_and_catalog() (or immediately afterwards, as long as the workspaces table exists first).

Example for non-SQLite experiments (illustrative):

 op.create_unique_constraint(
     "uq_experiments_workspace_name",
     "experiments",
     ["workspace", "name"],
 )
+op.create_foreign_key(
+    "fk_experiments_workspace",
+    "experiments",
+    "workspaces",
+    ["workspace"],
+    ["name"],
+)

And add analogous FKs for registered_models, evaluation_datasets, and webhooks in both the SQLite and non-SQLite branches, with corresponding drop_constraint calls in downgrade().

Also applies to: 351-359, 378-485


288-301: Align cascade behavior: add ON DELETE CASCADE to workspace-aware child FKs

The workspace migration updates FKs from:

  • model_versions → registered_models
  • registered_model_tags → registered_models
  • model_version_tags → model_versions

but only specifies onupdate="CASCADE". In contrast, registered_model_aliases correctly uses both onupdate="CASCADE" and ondelete="CASCADE".

If a registered model is deleted, you’ll currently:

  • Cascade-delete aliases, but
  • Leave orphaned model_versions, registered_model_tags, and model_version_tags rows.

To keep behavior consistent and avoid orphans, extend those FKs to include ondelete="CASCADE" in both SQLite batch ops and non-SQLite op.create_foreign_key calls. For example (pattern only):

 batch_op.create_foreign_key(
     "fk_model_versions_registered_models",
     "registered_models",
     ["workspace", "name"],
     ["workspace", "name"],
-    onupdate="CASCADE",
+    onupdate="CASCADE",
+    ondelete="CASCADE",
 )

Apply the same to:

  • SQLite: fk_model_versions_registered_models, fk_registered_model_tags_registered_models, fk_model_version_tags_model_versions.
  • Non-SQLite: the corresponding op.create_foreign_key definitions in the upgrade path.

downgrade() already recreates the legacy non-workspace FKs (with CASCADE semantics where applicable), so only the upgrade side needs adjustment for consistency.

Also applies to: 302-315, 316-331, 405-418, 420-437, 439-456

tests/db/schemas/postgresql.sql (2)

106-110: Critical: FK constraints to workspaces table remain missing.

This issue was previously identified. The workspaces table exists but root tables with workspace columns still lack foreign key constraints to enforce referential integrity.


198-200: Major: Inconsistent ON DELETE CASCADE behavior persists.

This issue was previously identified. The FK constraint on model_versions lacks ON DELETE CASCADE, creating inconsistency with registered_model_aliases which includes it.

tests/db/schemas/sqlite.sql (2)

106-110: Critical: FK constraints to workspaces table remain missing.

This issue was previously identified. The SQLite schema still lacks foreign key constraints from workspace columns to the workspaces table.


198-200: Major: Inconsistent ON DELETE CASCADE behavior persists.

This issue was previously identified. The SQLite schema shows the same inconsistent cascade deletion behavior.

tests/resources/db/latest_schema.sql (2)

106-110: Critical: FK constraints to workspaces table remain missing.

This issue was previously identified. The schema still needs foreign key constraints added for workspace columns referencing workspaces.name.


199-201: Major: Inconsistent ON DELETE CASCADE behavior persists.

This issue was previously identified. Foreign key constraints still lack ON DELETE CASCADE for model_versions, registered_model_tags, and model_version_tags.

mlflow/store/model_registry/dbmodels/models.py (5)

42-47: Critical: FK constraint to workspaces table remains missing.

This issue was previously identified. SqlRegisteredModel has a workspace column but no foreign key constraint to the workspaces table.

Also applies to: 57-57


126-133: Major: ondelete="cascade" still missing.

This issue was previously identified. The ForeignKeyConstraint for SqlModelVersion lacks ondelete="cascade" for database-level referential integrity.


176-183: Major: ondelete="cascade" still missing.

This issue was previously identified. The ForeignKeyConstraint for SqlRegisteredModelTag lacks ondelete="cascade".


211-218: Major: ondelete="cascade" still missing.

This issue was previously identified. The ForeignKeyConstraint for SqlModelVersionTag lacks ondelete="cascade".


301-306: Critical: FK constraint to workspaces table remains missing.

This issue was previously identified. SqlWebhook has a workspace column but no foreign key constraint to the workspaces table.

Also applies to: 317-322

tests/db/schemas/mssql.sql (2)

105-109: Critical: FK constraints to workspaces table remain missing.

This issue was previously identified. The MSSQL schema still lacks foreign key constraints from workspace columns to the workspaces table.


196-198: Major: Inconsistent ON DELETE CASCADE behavior persists.

This issue was previously identified. The MSSQL schema shows the same inconsistent cascade deletion behavior as other dialects.

🧹 Nitpick comments (5)
mlflow/store/__init__.py (1)

1-4: Temporary workspace models import looks fine; consider a stronger TODO/tracker reference

This side‑effect import is consistent with the existing _unity_catalog pattern and keeps tests happy without touching __all__, so no functional concerns. To reduce the risk of this “temporary” hook sticking around, consider turning the comment into an explicit TODO (optionally referencing an issue/PR) so it’s easier to find and remove once a workspace store is wired in.

tests/db/test_schema.py (1)

2-16: Unique-constraint reattachment helper is well-scoped; just keep the dialect map in sync

The _reattach_missing_unique_constraints / _get_unique_constraints pair gives you a clean, dialect-tolerant way to normalize missing UniqueConstraint objects before dumping schema, without touching live DB state. The guards for MSSQL and MySQL look correct and idempotent.

Going forward, just remember that _DIALECT_REFLECTED_UNIQUE_CONSTRAINTS is now the choke point for any additional workspace-related uniques that are mis-reflected; new constraints with similar issues will need an entry here.

Also applies to: 32-43, 46-107

tests/db/test_workspace_migration.py (2)

47-55: Include webhooks in _WORKSPACE_TABLES to fully exercise the migration surface

The migration and tests/db/check_migration.py both treat webhooks as workspace-backed, but _WORKSPACE_TABLES here omits it, so _assert_workspace_columns() never validates the webhooks.workspace column or its default.

To keep this test module in sync with the migration’s contract, consider adding "webhooks" to _WORKSPACE_TABLES and extending any related assertions as needed.

Also applies to: 143-146


57-94: Avoid duplicate sa.table declarations for tags/aliases/evaluation_datasets

_REGISTERED_MODEL_TAGS, _MODEL_VERSION_TAGS, _REGISTERED_MODEL_ALIASES, and _EVALUATION_DATASETS are each defined twice with identical schemas (once near the top and again around the helper insert functions). This works because the latter assignments just overwrite the former, but it makes the module harder to read and reason about.

You can safely keep a single definition of each table (preferably near the top) and reuse it across helpers and tests.

Also applies to: 587-684

mlflow/store/model_registry/sqlalchemy_store.py (1)

64-65: Workspace defaulting in registry store is consistent for writes; capture the single-workspace assumption

The changes correctly ensure that all new registry entities (registered models, model versions, their tags, and aliases) are created with workspace=DEFAULT_WORKSPACE_NAME, matching the new composite PKs and FK layout introduced by the migration.

Note, however, that read paths and tag/alias lookups (_get_registered_model*, _get_model_version*, _get_registered_model_alias) still filter only on name / version / key / alias without a workspace predicate. That’s fine as long as the runtime only ever uses a single workspace (the default), but it becomes ambiguous once non-default workspaces are introduced.

It’d be helpful to:

  • Explicitly document in this module (or in DEFAULT_WORKSPACE_NAME’s definition) that the store currently assumes a single default workspace, and
  • Plan to extend these filters to include workspace once the public API grows a workspace dimension, so lookups stay unambiguous.

No change is strictly required for the current default-only behavior, but calling out the assumption here will make the future multi-workspace extension safer.

Also applies to: 193-206, 686-707, 803-821, 1202-1212, 1264-1270

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b8fb77e and 3fb5a9e.

📒 Files selected for processing (19)
  • docs/api_reference/api_inventory.txt (2 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/store/__init__.py (1 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/model_registry/sqlalchemy_store.py (6 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/schemas/mssql.sql (13 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
  • tests/resources/db/latest_schema.sql (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • mlflow/utils/workspace_utils.py
  • mlflow/store/tracking/dbmodels/models.py
  • mlflow/entities/init.py
  • tests/db/schemas/mysql.sql
  • mlflow/store/workspace/dbmodels/models.py
  • docs/api_reference/api_inventory.txt
  • mlflow/entities/workspace.py
  • mlflow/store/workspace/dbmodels/init.py
🧰 Additional context used
🧬 Code graph analysis (5)
tests/db/test_workspace_migration.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)
  • upgrade (144-484)
  • downgrade (487-732)
tests/db/check_migration.py (1)
mlflow/store/model_registry/sqlalchemy_store.py (4)
  • create_model_version (730-838)
  • set_registered_model_alias (1247-1270)
  • create_registered_model (172-214)
  • create_webhook (1335-1367)
tests/db/test_schema.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
  • _get_unique_constraints (150-164)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
tests/db/test_schema.py (1)
  • _get_unique_constraints (90-107)
mlflow/store/model_registry/sqlalchemy_store.py (1)
mlflow/store/model_registry/dbmodels/models.py (3)
  • SqlRegisteredModelTag (155-190)
  • SqlModelVersionTag (193-230)
  • SqlRegisteredModelAlias (233-268)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (34)
  • GitHub Check: windows (3)
  • GitHub Check: windows (4)
  • GitHub Check: evaluate (1)
  • GitHub Check: genai
  • GitHub Check: windows (2)
  • GitHub Check: evaluate (2)
  • GitHub Check: windows (1)
  • GitHub Check: pyfunc (1)
  • GitHub Check: pyfunc (4)
  • GitHub Check: models (2)
  • GitHub Check: models (1)
  • GitHub Check: pyfunc (2)
  • GitHub Check: pyfunc (3)
  • GitHub Check: flavors
  • GitHub Check: python (3)
  • GitHub Check: python (2)
  • GitHub Check: python (1)
  • GitHub Check: database
  • GitHub Check: java
  • GitHub Check: build (dev)
  • GitHub Check: build (tracing)
  • GitHub Check: build (skinny)
  • GitHub Check: lint-macos
  • GitHub Check: r
  • GitHub Check: lint
  • GitHub Check: build
  • GitHub Check: typescript-sdk (24)
  • GitHub Check: typescript-sdk (22)
  • GitHub Check: js (ubuntu-latest, --testPathPattern)
  • GitHub Check: typescript-sdk (20)
  • GitHub Check: js (windows-latest, --testPathIgnorePatterns)
  • GitHub Check: js (ubuntu-latest, --testPathIgnorePatterns)
  • GitHub Check: js (windows-latest, --testPathPattern)
  • GitHub Check: protect
🔇 Additional comments (1)
tests/db/check_migration.py (1)

69-127: Seeding all workspace-backed tables before snapshotting looks solid

The extended log_everything() now exercises every entry in WORKSPACE_TABLES (including aliases, evaluation datasets, and webhooks), which avoids the previous empty-table issue in post_migration and gives the migration CI check real data to validate.

@mprahl mprahl force-pushed the workspaces-db-model branch from 3fb5a9e to 64ac13f Compare November 19, 2025 04:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (15)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)

219-231: Add FKs from workspace columns to workspaces.name for root tables

Right now experiments, registered_models, evaluation_datasets, and webhooks all gain a non-null workspace column plus a workspaces catalog, but there is no FK enforcing that workspace values actually exist in workspaces.name. That allows invalid workspaces to be inserted and weakens the integrity guarantees of the new catalog.

Consider adding FKs from each root table’s workspace column to workspaces.name in both branches:

  • SQLite: inside the relevant batch_op blocks for experiments, registered_models, evaluation_datasets, and webhooks.
  • Non-SQLite: after the op.add_column(... _workspace_column()) calls for those tables.

For example (SQLite branch):

 with _with_batch("experiments") as batch_op:
@@
     batch_op.create_unique_constraint(
         "uq_experiments_workspace_name",
         ["workspace", "name"],
     )
+    batch_op.create_foreign_key(
+        "fk_experiments_workspace",
+        "workspaces",
+        ["workspace"],
+        ["name"],
+    )

And similarly for registered_models, evaluation_datasets, and webhooks (plus corresponding op.create_foreign_key(...) calls in the non-SQLite path). You’d also need to reflect these FKs into the schema snapshots (e.g., tests/db/schemas/*.sql) to keep test_schema green.

Also applies to: 265-359, 377-485


288-315: Align cascade behavior: add ON DELETE CASCADE to remaining workspace-aware FKs

The new workspace-aware FKs from:

  • model_versions.workspace,name → registered_models.workspace,name
  • registered_model_tags.workspace,name → registered_models.workspace,name
  • model_version_tags.workspace,name,version → model_versions.workspace,name,version

only specify onupdate="CASCADE", while registered_model_aliases.workspace,name → registered_models.workspace,name uses both onupdate="CASCADE" and ondelete="CASCADE". This inconsistency means that deleting a registered model at the database level can leave orphaned versions and tags, but aliases are cleaned up.

To keep referential behavior consistent and avoid orphaned rows when deletions occur outside SQLAlchemy’s ORM cascades, consider adding ondelete="CASCADE" to these FKs in both branches, for example:

SQLite:

batch_op.create_foreign_key(
    "fk_model_versions_registered_models",
    "registered_models",
    ["workspace", "name"],
    ["workspace", "name"],
-    onupdate="CASCADE",
+    onupdate="CASCADE",
+    ondelete="CASCADE",
)

and similarly for fk_registered_model_tags_registered_models and fk_model_version_tags_model_versions.

Non-SQLite:

op.create_foreign_key(
    "fk_model_versions_registered_models",
    "model_versions",
    "registered_models",
    ["workspace", "name"],
    ["workspace", "name"],
-    onupdate="CASCADE",
+    onupdate="CASCADE",
+    ondelete="CASCADE",
)

with analogous updates for the tag FKs. Schema snapshots and ORM ForeignKeyConstraint definitions should be updated to match.

Also applies to: 405-456

tests/db/schemas/mysql.sql (2)

106-110: Critical: Schema still lacks foreign key constraints from workspace columns to workspaces table.

The workspaces catalog table exists but there are no FK constraints from workspace columns in root tables (evaluation_datasets, experiments, registered_models, webhooks) to workspaces.name. This allows insertion of invalid workspace values that don't exist in the workspaces table.


197-199: Major: Inconsistent ON DELETE CASCADE behavior persists.

The foreign key constraints continue to show inconsistent cascade deletion:

  • Line 209: registered_model_aliases has ON DELETE CASCADE ON UPDATE CASCADE
  • Line 199: model_versions lacks ON DELETE CASCADE
  • Line 219: registered_model_tags lacks ON DELETE CASCADE
  • Line 374: model_version_tags lacks ON DELETE CASCADE

This inconsistency means deleting a registered_model would cascade to aliases but orphan model_versions and tags.

Also applies to: 207-209, 217-219, 372-374

tests/resources/db/latest_schema.sql (2)

106-110: Critical: Foreign key constraints to workspaces table are still missing.

The workspaces catalog table is defined but root tables with workspace columns (evaluation_datasets, experiments, registered_models, webhooks) lack foreign key constraints to workspaces.name. This permits invalid workspace values.


199-201: Major: ON DELETE CASCADE inconsistency remains unresolved.

Foreign key constraints continue to have inconsistent cascade deletion behavior:

  • Line 210: registered_model_aliases includes both ON DELETE CASCADE ON UPDATE CASCADE
  • Line 200: model_versions has only ON UPDATE CASCADE
  • Line 220: registered_model_tags has only ON UPDATE CASCADE
  • Line 374: model_version_tags has only ON UPDATE CASCADE

Also applies to: 209-211, 219-221, 373-375

tests/db/schemas/sqlite.sql (2)

106-110: Critical: SQLite schema still missing foreign key constraints to workspaces table.

The workspaces table is defined but workspace columns in root tables (evaluation_datasets, experiments, registered_models, webhooks) lack FK constraints to workspaces.name, allowing invalid workspace references.


198-200: Major: SQLite schema has persistent inconsistent ON DELETE CASCADE.

The cascade deletion behavior remains inconsistent:

  • Line 210: registered_model_aliases includes ON DELETE CASCADE
  • Lines 200, 220, 374: model_versions, registered_model_tags, model_version_tags lack ON DELETE CASCADE

Also applies to: 208-210, 218-220, 372-374

mlflow/store/model_registry/dbmodels/models.py (5)

42-47: Critical: FK constraints to workspaces table remain missing from SqlRegisteredModel.

The workspace column in SqlRegisteredModel lacks a foreign key constraint to the workspaces table. At line 57, __table_args__ contains only PrimaryKeyConstraint without a ForeignKeyConstraint to workspaces.name, allowing invalid workspace values.

Also applies to: 57-57


126-133: Major: SqlModelVersion FK constraint still lacks ondelete="cascade".

The ForeignKeyConstraint at line 130 includes onupdate="cascade" but omits ondelete="cascade". While the ORM-level relationship specifies cascade="all", database-level cascade deletion is needed for referential integrity during direct SQL operations.


176-183: Major: SqlRegisteredModelTag FK constraint still lacks ondelete="cascade".

The ForeignKeyConstraint at line 180 includes onupdate="cascade" but omits ondelete="cascade", creating inconsistency with SqlRegisteredModelAlias which has both cascade options.


211-218: Major: SqlModelVersionTag FK constraint still lacks ondelete="cascade".

The ForeignKeyConstraint at line 215 includes onupdate="cascade" but omits ondelete="cascade". This means database-level deletions of model_versions won't cascade to their tags, requiring manual cleanup.


301-306: Critical: FK constraint to workspaces table missing from SqlWebhook.

The workspace column in SqlWebhook lacks a foreign key constraint to the workspaces table. At lines 317-322, __table_args__ contains only PrimaryKeyConstraint and indexes without a ForeignKeyConstraint to workspaces.name.

Also applies to: 317-322

tests/db/schemas/mssql.sql (2)

105-109: Critical: MSSQL schema still lacks foreign key constraints to workspaces table.

The workspaces table exists but workspace columns in root tables (evaluation_datasets, experiments, registered_models, webhooks) lack FK constraints to workspaces.name, permitting invalid workspace references.


196-198: Major: MSSQL schema has persistent inconsistent ON DELETE CASCADE.

The cascade deletion behavior remains inconsistent across foreign keys:

  • Line 207: registered_model_aliases has ON DELETE CASCADE
  • Lines 197, 217, 366: model_versions, registered_model_tags, model_version_tags lack ON DELETE CASCADE

Also applies to: 206-208, 216-218, 365-367

🧹 Nitpick comments (4)
tests/db/test_schema.py (1)

46-88: Minor: avoid duplication and unused parameter in unique-constraint helpers

The _reattach_missing_unique_constraints / _get_unique_constraints pairing looks solid and should fix dialect-specific reflection issues.

Two small maintainability nits:

  • _get_unique_constraints(inspector, dialect, table_name) never uses dialect; either drop the argument or use it for any future dialect branching to avoid confusion.
  • The implementation of _get_unique_constraints here closely mirrors the helper inside the migration module. If those need to stay behaviorally aligned over time (especially for MSSQL/MySQL edge cases), consider centralizing the logic in a shared utility to reduce drift risk.

Also applies to: 90-107

mlflow/store/model_registry/sqlalchemy_store.py (1)

64-65: Explicitly document or enforce the “default workspace only” assumption in the store

All write paths in this file now hard-code workspace=DEFAULT_WORKSPACE_NAME for registered models, model versions, tags, and aliases, which is fine for a single-workspace deployment. However, the read paths and lookups (_get_registered_model, _get_registered_model_tag, _get_model_version_tag, _get_registered_model_alias, search queries) still filter only by name / version and explicitly fail if more than one row matches.

That means:

  • If any other component starts inserting rows for additional workspaces, these helpers will begin raising INVALID_STATE or returning ambiguous results.
  • The effective API contract of this store is “operates only on the default workspace”, but this isn’t surfaced anywhere.

Consider either:

  • Making this explicit (e.g., via a short class-level comment and/or defensive checks that assert all rows are in DEFAULT_WORKSPACE_NAME), or
  • Starting to thread a workspace identifier through these methods and include it in all filters so multi-workspace usage is safe when you decide to expose it.

This can be done incrementally, but clarifying the intended scope now will help avoid subtle breakage later.

Also applies to: 172-207, 686-707, 730-839, 1186-1212, 1247-1270

tests/db/test_workspace_migration.py (2)

47-55: Include webhooks in workspace migration upgrade/downgrade assertions

The migration adds a non-null workspace column (and an index) to webhooks, but the tests in this module don’t currently cover it:

  • _WORKSPACE_TABLES omits "webhooks", so _assert_workspace_columns() never checks that webhooks.workspace exists and is backfilled.
  • The downgrade test’s loop over tables to check that workspace was removed also omits "webhooks".

To fully exercise the migration behavior, consider adding webhooks to both places, e.g.:

_WORKSPACE_TABLES = (
     "experiments",
     "registered_models",
     "model_versions",
     "registered_model_tags",
     "model_version_tags",
     "registered_model_aliases",
     "evaluation_datasets",
+    "webhooks",
 )

and in test_workspace_migration_downgrade_reverts_schema:

-        for table in (
+        for table in (
             "experiments",
             "registered_models",
             "model_versions",
             "registered_model_tags",
             "model_version_tags",
             "registered_model_aliases",
             "evaluation_datasets",
+            "webhooks",
         ):

This keeps the e2e tests aligned with the actual set of workspace-enabled tables.

Also applies to: 143-146, 867-875


57-95: Remove duplicate sa.table definitions for workspace-aware tables

_REGISTERED_MODEL_TAGS, _MODEL_VERSION_TAGS, _REGISTERED_MODEL_ALIASES, and _EVALUATION_DATASETS are each defined twice with identical sa.table(...) declarations: once near the top of the file and again around lines 587–683.

The duplicates are harmless at runtime (later assignments just overwrite the earlier ones) but add noise and can confuse future readers.

You can safely drop the second block of definitions and rely on the initial ones only, for example:

-_REGISTERED_MODEL_TAGS = sa.table(
-    "registered_model_tags",
-    sa.column("workspace"),
-    sa.column("key"),
-    sa.column("value"),
-    sa.column("name"),
-)
-
-
-_MODEL_VERSION_TAGS = sa.table(
-    "model_version_tags",
-    sa.column("workspace"),
-    sa.column("key"),
-    sa.column("value"),
-    sa.column("name"),
-    sa.column("version"),
-)
-
-...
-
-_EVALUATION_DATASETS = sa.table(
-    "evaluation_datasets",
-    sa.column("dataset_id"),
-    sa.column("name"),
-    sa.column("schema"),
-    sa.column("profile"),
-    sa.column("digest"),
-    sa.column("created_time"),
-    sa.column("last_update_time"),
-    sa.column("created_by"),
-    sa.column("last_updated_by"),
-    sa.column("workspace"),
-)

This keeps the test helpers DRY and easier to maintain.

Also applies to: 587-683

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fb5a9e and 64ac13f.

📒 Files selected for processing (19)
  • docs/api_reference/api_inventory.txt (2 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/model_registry/sqlalchemy_store.py (6 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/schemas/mssql.sql (13 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
  • tests/resources/db/latest_schema.sql (6 hunks)
  • tests/store/tracking/test_sqlalchemy_store_schema.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/store/tracking/test_sqlalchemy_store_schema.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • mlflow/utils/workspace_utils.py
  • mlflow/store/workspace/dbmodels/init.py
  • tests/db/check_migration.py
  • mlflow/store/workspace/dbmodels/models.py
  • mlflow/entities/init.py
  • mlflow/store/tracking/dbmodels/models.py
  • mlflow/entities/workspace.py
🧰 Additional context used
🧬 Code graph analysis (4)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
tests/db/test_schema.py (1)
  • _get_unique_constraints (90-107)
tests/db/test_schema.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
  • _get_unique_constraints (150-164)
mlflow/store/model_registry/sqlalchemy_store.py (1)
mlflow/store/model_registry/dbmodels/models.py (3)
  • SqlRegisteredModelTag (155-190)
  • SqlModelVersionTag (193-230)
  • SqlRegisteredModelAlias (233-268)
tests/db/test_workspace_migration.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)
  • upgrade (144-484)
  • downgrade (487-732)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (34)
  • GitHub Check: pyfunc (1)
  • GitHub Check: pyfunc (4)
  • GitHub Check: windows (4)
  • GitHub Check: pyfunc (2)
  • GitHub Check: pyfunc (3)
  • GitHub Check: windows (1)
  • GitHub Check: windows (2)
  • GitHub Check: evaluate (2)
  • GitHub Check: evaluate (1)
  • GitHub Check: windows (3)
  • GitHub Check: genai
  • GitHub Check: python-skinny
  • GitHub Check: java
  • GitHub Check: models (2)
  • GitHub Check: models (1)
  • GitHub Check: flavors
  • GitHub Check: database
  • GitHub Check: python (1)
  • GitHub Check: python (3)
  • GitHub Check: python (2)
  • GitHub Check: protect
  • GitHub Check: build (skinny)
  • GitHub Check: build (tracing)
  • GitHub Check: build (dev)
  • GitHub Check: r
  • GitHub Check: build
  • GitHub Check: js (windows-latest, --testPathIgnorePatterns)
  • GitHub Check: js (windows-latest, --testPathPattern)
  • GitHub Check: js (ubuntu-latest, --testPathPattern)
  • GitHub Check: js (ubuntu-latest, --testPathIgnorePatterns)
  • GitHub Check: core
  • GitHub Check: typescript-sdk (22)
  • GitHub Check: typescript-sdk (20)
  • GitHub Check: typescript-sdk (24)
🔇 Additional comments (1)
docs/api_reference/api_inventory.txt (1)

531-531: API inventory entries are correct and properly synchronized with source exports.

Verification confirms both entries document valid exports:

  • mlflow.entities.Workspace is exported via mlflow/entities/__init__.py (line 65, included in __all__ at line 129)
  • mlflow.entities.workspace.Workspace is accessible directly from mlflow/entities/workspace.py (class defined at line 9)

@mprahl mprahl force-pushed the workspaces-db-model branch from 64ac13f to c0691c6 Compare November 19, 2025 04:17

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
mlflow/R/mlflow/tests/testthat/test-run.R (1)

19-51: Now I need to check if this is an isolated issue or if other test files also use with_mocked_bindings:

Update mlflow/R/mlflow/DESCRIPTION to require testthat >= 3.1.7 (or >= 3.2.0 for stable).

The with_mocked_bindings() function was introduced as experimental in testthat 3.1.7, but the DESCRIPTION file specifies testthat (>= 2.0.0). This dependency is incompatible with the test code, which will fail on any system with testthat 2.x or 3.0.x. Update DESCRIPTION to reflect the minimum required version:

  • Use testthat (>= 3.1.7) to allow experimental status, or
  • Use testthat (>= 3.2.0) for the stable, released version
mlflow/R/mlflow/tests/testthat/test-client.R (1)

1-151: Update testthat version requirement in mlflow/R/mlflow/DESCRIPTION.

The test file uses with_mocked_bindings(), which was introduced experimentally in testthat 3.1.7 and became stable in testthat 3.2.0. The DESCRIPTION file currently requires only testthat (>= 2.0.0), which is incompatible. Update to testthat (>= 3.2.0) to ensure tests run correctly with the stable API.

mlflow/cli/__init__.py (1)

631-671: Inconsistent default handling between server and gc commands.

The gc command's --backend-store-uri option now defaults to None (line 633), but unlike the server command (lines 552-553), there's no runtime default computation or user-facing message informing users which backend store URI is being used.

This inconsistency may confuse users who expect the same transparent default behavior across commands. Consider adding similar logic after line 671 to compute and display the default backend store URI when not provided.

Apply this diff to add consistent default messaging:

 def gc(older_than, backend_store_uri, artifacts_destination, run_ids, experiment_ids, tracking_uri):
     """
     Permanently delete runs in the `deleted` lifecycle stage from the specified backend store.
     ...
     """
     from mlflow.utils.time import get_current_time_millis
 
+    if not backend_store_uri:
+        backend_store_uri = _get_default_tracking_uri()
+        click.echo(f"Backend store URI not provided. Using {backend_store_uri}", err=True)
+
     backend_store = _get_store(backend_store_uri, artifacts_destination)
♻️ Duplicate comments (13)
tests/db/check_migration.py (1)

52-61: Workspace backfill check is much stronger now; consider reusing the shared default constant

The extra resources created in log_everything() (model version + alias, extra experiment / registered model, webhook, and a direct evaluation_datasets insert) nicely exercise all entries in WORKSPACE_TABLES, so the post-migration workspace assertions should now see concrete data instead of empty tables.

To keep things DRY and aligned with the runtime behavior, it would still be better to replace the hard-coded "default" in:

assert set(df["workspace"]) == {"default"}, ...

with the shared constant (e.g. DEFAULT_WORKSPACE_NAME from mlflow.utils.workspace_utils), so the CI check stays in sync if the default workspace name ever changes. This is essentially the same suggestion raised in a previous review.

Also applies to: 85-127, 161-164

mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)

219-230: Add foreign keys from workspace columns to the workspaces catalog table

The migration adds non‑nullable workspace columns and creates the workspaces table plus indexes, but there are no FKs enforcing workspaceworkspaces.name for the root tables (experiments, registered_models, evaluation_datasets, webhooks). This allows invalid workspace values to be inserted and weakens referential integrity.

Concretely:

  • SQLite branch: batch_op.add_column(_workspace_column()) is used for experiments, registered_models, evaluation_datasets, and webhooks, but no batch_op.create_foreign_key(..., "workspaces", ["workspace"], ["name"], ...) calls are added.
  • Non‑SQLite branch: op.add_column(..., _workspace_column()) is used for the same tables, and _create_workspace_indexes_and_catalog() creates workspaces, but there are no corresponding op.create_foreign_key calls.

You should add FKs from each root table’s workspace column to workspaces.name in both branches, and ensure the downgrade drops these FKs before op.drop_table("workspaces").

Example for one table (SQLite branch):

with _with_batch("experiments") as batch_op:
    ...
    batch_op.add_column(_workspace_column())
    batch_op.create_unique_constraint(
        "uq_experiments_workspace_name",
        ["workspace", "name"],
    )
+   batch_op.create_foreign_key(
+       "fk_experiments_workspace",
+       "workspaces",
+       ["workspace"],
+       ["name"],
+   )

And similarly for the non‑SQLite branch using op.create_foreign_key(...) after adding the workspace columns.

Also applies to: 265-356, 378-485


288-315: Align cascade behavior on model registry FKs to avoid orphaned children

FKs tying model registry child tables to registered_models / model_versions are inconsistent:

  • fk_registered_model_aliases_registered_models has onupdate="CASCADE", ondelete="CASCADE" (both SQLite and non‑SQLite), which is good.
  • But the FKs for:
    • model_versions.workspace,nameregistered_models.workspace,name
    • registered_model_tags.workspace,nameregistered_models.workspace,name
    • model_version_tags.workspace,name,versionmodel_versions.workspace,name,version
      are created with onupdate="CASCADE" only (no ondelete) in both SQLite (batch_op.create_foreign_key) and non‑SQLite (op.create_foreign_key) paths.
  • In downgrade, the legacy FKs model_versions_name_fkey, registered_model_tags_name_fkey, and model_version_tags_mv_fkey are recreated with onupdate="CASCADE" only as well.

This means deleting a registered model will cascade to aliases but not to model versions or tags, leaving orphans and requiring manual cleanup.

Recommend adding ondelete="CASCADE" to all of these FKs in both upgrade and downgrade definitions, e.g.:

batch_op.create_foreign_key(
    "fk_model_versions_registered_models",
    "registered_models",
    ["workspace", "name"],
    ["workspace", "name"],
-   onupdate="CASCADE",
+   onupdate="CASCADE",
+   ondelete="CASCADE",
)

and the analogous op.create_foreign_key(...) and legacy *_name_fkey definitions.

Also applies to: 316-332, 405-419, 420-437, 449-456, 468-476, 568-581, 598-611, 613-624, 696-703, 704-711, 721-728

tests/resources/db/latest_schema.sql (2)

19-31: Workspace FKs to the workspaces catalog are still missing

As noted in the earlier review, the root tables with workspace (evaluation_datasets, experiments, registered_models, webhooks) are not constrained to workspaces(name), so invalid workspace names can be inserted. If you decide to enforce catalog integrity, this file (and the migration + dialect-specific schemas) will need corresponding FKs.

Also applies to: 34-45, 81-88, 91-103, 106-110


184-201: Cascade semantics remain inconsistent across workspace-related FKs

registered_model_aliases cascades on delete, but model_versions, registered_model_tags, and model_version_tags only specify ON UPDATE CASCADE. If the intent is to automatically clean up all child metadata when a registered model is deleted, the other FKs likely also need ON DELETE CASCADE; otherwise, documenting the asymmetry would help future maintainers.

Also applies to: 204-211, 214-221, 367-375

tests/db/schemas/sqlite.sql (2)

106-110: Critical: SQLite schema missing foreign key constraints to workspaces table.

The SQLite schema lacks foreign key constraints from workspace columns to workspaces.name, allowing invalid workspace references. This issue was flagged in previous reviews and remains unresolved.

Expected FK constraints for SQLite:

  • evaluation_datasets.workspace → workspaces.name
  • experiments.workspace → workspaces.name
  • registered_models.workspace → workspaces.name
  • webhooks.workspace → workspaces.name

198-200: Inconsistent ON DELETE CASCADE across model registry foreign keys.

The SQLite schema shows inconsistent cascade deletion behavior:

  • Line 210: registered_model_aliases includes ON DELETE CASCADE
  • Lines 200, 220, 374: model_versions, registered_model_tags, model_version_tags lack ON DELETE CASCADE

This issue was flagged in previous reviews and remains unresolved. The migration should be updated to include consistent ON DELETE CASCADE behavior across all model registry foreign keys for referential integrity.

Also applies to: 208-210, 218-220, 372-374

mlflow/store/model_registry/dbmodels/models.py (4)

126-133: Add ondelete="cascade" to the foreign key constraint for consistency.

The ForeignKeyConstraint includes onupdate="cascade" but lacks ondelete="cascade". While the ORM-level relationship (line 123) specifies cascade="all", having database-level cascade deletion ensures referential integrity even for direct SQL operations.

This issue was flagged in previous reviews and remains unresolved.

Apply this diff:

     __table_args__ = (
         ForeignKeyConstraint(
             ["workspace", "name"],
             ["registered_models.workspace", "registered_models.name"],
             onupdate="cascade",
+            ondelete="cascade",
         ),
         PrimaryKeyConstraint("workspace", "name", "version", name="model_version_pk"),
     )

176-183: Add ondelete="cascade" to maintain consistency.

Similar to SqlModelVersion, this foreign key constraint should include ondelete="cascade" for database-level referential integrity.

This issue was flagged in previous reviews and remains unresolved.

Apply this diff:

     __table_args__ = (
         ForeignKeyConstraint(
             ["workspace", "name"],
             ["registered_models.workspace", "registered_models.name"],
             onupdate="cascade",
+            ondelete="cascade",
         ),
         PrimaryKeyConstraint("workspace", "key", "name", name="registered_model_tag_pk"),
     )

211-218: Add ondelete="cascade" for referential integrity.

The foreign key constraint should include ondelete="cascade" to ensure tags are removed when the parent model version is deleted, maintaining consistency with the ORM-level cascade behavior.

This issue was flagged in previous reviews and remains unresolved.

Apply this diff:

     __table_args__ = (
         ForeignKeyConstraint(
             ["workspace", "name", "version"],
             ["model_versions.workspace", "model_versions.name", "model_versions.version"],
             onupdate="cascade",
+            ondelete="cascade",
         ),
         PrimaryKeyConstraint("workspace", "key", "name", "version", name="model_version_tag_pk"),
     )

42-47: Critical: FK constraints to workspaces table are missing from root models.

All workspace columns across SqlRegisteredModel, SqlModelVersion, SqlRegisteredModelTag, SqlModelVersionTag, SqlRegisteredModelAlias, and SqlWebhook lack foreign key constraints to the workspaces table. This allows invalid workspace values.

This critical issue was flagged in previous reviews and remains unresolved.

Root-level models (SqlRegisteredModel and SqlWebhook) must add FK constraints to enforce workspace existence:

For SqlRegisteredModel (around line 57):

     __table_args__ = (
+        ForeignKeyConstraint(
+            ["workspace"],
+            ["workspaces.name"],
+            name="fk_registered_models_workspace",
+        ),
         PrimaryKeyConstraint("workspace", "name", name="registered_model_pk"),
     )

For SqlWebhook (around lines 317-322):

     __table_args__ = (
+        ForeignKeyConstraint(
+            ["workspace"],
+            ["workspaces.name"],
+            name="fk_webhooks_workspace",
+        ),
         PrimaryKeyConstraint("webhook_id", name="webhook_pk"),
         Index("idx_webhooks_status", "status"),
         Index("idx_webhooks_name", "name"),
         Index("idx_webhooks_workspace", "workspace"),
     )

Also applies to: 88-93, 158-163, 196-201, 236-241, 301-306

tests/db/schemas/mssql.sql (2)

105-109: Critical: Add foreign key constraints from workspace columns to the workspaces table.

The MSSQL schema lacks foreign key constraints from workspace columns to the workspaces catalog table, allowing invalid workspace references.

This critical issue was flagged in previous reviews and remains unresolved.

Add FK constraints for MSSQL dialect:

-- Add after evaluation_datasets table definition
ALTER TABLE evaluation_datasets ADD CONSTRAINT fk_evaluation_datasets_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);

-- Add after experiments table definition  
ALTER TABLE experiments ADD CONSTRAINT fk_experiments_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);

-- Add after registered_models table definition
ALTER TABLE registered_models ADD CONSTRAINT fk_registered_models_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);

-- Add after webhooks table definition
ALTER TABLE webhooks ADD CONSTRAINT fk_webhooks_workspace 
  FOREIGN KEY(workspace) REFERENCES workspaces(name);

196-198: Inconsistent ON DELETE CASCADE in MSSQL schema.

The MSSQL schema has the same inconsistent cascade behavior:

  • registered_model_aliases (line 207) has ON DELETE CASCADE
  • model_versions (line 197), registered_model_tags (line 217), and model_version_tags (line 366) lack ON DELETE CASCADE

This major issue was flagged in previous reviews and remains unresolved.

Apply the fix:

 	workspace VARCHAR(63) COLLATE "SQL_Latin1_General_CP1_CI_AS" DEFAULT ('default') NOT NULL,
 	CONSTRAINT model_version_pk PRIMARY KEY (workspace, name, version),
-	CONSTRAINT fk_model_versions_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON UPDATE CASCADE
+	CONSTRAINT fk_model_versions_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON DELETE CASCADE ON UPDATE CASCADE
 )

 ...

 	workspace VARCHAR(63) COLLATE "SQL_Latin1_General_CP1_CI_AS" DEFAULT ('default') NOT NULL,
 	CONSTRAINT registered_model_tag_pk PRIMARY KEY (workspace, key, name),
-	CONSTRAINT fk_registered_model_tags_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON UPDATE CASCADE
+	CONSTRAINT fk_registered_model_tags_registered_models FOREIGN KEY(workspace, name) REFERENCES registered_models (workspace, name) ON DELETE CASCADE ON UPDATE CASCADE
 )

 ...

 	workspace VARCHAR(63) COLLATE "SQL_Latin1_General_CP1_CI_AS" DEFAULT ('default') NOT NULL,
 	CONSTRAINT model_version_tag_pk PRIMARY KEY (workspace, key, name, version),
-	CONSTRAINT fk_model_version_tags_model_versions FOREIGN KEY(workspace, name, version) REFERENCES model_versions (workspace, name, version) ON UPDATE CASCADE
+	CONSTRAINT fk_model_version_tags_model_versions FOREIGN KEY(workspace, name, version) REFERENCES model_versions (workspace, name, version) ON DELETE CASCADE ON UPDATE CASCADE
 )

Also applies to: 206-208, 216-218, 365-367

🧹 Nitpick comments (22)
mlflow/R/mlflow/tests/testthat/test-rest.R (1)

69-69: LGTM! Modern mocking API adopted correctly.

The migration from the deprecated with_mock to with_mocked_bindings is correct and properly targets the httr package namespace for mocking the GET function.

Minor formatting nitpick: There's an extra space in .package = "httr" (double space before =). Consider normalizing to single space for consistency.

-  with_mocked_bindings(.package  = "httr", GET = function(...) {
+  with_mocked_bindings(.package = "httr", GET = function(...) {
mlflow/R/mlflow/tests/testthat/test-model-registry.R (1)

14-14: LGTM! Consistent migration to modern mocking API.

All test functions have been correctly updated to use with_mocked_bindings(.package = "mlflow", ...) instead of the deprecated with_mock. The package namespace targeting is appropriate, and all test assertions remain intact.

Minor style inconsistency: Lines 42-43 place the .package parameter on a separate line, while all other tests keep it on the same line as with_mocked_bindings. Consider normalizing for consistency:

-  with_mocked_bindings(
-    .package = "mlflow",
+  with_mocked_bindings(.package = "mlflow",

Also applies to: 42-43, 79-79, 96-96, 115-115, 135-135, 148-148, 166-166, 211-211, 226-226, 242-242, 262-262, 282-282, 303-303, 323-323, 344-344

mlflow/R/mlflow/tests/testthat/test-client.R (1)

96-151: Approve with optional formatting refinement.

The migration to with_mocked_bindings for httr functions is correct, properly specifying .package = "httr".

Minor spacing inconsistency: lines 101, 120, and 136 use .package = (double space) while the rest of the file uses .package = (single space).

Apply this diff to normalize spacing:

- with_mocked_bindings(.package  = "httr", POST = function(...) {
+ with_mocked_bindings(.package = "httr", POST = function(...) {
- with_mocked_bindings(.package  = "httr", GET = function(...) {
+ with_mocked_bindings(.package = "httr", GET = function(...) {
- with_mocked_bindings(.package  = "httr", POST = function(...) {
+ with_mocked_bindings(.package = "httr", POST = function(...) {
mlflow/cli/__init__.py (1)

551-557: Good UX improvement with informative default messages.

The runtime default computation with user-facing messages provides excellent transparency. Users will know exactly which URIs are being used, which helps with debugging and understanding the system behavior.

Consider whether these messages should go to stderr (using click.echo(..., err=True)) instead of stdout to avoid interfering with command output parsing, similar to the security middleware messages at lines 572-596.

Apply this diff to send messages to stderr:

-        click.echo(f"Backend store URI not provided. Using {backend_store_uri}")
+        click.echo(f"Backend store URI not provided. Using {backend_store_uri}", err=True)

And:

-        click.echo(f"Registry store URI not provided. Using {registry_store_uri}")
+        click.echo(f"Registry store URI not provided. Using {registry_store_uri}", err=True)
mlflow/server/otel_api.py (1)

129-139: Clarify what “completed trace” means and consider checking the root span’s end time

The logic for counting “completed” traces currently treats “having a root span in this batch” as completion:

for span in trace_spans:
    if span.parent_id is None:
        # Only count traces with a root span as completed
        completed_trace_ids.add(trace_id)
        break

This may overcount if the root span is emitted before it has actually ended (e.g., if an exporter batches or streams spans mid-trace).

If the intention is truly “root span has finished”, consider also checking the root span’s end time:

-                for span in trace_spans:
-                    if span.parent_id is None:
-                        # Only count traces with a root span as completed
-                        # (logging of the root span indicates a completed trace)
-                        completed_trace_ids.add(trace_id)
-                        break
+                for span in trace_spans:
+                    if span.parent_id is None and span.end_time_ns is not None:
+                        # Only count traces whose root span has finished as completed
+                        completed_trace_ids.add(trace_id)
+                        break

If your exporters only send the root span once it’s finished, the current logic is functionally fine but the comment might be misleading; at minimum, aligning the comment to the actual behavior would help future readers.

It would be good to confirm how your OTLP exporters batch root spans (e.g., whether they’re ever sent before completion); if they always flush on end, this becomes purely a documentation choice.

tests/cli/test_eval.py (1)

176-224: Integration test for evaluate_traces + custom scorer is solid, with minor robustness considerations

The new test_evaluate_traces_integration wires together:

  • Real traces created via mlflow.start_span() with inputs/outputs.
  • An inline scorer defined with @scorer that parses a numeric score from outputs.
  • A patch of mlflow.cli.eval.resolve_scorers to inject that scorer.
  • A post-check via mlflow.search_traces(..., return_type="list") to assert that each trace has exactly one simple_scorer assessment with the expected value.

This gives good end-to-end coverage of the CLI, scorer resolution, and trace assessment persistence.

Two small things to keep in mind:

  • The assertion len(traces) == 3 assumes that evaluation does not create additional traces in the same location. If future changes cause evaluate() to log extra traces, this may become brittle; alternatively, you could filter down to the original trace_ids or assert “≥ 3” and then restrict the subsequent checks to known traces.
  • Sorting by t.data.spans[0].outputs is clever and should be stable given how you constructed the traces, but if the trace structure changes (e.g., multiple spans with outputs), basing the sort on trace.info.trace_id or the parsed score from simple_scorer might be a bit more future-proof.

As-is, the test is clear and valuable; these are just considerations if the tracing/eval plumbing evolves.

If you later extend evaluate() to emit additional traces or alter how outputs are stored in spans, please revisit this test to ensure the assumptions about len(traces) and t.data.spans[0].outputs still hold.

tests/tracing/test_otel_logging.py (1)

662-748: Consider verifying traces were persisted to the database.

The test correctly verifies telemetry events but doesn't confirm that the 2 traces were actually logged to the database. While the test focus is on telemetry, adding a database verification (similar to other tests in this file) would provide more complete coverage.

To verify database persistence, you could add at the end:

    traces = mlflow.search_traces(
        experiment_ids=[experiment_id], include_spans=False, return_type="list"
    )
    assert len(traces) == 2
tests/tracking/_tracking_service/test_utils.py (3)

48-56: Make default-URI tests robust to pre-set MLFLOW_TRACKING_URI.

This test (and the ones below that call _get_store() with no store_uri) assumes the default tracking URI, but doesn't clear MLFLOW_TRACKING_URI. If a developer runs the suite with this env var already set, these assertions could fail for reasons unrelated to the behavior under test. Consider explicitly deleting it at the start, e.g.:

 def test_tracking_scheme_with_existing_mlruns(tmp_path, monkeypatch):
-    monkeypatch.chdir(tmp_path)
+    monkeypatch.chdir(tmp_path)
+    monkeypatch.delenv(MLFLOW_TRACKING_URI.name, raising=False)

and similarly for the other default-URI tests in this block.


65-75: Consider factoring out a small helper to create a legacy mlruns layout.

This test repeats the same mlruns/0/meta.yaml setup as test_tracking_scheme_with_existing_mlruns. A tiny helper like create_legacy_mlruns(tmp_path) would reduce duplication and keep the intent of “existing file-store data” in one place, making future changes to the legacy-detection heuristic easier.


78-85: Be aware these tests instantiate a real SqlAlchemyStore (potentially slower DB path).

Unlike test_get_store_sqlalchemy_store* further down, these two tests call _get_store() expecting a SqlAlchemyStore without patching sqlalchemy.create_engine or schema helpers. That likely exercises real SQLite initialization against tmp_path, which is fine functionally but may be slower and more brittle if schema upgrade logic changes.

If CI time or flakiness becomes a concern, consider mirroring the lower tests and patching:

  • sqlalchemy.create_engine
  • mlflow.store.db.utils._verify_schema
  • mlflow.store.db.utils._initialize_tables

around these _get_store() calls while still asserting that the selection logic prefers SqlAlchemyStore when mlruns lacks valid meta.yaml.

Also applies to: 87-94

tests/db/compose.yml (1)

14-22: Pinned DB images to digests for deterministic CI behavior

Referencing the database images by content digest improves reproducibility of migration tests without altering service wiring. If you ever need to track upstream versions, consider a brief comment noting the source tag, but this is fine as-is.

Also applies to: 40-48, 67-73

docs/docs/self-hosting/security/basic-http-auth.mdx (1)

7-14: Scorer permissions docs look good; consider minor polish

The new references to scorers and the dedicated permissions table are clear and consistent with the experiment and registered model sections. Two small nits you may want to address:

  • Line 101: “an user” → “a user” for grammar.
  • In the scorer table, the first two rows annotate permissions as “can_update (on experiment)” / “can_read (on experiment)” while later rows just say “can_read” / “can_delete”. For consistency with the earlier experiment/model tables, either drop the parenthetical or clarify for all rows whether the permission is evaluated on the experiment or on the scorer resource itself.

Also applies to: 99-103, 584-629

mlflow/store/model_registry/file_store.py (1)

135-143: More actionable deprecation warning; consider registry-specific wording

The expanded warning with an explicit database example and GitHub issue link is a nice improvement for guiding users off the filesystem backend.

Since this class is the model registry FileStore, you might want to tweak the text from “Filesystem tracking backend (e.g., './mlruns')” to something that also (or primarily) mentions the filesystem-backed model registry, or to a generic “filesystem backend”, so the warning feels accurate in both tracking and registry contexts.

mlflow/server/auth/db/migrations/versions/0965eb92f5f0_add_scorer_permissions.py (1)

19-29: Consider adding a foreign key constraint for experiment_id.

The scorer_permissions table references experiment_id but doesn't define a foreign key constraint to the experiments table. While this might be intentional (e.g., to allow soft references or avoid cascading issues), it differs from typical relational design patterns.

Additionally, verify that scorer_name length of 256 is sufficient for all expected scorer identifiers.

If a foreign key is appropriate, apply this diff:

 def upgrade() -> None:
     op.create_table(
         "scorer_permissions",
         sa.Column("id", sa.Integer(), nullable=False, primary_key=True),
         sa.Column("experiment_id", sa.String(length=255), nullable=False),
         sa.Column("scorer_name", sa.String(length=256), nullable=False),
         sa.Column("user_id", sa.Integer(), nullable=False),
         sa.Column("permission", sa.String(length=255), nullable=True),
+        sa.ForeignKeyConstraint(["experiment_id"], ["experiments.experiment_id"], name="fk_scorer_perm_experiment_id"),
         sa.ForeignKeyConstraint(["user_id"], ["users.id"], name="fk_scorer_perm_user_id"),
         sa.UniqueConstraint("experiment_id", "scorer_name", "user_id", name="unique_scorer_user"),
     )
mlflow/ml-package-versions.yml (1)

753-755: Temporary constraint with open issue - consider tracking.

The qdrant-client<1.16.0 constraint is marked as temporary pending resolution of an upstream issue. Consider creating a tracking issue in your repository to ensure this constraint is revisited once the upstream issue is resolved.

tests/store/tracking/test_sqlalchemy_store_schema.py (1)

13-15: Temporary import - track cleanup.

This import is marked as temporary and should be removed once workspace models are properly integrated into the store layer. Consider creating a tracking issue to ensure this technical debt is addressed.

The comment indicates the proper solution:

# This can be removed once we have a workspace store imported.
mlflow/server/auth/entities.py (1)

10-11: ScorerPermission wiring is consistent; consider defending against None / missing keys

The new scorer_permissions field on User and the ScorerPermission class mirror the existing experiment and registered-model permission patterns, which keeps the API surface uniform.

To make this a bit more robust:

  • If there’s any code path that instantiates User without explicitly providing a list, self.scorer_permissions may be None, and "[p.to_json() for p in self.scorer_permissions]" will fail. You could defensively treat None as [] in to_json (and optionally in the property getter), e.g., for p in self.scorer_permissions or [].
  • Likewise, from_json currently assumes "scorer_permissions" is always present. If you expect to ever deserialize older payloads that don’t include this key, dictionary.get("scorer_permissions", []) would make the upgrade path smoother.

If the store layer always sets a concrete list and all callers are version-locked, current code is fine; the above would just future-proof the entity API a bit.

Also applies to: 18-18, 56-63, 73-73, 90-92, 182-230

tests/server/auth/test_sqlalchemy_store.py (1)

10-15: Scorer-permission test coverage is solid and mirrors existing patterns

The _sp_maker helper and new tests (test_create_scorer_permission, test_get_scorer_permission, list/update/delete, and delete_scorer_permissions_for_scorer) nicely parallel the existing experiment and registered-model permission tests, covering:

  • happy paths (entity fields, user_id wiring),
  • duplicate creation (RESOURCE_ALREADY_EXISTS),
  • invalid permission (INVALID_PARAMETER_VALUE), and
  • not-found scenarios (RESOURCE_DOES_NOT_EXIST).

Optionally, you could:

  • Add an isinstance(sp1, ScorerPermission) assertion in test_get_scorer_permission (as done for other permission types), and
  • Add an explicit “invalid permission on update” test for scorer permissions, mirroring test_update_experiment_permission / test_update_registered_model_permission.

Not required, but they’d keep parity across all three permission types.

Also applies to: 48-50, 468-623

tests/server/auth/test_auth.py (1)

635-805: Scorer permission tests cover main flows; a couple of small consistency nits

The three scorer tests exercise registration, MANAGE default, READ-only access, and delete-denial paths end‑to‑end and look logically correct.

Two minor polish suggestions:

  • In test_register_and_delete_scorer, the admin request to GET_SCORER_PERMISSION uses hard‑coded "admin", "password1234" under a User(username1, ...) context. Consider either:
    • Using ADMIN_USERNAME / ADMIN_PASSWORD for consistency with the rest of the file, or
    • Dropping the User context entirely around that admin call since you already pass explicit auth=....
  • The scorer_json = '{"name": "test_scorer", "type": "pyfunc"}' literal is duplicated across tests; extracting a small helper/constant would slightly reduce duplication, but is purely optional.
tests/db/test_workspace_migration.py (2)

47-55: Consider including webhooks in workspace column and index assertions

The migration adds workspace and idx_webhooks_workspace on webhooks, but _WORKSPACE_TABLES and _assert_workspace_columns() skip that table, and the upgrade test only checks workspace indexes for experiments, registered_models, and evaluation_datasets. This leaves the webhooks workspace column and index untested.

Suggest adding "webhooks" to _WORKSPACE_TABLES and asserting its workspace index similarly to the other tables.

Also applies to: 143-145, 829-842


57-95: Deduplicate workspace-aware table definitions to reduce confusion

_REGISTERED_MODEL_TAGS, _MODEL_VERSION_TAGS, _REGISTERED_MODEL_ALIASES, and _EVALUATION_DATASETS are defined twice with identical workspace-aware schemas. The earlier _LEGACY_* definitions already cover the pre-workspace shape.

Recommend keeping a single workspace-aware definition for each of these and relying on the _LEGACY_* tables for the legacy shape to make the test helpers easier to follow.

Also applies to: 587-684

mlflow/server/auth/sqlalchemy_store.py (1)

12-17: Scorer permission CRUD is consistent with existing permission patterns

The scorer permission methods mirror the experiment and registered model permission implementations:

  • Use _validate_permission() before writes.
  • Resolve users via _get_user() with proper RESOURCE_DOES_NOT_EXIST / INVALID_STATE handling.
  • Map IntegrityError to RESOURCE_ALREADY_EXISTS for creates.
  • Return ScorerPermission entities via to_mlflow_entity().

This is idiomatic for the existing store and should integrate cleanly with the new REST and client APIs.

Also applies to: 18-23, 269-356

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64ac13f and c0691c6.

⛔ Files ignored due to path filters (1)
  • docs/static/images/deep-learning/dl-training-ui.gif is excluded by !**/*.gif
📒 Files selected for processing (69)
  • .github/workflows/master.yml (1 hunks)
  • README.md (1 hunks)
  • docs/api_reference/api_inventory.txt (4 hunks)
  • docs/docs/classic-ml/deep-learning/pytorch/index.mdx (1 hunks)
  • docs/docs/community/usage-tracking.mdx (1 hunks)
  • docs/docs/self-hosting/security/basic-http-auth.mdx (3 hunks)
  • docs/docusaurus.config.ts (3 hunks)
  • docs/sidebars.ts (1 hunks)
  • docs/sidebarsClassicML.ts (3 hunks)
  • libs/skinny/README_SKINNY.md (1 hunks)
  • libs/typescript/core/tests/clients/client.test.ts (4 hunks)
  • mlflow/R/mlflow/NAMESPACE (0 hunks)
  • mlflow/R/mlflow/R/databricks-utils.R (2 hunks)
  • mlflow/R/mlflow/R/tracking-rest.R (4 hunks)
  • mlflow/R/mlflow/tests/testthat/test-client.R (7 hunks)
  • mlflow/R/mlflow/tests/testthat/test-databricks-utils.R (5 hunks)
  • mlflow/R/mlflow/tests/testthat/test-model-h2o.R (2 hunks)
  • mlflow/R/mlflow/tests/testthat/test-model-registry.R (16 hunks)
  • mlflow/R/mlflow/tests/testthat/test-model-xgboost.R (3 hunks)
  • mlflow/R/mlflow/tests/testthat/test-model.R (4 hunks)
  • mlflow/R/mlflow/tests/testthat/test-params.R (1 hunks)
  • mlflow/R/mlflow/tests/testthat/test-rest.R (1 hunks)
  • mlflow/R/mlflow/tests/testthat/test-run.R (4 hunks)
  • mlflow/cli/__init__.py (4 hunks)
  • mlflow/cli/eval.py (1 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/ml-package-versions.yml (31 hunks)
  • mlflow/server/auth/__init__.py (11 hunks)
  • mlflow/server/auth/client.py (2 hunks)
  • mlflow/server/auth/db/migrations/versions/0965eb92f5f0_add_scorer_permissions.py (1 hunks)
  • mlflow/server/auth/db/models.py (4 hunks)
  • mlflow/server/auth/entities.py (5 hunks)
  • mlflow/server/auth/routes.py (1 hunks)
  • mlflow/server/auth/sqlalchemy_store.py (2 hunks)
  • mlflow/server/otel_api.py (5 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/model_registry/file_store.py (1 hunks)
  • mlflow/store/model_registry/sqlalchemy_store.py (6 hunks)
  • mlflow/store/tracking/__init__.py (1 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/tracking/file_store.py (1 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/telemetry/events.py (2 hunks)
  • mlflow/tracking/_tracking_service/utils.py (3 hunks)
  • mlflow/tracking/request_header/default_request_header_provider.py (1 hunks)
  • mlflow/utils/cli_args.py (3 hunks)
  • mlflow/utils/server_cli_utils.py (2 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • tests/cli/test_eval.py (5 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/compose.yml (3 hunks)
  • tests/db/schemas/mssql.sql (13 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
  • tests/resources/db/latest_schema.sql (6 hunks)
  • tests/server/auth/db/test_cli.py (2 hunks)
  • tests/server/auth/test_auth.py (2 hunks)
  • tests/server/auth/test_sqlalchemy_store.py (3 hunks)
  • tests/store/model_registry/test_file_store.py (1 hunks)
  • tests/store/tracking/test_file_store.py (1 hunks)
  • tests/store/tracking/test_sqlalchemy_store_schema.py (1 hunks)
  • tests/tracing/test_otel_logging.py (2 hunks)
  • tests/tracking/_tracking_service/test_utils.py (1 hunks)
💤 Files with no reviewable changes (1)
  • mlflow/R/mlflow/NAMESPACE
✅ Files skipped from review due to trivial changes (3)
  • README.md
  • mlflow/R/mlflow/tests/testthat/test-params.R
  • mlflow/store/tracking/file_store.py
🚧 Files skipped from review as they are similar to previous changes (7)
  • mlflow/store/workspace/dbmodels/init.py
  • .github/workflows/master.yml
  • mlflow/utils/workspace_utils.py
  • mlflow/store/workspace/dbmodels/models.py
  • mlflow/store/tracking/dbmodels/models.py
  • tests/db/schemas/mysql.sql
  • docs/docusaurus.config.ts
🧰 Additional context used
🧬 Code graph analysis (21)
docs/sidebars.ts (1)
docs/docusaurusConfigUtils.ts (1)
  • apiReferencePrefix (6-16)
tests/db/check_migration.py (1)
mlflow/store/model_registry/sqlalchemy_store.py (4)
  • create_model_version (730-838)
  • set_registered_model_alias (1247-1270)
  • create_registered_model (172-214)
  • create_webhook (1335-1367)
mlflow/server/otel_api.py (4)
mlflow/entities/span.py (5)
  • events (191-207)
  • trace_id (116-118)
  • trace_id (785-789)
  • parent_id (146-150)
  • parent_id (812-813)
mlflow/telemetry/events.py (2)
  • TraceSource (326-330)
  • TracesReceivedByServerEvent (333-334)
mlflow/telemetry/track.py (1)
  • _record_event (86-101)
mlflow/store/tracking/sqlalchemy_store.py (1)
  • log_spans (3289-3468)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
tests/db/test_schema.py (1)
  • _get_unique_constraints (90-107)
docs/sidebarsClassicML.ts (1)
docs/docusaurusConfigUtils.ts (1)
  • apiReferencePrefix (6-16)
mlflow/server/auth/routes.py (1)
mlflow/server/handlers.py (1)
  • _get_rest_path (3710-3711)
mlflow/store/model_registry/sqlalchemy_store.py (1)
mlflow/store/model_registry/dbmodels/models.py (3)
  • SqlRegisteredModelTag (155-190)
  • SqlModelVersionTag (193-230)
  • SqlRegisteredModelAlias (233-268)
tests/db/test_workspace_migration.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)
  • upgrade (144-484)
  • downgrade (487-732)
tests/cli/test_eval.py (3)
mlflow/genai/scorers/base.py (1)
  • scorer (847-1031)
mlflow/tracking/fluent.py (1)
  • set_experiment (130-230)
mlflow/cli/eval.py (1)
  • evaluate_traces (61-128)
mlflow/utils/server_cli_utils.py (1)
mlflow/utils/uri.py (1)
  • is_local_uri (28-69)
tests/tracking/_tracking_service/test_utils.py (3)
mlflow/tracking/_tracking_service/utils.py (1)
  • _get_store (252-253)
mlflow/store/tracking/file_store.py (1)
  • FileStore (187-2811)
mlflow/store/tracking/sqlalchemy_store.py (1)
  • SqlAlchemyStore (188-4511)
tests/tracing/test_otel_logging.py (2)
mlflow/telemetry/client.py (2)
  • TelemetryClient (27-335)
  • add_record (113-129)
mlflow/telemetry/events.py (2)
  • TraceSource (326-330)
  • TracesReceivedByServerEvent (333-334)
mlflow/entities/__init__.py (1)
mlflow/entities/workspace.py (1)
  • Workspace (9-13)
tests/server/auth/test_auth.py (3)
tests/server/auth/auth_test_utils.py (2)
  • create_user (13-26)
  • User (29-41)
mlflow/server/auth/entities.py (7)
  • User (1-93)
  • permission (116-117)
  • permission (120-121)
  • permission (159-160)
  • permission (163-164)
  • permission (208-209)
  • permission (212-213)
tests/tracking/integration_test_utils.py (1)
  • _send_rest_tracking_post_request (112-120)
mlflow/tracking/_tracking_service/utils.py (4)
tests/store/tracking/test_file_store.py (1)
  • store (78-79)
mlflow/store/model_registry/file_store.py (1)
  • FileStore (122-1115)
mlflow/utils/file_utils.py (3)
  • exists (127-128)
  • path_to_local_file_uri (481-485)
  • path (281-282)
mlflow/environment_variables.py (2)
  • get (41-51)
  • get (75-98)
mlflow/cli/__init__.py (1)
mlflow/tracking/_tracking_service/utils.py (3)
  • _get_default_tracking_uri (55-60)
  • is_tracking_uri_set (63-67)
  • set_tracking_uri (70-121)
tests/server/auth/test_sqlalchemy_store.py (6)
mlflow/server/auth/entities.py (19)
  • ExperimentPermission (96-136)
  • RegisteredModelPermission (139-179)
  • ScorerPermission (182-230)
  • User (1-93)
  • experiment_id (108-109)
  • experiment_id (196-197)
  • scorer_name (200-201)
  • username (25-26)
  • permission (116-117)
  • permission (120-121)
  • permission (159-160)
  • permission (163-164)
  • permission (208-209)
  • permission (212-213)
  • id (21-22)
  • name (151-152)
  • user_id (112-113)
  • user_id (155-156)
  • user_id (204-205)
mlflow/server/auth/__init__.py (4)
  • create_scorer_permission (1398-1404)
  • get_scorer_permission (1408-1413)
  • update_scorer_permission (1417-1423)
  • delete_scorer_permission (1427-1432)
mlflow/server/auth/sqlalchemy_store.py (6)
  • create_scorer_permission (269-290)
  • get_scorer_permission (319-325)
  • list_scorer_permissions (327-335)
  • update_scorer_permission (337-344)
  • delete_scorer_permission (346-349)
  • delete_scorer_permissions_for_scorer (351-356)
tests/helper_functions.py (1)
  • random_str (49-56)
mlflow/exceptions.py (1)
  • MlflowException (67-113)
mlflow/protos/databricks_pb2.pyi (1)
  • ErrorCode (17-97)
mlflow/server/auth/client.py (3)
mlflow/server/auth/entities.py (18)
  • ExperimentPermission (96-136)
  • RegisteredModelPermission (139-179)
  • ScorerPermission (182-230)
  • User (1-93)
  • experiment_id (108-109)
  • experiment_id (196-197)
  • scorer_name (200-201)
  • username (25-26)
  • permission (116-117)
  • permission (120-121)
  • permission (159-160)
  • permission (163-164)
  • permission (208-209)
  • permission (212-213)
  • from_json (77-93)
  • from_json (131-136)
  • from_json (174-179)
  • from_json (224-230)
mlflow/server/auth/__init__.py (4)
  • create_scorer_permission (1398-1404)
  • get_scorer_permission (1408-1413)
  • update_scorer_permission (1417-1423)
  • delete_scorer_permission (1427-1432)
mlflow/server/auth/sqlalchemy_store.py (4)
  • create_scorer_permission (269-290)
  • get_scorer_permission (319-325)
  • update_scorer_permission (337-344)
  • delete_scorer_permission (346-349)
mlflow/server/auth/db/models.py (1)
mlflow/server/auth/entities.py (20)
  • ExperimentPermission (96-136)
  • RegisteredModelPermission (139-179)
  • ScorerPermission (182-230)
  • User (1-93)
  • scorer_permissions (57-58)
  • scorer_permissions (61-62)
  • id (21-22)
  • experiment_id (108-109)
  • experiment_id (196-197)
  • scorer_name (200-201)
  • user_id (112-113)
  • user_id (155-156)
  • user_id (204-205)
  • permission (116-117)
  • permission (120-121)
  • permission (159-160)
  • permission (163-164)
  • permission (208-209)
  • permission (212-213)
  • name (151-152)
mlflow/server/auth/sqlalchemy_store.py (5)
mlflow/server/auth/db/models.py (6)
  • SqlScorerPermission (77-94)
  • SqlUser (21-42)
  • to_mlflow_entity (31-42)
  • to_mlflow_entity (53-58)
  • to_mlflow_entity (69-74)
  • to_mlflow_entity (88-94)
mlflow/server/auth/entities.py (18)
  • ExperimentPermission (96-136)
  • RegisteredModelPermission (139-179)
  • ScorerPermission (182-230)
  • User (1-93)
  • experiment_id (108-109)
  • experiment_id (196-197)
  • scorer_name (200-201)
  • username (25-26)
  • permission (116-117)
  • permission (120-121)
  • permission (159-160)
  • permission (163-164)
  • permission (208-209)
  • permission (212-213)
  • user_id (112-113)
  • user_id (155-156)
  • user_id (204-205)
  • id (21-22)
mlflow/server/auth/__init__.py (4)
  • create_scorer_permission (1398-1404)
  • get_scorer_permission (1408-1413)
  • update_scorer_permission (1417-1423)
  • delete_scorer_permission (1427-1432)
mlflow/server/auth/client.py (4)
  • create_scorer_permission (548-578)
  • get_scorer_permission (580-605)
  • update_scorer_permission (607-634)
  • delete_scorer_permission (636-657)
mlflow/exceptions.py (1)
  • MlflowException (67-113)
mlflow/server/auth/__init__.py (6)
mlflow/java/client/src/main/java/org/mlflow/api/proto/Service.java (17)
  • DeleteScorer (183911-185270)
  • Response (21470-22086)
  • Response (23709-24774)
  • Response (26225-26881)
  • Response (27544-27953)
  • Response (28616-29025)
  • Response (29724-30133)
  • Response (31176-31832)
  • Response (33594-34250)
  • Response (35528-35937)
  • Response (36600-37009)
  • Response (37936-38345)
  • Response (40450-40859)
  • Response (42191-42600)
  • Response (43705-44114)
  • Response (45088-45497)
  • Response (46805-47214)
mlflow/server/auth/entities.py (15)
  • experiment_id (108-109)
  • experiment_id (196-197)
  • name (151-152)
  • username (25-26)
  • permission (116-117)
  • permission (120-121)
  • permission (159-160)
  • permission (163-164)
  • permission (208-209)
  • permission (212-213)
  • scorer_name (200-201)
  • to_json (64-74)
  • to_json (123-128)
  • to_json (166-171)
  • to_json (215-221)
mlflow/server/auth/client.py (4)
  • get_scorer_permission (580-605)
  • create_scorer_permission (548-578)
  • update_scorer_permission (607-634)
  • delete_scorer_permission (636-657)
mlflow/server/auth/sqlalchemy_store.py (5)
  • get_scorer_permission (319-325)
  • create_scorer_permission (269-290)
  • delete_scorer_permissions_for_scorer (351-356)
  • update_scorer_permission (337-344)
  • delete_scorer_permission (346-349)
mlflow/utils/proto_json_utils.py (1)
  • parse_dict (196-199)
mlflow/server/handlers.py (1)
  • catch_mlflow_exception (723-734)

Comment on lines +439 to +474
def train_distributed():
# Initialize distributed training
dist.init_process_group(backend="nccl")
rank = dist.get_rank()

# Wrap model with DDP
model_ddp = DDP(model.to(rank), device_ids=[rank])

# Create distributed sampler
from torch.utils.data.distributed import DistributedSampler

sampler = DistributedSampler(
train_dataset, num_replicas=dist.get_world_size(), rank=rank
)
train_loader = DataLoader(train_dataset, batch_size=32, sampler=sampler)

# Only log from rank 0
if rank == 0:
mlflow.start_run()
mlflow.log_params({"world_size": dist.get_world_size(), "backend": "nccl"})

# Training loop
epochs = 10
for epoch in range(epochs):
sampler.set_epoch(epoch) # Shuffle data differently each epoch
train_loss = train_epoch(model_ddp, train_loader)

# Log metrics from rank 0 only
if rank == 0:
mlflow.log_metric("train_loss", train_loss, step=epoch)

# Save model from rank 0
if rank == 0:
mlflow.pytorch.log_model(model, name="distributed_model")
mlflow.end_run()
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add cleanup and clarify distributed training execution.

The distributed training example has several issues:

  1. Missing cleanup: The process group is initialized but never destroyed, which can lead to resource leaks.
  2. Incomplete execution context: The example doesn't show how to launch the distributed training script (e.g., using torch.distributed.launch or torchrun).

Apply this diff to add proper cleanup:

     # Save model from rank 0
     if rank == 0:
         mlflow.pytorch.log_model(model, name="distributed_model")
         mlflow.end_run()
+
+    # Cleanup distributed resources
+    dist.destroy_process_group()

Additionally, consider adding a comment explaining how to run the distributed script:

# Run this script with torchrun:
# torchrun --nproc_per_node=2 train_distributed.py
def train_distributed():
    # Initialize distributed training
    dist.init_process_group(backend="nccl")
    ...
🤖 Prompt for AI Agents
In docs/docs/classic-ml/deep-learning/pytorch/index.mdx around lines 439 to 474,
the distributed training snippet never tears down the process group and lacks
guidance on how to launch the script; wrap the init/ training/ logging in a
try/finally (or try/except/finally) so that on all code paths you call
dist.destroy_process_group(), ensure mlflow.end_run() is called from rank 0 in
the finally block to avoid orphaned runs, and add a top-line comment showing the
recommended launch command (e.g., "Run this script with torchrun: torchrun
--nproc_per_node=2 train_distributed.py") so users know how to execute the
distributed job.

- [Daniel Lok](https://github.com/daniellok-db)
- [Gabriel Fu](https://github.com/gabrielfu)
- [Harutaka Kawamura](https://github.com/harupy)
- [Joel Robin P](https://github.com/joelrobin18)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Manual edit to autogenerated file—change the source instead.

Line 1 declares this file as autogenerated and forbids manual edits. The addition of Joel Robin P should be made in dev/pyproject.py (the source generator) rather than manually edited here. This ensures the change persists if the file is regenerated.

Regenerate the file after updating the source:

python dev/pyproject.py

Then remove the manual edit and commit the regenerated version.

🤖 Prompt for AI Agents
In libs/skinny/README_SKINNY.md around line 333, the file was manually edited to
add "Joel Robin P"; because the file is autogenerated you must instead add that
contributor to the generator source dev/pyproject.py, run the generator (python
dev/pyproject.py) to regenerate README_SKINNY.md, verify the change appears,
remove the manual edit from this file, and commit the regenerated file so the
change persists on future generations.

Comment thread mlflow/server/otel_api.py
Comment on lines +23 to +29
from mlflow.telemetry.events import TraceSource, TracesReceivedByServerEvent
from mlflow.telemetry.track import _record_event
from mlflow.tracing.utils.otlp import MLFLOW_EXPERIMENT_ID_HEADER, OTLP_TRACES_PATH
from mlflow.tracking.request_header.default_request_header_provider import (
_MLFLOW_PYTHON_CLIENT_USER_AGENT_PREFIX,
_USER_AGENT,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Emit a string for source instead of the raw Enum in telemetry params

The OTEL handler wiring looks correct: you derive trace_source from the User-Agent prefix and emit a single TracesReceivedByServerEvent summarizing the request. One thing to tighten:

  • You currently pass the Enum instance as a param:
trace_source = (
    TraceSource.MLFLOW_PYTHON_CLIENT
    if user_agent and user_agent.startswith(_MLFLOW_PYTHON_CLIENT_USER_AGENT_PREFIX)
    else TraceSource.UNKNOWN
)

_record_event(
    TracesReceivedByServerEvent,
    {
        "source": trace_source,
        "count": len(completed_trace_ids),
    },
)

Depending on how telemetry records are serialized, storing an Enum instead of a plain string can be brittle (e.g., JSON encoding or schema validation).

I recommend emitting the Enum’s value:

-            _record_event(
-                TracesReceivedByServerEvent,
-                {
-                    "source": trace_source,
-                    "count": len(completed_trace_ids),
-                },
-            )
+            _record_event(
+                TracesReceivedByServerEvent,
+                {
+                    "source": trace_source.value,
+                    "count": len(completed_trace_ids),
+                },
+            )

This also matches the documented example payload {"source": "MLFLOW_PYTHON_CLIENT", "count": 3}.

Please double-check how telemetry records are serialized (e.g., via Pydantic / JSON) to confirm you don’t rely on Enum objects being stored as-is; if you already normalize Enums centrally, this diff is still a harmless simplification.

Also applies to: 159-172


I'll verify the review comment by examining how telemetry records are serialized and whether the Enum needs to be converted to a string value.


---

🏁 Script executed:

```shell
#!/bin/bash
# Inspect the actual code at lines 23-29 and 159-172 in mlflow/server/otel_api.py
echo "=== Lines 23-29 ==="
sed -n '23,29p' mlflow/server/otel_api.py
echo ""
echo "=== Lines 159-172 ==="
sed -n '159,172p' mlflow/server/otel_api.py

Length of output: 878


🏁 Script executed:

#!/bin/bash
# Find and inspect _record_event function
echo "=== Finding _record_event definition ==="
rg -nP 'def _record_event' --type=py -A 10

Length of output: 846


🏁 Script executed:

#!/bin/bash
# Find TracesReceivedByServerEvent definition to check if it's Pydantic
echo "=== Finding TracesReceivedByServerEvent ==="
rg -nP 'class TracesReceivedByServerEvent' --type=py -A 15

Length of output: 191


🏁 Script executed:

#!/bin/bash
# Check how telemetry params are serialized
echo "=== Checking telemetry serialization ==="
rg -nP 'TracesReceivedByServerEvent' --type=py -B 3 -A 3 | head -50

Length of output: 3671


Use .value when emitting Enum to telemetry params

The review comment is correct. Test assertions in tests/tracing/test_otel_logging.py (lines 658 and 747) explicitly validate that the telemetry record params contain string values, not Enum instances:

assert record.params["source"] == TraceSource.MLFLOW_PYTHON_CLIENT.value
assert record.params["source"] == TraceSource.UNKNOWN.value

The current code at lines 159–172 in mlflow/server/otel_api.py passes the raw Enum ("source": trace_source), which would fail these tests. The _record_event function does not normalize Enums centrally; it passes the params dict as-is to the telemetry Record. Therefore, you must emit the Enum's string value:

_record_event(
    TracesReceivedByServerEvent,
    {
-       "source": trace_source,
+       "source": trace_source.value,
        "count": len(completed_trace_ids),
    },
)

This fix applies to both occurrences in the file.

🤖 Prompt for AI Agents
In mlflow/server/otel_api.py around lines 159 to 172, the telemetry params
currently pass the Enum object for the "source" field (trace_source) which
causes tests to fail; update both occurrences to emit the Enum's string value by
using trace_source.value when building the params dict (e.g., "source":
trace_source.value) so that _record_event receives plain strings instead of Enum
instances.

# Also used as default location for artifacts, when not provided, in non local file based backends
# (eg MySQL)
DEFAULT_LOCAL_FILE_AND_ARTIFACT_PATH = "./mlruns"
DEFAULT_TRACKING_URI = "sqlite:///mlflow.db"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider the implications of using a relative path for the default SQLite database.

The value "sqlite:///mlflow.db" uses three slashes, indicating a relative path. This means the database file will be created in the current working directory when MLflow commands are executed.

Users who run MLflow commands from different directories may inadvertently create multiple separate database files, leading to confusion when their experiments appear to be "missing."

Verify that:

  1. This behavior is intentional and documented for users
  2. The backward compatibility logic (checking for existing mlruns data) properly handles directory changes
  3. Users are guided to set MLFLOW_TRACKING_URI explicitly for production use
🤖 Prompt for AI Agents
mlflow/store/tracking/__init__.py around line 13: the default
DEFAULT_TRACKING_URI is a relative SQLite path ("sqlite:///mlflow.db") which
causes DB files to be created in whatever CWD a user runs MLflow from; change
the default to compute an absolute path at runtime (e.g., under the user home or
a well-known per-user directory like ~/.mlflow/mlflow.db) instead of a
hard-coded relative URI, update the backward-compatibility lookup logic to also
check the previous relative locations (CWD/mlflow.db and mlruns directories)
when migrating/locating existing data, and add/update docs and an inline comment
advising users to set MLFLOW_TRACKING_URI explicitly for production and
describing the default fallback behavior.


"""
return backend_store_uri == DEFAULT_LOCAL_FILE_AND_ARTIFACT_PATH
return backend_store_uri == DEFAULT_TRACKING_URI

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify this change aligns with the new default tracking URI semantics.

The comparison now checks against DEFAULT_TRACKING_URI (sqlite:///mlflow.db) instead of DEFAULT_LOCAL_FILE_AND_ARTIFACT_PATH (./mlruns). This means the validation logic at line 49 will now consider sqlite:///mlflow.db as the "default" backend store URI when checking artifacts-only mode.

Ensure that the CLI runtime default computation (in mlflow/cli/init.py) can return either value depending on existing data, and that this validation correctly handles both cases.

Run the following script to verify how the default is computed and used:


🏁 Script executed:

#!/bin/bash
# Description: Check how _get_default_tracking_uri is used and what values it can return

# Find usages of _get_default_tracking_uri
rg -nP --type=py -C3 '_get_default_tracking_uri\(\)'

# Find usages of _is_default_backend_store_uri
rg -nP --type=py -C3 '_is_default_backend_store_uri'

# Check artifacts_only_config_validation usage
rg -nP --type=py -C5 'artifacts_only_config_validation'

Length of output: 5028


🏁 Script executed:

#!/bin/bash
# Get the full implementation of _get_default_tracking_uri
rg -nP --type=py -A10 'def _get_default_tracking_uri'

# Get constant definitions
rg -nP --type=py 'DEFAULT_TRACKING_URI\s*=' 
rg -nP --type=py 'DEFAULT_LOCAL_FILE_AND_ARTIFACT_PATH\s*='

# Check path_to_local_file_uri usage
rg -nP --type=py -B2 -A2 'path_to_local_file_uri'

Length of output: 14303


🏁 Script executed:

#!/bin/bash
# Check what constants are imported in server_cli_utils.py
rg -nP --type=py -B10 '_is_default_backend_store_uri' mlflow/utils/server_cli_utils.py | head -30

# Check the imports at top of server_cli_utils.py
head -50 mlflow/utils/server_cli_utils.py

Length of output: 2464


Fix validation logic to recognize both possible default backend store URIs.

The change at line 45 introduces a critical bug: _is_default_backend_store_uri() now only checks for DEFAULT_TRACKING_URI ("sqlite:///mlflow.db"), but _get_default_tracking_uri() can return either "sqlite:///mlflow.db" or "./mlruns" (when legacy data exists).

When artifacts-only mode is used with existing mlruns data, the validation will incorrectly reject "./mlruns" as a non-default value, even though it is a legitimate default. The comparison at line 45 must account for both possible defaults:

return backend_store_uri in (DEFAULT_TRACKING_URI, DEFAULT_LOCAL_FILE_AND_ARTIFACT_PATH)
🤖 Prompt for AI Agents
In mlflow/utils/server_cli_utils.py around line 45, the helper
_is_default_backend_store_uri currently only checks equality with
DEFAULT_TRACKING_URI and fails to recognize the alternate legacy default
"./mlruns"; update the validation so it returns True when backend_store_uri
matches either DEFAULT_TRACKING_URI or DEFAULT_LOCAL_FILE_AND_ARTIFACT_PATH.
Replace the single equality check with a membership test against the two known
default constants to accept both possible default backend URIs.

Comment on lines +632 to +660
def test_otel_trace_received_telemetry_from_mlflow_client(mlflow_server: str):
"""
Test TraceReceivedByServerEvent telemetry shows source=MLFLOW_PYTHON_CLIENT for standard client.

Uses @mlflow.trace with standard MLflow client configuration, which automatically sends
User-Agent and X-MLflow-Client-Version headers to identify traces from MLflow client.
"""
mlflow.set_tracking_uri(mlflow_server)
mlflow.set_experiment("otel-telemetry-mlflow-client-test")

with mock.patch("mlflow.telemetry.track.get_telemetry_client") as mock_get_client:
mock_client = mock.MagicMock(spec=TelemetryClient)
mock_get_client.return_value = mock_client

@mlflow.trace
def test_function():
return "test result"

result = test_function()
assert result == "test result"

time.sleep(1)

if mock_client.add_record.called:
record = mock_client.add_record.call_args[0][0]
assert record.event_name == TracesReceivedByServerEvent.name
assert record.params["source"] == TraceSource.MLFLOW_PYTHON_CLIENT.value
assert record.params["count"] == 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Test may pass silently without verifying telemetry.

The conditional check at line 655 means the test will pass even if add_record is never called, defeating the test's purpose. Unlike the second test (line 742), which properly uses assert_called_once(), this test uses optional assertions inside an if block.

Apply this diff to fix the assertion logic:

-        time.sleep(1)
-
-        if mock_client.add_record.called:
-            record = mock_client.add_record.call_args[0][0]
-            assert record.event_name == TracesReceivedByServerEvent.name
-            assert record.params["source"] == TraceSource.MLFLOW_PYTHON_CLIENT.value
-            assert record.params["count"] == 1
+        time.sleep(1)
+
+        mock_client.add_record.assert_called_once()
+        record = mock_client.add_record.call_args[0][0]
+        assert record.event_name == TracesReceivedByServerEvent.name
+        assert record.params["source"] == TraceSource.MLFLOW_PYTHON_CLIENT.value
+        assert record.params["count"] == 1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_otel_trace_received_telemetry_from_mlflow_client(mlflow_server: str):
"""
Test TraceReceivedByServerEvent telemetry shows source=MLFLOW_PYTHON_CLIENT for standard client.
Uses @mlflow.trace with standard MLflow client configuration, which automatically sends
User-Agent and X-MLflow-Client-Version headers to identify traces from MLflow client.
"""
mlflow.set_tracking_uri(mlflow_server)
mlflow.set_experiment("otel-telemetry-mlflow-client-test")
with mock.patch("mlflow.telemetry.track.get_telemetry_client") as mock_get_client:
mock_client = mock.MagicMock(spec=TelemetryClient)
mock_get_client.return_value = mock_client
@mlflow.trace
def test_function():
return "test result"
result = test_function()
assert result == "test result"
time.sleep(1)
if mock_client.add_record.called:
record = mock_client.add_record.call_args[0][0]
assert record.event_name == TracesReceivedByServerEvent.name
assert record.params["source"] == TraceSource.MLFLOW_PYTHON_CLIENT.value
assert record.params["count"] == 1
def test_otel_trace_received_telemetry_from_mlflow_client(mlflow_server: str):
"""
Test TraceReceivedByServerEvent telemetry shows source=MLFLOW_PYTHON_CLIENT for standard client.
Uses @mlflow.trace with standard MLflow client configuration, which automatically sends
User-Agent and X-MLflow-Client-Version headers to identify traces from MLflow client.
"""
mlflow.set_tracking_uri(mlflow_server)
mlflow.set_experiment("otel-telemetry-mlflow-client-test")
with mock.patch("mlflow.telemetry.track.get_telemetry_client") as mock_get_client:
mock_client = mock.MagicMock(spec=TelemetryClient)
mock_get_client.return_value = mock_client
@mlflow.trace
def test_function():
return "test result"
result = test_function()
assert result == "test result"
time.sleep(1)
mock_client.add_record.assert_called_once()
record = mock_client.add_record.call_args[0][0]
assert record.event_name == TracesReceivedByServerEvent.name
assert record.params["source"] == TraceSource.MLFLOW_PYTHON_CLIENT.value
assert record.params["count"] == 1
🤖 Prompt for AI Agents
In tests/tracing/test_otel_logging.py around lines 632 to 660, the test
currently wraps assertions about mock_client.add_record inside an if block so
the test can silently pass when add_record was never called; replace the
conditional with a strict assertion (e.g., assert mock_client.add_record.called
or mock_client.add_record.assert_called_once()) and then extract the recorded
call (record = mock_client.add_record.call_args[0][0]) and assert
record.event_name, record.params["source"] and record.params["count"] as before
so the test fails if no telemetry was sent.

This adds the required workspace columns and the workspace catalog table
with the default workspace precreated. All workspace columns default to
"default" for now and we may choose to remove the defaults once the
tracking store and model registry store are made workspace aware to
catch application logic issues not properly setting the workspace.

Some model registry store changes were needed to account for the new
composite foreign key.

Signed-off-by: mprahl <mprahl@users.noreply.github.com>
@mprahl mprahl force-pushed the workspaces-db-model branch from c0691c6 to 1f0b624 Compare November 19, 2025 14:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
mlflow/genai/evaluation/entities.py (1)

171-190: tables property looks good, but __repr__ has a pandas truth‑value bug

The new tables property is straightforward and matches the new tests’ expectations ("eval_results" mapped to the same result_df when present, empty dict otherwise) — no issues there.

However, __repr__ currently does:

result_df_str = (
    f"{len(self.result_df)} rows x {len(self.result_df.columns)} cols"
    if self.result_df
    else "None"
)

Using if self.result_df on a pd.DataFrame will raise ValueError: The truth value of a DataFrame is ambiguous when result_df is a non‑empty DataFrame. This is a latent correctness bug that will surface whenever EvaluationResult is printed or logged.

A safe fix is to test explicitly for None:

-        result_df_str = (
-            f"{len(self.result_df)} rows x {len(self.result_df.columns)} cols"
-            if self.result_df
-            else "None"
-        )
+        if self.result_df is not None:
+            result_df_str = (
+                f"{len(self.result_df)} rows x {len(self.result_df.columns)} cols"
+            )
+        else:
+            result_df_str = "None"

With that change, the new tables property and the existing __repr__ are both safe and consistent.

mlflow/store/tracking/databricks_rest_store.py (1)

165-189: Critical: Handle None location in endpoint construction.

Making location optional with a default of None aligns with the abstract base class and other store implementations, but the endpoint construction on line 187 will fail if location is None. The f-string f"{_V4_TRACE_REST_API_PATH_PREFIX}/{location}/batchGet" will interpolate None into the URL, resulting in an invalid endpoint like .../None/batchGet.

Consider one of these approaches:

  1. Add validation to require location for this Databricks implementation:
 def batch_get_traces(self, trace_ids: list[str], location: str | None = None) -> list[Trace]:
     """
     Get a batch of complete traces with spans for given trace ids.

     Args:
         trace_ids: List of trace IDs to fetch.
         location: Location of the trace. For example, "catalog.schema" for UC schema.

     Returns:
         List of Trace objects.
     """
+    if location is None:
+        raise MlflowException(
+            "location parameter is required for Databricks trace store",
+            error_code=INVALID_PARAMETER_VALUE,
+        )
     trace_ids = [parse_trace_id_v4(trace_id)[1] for trace_id in trace_ids]
  1. Or handle the None case by extracting location from trace IDs or falling back to a different behavior (if applicable).
mlflow/store/model_registry/sqlalchemy_store.py (1)

216-243: Add workspace filtering to all read/search helper methods

The review comment is accurate. Verification confirms that all the identified helper methods lack workspace filtering despite tables having workspace-keyed primary keys:

  • SqlRegisteredModel uses PrimaryKeyConstraint("workspace", "name") but _get_registered_model() filters only on name
  • SqlModelVersion uses PrimaryKeyConstraint("workspace", "name", "version") but _get_sql_model_version() conditions include only name, version, and stage—no workspace
  • Similar gaps exist in _get_registered_model_tag(), _get_model_version_tag(), and _get_registered_model_alias()

While workspace=DEFAULT_WORKSPACE_NAME is set when creating records, this is insufficient for multi-workspace safety. The query methods must explicitly filter by workspace to prevent data collisions and eliminate the "Expected only 1 … Found {len(rms)}" errors that would occur with multiple workspaces.

Recommend adding workspace filtering (either explicit DEFAULT_WORKSPACE_NAME or parametrized) to all seven helper methods at lines 216–243, 377–440, 849–885, 1165–1185, 1237–1245, 1292–1326, and the associated tag/alias queries.

♻️ Duplicate comments (10)
tests/db/check_migration.py (1)

161-164: Avoid hard‑coding "default"; reuse the shared workspace constant and consider empty‑table semantics.

Two follow‑ups here:

  1. Shared constant instead of string literal (repeat of earlier feedback).
    Using the literal "default" for the workspace name will drift if DEFAULT_WORKSPACE_NAME ever changes. Importing and using the shared constant keeps this CI check aligned with the rest of the codebase:
-from mlflow.store.tracking.dbmodels.models import (
+from mlflow.store.tracking.dbmodels.models import (
     SqlModelVersion,
     ...
 )
+from mlflow.utils.workspace_utils import DEFAULT_WORKSPACE_NAME
...
-        for table in WORKSPACE_TABLES:
-            df = pd.read_sql(sa.text(f"SELECT DISTINCT workspace FROM {table}"), conn)
-            assert not df["workspace"].isna().any(), f"{table} contains NULL workspace values"
-            assert set(df["workspace"]) == {"default"}, f"{table} contains non-default workspaces"
+        for table in WORKSPACE_TABLES:
+            df = pd.read_sql(sa.text(f"SELECT DISTINCT workspace FROM {table}"), conn)
+            assert not df["workspace"].isna().any(), f"{table} contains NULL workspace values"
+            assert set(df["workspace"]) == {DEFAULT_WORKSPACE_NAME}, (
+                f"{table} contains non-default workspaces"
+            )
  1. Optional: bail out early for empty tables.
    You’ve mitigated the previous empty‑table issue by seeding more rows, but if any of these tables are ever legitimately empty in a migration scenario, set(df["workspace"]) == {DEFAULT_WORKSPACE_NAME} will still fail spuriously. Adding a simple if df.empty: continue keeps the check robust without weakening the signal.
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (2)

219-252: Still missing foreign keys from workspace columns to workspaces.name

The migration creates the workspaces catalog and adds non‑nullable workspace columns, but it never declares FKs from those columns to workspaces.name (for experiments, registered_models, evaluation_datasets, and webhooks). This allows rows with arbitrary workspace values that don’t exist in the catalog, undermining the integrity the catalog is meant to provide.

Please add ForeignKey/create_foreign_key constraints from each root table’s workspace column to workspaces(name) in both the SQLite batch blocks and the non‑SQLite path, and ensure the downgrade drops those FKs before removing the column/table.

Also applies to: 268-278, 351-356, 377-385, 478-485, 730-732


288-301: model_versions and tag tables still lack ON DELETE CASCADE to registered models

The upgraded schema continues the previous inconsistency where:

  • registered_model_aliases FKs to registered_models specify ondelete="CASCADE", but
  • model_versions and registered_model_tags FKs to registered_models, and model_version_tags FKs to model_versions, only use onupdate="CASCADE".

Deleting a registered model can therefore leave orphaned versions and tags. To keep referential behavior consistent with aliases and avoid manual cleanup, extend these FKs in both the SQLite batch operations and the non‑SQLite path to include ondelete="CASCADE" alongside the existing onupdate="CASCADE".

For example:

 batch_op.create_foreign_key(
     "fk_model_versions_registered_models",
     "registered_models",
     ["workspace", "name"],
     ["workspace", "name"],
-    onupdate="CASCADE",
+    onupdate="CASCADE",
+    ondelete="CASCADE",
 )

and similarly for the corresponding registered_model_tags / model_version_tags FKs and their non‑workspace counterparts in downgrade().

Also applies to: 309-314, 321-331, 411-418, 430-437, 449-456, 574-580, 590-596, 606-611, 618-624, 696-703, 705-711, 713-720, 721-728

tests/resources/db/latest_schema.sql (2)

19-31: Workspaces catalog exists but workspace columns still lack foreign key constraints.

evaluation_datasets.workspace, experiments.workspace, registered_models.workspace, and webhooks.workspace are now present, and workspaces(name) is defined, but none of these columns are constrained to the catalog. That still allows invalid workspace values and breaks the intent of the workspace table.

Recommend adding FKs like:

ALTER TABLE evaluation_datasets
  ADD CONSTRAINT fk_evaluation_datasets_workspace
  FOREIGN KEY (workspace) REFERENCES workspaces (name);

ALTER TABLE experiments
  ADD CONSTRAINT fk_experiments_workspace
  FOREIGN KEY (workspace) REFERENCES workspaces (name);

ALTER TABLE registered_models
  ADD CONSTRAINT fk_registered_models_workspace
  FOREIGN KEY (workspace) REFERENCES workspaces (name);

ALTER TABLE webhooks
  ADD CONSTRAINT fk_webhooks_workspace
  FOREIGN KEY (workspace) REFERENCES workspaces (name);

Also applies to: 34-45, 81-88, 91-103, 106-110


184-201: Model‑related foreign keys still have inconsistent ON DELETE CASCADE settings.

registered_model_aliases cascades deletes correctly, but:

  • model_versionsregistered_models (fk_model_versions_registered_models)
  • registered_model_tagsregistered_models (fk_registered_model_tags_registered_models)
  • model_version_tagsmodel_versions (fk_model_version_tags_model_versions)

all lack ON DELETE CASCADE. Deleting a registered model will leave orphaned versions/tags unless they’re cleaned up separately.

Recommend updating these constraints to include ON DELETE CASCADE ON UPDATE CASCADE for consistency with aliases and to match expected cleanup semantics.

Also applies to: 214-221, 367-375

tests/db/schemas/mysql.sql (2)

19-31: MySQL schema: workspace columns are not constrained to the workspaces catalog.

The MySQL schema mirrors the new workspace columns and workspaces table but still lacks FKs:

  • evaluation_datasets.workspace
  • experiments.workspace
  • registered_models.workspace
  • webhooks.workspace

None reference workspaces(name), so invalid workspace names remain possible. This file should reflect the finalized migration by adding corresponding FOREIGN KEY (workspace) REFERENCES workspaces (name) constraints (with explicit CONSTRAINT names) on each of these tables.

Also applies to: 34-45, 81-88, 91-103, 106-110


183-200: MySQL schema: foreign keys for model versions and tags still lack ON DELETE CASCADE.

As in latest_schema.sql, the MySQL schema shows:

  • model_versionsregistered_models (fk_model_versions_registered_models) with only ON UPDATE CASCADE.
  • registered_model_tagsregistered_models (fk_registered_model_tags_registered_models) with only ON UPDATE CASCADE.
  • model_version_tagsmodel_versions (fk_model_version_tags_model_versions) with only ON UPDATE CASCADE.

while registered_model_aliases cascades on delete.

For consistency and to avoid orphaned rows after deleting a registered model or version, these constraints should also include ON DELETE CASCADE.

Also applies to: 213-220, 367-375

tests/db/schemas/sqlite.sql (1)

19-31: SQLite workspace schema still missing FKs to workspaces and consistent ON DELETE CASCADE

The workspace-aware schema is wired up (columns + workspaces table), but two previously flagged issues remain:

  1. No FK from workspace columns to workspaces(name)

    • evaluation_datasets.workspace (Line 29), experiments.workspace (Line 41), registered_models.workspace (Line 86), and webhooks.workspace (Line 101) are not constrained to workspaces(name). This allows invalid workspace values and defeats the point of the catalog table.
  2. Model registry FKs still lack ON DELETE CASCADE

    • model_versionsregistered_models (Line 200)
    • registered_model_tagsregistered_models (Line 220)
    • model_version_tagsmodel_versions (Line 374)
      These only specify ON UPDATE CASCADE, so deleting a registered model / version can leave orphan versions/tags in SQLite tests.

Please extend the migration and regenerate this schema so:

  • Those four root tables have FKs ... FOREIGN KEY(workspace) REFERENCES workspaces(name) ....
  • The three model registry FKs also include ON DELETE CASCADE (matching other dialects and ORM intent).

Also applies to: 34-45, 81-88, 90-103, 106-110, 184-201, 204-221, 367-375

tests/db/schemas/mssql.sql (1)

19-31: MSSQL workspace schema still lacks FKs to workspaces and full cascade semantics

The MSSQL schema mirrors the unresolved issues seen in SQLite:

  1. No FK from workspace columns to workspaces(name)

    • evaluation_datasets.workspace (Line 29), experiments.workspace (Line 41), registered_models.workspace (Line 85), and webhooks.workspace (Line 100) are unconstrained with respect to workspaces. With a dedicated workspaces table (Lines 105-109), these should each have a FK like:
    ALTER TABLE evaluation_datasets
      ADD CONSTRAINT fk_evaluation_datasets_workspace
        FOREIGN KEY(workspace) REFERENCES workspaces(name);

    and analogous constraints for experiments, registered_models, and webhooks.

  2. Inconsistent ON DELETE CASCADE for model registry FKs

    • fk_model_versions_registered_models (Line 197),
    • fk_registered_model_tags_registered_models (Line 217),
    • fk_model_version_tags_model_versions (Line 366)

    only specify ON UPDATE CASCADE, while fk_registered_model_aliases_registered_models (Line 207) includes ON DELETE CASCADE. For consistency and to prevent orphan rows, these three FKs should also include ON DELETE CASCADE.

Please update the migration that generates this schema and then regenerate mssql.sql so these constraints are reflected here.

Also applies to: 34-44, 80-87, 90-102, 105-109, 181-198, 201-208, 211-218, 359-367

mlflow/store/model_registry/dbmodels/models.py (1)

57-57: Add missing FK to workspaces and ondelete="cascade" on registry FKs

Two sets of integrity gaps called out earlier still exist:

  1. Root models lack FK to workspaces

    • SqlRegisteredModel.__table_args__ (Line 57) defines only the composite PK.
    • SqlWebhook.__table_args__ (Lines 317-322) has PK + indexes only.

    With a SqlWorkspace/workspaces catalog present, workspace on these root tables should be constrained, e.g.:

    class SqlRegisteredModel(Base):
        ...
  • table_args = (PrimaryKeyConstraint("workspace", "name", name="registered_model_pk"),)
  • table_args = (
  •    ForeignKeyConstraint(
    
  •        ["workspace"],
    
  •        ["workspaces.name"],
    
  •        name="fk_registered_models_workspace",
    
  •    ),
    
  •    PrimaryKeyConstraint("workspace", "name", name="registered_model_pk"),
    
  • )

and similarly for `SqlWebhook`:

```diff
class SqlWebhook(Base):
    ...
-    __table_args__ = (
-        PrimaryKeyConstraint("webhook_id", name="webhook_pk"),
-        Index("idx_webhooks_status", "status"),
-        Index("idx_webhooks_name", "name"),
-        Index("idx_webhooks_workspace", "workspace"),
-    )
+    __table_args__ = (
+        ForeignKeyConstraint(
+            ["workspace"],
+            ["workspaces.name"],
+            name="fk_webhooks_workspace",
+        ),
+        PrimaryKeyConstraint("webhook_id", name="webhook_pk"),
+        Index("idx_webhooks_status", "status"),
+        Index("idx_webhooks_name", "name"),
+        Index("idx_webhooks_workspace", "workspace"),
+    )

This prevents invalid workspace values on the root entities.

  1. Model registry FKs still omit ondelete="cascade"

    • SqlModelVersion.__table_args__ FK to registered_models (Lines 126-131).
    • SqlRegisteredModelTag.__table_args__ FK to registered_models (Lines 176-181).
    • SqlModelVersionTag.__table_args__ FK to model_versions (Lines 211-216).

    To align DB-level behavior with the ORM cascade="all" relationships and with other dialect schemas, these should add ondelete="cascade":

    ForeignKeyConstraint(
        ["workspace", "name"],
        ["registered_models.workspace", "registered_models.name"],
  •  onupdate="cascade",
    
  •  onupdate="cascade",
    
  •  ondelete="cascade",
    
    )
    
    and similarly for the other two constraints.
    
    

Tightening these constraints will make workspace and registry behavior consistent across DB backends and direct SQL/ORM operations.

Also applies to: 126-133, 176-183, 211-218, 252-261, 301-306, 317-322

🧹 Nitpick comments (13)
tests/genai/evaluate/test_evaluation.py (1)

133-154: Backward‑compat check is sound; consider making the expectation a bit more explicit

The new assertion correctly enforces that tables["eval_results"] exists and has the same row count as result_df, which is sufficient for the stated backward‑compat goal. If you want to make the contract clearer and guard against future regressions, you could optionally add:

  • An explicit membership check: 'eval_results' in result.tables
  • (Only if desired) a stronger assertion on contents, e.g. result.tables["eval_results"].equals(result.result_df)

Not required for correctness, but would better document the intended API behavior.

tests/store/tracking/test_sqlalchemy_store.py (1)

11352-11500: get_trace tests accurately cover success, not-found, partial, and complete cases; consider relaxing message matching

The new tests exercise:

  • Happy path where log_spans + get_trace returns spans with correct IDs and timing.
  • Not-found behavior mapping directly to the _get_trace “not found” exception.
  • Partial traces with TraceMetadataKey.SIZE_STATS both when allow_partial is True (returns 1 span) and False (retries then raises the “not fully exported yet” error).

All of this aligns with the current get_trace / _get_trace / _get_spans_with_trace_info semantics and gives solid coverage.

One minor robustness tweak you might consider: the tests that assert on exception messages (test_get_trace_not_found and the partial-trace branch) currently match almost the full message string. If those messages ever change slightly, otherwise-correct behavior will fail the tests. You could instead:

  • Match only the invariant part of the message with a looser regex (e.g., r"Trace with ID .* is not found\."), or
  • Assert on the error_code in addition to or instead of the full text, if exposed.

Not strictly necessary, but it would make the tests less brittle to future wording changes.

mlflow/server/handlers.py (1)

667-674: Stricter bool query parsing looks good; consider factoring into a helper if reused more.

Converting "true"/"false" query params into actual booleans with validation is a nice improvement for GET handlers (e.g., GetTrace.allow_partial). If more boolean fields show up in query strings later, you might want to centralize this pattern into a small helper to keep _get_request_message lean, but it's not required right now.

mlflow/store/tracking/rest_store.py (1)

80-81: RestStore.get_trace correctly targets the V3 endpoint; consider whether a fallback is needed.

The implementation is consistent with the other V3 trace APIs (/traces/search, /traces/batchGet): it builds a GetTrace request including allow_partial, hits f"{_V3_TRACE_REST_API_PATH_PREFIX}/get", and returns Trace.from_proto(response_proto.trace). That looks correct.

If you expect this client to talk to older tracking servers that don’t implement GetTrace, you may want a compatibility path similar to get_trace_info (e.g., detect ENDPOINT_NOT_FOUND and fall back to batch_get_traces or the previous trace‑info + artifact flow). If all call sites are version‑gated to new servers, the current behavior is fine.

Also applies to: 466-472

tests/store/tracking/test_rest_store.py (1)

49-71: Nice coverage of RestStore.get_trace; consider also asserting endpoint path

The new tests correctly validate that GetTrace is used and that allow_partial is wired through in the JSON body. You may also want to assert the endpoint kwarg in _call_endpoint (e.g. equals f"{_V3_TRACE_REST_API_PATH_PREFIX}/get") to lock in the V3 path and catch future regressions in routing.

Also applies to: 1310-1363

mlflow/tracing/otel/translation/__init__.py (1)

35-54: sanitize_attributes correctly handles double‑dumped JSON; tighten scope to string values

The normalization logic for double‑dumped strings/dicts looks right and being applied up front in translate_span_when_storing should prevent redundant JSON encoding. To reduce unnecessary exceptions and make the intent clearer, you could first guard on isinstance(value, str) before calling json.loads, and (if desired) decide whether lists should also be normalized the same way as dicts rather than remaining double‑encoded.

Also applies to: 207-233

tests/db/test_schema.py (1)

11-16: Dialect‑aware reattachment logic is sound; minor helper cleanup

The _reattach_missing_unique_constraints flow looks correct for MySQL/MSSQL and should stabilize schema dumps across dialects. _get_unique_constraints doesn’t use its dialect argument, though—dropping that parameter (and its callers) or wiring it into behavior would simplify the API a bit.

Also applies to: 46-107

mlflow/store/tracking/dbmodels/models.py (2)

64-65: Workspace column and per-workspace uniqueness look consistent

Using DEFAULT_WORKSPACE_NAME for both ORM default and server_default plus moving global name uniqueness to UniqueConstraint("workspace", "name") is consistent with the new schema. No functional issues here as long as all callers still treat the tracking store as single-workspace for now. If/when Experiment entities gain an explicit workspace field, this model will need to propagate it in to_mlflow_entity / constructors.

Also applies to: 92-103, 131-138


1303-1323: Evaluation dataset workspace default is safe but not yet surfaced in API

Adding a non-null workspace column with default DEFAULT_WORKSPACE_NAME and indexing it is aligned with the multi-tenant schema. However, SqlEvaluationDataset.to_mlflow_entity / from_mlflow_entity don’t surface or accept workspace, so new datasets are effectively pinned to the default workspace. That’s fine for a first step, but once non-default workspaces are supported you’ll need to thread workspace through these methods (and through any callers) rather than relying solely on the DB default.

Also applies to: 1375-1380, 1421-1444

mlflow/store/model_registry/sqlalchemy_store.py (1)

64-65: Registry writes are correctly workspace-qualified but hard-coded to the default

create_registered_model, create_model_version, and the various set_*_tag / set_registered_model_alias helpers now populate workspace=DEFAULT_WORKSPACE_NAME, and tag/alias rows are created with matching workspace. That’s consistent with the new composite keys and keeps the current store effectively single-tenant.

To avoid repeating the literal everywhere and to ease future evolution to multi-workspace support, consider centralizing workspace resolution (e.g., a _get_workspace() helper or constructor parameter) rather than inlining DEFAULT_WORKSPACE_NAME at each call site.

Also applies to: 172-207, 686-707, 730-821, 1186-1212, 1247-1270

mlflow/genai/judges/instructions_judge/__init__.py (2)

31-37: Multi-turn session support is well-structured; consider surfacing session in input fields and polishing messages

The new {{ conversation }} variable, is_multi_turn property, session argument to __call__, and _validate_session logic together give a clear and robust story:

  • All traces in a session must share TraceMetadataKey.TRACE_SESSION, with very helpful error messages (including a docs link) when that’s not true.
  • Conversation is derived via resolve_conversation_from_session and injected into the template/user message only when present.
  • The template validator enforces that {{ conversation }} can only be combined with {{ expectations }}, which keeps the prompting surface simple.

Two improvements worth considering:

  1. Expose session via get_input_fields
    Right now get_input_fields() only returns fields for inputs, outputs, expectations, and trace. For templates that use {{ conversation }}, adding a JudgeField(name="session", description=...) would make it clearer to tooling and UI layers that this judge expects a session argument.

  2. Align warnings with the public API parameter name
    _warn_unused_parameters warns about "conversation" being unused, but from a caller’s perspective the parameter they pass is session. Either adjust the warning text to reference session or have _warn_unused_parameters accept the original session flag and infer whether it produced a conversation value, to avoid confusing users.

These are ergonomics / API-clarity tweaks; the core design and validation look sound.

Also applies to: 52-63, 154-158, 189-219, 248-279, 364-425, 433-475, 550-575


220-247: Internal parameter naming around session vs conversation is a bit confusing

_check_required_parameters’s fifth parameter is still named session: list[Trace] | None, but __call__ now passes the derived conversation list into that slot. Given the current implementation only checks for None vs non-None, this works, but it obscures the real intent of the parameter and may trip up future maintainers or type-checking.

Similarly, _warn_unused_parameters operates on the derived conversation value, which is fine, but combined with the warning text (“parameters … 'conversation'”) it can be hard to trace back to the original session argument.

I’d suggest, when convenient:

  • Either rename the _check_required_parameters argument from session to conversation (and update the docstring), or change the call site to pass the original session and check for that instead.
  • Consider passing both session and conversation into _warn_unused_parameters, so you can warn in terms of the public parameter name while still deciding based on the derived conversation’s presence.

These are internal cleanups; they don’t affect runtime behavior today but will make the multi-turn path easier to reason about.

Also applies to: 280-308, 392-398, 433-438, 477-508, 550-575

mlflow/store/tracking/sqlalchemy_store.py (1)

788-799: Model metric logging behavior looks correct; consider avoiding duplicate run lookups.

The new _log_model_metrics flow correctly:

  • Filters to metric.model_id is not None.
  • Reuses _validate_metric and metric de-duplication.
  • Sanitizes values via sanitize_metric_value to align with regular metric logging.
  • Persists experiment_id and dataset fields through SqlLoggedModelMetric.

Two minor nits:

  • log_metric now calls _get_run / _check_run_is_active twice (once here, once in _log_metrics), which is redundant I/O; if this path becomes hot, consider refactoring to reuse the loaded run or accept experiment_id directly into _log_metrics.
  • _log_model_metrics’s experiment_id parameter is annotated as str but is passed an int from run.experiment_id in log_batch / log_metric; consider widening the type hint to int | str for static type sanity.

Also applies to: 891-941, 1598-1612

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0691c6 and 1f0b624.

📒 Files selected for processing (41)
  • docs/api_reference/api_inventory.txt (4 hunks)
  • docs/docs/self-hosting/security/basic-http-auth.mdx (4 hunks)
  • mlflow/entities/__init__.py (2 hunks)
  • mlflow/entities/workspace.py (1 hunks)
  • mlflow/genai/evaluation/entities.py (1 hunks)
  • mlflow/genai/judges/instructions_judge/__init__.py (17 hunks)
  • mlflow/genai/judges/make_judge.py (2 hunks)
  • mlflow/genai/scorers/base.py (3 hunks)
  • mlflow/genai/utils/trace_utils.py (1 hunks)
  • mlflow/java/client/src/main/java/org/mlflow/api/proto/Service.java (10 hunks)
  • mlflow/protos/service.proto (1 hunks)
  • mlflow/server/handlers.py (7 hunks)
  • mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1 hunks)
  • mlflow/store/model_registry/dbmodels/models.py (11 hunks)
  • mlflow/store/model_registry/sqlalchemy_store.py (6 hunks)
  • mlflow/store/tracking/abstract_store.py (1 hunks)
  • mlflow/store/tracking/databricks_rest_store.py (1 hunks)
  • mlflow/store/tracking/dbmodels/models.py (5 hunks)
  • mlflow/store/tracking/rest_store.py (2 hunks)
  • mlflow/store/tracking/sqlalchemy_store.py (6 hunks)
  • mlflow/store/workspace/dbmodels/__init__.py (1 hunks)
  • mlflow/store/workspace/dbmodels/models.py (1 hunks)
  • mlflow/tracing/otel/translation/__init__.py (4 hunks)
  • mlflow/tracing/otel/translation/base.py (1 hunks)
  • mlflow/utils/workspace_utils.py (1 hunks)
  • tests/db/check_migration.py (3 hunks)
  • tests/db/schemas/mssql.sql (13 hunks)
  • tests/db/schemas/mysql.sql (6 hunks)
  • tests/db/schemas/postgresql.sql (6 hunks)
  • tests/db/schemas/sqlite.sql (6 hunks)
  • tests/db/test_schema.py (3 hunks)
  • tests/db/test_workspace_migration.py (1 hunks)
  • tests/genai/evaluate/test_evaluation.py (1 hunks)
  • tests/genai/judges/test_make_judge.py (3 hunks)
  • tests/resources/db/latest_schema.sql (6 hunks)
  • tests/server/test_handlers.py (5 hunks)
  • tests/store/tracking/test_rest_store.py (2 hunks)
  • tests/store/tracking/test_sqlalchemy_store.py (2 hunks)
  • tests/store/tracking/test_sqlalchemy_store_schema.py (1 hunks)
  • tests/tracing/otel/test_span_translation.py (4 hunks)
  • tests/tracking/test_rest_tracking.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/genai/judges/test_make_judge.py
🚧 Files skipped from review as they are similar to previous changes (5)
  • mlflow/store/workspace/dbmodels/init.py
  • mlflow/store/workspace/dbmodels/models.py
  • tests/store/tracking/test_sqlalchemy_store_schema.py
  • mlflow/entities/workspace.py
  • tests/db/test_workspace_migration.py
🧰 Additional context used
🧬 Code graph analysis (20)
mlflow/entities/__init__.py (1)
mlflow/entities/workspace.py (1)
  • Workspace (9-13)
tests/db/test_schema.py (1)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
  • _get_unique_constraints (150-164)
tests/store/tracking/test_sqlalchemy_store.py (1)
mlflow/store/tracking/sqlalchemy_store.py (4)
  • log_metric (788-798)
  • log_spans (3301-3480)
  • get_trace (3554-3567)
  • start_trace (2566-2625)
mlflow/store/db_migrations/versions/1b5f0d9ad7c1_add_workspace_columns_and_catalog.py (1)
tests/db/test_schema.py (1)
  • _get_unique_constraints (90-107)
tests/db/check_migration.py (1)
mlflow/store/model_registry/sqlalchemy_store.py (4)
  • create_model_version (730-838)
  • set_registered_model_alias (1247-1270)
  • create_registered_model (172-214)
  • create_webhook (1335-1367)
mlflow/genai/scorers/base.py (2)
mlflow/genai/judges/instructions_judge/__init__.py (1)
  • is_multi_turn (155-157)
mlflow/protos/service_pb2.pyi (1)
  • Trace (983-989)
tests/server/test_handlers.py (6)
mlflow/exceptions.py (2)
  • MlflowException (67-113)
  • MlflowNotImplementedException (215-219)
mlflow/store/tracking/sqlalchemy_store.py (4)
  • _get_trace (3569-3594)
  • get_trace (3554-3567)
  • batch_get_traces (3596-3635)
  • get_trace_info (2627-2639)
mlflow/server/handlers.py (2)
  • _get_trace (2961-2976)
  • get_trace_artifact_handler (3186-3212)
mlflow/store/tracking/abstract_store.py (3)
  • get_trace (332-346)
  • batch_get_traces (348-362)
  • get_trace_info (320-330)
mlflow/store/tracking/rest_store.py (3)
  • get_trace (466-471)
  • batch_get_traces (473-488)
  • get_trace_info (438-464)
mlflow/tracing/client.py (2)
  • get_trace (138-201)
  • get_trace_info (122-136)
mlflow/genai/judges/instructions_judge/__init__.py (2)
mlflow/genai/utils/trace_utils.py (4)
  • resolve_conversation_from_session (152-184)
  • resolve_expectations_from_trace (187-213)
  • resolve_inputs_from_trace (104-125)
  • resolve_outputs_from_trace (128-149)
mlflow/exceptions.py (2)
  • MlflowException (67-113)
  • invalid_parameter_value (104-113)
mlflow/store/model_registry/sqlalchemy_store.py (1)
mlflow/store/model_registry/dbmodels/models.py (3)
  • SqlRegisteredModelTag (155-190)
  • SqlModelVersionTag (193-230)
  • SqlRegisteredModelAlias (233-268)
mlflow/genai/utils/trace_utils.py (3)
mlflow/protos/service_pb2.pyi (1)
  • Trace (983-989)
mlflow/entities/trace.py (1)
  • Trace (26-332)
mlflow/entities/span.py (2)
  • inputs (158-160)
  • outputs (163-165)
mlflow/server/handlers.py (9)
mlflow/protos/service_pb2.pyi (1)
  • GetTrace (798-809)
mlflow/exceptions.py (4)
  • MlflowException (67-113)
  • invalid_parameter_value (104-113)
  • MlflowTracingException (172-181)
  • MlflowNotImplementedException (215-219)
mlflow/store/tracking/sqlalchemy_store.py (4)
  • _get_trace (3569-3594)
  • get_trace (3554-3567)
  • batch_get_traces (3596-3635)
  • get_trace_info (2627-2639)
mlflow/store/tracking/abstract_store.py (3)
  • get_trace (332-346)
  • batch_get_traces (348-362)
  • get_trace_info (320-330)
mlflow/store/tracking/rest_store.py (3)
  • get_trace (466-471)
  • batch_get_traces (473-488)
  • get_trace_info (438-464)
mlflow/tracing/client.py (3)
  • get_trace (138-201)
  • store (74-75)
  • get_trace_info (122-136)
mlflow/entities/trace.py (2)
  • to_proto (317-325)
  • to_dict (44-45)
mlflow/store/tracking/databricks_rest_store.py (2)
  • batch_get_traces (165-189)
  • get_trace_info (191-213)
mlflow/store/tracking/file_store.py (1)
  • get_trace_info (1755-1765)
mlflow/store/tracking/rest_store.py (5)
mlflow/java/client/src/main/java/org/mlflow/api/proto/Service.java (2)
  • GetTrace (90511-91932)
  • Trace (111624-112580)
mlflow/protos/service_pb2.pyi (2)
  • GetTrace (798-809)
  • Trace (983-989)
mlflow/store/tracking/abstract_store.py (1)
  • get_trace (332-346)
mlflow/entities/trace.py (2)
  • Trace (26-332)
  • from_proto (328-332)
mlflow/utils/proto_json_utils.py (1)
  • message_to_json (112-129)
mlflow/store/tracking/databricks_rest_store.py (4)
mlflow/store/tracking/abstract_store.py (1)
  • batch_get_traces (348-362)
mlflow/store/tracking/sqlalchemy_store.py (1)
  • batch_get_traces (3596-3635)
mlflow/store/tracking/rest_store.py (1)
  • batch_get_traces (473-488)
mlflow/entities/trace.py (1)
  • Trace (26-332)
mlflow/store/tracking/abstract_store.py (7)
mlflow/store/tracking/sqlalchemy_store.py (2)
  • get_trace (3554-3567)
  • batch_get_traces (3596-3635)
mlflow/store/tracking/rest_store.py (2)
  • get_trace (466-471)
  • batch_get_traces (473-488)
mlflow/tracking/client.py (1)
  • get_trace (1116-1145)
mlflow/tracing/fluent.py (1)
  • get_trace (619-660)
mlflow/tracing/client.py (1)
  • get_trace (138-201)
mlflow/exceptions.py (1)
  • MlflowNotImplementedException (215-219)
mlflow/store/tracking/databricks_rest_store.py (1)
  • batch_get_traces (165-189)
mlflow/store/tracking/sqlalchemy_store.py (8)
mlflow/server/handlers.py (1)
  • _get_trace (2961-2976)
mlflow/entities/trace_info.py (4)
  • experiment_id (173-181)
  • experiment_id (184-185)
  • request_id (168-170)
  • TraceInfo (20-239)
mlflow/store/tracking/dbmodels/models.py (19)
  • SqlLoggedModelMetric (1115-1207)
  • SqlTraceInfo (686-753)
  • to_mlflow_entity (143-158)
  • to_mlflow_entity (254-284)
  • to_mlflow_entity (317-324)
  • to_mlflow_entity (358-365)
  • to_mlflow_entity (411-423)
  • to_mlflow_entity (467-479)
  • to_mlflow_entity (510-517)
  • to_mlflow_entity (580-594)
  • to_mlflow_entity (671-678)
  • to_mlflow_entity (734-753)
  • to_mlflow_entity (904-957)
  • to_mlflow_entity (1084-1099)
  • to_mlflow_entity (1197-1207)
  • to_mlflow_entity (1252-1253)
  • to_mlflow_entity (1298-1299)
  • to_mlflow_entity (1382-1419)
  • SqlSpan (1705-1791)
mlflow/store/tracking/abstract_store.py (1)
  • get_trace (332-346)
mlflow/tracing/client.py (1)
  • get_trace (138-201)
mlflow/exceptions.py (2)
  • MlflowException (67-113)
  • MlflowTracingException (172-181)
mlflow/tracing/constant.py (2)
  • TraceTagKey (24-32)
  • SpansLocation (134-136)
mlflow/tracing/otel/translation/__init__.py (1)
  • translate_loaded_span (143-167)
tests/genai/evaluate/test_evaluation.py (2)
mlflow/genai/evaluation/entities.py (1)
  • tables (189-190)
mlflow/models/evaluation/base.py (1)
  • tables (681-699)
tests/tracing/otel/test_span_translation.py (3)
mlflow/tracing/otel/translation/__init__.py (1)
  • sanitize_attributes (207-233)
mlflow/tracing/otel/translation/vercel_ai.py (1)
  • VercelAITranslator (8-88)
mlflow/entities/span.py (2)
  • SpanType (44-63)
  • attributes (181-188)
tests/tracking/test_rest_tracking.py (4)
tests/telemetry/test_tracked_events.py (1)
  • mlflow_client (61-62)
mlflow/tracking/_tracking_service/utils.py (1)
  • set_tracking_uri (70-121)
mlflow/tracking/client.py (2)
  • tracking_uri (228-229)
  • start_span (1368-1519)
mlflow/tracing/fluent.py (2)
  • start_span (414-511)
  • trace (63-204)
tests/store/tracking/test_rest_store.py (2)
mlflow/protos/service_pb2.pyi (2)
  • GetTrace (798-809)
  • Trace (983-989)
mlflow/store/tracking/rest_store.py (1)
  • get_trace (466-471)
mlflow/tracing/otel/translation/__init__.py (2)
mlflow/tracing/otel/translation/vercel_ai.py (1)
  • VercelAITranslator (8-88)
mlflow/entities/span.py (3)
  • attributes (181-188)
  • get (871-879)
  • get (902-903)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (40)
  • GitHub Check: core_tests (1, 3)
  • GitHub Check: core_tests (2, 3)
  • GitHub Check: core_tests (2, 5)
  • GitHub Check: core_tests (1, 4)
  • GitHub Check: core_tests (2, 4)
  • GitHub Check: core_tests (1, 5)
  • GitHub Check: pyfunc (2)
  • GitHub Check: pyfunc (4)
  • GitHub Check: pyfunc (3)
  • GitHub Check: pyfunc (1)
  • GitHub Check: evaluate (1)
  • GitHub Check: genai
  • GitHub Check: evaluate (2)
  • GitHub Check: models (1)
  • GitHub Check: java
  • GitHub Check: python (1)
  • GitHub Check: models (2)
  • GitHub Check: flavors
  • GitHub Check: python (2)
  • GitHub Check: python (3)
  • GitHub Check: windows (4)
  • GitHub Check: windows (2)
  • GitHub Check: windows (1)
  • GitHub Check: python-skinny
  • GitHub Check: windows (3)
  • GitHub Check: database
  • GitHub Check: r
  • GitHub Check: build
  • GitHub Check: typescript-sdk (20)
  • GitHub Check: typescript-sdk (24)
  • GitHub Check: typescript-sdk (22)
  • GitHub Check: core
  • GitHub Check: build (skinny)
  • GitHub Check: build (dev)
  • GitHub Check: build (tracing)
  • GitHub Check: lint-macos
  • GitHub Check: js (windows-latest, --testPathIgnorePatterns)
  • GitHub Check: js (ubuntu-latest, --testPathPattern)
  • GitHub Check: js (ubuntu-latest, --testPathIgnorePatterns)
  • GitHub Check: js (windows-latest, --testPathPattern)
🔇 Additional comments (26)
docs/docs/self-hosting/security/basic-http-auth.mdx (1)

7-7: Scorer permissions documentation is well-integrated.

The additions to support scorers in the authentication documentation are consistent with the existing patterns for experiments and registered models. The new "Required Permissions for accessing scorers" section (lines 584–627) is appropriately positioned and clearly documents the five scorer operations with their respective endpoints, HTTP methods, and permission requirements.

Please verify that the scorer endpoint versions (3.0) and permission mappings are accurate with the actual implementation changes in this PR.

Also applies to: 100-100, 584-627

mlflow/entities/__init__.py (2)

65-65: LGTM! Import correctly added.

The import statement follows the existing pattern and is placed in the correct alphabetical position.


129-129: LGTM! Export correctly added.

The Workspace entity is properly added to the __all__ list, making it part of the public API.

mlflow/protos/service.proto (1)

2567-2568: Web search didn't provide specific information about the default change. Let me search the repository directly for any comments or commit hints.Let me search for any release notes or changelog that might document this change.

Based on my analysis of the code and search results, I cannot definitively verify the claim in the review comment. Here's what I found:

Current State:

  • The proto at lines 2567-2568 shows: optional bool allow_partial = 2 [default = false]
  • The RestStore Python method at line 466 defaults to: allow_partial: bool = False
  • Both currently default to false

Evidence from the Search:

  1. The handler at mlflow/server/handlers.py:3160 explicitly sets allow_partial=True with a comment about rendering in-progress traces—suggesting this is an intentional override for UI display
  2. Tests show mixed patterns: some explicitly set allow_partial (e.g., lines 2009, 2055), while test at line 2094 omits it
  3. No entry in the MLflow 3 breaking changes documentation mentions this change
  4. Web search found no changelog entry documenting this as a breaking change

The Issue:
The review comment asserts the change was "from default = true to default = false," but I cannot confirm the original default was true using available tools. Without git history access or version-specific documentation, I cannot verify this factual claim.

Verification Status:
While the current state does show default = false, and some callers do explicitly set the parameter, I cannot conclusively confirm whether this is actually a breaking change without evidence of what the previous default was.

mlflow/java/client/src/main/java/org/mlflow/api/proto/Service.java (5)

90488-90506: LGTM: Documentation consistently updated.

The documentation comments accurately reflect the new default value (false) for the allow_partial field across all interface methods, class fields, and builder methods.

Also applies to: 91352-91373, 91824-91880


91591-91597: LGTM: Builder reset methods correctly updated.

The clear() and clearAllowPartial() methods now properly reset the field to false, consistent with the new default value.

Also applies to: 91875-91880


91626-91634: LGTM: Correct optional field semantics.

The buildPartial() method now conditionally assigns allowPartial_ only when explicitly set (bit field check). This ensures proper handling of optional fields with explicit presence tracking, where unset fields correctly default to false via Java's default initialization.


187932-188478: LGTM: Protobuf descriptor updated.

The serialized protobuf descriptor correctly reflects the allow_partial default change from true to false in the GetTrace message definition. As generated code, this descriptor is automatically computed from the proto definition.


90488-91880: Now let me search for information specific to the MLflow proto definition and verify the actual change:

I need to verify the actual proto file definition and understand whether this truly represents a breaking change. Let me search for the source proto file:

Based on my verification, the original review comment appears to contain incorrect analysis. Here's why:

In proto3, for bools the default value is false, and for numeric types the default value is zero. This is the implicit default that applies to all boolean fields in proto3.

The [default = false] annotation visible in the Java generated code is documentation only, not a functional behavioral change. Proto3 only allows the implicit default value (the natural zero value for numbers/enums, false for booleans, or empty string for strings); no other default values are permitted.

The claim that changing allow_partial from true to false represents a breaking change is misleading without verifying:

  1. What the actual proto file specifies
  2. Whether this field previously had a non-default value specified
  3. Whether proto2 or proto3 syntax is used

To properly verify this concern, the actual .proto file definition would need to be examined to determine if this represents a real semantic change or if it's just documentation syntax in the generated Java code.


tests/store/tracking/test_sqlalchemy_store.py (1)

7675-7699: Logged-model metric test correctly validates experiment_id propagation

This test is well-constructed: it exercises log_metric with a model_id, then inspects SqlLoggedModelMetric to ensure the experiment_id persisted matches the run’s experiment (casting exp_id to int is correct given the DB schema). The flow matches the current log_metric / _log_model_metrics implementation, so this gives good regression coverage for the new behavior.

mlflow/utils/workspace_utils.py (1)

1-5: DEFAULT_WORKSPACE_NAME helper is clear and well-scoped

Centralizing the default workspace name here and exporting it via __all__ looks good and will help keep DB models/migrations consistent.

mlflow/genai/scorers/base.py (2)

120-127: is_multi_turn property is a good, conservative default

Exposing is_multi_turn as a read-only property with a default of False is a clean way to let subclasses opt into multi‑turn behavior without affecting existing scorers.


443-505: Verify whether multi-turn evaluation flows through Scorer.run() or bypass it

Verification confirms the gap exists: Scorer.run() at line 383 constructs a merged dict with only inputs, outputs, expectations, and trace—no session parameter. Both call sites (optimize/util.py and evaluation/utils.py) do not pass session through run().

However, __call__ at lines 443–505 does accept session. The filtering logic in run() means session is never forwarded to __call__ even if a scorer expects it.

To resolve this, either:

  1. Update run() to accept and forward session to __call__, OR
  2. Confirm that all multi-turn scorers call __call__ directly rather than through run()

Please verify which path aligns with your multi-turn evaluation design.

tests/tracking/test_rest_tracking.py (1)

2848-2871: test_get_trace_handler provides solid end-to-end coverage

Nice integration test: it exercises the new /ajax-api/3.0/mlflow/traces/get endpoint for both allow_partial=True/False, verifies status, trace ID, span count/name, and attributes. This should catch regressions in the REST wiring and JSON shape.

mlflow/genai/judges/make_judge.py (1)

113-117: Documentation and validation constraints verified

The underlying InstructionsJudge code properly enforces the documented constraints: lines 566–572 in mlflow/genai/judges/instructions_judge/__init__.py validate that {{ conversation }} can only coexist with {{ expectations }}, rejecting any combination with {{ inputs }}, {{ outputs }}, or {{ trace }}. This aligns with the updated docstring and multi-turn example.

mlflow/server/handlers.py (2)

2959-2976: _get_trace handler matches store API and proto contract.

The new _get_trace endpoint correctly validates trace_id and allow_partial, delegates to AbstractTrackingStore.get_trace, and returns GetTrace.Response(trace=trace.to_proto()), which aligns with the abstract store and proto definitions. I don’t see functional issues here.


4143-4143: Handler registration for GetTrace is consistent with other tracing APIs.

Wiring GetTrace to _get_trace in HANDLERS keeps the service surface consistent with the new V3 tracing APIs. No issues here.

tests/db/check_migration.py (1)

52-61: Workspace test coverage via seeding looks comprehensive.

Defining WORKSPACE_TABLES and then explicitly exercising all of them in log_everything() (extra experiment + registered model, alias, webhook, and direct insert into evaluation_datasets) is a good way to ensure the migration actually backfills real rows across all workspace‑backed tables rather than only the “happy path” ones. The raw SQL insert against evaluation_datasets is scoped to a single row and wrapped in engine.begin(), so it won’t interfere with the higher‑level ORM tests.

Also applies to: 69-127

tests/tracing/otel/test_span_translation.py (1)

10-11: Vercel span kind coverage and sanitize_attributes tests align well with the translation logic.

Including VercelAITranslator in test_json_serialized_values and asserting ai.generateTextLLM, ai.toolCallTOOL via the shared SPAN_KIND_ATTRIBUTE_KEY exercises the new translator wiring nicely. The new test_sanitize_attributes cases mirror the implementation’s behavior for double‑dumped strings/dicts and primitive JSON types, so regressions in that normalization logic should be caught early. I don’t see any issues here.

Also applies to: 20-21, 69-76, 335-367

mlflow/store/tracking/abstract_store.py (1)

332-347: Trace retrieval API surface looks consistent and future‑proof

get_trace and the updated batch_get_traces signature align with the concrete store implementations (SQLAlchemy, REST, Databricks) and use MlflowNotImplementedException to surface 501s cleanly; the docstrings accurately describe allow_partial and location. No changes needed.

Also applies to: 348-363

tests/server/test_handlers.py (3)

18-24: Imports and handler wiring for traces look correct

Bringing in MlflowNotImplementedException, RESOURCE_DOES_NOT_EXIST, GetTrace, _get_trace, and get_trace_artifact_handler aligns the tests with the new trace APIs and error semantics. Nothing to change here.

Also applies to: 48-64, 72-103, 128-129


2006-2111: _get_trace handler tests cover success and not-found paths appropriately

The new test_get_trace_handler and test_get_trace_handler_with_allow_partial_false correctly exercise propagation of the allow_partial flag into get_trace and validate response serialization. The not-found test asserts a 404 with RESOURCE_DOES_NOT_EXIST, matching the expected MlflowException → HTTP mapping. These look solid.


2113-2311: Comprehensive coverage of get_trace_artifact_handler fallback behavior

The suite of get_trace_artifact_handler* tests nicely exercises:

  • Happy path when get_trace(..., allow_partial=True) is implemented.
  • 400 when request_id is missing.
  • 404 when get_trace raises RESOURCE_DOES_NOT_EXIST.
  • Fallback to batch_get_traces when get_trace raises MlflowNotImplementedException.
  • 404 when batch_get_traces returns an empty list.
  • Final fallback to get_trace_info + _get_trace_artifact_repo().download_trace_data() when both trace APIs raise MlflowNotImplementedException.

Two minor notes:

  • The tests assume _fetch_trace_data_from_store calls get_trace(trace_id, allow_partial=True) first and then batch_get_traces([trace_id], None); any change in that helper’s contract will require updating these expectations.
  • The assertions on error messages ("Trace with id={trace_id} not found") effectively bake the exact message into the public contract; keep that stable or relax the match if you expect wording changes.

Otherwise, the scenarios and mocks line up well with the intended handler behavior.

tests/db/schemas/postgresql.sql (1)

19-31: PostgreSQL schema matches workspace-aware ORM changes

The added workspace VARCHAR(63) DEFAULT 'default' NOT NULL columns and updated keys/constraints for:

  • evaluation_datasets, experiments,
  • registered_models, model_versions,
  • registered_model_aliases, registered_model_tags, model_version_tags,
  • plus webhooks and the new workspaces table

are all consistent with the workspace-qualified ORM definitions and composite keys in the model registry and tracking models.

One thing to keep in mind is the tight coupling between the literal 'default' here and DEFAULT_WORKSPACE_NAME; if that constant ever changes, these schema snapshots and migrations will need to be regenerated together. Otherwise the DDL looks coherent.

Also applies to: 34-45, 81-88, 91-103, 106-110, 184-201, 204-211, 214-221, 365-372

docs/api_reference/api_inventory.txt (1)

531-531: Workspace and scorer permission APIs correctly surfaced in public inventory

The new Workspace entities and scorer permission client/entity symbols are consistently listed and aligned with existing naming patterns. No issues from the API-inventory perspective.

Also applies to: 631-631, 1182-1208

mlflow/store/model_registry/dbmodels/models.py (1)

36-37: Workspace-aware registry and webhook models look structurally consistent

The introduction of workspace columns with DEFAULT_WORKSPACE_NAME and composite primary keys across SqlRegisteredModel, SqlModelVersion, SqlRegisteredModelTag, SqlModelVersionTag, and SqlRegisteredModelAlias, plus the workspace index on SqlWebhook, all align with the workspace-scoped schemas in the test DDL. The ORM-side relationships (model_versions, registered_model_tags, model_version_tags, registered_model_aliases) remain coherent with the new composite keys.

Also applies to: 42-47, 49-57, 88-96, 126-133, 158-166, 176-183, 196-204, 211-223, 236-243, 252-261, 301-306, 317-322

<tr>
<td>`admin`</td>
<td>`password`</td>
<td>`password1234`</td>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update example code to use the correct admin password.

The default admin password table on line 895 shows password1234, but the example code on line 905 still references the old password password. This inconsistency could cause authentication failures for users following the documentation.

Apply this diff to fix the example:

  # authenticate as built-in admin user
  export MLFLOW_TRACKING_USERNAME=admin
- export MLFLOW_TRACKING_PASSWORD=password
+ export MLFLOW_TRACKING_PASSWORD=password1234

Also applies to: 905-905

🤖 Prompt for AI Agents
docs/docs/self-hosting/security/basic-http-auth.mdx lines 895 and 905: the
default admin password in the table on line 895 is `password1234` but the
example code on line 905 still uses `password`; update the example on line 905
to use `password1234` so both match, and scan nearby lines for any other
references to `password` and replace them with `password1234` to keep the
documentation consistent.

Comment on lines +152 to +185
def resolve_conversation_from_session(
session: list[Trace],
) -> list[dict[str, str]]:
"""
Extract conversation history from traces in session.

Args:
session: List of traces from the same session.

Returns:
List of conversation messages in the format [{"role": "user"|"assistant", "content": str}].
Each trace contributes two messages: user (from input) and assistant (from output).
"""
# Sort traces by creation time (timestamp_ms)
sorted_traces = sorted(session, key=lambda t: t.info.timestamp_ms)

conversation = []
for trace in sorted_traces:
# Extract and parse input (user message)
inputs = extract_inputs_from_trace(trace)
if inputs:
user_content = parse_inputs_to_str(inputs)
if user_content and user_content.strip():
conversation.append({"role": "user", "content": user_content})

# Extract and parse output (assistant message)
outputs = extract_outputs_from_trace(trace)
if outputs:
assistant_content = parse_outputs_to_str(outputs)
if assistant_content and assistant_content.strip():
conversation.append({"role": "assistant", "content": assistant_content})

return conversation

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid dropping valid but falsy inputs/outputs when building the conversation

resolve_conversation_from_session currently does if inputs: / if outputs:, so values like 0, False, {}, or [] will be skipped even though they may serialize to meaningful content. You already filter out empty/whitespace after string conversion, so it’s safer to only guard against None at this stage.

Consider:

-    for trace in sorted_traces:
-        # Extract and parse input (user message)
-        inputs = extract_inputs_from_trace(trace)
-        if inputs:
-            user_content = parse_inputs_to_str(inputs)
-            if user_content and user_content.strip():
-                conversation.append({"role": "user", "content": user_content})
+    for trace in sorted_traces:
+        # Extract and parse input (user message)
+        inputs = extract_inputs_from_trace(trace)
+        if inputs is not None:
+            user_content = parse_inputs_to_str(inputs)
+            if user_content and user_content.strip():
+                conversation.append({"role": "user", "content": user_content})
@@
-        # Extract and parse output (assistant message)
-        outputs = extract_outputs_from_trace(trace)
-        if outputs:
-            assistant_content = parse_outputs_to_str(outputs)
-            if assistant_content and assistant_content.strip():
-                conversation.append({"role": "assistant", "content": assistant_content})
+        # Extract and parse output (assistant message)
+        outputs = extract_outputs_from_trace(trace)
+        if outputs is not None:
+            assistant_content = parse_outputs_to_str(outputs)
+            if assistant_content and assistant_content.strip():
+                conversation.append({"role": "assistant", "content": assistant_content})

This preserves legitimate falsy values while still excluding empty/whitespace-only messages.

🤖 Prompt for AI Agents
In mlflow/genai/utils/trace_utils.py around lines 152 to 185, the current guards
use `if inputs:` and `if outputs:` which drop valid but falsy values (e.g., 0,
False, {}, []). Change those checks to explicitly test for None (e.g., `if
inputs is not None:` and `if outputs is not None:`) so falsy-but-valid values
are passed to the parsing functions; keep the existing conversion and
`strip()`-based empty-string filtering after parsing.

Comment thread mlflow/server/handlers.py
Comment on lines +3155 to +3181
def _fetch_trace_data_from_store(
store: AbstractTrackingStore, request_id: str
) -> dict[str, Any] | None:
try:
# allow partial so the frontend can render in-progress traces
trace = store.get_trace(request_id, allow_partial=True)
return trace.data.to_dict()
except MlflowTracingException:
return None
except MlflowNotImplementedException:
# fallback to batch_get_traces if get_trace is not implemented
pass

try:
traces = store.batch_get_traces([request_id], None)
match traces:
case [trace]:
return trace.data.to_dict()
case _:
raise MlflowException(
f"Trace with id={request_id} not found.",
error_code=RESOURCE_DOES_NOT_EXIST,
)
# For stores that don't support batch get traces, or if trace data is not in the store,
# return None to signal fallback to artifact repository
except (MlflowTracingException, MlflowNotImplementedException):
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Fallback behavior from get_trace to batch_get_traces / artifacts may be too narrow for older servers.

The overall flow (prefer store.get_trace(allow_partial=True), fall back to batch_get_traces, then finally to the artifact repo) is solid, but there are a couple of edge cases worth tightening:

  1. Older tracking servers without GetTrace:

    • For RestStore‑backed tracking stores talking to an older server that doesn’t support GetTrace, store.get_trace() will likely raise a generic MlflowException with error_code=ENDPOINT_NOT_FOUND, not MlflowNotImplementedException.
    • _fetch_trace_data_from_store currently only treats MlflowNotImplementedException as “feature not available”, so ENDPOINT_NOT_FOUND will bubble out and prevent fallback to batch_get_traces / artifacts, even though those older paths might still work.
    • Consider treating MlflowException with ENDPOINT_NOT_FOUND like “not implemented” here so the new handler degrades gracefully on older backends.
  2. Distinguishing “trace truly missing” from “data not in store”:

    • When batch_get_traces returns anything other than a single trace, you immediately raise MlflowException("Trace with id=... not found", RESOURCE_DOES_NOT_EXIST).
    • If there are scenarios where batch_get_traces returns an empty list while get_trace_info + artifact JSON still work (e.g., older traces whose spans live only in artifacts), this will now short‑circuit the artifact fallback you added.
    • If that scenario is supported, you may want batch_get_traces to signal “no DB data but trace exists” via MlflowTracingException, or you can explicitly check for traces == [] and return None to let get_trace_artifact_handler fall back.

A minimal adjustment for (1) could look like:

-    except MlflowTracingException:
-        return None
-    except MlflowNotImplementedException:
-        # fallback to batch_get_traces if get_trace is not implemented
-        pass
+    except MlflowTracingException:
+        return None
+    except MlflowNotImplementedException:
+        # fallback to batch_get_traces if get_trace is not implemented
+        pass
+    except MlflowException as e:
+        # Older tracking servers may not implement GetTrace and return ENDPOINT_NOT_FOUND.
+        if e.error_code == databricks_pb2.ErrorCode.Name(databricks_pb2.ENDPOINT_NOT_FOUND):
+            return None
+        raise

This keeps real server‑side failures visible while preserving the intended fallback for unsupported endpoints.

Also applies to: 3186-3212


I'll verify this review comment by examining the codebase to understand the error handling behavior and whether the suggested fix is necessary and correct.


---

🏁 Script executed:

```shell
#!/bin/bash
# Locate and inspect the handlers.py file to see the current implementation
fd -e "handlers.py" -p "mlflow/server" --exec cat {} \;

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for RestStore implementation to understand how it handles older servers
fd -e "py" -p "mlflow" | xargs rg -l "class RestStore" | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && cat "{}"'

Length of output: 88081


🏁 Script executed:

#!/bin/bash
# Check for ENDPOINT_NOT_FOUND error code usage in the codebase
rg -n "ENDPOINT_NOT_FOUND" --type py | head -20

Length of output: 2360


🏁 Script executed:

#!/bin/bash
# Search for databricks_pb2 imports and ErrorCode references
rg -n "databricks_pb2.*ErrorCode" --type py | head -20

Length of output: 2539


🏁 Script executed:

#!/bin/bash
# Look for existing patterns of handling MlflowException with specific error codes
rg -n "MlflowException.*error_code" --type py -A 3 | head -40

Length of output: 1957



---

🏁 Script executed:

```shell
#!/bin/bash
# Find handlers.py using ripgrep
rg -l "handlers\.py" --type py | head -5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for _fetch_trace_data_from_store function
rg -n "_fetch_trace_data_from_store" --type py -A 30

Length of output: 4085


🏁 Script executed:

#!/bin/bash
# Check get_trace method in RestStore for exception handling
rg -n "def get_trace" mlflow/store/tracking/rest_store.py -A 10

Length of output: 1011


🏁 Script executed:

#!/bin/bash
# Check batch_get_traces return behavior for empty traces
rg -n "def batch_get_traces" mlflow/store/tracking/rest_store.py -A 10

Length of output: 432


🏁 Script executed:

#!/bin/bash
# Check if handlers.py exists and list its location
find . -name "handlers.py" -path "*/mlflow/server/*" 2>/dev/null | head -5

Length of output: 27



---

🏁 Script executed:

```shell
#!/bin/bash
# Check _call_endpoint implementation in rest_store.py
rg -n "def _call_endpoint" mlflow/store/tracking/rest_store.py -A 15

Length of output: 604


🏁 Script executed:

#!/bin/bash
# Check how get_trace_info handles ENDPOINT_NOT_FOUND (lines 438-465)
sed -n '438,465p' mlflow/store/tracking/rest_store.py

Length of output: 1333


🏁 Script executed:

#!/bin/bash
# Check exception imports in handlers.py
rg -n "^from mlflow.*import|^import" mlflow/server/handlers.py | grep -E "(Exception|Error)" | head -20

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Search for MlflowNotImplementedException to understand when it's raised
rg -n "class MlflowNotImplementedException" --type py -A 5

Length of output: 338


🏁 Script executed:

#!/bin/bash
# Check if ENDPOINT_NOT_FOUND is imported in handlers.py
rg -n "ENDPOINT_NOT_FOUND" mlflow/server/handlers.py

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at call_endpoint function to see how it handles missing endpoints
rg -n "def call_endpoint" mlflow/utils/rest_utils.py -A 30

Length of output: 2880



---

🏁 Script executed:

```shell
#!/bin/bash
# Check verify_rest_response function
rg -n "def verify_rest_response" mlflow/utils/rest_utils.py -A 40

Length of output: 1929


🏁 Script executed:

#!/bin/bash
# Search for test cases using batch_get_traces
rg -n "batch_get_traces" --type py -B 2 -A 5 | head -60

Length of output: 4225


🏁 Script executed:

#!/bin/bash
# Check AbstractTrackingStore interface for batch_get_traces signature
rg -n "def batch_get_traces" mlflow/store/tracking/abstract_store.py -A 10

Length of output: 445


🏁 Script executed:

#!/bin/bash
# Look for handlers that call _fetch_trace_data_from_store
rg -n "get_trace_artifact_handler\|_fetch_trace_data_from_store" mlflow/server/handlers.py -B 5 -A 10 | head -50

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check RestException class and how ENDPOINT_NOT_FOUND is set
rg -n "class RestException" mlflow/utils/rest_utils.py -A 20

Length of output: 0



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for handler function that uses _fetch_trace_data_from_store
rg -n "_fetch_trace_data_from_store" mlflow/server/handlers.py -B 20 | tail -40

Length of output: 1506


🏁 Script executed:

#!/bin/bash
# Get the full context of the handler that calls _fetch_trace_data_from_store
sed -n '3183,3215p' mlflow/server/handlers.py

Length of output: 1039


🏁 Script executed:

#!/bin/bash
# Search for RestException definition and error_code
rg -n "class RestException\|raise RestException" mlflow/utils/rest_utils.py -A 3 | head -50

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check imports in mlflow/utils/rest_utils.py for RestException
rg -n "RestException|from.*RestException" mlflow/utils/rest_utils.py | head -10

Length of output: 123


🏁 Script executed:

#!/bin/bash
# Check what MlflowException gets raised from call_endpoint 
rg -n "MlflowException.*ENDPOINT_NOT_FOUND\|RestException" mlflow/utils/rest_utils.py -B 3 -A 3 | head -60

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Trace exception conversion from RestException to MlflowException
rg -n "except RestException" mlflow/ --type py -A 3 | head -40

Length of output: 1955



---

🏁 Script executed:

```shell
#!/bin/bash
# Find where RestException is imported from
rg -n "from.*RestException\|import.*RestException" mlflow/ --type py | head -10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check the exception module for RestException
rg -n "class RestException" mlflow/ --type py -A 5

Length of output: 316


🏁 Script executed:

#!/bin/bash
# Look at how call_endpoint handles exceptions (full implementation)
sed -n '568,610p' mlflow/utils/rest_utils.py

Length of output: 1378


🏁 Script executed:

#!/bin/bash
# Check if RestException inherits from MlflowException
rg -n "RestException" mlflow/exceptions.py -B 2 -A 10

Length of output: 913


🏁 Script executed:

#!/bin/bash
# Search for where exceptions are caught and re-raised in tracking
rg -n "except.*Exception.*get_trace\|except.*Exception.*batch_get" mlflow/store/tracking/rest_store.py -B 2 -A 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check test file for _fetch_trace_data_from_store behavior
rg -n "_fetch_trace_data_from_store\|get_trace_artifact_handler" tests/ --type py -B 3 -A 10

Length of output: 0


Fallback behavior from get_trace to batch_get_traces / artifacts needs to handle ENDPOINT_NOT_FOUND and empty responses.

Analysis of the code confirms two edge cases require handling:

  1. Older servers without GetTrace: When store.get_trace() is called against an older MLflow server lacking the GetTrace endpoint, it raises RestException (subclass of MlflowException) with error_code=ENDPOINT_NOT_FOUND. The function currently only catches MlflowNotImplementedException, so this exception propagates instead of falling back to batch_get_traces. The pattern in RestStore.get_trace_info() (line 457 of rest_store.py) handles this correctly by explicitly checking for ENDPOINT_NOT_FOUND.

  2. Empty batch_get_traces responses: When batch_get_traces() returns an empty list (possible for traces with spans only in artifacts), the current match statement raises MlflowException instead of returning None to trigger the artifact repository fallback at line 3199.

Add a handler for ENDPOINT_NOT_FOUND in the first exception block and check for empty traces in the batch_get_traces branch:

-    except MlflowTracingException:
-        return None
-    except MlflowNotImplementedException:
-        # fallback to batch_get_traces if get_trace is not implemented
-        pass
+    except MlflowTracingException:
+        return None
+    except MlflowNotImplementedException:
+        # fallback to batch_get_traces if get_trace is not implemented
+        pass
+    except MlflowException as e:
+        # Older tracking servers may not implement GetTrace and return ENDPOINT_NOT_FOUND.
+        if e.error_code == databricks_pb2.ErrorCode.Name(databricks_pb2.ENDPOINT_NOT_FOUND):
+            pass  # fallback to batch_get_traces
+        else:
+            raise

     try:
         traces = store.batch_get_traces([request_id], None)
         match traces:
             case [trace]:
                 return trace.data.to_dict()
-            case _:
-                raise MlflowException(
-                    f"Trace with id={request_id} not found.",
-                    error_code=RESOURCE_DOES_NOT_EXIST,
-                )
+            case []:
+                # Empty result from batch_get_traces; fallback to artifact repository
+                return None
+            case _:
+                raise MlflowException(
+                    f"Trace with id={request_id} not found.",
+                    error_code=RESOURCE_DOES_NOT_EXIST,
+                )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _fetch_trace_data_from_store(
store: AbstractTrackingStore, request_id: str
) -> dict[str, Any] | None:
try:
# allow partial so the frontend can render in-progress traces
trace = store.get_trace(request_id, allow_partial=True)
return trace.data.to_dict()
except MlflowTracingException:
return None
except MlflowNotImplementedException:
# fallback to batch_get_traces if get_trace is not implemented
pass
try:
traces = store.batch_get_traces([request_id], None)
match traces:
case [trace]:
return trace.data.to_dict()
case _:
raise MlflowException(
f"Trace with id={request_id} not found.",
error_code=RESOURCE_DOES_NOT_EXIST,
)
# For stores that don't support batch get traces, or if trace data is not in the store,
# return None to signal fallback to artifact repository
except (MlflowTracingException, MlflowNotImplementedException):
return None
def _fetch_trace_data_from_store(
store: AbstractTrackingStore, request_id: str
) -> dict[str, Any] | None:
try:
# allow partial so the frontend can render in-progress traces
trace = store.get_trace(request_id, allow_partial=True)
return trace.data.to_dict()
except MlflowTracingException:
return None
except MlflowNotImplementedException:
# fallback to batch_get_traces if get_trace is not implemented
pass
except MlflowException as e:
# Older tracking servers may not implement GetTrace and return ENDPOINT_NOT_FOUND.
if e.error_code == databricks_pb2.ErrorCode.Name(databricks_pb2.ENDPOINT_NOT_FOUND):
pass # fallback to batch_get_traces
else:
raise
try:
traces = store.batch_get_traces([request_id], None)
match traces:
case [trace]:
return trace.data.to_dict()
case []:
# Empty result from batch_get_traces; fallback to artifact repository
return None
case _:
raise MlflowException(
f"Trace with id={request_id} not found.",
error_code=RESOURCE_DOES_NOT_EXIST,
)
# For stores that don't support batch get traces, or if trace data is not in the store,
# return None to signal fallback to artifact repository
except (MlflowTracingException, MlflowNotImplementedException):
return None
🤖 Prompt for AI Agents
In mlflow/server/handlers.py around lines 3155 to 3181, the fallback from
get_trace to batch_get_traces must handle servers that return an MlflowException
with error_code ENDPOINT_NOT_FOUND and treat empty batch_get_traces responses as
missing trace data; update the first except to also catch MlflowException and,
when caught, check if exc.error_code == ENDPOINT_NOT_FOUND then fall through to
batch_get_traces (re-raise otherwise), and in the batch_get_traces branch, if
traces is an empty list return None instead of raising an MlflowException so the
caller will fall back to artifact repository.

Comment on lines +3554 to +3567
def get_trace(self, trace_id: str, *, allow_partial: bool = False) -> Trace:
if not allow_partial:
for retry_count in range(3):
# only retry if the spans are not fully exported
if trace := self._get_trace(trace_id, allow_partial):
return trace
elif retry_count < 2:
time.sleep(2**retry_count)
raise MlflowException(
message=f"Trace with ID {trace_id} is not fully exported yet, "
"please try again later.",
error_code=RESOURCE_DOES_NOT_EXIST,
)
return self._get_trace(trace_id, allow_partial)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Trace retrieval mostly matches allow_partial semantics; edge cases around zero spans and non‑tracking locations may need clarification.

Behavior is generally sound (retry loop for allow_partial=False, completeness check via SIZE_STATS, and centralized span translation), but there are two subtle points to verify:

  1. When allow_partial=False, _get_trace never returns a trace if spans is empty, even when SIZE_STATS indicates 0 spans or no stats are present; such traces are always treated as “not fully exported yet”. If 0‑span traces are considered valid in your model, this will surface as spurious “try again later” errors instead of returning an empty trace.
  2. _get_spans_with_trace_info raises MlflowTracingException when SPANS_LOCATION != TRACKING_STORE. get_trace / batch_get_traces don’t catch this, so callers must either rely on that exception or handle artifact‑repo fallbacks themselves; the comment about “handler can capture the exception and load data from artifact repo instead” suggests there may be missing or external handling to confirm.

If 0‑span traces should be returnable and handler‑side fallback for non‑tracking locations is expected, a small follow‑up adjustment might be warranted.

Also applies to: 3569-3594, 3616-3634, 3637-3670


I'll verify the review comment by examining the actual implementation of the tracing methods and their handling of edge cases.


🏁 Script executed:

#!/bin/bash
# Examine the get_trace, _get_trace, and related methods around the specified lines
sed -n '3554,3670p' mlflow/store/tracking/sqlalchemy_store.py

Length of output: 5032


🏁 Script executed:

#!/bin/bash
# Look at the _get_trace implementation to see how it handles empty spans
grep -n "_get_trace" mlflow/store/tracking/sqlalchemy_store.py | head -20

Length of output: 1047


🏁 Script executed:

#!/bin/bash
# Search for _get_spans_with_trace_info and its exception handling
rg -n "_get_spans_with_trace_info" -A 10 -B 2 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 1809


🏁 Script executed:

#!/bin/bash
# Look for SIZE_STATS and its usage pattern
rg -n "SIZE_STATS" -B 3 -A 3 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 381


🏁 Script executed:

#!/bin/bash
# Search for SPANS_LOCATION and TRACKING_STORE logic
rg -n "SPANS_LOCATION|TRACKING_STORE" -B 2 -A 2 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 946


🏁 Script executed:

#!/bin/bash
# Check for exception handling around batch_get_traces
grep -n "batch_get_traces" mlflow/store/tracking/sqlalchemy_store.py | head -5

Length of output: 193


🏁 Script executed:

#!/bin/bash
# Look for test files that might clarify expected behavior
fd -e py "test.*trace" mlflow/store/tracking/ | head -10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for MlflowTracingException handling patterns
rg -n "MlflowTracingException" -B 2 -A 2 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 548


🏁 Script executed:

#!/bin/bash
# Look for any comments about handlers or artifact repo fallbacks
rg -n "handler|artifact.*repo|fallback" -i -B 2 -A 2 mlflow/store/tracking/sqlalchemy_store.py | head -40

Length of output: 969


🏁 Script executed:

#!/bin/bash
# Examine batch_get_traces around lines 3569-3594
sed -n '3569,3594p' mlflow/store/tracking/sqlalchemy_store.py

Length of output: 1176


🏁 Script executed:

#!/bin/bash
# Check lines 3616-3634 and 3637-3670
sed -n '3616,3670p' mlflow/store/tracking/sqlalchemy_store.py

Length of output: 2493


🏁 Script executed:

#!/bin/bash
# Look for how empty spans are handled in _get_trace
rg -n "def _get_trace" -A 40 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 7536


🏁 Script executed:

#!/bin/bash
# Search for the trace translation and span collection logic
rg -n "def _spans_to_trace|_collect.*span" -A 15 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for SIZE_STATS in the context of completeness checking
rg -n "completeness|SIZE_STATS.*0|spans.*empty" -i -B 3 -A 3 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check for any documentation about valid traces with 0 spans
rg -n "Trace|trace.*empty|zero.*span" -i -B 2 -A 2 mlflow/store/tracking/sqlalchemy_store.py | head -50

Length of output: 1490


🏁 Script executed:

#!/bin/bash
# Search for Trace dataclass or model definition
fd -e py | xargs grep -l "class Trace" | head -5

Length of output: 171


🏁 Script executed:

#!/bin/bash
# Look for where the exception about "not fully exported" is used
rg -n "not fully exported" -B 5 -A 5 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 1753


🏁 Script executed:

#!/bin/bash
# Check the actual implementation area around line 3554
sed -n '3540,3580p' mlflow/store/tracking/sqlalchemy_store.py

Length of output: 1938


🏁 Script executed:

#!/bin/bash
# Look for the complete _get_trace definition
ast-grep --pattern 'def _get_trace($_, $_) { $$$ }'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for where batch_get_traces calls or handles these methods
sed -n '3569,3610p' mlflow/store/tracking/sqlalchemy_store.py

Length of output: 1649


🏁 Script executed:

#!/bin/bash
# Check if there are any try-except blocks around _get_spans_with_trace_info calls
rg -n "try:" -A 10 mlflow/store/tracking/sqlalchemy_store.py | grep -A 10 "_get_spans_with_trace_info"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for test files that test get_trace with edge cases
fd -e py | xargs grep -l "get_trace.*allow_partial" | head -5

Length of output: 180


🏁 Script executed:

#!/bin/bash
# Check for any comments or documentation about the retry logic
sed -n '3550,3570p' mlflow/store/tracking/sqlalchemy_store.py

Length of output: 928


🏁 Script executed:

#!/bin/bash
# Look at the broader context of how traces are constructed
grep -n "def.*_get_trace\|def.*_spans_to_trace\|def get_trace\|def batch_get_traces" mlflow/store/tracking/sqlalchemy_store.py

Length of output: 559


🏁 Script executed:

#!/bin/bash
# Check what happens when _get_trace returns None or empty trace
rg -n "if.*_get_trace|return.*_get_trace" -A 2 -B 2 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 500


🏁 Script executed:

#!/bin/bash
# Search for SIZE_STATS definition and how it's populated
rg -n "SIZE_STATS\s*=" -B 5 -A 10 mlflow/store/tracking/sqlalchemy_store.py | head -50

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for the complete definition of _get_spans_with_trace_info
rg -n "def _get_spans_with_trace_info" -A 30 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 1663


🏁 Script executed:

#!/bin/bash
# Check if there are any handlers or callbacks for exception cases
rg -n "handler|Handler|callback" -i -B 2 -A 2 mlflow/store/tracking/sqlalchemy_store.py | head -50

Length of output: 412


🏁 Script executed:

#!/bin/bash
# Look for imports that might hint at exception handling patterns
head -50 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 1161


🏁 Script executed:

#!/bin/bash
# Check MlflowTracingException definition
fd -e py | xargs grep -l "class MlflowTracingException" | head -3

Length of output: 20


🏁 Script executed:

#!/bin/bash
# Search for documentation or comments about spans location
rg -n "SPANS_LOCATION.*TRACKING_STORE|non.*tracking.*location" -i -B 3 -A 3 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 537


🏁 Script executed:

#!/bin/bash
# Look for the critical section around lines 3554-3570 with context
sed -n '3545,3600p' mlflow/store/tracking/sqlalchemy_store.py

Length of output: 2501


🏁 Script executed:

#!/bin/bash
# Search for all mentions of "artifact" in context of tracing
rg -n "artifact" -i -B 2 -A 2 mlflow/store/tracking/sqlalchemy_store.py | head -60

Length of output: 2764


🏁 Script executed:

#!/bin/bash
# Check the RESOURCE_DOES_NOT_EXIST error code context
rg -n "RESOURCE_DOES_NOT_EXIST" -B 3 -A 3 mlflow/store/tracking/sqlalchemy_store.py

Length of output: 8176


🏁 Script executed:

#!/bin/bash
# Look at what _get_trace actually does - find its full implementation
rg -n "^\s*def _get_trace\(" -A 50 mlflow/store/tracking/sqlalchemy_store.py | head -70

Length of output: 2292


🏁 Script executed:

#!/bin/bash
# Check line 3554 to confirm the method signature
sed -n '3554,3555p' mlflow/store/tracking/sqlalchemy_store.py

Length of output: 110


Both trace retrieval concerns verified; implementation matches described behavior.

Verification confirms the two raised issues:

  1. Empty spans treated as incomplete: When allow_partial=False and a trace has zero spans, line 3588's condition if allow_partial or spans: evaluates to false (since [] is falsy), causing _get_trace to return None. This triggers the retry loop in get_trace, ultimately raising "not fully exported yet" error even for validly empty traces. The SIZE_STATS check only prevents this if metadata explicitly declares 0 expected spans; if SIZE_STATS is absent, empty traces are always treated as incomplete.

  2. Unhandled MlflowTracingException for non-tracking locations: _get_spans_with_trace_info intentionally raises MlflowTracingException when SPANS_LOCATION != TRACKING_STORE (line 3645), with a comment stating handlers should catch and load from artifact repo. However, neither get_trace nor batch_get_traces includes exception handling around these calls—the exception propagates directly to callers. In batch_get_traces, this means any trace stored outside the tracking store will fail the entire batch operation.

If 0-span traces should be returnable and artifact-repo fallback is implemented externally (e.g., in mlflow/server/handlers.py), these behaviors may be intentional. If not, _get_trace needs to distinguish between empty spans and missing data, and callers may need local fallback handling for non-tracking locations.

🤖 Prompt for AI Agents
In mlflow/store/tracking/sqlalchemy_store.py around lines 3554-3567, change the
logic so get_trace/_get_trace treat an empty list of spans as a valid,
fully-exported trace (i.e., distinguish between None meaning “not
found/incomplete” and [] meaning “found but zero spans”) by checking spans is
not None (or consulting SIZE_STATS==0) instead of truthiness; and add a local
try/except around calls that can raise MlflowTracingException (both in get_trace
and batch_get_traces) to catch MlflowTracingException, log/annotate the trace as
stored outside the tracking store, and allow callers (or the handler layer) to
perform the artifact-repo fallback instead of letting the exception propagate
and fail the operation.

Comment on lines 154 to 160
value = attributes.get(key)
if isinstance(value, str):
try:
result = json.loads(value)
if isinstance(result, str):
# the span attributes may be dumped several times in different places
# (e.g. Span.from_otel_proto, span.to_dict)
# so we try to load it twice here to get the dumped-once value
try:
if json.loads(result):
return result
return None
except json.JSONDecodeError:
pass
return value if result else None
return value if json.loads(value) else None
except json.JSONDecodeError:
pass # Use the string value as-is
return value

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider potential data loss with falsy JSON values.

The expression return value if json.loads(value) else None filters out any JSON value that's falsy in Python, including "false", "0", "[]", "{}", and "null". While the docstring mentions filtering empty dictionaries and null (which seems intentional), filtering valid values like boolean false, numeric 0, or empty arrays [] could lead to data loss if these are legitimate attribute values in OTEL spans.

Additionally, parsing JSON solely to check truthiness (then discarding the result and returning the original string) is inefficient and unclear.

Consider either:

  1. Documenting the behavior explicitly if filtering all falsy JSON values is intentional:
    def _get_and_check_attribute_value(self, attributes: dict[str, Any], key: str) -> Any:
        """
        Get attribute value from OTEL attributes by checking whether the value is valid or not.
-       This avoids fetching the value if it's empty dictionary or null.
+       This avoids fetching the value if it's a falsy JSON value (null, false, 0, {}, []).

        Args:
            attributes: Dictionary of span attributes
            key: Attribute key

        Returns:
            Attribute value or None if not found
        """
  1. Filtering only truly empty values if false/0/[] should be preserved:
value = attributes.get(key)
if isinstance(value, str):
    try:
        parsed = json.loads(value)
        # Only filter null and empty containers
        if parsed is None or (isinstance(parsed, (dict, list)) and not parsed):
            return None
        return value
    except json.JSONDecodeError:
        pass  # Use the string value as-is
return value
🤖 Prompt for AI Agents
In mlflow/tracing/otel/translation/base.py around lines 154 to 160, the current
code discards any JSON-parsable string that evaluates to falsy (e.g., "false",
"0", "[]", "{}"), which can lose legitimate attribute values; update the logic
to json.loads once into a variable and only return None when the parsed value is
explicitly None or an empty container (empty dict or list), otherwise return the
original string; keep the JSONDecodeError handling to fall back to the raw
string; alternatively, if the original broad falsy filtering was intentional,
add a docstring comment explaining that behavior.

@mprahl mprahl closed this Dec 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant