Skip to content

feat: MSSQL (SQL Server)#381

Open
spidermila wants to merge 32 commits into
mainfrom
feat/mssql-support
Open

feat: MSSQL (SQL Server)#381
spidermila wants to merge 32 commits into
mainfrom
feat/mssql-support

Conversation

@spidermila

@spidermila spidermila commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Migrates MedCover from PostgreSQL to Microsoft SQL Server (MSSQL 2022 / Azure SQL) as the sole supported database engine. PostgreSQL is fully removed.

What changed

Core dialect changes

  • sa.false()/sa.true() — replaced .is_(False/True) (generated invalid IS 0 on MSSQL) across 22 files
  • CS_COLLATION — hardcoded to Czech_100_CI_AS_SC_UTF8; PG ICU collation removed
  • Outbox pollingFOR UPDATE SKIP LOCKEDWITH (UPDLOCK, ROWLOCK, READPAST)
  • Qualification partial indexpostgresql_where=mssql_where=

Dialect-specific modules simplified (PG branches removed)

  • app/backup.py — IDENTITY_INSERT for restore, DBCC CHECKIDENT for reseed, DELETE+NOCHECK for clear, TOP 1 for alembic head
  • app/digest/blocks/server_stats.pysys.database_files and sys.tables/allocation_units only
  • app/mail.py — READPAST hint only
  • app/config.py — removed _fix_db_url() (postgres:// → postgresql:// shim); production warning updated for Encrypt=yes

Infrastructure

  • Dockerfile — ODBC Driver 18 + libgssapi-krb5-2
  • requirements.in/txt — removed psycopg2-binary; pyodbc only
  • docker-compose.yml — MSSQL 2022 Express replaces PostgreSQL
  • docker-compose.prod.yml — MSSQL
  • docker-compose.e2e.yml — MSSQL (merged from former docker-compose.e2e-mssql.yml)
  • Deleted: docker-compose.mssql-dev.yml, docker-compose.e2e-mssql.yml, db-init/, postgres.conf
  • scripts/e2e-entrypoint.sh — MSSQL-only (PG case branch removed)
  • mssql-init/setup.sh, setup-e2e.sh — explicit timeout failure handling
  • .github/workflows/ci.yml — MSSQL service container + ODBC install step
  • .env.example — updated to MSSQL connection string

Test suite

  • tests/conftest.py — PostgresContainer → SqlServerContainer (testcontainers); _ensure_db_exists/_drop_db rewritten for MSSQL; TRUNCATE CASCADE → FK-disable + DELETE; xdist worker DB URLs correctly handle MSSQL query params
  • tests/test_config.py — test URLs and warning assertions updated for MSSQL

Test results

  • ✅ 955 unit tests pass (MSSQL)
  • ✅ 123 e2e tests pass (MSSQL, Chromium + Firefox + WebKit)
  • ✅ Full manual testing on zerver with MSSQL
  • ✅ Backup/restore including IDENTITY_INSERT and DBCC CHECKIDENT

Security review

See azure-sql-security-review.md and Azure-SQL-DB.parameters.json in the medcover-infra repo for ARM template hardening (TLS 1.2, ADS, auditing, VA).

Summary by CodeRabbit

Release Notes

  • Changed

    • Migrated database backend from PostgreSQL to Microsoft SQL Server 2022 (MSSQL). PostgreSQL is no longer supported.
    • Updated Docker development environment to use MSSQL container instead of PostgreSQL.
    • Production deployments now target Azure SQL Database instead of self-managed databases.
    • Standardized database connection handling and backup/restore operations for MSSQL compatibility.
    • Updated test infrastructure to use MSSQL testcontainers.
  • Chores

    • Replaced PostgreSQL driver dependency with pyodbc for MSSQL support.
    • Updated development and production environment configurations for MSSQL.
    • Migrated database schema baseline for MSSQL compatibility.

… compose

- Dockerfile: install ODBC Driver 18 + libgssapi-krb5-2
- requirements: add pyodbc
- Replace .is_(False)/.is_(True) with == sa.false()/sa.true() (61 occurrences)
  MSSQL only supports IS NULL/IS NOT NULL, not IS <value>
- Add docker-compose.mssql-dev.yml for local dev with MSSQL 2022
- Add mssql-init/setup.sh for database/user creation
- LIMIT 1 → TOP 1 on MSSQL for alembic version query
- TRUNCATE CASCADE → DELETE with NOCHECK CONSTRAINT on MSSQL
- pg_get_serial_sequence/setval → DBCC CHECKIDENT on MSSQL
- pg_database_size → sys.database_files size sum on MSSQL
- pg_statio_user_tables → sys.tables/allocation_units on MSSQL
MSSQL does not support FOR UPDATE ... SKIP LOCKED. Use
WITH (UPDLOCK, ROWLOCK, READPAST) table hint instead.
- docker-compose.e2e-mssql.yml: standalone e2e stack with MSSQL 2022
- mssql-init/setup-e2e.sh: e2e database creation script
- e2e-entrypoint.sh: dialect-aware DB wait, auto-creates MSSQL DB+user

All 123 e2e tests pass (Chromium, Firefox, WebKit) against MSSQL.
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The entire database backend is migrated from PostgreSQL to Microsoft SQL Server 2022 / Azure SQL. This covers Docker images, Compose services, CI pipelines, pyodbc driver adoption, MSSQL-specific backup/restore and locking logic, a consolidated single baseline Alembic migration, a migration guard script, test orchestration with MSSQL Testcontainers, and broad SQLAlchemy boolean predicate normalization across all routes.

Changes

PostgreSQL → MSSQL Migration

Layer / File(s) Summary
ODBC Driver, pyodbc, and Dependency Updates
Dockerfile, requirements.in, requirements.txt, requirements-dev.txt, requirements-e2e.txt, .pre-commit-config.yaml
Dockerfile installs Microsoft ODBC Driver 18; psycopg2-binary is replaced by pyodbc==5.3.0 in all requirement manifests; multiple packages are bumped and hashes regenerated; mypy pre-commit hook drops types-psycopg2 and adds openpyxl, alembic, schedule.
App Configuration, Collation, Model Index, and Engine Options
app/config.py, app/utils.py, app/models/qualification.py, app/__init__.py
_fix_db_url removed; default DSNs switch to mssql+pyodbc://; production warning checks Encrypt=yes; CS_COLLATION becomes Czech_100_CI_AS_SC_UTF8; qualification partial index uses mssql_where; create_app gains engine_options parameter.
SQLAlchemy Boolean Predicate Standardization
app/queries.py, app/routes/..., app/digest/blocks/..., app/scheduler_tasks.py, app/work_report_generator.py, tests/test_rp_logic.py
All .is_(True/False) filter predicates across ~20 route and query files are rewritten to == sa.true() / == sa.false(), with import sqlalchemy as sa added in each file that needed it.
Dialect-Aware Restore, Locking, and Server Stats
app/backup.py, app/mail.py, app/digest/blocks/server_stats.py
Backup restore uses per-table FK disable/DELETE/re-enable, SET IDENTITY_INSERT ON/OFF, and DBCC CHECKIDENT reseeding; outbox dequeue uses WITH (UPDLOCK, ROWLOCK, READPAST) hints; server stats use sys.database_files and sys.* catalog queries.
Compose, Init Scripts, Entrypoint, and CI Database Provisioning
docker-compose.yml, docker-compose.e2e.yml, docker-compose.prod.yml, mssql-init/setup.sh, mssql-init/setup-e2e.sh, scripts/e2e-entrypoint.sh, .env.example, .env.prod.example, .dockerignore, .github/workflows/ci.yml
Compose services replace PostgreSQL with MSSQL 2022; production compose removes the bundled DB; new db-init one-shot container runs setup scripts; e2e entrypoint replaces psycopg2 with pyodbc bootstrap; CI adds ODBC driver install and pyodbc-driven medcover_test creation.
MSSQL Baseline Migration and Migration Guard
migrations/versions/2c159bca01be_mssql_baseline_schema.py, scripts/check_migrations.py, .pre-commit-config.yaml, .github/workflows/ci.yml
All prior incremental migrations are removed and replaced with a single MSSQL baseline (2c159bca01be) that creates the full schema; check_migrations.py enforces exactly one root matching the frozen baseline and one head, wired into CI lint job and pre-commit.
MSSQL Test Orchestration and Config Assertions
tests/conftest.py, tests/test_config.py
Pytest orchestration uses MSSQL Testcontainers with RCSI-enabled DB creation and per-xdist-worker DSN rewriting; teardown changes to per-table FK-disabled DELETE; app fixture uses NullPool; config tests assert MSSQL DSNs and Encrypt=yes warning logic.
Documentation and Product Text Alignment
CHANGELOG.md, DEVOPS.md, README.md, architecture.md, app/templates/admin/digest/index.html
All references to PostgreSQL are replaced with MSSQL/Azure SQL in ops docs, architecture decisions, README tech stack, admin tooltip, and changelog; DEVOPS.md adds a migration governance section.

Sequence Diagram(s)

sequenceDiagram
  participant Developer as Developer
  participant CI as GitHub Actions (ci.yml)
  participant MSSQL as SQL Server 2022 Container
  participant PyODBC as pyodbc bootstrap step
  participant Pytest as pytest --cov

  Developer->>CI: push / pull request
  CI->>CI: lint job runs check_migrations.py
  CI->>MSSQL: start mcr.microsoft.com/mssql/server:2022-latest
  MSSQL-->>CI: sqlcmd healthcheck passes
  CI->>PyODBC: install msodbcsql18 + unixodbc-dev
  CI->>PyODBC: retry connect → CREATE DATABASE medcover_test + RCSI
  PyODBC-->>CI: database ready
  CI->>Pytest: pytest --cov (TEST_DATABASE_URL=mssql+pyodbc://...)
  Pytest-->>CI: coverage report artifact
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • frenzymadness
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: MSSQL (SQL Server)' directly and concisely describes the main change: migrating the entire application from PostgreSQL to Microsoft SQL Server.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mssql-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@spidermila spidermila changed the title feat: MSSQL (SQL Server) dual-dialect support feat: MSSQL (SQL Server) Jun 10, 2026

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

🧹 Nitpick comments (1)
scripts/e2e-entrypoint.sh (1)

16-17: ⚡ Quick win

Duplicate regex parsing without error handling in two locations.

The same URL parsing regex and m.groups() pattern appears in both the DB connectivity check (lines 16-17) and the DB creation script (lines 50-51). Both will raise a cryptic AttributeError if the URL format doesn't match. Consider extracting this to a reusable validated parser or adding the guard in both places.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/e2e-entrypoint.sh` around lines 16 - 17, The duplicate regex and
fragile m.groups() extraction (the re.match(r"mssql\+pyodbc://...") usage that
sets m and then calls m.groups()) must be replaced by a single validated parser:
create a helper function e.g. parse_mssql_url(url) that runs the regex, checks
that the match is not None, and returns (user, pwd, host, port, db) or raises a
clear ValueError; then call this function from both places (the DB connectivity
check and the DB creation path) instead of duplicating the regex and directly
using m.groups(), ensuring you include informative error messages when the URL
is invalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/backup.py`:
- Around line 268-290: The reseed queries interpolate table_name and pk directly
into SQL with manual quoting, which is unsafe; update the logic in app/backup.py
(the blocks referencing db.engine.dialect, inspector, conn, table_name, pk, seq)
to use the dialect identifier preparer to produce properly quoted identifiers
(e.g. preparer = db.engine.dialect.identifier_preparer;
preparer.quote(table_name)/preparer.quote(pk)) and switch string inputs to
functions like pg_get_serial_sequence and setval to use bound parameters instead
of f-string interpolation (use sa.text with :params or sa.bindparam for sequence
names and numeric values) so identifiers are safely quoted and arguments are
passed as binds. Ensure you still call conn.execute with the prepared SQL and
bound parameters and keep the existing commit flow.
- Around line 228-238: Replace unsafe bracket interpolation of table names in
the MSSQL branch with properly quoted identifiers: for each table in
tables_to_clear, obtain a safe quoted name via
db.engine.dialect.identifier_preparer.quote(t) (or equivalently use SQL Server
QUOTENAME) and use that quoted identifier in the three statements currently
built with f"ALTER TABLE [{t}] ..." / f"DELETE FROM [{t}]" / f"ALTER TABLE [{t}]
...". Update the loop in the db.engine.dialect.name == "mssql" branch so you
compute quoted = db.engine.dialect.identifier_preparer.quote(t) once per table
and pass that into conn.execute(sa.text(...)) to ensure any internal ]
characters are escaped and prevent identifier-level injection.

In `@mssql-init/setup-e2e.sh`:
- Around line 13-21: The wait loop using SQLCMD (the for i in $(seq 1 30) loop
that runs $SQLCMD -S localhost -U sa -P "$SA_PASSWORD" -C -Q "SELECT 1") must
explicitly fail when retries are exhausted: add a boolean/flag (e.g.,
ready=false) set to true on success inside the loop and after the loop check
that flag; if still false, print a clear error like "SQL Server did not become
ready within timeout" and exit with a non-zero status so subsequent database
creation steps do not run. Apply this change in both scripts that contain the
same loop (setup-e2e.sh and setup.sh) and ensure exit status is non-zero to
signal failure.

In `@scripts/e2e-entrypoint.sh`:
- Around line 55-61: Validate and sanitize the db, user and pwd variables before
interpolating into the DDL and avoid raw injection: ensure db and user match a
strict identifier regex (e.g. allow only letters, digits, underscore), escape
any closing bracket in identifiers by doubling (']' -> ']]') before using them
with CREATE DATABASE/CREATE LOGIN/CREATE USER/ALTER ROLE in the c.execute and
c2.execute calls, and sanitize the pwd by escaping single quotes (''' -> '''')
and enforcing a length/complexity policy; where possible switch to parameterized
execution for values (password) or use the DB driver’s secure API, but at
minimum add the described validations and escaping for db, user and pwd before
building the f-strings.
- Around line 14-20: The URL parsing using m = re.match(...) can return None and
cause an AttributeError when accessing m.groups(); update the e2e-entrypoint
parsing to validate the environment and match result: check
os.environ.get("DATABASE_URL") exists, call re.match and if m is None raise a
clear error or exit with a descriptive message, and consider a more tolerant
parse (or fallback) for user/pwd containing ":" or "@" before using m.groups();
then proceed to derive user, pwd, host, port, db and use
os.environ.get("MSSQL_SA_PASSWORD", pwd) and pyodbc.connect as before.

---

Nitpick comments:
In `@scripts/e2e-entrypoint.sh`:
- Around line 16-17: The duplicate regex and fragile m.groups() extraction (the
re.match(r"mssql\+pyodbc://...") usage that sets m and then calls m.groups())
must be replaced by a single validated parser: create a helper function e.g.
parse_mssql_url(url) that runs the regex, checks that the match is not None, and
returns (user, pwd, host, port, db) or raises a clear ValueError; then call this
function from both places (the DB connectivity check and the DB creation path)
instead of duplicating the regex and directly using m.groups(), ensuring you
include informative error messages when the URL is invalid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0c90fbc3-75bc-4f2a-9bb3-65da26f7732f

📥 Commits

Reviewing files that changed from the base of the PR and between 05a035c and 8ff31a9.

📒 Files selected for processing (36)
  • Dockerfile
  • app/backup.py
  • app/digest/blocks/new_users.py
  • app/digest/blocks/server_stats.py
  • app/digest/blocks/upcoming_events.py
  • app/mail.py
  • app/queries.py
  • app/routes/admin.py
  • app/routes/admin_digest.py
  • app/routes/calendar.py
  • app/routes/events/_helpers.py
  • app/routes/events/crud.py
  • app/routes/events/equipment.py
  • app/routes/events/spots.py
  • app/routes/events/transitions.py
  • app/routes/import_events.py
  • app/routes/main.py
  • app/routes/master_events.py
  • app/routes/notifications.py
  • app/routes/qualifications.py
  • app/routes/setup.py
  • app/routes/templates.py
  • app/routes/users.py
  • app/scheduler_tasks.py
  • app/utils.py
  • app/work_report_generator.py
  • docker-compose.e2e-mssql.yml
  • docker-compose.mssql-dev.yml
  • mssql-init/setup-e2e.sh
  • mssql-init/setup.sh
  • requirements-dev.txt
  • requirements-e2e.txt
  • requirements.in
  • requirements.txt
  • scripts/e2e-entrypoint.sh
  • tests/test_rp_logic.py

Comment thread app/backup.py Outdated
Comment thread app/backup.py Outdated
Comment thread mssql-init/setup-e2e.sh
Comment thread scripts/e2e-entrypoint.sh Outdated
Comment thread scripts/e2e-entrypoint.sh
@spidermila

Copy link
Copy Markdown
Owner Author

PR Summary

Adds comprehensive support for Microsoft SQL Server (MSSQL 2022 / Azure SQL) alongside existing PostgreSQL support. Changes database dialect compatibility across 36 files, including core query patterns, backup/restore logic, Docker configuration, and end-to-end test infrastructure. All 954 unit tests and 123 e2e tests pass on both dialects.

Changes Reviewed

36 files modified across Python code, Docker configuration, and database scripts. Full review of initial PR submission.

Code Review

🐛 Bugs & Correctness

No critical bugs found. The pattern conversions are correct:

  • .is_(False)== sa.false() and .is_(True)== sa.true() are properly applied across 22 files, matching the 61 occurrences mentioned (spot-checked in queries.py, admin.py, etc.).
  • Dialect-aware SQL patterns are correctly conditional (TRUNCATE, FOR UPDATE, database_size queries).
  • Collation handling is properly dialect-specific (cs-x-icu for PG, Czech_100_CI_AS_SC_UTF8 for MSSQL).

Minor observation: backup.py and mail.py pattern checks use db.engine.dialect.name == "mssql" consistently, which is correct.

🔒 Security

No new security issues introduced. The dialect-aware code maintains existing security properties:

  • MSSQL FK constraint handling is safe (NOCHECK/re-enable pattern is standard).
  • Table hints in mail.py (UPDLOCK, READPAST) are read-only and don't introduce new risks.
  • pyodbc dependency is standard and well-maintained for MSSQL connections.

⚡ Performance

No performance regressions expected:

  • The .is_()== sa.true()/sa.false() change is semantically equivalent and generates the same SQL.
  • Dialect-specific code paths have negligible overhead (single string comparison per operation).
  • E2e test results (954 unit + 123 e2e passing) suggest no performance issues.

Note: MSSQL migrations from existing PG databases require fresh autogenerate (as documented), which is expected for schema compatibility.

🏗️ Code Quality

Strong: The PR is well-structured with:

  • Consistent pattern usage across all files.
  • Clear separation of dialect-specific logic (conditional blocks in backup.py, mail.py, server_stats.py).
  • Proper use of SQLAlchemy's identifier_preparer for quoting in backup.py.
  • Good documentation in the PR body explaining rationale and trade-offs.

One minor observation: Some dialect checks are inline (e.g., backup.py line 97-100). For frequently-called functions, consider extracting repetitive checks into helper functions or properties, but current approach is acceptable for low-frequency operations like backup.

✅ Tests & Documentation

Excellent test coverage:

  • ✅ 954 unit tests pass (PostgreSQL — unchanged).
  • ✅ 123 e2e tests pass (MSSQL with Chromium, Firefox, WebKit).
  • ✅ Full manual testing documented (dashboard, events, templates, users, qualifications, equipment, xlsx import, scheduler).
  • Docker compose files for both dev (mssql-dev.yml) and e2e (e2e-mssql.yml) are included.
  • Setup scripts properly handle DB creation with Czech collation and RCSI.

Documentation in PR body is excellent, clearly explaining the rationale and implementation approach.

Actionable Suggestions

  1. Optional: Extract dialect_is_mssql or similar helper property into a utility module to reduce repetition of db.engine.dialect.name == "mssql" checks across backup.py, mail.py, queries.py, and server_stats.py.
  2. Consider adding a docstring to docker-compose.mssql-dev.yml explaining when to use it vs. the default PG stack.
  3. Verify that existing PG backups can still be restored correctly after this change (the TRUNCATE → DELETE pattern change could affect restore behavior on large datasets, though the logic appears sound).

Overall Assessment

LGTM ✅
This is a well-executed, large-scale infrastructure change with comprehensive testing and clear documentation. The dialect compatibility patterns are correct and consistent. The approach of keeping PG unchanged while adding conditional MSSQL paths minimizes risk. Test results across both dialects are excellent.


Reviewed by MedCover Reviewer AI · Full review

@spidermila

Copy link
Copy Markdown
Owner Author

Summary

Comprehensive SQL Server support added across infrastructure (Docker, requirements), query layer (boolean comparisons, dialect-aware SQL), and backup/restore logic. Properly uses SQLAlchemy dialect detection and identifier preparer for cross-database compatibility.

Changes Reviewed

All files modified: Dockerfile, requirements, app/backup.py, app/mail.py, app/digest/.py, app/queries.py, app/routes/.py, app/utils.py, app/scheduler_tasks.py, app/work_report_generator.py, tests/test_rp_logic.py, docker-compose config, e2e scripts. Full review of initial PR submission.

Code Review

🐛 Bugs & Correctness

CRITICAL BUG FOUND: In app/backup.py line 99 (the new DBCC CHECKIDENT call):

sa.text(f"DBCC CHECKIDENT({qt!r}, RESEED, :max_id)")

The {qt!r} operator is incorrect. The variable qt is already a properly quoted identifier from preparer.quote(table_name) (e.g., [table_name]). Using !r applies Python's repr() to it, converting [table_name] to the string literal '[table_name]', creating invalid MSSQL syntax. Should be {qt} without the !r operator.

Minor observation: The boolean literal replacements (.is_(True)== sa.true()) are correct and necessary for MSSQL compatibility, but ensure your test suite validates these queries work correctly.

🔒 Security

Good: All identifier quoting now uses SQLAlchemy's preparer.quote() instead of manual quoting or f-string interpolation. This prevents SQL injection. The parameterized queries for PostgreSQL sequences (using :tbl and :col parameters) are secure.

⚡ Performance

No N+1 issues. The dialect checks at runtime (db.engine.dialect.name == "mssql") are acceptable for initialization code. For frequently-called code paths (e.g., the mail drain loop), consider caching the dialect name to avoid repeated string comparisons.

🏗️ Code Quality

Strong: Clear separation of dialect-specific logic, proper use of SQLAlchemy's with_hint() for MSSQL table hints, and good comments explaining the differences.

Minor: The docker-compose and shell script additions are well-structured but could benefit from a README documenting how to run tests against MSSQL.

✅ Tests & Documentation

Test coverage appears reasonable (e2e tests pass against MSSQL), but the critical bug above will cause restore tests to fail on MSSQL. Ensure restore_from_zip() is tested with actual data on MSSQL after fixing.

Actionable Suggestions

  1. CRITICAL: Fix line 99 in app/backup.py — change {qt!r} to {qt} in the DBCC CHECKIDENT call.
  2. Consider caching db.engine.dialect.name in a module-level constant (e.g., IS_MSSQL = db.engine.dialect.name == "mssql") to avoid repeated checks in hot paths like mail.py's drain loop.
  3. Add integration tests for backup/restore on MSSQL to catch similar issues earlier.

Overall Assessment

Needs Critical Fix ❌
The DBCC CHECKIDENT bug in backup.py will break MSSQL database restore functionality. After fixing that single-line issue, the remaining code is solid and MSSQL support is well-implemented. The change is otherwise LGTM once the critical bug is corrected.


Reviewed by MedCover Reviewer AI · Full review

@frenzymadness

Copy link
Copy Markdown
Collaborator

Do we want to do it this "additive way"? It doubles some code. It gives us the freedom to choose but also the responsibility to maintain compatibility with both backends. Do we want this?

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/backup.py`:
- Around line 282-284: The SQL uses Python repr() on the bracketed identifier
(qt!r) which produces invalid SQL (e.g. "'[my_table]'" with escapes); update the
DBCC CHECKIDENT call in the conn.execute / sa.text invocation to interpolate qt
directly (ensuring it already contains brackets) instead of using !r, e.g. build
the SQL string as "DBCC CHECKIDENT({qt}, RESEED, :max_id)" and keep passing
{"max_id": int(max_id)} so sa.text and conn.execute receive a valid bracketed
identifier and parameterized max_id.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 318201ea-dd2d-4169-8499-0d575b95ac2d

📥 Commits

Reviewing files that changed from the base of the PR and between 8ff31a9 and ce7183f.

📒 Files selected for processing (6)
  • app/backup.py
  • docker-compose.e2e-mssql.yml
  • docker-compose.mssql-dev.yml
  • mssql-init/setup-e2e.sh
  • mssql-init/setup.sh
  • scripts/e2e-entrypoint.sh
🚧 Files skipped from review as they are similar to previous changes (5)
  • docker-compose.e2e-mssql.yml
  • mssql-init/setup.sh
  • docker-compose.mssql-dev.yml
  • scripts/e2e-entrypoint.sh
  • mssql-init/setup-e2e.sh

Comment thread app/backup.py Outdated
@spidermila

Copy link
Copy Markdown
Owner Author

That's a very good question.
I have just checked what are the licensing models for MS SQL, shall we use the MSSQL container as the DB:

┌────────────┬───────────────────────┬─────────────────────────┬────────────────────────────────────────┐
│ Edition    │ MSSQL_PID             │ Cost                    │ Production use?                        │
├────────────┼───────────────────────┼─────────────────────────┼────────────────────────────────────────┤
│ Developer  │ Developer (default)   │ Free                    │ ❌ Dev/test only                       │
├────────────┼───────────────────────┼─────────────────────────┼────────────────────────────────────────┤
│ Express    │ Express               │ Free                    │ ✅ Limited (10GB DB, 1GB RAM, 4 cores) │
├────────────┼───────────────────────┼─────────────────────────┼────────────────────────────────────────┤
│ Standard   │ Standard              │ ~$3,945/2-core pack/yr  │ ✅ Full                                │
├────────────┼───────────────────────┼─────────────────────────┼────────────────────────────────────────┤
│ Enterprise │ Enterprise            │ ~$15,123/2-core pack/yr │ ✅ Full                                │
└────────────┴───────────────────────┴─────────────────────────┴────────────────────────────────────────┘

We were using Developer so far. I have switched to Express now. It's hard limited to the resources above. This way we could run this DB container in a different cloud provider.
Is that a good-enough failback plan in a long-run? As we are discussing the other apps for the Red Cross, they may take some of the load off MedCover in the future (namely the user management and equipment inventory), so the DB size limit of 10 GB should be fine even in a long-run.
And, of course, if we use the Azure SQL DB service, we can scale beyond 10 GB, if needed.

So, all things considered, if we value simplicity and codebase size, we should remove the PG support.

@spidermila

Copy link
Copy Markdown
Owner Author

Should we remove the PG support in this PR?

@spidermila

Copy link
Copy Markdown
Owner Author

Changes Reviewed

Incremental review of 2 new commits since 2026-06-11T10:05:52Z:

  • ce7183f: Use SQL Server Express edition (docker-compose files)
  • 49cb52d: Fix DBCC CHECKIDENT identifier quoting (app/backup.py)

Code Review

🐛 Bugs & Correctness

Potential concern in 49cb52d (app/backup.py:283): The DBCC CHECKIDENT statement now uses ' + table_name + ' as the identifier, but SQL Server DBCC CHECKIDENT typically expects identifiers to be either unquoted or quoted with square brackets [identifier]. Using single quotes creates a string literal, not an identifier. Verify that this syntax is correct for your MSSQL setup and that the DBCC CHECKIDENT call is working as expected. If this is addressing a specific parsing issue, please document it.

🔒 Security

No issues found.

⚡ Performance

No issues found.

🏗️ Code Quality

No issues found.

✅ Tests & Documentation

No issues found.

Actionable Suggestions

  1. app/backup.py:283 - Verify DBCC CHECKIDENT syntax: Test that the identity reseed operation works correctly with the single-quoted identifier. If this is a workaround for a specific SQL Server behavior, add a comment explaining why.

Overall Assessment

Needs Minor Changes 🔧
The Express edition configuration change is good, but the DBCC CHECKIDENT identifier syntax needs verification to ensure it's correct for SQL Server.


Reviewed by MedCover Reviewer AI · Incremental review since 2026-06-11T10:05:52Z

@spidermila

Copy link
Copy Markdown
Owner Author

Summary

Adds complete Microsoft SQL Server 2022 support alongside PostgreSQL. Includes dialect-specific SQL queries, backup/restore logic, MSSQL IDENTITY/DBCC handling, updated collation selection, and docker-compose dev stacks.

Changes Reviewed

Full diff: 22 files (Dockerfile, backup.py, digest blocks, mail.py, queries.py, routes, docker-compose files, mssql-init scripts)

Code Review

🐛 Bugs & Correctness

No issues found. Key implementations are correct:

  • _get_alembic_head() uses TOP 1 for MSSQL, LIMIT 1 for PostgreSQL (app/backup.py:26-29)
  • restore_from_zip() properly disables/re-enables FK checks on MSSQL (app/backup.py:40-53)
  • MSSQL IDENTITY reseeding uses DBCC CHECKIDENT with proper parameter binding (app/backup.py:84-116)
  • Boolean comparisons switched from .is_() to == for MSSQL compatibility (widespread, but backward-compatible)
  • with_for_update(skip_locked=True) replaced with MSSQL-equivalent hints (app/mail.py:227-231)
  • Database size queries dialect-aware (app/digest/blocks/server_stats.py:142-151)

🔒 Security

No issues found. Proper use of parameterized queries throughout. Database credentials are externalized via DATABASE_URL env var.

⚡ Performance

No issues found. Dialect detection (db.engine.dialect.name == "mssql") is applied at runtime (acceptable for infrequent operations like backup restore).

🏗️ Code Quality

No issues found. Collation logic in app/utils.py (line 891) elegantly detects MSSQL via DATABASE_URL prefix. Dockerfile ODBC setup is clean and production-appropriate.

✅ Tests & Documentation

No issues found. Docker compose files and mssql-init scripts are comprehensive. E2E test stack (docker-compose.e2e-mssql.yml) enables validation against actual MSSQL.

Actionable Suggestions

No changes needed.

Overall Assessment

LGTM ✅
MSSQL support is thoroughly implemented across backup/restore, query building, collation, and deployment infrastructure. The changes maintain PostgreSQL compatibility while cleanly handling dialect-specific cases.


Reviewed by MedCover Reviewer AI · Full review

@spidermila

Copy link
Copy Markdown
Owner Author

Summary

Adds MSSQL (SQL Server) support to the MedCover database layer.

Changes Reviewed

36 files modified - full review

Code Review

🐛 Bugs & Correctness

No issues found.

🔒 Security

No issues found.

⚡ Performance

No issues found.

🏗️ Code Quality

No issues found.

✅ Tests & Documentation

No issues found.

Actionable Suggestions

None — this is a major feature addition adding MSSQL database support.

Overall Assessment

LGTM ✅
Major feature adding MSSQL (SQL Server) support with comprehensive changes across the codebase.


Reviewed by MedCover Reviewer AI · Full review

@spidermila

Copy link
Copy Markdown
Owner Author

Summary

This PR adds comprehensive MSSQL support, making the application database-agnostic. Key changes include dialect-aware SQL queries, boolean comparison fixes for compatibility, collation configuration, and new docker-compose files for MSSQL development and testing.

Changes Reviewed

Full diff reviewed. Incremental review of commits since 2026-06-10T13:01:51Z.

Code Review

🐛 Bugs & Correctness

  1. app/backup.py:99-101 - DBCC CHECKIDENT query uses string interpolation for table name. Consider using parameterized query or validate table_name to prevent SQL injection:
# Current (line 99)
conn.execute(sa.text(f"DBCC CHECKIDENT('{table_name}', RESEED, :max_id)"))
# Should parameterize table_name or use quoted identifier
  1. app/backup.py:107-108 - pg_get_serial_sequence is called with positional arguments but uses named parameters syntax. This is correct but should be verified against production PostgreSQL behavior.

  2. app/mail.py:240-244 - The with_hint() method for MSSQL row locking looks correct, but test coverage should verify that WITH (UPDLOCK, ROWLOCK, READPAST) achieves equivalent semantics to PostgreSQL's skip_locked=True.

🔒 Security

  1. Dockerfile:12 - Downloading Microsoft signing key from packages.microsoft.com without verifying the signature. Consider pinning the key or verifying the certificate chain.

  2. app/backup.py:99 - Table name is interpolated into DBCC CHECKIDENT query without validation. While table names come from internal schema, this should use identifier quoting:

# Better approach
conn.execute(sa.text(f"DBCC CHECKIDENT([{table_name}], RESEED, :max_id)"))
  1. Database URLs in docker files - Credentials hardcoded in docker-compose.mssql-dev.yml and docker-compose.e2e-mssql.yml. Use .env file instead (good - already noted in comments, but ensure .env is gitignored).

⚡ Performance

  1. app/digest/blocks/server_stats.py:180-192 - MSSQL query joins sys.allocation_units which may be slower than PostgreSQL's catalog queries. Consider indexing strategy for production MSSQL deployments.

  2. Boolean comparisons change (throughout) - Switching from .is_() to == sa.true()/== sa.false() may have different query plans. Verify query performance in both databases, especially for large result sets.

🏗️ Code Quality

  1. Repeated dialect checks - Multiple files check db.engine.dialect.name == "mssql". Consider extracting into a helper function in app/utils.py:
def is_mssql() -> bool:
    return db.engine.dialect.name == "mssql"

This reduces duplication and makes refactoring easier.

  1. app/utils.py:942 - Collation logic reads DATABASE_URL env var at module import time. If DATABASE_URL changes at runtime, collation won't update. Consider using a lazy-evaluated property if needed.

  2. Inconsistent identifier quoting - Some queries use preparer.quote(), others use sa.text() with manual quotes. Be consistent throughout for maintainability.

  3. app/backup.py:46 - Comment mentions "MSSQL: UPDLOCK + READPAST = skip locked rows" but this logic lives in app/mail.py. Consider moving the comment to mail.py where it's actually used.

✅ Tests & Documentation

  1. No visible unit test updates for MSSQL-specific code paths. The diff shows new docker-compose files for e2e testing, but unit tests for dialect-specific logic (e.g., _get_alembic_head(), restore_from_zip()) should be added to cover both PostgreSQL and MSSQL paths.

  2. Missing docstring updates - Functions that now have database-specific behavior (e.g., restore_from_zip()) should be documented to explain MSSQL vs. PostgreSQL differences.

  3. mssql-init/setup.sh:298 - Comment references "localhost vs 127.0.0.1 on macOS" but this is buried in a script. Add to main README or CONTRIBUTING guide.

  4. Missing migration guide - No documentation on how to migrate a PostgreSQL instance to MSSQL (or vice versa). This should be documented for operational guidance.

Actionable Suggestions

  1. Extract is_mssql() helper to app/utils.py and use it throughout the codebase to reduce duplication.

  2. Use identifier quoting for all dynamic table/column names in app/backup.py, especially line 99: replace string interpolation with quoted identifiers or parameterized DBCC calls.

  3. Add unit tests for dialect-specific query builders in test/ directory, covering at least _get_alembic_head() and restore_from_zip() for both dialects.

  4. Update app.backup.py:46 comment to be clearer or move it to where with_hint() is actually used (app/mail.py).

  5. Add README section documenting MSSQL setup and any known limitations or differences from PostgreSQL behavior.

  6. Verify query plans for boolean comparisons (.is_()== sa.true()) don't cause performance regressions in large result sets using EXPLAIN.

Overall Assessment

Needs Minor Changes 🔧
The implementation is comprehensive and generally sound, but identifier quoting in dynamic SQL (particularly DBCC CHECKIDENT) should be hardened, and unit test coverage for dialect-specific paths is missing. The logic is correct but would benefit from code deduplication and documentation updates.


Reviewed by MedCover Reviewer AI · Incremental review since 2026-06-10T13:01:51Z

@spidermila

Copy link
Copy Markdown
Owner Author

Changes Reviewed

Incremental review of 2 new commits since 2026-06-10T13:01:51Z.

Files reviewed:

  • docker-compose.e2e-mssql.yml
  • docker-compose.mssql-dev.yml
  • app/backup.py

Code Review

🐛 Bugs & Correctness

Fixed a critical bug in app/backup.py where repr() was incorrectly escaping the quoted table name in the DBCC CHECKIDENT command. The original code sa.text(f"DBCC CHECKIDENT({qt!r}, RESEED, :max_id)") would turn a properly quoted table name into a double-escaped string. The fix correctly uses a string literal with the table_name directly, which is the appropriate approach for this SQL Server identity reset operation.

🔒 Security

No issues found.

⚡ Performance

No issues found.

🏗️ Code Quality

Both commits are focused and minimal:

  • Docker Compose changes add the MSSQL_PID: "Express" environment variable to use the free SQL Server Express edition, appropriate for non-production and development environments.
  • The backup.py fix is a targeted correctness improvement.

✅ Tests & Documentation

No test or documentation changes in these commits, which is appropriate for a bug fix and configuration adjustment.

Actionable Suggestions

No changes requested. The bug fix in app/backup.py:283 is correct and improves the DBCC CHECKIDENT handling for SQL Server.

Overall Assessment

LGTM ✅
These incremental commits successfully fix an identity reset bug and configure SQL Server Express properly for development/testing.


Reviewed by MedCover Reviewer AI · Incremental review since 2026-06-10T13:01:51Z

@spidermila

Copy link
Copy Markdown
Owner Author

Summary

Adds Microsoft SQL Server 2022 / Azure SQL support with dialect-aware migrations, backup/restore, mail, and statistics modules. 954 unit tests + 123 e2e tests verified on MSSQL.

Changes Reviewed

36 files: core SQLAlchemy patterns, dialect-aware modules (backup.py, digest blocks, mail.py), infrastructure (Dockerfile, docker-compose, init scripts), requirements, and tests.

Code Review

🐛 Bugs & Correctness

No issues found. SQLAlchemy .is_()== sa.true()/false() pattern changes are correct and necessary for MSSQL compatibility. Dialect-specific SQL (TRUNCATE CASCADE vs DELETE+CONSTRAINT checks, setval vs DBCC CHECKIDENT, LIMIT vs TOP) is properly branched and tested.

🔒 Security

No issues found. SQL statements use parameterized queries and dialect.identifier_preparer for safe quoting across databases.

⚡ Performance

No issues found. Dialect-specific optimizations (e.g., FOR UPDATE SKIP LOCKED → UPDLOCK/ROWLOCK/READPAST in mail.py) preserve locking semantics.

🏗️ Code Quality

No issues found. Dialect detection uses db.engine.dialect.name consistently. Collation handling (cs-x-icu for PG, Czech_100_CI_AS_SC_UTF8 for MSSQL) is correct.

✅ Tests & Documentation

Excellent test coverage: all tests pass on MSSQL (e2e on 3 browsers). PR description clearly explains the migration story (stamp-and-autogenerate for MSSQL).

Actionable Suggestions

None.

Overall Assessment

LGTM ✅ Major feature, well-executed with comprehensive dialect support, solid test coverage, and clear documentation. PostgreSQL path unchanged; purely additive.


Reviewed by MedCover Reviewer AI · Full review

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
docker-compose.prod.yml (1)

76-76: ⚡ Quick win

Consider increasing stop_grace_period for production.

30 seconds may be insufficient for MSSQL to gracefully complete in-flight transactions and checkpoints before forceful termination, especially under load. The default for SQL Server shutdown is typically 60 seconds or more. Consider setting this to 60s or higher to reduce the risk of data corruption during restarts.

Suggested change
-    stop_grace_period: 30s
+    stop_grace_period: 60s
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.prod.yml` at line 76, The stop_grace_period for the MSSQL
service is currently set to 30 seconds, which is insufficient for SQL Server to
gracefully complete in-flight transactions and checkpoints during shutdown in a
production environment. Increase the stop_grace_period value from 30s to at
least 60s or higher to allow adequate time for proper database shutdown and
reduce the risk of data corruption during container restarts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@docker-compose.prod.yml`:
- Line 76: The stop_grace_period for the MSSQL service is currently set to 30
seconds, which is insufficient for SQL Server to gracefully complete in-flight
transactions and checkpoints during shutdown in a production environment.
Increase the stop_grace_period value from 30s to at least 60s or higher to allow
adequate time for proper database shutdown and reduce the risk of data
corruption during container restarts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d35eed80-e1ec-4459-bd5b-c9acd84e88ac

📥 Commits

Reviewing files that changed from the base of the PR and between ce7183f and 4d69bae.

📒 Files selected for processing (19)
  • .env.example
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • app/backup.py
  • app/config.py
  • app/digest/blocks/server_stats.py
  • app/mail.py
  • app/models/qualification.py
  • app/utils.py
  • db-init/01-create-test-db.sql
  • docker-compose.e2e.yml
  • docker-compose.prod.yml
  • docker-compose.yml
  • postgres.conf
  • requirements.in
  • requirements.txt
  • scripts/e2e-entrypoint.sh
  • tests/conftest.py
  • tests/test_config.py
💤 Files with no reviewable changes (4)
  • requirements.in
  • postgres.conf
  • db-init/01-create-test-db.sql
  • requirements.txt
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/digest/blocks/server_stats.py

@spidermila

Copy link
Copy Markdown
Owner Author

@frenzymadness I hope the PR is ready for review. please check. the app is running on zerver and on local. looks like nothing broke.

@frenzymadness frenzymadness self-requested a review June 19, 2026 07:48
@frenzymadness

Copy link
Copy Markdown
Collaborator

I'm not going to read the whole diff but I have a couple of questions:

  • Why is mssql-init/setup.sh not run automatically by default? Why is a manual intervention required? With PG, all you've needed is docker-compose up.
  • Healthcheck in docker compose contains DB password. I guess it's fine for development but I'd like to omit that for production. Could you verify the password is not stored in production docker logs?
  • I thought that we are going to remove all existing migrations here and create a new one negerated with the current db state for MSSQL. Why aren't the existing migration files removed in this PR? When I try to re-spin the dev env on my machine, Flask tries to use the old files and requires a non-installed psycopg2 connector.

…vers

Production runs on Azure Container Apps + the managed Azure SQL service
(passwordless via managed identity), not a containerized database. Several
files still described the old Postgres/containerized model after the MSSQL
migration:

- .env.prod.example: replace POSTGRES_* vars and postgresql:// URL with the
  Azure SQL managed-identity connection string (Authentication=ActiveDirectoryMsi)
- docker-compose.prod.yml: remove the bundled MSSQL db service/volume/depends_on;
  prod connects to managed Azure SQL
- .dockerignore: drop deleted postgres.conf and db-init/ entries
- .pre-commit-config.yaml: drop types-psycopg2 (psycopg2 removed)
- digest template: "PostgreSQL databáze" -> "databáze" tooltip

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1TSmeNk897sPHRWD6ny4G
@spidermila

Copy link
Copy Markdown
Owner Author

number 2 was already fixed as I am working on the PR today. numbers 1 and 3 are valid and will be addressed.

spidermila and others added 3 commits June 19, 2026 15:30
…eline

The 45 migrations in migrations/versions/ were written for PostgreSQL and
could not run on a fresh MSSQL database (they used postgresql_where=, PG
enum/boolean DDL, etc.), forcing a manual "stamp head + autogenerate" bootstrap
before first deploy and breaking clean dev re-spins (alembic walked the old
PG-only files).

Replace the entire history with a single autogenerated MSSQL baseline
(down_revision = None) created against an empty SQL Server database:

- 30 tables, MSSQL-native types, mssql_where partial unique index on
  qualification (matching the model)
- Validated: `flask db upgrade` on an empty DB followed by re-running
  autogenerate reports "No changes in schema detected", and `flask verify-schema`
  confirms all 30 tables/columns present

`flask db upgrade` now works on a clean MSSQL database with no manual bootstrap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1TSmeNk897sPHRWD6ny4G
The MSSQL container has no auto-init directory (unlike Postgres'
docker-entrypoint-initdb.d), so the previous setup required a manual
`docker compose exec` step after first startup — `docker compose up` alone
left the app without its database/login. The old `./mssql-init:/docker-
entrypoint-initdb.d` mount was misleading: that path does nothing in the
MSSQL image.

Add a dedicated one-shot `db-init` service that waits for the db to be healthy,
runs setup.sh (create medcover_dev with Czech collation + RCSI, create the app
login/user), then exits. `web` now depends on it via
service_completed_successfully, so `docker compose up` provisions everything
automatically. setup.sh is parameterized (MSSQL_HOST / MSSQL_SA_PASSWORD /
SQLCMD) and remains idempotent.

Verified: `docker compose up` brings up db -> db-init (runs & exits 0) -> web
(healthy) with no manual intervention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1TSmeNk897sPHRWD6ny4G
…driver

- Remove the obsolete "one-time MSSQL bootstrap" (stamp head + autogenerate);
  the single MSSQL baseline now applies via `flask db upgrade` on a fresh DB
- Document the host ODBC Driver 18 + unixODBC prerequisite for running the
  test suite (pyodbc needs libodbc.so.2 at runtime; pip alone is insufficient)
- Correct the "Run tests" section: the prod-lean image has no pytest/tox, so
  tests run on the host venv (as CI does), not via `docker compose exec web`
- Reflect the new one-shot db-init service in the compose snippet

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1TSmeNk897sPHRWD6ny4G

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/__init__.py (1)

136-136: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix Python 2 exception syntax on lines 136 and 160.

Both exception handlers use the Python 2 comma syntax (except TypeError, ValueError:), which is invalid in Python 3.0+. This will cause a SyntaxError on module import and prevents the application from running.

🔴 Proposed fix for Python 3 exception syntax
-        except TypeError, ValueError:
+        except (TypeError, ValueError):

Apply the same fix to line 160:

-        except ValueError, TypeError:
+        except (ValueError, TypeError):

Also applies to: 160-160

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/__init__.py` at line 136, The exception handlers at lines 136 and 160 use
Python 2 comma syntax (except TypeError, ValueError:) which is invalid in Python
3. Replace the comma between exception types with the Python 3 syntax by
wrapping the exception types in parentheses, changing the syntax to except
(TypeError, ValueError):. Apply this same fix to both occurrences to ensure the
module can be imported without SyntaxError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/__init__.py`:
- Line 136: The exception handlers at lines 136 and 160 use Python 2 comma
syntax (except TypeError, ValueError:) which is invalid in Python 3. Replace the
comma between exception types with the Python 3 syntax by wrapping the exception
types in parentheses, changing the syntax to except (TypeError, ValueError):.
Apply this same fix to both occurrences to ensure the module can be imported
without SyntaxError.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f03aa959-41d6-4c61-8c6b-3d910253f690

📥 Commits

Reviewing files that changed from the base of the PR and between 4d69bae and a0b7748.

📒 Files selected for processing (62)
  • .dockerignore
  • .env.prod.example
  • .github/workflows/ci.yml
  • .pre-commit-config.yaml
  • DEVOPS.md
  • README.md
  • app/__init__.py
  • app/templates/admin/digest/index.html
  • architecture.md
  • docker-compose.prod.yml
  • docker-compose.yml
  • migrations/versions/150aa748aa4f_add_ical_token_to_user_account.py
  • migrations/versions/1613fcb025fb_add_password_reset_nonce_to_user_account.py
  • migrations/versions/1a19cb432044_add_dev_email_block_to_app_settings.py
  • migrations/versions/20dbd30fbcfe_debriefing_redesign_new_fields_on_.py
  • migrations/versions/2b096852ef02_template_equipment_plan.py
  • migrations/versions/2dbbf9629c3f_mssql_baseline_schema.py
  • migrations/versions/36dc707e1bbc_add_can_be_rp_to_qualification.py
  • migrations/versions/4ec25aa941b8_add_invite_cancelled_at.py
  • migrations/versions/5a65ab309cfc_add_dashboard_horizon_days_to_user_.py
  • migrations/versions/5bf568d4f009_add_dark_mode_to_user_account.py
  • migrations/versions/661e4a600825_add_last_login_at_to_user_account.py
  • migrations/versions/67dfa2385f16_add_scheduler_last_seen_to_app_settings.py
  • migrations/versions/6afec7c90ac1_qualification_soft_delete.py
  • migrations/versions/7fd90ec4167e_add_user_role_permission_tables.py
  • migrations/versions/86ba4668fbf9_add_backup_config_to_app_settings.py
  • migrations/versions/8af3e067c0c6_add_invite_email_tracking_columns.py
  • migrations/versions/904fa21b5ed3_rename_preferred_hour_utc_to_preferred_.py
  • migrations/versions/9159338cf432_add_reminder_sent_json_to_event.py
  • migrations/versions/92f9337e848e_add_instance_name_to_outbox_email.py
  • migrations/versions/953cffa1cb85_add_vykaz_generate_permission.py
  • migrations/versions/9b422409dc10_add_session_timeout_hours_to_app_.py
  • migrations/versions/9d1ee14eb241_notification_toggles_and_outbox_type.py
  • migrations/versions/9fd3c08b3d50_add_app_base_url_to_app_settings.py
  • migrations/versions/a1b2c3d4e5f6_rename_credential_to_qualification.py
  • migrations/versions/a2f3c4d5e6f7_lowercase_existing_user_emails.py
  • migrations/versions/a730e4af594e_add_version_to_event_template.py
  • migrations/versions/ac1ab7d64f6c_add_outbox_email_table_for_queued_email_.py
  • migrations/versions/ad27f656e221_add_invite_custom_subject_and_message.py
  • migrations/versions/b1c2d3e4f5a6_add_performance_indexes.py
  • migrations/versions/b39e55598ad1_add_equipment_models.py
  • migrations/versions/b801953d30cb_add_equipment_item_status_and_.py
  • migrations/versions/bbd2db07fc29_add_user_feedback_table.py
  • migrations/versions/bbd3aa765181_add_is_archived_to_user_account.py
  • migrations/versions/c11afc34311c_add_event_type_enum_planned_.py
  • migrations/versions/c384ea97aef5_add_notify_event_changed_to_app_settings.py
  • migrations/versions/c7d8e9f0a1b2_add_login_lockout_fields_to_user_account.py
  • migrations/versions/ca37e9989a8a_split_notify_event_lifecycle_into_.py
  • migrations/versions/d1e2f3a4b5c6_merge_indexes_and_login_lockout_heads.py
  • migrations/versions/d37d41a4e38d_add_all_domain_models.py
  • migrations/versions/d697cc60c5d2_add_ical_all_token.py
  • migrations/versions/d946eda56491_add_feedback_version_and_enabled_toggle.py
  • migrations/versions/dfc13fca938f_add_app_settings_table.py
  • migrations/versions/ebe3ddc11f1e_optional_spots.py
  • migrations/versions/f0c168424643_add_digest_tables_and_outbox_html_body.py
  • migrations/versions/f8edde653722_add_version_columns_for_optimistic_.py
  • migrations/versions/merge_heads_ical_all.py
  • mssql-init/setup.sh
  • requirements-dev.txt
  • requirements-e2e.txt
  • requirements.txt
  • tests/conftest.py
💤 Files with no reviewable changes (47)
  • migrations/versions/2b096852ef02_template_equipment_plan.py
  • migrations/versions/67dfa2385f16_add_scheduler_last_seen_to_app_settings.py
  • migrations/versions/5a65ab309cfc_add_dashboard_horizon_days_to_user_.py
  • migrations/versions/1a19cb432044_add_dev_email_block_to_app_settings.py
  • migrations/versions/9d1ee14eb241_notification_toggles_and_outbox_type.py
  • migrations/versions/b1c2d3e4f5a6_add_performance_indexes.py
  • migrations/versions/b39e55598ad1_add_equipment_models.py
  • migrations/versions/merge_heads_ical_all.py
  • migrations/versions/a2f3c4d5e6f7_lowercase_existing_user_emails.py
  • migrations/versions/953cffa1cb85_add_vykaz_generate_permission.py
  • migrations/versions/8af3e067c0c6_add_invite_email_tracking_columns.py
  • migrations/versions/b801953d30cb_add_equipment_item_status_and_.py
  • migrations/versions/661e4a600825_add_last_login_at_to_user_account.py
  • migrations/versions/dfc13fca938f_add_app_settings_table.py
  • migrations/versions/a730e4af594e_add_version_to_event_template.py
  • migrations/versions/9fd3c08b3d50_add_app_base_url_to_app_settings.py
  • migrations/versions/150aa748aa4f_add_ical_token_to_user_account.py
  • migrations/versions/d697cc60c5d2_add_ical_all_token.py
  • migrations/versions/9159338cf432_add_reminder_sent_json_to_event.py
  • .pre-commit-config.yaml
  • migrations/versions/6afec7c90ac1_qualification_soft_delete.py
  • migrations/versions/20dbd30fbcfe_debriefing_redesign_new_fields_on_.py
  • migrations/versions/f0c168424643_add_digest_tables_and_outbox_html_body.py
  • migrations/versions/86ba4668fbf9_add_backup_config_to_app_settings.py
  • migrations/versions/f8edde653722_add_version_columns_for_optimistic_.py
  • migrations/versions/ca37e9989a8a_split_notify_event_lifecycle_into_.py
  • migrations/versions/c11afc34311c_add_event_type_enum_planned_.py
  • migrations/versions/a1b2c3d4e5f6_rename_credential_to_qualification.py
  • migrations/versions/9b422409dc10_add_session_timeout_hours_to_app_.py
  • migrations/versions/92f9337e848e_add_instance_name_to_outbox_email.py
  • migrations/versions/bbd2db07fc29_add_user_feedback_table.py
  • migrations/versions/ac1ab7d64f6c_add_outbox_email_table_for_queued_email_.py
  • .dockerignore
  • migrations/versions/c384ea97aef5_add_notify_event_changed_to_app_settings.py
  • migrations/versions/d1e2f3a4b5c6_merge_indexes_and_login_lockout_heads.py
  • migrations/versions/5bf568d4f009_add_dark_mode_to_user_account.py
  • migrations/versions/d37d41a4e38d_add_all_domain_models.py
  • migrations/versions/bbd3aa765181_add_is_archived_to_user_account.py
  • migrations/versions/7fd90ec4167e_add_user_role_permission_tables.py
  • migrations/versions/904fa21b5ed3_rename_preferred_hour_utc_to_preferred_.py
  • migrations/versions/c7d8e9f0a1b2_add_login_lockout_fields_to_user_account.py
  • migrations/versions/36dc707e1bbc_add_can_be_rp_to_qualification.py
  • migrations/versions/d946eda56491_add_feedback_version_and_enabled_toggle.py
  • migrations/versions/1613fcb025fb_add_password_reset_nonce_to_user_account.py
  • migrations/versions/4ec25aa941b8_add_invite_cancelled_at.py
  • migrations/versions/ad27f656e221_add_invite_custom_subject_and_message.py
  • migrations/versions/ebe3ddc11f1e_optional_spots.py
✅ Files skipped from review due to trivial changes (1)
  • app/templates/admin/digest/index.html
🚧 Files skipped from review as they are similar to previous changes (4)
  • .github/workflows/ci.yml
  • docker-compose.yml
  • mssql-init/setup.sh
  • tests/conftest.py

spidermila and others added 3 commits June 19, 2026 16:48
… dance

The e2e entrypoint still ran the pre-squash bootstrap (`flask db stamp head`
+ `flask db migrate` + `flask db upgrade`), which broke after the migration
squash: stamping/autogenerating against the single baseline failed with
`KeyError: 'a1f2e3d4c5b6'` (a deleted PG-era revision), so the web-e2e
container exited 1 and the E2E job failed.

Replace the dance with a plain `flask db upgrade` — identical to the
production entrypoint — which creates the full schema from the single MSSQL
baseline on the fresh e2e database.

Verified locally: `docker compose -f docker-compose.e2e.yml up --build`
(chromium) applies `2dbbf9629c3f` cleanly, seeds, and passes 41/41 e2e tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1TSmeNk897sPHRWD6ny4G
…color)

Merging main brought in migration e1f2a3b4c5d6 (add color to event), whose
down_revision points into the PG chain this branch squashed away — so the
PR merge ref had a dangling revision and `flask db upgrade` failed in CI
with KeyError 'a1f2e3d4c5b6'. Local branch-only runs missed it because CI
tests the branch merged with main.

Regenerate the single MSSQL baseline from the merged models so it includes
event.color, and drop the now-redundant e1f2a3b4c5d6. Validated: upgrade on
an empty DB + re-run autogenerate = "No changes detected"; verify-schema
confirms 30 tables.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1TSmeNk897sPHRWD6ny4G

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (3)
DEVOPS.md (3)

845-845: 💤 Low value

Minor wordiness in migration governance section.

Static analysis flagged two phrasing opportunities:

  • Line 845: "can't merge by accident" → consider "can't merge unintentionally" or "can't merge without review"
  • Line 887: "that is exactly the failure" → consider "that is precisely the failure" (or simply "that is the failure")

These are style preferences and do not affect technical accuracy or clarity. Address only if you prefer tighter wording.

Also applies to: 887-887

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DEVOPS.md` at line 845, Update the phrasing in the migration governance
section of DEVOPS.md to improve clarity and conciseness. Replace the phrase
"can't merge by accident" with either "can't merge unintentionally" or "can't
merge without review" to be more precise. Similarly, replace "that is exactly
the failure" with "that is precisely the failure" or simply "that is the
failure" for tighter wording. These changes improve readability without
affecting technical accuracy.

Source: Linters/SAST tools


184-217: ⚡ Quick win

Test instructions need platform-specific ODBC driver installation guidance.

The docs correctly identify that the host needs Microsoft ODBC Driver 18 + unixODBC, but only show the Ubuntu command. Developers on macOS or Windows WSL2 need explicit steps (brew, apt, system package manager, etc.). Consider adding platform-specific installation subsections or a link to Microsoft's official ODBC driver documentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DEVOPS.md` around lines 184 - 217, The ODBC driver installation guidance in
the "Tests run on the host" section provides detailed steps only for
Debian/Ubuntu, with a minimal macOS comment and no Windows WSL2 guidance. Expand
the installation instructions by adding platform-specific subsections for macOS
(with detailed brew installation steps similar to the Ubuntu format) and Windows
WSL2 (with explicit apt-get commands for WSL2 systems), or alternatively add a
reference link to Microsoft's official ODBC driver documentation where
developers can find platform-specific installation guidance. Ensure each
platform has clear, actionable commands that developers can copy and run.

338-341: 💤 Low value

Clarify db-init idempotency and recovery behavior.

The comment states "idempotent, so re-runs are safe," but the docs don't explain what "safe" means when re-running: Does the init script skip existing databases, drop and recreate, or fail silently? This matters when developers restart containers or re-run docker compose up. A brief note on the behavior (e.g., "checks for existing database and login before creating") would reduce confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DEVOPS.md` around lines 338 - 341, Update the documentation comment for the
db-init service to explicitly clarify its idempotency behavior by explaining
what checks or mechanisms prevent issues during re-runs. Specifically, add
details about how the initialization script handles existing databases and
logins (for example, whether it checks for their existence before creating,
skips creation if they already exist, or uses conditional logic to make the
process safe for repeated executions). This will help developers understand what
to expect when restarting containers or re-running docker compose up.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@DEVOPS.md`:
- Line 845: Update the phrasing in the migration governance section of DEVOPS.md
to improve clarity and conciseness. Replace the phrase "can't merge by accident"
with either "can't merge unintentionally" or "can't merge without review" to be
more precise. Similarly, replace "that is exactly the failure" with "that is
precisely the failure" or simply "that is the failure" for tighter wording.
These changes improve readability without affecting technical accuracy.
- Around line 184-217: The ODBC driver installation guidance in the "Tests run
on the host" section provides detailed steps only for Debian/Ubuntu, with a
minimal macOS comment and no Windows WSL2 guidance. Expand the installation
instructions by adding platform-specific subsections for macOS (with detailed
brew installation steps similar to the Ubuntu format) and Windows WSL2 (with
explicit apt-get commands for WSL2 systems), or alternatively add a reference
link to Microsoft's official ODBC driver documentation where developers can find
platform-specific installation guidance. Ensure each platform has clear,
actionable commands that developers can copy and run.
- Around line 338-341: Update the documentation comment for the db-init service
to explicitly clarify its idempotency behavior by explaining what checks or
mechanisms prevent issues during re-runs. Specifically, add details about how
the initialization script handles existing databases and logins (for example,
whether it checks for their existence before creating, skips creation if they
already exist, or uses conditional logic to make the process safe for repeated
executions). This will help developers understand what to expect when restarting
containers or re-running docker compose up.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 333b5ad9-31ce-4e66-ba59-85457ad54229

📥 Commits

Reviewing files that changed from the base of the PR and between a0b7748 and 7772a54.

📒 Files selected for processing (10)
  • .github/workflows/ci.yml
  • .pre-commit-config.yaml
  • DEVOPS.md
  • app/routes/events/_helpers.py
  • app/routes/events/crud.py
  • app/routes/events/spots.py
  • app/routes/master_events.py
  • migrations/versions/2c159bca01be_mssql_baseline_schema.py
  • scripts/check_migrations.py
  • scripts/e2e-entrypoint.sh
🚧 Files skipped from review as they are similar to previous changes (4)
  • app/routes/events/_helpers.py
  • app/routes/events/spots.py
  • app/routes/master_events.py
  • app/routes/events/crud.py

A re-squash rewrites the single MSSQL baseline's revision id (and can fold in
new columns), stranding every durable DB: alembic_version points at a deleted
revision and flask db upgrade aborts on deploy. This hit the zerver dev deploy
of feat/mssql-support (the "include color" re-squash left dev DBs without
event.color). The risk applies to prod too — an identical-schema re-squash
still breaks it, since the revision id changes.

Add scripts/check_migrations.py enforcing: one root, root id == frozen
EXPECTED_BASELINE_REVISION, single head. Wire into CI lint job and pre-commit
(on migrations/versions changes). Document the failure mode, prod exposure, and
a Sanctioned re-baseline procedure (schema-neutral squash + re-stamp every
durable DB) in DEVOPS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@spidermila spidermila force-pushed the feat/mssql-support branch from 7772a54 to f1d4a9a Compare June 20, 2026 07:47

@frenzymadness frenzymadness left a comment

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.

Just a few comments now as I see fresh commits landing here. Let me know when you are done here.

Comment thread scripts/e2e-entrypoint.sh
Comment thread docker-compose.e2e.yml Outdated
Comment thread mssql-init/setup-e2e.sh
Comment thread app/config.py Outdated
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