-
Notifications
You must be signed in to change notification settings - Fork 180
Make SQLite primary keys AUTOINCREMENT
#1390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| -- No-op. PostgreSQL sequences already prevent automatically generated job IDs | ||
| -- from being reused. | ||
| SELECT 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| -- No-op. PostgreSQL sequences already prevent automatically generated job IDs | ||
| -- from being reused. | ||
| SELECT 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| -- No-op. PostgreSQL sequences already prevent automatically generated job IDs | ||
| -- from being reused. | ||
| SELECT 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| -- No-op. PostgreSQL sequences already prevent automatically generated job IDs | ||
| -- from being reused. | ||
| SELECT 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| -- Rebuild river_job to restore SQLite's default ROWID allocation behavior. | ||
|
|
||
| DROP INDEX /* TEMPLATE: schema */river_job_kind; | ||
| DROP INDEX /* TEMPLATE: schema */river_job_state_and_finalized_at_index; | ||
| DROP INDEX /* TEMPLATE: schema */river_job_prioritized_fetching_index; | ||
| DROP INDEX /* TEMPLATE: schema */river_job_unique_idx; | ||
|
|
||
| ALTER TABLE /* TEMPLATE: schema */river_job RENAME TO river_job_old; | ||
|
|
||
| CREATE TABLE /* TEMPLATE: schema */river_job ( | ||
| id integer PRIMARY KEY, | ||
| args blob NOT NULL DEFAULT (jsonb('{}')), | ||
| attempt integer NOT NULL DEFAULT 0, | ||
| attempted_at timestamp, | ||
| attempted_by blob, -- json | ||
| created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| errors blob, -- json | ||
| finalized_at timestamp, | ||
| kind text NOT NULL, | ||
| max_attempts integer NOT NULL DEFAULT 25, | ||
| metadata blob NOT NULL DEFAULT (jsonb('{}')), | ||
| priority integer NOT NULL DEFAULT 1, | ||
| queue text NOT NULL DEFAULT 'default', | ||
| state text NOT NULL DEFAULT 'available', | ||
| scheduled_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| tags blob NOT NULL DEFAULT (jsonb('[]')), | ||
| unique_key blob, | ||
| unique_states integer, | ||
| CONSTRAINT finalized_or_finalized_at_null CHECK ( | ||
| (finalized_at IS NULL AND state NOT IN ('cancelled', 'completed', 'discarded')) OR | ||
| (finalized_at IS NOT NULL AND state IN ('cancelled', 'completed', 'discarded')) | ||
| ), | ||
| CONSTRAINT priority_in_range CHECK (priority >= 1 AND priority <= 4), | ||
| CONSTRAINT queue_length CHECK (length(queue) > 0 AND length(queue) < 128), | ||
| CONSTRAINT kind_length CHECK (length(kind) > 0 AND length(kind) < 128), | ||
| CONSTRAINT state_valid CHECK (state IN ('available', 'cancelled', 'completed', 'discarded', 'pending', 'retryable', 'running', 'scheduled')) | ||
| ); | ||
|
|
||
| INSERT INTO /* TEMPLATE: schema */river_job ( | ||
| id, | ||
| args, | ||
| attempt, | ||
| attempted_at, | ||
| attempted_by, | ||
| created_at, | ||
| errors, | ||
| finalized_at, | ||
| kind, | ||
| max_attempts, | ||
| metadata, | ||
| priority, | ||
| queue, | ||
| state, | ||
| scheduled_at, | ||
| tags, | ||
| unique_key, | ||
| unique_states | ||
| ) | ||
| SELECT | ||
| id, | ||
| args, | ||
| attempt, | ||
| attempted_at, | ||
| attempted_by, | ||
| created_at, | ||
| errors, | ||
| finalized_at, | ||
| kind, | ||
| max_attempts, | ||
| metadata, | ||
| priority, | ||
| queue, | ||
| state, | ||
| scheduled_at, | ||
| tags, | ||
| unique_key, | ||
| unique_states | ||
| FROM /* TEMPLATE: schema */river_job_old; | ||
|
|
||
| DROP TABLE /* TEMPLATE: schema */river_job_old; | ||
|
|
||
| CREATE INDEX /* TEMPLATE: schema */river_job_kind ON river_job (kind); | ||
| CREATE INDEX /* TEMPLATE: schema */river_job_state_and_finalized_at_index ON river_job (state, finalized_at) WHERE finalized_at IS NOT NULL; | ||
| CREATE INDEX /* TEMPLATE: schema */river_job_prioritized_fetching_index ON river_job (state, queue, priority, scheduled_at, id); | ||
| CREATE UNIQUE INDEX /* TEMPLATE: schema */river_job_unique_idx ON river_job (unique_key) | ||
| WHERE unique_key IS NOT NULL | ||
| AND unique_states IS NOT NULL | ||
| AND CASE state | ||
| WHEN 'available' THEN unique_states & (1 << 0) | ||
| WHEN 'cancelled' THEN unique_states & (1 << 1) | ||
| WHEN 'completed' THEN unique_states & (1 << 2) | ||
| WHEN 'discarded' THEN unique_states & (1 << 3) | ||
| WHEN 'pending' THEN unique_states & (1 << 4) | ||
| WHEN 'retryable' THEN unique_states & (1 << 5) | ||
| WHEN 'running' THEN unique_states & (1 << 6) | ||
| WHEN 'scheduled' THEN unique_states & (1 << 7) | ||
| ELSE 0 | ||
| END >= 1; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| -- Rebuild river_job so automatically generated IDs are never reused after the | ||
| -- job holding the largest ID is deleted. Unlike PostgreSQL sequences, SQLite's | ||
| -- default ROWID allocator may otherwise reuse that deleted ID. | ||
|
|
||
| DROP INDEX /* TEMPLATE: schema */river_job_kind; | ||
| DROP INDEX /* TEMPLATE: schema */river_job_state_and_finalized_at_index; | ||
| DROP INDEX /* TEMPLATE: schema */river_job_prioritized_fetching_index; | ||
| DROP INDEX /* TEMPLATE: schema */river_job_unique_idx; | ||
|
|
||
| ALTER TABLE /* TEMPLATE: schema */river_job RENAME TO river_job_old; | ||
|
|
||
| CREATE TABLE /* TEMPLATE: schema */river_job ( | ||
| id integer PRIMARY KEY AUTOINCREMENT, | ||
| args blob NOT NULL DEFAULT (jsonb('{}')), | ||
| attempt integer NOT NULL DEFAULT 0, | ||
| attempted_at timestamp, | ||
| attempted_by blob, -- json | ||
| created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| errors blob, -- json | ||
| finalized_at timestamp, | ||
| kind text NOT NULL, | ||
| max_attempts integer NOT NULL DEFAULT 25, | ||
| metadata blob NOT NULL DEFAULT (jsonb('{}')), | ||
| priority integer NOT NULL DEFAULT 1, | ||
| queue text NOT NULL DEFAULT 'default', | ||
| state text NOT NULL DEFAULT 'available', | ||
| scheduled_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| tags blob NOT NULL DEFAULT (jsonb('[]')), | ||
| unique_key blob, | ||
| unique_states integer, | ||
| CONSTRAINT finalized_or_finalized_at_null CHECK ( | ||
| (finalized_at IS NULL AND state NOT IN ('cancelled', 'completed', 'discarded')) OR | ||
| (finalized_at IS NOT NULL AND state IN ('cancelled', 'completed', 'discarded')) | ||
| ), | ||
| CONSTRAINT priority_in_range CHECK (priority >= 1 AND priority <= 4), | ||
| CONSTRAINT queue_length CHECK (length(queue) > 0 AND length(queue) < 128), | ||
| CONSTRAINT kind_length CHECK (length(kind) > 0 AND length(kind) < 128), | ||
| CONSTRAINT state_valid CHECK (state IN ('available', 'cancelled', 'completed', 'discarded', 'pending', 'retryable', 'running', 'scheduled')) | ||
| ); | ||
|
|
||
| INSERT INTO /* TEMPLATE: schema */river_job ( | ||
| id, | ||
| args, | ||
| attempt, | ||
| attempted_at, | ||
| attempted_by, | ||
| created_at, | ||
| errors, | ||
| finalized_at, | ||
| kind, | ||
| max_attempts, | ||
| metadata, | ||
| priority, | ||
| queue, | ||
| state, | ||
| scheduled_at, | ||
| tags, | ||
| unique_key, | ||
| unique_states | ||
| ) | ||
| SELECT | ||
| id, | ||
| args, | ||
| attempt, | ||
| attempted_at, | ||
| attempted_by, | ||
| created_at, | ||
| errors, | ||
| finalized_at, | ||
| kind, | ||
| max_attempts, | ||
| metadata, | ||
| priority, | ||
| queue, | ||
| state, | ||
| scheduled_at, | ||
| tags, | ||
| unique_key, | ||
| unique_states | ||
| FROM /* TEMPLATE: schema */river_job_old; | ||
|
|
||
| DROP TABLE /* TEMPLATE: schema */river_job_old; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dropping Upgrade scenario and suggested fixPro's migrations add If existing Pro SQLite databases are intentionally unsupported, please make this migration fail before the rebuild when it finds an installed Pro schema, and document the reset/upgrade path. If they are supported, the two migration lines need a coordinated upgrade that preserves or reconstructs Pro's data and schema in both allowed directions. A regression test should start at main 007 with Pro installed and populated, then migrate main to 008 and exercise Pro operations. |
||
|
|
||
| CREATE INDEX /* TEMPLATE: schema */river_job_kind ON river_job (kind); | ||
| CREATE INDEX /* TEMPLATE: schema */river_job_state_and_finalized_at_index ON river_job (state, finalized_at) WHERE finalized_at IS NOT NULL; | ||
| CREATE INDEX /* TEMPLATE: schema */river_job_prioritized_fetching_index ON river_job (state, queue, priority, scheduled_at, id); | ||
| CREATE UNIQUE INDEX /* TEMPLATE: schema */river_job_unique_idx ON river_job (unique_key) | ||
| WHERE unique_key IS NOT NULL | ||
| AND unique_states IS NOT NULL | ||
| AND CASE state | ||
| WHEN 'available' THEN unique_states & (1 << 0) | ||
| WHEN 'cancelled' THEN unique_states & (1 << 1) | ||
| WHEN 'completed' THEN unique_states & (1 << 2) | ||
| WHEN 'discarded' THEN unique_states & (1 << 3) | ||
| WHEN 'pending' THEN unique_states & (1 << 4) | ||
| WHEN 'retryable' THEN unique_states & (1 << 5) | ||
| WHEN 'running' THEN unique_states & (1 << 6) | ||
| WHEN 'scheduled' THEN unique_states & (1 << 7) | ||
| ELSE 0 | ||
| END >= 1; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AUTOINCREMENTstarts tracking IDs from the rows copied into the newriver_job, so an ID that exists only in Pro's dead letter table can still be handed out again after this migration. That leaves the original dead letter conflict possible for an upgraded database.Example and suggested fix
Suppose the highest live job ID is 41 and
river_job_dead_lettercontains an earlier job with ID 42. Copying the live rows initializessqlite_sequenceto 41; the next job gets 42, and moving it to dead letter fails on the existing row's primary key. I reproduced this allocation and conflict in SQLite. An empty live job table has the same problem for every archived ID.If existing Pro SQLite databases are supported, their upgrade should advance the job sequence past the maximum ID in both live and dead letter tables, in the same transaction, and test the subsequent dead letter move. For OSS-only databases, IDs that were issued and then deleted without any surviving record cannot be recovered; the migration can only guarantee no reuse going forward from the highest ID it can observe.