diff --git a/web/server/codechecker_server/database/run_db_model.py b/web/server/codechecker_server/database/run_db_model.py index f240ae7826..b19b83dbea 100644 --- a/web/server/codechecker_server/database/run_db_model.py +++ b/web/server/codechecker_server/database/run_db_model.py @@ -13,8 +13,9 @@ import os from typing import Optional -from sqlalchemy import Boolean, Column, DateTime, Enum, ForeignKey, Integer, \ - LargeBinary, MetaData, String, UniqueConstraint, Table, Text, JSON +from sqlalchemy import BigInteger, Boolean, Column, DateTime, Enum, \ + ForeignKey, Integer, LargeBinary, MetaData, String, UniqueConstraint, \ + Table, Text, JSON from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from sqlalchemy.sql.expression import true, false @@ -185,8 +186,15 @@ def __init__(self, run_history_id, analyzer_type, version, successful, deferrable=True, initially="DEFERRED", ondelete="CASCADE"), - index=True), - Column('analysis_info_id', Integer, ForeignKey('analysis_info.id')) + nullable=False, + index=True, + primary_key=True), + Column( + 'analysis_info_id', + Integer, + ForeignKey('analysis_info.id'), + nullable=False, + primary_key=True) ) @@ -277,9 +285,9 @@ class BugPathEvent(Base): file_id = Column(Integer, ForeignKey('files.id', deferrable=True, initially="DEFERRED", ondelete='CASCADE'), index=True) - report_id = Column(Integer, ForeignKey('reports.id', deferrable=True, - initially="DEFERRED", - ondelete='CASCADE'), + report_id = Column(BigInteger, ForeignKey('reports.id', deferrable=True, + initially="DEFERRED", + ondelete='CASCADE'), index=True, primary_key=True) @@ -307,9 +315,9 @@ class BugReportPoint(Base): file_id = Column(Integer, ForeignKey('files.id', deferrable=True, initially="DEFERRED", ondelete='CASCADE'), index=True) - report_id = Column(Integer, ForeignKey('reports.id', deferrable=True, - initially="DEFERRED", - ondelete='CASCADE'), + report_id = Column(BigInteger, ForeignKey('reports.id', deferrable=True, + initially="DEFERRED", + ondelete='CASCADE'), index=True, primary_key=True) @@ -331,9 +339,9 @@ class ExtendedReportData(Base): id = Column(Integer, autoincrement=True, primary_key=True) - report_id = Column(Integer, ForeignKey('reports.id', deferrable=True, - initially="DEFERRED", - ondelete='CASCADE'), + report_id = Column(BigInteger, ForeignKey('reports.id', deferrable=True, + initially="DEFERRED", + ondelete='CASCADE'), index=True) file_id = Column(Integer, ForeignKey('files.id', deferrable=True, @@ -370,13 +378,20 @@ def __init__(self, line_begin, col_begin, line_end, col_end, Base.metadata, Column( 'report_id', - Integer, + BigInteger, ForeignKey('reports.id', deferrable=True, initially="DEFERRED", ondelete="CASCADE"), - index=True), - Column('analysis_info_id', Integer, ForeignKey('analysis_info.id')) + nullable=False, + index=True, + primary_key=True), + Column( + 'analysis_info_id', + Integer, + ForeignKey('analysis_info.id'), + nullable=False, + primary_key=True) ) @@ -391,7 +406,7 @@ def __init__(self, line_begin, col_begin, line_end, col_end, class Report(Base): __tablename__ = 'reports' - id = Column(Integer, autoincrement=True, primary_key=True) + id = Column(BigInteger, autoincrement=True, primary_key=True) file_id = Column(Integer, ForeignKey('files.id', deferrable=True, initially="DEFERRED", ondelete='CASCADE'), @@ -497,7 +512,7 @@ def __init__(self, report_id: int, key: str, value: str): self.value = value report_id = Column( - Integer, + BigInteger, ForeignKey("reports.id", ondelete="CASCADE"), primary_key=True, index=True) diff --git a/web/server/codechecker_server/migrations/report/versions/a7f3c8e21b90_report_id_bigint.py b/web/server/codechecker_server/migrations/report/versions/a7f3c8e21b90_report_id_bigint.py new file mode 100644 index 0000000000..37d1da92c0 --- /dev/null +++ b/web/server/codechecker_server/migrations/report/versions/a7f3c8e21b90_report_id_bigint.py @@ -0,0 +1,196 @@ +""" +Report id bigint + +Revision ID: a7f3c8e21b90 +Revises: 24c9660f82b1 +Create Date: 2026-06-30 00:00:00.000000 +""" + +from alembic import op +import sqlalchemy as sa + + +# Revision identifiers, used by Alembic. +revision = 'a7f3c8e21b90' +down_revision = '24c9660f82b1' +branch_labels = None +depends_on = None + + +REPORT_ID_FK_TABLES = [ + 'bug_path_events', + 'bug_report_points', + 'extended_report_data', + 'report_analysis_info', + 'report_annotations', +] + +ASSOCIATION_TABLES = [ + { + 'name': 'report_analysis_info', + 'pk_name': 'pk_report_analysis_info', + 'columns': ('report_id', 'analysis_info_id'), + }, + { + 'name': 'run_history_analysis_info', + 'pk_name': 'pk_run_history_analysis_info', + 'columns': ('run_history_id', 'analysis_info_id'), + }, +] + + +def _alter_report_id_columns_upgrade(type_, existing_type): + for table in REPORT_ID_FK_TABLES: + op.alter_column( + table, + 'report_id', + existing_type=existing_type, + type_=type_, + existing_nullable=(table == 'report_analysis_info'), + nullable=False) + + +def _alter_report_id_columns_downgrade(type_, existing_type): + for table in REPORT_ID_FK_TABLES: + op.alter_column( + table, + 'report_id', + existing_type=existing_type, + type_=type_, + existing_nullable=False, + nullable=(table == 'report_analysis_info')) + + +def _cleanup_association_tables(dialect): + for table_info in ASSOCIATION_TABLES: + table = table_info['name'] + columns = table_info['columns'] + null_condition = " OR ".join(f"{column} IS NULL" for column in columns) + op.execute(sa.text( + f"DELETE FROM {table} WHERE {null_condition}")) + + col_a = " AND ".join( + f"a.{column} IS NOT DISTINCT FROM b.{column}" + for column in columns) + if dialect == 'postgresql': + op.execute(sa.text(f""" + DELETE FROM {table} AS a + USING {table} AS b + WHERE a.ctid < b.ctid + AND {col_a} + """)) + else: + grouped_columns = ", ".join(columns) + op.execute(sa.text(f""" + DELETE FROM {table} + WHERE rowid NOT IN ( + SELECT MIN(rowid) + FROM {table} + GROUP BY {grouped_columns} + ) + """)) + + +def _tighten_association_tables_postgresql(): + op.alter_column( + 'run_history_analysis_info', + 'run_history_id', + existing_type=sa.Integer(), + nullable=False) + for table_info in ASSOCIATION_TABLES: + op.alter_column( + table_info['name'], + 'analysis_info_id', + existing_type=sa.Integer(), + nullable=False) + + op.create_primary_key( + 'pk_report_analysis_info', + 'report_analysis_info', + ['report_id', 'analysis_info_id']) + op.create_primary_key( + 'pk_run_history_analysis_info', + 'run_history_analysis_info', + ['run_history_id', 'analysis_info_id']) + + +def _tighten_association_tables_sqlite(): + for table_info in ASSOCIATION_TABLES: + with op.batch_alter_table(table_info['name']) as batch_op: + for column in table_info['columns']: + batch_op.alter_column(column, nullable=False) + batch_op.create_primary_key( + table_info['pk_name'], + list(table_info['columns'])) + + +def _relax_association_tables_postgresql(): + op.drop_constraint( + 'pk_run_history_analysis_info', + 'run_history_analysis_info', + type_='primary') + op.drop_constraint( + 'pk_report_analysis_info', + 'report_analysis_info', + type_='primary') + + op.alter_column( + 'run_history_analysis_info', + 'run_history_id', + existing_type=sa.Integer(), + nullable=True) + for table_info in ASSOCIATION_TABLES: + op.alter_column( + table_info['name'], + 'analysis_info_id', + existing_type=sa.Integer(), + nullable=True) + + +def _relax_association_tables_sqlite(): + for table_info in ASSOCIATION_TABLES: + with op.batch_alter_table(table_info['name']) as batch_op: + batch_op.drop_constraint(table_info['pk_name'], type_='primary') + for column in table_info['columns']: + batch_op.alter_column(column, nullable=True) + + +def upgrade(): + ctx = op.get_context() + dialect = ctx.dialect.name + + _cleanup_association_tables(dialect) + + if dialect == 'postgresql': + _alter_report_id_columns_upgrade(sa.BigInteger(), sa.Integer()) + op.alter_column( + 'reports', + 'id', + existing_type=sa.Integer(), + type_=sa.BigInteger(), + existing_nullable=False, + autoincrement=True) + op.execute(sa.text('ALTER SEQUENCE reports_id_seq AS BIGINT')) + _tighten_association_tables_postgresql() + elif dialect == 'sqlite': + # SQLite INTEGER columns already store 64-bit signed integers. + _tighten_association_tables_sqlite() + + +def downgrade(): + ctx = op.get_context() + dialect = ctx.dialect.name + + if dialect == 'postgresql': + _relax_association_tables_postgresql() + _alter_report_id_columns_downgrade(sa.Integer(), sa.BigInteger()) + op.alter_column( + 'reports', + 'id', + existing_type=sa.BigInteger(), + type_=sa.Integer(), + existing_nullable=False, + autoincrement=True) + op.execute(sa.text('ALTER SEQUENCE reports_id_seq AS INTEGER')) + elif dialect == 'sqlite': + _relax_association_tables_sqlite()