Skip to content

feat(data-connector): add schema versioning with safe-by-default migrations - #544

Merged
slin1237 merged 9 commits into
mainfrom
slin/db-3
Feb 27, 2026
Merged

slin1237 merged 9 commits into
mainfrom
slin/db-3

Conversation

@slin1237

@slin1237 slin1237 commented Feb 26, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Add tracked, per-backend migration system replacing ad-hoc ALTER TABLE calls with versioned migrations recorded in a _schema_versions table
  • Safe by default: auto_migrate defaults to false — startup fails with the exact SQL statements needed when pending migrations exist, so operators can review and apply manually
  • Opt-in automatic migration via auto_migrate: true for development/convenience

What changed

New files

  • versioning.rs: Migration infrastructure — Migration struct, version tracking table DDL, Oracle runner (sync, ORA-00001 concurrent safety), Postgres runner (async, pg_advisory_lock serialization), pending_migrations_error for actionable failure messages
  • oracle_migrations.rs: Oracle migration definitions (v1: add safety_identifier via PL/SQL, v2: remove user_id via PL/SQL) with idempotency via exception handling
  • postgres_migrations.rs: Postgres migration definitions (v1: add safety_identifier IF NOT EXISTS, v2: drop user_id IF EXISTS)

Modified files

  • schema.rs: Add version: Option<u32> and auto_migrate: bool (default false) to SchemaConfig with serde support and tests
  • oracle.rs: Remove ad-hoc alter_safety_identifier_column and remove_user_id_column_if_exists (~91 lines), wire ORACLE_MIGRATIONS into OracleStore::new() after table creation. Fix ensure_oracle_versions_table to use all_tables with owner filter for cross-schema deployments
  • postgres.rs: Add PostgresStore::run_migrations() method wiring POSTGRES_MIGRATIONS
  • factory.rs: Call store.run_migrations() after Postgres table creation
  • lib.rs: Add versioning, oracle_migrations, postgres_migrations modules
  • README.md: Document schema versioning section with safe-by-default behavior, migration table, config fields, concurrency safety, and YAML examples

Why

Ad-hoc ALTER TABLE calls had no version tracking, re-ran on every startup relying solely on idempotency, and had race conditions between concurrent instances. This provides:

  • Run-once guarantee via _schema_versions tracking table
  • Safe by default — operators see exactly what SQL will run before it touches their database
  • Concurrency safety — pg_advisory_lock for Postgres, ORA-00001 duplicate detection for Oracle
  • SchemaConfig-aware — migrations respect custom table/column names and skip_columns

How

Migration struct holds version, description, and an up function (fn(&SchemaConfig) -> Vec<String>). Per-backend migration lists are static arrays in dedicated files. Runners check current version from _schema_versions, compare against pending migrations, and either apply (auto_migrate: true) or fail with actionable error (auto_migrate: false). Oracle uses PL/SQL exception handling for DDL idempotency. Postgres uses transactions per migration.

Test plan

  • cargo test -p data-connector — 208 tests pass
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • Versioning DDL generation tests (Oracle with/without owner, Postgres IF NOT EXISTS)
  • Migration definition tests (sequential numbering, DDL content, skip_columns respect)
  • pending_migrations_error output tests (SQL included, version hints, auto_migrate hint)
  • Serde roundtrip tests for version and auto_migrate fields
  • Integration: factory wiring ensures migrations run after table creation

Summary by CodeRabbit

  • New Features

    • Schema versioning with per-backend migrations for Postgres and Oracle, tracked in a central versions table; migrations run during startup with safe defaults (auto_migrate=false).
    • Startup now invokes migrations as part of storage initialization and includes concurrency-safe migration application.
  • Documentation

    • Expanded Schema Versioning guide with examples for auto-applying or skipping migrations and migration control details.
  • Tests

    • Added tests for migration sequencing, generated SQL/PLSQL, skip behaviors, and concurrency semantics.

@slin1237
slin1237 requested a review from key4ng as a code owner February 26, 2026 16:02
@github-actions github-actions Bot added documentation Improvements or additions to documentation data-connector Data connector crate changes labels Feb 26, 2026
@coderabbitai

coderabbitai Bot commented Feb 26, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a schema versioning and migration framework for Postgres and Oracle, new SchemaConfig fields (version, auto_migrate), backend-specific migration modules, orchestration to run migrations after initial table creation, README documentation updates, and concurrency-safe migration logic. No public API signatures changed.

Changes

