Skip to content

Refactor SQLite access into a maintainable persistence layer #260

Closed

Description

@giscebot

Context

Requested by @ecarreras after reviewing the amount of inline SQLite access in the bridge.

The problem is not that the project uses explicit SQL. SQLite and hand-written SQL are a good fit for the queue semantics and operational footprint. The maintainability problem is that connection setup, schema compatibility, transactions, queries, row mapping and business decisions are mixed across HTTP handlers, services and domain modules.

Current inventory on main (v0.73.0), excluding tests:

Module SQL API calls Main responsibility currently mixed with SQL
queue.py 107 ingestion, deduplication, coalescing, claiming, lifecycle, runtime processes, state, schema compatibility and backfills
feedback.py 31 feedback capture, proposals, rules, enrichment and GitHub-source resolution
dashboard_data.py 25 dashboard read models, filters, metrics and compatibility checks
backend.py 17 webhook monitoring queries and writes inside FastAPI handlers
mcp.py 11 token persistence, authentication and schema compatibility
observability.py 9 process samples and alert lifecycle
actors.py 8 actor backfill plus schema compatibility
web_push.py 7 subscriptions and delivery status
webhook.py 5 delivery/hook persistence

There are also direct sqlite3.connect() call sites in multiple modules with different defaults. Only some connections enable foreign keys; read-only behavior, timeouts, row factories and transaction modes are not centrally enforced.

Schema evolution is split between:

  • sql/schema.sql;
  • JobQueue._ensure_columns() and _ensure_indexes();
  • mcp._ensure_schema();
  • actor backfill schema helpers;
  • feature-specific startup backfills.

Issue #190 already covers versioned migrations and should remain the source of truth for that part. This issue is the umbrella for the persistence architecture around it.

Goal

Keep SQL explicit, but isolate it behind a small persistence layer so that:

  • application/services express business operations instead of SQL statements;
  • transaction boundaries are named and reviewable;
  • every connection uses the same operational policy;
  • queue concurrency invariants remain protected;
  • dashboard queries evolve independently from write workflows;
  • schema changes go through versioned migrations rather than feature-local shims.

Non-goals

  • Introduce SQLAlchemy or a full ORM.
  • Build a generic repository framework.
  • Convert every query into a separate .sql resource.
  • Rewrite the queue in one large PR.
  • Hide SQLite-specific primitives such as BEGIN IMMEDIATE, WAL or partial indexes where they are part of the design.

Priority 0 — establish safe foundations

  • Complete Introduce versioned SQLite migrations #190: add versioned, idempotent migrations and remove feature-local schema mutation once migrated.
  • Add one Database/connection factory under a persistence package, with explicit read-write and read-only entrypoints.
  • Centralize timeout, row_factory, PRAGMA foreign_keys=ON, WAL/busy policy and read-only/query-only behavior.
  • Add an explicit transaction context supporting deferred and BEGIN IMMEDIATE transactions; do not rely on each caller's implicit sqlite3 behavior.
  • Document the transaction contract for ingestion, deduplication, coalescing, claiming and finishing jobs.
  • Add concurrency/contract tests before moving queue SQL. They must prove:
    • one canonical job per source/event;
    • no two workers claim the same job;
    • active jobs with the same work_key stay serialized;
    • pause state is read inside the claim transaction;
    • acknowledgements and job-run state cannot be partially updated.
  • Preserve backup/restore as a specialized database operation; it does not need to pass through repositories.
  • Make ordinary JobQueue construction validation-only and require active-job checks plus backup/rollback in the explicit migration path — fix: require explicit SQLite migration preflight #283 (merged).

Priority 1 — separate persistence by domain

Priority 1 — create an explicit dashboard read model

Priority 2 — enforce boundaries and performance

Suggested delivery slices

  1. Database foundation + migrations: Introduce versioned SQLite migrations #190, connection policy and transaction helpers.
  2. Queue write path: ingestion/claim/finish repositories with invariant tests.
  3. Dashboard read path: DashboardQueries and removal of SQL from FastAPI handlers.
  4. Feature stores: webhook, feedback, MCP, push and observability repositories.
  5. Boundary enforcement: architecture tests, query-plan tests and removal of compatibility shims.

