chore: import upstream snapshot with attribution
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Has been cancelled
Java Checkstyle / java-checkstyle (push) Has been cancelled
Maven Collate Tests / maven-collate-ci (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Has been cancelled
Publish Package to Maven Central Repository / publish-maven-packages (push) Has been cancelled
OpenMetadata Service Unit Tests / Detect Changes (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Has been cancelled
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:35:45 +08:00
commit bf2343b7e4
16049 changed files with 3531137 additions and 0 deletions
@@ -0,0 +1,263 @@
-- Post data migration script for Task System Redesign - OpenMetadata 2.0.0
-- This script runs after the data migration completes
-- =====================================================
-- NOTE: Suggestion migration (suggestions → task_entity),
-- thread-based task migration (thread_entity → task_entity),
-- and legacy system activity migration
-- (thread_entity generated feed rows → activity_stream)
-- are handled in Java MigrationUtil because they require
-- entity-link aware transformation logic.
-- =====================================================
-- =====================================================
-- PHASE 2D: Migrate announcements from thread_entity → announcement_entity
-- =====================================================
INSERT INTO announcement_entity (id, json, fqnHash)
SELECT
a_id AS id,
a_json AS json,
a_fqnHash AS fqnHash
FROM (
SELECT
JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.id')) AS a_id,
JSON_OBJECT(
'id', JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.id')),
'name', CONCAT('announcement-', JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.id'))),
'fullyQualifiedName', CONCAT('announcement-', JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.id'))),
'displayName', NULLIF(JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.message')), ''),
'description', COALESCE(
JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.announcement.description')),
JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.message')),
''
),
'entityLink', JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.about')),
'startTime', CAST(JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.announcement.startTime')) AS UNSIGNED),
'endTime', CAST(JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.announcement.endTime')) AS UNSIGNED),
'status', CASE
WHEN CAST(JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.announcement.endTime')) AS UNSIGNED) < UNIX_TIMESTAMP() * 1000
THEN 'Expired'
WHEN CAST(JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.announcement.startTime')) AS UNSIGNED) > UNIX_TIMESTAMP() * 1000
THEN 'Scheduled'
ELSE 'Active'
END,
'createdBy', JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.createdBy')),
'updatedBy', COALESCE(JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.updatedBy')), JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.createdBy'))),
'createdAt', CAST(JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.threadTs')) AS UNSIGNED),
'updatedAt', CAST(
COALESCE(
JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.updatedAt')),
JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.threadTs'))
) AS UNSIGNED
),
'deleted', false,
'version', 0.1,
'reactions', COALESCE(JSON_EXTRACT(t.json, '$.reactions'), JSON_ARRAY())
) AS a_json,
MD5(CONCAT('announcement-', JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.id')))) AS a_fqnHash
FROM thread_entity t
WHERE JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.type')) = 'Announcement'
AND NOT EXISTS (
SELECT 1 FROM announcement_entity a WHERE a.id = JSON_UNQUOTE(JSON_EXTRACT(t.json, '$.id'))
)
) migrated;
-- =====================================================
-- PHASE 2E: Rename legacy thread storage to fail stale references
-- =====================================================
SET @thread_entity_exists = (
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = 'thread_entity'
);
SET @thread_entity_legacy_exists = (
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = 'thread_entity_legacy'
);
SET @rename_thread_entity_sql = IF(
@thread_entity_exists = 1 AND @thread_entity_legacy_exists = 0,
'RENAME TABLE thread_entity TO thread_entity_legacy',
'SELECT 1'
);
PREPARE rename_thread_entity_stmt FROM @rename_thread_entity_sql;
EXECUTE rename_thread_entity_stmt;
DEALLOCATE PREPARE rename_thread_entity_stmt;
-- =====================================================
-- PHASE 2F: Lower workflow trigger polling intervals
-- =====================================================
-- Reduce WorkflowEventConsumer poll interval from 10s to 1s.
-- The legacy 10s default added up to a 10s wait between an entity change and the
-- workflow-triggered approval task being created. On CI under resource pressure this
-- often drifted to >2 minutes when combined with Flowable's 60s async job poll. The
-- new value keeps the trigger pipeline near-real-time.
UPDATE event_subscription_entity
SET json = JSON_SET(json, '$.pollInterval', 1)
WHERE name = 'WorkflowEventConsumer'
AND CAST(JSON_EXTRACT(json, '$.pollInterval') AS UNSIGNED) > 1;
-- Lower Flowable async/timer job acquisition intervals to keep workflow-driven
-- task creation responsive. The previous 60s default was a Flowable production setting
-- carried over verbatim; for OpenMetadata's interactive task UX we want sub-second pickup.
UPDATE openmetadata_settings
SET json = JSON_SET(
JSON_SET(json, '$.executorConfiguration.asyncJobAcquisitionInterval', 1000),
'$.executorConfiguration.timerJobAcquisitionInterval', 5000)
WHERE configType = 'workflowSettings'
AND JSON_EXTRACT(json, '$.executorConfiguration') IS NOT NULL
AND (CAST(JSON_EXTRACT(json, '$.executorConfiguration.asyncJobAcquisitionInterval') AS UNSIGNED) > 1000
OR CAST(JSON_EXTRACT(json, '$.executorConfiguration.timerJobAcquisitionInterval') AS UNSIGNED) > 5000);
-- pipelineStatuses is a derived field, read on demand from entity_extension_time_series, and is
-- now an array instead of a single object. Strip any stale single-object value that a GET->PUT
-- round-trip may have persisted into the stored entity JSON so it cannot break deserialization.
UPDATE ingestion_pipeline_entity
SET json = JSON_REMOVE(json, '$.pipelineStatuses')
WHERE JSON_CONTAINS_PATH(json, 'one', '$.pipelineStatuses');
-- MCP Server and MCP Chat are no longer internal Applications; their enablement now lives in
-- platform settings (mcpConfiguration). The MCP Chat app was never shipped to customers, so
-- aiSettings.mcpChat keeps its seeded default shape (no config carry-over).
-- MCP Server: keep it disabled if the server app was not installed (mcpConfiguration defaults
-- to enabled=true, which would otherwise turn it on).
UPDATE openmetadata_settings
SET json = JSON_SET(json, '$.enabled', false)
WHERE configType = 'mcpConfiguration'
AND NOT EXISTS (SELECT 1 FROM installed_apps ia WHERE ia.name = 'McpApplication');
-- Retire the MCP apps (their Java classes and marketplace seeds are removed). Keep the bot users
-- so the MCP server keeps its principal.
DELETE er FROM entity_relationship er
JOIN installed_apps ia ON er.fromId = ia.id OR er.toId = ia.id
WHERE ia.name IN ('McpApplication', 'McpChatApplication');
DELETE er FROM entity_relationship er
JOIN apps_marketplace ia ON er.fromId = ia.id OR er.toId = ia.id
WHERE ia.name IN ('McpApplication', 'McpChatApplication');
DELETE FROM installed_apps WHERE name IN ('McpApplication', 'McpChatApplication');
DELETE FROM apps_marketplace WHERE name IN ('McpApplication', 'McpChatApplication');
-- Post data migration script for Task workflow cutover - OpenMetadata 2.0.0 (moved from 2.0.1)
-- RdfIndexApp: switch to weekly Saturday cron and full-rebuild every run.
-- Previous defaults (daily, incremental) were producing unbounded triple growth
-- because relationship-removal paths weren't fully reconciled. With per-run
-- CLEAR ALL the dataset always converges to MySQL state; weekly cadence keeps
-- per-run cost from saturating Fuseki.
--
-- Also rewrite `entities` to `["all"]`. Pre-upgrade, an operator could have
-- narrowed RDF indexing to a subset of entity types; the new recreateIndex=true
-- semantics issues a CLEAR ALL before indexing, which would otherwise wipe
-- triples for entity types still in MySQL but missing from the subset list.
-- Forcing the subset list back to `["all"]` ensures the post-CLEAR-ALL run
-- repopulates the graph fully; operators can re-narrow after the migration if
-- they need partial indexing.
UPDATE installed_apps
SET json = JSON_SET(
JSON_SET(
json,
'$.appConfiguration.recreateIndex', CAST('true' AS JSON),
'$.appSchedule.cronExpression', '0 0 * * 6'
),
'$.appConfiguration.entities', JSON_ARRAY('all')
)
WHERE name = 'RdfIndexApp';
UPDATE apps_marketplace
SET json = JSON_SET(json, '$.appConfiguration.recreateIndex', CAST('true' AS JSON))
WHERE name = 'RdfIndexApp';
-- Backfill policyAgentConfig defaults on existing Snowflake services. The schema-level
-- defaults in snowflakeConnection.json only apply at create-time deserialization; rows
-- already persisted carry the previous all-false shape and won't pick up the new defaults
-- without this rewrite. Only fields that are currently false are flipped — any operator-set
-- true value is preserved.
UPDATE dbservice_entity
SET json = JSON_SET(json, '$.connection.config.policyAgentConfig.enabled', true)
WHERE serviceType = 'Snowflake'
AND JSON_EXTRACT(json, '$.connection.config.policyAgentConfig.enabled') = false;
UPDATE dbservice_entity
SET json = JSON_SET(json, '$.connection.config.policyAgentConfig.supportsFullAccess', true)
WHERE serviceType = 'Snowflake'
AND JSON_EXTRACT(json, '$.connection.config.policyAgentConfig.supportsFullAccess') = false;
UPDATE dbservice_entity
SET json = JSON_SET(json, '$.connection.config.policyAgentConfig.supportsMaskedAccess', true)
WHERE serviceType = 'Snowflake'
AND JSON_EXTRACT(json, '$.connection.config.policyAgentConfig.supportsMaskedAccess') = false;
-- Services that pre-date the policyAgentConfig field entirely (older rows where the whole
-- object is missing) — write the full block in one shot. JSON_SET creates the key path.
UPDATE dbservice_entity
SET json = JSON_SET(
json,
'$.connection.config.policyAgentConfig',
JSON_OBJECT(
'enabled', true,
'supportsColumnAccess', false,
'supportsFullAccess', true,
'supportsMaskedAccess', true
)
)
WHERE serviceType = 'Snowflake'
AND JSON_EXTRACT(json, '$.connection.config.policyAgentConfig') IS NULL;
-- Databricks: enabled/Full default to true, Column and Masked stay false.
-- Two guarded flips + one full-object write for legacy rows.
UPDATE dbservice_entity
SET json = JSON_SET(json, '$.connection.config.policyAgentConfig.enabled', true)
WHERE serviceType = 'Databricks'
AND JSON_EXTRACT(json, '$.connection.config.policyAgentConfig.enabled') = false;
UPDATE dbservice_entity
SET json = JSON_SET(json, '$.connection.config.policyAgentConfig.supportsFullAccess', true)
WHERE serviceType = 'Databricks'
AND JSON_EXTRACT(json, '$.connection.config.policyAgentConfig.supportsFullAccess') = false;
UPDATE dbservice_entity
SET json = JSON_SET(
json,
'$.connection.config.policyAgentConfig',
JSON_OBJECT(
'enabled', true,
'supportsColumnAccess', false,
'supportsFullAccess', true,
'supportsMaskedAccess', false
)
)
WHERE serviceType = 'Databricks'
AND JSON_EXTRACT(json, '$.connection.config.policyAgentConfig') IS NULL;
-- Corrective heal for instances that already ran the earlier version of this script.
-- Earlier the Databricks block forced supportsMaskedAccess to true; the intended
-- default is false. Reset it on every Databricks row that currently has it true.
UPDATE dbservice_entity
SET json = JSON_SET(json, '$.connection.config.policyAgentConfig.supportsMaskedAccess', false)
WHERE serviceType = 'Databricks'
AND JSON_EXTRACT(json, '$.connection.config.policyAgentConfig.supportsMaskedAccess') = true;
-- Postgres no longer declares policyAgentConfig. Earlier this script backfilled the
-- object onto Postgres rows; remove it so the stored shape matches the schema.
UPDATE dbservice_entity
SET json = JSON_REMOVE(json, '$.connection.config.policyAgentConfig')
WHERE serviceType = 'Postgres'
AND JSON_EXTRACT(json, '$.connection.config.policyAgentConfig') IS NOT NULL;
-- Remove runtime-only fields (openMetadataServerConnection, privateConfiguration) from stored application data.
UPDATE installed_apps
SET json = JSON_REMOVE(json, '$.openMetadataServerConnection', '$.privateConfiguration')
WHERE JSON_EXTRACT(json, '$.openMetadataServerConnection') IS NOT NULL
OR JSON_EXTRACT(json, '$.privateConfiguration') IS NOT NULL;
UPDATE entity_extension
SET json = JSON_REMOVE(json, '$.openMetadataServerConnection', '$.privateConfiguration')
WHERE extension LIKE 'app.version.%'
AND (JSON_EXTRACT(json, '$.openMetadataServerConnection') IS NOT NULL
OR JSON_EXTRACT(json, '$.privateConfiguration') IS NOT NULL);
@@ -0,0 +1,512 @@
-- Task System Redesign - OpenMetadata 2.0.0
-- This migration creates the new Task entity tables and related infrastructure
-- CSV import/export and bulk-edit background job metadata.
ALTER TABLE background_jobs
ADD COLUMN progress int DEFAULT 0,
ADD COLUMN total int DEFAULT 0,
ADD COLUMN result longtext,
ADD COLUMN error longtext,
ADD COLUMN message varchar(2048),
ADD COLUMN cancelRequested boolean DEFAULT false,
ADD COLUMN completedAt bigint;
CREATE INDEX idx_background_jobs_job_type_created_by
ON background_jobs (jobType, createdBy, createdAt);
CREATE INDEX idx_background_jobs_status_updated_at
ON background_jobs (status, updatedAt);
CREATE TABLE IF NOT EXISTS background_job_logs (
logId varchar(36) NOT NULL,
jobId bigint unsigned NOT NULL,
createdAt bigint NOT NULL,
level varchar(16) NOT NULL,
message varchar(4096) NOT NULL,
PRIMARY KEY (logId),
KEY idx_background_job_logs_job_id_created_at (jobId, createdAt),
CONSTRAINT fk_background_job_logs_job_id
FOREIGN KEY (jobId) REFERENCES background_jobs(id)
ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS task_entity (
id varchar(36) NOT NULL,
json json NOT NULL,
fqnHash varchar(768) NOT NULL,
taskId varchar(20) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.taskId'))) STORED NOT NULL,
name varchar(256) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.name'))) STORED NOT NULL,
category varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.category'))) STORED NOT NULL,
type varchar(64) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.type'))) STORED NOT NULL,
status varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.status'))) STORED NOT NULL,
priority varchar(16) GENERATED ALWAYS AS (COALESCE(json_unquote(json_extract(`json`,_utf8mb4'$.priority')), 'Medium')) STORED,
createdAt bigint GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.createdAt'))) STORED NOT NULL,
updatedAt bigint GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.updatedAt'))) STORED NOT NULL,
deleted tinyint(1) GENERATED ALWAYS AS (json_extract(`json`,_utf8mb4'$.deleted')) STORED,
aboutFqnHash varchar(256) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.aboutFqnHash'))) STORED,
createdById varchar(36) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.createdById'))) STORED,
approvedById varchar(36) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.approvedById'))) STORED,
PRIMARY KEY (id),
UNIQUE KEY uk_fqn_hash (fqnHash),
KEY idx_task_id (taskId),
KEY idx_status (status),
KEY idx_category (category),
KEY idx_type (type),
KEY idx_priority (priority),
KEY idx_created_at (createdAt),
KEY idx_updated_at (updatedAt),
KEY idx_deleted (deleted),
KEY idx_status_category (status, category),
KEY idx_about_fqn_hash (aboutFqnHash),
KEY idx_status_about (status, aboutFqnHash),
KEY idx_created_by_id (createdById),
KEY idx_created_by_category (createdById, category),
KEY idx_approved_by_id (approvedById)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- For 2.0.0 environments that ran the CREATE TABLE above before the
-- approvedById generated column was added inline, attach it now. CREATE TABLE
-- IF NOT EXISTS is a no-op on those environments so the column would never
-- appear otherwise. MySQL doesn't reliably support `ADD COLUMN IF NOT EXISTS`
-- across 8.0 versions and has no `ADD KEY IF NOT EXISTS`, so guard both via
-- information_schema.
SET @ddl = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'task_entity'
AND column_name = 'approvedById'
),
'SELECT 1',
'ALTER TABLE task_entity ADD COLUMN approvedById varchar(36) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4''$.approvedById''))) STORED'
)
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'task_entity'
AND index_name = 'idx_approved_by_id'
),
'SELECT 1',
'ALTER TABLE task_entity ADD KEY idx_approved_by_id (approvedById)'
)
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS new_task_sequence (
id bigint NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO new_task_sequence (id) SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM new_task_sequence);
-- =====================================================
-- ACTIVITY STREAM TABLE (Partitioned by time)
-- Lightweight, ephemeral activity notifications
-- NOT for audit/compliance - use entity version history
-- Partitions are managed dynamically by ActivityStreamPartitionManager
-- =====================================================
CREATE TABLE IF NOT EXISTS activity_stream (
id varchar(36) NOT NULL,
eventType varchar(64) NOT NULL,
entityType varchar(64) NOT NULL,
entityId varchar(36) NOT NULL,
entityFqnHash varchar(768) CHARACTER SET ascii COLLATE ascii_bin,
about varchar(2048),
aboutFqnHash varchar(768) CHARACTER SET ascii COLLATE ascii_bin,
-- Nullable for system events and hard-deleted users; actorName is the display fallback.
actorId varchar(36),
actorName varchar(256),
timestamp bigint NOT NULL,
summary varchar(500),
fieldName varchar(256),
oldValue text,
newValue text,
domains json,
json json NOT NULL,
PRIMARY KEY (id, timestamp),
KEY idx_activity_timestamp (timestamp),
KEY idx_activity_entity (entityType, entityId, timestamp),
KEY idx_activity_actor (actorId, timestamp),
KEY idx_activity_event_type (eventType, timestamp),
KEY idx_activity_entity_fqn (entityFqnHash, timestamp),
KEY idx_activity_about (aboutFqnHash, timestamp)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
PARTITION BY RANGE (timestamp) (
-- Catch-all partition - ActivityStreamPartitionManager will reorganize this
-- by splitting it into monthly partitions as needed
PARTITION p_max VALUES LESS THAN MAXVALUE
);
-- Activity stream configuration per domain
CREATE TABLE IF NOT EXISTS activity_stream_config (
id varchar(36) NOT NULL,
json json NOT NULL,
scope varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.scope'))) STORED NOT NULL,
domainId varchar(36) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.scopeReference.id'))) STORED,
enabled tinyint(1) GENERATED ALWAYS AS (json_extract(`json`,_utf8mb4'$.enabled')) STORED,
retentionDays int GENERATED ALWAYS AS (json_extract(`json`,_utf8mb4'$.retentionDays')) STORED,
PRIMARY KEY (id),
UNIQUE KEY uk_domain_config (domainId),
KEY idx_scope (scope),
KEY idx_enabled (enabled)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- =====================================================
-- ANNOUNCEMENT ENTITY TABLE
-- Standalone entity for asset announcements (migrated from thread_entity)
-- =====================================================
CREATE TABLE IF NOT EXISTS announcement_entity (
id varchar(36) NOT NULL,
json json NOT NULL,
fqnHash varchar(768) NOT NULL,
name varchar(256) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.name'))) STORED NOT NULL,
entityLink varchar(512) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.entityLink'))) STORED,
status varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.status'))) STORED,
startTime bigint GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.startTime'))) STORED,
endTime bigint GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.endTime'))) STORED,
createdBy varchar(256) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.createdBy'))) STORED,
createdAt bigint GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.createdAt'))) STORED,
updatedAt bigint GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.updatedAt'))) STORED,
deleted tinyint(1) GENERATED ALWAYS AS (json_extract(`json`,_utf8mb4'$.deleted')) STORED,
PRIMARY KEY (id),
UNIQUE KEY uk_announcement_fqn_hash (fqnHash),
KEY idx_announcement_status (status),
KEY idx_announcement_entity_link (entityLink),
KEY idx_announcement_start_time (startTime),
KEY idx_announcement_end_time (endTime),
KEY idx_announcement_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- =====================================================
-- TASK FORM SCHEMA ENTITY TABLE
-- Stores form schemas for different task types
-- =====================================================
CREATE TABLE IF NOT EXISTS task_form_schema_entity (
id varchar(36) NOT NULL,
json json NOT NULL,
fqnHash varchar(768) NOT NULL,
name varchar(256) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.name'))) STORED NOT NULL,
taskType varchar(64) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.taskType'))) STORED,
taskCategory varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.taskCategory'))) STORED,
updatedAt bigint GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.updatedAt'))) STORED,
deleted tinyint(1) GENERATED ALWAYS AS (json_extract(`json`,_utf8mb4'$.deleted')) STORED,
PRIMARY KEY (id),
UNIQUE KEY uk_task_form_schema_fqn_hash (fqnHash),
KEY idx_task_form_schema_name (name),
KEY idx_task_form_schema_task_type (taskType),
KEY idx_task_form_schema_deleted (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- =====================================================
-- KNOWLEDGE CENTER + CONTEXT CENTER DRIVE (Collate → OM port)
-- Appended below the Task Redesign tables to preserve main's
-- migration order when merging.
-- =====================================================
-- MCP tables are created in 1.13.0 migration.
-- Knowledge Center: page entity table (Article, QuickLink).
-- Existing Collate customers already have this table from 1.2.0-collate with
-- subsequent shape changes through 1.6.0-collate (nameHash -> fqnHash VARCHAR(756),
-- pageType generated column, composite deleted index). CREATE TABLE IF NOT EXISTS
-- is a no-op for them and creates the final shape for fresh OpenMetadata installs.
CREATE TABLE IF NOT EXISTS knowledge_center (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
fqnHash VARCHAR(756) NOT NULL COLLATE ascii_bin,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') STORED NOT NULL,
json JSON NOT NULL,
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') STORED NOT NULL,
deleted BOOLEAN GENERATED ALWAYS AS (json -> '$.deleted') STORED,
pageType VARCHAR(16) GENERATED ALWAYS AS (json ->> '$.pageType') STORED NOT NULL,
PRIMARY KEY (id),
UNIQUE (fqnHash),
INDEX knowledge_center_name_index (name),
INDEX index_knowledge_center_deleted (fqnHash, deleted)
);
-- Context Center Drive: Folder entity table.
CREATE TABLE IF NOT EXISTS drive_folder (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') STORED NOT NULL,
nameHash VARCHAR(256) NOT NULL COLLATE ascii_bin,
json JSON NOT NULL,
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') STORED NOT NULL,
deleted BOOLEAN GENERATED ALWAYS AS (json -> '$.deleted') STORED,
PRIMARY KEY (id),
UNIQUE KEY unique_drive_folder_name (nameHash),
INDEX idx_drive_folder_updated_at (updatedAt)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Context Center Drive: File entity table (uploaded PDF/image/spreadsheet/office docs).
CREATE TABLE IF NOT EXISTS context_file (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') STORED NOT NULL,
nameHash VARCHAR(256) NOT NULL COLLATE ascii_bin,
json JSON NOT NULL,
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') STORED NOT NULL,
deleted BOOLEAN GENERATED ALWAYS AS (json -> '$.deleted') STORED,
PRIMARY KEY (id),
UNIQUE KEY unique_context_file_name (nameHash),
INDEX idx_context_file_updated_at (updatedAt)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Attachments: Asset entity table for uploaded file blobs referenced by ContextFiles, Pages, etc.
-- Existing Collate customers have this from 1.7.0-collate. CREATE TABLE IF NOT EXISTS is a no-op for them.
CREATE TABLE IF NOT EXISTS asset_entity (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.fileName') STORED NOT NULL,
url VARCHAR(1024) GENERATED ALWAYS AS (json ->> '$.url') STORED NOT NULL,
fullyQualifiedName VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.fullyQualifiedName') STORED NOT NULL,
assetType VARCHAR(100) GENERATED ALWAYS AS (json ->> '$.assetType') STORED NOT NULL,
json JSON NOT NULL,
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') STORED NOT NULL,
fqnHash VARCHAR(768) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
deleted BOOLEAN GENERATED ALWAYS AS (json -> '$.deleted') STORED,
PRIMARY KEY (id),
INDEX fqnhash_index (fqnHash),
INDEX asset_type_index (assetType),
INDEX idx_asset_deleted (deleted)
);
-- Context Center Drive: File content snapshot table (revisions, extracted text).
CREATE TABLE IF NOT EXISTS context_file_content (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') STORED NOT NULL,
nameHash VARCHAR(256) NOT NULL COLLATE ascii_bin,
json JSON NOT NULL,
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') STORED NOT NULL,
deleted BOOLEAN GENERATED ALWAYS AS (json -> '$.deleted') STORED,
PRIMARY KEY (id),
UNIQUE KEY unique_context_file_content_name (nameHash),
INDEX idx_context_file_content_updated_at (updatedAt)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Add tag_usage.metadata column if missing (newer tag usage payloads carry metadata).
SET @ddl = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'tag_usage'
AND column_name = 'metadata'
),
'SELECT 1',
'ALTER TABLE tag_usage ADD COLUMN metadata JSON NULL'
)
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Add audit_log_event.search_text column if missing (searchable audit log text).
SET @ddl = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'audit_log_event'
AND column_name = 'search_text'
),
'SELECT 1',
'ALTER TABLE audit_log_event ADD COLUMN search_text LONGTEXT NULL'
)
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Distributed reindex job tracking.
CREATE TABLE IF NOT EXISTS search_index_job (
id VARCHAR(64) NOT NULL,
status VARCHAR(64) NOT NULL,
jobConfiguration JSON NOT NULL,
targetIndexPrefix VARCHAR(256) NOT NULL,
stagedIndexMapping JSON DEFAULT NULL,
totalRecords BIGINT NOT NULL DEFAULT 0,
processedRecords BIGINT NOT NULL DEFAULT 0,
successRecords BIGINT NOT NULL DEFAULT 0,
failedRecords BIGINT NOT NULL DEFAULT 0,
stats JSON NOT NULL,
createdBy VARCHAR(256) NOT NULL,
createdAt BIGINT NOT NULL,
startedAt BIGINT DEFAULT NULL,
completedAt BIGINT DEFAULT NULL,
updatedAt BIGINT NOT NULL,
errorMessage LONGTEXT DEFAULT NULL,
registrationDeadline BIGINT DEFAULT NULL,
registeredServerCount INT DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_search_index_job_status_created_at (status, createdAt DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Retry queue for failed search-index writes.
CREATE TABLE IF NOT EXISTS search_index_retry_queue (
entityId VARCHAR(64) NOT NULL,
entityFqn VARCHAR(700) NOT NULL,
failureReason LONGTEXT DEFAULT NULL,
status VARCHAR(64) NOT NULL,
entityType VARCHAR(128) NOT NULL,
retryCount INT NOT NULL DEFAULT 0,
claimedAt TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (entityId, entityFqn),
KEY idx_search_index_retry_queue_status (status),
KEY idx_search_index_retry_queue_claimed_at (claimedAt)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ContextMemory entity - reusable Context Center memory.
CREATE TABLE IF NOT EXISTS context_memory (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') STORED NOT NULL,
nameHash VARCHAR(256) NOT NULL COLLATE ascii_bin,
json JSON NOT NULL,
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') STORED NOT NULL,
deleted BOOLEAN GENERATED ALWAYS AS (json -> '$.deleted') STORED,
PRIMARY KEY (id),
UNIQUE KEY unique_context_memory_name (nameHash),
INDEX idx_context_memory_updated_at (updatedAt)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- AI Governance Studio (Phase 4): framework + control entity tables.
CREATE TABLE IF NOT EXISTS ai_governance_framework_entity (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') NOT NULL,
fqnHash VARCHAR(768) NOT NULL,
json JSON NOT NULL,
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') NOT NULL,
impersonatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.impersonatedBy') VIRTUAL,
deleted BOOLEAN GENERATED ALWAYS AS (JSON_EXTRACT(json, '$.deleted')),
PRIMARY KEY (id),
UNIQUE KEY unique_name (fqnHash),
INDEX name_index (name),
INDEX deleted_index (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI Governance Framework entities';
CREATE TABLE IF NOT EXISTS ai_framework_control_entity (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') NOT NULL,
fqnHash VARCHAR(768) NOT NULL,
json JSON NOT NULL,
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') NOT NULL,
impersonatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.impersonatedBy') VIRTUAL,
deleted BOOLEAN GENERATED ALWAYS AS (JSON_EXTRACT(json, '$.deleted')),
PRIMARY KEY (id),
UNIQUE KEY unique_name (fqnHash),
INDEX name_index (name),
INDEX deleted_index (deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI Framework Control entities';
CREATE TABLE IF NOT EXISTS audit_report_entity (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') NOT NULL,
fqnHash VARCHAR(768) NOT NULL,
json JSON NOT NULL,
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') NOT NULL,
impersonatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.impersonatedBy') VIRTUAL,
deleted BOOLEAN GENERATED ALWAYS AS (JSON_EXTRACT(json, '$.deleted')),
status VARCHAR(32) GENERATED ALWAYS AS (json ->> '$.status') VIRTUAL,
requestSignature VARCHAR(512) GENERATED ALWAYS AS (json ->> '$.requestSignature') STORED,
PRIMARY KEY (id),
UNIQUE KEY unique_name (fqnHash),
INDEX name_index (name),
INDEX status_index (status),
INDEX deleted_index (deleted),
INDEX request_signature_index (requestSignature)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI Audit Report entities';
-- Database-backed user session store for multi-pod session management (issue #21971).
CREATE TABLE IF NOT EXISTS `user_session` (
`id` varchar(64) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.id'))) STORED NOT NULL,
`userId` varchar(36) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.userId'))) STORED,
`status` varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.status'))) STORED NOT NULL,
`expiresAt` bigint unsigned GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.expiresAt'))) STORED NOT NULL,
`idleExpiresAt` bigint unsigned GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.idleExpiresAt'))) STORED NOT NULL,
`updatedAt` bigint unsigned GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.updatedAt'))) STORED NOT NULL,
`sessionType` varchar(32) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.type'))) VIRTUAL,
`provider` varchar(64) GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.provider'))) VIRTUAL,
`version` bigint unsigned GENERATED ALWAYS AS (json_unquote(json_extract(`json`,_utf8mb4'$.version'))) VIRTUAL,
`lastAccessedAt` bigint unsigned GENERATED ALWAYS AS (nullif(json_unquote(json_extract(`json`,_utf8mb4'$.lastAccessedAt')),'null')) VIRTUAL,
`refreshLeaseUntil` bigint unsigned GENERATED ALWAYS AS (nullif(json_unquote(json_extract(`json`,_utf8mb4'$.refreshLeaseUntil')),'null')) VIRTUAL,
`json` json NOT NULL,
PRIMARY KEY (`id`),
KEY `user_session_user_status` (`userId`,`status`),
KEY `user_session_expiry` (`status`,`expiresAt`),
KEY `user_session_idle_expiry` (`status`,`idleExpiresAt`),
KEY `user_session_prune` (`status`,`updatedAt`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Per-entity `name` index for entity tables first created in 2.0.0, so the
-- distributed reindex's `... ORDER BY name, id LIMIT 1 OFFSET :n` cursor query
-- (EntityRepository.getCursorAtOffset) runs index-only instead of a filesort that
-- can exhaust sort memory (ER_OUT_OF_SORTMEMORY) on large tables. Added here rather
-- than in 1.13.1 because these tables are created above, in this same 2.0.0 migration.
CREATE INDEX task_entity_name_index ON task_entity (name);
CREATE INDEX announcement_entity_name_index ON announcement_entity (name);
CREATE INDEX drive_folder_name_index ON drive_folder (name);
CREATE INDEX asset_entity_name_index ON asset_entity (name);
-- context_file / context_memory are also created above in this migration and are reindexed;
-- they only had a `nameHash` unique key, so add the leading-`name` index the cursor query needs.
CREATE INDEX context_file_name_index ON context_file (name);
CREATE INDEX context_memory_name_index ON context_memory (name);
-- MCP conversation table for MCP Client message tracking
CREATE TABLE IF NOT EXISTS mcp_conversation (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
json JSON NOT NULL,
userId VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.user.id') NOT NULL,
createdAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.createdAt') NOT NULL,
updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') NOT NULL,
createdBy VARCHAR(50) GENERATED ALWAYS AS (json ->> '$.createdBy') NOT NULL,
updatedBy VARCHAR(50) GENERATED ALWAYS AS (json ->> '$.updatedBy') NOT NULL,
messageCount INT GENERATED ALWAYS AS (json ->> '$.messageCount') STORED,
PRIMARY KEY (id),
INDEX idx_mcp_conversation_user_updated (userId, updatedAt DESC)
);
-- MCP message table for MCP Client message tracking
CREATE TABLE IF NOT EXISTS mcp_message (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL,
json JSON NOT NULL,
conversationId VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.conversationId') STORED NOT NULL,
sender VARCHAR(10) GENERATED ALWAYS AS (json ->> '$.sender') STORED NOT NULL,
messageIndex INT GENERATED ALWAYS AS (json ->> '$.index') STORED,
timestamp BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.timestamp') NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_mcp_message_conversation FOREIGN KEY (conversationId) REFERENCES mcp_conversation(id) ON DELETE CASCADE,
INDEX idx_mcp_message_conversation_index (conversationId, messageIndex),
INDEX idx_mcp_message_conversation_created (conversationId, timestamp)
);
-- Task workflow cutover support - OpenMetadata 2.0.0 (moved from 2.0.1)
-- Maps legacy thread task IDs to new task entity IDs for migration traceability and redirects.
CREATE TABLE IF NOT EXISTS task_migration_mapping (
old_thread_id varchar(36) NOT NULL,
new_task_id varchar(36) NOT NULL,
migrated_at bigint NOT NULL,
source varchar(64) DEFAULT 'thread_task_migration',
PRIMARY KEY (old_thread_id),
KEY idx_task_migration_mapping_new_task_id (new_task_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -0,0 +1,248 @@
-- Post data migration script for Task System Redesign - OpenMetadata 2.0.0
-- This script runs after the data migration completes
-- =====================================================
-- NOTE: Suggestion migration (suggestions → task_entity),
-- thread-based task migration (thread_entity → task_entity),
-- and legacy system activity migration
-- (thread_entity generated feed rows → activity_stream)
-- are handled in Java MigrationUtil because they require
-- entity-link aware transformation logic.
-- =====================================================
-- =====================================================
-- PHASE 2D: Migrate announcements from thread_entity → announcement_entity
-- =====================================================
INSERT INTO announcement_entity (id, json, fqnhash)
SELECT
json->>'id' AS id,
jsonb_build_object(
'id', json->>'id',
'name', 'announcement-' || (json->>'id'),
'fullyQualifiedName', 'announcement-' || (json->>'id'),
'displayName', NULLIF(json->>'message', ''),
'description', COALESCE(
json->'announcement'->>'description',
json->>'message',
''
),
'entityLink', json->>'about',
'startTime', (json->'announcement'->>'startTime')::bigint,
'endTime', (json->'announcement'->>'endTime')::bigint,
'status', CASE
WHEN (json->'announcement'->>'endTime')::bigint < (extract(epoch from now()) * 1000)::bigint
THEN 'Expired'
WHEN (json->'announcement'->>'startTime')::bigint > (extract(epoch from now()) * 1000)::bigint
THEN 'Scheduled'
ELSE 'Active'
END,
'createdBy', json->>'createdBy',
'updatedBy', COALESCE(json->>'updatedBy', json->>'createdBy'),
'createdAt', (json->>'threadTs')::bigint,
'updatedAt', COALESCE((json->>'updatedAt')::bigint, (json->>'threadTs')::bigint),
'deleted', false,
'version', 0.1,
'reactions', COALESCE(json->'reactions', '[]'::jsonb)
) AS json,
md5('announcement-' || (json->>'id')) AS fqnhash
FROM thread_entity t
WHERE json->>'type' = 'Announcement'
AND NOT EXISTS (
SELECT 1 FROM announcement_entity a WHERE a.id = t.json->>'id'
)
ON CONFLICT (id) DO NOTHING;
-- =====================================================
-- PHASE 2E: Rename legacy thread storage to fail stale references
-- =====================================================
ALTER TABLE IF EXISTS thread_entity RENAME TO thread_entity_legacy;
-- =====================================================
-- PHASE 2F: Lower workflow trigger polling intervals
-- =====================================================
-- Reduce WorkflowEventConsumer poll interval from 10s to 1s.
-- The legacy 10s default added up to a 10s wait between an entity change and the
-- workflow-triggered approval task being created. On CI under resource pressure this
-- often drifted to >2 minutes when combined with Flowable's 60s async job poll. The
-- new value keeps the trigger pipeline near-real-time.
UPDATE event_subscription_entity
SET json = jsonb_set(json, '{pollInterval}', '1'::jsonb)
WHERE name = 'WorkflowEventConsumer'
AND (json->>'pollInterval')::int > 1;
-- Lower Flowable async/timer job acquisition intervals to keep workflow-driven
-- task creation responsive. The previous 60s default was a Flowable production setting
-- carried over verbatim; for OpenMetadata's interactive task UX we want sub-second pickup.
UPDATE openmetadata_settings
SET json = jsonb_set(
jsonb_set(json, '{executorConfiguration,asyncJobAcquisitionInterval}', '1000'::jsonb),
'{executorConfiguration,timerJobAcquisitionInterval}', '5000'::jsonb)
WHERE configtype = 'workflowSettings'
AND json->'executorConfiguration' IS NOT NULL
AND ((json->'executorConfiguration'->>'asyncJobAcquisitionInterval')::int > 1000
OR (json->'executorConfiguration'->>'timerJobAcquisitionInterval')::int > 5000);
-- pipelineStatuses is a derived field, read on demand from entity_extension_time_series, and is
-- now an array instead of a single object. Strip any stale single-object value that a GET->PUT
-- round-trip may have persisted into the stored entity JSON so it cannot break deserialization.
UPDATE ingestion_pipeline_entity
SET json = (json::jsonb #- '{pipelineStatuses}')::json
WHERE json::jsonb #> '{pipelineStatuses}' IS NOT NULL;
-- MCP Server and MCP Chat are no longer internal Applications; their enablement now lives in
-- platform settings (mcpConfiguration). The MCP Chat app was never shipped to customers, so
-- aiSettings.mcpChat keeps its seeded default shape (no config carry-over).
-- MCP Server: keep it disabled if the server app was not installed (mcpConfiguration defaults
-- to enabled=true, which would otherwise turn it on).
UPDATE openmetadata_settings
SET json = jsonb_set(json, '{enabled}', 'false'::jsonb)
WHERE configtype = 'mcpConfiguration'
AND NOT EXISTS (SELECT 1 FROM installed_apps ia WHERE ia.name = 'McpApplication');
-- Retire the MCP apps (their Java classes and marketplace seeds are removed). Keep the bot users
-- so the MCP server keeps its principal.
DELETE FROM entity_relationship er USING installed_apps ia
WHERE (er.fromId = ia.id OR er.toId = ia.id) AND ia.name IN ('McpApplication', 'McpChatApplication');
DELETE FROM entity_relationship er USING apps_marketplace ia
WHERE (er.fromId = ia.id OR er.toId = ia.id) AND ia.name IN ('McpApplication', 'McpChatApplication');
DELETE FROM installed_apps WHERE name IN ('McpApplication', 'McpChatApplication');
DELETE FROM apps_marketplace WHERE name IN ('McpApplication', 'McpChatApplication');
-- Post data migration script for Task workflow cutover - OpenMetadata 2.0.0 (moved from 2.0.1)
-- RdfIndexApp: switch to weekly Saturday cron and full-rebuild every run.
-- Previous defaults (daily, incremental) were producing unbounded triple growth
-- because relationship-removal paths weren't fully reconciled. With per-run
-- CLEAR ALL the dataset always converges to MySQL state; weekly cadence keeps
-- per-run cost from saturating Fuseki.
--
-- Also rewrite `entities` to `["all"]`. Pre-upgrade, an operator could have
-- narrowed RDF indexing to a subset of entity types; the new recreateIndex=true
-- semantics issues a CLEAR ALL before indexing, which would otherwise wipe
-- triples for entity types still in MySQL but missing from the subset list.
-- Forcing the subset list back to `["all"]` ensures the post-CLEAR-ALL run
-- repopulates the graph fully; operators can re-narrow after the migration if
-- they need partial indexing.
UPDATE installed_apps
SET json = jsonb_set(
jsonb_set(
jsonb_set(json::jsonb, '{appConfiguration,recreateIndex}', 'true'),
'{appSchedule,cronExpression}',
'"0 0 * * 6"'
),
'{appConfiguration,entities}',
'["all"]'::jsonb
)
WHERE name = 'RdfIndexApp';
UPDATE apps_marketplace
SET json = jsonb_set(json::jsonb, '{appConfiguration,recreateIndex}', 'true')
WHERE name = 'RdfIndexApp';
-- Backfill policyAgentConfig defaults on existing Snowflake services. The schema-level
-- defaults in snowflakeConnection.json only apply at create-time deserialization; rows
-- already persisted carry the previous all-false shape and won't pick up the new defaults
-- without this rewrite. Only fields that are currently false are flipped — any operator-set
-- true value is preserved.
UPDATE dbservice_entity
SET json = jsonb_set(
json::jsonb,
'{connection,config,policyAgentConfig,enabled}',
to_jsonb(true)
)
WHERE serviceType = 'Snowflake'
AND json::jsonb #> '{connection,config,policyAgentConfig,enabled}' = 'false'::jsonb;
UPDATE dbservice_entity
SET json = jsonb_set(
json::jsonb,
'{connection,config,policyAgentConfig,supportsFullAccess}',
to_jsonb(true)
)
WHERE serviceType = 'Snowflake'
AND json::jsonb #> '{connection,config,policyAgentConfig,supportsFullAccess}' = 'false'::jsonb;
UPDATE dbservice_entity
SET json = jsonb_set(
json::jsonb,
'{connection,config,policyAgentConfig,supportsMaskedAccess}',
to_jsonb(true)
)
WHERE serviceType = 'Snowflake'
AND json::jsonb #> '{connection,config,policyAgentConfig,supportsMaskedAccess}' = 'false'::jsonb;
-- Services that pre-date the policyAgentConfig field entirely (older rows where the whole
-- object is missing) — write the full block in one shot. `jsonb_set(..., true)` creates the
-- key if absent.
UPDATE dbservice_entity
SET json = jsonb_set(
json::jsonb,
'{connection,config,policyAgentConfig}',
'{"enabled":true,"supportsColumnAccess":false,"supportsFullAccess":true,"supportsMaskedAccess":true}'::jsonb,
true
)
WHERE serviceType = 'Snowflake'
AND json::jsonb #> '{connection,config,policyAgentConfig}' IS NULL;
-- Databricks: enabled/Full default to true, Column and Masked stay false.
-- Two guarded flips + one full-object write for legacy rows.
UPDATE dbservice_entity
SET json = jsonb_set(
json::jsonb,
'{connection,config,policyAgentConfig,enabled}',
to_jsonb(true)
)
WHERE serviceType = 'Databricks'
AND json::jsonb #> '{connection,config,policyAgentConfig,enabled}' = 'false'::jsonb;
UPDATE dbservice_entity
SET json = jsonb_set(
json::jsonb,
'{connection,config,policyAgentConfig,supportsFullAccess}',
to_jsonb(true)
)
WHERE serviceType = 'Databricks'
AND json::jsonb #> '{connection,config,policyAgentConfig,supportsFullAccess}' = 'false'::jsonb;
UPDATE dbservice_entity
SET json = jsonb_set(
json::jsonb,
'{connection,config,policyAgentConfig}',
'{"enabled":true,"supportsColumnAccess":false,"supportsFullAccess":true,"supportsMaskedAccess":false}'::jsonb,
true
)
WHERE serviceType = 'Databricks'
AND json::jsonb #> '{connection,config,policyAgentConfig}' IS NULL;
-- Corrective heal for instances that already ran the earlier version of this script.
-- Earlier the Databricks block forced supportsMaskedAccess to true; the intended
-- default is false. Reset it on every Databricks row that currently has it true.
UPDATE dbservice_entity
SET json = jsonb_set(
json::jsonb,
'{connection,config,policyAgentConfig,supportsMaskedAccess}',
to_jsonb(false)
)
WHERE serviceType = 'Databricks'
AND json::jsonb #> '{connection,config,policyAgentConfig,supportsMaskedAccess}' = 'true'::jsonb;
-- Postgres no longer declares policyAgentConfig. Earlier this script backfilled the
-- object onto Postgres rows; remove it so the stored shape matches the schema.
UPDATE dbservice_entity
SET json = (json::jsonb #- '{connection,config,policyAgentConfig}')
WHERE serviceType = 'Postgres'
AND json::jsonb #> '{connection,config,policyAgentConfig}' IS NOT NULL;
-- Remove runtime-only fields (openMetadataServerConnection, privateConfiguration) from stored application data.
UPDATE installed_apps
SET json = (json::jsonb - 'openMetadataServerConnection' - 'privateConfiguration')
WHERE jsonb_exists(json::jsonb, 'openMetadataServerConnection')
OR jsonb_exists(json::jsonb, 'privateConfiguration');
UPDATE entity_extension
SET json = (json::jsonb - 'openMetadataServerConnection' - 'privateConfiguration')
WHERE extension LIKE 'app.version.%'
AND (jsonb_exists(json::jsonb, 'openMetadataServerConnection')
OR jsonb_exists(json::jsonb, 'privateConfiguration'));
@@ -0,0 +1,471 @@
-- Task System Redesign - OpenMetadata 2.0.0
-- This migration creates the new Task entity tables and related infrastructure
-- CSV import/export and bulk-edit background job metadata.
ALTER TABLE background_jobs
ADD COLUMN IF NOT EXISTS progress integer DEFAULT 0,
ADD COLUMN IF NOT EXISTS total integer DEFAULT 0,
ADD COLUMN IF NOT EXISTS result text,
ADD COLUMN IF NOT EXISTS error text,
ADD COLUMN IF NOT EXISTS message character varying(2048),
ADD COLUMN IF NOT EXISTS cancelRequested boolean DEFAULT false,
ADD COLUMN IF NOT EXISTS completedAt bigint;
CREATE INDEX IF NOT EXISTS idx_background_jobs_job_type_created_by
ON background_jobs (jobType, createdBy, createdAt);
CREATE INDEX IF NOT EXISTS idx_background_jobs_status_updated_at
ON background_jobs (status, updatedAt);
CREATE TABLE IF NOT EXISTS background_job_logs (
logId character varying(36) NOT NULL,
jobId bigint NOT NULL,
createdAt bigint NOT NULL,
level character varying(16) NOT NULL,
message character varying(4096) NOT NULL,
PRIMARY KEY (logId),
CONSTRAINT fk_background_job_logs_job_id
FOREIGN KEY (jobId) REFERENCES background_jobs(id)
ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_background_job_logs_job_id_created_at
ON background_job_logs (jobId, createdAt);
CREATE TABLE IF NOT EXISTS task_entity (
id character varying(36) NOT NULL,
json jsonb NOT NULL,
fqnhash character varying(768) NOT NULL,
taskid character varying(20) GENERATED ALWAYS AS ((json ->> 'taskId'::text)) STORED NOT NULL,
name character varying(256) GENERATED ALWAYS AS ((json ->> 'name'::text)) STORED NOT NULL,
category character varying(32) GENERATED ALWAYS AS ((json ->> 'category'::text)) STORED NOT NULL,
type character varying(64) GENERATED ALWAYS AS ((json ->> 'type'::text)) STORED NOT NULL,
status character varying(32) GENERATED ALWAYS AS ((json ->> 'status'::text)) STORED NOT NULL,
priority character varying(16) GENERATED ALWAYS AS (COALESCE((json ->> 'priority'::text), 'Medium'::text)) STORED,
createdat bigint GENERATED ALWAYS AS (((json ->> 'createdAt'::text))::bigint) STORED NOT NULL,
updatedat bigint GENERATED ALWAYS AS (((json ->> 'updatedAt'::text))::bigint) STORED NOT NULL,
deleted boolean GENERATED ALWAYS AS (((json ->> 'deleted'::text))::boolean) STORED,
aboutfqnhash character varying(256) GENERATED ALWAYS AS ((json ->> 'aboutFqnHash'::text)) STORED,
createdbyid character varying(36) GENERATED ALWAYS AS ((json ->> 'createdById'::text)) STORED,
approvedbyid character varying(36) GENERATED ALWAYS AS ((json ->> 'approvedById'::text)) STORED,
PRIMARY KEY (id),
CONSTRAINT uk_task_fqn_hash UNIQUE (fqnhash)
);
CREATE INDEX IF NOT EXISTS idx_task_taskid ON task_entity (taskid);
CREATE INDEX IF NOT EXISTS idx_task_status ON task_entity (status);
CREATE INDEX IF NOT EXISTS idx_task_category ON task_entity (category);
CREATE INDEX IF NOT EXISTS idx_task_type ON task_entity (type);
CREATE INDEX IF NOT EXISTS idx_task_priority ON task_entity (priority);
CREATE INDEX IF NOT EXISTS idx_task_createdat ON task_entity (createdat);
CREATE INDEX IF NOT EXISTS idx_task_updatedat ON task_entity (updatedat);
CREATE INDEX IF NOT EXISTS idx_task_deleted ON task_entity (deleted);
CREATE INDEX IF NOT EXISTS idx_task_status_category ON task_entity (status, category);
CREATE INDEX IF NOT EXISTS idx_task_about_fqn_hash ON task_entity (aboutfqnhash);
CREATE INDEX IF NOT EXISTS idx_task_status_about ON task_entity (status, aboutfqnhash);
CREATE INDEX IF NOT EXISTS idx_task_created_by_id ON task_entity (createdbyid);
CREATE INDEX IF NOT EXISTS idx_task_created_by_category ON task_entity (createdbyid, category);
-- For 2.0.0 environments that ran the CREATE TABLE above before the
-- approvedbyid generated column was added inline, attach it now. CREATE TABLE
-- IF NOT EXISTS is a no-op on those environments so the column would never
-- appear otherwise. Postgres supports `ADD COLUMN IF NOT EXISTS` natively.
-- The ALTER must run before idx_task_approved_by_id is created — otherwise
-- existing-2.0.0 deployments would fail the CREATE INDEX with "column does
-- not exist" before the ADD COLUMN ever runs.
ALTER TABLE task_entity
ADD COLUMN IF NOT EXISTS approvedbyid character varying(36)
GENERATED ALWAYS AS ((json ->> 'approvedById'::text)) STORED;
CREATE INDEX IF NOT EXISTS idx_task_approved_by_id ON task_entity (approvedbyid);
CREATE TABLE IF NOT EXISTS new_task_sequence (
id bigint NOT NULL DEFAULT 0
);
INSERT INTO new_task_sequence (id) SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM new_task_sequence);
-- =====================================================
-- ACTIVITY STREAM TABLE (Partitioned by time)
-- Lightweight, ephemeral activity notifications
-- NOT for audit/compliance - use entity version history
-- Partitions are managed dynamically by ActivityStreamPartitionManager
-- =====================================================
CREATE TABLE IF NOT EXISTS activity_stream (
id character varying(36) NOT NULL,
eventtype character varying(64) NOT NULL,
entitytype character varying(64) NOT NULL,
entityid character varying(36) NOT NULL,
entityfqnhash character varying(768),
about character varying(2048),
aboutfqnhash character varying(768),
-- Nullable for system events and hard-deleted users; actorname is the display fallback.
actorid character varying(36),
actorname character varying(256),
timestamp bigint NOT NULL,
summary character varying(500),
fieldname character varying(256),
oldvalue text,
newvalue text,
domains jsonb,
json jsonb NOT NULL,
PRIMARY KEY (id, timestamp)
) PARTITION BY RANGE (timestamp);
-- Default partition catches all data until monthly partitions are created
-- ActivityStreamPartitionManager will create monthly partitions and detach old ones
CREATE TABLE IF NOT EXISTS activity_stream_default PARTITION OF activity_stream DEFAULT;
-- Indexes for activity stream (created on parent, inherited by partitions)
CREATE INDEX IF NOT EXISTS idx_activity_timestamp ON activity_stream (timestamp);
CREATE INDEX IF NOT EXISTS idx_activity_entity ON activity_stream (entitytype, entityid, timestamp);
CREATE INDEX IF NOT EXISTS idx_activity_actor ON activity_stream (actorid, timestamp);
CREATE INDEX IF NOT EXISTS idx_activity_event_type ON activity_stream (eventtype, timestamp);
CREATE INDEX IF NOT EXISTS idx_activity_entity_fqn ON activity_stream (entityfqnhash, timestamp);
CREATE INDEX IF NOT EXISTS idx_activity_about ON activity_stream (aboutfqnhash, timestamp);
-- Activity stream configuration per domain
CREATE TABLE IF NOT EXISTS activity_stream_config (
id character varying(36) NOT NULL,
json jsonb NOT NULL,
scope character varying(32) GENERATED ALWAYS AS ((json ->> 'scope'::text)) STORED NOT NULL,
domainid character varying(36) GENERATED ALWAYS AS ((json -> 'scopeReference' ->> 'id'::text)) STORED,
enabled boolean GENERATED ALWAYS AS (((json ->> 'enabled'::text))::boolean) STORED,
retentiondays integer GENERATED ALWAYS AS (((json ->> 'retentionDays'::text))::integer) STORED,
PRIMARY KEY (id),
CONSTRAINT uk_activity_domain_config UNIQUE (domainid)
);
CREATE INDEX IF NOT EXISTS idx_activity_config_scope ON activity_stream_config (scope);
CREATE INDEX IF NOT EXISTS idx_activity_config_enabled ON activity_stream_config (enabled);
-- =====================================================
-- ANNOUNCEMENT ENTITY TABLE
-- Standalone entity for asset announcements (migrated from thread_entity)
-- =====================================================
CREATE TABLE IF NOT EXISTS announcement_entity (
id character varying(36) NOT NULL,
json jsonb NOT NULL,
fqnhash character varying(768) NOT NULL,
name character varying(256) GENERATED ALWAYS AS ((json ->> 'name'::text)) STORED NOT NULL,
entitylink character varying(512) GENERATED ALWAYS AS ((json ->> 'entityLink'::text)) STORED,
status character varying(32) GENERATED ALWAYS AS ((json ->> 'status'::text)) STORED,
starttime bigint GENERATED ALWAYS AS (((json ->> 'startTime'::text))::bigint) STORED,
endtime bigint GENERATED ALWAYS AS (((json ->> 'endTime'::text))::bigint) STORED,
createdby character varying(256) GENERATED ALWAYS AS ((json ->> 'createdBy'::text)) STORED,
createdat bigint GENERATED ALWAYS AS (((json ->> 'createdAt'::text))::bigint) STORED,
updatedat bigint GENERATED ALWAYS AS (((json ->> 'updatedAt'::text))::bigint) STORED,
deleted boolean GENERATED ALWAYS AS (((json ->> 'deleted'::text))::boolean) STORED,
PRIMARY KEY (id),
CONSTRAINT uk_announcement_fqn_hash UNIQUE (fqnhash)
);
CREATE INDEX IF NOT EXISTS idx_announcement_status ON announcement_entity (status);
CREATE INDEX IF NOT EXISTS idx_announcement_entitylink ON announcement_entity (entitylink);
CREATE INDEX IF NOT EXISTS idx_announcement_starttime ON announcement_entity (starttime);
CREATE INDEX IF NOT EXISTS idx_announcement_endtime ON announcement_entity (endtime);
CREATE INDEX IF NOT EXISTS idx_announcement_deleted ON announcement_entity (deleted);
-- =====================================================
-- TASK FORM SCHEMA ENTITY TABLE
-- Stores form schemas for different task types
-- =====================================================
CREATE TABLE IF NOT EXISTS task_form_schema_entity (
id character varying(36) NOT NULL,
json jsonb NOT NULL,
fqnhash character varying(768) NOT NULL,
name character varying(256) GENERATED ALWAYS AS ((json ->> 'name'::text)) STORED NOT NULL,
tasktype character varying(64) GENERATED ALWAYS AS ((json ->> 'taskType'::text)) STORED,
taskcategory character varying(32) GENERATED ALWAYS AS ((json ->> 'taskCategory'::text)) STORED,
updatedat bigint GENERATED ALWAYS AS (((json ->> 'updatedAt'::text))::bigint) STORED,
deleted boolean GENERATED ALWAYS AS (((json ->> 'deleted'::text))::boolean) STORED,
PRIMARY KEY (id),
CONSTRAINT uk_task_form_schema_fqn_hash UNIQUE (fqnhash)
);
CREATE INDEX IF NOT EXISTS idx_task_form_schema_name ON task_form_schema_entity (name);
CREATE INDEX IF NOT EXISTS idx_task_form_schema_tasktype ON task_form_schema_entity (tasktype);
CREATE INDEX IF NOT EXISTS idx_task_form_schema_deleted ON task_form_schema_entity (deleted);
-- =====================================================
-- KNOWLEDGE CENTER + CONTEXT CENTER DRIVE (Collate → OM port)
-- Appended below the Task Redesign tables to preserve main's
-- migration order when merging.
-- =====================================================
-- MCP tables are created in 1.13.0 migration.
-- Knowledge Center: page entity table (Article, QuickLink).
-- Existing Collate customers already have this table from 1.2.0-collate with
-- subsequent shape changes through 1.6.0-collate (nameHash -> fqnHash VARCHAR(756),
-- pageType generated column, composite deleted index). CREATE TABLE IF NOT EXISTS
-- is a no-op for them and creates the final shape for fresh OpenMetadata installs.
CREATE TABLE IF NOT EXISTS knowledge_center (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL,
fqnHash VARCHAR(756) NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> 'name') STORED NOT NULL,
json JSONB NOT NULL,
updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL,
deleted BOOLEAN GENERATED ALWAYS AS (COALESCE((json ->> 'deleted')::boolean, false)) STORED,
pageType VARCHAR(16) GENERATED ALWAYS AS (json ->> 'pageType') STORED NOT NULL,
PRIMARY KEY (id),
UNIQUE (fqnHash)
);
CREATE INDEX IF NOT EXISTS knowledge_center_name_index ON knowledge_center (name);
CREATE INDEX IF NOT EXISTS index_knowledge_center_deleted ON knowledge_center (fqnHash, deleted);
-- Context Center Drive: Folder entity table.
CREATE TABLE IF NOT EXISTS drive_folder (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> 'name') STORED NOT NULL,
nameHash VARCHAR(256) NOT NULL,
json JSONB NOT NULL,
updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL,
deleted BOOLEAN GENERATED ALWAYS AS (COALESCE((json ->> 'deleted')::boolean, false)) STORED,
PRIMARY KEY (id),
UNIQUE (nameHash)
);
CREATE INDEX IF NOT EXISTS idx_drive_folder_updated_at ON drive_folder (updatedAt);
-- Context Center Drive: File entity table (uploaded PDF/image/spreadsheet/office docs).
CREATE TABLE IF NOT EXISTS context_file (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> 'name') STORED NOT NULL,
nameHash VARCHAR(256) NOT NULL,
json JSONB NOT NULL,
updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL,
deleted BOOLEAN GENERATED ALWAYS AS (COALESCE((json ->> 'deleted')::boolean, false)) STORED,
PRIMARY KEY (id),
UNIQUE (nameHash)
);
CREATE INDEX IF NOT EXISTS idx_context_file_updated_at ON context_file (updatedAt);
-- Attachments: Asset entity table for uploaded file blobs referenced by ContextFiles, Pages, etc.
-- Existing Collate customers have this from 1.7.0-collate. CREATE TABLE IF NOT EXISTS is a no-op for them.
CREATE TABLE IF NOT EXISTS asset_entity (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> 'fileName') STORED NOT NULL,
url VARCHAR(1024) GENERATED ALWAYS AS (json ->> 'url') STORED NOT NULL,
fullyQualifiedName VARCHAR(256) GENERATED ALWAYS AS (json ->> 'fullyQualifiedName') STORED NOT NULL,
assetType VARCHAR(100) GENERATED ALWAYS AS (json ->> 'assetType') STORED NOT NULL,
json JSONB NOT NULL,
updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL,
fqnHash VARCHAR(768) NOT NULL,
deleted BOOLEAN GENERATED ALWAYS AS (COALESCE(CAST(json ->> 'deleted' AS BOOLEAN), false)) STORED,
PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS fqnhash_index ON asset_entity (fqnHash);
CREATE INDEX IF NOT EXISTS asset_type_index ON asset_entity (assetType);
CREATE INDEX IF NOT EXISTS idx_asset_deleted ON asset_entity (deleted);
-- Context Center Drive: File content snapshot table (revisions, extracted text).
CREATE TABLE IF NOT EXISTS context_file_content (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> 'name') STORED NOT NULL,
nameHash VARCHAR(256) NOT NULL,
json JSONB NOT NULL,
updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL,
deleted BOOLEAN GENERATED ALWAYS AS (COALESCE((json ->> 'deleted')::boolean, false)) STORED,
PRIMARY KEY (id),
UNIQUE (nameHash)
);
CREATE INDEX IF NOT EXISTS idx_context_file_content_updated_at ON context_file_content (updatedAt);
-- Add tag_usage.metadata column if missing (newer tag usage payloads carry metadata).
ALTER TABLE IF EXISTS tag_usage
ADD COLUMN IF NOT EXISTS metadata JSONB;
-- Add audit_log_event.search_text column if missing (searchable audit log text).
ALTER TABLE IF EXISTS audit_log_event
ADD COLUMN IF NOT EXISTS search_text TEXT;
-- Distributed reindex job tracking.
CREATE TABLE IF NOT EXISTS search_index_job (
id VARCHAR(64) PRIMARY KEY,
status VARCHAR(64) NOT NULL,
jobConfiguration JSONB NOT NULL,
targetIndexPrefix VARCHAR(256) NOT NULL,
stagedIndexMapping JSONB NULL,
totalRecords BIGINT NOT NULL DEFAULT 0,
processedRecords BIGINT NOT NULL DEFAULT 0,
successRecords BIGINT NOT NULL DEFAULT 0,
failedRecords BIGINT NOT NULL DEFAULT 0,
stats JSONB NOT NULL DEFAULT '{}'::jsonb,
createdBy VARCHAR(256) NOT NULL,
createdAt BIGINT NOT NULL,
startedAt BIGINT NULL,
completedAt BIGINT NULL,
updatedAt BIGINT NOT NULL,
errorMessage TEXT NULL,
registrationDeadline BIGINT NULL,
registeredServerCount INTEGER NULL
);
CREATE INDEX IF NOT EXISTS idx_search_index_job_status_created_at
ON search_index_job (status, createdAt DESC);
-- Retry queue for failed search-index writes.
CREATE TABLE IF NOT EXISTS search_index_retry_queue (
entityId VARCHAR(64) NOT NULL,
entityFqn VARCHAR(768) NOT NULL,
failureReason TEXT NULL,
status VARCHAR(64) NOT NULL,
entityType VARCHAR(128) NOT NULL,
retryCount INTEGER NOT NULL DEFAULT 0,
claimedAt TIMESTAMP NULL,
PRIMARY KEY (entityId, entityFqn)
);
CREATE INDEX IF NOT EXISTS idx_search_index_retry_queue_status
ON search_index_retry_queue (status);
CREATE INDEX IF NOT EXISTS idx_search_index_retry_queue_claimed_at
ON search_index_retry_queue (claimedAt);
-- ContextMemory entity - reusable Context Center memory.
CREATE TABLE IF NOT EXISTS context_memory (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json ->> 'name') STORED NOT NULL,
nameHash VARCHAR(256) NOT NULL,
json JSONB NOT NULL,
updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL,
deleted BOOLEAN GENERATED ALWAYS AS ((json ->> 'deleted')::boolean) STORED,
PRIMARY KEY (id),
UNIQUE (nameHash)
);
CREATE INDEX IF NOT EXISTS idx_context_memory_updated_at ON context_memory (updatedAt);
-- AI Governance Studio (Phase 4): framework + control entity tables.
CREATE TABLE IF NOT EXISTS ai_governance_framework_entity (
id VARCHAR(36) GENERATED ALWAYS AS (json->>'id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json->>'name') STORED NOT NULL,
fqnHash VARCHAR(768) NOT NULL,
json JSONB NOT NULL,
updatedAt BIGINT GENERATED ALWAYS AS ((json->>'updatedAt')::bigint) STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json->>'updatedBy') STORED NOT NULL,
impersonatedBy VARCHAR(256) GENERATED ALWAYS AS (json->>'impersonatedBy') STORED,
deleted BOOLEAN GENERATED ALWAYS AS ((json->>'deleted')::boolean) STORED,
PRIMARY KEY (id),
UNIQUE (fqnHash)
);
CREATE INDEX IF NOT EXISTS ai_governance_framework_name_index ON ai_governance_framework_entity(name);
CREATE INDEX IF NOT EXISTS ai_governance_framework_deleted_index ON ai_governance_framework_entity(deleted);
COMMENT ON TABLE ai_governance_framework_entity IS 'AI Governance Framework entities';
CREATE TABLE IF NOT EXISTS ai_framework_control_entity (
id VARCHAR(36) GENERATED ALWAYS AS (json->>'id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json->>'name') STORED NOT NULL,
fqnHash VARCHAR(768) NOT NULL,
json JSONB NOT NULL,
updatedAt BIGINT GENERATED ALWAYS AS ((json->>'updatedAt')::bigint) STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json->>'updatedBy') STORED NOT NULL,
impersonatedBy VARCHAR(256) GENERATED ALWAYS AS (json->>'impersonatedBy') STORED,
deleted BOOLEAN GENERATED ALWAYS AS ((json->>'deleted')::boolean) STORED,
PRIMARY KEY (id),
UNIQUE (fqnHash)
);
CREATE INDEX IF NOT EXISTS ai_framework_control_name_index ON ai_framework_control_entity(name);
CREATE INDEX IF NOT EXISTS ai_framework_control_deleted_index ON ai_framework_control_entity(deleted);
COMMENT ON TABLE ai_framework_control_entity IS 'AI Framework Control entities';
CREATE TABLE IF NOT EXISTS audit_report_entity (
id VARCHAR(36) GENERATED ALWAYS AS (json->>'id') STORED NOT NULL,
name VARCHAR(256) GENERATED ALWAYS AS (json->>'name') STORED NOT NULL,
fqnHash VARCHAR(768) NOT NULL,
json JSONB NOT NULL,
updatedAt BIGINT GENERATED ALWAYS AS ((json->>'updatedAt')::bigint) STORED NOT NULL,
updatedBy VARCHAR(256) GENERATED ALWAYS AS (json->>'updatedBy') STORED NOT NULL,
impersonatedBy VARCHAR(256) GENERATED ALWAYS AS (json->>'impersonatedBy') STORED,
deleted BOOLEAN GENERATED ALWAYS AS ((json->>'deleted')::boolean) STORED,
status VARCHAR(32) GENERATED ALWAYS AS (json->>'status') STORED,
requestSignature VARCHAR(512) GENERATED ALWAYS AS (json->>'requestSignature') STORED,
PRIMARY KEY (id),
UNIQUE (fqnHash)
);
CREATE INDEX IF NOT EXISTS audit_report_name_index ON audit_report_entity(name);
CREATE INDEX IF NOT EXISTS audit_report_status_index ON audit_report_entity(status);
CREATE INDEX IF NOT EXISTS audit_report_deleted_index ON audit_report_entity(deleted);
CREATE INDEX IF NOT EXISTS audit_report_request_signature_index ON audit_report_entity(requestSignature);
COMMENT ON TABLE audit_report_entity IS 'AI Audit Report entities';
-- Database-backed user session store for multi-pod session management (issue #21971).
CREATE TABLE IF NOT EXISTS user_session (
id character varying(64) GENERATED ALWAYS AS ((json ->> 'id'::text)) STORED NOT NULL,
userid character varying(36) GENERATED ALWAYS AS ((json ->> 'userId'::text)) STORED,
status character varying(32) GENERATED ALWAYS AS ((json ->> 'status'::text)) STORED NOT NULL,
expiresat bigint GENERATED ALWAYS AS (((json ->> 'expiresAt'::text))::bigint) STORED NOT NULL,
idleexpiresat bigint GENERATED ALWAYS AS (((json ->> 'idleExpiresAt'::text))::bigint) STORED NOT NULL,
updatedat bigint GENERATED ALWAYS AS (((json ->> 'updatedAt'::text))::bigint) STORED NOT NULL,
sessiontype character varying(32) GENERATED ALWAYS AS ((json ->> 'type'::text)) STORED,
provider character varying(64) GENERATED ALWAYS AS ((json ->> 'provider'::text)) STORED,
version bigint GENERATED ALWAYS AS (((json ->> 'version'::text))::bigint) STORED,
lastaccessedat bigint GENERATED ALWAYS AS (((json ->> 'lastAccessedAt'::text))::bigint) STORED,
refreshleaseuntil bigint GENERATED ALWAYS AS (((json ->> 'refreshLeaseUntil'::text))::bigint) STORED,
json jsonb NOT NULL,
CONSTRAINT user_session_pkey PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS user_session_user_status_idx ON user_session USING btree (userid, status);
CREATE INDEX IF NOT EXISTS user_session_expiry_idx ON user_session USING btree (status, expiresat);
CREATE INDEX IF NOT EXISTS user_session_idle_expiry_idx ON user_session USING btree (status, idleexpiresat);
CREATE INDEX IF NOT EXISTS user_session_prune_idx ON user_session USING btree (status, updatedat);
-- Per-entity `name` index for entity tables first created in 2.0.0, so the distributed
-- reindex's `... ORDER BY name, id LIMIT 1 OFFSET :n` cursor query
-- (EntityRepository.getCursorAtOffset) runs index-only instead of a sort that can exhaust
-- work_mem on large tables. Added here (not 1.13.1) because these tables are created above
-- in this same 2.0.0 migration. Idempotent via IF NOT EXISTS.
CREATE INDEX IF NOT EXISTS task_entity_name_index ON task_entity (name);
CREATE INDEX IF NOT EXISTS announcement_entity_name_index ON announcement_entity (name);
CREATE INDEX IF NOT EXISTS drive_folder_name_index ON drive_folder (name);
CREATE INDEX IF NOT EXISTS asset_entity_name_index ON asset_entity (name);
-- context_file / context_memory are also created above in this migration and are reindexed;
-- they only had a `nameHash` unique key, so add the leading-`name` index the cursor query needs.
CREATE INDEX IF NOT EXISTS context_file_name_index ON context_file (name);
CREATE INDEX IF NOT EXISTS context_memory_name_index ON context_memory (name);
-- MCP conversation table for MCP Client message tracking
CREATE TABLE IF NOT EXISTS mcp_conversation (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL,
json JSONB NOT NULL,
userId VARCHAR(256) GENERATED ALWAYS AS ((json -> 'user') ->> 'id') STORED NOT NULL,
createdAt BIGINT GENERATED ALWAYS AS ((json ->> 'createdAt')::bigint) STORED NOT NULL,
updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL,
createdBy VARCHAR(50) GENERATED ALWAYS AS (json ->> 'createdBy') STORED NOT NULL,
updatedBy VARCHAR(50) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL,
messageCount INT GENERATED ALWAYS AS ((json ->> 'messageCount')::int) STORED,
PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS idx_mcp_conversation_user_updated ON mcp_conversation (userId, updatedAt DESC);
-- MCP message table for MCP Client message tracking
CREATE TABLE IF NOT EXISTS mcp_message (
id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL,
json JSONB NOT NULL,
conversationId VARCHAR(36) GENERATED ALWAYS AS (json ->> 'conversationId') STORED NOT NULL,
sender VARCHAR(10) GENERATED ALWAYS AS (json ->> 'sender') STORED NOT NULL,
messageIndex INT GENERATED ALWAYS AS ((json ->> 'index')::int) STORED,
timestamp BIGINT GENERATED ALWAYS AS ((json ->> 'timestamp')::bigint) STORED NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_mcp_message_conversation FOREIGN KEY (conversationId) REFERENCES mcp_conversation(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_mcp_message_conversation_index ON mcp_message (conversationId, messageIndex);
CREATE INDEX IF NOT EXISTS idx_mcp_message_conversation_created ON mcp_message (conversationId, timestamp);
-- Task workflow cutover support - OpenMetadata 2.0.0 (moved from 2.0.1)
-- Maps legacy thread task IDs to new task entity IDs for migration traceability and redirects.
CREATE TABLE IF NOT EXISTS task_migration_mapping (
old_thread_id character varying(36) NOT NULL,
new_task_id character varying(36) NOT NULL,
migrated_at bigint NOT NULL,
source character varying(64) DEFAULT 'thread_task_migration',
PRIMARY KEY (old_thread_id)
);
CREATE INDEX IF NOT EXISTS idx_task_migration_mapping_new_task_id
ON task_migration_mapping (new_task_id);