From 066aa378ae9f99d360886ff352bd0e5a47394e69 Mon Sep 17 00:00:00 2001 From: Denis Cornehl <denis@cornehl.org> Date: Wed, 21 Feb 2024 16:51:47 +0100 Subject: [PATCH] introduce aggregated build status on release & start using it --- ...695c7a88206171881f2d9951eb2f40125b244.json | 28 --- ...10ecf32505d1dad95e7d05bc310e9deb47411.json | 32 --- ...781ea30207795b833c4951f3c8303c49e699e.json | 12 + ...4741c42f9476ed8573511fe141883b86482d.json} | 8 +- ...7087ffc214cdaf08feb7bd25fa2b3ec7618b0.json | 22 ++ ...76af53e2e45f860edd6b2bb42ed5b45ae4efa.json | 34 +++ ...015344ee7f1d6add8cdeb3b71aee4dfd95eba.json | 32 +++ ...f63660684cdb61af270631fe29dfca2ed466c.json | 71 ------ ...958d5cee193e28ec6532bc1b1fbb98cfc3f16.json | 69 ++++++ ...f892efba0a451bb14fff77876ca250b0fab1.json} | 6 +- ...309082057_release_status_view.sql.down.sql | 1 + ...40309082057_release_status_view.sql.up.sql | 22 ++ src/db/add_package.rs | 23 +- src/db/types.rs | 25 ++ src/web/crate_details.rs | 231 ++++++++++++++---- src/web/releases.rs | 28 +-- src/web/rustdoc.rs | 4 +- 17 files changed, 448 insertions(+), 200 deletions(-) delete mode 100644 .sqlx/query-0f51891df12ccdbecbdffef1588695c7a88206171881f2d9951eb2f40125b244.json delete mode 100644 .sqlx/query-1379f2cb71474de2ce28373283210ecf32505d1dad95e7d05bc310e9deb47411.json create mode 100644 .sqlx/query-192d91c85fff00d311fa28dd7f5781ea30207795b833c4951f3c8303c49e699e.json rename .sqlx/{query-19123a7751cdf06658d0341289fb4d63bacdfb30ea9addededd2a55fb5638ad7.json => query-2e8ab3494453908d26decae9f9f64741c42f9476ed8573511fe141883b86482d.json} (63%) create mode 100644 .sqlx/query-42b5d5684d3813768048b4770997087ffc214cdaf08feb7bd25fa2b3ec7618b0.json create mode 100644 .sqlx/query-5ecd90db215d7bf3178869a320176af53e2e45f860edd6b2bb42ed5b45ae4efa.json create mode 100644 .sqlx/query-b823051d59855fc332ad37ecbcd015344ee7f1d6add8cdeb3b71aee4dfd95eba.json delete mode 100644 .sqlx/query-d672be5ae066efe9678be495580f63660684cdb61af270631fe29dfca2ed466c.json create mode 100644 .sqlx/query-d896b69c6f6061b0652862e2baa958d5cee193e28ec6532bc1b1fbb98cfc3f16.json rename .sqlx/{query-9a6a50ddbc1d07ef5648726f135eeca83ec922c4e9da07516b5f963b3551f4ee.json => query-e8f8c3649576bcba99393a2e78c9f892efba0a451bb14fff77876ca250b0fab1.json} (66%) create mode 100644 migrations/20240309082057_release_status_view.sql.down.sql create mode 100644 migrations/20240309082057_release_status_view.sql.up.sql diff --git a/.sqlx/query-0f51891df12ccdbecbdffef1588695c7a88206171881f2d9951eb2f40125b244.json b/.sqlx/query-0f51891df12ccdbecbdffef1588695c7a88206171881f2d9951eb2f40125b244.json deleted file mode 100644 index f7341988f..000000000 --- a/.sqlx/query-0f51891df12ccdbecbdffef1588695c7a88206171881f2d9951eb2f40125b244.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT\n target_name,\n rustdoc_status\n FROM releases\n WHERE releases.id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "target_name", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "rustdoc_status", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false, - false - ] - }, - "hash": "0f51891df12ccdbecbdffef1588695c7a88206171881f2d9951eb2f40125b244" -} diff --git a/.sqlx/query-1379f2cb71474de2ce28373283210ecf32505d1dad95e7d05bc310e9deb47411.json b/.sqlx/query-1379f2cb71474de2ce28373283210ecf32505d1dad95e7d05bc310e9deb47411.json deleted file mode 100644 index f1fbb008c..000000000 --- a/.sqlx/query-1379f2cb71474de2ce28373283210ecf32505d1dad95e7d05bc310e9deb47411.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "WITH dates AS (\n -- we need this series so that days in the statistic that don't have any releases are included\n SELECT generate_series(\n CURRENT_DATE - INTERVAL '30 days',\n CURRENT_DATE - INTERVAL '1 day',\n '1 day'::interval\n )::date AS date_\n ),\n release_stats AS (\n SELECT\n release_time::date AS date_,\n COUNT(*) AS counts,\n SUM(CAST((\n is_library = TRUE AND (\n SELECT builds.build_status\n FROM builds\n WHERE builds.rid = releases.id\n ORDER BY builds.build_time DESC\n LIMIT 1\n ) != 'success'\n ) AS INT)) AS failures\n FROM\n releases\n WHERE\n release_time >= CURRENT_DATE - INTERVAL '30 days' AND\n release_time < CURRENT_DATE\n GROUP BY\n release_time::date\n )\n SELECT\n dates.date_ AS \"date!\",\n COALESCE(rs.counts, 0) AS \"counts!\",\n COALESCE(rs.failures, 0) AS \"failures!\"\n FROM\n dates\n LEFT OUTER JOIN Release_stats AS rs ON dates.date_ = rs.date_\n\n ORDER BY\n dates.date_\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "date!", - "type_info": "Date" - }, - { - "ordinal": 1, - "name": "counts!", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "failures!", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - null, - null, - null - ] - }, - "hash": "1379f2cb71474de2ce28373283210ecf32505d1dad95e7d05bc310e9deb47411" -} diff --git a/.sqlx/query-192d91c85fff00d311fa28dd7f5781ea30207795b833c4951f3c8303c49e699e.json b/.sqlx/query-192d91c85fff00d311fa28dd7f5781ea30207795b833c4951f3c8303c49e699e.json new file mode 100644 index 000000000..1cb245393 --- /dev/null +++ b/.sqlx/query-192d91c85fff00d311fa28dd7f5781ea30207795b833c4951f3c8303c49e699e.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM builds", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "192d91c85fff00d311fa28dd7f5781ea30207795b833c4951f3c8303c49e699e" +} diff --git a/.sqlx/query-19123a7751cdf06658d0341289fb4d63bacdfb30ea9addededd2a55fb5638ad7.json b/.sqlx/query-2e8ab3494453908d26decae9f9f64741c42f9476ed8573511fe141883b86482d.json similarity index 63% rename from .sqlx/query-19123a7751cdf06658d0341289fb4d63bacdfb30ea9addededd2a55fb5638ad7.json rename to .sqlx/query-2e8ab3494453908d26decae9f9f64741c42f9476ed8573511fe141883b86482d.json index 509731d88..201281b82 100644 --- a/.sqlx/query-19123a7751cdf06658d0341289fb4d63bacdfb30ea9addededd2a55fb5638ad7.json +++ b/.sqlx/query-2e8ab3494453908d26decae9f9f64741c42f9476ed8573511fe141883b86482d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n crates.id AS crate_id,\n releases.id AS release_id,\n crates.name,\n releases.version,\n releases.description,\n releases.dependencies,\n releases.readme,\n releases.description_long,\n releases.release_time,\n COALESCE(builds.build_status, 'failure') as \"build_status!: BuildStatus\",\n (\n SELECT id\n FROM builds\n WHERE\n builds.rid = releases.id AND\n builds.build_status = 'success'\n ORDER BY build_time DESC\n LIMIT 1\n ) AS latest_build_id,\n releases.rustdoc_status,\n releases.archive_storage,\n releases.repository_url,\n releases.homepage_url,\n releases.keywords,\n releases.have_examples,\n releases.target_name,\n repositories.host as \"repo_host?\",\n repositories.stars as \"repo_stars?\",\n repositories.forks as \"repo_forks?\",\n repositories.issues as \"repo_issues?\",\n repositories.name as \"repo_name?\",\n releases.is_library,\n releases.yanked,\n releases.doc_targets,\n releases.license,\n releases.documentation_url,\n releases.default_target,\n builds.rustc_version as \"rustc_version?\",\n doc_coverage.total_items,\n doc_coverage.documented_items,\n doc_coverage.total_items_needing_examples,\n doc_coverage.items_with_examples\n FROM releases\n INNER JOIN crates ON releases.crate_id = crates.id\n LEFT JOIN doc_coverage ON doc_coverage.release_id = releases.id\n LEFT JOIN repositories ON releases.repository_id = repositories.id\n LEFT JOIN LATERAL (\n SELECT * FROM builds\n WHERE builds.rid = releases.id\n ORDER BY builds.build_time\n DESC LIMIT 1\n ) AS builds ON true\n WHERE crates.name = $1 AND releases.version = $2;", + "query": "SELECT\n crates.id AS crate_id,\n releases.id AS release_id,\n crates.name,\n releases.version,\n releases.description,\n releases.dependencies,\n releases.readme,\n releases.description_long,\n releases.release_time,\n release_build_status.build_status as \"build_status!: BuildStatus\",\n (\n -- this is the latest build ID that generated content\n -- it's used to invalidate some blob storage related caches.\n SELECT id\n FROM builds\n WHERE\n builds.rid = releases.id AND\n builds.build_status = 'success'\n ORDER BY build_time DESC\n LIMIT 1\n ) AS latest_build_id,\n releases.rustdoc_status,\n releases.archive_storage,\n releases.repository_url,\n releases.homepage_url,\n releases.keywords,\n releases.have_examples,\n releases.target_name,\n repositories.host as \"repo_host?\",\n repositories.stars as \"repo_stars?\",\n repositories.forks as \"repo_forks?\",\n repositories.issues as \"repo_issues?\",\n repositories.name as \"repo_name?\",\n releases.is_library,\n releases.yanked,\n releases.doc_targets,\n releases.license,\n releases.documentation_url,\n releases.default_target,\n (\n -- we're using the rustc version here to set the correct CSS file\n -- in the metadata.\n -- So we're only interested in successful builds here.\n SELECT rustc_version\n FROM builds\n WHERE\n builds.rid = releases.id AND\n builds.build_status = 'success'\n ORDER BY builds.build_time\n DESC LIMIT 1\n ) as \"rustc_version?\",\n doc_coverage.total_items,\n doc_coverage.documented_items,\n doc_coverage.total_items_needing_examples,\n doc_coverage.items_with_examples\n FROM releases\n INNER JOIN release_build_status ON releases.id = release_build_status.id\n INNER JOIN crates ON releases.crate_id = crates.id\n LEFT JOIN doc_coverage ON doc_coverage.release_id = releases.id\n LEFT JOIN repositories ON releases.repository_id = repositories.id\n WHERE crates.name = $1 AND releases.version = $2;", "describe": { "columns": [ { @@ -201,7 +201,7 @@ true, true, false, - null, + true, null, false, false, @@ -221,12 +221,12 @@ true, true, false, - false, + null, true, true, true, true ] }, - "hash": "19123a7751cdf06658d0341289fb4d63bacdfb30ea9addededd2a55fb5638ad7" + "hash": "2e8ab3494453908d26decae9f9f64741c42f9476ed8573511fe141883b86482d" } diff --git a/.sqlx/query-42b5d5684d3813768048b4770997087ffc214cdaf08feb7bd25fa2b3ec7618b0.json b/.sqlx/query-42b5d5684d3813768048b4770997087ffc214cdaf08feb7bd25fa2b3ec7618b0.json new file mode 100644 index 000000000..552902659 --- /dev/null +++ b/.sqlx/query-42b5d5684d3813768048b4770997087ffc214cdaf08feb7bd25fa2b3ec7618b0.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT crate_id\n FROM releases\n WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "crate_id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + }, + "hash": "42b5d5684d3813768048b4770997087ffc214cdaf08feb7bd25fa2b3ec7618b0" +} diff --git a/.sqlx/query-5ecd90db215d7bf3178869a320176af53e2e45f860edd6b2bb42ed5b45ae4efa.json b/.sqlx/query-5ecd90db215d7bf3178869a320176af53e2e45f860edd6b2bb42ed5b45ae4efa.json new file mode 100644 index 000000000..3641dd849 --- /dev/null +++ b/.sqlx/query-5ecd90db215d7bf3178869a320176af53e2e45f860edd6b2bb42ed5b45ae4efa.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT build_status as \"build_status!: BuildStatus\"\n FROM crates\n INNER JOIN releases ON crates.id = releases.crate_id\n INNER JOIN release_build_status ON releases.id = release_build_status.id\n WHERE crates.name = $1 AND releases.version = $2", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "build_status!: BuildStatus", + "type_info": { + "Custom": { + "name": "build_status", + "kind": { + "Enum": [ + "in_progress", + "success", + "failure" + ] + } + } + } + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + true + ] + }, + "hash": "5ecd90db215d7bf3178869a320176af53e2e45f860edd6b2bb42ed5b45ae4efa" +} diff --git a/.sqlx/query-b823051d59855fc332ad37ecbcd015344ee7f1d6add8cdeb3b71aee4dfd95eba.json b/.sqlx/query-b823051d59855fc332ad37ecbcd015344ee7f1d6add8cdeb3b71aee4dfd95eba.json new file mode 100644 index 000000000..728b0b892 --- /dev/null +++ b/.sqlx/query-b823051d59855fc332ad37ecbcd015344ee7f1d6add8cdeb3b71aee4dfd95eba.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "WITH dates AS (\n -- we need this series so that days in the statistic that don't have any releases are included\n SELECT generate_series(\n CURRENT_DATE - INTERVAL '30 days',\n CURRENT_DATE - INTERVAL '1 day',\n '1 day'::interval\n )::date AS date_\n ),\n release_stats AS (\n SELECT\n release_time::date AS date_,\n SUM(CAST(\n release_build_status.build_status != 'in_progress' AS INT\n )) AS counts,\n SUM(CAST((\n is_library = TRUE AND\n release_build_status.build_status = 'failure'\n ) AS INT)) AS failures\n FROM releases\n INNER JOIN release_build_status ON releases.id = release_build_status.id\n\n WHERE\n release_time >= CURRENT_DATE - INTERVAL '30 days' AND\n release_time < CURRENT_DATE\n GROUP BY\n release_time::date\n )\n SELECT\n dates.date_ AS \"date!\",\n COALESCE(rs.counts, 0) AS \"counts!\",\n COALESCE(rs.failures, 0) AS \"failures!\"\n FROM\n dates\n LEFT OUTER JOIN Release_stats AS rs ON dates.date_ = rs.date_\n\n ORDER BY\n dates.date_\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "date!", + "type_info": "Date" + }, + { + "ordinal": 1, + "name": "counts!", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "failures!", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + null, + null, + null + ] + }, + "hash": "b823051d59855fc332ad37ecbcd015344ee7f1d6add8cdeb3b71aee4dfd95eba" +} diff --git a/.sqlx/query-d672be5ae066efe9678be495580f63660684cdb61af270631fe29dfca2ed466c.json b/.sqlx/query-d672be5ae066efe9678be495580f63660684cdb61af270631fe29dfca2ed466c.json deleted file mode 100644 index 5a4059bfb..000000000 --- a/.sqlx/query-d672be5ae066efe9678be495580f63660684cdb61af270631fe29dfca2ed466c.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO releases (\n crate_id, version, release_time,\n dependencies, target_name, yanked, build_status,\n rustdoc_status, test_status, license, repository_url,\n homepage_url, description, description_long, readme,\n keywords, have_examples, downloads, files,\n doc_targets, is_library, doc_rustc_version,\n documentation_url, default_target, features,\n repository_id, archive_storage\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9,\n $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $21, $22, $23, $24, $25, $26, $27\n )\n ON CONFLICT (crate_id, version) DO UPDATE\n SET release_time = $3,\n dependencies = $4,\n target_name = $5,\n yanked = $6,\n build_status = $7,\n rustdoc_status = $8,\n test_status = $9,\n license = $10,\n repository_url = $11,\n homepage_url = $12,\n description = $13,\n description_long = $14,\n readme = $15,\n keywords = $16,\n have_examples = $17,\n downloads = $18,\n files = $19,\n doc_targets = $20,\n is_library = $21,\n doc_rustc_version = $22,\n documentation_url = $23,\n default_target = $24,\n features = $25,\n repository_id = $26,\n archive_storage = $27\n RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Int4", - "Varchar", - "Timestamptz", - "Json", - "Varchar", - "Bool", - "Bool", - "Bool", - "Bool", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Json", - "Bool", - "Int4", - "Json", - "Json", - "Bool", - "Varchar", - "Varchar", - "Varchar", - { - "Custom": { - "name": "_feature", - "kind": { - "Array": { - "Custom": { - "name": "feature", - "kind": { - "Composite": [ - [ - "name", - "Text" - ], - [ - "subfeatures", - "TextArray" - ] - ] - } - } - } - } - } - }, - "Int4", - "Bool" - ] - }, - "nullable": [ - false - ] - }, - "hash": "d672be5ae066efe9678be495580f63660684cdb61af270631fe29dfca2ed466c" -} diff --git a/.sqlx/query-d896b69c6f6061b0652862e2baa958d5cee193e28ec6532bc1b1fbb98cfc3f16.json b/.sqlx/query-d896b69c6f6061b0652862e2baa958d5cee193e28ec6532bc1b1fbb98cfc3f16.json new file mode 100644 index 000000000..35d0d9706 --- /dev/null +++ b/.sqlx/query-d896b69c6f6061b0652862e2baa958d5cee193e28ec6532bc1b1fbb98cfc3f16.json @@ -0,0 +1,69 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO releases (\n crate_id, version, release_time,\n dependencies, target_name, yanked,\n rustdoc_status, test_status, license, repository_url,\n homepage_url, description, description_long, readme,\n keywords, have_examples, downloads, files,\n doc_targets, is_library,\n documentation_url, default_target, features,\n repository_id, archive_storage\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9,\n $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $21, $22, $23, $24, $25\n )\n ON CONFLICT (crate_id, version) DO UPDATE\n SET release_time = $3,\n dependencies = $4,\n target_name = $5,\n yanked = $6,\n rustdoc_status = $7,\n test_status = $8,\n license = $9,\n repository_url = $10,\n homepage_url = $11,\n description = $12,\n description_long = $13,\n readme = $14,\n keywords = $15,\n have_examples = $16,\n downloads = $17,\n files = $18,\n doc_targets = $19,\n is_library = $20,\n documentation_url = $21,\n default_target = $22,\n features = $23,\n repository_id = $24,\n archive_storage = $25\n RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4", + "Varchar", + "Timestamptz", + "Json", + "Varchar", + "Bool", + "Bool", + "Bool", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Json", + "Bool", + "Int4", + "Json", + "Json", + "Bool", + "Varchar", + "Varchar", + { + "Custom": { + "name": "_feature", + "kind": { + "Array": { + "Custom": { + "name": "feature", + "kind": { + "Composite": [ + [ + "name", + "Text" + ], + [ + "subfeatures", + "TextArray" + ] + ] + } + } + } + } + } + }, + "Int4", + "Bool" + ] + }, + "nullable": [ + false + ] + }, + "hash": "d896b69c6f6061b0652862e2baa958d5cee193e28ec6532bc1b1fbb98cfc3f16" +} diff --git a/.sqlx/query-9a6a50ddbc1d07ef5648726f135eeca83ec922c4e9da07516b5f963b3551f4ee.json b/.sqlx/query-e8f8c3649576bcba99393a2e78c9f892efba0a451bb14fff77876ca250b0fab1.json similarity index 66% rename from .sqlx/query-9a6a50ddbc1d07ef5648726f135eeca83ec922c4e9da07516b5f963b3551f4ee.json rename to .sqlx/query-e8f8c3649576bcba99393a2e78c9f892efba0a451bb14fff77876ca250b0fab1.json index 6f2eaf532..319634d31 100644 --- a/.sqlx/query-9a6a50ddbc1d07ef5648726f135eeca83ec922c4e9da07516b5f963b3551f4ee.json +++ b/.sqlx/query-e8f8c3649576bcba99393a2e78c9f892efba0a451bb14fff77876ca250b0fab1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n releases.id,\n releases.version,\n COALESCE(builds.build_status, 'failure') as \"build_status!: BuildStatus\",\n releases.yanked,\n releases.is_library,\n releases.rustdoc_status,\n releases.target_name\n FROM releases\n LEFT JOIN LATERAL (\n SELECT build_status FROM builds\n WHERE builds.rid = releases.id\n ORDER BY builds.build_time\n DESC LIMIT 1\n ) AS builds ON true\n WHERE\n releases.crate_id = $1", + "query": "SELECT\n releases.id,\n releases.version,\n release_build_status.build_status as \"build_status!: BuildStatus\",\n releases.yanked,\n releases.is_library,\n releases.rustdoc_status,\n releases.target_name\n FROM releases\n INNER JOIN release_build_status ON releases.id = release_build_status.id\n WHERE\n releases.crate_id = $1", "describe": { "columns": [ { @@ -58,12 +58,12 @@ "nullable": [ false, false, - null, + true, false, false, false, false ] }, - "hash": "9a6a50ddbc1d07ef5648726f135eeca83ec922c4e9da07516b5f963b3551f4ee" + "hash": "e8f8c3649576bcba99393a2e78c9f892efba0a451bb14fff77876ca250b0fab1" } diff --git a/migrations/20240309082057_release_status_view.sql.down.sql b/migrations/20240309082057_release_status_view.sql.down.sql new file mode 100644 index 000000000..ff27e9eef --- /dev/null +++ b/migrations/20240309082057_release_status_view.sql.down.sql @@ -0,0 +1 @@ +DROP VIEW release_build_status; diff --git a/migrations/20240309082057_release_status_view.sql.up.sql b/migrations/20240309082057_release_status_view.sql.up.sql new file mode 100644 index 000000000..ac2ceb025 --- /dev/null +++ b/migrations/20240309082057_release_status_view.sql.up.sql @@ -0,0 +1,22 @@ +CREATE OR REPLACE VIEW release_build_status AS ( + SELECT + summary.id, + summary.last_build_time, + CASE + WHEN summary.success_count > 0 THEN 'success'::build_status + WHEN summary.failure_count > 0 THEN 'failure'::build_status + ELSE 'in_progress'::build_status + END as build_status + + FROM ( + SELECT + r.id, + MAX(b.build_time) as last_build_time, + SUM(CASE WHEN b.build_status = 'success' THEN 1 ELSE 0 END) as success_count, + SUM(CASE WHEN b.build_status = 'failure' THEN 1 ELSE 0 END) as failure_count + FROM + releases as r + LEFT OUTER JOIN builds AS b on b.rid = r.id + GROUP BY r.id + ) as summary +); diff --git a/src/db/add_package.rs b/src/db/add_package.rs index 03a860cbe..6fcfaaacf 100644 --- a/src/db/add_package.rs +++ b/src/db/add_package.rs @@ -144,6 +144,17 @@ pub async fn update_latest_version_id(conn: &mut sqlx::PgConnection, crate_id: i Ok(()) } +async fn crate_id_from_release_id(conn: &mut sqlx::PgConnection, release_id: i32) -> Result<i32> { + Ok(sqlx::query_scalar!( + "SELECT crate_id + FROM releases + WHERE id = $1", + release_id, + ) + .fetch_one(&mut *conn) + .await?) +} + pub(crate) async fn add_doc_coverage( conn: &mut sqlx::PgConnection, release_id: i32, @@ -183,7 +194,8 @@ pub(crate) async fn add_build_into_database( ) -> Result<i32> { debug!("Adding build into database"); let hostname = hostname::get()?; - Ok(sqlx::query_scalar!( + + let build_id = sqlx::query_scalar!( "INSERT INTO builds (rid, rustc_version, docsrs_version, build_status, build_server) VALUES ($1, $2, $3, $4, $5) RETURNING id", @@ -194,7 +206,14 @@ pub(crate) async fn add_build_into_database( hostname.to_str().unwrap_or(""), ) .fetch_one(&mut *conn) - .await?) + .await?; + + let crate_id = crate_id_from_release_id(&mut *conn, release_id).await?; + update_latest_version_id(&mut *conn, crate_id) + .await + .context("couldn't update latest version id")?; + + Ok(build_id) } async fn initialize_package_in_database( diff --git a/src/db/types.rs b/src/db/types.rs index ddf89d79f..17140c595 100644 --- a/src/db/types.rs +++ b/src/db/types.rs @@ -26,6 +26,7 @@ impl sqlx::postgres::PgHasArrayType for Feature { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)] #[sqlx(type_name = "build_status", rename_all = "snake_case")] +#[serde(rename_all = "snake_case")] pub(crate) enum BuildStatus { Success, Failure, @@ -37,3 +38,27 @@ impl BuildStatus { matches!(self, BuildStatus::Success) } } + +impl sqlx::postgres::PgHasArrayType for BuildStatus { + fn array_type_info() -> sqlx::postgres::PgTypeInfo { + sqlx::postgres::PgTypeInfo::with_name("_build_status") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_case::test_case; + + #[test_case(BuildStatus::Success, "success")] + #[test_case(BuildStatus::Failure, "failure")] + #[test_case(BuildStatus::InProgress, "in_progress")] + fn test_build_status_serialization(status: BuildStatus, expected: &str) { + let serialized = serde_json::to_string(&status).unwrap(); + assert_eq!(serialized, format!("\"{}\"", expected)); + assert_eq!( + serde_json::from_str::<BuildStatus>(&serialized).unwrap(), + status + ); + } +} diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs index babe3ebad..7e896a01e 100644 --- a/src/web/crate_details.rs +++ b/src/web/crate_details.rs @@ -90,6 +90,17 @@ where pub(crate) struct Release { pub id: i32, pub version: semver::Version, + /// Aggregated build status of the release. + /// * no builds -> build In progress + /// * any build is successful -> Success + /// -> even with failed or in-progress builds we have docs to show + /// * any build is failed -> Failure + /// -> we can only have Failure or InProgress here, so the Failure is the + /// important part on this aggregation level. + /// * the rest is all builds are in-progress -> InProgress + /// -> if we have any builds, and the previous conditions don't match, we end + /// up here, but we still check. + /// calculated in a database view : `release_build_status` pub build_status: BuildStatus, pub yanked: bool, pub is_library: bool, @@ -132,8 +143,10 @@ impl CrateDetails { releases.readme, releases.description_long, releases.release_time, - COALESCE(builds.build_status, 'failure') as "build_status!: BuildStatus", + release_build_status.build_status as "build_status!: BuildStatus", ( + -- this is the latest build ID that generated content + -- it's used to invalidate some blob storage related caches. SELECT id FROM builds WHERE @@ -160,21 +173,27 @@ impl CrateDetails { releases.license, releases.documentation_url, releases.default_target, - builds.rustc_version as "rustc_version?", + ( + -- we're using the rustc version here to set the correct CSS file + -- in the metadata. + -- So we're only interested in successful builds here. + SELECT rustc_version + FROM builds + WHERE + builds.rid = releases.id AND + builds.build_status = 'success' + ORDER BY builds.build_time + DESC LIMIT 1 + ) as "rustc_version?", doc_coverage.total_items, doc_coverage.documented_items, doc_coverage.total_items_needing_examples, doc_coverage.items_with_examples FROM releases + INNER JOIN release_build_status ON releases.id = release_build_status.id INNER JOIN crates ON releases.crate_id = crates.id LEFT JOIN doc_coverage ON doc_coverage.release_id = releases.id LEFT JOIN repositories ON releases.repository_id = repositories.id - LEFT JOIN LATERAL ( - SELECT * FROM builds - WHERE builds.rid = releases.id - ORDER BY builds.build_time - DESC LIMIT 1 - ) AS builds ON true WHERE crates.name = $1 AND releases.version = $2;"#, name, version.to_string(), @@ -333,13 +352,16 @@ impl CrateDetails { } pub(crate) fn latest_release(releases: &[Release]) -> Option<&Release> { - if let Some(release) = releases - .iter() - .find(|release| release.version.pre.is_empty() && !release.yanked) - { + if let Some(release) = releases.iter().find(|release| { + release.version.pre.is_empty() + && !release.yanked + && release.build_status != BuildStatus::InProgress + }) { Some(release) } else { - releases.first() + releases + .iter() + .find(|release| release.build_status != BuildStatus::InProgress) } } @@ -352,46 +374,41 @@ pub(crate) async fn releases_for_crate( r#"SELECT releases.id, releases.version, - COALESCE(builds.build_status, 'failure') as "build_status!: BuildStatus", + release_build_status.build_status as "build_status!: BuildStatus", releases.yanked, releases.is_library, releases.rustdoc_status, releases.target_name FROM releases - LEFT JOIN LATERAL ( - SELECT build_status FROM builds - WHERE builds.rid = releases.id - ORDER BY builds.build_time - DESC LIMIT 1 - ) AS builds ON true + INNER JOIN release_build_status ON releases.id = release_build_status.id WHERE releases.crate_id = $1"#, crate_id, ) .fetch(&mut *conn) .try_filter_map(|row| async move { - Ok( - match semver::Version::parse(&row.version).with_context(|| { - format!( - "invalid semver in database for crate {crate_id}: {}", - row.version - ) - }) { - Ok(semversion) => Some(Release { - id: row.id, - version: semversion, - build_status: row.build_status, - yanked: row.yanked, - is_library: row.is_library, - rustdoc_status: row.rustdoc_status, - target_name: row.target_name, - }), - Err(err) => { - report_error(&err); - None - } - }, - ) + let semversion = match semver::Version::parse(&row.version).with_context(|| { + format!( + "invalid semver in database for crate {crate_id}: {}", + row.version + ) + }) { + Ok(semver) => semver, + Err(err) => { + report_error(&err); + return Ok(None); + } + }; + + Ok(Some(Release { + id: row.id, + version: semversion, + build_status: row.build_status, + yanked: row.yanked, + is_library: row.is_library, + rustdoc_status: row.rustdoc_status, + target_name: row.target_name, + })) }) .try_collect() .await?; @@ -691,17 +708,46 @@ pub(crate) async fn get_all_platforms( #[cfg(test)] mod tests { use super::*; - use crate::registry_api::CrateOwner; use crate::test::{ assert_cache_control, assert_redirect, assert_redirect_cached, async_wrapper, wrapper, FakeBuild, TestDatabase, TestEnvironment, }; + use crate::{db::types::BuildStatus, registry_api::CrateOwner}; use anyhow::Error; use kuchikiki::traits::TendrilSink; use reqwest::StatusCode; use semver::Version; use std::collections::HashMap; + async fn release_build_status( + conn: &mut sqlx::PgConnection, + name: &str, + version: &str, + ) -> BuildStatus { + let status = sqlx::query_scalar!( + r#" + SELECT build_status as "build_status!: BuildStatus" + FROM crates + INNER JOIN releases ON crates.id = releases.crate_id + INNER JOIN release_build_status ON releases.id = release_build_status.id + WHERE crates.name = $1 AND releases.version = $2"#, + name, + version + ) + .fetch_one(&mut *conn) + .await + .unwrap(); + + assert_eq!( + crate_details(&mut *conn, name, version, None) + .await + .build_status, + status + ); + + status + } + async fn crate_details( conn: &mut sqlx::PgConnection, name: &str, @@ -1767,4 +1813,103 @@ mod tests { Ok(()) }) } + + #[test] + fn test_build_status_no_builds() { + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("dummy") + .version("0.1.0") + .create_async() + .await?; + + let mut conn = env.async_db().await.async_conn().await; + sqlx::query!("DELETE FROM builds") + .execute(&mut *conn) + .await?; + + assert_eq!( + release_build_status(&mut conn, "dummy", "0.1.0").await, + BuildStatus::InProgress + ); + + Ok(()) + }) + } + + #[test] + fn test_build_status_successful() { + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("dummy") + .version("0.1.0") + .builds(vec![ + FakeBuild::default().build_status(BuildStatus::Success), + FakeBuild::default().build_status(BuildStatus::Failure), + FakeBuild::default().build_status(BuildStatus::InProgress), + ]) + .create_async() + .await?; + + let mut conn = env.async_db().await.async_conn().await; + + assert_eq!( + release_build_status(&mut conn, "dummy", "0.1.0").await, + BuildStatus::Success + ); + + Ok(()) + }) + } + + #[test] + fn test_build_status_failed() { + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("dummy") + .version("0.1.0") + .builds(vec![ + FakeBuild::default().build_status(BuildStatus::Failure), + FakeBuild::default().build_status(BuildStatus::InProgress), + ]) + .create_async() + .await?; + + let mut conn = env.async_db().await.async_conn().await; + + assert_eq!( + release_build_status(&mut conn, "dummy", "0.1.0").await, + BuildStatus::Failure + ); + + Ok(()) + }) + } + + #[test] + fn test_build_status_in_progress() { + async_wrapper(|env| async move { + env.async_fake_release() + .await + .name("dummy") + .version("0.1.0") + .builds(vec![ + FakeBuild::default().build_status(BuildStatus::InProgress) + ]) + .create_async() + .await?; + + let mut conn = env.async_db().await.async_conn().await; + + assert_eq!( + release_build_status(&mut conn, "dummy", "0.1.0").await, + BuildStatus::InProgress + ); + + Ok(()) + }) + } } diff --git a/src/web/releases.rs b/src/web/releases.rs index 139e2ecf6..36f179e2e 100644 --- a/src/web/releases.rs +++ b/src/web/releases.rs @@ -77,9 +77,9 @@ pub(crate) async fn get_releases( // WARNING: it is _crucial_ that this always be hard-coded and NEVER be user input let (ordering, filter_failed): (&'static str, _) = match order { - Order::ReleaseTime => ("builds.build_time", false), + Order::ReleaseTime => ("release_build_status.last_build_time", false), Order::GithubStars => ("repositories.stars", false), - Order::RecentFailures => ("builds.build_time", true), + Order::RecentFailures => ("release_build_status.last_build_time", true), Order::FailuresByGithubStars => ("repositories.stars", true), }; @@ -89,14 +89,14 @@ pub(crate) async fn get_releases( releases.description, releases.target_name, releases.rustdoc_status, - builds.build_time, + release_build_status.last_build_time AS build_time, repositories.stars FROM crates {1} - INNER JOIN builds ON releases.id = builds.rid + INNER JOIN release_build_status ON releases.id = release_build_status.id LEFT JOIN repositories ON releases.repository_id = repositories.id WHERE - ((NOT $3) OR (builds.build_status != 'success' AND releases.is_library = TRUE)) + ((NOT $3) OR (release_build_status.build_status = 'failure' AND releases.is_library = TRUE)) AND {0} IS NOT NULL ORDER BY {0} DESC @@ -686,18 +686,16 @@ pub(crate) async fn activity_handler(mut conn: DbConnection) -> AxumResult<impl release_stats AS ( SELECT release_time::date AS date_, - COUNT(*) AS counts, + SUM(CAST( + release_build_status.build_status != 'in_progress' AS INT + )) AS counts, SUM(CAST(( - is_library = TRUE AND ( - SELECT builds.build_status - FROM builds - WHERE builds.rid = releases.id - ORDER BY builds.build_time DESC - LIMIT 1 - ) != 'success' + is_library = TRUE AND + release_build_status.build_status = 'failure' ) AS INT)) AS failures - FROM - releases + FROM releases + INNER JOIN release_build_status ON releases.id = release_build_status.id + WHERE release_time >= CURRENT_DATE - INTERVAL '30 days' AND release_time < CURRENT_DATE diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index 2a9d6e44e..56a0986e5 100644 --- a/src/web/rustdoc.rs +++ b/src/web/rustdoc.rs @@ -1,7 +1,7 @@ //! rustdoc handler use crate::{ - db::{types::BuildStatus, Pool}, + db::Pool, storage::rustdoc_archive_path, utils, web::{ @@ -550,7 +550,7 @@ pub(crate) async fn rustdoc_html_server_handler( // Find the path of the latest version for the `Go to latest` and `Permalink` links let mut current_target = String::new(); - let target_redirect = if latest_release.build_status == BuildStatus::Success { + let target_redirect = if latest_release.build_status.is_success() { let target = if target.is_empty() { current_target = krate.metadata.default_target.clone(); &krate.metadata.default_target