From 603aa5e3cc0aff3b1755fee98447e968a591ef0a Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Sun, 11 Jan 2026 14:24:34 +0100 Subject: [PATCH 1/2] show average build durations in build lists, crate details --- ...ccd8494d47310bdd2df36b2cdfbafde7d1fa6.json | 22 ++++ ...9aebea927233c23192fd7f4b1639b746d38bf.json | 22 ++++ ...c220a39117897820d0482ab3b08f8245cd46.json} | 10 +- ...ecf08566aecfa06e6a2c4a146b54213498b77.json | 22 ++++ Cargo.lock | 1 + ...9aebea927233c23192fd7f4b1639b746d38bf.json | 22 ++++ ...c220a39117897820d0482ab3b08f8245cd46.json} | 10 +- ...ecf08566aecfa06e6a2c4a146b54213498b77.json | 22 ++++ ...ccd8494d47310bdd2df36b2cdfbafde7d1fa6.json | 22 ++++ ...9aebea927233c23192fd7f4b1639b746d38bf.json | 22 ++++ ...c220a39117897820d0482ab3b08f8245cd46.json} | 10 +- ...ecf08566aecfa06e6a2c4a146b54213498b77.json | 22 ++++ crates/bin/docs_rs_web/Cargo.toml | 1 - crates/bin/docs_rs_web/src/handlers/builds.rs | 26 +++- .../docs_rs_web/src/handlers/crate_details.rs | 113 +++++++++++++++++- crates/bin/docs_rs_web/src/page/templates.rs | 54 ++++++--- .../docs_rs_web/templates/crate/builds.html | 32 ++++- .../docs_rs_web/templates/crate/details.html | 38 ++++-- crates/bin/docs_rs_web/templates/macros.html | 2 +- .../docs_rs_web/templates/style/style.scss | 3 +- .../20260129171944_build-primary-key.down.sql | 2 + .../20260129171944_build-primary-key.up.sql | 2 + crates/lib/docs_rs_types/Cargo.toml | 1 + crates/lib/docs_rs_types/src/convert/mod.rs | 86 +++++++++++++ crates/lib/docs_rs_types/src/duration.rs | 65 ++++++++++ crates/lib/docs_rs_types/src/lib.rs | 3 + 26 files changed, 590 insertions(+), 45 deletions(-) create mode 100644 .sqlx/query-0c639107bd612677b3f24a5204accd8494d47310bdd2df36b2cdfbafde7d1fa6.json create mode 100644 .sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json rename .sqlx/{query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json => query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json} (53%) create mode 100644 .sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json create mode 100644 crates/bin/cratesfyi/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json rename crates/bin/cratesfyi/.sqlx/{query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json => query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json} (53%) create mode 100644 crates/bin/cratesfyi/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json create mode 100644 crates/bin/docs_rs_web/.sqlx/query-0c639107bd612677b3f24a5204accd8494d47310bdd2df36b2cdfbafde7d1fa6.json create mode 100644 crates/bin/docs_rs_web/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json rename crates/bin/docs_rs_web/.sqlx/{query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json => query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json} (53%) create mode 100644 crates/bin/docs_rs_web/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json create mode 100644 crates/lib/docs_rs_database/migrations/20260129171944_build-primary-key.down.sql create mode 100644 crates/lib/docs_rs_database/migrations/20260129171944_build-primary-key.up.sql create mode 100644 crates/lib/docs_rs_types/src/convert/mod.rs create mode 100644 crates/lib/docs_rs_types/src/duration.rs diff --git a/.sqlx/query-0c639107bd612677b3f24a5204accd8494d47310bdd2df36b2cdfbafde7d1fa6.json b/.sqlx/query-0c639107bd612677b3f24a5204accd8494d47310bdd2df36b2cdfbafde7d1fa6.json new file mode 100644 index 000000000..090497497 --- /dev/null +++ b/.sqlx/query-0c639107bd612677b3f24a5204accd8494d47310bdd2df36b2cdfbafde7d1fa6.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT crate_id as \"id: CrateId\"\n FROM releases\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: CrateId", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0c639107bd612677b3f24a5204accd8494d47310bdd2df36b2cdfbafde7d1fa6" +} diff --git a/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json b/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json new file mode 100644 index 000000000..44024a33b --- /dev/null +++ b/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n\n FROM\n crates AS c\n INNER JOIN releases AS r on c.id = r.crate_id\n INNER JOIN builds AS b on r.id = b.rid\n\n WHERE\n c.id = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "duration!: Duration", + "type_info": "Interval" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + null + ] + }, + "hash": "2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf" +} diff --git a/.sqlx/query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json b/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json similarity index 53% rename from .sqlx/query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json rename to .sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json index 26969a794..9a23e4b28 100644 --- a/.sqlx/query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json +++ b/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", + "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n CASE\n WHEN builds.build_started IS NULL\n -- for old builds, `build_started` is empty.\n THEN NULL\n ELSE\n CASE\n -- for in-progress builds we show the duration until now\n WHEN builds.build_finished IS NULL\n THEN (CURRENT_TIMESTAMP - builds.build_started)\n -- for finished builds we can show the full duration\n ELSE (builds.build_finished - builds.build_started)\n END\n END AS \"build_duration?: Duration\",\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", "describe": { "columns": [ { @@ -41,6 +41,11 @@ }, { "ordinal": 5, + "name": "build_duration?: Duration", + "type_info": "Interval" + }, + { + "ordinal": 6, "name": "errors", "type_info": "Text" } @@ -57,8 +62,9 @@ true, false, null, + null, true ] }, - "hash": "b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee" + "hash": "7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46" } diff --git a/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json b/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json new file mode 100644 index 000000000..8c1d8916b --- /dev/null +++ b/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n FROM builds AS b\n WHERE\n b.rid = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "duration!: Duration", + "type_info": "Interval" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + null + ] + }, + "hash": "9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77" +} diff --git a/Cargo.lock b/Cargo.lock index c74628743..ff0fc7127 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2279,6 +2279,7 @@ dependencies = [ "sqlx", "strum", "test-case", + "thiserror 2.0.18", "tokio", ] diff --git a/crates/bin/cratesfyi/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json b/crates/bin/cratesfyi/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json new file mode 100644 index 000000000..44024a33b --- /dev/null +++ b/crates/bin/cratesfyi/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n\n FROM\n crates AS c\n INNER JOIN releases AS r on c.id = r.crate_id\n INNER JOIN builds AS b on r.id = b.rid\n\n WHERE\n c.id = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "duration!: Duration", + "type_info": "Interval" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + null + ] + }, + "hash": "2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf" +} diff --git a/crates/bin/cratesfyi/.sqlx/query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json b/crates/bin/cratesfyi/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json similarity index 53% rename from crates/bin/cratesfyi/.sqlx/query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json rename to crates/bin/cratesfyi/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json index 26969a794..9a23e4b28 100644 --- a/crates/bin/cratesfyi/.sqlx/query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json +++ b/crates/bin/cratesfyi/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", + "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n CASE\n WHEN builds.build_started IS NULL\n -- for old builds, `build_started` is empty.\n THEN NULL\n ELSE\n CASE\n -- for in-progress builds we show the duration until now\n WHEN builds.build_finished IS NULL\n THEN (CURRENT_TIMESTAMP - builds.build_started)\n -- for finished builds we can show the full duration\n ELSE (builds.build_finished - builds.build_started)\n END\n END AS \"build_duration?: Duration\",\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", "describe": { "columns": [ { @@ -41,6 +41,11 @@ }, { "ordinal": 5, + "name": "build_duration?: Duration", + "type_info": "Interval" + }, + { + "ordinal": 6, "name": "errors", "type_info": "Text" } @@ -57,8 +62,9 @@ true, false, null, + null, true ] }, - "hash": "b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee" + "hash": "7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46" } diff --git a/crates/bin/cratesfyi/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json b/crates/bin/cratesfyi/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json new file mode 100644 index 000000000..8c1d8916b --- /dev/null +++ b/crates/bin/cratesfyi/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n FROM builds AS b\n WHERE\n b.rid = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "duration!: Duration", + "type_info": "Interval" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + null + ] + }, + "hash": "9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77" +} diff --git a/crates/bin/docs_rs_web/.sqlx/query-0c639107bd612677b3f24a5204accd8494d47310bdd2df36b2cdfbafde7d1fa6.json b/crates/bin/docs_rs_web/.sqlx/query-0c639107bd612677b3f24a5204accd8494d47310bdd2df36b2cdfbafde7d1fa6.json new file mode 100644 index 000000000..090497497 --- /dev/null +++ b/crates/bin/docs_rs_web/.sqlx/query-0c639107bd612677b3f24a5204accd8494d47310bdd2df36b2cdfbafde7d1fa6.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT crate_id as \"id: CrateId\"\n FROM releases\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id: CrateId", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0c639107bd612677b3f24a5204accd8494d47310bdd2df36b2cdfbafde7d1fa6" +} diff --git a/crates/bin/docs_rs_web/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json b/crates/bin/docs_rs_web/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json new file mode 100644 index 000000000..44024a33b --- /dev/null +++ b/crates/bin/docs_rs_web/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n\n FROM\n crates AS c\n INNER JOIN releases AS r on c.id = r.crate_id\n INNER JOIN builds AS b on r.id = b.rid\n\n WHERE\n c.id = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "duration!: Duration", + "type_info": "Interval" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + null + ] + }, + "hash": "2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf" +} diff --git a/crates/bin/docs_rs_web/.sqlx/query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json b/crates/bin/docs_rs_web/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json similarity index 53% rename from crates/bin/docs_rs_web/.sqlx/query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json rename to crates/bin/docs_rs_web/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json index 26969a794..9a23e4b28 100644 --- a/crates/bin/docs_rs_web/.sqlx/query-b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee.json +++ b/crates/bin/docs_rs_web/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", + "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n CASE\n WHEN builds.build_started IS NULL\n -- for old builds, `build_started` is empty.\n THEN NULL\n ELSE\n CASE\n -- for in-progress builds we show the duration until now\n WHEN builds.build_finished IS NULL\n THEN (CURRENT_TIMESTAMP - builds.build_started)\n -- for finished builds we can show the full duration\n ELSE (builds.build_finished - builds.build_started)\n END\n END AS \"build_duration?: Duration\",\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", "describe": { "columns": [ { @@ -41,6 +41,11 @@ }, { "ordinal": 5, + "name": "build_duration?: Duration", + "type_info": "Interval" + }, + { + "ordinal": 6, "name": "errors", "type_info": "Text" } @@ -57,8 +62,9 @@ true, false, null, + null, true ] }, - "hash": "b7d805d65e97349dde8ac57e176bc92ae7ef49af5b09b0fc532f763947bd1aee" + "hash": "7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46" } diff --git a/crates/bin/docs_rs_web/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json b/crates/bin/docs_rs_web/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json new file mode 100644 index 000000000..8c1d8916b --- /dev/null +++ b/crates/bin/docs_rs_web/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n FROM builds AS b\n WHERE\n b.rid = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "duration!: Duration", + "type_info": "Interval" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + null + ] + }, + "hash": "9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77" +} diff --git a/crates/bin/docs_rs_web/Cargo.toml b/crates/bin/docs_rs_web/Cargo.toml index 4568a2b19..b8f71903e 100644 --- a/crates/bin/docs_rs_web/Cargo.toml +++ b/crates/bin/docs_rs_web/Cargo.toml @@ -97,4 +97,3 @@ pretty_assertions = { workspace = true } tempfile = { workspace = true } test-case = { workspace = true } walkdir = { workspace = true } - diff --git a/crates/bin/docs_rs_web/src/handlers/builds.rs b/crates/bin/docs_rs_web/src/handlers/builds.rs index 70d3aba22..b0c2555e3 100644 --- a/crates/bin/docs_rs_web/src/handlers/builds.rs +++ b/crates/bin/docs_rs_web/src/handlers/builds.rs @@ -21,7 +21,7 @@ use docs_rs_build_limits::Limits; use docs_rs_build_queue::{AsyncBuildQueue, PRIORITY_MANUAL_FROM_CRATES_IO}; use docs_rs_context::Context; use docs_rs_headers::CanonicalUrl; -use docs_rs_types::{BuildId, BuildStatus, KrateName, ReqVersion, Version}; +use docs_rs_types::{BuildId, BuildStatus, Duration, KrateName, ReqVersion, Version}; use http::StatusCode; use std::sync::Arc; @@ -32,6 +32,7 @@ pub(crate) struct Build { docsrs_version: Option, build_status: BuildStatus, build_time: Option>, + build_duration: Option, errors: Option, } @@ -195,6 +196,19 @@ async fn get_builds( builds.docsrs_version, builds.build_status as "build_status: BuildStatus", COALESCE(builds.build_finished, builds.build_started) as build_time, + CASE + WHEN builds.build_started IS NULL + -- for old builds, `build_started` is empty. + THEN NULL + ELSE + CASE + -- for in-progress builds we show the duration until now + WHEN builds.build_finished IS NULL + THEN (CURRENT_TIMESTAMP - builds.build_started) + -- for finished builds we can show the full duration + ELSE (builds.build_finished - builds.build_started) + END + END AS "build_duration?: Duration", builds.errors FROM builds INNER JOIN releases ON releases.id = builds.rid @@ -530,14 +544,14 @@ mod tests { }; Overrides::save(&mut conn, &FOO, limits).await?; - let page = kuchikiki::parse_html().one( + let page = kuchikiki::parse_html().one(dbg!( env.web_app() .await - .get(&format!("/crate/foo/{V1}/builds")) + .assert_success(&format!("/crate/foo/{V1}/builds")) .await? .text() - .await?, - ); + .await? + )); let header = page.select(".about h4").unwrap().next().unwrap(); assert_eq!(header.text_contents(), "foo's sandbox limits"); @@ -550,7 +564,7 @@ mod tests { let values: Vec<_> = values.iter().map(|v| &**v).collect(); assert!(values.contains(&"6.44 GB")); - assert!(values.contains(&"2 hours")); + assert!(values.contains(&"2h")); assert!(values.contains(&"102.4 kB")); assert!(values.contains(&"blocked")); assert!(values.contains(&"1")); diff --git a/crates/bin/docs_rs_web/src/handlers/crate_details.rs b/crates/bin/docs_rs_web/src/handlers/crate_details.rs index 5e7884661..f7de68178 100644 --- a/crates/bin/docs_rs_web/src/handlers/crate_details.rs +++ b/crates/bin/docs_rs_web/src/handlers/crate_details.rs @@ -23,7 +23,9 @@ use docs_rs_database::crate_details::{Release, latest_release, parse_doc_targets use docs_rs_headers::CanonicalUrl; use docs_rs_registry_api::OwnerKind; use docs_rs_storage::{AsyncStorage, PathNotFoundError}; -use docs_rs_types::{BuildId, BuildStatus, CrateId, KrateName, ReleaseId, ReqVersion, Version}; +use docs_rs_types::{ + BuildId, BuildStatus, CrateId, Duration, KrateName, ReleaseId, ReqVersion, Version, +}; use futures_util::stream::TryStreamExt; use serde_json::Value; use std::sync::Arc; @@ -342,6 +344,57 @@ impl CrateDetails { } } +#[derive(Debug, Clone, Default)] +struct BuildStatistics { + avg_build_duration_release: Option, + avg_build_duration_crate: Option, +} + +impl BuildStatistics { + fn has_data(&self) -> bool { + self.avg_build_duration_crate.is_some() || self.avg_build_duration_release.is_some() + } + + async fn fetch_for_release( + conn: &mut sqlx::PgConnection, + crate_id: CrateId, + release_id: ReleaseId, + ) -> Result { + Ok(Self { + avg_build_duration_release: sqlx::query_scalar!( + r#" + SELECT AVG(b.build_finished - b.build_started) AS "duration!: Duration" + FROM builds AS b + WHERE + b.rid = $1 AND + b.build_status = 'success' AND + b.build_started IS NOT NULL"#, + release_id as _, + ) + .fetch_optional(&mut *conn) + .await?, + avg_build_duration_crate: sqlx::query_scalar!( + r#" + SELECT + AVG(b.build_finished - b.build_started) AS "duration!: Duration" + + FROM + crates AS c + INNER JOIN releases AS r on c.id = r.crate_id + INNER JOIN builds AS b on r.id = b.rid + + WHERE + c.id = $1 AND + b.build_status = 'success' AND + b.build_started IS NOT NULL"#, + crate_id as _, + ) + .fetch_optional(&mut *conn) + .await?, + }) + } +} + #[derive(Debug, Clone, Template)] #[template(path = "crate/details.html")] struct CrateDetailsPage { @@ -352,6 +405,7 @@ struct CrateDetailsPage { documented_items: Option, total_items: Option, total_items_needing_examples: Option, + build_statistics: BuildStatistics, items_with_examples: Option, homepage_url: Option, documentation_url: Option, @@ -418,6 +472,9 @@ pub(crate) async fn crate_details_handler( Err(e) => warn!(?e, "error fetching readme"), } + let build_statistics = + BuildStatistics::fetch_for_release(&mut conn, details.crate_id, details.release_id).await?; + let CrateDetails { version, name, @@ -454,6 +511,7 @@ pub(crate) async fn crate_details_handler( documented_items, total_items, total_items_needing_examples, + build_statistics, items_with_examples, homepage_url, documentation_url, @@ -644,6 +702,7 @@ mod tests { use docs_rs_registry_api::CrateOwner; use docs_rs_test_fakes::{FakeBuild, fake_release_that_failed_before_build}; use docs_rs_types::KrateName; + use docs_rs_types::testing::{FOO, V1}; use http::StatusCode; use kuchikiki::traits::TendrilSink; use pretty_assertions::assert_eq; @@ -2311,4 +2370,56 @@ path = "src/lib.rs" Ok(()) }); } + + #[tokio::test(flavor = "multi_thread")] + async fn test_build_stats_no_data() -> Result<()> { + let env = TestEnvironment::new().await?; + let mut conn = env.async_conn().await?; + + let stats = + BuildStatistics::fetch_for_release(&mut conn, CrateId(41), ReleaseId(42)).await?; + assert!(!stats.has_data()); + assert!(stats.avg_build_duration_release.is_none()); + assert!(stats.avg_build_duration_crate.is_none()); + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_build_stats_with_build() -> Result<()> { + let env = TestEnvironment::new().await?; + + let rid = env + .fake_release() + .await + .name(&FOO) + .version(V1) + .create() + .await?; + + let mut conn = env.async_conn().await?; + let crate_id = sqlx::query_scalar!( + r#" + SELECT crate_id as "id: CrateId" + FROM releases + WHERE id = $1 + "#, + rid as _ + ) + .fetch_one(&mut *conn) + .await?; + + let stats = BuildStatistics::fetch_for_release(&mut conn, crate_id, rid).await?; + assert!(stats.has_data()); + assert!(stats.avg_build_duration_release.is_some()); + assert!(stats.avg_build_duration_crate.is_some()); + + assert!( + !BuildStatistics::fetch_for_release(&mut conn, CrateId(41), ReleaseId(42)) + .await? + .has_data() + ); + + Ok(()) + } } diff --git a/crates/bin/docs_rs_web/src/page/templates.rs b/crates/bin/docs_rs_web/src/page/templates.rs index 90f41f3f6..c570920f6 100644 --- a/crates/bin/docs_rs_web/src/page/templates.rs +++ b/crates/bin/docs_rs_web/src/page/templates.rs @@ -94,6 +94,7 @@ pub mod filters { use chrono::{DateTime, Utc}; use std::borrow::Cow; use std::fmt::Display; + use std::time::Duration; pub fn escape_html_inner(input: &str) -> askama::Result { if !input.chars().any(|c| "&<>\"'/".contains(c)) { @@ -144,27 +145,26 @@ pub mod filters { } #[askama::filter_fn] - pub fn format_secs(mut value: f32, _: &dyn Values) -> askama::Result { - const TIMES: &[&str] = &["seconds", "minutes", "hours"]; + pub fn format_duration(duration: &Duration, _: &dyn Values) -> askama::Result> { + let mut secs = duration.as_secs(); - let mut chosen_time = &TIMES[0]; + let hours = secs / 3_600; + secs %= 3_600; + let minutes = secs / 60; + let seconds = secs % 60; - for time in &TIMES[1..] { - if value / 60.0 >= 1.0 { - chosen_time = time; - value /= 60.0; - } else { - break; - } + let mut parts = Vec::new(); + if hours > 0 { + parts.push(format!("{hours}h")); } - - // TODO: This formatting section can be optimized, two string allocations aren't needed - let mut value = format!("{value:.1}"); - if value.ends_with(".0") { - value.truncate(value.len() - 2); + if minutes > 0 { + parts.push(format!("{minutes}m")); + } + if seconds > 0 || parts.is_empty() { + parts.push(format!("{seconds}s")); } - Ok(format!("{value} {chosen_time}")) + Ok(Safe(parts.join(" "))) } /// Dedent a string by removing all leading whitespace @@ -293,3 +293,25 @@ fn render( askama::filters::Safe(icon) } + +#[cfg(test)] +mod tests { + use super::*; + use std::{any::Any, collections::HashMap, time::Duration}; + use test_case::test_case; + + #[test_case(Duration::from_secs(0) => "0s"; "zero")] + #[test_case(Duration::from_secs(1) => "1s"; "simple")] + #[test_case(Duration::from_micros(2123456) => "2s"; "cuts microseconds")] + #[test_case(Duration::from_secs(3723) => "1h 2m 3s"; "hours minutes seconds")] + #[test_case(Duration::from_secs(120) => "2m"; "just minutes")] + #[test_case(Duration::from_secs(2123456) => "589h 50m 56s"; "big")] + fn test_format_duration(duration: Duration) -> String { + let values: HashMap<&str, Box> = HashMap::new(); + + filters::format_duration::default() + .execute(&duration, &values) + .unwrap() + .to_string() + } +} diff --git a/crates/bin/docs_rs_web/templates/crate/builds.html b/crates/bin/docs_rs_web/templates/crate/builds.html index 5e823c041..2ef062f90 100644 --- a/crates/bin/docs_rs_web/templates/crate/builds.html +++ b/crates/bin/docs_rs_web/templates/crate/builds.html @@ -26,7 +26,6 @@
    -
  • {%- for build in builds -%}
  • {%- if build.build_status != "in_progress" -%} @@ -41,14 +40,14 @@ {{ crate::icons::IconX.render_solid(false, false, "") }} {%- endif -%} {#- -#} -
    +
    {%- if let Some(rustc_version) = build.rustc_version -%} {{ rustc_version }} {%- else -%} — {%- endif -%}
    {#- -#} -
    +
    {%- if let Some(docsrs_version) = build.docsrs_version -%} {{ docsrs_version }} {%- else -%} @@ -61,7 +60,14 @@ {%- else -%} — {%- endif -%} -
    {#- -#} +
    +
    + {%- if let Some(build_duration) = build.build_duration -%} + took {{ build_duration|format_duration }} + {%- else -%} + — + {%- endif -%} +
    {#- -#} {%- else -%} @@ -70,8 +76,21 @@
    {{- crate::icons::IconGear.render_solid(false, true, "") -}}
    {#- -#} -
    {#- -#} - In the build queue {#- -#} +
    {#- -#} + in progress {#- -#} +
    +
    + — +
    {#- -#} +
    + — +
    {#- -#} +
    + {%- if let Some(build_duration) = build.build_duration -%} + since {{ build_duration|format_duration }} + {%- else -%} + — + {%- endif -%}
    @@ -107,3 +126,4 @@

    {{ metadata.name }}'s sandbox limits

    {%- endblock body -%} + diff --git a/crates/bin/docs_rs_web/templates/crate/details.html b/crates/bin/docs_rs_web/templates/crate/details.html index 555962e38..dbcc8027a 100644 --- a/crates/bin/docs_rs_web/templates/crate/details.html +++ b/crates/bin/docs_rs_web/templates/crate/details.html @@ -39,17 +39,41 @@ {%- if let Some(source_size) = source_size -%}
  • Size
  • - Source code size: {{(*source_size)|filesizeformat}} - {{- crate::icons::IconCircleInfo.render_solid(false, false, "") -}} - This is the summed size of all the files inside the crates.io package for this release. + Source code size: {{(*source_size)|filesizeformat}} + {{- crate::icons::IconCircleInfo.render_solid(false, false, "") -}} + This is the summed size of all the files inside the crates.io package for this release. +
  • {%- if let Some(doc_size) = documentation_size -%}
  • - Documentation size: {{(*doc_size)|filesizeformat}} - {{- crate::icons::IconCircleInfo.render_solid(false, false, "") -}} - This is the summed size of all files generated by rustdoc for all configured targets - + Documentation size: {{(*doc_size)|filesizeformat}} + {{- crate::icons::IconCircleInfo.render_solid(false, false, "") -}} + This is the summed size of all files generated by rustdoc for all configured targets + + +
  • + {%- endif -%} + {%- endif -%} + + {%- if build_statistics.has_data() -%} +
  • Ø build duration
  • + {%- if let Some(duration) = build_statistics.avg_build_duration_release -%} +
  • + this release: {{duration|format_duration}} + {{- crate::icons::IconCircleInfo.render_solid(false, false, "") -}} + Average build duration of successful builds. + + +
  • + {%- endif -%} + {%- if let Some(duration) = build_statistics.avg_build_duration_crate -%} +
  • + all releases: {{duration|format_duration}} + {{- crate::icons::IconCircleInfo.render_solid(false, false, "") -}} + Average build duration of successful builds in releases after 2024-10-23. + +
  • {%- endif -%} {%- endif -%} diff --git a/crates/bin/docs_rs_web/templates/macros.html b/crates/bin/docs_rs_web/templates/macros.html index c6d3d10e3..95c43cc56 100644 --- a/crates/bin/docs_rs_web/templates/macros.html +++ b/crates/bin/docs_rs_web/templates/macros.html @@ -41,7 +41,7 @@ Maximum rustdoc execution time - {{ limits.timeout.as_secs_f32()|format_secs }} + {{ limits.timeout|format_duration }} diff --git a/crates/bin/docs_rs_web/templates/style/style.scss b/crates/bin/docs_rs_web/templates/style/style.scss index dd5ec025f..ac8eb7faf 100644 --- a/crates/bin/docs_rs_web/templates/style/style.scss +++ b/crates/bin/docs_rs_web/templates/style/style.scss @@ -386,7 +386,8 @@ div.recent-releases-container { } } - .date { + .date, + .duration { font-weight: normal; @media #{$media-sm} { diff --git a/crates/lib/docs_rs_database/migrations/20260129171944_build-primary-key.down.sql b/crates/lib/docs_rs_database/migrations/20260129171944_build-primary-key.down.sql new file mode 100644 index 000000000..a102fea22 --- /dev/null +++ b/crates/lib/docs_rs_database/migrations/20260129171944_build-primary-key.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE builds DROP CONSTRAINT builds_pkey; +DROP INDEX builds_build_started_idx ; diff --git a/crates/lib/docs_rs_database/migrations/20260129171944_build-primary-key.up.sql b/crates/lib/docs_rs_database/migrations/20260129171944_build-primary-key.up.sql new file mode 100644 index 000000000..06db03aa5 --- /dev/null +++ b/crates/lib/docs_rs_database/migrations/20260129171944_build-primary-key.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE builds ADD PRIMARY KEY (id); +CREATE INDEX builds_build_started_idx ON builds USING btree (build_started); diff --git a/crates/lib/docs_rs_types/Cargo.toml b/crates/lib/docs_rs_types/Cargo.toml index c363f0c18..0e07d2f86 100644 --- a/crates/lib/docs_rs_types/Cargo.toml +++ b/crates/lib/docs_rs_types/Cargo.toml @@ -19,6 +19,7 @@ serde_json = { workspace = true } serde_with = { workspace = true } sqlx = { workspace = true } strum = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/crates/lib/docs_rs_types/src/convert/mod.rs b/crates/lib/docs_rs_types/src/convert/mod.rs new file mode 100644 index 000000000..9ad5d5996 --- /dev/null +++ b/crates/lib/docs_rs_types/src/convert/mod.rs @@ -0,0 +1,86 @@ +use sqlx::postgres::types::PgInterval; +use std::time::Duration; + +#[derive(Debug, thiserror::Error)] +pub enum IntervalError { + #[error("months not supported")] + MonthsNotSupported, + #[error("negative duration")] + NegativeDuration, + #[error("duration too large")] + DurationTooLarge, +} + +pub(crate) fn interval_to_duration(interval: PgInterval) -> Result { + if interval.months != 0 { + return Err(IntervalError::MonthsNotSupported); + } + + if interval.days < 0 || interval.microseconds < 0 { + return Err(IntervalError::NegativeDuration); + } + + Ok(Duration::from_hours(interval.days as u64 * 24) + + Duration::from_micros(interval.microseconds as u64)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_month_is_invalid() { + let interval = PgInterval { + months: 1, + days: 0, + microseconds: 0, + }; + let result = interval_to_duration(interval); + assert!(matches!(result, Err(IntervalError::MonthsNotSupported))); + } + + #[test] + fn test_negative_day_is_invalid() { + let interval = PgInterval { + months: 0, + days: -1, + microseconds: 0, + }; + let result = interval_to_duration(interval); + assert!(matches!(result, Err(IntervalError::NegativeDuration))); + } + + #[test] + fn test_negative_ms_is_invalid() { + let interval = PgInterval { + months: 0, + days: 0, + microseconds: -1, + }; + let result = interval_to_duration(interval); + assert!(matches!(result, Err(IntervalError::NegativeDuration))); + } + + #[test] + fn test_simple_conversion() { + let interval = PgInterval { + months: 0, + days: 1, + microseconds: 1_000_000, + }; + let result = interval_to_duration(interval).unwrap(); + assert_eq!(result, Duration::from_secs(86401)); + } + + #[test] + fn test_with_microseconds_conversion() { + const MICROS: i64 = 1_123_456; + let interval = PgInterval { + months: 0, + days: 0, + microseconds: MICROS, + }; + let result = interval_to_duration(interval).unwrap(); + assert_eq!(result, Duration::from_micros(MICROS as u64)); + } +} diff --git a/crates/lib/docs_rs_types/src/duration.rs b/crates/lib/docs_rs_types/src/duration.rs new file mode 100644 index 000000000..8b36b6e74 --- /dev/null +++ b/crates/lib/docs_rs_types/src/duration.rs @@ -0,0 +1,65 @@ +mod duration_impl { + use sqlx::postgres::types::PgInterval; + use sqlx::{ + Postgres, + error::BoxDynError, + postgres::{PgTypeInfo, PgValueRef}, + prelude::*, + }; + use std::ops::Deref; + use std::time::Duration as StdDuration; + + /// NewType around std Duration to be able to use it with sqlx. + /// + /// For now only for decoding intervals from the database. + #[derive(Clone, Debug, Eq, Hash, PartialEq)] + pub struct Duration(pub StdDuration); + + impl Duration { + pub const fn from_secs(secs: u64) -> Duration { + Self(StdDuration::from_secs(secs)) + } + } + + impl Deref for Duration { + type Target = StdDuration; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl From for Duration { + fn from(duration: StdDuration) -> Self { + Self(duration) + } + } + + impl Type for Duration { + fn type_info() -> PgTypeInfo { + >::type_info() + } + + fn compatible(ty: &PgTypeInfo) -> bool { + >::compatible(ty) + } + } + + impl TryFrom for Duration { + type Error = crate::convert::IntervalError; + + fn try_from(value: PgInterval) -> Result { + Ok(Self(crate::convert::interval_to_duration(value)?)) + } + } + + impl<'r> Decode<'r, Postgres> for Duration { + fn decode(value: PgValueRef<'r>) -> Result { + let interval: PgInterval = Decode::::decode(value)?; + + Ok(interval.try_into()?) + } + } +} + +pub use duration_impl::Duration; diff --git a/crates/lib/docs_rs_types/src/lib.rs b/crates/lib/docs_rs_types/src/lib.rs index 1373cc6fe..acaa4b148 100644 --- a/crates/lib/docs_rs_types/src/lib.rs +++ b/crates/lib/docs_rs_types/src/lib.rs @@ -1,6 +1,8 @@ mod build_status; mod compression_algorithm; +pub(crate) mod convert; pub mod doc_coverage; +mod duration; mod feature; mod ids; mod krate_name; @@ -12,6 +14,7 @@ mod version; pub use build_status::BuildStatus; pub use compression_algorithm::{CompressionAlgorithm, compression_from_file_extension}; pub use doc_coverage::{DocCoverage, RawFileCoverage}; +pub use duration::Duration; pub use feature::Feature; pub use ids::{BuildId, CrateId, ReleaseId}; pub use krate_name::KrateName; From 2c42292cf0bacbc0a4cf42ebf88e041884e80aca Mon Sep 17 00:00:00 2001 From: Denis Cornehl Date: Fri, 30 Jan 2026 06:13:36 +0100 Subject: [PATCH 2/2] fix crate-details view when there are no builds --- ...16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json | 6 +++--- ...3dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json | 4 ++-- ...2450f53054049157f3b7ee2b715173f5accf559df3cfc.json} | 6 +++--- ...6d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json} | 6 +++--- ...3dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json | 4 ++-- ...2450f53054049157f3b7ee2b715173f5accf559df3cfc.json} | 6 +++--- ...16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json | 6 +++--- ...dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json} | 4 ++-- ...2450f53054049157f3b7ee2b715173f5accf559df3cfc.json} | 6 +++--- crates/bin/docs_rs_web/src/handlers/builds.rs | 10 +++++----- crates/bin/docs_rs_web/src/handlers/crate_details.rs | 10 ++++++---- 11 files changed, 35 insertions(+), 33 deletions(-) rename crates/bin/docs_rs_web/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json => .sqlx/query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json (72%) rename crates/bin/cratesfyi/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json => .sqlx/query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json (66%) rename .sqlx/{query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json => query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json} (78%) rename crates/bin/cratesfyi/.sqlx/{query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json => query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json} (72%) rename .sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json => crates/bin/cratesfyi/.sqlx/query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json (66%) rename crates/bin/{docs_rs_web/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json => cratesfyi/.sqlx/query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json} (78%) rename .sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json => crates/bin/docs_rs_web/.sqlx/query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json (72%) rename crates/bin/docs_rs_web/.sqlx/{query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json => query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json} (66%) rename crates/bin/{cratesfyi/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json => docs_rs_web/.sqlx/query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json} (78%) diff --git a/crates/bin/docs_rs_web/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json b/.sqlx/query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json similarity index 72% rename from crates/bin/docs_rs_web/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json rename to .sqlx/query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json index 8c1d8916b..8bcb59e94 100644 --- a/crates/bin/docs_rs_web/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json +++ b/.sqlx/query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json @@ -1,11 +1,11 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n FROM builds AS b\n WHERE\n b.rid = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "query": "\n SELECT AVG(b.build_finished - b.build_started) AS \"duration?: Duration\"\n FROM builds AS b\n WHERE\n b.rid = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", "describe": { "columns": [ { "ordinal": 0, - "name": "duration!: Duration", + "name": "duration?: Duration", "type_info": "Interval" } ], @@ -18,5 +18,5 @@ null ] }, - "hash": "9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77" + "hash": "2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e" } diff --git a/crates/bin/cratesfyi/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json b/.sqlx/query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json similarity index 66% rename from crates/bin/cratesfyi/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json rename to .sqlx/query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json index 9a23e4b28..5678b194e 100644 --- a/crates/bin/cratesfyi/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json +++ b/.sqlx/query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n CASE\n WHEN builds.build_started IS NULL\n -- for old builds, `build_started` is empty.\n THEN NULL\n ELSE\n CASE\n -- for in-progress builds we show the duration until now\n WHEN builds.build_finished IS NULL\n THEN (CURRENT_TIMESTAMP - builds.build_started)\n -- for finished builds we can show the full duration\n ELSE (builds.build_finished - builds.build_started)\n END\n END AS \"build_duration?: Duration\",\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", + "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n CASE\n WHEN builds.build_started IS NULL\n -- for old builds, `build_started` is empty.\n THEN NULL\n ELSE\n CASE\n -- for in-progress builds we show the duration until now\n WHEN builds.build_status = 'in_progress' THEN (CURRENT_TIMESTAMP - builds.build_started)\n -- there are broken builds where the status is `error`, and `build_finished` is NULL\n WHEN builds.build_finished IS NULL THEN NULL\n -- for finished builds we can show the full duration\n ELSE (builds.build_finished - builds.build_started)\n END\n END AS \"build_duration?: Duration\",\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", "describe": { "columns": [ { @@ -66,5 +66,5 @@ true ] }, - "hash": "7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46" + "hash": "5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182" } diff --git a/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json b/.sqlx/query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json similarity index 78% rename from .sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json rename to .sqlx/query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json index 44024a33b..462c1da3d 100644 --- a/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json +++ b/.sqlx/query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json @@ -1,11 +1,11 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n\n FROM\n crates AS c\n INNER JOIN releases AS r on c.id = r.crate_id\n INNER JOIN builds AS b on r.id = b.rid\n\n WHERE\n c.id = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "query": "\n SELECT\n AVG(b.build_finished - b.build_started) AS \"duration?: Duration\"\n\n FROM\n crates AS c\n INNER JOIN releases AS r on c.id = r.crate_id\n INNER JOIN builds AS b on r.id = b.rid\n\n WHERE\n c.id = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", "describe": { "columns": [ { "ordinal": 0, - "name": "duration!: Duration", + "name": "duration?: Duration", "type_info": "Interval" } ], @@ -18,5 +18,5 @@ null ] }, - "hash": "2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf" + "hash": "c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc" } diff --git a/crates/bin/cratesfyi/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json b/crates/bin/cratesfyi/.sqlx/query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json similarity index 72% rename from crates/bin/cratesfyi/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json rename to crates/bin/cratesfyi/.sqlx/query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json index 8c1d8916b..8bcb59e94 100644 --- a/crates/bin/cratesfyi/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json +++ b/crates/bin/cratesfyi/.sqlx/query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json @@ -1,11 +1,11 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n FROM builds AS b\n WHERE\n b.rid = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "query": "\n SELECT AVG(b.build_finished - b.build_started) AS \"duration?: Duration\"\n FROM builds AS b\n WHERE\n b.rid = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", "describe": { "columns": [ { "ordinal": 0, - "name": "duration!: Duration", + "name": "duration?: Duration", "type_info": "Interval" } ], @@ -18,5 +18,5 @@ null ] }, - "hash": "9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77" + "hash": "2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e" } diff --git a/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json b/crates/bin/cratesfyi/.sqlx/query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json similarity index 66% rename from .sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json rename to crates/bin/cratesfyi/.sqlx/query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json index 9a23e4b28..5678b194e 100644 --- a/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json +++ b/crates/bin/cratesfyi/.sqlx/query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n CASE\n WHEN builds.build_started IS NULL\n -- for old builds, `build_started` is empty.\n THEN NULL\n ELSE\n CASE\n -- for in-progress builds we show the duration until now\n WHEN builds.build_finished IS NULL\n THEN (CURRENT_TIMESTAMP - builds.build_started)\n -- for finished builds we can show the full duration\n ELSE (builds.build_finished - builds.build_started)\n END\n END AS \"build_duration?: Duration\",\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", + "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n CASE\n WHEN builds.build_started IS NULL\n -- for old builds, `build_started` is empty.\n THEN NULL\n ELSE\n CASE\n -- for in-progress builds we show the duration until now\n WHEN builds.build_status = 'in_progress' THEN (CURRENT_TIMESTAMP - builds.build_started)\n -- there are broken builds where the status is `error`, and `build_finished` is NULL\n WHEN builds.build_finished IS NULL THEN NULL\n -- for finished builds we can show the full duration\n ELSE (builds.build_finished - builds.build_started)\n END\n END AS \"build_duration?: Duration\",\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", "describe": { "columns": [ { @@ -66,5 +66,5 @@ true ] }, - "hash": "7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46" + "hash": "5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182" } diff --git a/crates/bin/docs_rs_web/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json b/crates/bin/cratesfyi/.sqlx/query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json similarity index 78% rename from crates/bin/docs_rs_web/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json rename to crates/bin/cratesfyi/.sqlx/query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json index 44024a33b..462c1da3d 100644 --- a/crates/bin/docs_rs_web/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json +++ b/crates/bin/cratesfyi/.sqlx/query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json @@ -1,11 +1,11 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n\n FROM\n crates AS c\n INNER JOIN releases AS r on c.id = r.crate_id\n INNER JOIN builds AS b on r.id = b.rid\n\n WHERE\n c.id = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "query": "\n SELECT\n AVG(b.build_finished - b.build_started) AS \"duration?: Duration\"\n\n FROM\n crates AS c\n INNER JOIN releases AS r on c.id = r.crate_id\n INNER JOIN builds AS b on r.id = b.rid\n\n WHERE\n c.id = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", "describe": { "columns": [ { "ordinal": 0, - "name": "duration!: Duration", + "name": "duration?: Duration", "type_info": "Interval" } ], @@ -18,5 +18,5 @@ null ] }, - "hash": "2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf" + "hash": "c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc" } diff --git a/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json b/crates/bin/docs_rs_web/.sqlx/query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json similarity index 72% rename from .sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json rename to crates/bin/docs_rs_web/.sqlx/query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json index 8c1d8916b..8bcb59e94 100644 --- a/.sqlx/query-9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77.json +++ b/crates/bin/docs_rs_web/.sqlx/query-2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e.json @@ -1,11 +1,11 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n FROM builds AS b\n WHERE\n b.rid = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "query": "\n SELECT AVG(b.build_finished - b.build_started) AS \"duration?: Duration\"\n FROM builds AS b\n WHERE\n b.rid = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", "describe": { "columns": [ { "ordinal": 0, - "name": "duration!: Duration", + "name": "duration?: Duration", "type_info": "Interval" } ], @@ -18,5 +18,5 @@ null ] }, - "hash": "9399c68093aa75e16820173a658ecf08566aecfa06e6a2c4a146b54213498b77" + "hash": "2c7c1c2f69ccea2fbb16d6d7706b41d9a17670801c85b53b235b306ec8eef95e" } diff --git a/crates/bin/docs_rs_web/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json b/crates/bin/docs_rs_web/.sqlx/query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json similarity index 66% rename from crates/bin/docs_rs_web/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json rename to crates/bin/docs_rs_web/.sqlx/query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json index 9a23e4b28..5678b194e 100644 --- a/crates/bin/docs_rs_web/.sqlx/query-7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46.json +++ b/crates/bin/docs_rs_web/.sqlx/query-5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n CASE\n WHEN builds.build_started IS NULL\n -- for old builds, `build_started` is empty.\n THEN NULL\n ELSE\n CASE\n -- for in-progress builds we show the duration until now\n WHEN builds.build_finished IS NULL\n THEN (CURRENT_TIMESTAMP - builds.build_started)\n -- for finished builds we can show the full duration\n ELSE (builds.build_finished - builds.build_started)\n END\n END AS \"build_duration?: Duration\",\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", + "query": "SELECT\n builds.id as \"id: BuildId\",\n builds.rustc_version,\n builds.docsrs_version,\n builds.build_status as \"build_status: BuildStatus\",\n COALESCE(builds.build_finished, builds.build_started) as build_time,\n CASE\n WHEN builds.build_started IS NULL\n -- for old builds, `build_started` is empty.\n THEN NULL\n ELSE\n CASE\n -- for in-progress builds we show the duration until now\n WHEN builds.build_status = 'in_progress' THEN (CURRENT_TIMESTAMP - builds.build_started)\n -- there are broken builds where the status is `error`, and `build_finished` is NULL\n WHEN builds.build_finished IS NULL THEN NULL\n -- for finished builds we can show the full duration\n ELSE (builds.build_finished - builds.build_started)\n END\n END AS \"build_duration?: Duration\",\n builds.errors\n FROM builds\n INNER JOIN releases ON releases.id = builds.rid\n INNER JOIN crates ON releases.crate_id = crates.id\n WHERE\n crates.name = $1 AND\n releases.version = $2\n ORDER BY builds.id DESC", "describe": { "columns": [ { @@ -66,5 +66,5 @@ true ] }, - "hash": "7e213094a5cd195c9b5501d12c98c220a39117897820d0482ab3b08f8245cd46" + "hash": "5b6e45f40cd1fcbba53dcd54c2447f1bd43d2fd58c6b836643c04e93cb08f182" } diff --git a/crates/bin/cratesfyi/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json b/crates/bin/docs_rs_web/.sqlx/query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json similarity index 78% rename from crates/bin/cratesfyi/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json rename to crates/bin/docs_rs_web/.sqlx/query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json index 44024a33b..462c1da3d 100644 --- a/crates/bin/cratesfyi/.sqlx/query-2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf.json +++ b/crates/bin/docs_rs_web/.sqlx/query-c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc.json @@ -1,11 +1,11 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n AVG(b.build_finished - b.build_started) AS \"duration!: Duration\"\n\n FROM\n crates AS c\n INNER JOIN releases AS r on c.id = r.crate_id\n INNER JOIN builds AS b on r.id = b.rid\n\n WHERE\n c.id = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", + "query": "\n SELECT\n AVG(b.build_finished - b.build_started) AS \"duration?: Duration\"\n\n FROM\n crates AS c\n INNER JOIN releases AS r on c.id = r.crate_id\n INNER JOIN builds AS b on r.id = b.rid\n\n WHERE\n c.id = $1 AND\n b.build_status = 'success' AND\n b.build_started IS NOT NULL", "describe": { "columns": [ { "ordinal": 0, - "name": "duration!: Duration", + "name": "duration?: Duration", "type_info": "Interval" } ], @@ -18,5 +18,5 @@ null ] }, - "hash": "2df26f6575cb3b80255731a15229aebea927233c23192fd7f4b1639b746d38bf" + "hash": "c187e208794078317762450f53054049157f3b7ee2b715173f5accf559df3cfc" } diff --git a/crates/bin/docs_rs_web/src/handlers/builds.rs b/crates/bin/docs_rs_web/src/handlers/builds.rs index b0c2555e3..a9679fb14 100644 --- a/crates/bin/docs_rs_web/src/handlers/builds.rs +++ b/crates/bin/docs_rs_web/src/handlers/builds.rs @@ -203,8 +203,9 @@ async fn get_builds( ELSE CASE -- for in-progress builds we show the duration until now - WHEN builds.build_finished IS NULL - THEN (CURRENT_TIMESTAMP - builds.build_started) + WHEN builds.build_status = 'in_progress' THEN (CURRENT_TIMESTAMP - builds.build_started) + -- there are broken builds where the status is `error`, and `build_finished` is NULL + WHEN builds.build_finished IS NULL THEN NULL -- for finished builds we can show the full duration ELSE (builds.build_finished - builds.build_started) END @@ -255,7 +256,7 @@ mod tests { let response = env .web_app() .await - .get("/crate/foo/0.1.0/builds") + .assert_success("/crate/foo/0.1.0/builds") .await? .error_for_status()?; response.assert_cache_control(CachePolicy::NoCaching, env.config()); @@ -268,8 +269,7 @@ mod tests { .collect(); assert_eq!(rows.len(), 1); - // third column contains build-start time, even when the rest is empty - assert_eq!(rows[0].chars().filter(|&c| c == '—').count(), 2); + assert_eq!(rows[0].chars().filter(|&c| c == '—').count(), 3); Ok(()) }); diff --git a/crates/bin/docs_rs_web/src/handlers/crate_details.rs b/crates/bin/docs_rs_web/src/handlers/crate_details.rs index f7de68178..7f809f522 100644 --- a/crates/bin/docs_rs_web/src/handlers/crate_details.rs +++ b/crates/bin/docs_rs_web/src/handlers/crate_details.rs @@ -363,7 +363,7 @@ impl BuildStatistics { Ok(Self { avg_build_duration_release: sqlx::query_scalar!( r#" - SELECT AVG(b.build_finished - b.build_started) AS "duration!: Duration" + SELECT AVG(b.build_finished - b.build_started) AS "duration?: Duration" FROM builds AS b WHERE b.rid = $1 AND @@ -372,11 +372,12 @@ impl BuildStatistics { release_id as _, ) .fetch_optional(&mut *conn) - .await?, + .await? + .flatten(), avg_build_duration_crate: sqlx::query_scalar!( r#" SELECT - AVG(b.build_finished - b.build_started) AS "duration!: Duration" + AVG(b.build_finished - b.build_started) AS "duration?: Duration" FROM crates AS c @@ -390,7 +391,8 @@ impl BuildStatistics { crate_id as _, ) .fetch_optional(&mut *conn) - .await?, + .await? + .flatten(), }) } }