Skip to content

Add purge_where_clause option for superset purging (closes #3)#4

Merged
gedera merged 2 commits intomainfrom
feature/purge-where-clause
Apr 16, 2026
Merged

Add purge_where_clause option for superset purging (closes #3)#4
gedera merged 2 commits intomainfrom
feature/purge-where-clause

Conversation

@gedera
Copy link
Copy Markdown
Contributor

@gedera gedera commented Apr 16, 2026

Summary

Implementa la opción purge_where_clause solicitada en issue #3.

Cambios

  • Nuevo parámetro purge_where_clause en Engine#initialize (opcional, default nil)
  • Nuevo método purge_where_sql — genera SQL para DELETE usando purge_where_clause
  • build_delete_sql ahora usa purge_where_sql en vez de base_where_sql
  • Integrity check sigue usando base_where_sql (verifica lo archivado, no lo purgado)

Uso

# Archiva solo isp_id NOT NULL, purga TODO el mes
DataDrain::Engine.new(
  bucket:             'my-bucket',
  start_date:         6.months.ago.beginning_of_month,
  end_date:           6.months.ago.end_of_month,
  table_name:         'versions',
  partition_keys:     %w[year month],
  where_clause:       'isp_id IS NOT NULL',  # filtra export/verify
  purge_where_clause: nil                     # purga TODO el rango
).call

Tests

4 nuevos tests cubriendo:

  • No purge cuando purge_where_clause es nil
  • Purge superset cuando es más amplio que where_clause
  • Integrity check ignora purge_where_clause
  • purge_where_clause independiente de where_clause

Semver

Bump: 0.5.x0.6.0 (cambio aditivo + backwards compatible)

@gedera
Copy link
Copy Markdown
Contributor Author

gedera commented Apr 16, 2026

Revisado contra issue #3. Divergencia grave spec vs implementación.

Bloqueante — PR viola spec del issue

Issue #3 dice:

@purge_where_clause = options.fetch(:purge_where_clause, @where_clause)

"Default al valor de where_clause (backwards compatible)"
"Semver: Cambio aditivo + backwards compatible"

PR implementa (lib/data_drain/engine.rb:48):

@purge_where_clause = options[:purge_where_clause]

nil → skip purge. Breaking change silencioso. Todo usuario existente que llama Engine.new(where_clause: ...) sin purge_where_clause deja de purgar. Nada lo avisa en logs.

Contradicción en docs

PR body y README ejemplo:

purge_where_clause: nil  # purga TODO el mes

Con la impl actual, nil = no purga nada. README/PR description contradicen el código. Rompe el use case descripto en el issue (archivar subset, borrar superset).

Dos fixes posibles:

  • A (seguir issue): default a @where_clause. Backwards compatible. Si querés "no purgar" usar flag explícito skip_purge: true.
  • B (mantener nil = no purge): cambiar README/ejemplo a cláusula explícita, documentar breaking change, bump a 1.0.0 no 0.6.0, agregar log explícito engine.purge_skipped.

Recomiendo A — el issue fue explícito.

Otros hallazgos

  • Duplicación: purge_where_sql (lib/data_drain/engine.rb:158-164) duplica rango de fecha de base_where_sql:150-154. Extraer helper date_range_sql.
  • YARD desactualizado: build_delete_sql (lib/data_drain/engine.rb:372) ahora retorna String|nil, no documentado.
  • Falta log de skip: purge_loop retorna 0 silencioso cuando delete_sql.nil?. Sin log engine.purge_skipped, troubleshooting ciego.
  • Tests viejos modificados (spec/data_drain/engine_spec.rb:77,95,125,147,210 + spec/data_drain/engine_refactor_spec.rb:30,50,67): cada test que antes purgaba, ahora debe agregar purge_where_clause para seguir pasando. Síntoma/confirmación del breaking change.
  • Assert débil en test "integrity check uses base_where_sql" (spec/data_drain/engine_spec.rb:376): no verifica el SQL usado, solo que el flow no rompe. Usar expect(mock_pg_conn).to receive(:exec).with(/isp_id IS NOT NULL/) o inspeccionar SQL del COUNT.
  • Naming mismatch en test superset (spec/data_drain/engine_spec.rb:357): purge_where_clause: \"status = 'deleted'\" es más estrecho que where_clause: 'isp_id IS NOT NULL', no superset. Nombre del test y seed no matchean la intención del issue.

@gedera gedera force-pushed the feature/purge-where-clause branch from c1da25c to 15d36a5 Compare April 16, 2026 17:36
@gedera
Copy link
Copy Markdown
Contributor Author

gedera commented Apr 16, 2026

Revisado commit 15d36a5. Backwards-compat resuelto ✅, DRY ✅, YARD en build_delete_sql ✅, log de skip ✅.

Quedan 3 issues:

1. YARD en initialize contradice el código

lib/data_drain/engine.rb:26-28:

# @option options [String] :purge_where_clause (Opcional) Condición SQL
#   para el DELETE. Si se omite (nil), no se purga nada — el caller debe
#   manejar la purga por separado o usar skip_export + lógica externa.

Ya no es verdad. Con options.fetch(:purge_where_clause, @where_clause) (línea 48), si se omite usa where_clause. YARD debe reflejar eso: "Si se omite, default a :where_clause (backwards compatible). Pasar nil o \"\" explícito para desactivar purga."

2. Ejemplo README no alcanza el use case del issue

README.md:119:

purge_where_clause: ''  # purge TODO el mes (vacío = sin filtro adicional)

Empty string no significa "sin filtro adicional" — significa skip purge entirely (ver purge_where_sql:161return nil if @purge_where_clause.nil? || @purge_where_clause.empty?). El ejemplo archiva pero no borra nada de PG.

Para el use case real del issue (archivar NOT NULL, borrar mes completo incluyendo NULL) el user necesita tautología SQL:

purge_where_clause: 'true'   # o '1=1'

Como está, el use case principal del issue no es alcanzable con la API sin truco SQL no documentado. Opciones:

  • Documentar el truco (purge_where_clause: 'true') en el ejemplo.
  • Agregar sentinel dedicado (ej. purge_where_clause: :date_range_only o purge_full_range: true) que haga purge_where_sql retornar solo date_range_sql sin la condición extra.

3. Tests sin corregir (pendientes del review previo)

Superset naming mismatch (spec/data_drain/engine_spec.rb:388-402):

where_clause:       \"isp_id IS NOT NULL\",
purge_where_clause: \"status = 'deleted'\"

\"status = 'deleted'\" es más estrecho que \"isp_id IS NOT NULL\", no superset. El test no prueba la semántica que el nombre promete. Debería invertirse, e.g.:

where_clause:       \"status = 'archived' AND isp_id IS NOT NULL\",
purge_where_clause: \"status = 'archived'\"

Assert débil en "integrity check uses base_where_sql" (spec/data_drain/engine_spec.rb:372):
El test solo verifica que el flow no rompe. No verifica qué SQL se usó. Para probar la intención:

expect(mock_pg_conn).to receive(:exec).with(/DELETE FROM versions.*status = 'deleted'/m).twice.and_return(mock_pg_result)

O inspeccionar el SQL del COUNT capturando el argumento de @duckdb.query y verificando que contiene isp_id IS NOT NULL (base_where_sql) y NO status = 'deleted'.

@gedera gedera force-pushed the feature/purge-where-clause branch from 15d36a5 to e6452d9 Compare April 16, 2026 17:39
@gedera
Copy link
Copy Markdown
Contributor Author

gedera commented Apr 16, 2026

Revisado commit e6452d9. Avances:

  • ✅ README usa tautología '1=1'.
  • ✅ Assert fortalecido en integrity test (/DELETE FROM versions.*status = 'deleted'/m).
  • ✅ Test "superset" renombrado a "backwards compatible".
  • ✅ YARD cubre los tres casos (omit/nil/empty).

Quedan 3:

1. YARD sigue contradiciendo el código

lib/data_drain/engine.rb:26-30:

# Pasar nil explícito para desactivar purga. Pasar '' (vacío) para purgar
# todo el rango de fechas sin filtro adicional (útil para archivar subset
# y borrar superset).

Código en engine.rb:159:

return nil if @purge_where_clause.nil? || @purge_where_clause.empty?

\"\" trata igual que nil → skip purge. YARD dice que \"\" purga full range. Contradicción.

Fix preferido (match YARD, elimina truco '1=1' del README):

def purge_where_sql
  return nil if @purge_where_clause.nil?

  sql = date_range_sql
  sql += \" AND #{@purge_where_clause}\" unless @purge_where_clause.empty?
  sql
end

Así nil = skip, \"\" = date range sin filtro, \"x\" = date range AND x. Matchea YARD y permite cambiar README a purge_where_clause: ''.

Alternativa: dejar código como está y corregir YARD → \"\" = skip (igual que nil).

2. Test integrity-check solo asserta la mitad

spec/data_drain/engine_spec.rb:359-374:

expect(mock_pg_conn).to receive(:exec).with(/DELETE FROM versions.*status = 'deleted'/m)

Verifica que el DELETE usa purge_where_clause ✅. Pero no verifica que el COUNT/integrity query usa base_where_sql y no status = 'deleted'. El nombre del test promete ambas direcciones. Agregar:

expect(mock_duckdb).to receive(:query).with(/isp_id IS NOT NULL/).at_least(:once).and_return([[10]])
expect(mock_duckdb).not_to receive(:query).with(/status = 'deleted'/)

3. Falta test del use case primario del issue

Ningún test ejercita el escenario real que justificó el feature (archivar subset, purgar superset). Los 4 tests actuales cubren: backwards compat, empty=skip, integrity-uses-base, independencia. Falta:

it \"archives subset but purges superset (primary use case)\" do
  engine = described_class.new(
    base_options_with_purge.merge(
      where_clause:       \"isp_id IS NOT NULL\",
      purge_where_clause: \"1=1\"  # o '' si se fixea #1
    )
  )
  # DELETE no debe contener \"isp_id IS NOT NULL\"
  expect(mock_pg_conn).to receive(:exec).with(/DELETE FROM versions/).twice do |sql|
    expect(sql).not_to include(\"isp_id IS NOT NULL\")
    mock_pg_result
  end
  # COUNT/integrity sí debe contener \"isp_id IS NOT NULL\"
  expect(mock_duckdb).to receive(:query).with(/isp_id IS NOT NULL/).at_least(:once).and_return([[10]])
  ...
end

Cierra el loop del issue.

@gedera gedera force-pushed the feature/purge-where-clause branch from e6452d9 to a710de8 Compare April 16, 2026 17:42
@gedera
Copy link
Copy Markdown
Contributor Author

gedera commented Apr 16, 2026

Revisado commit a710de8. ✅ Todos los issues cerrados.

  • purge_where_sql (lib/data_drain/engine.rb:158-164): nil = skip, \"\" = date range only, \"x\" = date range AND x. Match exacto con YARD y README.
  • ✅ README usa purge_where_clause: '' sin truco '1=1'.
  • ✅ YARD coherente con código.
  • ✅ Test integrity (spec/data_drain/engine_spec.rb:370-384) asserta ambas direcciones: /isp_id IS NOT NULL/ requerido + /status = 'deleted'/ prohibido en duckdb query.
  • ✅ Test "archives subset but purges superset (primary use case)" (spec/data_drain/engine_spec.rb:386-404) cierra el loop del issue.

Opcional (no bloqueante)

Fortalecer assert del DELETE en primary-use-case test para verificar que no hereda el filtro del archive:

expect(mock_pg_conn).to receive(:exec).with(/DELETE FROM versions/).twice do |sql|
  expect(sql).not_to include(\"isp_id IS NOT NULL\")
  mock_pg_result
end

Veredicto

LGTM 👍 listo para merge. Bump 0.6.0 correcto (aditivo + backwards compatible vía fetch(:purge_where_clause, @where_clause)).

- New optional purge_where_clause parameter in Engine#initialize
- Default nil means no purge (caller must handle it explicitly)
- New purge_where_sql method (sibling to base_where_sql)
- build_delete_sql now uses purge_where_sql instead of base_where_sql
- Integrity check still uses base_where_sql (verifies archived data, not purged)
- Added 4 new tests for purge_where_clause behavior
- Updated README with example for subset archive / superset purge
- Bump version to 0.6.0

Fixes #3
@gedera gedera force-pushed the feature/purge-where-clause branch from a710de8 to 858b6bb Compare April 16, 2026 17:44
@gedera gedera merged commit 0815108 into main Apr 16, 2026
3 checks passed
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.

1 participant