# Refining SQL Data Models Without Breaking Legacy Code

DevFeed: [Refining SQL Data Models Without Breaking Legacy Code](<https://devfeed.tech/articles/refinement-without-specification-25504.md>)

Original publisher: [Read original article](<https://buttondown.com/hillelwayne/archive/refinement-without-specification/>)

Author: Hillel Wayne

Published: 2026-01-20T17:49:07Z

Content type: article

Language: en

Sources: [Newsletter feed for Hillel Wayne's Newsletter](<https://devfeed.tech/sources/newsletter-feed-for-hillel-wayne-s-newsletter.md>)

Topics: [SQL](<https://devfeed.tech/topics/sql.md>), [Databases](<https://devfeed.tech/topics/databases.md>), [legacy](<https://devfeed.tech/topics/legacy.md>)

Tags: [database](<https://devfeed.tech/tags/database.md>), [event-sourcing](<https://devfeed.tech/tags/event-sourcing.md>), [legacy-code](<https://devfeed.tech/tags/legacy-code.md>), [sql](<https://devfeed.tech/tags/sql.md>), [sql-database](<https://devfeed.tech/tags/sql-database.md>)

## AI overview

This article explains how refinement mappings can support SQL schema changes while preserving compatibility with existing code. It applies the approach to migrating a boolean column to a timestamp, then to an event-sourcing-like model, and examines how mutability constraints affect whether the refinement remains valid.

## Source excerpt

Imagine we have a SQL database with a user table, and users have a non-nullable is_activated boolean column. Having read That Boolean Should Probably Be Something else, you decide to migrate it to a nullable activated_at column. You can change any of the SQL queries that read/update the user table but not any of the code that uses the results of these queries. Can we make this change in a way that preserves all external properties? Yes. If an update would set is_activated to true, instead set it to the current date. Now define the refinement mapping that takes a new_user and returns an old_user. All columns will be unchanged except is_activated, which will be f(new_user).is_activated = if new_user.activated_at == NULL then FALSE else TRUE Now new code can use new_user directly while legacy code can use f(new_user) instead, which will behave indistinguishably from the old_user. A little more time passes and you decide to switch to an event sourcing-like model. So instead of an activated_at column, you have a user_events table, where every record is (user_id, timestamp, event). So adding an activate event will activate the user, adding a deactivate event will deactivate the user. Once again, we can update the queries but not any of the code that uses the results of these queries. Can we make a change that preserves all external properties? Yes. If an update would change is_activated, instead have it add an appropriate record to the event table. Now, define the refinement mapping that takes newer_user and returns new_user. The activated_at field will be computed like this: g(newer_user).activated_at = # last_activated_event let lae = newer_user.events .filter(event = "activate" | "deactivate") .last, in if lae.event == "activate" then lae.timestamp else NULL Now new code can use newer_user directly while old code can use g(newer_user) and the really old code can use f(g(newer_user)). Mutability constraints I said "these preserve all external properties" and that was a