Skip to content

Commit 4bc3f50

Browse files
committed
Add not null constraint to benchmark table's primary key in SQLite
SQLite allows primary keys to be null. For consistency with Postgres and sanity, add a not null constraint to the benchmark table's primary key. Also, add facility to support greater variety of migrations.
1 parent 6b155f9 commit 4bc3f50

File tree

1 file changed

+254
-115
lines changed

1 file changed

+254
-115
lines changed

database/src/pool/sqlite.rs

Lines changed: 254 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -64,115 +64,259 @@ impl Sqlite {
6464
}
6565
}
6666

67-
static MIGRATIONS: &[&str] = &[
68-
"",
69-
r#"
70-
create table benchmark(
71-
name text primary key,
72-
-- Whether this benchmark supports stable
73-
stabilized bool not null
74-
);
75-
create table artifact(
76-
id integer primary key not null,
77-
name text not null unique,
78-
date integer,
79-
type text not null
80-
);
81-
create table collection(
82-
id integer primary key not null
83-
);
84-
create table error_series(
85-
id integer primary key not null,
86-
crate text not null unique references benchmark(name) on delete cascade on update cascade
87-
);
88-
create table error(
89-
series integer not null references error_series(id) on delete cascade on update cascade,
90-
aid integer not null references artifact(id) on delete cascade on update cascade,
91-
error text,
92-
PRIMARY KEY(series, aid)
93-
);
94-
create table pstat_series(
95-
id integer primary key not null,
96-
crate text not null references benchmark(name) on delete cascade on update cascade,
97-
profile text not null,
98-
cache text not null,
99-
statistic text not null,
100-
UNIQUE(crate, profile, cache, statistic)
101-
);
102-
create table pstat(
103-
series integer references pstat_series(id) on delete cascade on update cascade,
104-
aid integer references artifact(id) on delete cascade on update cascade,
105-
cid integer references collection(id) on delete cascade on update cascade,
106-
value double not null,
107-
PRIMARY KEY(series, aid, cid)
108-
);
109-
create table self_profile_query_series(
110-
id integer primary key not null,
111-
crate text not null references benchmark(name) on delete cascade on update cascade,
112-
profile text not null,
113-
cache text not null,
114-
query text not null,
115-
UNIQUE(crate, profile, cache, query)
116-
);
117-
create table self_profile_query(
118-
series integer references self_profile_query_series(id) on delete cascade on update cascade,
119-
aid integer references artifact(id) on delete cascade on update cascade,
120-
cid integer references collection(id) on delete cascade on update cascade,
121-
self_time integer,
122-
blocked_time integer,
123-
incremental_load_time integer,
124-
number_of_cache_hits integer,
125-
invocation_count integer,
126-
PRIMARY KEY(series, aid, cid)
127-
);
128-
create table pull_request_builds(
129-
bors_sha text unique,
130-
pr integer not null,
131-
parent_sha text,
132-
complete boolean,
133-
requested timestamp without time zone
134-
);
135-
"#,
136-
r#"
137-
create table artifact_collection_duration(
138-
aid integer primary key not null references artifact(id) on delete cascade on update cascade,
139-
date_recorded timestamp without time zone not null,
140-
duration integer not null
141-
);
142-
"#,
143-
r#"
144-
create table collector_progress(
145-
aid integer not null references artifact(id) on delete cascade on update cascade,
146-
step text not null,
147-
start integer,
148-
end integer,
149-
UNIQUE(aid, step)
150-
);
151-
"#,
152-
r#"alter table collection add column perf_commit text;"#,
153-
r#"alter table pull_request_builds add column include text;"#,
154-
r#"alter table pull_request_builds add column exclude text;"#,
155-
r#"alter table pull_request_builds add column runs integer;"#,
156-
r#"
157-
create table rustc_compilation(
158-
aid integer references artifact(id) on delete cascade on update cascade,
159-
cid integer references collection(id) on delete cascade on update cascade,
160-
crate text not null,
161-
duration integer not null,
162-
PRIMARY KEY(aid, cid, crate)
163-
);
164-
"#,
165-
r#"alter table pull_request_builds rename to pull_request_build"#,
166-
r#"
167-
create table raw_self_profile(
168-
aid integer references artifact(id) on delete cascade on update cascade,
169-
cid integer references collection(id) on delete cascade on update cascade,
170-
crate text not null references benchmark(name) on delete cascade on update cascade,
171-
profile text not null,
172-
cache text not null,
173-
PRIMARY KEY(aid, cid, crate, profile, cache)
174-
);
175-
"#,
67+
struct Migration {
68+
/// One or more SQL statements, each terminated by a semicolon.
69+
sql: &'static str,
70+
71+
/// If false, indicates that foreign key checking should be delayed until after execution of
72+
/// the migration SQL, and foreign key `ON UPDATE` and `ON DELETE` actions disabled completely.
73+
foreign_key_constraints_enabled: bool,
74+
}
75+
76+
impl Migration {
77+
/// Returns a `Migration` with foreign key constraints enabled during execution.
78+
const fn new(sql: &'static str) -> Migration {
79+
Migration {
80+
sql,
81+
foreign_key_constraints_enabled: true,
82+
}
83+
}
84+
85+
/// Returns a `Migration` with foreign key checking delayed until after execution, and foreign
86+
/// key `ON UPDATE` and `ON DELETE` actions disabled completely.
87+
///
88+
/// SQLite has limited `ALTER TABLE` capabilities, so some schema alterations require the
89+
/// approach of replacing a table with a new one having the desired schema. Because there might
90+
/// be other tables with foreign key constraints on the table, these constraints need to be
91+
/// disabled during execution of such migration SQL, and reenabled after. Otherwise, dropping
92+
/// the old table may trigger `ON DELETE` actions in the referencing tables. See [SQLite
93+
/// documentation](https://www.sqlite.org/lang_altertable.html) for more information.
94+
const fn without_foreign_key_constraints(sql: &'static str) -> Migration {
95+
Migration {
96+
sql,
97+
foreign_key_constraints_enabled: false,
98+
}
99+
}
100+
101+
fn execute(&self, conn: &mut rusqlite::Connection, migration_id: i32) {
102+
if self.foreign_key_constraints_enabled {
103+
let tx = conn.transaction().unwrap();
104+
tx.execute_batch(&self.sql).unwrap();
105+
tx.pragma_update(None, "user_version", &migration_id)
106+
.unwrap();
107+
tx.commit().unwrap();
108+
return;
109+
}
110+
111+
// The following steps are reproduced from https://www.sqlite.org/lang_altertable.html,
112+
// from the section titled, "Making Other Kinds Of Table Schema Changes".
113+
114+
// 1. If foreign key constraints are enabled, disable them using PRAGMA foreign_keys=OFF.
115+
conn.pragma_update(None, "foreign_keys", &"OFF").unwrap();
116+
117+
// 2. Start a transaction.
118+
let tx = conn.transaction().unwrap();
119+
120+
// The migration SQL is responsible for steps 3 through 9.
121+
122+
// 3. Remember the format of all indexes, triggers, and views associated with table X.
123+
// This information will be needed in step 8 below. One way to do this is to run a
124+
// query like the following: SELECT type, sql FROM sqlite_schema WHERE tbl_name='X'.
125+
//
126+
// 4. Use CREATE TABLE to construct a new table "new_X" that is in the desired revised
127+
// format of table X. Make sure that the name "new_X" does not collide with any
128+
// existing table name, of course.
129+
//
130+
// 5. Transfer content from X into new_X using a statement like: INSERT INTO new_X SELECT
131+
// ... FROM X.
132+
//
133+
// 6. Drop the old table X: DROP TABLE X.
134+
//
135+
// 7. Change the name of new_X to X using: ALTER TABLE new_X RENAME TO X.
136+
//
137+
// 8. Use CREATE INDEX, CREATE TRIGGER, and CREATE VIEW to reconstruct indexes, triggers,
138+
// and views associated with table X. Perhaps use the old format of the triggers,
139+
// indexes, and views saved from step 3 above as a guide, making changes as appropriate
140+
// for the alteration.
141+
//
142+
// 9. If any views refer to table X in a way that is affected by the schema change, then
143+
// drop those views using DROP VIEW and recreate them with whatever changes are
144+
// necessary to accommodate the schema change using CREATE VIEW.
145+
tx.execute_batch(&self.sql).unwrap();
146+
147+
// 10. If foreign key constraints were originally enabled then run PRAGMA foreign_key_check
148+
// to verify that the schema change did not break any foreign key constraints.
149+
tx.pragma_query(None, "foreign_key_check", |row| {
150+
let table: String = row.get_unwrap(0);
151+
let row_id: Option<i64> = row.get_unwrap(1);
152+
let foreign_table: String = row.get_unwrap(2);
153+
let fk_idx: i64 = row.get_unwrap(3);
154+
155+
tx.query_row::<(), _, _>(
156+
"select * from pragma_foreign_key_list(?) where id = ?",
157+
params![&table, &fk_idx],
158+
|row| {
159+
let col: String = row.get_unwrap(3);
160+
let foreign_col: String = row.get_unwrap(4);
161+
panic!(
162+
"Foreign key violation encountered during migration\n\
163+
table: {},\n\
164+
column: {},\n\
165+
row_id: {:?},\n\
166+
foreign table: {},\n\
167+
foreign column: {}\n\
168+
migration ID: {}\n",
169+
table, col, row_id, foreign_table, foreign_col, migration_id,
170+
);
171+
},
172+
)
173+
.unwrap();
174+
Ok(())
175+
})
176+
.unwrap();
177+
178+
tx.pragma_update(None, "user_version", &migration_id)
179+
.unwrap();
180+
181+
// 11. Commit the transaction started in step 2.
182+
tx.commit().unwrap();
183+
184+
// 12. If foreign keys constraints were originally enabled, reenable them now.
185+
conn.pragma_update(None, "foreign_keys", &"ON").unwrap();
186+
}
187+
}
188+
189+
static MIGRATIONS: &[Migration] = &[
190+
Migration::new(""),
191+
Migration::new(
192+
r#"
193+
create table benchmark(
194+
name text primary key,
195+
-- Whether this benchmark supports stable
196+
stabilized bool not null
197+
);
198+
create table artifact(
199+
id integer primary key not null,
200+
name text not null unique,
201+
date integer,
202+
type text not null
203+
);
204+
create table collection(
205+
id integer primary key not null
206+
);
207+
create table error_series(
208+
id integer primary key not null,
209+
crate text not null unique references benchmark(name) on delete cascade on update cascade
210+
);
211+
create table error(
212+
series integer not null references error_series(id) on delete cascade on update cascade,
213+
aid integer not null references artifact(id) on delete cascade on update cascade,
214+
error text,
215+
PRIMARY KEY(series, aid)
216+
);
217+
create table pstat_series(
218+
id integer primary key not null,
219+
crate text not null references benchmark(name) on delete cascade on update cascade,
220+
profile text not null,
221+
cache text not null,
222+
statistic text not null,
223+
UNIQUE(crate, profile, cache, statistic)
224+
);
225+
create table pstat(
226+
series integer references pstat_series(id) on delete cascade on update cascade,
227+
aid integer references artifact(id) on delete cascade on update cascade,
228+
cid integer references collection(id) on delete cascade on update cascade,
229+
value double not null,
230+
PRIMARY KEY(series, aid, cid)
231+
);
232+
create table self_profile_query_series(
233+
id integer primary key not null,
234+
crate text not null references benchmark(name) on delete cascade on update cascade,
235+
profile text not null,
236+
cache text not null,
237+
query text not null,
238+
UNIQUE(crate, profile, cache, query)
239+
);
240+
create table self_profile_query(
241+
series integer references self_profile_query_series(id) on delete cascade on update cascade,
242+
aid integer references artifact(id) on delete cascade on update cascade,
243+
cid integer references collection(id) on delete cascade on update cascade,
244+
self_time integer,
245+
blocked_time integer,
246+
incremental_load_time integer,
247+
number_of_cache_hits integer,
248+
invocation_count integer,
249+
PRIMARY KEY(series, aid, cid)
250+
);
251+
create table pull_request_builds(
252+
bors_sha text unique,
253+
pr integer not null,
254+
parent_sha text,
255+
complete boolean,
256+
requested timestamp without time zone
257+
);
258+
"#,
259+
),
260+
Migration::new(
261+
r#"
262+
create table artifact_collection_duration(
263+
aid integer primary key not null references artifact(id) on delete cascade on update cascade,
264+
date_recorded timestamp without time zone not null,
265+
duration integer not null
266+
);
267+
"#,
268+
),
269+
Migration::new(
270+
r#"
271+
create table collector_progress(
272+
aid integer not null references artifact(id) on delete cascade on update cascade,
273+
step text not null,
274+
start integer,
275+
end integer,
276+
UNIQUE(aid, step)
277+
);
278+
"#,
279+
),
280+
Migration::new("alter table collection add column perf_commit text"),
281+
Migration::new("alter table pull_request_builds add column include text"),
282+
Migration::new("alter table pull_request_builds add column exclude text"),
283+
Migration::new("alter table pull_request_builds add column runs integer"),
284+
Migration::new(
285+
r#"
286+
create table rustc_compilation(
287+
aid integer references artifact(id) on delete cascade on update cascade,
288+
cid integer references collection(id) on delete cascade on update cascade,
289+
crate text not null,
290+
duration integer not null,
291+
PRIMARY KEY(aid, cid, crate)
292+
);
293+
"#,
294+
),
295+
Migration::new("alter table pull_request_builds rename to pull_request_build"),
296+
Migration::new(
297+
r#"
298+
create table raw_self_profile(
299+
aid integer references artifact(id) on delete cascade on update cascade,
300+
cid integer references collection(id) on delete cascade on update cascade,
301+
crate text not null references benchmark(name) on delete cascade on update cascade,
302+
profile text not null,
303+
cache text not null,
304+
PRIMARY KEY(aid, cid, crate, profile, cache)
305+
);
306+
"#,
307+
),
308+
// Add not null constraint to benchmark name.
309+
Migration::without_foreign_key_constraints(
310+
r#"
311+
create table benchmark_new(
312+
name text primary key not null,
313+
stabilized bool not null
314+
);
315+
insert into benchmark_new select * from benchmark where name is not null;
316+
drop table benchmark;
317+
alter table benchmark_new rename to benchmark;
318+
"#,
319+
),
176320
];
177321

178322
#[async_trait::async_trait]
@@ -193,12 +337,7 @@ impl ConnectionManager for Sqlite {
193337
)
194338
.unwrap();
195339
for mid in (version as usize + 1)..MIGRATIONS.len() {
196-
let sql = MIGRATIONS[mid];
197-
let tx = conn.transaction().unwrap();
198-
tx.execute_batch(&sql).unwrap();
199-
tx.pragma_update(None, "user_version", &(mid as i32))
200-
.unwrap();
201-
tx.commit().unwrap();
340+
MIGRATIONS[mid].execute(&mut conn, mid as i32);
202341
}
203342
});
204343

0 commit comments

Comments
 (0)