From f1a66c1f17594d879fed3a9f2c3ac7ae10a57d2f Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Sun, 5 Mar 2017 12:37:18 -0500 Subject: [PATCH 1/6] Remove the `max_version` column from Crates Updating columns like this tends to be pretty brittle unless it's done via a trigger. We can't update this one from a trigger, since PG doesn't know about semver's comparison rules. Either way, we don't really need to cache this unless it turns out to be a performance bottleneck. We can instead calculate the value on the fly. This does introduce some N+1 queries, and those may end up being a hotspot. We can definitely get rid of them, but it's a pain in the ass to do manually and Diesel has support for it built in, so I'd rather just fix them as we move those endpoints over. --- src/bin/migrate.rs | 6 +++++ src/krate.rs | 58 +++++++++++++++++++++++++--------------------- src/tests/all.rs | 1 - src/user/mod.rs | 5 ++-- 4 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs index fa905a7d7cf..4af4976f258 100644 --- a/src/bin/migrate.rs +++ b/src/bin/migrate.rs @@ -868,6 +868,12 @@ fn migrations() -> Vec { tx.execute("DROP TABLE reserved_crate_names", &[])?; Ok(()) }), + Migration::new(20170305123234, |tx| { + tx.execute("ALTER TABLE crates DROP COLUMN max_version", &[])?; + Ok(()) + }, |_| { + panic!("Unreversible migration") + }), ]; // NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"` diff --git a/src/krate.rs b/src/krate.rs index 6a228471f6e..d4f4afb389a 100644 --- a/src/krate.rs +++ b/src/krate.rs @@ -44,7 +44,6 @@ pub struct Crate { pub updated_at: Timespec, pub created_at: Timespec, pub downloads: i32, - pub max_version: semver::Version, pub description: Option, pub homepage: Option, pub documentation: Option, @@ -233,18 +232,20 @@ impl Crate { } pub fn minimal_encodable(self, + max_version: semver::Version, badges: Option>) -> EncodableCrate { - self.encodable(None, None, None, badges) + self.encodable(max_version, None, None, None, badges) } pub fn encodable(self, + max_version: semver::Version, versions: Option>, keywords: Option<&[Keyword]>, categories: Option<&[Category]>, badges: Option>) -> EncodableCrate { let Crate { - name, created_at, updated_at, downloads, max_version, description, + name, created_at, updated_at, downloads, description, homepage, documentation, license, repository, readme: _, id: _, max_upload_size: _, } = self; @@ -255,7 +256,7 @@ impl Crate { let keyword_ids = keywords.map(|kws| kws.iter().map(|kw| kw.keyword.clone()).collect()); let category_ids = categories.map(|cats| cats.iter().map(|cat| cat.slug.clone()).collect()); let badges = badges.map(|bs| { - bs.iter().map(|b| b.clone().encodable()).collect() + bs.into_iter().map(|b| b.encodable()).collect() }); EncodableCrate { id: name.clone(), @@ -282,6 +283,16 @@ impl Crate { } } + pub fn max_version(&self, conn: &GenericConnection) -> CargoResult { + let stmt = conn.prepare("SELECT num FROM versions WHERE crate_id = $1 + AND yanked = 'f'")?; + let rows = stmt.query(&[&self.id])?; + Ok(rows.iter() + .map(|r| semver::Version::parse(&r.get::<_, String>("num")).unwrap()) + .max() + .unwrap_or_else(|| semver::Version::parse("0.0.0").unwrap())) + } + pub fn versions(&self, conn: &GenericConnection) -> CargoResult> { let stmt = conn.prepare("SELECT * FROM versions \ WHERE crate_id = $1")?; @@ -384,14 +395,6 @@ impl Crate { } None => {} } - let zero = semver::Version::parse("0.0.0").unwrap(); - if *ver > self.max_version || self.max_version == zero { - self.max_version = ver.clone(); - } - let stmt = conn.prepare("UPDATE crates SET max_version = $1 - WHERE id = $2 RETURNING updated_at")?; - let rows = stmt.query(&[&self.max_version.to_string(), &self.id])?; - self.updated_at = rows.get(0).get("updated_at"); Version::insert(conn, self.id, ver, features, authors) } @@ -434,7 +437,7 @@ impl Crate { INNER JOIN crates ON crates.id = versions.crate_id WHERE dependencies.crate_id = $1 - AND versions.num = crates.max_version + AND versions.num = $2 "; let fetch_sql = format!("SELECT DISTINCT ON (crate_downloads, crate_name) dependencies.*, @@ -442,18 +445,19 @@ impl Crate { crates.name AS crate_name {} ORDER BY crate_downloads DESC - OFFSET $2 - LIMIT $3", + OFFSET $3 + LIMIT $4", select_sql); let count_sql = format!("SELECT COUNT(DISTINCT(crates.id)) {}", select_sql); let stmt = conn.prepare(&fetch_sql)?; - let vec: Vec<_> = stmt.query(&[&self.id, &offset, &limit])? + let max_version = self.max_version(conn)?.to_string(); + let vec: Vec<_> = stmt.query(&[&self.id, &max_version, &offset, &limit])? .iter() .map(|r| (Model::from_row(&r), r.get("crate_name"), r.get("crate_downloads"))) .collect(); let stmt = conn.prepare(&count_sql)?; - let cnt: i64 = stmt.query(&[&self.id])?.iter().next().unwrap().get(0); + let cnt: i64 = stmt.query(&[&self.id, &max_version])?.iter().next().unwrap().get(0); Ok((vec, cnt)) } @@ -461,7 +465,6 @@ impl Crate { impl Model for Crate { fn from_row(row: &Row) -> Crate { - let max: String = row.get("max_version"); Crate { id: row.get("id"), name: row.get("name"), @@ -472,7 +475,6 @@ impl Model for Crate { documentation: row.get("documentation"), homepage: row.get("homepage"), readme: row.get("readme"), - max_version: semver::Version::parse(&max).unwrap(), license: row.get("license"), repository: row.get("repository"), max_upload_size: row.get("max_upload_size"), @@ -603,8 +605,9 @@ pub fn index(req: &mut Request) -> CargoResult { let mut crates = Vec::new(); for row in stmt.query(&args)?.iter() { let krate: Crate = Model::from_row(&row); - let badges = krate.badges(conn); - crates.push(krate.minimal_encodable(badges.ok())); + let badges = krate.badges(conn)?; + let max_version = krate.max_version(conn)?; + crates.push(krate.minimal_encodable(max_version, Some(badges))); } // Query for the total count of crates @@ -637,10 +640,11 @@ pub fn summary(req: &mut Request) -> CargoResult { let to_crates = |stmt: pg::stmt::Statement| -> CargoResult> { let rows = stmt.query(&[])?; - Ok(rows.iter().map(|r| { + rows.iter().map(|r| { let krate: Crate = Model::from_row(&r); - krate.minimal_encodable(None) - }).collect::>()) + let max_version = krate.max_version(tx)?; + Ok(krate.minimal_encodable(max_version, None)) + }).collect() }; let new_crates = tx.prepare("SELECT * FROM crates \ ORDER BY created_at DESC LIMIT 10")?; @@ -692,6 +696,7 @@ pub fn show(req: &mut Request) -> CargoResult { let kws = krate.keywords(conn)?; let cats = krate.categories(conn)?; let badges = krate.badges(conn)?; + let max_version = krate.max_version(conn)?; #[derive(RustcEncodable)] struct R { @@ -702,7 +707,7 @@ pub fn show(req: &mut Request) -> CargoResult { } Ok(req.json(&R { krate: krate.clone().encodable( - Some(ids), Some(&kws), Some(&cats), Some(badges) + max_version, Some(ids), Some(&kws), Some(&cats), Some(badges) ), versions: versions.into_iter().map(|v| { v.encodable(&krate.name) @@ -785,6 +790,7 @@ pub fn new(req: &mut Request) -> CargoResult { &krate, new_crate.badges.unwrap_or_else(HashMap::new) )?; + let max_version = krate.max_version(req.tx()?)?; // Upload the crate to S3 let mut handle = req.app().handle(); @@ -855,7 +861,7 @@ pub fn new(req: &mut Request) -> CargoResult { #[derive(RustcEncodable)] struct R { krate: EncodableCrate, warnings: Warnings } Ok(req.json(&R { - krate: krate.minimal_encodable(None), + krate: krate.minimal_encodable(max_version, None), warnings: warnings })) } diff --git a/src/tests/all.rs b/src/tests/all.rs index a008de37d67..7d4acf62173 100755 --- a/src/tests/all.rs +++ b/src/tests/all.rs @@ -193,7 +193,6 @@ fn krate(name: &str) -> Crate { updated_at: time::now().to_timespec(), created_at: time::now().to_timespec(), downloads: 10, - max_version: semver::Version::parse("0.0.0").unwrap(), documentation: None, homepage: None, description: None, diff --git a/src/user/mod.rs b/src/user/mod.rs index 111127c5986..6ca3c47b84c 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -337,8 +337,9 @@ pub fn updates(req: &mut Request) -> CargoResult { // Encode everything! let crates = crates.into_iter().map(|c| { - c.minimal_encodable(None) - }).collect(); + let max_version = c.max_version(tx)?; + Ok(c.minimal_encodable(max_version, None)) + }).collect::>()?; let versions = versions.into_iter().map(|v| { let id = v.crate_id; v.encodable(&map[&id]) From eedadbe548f953523c5ba455342bdde482fb207c Mon Sep 17 00:00:00 2001 From: Gabriel Smith Date: Tue, 7 Mar 2017 09:52:53 -0500 Subject: [PATCH 2/6] Pull in tests from #582 --- .../krate_publish_after_yank_max_version | 41 ++++++ src/tests/http-data/krate_yank_max_version | 41 ++++++ src/tests/krate.rs | 124 ++++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 src/tests/http-data/krate_publish_after_yank_max_version create mode 100644 src/tests/http-data/krate_yank_max_version diff --git a/src/tests/http-data/krate_publish_after_yank_max_version b/src/tests/http-data/krate_publish_after_yank_max_version new file mode 100644 index 00000000000..2de5bd31263 --- /dev/null +++ b/src/tests/http-data/krate_publish_after_yank_max_version @@ -0,0 +1,41 @@ +===REQUEST 339 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/fyk_max/fyk_max-1.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk= +Content-Length: 0 +Host: alexcrichton-test.s3.amazonaws.com +Content-Type: application/x-tar +Date: Sun, 28 Jun 2015 14:07:17 -0700 + + +===RESPONSE 258 +HTTP/1.1 200 +x-amz-request-id: CB0E925D8E3AB3E8 +x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g= +date: Sun, 28 Jun 2015 21:07:51 GMT +etag: "d41d8cd98f00b204e9800998ecf8427e" +content-length: 0 +server: AmazonS3 + + +===REQUEST 339 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/fyk_max/fyk_max-2.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk= +Content-Length: 0 +Host: alexcrichton-test.s3.amazonaws.com +Content-Type: application/x-tar +Date: Sun, 28 Jun 2015 14:07:17 -0700 + + +===RESPONSE 258 +HTTP/1.1 200 +x-amz-request-id: CB0E925D8E3AB3E8 +x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g= +date: Sun, 28 Jun 2015 21:07:51 GMT +etag: "d41d8cd98f00b204e9800998ecf8427e" +content-length: 0 +server: AmazonS3 + diff --git a/src/tests/http-data/krate_yank_max_version b/src/tests/http-data/krate_yank_max_version new file mode 100644 index 00000000000..2de5bd31263 --- /dev/null +++ b/src/tests/http-data/krate_yank_max_version @@ -0,0 +1,41 @@ +===REQUEST 339 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/fyk_max/fyk_max-1.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk= +Content-Length: 0 +Host: alexcrichton-test.s3.amazonaws.com +Content-Type: application/x-tar +Date: Sun, 28 Jun 2015 14:07:17 -0700 + + +===RESPONSE 258 +HTTP/1.1 200 +x-amz-request-id: CB0E925D8E3AB3E8 +x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g= +date: Sun, 28 Jun 2015 21:07:51 GMT +etag: "d41d8cd98f00b204e9800998ecf8427e" +content-length: 0 +server: AmazonS3 + + +===REQUEST 339 +PUT http://alexcrichton-test.s3.amazonaws.com/crates/fyk_max/fyk_max-2.0.0.crate HTTP/1.1 +Accept: */* +Proxy-Connection: Keep-Alive +Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk= +Content-Length: 0 +Host: alexcrichton-test.s3.amazonaws.com +Content-Type: application/x-tar +Date: Sun, 28 Jun 2015 14:07:17 -0700 + + +===RESPONSE 258 +HTTP/1.1 200 +x-amz-request-id: CB0E925D8E3AB3E8 +x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g= +date: Sun, 28 Jun 2015 21:07:51 GMT +etag: "d41d8cd98f00b204e9800998ecf8427e" +content-length: 0 +server: AmazonS3 + diff --git a/src/tests/krate.rs b/src/tests/krate.rs index 767eade8491..4ada73d6eb7 100644 --- a/src/tests/krate.rs +++ b/src/tests/krate.rs @@ -882,6 +882,130 @@ fn yank_not_owner() { ::json::<::Bad>(&mut response); } +#[test] +fn yank_max_version() { + #[derive(RustcDecodable)] + struct O { + ok: bool, + } + let (_b, app, middle) = ::app(); + + // Upload a new crate + let mut req = ::new_req(app, "fyk_max", "1.0.0"); + ::mock_user(&mut req, ::user("foo")); + let mut response = ok_resp!(middle.call(&mut req)); + + // double check the max version + let json: GoodCrate = ::json(&mut response); + assert_eq!(json.krate.max_version, "1.0.0"); + + // add version 2.0.0 + let body = ::new_req_body_version_2(::krate("fyk_max")); + let mut response = ok_resp!(middle.call(req.with_path("/api/v1/crates/new") + .with_method(Method::Put) + .with_body(&body))); + let json: GoodCrate = ::json(&mut response); + assert_eq!(json.krate.max_version, "2.0.0"); + + // yank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Delete) + .with_path("/api/v1/crates/fyk_max/1.0.0/yank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert_eq!(json.krate.max_version, "2.0.0"); + + // unyank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Put) + .with_path("/api/v1/crates/fyk_max/1.0.0/unyank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert_eq!(json.krate.max_version, "2.0.0"); + + // yank version 2.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Delete) + .with_path("/api/v1/crates/fyk_max/2.0.0/yank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert_eq!(json.krate.max_version, "1.0.0"); + + // yank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Delete) + .with_path("/api/v1/crates/fyk_max/1.0.0/yank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert_eq!(json.krate.max_version, "0.0.0"); + + // unyank version 2.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Put) + .with_path("/api/v1/crates/fyk_max/2.0.0/unyank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert_eq!(json.krate.max_version, "2.0.0"); + + // unyank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Put) + .with_path("/api/v1/crates/fyk_max/1.0.0/unyank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert_eq!(json.krate.max_version, "2.0.0"); +} + +#[test] +fn publish_after_yank_max_version() { + #[derive(RustcDecodable)] + struct O { + ok: bool, + } + let (_b, app, middle) = ::app(); + + // Upload a new crate + let mut req = ::new_req(app, "fyk_max", "1.0.0"); + ::mock_user(&mut req, ::user("foo")); + let mut response = ok_resp!(middle.call(&mut req)); + + // double check the max version + let json: GoodCrate = ::json(&mut response); + assert_eq!(json.krate.max_version, "1.0.0"); + + // yank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Delete) + .with_path("/api/v1/crates/fyk_max/1.0.0/yank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert_eq!(json.krate.max_version, "0.0.0"); + + // add version 2.0.0 + let body = ::new_req_body_version_2(::krate("fyk_max")); + let mut response = ok_resp!(middle.call(req.with_path("/api/v1/crates/new") + .with_method(Method::Put) + .with_body(&body))); + let json: GoodCrate = ::json(&mut response); + assert_eq!(json.krate.max_version, "2.0.0"); + + // unyank version 1.0.0 + let mut r = ok_resp!(middle.call(req.with_method(Method::Put) + .with_path("/api/v1/crates/fyk_max/1.0.0/unyank"))); + assert!(::json::(&mut r).ok); + let mut response = ok_resp!(middle.call(req.with_method(Method::Get) + .with_path("/api/v1/crates/fyk_max"))); + let json: CrateResponse = ::json(&mut response); + assert_eq!(json.krate.max_version, "2.0.0"); +} + #[test] fn bad_keywords() { let (_b, app, middle) = ::app(); From 31b44cc818cc46fc290347bddc20033cdeb9718a Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Tue, 7 Mar 2017 10:09:05 -0500 Subject: [PATCH 3/6] Make the `max_version` migration reversible This version of the down function won't necessariliy 100% replicate the data that was dropped, but it'll get pretty darn close and probably close enough for our purposes if we end up actually having to roll this back. --- src/bin/migrate.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs index 4af4976f258..88c5731218e 100644 --- a/src/bin/migrate.rs +++ b/src/bin/migrate.rs @@ -871,8 +871,15 @@ fn migrations() -> Vec { Migration::new(20170305123234, |tx| { tx.execute("ALTER TABLE crates DROP COLUMN max_version", &[])?; Ok(()) - }, |_| { - panic!("Unreversible migration") + }, |tx| { + tx.execute("ALTER TABLE crates ADD COLUMN max_version VARCHAR NOT NULL DEFAULT '0.0.0'", &[])?; + tx.execute("UPDATE crates SET max_version = COALESCE(( + SELECT num FROM VERSIONS + WHERE num SIMILAR TO '[0-9]+\\.[0-9]+\\.[0-9]+' + ORDER BY split_part(num, '.', 1)::int, split_part(num, '.', 2)::int, split_part(num, '.', 3)::int + LIMIT 1 + ), '0.0.0')", &[])?; + Ok(()) }), ]; // NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"` From 99afd2f34a92e62e96276ecdc2b5ceace2a9d79c Mon Sep 17 00:00:00 2001 From: Gabriel Smith Date: Sat, 4 Mar 2017 14:44:40 -0500 Subject: [PATCH 4/6] Disable "Version does not exist" error if the version is null --- app/routes/crate/version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/crate/version.js b/app/routes/crate/version.js index 94c08cb4046..8785c3aca39 100644 --- a/app/routes/crate/version.js +++ b/app/routes/crate/version.js @@ -29,7 +29,7 @@ export default Ember.Route.extend({ return crate.get('versions') .then(versions => { const version = versions.find(version => version.get('num') === params.version_num); - if (!version) { + if (params.version_num && !version) { this.controllerFor('application').set('nextFlashError', `Version '${params.version_num}' of crate '${crate.get('name')}' does not exist`); } From f44f544661ecffa1a1d90ded67a7f967646733c1 Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Tue, 7 Mar 2017 10:10:57 -0500 Subject: [PATCH 5/6] Don't use version num 0.0.0, all versions have been yanked --- app/routes/crate/version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/crate/version.js b/app/routes/crate/version.js index 8785c3aca39..4f26c77f9aa 100644 --- a/app/routes/crate/version.js +++ b/app/routes/crate/version.js @@ -11,7 +11,7 @@ export default Ember.Route.extend({ const maxVersion = crate.get('max_version'); // Fall back to the crate's `max_version` property - if (!requestedVersion) { + if (!requestedVersion && maxVersion !== '0.0.0') { params.version_num = maxVersion; } From 13103497baca2da55d3749f5e25759ecf268651d Mon Sep 17 00:00:00 2001 From: "Carol (Nichols || Goulding)" Date: Tue, 7 Mar 2017 10:41:57 -0500 Subject: [PATCH 6/6] Remove the migration to drop the column, we'll do that in a separate deploy --- src/bin/migrate.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs index 88c5731218e..fa905a7d7cf 100644 --- a/src/bin/migrate.rs +++ b/src/bin/migrate.rs @@ -868,19 +868,6 @@ fn migrations() -> Vec { tx.execute("DROP TABLE reserved_crate_names", &[])?; Ok(()) }), - Migration::new(20170305123234, |tx| { - tx.execute("ALTER TABLE crates DROP COLUMN max_version", &[])?; - Ok(()) - }, |tx| { - tx.execute("ALTER TABLE crates ADD COLUMN max_version VARCHAR NOT NULL DEFAULT '0.0.0'", &[])?; - tx.execute("UPDATE crates SET max_version = COALESCE(( - SELECT num FROM VERSIONS - WHERE num SIMILAR TO '[0-9]+\\.[0-9]+\\.[0-9]+' - ORDER BY split_part(num, '.', 1)::int, split_part(num, '.', 2)::int, split_part(num, '.', 3)::int - LIMIT 1 - ), '0.0.0')", &[])?; - Ok(()) - }), ]; // NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"`