diff --git a/.github/workflows/pr-test-rust.yml b/.github/workflows/pr-test-rust.yml index 506196dbe1..24e5e05e59 100644 --- a/.github/workflows/pr-test-rust.yml +++ b/.github/workflows/pr-test-rust.yml @@ -419,6 +419,7 @@ jobs: run: | bash scripts/ci_agentic_svc_deps.sh setup-oracle-client bash scripts/ci_agentic_svc_deps.sh create-oracle-user oracle-db + bash scripts/ci_agentic_svc_deps.sh create-oracle-flyway-user oracle-db - name: Run E2E tests env: @@ -431,9 +432,11 @@ jobs: bash scripts/ci_killall_sglang.sh "nuk_gpus" ${{ matrix.env_vars }} ROUTER_LOCAL_MODEL_PATH="/home/ubuntu/models" pytest ${{ matrix.reruns }} ${{ matrix.parallel_opts }} ${{ matrix.ignore_opts }} ${{ matrix.test_dirs }} ${{ matrix.test_filter }} -s -vv -o log_cli=true --log-cli-level=INFO - - name: Cleanup Oracle test user + - name: Cleanup Oracle test users if: always() && matrix.setup_agentic_deps - run: bash scripts/ci_agentic_svc_deps.sh cleanup-oracle-user oracle-db + run: | + bash scripts/ci_agentic_svc_deps.sh cleanup-oracle-flyway-user oracle-db + bash scripts/ci_agentic_svc_deps.sh cleanup-oracle-user oracle-db - name: Upload benchmark results if: matrix.upload_benchmarks && success() diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index fcb875cd96..964f673f41 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["cdylib"] pyo3 = { version = "0.28.2", features = ["extension-module", "abi3-py38"] } tokio = { version = "1.42.0", features = ["full"] } once_cell = "1.19" +serde_yaml = "0.9" [dependencies.smg] path = "../../model_gateway" diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 3a99f5257c..0acaf06d64 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -452,6 +452,7 @@ struct Router { enable_trace: bool, otlp_traces_endpoint: String, control_plane_auth: Option, + schema_config: Option, } impl Router { @@ -587,24 +588,49 @@ impl Router { HistoryBackendType::Redis => config::HistoryBackend::Redis, }; + // Load schema config from YAML file if provided + let schema = if let Some(ref path) = self.schema_config { + let content = std::fs::read_to_string(path).map_err(|e| { + config::ConfigError::ValidationFailed { + reason: format!("Failed to read schema config file '{path}': {e}"), + } + })?; + let schema: config::SchemaConfig = serde_yaml::from_str(&content).map_err(|e| { + config::ConfigError::ValidationFailed { + reason: format!("Failed to parse schema config file '{path}': {e}"), + } + })?; + Some(schema) + } else { + None + }; + let oracle = if matches!(self.history_backend, HistoryBackendType::Oracle) { - self.oracle_config - .as_ref() - .map(|cfg| cfg.to_config_oracle()) + self.oracle_config.as_ref().map(|cfg| { + let mut c = cfg.to_config_oracle(); + c.schema.clone_from(&schema); + c + }) } else { None }; let postgres_config = if matches!(self.history_backend, HistoryBackendType::Postgres) { - self.postgres_config - .as_ref() - .map(|cfg| cfg.to_config_postgres()) + self.postgres_config.as_ref().map(|cfg| { + let mut c = cfg.to_config_postgres(); + c.schema.clone_from(&schema); + c + }) } else { None }; let redis_config = if matches!(self.history_backend, HistoryBackendType::Redis) { - self.redis_config.as_ref().map(|cfg| cfg.to_config_redis()) + self.redis_config.as_ref().map(|cfg| { + let mut c = cfg.to_config_redis(); + c.schema = schema; + c + }) } else { None }; @@ -779,6 +805,7 @@ impl Router { enable_trace = false, otlp_traces_endpoint = String::from("localhost:4317"), control_plane_auth = None, + schema_config = None, ))] #[expect(clippy::too_many_arguments)] #[expect( @@ -874,6 +901,7 @@ impl Router { enable_trace: bool, otlp_traces_endpoint: String, control_plane_auth: Option, + schema_config: Option, ) -> PyResult { let mut all_urls = worker_urls.clone(); @@ -979,6 +1007,7 @@ impl Router { enable_trace, otlp_traces_endpoint, control_plane_auth, + schema_config, }) } diff --git a/bindings/python/src/smg/router_args.py b/bindings/python/src/smg/router_args.py index 2a2367420b..68a9021b28 100644 --- a/bindings/python/src/smg/router_args.py +++ b/bindings/python/src/smg/router_args.py @@ -129,6 +129,7 @@ class RouterArgs: redis_url: str | None = None redis_pool_max: int = 16 redis_retention_days: int = 30 + schema_config: str | None = None # mTLS configuration for worker communication client_cert_path: str | None = None client_key_path: str | None = None @@ -871,6 +872,14 @@ def add_cli_args( help="Redis data retention in days (-1 for persistent, default: 30, env: REDIS_RETENTION_DAYS)", ) + # Schema configuration + backend_group.add_argument( + f"--{prefix}schema-config", + type=str, + default=None, + help="Path to a YAML schema config file for storage table/column remapping", + ) + # TLS/mTLS configuration tls_group.add_argument( f"--{prefix}client-cert-path", diff --git a/e2e_test/fixtures/hooks.py b/e2e_test/fixtures/hooks.py index e2f1a774d9..e4db03476e 100644 --- a/e2e_test/fixtures/hooks.py +++ b/e2e_test/fixtures/hooks.py @@ -463,7 +463,7 @@ def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line( "markers", "storage(backend): mark test to use a specific history storage backend " - "(memory, oracle). Default is memory.", + "(memory, oracle, oracle-custom). Default is memory.", ) config.addinivalue_line( "markers", diff --git a/e2e_test/infra/gateway.py b/e2e_test/infra/gateway.py index 895e262410..c37f8d14d7 100644 --- a/e2e_test/infra/gateway.py +++ b/e2e_test/infra/gateway.py @@ -155,7 +155,7 @@ def start( decode_workers: List of decode ModelInstance objects for PD mode. igw_mode: Start in IGW mode (no workers, add via API). cloud_backend: Cloud backend type ("openai", "xai", or "anthropic"). - history_backend: History backend for cloud mode ("memory" or "oracle"). + history_backend: History backend for cloud mode ("memory", "oracle", or "oracle-custom"). policy: Routing policy (round_robin, random, etc.) timeout: Startup timeout in seconds. show_output: Show subprocess output (env var override). @@ -277,15 +277,41 @@ def start( raise ValueError(f"Unsupported cloud backend: {cloud_backend}") backend_type = cloud_backend_type.get(cloud_backend, "openai") + + # oracle-custom: use Flyway-managed schema with schema-config + actual_history_backend = history_backend + if history_backend == "oracle-custom": + actual_history_backend = "oracle" + # Override Oracle env vars to use Flyway user credentials + flyway_user = os.environ.get("ATP_FLYWAY_USER", "") + flyway_password = os.environ.get("ATP_FLYWAY_PASSWORD", "") + flyway_dsn = os.environ.get("ATP_FLYWAY_DSN", "") + if not all([flyway_user, flyway_password, flyway_dsn]): + raise ValueError( + "ATP_FLYWAY_USER, ATP_FLYWAY_PASSWORD, and ATP_FLYWAY_DSN " + "environment variables required for oracle-custom backend" + ) + self._env["ATP_USER"] = flyway_user + self._env["ATP_PASSWORD"] = flyway_password + self._env["ATP_DSN"] = flyway_dsn + mode_args = [ "--backend", backend_type, "--worker-urls", worker_url, "--history-backend", - history_backend, + actual_history_backend, ] + if history_backend == "oracle-custom": + mode_args.extend( + [ + "--schema-config", + "scripts/oracle_flyway/schema-config.yaml", + ] + ) + self._launch( mode_args=mode_args, timeout=timeout, @@ -621,7 +647,7 @@ def launch_cloud_gateway( Args: runtime: Cloud runtime ("openai" or "xai") - history_backend: History storage backend ("memory" or "oracle") + history_backend: History storage backend ("memory", "oracle", or "oracle-custom") extra_args: Additional router arguments timeout: Startup timeout in seconds show_output: Show subprocess output diff --git a/e2e_test/responses/test_state_management.py b/e2e_test/responses/test_state_management.py index 7eaad5501e..9fdee68059 100644 --- a/e2e_test/responses/test_state_management.py +++ b/e2e_test/responses/test_state_management.py @@ -18,13 +18,12 @@ # ============================================================================= -# Cloud Backend Tests (OpenAI, xAI) +# Cloud Backend Tests (base mixin — no parametrize, subclasses add their own) # ============================================================================= -@pytest.mark.parametrize("setup_backend", ["openai", "xai"], indirect=True) -class TestStateManagementCloud: - """State management tests against cloud APIs.""" +class _StateManagementCloudBase: + """Base test methods for state management against cloud APIs.""" def test_basic_response_creation(self, setup_backend, smg): """Test basic response creation without state.""" @@ -185,6 +184,41 @@ def test_mutually_exclusive_parameters(self, setup_backend, smg): ) +# ============================================================================= +# Cloud Backend Tests (OpenAI) +# ============================================================================= + + +@pytest.mark.parametrize("setup_backend", ["openai"], indirect=True) +class TestStateManagementCloud(_StateManagementCloudBase): + """State management tests against OpenAI cloud API.""" + + +# ============================================================================= +# Cloud Backend Tests (xAI) +# ============================================================================= + + +@pytest.mark.parametrize("setup_backend", ["xai"], indirect=True) +class TestStateManagementCloudXai(_StateManagementCloudBase): + """State management tests against xAI cloud API.""" + + +# ============================================================================= +# Cloud Backend Tests with Flyway-managed Oracle schema (oracle-custom) +# ============================================================================= + + +@pytest.mark.storage("oracle-custom") +@pytest.mark.parametrize("setup_backend", ["openai"], indirect=True) +class TestStateManagementOracleCustom(_StateManagementCloudBase): + """State management tests against Oracle with Flyway-managed schema (schema-config). + + The storage("oracle-custom") marker causes the gateway to launch with + --schema-config pointing to the Flyway schema, using ATP_FLYWAY_* env vars. + """ + + # ============================================================================= # Local Backend Tests (gRPC with Qwen model) # ============================================================================= diff --git a/model_gateway/src/config/types.rs b/model_gateway/src/config/types.rs index 1187405770..c2e8da10ac 100644 --- a/model_gateway/src/config/types.rs +++ b/model_gateway/src/config/types.rs @@ -3,7 +3,9 @@ use std::collections::HashMap; use openai_protocol::worker::HealthCheckConfig as ProtocolHealthCheckConfig; use serde::{Deserialize, Serialize}; // Re-export storage config types from data_connector -pub use smg_data_connector::{HistoryBackend, OracleConfig, PostgresConfig, RedisConfig}; +pub use smg_data_connector::{ + HistoryBackend, OracleConfig, PostgresConfig, RedisConfig, SchemaConfig, +}; use super::{validation::ConfigValidator, ConfigResult}; use crate::core::ConnectionMode; diff --git a/model_gateway/src/main.rs b/model_gateway/src/main.rs index 3b14bc3529..49044abdaa 100644 --- a/model_gateway/src/main.rs +++ b/model_gateway/src/main.rs @@ -6,8 +6,8 @@ use smg::{ config::{ CircuitBreakerConfig, ConfigError, ConfigResult, DiscoveryConfig, HealthCheckConfig, HistoryBackend, ManualAssignmentMode, MetricsConfig, OracleConfig, PolicyConfig, - PostgresConfig, RedisConfig, RetryConfig, RouterConfig, RoutingMode, TokenizerCacheConfig, - TraceConfig, + PostgresConfig, RedisConfig, RetryConfig, RouterConfig, RoutingMode, SchemaConfig, + TokenizerCacheConfig, TraceConfig, }, core::ConnectionMode, observability::{ @@ -458,6 +458,10 @@ struct CliArgs { #[arg(long, help_heading = "Backend")] storage_hook_wasm_path: Option, + /// Path to a YAML schema config file for storage table/column remapping + #[arg(long, help_heading = "Backend")] + schema_config: Option, + // ==================== Oracle Database ==================== /// Path to Oracle ATP wallet directory #[arg(long, env = "ATP_WALLET_PATH", help_heading = "Oracle Database")] @@ -792,6 +796,23 @@ impl CliArgs { } } + fn load_schema_config(&self) -> ConfigResult> { + match &self.schema_config { + Some(path) => { + let content = + std::fs::read_to_string(path).map_err(|e| ConfigError::ValidationFailed { + reason: format!("Failed to read schema config file '{path}': {e}"), + })?; + let schema: SchemaConfig = + serde_yaml::from_str(&content).map_err(|e| ConfigError::ValidationFailed { + reason: format!("Failed to parse schema config file '{path}': {e}"), + })?; + Ok(Some(schema)) + } + None => Ok(None), + } + } + fn resolve_oracle_connect_details(&self) -> ConfigResult { if let Some(dsn) = self.oracle_dsn.clone() { return Ok(OracleConnectSource::Dsn { descriptor: dsn }); @@ -817,7 +838,7 @@ impl CliArgs { }) } - fn build_oracle_config(&self) -> ConfigResult { + fn build_oracle_config(&self, schema: Option) -> ConfigResult { let (wallet_path, connect_descriptor) = match self.resolve_oracle_connect_details()? { OracleConnectSource::Dsn { descriptor } => (None, descriptor), OracleConnectSource::Wallet { path, alias } => (Some(path), alias), @@ -878,11 +899,11 @@ impl CliArgs { pool_min, pool_max, pool_timeout_secs, - schema: None, + schema, }) } - fn build_postgres_config(&self) -> ConfigResult { + fn build_postgres_config(&self, schema: Option) -> ConfigResult { let db_url = self.postgres_db_url.clone().unwrap_or_default(); let pool_max = self .postgres_pool_max_size @@ -890,7 +911,7 @@ impl CliArgs { let pcf = PostgresConfig { db_url, pool_max, - schema: None, + schema, }; pcf.validate().map_err(|e| ConfigError::ValidationFailed { reason: e.to_string(), @@ -898,7 +919,7 @@ impl CliArgs { Ok(pcf) } - fn build_redis_config(&self) -> ConfigResult { + fn build_redis_config(&self, schema: Option) -> ConfigResult { let url = self.redis_url.clone().unwrap_or_default(); let pool_max = self.redis_pool_max_size.unwrap_or(16); @@ -912,7 +933,7 @@ impl CliArgs { url, pool_max, retention_days, - schema: None, + schema, }; rcf.validate().map_err(|e| ConfigError::ValidationFailed { reason: e.to_string(), @@ -1012,18 +1033,20 @@ impl CliArgs { _ => HistoryBackend::Memory, }; + let schema = self.load_schema_config()?; + let oracle = if history_backend == HistoryBackend::Oracle { - Some(self.build_oracle_config()?) + Some(self.build_oracle_config(schema.clone())?) } else { None }; let postgres = if history_backend == HistoryBackend::Postgres { - Some(self.build_postgres_config()?) + Some(self.build_postgres_config(schema.clone())?) } else { None }; let redis = if history_backend == HistoryBackend::Redis { - Some(self.build_redis_config()?) + Some(self.build_redis_config(schema)?) } else { None }; diff --git a/scripts/ci_agentic_svc_deps.sh b/scripts/ci_agentic_svc_deps.sh index 15ae9d4618..daf013a918 100755 --- a/scripts/ci_agentic_svc_deps.sh +++ b/scripts/ci_agentic_svc_deps.sh @@ -5,6 +5,8 @@ # bash ci_agentic_svc_deps.sh setup-oracle-client # bash ci_agentic_svc_deps.sh create-oracle-user # bash ci_agentic_svc_deps.sh cleanup-oracle-user +# bash ci_agentic_svc_deps.sh create-oracle-flyway-user +# bash ci_agentic_svc_deps.sh cleanup-oracle-flyway-user set -uo pipefail @@ -143,10 +145,111 @@ except Exception as e: PYEOF } +cmd_create_oracle_flyway_user() { + set -e + local oracle_host="${1:-oracle-db}" + local oracle_dsn="${oracle_host}:1521/FREEPDB1" + + pip install oracledb + + # Use last two segments of pod name (same logic as create-oracle-user) + RAW_NAME=$(echo "$HOSTNAME" | rev | cut -d'-' -f1,2 | rev | tr '[:lower:]-' '[:upper:]_') + FLYWAY_USER="FLYWAY_${RAW_NAME}" + # Prefix with 'P' so the password always starts with a letter (Oracle requirement) + FLYWAY_PASS="P$(openssl rand -hex 8)" + echo "Creating Oracle Flyway test user: $FLYWAY_USER" + + export ORA_FLYWAY_USER="$FLYWAY_USER" + export ORA_FLYWAY_PASS="$FLYWAY_PASS" + export ORA_DSN="$oracle_dsn" + + # Locate Flyway SQL files relative to the repo root + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + export FLYWAY_SQL_DIR="$REPO_ROOT/scripts/oracle_flyway/sql" + + python3 << 'PYEOF' +import os, glob, oracledb + +user = os.environ["ORA_FLYWAY_USER"] +pwd = os.environ["ORA_FLYWAY_PASS"] +dsn = os.environ["ORA_DSN"] +sql_dir = os.environ["FLYWAY_SQL_DIR"] + +# Create user with extra privileges needed for V2 sweep procedures/scheduler jobs +conn = oracledb.connect(user="system", password="oracle", dsn=dsn) +cur = conn.cursor() +cur.execute(f'CREATE USER {user} IDENTIFIED BY "{pwd}" QUOTA UNLIMITED ON USERS') +cur.execute(f"GRANT CONNECT, RESOURCE TO {user}") +conn.commit() +conn.close() +print(f"Oracle Flyway user {user} created successfully") + +# Run Flyway SQL files against the new user +flyway_conn = oracledb.connect(user=user, password=pwd, dsn=dsn) +flyway_cur = flyway_conn.cursor() + +sql_files = sorted(glob.glob(os.path.join(sql_dir, "V*.sql"))) +for sql_file in sql_files: + print(f"Executing {os.path.basename(sql_file)}...") + with open(sql_file) as f: + content = f.read() + + # Split on semicolons and execute each statement + for stmt in content.split(";"): + # Strip comments and whitespace + lines = [l for l in stmt.split("\n") if not l.strip().startswith("--")] + cleaned = "\n".join(lines).strip() + if cleaned: + flyway_cur.execute(cleaned) + +flyway_conn.commit() +flyway_conn.close() +print("Flyway SQL files executed successfully") +PYEOF + + echo "ATP_FLYWAY_USER=$FLYWAY_USER" >> "$GITHUB_ENV" + echo "ATP_FLYWAY_PASSWORD=$FLYWAY_PASS" >> "$GITHUB_ENV" + echo "ATP_FLYWAY_DSN=$oracle_dsn" >> "$GITHUB_ENV" +} + +cmd_cleanup_oracle_flyway_user() { + local oracle_host="${1:-oracle-db}" + local oracle_dsn="${oracle_host}:1521/FREEPDB1" + + if [ -z "${ATP_FLYWAY_USER:-}" ] || [ "$ATP_FLYWAY_USER" = "system" ]; then + echo "No Flyway test user to clean up" + return 0 + fi + + echo "Dropping Oracle Flyway test user: $ATP_FLYWAY_USER" + pip install oracledb 2>/dev/null || true + + export ORA_DROP_USER="$ATP_FLYWAY_USER" + export ORA_DSN="$oracle_dsn" + + python3 << 'PYEOF' || echo "Warning: cleanup script failed" +import os, oracledb +try: + user = os.environ["ORA_DROP_USER"] + dsn = os.environ["ORA_DSN"] + conn = oracledb.connect(user="system", password="oracle", dsn=dsn) + cur = conn.cursor() + cur.execute(f"DROP USER {user} CASCADE") + conn.commit() + conn.close() + print("Oracle Flyway test user dropped successfully") +except Exception as e: + print(f"Warning: failed to drop Flyway test user: {e}") +PYEOF +} + case "$COMMAND" in check) cmd_check "$@" ;; setup-oracle-client) cmd_setup_oracle_client ;; create-oracle-user) cmd_create_oracle_user "$@" ;; cleanup-oracle-user) cmd_cleanup_oracle_user "$@" ;; + create-oracle-flyway-user) cmd_create_oracle_flyway_user "$@" ;; + cleanup-oracle-flyway-user) cmd_cleanup_oracle_flyway_user "$@" ;; *) echo "Unknown command: $COMMAND"; exit 1 ;; esac diff --git a/scripts/oracle_flyway/schema-config.yaml b/scripts/oracle_flyway/schema-config.yaml new file mode 100644 index 0000000000..3deeba47a6 --- /dev/null +++ b/scripts/oracle_flyway/schema-config.yaml @@ -0,0 +1,19 @@ +# SMG SchemaConfig for genai-data-plane Oracle tables (Flyway-managed) +version: 3 +auto_migrate: false + +conversations: + table: CONVERSATIONS_V2 + +responses: + table: RESPONSES + columns: + id: RESPONSE_ID + input: INPUT_ITEMS + raw_response: RESPONSE_OBJECT + +conversation_items: + table: CONVERSATION_ITEMS + +conversation_item_links: + table: CONVERSATION_ITEM_LINKS diff --git a/scripts/oracle_flyway/sql/V1__Create_responses_table.sql b/scripts/oracle_flyway/sql/V1__Create_responses_table.sql new file mode 100644 index 0000000000..5e14c7fa27 --- /dev/null +++ b/scripts/oracle_flyway/sql/V1__Create_responses_table.sql @@ -0,0 +1,22 @@ +-- V1: Create the existing RESPONSES table (as it exists in genai-data-plane) +-- This represents the pre-migration baseline schema. + +CREATE TABLE RESPONSES ( + RESPONSE_ID VARCHAR2(255) NOT NULL, + CONVERSATION_ID VARCHAR2(255), + PREVIOUS_RESPONSE_ID VARCHAR2(255), + INPUT_ITEMS CLOB, + RESPONSE_OBJECT CLOB, + MODEL VARCHAR2(255), + CREATED_AT TIMESTAMP WITH TIME ZONE, + CONVERSATION_STORE_ID VARCHAR2(255), + GENERATIVE_AI_PROJECT_ID VARCHAR2(255), + SUBJECT_ID VARCHAR2(255), + EXPIRES_AT TIMESTAMP WITH TIME ZONE, + CONSTRAINT PK_RESPONSES PRIMARY KEY (RESPONSE_ID) +); + +-- Indexes for common query patterns +CREATE INDEX IDX_RESPONSES_CONV_ID ON RESPONSES (CONVERSATION_ID); +CREATE INDEX IDX_RESPONSES_EXPIRES_AT ON RESPONSES (EXPIRES_AT); +CREATE INDEX IDX_RESPONSES_CREATED_AT ON RESPONSES (CREATED_AT); diff --git a/scripts/oracle_flyway/sql/V2__Create_v2_conversations_and_alter_responses.sql b/scripts/oracle_flyway/sql/V2__Create_v2_conversations_and_alter_responses.sql new file mode 100644 index 0000000000..cdeb8c44f4 --- /dev/null +++ b/scripts/oracle_flyway/sql/V2__Create_v2_conversations_and_alter_responses.sql @@ -0,0 +1,58 @@ +-- V2: Create V2 conversation tables and alter RESPONSES +-- CI-only version: tables + indexes, no sweep procedures or scheduler jobs. + +------------------------------------------------------------ +-- 1. CONVERSATIONS_V2 +------------------------------------------------------------ +CREATE TABLE CONVERSATIONS_V2 ( + ID VARCHAR2(255) NOT NULL, + CONVERSATION_STORE_ID VARCHAR2(255), + GENERATIVE_AI_PROJECT_ID VARCHAR2(255), + CREATED_AT TIMESTAMP WITH TIME ZONE NOT NULL, + METADATA CLOB, + SHORT_TERM_MEMORY CLOB, + SHORT_TERM_MEMORY_BYTES NUMBER, + UPDATED_AT TIMESTAMP WITH TIME ZONE, + EXPIRES_AT TIMESTAMP WITH TIME ZONE DEFAULT (SYSTIMESTAMP + INTERVAL '30' DAY) NOT NULL, + CONSTRAINT PK_CONVERSATIONS_V2 PRIMARY KEY (ID) +); + +CREATE INDEX IDX_CONV_V2_EXPIRES_AT ON CONVERSATIONS_V2 (EXPIRES_AT); +CREATE INDEX IDX_CONV_V2_PROJECT_ID ON CONVERSATIONS_V2 (GENERATIVE_AI_PROJECT_ID); +CREATE INDEX IDX_CONV_V2_STORE_ID ON CONVERSATIONS_V2 (CONVERSATION_STORE_ID); + +------------------------------------------------------------ +-- 2. CONVERSATION_ITEMS +------------------------------------------------------------ +CREATE TABLE CONVERSATION_ITEMS ( + ID VARCHAR2(255) NOT NULL, + RESPONSE_ID VARCHAR2(255), + ITEM_TYPE VARCHAR2(100), + ROLE VARCHAR2(50), + CONTENT CLOB, + STATUS VARCHAR2(50), + CREATED_AT TIMESTAMP WITH TIME ZONE, + EXPIRES_AT TIMESTAMP WITH TIME ZONE DEFAULT (SYSTIMESTAMP + INTERVAL '30' DAY) NOT NULL, + CONSTRAINT PK_CONVERSATION_ITEMS PRIMARY KEY (ID) +); + +CREATE INDEX IDX_CONV_ITEMS_EXPIRES_AT ON CONVERSATION_ITEMS (EXPIRES_AT); +CREATE INDEX IDX_CONV_ITEMS_RESPONSE_ID ON CONVERSATION_ITEMS (RESPONSE_ID); +CREATE INDEX IDX_CONV_ITEMS_ITEM_TYPE ON CONVERSATION_ITEMS (ITEM_TYPE); + +------------------------------------------------------------ +-- 3. CONVERSATION_ITEM_LINKS +------------------------------------------------------------ +CREATE TABLE CONVERSATION_ITEM_LINKS ( + CONVERSATION_ID VARCHAR2(255) NOT NULL, + ITEM_ID VARCHAR2(255) NOT NULL, + ADDED_AT TIMESTAMP WITH TIME ZONE NOT NULL, + CONSTRAINT PK_CONV_ITEM_LINKS PRIMARY KEY (CONVERSATION_ID, ITEM_ID) +); + +CREATE INDEX IDX_CONV_ITEM_LINKS_ORDER ON CONVERSATION_ITEM_LINKS (CONVERSATION_ID, ADDED_AT); + +------------------------------------------------------------ +-- 4. ALTER RESPONSES: add SAFETY_IDENTIFIER +------------------------------------------------------------ +ALTER TABLE RESPONSES ADD (SAFETY_IDENTIFIER VARCHAR2(255));