Skip to content

Auditing/versioning v2#510

Open
GianlucaFicarelli wants to merge 23 commits intomainfrom
audit_v2
Open

Auditing/versioning v2#510
GianlucaFicarelli wants to merge 23 commits intomainfrom
audit_v2

Conversation

@GianlucaFicarelli
Copy link
Copy Markdown
Collaborator

@GianlucaFicarelli GianlucaFicarelli commented Jan 16, 2026

This PR adds auditing/versioning using an external library sqlalchemy-history (fork of sqlalchemy-continuum).
Only sqlalchemy-continuum would support native_versioning (faster, using potgresql triggers), but the defined triggers have some limitations with polymorphism (they don't create a version of all the tables, and they don't set transaction_end on the previous version), so they would require some changes.

To consider all the deletions through the sqlalchemy models, the ON DELETE CASCADE defined in the db schema have been removed and replaced with a corresponding relationship with cascade.
This is less efficient, especially when deleting entities with many children, but the deletion is not the usual case so it could be acceptable. We can also reconsider the use of triggers later.

The tests are a lot slower because we have twice the number of tables, and we truncate them after each test. Using rollback instead of truncate could improve the performance, but it requires some changes in the instantiation of the sessions during tests.
Alternatively, we can disable versioning during tests that don't check versioning, and truncate only the non-version tables.

* origin/main:
  Add entity_type filter to measurement_label (#496)
  Add StructuralDomain.not_applicable (#495)
  Increase statement_timeout and lock_timeout in db migration (#494)
  Use scope=function in get_db dependency (#492)
* origin/main:
  Update `brain-atlas` endpoints (#506)
  Add published_in filter in ScientificArtifact (#505)
  Make nullable attributes of EMDenseReconstructionDataset (#503)
  Remove all endpoints that had a `InBrainRegionDep`, but don't have a brain_region_id (#501)
  Add pref_label/alt_label ilike_search to ETypeClass/MTypeClass filters (#500)
  add `number_neurons` to Simulation` (#493)
  Fix incorrect returned values when getting `ascendants_and_descendants` (#498)
* origin/main:
  Hotfix migration (#508)
  Add published_in__ilike filter for circuit (#509)
@GianlucaFicarelli GianlucaFicarelli self-assigned this Jan 16, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 16, 2026

Codecov Report

❌ Patch coverage is 93.91892% with 18 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
app/service/versioning.py 92.92% 4 Missing and 4 partials ⚠️
app/db/triggers.py 61.53% 5 Missing ⚠️
app/service/cell_morphology.py 44.44% 5 Missing ⚠️
Flag Coverage Δ
pytest 97.46% <93.91%> (-0.18%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
app/db/model.py 99.19% <100.00%> (+0.10%) ⬆️
app/db/utils.py 83.78% <100.00%> (ø)
app/db/versioning_plugins/user_context.py 100.00% <100.00%> (ø)
app/dependencies/db.py 100.00% <100.00%> (ø)
app/routers/__init__.py 100.00% <ø> (ø)
app/routers/cell_morphology.py 100.00% <100.00%> (ø)
app/routers/versioning.py 100.00% <100.00%> (ø)
app/utils/routers.py 80.00% <100.00%> (+2.72%) ⬆️
app/db/triggers.py 81.25% <61.53%> (-18.75%) ⬇️
app/service/cell_morphology.py 91.22% <44.44%> (-8.78%) ⬇️
... and 1 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines +24 to +26
read_mtypes_history = router.get("/{id_}/mtypes/history")(
app.service.cell_morphology.read_mtypes_history
)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just an example, it's going to be removed.

)


def read_mtypes_history(
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Copy link
Copy Markdown
Collaborator

@mgeplf mgeplf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No red flags, sqlalchemy-history seems to not be too intrusive considering what it unlocks; very impressive.

# FIXME: # noqa: FIX001
# the creation of a new agent would fail with: Session is already flushing
# so the user_id is logged only if it already exists.
# Alternative: ensure that the person is created at the beginning of the request,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the Alternative a note to yourself and should be removed? If not, I don't follow it.

Comment thread pyproject.toml
required-version = ">=0.8.0"

[tool.uv.sources]
sqlalchemy-history = { git = "https://github.com/GianlucaFicarelli/sqlalchemy-history", branch = "obi" }
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be forked into OBI?

Comment thread app/db/model.py
TransactionChangesPlugin(),
],
options={
"enum_prefix": "",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a comment what this enum_prefix is for?

Comment thread app/db/model.py
uselist=True,
passive_deletes=True, # rely on PostgreSQL ON DELETE CASCADE to remove association rows
)
) # deletion cascade of generation
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know enough about the deletion, and cascade behavior that we want to be able to evaluate the correctness of this - I will leave that to @eleftherioszisis.

Comment thread app/db/utils.py
if issubclass(mapper.class_, MeasurableEntityMixin)
and issubclass(mapper.class_, Entity)
and mapper.class_.__tablename__
and getattr(mapper.class_, "__tablename__", None)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this to skip the versioned table? Or something else?

Comment thread app/service/versioning.py
resource_route: ResourceRoute,
resource_id: uuid.UUID,
) -> int:
"""Return the number fo versions."""
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Return the number fo versions."""
"""Return the number of versions."""

Comment thread app/service/versioning.py
base_query = constrain_to_accessible_entities(
base_query, project_id=user_context.project_id, db_model_class=id_model_class
)
if version_num < 0:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would it be negative?

Comment thread Dockerfile
libpq-dev \
ca-certificates
ca-certificates \
git
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is for convenience, right? not something to do w/ versioning?

Comment thread app/db/utils.py
EntityType[mapper.class_.__tablename__]: mapper.class_
for mapper in Base.registry.mappers
if hasattr(EntityType, mapper.class_.__tablename__)
if getattr(mapper.class_, "__tablename__", None) in EntityType
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this change a result of some corner case? It would be nice to have a comment of the reason if this is the case.

Comment thread app/db/utils.py

# If polymorphic_identity is different from __tablename__, it's a subclass
return mapper_args["polymorphic_identity"] != mapper_class.__tablename__
return mapper_args["polymorphic_identity"] != getattr(mapper_class, "__tablename__", None)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it worth to create little helper funcs with docstrings to make these more readable?

Comment thread app/dependencies/db.py
# are executed before the response is sent back to the client.
SessionDep = Annotated[Session, Depends(get_db, scope="function")]
PlainSessionDep = Annotated[Session, Depends(get_db, scope="function")]
SessionDep = Annotated[Session, Depends(get_db_with_user_context, scope="function")]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe UserSessionDep?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants