From 4b5aeefdade60f3dfa094edc45856aefff399857 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Wed, 30 Oct 2024 17:07:32 -0700 Subject: [PATCH] what a query --- Cargo.lock | 19 ++- Cargo.toml | 4 +- examples/admin/static/js/jobs_controller.js | 7 - examples/scheduled-jobs/rwf.toml | 2 +- examples/scheduled-jobs/src/main.rs | 4 +- {examples/admin => rwf-admin}/Cargo.toml | 7 +- {examples/admin => rwf-admin}/rwf.toml | 0 rwf-admin/src/controllers/mod.rs | 151 ++++++++++++++++++ rwf-admin/src/lib.rs | 13 ++ {examples/admin => rwf-admin}/src/main.rs | 6 +- rwf-admin/static/js/requests_controller.js | 62 +++++++ .../templates/rwf_admin/footer.html | 0 .../templates/rwf_admin/head.html | 12 +- .../templates/rwf_admin/index.html | 0 .../templates/rwf_admin/jobs.html | 18 +-- .../templates/rwf_admin/nav.html | 1 + rwf-admin/templates/rwf_admin/reload.html | 11 ++ rwf-admin/templates/rwf_admin/requests.html | 14 ++ rwf-macros/src/model.rs | 6 +- rwf/src/admin/controllers.rs | 80 ---------- rwf/src/admin/mod.rs | 11 -- rwf/src/admin/models.rs | 1 - rwf/src/analytics/requests.rs | 7 + rwf/src/lib.rs | 1 - rwf/src/model/column.rs | 37 ++++- rwf/src/model/mod.rs | 15 +- rwf/src/model/picked.rs | 23 +++ rwf/src/model/select.rs | 21 ++- rwf/src/view/template/error.rs | 22 ++- rwf/src/view/template/lexer/value.rs | 20 ++- 30 files changed, 426 insertions(+), 149 deletions(-) delete mode 100644 examples/admin/static/js/jobs_controller.js rename {examples/admin => rwf-admin}/Cargo.toml (56%) rename {examples/admin => rwf-admin}/rwf.toml (100%) create mode 100644 rwf-admin/src/controllers/mod.rs create mode 100644 rwf-admin/src/lib.rs rename {examples/admin => rwf-admin}/src/main.rs (82%) create mode 100644 rwf-admin/static/js/requests_controller.js rename {examples/admin => rwf-admin}/templates/rwf_admin/footer.html (100%) rename {examples/admin => rwf-admin}/templates/rwf_admin/head.html (73%) rename {examples/admin => rwf-admin}/templates/rwf_admin/index.html (100%) rename {examples/admin => rwf-admin}/templates/rwf_admin/jobs.html (86%) rename {examples/admin => rwf-admin}/templates/rwf_admin/nav.html (81%) create mode 100644 rwf-admin/templates/rwf_admin/reload.html create mode 100644 rwf-admin/templates/rwf_admin/requests.html delete mode 100644 rwf/src/admin/controllers.rs delete mode 100644 rwf/src/admin/mod.rs delete mode 100644 rwf/src/admin/models.rs create mode 100644 rwf/src/model/picked.rs diff --git a/Cargo.lock b/Cargo.lock index 239afa8a..45b68f45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,14 +17,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "admin" -version = "0.1.0" -dependencies = [ - "rwf 0.1.3", - "tokio", -] - [[package]] name = "aead" version = "0.5.2" @@ -1697,6 +1689,17 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "rwf-admin" +version = "0.1.0" +dependencies = [ + "rwf 0.1.3", + "serde", + "serde_json", + "time", + "tokio", +] + [[package]] name = "rwf-cli" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index f783184c..aefe086d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,5 +16,7 @@ members = [ "rwf-macros", "rwf-tests", "examples/django", - "examples/request-tracking", "examples/engine", "examples/admin", + "examples/request-tracking", + "examples/engine", + "rwf-admin", ] diff --git a/examples/admin/static/js/jobs_controller.js b/examples/admin/static/js/jobs_controller.js deleted file mode 100644 index 6ad090b1..00000000 --- a/examples/admin/static/js/jobs_controller.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Controller } from "hotwired/stimuls"; - -export default class extends Controller { - reload() { - Turbo.visit(window.location.pathname, { action: "replace" }); - } -} diff --git a/examples/scheduled-jobs/rwf.toml b/examples/scheduled-jobs/rwf.toml index 8d1a1ef0..694cea68 100644 --- a/examples/scheduled-jobs/rwf.toml +++ b/examples/scheduled-jobs/rwf.toml @@ -1,4 +1,4 @@ [general] [database] -name = "rwf_bg_jobs" \ No newline at end of file +name = "rwf_admin" diff --git a/examples/scheduled-jobs/src/main.rs b/examples/scheduled-jobs/src/main.rs index 1e42ea57..077cd3d5 100644 --- a/examples/scheduled-jobs/src/main.rs +++ b/examples/scheduled-jobs/src/main.rs @@ -3,6 +3,7 @@ use rwf::job::{Error as JobError, Job, Worker}; use rwf::prelude::*; use serde::{Deserialize, Serialize}; +use tokio::time::sleep; #[derive(Clone, Serialize, Deserialize, Default)] struct MyJob; @@ -10,6 +11,7 @@ struct MyJob; #[rwf::async_trait] impl Job for MyJob { async fn execute(&self, _args: serde_json::Value) -> Result<(), JobError> { + sleep(std::time::Duration::from_secs(1)).await; Ok(()) } } @@ -27,7 +29,7 @@ async fn main() -> Result<(), Error> { .start() .await?; - Server::new(vec![]).launch("0.0.0.0:8000").await?; + sleep(std::time::Duration::MAX).await; Ok(()) } diff --git a/examples/admin/Cargo.toml b/rwf-admin/Cargo.toml similarity index 56% rename from examples/admin/Cargo.toml rename to rwf-admin/Cargo.toml index d5c0fbc7..7f0d2b81 100644 --- a/examples/admin/Cargo.toml +++ b/rwf-admin/Cargo.toml @@ -1,10 +1,13 @@ [package] -name = "admin" +name = "rwf-admin" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rwf = { path = "../../rwf" } +rwf = { path = "../rwf" } tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +time = { version = "0.3", features = ["serde"] } diff --git a/examples/admin/rwf.toml b/rwf-admin/rwf.toml similarity index 100% rename from examples/admin/rwf.toml rename to rwf-admin/rwf.toml diff --git a/rwf-admin/src/controllers/mod.rs b/rwf-admin/src/controllers/mod.rs new file mode 100644 index 00000000..e91e72f2 --- /dev/null +++ b/rwf-admin/src/controllers/mod.rs @@ -0,0 +1,151 @@ +use rwf::job::JobModel; +use rwf::prelude::*; +use rwf::serde::Serialize; + +#[derive(Default)] +pub struct Index; + +#[async_trait] +impl Controller for Index { + async fn handle(&self, _request: &Request) -> Result { + Ok(Response::new().redirect("jobs")) + } +} + +#[derive(Default)] +pub struct Jobs; + +#[derive(macros::Context)] +struct JobsContext { + queued: i64, + running: i64, + errors: i64, + latency: i64, + jobs: Vec, +} + +impl JobsContext { + pub async fn load() -> Result { + let mut conn = Pool::connection().await?; + let queued = JobModel::queued().count(&mut conn).await?; + let errors = JobModel::errors().count(&mut conn).await?; + let running = JobModel::running().count(&mut conn).await?; + + let jobs = JobModel::all() + .order(("id", "DESC")) + .limit(25) + .fetch_all(&mut conn) + .await?; + + let latency = JobModel::queued() + .order("created_at") + .take_one() + .fetch_optional(&mut conn) + .await?; + + let latency = if let Some(latency) = latency { + (OffsetDateTime::now_utc() - latency.created_at).whole_seconds() + } else { + Duration::seconds(0).whole_seconds() + }; + + Ok(Self { + queued, + errors, + running, + jobs, + latency, + }) + } +} + +#[async_trait] +impl Controller for Jobs { + async fn handle(&self, _request: &Request) -> Result { + let template = Template::load("templates/rwf_admin/jobs.html")?; + Ok(Response::new().html(template.render(JobsContext::load().await?)?)) + } +} + +#[derive(Clone, macros::Model, Serialize)] +struct RequestByCode { + count: i64, + code: String, + #[serde(with = "time::serde::rfc2822")] + created_at: OffsetDateTime, +} + +impl RequestByCode { + fn count(minutes: i64) -> Scope { + Self::find_by_sql( + "WITH timestamps AS ( + SELECT date_trunc('minute', now() - (n || ' minute')::interval) AS created_at FROM generate_series(0, $1::bigint) n + ) + SELECT + 'ok' AS code, + COALESCE(e2.count, 0) AS count, + timestamps.created_at AS created_at + FROM timestamps + LEFT JOIN LATERAL ( + SELECT + COUNT(*) AS count, + DATE_TRUNC('minute', created_at) AS created_at + FROM rwf_requests + WHERE + created_at BETWEEN timestamps.created_at AND timestamps.created_at + INTERVAL '1 minute' + AND code BETWEEN 100 AND 299 + GROUP BY 2 + ) e2 ON true + UNION ALL + SELECT + 'warn' AS code, + COALESCE(e2.count, 0) AS count, + timestamps.created_at AS created_at + FROM timestamps + LEFT JOIN LATERAL ( + SELECT + COUNT(*) AS count, + DATE_TRUNC('minute', created_at) AS created_at + FROM rwf_requests + WHERE + created_at BETWEEN timestamps.created_at AND timestamps.created_at + INTERVAL '1 minute' + AND code BETWEEN 300 AND 499 + GROUP BY 2 + ) e2 ON true + UNION ALL + SELECT + 'error' AS code, + COALESCE(e2.count, 0) AS coount, + timestamps.created_at AS created_at + FROM timestamps + LEFT JOIN LATERAL ( + SELECT + COUNT(*) AS count, + DATE_TRUNC('minute', created_at) AS created_at + FROM rwf_requests + WHERE + created_at BETWEEN timestamps.created_at AND timestamps.created_at + INTERVAL '1 minute' + AND code BETWEEN 500 AND 599 + GROUP BY 2 + ) e2 ON true + ORDER BY 3;", + &[minutes.to_value()], + ) + } +} + +#[derive(Default)] +pub struct Requests; + +#[async_trait] +impl Controller for Requests { + async fn handle(&self, _request: &Request) -> Result { + let requests = { + let mut conn = Pool::connection().await?; + RequestByCode::count(60).fetch_all(&mut conn).await? + }; + let requests = serde_json::to_string(&requests)?; + + render!("templates/rwf_admin/requests.html", "requests" => requests) + } +} diff --git a/rwf-admin/src/lib.rs b/rwf-admin/src/lib.rs new file mode 100644 index 00000000..1a2100fe --- /dev/null +++ b/rwf-admin/src/lib.rs @@ -0,0 +1,13 @@ +use rwf::controller::Engine; +use rwf::prelude::*; + +mod controllers; +use controllers::*; + +pub fn engine() -> Engine { + Engine::new(vec![ + route!("/" => Index), + route!("/jobs" => Jobs), + route!("/requests" => Requests), + ]) +} diff --git a/examples/admin/src/main.rs b/rwf-admin/src/main.rs similarity index 82% rename from examples/admin/src/main.rs rename to rwf-admin/src/main.rs index a47c4e58..bedbf2ae 100644 --- a/examples/admin/src/main.rs +++ b/rwf-admin/src/main.rs @@ -1,7 +1,6 @@ -use rwf::admin; use rwf::controller::BasicAuth; use rwf::{ - controller::TurboStream, + controller::{StaticFiles, TurboStream}, http::{self, Server}, prelude::*, }; @@ -17,7 +16,7 @@ async fn main() -> Result<(), http::Error> { // Basic auth is just an example, it's not secure. I would recommend using SessionAuth // and checking that the user is an admin using an internal check. - let admin = admin::engine().auth(AuthHandler::new(BasicAuth { + let admin = rwf_admin::engine().auth(AuthHandler::new(BasicAuth { user: "admin".to_string(), password: "admin".to_string(), })); @@ -25,6 +24,7 @@ async fn main() -> Result<(), http::Error> { Server::new(vec![ engine!("/admin" => admin), route!("/turbo-stream" => TurboStream), + StaticFiles::serve("static")?, ]) .launch("0.0.0.0:8000") .await diff --git a/rwf-admin/static/js/requests_controller.js b/rwf-admin/static/js/requests_controller.js new file mode 100644 index 00000000..39fb2a95 --- /dev/null +++ b/rwf-admin/static/js/requests_controller.js @@ -0,0 +1,62 @@ +import { Controller } from "hotwired/stimulus"; + +export default class extends Controller { + static targets = ["requestsOk", "chart"]; + + connect() { + const data = JSON.parse(this.requestsOkTarget.innerHTML); + const labels = Array.from( + new Set( + data.map((item) => new Date(item.created_at).toLocaleTimeString()), + ), + ); + const ok = data + .filter((item) => item.code === "ok") + .map((item) => item.count); + const warn = data + .filter((item) => item.code === "warn") + .map((item) => item.count); + const error = data + .filter((item) => item.code === "error") + .map((item) => item.count); + + const options = { + scales: { + x: { + ticks: { + callback: (t, i) => (i % 10 === 0 ? labels[i] : null), + }, + stacked: true, + }, + y: { + stacked: true, + }, + }, + }; + + const chartData = { + labels, + datasets: [ + { + label: "100-299", + data: ok, + }, + { + label: "500-599", + data: error, + // backgroundColor: "red", + }, + { + label: "300-499", + data: warn, + }, + ], + }; + + new Chart(this.chartTarget, { + type: "bar", + data: chartData, + options, + }); + } +} diff --git a/examples/admin/templates/rwf_admin/footer.html b/rwf-admin/templates/rwf_admin/footer.html similarity index 100% rename from examples/admin/templates/rwf_admin/footer.html rename to rwf-admin/templates/rwf_admin/footer.html diff --git a/examples/admin/templates/rwf_admin/head.html b/rwf-admin/templates/rwf_admin/head.html similarity index 73% rename from examples/admin/templates/rwf_admin/head.html rename to rwf-admin/templates/rwf_admin/head.html index 4cbd8d8a..64bc561a 100644 --- a/examples/admin/templates/rwf_admin/head.html +++ b/rwf-admin/templates/rwf_admin/head.html @@ -16,13 +16,13 @@ src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js" > - + <%- rwf_turbo_stream("/turbo-stream") %> diff --git a/examples/admin/templates/rwf_admin/index.html b/rwf-admin/templates/rwf_admin/index.html similarity index 100% rename from examples/admin/templates/rwf_admin/index.html rename to rwf-admin/templates/rwf_admin/index.html diff --git a/examples/admin/templates/rwf_admin/jobs.html b/rwf-admin/templates/rwf_admin/jobs.html similarity index 86% rename from examples/admin/templates/rwf_admin/jobs.html rename to rwf-admin/templates/rwf_admin/jobs.html index 6f4ec381..61df1aab 100644 --- a/examples/admin/templates/rwf_admin/jobs.html +++ b/rwf-admin/templates/rwf_admin/jobs.html @@ -1,16 +1,8 @@ <%% "templates/rwf_admin/head.html" %> <%% "templates/rwf_admin/nav.html" %>
-
-

