Skip to content

Commit

Permalink
null variables (#136)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
lovasoa authored Nov 19, 2023
1 parent edd94df commit af7e16f
Show file tree
Hide file tree
Showing 9 changed files with 51 additions and 44 deletions.
29 changes: 25 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down
8 changes: 4 additions & 4 deletions examples/user-authentication/protected_page.sql
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 0 additions & 7 deletions examples/user-authentication/sqlpage/migrations/0000_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion sqlpage/apexcharts.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion sqlpage/tabler-icons.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 7 additions & 23 deletions src/webserver/database/execute_queries.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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 {
Expand Down Expand Up @@ -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<String> = connection.fetch_optional(query).await?
.and_then(|row| row.try_get::<Option<String>, _>(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);
}
},
Expand Down Expand Up @@ -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<PoolConnection<sqlx::Any>>,
Expand Down
9 changes: 9 additions & 0 deletions tests/sql_test_files/it_works_set_variable_to_null.sql
Original file line number Diff line number Diff line change
@@ -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;

0 comments on commit af7e16f

Please sign in to comment.