mirror of
https://github.com/jbranchaud/til
synced 2026-09-01 01:01:46 +00:00
Add Add Check Constraint To Existing Column as a SQLite TIL
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# Add Check Constraint To Existing Column
|
||||
|
||||
To not bury the lede, SQLite does not support adding a check constraint to an
|
||||
existing column. Instead, if I want to add one after the fact, I can reconstruct
|
||||
the table with the check constraint specified up front and then migrate the
|
||||
data.
|
||||
|
||||
Here is a `sessions` table that includes `start_time` and `end_time` columns
|
||||
that use the `text` data type to represent points in time.
|
||||
|
||||
```sql
|
||||
sqlite> .schema sessions
|
||||
CREATE TABLE sessions (
|
||||
id integer primary key,
|
||||
active integer not null check (active in (0, 1)),
|
||||
project_id integer not null references projects(id) on delete cascade,
|
||||
start_time text not null,
|
||||
end_time text,
|
||||
created_at text not null default (datetime('now')),
|
||||
updated_at text not null default (datetime('now'))
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_sessions_single_active
|
||||
on sessions(active)
|
||||
where active = 1;
|
||||
```
|
||||
|
||||
I want `start_time` and `end_time` to both enforce the shape of the timestamp
|
||||
strings with `check` constraints. To do that, I need to start a transaction,
|
||||
create a new version of the table with the check constraints, migrate the data,
|
||||
rename `sessions` to `sessions_old`, rename `sessions_new` to `sessions` (that's
|
||||
the in-place swap), and then commit the transaction. `sessions_old` can be
|
||||
dropped later once I feel good about the migration.
|
||||
|
||||
```sql
|
||||
begin transaction;
|
||||
|
||||
create table sessions_new (
|
||||
id integer primary key,
|
||||
active integer not null check (active in (0, 1)),
|
||||
project_id integer not null references projects(id) on delete cascade,
|
||||
start_time text not null,
|
||||
end_time text,
|
||||
created_at text not null default (datetime('now')),
|
||||
updated_at text not null default (datetime('now')),
|
||||
check(start_time is strftime('%Y-%m-%dT%H:%M:%fZ', start_time)),
|
||||
check(end_time is strftime('%Y-%m-%dT%H:%M:%fZ', end_time))
|
||||
);
|
||||
|
||||
insert into sessions_new (
|
||||
id, active, project_id, start_time, end_time, created_at, updated_at
|
||||
)
|
||||
select id, active, project_id, start_time, end_time, created_at, updated_at
|
||||
from sessions;
|
||||
|
||||
alter table sessions rename to sessions_old;
|
||||
|
||||
alter table sessions_new rename to sessions;
|
||||
|
||||
drop index if exists idx_sessions_single_active; -- on sessions_old
|
||||
|
||||
create unique index idx_sessions_single_active
|
||||
on sessions(active)
|
||||
where active = 1;
|
||||
|
||||
commit;
|
||||
```
|
||||
|
||||
Notice that after the table renames I also drop index (now pointing to
|
||||
`sessions_old`) and recreate it for the _new_ `sessions` table.
|
||||
|
||||
This approach worked well for my situation, but may not be a one-size-fits-all
|
||||
solution. Depending on how the database is deployed, the size of the tables, and
|
||||
usage, this approach may not scale. Always do a dry-run of database migrations
|
||||
like this.
|
||||
Reference in New Issue
Block a user