feat: add verified manual pipeline migration for plugin runners

This commit is contained in:
RockChinQ
2026-09-16 18:13:11 +00:00
parent 273b1ea3cf
commit 8c119bc4b6
97 changed files with 16662 additions and 123 deletions
@@ -0,0 +1,53 @@
"""Create the manual migration journal only; never convert pipeline JSON."""
from alembic import op
import sqlalchemy as sa
revision = '0027_pipeline_migration'
down_revision = '0026_merge_master_beta'
branch_labels = None
depends_on = None
def upgrade():
bind = op.get_bind()
table = 'pipeline_migration_snapshots'
if not sa.inspect(bind).has_table(table):
op.create_table(
table,
sa.Column('uuid', sa.String(36), primary_key=True),
sa.Column(
'workspace_uuid', sa.String(36), sa.ForeignKey('workspaces.uuid', ondelete='CASCADE'), nullable=False
),
sa.Column('pipeline_uuid', sa.String(255), nullable=False),
sa.Column('source_fingerprint', sa.String(64), nullable=False),
sa.Column('target_fingerprint', sa.String(64), nullable=False),
sa.Column('planner_version', sa.String(64), nullable=False),
sa.Column('source_snapshot', sa.JSON(), nullable=False),
sa.Column('state', sa.String(32), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.ForeignKeyConstraint(
['workspace_uuid', 'pipeline_uuid'],
['legacy_pipelines.workspace_uuid', 'legacy_pipelines.uuid'],
name='fk_pipeline_migration_workspace_pipeline',
ondelete='CASCADE',
),
sa.UniqueConstraint(
'workspace_uuid', 'pipeline_uuid', 'source_fingerprint', name='uq_pipeline_migration_source'
),
sa.CheckConstraint("state IN ('activation_pending', 'active')", name='ck_pipeline_migration_state'),
)
op.create_index('ix_pipeline_migration_workspace_pipeline', table, ['workspace_uuid', 'pipeline_uuid'])
if bind.dialect.name == 'postgresql':
op.execute(f'ALTER TABLE {table} ENABLE ROW LEVEL SECURITY')
op.execute(f'ALTER TABLE {table} FORCE ROW LEVEL SECURITY')
op.execute(f'DROP POLICY IF EXISTS langbot_workspace_isolation ON {table}')
expression = "workspace_uuid = NULLIF(current_setting('langbot.workspace_uuid', true), '')"
op.execute(
f'CREATE POLICY langbot_workspace_isolation ON {table} FOR ALL TO PUBLIC '
f'USING ({expression}) WITH CHECK ({expression})'
)
def downgrade():
op.drop_table('pipeline_migration_snapshots')
+1
View File
@@ -71,6 +71,7 @@ _ALEMBIC_TENANT_TABLES = {
'rerank_models',
'legacy_pipelines',
'pipeline_run_records',
'pipeline_migration_snapshots',
'plugin_settings',
'knowledge_bases',
'knowledge_base_files',
@@ -0,0 +1,27 @@
"""Transaction-held, tenant/pipeline-scoped admission for form producers.
Every pending producer must hold this row until its INSERT commits. Migration
holds the same row for both CAS and activation. No table/process/advisory lock.
The no-op UPDATE also acquires SQLite's write reservation before any reads.
"""
import sqlalchemy as sa
from ..entity.persistence.pipeline import LegacyPipeline
async def lock_pipeline_admission(session, workspace_uuid: str, pipeline_uuid: str):
if not workspace_uuid or not pipeline_uuid:
raise ValueError('Pipeline admission requires exact ownership')
t = LegacyPipeline.__table__
result = await session.execute(
sa.update(t)
.where(t.c.workspace_uuid == workspace_uuid, t.c.uuid == pipeline_uuid)
.values(updated_at=t.c.updated_at)
)
if result.rowcount != 1:
raise ValueError('Pipeline admission authority unavailable')
return (
await session.execute(
sa.select(t.c.config).where(t.c.workspace_uuid == workspace_uuid, t.c.uuid == pipeline_uuid)
)
).scalar_one()
+15 -2
View File
@@ -58,6 +58,7 @@ TENANT_TABLE_COLUMNS: dict[str, str] = {
'rerank_models': 'workspace_uuid',
'legacy_pipelines': 'workspace_uuid',
'pipeline_run_records': 'workspace_uuid',
'pipeline_migration_snapshots': 'workspace_uuid',
'plugin_settings': 'workspace_uuid',
'knowledge_bases': 'workspace_uuid',
'knowledge_base_files': 'workspace_uuid',
@@ -467,9 +468,21 @@ def _validate_scoped_statement_call(args: tuple[typing.Any, ...], kwargs: dict[s
raise ScopedSessionTransactionError('TenantUnitOfWork does not allow literal-execute SQL parameters')
if isinstance(element, sqlalchemy.sql.elements.Cast) and type(element.type) not in {Vector, HALFVEC}:
raise ScopedSessionTransactionError(
'TenantUnitOfWork only allows the trusted pgvector cast used by tenant vector search'
# Manual Pipeline migration compares DB-native JSON text, because
# PostgreSQL JSON has no equality operator. Permit only these fixed
# mapped columns and a built-in Text target, never arbitrary casts.
source = element.clause
pipeline_json_cas = (
type(element.type) is sqlalchemy.Text
and isinstance(source, sqlalchemy.Column)
and type(source.type) is sqlalchemy.JSON
and getattr(getattr(source, 'table', None), 'name', None) == 'legacy_pipelines'
and source.name in {'config', 'stages', 'extensions_preferences'}
)
if not pipeline_json_cas:
raise ScopedSessionTransactionError(
'TenantUnitOfWork only allows trusted pgvector and Pipeline JSON CAS casts'
)
if isinstance(element, sqlalchemy.sql.functions.FunctionElement):
function_name = str(getattr(element, 'name', '')).casefold()