From 8e43ad4912aad71ad3b323c1717da94a7f0e7f20 Mon Sep 17 00:00:00 2001 From: YueGuobin Date: Tue, 3 Mar 2026 18:03:16 +0800 Subject: [PATCH] feat(db): replace PostgreSQL-specific JSONB with cross-database JSON type - Change `JSONB` columns to generic `JSON` in LLMModelConfig model for database compatibility - Update migration to use `sa.String(32)` for UUID and `sa.JSON()` for config fields - Remove PostgreSQL dialect imports to support multiple database backends - Keep existing constraints and indexes with PostgreSQL-specific annotations where needed --- gns3server/db/models/llm_model_configs.py | 16 ++--- ...20260303_create_llm_model_configs_table.py | 62 +++++++++++-------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/gns3server/db/models/llm_model_configs.py b/gns3server/db/models/llm_model_configs.py index a7e5fe8ad..eaddaf758 100644 --- a/gns3server/db/models/llm_model_configs.py +++ b/gns3server/db/models/llm_model_configs.py @@ -15,8 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from sqlalchemy import Column, Boolean, ForeignKey, CheckConstraint, Index, Integer, String, text -from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy import Column, Boolean, ForeignKey, CheckConstraint, Index, Integer, String, text, JSON from sqlalchemy.orm import relationship from .base import BaseTable, generate_uuid, GUID @@ -37,16 +36,16 @@ class LLMModelConfig(BaseTable): config_id = Column(GUID, primary_key=True, default=generate_uuid) name = Column(String(100), nullable=False) # Configuration name (table-level for indexing) model_type = Column(String(50), nullable=False) # Model type: text, vision, stt, tts, multimodal, etc. - config = Column(JSONB, nullable=False) # Config fields: provider, base_url, model, temperature, api_key, etc. + config = Column(JSON, nullable=False) # Config fields: provider, base_url, model, temperature, api_key, etc. user_id = Column(GUID, ForeignKey("users.user_id", ondelete="CASCADE"), nullable=True) group_id = Column(GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE"), nullable=True) is_default = Column(Boolean, default=False, nullable=False) version = Column(Integer, default=0, nullable=False) # Optimistic locking version # Reserved fields for future use (currently unused in code) - reserved_jsonb_1 = Column(JSONB, nullable=True) - reserved_jsonb_2 = Column(JSONB, nullable=True) - reserved_jsonb_3 = Column(JSONB, nullable=True) + reserved_jsonb_1 = Column(JSON, nullable=True) + reserved_jsonb_2 = Column(JSON, nullable=True) + reserved_jsonb_3 = Column(JSON, nullable=True) # Relationships user = relationship("User", backref="llm_model_configs") @@ -65,16 +64,17 @@ class LLMModelConfig(BaseTable): "model_type IN ('text', 'vision', 'stt', 'tts', 'multimodal', 'embedding', 'reranking', 'other')", name="valid_model_type_check" ), - # Each user can have at most one default config (partial unique index) + # Each user can have at most one default config (partial unique index, PostgreSQL only) Index("unique_user_default", "user_id", unique=True, postgresql_where=text("is_default = TRUE AND user_id IS NOT NULL")), - # Each group can have at most one default config (partial unique index) + # Each group can have at most one default config (partial unique index, PostgreSQL only) Index("unique_group_default", "group_id", unique=True, postgresql_where=text("is_default = TRUE AND group_id IS NOT NULL")), # Indexes for efficient queries Index("idx_llm_model_configs_user_id", "user_id"), Index("idx_llm_model_configs_group_id", "group_id"), Index("idx_llm_model_configs_model_type", "model_type"), + # GIN index for JSON config (PostgreSQL only) Index("idx_llm_model_configs_config", "config", postgresql_using="gin"), ) diff --git a/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py b/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py index 7f6a822be..e3b812a52 100644 --- a/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py +++ b/gns3server/db_migrations/versions/20260303_create_llm_model_configs_table.py @@ -7,7 +7,6 @@ Create Date: 2026-03-03 """ from alembic import op import sqlalchemy as sa -from sqlalchemy.dialects import postgresql from sqlalchemy import inspect # revision identifiers, used by Alembic. @@ -18,9 +17,10 @@ depends_on = None def upgrade() -> None: - # Get the current connection + # Get the current connection and dialect conn = op.get_bind() inspector = inspect(conn) + dialect_name = conn.dialect.name # Check if table already exists (idempotent for databases created from code) tables = inspector.get_table_names() @@ -29,22 +29,22 @@ def upgrade() -> None: # Table already exists from Base.metadata.create_all, skip creation return - # Create llm_model_configs table + # Create llm_model_configs table with cross-database compatible types op.create_table( 'llm_model_configs', - sa.Column('config_id', postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column('config_id', sa.String(32), primary_key=True), sa.Column('name', sa.String(100), nullable=False), sa.Column('model_type', sa.String(50), nullable=False), - sa.Column('config', postgresql.JSONB(), nullable=False), - sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.user_id', ondelete='CASCADE'), nullable=True), - sa.Column('group_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('user_groups.user_group_id', ondelete='CASCADE'), nullable=True), + sa.Column('config', sa.JSON(), nullable=False), + sa.Column('user_id', sa.String(32), sa.ForeignKey('users.user_id', ondelete='CASCADE'), nullable=True), + sa.Column('group_id', sa.String(32), sa.ForeignKey('user_groups.user_group_id', ondelete='CASCADE'), nullable=True), sa.Column('is_default', sa.Boolean(), default=False, nullable=False), sa.Column('version', sa.Integer(), nullable=False, server_default='0'), sa.Column('created_at', sa.DateTime(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=True), - sa.Column('reserved_jsonb_1', postgresql.JSONB(), nullable=True, comment='Reserved field for future use'), - sa.Column('reserved_jsonb_2', postgresql.JSONB(), nullable=True, comment='Reserved field for future use'), - sa.Column('reserved_jsonb_3', postgresql.JSONB(), nullable=True, comment='Reserved field for future use'), + sa.Column('reserved_jsonb_1', sa.JSON(), nullable=True, comment='Reserved field for future use'), + sa.Column('reserved_jsonb_2', sa.JSON(), nullable=True, comment='Reserved field for future use'), + sa.Column('reserved_jsonb_3', sa.JSON(), nullable=True, comment='Reserved field for future use'), sa.CheckConstraint( "(user_id IS NOT NULL AND group_id IS NULL) OR " "(user_id IS NULL AND group_id IS NOT NULL)", @@ -60,31 +60,39 @@ def upgrade() -> None: op.create_index('idx_llm_model_configs_user_id', 'llm_model_configs', ['user_id']) op.create_index('idx_llm_model_configs_group_id', 'llm_model_configs', ['group_id']) op.create_index('idx_llm_model_configs_model_type', 'llm_model_configs', ['model_type']) - op.create_index('idx_llm_model_configs_config', 'llm_model_configs', ['config'], postgresql_using='gin') - # Create partial unique indexes for default configs - # Each user can have at most one default config - op.execute(""" - CREATE UNIQUE INDEX unique_user_default - ON llm_model_configs (user_id) - WHERE is_default = TRUE AND user_id IS NOT NULL - """) - # Each group can have at most one default config - op.execute(""" - CREATE UNIQUE INDEX unique_group_default - ON llm_model_configs (group_id) - WHERE is_default = TRUE AND group_id IS NOT NULL - """) + # PostgreSQL-specific indexes + if dialect_name == 'postgresql': + # GIN index for JSONB config column + op.create_index('idx_llm_model_configs_config', 'llm_model_configs', ['config'], postgresql_using='gin') + + # Partial unique indexes for default configs + op.execute(""" + CREATE UNIQUE INDEX unique_user_default + ON llm_model_configs (user_id) + WHERE is_default = TRUE AND user_id IS NOT NULL + """) + op.execute(""" + CREATE UNIQUE INDEX unique_group_default + ON llm_model_configs (group_id) + WHERE is_default = TRUE AND group_id IS NOT NULL + """) def downgrade() -> None: + # Get the current connection and dialect + conn = op.get_bind() + dialect_name = conn.dialect.name + # Drop indexes - op.drop_index('idx_llm_model_configs_config', table_name='llm_model_configs') op.drop_index('idx_llm_model_configs_model_type', table_name='llm_model_configs') op.drop_index('idx_llm_model_configs_group_id', table_name='llm_model_configs') op.drop_index('idx_llm_model_configs_user_id', table_name='llm_model_configs') - op.drop_index('unique_user_default', table_name='llm_model_configs') - op.drop_index('unique_group_default', table_name='llm_model_configs') + + if dialect_name == 'postgresql': + op.drop_index('idx_llm_model_configs_config', table_name='llm_model_configs') + op.drop_index('unique_user_default', table_name='llm_model_configs') + op.drop_index('unique_group_default', table_name='llm_model_configs') # Drop table op.drop_table('llm_model_configs')