Skip to content

Migrate to Elasticsearch 8.19, Python 3.12 and SQLAlchemy 2.0#2211

Open
Nayor wants to merge 54 commits into
masterfrom
python-3.12-migration
Open

Migrate to Elasticsearch 8.19, Python 3.12 and SQLAlchemy 2.0#2211
Nayor wants to merge 54 commits into
masterfrom
python-3.12-migration

Conversation

@Nayor

@Nayor Nayor commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Two chained infrastructure migrations, bundled in this branch since it was built on top of the (unpushed) ES migration branch:

  1. Elasticsearch 2.4 → 8.19 — one physical index per document type (c2corg_a, c2corg_w, ...) unified via a c2corg_* pattern with a c2corg_doc_type discriminant field. Validated by reindexing 1.8M real anonymized production documents.
  2. Python 3.9 → 3.12 / SQLAlchemy 1.3 → 2.0 — full migration of models/, views/ and scripts/, including:
    • Dockerfiles moved to python:3.12-slim-bookworm
    • SQLAlchemy==2.0.51, geoalchemy2==0.20.0
    • String-based loader options (load_only, joinedload, contains_eager, undefer) converted to class-bound attributes, as required by SQLAlchemy 2.0
    • Query.from_self() removal worked around in the pagination helper (models/utils.py)
    • Various select([...]) legacy list-syntax and .as_scalar() calls updated to select(...) / .scalar_subquery()
    • Fixed a premature-autoflush bug in Document.update() that was mixing transient and persistent locale objects
    • Fixed a geoalchemy2 0.20 regression causing DuplicateTable on idx_documents_geometries_geom_detail during the initial alembic migration

Test status

958 tests total, 958 passing.

Known blocker — do not merge yet