Jobs

- Reload -
+ <% for name in ["jobs"] %> + <%% "templates/rwf_admin/reload.html" %> + <% end %>
@@ -46,6 +38,7 @@

<%= latency %>s

+ <% if jobs %> @@ -60,7 +53,7 @@

<%= latency %>s

<% for job in jobs %> - + @@ -73,6 +66,7 @@

<%= latency %>s

<% end %>
<%= job.id %><%= job.name %><%= job.name %> <%= job.args %>
+ <% end %>
<%% "templates/rwf_admin/footer.html" %> diff --git a/examples/admin/templates/rwf_admin/nav.html b/rwf-admin/templates/rwf_admin/nav.html similarity index 81% rename from examples/admin/templates/rwf_admin/nav.html rename to rwf-admin/templates/rwf_admin/nav.html index c79eaca6..ed800425 100644 --- a/examples/admin/templates/rwf_admin/nav.html +++ b/rwf-admin/templates/rwf_admin/nav.html @@ -3,6 +3,7 @@
diff --git a/rwf-admin/templates/rwf_admin/reload.html b/rwf-admin/templates/rwf_admin/reload.html new file mode 100644 index 00000000..3c5acb01 --- /dev/null +++ b/rwf-admin/templates/rwf_admin/reload.html @@ -0,0 +1,11 @@ +
+