Each slice should be independently deployable and preserve the current schema/API behavior. Avoid a flag-day rewrite.

Acceptance criteria

Delivery tracking

Activity

  1. ecarreras commented on Oct 7, 2026

    @ecarreras
    Member

    @giscebot comença a treballar en les tasques de Prioritat 0 si necessites separar-ho amb varies pull-requests segons el que facis ho fas. Un cop feta la pull-request actualitza la descripció de la tasca vinculant la pull-request

  2. giscebot commented on Oct 7, 2026

    @giscebot
    CollaboratorAuthor

    Primera entrega de Prioritat 0 oberta: #267.

    Inclou la base Database amb connexions read-write/read-only, transaccions DEFERRED/IMMEDIATE, política comuna per les connexions migrades, contracte documental de les transaccions de cua i tests reals de concurrència/rollback. He mantingut l'extracció de repositoris fora d'aquesta PR perquè sigui una fase revisable i no un flag day.

    Validació: 472 tests locals i els tres checks de CI en verd (dashboard, Python 3.11 i Python 3.12). També he actualitzat la descripció de la issue amb #263 i #267. Les connexions feature-local que encara queden i l'extracció del SQL de cua continuen com a següents entregues.

  3. ecarreras commented on Oct 7, 2026

    @ecarreras
    Member

    @giscebot què més queda de la prioritat 0 ? t'he dit que actualitzessis la descripció de la issue enallçant les pull-requests i marcat els checks dels items fets

  4. giscebot commented on Oct 7, 2026

    @giscebot
    CollaboratorAuthor

    Tens raó: havia actualitzat el tracking, però no els checks de la descripció. Ja està corregit.\n\nDe Prioritat 0 no queda res:\n- #263 va completar les migracions versionades.\n- #267 va completar la base Database RW/RO, la política comuna de connexions, les transaccions DEFERRED/IMMEDIATE, el contracte de cua, les proves de concurrència/rollback i l'excepció explícita de backup/restore.\n\nHe marcat tots aquests ítems i #267 al Delivery tracking. El següent bloc pendent ja és Prioritat 1: extreure repositoris per domini i migrar les connexions feature-local/SQL del dashboard sense canviar comportament.

  5. ecarreras commented on Oct 7, 2026

    @ecarreras
    Member

    @giscebot comença doncs ara amb Priority 1 — separate persistence by domain Recorda que després has d'actualtizar la descripció d'aquesta issue amb la feina feta

  6. giscebot commented on Oct 7, 2026

    @giscebot
    CollaboratorAuthor

    Primera entrega de Priority 1 — separate persistence by domain oberta: #273.

    Extreu AcknowledgementRepository i StateRepository, afegeix els DTOs tipats AcknowledgementClaim i ExecutorPauseState, i manté JobQueue com a façana compatible. Les dues invariants delicades no es parteixen: l'acknowledgement es crea dins la transacció d'ingesta i la pausa es llegeix dins la transacció BEGIN IMMEDIATE del claim.

    Validació: 485 tests locals i els tres checks de CI en verd (dashboard, Python 3.11 i Python 3.12); la PR és CLEAN i mergeable. També he actualitzat la descripció de la issue amb subchecks per adapter i el tracking de #273, que queda sense marcar fins que es faci merge.

    Encara queden JobRepository, IngestionRepository, RuntimeRepository i els repositoris/read models de la resta de dominis; #273 no afirma tancar la Prioritat 1 ni la issue paraigua.

  7. ecarreras commented on Oct 7, 2026

    @ecarreras
    Member

    @giscebot revisa que falta del punt Priority 1 — separate persistence by domain actualiza la descripció i si ja està fet tot continua amb l'altre apartat de prioritat 1

  8. giscebot commented on Oct 7, 2026

    @giscebot
    CollaboratorAuthor

    Revisió feta: Priority 1 — separate persistence by domain encara no està complet, així que no he saltat encara al read model del dashboard.

    He actualitzat la descripció de la issue:

    #275 extreu runs, heartbeats, processos runtime, worklog, events de sessió i progrés fora de JobQueue, amb RuntimeProcess tipat i transaccions BEGIN IMMEDIATE pels fluxos read→write. Manté JobQueue com a façana i no canvia esquema ni API pública.

    Validació: 521 tests locals; CI dashboard, Python 3.11 i Python 3.12 en verd; PR CLEAN i mergeable. Encara queden JobRepository, IngestionRepository i els repositoris feature-specific abans de donar aquest bloc per acabat.

  9. ecarreras commented on Oct 7, 2026

    @ecarreras
    Member

    @giscebot continua amb els punts de Priority 1 — separate persistence by domain fins a completar, si ho creus necessari fas stacked pull request

  10. WaylonSmithersJr commented on Oct 7, 2026

    @WaylonSmithersJr

    I found a migration-safety gap while reviewing #260 / #263 against an existing deployment.

    JobQueue.__init__() always calls init(), which calls _initialize_database() and apply_migrations() (queue.py). This happens in ordinary entry points, not only gab migrate-db—for example gab status/other CLI commands and backend/webhook queue construction. It does not check for active jobs or take a pre-migration backup.

    I reproduced this on a temporary SQLite backup of our deployed DB (production was not modified): before construction it had an active waiting_approval job and no schema_migrations/job_commit_statuses tables. A single JobQueue(snapshot_path) using current main created both tables and recorded migrations 1 and 2 while the waiting_approval job remained active. No backup or quiet-window preflight ran.

    This means the documented autoupdate guard (active_jobs_block_migration, backup before migration) can be bypassed by any new-version process that opens the DB before the controlled migration step. The guard is especially relevant for a dashboard/webhook process or a manual CLI command started after package installation. The rolling SCHEMA snapshot can also change a legacy DB before apply_migrations() runs.

    Could we make ordinary JobQueue construction validate the existing schema/version and fail or defer when migration is required, leaving upgrades to an explicit migrate-db path with the same quiet-queue/backup preflight? Fresh DB initialization can remain a separate permitted path. A regression test with an active waiting_approval job and pending migration would pin this behavior.

  11. giscebot commented on Oct 7, 2026

    @giscebot
    CollaboratorAuthor

    Completada la implementació de Priority 1 — separate persistence by domain en un stack de quatre PRs:

    1. #277 — JobRepository i IngestionRepository.
    2. #278 — WebhookRepository i McpTokenRepository.
    3. #279 — WebPushRepository i ObservabilityRepository.
    4. #280 — FeedbackRepository.

    Ordre de merge: #277 → #278 → #279 → #280. Les quatre PRs estan CLEAN, mergeables i amb dashboard/Python 3.11/Python 3.12 en verd. A cada capçalera del stack he executat també la suite local completa: 523 tests.

    He mantingut classificació/política, transport, autenticació, enriquiment GitHub i LLM fora dels repositoris; els adapters concentren connexions, SQL, transaccions i DTOs. No hi ha canvis d’esquema ni d’API pública.

    També he actualitzat la descripció de la issue: #275 consta com a fusionada i tots els ítems nous queden vinculats a la seva PR. Els checks del stack continuen oberts fins que les PRs es fusionin. El següent bloc és l’altre apartat de Priority 1: dashboard read model; aquest stack no afirma tancar la issue paraigua.

  12. ecarreras commented on Oct 8, 2026

    @ecarreras
    Member

    @giscebot revisa la issue I actualiza amb tot el que ja està fet i continua amb el que creguis més important, fes tota una secció i ho fas amb les pull-request que necessitis i stacked de GitHub

  13. giscebot commented on Oct 8, 2026

    @giscebot
    CollaboratorAuthor

    Revisió i següent secció completades.

    Ordre de merge: #283 → #284. Els checks de la descripció per aquestes dues entregues continuen oberts fins que es fusionin; no he marcat feina pendent com a feta.

  14. ecarreras commented on Oct 8, 2026

    @ecarreras
    Member

    @giscebot actualiza la descripció de la issue amb l'estat actual marcant el que ja estigui fet i continua treballant amb els redactors que toqui segons prioritats

  15. giscebot commented on Oct 8, 2026

    @giscebot
    CollaboratorAuthor

    He actualitzat la descripció amb l'estat real: #283 i #284 consten com a fusionades, Priority 0 i els dos blocs de Priority 1 queden marcats, i Priority 2 ara separa els hot paths que encara no tenen cobertura de pla de consulta.

    Nova entrega oberta: #294.

    Aquesta PR completa el primer tall de Priority 2:

    • afegeix un test AST que impedeix noves connexions o SQL fora de persistència, migracions i DashboardQueries;
    • extreu el SQL del backfill d'actors a ActorBackfillRepository i elimina el shim DDL local;
    • mou la inicialització SQL de JobQueue al paquet de migracions;
    • documenta com estendre persistència i qui és responsable de la retenció/neteja de cada família de dades.

    Validació: 541 tests locals; dashboard, Python 3.11 i Python 3.12 en verd; PR MERGEABLE. No hi ha canvi d'esquema ni d'API. El següent bloc pendent és completar els EXPLAIN QUERY PLAN dels hot paths i després instrumentar operacions lentes/ocupades amb noms segurs.

  16. ecarreras commented on Oct 8, 2026

    @ecarreras
    Member

    @giscebot pots actualitzar la descripció de la issue i revisar què queda pendent

  17. giscebot commented on Oct 8, 2026

    @giscebot
    CollaboratorAuthor

    He actualitzat la descripció amb l’estat real després del merge de #294: ara consten com a completats el test de frontera arquitectònica, l’extracció del backfill d’actors, la restricció de sqlite3.connect(), l’absència de SQL directe fora de persistència/read model, la documentació d’extensió i l’ownership de retenció.

    Queda pendent, per ordre recomanat:

    1. Afegir EXPLAIN QUERY PLAN pels hot paths encara descoberts: claim de jobs, paginació/finestra de cobertura de webhooks, lookup de regles de feedback i tokens MCP/subscripcions push actius.
    2. Instrumentar operacions SQLite lentes o bloquejades amb noms d’operació segurs, sense SQL cru ni paràmetres sensibles.
    3. Tancar els criteris d’acceptació transversals: auditar modes/límits de totes les transaccions, fixar query count/plan de detail i metrics del dashboard, i executar la suite completa al tall final.

    Matís: #294 defineix qui és responsable de cada retenció, però no introdueix encara una política automàtica d’eliminació de jobs completats; la documentació ho deixa explícitament com una operació futura i separada. La següent entrega natural és la cobertura de plans de consulta, deixant la instrumentació en una PR pròpia.

  18. ecarreras commented on Oct 9, 2026

    @ecarreras
    Member

    @giscebot ataca ja la última part que quedi per tancar ja tot el refactor

  19. giscebot commented on Oct 9, 2026

    @giscebot
    CollaboratorAuthor

    Última entrega del refactor oberta: #295.

    Completa els pendents de Priority 2:

    • totes les lectures/escriptures de repositori tenen nom d’operació estable i totes les escriptures declaren explícitament DEFERRED o IMMEDIATE; un test d’arquitectura impedeix regressions;
    • les operacions lentes i els errors SQLITE_BUSY/SQLITE_LOCKED generen telemetria amb nom, mode i durada, sense SQL, paràmetres ni text d’error;
    • la migració 3 afegeix els índexs dels hot paths i els tests executen EXPLAIN QUERY PLAN sobre les consultes reals de claim, webhooks, feedback, MCP, push i detall del dashboard;
    • list/detail/metrics del dashboard queden amb query counts i plans fixats.

    Validació: 550 tests locals; CI dashboard, Python 3.11 i Python 3.12 en verd; PR MERGEABLE sobre main. He actualitzat la descripció de #260 vinculant #295 a tots els checks pendents i els he deixat oberts fins al merge. La PR inclou Closes #260, de manera que el merge tancarà el paraigua.

  20. giscebot commented on Oct 9, 2026

    @giscebot
    CollaboratorAuthor

    Refactor complet i tracking tancat.

    La PR final #295 s’ha fusionat a main amb el commit ba66d343. Els checks de dashboard, Python 3.11 i Python 3.12 són verds; la validació final documentada és de 550 tests.

    He actualitzat la descripció de #260: plans de consulta, telemetria SQLite, límits de transacció, regressions del dashboard, suite final i delivery tracking consten ara com a completats amb #295 fusionada. No hi queda cap checklist pendent i la issue resta tancada.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions