La mia ultima release e’ stata la v1.2.2 a maggio 2019. Dopo, Geremia Taglialatela ha preso in mano la manutenzione e l’ha portato alla v5.0.0 con supporto per Rails 8.1 e Ruby 4.0. 34 release in 14 anni, 201 stelle, e ancora attivamente mantenuta. La documentazione API e il repo sono entrambi vivi.
Sette anni fa ho rilasciato ChronoModel v0.1.0 โ una gem Ruby che da’ ai modelli ActiveRecord capacita’ temporali su PostgreSQL. Cinque giorni di hacking, trentasei commit, nessun test, e una confessione sul monkey-patching della costante dell’adapter PostgreSQL.
Oggi taggo la v1.0.0. Il messaggio di commit e’ :gem: this is v1.0.0. Non proprio un discorso memorabile, ma il codice parla da solo: 506 commit, 31 release, 52 file modificati, 5.392 righe aggiunte. L’idea di base โ viste aggiornabili su public, dati correnti su temporal, storico su history con table inheritance โ non e’ mai cambiata. Tutto il resto si'.
Cosa e’ cambiato
Tre cose erano sbagliate nella v0.1.0, e l’avevo detto io stesso. Tutte e tre risolte lo stesso giorno โ San Valentino 2014, la release v0.6.0. Una riscrittura completa del layer database mantenendo l’API Ruby identica. La versione minima di PostgreSQL e’ saltata dalla 9.0 alla 9.3. Se la v0.1.0 era “questo funziona,” la v0.6.0 era “questo funziona correttamente.”
Regole โ trigger INSTEAD OF
Il design originale usava le regole di PostgreSQL per rendere scrivibili le viste public. Le regole funzionano, ma hanno spigoli vivi โ riscrivono le query a parse time, non gestiscono bene le clausole RETURNING, e il debugging e’ un incubo.
Le ho strappate tutte e sostituite con trigger INSTEAD OF. Stesso comportamento, modello di esecuzione piu’ pulito. I trigger scattano al momento dell’esecuzione, gestiscono RETURNING naturalmente, e si possono davvero debuggare. Il messaggio di commit dice “BREAKING CHANGE” โ perche’ lo era. Ogni tabella temporale aveva bisogno di una migration per passare al nuovo sistema.
box()/point() โ tsrange
Il vincolo di esclusione originale era il mio hack piu’ orgoglioso โ abusare degli indici geometrici GiST per impedire entry storiche sovrapposte codificando gli intervalli temporali come box 2D. Funzionava, ma era un hack. PostgreSQL 9.2 ha introdotto i range type nativi, e dalla 9.3 erano solidi.
Sostituito l’hack geometrico con colonne tsrange native. Il vincolo e’ passato da questo:
-- v0.1.0: codifica il tempo come geometria, spera nel meglio
EXCLUDE USING gist (
box(
point( date_part('epoch', valid_from), id ),
point( date_part('epoch', valid_to - INTERVAL '1 msec'), id )
) WITH &&
)
a questo:
-- v0.6.0: dici quello che intendi
EXCLUDE USING gist ( id WITH =, validity WITH && )
E le clausole WHERE per le query temporali si sono pulite altrettanto drasticamente:
-- v0.1.0: "che anno e'?!" come problema geometrico
WHERE box(point(date_part('epoch', valid_from), 0),
point(date_part('epoch', valid_to), 0))
&& box(point(date_part('epoch', '2014-01-01'), 0),
point(date_part('epoch', '2014-01-01'), 0))
-- v0.6.0: basta chiedere
WHERE '2014-01-01' <@ validity
Il database capisce quello che sta enforcing, e anche chiunque legga il query log.
Monkey-patching โ adapter corretto
La “verita’ scomoda” della v0.1.0:
silence_warnings do
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter = ChronoModel::Adapter
end
Sparita. ChronoModel adesso si registra come sottoclasse dell’adapter. Si configura in database.yml con adapter: chronomodel e ActiveRecord lo carica attraverso la sua risoluzione standard degli adapter. Nessuna costante e’ stata maltrattata.
I test
Il post della v0.1.0 diceva “nessun test per ora โ arriveranno, promesso.” Sono arrivati. La v0.3.0 (giugno 2012, sei settimane dopo) ha aggiunto spec RSpec complete. Alla v1.0.0 ci sono 5.000+ righe di codice di test che coprono tabelle temporali, query storiche, associazioni, time query, STI, indici, migration, schema dump, e comportamento standard di ActiveRecord.
La suite di test gira su multiple versioni di Rails via Appraisal โ Rails 5.0, 5.1, e 5.2 per la v1.0.0. La v0.13.1, taggata trenta minuti prima della v1.0.0, e’ l’ultima versione a supportare Rails 4.2.
Il weekend del 6 aprile
Lo sprint finale e’ un weekend. Il supporto Rails 5.0-5.2 arriva nel pomeriggio, Rails 4.2 viene droppato, le spec vengono aggiunte, i deprecation warning vengono risolti. Poi tre release in meno di un’ora:
- 20:25 โ v0.13.1: “the last version to support Rails 4.2”
- 20:54 โ v1.0.0:
:gem: this is v1.0.0 - 21:17 โ v1.0.1, perche’ ovviamente c’e’ una v1.0.1
Poi il refactoring va avanti fino alle 5 di mattina โ estrazione dell’adapter in moduli puliti, riscrittura di on_schema con thread-local storage, fix degli smell di CodeClimate, aumento della coverage. Perche’ taggare la 1.0 non significa che ti fermi. Significa che finalmente hai il permesso di fare pulizia come si deve.
Cosa non e’ cambiato
L’architettura a tre schemi. L’opzione temporal: true nelle migration. Il mixin include ChronoModel::TimeMachine. L’interfaccia di query as_of. L’idea che i dati temporali appartengono al database, non ai callback dell’applicazione.
# Questo funzionava nel 2012. Funziona ancora nel 2019.
Country.as_of(1.year.ago).find_by(code: 'IT')
506 commit per rendere gli internals all’altezza dell’interfaccia. Sette anni di produzione all’IFAD senza un singolo incidente di perdita dati.
Il sorgente e’ su GitHub, la documentazione API copre ogni metodo pubblico. gem 'chrono_model', '~> 1.0' e sei a posto.
Viaggiare nel tempo non dovrebbe costare una licenza Oracle. E ancora non la costa.