<%= name.capitalize %>

+ Reload +
diff --git a/rwf-admin/templates/rwf_admin/requests.html b/rwf-admin/templates/rwf_admin/requests.html new file mode 100644 index 00000000..3c3931d7 --- /dev/null +++ b/rwf-admin/templates/rwf_admin/requests.html @@ -0,0 +1,14 @@ +<%% "templates/rwf_admin/head.html" %> +<%% "templates/rwf_admin/nav.html" %> +
+ <% for name in ["requests"] %> + <%% "templates/rwf_admin/reload.html" %> + <% end %> +
+ + +
+
+<%% "templates/rwf_admin/footer.html" %> diff --git a/rwf-macros/src/model.rs b/rwf-macros/src/model.rs index 7b0d4609..42e3c84e 100644 --- a/rwf-macros/src/model.rs +++ b/rwf-macros/src/model.rs @@ -27,7 +27,11 @@ pub fn impl_derive_model(input: TokenStream) -> TokenStream { } } } else { - quote! {} + quote! { + fn id(&self) -> rwf::model::Value { + rwf::model::Value::Null + } + } }; let without_id = data diff --git a/rwf/src/admin/controllers.rs b/rwf/src/admin/controllers.rs deleted file mode 100644 index 851a6689..00000000 --- a/rwf/src/admin/controllers.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::job::JobModel; -use crate::{prelude::*, view}; - -#[derive(Default)] -pub struct Index; - -#[async_trait] -impl Controller for Index { - async fn handle(&self, _request: &Request) -> Result { - Ok(Response::new().redirect("jobs")) - } -} - -#[derive(Default)] -pub struct Jobs; - -struct JobsContext { - queued: i64, - running: i64, - errors: i64, - latency: i64, - jobs: Vec, -} - -impl TryInto for JobsContext { - type Error = view::Error; - - fn try_into(self) -> Result { - let mut context = view::Context::new(); - context.set("queued", self.queued)?; - context.set("running", self.running)?; - context.set("errors", self.errors)?; - context.set("latency", self.latency)?; - context.set("jobs", self.jobs)?; - Ok(context) - } -} - -impl JobsContext { - pub async fn load() -> Result { - let mut conn = Pool::connection().await?; - let queued = JobModel::queued().count(&mut conn).await?; - let errors = JobModel::errors().count(&mut conn).await?; - let running = JobModel::running().count(&mut conn).await?; - - let jobs = JobModel::all() - .order(("id", "DESC")) - .limit(25) - .fetch_all(&mut conn) - .await?; - - let latency = JobModel::queued() - .order("created_at") - .take_one() - .fetch_optional(&mut conn) - .await?; - - let latency = if let Some(latency) = latency { - (OffsetDateTime::now_utc() - latency.created_at).whole_seconds() - } else { - Duration::seconds(0).whole_seconds() - }; - - Ok(Self { - queued, - errors, - running, - jobs, - latency, - }) - } -} - -#[async_trait] -impl Controller for Jobs { - async fn handle(&self, _request: &Request) -> Result { - let template = Template::load("templates/rwf_admin/jobs.html")?; - Ok(Response::new().html(template.render(JobsContext::load().await?)?)) - } -} diff --git a/rwf/src/admin/mod.rs b/rwf/src/admin/mod.rs deleted file mode 100644 index 837d6308..00000000 --- a/rwf/src/admin/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod controllers; -pub mod models; - -use crate::controller::{Controller, Engine}; - -pub fn engine() -> Engine { - Engine::new(vec![ - controllers::Index::default().route("/"), - controllers::Jobs::default().route("/jobs"), - ]) -} diff --git a/rwf/src/admin/models.rs b/rwf/src/admin/models.rs deleted file mode 100644 index 8b137891..00000000 --- a/rwf/src/admin/models.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/rwf/src/analytics/requests.rs b/rwf/src/analytics/requests.rs index 58287dcf..cbfd931c 100644 --- a/rwf/src/analytics/requests.rs +++ b/rwf/src/analytics/requests.rs @@ -1,6 +1,7 @@ use std::net::IpAddr; use crate::model::{Error, FromRow, Model, ToValue, Value}; + use time::OffsetDateTime; #[derive(Clone)] @@ -15,6 +16,12 @@ pub struct Request { duration: f32, } +// impl Request { +// fn minute() -> Scope { +// todo!() +// } +// } + impl FromRow for Request { fn from_row(row: tokio_postgres::Row) -> Result { Ok(Self { diff --git a/rwf/src/lib.rs b/rwf/src/lib.rs index 6e858a55..418e479e 100644 --- a/rwf/src/lib.rs +++ b/rwf/src/lib.rs @@ -1,4 +1,3 @@ -pub mod admin; pub mod analytics; pub mod colors; pub mod comms; diff --git a/rwf/src/model/column.rs b/rwf/src/model/column.rs index ef1562d4..f5f79555 100644 --- a/rwf/src/model/column.rs +++ b/rwf/src/model/column.rs @@ -79,10 +79,11 @@ impl Column { #[derive(Debug, Clone)] pub struct Columns { - columns: Vec, + pub columns: Vec, table_name: Option, exists: bool, all: bool, + count: bool, } impl Default for Columns { @@ -92,11 +93,24 @@ impl Default for Columns { table_name: None, exists: false, all: true, + count: false, } } } impl Columns { + pub fn pick(columns: &[impl ToColumn]) -> Self { + Self { + columns: columns.iter().map(|c| c.to_column()).collect(), + all: false, + ..Default::default() + } + } + + pub fn picked(&self) -> bool { + !self.columns.is_empty() + } + pub fn table_name(mut self, table_name: impl ToString) -> Self { self.table_name = Some(table_name.to_string()); self @@ -112,6 +126,11 @@ impl Columns { self } + pub fn count(mut self) -> Self { + self.count = true; + self + } + pub fn add_column(mut self, column: impl ToColumn) -> Self { self.columns.push(column.to_column()); self @@ -123,16 +142,20 @@ impl ToSql for Columns { if self.exists { "COUNT(*) AS count".into() } else { - let mut columns = if self.columns.is_empty() || self.all { - if let Some(ref table_name) = self.table_name { - vec![format!(r#""{}".*"#, table_name)] - } else { - vec!["*".to_string()] - } + let mut columns = if self.count { + vec!["COUNT(*) AS count".to_string()] } else { vec![] }; + if self.columns.is_empty() || self.all { + if let Some(ref table_name) = self.table_name { + columns.push(format!(r#""{}".*"#, table_name)); + } else { + columns.push("*".to_string()); + } + } + columns.extend(self.columns.iter().map(|column| column.to_sql())); columns.join(", ") diff --git a/rwf/src/model/mod.rs b/rwf/src/model/mod.rs index 180b6b43..00f6108e 100644 --- a/rwf/src/model/mod.rs +++ b/rwf/src/model/mod.rs @@ -17,6 +17,7 @@ pub mod limit; pub mod lock; pub mod migrations; pub mod order_by; +pub mod picked; pub mod placeholders; pub mod pool; pub mod prelude; @@ -37,6 +38,7 @@ pub use limit::Limit; pub use lock::Lock; pub use migrations::{migrate, rollback, Migrations}; pub use order_by::{OrderBy, OrderColumn, ToOrderBy}; +pub use picked::Picked; pub use placeholders::Placeholders; pub use pool::{get_connection, get_pool, start_transaction, Connection, ConnectionGuard, Pool}; pub use row::Row; @@ -151,6 +153,7 @@ pub enum Query { query: String, placeholders: Placeholders, }, + Picked(Picked), } impl ToSql for Query { @@ -165,6 +168,7 @@ impl ToSql for Query { InsertIfNotExists { select, insert, .. } => { format!("{}; {};", select.to_sql(), insert.to_sql()) } + Picked(picked) => picked.select.to_sql(), } } } @@ -537,6 +541,14 @@ impl Query { Ok(result) } } + + Query::Picked(picked) => { + let select = &picked.select; + let query = select.to_sql(); + let placeholdres = { select.placeholders() }; + let values = placeholdres.values(); + client.query_cached(&query, &values).await + } }; match result { @@ -578,6 +590,7 @@ impl Query { Query::Select(select) => select.placeholders, Query::Update(update) => update.placeholders, Query::Insert(insert) => insert.placeholders, + Query::Picked(picked) => picked.select.placeholders, _ => todo!("explain"), }; @@ -637,7 +650,7 @@ impl Query { fn action(&self) -> &'static str { match self { - Query::Select(_) => "load", + Query::Select(_) | Query::Picked(_) => "load", Query::Update(_) => "save", Query::Raw { .. } => "query", Query::Insert(_) => "save", diff --git a/rwf/src/model/picked.rs b/rwf/src/model/picked.rs new file mode 100644 index 00000000..83b1b234 --- /dev/null +++ b/rwf/src/model/picked.rs @@ -0,0 +1,23 @@ +//! Select only a few columns. +#![allow(dead_code, unused_variables)] + +use std::collections::HashMap; + +use super::*; + +#[derive(Debug, Clone)] +pub struct Picked { + pub select: Select, + columns: HashMap, +} + +impl Picked { + pub fn group(mut self, group: &[impl ToColumn]) -> Self { + self.select = self.select.group(group); + self + } + + fn from_row(&self, row: tokio_postgres::Row) -> Result { + todo!() + } +} diff --git a/rwf/src/model/select.rs b/rwf/src/model/select.rs index c22d94ec..d0506627 100644 --- a/rwf/src/model/select.rs +++ b/rwf/src/model/select.rs @@ -28,6 +28,7 @@ pub struct Select { pub where_clause: WhereClause, pub joins: Joins, lock: Lock, + group: bool, _phantom: PhantomData, } @@ -44,6 +45,7 @@ impl Select { where_clause: WhereClause::default(), joins: Joins::default(), lock: Lock::default(), + group: false, _phantom: PhantomData, } } @@ -231,16 +233,33 @@ impl Select { self.columns = self.columns.add_column(column); self } + + pub fn group(mut self, columns: &[impl ToColumn]) -> Self { + self.group = true; + self.columns = Columns::pick(columns); + self + } + + pub fn count(mut self) -> Self { + self.columns = self.columns.count(); + self + } } impl ToSql for Select { fn to_sql(&self) -> String { + let group = if self.group { + format!("GROUP BY {} ", self.columns.to_sql()) + } else { + "".to_string() + }; format!( - r#"SELECT {} FROM "{}"{}{}{}{}{}"#, + r#"SELECT {} FROM "{}"{}{}{}{}{}{}"#, self.columns.to_sql(), self.table_name.escape(), self.joins.to_sql(), self.where_clause.to_sql(), + group, self.order_by.to_sql(), self.limit.to_sql(), self.lock.to_sql(), diff --git a/rwf/src/view/template/error.rs b/rwf/src/view/template/error.rs index ff4c297a..47e97e26 100644 --- a/rwf/src/view/template/error.rs +++ b/rwf/src/view/template/error.rs @@ -65,15 +65,29 @@ impl Error { _ => "".to_string(), }; - let context = source.lines().nth(token.line() - 1); // std::fs lines start at 0 + println!( + "token {:?}, {}, {}", + token, + token.line(), + token.token().len() + ); + + let context = source.lines().nth(std::cmp::max(1, token.line()) - 1); // std::fs lines start at 0 let leading_spaces = if let Some(ref context) = context { context.len() - context.trim().len() } else { 0 }; - let underline = vec![' '; token.column() - token.token().len() + 1 - leading_spaces] - .into_iter() - .collect::() + println!("leading spaces: {}", leading_spaces); + let underline = vec![ + ' '; + std::cmp::max( + 0, + token.column() as i64 - token.token().len() as i64 + 1 - leading_spaces as i64 + ) as usize + ] + .into_iter() + .collect::() + &format!("^ {}", error_msg); let line_number = format!("{} | ", token.line()); diff --git a/rwf/src/view/template/lexer/value.rs b/rwf/src/view/template/lexer/value.rs index e1b156a5..f6ced653 100644 --- a/rwf/src/view/template/lexer/value.rs +++ b/rwf/src/view/template/lexer/value.rs @@ -205,6 +205,15 @@ impl Value { "to_uppercase" | "upcase" => Value::String(value.to_uppercase()), "to_lowercase" | "downcase" => Value::String(value.to_lowercase()), "trim" => Value::String(value.trim().to_string()), + "capitalize" => { + let mut iter = value.chars(); + let uppercase = match iter.next() { + None => String::new(), + Some(letter) => letter.to_uppercase().chain(iter).collect(), + }; + + Value::String(uppercase) + } _ => return Err(Error::UnknownMethod(method_name.into())), }, @@ -239,6 +248,10 @@ impl Value { Value::List(list.clone().into_iter().rev().collect::>()) } + "empty" => Value::Boolean(list.is_empty()), + + "len" => Value::Integer(list.len() as i64), + _ => return Err(Error::UnknownMethod(method_name.into())), }, }, @@ -479,6 +492,7 @@ impl ToTemplateValue for crate::model::Value { } ModelValue::Json(json) => serde_json::to_string(json).unwrap().to_template_value(), ModelValue::Int(int) => (*int as i64).to_template_value(), + ModelValue::Null => Ok(Value::Null), value => todo!("model value {:?} to template value", value), } } @@ -493,7 +507,11 @@ impl ToTemplateValue for T { return Err(Error::SerializationError); } - let mut hash = HashMap::from([("id".to_string(), self.id().to_template_value()?)]); + let mut hash = HashMap::new(); + + if !self.id().is_null() { + hash.insert("id".to_string(), self.id().to_template_value()?); + } for (key, value) in columns.iter().zip(values.iter()) { hash.insert(key.to_string(), value.to_template_value()?);