[NFC] Merge report path tables in the database - #4946
Conversation
4417c95 to
7e39437
Compare
7e39437 to
a5ea08b
Compare
a75e0f6 to
8c963fd
Compare
| used_file_ids.add(file_path_to_id[macro.file.path]) | ||
|
|
||
| report_path_data = ReportPathData(db_report.id, path_data) | ||
| # TODO: Here we query the File objects with session.get() that runs |
There was a problem hiding this comment.
Should these TODOs be in the code? I'd instead create a ticket for this and add it to the sprint.
| def __add_report_context(self, session, file_path_to_id): | ||
| def __add_report_context( | ||
| self, | ||
| session: DBSession, |
There was a problem hiding this comment.
The correct type annonation for session is SA_Session not DBSession.
There was a problem hiding this comment.
Are you sure about it? When I follow back the type of session through the function call chain, it ends up in a DBSession.
| idx, file_path_to_id[path_pos.file.path], db_report.id)) | ||
| for path_pos in report.bug_path_positions: | ||
| path_data.append({ | ||
| "from": { |
There was a problem hiding this comment.
While this works, using a raw dictionary for structured data is prone to errors.
Just to name a few looking at this example:
- Making a typo in
frome.g.frmo, and appending that topath_data. - Not following the structure correctly, e.g. placing
rowoutsidefrom. - Entirely missing a field, such as appending an element that has no
fid
These are all mistakes that are easy to make but hard to notice.
In contrast, consider putting all these information into a Python dataclass which are designed for these usecases.
With a dataclass, all of the fields are required params, and a type checker can also verify that a dataclass is properly constructed. Type checkers usually treat raw dictionaries as unstructured data and don't perform checks on them.
This comment applies especially to report_server.py where this data retrieved.
A raw dictionary can be an internal representation of the data, which is eventually stored (and compressed) in the database. But in my opinion, developers should access it via a type safe interface.
| # session object. We should investigate whether it's possible to | ||
| # provide a session object that has the File objects already, in | ||
| # order to save extra query time. | ||
| report_path_data.files.extend( |
There was a problem hiding this comment.
Running individual SELECT statements for all the files seems like a performance regression to me. And also considering that this code runs for every report during a store.
The questions are:
- Why do we need to select the individual
Filehere associated with afile_id? Why not just insert thefile_idinto the database? - Would that be an option to perform a
JOINbetween the two tables and not individualSELECTstatements?
|
|
||
| def get_reports_by_bugpath_filter_for_single_origin( | ||
| session, | ||
| session: DBSession, |
There was a problem hiding this comment.
I think this type annotation for session is wrong here as well.
| initially="DEFERRED", | ||
| ondelete="CASCADE"), | ||
| index=True), | ||
| Column('file_id', Integer, ForeignKey('files.id')) |
There was a problem hiding this comment.
On delete CASCADE was not added here, is that intentional? If a File is removed, shouldn't this row be removed as well?
| .filter(ReportPathData.report_id.in_(report_ids)) | ||
|
|
||
| for rpd in report_path_data: | ||
| files = {f.id: f.filepath for f in rpd.files} |
There was a problem hiding this comment.
In this case, I'm not sure how SQLAlchemy retrieves the File data from the database.
- Does it perform an individual
SELECTfor eachfile_id? If so, this looks like a performance concern to me. - Or a
JOINbetween the tables?
This is worth considering because this code runs in a loop.
Another follow up question: does it happen that a single File is queried multiple times for the same file_id since this code is running in a loop?
There was a problem hiding this comment.
Actually, the query is running from the report's point of view. This query selects the set of files belonging to a given report. This happens with a single select statement.
There was a problem hiding this comment.
I'm concerned about the performance of the current implementation.
Can't we query the required Files once instead of individual SELECTs for each report_path_data?
E.g. a proposed solution is to move files = {..} above the for loop.
8c963fd to
693fef0
Compare
barnabasdomozi
left a comment
There was a problem hiding this comment.
Please also fix the failing test cases.
| type: str | ||
| msg: Optional[str] = None | ||
|
|
||
| def to_dict(self): |
There was a problem hiding this comment.
| col_begin = Column(Integer) | ||
| line_end = Column(Integer) | ||
| col_end = Column(Integer) | ||
| @hybrid_property |
There was a problem hiding this comment.
Why use @hybrid_property here?
Can't we use a regular @property instead?
| .filter(ReportPathData.report_id.in_(report_ids)) | ||
|
|
||
| for rpd in report_path_data: | ||
| files = {f.id: f.filepath for f in rpd.files} |
There was a problem hiding this comment.
I'm concerned about the performance of the current implementation.
Can't we query the required Files once instead of individual SELECTs for each report_path_data?
E.g. a proposed solution is to move files = {..} above the for loop.
| } | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, data: dict): |
There was a problem hiding this comment.
There is also a built-in way to construct a dataclass from a dict.
| macro.range.end_line, macro.range.end_col, | ||
| macro.message, file_path_to_id[macro.file.path], | ||
| db_report.id, data_type)) | ||
| rpdf = ReportPathDataFile.insert().values([{ |
There was a problem hiding this comment.
Is this running multiple INSERT statements or only one and inserts in bulk?
a074a50 to
3ce57bf
Compare
|
|
||
| def get_reports_by_bugpath_filter(session, file_filter_q) -> Set[int]: | ||
| def get_reports_by_bugpath_filter( | ||
| session: DBSession, |
There was a problem hiding this comment.
session should by typed SA_Session here as well.
| endCol=brp.col_end, | ||
| def bugreportpoint_db_to_api(brp: ReportPathData.Item) -> ttypes.BugPathPos: | ||
| return ttypes.BugPathPos( | ||
| startLine=brp.to_row, |
There was a problem hiding this comment.
I think startLine should be from_row and not to_row.
to_row is also used for endLine.
| self.__validate_and_add_report_annotations( | ||
| session, db_report.id, report.annotations) | ||
|
|
||
| rpdf = ReportPathDataFile.insert().values(path_data_files) |
There was a problem hiding this comment.
If path_data_files is any empty list in the end, this INSERT should be skipped.
|
|
||
| def get_report_details(session, report_ids): | ||
| def get_report_details( | ||
| session: DBSession, |
There was a problem hiding this comment.
session should by typed SA_Session here as well.
3ce57bf to
ba0724d
Compare
| sa.ForeignKeyConstraint( | ||
| ['file_id'], | ||
| ['files.id'], | ||
| name=op.f('fk_report_path_data_files_file_id_files')), |
There was a problem hiding this comment.
file_id has no ondelete=CASCADE constraints as defined in the model file.
|
|
||
| def __init__(self, line_begin, col_begin, line_end, col_end, | ||
| message, file_id, report_id, data_type): | ||
| files = relationship("File", secondary=ReportPathDataFile) |
There was a problem hiding this comment.
get_report_details() no longer relies on calling this .files relationship anymore. Is this relationship still needed or can be removed?
| session, db_report.id, report.annotations) | ||
|
|
||
| if path_data_files: | ||
| rpdf = ReportPathDataFile.insert().values(path_data_files) |
There was a problem hiding this comment.
Consider:
session.execute(ReportPathDataFile.insert(), path_data_files)
This way, SQLAlchemy still inserts the data in large batches but keeps it under maximum insertion limits enforced by PostgreSQL / SQLite.
ba0724d to
2f764a4
Compare
barnabasdomozi
left a comment
There was a problem hiding this comment.
I just did one final step and tested the migration locally on an SQLite database.
When this schema migration is executed together in chain with another migration, it results in the following error:
Traceback (most recent call last):
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
cursor.execute(statement, parameters)
sqlite3.OperationalError: cannot start a transaction within a transaction
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/workspace/github/ericsson-codechecker/build/CodeChecker/lib/python3/codechecker_server/database/database.py", line 334, in upgrade
command.upgrade(cfg, "head")
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/alembic/command.py", line 487, in upgrade
script.run_env()
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/alembic/script/base.py", line 550, in run_env
util.load_python_file(self.dir, "env.py")
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/alembic/util/pyfiles.py", line 114, in load_python_file
module = load_module_py(module_id, path)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/alembic/util/pyfiles.py", line 132, in load_module_py
spec.loader.exec_module(module) # type: ignore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap_external>", line 995, in exec_module
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
File "/workspace/github/ericsson-codechecker/build/CodeChecker/lib/python3/codechecker_server/migrations/report/env.py", line 66, in <module>
run_migrations_online()
File "/workspace/github/ericsson-codechecker/build/CodeChecker/lib/python3/codechecker_server/migrations/report/env.py", line 53, in run_migrations_online
migrate(connection)
File "/workspace/github/ericsson-codechecker/build/CodeChecker/lib/python3/codechecker_server/migrations/report/env.py", line 49, in migrate
context.run_migrations()
File "<string>", line 8, in run_migrations
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/alembic/runtime/environment.py", line 967, in run_migrations
self.get_context().run_migrations(**kw)
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/alembic/runtime/migration.py", line 621, in run_migrations
step.migration_fn(**kw)
File "/workspace/github/ericsson-codechecker/web/server/codechecker_server/migrations/report/versions/6c3c93a826f4_report_path_data_table.py", line 77, in upgrade
conn.execute(sa.text("BEGIN"))
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 1421, in execute
return meth(
^^^^^
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/sqlalchemy/sql/elements.py", line 526, in _execute_on_connection
return connection._execute_clauseelement(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 1643, in _execute_clauseelement
ret = self._execute_context(
^^^^^^^^^^^^^^^^^^^^^^
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 1848, in _execute_context
return self._exec_single_context(
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 1988, in _exec_single_context
self._handle_dbapi_exception(
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 2365, in _handle_dbapi_exception
raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/sqlalchemy/engine/base.py", line 1969, in _exec_single_context
self.dialect.do_execute(
File "/workspace/github/codechecker/venv_dev/lib/python3.12/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
cursor.execute(statement, parameters)
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) cannot start a transaction within a transaction
[SQL: BEGIN]
(Background on this error at: https://sqlalche.me/e/20/e3q8)
[INFO 2026-08-25 14:52:15] - [Default] Done upgrading. Schema upgrade failed.
2f764a4 to
0dd14c2
Compare
e392f09 to
d25a4b3
Compare
d25a4b3 to
554ad78
Compare
There are 3 database tables for storing information about the report paths: - BugPathEvent - BugReportPoint - ExtendedReportData These are now merged to ReportPathData. These tabels were the biggest ones in terms of database size and their handling and querying was problematic. The new table contains all report path related information as a zip blob. Keeping track of the files touched by the report path is still important, so a join-table is storing this information.
barnabasdomozi
left a comment
There was a problem hiding this comment.
LGTM, great work
|
Thank you for the extensive review and the useful comments! |
There are 3 database tables for storing information about the report paths:
These are now merged to ReportPathData.
These tabels were the biggest ones in terms of database size and their handling and querying was problematic. The new table contains all report path related information as a zip blob. Keeping track of the files touched by the report path is still important, so a join-table is storing this information.