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
This commit is contained in:
YueGuobin 2026-03-03 18:03:16 +08:00
parent 3632641127
commit 8e43ad4912
2 changed files with 43 additions and 35 deletions

View File

@ -15,8 +15,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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"),
)

View File

@ -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')