requirements.txt currently pins ColanderAlchemy to a local filesystem path (file:///.../ColanderAlchemy) because the upstream c2corg/ColanderAlchemy fork needs SQLAlchemy 2.0 support (patched locally, branch sqlalchemy-2.0-support) but we don't have push access to publish it. This needs a real fork/publish location before merge.

Not in scope

PostgreSQL migration to pg18/PostGIS 3.6 — separate, untouched chantier.

jphilou added 30 commits May 22, 2024 17:42
Test de création de la branch remote sur un texte sans impact.
- Mapping Update (scripts and code)
- Code update for deprecation update (fields, missings, ...)
- Component update (elasticsearch, elasticsearch DSL)
- Migration scripts and logstash engine / pipeline
- CI scripts
- Mapping Update (scripts and code)
- Code update for deprecation update (fields, missings, ...)
- Component update (elasticsearch, elasticsearch DSL)
- Migration scripts and logstash engine / pipeline
- CI scripts
- Mapping Update (scripts and code)
- Code update for deprecation update (fields, missings, ...)
- Component update (elasticsearch, elasticsearch DSL)
- Migration scripts and logstash engine / pipeline
- CI scripts
- Mapping Update (scripts and code)
- Code update for deprecation update (fields, missings, ...)
- Component update (elasticsearch, elasticsearch DSL)
- Migration scripts and logstash engine / pipeline
- CI scripts
CI:
- retirer dans l'installation de ES 5.6 les paramètres inconsistant
+ install plugin analysis-icu sur ES
CI:
- retirer dans l'installation de ES 5.6 les paramètres inconsistant
+ install plugin analysis-icu sur ES
Script de création / mapping / settings des indices ES
- CI/CD : ES Download version
…-to-6.8' into elasticsearch-migration-from-5.6-to-6.8
- Update settings es in config file for github-actions
- test_fill_index fix coquille
- update mapping and code
- update script for data migration
- artifact fix for ES download
jphilou and others added 23 commits June 25, 2024 18:25
- artifact fix for ES download
- mapping
- class
- requirements.txt
- docker definition
- artifact fix for ES download
- mapping
- class
- requirements.txt
- docker definition
- artifact fix for ES download
- mapping
- class
- requirements.txt
- docker definition
- CI update
- logstash data migration update
- CI elastic 8.22
- README.md Update
Merges the existing elasticsearch-migration-from-7.17-to-8.14 branch
(2.6->5.6->6.8->7.x->7.17->8.14, authored separately) into current
master, bumping the final target to elasticsearch/elasticsearch-dsl
8.19.x, and resolving conflicts from ~2 years of master drift.

Key changes carried over from that branch:
- DocType -> Document, String -> Text (elasticsearch_dsl 8.x API)
- ES mapping types removed (ES7+): documents now carry a
  `c2corg_doc_type` field instead of relying on per-type `_type`
- One physical ES index per document type (c2corg_a, c2corg_b, ...),
  queried through a common `c2corg_*` index pattern
- `Missing` query -> `Q("exists", ...)` wrapped in bool/must_not
- ES auth support (scheme/user/passwd) in search config
- CI updated to install/run ES 8.19.18 directly instead of ES 2.4.6

Cleaned up from the source branch: removed a leftover debug
pprint() call, dead commented-out code, and an accidental drop of
ACLDefault on DocumentRest.

Validated locally against real (anonymized) production data:
restored dump into ES-8-targeted sandbox, created all 10 per-type
indices, ran a full reindex from Postgres (1.8M documents), and
confirmed search queries return correct, type-filtered results.
The pycurl calls used to create/configure ES indices had no timeout
set, so a stalled connection (e.g. under I/O contention) could hang
the process indefinitely instead of failing and letting the caller
retry. Observed this hang concretely while running the test suite
locally: the process sat idle (near-0% CPU, no network I/O) for
several minutes with no way to recover short of killing it.
config/env.sandbox is a throwaway env file pointing at local
migration-rehearsal container hostnames, not meant to be committed.
- Bump base images (docker/Dockerfile, docker/Dockerfile.dev,
  .devcontainer/Dockerfile) from python:3.9 to python:3.12-slim-bookworm,
  and fix bullseye-only package name (libffi7 -> libffi8) for bookworm.
- Bump CI to Python 3.12.
- requirements.txt: SQLAlchemy 1.3.24 -> 2.0.51, geoalchemy2 0.4.2 ->
  0.20.0. ColanderAlchemy temporarily points at a local, unpublished
  patch (branch sqlalchemy-2.0-support in the ColanderAlchemy repo)
  that relaxes its own SQLAlchemy pin to >=1.4 - needs write access to
  c2corg/ColanderAlchemy to publish for real.
- models/__init__.py: declarative_base import moved from
  sqlalchemy.ext.declarative (pre-1.4) to sqlalchemy.orm.
- ext/colander_ext.py: removed the geoalchemy2.compat import
  (buffer/bytes were just Python 2/3 shims; bytes is the builtin,
  buffer() is memoryview() on Python 3).
- models/document.py, models/stoparea.py: removed the `management=True`
  Geometry() kwarg, which geoalchemy2 0.20 no longer supports (modern
  PostGIS uses typmod columns, not the old catalog-managed style).
- models/association_views.py, area_association.py,
  topo_map_association.py, feed.py: ported old-style
  select([col1, col2]) calls to the SQLAlchemy 1.4+/2.0 select(col1,
  col2) signature; added .scalar_subquery() to two column_property()
  selects in feed.py per the 2.0 deprecation warning.

Verified: `import c2corg_api.models` (all ~20 model files, every
document type) now succeeds cleanly, and ColanderAlchemy schema
generation (SQLAlchemySchemaNode direct instantiation, the pattern
this codebase actually uses) produces correct field lists.

Not yet done: the view layer (c2corg_api/views/) and scripts/ almost
certainly have their own SQLAlchemy 1.3-era Query patterns that need
the same kind of review; this commit only gets the model layer to
import and construct schemas cleanly under SQLAlchemy 2.0.
Completes the Python 3.12 / SQLAlchemy 2.0 migration for c2corg_api/views
and c2corg_api/scripts, found empirically by running the full test suite:

- sqlalchemy.orm.util no longer exports with_polymorphic/aliased (moved to
  sqlalchemy.orm); sqlalchemy.sql.elements.not_ moved to top-level sqlalchemy.
- String-based loader options (load_only, joinedload, contains_eager,
  undefer) are rejected outright in 2.0; switched to class-bound attributes
  across document_listings.py, document.py, document_changes.py,
  document_history.py, document_version.py, user_preferences.py, feed.py
  and scripts/es/sync.py. Route.locales needed .of_type(RouteLocale) when
  load_only-ing polymorphic locale columns.
- Query.from_self() was removed in 2.0; rewrote the windowed_query/
  column_windows pagination helper in models/utils.py using an explicit
  subquery.
- collections.Mapping removed in Python 3.10+; use collections.abc.Mapping.
- SQLAlchemy 1.4+ no longer implicitly adds order_by() columns to the
  SELECT list under .distinct(), causing InvalidColumnReference; fixed in
  waypoint.py/route.py's set_recent_outings and generalized
  get_first_column() to unpack Row objects positionally.
- Several single-column query results were used directly as scalar ids
  without unpacking (silently worked in 1.3, "can't adapt type 'Row'" in
  2.0); fixed in document_delete.py and scripts/es/sync.py.
- geoalchemy2 0.20 routes use_typmod=False columns (needed on geom_detail
  to store mixed 2D/3D/4D geometries) through its legacy "managed"
  AddGeometryColumn() codepath, whose automatic spatial index creation
  collides with alembic's own index creation. Set spatial_index=False on
  that column in the init migration and create the index explicitly.
- Leftover legacy select([...]) list-syntax + .as_scalar() in
  models/feed.py, missed by the earlier SQLAlchemy 2.0 select() sweep.
- Document.update() mixed transient and persistent locale objects in the
  same loop without their FK set yet, letting an unrelated expired-
  attribute access trigger a premature autoflush that inserted a locale
  row before its document_id was set. Wrapped in DBSession.no_autoflush.

Full pytest suite: 956 passed, 2 failed (958 total). The two failures
are a PostGIS-side loss of the M coordinate on 4D geometry round-trips,
reproducible only against the pg18 sandbox (PostGIS 3.6) used for the
separate, not-yet-started database migration - not caused by this change.
geoalchemy2 binds a non-extended WKBElement by converting it through
Shapely, which has no support for M (4th dimension) coordinates and
silently drops them on write. wkbelement_from_geojson() now embeds the
SRID directly in the WKB header (turning it into real EWKB), which
geoalchemy2 detects as already-extended and binds as a hex string
directly instead, preserving all dimensions. Fixes the last two
failures from the SQLAlchemy 2.0 migration test run
(test_ext_colander::test_save_and_load_4d, test_route::test_post_success_4d).

Also replace an implicit scalar-subquery coercion in
update_langs_of_changes() with an explicit .scalar_subquery() call, as
recommended by the SQLAlchemy 2.0 deprecation warning.
@codacy-production

codacy-production Bot commented Jul 20, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 4 high · 11 minor

Alerts:
⚠ 15 issues (≤ 0 issues of at least minor severity)

Results:
15 new issues

Category Results
Documentation 10 minor
ErrorProne 4 high
CodeStyle 1 minor

View in Codacy

🟢 Metrics 7 complexity · 6 duplication

Metric Results
Complexity 7
Duplication 6

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@Nayor
Nayor force-pushed the python-3.12-migration branch from c5aae62 to aaccdb4 Compare July 20, 2026 21:17
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.

2 participants