Cohort / File(s) Summary
Documentation
data_connector/README.md
Adds Schema Versioning section, examples, migration controls (version, auto_migrate), startup sequencing, and concurrency notes.
Core versioning
data_connector/src/versioning.rs, data_connector/src/lib.rs
New migration framework (Migration type, _schema_versions DDL), run_oracle_migrations and run_postgres_migrations entry points, concurrency/locking and pending-migrations error reporting.
Schema config
data_connector/src/schema.rs
Adds version: Option<u32> and auto_migrate: bool to SchemaConfig, default helper, and serialization/tests for defaults and omission.
Postgres integration
data_connector/src/postgres.rs, data_connector/src/postgres_migrations.rs, data_connector/src/factory.rs
Adds POSTGRES_MIGRATIONS (v1 add safety_identifier, v2 drop user_id), PostgresStore::run_migrations, invokes migrations from factory after table creation, and constructs item storage with cloned store.
Oracle integration
data_connector/src/oracle.rs, data_connector/src/oracle_migrations.rs
Adds ORACLE_MIGRATIONS (idempotent PL/SQL v1 add safety_identifier, v2 drop USER_ID), removes legacy init-time column-alter helpers, broadens non-fatal index-error handling, and invokes run_oracle_migrations after schema init.
Tests
data_connector/src/*_migrations.rs, data_connector/src/schema.rs tests
Unit tests for migration SQL/PLSQL generation, sequencing, skip behavior, concurrency messages, serde defaults, and schema-driven DDL generation.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Factory as StorageFactory
    participant Store as PostgresStore/OracleStore
    participant Versioning as Versioning Module
    participant DB as Database

    App->>Factory: create_storage()
    Factory->>Store: new() / init_schema()
    Store->>DB: Create tables (responses, items, ...)
    DB-->>Store: Tables created

    Factory->>Store: run_migrations()
    Store->>Versioning: run_postgres_migrations / run_oracle_migrations(schema, migrations, version, auto_migrate)
    Versioning->>DB: Ensure _schema_versions table exists
    DB-->>Versioning: Table ready
    Versioning->>DB: Read current version(s)
    DB-->>Versioning: Current version

    alt auto_migrate true AND pending migrations
        Versioning->>Versioning: Determine pending migrations
        loop For each pending migration
            Versioning->>DB: Execute migration SQL/PLSQL
            DB-->>Versioning: Migration executed
            Versioning->>DB: Record applied version in _schema_versions
            DB-->>Versioning: Recorded
        end
        Versioning-->>Store: Return applied versions
    else auto_migrate false AND pending migrations
        Versioning-->>Store: Return error listing pending migrations and SQL
    end

    Store-->>Factory: migrations result
    Factory-->>App: storage ready / error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • key4ng
  • CatherineSue

Poem

🐇 I hopped through DDL, nudged each version in line,
I wrote safe migrations—PL/SQL and Postgres fine.
schema_versions hums, keeping changes in tune,
I gently drop old columns beneath the moon.
🥕 Hooray — migrations done; time for a carrot!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing schema versioning with migrations that default to being safe (non-automatic).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch slin/db-3

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @slin1237, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the data connector's database schema management by introducing a robust, versioned migration system. It moves away from untracked, ad-hoc schema changes to a structured approach that ensures consistency, provides explicit control over database modifications, and improves operational safety. The new system allows for clear tracking of schema evolution, offers a safe-by-default mechanism for applying changes, and incorporates concurrency safeguards for multi-instance deployments.

Highlights

  • Schema Versioning System: Introduced a new, tracked, per-backend migration system that replaces ad-hoc ALTER TABLE calls with versioned migrations recorded in a _schema_versions table for both Oracle and Postgres.
  • Safe-by-Default Migrations: Implemented a 'safe by default' approach where auto_migrate is false. If pending migrations exist, startup fails with the exact SQL statements needed, allowing operators to review and apply them manually. Opt-in automatic migration is available via auto_migrate: true.
  • Concurrency Safety: Ensured concurrency safety for migrations: Postgres uses pg_advisory_lock to serialize migrations, while Oracle uses PL/SQL exception handling for DDL idempotency and ORA-00001 duplicate detection for concurrent instances.
  • Configurable Migration Control: Added version and auto_migrate fields to SchemaConfig, allowing users to specify a starting schema version to skip already applied migrations and to control automatic migration behavior.
  • Refactored Database Logic: Removed ad-hoc DDL statements from oracle.rs and integrated the new versioned migration system into both Oracle and Postgres store initialization, centralizing migration logic.
Changelog
  • data_connector/README.md
    • Added version and auto_migrate fields to SchemaConfig examples.
    • Updated SchemaConfig field list to include version and auto_migrate.
    • Introduced a new 'Schema Versioning' section detailing the migration system, its safe-by-default behavior, startup process, current migrations, and configuration options.
    • Documented concurrency safety mechanisms for Postgres and Oracle.
  • data_connector/src/factory.rs
    • Modified create_postgres_storage to call store.run_migrations() after all tables are created.
  • data_connector/src/lib.rs
    • Added oracle_migrations, postgres_migrations, and versioning modules.
  • data_connector/src/oracle.rs
    • Imported ORACLE_MIGRATIONS and crate::versioning.
    • Integrated crate::versioning::run_oracle_migrations into OracleStore::new() after initial schema creation.
    • Removed alter_safety_identifier_column and remove_user_id_column_if_exists methods.
  • data_connector/src/oracle_migrations.rs
    • New file defining ORACLE_MIGRATIONS static array.
    • Implemented oracle_v1_up to add safety_identifier column using PL/SQL with idempotency.
    • Implemented oracle_v2_up to drop user_id column using PL/SQL with idempotency.
    • Added unit tests for migration sequencing, DDL generation, and skipped columns.
  • data_connector/src/postgres.rs
    • Imported POSTGRES_MIGRATIONS.
    • Added run_migrations async method to PostgresStore to execute versioned migrations using crate::versioning::run_postgres_migrations.
  • data_connector/src/postgres_migrations.rs
    • New file defining POSTGRES_MIGRATIONS static array.
    • Implemented pg_v1_up to add safety_identifier column using IF NOT EXISTS.
    • Implemented pg_v2_up to drop user_id column using IF EXISTS.
    • Added unit tests for migration sequencing, DDL generation, and skipped columns.
  • data_connector/src/schema.rs
    • Added version: Option<u32> and auto_migrate: bool fields to SchemaConfig struct with serde attributes.
    • Updated Default implementation for SchemaConfig to include new fields.
    • Added default_auto_migrate function.
    • Added serde roundtrip tests for version and auto_migrate fields.
  • data_connector/src/versioning.rs
    • New file containing Migration struct definition.
    • Defined VERSIONS_TABLE constant.
    • Implemented oracle_create_versions_table and postgres_create_versions_table DDL generation.
    • Implemented pending_migrations_error for generating actionable error messages.
    • Implemented run_oracle_migrations, ensure_oracle_versions_table, and oracle_current_version for Oracle migration logic.
    • Implemented run_postgres_migrations, apply_postgres_migrations, and postgres_current_version for Postgres migration logic, including advisory locking.
    • Added unit tests for DDL generation, schema config respect, and error message content.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f85d491398

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread data_connector/src/versioning.rs Outdated
Comment thread data_connector/src/oracle.rs Outdated

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@data_connector/src/oracle_migrations.rs`:
- Around line 37-44: The current oracle_v2_up function unconditionally issues a
DROP (USER_ID) which can remove the active safety_identifier column when the
schema maps that logical field to USER_ID; change oracle_v2_up to first inspect
the responses mapping (the s variable from schema.responses) and skip emitting
the DROP when s.safety_identifier (or the responses alias for the safety
identifier) resolves to "USER_ID" (use a case-insensitive comparison), returning
an empty Vec<String> in that case; otherwise keep emitting the existing PL/SQL
DROP statement. Ensure you reference and check the SchemaConfig.responses ->
safety_identifier mapping before constructing the SQL.

In `@data_connector/src/postgres_migrations.rs`:
- Around line 35-39: pg_v2_up currently always issues "ALTER TABLE ... DROP
COLUMN IF EXISTS user_id", which can accidentally remove a configured safety
identifier column; change pg_v2_up (and reference SchemaConfig,
schema.responses, s.qualified_table, and responses.columns.safety_identifier) to
inspect s.columns.safety_identifier (as_deref()) and only emit the DROP COLUMN
statement when the configured safety_identifier is absent or equals "user_id"
(i.e., the default); otherwise return an empty Vec so no column is dropped.

In `@data_connector/src/versioning.rs`:
- Around line 234-240: The code interpolates schema.owner directly into SQL (see
schema.owner usage in building check_sql and other queries) which can break
queries or enable injection; add validation/sanitization for owner (e.g.,
implement a validate_owner(owner: &str) function that enforces non-empty and
only ASCII letters, digits, and underscores, and call it when constructing the
qualified table name or during schema initialization) or switch to parameterized
queries where supported; update all places that format owner into SQL (the
check_sql construction and the other occurrences referenced in the review) to
use the validated value (or a safely escaped/quoted form) before formatting the
SQL string.
- Around line 388-393: The code casts Migration::version (u32) to i32 when
inserting into the VERSIONS_TABLE which creates a silent type mismatch; change
the Migration struct's version field from u32 to i32 (update the Migration
definition and all usages/tests that construct or compare migrations) so types
align and you can remove the cast in the tx.execute call, or if you prefer to
keep u32, add a clear doc comment on Migration::version stating the Postgres
INTEGER limit and validate/convert to i32 at insertion time (e.g., check for
overflow and return an error) so the contract is explicit; update references to
VERSIONS_TABLE, Migration::version and the tx.execute insertion site
accordingly.
- Around line 233-240: The existence check uppercases the versions table name
and owner when schema.owner is Some, which breaks matching for quoted/lowercase
identifiers created by oracle_create_versions_table; update the logic so that
when schema.owner.is_some() you use the raw VERSIONS_TABLE (not
VERSIONS_TABLE.to_ascii_uppercase()) and use the owner string as provided (not
owner.to_ascii_uppercase()) in the all_tables query (i.e., change how check_sql
is built for the Some(owner) branch), leaving the uppercase behavior only for
the user_tables branch when owner is None.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e45fbec and f85d491.

📒 Files selected for processing (9)
  • data_connector/README.md
  • data_connector/src/factory.rs
  • data_connector/src/lib.rs
  • data_connector/src/oracle.rs
  • data_connector/src/oracle_migrations.rs
  • data_connector/src/postgres.rs
  • data_connector/src/postgres_migrations.rs
  • data_connector/src/schema.rs
  • data_connector/src/versioning.rs

Comment thread data_connector/src/oracle_migrations.rs
Comment thread data_connector/src/postgres_migrations.rs
Comment thread data_connector/src/versioning.rs Outdated
Comment thread data_connector/src/versioning.rs
Comment thread data_connector/src/versioning.rs

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This is an excellent pull request that introduces a robust, versioned schema migration system, replacing the previous ad-hoc ALTER TABLE calls. The new system is well-designed, with safety as a default, clear separation of concerns for different database backends, and thoughtful handling of concurrency and idempotency. The actionable error messages for pending migrations are a great touch for operator experience.

I've found one high-severity issue related to panic safety in the Postgres migration runner that could lead to deadlocks, and one minor point of improvement in the Oracle helper. Please see the detailed comments.

Comment thread data_connector/src/versioning.rs
Comment thread data_connector/src/versioning.rs

@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

♻️ Duplicate comments (2)
data_connector/src/postgres_migrations.rs (1)

35-39: ⚠️ Potential issue | 🔴 Critical

Guard user_id drop when it is the configured safety identifier.

Line 38 unconditionally drops user_id. If responses.columns.safety_identifier is mapped to user_id, this migration deletes the active identifier column.

💡 Proposed fix
 fn pg_v2_up(schema: &SchemaConfig) -> Vec<String> {
     let s = &schema.responses;
+    if s.is_skipped("safety_identifier")
+        || s.col("safety_identifier").eq_ignore_ascii_case("user_id")
+    {
+        return vec![];
+    }
     let table = s.qualified_table(schema.owner.as_deref());
     vec![format!("ALTER TABLE {table} DROP COLUMN IF EXISTS user_id")]
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@data_connector/src/postgres_migrations.rs` around lines 35 - 39, The
migration unconditionally drops user_id in pg_v2_up; change pg_v2_up to check
schema.responses.columns.safety_identifier and only include the "ALTER TABLE ...
DROP COLUMN IF EXISTS user_id" statement when the configured safety identifier
is not "user_id" (i.e., if responses.columns.safety_identifier.as_deref() !=
Some("user_id")). Update the vec! construction in pg_v2_up to conditionally push
the DROP statement based on that check so the active safety identifier column is
never removed.
data_connector/src/oracle_migrations.rs (1)

37-44: ⚠️ Potential issue | 🔴 Critical

Do not drop USER_ID when it backs safety_identifier.

Line 42 always drops USER_ID; this can delete the active safety_identifier column when the schema maps that logical field to USER_ID.

💡 Proposed fix
 fn oracle_v2_up(schema: &SchemaConfig) -> Vec<String> {
     let s = &schema.responses;
+    if s.is_skipped("safety_identifier")
+        || s.col("safety_identifier").eq_ignore_ascii_case("USER_ID")
+    {
+        return vec![];
+    }
     let table = s.qualified_table(schema.owner.as_deref());
     // PL/SQL block: ORA-00904 = "invalid identifier" (column doesn't exist)
     vec![format!(
         "BEGIN EXECUTE IMMEDIATE 'ALTER TABLE {table} DROP (USER_ID)'; \
          EXCEPTION WHEN OTHERS THEN IF SQLCODE != -904 THEN RAISE; END IF; END;"
     )]
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@data_connector/src/oracle_migrations.rs` around lines 37 - 44, The current
oracle_v2_up always drops the literal USER_ID column which can remove the active
safety_identifier column when the schema maps that logical field to USER_ID;
update oracle_v2_up to first resolve the physical column name that backs the
logical field "safety_identifier" from schema.responses (using the existing
SchemaConfig/Responses API you have), then only generate a DROP for that
resolved column if it is not equal to "USER_ID" (or skip the DROP entirely when
the resolved column equals "USER_ID"); keep using table = s.qualified_table(...)
and ensure the DROP statement references the resolved column name instead of
hardcoding "USER_ID".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@data_connector/src/versioning.rs`:
- Around line 260-265: The code currently reads MAX(version) into row:
Option<i64> and blindly casts row.unwrap_or(0) as u32 which can panic or produce
incorrect results for negative/overflow values; update the function that
contains conn/query_row_as_named (use the local variables conn, versions_table,
row) to: unwrap the Option into an i64 defaulting to 0, then validate that the
i64 is >= 0 and <= u32::MAX, and return a descriptive Err (map_err or custom
error) if it is out of range; only after validation convert to u32 and return
Ok(value).

---

Duplicate comments:
In `@data_connector/src/oracle_migrations.rs`:
- Around line 37-44: The current oracle_v2_up always drops the literal USER_ID
column which can remove the active safety_identifier column when the schema maps
that logical field to USER_ID; update oracle_v2_up to first resolve the physical
column name that backs the logical field "safety_identifier" from
schema.responses (using the existing SchemaConfig/Responses API you have), then
only generate a DROP for that resolved column if it is not equal to "USER_ID"
(or skip the DROP entirely when the resolved column equals "USER_ID"); keep
using table = s.qualified_table(...) and ensure the DROP statement references
the resolved column name instead of hardcoding "USER_ID".

In `@data_connector/src/postgres_migrations.rs`:
- Around line 35-39: The migration unconditionally drops user_id in pg_v2_up;
change pg_v2_up to check schema.responses.columns.safety_identifier and only
include the "ALTER TABLE ... DROP COLUMN IF EXISTS user_id" statement when the
configured safety identifier is not "user_id" (i.e., if
responses.columns.safety_identifier.as_deref() != Some("user_id")). Update the
vec! construction in pg_v2_up to conditionally push the DROP statement based on
that check so the active safety identifier column is never removed.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f85d491 and 4e11eca.

📒 Files selected for processing (9)
  • data_connector/README.md
  • data_connector/src/factory.rs
  • data_connector/src/lib.rs
  • data_connector/src/oracle.rs
  • data_connector/src/oracle_migrations.rs
  • data_connector/src/postgres.rs
  • data_connector/src/postgres_migrations.rs
  • data_connector/src/schema.rs
  • data_connector/src/versioning.rs

Comment thread data_connector/src/versioning.rs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e11eca54b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread data_connector/src/versioning.rs
Comment thread data_connector/src/versioning.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 27a89f4507

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread data_connector/src/postgres_migrations.rs Outdated
Comment thread data_connector/src/oracle_migrations.rs Outdated

@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

♻️ Duplicate comments (1)
data_connector/src/versioning.rs (1)

262-273: ⚠️ Potential issue | 🟡 Minor

Avoid unchecked signed/unsigned casts for schema versions.

Lines 272 and 435 cast signed DB values to u32 without range checks, and Line 398 casts u32 to i32 for insert. A negative/corrupt row or oversized migration version can silently wrap and miscompute pending migrations.

🛡️ Proposed defensive conversion
 fn oracle_current_version(conn: &oracle::Connection, schema: &SchemaConfig) -> Result<u32, String> {
@@
-    Ok(row.unwrap_or(0) as u32)
+    let raw = row.unwrap_or(0);
+    u32::try_from(raw)
+        .map_err(|_| format!("invalid schema version value in {versions_table}: {raw}"))
 }
@@
-        tx.execute(
+        let version_i32 = i32::try_from(migration.version).map_err(|_| {
+            format!(
+                "migration version {} exceeds PostgreSQL INTEGER range",
+                migration.version
+            )
+        })?;
+        tx.execute(
             &format!("INSERT INTO {VERSIONS_TABLE} (version, description) VALUES ($1, $2)"),
-            &[&(migration.version as i32), &migration.description],
+            &[&version_i32, &migration.description],
         )
@@
     let version: i32 = row.get(0);
-    Ok(version as u32)
+    u32::try_from(version)
+        .map_err(|_| format!("invalid schema version value in {VERSIONS_TABLE}: {version}"))
 }
#!/bin/bash
set -euo pipefail

# Find unchecked numeric casts in versioning flow.
rg -n --type=rust 'as u32|as i32|MAX\(version\)|COALESCE\(MAX\(version\), 0\)' data_connector/src/versioning.rs

Also applies to: 398-399, 425-436

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@data_connector/src/versioning.rs` around lines 262 - 273, The code currently
performs unchecked casts between signed DB types and u32 (e.g., in
oracle_current_version where an Option<i64> from MAX(version) is cast to u32,
and where a u32 version is cast to i32 for insertion), which can silently wrap
on negative or out-of-range values; update these conversion sites (referencing
oracle_current_version, the MAX(version) query handling, and the code path that
casts u32 -> i32 for inserts) to perform defensive checks: treat None as zero
explicitly, verify the i64 is >= 0 and <= u32::MAX before converting (use
i64::try_into or TryFrom/TryInto with explicit bounds check), and validate any
u32 -> i32 conversion by ensuring the u32 value <= i32::MAX before casting
(return an Err with a clear message if checks fail) so corrupted or oversized
values are rejected rather than wrapped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@data_connector/src/versioning.rs`:
- Around line 67-75: The versions table is currently unqualified, causing
cross-schema migration state leaks; update postgres_create_versions_table to
take &SchemaConfig, add a helper (e.g., qualified_table(owner: Option<&str>) ->
String or reuse qualified_table(schema.owner.as_deref())) that returns the
schema-qualified name for VERSIONS_TABLE, and replace all hardcoded
{VERSIONS_TABLE} usages in CREATE TABLE, INSERT, and SELECT queries (including
the functions that call or compose those SQL strings) to use the qualified name
so the table is created and queried under the correct SchemaConfig.owner.

---

Duplicate comments:
In `@data_connector/src/versioning.rs`:
- Around line 262-273: The code currently performs unchecked casts between
signed DB types and u32 (e.g., in oracle_current_version where an Option<i64>
from MAX(version) is cast to u32, and where a u32 version is cast to i32 for
insertion), which can silently wrap on negative or out-of-range values; update
these conversion sites (referencing oracle_current_version, the MAX(version)
query handling, and the code path that casts u32 -> i32 for inserts) to perform
defensive checks: treat None as zero explicitly, verify the i64 is >= 0 and <=
u32::MAX before converting (use i64::try_into or TryFrom/TryInto with explicit
bounds check), and validate any u32 -> i32 conversion by ensuring the u32 value
<= i32::MAX before casting (return an Err with a clear message if checks fail)
so corrupted or oversized values are rejected rather than wrapped.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e11eca and 27a89f4.

📒 Files selected for processing (4)
  • data_connector/src/oracle.rs
  • data_connector/src/oracle_migrations.rs
  • data_connector/src/postgres_migrations.rs
  • data_connector/src/versioning.rs

Comment thread data_connector/src/versioning.rs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4820ebe4e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread data_connector/src/versioning.rs
Comment thread data_connector/src/factory.rs Outdated

@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: 2

♻️ Duplicate comments (1)
data_connector/src/versioning.rs (1)

67-75: ⚠️ Potential issue | 🟠 Major

Postgres _schema_versions is not schema-scoped, so migration state can leak across owners.

Line 68 creates the table unqualified, and Lines 397/428 write/read it unqualified. With different SchemaConfig.owner values in one database, one schema can incorrectly inherit another schema’s migration state and skip required migrations.

💡 Proposed fix
+fn postgres_versions_table(schema: &SchemaConfig) -> String {
+    match &schema.owner {
+        Some(owner) => format!("{owner}.{VERSIONS_TABLE}"),
+        None => VERSIONS_TABLE.to_string(),
+    }
+}
+
-pub fn postgres_create_versions_table() -> String {
+pub fn postgres_create_versions_table(schema: &SchemaConfig) -> String {
+    let versions_table = postgres_versions_table(schema);
     format!(
-        "CREATE TABLE IF NOT EXISTS {VERSIONS_TABLE} (\
+        "CREATE TABLE IF NOT EXISTS {versions_table} (\
          version INTEGER NOT NULL PRIMARY KEY, \
          description VARCHAR(512), \
          applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW())"
     )
 }

-    client.batch_execute(&postgres_create_versions_table()).await?;
+    client
+        .batch_execute(&postgres_create_versions_table(schema))
+        .await?;

-            &format!("INSERT INTO {VERSIONS_TABLE} (version, description) VALUES ($1, $2)"),
+            &format!(
+                "INSERT INTO {} (version, description) VALUES ($1, $2)",
+                postgres_versions_table(schema)
+            ),

-async fn postgres_current_version(client: &tokio_postgres::Client) -> Result<u32, String> {
+async fn postgres_current_version(
+    client: &tokio_postgres::Client,
+    schema: &SchemaConfig,
+) -> Result<u32, String> {
+    let versions_table = postgres_versions_table(schema);
     let row = client
         .query_one(
-            &format!("SELECT COALESCE(MAX(version), 0) FROM {VERSIONS_TABLE}"),
+            &format!("SELECT COALESCE(MAX(version), 0) FROM {versions_table}"),
             &[],
         )
#!/bin/bash
set -euo pipefail

# Show all Postgres versions-table references and whether they are schema-qualified.
rg -n --type=rust 'postgres_create_versions_table|INSERT INTO \{VERSIONS_TABLE\}|FROM \{VERSIONS_TABLE\}|COALESCE\(MAX\(version\), 0\)' data_connector/src/versioning.rs

Also applies to: 289-292, 397-399, 425-429

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@data_connector/src/versioning.rs` around lines 67 - 75, The versions table is
created and referenced without schema-qualification which lets migration state
leak across different SchemaConfig.owner values; update
postgres_create_versions_table to create the table in the owner schema (use the
SchemaConfig.owner value to prefix the table name), and update every SQL
template that uses {VERSIONS_TABLE} (the INSERT INTO {VERSIONS_TABLE}, FROM
{VERSIONS_TABLE}, COALESCE(MAX(version), 0) references) to include the same
schema-qualified identifier; ensure you properly quote/escape the schema and
table identifiers (use double-quoting or a safe identifier-quoting helper) when
formatting strings so owners with special characters are handled safely.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@data_connector/src/oracle.rs`:
- Around line 85-92: The migration can add columns required for indexes, but
init_schema's earlier attempt to create those indexes may fail (ORA-00904) and
be suppressed, leaving indexes uncreated until restart; fix by re-invoking the
schema initialization after running migrations: after
crate::versioning::run_oracle_migrations(...) returns successfully, call the
same init_schemas/init_schema path used earlier (the function that creates
RESPONSES_USER_IDX and other indexes) with the current connection and schema to
retry index creation in the same startup, and propagate or log any errors from
that retry so failures are visible.

In `@data_connector/src/versioning.rs`:
- Around line 239-257: The TOCTOU occurs where you check for the versions table
(using schema.owner and VERSIONS_TABLE with conn.query_row_as) then call
oracle_create_versions_table and conn.execute to create it; modify the execute
error handling to detect Oracle's ORA-00955 "name is already used by an existing
object" and treat it as success (silently proceed) instead of returning an
error, mirroring the existing migration conflict handling and the
create_index_if_missing pattern; specifically wrap the conn.execute error
mapping to check the error string/code for ORA-00955 and only return an error
for other cases, then still call conn.commit() on success.

---

Duplicate comments:
In `@data_connector/src/versioning.rs`:
- Around line 67-75: The versions table is created and referenced without
schema-qualification which lets migration state leak across different
SchemaConfig.owner values; update postgres_create_versions_table to create the
table in the owner schema (use the SchemaConfig.owner value to prefix the table
name), and update every SQL template that uses {VERSIONS_TABLE} (the INSERT INTO
{VERSIONS_TABLE}, FROM {VERSIONS_TABLE}, COALESCE(MAX(version), 0) references)
to include the same schema-qualified identifier; ensure you properly
quote/escape the schema and table identifiers (use double-quoting or a safe
identifier-quoting helper) when formatting strings so owners with special
characters are handled safely.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 27a89f4 and a4820eb.

📒 Files selected for processing (4)
  • data_connector/src/oracle.rs
  • data_connector/src/oracle_migrations.rs
  • data_connector/src/postgres_migrations.rs
  • data_connector/src/versioning.rs

Comment thread data_connector/src/oracle.rs
Comment thread data_connector/src/versioning.rs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 287e8ed001

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread data_connector/src/oracle.rs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2cd7f2344e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread data_connector/src/oracle.rs
Comment thread data_connector/src/postgres.rs

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b31b5615e2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +316 to +318
let current = postgres_current_version(client).await?;
let skip_up_to = config_version.unwrap_or(0);
let effective_start = current.max(skip_up_to);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Seed baseline version before enforcing pending migrations

This runner derives effective_start solely from _schema_versions, so on a brand-new deployment (where init_schema already created tables with the latest shape) current is still 0 and all migrations are treated as pending. With the default auto_migrate: false, startup then fails on clean installs until operators run no-op DDL plus manual version inserts, which is a regression for default bootstrapping. Consider stamping fresh schemas to the latest version (or otherwise detecting a bootstrap state) before applying the pending-migration gate.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8338219a2e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +162 to +164
// When auto_migrate is off, fail with actionable info
if !auto_migrate {
return Err(pending_migrations_error(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip pending-migration failure on freshly bootstrapped schemas

This auto_migrate gate fails startup whenever _schema_versions is empty, even though the same startup path has already created tables with the latest shape (for example, responses is created with safety_identifier and no user_id before migration checks). On a brand-new Oracle/Postgres deployment with default auto_migrate: false, this produces a hard failure and forces operators to run redundant no-op migration SQL plus manual version inserts just to boot. The migration runner should baseline freshly initialized schemas (or otherwise detect no-op pending migrations) instead of treating them as a required manual migration.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 742c14a038

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +331 to +335
if !auto_migrate {
return Err(pending_migrations_error(
"postgres",
effective_start,
&pending,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Seed baseline version for freshly created schemas

On a brand-new Postgres/Oracle database, table initialization already creates the latest responses shape (including safety_identifier and without user_id), but _schema_versions starts empty, so current is 0 and this branch raises a migration-required error when auto_migrate is left at its default false. That makes first boot fail even though there is no real schema delta to apply, forcing operators to run no-op historical migrations or set skip flags; this should treat freshly bootstrapped schemas as up-to-date by recording a baseline version after initial DDL.

Useful? React with 👍 / 👎.

Comment on lines +456 to +457
let version: i32 = row.get(0);
Ok(version as u32)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate schema version before casting to unsigned

The versions table allows any signed integer, but this cast converts negative values to huge u32 numbers (-1 becomes 4294967295). If an operator manually inserts a bad version row (which this feature explicitly supports via manual SQL guidance), migration checks will silently think the schema is far ahead and skip required migrations; reject negative values (or add a DB check constraint) instead of using an unchecked as u32 conversion.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot added ci CI/CD configuration changes model-gateway Model gateway crate changes and removed ci CI/CD configuration changes model-gateway Model gateway crate changes labels Feb 27, 2026
@mergify

mergify Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Hi @slin1237, the DCO sign-off check has failed. All commits must include a Signed-off-by line.

To fix existing commits:

# Sign off the last N commits (replace N with the number of unsigned commits)
git rebase HEAD~N --signoff
git push --force-with-lease

To sign off future commits automatically:

  • Use git commit -s every time, or
  • VSCode: enable Git: Always Sign Off in Settings
  • PyCharm: enable Sign-off commit in the Commit tool window

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93b6bd92f3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +190 to +192
if !applied.is_empty() {
store.ensure_response_indexes().await?;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry deferred Postgres index creation unconditionally

PostgresResponseStorage::new now downgrades responses_safety_idx creation failures to a debug log when the migrated column is not present yet, and this function retries index creation only when run_migrations() reports locally applied versions. In concurrent startup, another instance can apply the migrations first (so applied is empty here), which skips this retry path even though this process already deferred index creation earlier; that leaves safety_identifier queries running without the intended index until a later restart.

Useful? React with 👍 / 👎.

@slin1237
slin1237 force-pushed the slin/db-3 branch 2 times, most recently from a20c81b to 99d0a29 Compare February 27, 2026 02:39
…ations

Add a tracked, per-backend migration system that replaces ad-hoc ALTER TABLE
calls with versioned migrations recorded in a _schema_versions table.

What changed:
- data_connector/src/versioning.rs: new migration infrastructure with
  Migration struct, version tracking table DDL, Oracle runner (sync with
  ORA-00001 concurrent safety), Postgres runner (async with pg_advisory_lock
  serialization), and pending_migrations_error for actionable failure messages
- data_connector/src/oracle_migrations.rs: Oracle migration definitions
  (v1: add safety_identifier, v2: remove user_id) using PL/SQL idempotency
- data_connector/src/postgres_migrations.rs: Postgres migration definitions
  (v1: add safety_identifier IF NOT EXISTS, v2: drop user_id IF EXISTS)
- data_connector/src/schema.rs: add version (Option<u32>) and auto_migrate
  (bool, default false) fields to SchemaConfig with serde support
- data_connector/src/oracle.rs: remove ad-hoc alter_safety_identifier_column
  and remove_user_id_column_if_exists (91 lines), wire ORACLE_MIGRATIONS
  into OracleStore::new after table creation
- data_connector/src/postgres.rs: add PostgresStore::run_migrations method,
  wire POSTGRES_MIGRATIONS via factory
- data_connector/src/factory.rs: call store.run_migrations() after Postgres
  table creation, clone store to keep it alive
- data_connector/src/lib.rs: add versioning, oracle_migrations, and
  postgres_migrations modules
- data_connector/README.md: document schema versioning section with
  safe-by-default behavior, migration table, config fields, concurrency
  safety, and YAML examples

Why:
Ad-hoc ALTER TABLE calls had no version tracking, re-ran on every startup
relying solely on idempotency, and had race conditions between concurrent
instances. A versioned migration system provides:
- Version tracking via _schema_versions table (run-once guarantee)
- Safe-by-default: auto_migrate defaults to false; when pending migrations
  exist, startup fails with the exact SQL statements needed so operators
  can review and apply manually
- Opt-in auto-migration for development/convenience (auto_migrate: true)
- Concurrency safety: pg_advisory_lock for Postgres, ORA-00001 catch for
  Oracle concurrent instances
- Migration definitions respect SchemaConfig (custom table/column names,
  skip_columns)

How:
Migration struct holds version number, description, and an up function that
takes &SchemaConfig and returns Vec<String> of DDL statements. Per-backend
migration lists are static arrays in dedicated files. Runners check current
version from _schema_versions, compare against pending migrations, and either
apply (auto_migrate=true) or fail with actionable error (auto_migrate=false).
Oracle uses PL/SQL exception handling for DDL idempotency since DDL auto-
commits. Postgres uses transactions per migration since DDL is transactional.
Oracle existence check uses all_tables with owner filter for cross-schema
deployments.

Signed-off-by: Si Lin <silin@umich.edu>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
Three bugs identified by automated PR review, all fixed with tests:

1. Oracle _schema_versions existence check used wrong case for quoted
   identifiers. When schema.owner is set, the DDL creates a quoted table
   (OWNER."_schema_versions" = lowercase in catalog), but the check looked
   for _SCHEMA_VERSIONS (uppercase). Now uses lowercase for owner-qualified
   checks and uppercase for unquoted (current user) checks.
   - versioning.rs: ensure_oracle_versions_table uses case-aware SQL

2. Oracle init_schema creates safety_identifier index before migrations
   add the column on legacy schemas. create_index_if_missing now tolerates
   ORA-00904 (invalid identifier) so the index creation is skipped when
   the column doesn't exist yet. Next startup after migration creates it.
   - oracle.rs: create_index_if_missing handles ORA-00904

3. v2 migration (drop USER_ID) could destroy a column that SchemaConfig
   maps to USER_ID. Both Oracle and Postgres v2 now check if any column
   mapping targets USER_ID before dropping.
   - oracle_migrations.rs: oracle_v2_up checks column mappings
   - postgres_migrations.rs: pg_v2_up checks column mappings
   - Added tests for both skip cases

Signed-off-by: Si Lin <silin@umich.edu>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
Oracle does not allow identifiers starting with `_` unless they are
double-quoted. The CREATE TABLE, INSERT, and SELECT statements for the
_schema_versions table were using the name unquoted when no schema
owner was configured, producing ORA-00911 ("invalid character").

What changed:
- versioning.rs: extract `oracle_versions_table()` helper that always
  returns a quoted identifier (`"_schema_versions"` or
  `OWNER."_schema_versions"`)
- Replace 3 inline match arms in `oracle_create_versions_table`,
  `run_oracle_migrations`, and `oracle_current_version` with the helper
- Fix `ensure_oracle_versions_table` existence check for the no-owner
  case: look for lowercase name in user_tables since the quoted
  identifier preserves case
- Add test for the helper covering both owner and no-owner cases
- Update existing DDL test to assert quoting

Why:
E2E Oracle tests fail with ORA-00911 because `CREATE TABLE
_schema_versions (...)` is invalid Oracle SQL.

Signed-off-by: simolin <simolin@users.noreply.github.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
What changed:
- versioning.rs: catch ORA-00955 (table already exists) in
  ensure_oracle_versions_table to handle concurrent startup race
- versioning.rs: include INSERT INTO _schema_versions SQL in the
  pending-migration error message so operators recording the version
  after manual DDL application
- oracle.rs: re-run init_schemas after migrations so that indexes on
  newly-added columns (e.g. safety_identifier from v1) are created in
  the same startup cycle instead of requiring a restart
- postgres.rs: separate CREATE INDEX from CREATE TABLE in
  PostgresResponseStorage::new() so legacy tables missing migrated
  columns don't block startup; add ensure_response_indexes() to retry
  index creation after migrations
- factory.rs: call ensure_response_indexes() after Postgres migrations
  when any migrations were applied

Why:
PR review bots identified several real issues: Oracle TOCTOU race on
_schema_versions creation, missing version-tracking INSERT in manual
migration guidance, and index creation on migrated columns failing on
legacy schemas (blocking upgrades for both Oracle and Postgres).

Signed-off-by: simolin <simolin@users.noreply.github.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
What changed:
- oracle_migrations.rs: v2 migration also checks extra_columns keys
  for USER_ID before dropping the column
- postgres_migrations.rs: same guard for Postgres v2 migration
- Added tests for extra_columns guard in both backends

Why:
user_id is not a core column, so SchemaConfig validation allows it as
an extra_column. Without this check, v2 migration would drop a
legitimately configured extra column and its data.

Signed-off-by: simolin <simolin@users.noreply.github.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
What changed:
- model_gateway/src/main.rs: add --db-auto-migrate CLI flag (env:
  DB_AUTO_MIGRATE) that wires into SchemaConfig.auto_migrate for both
  Oracle and Postgres backends
- model_gateway/src/config/types.rs: re-export SchemaConfig from
  data-connector crate
- .github/workflows/pr-test-rust.yml: set DB_AUTO_MIGRATE=true in
  agentic-apis E2E test env so ephemeral CI databases auto-migrate

Why:
The safe-by-default auto_migrate=false design causes E2E tests to fail
on fresh CI Oracle databases since there is no _schema_versions table
and pending migrations block startup. CI environments need auto_migrate
enabled, while production keeps the safe default.

Signed-off-by: simolin <simolin@users.noreply.github.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
…n control"

This reverts commit 63132e9.

Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
…tions

default_auto_migrate() now checks the DB_AUTO_MIGRATE environment
variable ("true" or "1") before falling back to false. This allows
CI and ephemeral environments to opt into automatic migrations
without requiring CLI changes or config file modifications.

SchemaConfig::default() delegates to default_auto_migrate() instead
of hardcoding false, so both serde deserialization and programmatic
construction respect the env var.

The CI Oracle setup script (ci_agentic_svc_deps.sh) now exports
DB_AUTO_MIGRATE=true alongside the ATP_* credentials, fixing the
agentic-apis E2E test failure on fresh Oracle databases where
pending migrations caused startup to abort.

Signed-off-by: Simolin <simolin@users.noreply.github.com>
Signed-off-by: Simo Lin <linsimo.mark@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data-connector Data connector crate changes documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant