From af7e16f2ccdde63cee2de72b11dd37ab6c5d5c9d Mon Sep 17 00:00:00 2001 From: Ophir LOJKINE Date: Sun, 19 Nov 2023 20:09:18 +0100 Subject: [PATCH] null variables (#136) * update dependencies * Add support for resetting variables to a `NULL` value using `SET`. Previously, storing `NULL` in a variable would store the string `'null'` instead of the `NULL` value. --- CHANGELOG.md | 29 +++++++++++++++--- Cargo.lock | 6 ++-- Cargo.toml | 2 +- .../user-authentication/protected_page.sql | 8 ++--- .../sqlpage/migrations/0000_init.sql | 7 ----- sqlpage/apexcharts.js | 2 +- sqlpage/tabler-icons.svg | 2 +- src/webserver/database/execute_queries.rs | 30 +++++-------------- .../it_works_set_variable_to_null.sql | 9 ++++++ 9 files changed, 51 insertions(+), 44 deletions(-) create mode 100644 tests/sql_test_files/it_works_set_variable_to_null.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 737f4e3b..17b6e408 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,33 @@ # CHANGELOG.md -## unreleased +## 0.16.0 (2023-11-19) - Add special handling of hidden inputs in [forms](https://sql.ophir.dev/documentation.sql?component=form#component). Hidden inputs are now completely invisible to the end user, facilitating the implementation of multi-step forms, csrf protaction, and other complex forms. - - 18 new icons available (see https://github.com/tabler/tabler-icons/releases/tag/v2.40.0) + - 36 new icons available + - https://github.com/tabler/tabler-icons/releases/tag/v2.40.0 + - https://github.com/tabler/tabler-icons/releases/tag/v2.41.0 - Support multiple statements in [`on_connect.sql`](./configuration.md) in MySQL. - - Randomize postgres prepared statement names to avoid name collisions. This should fix a bug where SQLPage would report errors like `prepared statement "sqlx_s_3" already exists` when using a connection pooler in front of a PostgreSQL database. - - Delegate statement preparation to sqlx. The logic of preparing statements and caching them for later reuse is now entirely delegated to the sql driver library (sqlx). This simplifies the code and logic inside sqlpage itself. More importantly, statements are now prepared in a streaming fashion when a file is first loaded, instead of all at once, which allows referencing a temporary table created at the start of a file in a later statement in the same file. + - Randomize postgres prepared statement names to avoid name collisions. This should fix a bug where SQLPage would report errors like `prepared statement "sqlx_s_1" already exists` when using a connection pooler in front of a PostgreSQL database. It is still not recommended to use SQLPage with an external connection pooler (such as pgbouncer), because SQLPage already implements its own connection pool. If you really want to use a connection pooler, you should set the [`max_connections`](./configuration.md) configuration parameter to `1` to disable the connection pooling logic in SQLPage. + - SQL statements are now prepared lazily right before their first execution, instead of all at once when a file is first loaded, which allows **referencing a temporary table created at the start of a file in a later statement** in the same file. This works by delegating statement preparation to the database interface library we use (sqlx). The logic of preparing statements and caching them for later reuse is now entirely delegated to sqlx. This also nicely simplifies the code and logic inside sqlpage itself, and should slightly improve performance and memory usage. + - Creating temporary tables at the start of a file is a nice way to keep state between multiple statements in a single file, without having to use variables, which can contain only a single string value: + ```sql + DROP VIEW IF EXISTS current_user; + + CREATE TEMPORARY VIEW current_user AS + SELECT * FROM users + INNER JOIN sessions ON sessions.user_id = users.id + WHERE sessions.session_id = sqlpage.cookie('session_id'); + + SELECT 'card' as component, + 'Welcome, ' || username as title + FROM current_user; + ``` + - Add support for resetting variables to a `NULL` value using `SET`. Previously, storing `NULL` in a variable would store the string `'null'` instead of the `NULL` value. This is now fixed. + ```sql + SET myvar = NULL; + SELECT 'card' as component; + SELECT $myvar IS NULL as title; -- this used to display false, it now displays true + ``` ## 0.15.2 (2023-11-12) diff --git a/Cargo.lock b/Cargo.lock index 49352970..cecd5334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2029,9 +2029,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.24" +version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ "bitflags 2.4.1", "errno", @@ -2269,7 +2269,7 @@ dependencies = [ [[package]] name = "sqlpage" -version = "0.15.2" +version = "0.16.0" dependencies = [ "actix-rt", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index 55f00c51..b025127e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sqlpage" -version = "0.15.2" +version = "0.16.0" edition = "2021" description = "A SQL-only web application framework. Takes .sql files and formats the query result using pre-made configurable professional-looking components." keywords = ["web", "sql", "framework"] diff --git a/examples/user-authentication/protected_page.sql b/examples/user-authentication/protected_page.sql index f105e897..31f2a413 100644 --- a/examples/user-authentication/protected_page.sql +++ b/examples/user-authentication/protected_page.sql @@ -1,12 +1,12 @@ +SET username = (SELECT username FROM login_session WHERE id = sqlpage.cookie('session')); + SELECT 'redirect' AS component, 'signin.sql?error' AS link -WHERE logged_in_user(sqlpage.cookie('session')) IS NULL; --- logged_in_user is a custom postgres function defined in the first migration of this example --- that avoids having to repeat `(SELECT username FROM login_session WHERE id = session_id)` everywhere. +WHERE $username IS NULL; SELECT 'shell' AS component, 'Protected page' AS title, 'lock' AS icon, '/' AS link, 'logout' AS menu_item; SELECT 'text' AS component, - 'Welcome, ' || logged_in_user(sqlpage.cookie('session')) || ' !' AS title, + 'Welcome, ' || $username || ' !' AS title, 'This content is [top secret](https://youtu.be/dQw4w9WgXcQ). You cannot view it if you are not connected.' AS contents_md; \ No newline at end of file diff --git a/examples/user-authentication/sqlpage/migrations/0000_init.sql b/examples/user-authentication/sqlpage/migrations/0000_init.sql index eb8e98ef..34c0f0d4 100644 --- a/examples/user-authentication/sqlpage/migrations/0000_init.sql +++ b/examples/user-authentication/sqlpage/migrations/0000_init.sql @@ -8,10 +8,3 @@ CREATE TABLE login_session ( username TEXT NOT NULL REFERENCES user_info(username), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); - --- A small pure utility function to get the current user from a session cookie. --- In a database that does not support functions, you could inline this query --- or use a view if you need more information from the user table -CREATE FUNCTION logged_in_user(session_id TEXT) RETURNS TEXT AS $$ - SELECT username FROM login_session WHERE id = session_id; -$$ LANGUAGE SQL STABLE; diff --git a/sqlpage/apexcharts.js b/sqlpage/apexcharts.js index 6d2b3f53..5a3ae4c3 100644 --- a/sqlpage/apexcharts.js +++ b/sqlpage/apexcharts.js @@ -1,4 +1,4 @@ -/* !include https://cdn.jsdelivr.net/npm/apexcharts@3.43.2-0/dist/apexcharts.min.js */ +/* !include https://cdn.jsdelivr.net/npm/apexcharts@3.44.0/dist/apexcharts.min.js */ function sqlpage_chart() { diff --git a/sqlpage/tabler-icons.svg b/sqlpage/tabler-icons.svg index 711fa9f4..ef48d11d 100644 --- a/sqlpage/tabler-icons.svg +++ b/sqlpage/tabler-icons.svg @@ -1 +1 @@ -/* !include https://cdn.jsdelivr.net/npm/@tabler/icons@2.40.0/tabler-sprite.svg */ \ No newline at end of file +/* !include https://cdn.jsdelivr.net/npm/@tabler/icons@2.41.0/tabler-sprite.svg */ \ No newline at end of file diff --git a/src/webserver/database/execute_queries.rs b/src/webserver/database/execute_queries.rs index 83dc83ae..6dffa36b 100644 --- a/src/webserver/database/execute_queries.rs +++ b/src/webserver/database/execute_queries.rs @@ -1,7 +1,6 @@ use anyhow::anyhow; use futures_util::stream::Stream; use futures_util::StreamExt; -use serde_json::Value; use std::borrow::Cow; use std::collections::HashMap; @@ -14,7 +13,6 @@ use sqlx::pool::PoolConnection; use sqlx::{Any, AnyConnection, Arguments, Either, Executor, Row, Statement}; use super::sql_pseudofunctions::StmtParam; -use super::sql_to_json::sql_to_json; use super::{highlight_sql_error, Database, DbItem}; impl Database { @@ -55,11 +53,15 @@ pub fn stream_query_results<'a>( ParsedStatement::SetVariable { variable, value} => { let query = bind_parameters(value, request).await?; let connection = take_connection(db, &mut connection_opt).await?; - let row = connection.fetch_optional(query).await?; + log::debug!("Executing query to set the {variable:?} variable: {:?}", query.sql); + let value: Option = connection.fetch_optional(query).await? + .and_then(|row| row.try_get::, _>(0).ok().flatten()); let (vars, name) = vars_and_name(request, variable)?; - if let Some(row) = row { - vars.insert(name.clone(), row_to_varvalue(&row)); + if let Some(value) = value { + log::debug!("Setting variable {name} to {value:?}"); + vars.insert(name.clone(), SingleOrVec::Single(value)); } else { + log::debug!("Removing variable {name}"); vars.remove(&name); } }, @@ -92,24 +94,6 @@ fn vars_and_name<'a>( } } -fn row_to_varvalue(row: &AnyRow) -> SingleOrVec { - let Some(col) = row.columns().first() else { - return SingleOrVec::Single(String::new()); - }; - match sql_to_json(row, col) { - Value::String(s) => SingleOrVec::Single(s), - Value::Array(vals) => SingleOrVec::Vec( - vals.into_iter() - .map(|v| match v { - Value::String(s) => s, - other => other.to_string(), - }) - .collect(), - ), - other => SingleOrVec::Single(other.to_string()), - } -} - async fn take_connection<'a, 'b>( db: &'a Database, conn: &'b mut Option>, diff --git a/tests/sql_test_files/it_works_set_variable_to_null.sql b/tests/sql_test_files/it_works_set_variable_to_null.sql new file mode 100644 index 00000000..e2435dfc --- /dev/null +++ b/tests/sql_test_files/it_works_set_variable_to_null.sql @@ -0,0 +1,9 @@ +set i_am_null = NULL; +select 'text' as component, + CASE + WHEN $i_am_null IS NULL + THEN + 'It works !' + ELSE + 'error: expected null, got: ' || $i_am_null + END as contents;