chore: import upstream snapshot with attribution
CI / compile_and_lint (push) Failing after 0s
CI / docker_build (linux/amd64, -linux-amd64-duckdb, duckdb) (push) Failing after 0s
CI / docker_build (linux/arm64, -linux-arm64, minimal) (push) Failing after 2s
CI / docker_build (linux/arm64, -linux-arm64-duckdb, duckdb) (push) Failing after 1s
CI / docker_build (linux/amd64, minimal) (push) Failing after 1s
CI / test (, sqlite, sqlite::memory:) (push) Has been skipped
CI / test (mssql, mssql, mssql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (mysql, mysql, mysql://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / test (oracle, oracle, Driver=Oracle 21 ODBC driver;Dbq=//127.0.0.1:1521/FREEPDB1;Uid=root;Pwd=Password123!) (push) Has been skipped
CI / test (postgres, odbc, Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!, true) (push) Has been skipped
CI / test (postgres, postgres, postgres://root:Password123!@127.0.0.1/sqlpage) (push) Has been skipped
CI / playwright (push) Has been skipped
CI / docker_build (linux/arm/v7, -linux-arm-v7, minimal) (push) Failing after 0s
CI / hurl_examples (push) Failing after 8s
deploy website / deploy_official_site (push) Failing after 1s
CI / hurl (${{ matrix.example }}) (push) Has been skipped
CI / docker_push (duckdb) (push) Has been cancelled
CI / docker_push (minimal) (push) Has been cancelled
CI / windows_test (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:57 +08:00
commit d718c5a372
986 changed files with 74597 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,177 @@
-- Hero
INSERT INTO
component(name, icon, description)
VALUES
(
'hero',
'home',
'Display a large title and description for your page, with an optional large illustrative image. Useful in your home page, for instance.'
);
INSERT INTO
parameter(
component,
name,
description,
type,
top_level,
optional
)
SELECT
'hero',
*
FROM
(
VALUES
-- top level
(
'title',
'The title of your page. Will be shown in very large characters at the top.',
'TEXT',
TRUE,
TRUE
),
(
'description',
'A description of the page. Displayed below the title, in smaller characters and slightly greyed out.',
'TEXT',
TRUE,
TRUE
),
(
'description_md',
'A description of the page. Displayed below the title, in smaller characters and slightly greyed out - formatted using markdown.',
'TEXT',
TRUE,
TRUE
),
(
'image',
'The URL of an image to display next to the page title.',
'URL',
TRUE,
TRUE
),
(
'video',
'The URL of a video to display next to the page title.',
'URL',
TRUE,
TRUE
),
(
'link',
'Creates a large "call to action" button below the description, linking to the specified URL.',
'URL',
TRUE,
TRUE
),
(
'link_text',
'The text to display in the call to action button. Defaults to "Go".',
'TEXT',
TRUE,
TRUE
),
(
'poster',
'URL of the image to be displayed before the video starts. Ignored if no video is present.',
'URL',
TRUE,
TRUE
),
(
'nocontrols',
'Hide the video controls (play, pause, volume, etc.), and autoplay the video.',
'BOOLEAN',
TRUE,
TRUE
),
('muted', 'Mute the video', 'BOOLEAN', TRUE, TRUE),
('autoplay', 'Automatically start playing the video', 'BOOLEAN', TRUE, TRUE),
('loop', 'Loop the video', 'BOOLEAN', TRUE, TRUE),
(
'reverse',
'Reverse the order of the image and text: the image will be on the left, and the text on the right.',
'BOOLEAN',
TRUE,
TRUE
),
-- item level
(
'title',
'The name of a single feature section highlighted by this hero.',
'TEXT',
FALSE,
TRUE
),
(
'description',
'Description of the feature section.',
'TEXT',
FALSE,
TRUE
),
(
'description_md',
'Description of the feature section - formatted using markdown.',
'TEXT',
FALSE,
TRUE
),
(
'icon',
'Icon of the feature section.',
'ICON',
FALSE,
TRUE
),
(
'link',
'An URL to which the user should be taken when they click on the section title.',
'TEXT',
FALSE,
TRUE
)
) x;
INSERT INTO
example(component, description, properties)
VALUES
(
'hero',
'The simplest possible hero section',
json(
'[{
"component":"hero",
"title": "Welcome",
"description": "This is a very simple site built with SQLPage."
}]'
)
),
(
'hero',
'A hero with a background image.',
json(
'[{
"component":"hero",
"title": "SQLPage",
"description_md": "Documentation for the *SQLPage* low-code web application framework.",
"image": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Lac_de_Zoug.jpg/640px-Lac_de_Zoug.jpg",
"link": "/documentation.sql",
"link_text": "Read Documentation !"},' || '{"title": "Fast", "description": "Pages load instantly, even on slow mobile networks.", "icon": "car", "color": "red", "link": "/"},' || '{"title": "Beautiful", "description": "Uses pre-defined components that look professional.", "icon": "eye", "color": "green", "link": "/"},' || '{"title": "Easy", "description_md": "You can teach yourself enough SQL to use [**SQLPage**](https://sql-page.com) in a weekend.", "icon": "sofa", "color": "blue", "link": "/"}' || ']'
)
),
(
'hero',
'A hero with a video',
json(
'[{
"component":"hero",
"title": "Databases",
"reverse": true,
"description_md": "# “The goal is to turn data into information, and information into insight.”",
"poster": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Lac_de_Zoug.jpg/640px-Lac_de_Zoug.jpg",
"video": "/sqlpage_introduction_video.webm"
}]')
);
@@ -0,0 +1,188 @@
-- Alert component
INSERT INTO component(name, icon, description)
VALUES (
'alert',
'alert-triangle',
'A visually distinctive message or notification.'
);
INSERT INTO parameter(
component,
name,
description,
type,
top_level,
optional
)
VALUES (
'alert',
'title',
'Title of the alert message.',
'TEXT',
TRUE,
FALSE
),
(
'alert',
'icon',
'Icon name (from tabler-icons.io) to display next to the alert message.',
'TEXT',
TRUE,
TRUE
),
(
'alert',
'color',
'The color theme for the alert message.',
'COLOR',
TRUE,
TRUE
),
(
'alert',
'description',
'Detailed description or content of the alert message.',
'TEXT',
TRUE,
TRUE
),
(
'alert',
'description_md',
'Detailed description or content of the alert message, in Markdown format, allowing you to use rich text formatting, including **bold** and *italic* text.',
'TEXT',
TRUE,
TRUE
),
(
'alert',
'dismissible',
'Whether the user can close the alert message.',
'BOOLEAN',
TRUE,
TRUE
),
(
'alert',
'important',
'Set this to TRUE to make the alert message more prominent.',
'BOOLEAN',
TRUE,
TRUE
),
(
'alert',
'link',
'A URL to link to from the alert message.',
'URL',
TRUE,
TRUE
),
(
'alert',
'link_text',
'Customize the text of the link in the alert message. The default is "Ok".',
'TEXT',
TRUE,
TRUE
),
(
'alert',
'link',
'A URL to link to from the alert message.',
'URL',
FALSE,
TRUE
),
(
'alert',
'title',
'Customize the text of the link in the alert message. The default is "Ok".',
'TEXT',
FALSE,
TRUE
),
(
'alert',
'color',
'Customize the color of the link.',
'COLOR',
FALSE,
TRUE
);
-- Insert example(s) for the component
INSERT INTO example(component, description, properties)
VALUES (
'alert',
'A basic alert message',
JSON(
'[{"component":"alert", "title":"Attention", "description":"This is an important message."}]'
)
),
(
'alert',
'A list of notifications',
JSON(
'[
{"component" :"alert", "title":"Success","description":"Item successfully added to your cart.", "icon":"check", "color": "green"},
{"component":"alert", "title":"Warning","description":"Your cart is almost full.", "icon":"alert-triangle", "color": "yellow"},
{"component":"alert", "title":"Error","description":"Your cart is full.", "icon":"alert-circle", "color": "red"}
]'
)
),
(
'alert',
'A full-featured notification message with multiple links',
JSON(
'[
{
"component" :"alert",
"title": "Your dashboard is ready!",
"icon":"analyze",
"color":"teal",
"dismissible": true,
"description":"Your public web dashboard was successfully created."
},
{
"link":"dashboard.sql",
"title": "View your dashboard"
},
{
"link" :"index.sql",
"title": "Back to home",
"color": "secondary"
}
]'
)
),
(
'alert',
'An important danger alert message with an icon and color',
JSON(
'[
{
"component":"alert",
"title":"Alert",
"icon":"alert-circle",
"color":"red",
"important": true,
"dismissible": true,
"description":"SQLPage is entirely free and open source.",
"link":"https://github.com/sqlpage/SQLPage",
"link_text":"See source code"
}]'
)
),
(
'alert',
'An alert message with a Markdown-formatted description',
JSON(
'[
{
"component":"alert",
"title":"Free and open source",
"icon": "free-rights",
"color": "info",
"description_md":"*SQLPage* is entirely free and open source. You can **contribute** to it on [GitHub](https://github.com/sqlpage/SQLPage)."
}]'
)
);
@@ -0,0 +1,116 @@
-- Insert the http_header component into the component table
INSERT INTO component (name, description, icon)
VALUES (
'http_header',
'
An advanced component to set arbitrary HTTP headers: can be used to set a custom caching policy to your pages, or implement custom redirections, for example.
If you are a beginner, you probably don''t need this component.
When used, this component has to be the first component in the page, because once the page is sent to the browser, it is too late to change the headers.
HTTP headers are additional pieces of information sent with responses to web requests that provide instructions
or metadata about the data being sent — for example,
setting cache control directives to control caching behavior
or specifying the content type of a response.
Any valid HTTP header name can be used as a top-level parameter for this component.
The examples shown here are just that, examples; and you can create any custom header
if needed simply by declaring it.
If your header''s name contains a dash or any other special character,
you will have to use your database''s quoting mechanism to declare it.
In standard SQL, you can use double quotes to quote identifiers (like "X-My-Header"),
in Microsoft SQL Server, you can use square brackets (like [X-My-Header]).
',
'world-www'
);
-- Insert the parameters for the http_header component into the parameter table
INSERT INTO parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES (
'http_header',
'Cache-Control',
'Directives for how long the page should be cached by the browser. Set this to max-age=N to keep the page in cache for N seconds.',
'TEXT',
TRUE,
TRUE
),
(
'http_header',
'Content-Disposition',
'Provides instructions on how the response content should be displayed or handled by the client, such as inline or as an attachment.',
'TEXT',
TRUE,
TRUE
),
(
'http_header',
'Location',
'Specifies the URL to redirect the client to, usually used in 3xx redirection responses.',
'TEXT',
TRUE,
TRUE
),
(
'http_header',
'Set-Cookie',
'Sets a cookie in the client browser, used for session management and storing user-related information.',
'TEXT',
TRUE,
TRUE
),
(
'http_header',
'Access-Control-Allow-Origin',
'Specifies which origins are allowed to access the resource in a cross-origin request, used for implementing Cross-Origin Resource Sharing (CORS).',
'TEXT',
TRUE,
TRUE
);
-- Insert an example usage of the http_header component into the example table
INSERT INTO example (component, description, properties)
VALUES (
'http_header',
'Set cache control directives for caching behavior. In this example, the response can be cached by the browser
and served from the cache for up to 600 seconds (10 minutes) after it is first requested.
During that time, even if the cached response becomes stale (outdated), the browser can still use it (stale-while-revalidate)
for up to 3600 seconds (1 hour) while it retrieves a fresh copy from the server in the background.
If there is an error while trying to retrieve a fresh copy from the server,
the browser can continue to serve the stale response for up to 86400 seconds (24 hours) (stale-if-error) instead of showing an error page.
This caching behavior helps improve the performance and responsiveness of the website by reducing the number of requests made to the server
and allowing the browser to serve content from its cache when appropriate.',
JSON(
'[{
"component": "http_header",
"Cache-Control": "public, max-age=600, stale-while-revalidate=3600, stale-if-error=86400"
}]'
)
),
(
'http_header',
'Redirect the user to another page. In this example, the user is redirected to a file named another-page.sql at the root of the website. The current page will not be displayed at all.
This is useful in particular for content creation pages that contain only INSERT statements, because you can redirect the user to the page that lists the content after it has been created.',
JSON(
'[{
"component": "http_header",
"Location": "/another-page.sql"
}]'
)
),
(
'http_header',
'Set a custom non-standard header for the response. In this example, the response will include a custom header named X-My-Header with the value "my value".',
JSON(
'[{
"component": "http_header",
"X-My-Header": "my value"
}]'
)
);
@@ -0,0 +1,125 @@
-- Insert the http_header component into the component table
INSERT INTO component (name, description, icon)
VALUES (
'cookie',
'
Sets a cookie in the client browser, used for session management and storing user-related information.
This component creates a single cookie. Since cookies need to be set before the response body is sent to the client,
this component should be **placed at the top of the page**, before any other components that generate output.
After being set, a cookie can be accessed anywhere in your SQL code using the `sqlpage.cookie(''cookie_name'')` pseudo-function.
Note that if your site is accessed over HTTP (and not HTTPS), you have to set `false as secure` to force browsers to accept your cookies.',
'cookie'
);
-- Insert the parameters for the http_header component into the parameter table
INSERT INTO parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES (
'cookie',
'name',
'The name of the cookie to set.',
'TEXT',
TRUE,
FALSE
),
(
'cookie',
'value',
'The value of the cookie to set.',
'TEXT',
TRUE,
TRUE
),
(
'cookie',
'path',
'The path for which the cookie will be sent. If not specified, the cookie will be sent for all paths.',
'TEXT',
TRUE,
TRUE
),
(
'cookie',
'domain',
'The domain for which the cookie will be sent. If not specified, the cookie will be sent for all domains.',
'TEXT',
TRUE,
TRUE
),
(
'cookie',
'secure',
'Whether the cookie should only be sent over a secure (HTTPS) connection. Defaults to TRUE.',
'BOOLEAN',
TRUE,
TRUE
),
(
'cookie',
'http_only',
'Whether the cookie should only be accessible via HTTP and not via client-side scripts. If not specified, the cookie will be accessible via both HTTP and client-side scripts.',
'BOOLEAN',
TRUE,
TRUE
),
(
'cookie',
'remove',
'Set to TRUE to remove the cookie from the client browser. When specified, other parameters are ignored.',
'BOOLEAN',
TRUE,
TRUE
),
(
'cookie',
'max_age',
'The maximum age of the cookie in seconds. number of seconds until the cookie expires. If both Expires and Max-Age are set, Max-Age has precedence.',
'INTEGER',
TRUE,
TRUE
),
(
'cookie',
'expires',
'The date at which the cookie expires (either a timestamp or a date object). If not specified, the cookie will expire when the browser is closed.',
'TIMESTAMP',
TRUE,
TRUE
),
(
'cookie',
'same_site',
'Whether the cookie should only be sent for requests originating from the same site. See owasp.org/www-community/SameSite. `strict` is the recommended and default value, but you may want to set it to `lax` if you want your users to keep their session when they click on a link to your site from an external site.',
'TEXT',
TRUE,
TRUE
);
-- Insert an example usage of the http_header component into the example table
INSERT INTO example (component, description)
VALUES (
'cookie',
'Create a cookie named `username` with the value `John Doe`...
```sql
SELECT ''cookie'' as component,
''username'' as name,
''John Doe'' as value
FALSE AS secure; -- You can remove this if the site is served over HTTPS.
```
and then display the value of the cookie using the [`sqlpage.cookie`](functions.sql?function=cookie) function:
```sql
SELECT ''text'' as component,
''Your name is '' || COALESCE(sqlpage.cookie(''username''), ''not known to us'');
```
'
);
@@ -0,0 +1,25 @@
-- Insert the http_header component into the component table
INSERT INTO component (name, description, icon)
VALUES (
'debug',
'Visualize any set of values as JSON.
Can be used to display all the parameters passed to the component.
Useful for debugging: just replace the name of the component you want to debug with ''debug'', and see all the top-level and row-level parameters that are passed to it, and their types.',
'bug'
);
-- Insert an example usage of the http_header component into the example table
INSERT INTO example (component, description, properties)
VALUES (
'debug',
'At any time, if you are confused about what data you are passing to a component, just replace the component name with ''debug'' to see all the parameters that are passed to it.',
JSON('[{"component": "debug", "my_top_level_property": true}, {"x": "y", "z": 42}, {"a": "b", "c": null}]')
),
(
'debug',
'Show the result of a SQLPage function:
```sql
select ''debug'' as component, sqlpage.environment_variable(''HOME'');
```',
NULL
);
@@ -0,0 +1,189 @@
-- Insert the http_header component into the component table
INSERT INTO component (name, description, icon, introduced_in_version)
VALUES (
'authentication',
'
Create pages with password-restricted access.
When you want to add user authentication to your SQLPage application,
you have two main options:
1. The `authentication` component:
- lets you manage usernames and passwords yourself
- does not require any external service
- gives you fine-grained control over
- which pages and actions are protected
- the look of the [login form](?component=login)
- the duration of the session
- the permissions of each user
2. [**Single sign-on**](/sso)
- lets users log in with their existing accounts (like Google, Microsoft, or your organization''s own identity provider)
- requires setting up an external service (Google, Microsoft, etc.)
- frees you from implementing a lot of features like password reset, account creation, user management, etc.
This page describes the first option.
When used, this component has to be at the top of your page,
because once the page has begun being sent to the browser,
it is too late to restrict access to it.
The authentication component checks if the user has sent the correct password,
and if not, redirects them to the URL specified in the link parameter.
If you don''t want to re-check the password on every page (which is an expensive operation),
you can check the password only once and store a session token in your database
(see the session example below).
You can use the [cookie component](?component=cookie) to set the session token cookie in the client browser,
and then check whether the token matches what you stored in subsequent pages.',
'lock',
'0.7.2'
);
-- Insert the parameters for the http_header component into the parameter table
INSERT INTO parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES (
'authentication',
'link',
'The URL to redirect the user to if they are not logged in. If this parameter is not specified, the user will stay on the current page, but be asked to log in using a popup in their browser (HTTP basic authentication).',
'TEXT',
TRUE,
TRUE
),
(
'authentication',
'password',
'The password that was sent by the user. You can set this to :password if you have a login form leading to your page.',
'TEXT',
TRUE,
TRUE
),
(
'authentication',
'password_hash',
'The hash of the password that you stored for the user that is currently trying to log in. These hashes can be generated ahead of time using a tool like https://argon2.online/.',
'TEXT',
TRUE,
TRUE
);
-- Insert an example usage of the http_header component into the example table
INSERT INTO example (component, description)
VALUES (
'authentication',
'
### Usage with HTTP basic authentication
The most basic usage of the authentication component is with the
[`sqlpage.basic_auth_username()`](functions.sql?function=basic_auth_username#function) and
[`sqlpage.basic_auth_password()`](functions.sql?function=basic_auth_password#function) functions.
The component will check if the provided password matches the stored [password hash](/examples/hash_password.sql),
and if not, it will prompt the user to enter a password in a browser popup:
```sql
SELECT ''authentication'' AS component,
''$argon2i$v=19$m=8,t=1,p=1$YWFhYWFhYWE$oKBq5E8XFTHO2w'' AS password_hash, -- this is a hash of the password ''password''
sqlpage.basic_auth_password() AS password; -- this is the password that the user entered in the browser popup
```
You can [generate a password hash using the `hash_password` function](/examples/hash_password.sql).
If you want to have multiple users with different passwords,
you could store them with their password hashes in the database,
or just hardcode them use a `CASE` statement:
```sql
SELECT ''authentication'' AS component,
case sqlpage.basic_auth_username()
when ''admin''
then ''$argon2i$v=19$m=8,t=1,p=1$YWFhYWFhYWE$oKBq5E8XFTHO2w'' -- the password is ''password''
when ''user''
then ''$argon2i$v=19$m=8,t=1,p=1$YWFhYWFhYWE$qsrWdjgl96ooYw'' -- the password is ''user''
end AS password_hash, -- this is a hash of the password ''password''
sqlpage.basic_auth_password() AS password; -- this is the password that the user entered in the browser popup
```
Try this example online: [SQL Basic Auth](/examples/authentication/basic_auth.sql).
### Advanced user session management
*Basic auth* is the simplest way to password-protect a page,
but it is not very flexible nor user-friendly,
because the browser will show an unstyled popup asking for the username and password.
For more advanced authentication, you can store user information and user sessions in your database.
You can then use the [`form`](components.sql?component=form#component) component to create a custom login form.
When the user submits the form, you check if the password is correct using the `authentication` component.
You then store a unique string of numbers and letters (a session token) both in the user''s browser
using the [`cookie`](components.sql?component=cookie#component) component and in your database.
Then, in all the pages that require authentication, you check if the cookie is present and matches the session token in your database.
You can check if the user has sent the correct password in a form, and if not, redirect them to a login page.
Create a login form in a file called `login.sql` that uses the [login component](?component=login):
```sql
select ''login'' as component;
```
And then, in `create_session_token.sql` :
```sql
SELECT ''authentication'' AS component,
''login.sql'' AS link,
''$argon2id$v=19$m=16,t=2,p=1$TERTd0lIcUpraWFTcmRQYw$+bjtag7Xjb6p1dsuYOkngw'' AS password_hash, -- generated using sqlpage.hash_password
:password AS password; -- this is the password that the user sent through our form
-- The code after this point is only executed if the user has sent the correct password
```
and in `login.sql` :
```sql
SELECT ''form'' AS component, ''Login'' AS title, ''my_protected_page.sql'' AS action;
SELECT ''password'' AS type, ''password'' AS name, ''Password'' AS label;
```
### Advanced: usage with a session token
Calling the `authentication` component is expensive.
The password hashing algorithm is designed to be slow, so that it is difficult to brute-force the password,
even if an attacker gets access to the database.
If you want to avoid calling the `authentication` component on every page, you can use a session token.
A session token is a random string that is generated when the user logs in, and stored in the database.
It has a limited lifetime, and is stored in a cookie in the user''s browser.
When the user visits a page, the session token is sent to the server, and the server checks if it is valid.
```sql
SELECT ''authentication'' AS component,
''login.sql'' AS link,
(SELECT password_hash FROM user WHERE username = :username) AS password_hash,
:password AS password;
-- The code after this point is only executed if the user has sent the correct password
-- Generate a random session token
INSERT INTO session (id, username)
VALUES (sqlpage.random_string(32), :username)
RETURNING
''cookie'' AS component,
''session_token'' AS name,
id AS value;
```
### Single sign-on with OIDC (OpenID Connect)
If you don''t want to manage your own user database,
you can [use OpenID Connect and OAuth2](/sso) to authenticate users.
This allows users to log in with their Google, Microsoft, or internal company account.
');
@@ -0,0 +1,480 @@
CREATE TABLE IF NOT EXISTS sqlpage_functions (
"name" TEXT PRIMARY KEY,
"icon" TEXT,
"description_md" TEXT,
"return_type" TEXT,
"introduced_in_version" TEXT
);
CREATE TABLE IF NOT EXISTS sqlpage_function_parameters (
"function" TEXT REFERENCES sqlpage_functions("name"),
"index" INTEGER,
"name" TEXT,
"description_md" TEXT,
"type" TEXT
);
INSERT INTO sqlpage_functions (
"name",
"return_type",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'cookie',
'TEXT',
'0.7.1',
'cookie',
'Reads a [cookie](https://en.wikipedia.org/wiki/HTTP_cookie) with the given name from the request.
Returns the value of the cookie as text, or NULL if the cookie is not present.
Cookies can be set using the [cookie component](documentation.sql?component=cookie#component).
### Example
#### Set a cookie
Set a cookie called `username` to greet the user by name every time they visit the page:
```sql
select ''cookie'' as component, ''username'' as name, :username as value;
SELECT ''form'' as component;
SELECT ''username'' as name, ''text'' as type;
```
#### Read a cookie
Read a cookie called `username` and greet the user by name:
```sql
SELECT ''text'' as component,
''Hello, '' || sqlpage.cookie(''username'') || ''!'' as contents;
```
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'cookie',
1,
'name',
'The name of the cookie to read.',
'TEXT'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'header',
'0.7.2',
'heading',
'Reads a [header](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields) with the given name from the request.
Returns the value of the header as text, or NULL if the header is not present.
### Example
Log the [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) of the browser making the request in the database:
```sql
INSERT INTO user_agent_log (user_agent) VALUES (sqlpage.header(''user-agent''));
```
If you need access to all headers at once, use [`sqlpage.headers()`](?function=headers) instead.
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'header',
1,
'name',
'The name of the HTTP header to read.',
'TEXT'
);
INSERT INTO sqlpage_functions (
"name",
"return_type",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'basic_auth_username',
'TEXT',
'0.7.2',
'user',
'Returns the username from the [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) header of the request.
If the header is not present, this function raises an authorization error that will prompt the user to enter their credentials.
### Example
```sql
SELECT ''authentication'' AS component,
(SELECT password_hash from users where name = sqlpage.basic_auth_username()) AS password_hash,
sqlpage.basic_auth_password() AS password;
```
'
),
(
'basic_auth_password',
'TEXT',
'0.7.2',
'key',
'Returns the password from the [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) header of the request.
If the header is not present, this function raises an authorization error that will prompt the user to enter their credentials.
### Example
```sql
SELECT ''authentication'' AS component,
(SELECT password_hash from users where name = sqlpage.basic_auth_username()) AS password_hash,
sqlpage.basic_auth_password() AS password;
```
'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'hash_password',
'0.7.2',
'spy',
'
Hashes a password with the Argon2id variant and outputs it in the [PHC string format](https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md), ready to store in your users table.
Every call generates a brand new cryptographic salt so that two people choosing the same password still end up with different hashes, which defeats rainbow-table attacks and lets you safely reveal only the hash.
Use this function only when creating or resetting a password (for example while inserting a brand new user): it writes the stored value. Later, at login time, the [authentication component](documentation.sql?component=authentication#component) reads the stored hash, hashes the visitor''s password with the embedded salt and parameters, and grants access only if they match.
### Example
```sql
SELECT ''form'' AS component;
SELECT ''username'' AS name;
SELECT ''password'' AS name, ''password'' AS type;
INSERT INTO users (name, password_hash) VALUES (:username, sqlpage.hash_password(:password));
```
### Try online
You can try the password hashing function [on this page](/examples/hash_password.sql).
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'hash_password',
1,
'password',
'The password to hash.',
'TEXT'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'random_string',
'0.7.2',
'arrows-shuffle',
'Returns a cryptographically secure random string of the given length.
### Example
Generate a random string of 32 characters and use it as a session ID stored in a cookie:
```sql
INSERT INTO login_session (session_token, username) VALUES (sqlpage.random_string(32), :username)
RETURNING
''cookie'' AS component,
''session_id'' AS name,
session_token AS value;
```
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'random_string',
1,
'length',
'The length of the string to generate.',
'INTEGER'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'current_working_directory',
'0.11.0',
'folder-question',
'Returns the [current working directory](https://en.wikipedia.org/wiki/Working_directory) of the SQLPage server process.
### Example
```sql
SELECT ''text'' AS component;
SELECT ''Currently running from '' AS contents;
SELECT sqlpage.current_working_directory() as contents, true as code;
```
#### Result
Currently running from `/home/user/my_sqlpage_website`
#### Notes
The current working directory is the directory from which the SQLPage server process was started.
By default, this is also the directory from which `.sql` files are loaded and served.
However, this can be changed by setting the `web_root` [configuration option](https://github.com/sqlpage/SQLPage/blob/main/configuration.md).
'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'web_root',
'0.42.0',
'folder-code',
'Returns the web root directory where SQLPage serves `.sql` files from.
### Example
```sql
SELECT ''text'' AS component;
SELECT ''SQL files are served from '' AS contents;
SELECT sqlpage.web_root() as contents, true as code;
```
#### Result
SQL files are served from `/home/user/my_sqlpage_website`
#### Notes
The web root is the directory from which `.sql` files are loaded and served.
By default, it is the current working directory, but it can be changed using:
- the `--web-root` command line argument
- the `web_root` [configuration option](https://github.com/sqlpage/SQLPage/blob/main/configuration.md) in `sqlpage.json`
- the `WEB_ROOT` environment variable
This is more reliable than `sqlpage.current_working_directory()` when you need to reference the location of your SQL files.
'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'configuration_directory',
'0.42.0',
'folder-cog',
'Returns the configuration directory where SQLPage looks for `sqlpage.json`, templates, and migrations.
### Example
```sql
SELECT ''text'' AS component;
SELECT ''Configuration files are in '' AS contents;
SELECT sqlpage.configuration_directory() as contents, true as code;
```
#### Result
Configuration files are in `/home/user/my_sqlpage_website/sqlpage`
#### Notes
The configuration directory is where SQLPage looks for:
- `sqlpage.json` (the configuration file)
- `templates/` (custom component templates)
- `migrations/` (database migration files)
By default, it is `./sqlpage` relative to the current working directory, but it can be changed using:
- the `--config-dir` command line argument
- the `SQLPAGE_CONFIGURATION_DIRECTORY` or `CONFIGURATION_DIRECTORY` environment variable
This function is useful when you need to reference configuration-related files in your SQL code.
'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'environment_variable',
'0.11.0',
'variable',
'Returns the value of the given [environment variable](https://en.wikipedia.org/wiki/Environment_variable).
### Example
```sql
SELECT ''text'' AS component;
SELECT ''The value of the HOME environment variable is '' AS contents;
SELECT sqlpage.environment_variable(''HOME'') as contents, true as code;
```'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'environment_variable',
1,
'name',
'The name of the environment variable to read. Must be a literal string.',
'TEXT'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'version',
'0.11.0',
'git-commit',
'Returns the current version of SQLPage as a string.'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'exec',
'0.12.0',
'terminal-2',
'Executes a shell command and returns its output as text.
### Example
#### Fetch data from a remote API using curl
```sql
select ''card'' as component;
select value->>''name'' as title, value->>''email'' as description
from json_each(sqlpage.exec(''curl'', ''https://jsonplaceholder.typicode.com/users''));
```
#### Notes
- This function is disabled by default for security reasons. You can enable it by setting `"allow_exec" : true` in `sqlpage/sqlpage.json`. Enable it only if you trust all the users that can access your SQLPage server files (both locally and on the database).
- Be careful when using this function, as it can be used to execute arbitrary shell commands on your server. Do not use it with untrusted input.
- The command is executed in the current working directory of the SQLPage server process.
- The command is executed with the same user as the SQLPage server process.
- The environment variables of the SQLPage server process are passed to the command, including potentially sensitive variables such as `DATABASE_URL`.
- The command is executed asynchronously, but the SQLPage server has to wait for it to finish before sending the result to the client.
This means that the SQLPage server will not be blocked while the command is running, it will be able to serve other requests, but it will not be able to serve the current request until the command has finished.
You should generally avoid long running commands.
- If the program name is NULL, the result will be NULL.
- If any argument is NULL, it will be passed to the command as an empty string.
- If the command exits with a non-zero exit code, the function will raise an error.
- Arbitrary SQL operations are not allowed as sqlpage function arguments. Use `SET` to assign the result of a SQL query to a variable, and then use that variable as an argument to `sqlpage.exec`.
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'exec',
1,
'program',
'The name of the program to execute. Must be a literal string.',
'TEXT'
),
(
'exec',
2,
'arguments...',
'The arguments to pass to the program.',
'TEXT'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'url_encode',
'0.12.0',
'percentage',
'Returns the given string, with all characters that are not allowed in a URL encoded.
### Example
```sql
select ''text'' as component;
select ''https://example.com/?q='' || sqlpage.url_encode($user_search) as contents;
```
#### Result
`https://example.com/?q=hello%20world`
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'url_encode',
1,
'string',
'The string to encode.',
'TEXT'
);
@@ -0,0 +1,69 @@
INSERT INTO component (name, description, icon, introduced_in_version)
VALUES (
'redirect',
'Redirects the user to another page.
This component helps you:
1. Send users to a different page
1. Stop execution of the current page
### Conditional logic
There is no `IF` statement in SQL. Even when you use a [`CASE` expression](https://modern-sql.com/caniuse/case_(simple)), all branches are always evaluated (and only one is returned).
To conditionally execute a component or a [SQLPage function](/functions.sql), you can use the `redirect` component.
A common use case is error handling. You may want to proceed with the rest of a page only when certain pre-conditions are met.
```sql
SELECT
''redirect'' AS component,
''error_page.sql'' AS link
WHERE NOT your_condition;
-- The rest of the page is only executed if the condition is true
```
### Technical limitation
You must use this component **at the beginning of your SQL file**, before any other components that might send content to the browser.
Since the component needs to tell the browser to go to a different page by sending an *HTTP header*,
it will fail if the HTTP headers have already been sent by the time it is executed.
> **Important difference from [http_header](?component=http_header)**
>
> This component completely stops the page from running after it''s called.
> This makes it a good choice for protecting sensitive information from unauthorized users.
',
'arrow-right',
'0.7.2'
);
-- Insert the parameters for the http_header component into the parameter table
INSERT INTO parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES (
'redirect',
'link',
'The URL to redirect the user to.',
'TEXT',
TRUE,
FALSE
);
-- Insert an example usage of the http_header component into the example table
INSERT INTO example (component, description)
VALUES (
'redirect',
'
Redirect a user to the login page if they are not logged in:
```sql
SELECT ''redirect'' AS component, ''login.sql'' AS link
WHERE NOT EXISTS (SELECT 1 FROM login_session WHERE id = sqlpage.cookie(''session_id''));
```
'
);
@@ -0,0 +1,257 @@
INSERT INTO
component (name, description, icon, introduced_in_version)
VALUES
(
'map',
'
## Visualize SQL data on a map.
The map component displays a custom interactive map with markers on it.
In its simplest form, the component displays points on a map from a table of latitudes and longitudes.
But it can also be used by cartographers in combination with PostgreSQL''s PostGIS or SQLite''s spatialite,
to create custom visualizations of geospatial data.
Use the `geojson` property to generate rich maps from a GIS database.
### Example Use Cases
1. **Store Locator**: Build an interactive map to find the nearest store information using SQL-stored geospatial data.
2. **Delivery Route Optimization**: Visualize the results of delivery route optimization algorithms.
3. **Sales Heatmap**: Identify high-performing regions by mapping sales data stored in SQL.
4. **Real-Time Tracking**: Create dynamic dashboards that track vehicles, assets, or users live using PostGIS or MS SQL Server geospatial time series data. Use the [shell](?component=shell) component to auto-refresh the map.
5. **Demographic Insights**: Map customer demographics or trends geographically to uncover opportunities for growth or better decision-making.
',
'map',
'0.8.0'
);
-- Insert the parameters for the http_header component into the parameter table
INSERT INTO
parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES
(
'map',
'latitude',
'Latitude of the center of the map. If omitted, the map will be centered on its markers.',
'REAL',
TRUE,
TRUE
),
(
'map',
'longitude',
'Longitude of the center of the map.',
'REAL',
TRUE,
TRUE
),
(
'map',
'zoom',
'Zoom Level to apply to the map. Defaults to 5.',
'REAL',
TRUE,
TRUE
),
(
'map',
'max_zoom',
'How far the map can be zoomed in. Defaults to 18. Added in v0.15.2.',
'INTEGER',
TRUE,
TRUE
),
(
'map',
'tile_source',
'Custom map tile images to use, as a URL. Defaults to "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png". Added in v0.15.2.',
'URL',
TRUE,
TRUE
),
(
'map',
'attribution',
'Text to display at the bottom right of the map. Defaults to "© OpenStreetMap".',
'HTML',
TRUE,
TRUE
),
(
'map',
'height',
'Height of the map, in pixels. Default to 350px',
'INTEGER',
TRUE,
TRUE
),
(
'map',
'latitude',
'Latitude of the marker. Required only if geojson is not set.',
'REAL',
FALSE,
FALSE
),
(
'map',
'longitude',
'Longitude of the marker. Required only if geojson is not set.',
'REAL',
FALSE,
FALSE
),
(
'map',
'title',
'Title of the marker, displayed on hover and in the tooltip when the marker is clicked.',
'TEXT',
FALSE,
TRUE
),
(
'map',
'link',
'A link to associate to the marker''s title. If set, the marker tooltip''s title will be clickable and will open the link.',
'TEXT',
FALSE,
TRUE
),
(
'map',
'description',
'Plain text description of the marker, to be displayed in a tooltip when the marker is clicked.',
'TEXT',
FALSE,
TRUE
),
(
'map',
'description_md',
'Description of the marker, in markdown, rendered in a tooltip when the marker is clicked.',
'TEXT',
FALSE,
TRUE
),
(
'map',
'icon',
'Name of the icon to use for the marker',
'ICON',
FALSE,
TRUE
),
(
'map',
'color',
'Background color of the marker on the map. Requires "icon" to be set.',
'COLOR',
FALSE,
TRUE
),
(
'map',
'geojson',
'A GeoJSON geometry (line, polygon, ...) to display on the map. Can be styled using geojson properties using the name of leaflet path options. Introduced in 0.15.1. Accepts raw strings in addition to JSON objects since 0.15.2.',
'JSON',
FALSE,
TRUE
),
(
'map',
'size',
'Size of the marker icon. Requires "icon" to be set. Introduced in 0.15.2.',
'INTEGER',
FALSE,
TRUE
);
-- Insert an example usage of the map component into the example table
INSERT INTO
example (component, description, properties)
VALUES
(
'map',
'
### Adding a marker to a map
Showing how to place a marker on a map. Useful for basic location displays like showing a single office location, event venue, or point of interest. The marker shows basic hover and click interactions.
',
JSON (
'[{ "component": "map" }, { "title": "New Delhi", "latitude": 28.6139, "longitude": 77.2090 }]'
)
),
(
'map',
'
### Advanced map customization using GeoJSON and custom map tiles
This example demonstrates using topographic map tiles, custom marker styling,
and clickable markers that link to external content - perfect for educational or tourism applications.
It uses [GeoJSON](https://en.wikipedia.org/wiki/GeoJSON) to display polygons and lines.
- You can generate GeoJSON data from PostGIS geometries using the [`ST_AsGeoJSON`](https://postgis.net/docs/ST_AsGeoJSON.html) function.
- In spatialite, you can use the [`AsGeoJSON`](https://www.gaia-gis.it/gaia-sins/spatialite-sql-5.1.0.html#p3misc) function.
- In MySQL, you can use the [`ST_AsGeoJSON()`](https://dev.mysql.com/doc/refman/8.0/en/spatial-geojson-functions.html#function_st-asgeojson) function.
',
JSON (
'[{ "component": "map", "zoom": 5, "max_zoom": 8, "height": 600, "latitude": -25, "longitude": 28, "tile_source": "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png", "attribution": "" },
{ "icon": "peace",
"size": 20,
"link": "https://en.wikipedia.org/wiki/Nelson_Mandela",
"geojson": "{\"type\":\"Feature\", \"properties\": { \"title\":\"Mvezo, Birth Place of Nelson Mandela\" }, \"geometry\": { \"type\":\"Point\", \"coordinates\": [28.49, -31.96] }}"}]'
)
),
(
'map',
'
### Maps with links and rich descriptions
Demonstrates how to create an engaging map with custom icons, colors, rich descriptions with markdown support, and connecting points with lines.
Perfect for visualizing multi-dimensional relationships between points on a map, like routes between locations.
Note that the map tile source is set to a MapTiler map. The API key included in the URL in this demo will not work on your own website.
You should get your own API key at [MapTiler](https://www.maptiler.com/cloud/).
',
JSON (
'[
{ "component": "map", "title": "Paris", "zoom": 13, "latitude": 48.85, "longitude": 2.34, "tile_source": "https://api.maptiler.com/maps/streets-v2/{z}/{x}/{y}.png?key=RwoF6Y3gcKx4OBMbvqOY" },
{ "title": "Notre Dame", "icon": "building-castle", "color": "indigo", "latitude": 48.8530, "longitude": 2.3498, "description_md": "A beautiful cathedral.", "link": "https://en.wikipedia.org/wiki/Notre-Dame_de_Paris" },
{ "title": "Eiffel Tower", "icon": "tower", "color": "red", "latitude": 48.8584, "longitude": 2.2945, "description_md": "A tall tower. [Wikipedia](https://en.wikipedia.org/wiki/Eiffel_Tower)" },
{ "title": "Tower to Cathedral", "geojson": {"type": "LineString", "coordinates": [[2.2945, 48.8584], [2.3498, 48.8530]]}, "color": "teal", "description": "A nice 45 minutes walk." }
]'
)
),
(
'map',
'
### Abstract geometric visualizations
Example showing how to create abstract geometric visualizations without a base map.
Useful for displaying spatial data that doesn''t need geographic context, like floor plans, seating charts,
or abstract 2D data visualizations.
',
JSON (
'[
{ "component": "map", "tile_source": false },
{ "title": "MySQL",
"color": "red", "description": "This literal red square is defined as a GeoJSON polygon. Each (x,y) coordinate is a JSON array.",
"geojson": {"type": "Polygon", "coordinates": [[[0, 0], [0, 4], [4, 4], [4, 0], [0, 0]]]}
},
{
"title": "SQLite",
"color": "blue", "description": "This 2D shape was generated by a SQL query.",
"geojson": {"type": "Polygon", "coordinates": [[[5, 0], [9, 0], [7, 4], [5, 0]]]}
}
]'
)
);
@@ -0,0 +1,211 @@
INSERT INTO
component (name, description, icon, introduced_in_version)
VALUES
(
'json',
'Converts SQL query results into the JSON machine-readable data format. Ideal to quickly build APIs for interfacing with external systems.
**JSON** is a widely used data format for programmatic data exchange.
For example, you can use it to integrate with web services written in different languages,
with mobile or desktop apps, or with [custom client-side components](/custom_components.sql) inside your SQLPage app.
Use it when your application needs to expose data to external systems.
If you only need to render standard web pages,
and do not need other software to access your data,
you can ignore this component.
This component **must appear at the top of your SQL file**, before any other data has been sent to the browser.
An HTTP response can have only a single datatype, and it must be declared in the headers.
So if you have already called the `shell` component, or another traditional HTML component,
you cannot use this component in the same file.
SQLPage can also return JSON or JSON Lines when the incoming request says it prefers them with an HTTP `Accept` header, so the same `/users.sql` page can show a table in a browser but return raw data to `curl -H "Accept: application/json" http://localhost:8080/users.sql`.
Use this component when you want to control the payload or force JSON output even for requests that would normally get HTML.
',
'code',
'0.9.0'
);
-- Insert the parameters for the http_header component into the parameter table
INSERT INTO
parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES
(
'json',
'contents',
'A single JSON payload to send. You can use your database''s built-in json functions to build the value to enter here. If not provided, the contents will be taken from the next SQL statements and rendered as a JSON array.',
'TEXT',
TRUE,
TRUE
),
(
'json',
'type',
'The type of the JSON payload to send: "array", "jsonlines", or "sse".
In "array" mode, each query result is rendered as a JSON object in a single top-level array.
In "jsonlines" mode, results are rendered as JSON objects in separate lines, without a top-level array.
In "sse" mode, results are rendered as JSON objects in separate lines, prefixed by "data: ", which allows you to read the results as server-sent events in real-time from javascript.',
'TEXT',
TRUE,
TRUE
);
-- Insert an example usage of the http_header component into the example table
INSERT INTO
example (component, description)
VALUES
(
'json',
'
## Send query results as a single JSON array: `''array'' as type`
The default `array` mode sends the query results as a single JSON array.
If a query returns an error, the array will contain an object with an `error` property.
If multiple queries are executed, all query results will be concatenated into a single array
of heterogeneous objects.
### SQL
```sql
select ''json'' AS component;
select * from users;
```
### Result
```json
[
{"username":"James","userid":1},
{"username":"John","userid":2}
]
```
Clients can also receive JSON or JSON Lines automatically by requesting the same SQL file with an HTTP `Accept` header such as `application/json` or `application/x-ndjson` when the component is omitted, for example:
```
curl -H "Accept: application/json" http://localhost:8080/users.sql
```
'
),
(
'json',
'
## Send a single JSON object: `''jsonlines'' as type`
In `jsonlines` mode, each query result is rendered as a JSON object in a separate line,
without a top-level array.
If there is a single query result, the response will be a valid JSON object.
If there are multiple query results, you will need to parse each line of the response as a separate JSON object.
If a query returns an error, the response will be a JSON object with an `error` property.
### SQL
The following SQL creates an API endpoint that takes a `user_id` URL parameter
and returns a single JSON object containing the user''s details, with one json object key per column in the `users` table.
```sql
select ''json'' AS component, ''jsonlines'' AS type;
select * from users where id = $user_id LIMIT 1;
```
> Note the `LIMIT 1` clause. The `jsonlines` type will send one JSON object per result row,
> separated only by a single newline character (\n).
> So if your query returns multiple rows, the result will not be a single valid JSON object,
> like most JSON parsers expect.
### Result
```json
{ "username":"James", "userid":1 }
```
'
),
(
'json',
'
## Create a complex API endpoint: the `''contents''` property
You can create an API endpoint that will return a JSON value in any format you want,
to implement a complex API.
You should use [the json functions provided by your database](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) to form the value you pass to the `contents` property.
To build a json array out of rows from the database, you can use:
- `json_group_array()` in SQLite,
- `json_agg()` in Postgres, or
- `JSON_ARRAYAGG()` in MySQL.
- `FOR JSON PATH` in SQL Server.
```sql
SELECT ''json'' AS component,
JSON_OBJECT(
''users'', (
SELECT JSON_GROUP_ARRAY(
JSON_OBJECT(
''username'', username,
''userid'', id
)
) FROM users
)
) AS contents;
```
This will return a JSON response that looks like this:
```json
{
"users" : [
{ "username":"James", "userid":1 }
]
}
```
If you want to handle custom API routes, like `POST /api/users/:id`,
you can use
- the [`404.sql` file](/your-first-sql-website/custom_urls.sql) to handle the request despite the URL not matching any file,
- the [`request_method` function](/functions.sql?function=request_method#function) to differentiate between GET and POST requests,
- and the [`path` function](/functions.sql?function=path#function) to extract the `:id` parameter from the URL.
'
),
(
'json',
'
## Access query results in real-time with server-sent events: `''sse'' as type`
Using server-sent events, you can stream large query results to the client in real-time,
row by row.
This allows building sophisticated dynamic web applications that will start processing and displaying
the first rows of data in the browser while the database server is still processing the end of the query.
### SQL
```sql
select ''json'' AS component, ''sse'' AS type;
select * from users;
```
### JavaScript
```javascript
const eventSource = new EventSource("users.sql");
eventSource.onmessage = function (event) {
const user = JSON.parse(event.data);
console.log(user.username);
}
eventSource.onerror = () => eventSource.close(); // do not reconnect after reading all the data
```
'
);
@@ -0,0 +1,185 @@
CREATE TABLE blog_posts (
title TEXT PRIMARY KEY,
description TEXT NOT NULL,
icon TEXT NOT NULL,
external_url TEXT,
content TEXT,
created_at TIMESTAMP NOT NULL
);
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'SQLPage versus No-Code tools',
'What are the advantages and disadvantages of SQLPage compared to No-Code tools?',
'code-minus',
'2023-08-03',
'
# Choosing Your Path: No-Code, Low-Code, or SQL-Based Development
The platform you select shapes the entire trajectory of your application.
Each approach offers distinct advantages, yet demands different compromises - a choice that warrants careful consideration.
## No-Code Platforms: Speed with Limitations
No-Code platforms present a visual canvas for building applications without traditional programming. Whilst brilliant for rapid prototypes and straightforward departmental tools, they falter when confronted with complexity and scale.
**Best suited to**: Quick internal tools and simple workflows
### **Notable examples**
- [NocoBase](https://www.nocobase.com/)
- [NocoDB](https://www.nocodb.com/)
- [Saltcorn](https://github.com/saltcorn/saltcorn)
## Low-Code Platforms: The Flexible Middle Ground
These platforms artfully combine visual development with traditional coding. They maintain the power of custom code whilst accelerating development through carefully designed components.
**Best suited to**: Complex applications requiring both speed and customisation
### **Notable examples**
- [Budibase](https://budibase.com/)
- [Directus](https://github.com/directus/directus)
- [Rowy](https://github.com/rowyio/rowy)
## SQL-Based Development: Elegant Simplicity
SQLPage offers a refreshingly direct approach: pure SQL-driven web applications.
For those versed in SQL, it enables sophisticated data-driven applications without the overhead of additional frameworks.
**Best suited to**: Data-centric applications and dashboards
**Details**: [SQLPage on GitHub](https://github.com/sqlpage/SQLPage)
## The AI Revolution in Development
The emergence of Large Language Models (LLMs) has fundamentally shifted the landscape of application development. Tools that once demanded extensive coding expertise have become remarkably more accessible. AI assistants like ChatGPT excel particularly at generating SQL queries and database operations, making SQL-based platforms surprisingly approachable even for those with limited database experience. These AI companions serve as expert pair programmers, offering suggestions, debugging assistance, and ready-to-use code snippets.
This transformation especially benefits platforms like SQLPage, where the AI''s prowess in SQL generation can bridge the traditional expertise gap. Even complex queries and database operations can be created through natural language conversations with AI assistants, democratising access to sophisticated data manipulation capabilities.
## Making an Informed Choice
Selecting the right development approach requires weighing multiple factors against your project''s specific needs.
Consider these key decision points to guide your platform selection:
### **Time Constraints**
- Immediate delivery required → No-Code
- Several days available → SQLPage or Low-Code
### **Data Complexity**
- Structured data manipulation → SQLPage
- Complex workflows → Low-Code
### **Team Expertise**
- SQL skills → SQLPage
- Limited technical expertise → No-Code
- Varied technical capabilities → Low-Code
### **Control Requirements**
- Precise data layer control → SQLPage
- Visual design flexibility → Low-Code
- Speed over customisation → No-Code
## Further Investigation
For a thorough demonstration of SQLPage''s capabilities: [Building a Full Web Application with SQLPage](https://www.youtube.com/watch?v=mXdgmSdaXkg)
');
INSERT INTO blog_posts (title, description, icon, created_at, external_url)
VALUES (
'Repeating yourself thrice won''t make you a 3X developer',
'A dive into the traditional 3-tier architecture and the DRY principle, and how tools like SQLPage helps you avoid repeating yourself.',
'box-multiple-3',
'2023-08-01',
'https://yrashk.medium.com/repeating-yourself-thrice-doesnt-turn-you-into-a-3x-developer-a778495229c0'
);
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES (
'3 solutions to the 3 layer problem',
'What is the 3 layer problem, and how SQLPage solves it?',
'adjustments-question',
'2023-08-10',
'
# 3 solutions to the 3 layer problem
> Some interesting questions emerged from the article [Repeating yourself thrice doesn''t turn you into a 3X developer](https://yrashk.medium.com/repeating-yourself-thrice-doesnt-turn-you-into-a-3x-developer-a778495229c0).
> This short follow-up article aims to answer them and clarify some points.
Hello all,
I am Ophir Lojkine, the main contributor of the open-source application server **SQLPage**.
The previous article focused on the conventional model of splitting applications into three distinct tiers:
1. a graphical interface (_front-end_),
2. an application server (_back-end_),
3. and a database.
In many projects, this results in three distinct implementations of the applications data model:
1. First, in SQL, in the form of tables, views, and relationships in the database,
2. Then, in _server side_ languages such as Java, Python, or PHP, to create an API managing access to the data, and to implement the business logic of the application,
3. Finally, in JavaScript or TypeScript to implement data manipulation in the user interface.
![Traditional tiers model](blog/three-layers.svg)
---
The topic of interest here is the duplication of the data model between the different layers,
and the communication overhead between them.
We are not talking about how the code is structured within each layer.
It can follow a Model-View-Controller pattern or not, it doesn''t matter.
This three-layer model has several advantages:
specialization of the programmers,
parallelization of work,
scalability,
separation of concerns,
and an optimal exploitation of the capacities of the infrastructure on which each layer is deployed:
web browser, server application and database.
Nevertheless, in large-scale projects,
there is often a certain redundancy of the code between the different layers,
as well as a non-negligible share of code dedicated to communication between them.
For small teams and solo developers, this becomes a major drawback.
## 3 solutions
Fortunately, there are several approaches to solving this problem:
1. For **UI-centric applications** without complicated data processing needs,
you can almost completely abandon server-side development and **directly expose the data to the frontend**.
Open-source tools available in this space include Supabase, PocketBase or Hasura.
2. For **applications with a predominant business logic**, traditional _web frameworks_
solve this problem by centralizing frontend and database control in the backend code.
A common solution involves using an ORM and templating system instead of a dedicated javascript application.
Popular solutions include Django, Ruby on Rails, or Symphony.
3. For simpler applications, it is possible to **avoid both backend and frontend development**
by adopting a _database-first_ approach.
This alternative, although less widespread, allows taking advantage of under-exploited modern capacities of relational databases.
The purpose of the original article was to introduce this lesser known approach.
* **SQLPage** is representative of this last category,
which allows designing a complete web application _in SQL_.
This leads to a loss of control over the precise visual appearance of the application,
which will get a “standardized” look and feel.
On the other hand, this translates into significant gains in terms of development speed,
simplicity and performance.
This solution is not intended to compete with traditional _frameworks_,
but rather integrate earlier in the life cycle of a project.
It thus makes it possible to quickly develop a data structure adapted to the application,
and to iterate over it while benefiting from continuous visual feedback on the final result.
Then, when the application grows, its easy to add a classic frontend and backend on top of the existing database,
without having to start from scratch.
Whichever approach is chosen in the end,
a solid understanding of the conventional three-tier architecture,
as well as a clear perspective on the challenges it creates and the possible solutions,
facilitates decision-making and the evolution of the project with the best suited technologies.
'
);
@@ -0,0 +1,121 @@
INSERT INTO component (name, description, icon, introduced_in_version)
VALUES (
'tab',
'Build a tabbed interface, with each tab being a link to a page. Each tab can be in two states: active or inactive.',
'row-insert-bottom',
'0.9.5'
);
INSERT INTO parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES (
'tab',
'title',
'Text to display on the tab. If link is not set, the link will be the current page with a ''$tab'' parameter set to the tab''s title. If ''id'' is set, the page will be scrolled to the tab.',
'TEXT',
FALSE,
FALSE
),
(
'tab',
'link',
'Link to the page to display when the tab is clicked. By default, the link refers to the current page, with a ''tab'' parameter set to the tab''s title and hash set to the id (if passed) - this brings us back to the location of the tab after submission.',
'TEXT',
FALSE,
TRUE
),
(
'tab',
'active',
'Whether the tab is active or not. Defaults to false.',
'BOOLEAN',
FALSE,
TRUE
),
(
'tab',
'icon',
'Name of the icon to display on the tab. See tabler-icons.io for a list of available icons.',
'TEXT',
FALSE,
TRUE
),
(
'tab',
'color',
'Color of the tab. See preview.tabler.io/colors.html for a list of available colors.',
'TEXT',
FALSE,
TRUE
),
(
'tab',
'description',
'Description of the tab. This is displayed when the user hovers over the tab.',
'TEXT',
FALSE,
TRUE
),
(
'tab',
'center',
'Whether the tabs should be centered or not. Defaults to false.',
'BOOLEAN',
TRUE,
TRUE
)
;
INSERT INTO example (component, description, properties)
VALUES (
'tab',
'This example shows a very basic set of three tabs. The first tab is active. You could use this at the top of a page for easy navigation.
To implement contents that change based on the active tab, use the `tab` parameter in the page query string.
For example, if the page is `/my-page.sql`, then the first tab will have a link of `/my-page.sql?tab=My+First+tab`.
You could then for instance display contents coming from the database based on the value of the `tab` parameter.
For instance: `SELECT ''text'' AS component, contents_md FROM my_page_contents WHERE tab = $tab`.
Or you could write different queries for different tabs and use the `$tab` parameter with a static value in a where clause to switch between tabs:
```sql
select ''tab'' as component;
select ''Projects'' as title, $tab = ''Projects'' as active;
select ''Tasks'' as title, $tab = ''Tasks'' as active;
select ''table'' as component;
select * from my_projects where $tab = ''Projects'';
select * from my_tasks where $tab = ''Tasks'';
```
Note that the example below is completely static, and does not use the `tab` parameter to actually switch between tabs.
View the [dynamic tabs example](/examples/tabs/).
',
JSON(
'[
{ "component": "tab" },
{ "title": "This tab does not exist", "active": true, "link": "?component=tab&tab=tab_1" },
{ "title": "I am not a true tab", "link": "?component=tab&tab=tab_2" },
{ "title": "Do not click here", "link": "?component=tab&tab=tab_3" }
]'
)
),
(
'tab',
'This example shows a more sophisticated set of tabs. The tabs are centered, the active tab has a different color, and all the tabs have a custom link and icon.',
JSON(
'[
{ "component": "tab", "center": true },
{ "title": "Hero", "link": "?component=hero#component", "icon": "home", "description": "The hero component is a full-width banner with a title and an image." },
{ "title": "Tab", "link": "?component=tab#component", "icon": "user", "color": "purple" },
{ "title": "Card", "link": "?component=card#component", "icon": "credit-card" }
]'
)
)
;
@@ -0,0 +1,97 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES (
'Im sorry I forked you',
'SQLPage forked the sqlx database drivers library. Here is why.',
'git-fork',
'2023-08-13',
'
# Im sorry I forked you
Ive been immersed in open source coding since my teenage years, and now, I cant fathom a world without it.
Throughout my career as a computer engineer, Ive yet to encounter a tech company that isnt built on the foundation of free and open source software.
Each company adds its proprietary touch to this vast open source landscape,
but they are ultimately just forming a unique blend atop a colossal iceberg of shared resources.
In the software world, open source truly is the driving force behind innovation.
## Unconventional Financial Currents in Software
The software industry operates on a financial current that defies conventional norms.
Unlike other sectors where key players like oil companies rake in the riches by supplying essentials to other businesses,
the software realm flips the script.
In this landscape, its user-facing giants like Google that reap the profits,
while the very creators crafting the software forming the bedrock for Google and countless others often find themselves on a different end of the economic spectrum.
Open source developers observe this intriguing dynamic,
sometimes even finding satisfaction in witnessing how their freely contributed software
fuels the creation of multimillion-dollar ventures.
## SQLx: A Rust Marvel
[sqlx](https://crates.io/crates/sqlx) is one of the numerous software libraries
that lie at the foundation of this software iceberg.
Its a formidable SQL database driver for the *Rust* programming language,
that harmonizes connection to a multitude of databases.
It garners approximately 20,000 daily downloads.
### Version 0.7
sqlxs main maintainer sought to find a middle ground crafting good open source software while seeking a sustainable livelihood.
This endeavor led to a pivotal decision: extracting the database drivers from the core library.
While retaining most drivers as open source, **compatibility with Microsoft SQL Server was relinquished**.
This significant architectural shift also necessitated the removal of some other features from the core framework,
and the introduction of a new API, making it non trivial to migrate from the previous version.
## SQLPage
As the principal caretaker of the [SQLPage web application server](/), which relies on sqlx,
I faced a pivotal juncture. The path ahead diverged into two distinct trails:
1. a challenging migration to sqlx v0.7, making a cross on MSSQL support;
2. persisting with v0.6, a realm housing outdated and potentially vulnerable dependencies.
After a lot of hesitation I chose a third path: **forking sqlx**.
## Im sorry I forked you
Im sorry I forked you, sqlx. I really am all for financially sustainable open source.
My hope is that the newfound proprietary drivers find success,
duly compensating *@abonander* for the invaluable contributions made.
But I really need a good fully open source set of database drivers for Rust,
I need some of the features that were removed in v0.7, and most importantly,
I want to support SQL Server in SQLPage.
So I created [sqlx-oldapi](https://lib.rs/crates/sqlx-oldapi), a fork of sqlx v0.6.
In the fork:
- Ive meticulously updated all dependencies to their latest iterations, ensuring the foundation remains robust and secure.
- Essential features that were missing have been thoughtfully incorporated to address specific needs, and longstanding bugs have been resolved. Notably, data type support has been fortified, with efficient lossy decoding of `DECIMAL` values as floats across all drivers.
- My endeavors have been focused on elevating the SQL Server driver to the same level as its peers. This involved fixing bugs and crashes, and supporting new data types like `DATETIME` and `DECIMAL`.
The full list of changes can be found in the [changelog](https://github.com/lovasoa/sqlx/blob/main/CHANGELOG.md).
## Concluding Notes
- My best wishes extend to sqlx on their pursuit of successful monetization.
May their path be paved with prosperity as they navigate this new chapter.
- To fellow developers facing a similar crossroads as I did with SQLPage,
know that [sqlx-oldapi](https://lib.rs/crates/sqlx-oldapi) awaits you, ready to empower your endeavors, for free.
Contributions and bug reports are all welcomed [on github](https://github.com/lovasoa/sqlx-oldapi).
And if you are curious about why this pages URL ends in `.sql`, check out [SQLPage](/).
---
**Important addendum**:
The main contributor to sqlx reacted to this post, and they wanted to clarify two things:
- sqlx is a project of the *Launchbase* company, not a personal project of *@abonander*.
Although he is the main contributor, important decisions are taken in consultation with the company.
If new drivers are monetized, the money will go to the company, not to him personally.
The stated goal is to then allocate the money to the development of sqlx.
- Most importantly: the current plan for the new drivers **is not to release them as proprietary**
code as initially planned, but to release them as open source, under the more restrictive *AGPL* license.
This means that they will be free to use for similarly licensed open source projects
(unlike SQLPage, which is free to use even in conjunction with proprietary software, under the *MIT* license).
Companies that want to use these future sqlx drivers in proprietary software will have to pay for a commercial license.
That change in the decision has been announced in 2022, and I should have been aware of it when I wrote this post. My bad.
');
@@ -0,0 +1,103 @@
INSERT INTO component (name, description, icon, introduced_in_version)
VALUES (
'timeline',
'A list of events with a vertical line connecting them.',
'git-commit',
'0.13.0'
);
INSERT INTO parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES (
'timeline',
'simple',
'If set to true, the timeline will be displayed in a condensed format without icons.',
'BOOLEAN',
TRUE,
TRUE
),
(
'timeline',
'title',
'Name of the event.',
'TEXT',
FALSE,
FALSE
),
(
'timeline',
'date',
'Date of the event.',
'TEXT',
FALSE,
FALSE
),
(
'timeline',
'icon',
'Name of the icon to display next to the event. See tabler-icons.io for a list of available icons.',
'TEXT',
FALSE,
TRUE
),
(
'timeline',
'color',
'Color of the icon. See preview.tabler.io/colors.html for a list of available colors.',
'TEXT',
FALSE,
TRUE
),
(
'timeline',
'description',
'Textual description of the event.',
'TEXT',
FALSE,
TRUE
),
(
'timeline',
'description_md',
'Description of the event in Markdown.',
'TEXT',
FALSE,
TRUE
),
(
'timeline',
'link',
'Link to a page with more information about the event.',
'TEXT',
FALSE,
TRUE
);
INSERT INTO example (component, description, properties)
VALUES (
'timeline',
'A basic timeline with just names and dates.',
JSON(
'[
{ "component": "timeline", "simple": true },
{ "title": "New message from Elon Musk", "date": "13:00" },
{ "title": "Jeff Bezos assigned task \"work more\" to you.", "date": "yesterday, 16:35" }
]'
)
),
(
'timeline',
'A full-fledged timeline with icons, colors, and rich text descriptions.',
JSON(
'[
{ "component": "timeline" },
{ "title": "v0.13.0 was just released !", "link": "https://github.com/sqlpage/SQLPage/releases/", "date": "2023-10-16", "icon": "brand-github", "color": "green", "description_md": "This version introduces the `timeline` component." },
{ "title": "They are talking about us...", "description_md": "[This article](https://www.postgresql.org/about/news/announcing-sqlpage-build-dynamic-web-applications-in-sql-2672/) on the official PostgreSQL website mentions SQLPage.", "date": "2023-07-12", "icon": "database", "color": "blue" }
]'
)
)
;
@@ -0,0 +1,119 @@
-- Button Component Documentation
-- Component Definition
INSERT INTO component(name, icon, description, introduced_in_version) VALUES
('button', 'hand-click', 'A versatile button component do display one or multiple button links of different styles.', '0.14.0');
-- Inserting parameter information for the button component
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'button', * FROM (VALUES
-- Top-level parameters (for the whole button list)
('justify', 'The horizontal alignment of the button list (e.g., start, end, center, between).', 'TEXT', TRUE, TRUE),
('size', 'The size of the buttons (e.g., sm, lg).', 'TEXT', TRUE, TRUE),
('shape', 'Shape of the buttons (e.g., pill, square)', 'TEXT', TRUE, TRUE),
-- Item-level parameters (for each button)
('link', 'The URL to which the button should navigate when clicked. If the form attribute is specified, then this overrides the page to which the form is submitted.', 'URL', FALSE, TRUE),
('color', 'The color of the button (e.g., red, green, blue, but also primary, warning, danger, etc.). Only base color names are supported, not variations like "blue-lt" or "gray-300". Use a custom CSS stylesheet to further customize the colors.', 'COLOR', FALSE, TRUE),
('title', 'The text displayed on the button.', 'TEXT', FALSE, TRUE),
('tooltip', 'Text displayed when the user hovers over the button.', 'TEXT', FALSE, TRUE),
('disabled', 'Whether the button is disabled or not.', 'BOOLEAN', FALSE, TRUE),
('outline', 'Outline color of the button (e.g. red, purple, ...)', 'COLOR', FALSE, TRUE),
('space_after', 'Whether there should be extra space to the right of the button. In a line of buttons, this will put the buttons before this one on the left, and the ones after on the right.', 'BOOLEAN', FALSE, TRUE),
('icon_after', 'Name of an icon to display after the text in the button', 'ICON', FALSE, TRUE),
('icon', 'Name of an icon to be displayed on the left side of the button.', 'ICON', FALSE, TRUE),
('image', 'Path to image file (relative. relative to web root or URL) to be displayed on the button.', 'TEXT', FALSE, TRUE),
('narrow', 'Whether to trim horizontal padding.', 'BOOLEAN', FALSE, TRUE),
('form', 'Identifier (id) of the form to which the button should submit.', 'TEXT', FALSE, TRUE),
('rel', '"nofollow" when the contents of the target link are not endorsed, "noopener" when the target is not trusted, and "noreferrer" to hide where the user came from when they open the link.', 'TEXT', FALSE, TRUE),
('target', '"_blank" to open the link in a new tab, "_self" to open it in the same tab, "_parent" to open it in the parent frame, or "_top" to open it in the full body of the window.', 'TEXT', FALSE, TRUE),
('download', 'If defined, the link will download the target instead of navigating to it. Set the value to the desired name of the downloaded file.', 'TEXT', FALSE, TRUE),
('id', 'HTML Identifier to add to the button element.', 'TEXT', FALSE, TRUE)
) x;
-- Inserting example information for the button component
INSERT INTO example(component, description, properties) VALUES
('button', 'A basic button with a link',
json('[{"component":"button"}, {"link":"/documentation.sql", "title":"Enabled"}, {"link":"#", "title":"Disabled", "disabled":true}]'))
;
INSERT INTO example(component, description, properties) VALUES
('button', 'A button with a custom shape, size, and outline color',
json('[{"component":"button", "size":"sm", "shape":"pill" },
{"title":"Purple", "outline":"purple" },
{"title":"Orange", "outline":"orange" },
{"title":"Red", "outline":"red" }]')
);
INSERT INTO example(component, description, properties) VALUES
('button', 'A list of buttons aligned in the center',
json('[{"component":"button", "justify":"center"},
{"link":"#", "color":"light", "title":"Light"},
{"link":"#", "color":"success", "title":"Success"},
{"link":"#", "color":"info", "title":"Info"},
{"link":"#", "color":"dark", "title":"Dark"},
{"link":"#", "color":"warning", "title":"Warning"},
{"link":"#", "color":"danger", "title":"Narrow"}]')
);
INSERT INTO example(component, description, properties) VALUES
('button', 'Icon buttons using the narrow property',
json('[{"component":"button"},
{"link":"#", "narrow":true, "icon":"edit", "color":"primary", "tooltip":"Edit" },
{"link":"#", "narrow":true, "icon":"trash", "color":"danger", "tooltip":"Delete" },
{"link":"#", "narrow":true, "icon":"corner-down-right", "color":"info", "tooltip":"Preview" },
{"link":"#", "narrow":true, "icon":"download", "color":"success", "tooltip":"Download" },
{"link":"#", "narrow":true, "icon":"upload", "color":"warning", "tooltip":"Upload" },
{"link":"#", "narrow":true, "icon":"info-circle", "color":"cyan", "tooltip":"Info" },
{"link":"#", "narrow":true, "icon":"help-circle", "color":"purple", "tooltip":"Help" },
{"link":"#", "narrow":true, "icon":"settings", "color":"indigo", "tooltip":"Settings" }]')
);
INSERT INTO example(component, description, properties) VALUES
('button', 'Buttons with icons and different sizes',
json('[{"component":"button", "size":"lg" },
{"link":"#", "outline":"azure", "title":"Edit", "icon":"edit"},
{"link":"#", "outline":"danger", "title":"Delete", "icon":"trash"}]')
);
INSERT INTO example(component, description, properties) VALUES
('button', 'A row of square buttons with spacing in between',
json('[{"component":"button", "shape":"square"},
{"link":"#", "color":"green", "title":"Save", "icon": "device-floppy" },
{"link":"#", "title":"Cancel", "space_after":true, "tooltip": "This will delete your draft"},
{"link":"#", "outline":"indigo", "title":"Preview", "icon_after": "corner-down-right", "tooltip": "View temporary draft" }]')
);
INSERT INTO example(component, description, properties) VALUES
('button', 'Multiple buttons sending the same form to different pages.
We use `'''' AS validate` to remove the submit button from inside the form itself,
and instead use the button component to submit the form to pages with different GET variables.
In the target page, we could then use the GET variable `$action` to determine what to do with the form data.
',
json('[{"component":"form", "id": "poem", "validate": ""},
{"type": "textarea", "name": "Poem", "placeholder": "Write a poem"},
{"component":"button"},
{"link":"?action=save", "form":"poem", "color":"primary", "title":"Save" },
{"link":"?action=preview", "form":"poem", "outline":"yellow", "title":"Preview" }]')
);
INSERT INTO example(component, description, properties) VALUES
('button', 'A button that downloads a file when clicked, and prevents search engines from following the link.',
json('[{"component":"button"},
{"link":"/sqlpage_introduction_video.webm",
"title":"Download Video",
"icon":"download",
"download":"Introduction Video.webm",
"rel":"nofollow"
}]')
);
INSERT INTO example(component, description, properties) VALUES
('button', 'A button with an image-based icon.',
json('[{"component":"button"},
{"link":"https://en.wikipedia.org/wiki/File:Globe.svg",
"title":"Open an article",
"image":"https://upload.wikimedia.org/wikipedia/commons/f/fa/Globe.svg"
}]')
);
@@ -0,0 +1,119 @@
-- Insert the 'variables' function into sqlpage_functions table
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'variables',
'0.15.0',
'variable',
'Returns a JSON string containing variables from the HTTP request and user-defined variables.
The [database''s json handling functions](/blog?post=JSON+in+SQL%3A+A+Comprehensive+Guide) can then be used to process the data.
## Variable Types
SQLPage distinguishes between three types of variables:
- **GET variables**: URL parameters from the [query string](https://en.wikipedia.org/wiki/Query_string) (immutable)
- **POST variables**: Values from form fields [submitted](https://en.wikipedia.org/wiki/POST_(HTTP)#Use_for_submitting_web_forms) by the user (immutable)
- **SET variables**: User-defined variables created with the `SET` command (mutable)
For more information about SQLPage variables, see the [*SQL in SQLPage* guide](/extensions-to-sql).
## Usage
- `sqlpage.variables()` - returns all variables (GET, POST, and SET combined). When multiple variables of the same name are present, the order of precedence is: set > post > get.
- `sqlpage.variables(''get'')` - returns only URL parameters
- `sqlpage.variables(''post'')` - returns only POST form data
- `sqlpage.variables(''set'')` - returns only user-defined variables created with `SET`
When a SET variable has the same name as a GET or POST variable, the SET variable takes precedence in the combined result.
## Example: a form with a variable number of fields
### Making a form based on questions in a database table
We can create a form which has a field for each value in a given table like this:
```sql
select ''form'' as component, ''handle_survey_answer.sql'' as action;
select question_id as name, question_text as label from survey_questions;
```
### Handling form responses using `sqlpage.variables`
In `handle_survey_answer.sql`, one can process the form results even if we don''t know in advance
how many fields it contains.
The function to parse JSON data varies depending on the database engine you use.
#### In SQLite
In SQLite, one can use [`json_each`](https://www.sqlite.org/json1.html#jeach) :
```sql
insert into survey_answers(question_id, answer)
select "key", "value" from json_each(sqlpage.variables(''post''))
```
#### In Postgres
Postgres has [`json_each_text`](https://www.postgresql.org/docs/9.3/functions-json.html) :
```sql
INSERT INTO survey_answers (question_id, answer)
SELECT key AS question_id, value AS answer
FROM json_each_text(sqlpage.variables(''post'')::json);
```
#### In Microsoft SQL Server
```sql
INSERT INTO survey_answers
SELECT [key] AS question_id, [value] AS answer
FROM OPENJSON(sqlpage.variables(''post''));
```
#### In MySQL
MySQL has [`JSON_TABLE`](https://dev.mysql.com/doc/refman/8.0/en/json-table-functions.html),
and [`JSON_KEYS`](https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#function_json-keys)
which are a little bit less straightforward to use:
```sql
INSERT INTO survey_answers (question_id, answer)
SELECT
question_id,
json_unquote(
json_extract(
sqlpage.variables(''post''),
concat(''$."'', question_id, ''"'')
)
)
FROM json_table(
json_keys(sqlpage.variables(''post'')),
''$[*]'' columns (question_id int path ''$'')
) as question_ids
```
'
);
-- Insert the parameters for the 'variables' function into sqlpage_function_parameters table
-- Parameter 1: 'method' parameter
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'variables',
1,
'method',
'Optional. Filter variables by source: ''get'' (URL parameters), ''post'' (form data), or ''set'' (user-defined variables). When not provided, all variables are returned with SET variables taking precedence over request parameters.',
'TEXT'
);
@@ -0,0 +1,26 @@
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'path',
'0.15.0',
'slashes',
'Returns the request path of the current page.
This is useful to generate links to the current page, and when you have a proxy in front of your SQLPage server that rewrites the URL.
### Example
If we have a page in a file named `my page.sql` at the root of your SQLPage installation
then the following SQL query:
```sql
select ''text'' as component, sqlpage.path() as contents;
```
will return `/my%20page.sql`.
> Note that the path is URL-encoded.
');
@@ -0,0 +1,29 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES (
'Come see me build twitter live on stage in Prague',
'I will speak about SQLPage at pgconf.eu in Prague on December 14th. Come see me !',
'calendar-event',
'2023-11-11',
'
# SQLPage live on stage
Hello everyone !
If some of you european SQLPagers are around Prague this december,
I will be giving a talk about SQLPage at [pgconf.eu](https://2023.pgconf.eu/) on December 14th.
I will be talking about website building in general, SQLPage in particular,
cool things you can do with it,
how it works, how and why I built it,
and why I still think it can cut web development and prototyping times by a factor of 10.
I will also be building a tiny social network live on stage, using SQLPage.
Come see me !
- [pgconf.eu](https://2023.pgconf.eu/)
- [Information about the talk](https://www.postgresql.eu/events/pgconfeu2023/schedule/session/4687-sqlpage-building-a-full-web-application-with-nothing-but-postgres-and-sql-queries/)
- When ? **December 14th, 2023**, **09:30 - 10:20**
- Where ? Zenit Room, [Clarion Congress Hotel, Prague, Czech Republic](https://maps.app.goo.gl/qHCAaRFRjXmx2kQA7)
');
@@ -0,0 +1,195 @@
-- Insert the 'variables' function into sqlpage_functions table
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'uploaded_file_path',
'0.17.0',
'upload',
'Returns the path to a temporary file containing the contents of an uploaded file.
## Example: handling a picture upload
### Making a form
```sql
select ''form'' as component, ''handle_picture_upload.sql'' as action;
select ''myfile'' as name, ''file'' as type, ''Picture'' as label;
select ''title'' as name, ''text'' as type, ''Title'' as label;
```
### Handling the form response
### Inserting an image file as a [data URL](https://en.wikipedia.org/wiki/Data_URI_scheme) into the database
In `handle_picture_upload.sql`, one can process the form results like this:
```sql
insert into pictures (title, path) values (:title, sqlpage.read_file_as_data_url(sqlpage.uploaded_file_path(''myfile'')));
```
> *Note*: Data URLs are larger than the original file, so it is not recommended to use them for large files.
### Inserting file contents as text into the database
When the uploaded file is a simple raw text file (e.g. a `.txt` file),
one can use the [`sqlpage.read_file_as_text`](?function=read_file_as_text#function)
function to insert the contents of the file into the database like this:
```sql
insert into text_documents (title, path) values (:title, sqlpage.read_file_as_text(sqlpage.uploaded_file_path(''my_text_file'')));
```
### Saving the uploaded file to a permanent location
When the uploaded file is larger than a few megabytes, it is not recommended to store it in the database.
Instead, one can save the file to a permanent location on the server, and store the path to the file in the database.
You can move the file to a permanent location using the [`sqlpage.persist_uploaded_file`](?function=persist_uploaded_file#function) function.
### Advanced file handling
For more advanced file handling, such as uploading files to a cloud storage service,
you can write a small script in your favorite programming language,
and call it using the [`sqlpage.exec`](?function=exec#function) function.
For instance, one could save the following small bash script to `/usr/local/bin/upload_to_s3`:
```bash
#!/bin/bash
aws s3 cp "$1" s3://your-s3-bucket-name/
echo "https://your-s3-bucket-url/$(basename "$1")"
```
Then, you can call it from SQL like this:
```sql
set url = sqlpage.exec(''upload_to_s3'', sqlpage.uploaded_file_path(''myfile''));
insert into uploaded_files (title, path) values (:title, $url);
```
'
),
(
'uploaded_file_mime_type',
'0.18.0',
'file-settings',
'Returns the MIME type of an uploaded file.
## Example: handling a picture upload
When letting the user upload a picture, you may want to check that the uploaded file is indeed an image.
```sql
select ''redirect'' as component,
''invalid_file.sql'' as link
where sqlpage.uploaded_file_mime_type(''myfile'') not like ''image/%'';
```
In `invalid_file.sql`, you can display an error message to the user:
```sql
select ''alert'' as component, ''Error'' as title,
''Invalid file type'' as description,
''alert-circle'' as icon, ''red'' as color;
```
## Example: white-listing file types
You could have a database table containing the allowed MIME types, and check that the uploaded file is of one of those types:
```sql
select ''redirect'' as component,
''invalid_file.sql'' as link
where sqlpage.uploaded_file_mime_type(''myfile'') not in (select mime_type from allowed_mime_types);
```
'
),
(
'uploaded_file_name',
'0.23.0',
'file-description',
'Returns the `filename` value in the `content-disposition` header.
## Example: saving uploaded file metadata for later download
### Making a form
```sql
select ''form'' as component, ''handle_file_upload.sql'' as action;
select ''myfile'' as name, ''file'' as type, ''File'' as label;
```
### Handling the form response
### Inserting an arbitrary file as a [data URL](https://en.wikipedia.org/wiki/Data_URI_scheme) into the database
In `handle_file_upload.sql`, one can process the form results like this:
```sql
insert into uploaded_files (fname, content, uploaded) values (
sqlpage.uploaded_file_name(''myfile''),
sqlpage.read_file_as_data_url(sqlpage.uploaded_file_path(''myfile'')),
CURRENT_TIMESTAMP
);
```
> *Note*: Data URLs are larger than the original file, so it is not recommended to use them for large files.
### Downloading the uploaded files
The file can be downloaded by clicking a link like this:
```sql
select ''button'' as component;
select name as title, content as link from uploaded_files where name = $file_name limit 1;
```
> *Note*: because the file is ecoded as a data uri, the file is transferred to the client whether or not the link is clicked
### Large files
See the [`sqlpage.uploaded_file_path`](?function=uploaded_file_path#function) function.
See the [`sqlpage.persist_uploaded_file`](?function=persist_uploaded_file#function) function.
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'uploaded_file_path',
1,
'name',
'Name of the file input field in the form.',
'TEXT'
),
(
'uploaded_file_path',
2,
'allowed_mime_type',
'Makes the function return NULL if the uploaded file is not of the specified MIME type.
If omitted, any MIME type is allowed.
This makes it possible to restrict the function to only accept certain file types.',
'TEXT'
),
(
'uploaded_file_mime_type',
1,
'name',
'Name of the file input field in the form.',
'TEXT'
),
(
'uploaded_file_name',
1,
'name',
'Name of the file input field in the form.',
'TEXT'
)
;
@@ -0,0 +1,66 @@
-- Insert the 'variables' function into sqlpage_functions table
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'read_file_as_data_url',
'0.17.0',
'file-dollar',
'Returns a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs)
containing the contents of the given file.
The file path is relative to the `web root` directory, which is the directory from which your website is served.
By default, this is the directory SQLPage is launched from, but you can change it
with the `web_root` [configuration option](https://github.com/sqlpage/SQLPage/blob/main/configuration.md).
If the given argument is null, the function will return null.
As with other functions, if an error occurs during execution
(because the file does not exist, for instance),
the function will display an error message and the
database query will not be executed.
If you are using a `sqlpage_files` table to store files directly in the database (serverless mode),
the function will attempt to read the file from the database filesystem if it is not found on the local disk,
using the same logic as for serving files in response to HTTP requests.
## MIME type
Data URLs contain the [MIME type](https://en.wikipedia.org/wiki/Media_type) of the file they represent.
If the first argument to this function is the result of a call to the `sqlpage.uploaded_file_path` function,
the declared MIME type of the uploaded file transmitted by the browser will be used.
Otherwise, the MIME type will be guessed from the file extension, without looking at the file contents.
## Example: inlining a picture
```sql
select ''card'' as component;
select ''Picture'' as title, sqlpage.read_file_as_data_url(''/path/to/picture.jpg'') as top_image;
```
> **Note:** Data URLs are larger than the original file they represent, so they should only be used for small files
> (under a few hundred kilobytes).
> Otherwise, the page will take a long time to load.
');
-- Insert the parameters for the 'variables' function into sqlpage_function_parameters table
-- Parameter 1: 'method' parameter
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'read_file_as_data_url',
1,
'name',
'Path to the file to read.',
'TEXT'
);
@@ -0,0 +1,54 @@
-- Insert the 'variables' function into sqlpage_functions table
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'read_file_as_text',
'0.17.0',
'file-invoice',
'Returns a string containing the contents of the given file.
The file must be a raw text file using UTF-8 encoding.
The file path is relative to the `web root` directory, which is the directory from which your website is served
(not necessarily the directory SQLPage is launched from).
If the given argument is null, the function will return null.
As with other functions, if an error occurs during execution
(because the file does not exist, for instance),
the function will display an error message and the
database query will not be executed.
If you are using a `sqlpage_files` table to store files directly in the database (serverless mode),
the function will attempt to read the file from the database filesystem if it is not found on the local disk,
using the same logic as for serving files in response to HTTP requests.
## Example
### Rendering a markdown file
```sql
select ''text'' as component, sqlpage.read_file_as_text(''/path/to/file.md'') as contents_md;
```
');
-- Insert the parameters for the 'variables' function into sqlpage_function_parameters table
-- Parameter 1: 'method' parameter
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'read_file_as_text',
1,
'name',
'Path to the file to read.',
'TEXT'
);
@@ -0,0 +1,170 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES (
'SQLPage v0.17',
'SQLPage v0.17 introduces file uploads, HTTPS, and more.',
'git-fork',
'2023-11-28',
'
# SQLPage v0.17 is out !
[SQLPage](/) is a web application server that lets you build entire web applications with just SQL queries.
v0.17 was just released, and it''s worth a blog post to highlight some of the coolest new features.
Mostly, this release makes it a matter of minutes to build a data import pipeline for your website,
and a matter of seconds to deploy your SQLPage website securely with automatic HTTPS certificates.
## Uploads
This release is all about a long awaited feature: **file uploads**.
Your SQLPage website can now accept file uploads from users,
store them either in a directory or directly in a database table.
You can add a file upload button to a form with a simple
```sql
select ''form'' as component;
select ''profile_picture'' as name, ''file'' as type;
```
when received by the server, the file will be saved in a temporary directory
(customizable with `TMPDIR` on linux).
You can access the temporary file path with
the new [`sqlpage.uploaded_file_path`](/functions.sql?function=uploaded_file_path#function) function.
You can then persist the upload as a permanent file on the server with the
[`sqlpage.exec`](https://sql-page.com/functions.sql?function=exec#function) function:
```sql
set file_path = sqlpage.uploaded_file_path(''profile_picture'');
select sqlpage.exec(''mv'', $file_path, ''/path/to/my/file'');
```
or you can store it directly in a database table with the new
[`sqlpage.read_file_as_data_url`](https://sql-page.com/functions.sql?function=read_file_as_data_url#function) and
[`sqlpage.read_file_as_text`](https://sql-page.com/functions.sql?function=read_file_as_text#function) functions:
```sql
insert into files (url) values (sqlpage.read_file_as_data_url(sqlpage.uploaded_file_path(''profile_picture'')))
returning ''text'' as component, ''Uploaded new file with id: '' || id as contents;
```
The maximum size of uploaded files is configurable with the [`max_uploaded_file_size`](https://github.com/sqlpage/SQLPage/blob/main/configuration.md)
configuration parameter. By default, it is set to 5 MiB.
### Parsing CSV files
SQLPage can also parse uploaded [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) files and insert them directly into a database table.
SQLPage re-uses PostgreSQL''s [`COPY` syntax](https://www.postgresql.org/docs/current/sql-copy.html)
to import the CSV file into the database.
When connected to a PostgreSQL database, SQLPage will use the native `COPY` statement,
for super fast and efficient on-database CSV parsing.
But it will also work with any other database as well, by
parsing the CSV locally and emulating the same behavior with simple `INSERT` statements.
#### `user_file_upload.sql`
```sql
select ''form'' as component, ''bulk_user_import.sql'' as action;
select ''user_csv_file'' as name, ''file'' as type, ''text/csv'' as accept;
```
#### `bulk_user_import.sql`
```sql
-- create a temporary table to preprocess the data
create temporary table if not exists csv_import(name text, age text);
delete from csv_import; -- empty the table
-- If you don''t have any preprocessing to do, you can skip the temporary table and use the target table directly
copy csv_import(name, age) from ''user_csv_file''
with (header true, delimiter '','', quote ''"'', null ''NaN''); -- all the options are optional
-- since header is true, the first line of the file will be used to find the "name" and "age" columns
-- if you don''t have a header line, the first column in the CSV will be interpreted as the first column of the table, etc
-- run any preprocessing you want on the data here
-- insert the data into the users table
insert into users (name, birth_date)
select upper(name), date_part(''year'', CURRENT_DATE) - cast(age as int) from csv_import;
```
### New functions
#### Handle uploaded files
- [`sqlpage.uploaded_file_path`](https://sql-page.com/functions.sql?function=uploaded_file_path#function) to get the temprary local path of a file uploaded by the user. This path will be valid until the end of the current request, and will be located in a temporary directory (customizable with `TMPDIR`). You can use [`sqlpage.exec`](https://sql-page.com/functions.sql?function=exec#function) to operate on the file, for instance to move it to a permanent location.
- [`sqlpage.uploaded_file_mime_type`](https://sql-page.com/functions.sql?function=uploaded_file_mime_type#function) to get the type of file uploaded by the user. This is the MIME type of the file, such as `image/png` or `text/csv`. You can use this to easily check that the file is of the expected type before storing it.
The new [*Image gallery* example](https://github.com/sqlpage/SQLPage/tree/main/examples/image%20gallery%20with%20user%20uploads)
in the official repository shows how to use these functions to create a simple image gallery with user uploads.
#### Read files
These new functions are useful to read the contents of a file uploaded by the user,
but can also be used to read any file on the computer where SQLPage is running:
- [`sqlpage.read_file_as_text`](https://sql-page.com/functions.sql?function=read_file_as_text#function) reads the contents of a file on the server and returns a text string.
- [`sqlpage.read_file_as_data_url`](https://sql-page.com/functions.sql?function=read_file_as_data_url#function) reads the contents of a file on the server and returns a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). This is useful to embed images directly in web pages, or make link
## HTTPS
This is the other big feature of this release: SQLPage now supports HTTPS !
Until now, if you wanted to use HTTPS with SQLPage, you had to put it behind a
*reverse proxy*, which is what the official documentation website does.
This required a lot of manual configuration
that would compromise your security if you get it wrong.
With SQLPage v0.17, you just give your domain name,
and it takes care of everything.
And while we''re at it, SQLPage also supports HTTP/2, for even faster page loads.
To enable HTTPS, you need to buy a [domain name](https://en.wikipedia.org/wiki/Domain_name)
and make it point to the server where SQLPage is running.
Then set the `https_domain` configuration parameter to `yourdomain.com` in your [`sqlpage.json` configuration file](./configuration.md).
```json
{
"https_domain": "my-cool-website.com"
}
```
That''s it. No external tool to install, no certificate to generate, no configuration to tweak.
No need to restart SQLPage regularly either, or to worry about renewing your certificate when it expires.
SQLPage will automatically request a certificate from [Let''s Encrypt](https://letsencrypt.org/) by default,
and does not even need to listen on port 80 to do so.
## SQL parser improvements
SQLPage needs to parse SQL queries to be able to bind the right parameters to them,
and to inject the results of built-in sqlpage functions in them.
The parser we use is very powerful and supports most SQL features,
but there are some edge cases where it fails to parse a query.
That''s why we contribute to it a lot, and bring the latest version of the parser to SQLPage as soon as it is released.
### JSON functions in MS SQL Server
SQLPage now supports the [`FOR JSON` syntax](https://learn.microsoft.com/en-us/sql/relational-databases/json/format-query-results-as-json-with-for-json-sql-server?view=sql-server-ver16&tabs=json-path) in MS SQL Server.
This unlocks a lot of new possibilities, that were previously only available in other databases.
This is particularly interesting to build complex menus with the `shell` component,
to build multiple-answer select inputs with the `form` component,
and to create JSON APIs.
### Other sql syntax enhancements
- SQLPage now supports the custom `CONVERT` expression syntax for MS SQL Server, and the one for MySQL.
- The `VARCHAR(MAX)` type in MS SQL Server new works. We now use it for all variables bound as parameters to your SQL queries (we used to use `VARCHAR(8000)` before).
- `INSERT INTO ... DEFAULT VALUES ...` is now parsed correctly.
## Other news
- Dates and timestamps returned from the database are now always formatted in ISO 8601 format, which is the standard format for dates in JSON. This makes it easier to use dates in SQLPage.
- The `cookie` component now supports setting an explicit expiration date for cookies.
- The `cookie` component now supports setting the `SameSite` attribute of cookies, and defaults to `SameSite=Strict` for all cookies. What this means in practice is that cookies set by SQLPage will not be sent to your website if the user is coming from another website. This prevents someone from tricking your users into executing SQLPage queries on your website by sending them a malicious link.
- Bugfix: setting `min` or `max` to `0` in a number field in the `form` component now works as expected.
- Added support for `.env` files to set SQLPage''s [environment variables](./configuration.md#environment-variables).
- Better responsive design in the card component. Up to 5 cards per line on large screens. The number of cards per line is still customizable with the `columns` attribute.
- [New icons](https://tabler-icons.io/changelog):
- ![new icons in tabler 42](https://github.com/tabler/tabler-icons/assets/1282324/00856af9-841d-4aa9-995d-121c7ddcc005)
');
@@ -0,0 +1,30 @@
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'protocol',
'0.17.1',
'network',
'Returns the protocol that was used to access the current page.
This can be either `http` or `https`.
This is useful to generate links to the current page.
### Example
```sql
select ''text'' as component,
sqlpage.protocol() || ''://'' || sqlpage.header(''host'') || sqlpage.path() as contents;
```
will return `https://example.com/example.sql`.
> Note that the path is URL-encoded. The protocol is resolved in this order:
> - `Forwarded` header
> - `X-Forwarded-Proto` header
> request target / URI
');
@@ -0,0 +1,128 @@
INSERT INTO component (name, description, icon, introduced_in_version)
VALUES (
'tracking',
'Component for visualising activity logs or other monitoring-related data.',
'timeline-event-text',
'0.18.0'
);
INSERT INTO parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES (
'tracking',
'title',
'Title of the tracking component.',
'TEXT',
TRUE,
FALSE
),
(
'tracking',
'information',
'A short text displayed below the title.',
'TEXT',
TRUE,
TRUE
),
(
'tracking',
'description',
'A short paragraph.',
'TEXT',
TRUE,
TRUE
),
(
'tracking',
'description_md',
'A short paragraph formatted using markdown.',
'TEXT',
TRUE,
TRUE
),
(
'tracking',
'width',
'Width of the component, between 1 and 12.',
'INTEGER',
TRUE,
TRUE
),
(
'tracking',
'placement',
'Position of the tooltip (e.g. top, bottom, right, left)',
'TEXT',
TRUE,
TRUE
),
(
'tracking',
'color',
'Color of the tracked item (e.g. success, warning, danger)',
'TEXT',
FALSE,
TRUE
),
(
'tracking',
'title',
'Description of the state.',
'TEXT',
FALSE,
FALSE
),
(
'tracking',
'center',
'Whether to center the component.',
'BOOLEAN',
TRUE,
TRUE
);
-- Insert example(s) for the component
INSERT INTO example(component, description, properties)
VALUES
(
'tracking',
'A basic example of servers tracking component',
JSON(
'[
{"component":"tracking","title":"Servers status"},
{"title":"No data"},
{"title":"No data"},
{"title":"No data"},
{"title":"No data"},
{"title":"No data"},
{"title":"No data"},
{"title":"No data"},
{"title":"No data"}
]'
)
),
(
'tracking',
'An example of servers tracking component',
JSON(
'[
{"component":"tracking","title":"Servers status","information":"60% are running","description_md":"Status of all **currently running servers**","placement":"top","width":4},
{"color":"success","title":"operational"},
{"color":"success","title":"operational"},
{"color":"success","title":"operational"},
{"color":"danger","title":"Downtime"},
{"title":"No data"},
{"color":"success","title":"operational"},
{"color":"warning","title":"Big load"},
{"color":"success","title":"operational"}
]'
)
);
@@ -0,0 +1,152 @@
INSERT INTO component (name, description, icon, introduced_in_version)
VALUES (
'divider',
'Dividers help organize content and make the interface layout clear and uncluttered.',
'separator',
'0.18.0'
);
INSERT INTO parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES (
'divider',
'contents',
'A text in the divider.',
'TEXT',
TRUE,
TRUE
),
(
'divider',
'position',
'Position of the text (e.g. left, right).',
'TEXT',
TRUE,
TRUE
),
(
'divider',
'color',
'The name of a color for this span of text.',
'COLOR',
TRUE,
TRUE
),
(
'divider',
'size',
'The size of the divider text, from 1 to 6.',
'INTEGER',
TRUE,
TRUE
),
(
'divider',
'bold',
'Whether the text is bold.',
'BOOLEAN',
TRUE,
TRUE
),
(
'divider',
'italics',
'Whether the text is italicized.',
'BOOLEAN',
TRUE,
TRUE
),
(
'divider',
'underline',
'Whether the text is underlined.',
'BOOLEAN',
TRUE,
TRUE
),
(
'divider',
'link',
'URL of the link for the divider text. Available only when contents is present.',
'URL',
TRUE,
TRUE
);
-- Insert example(s) for the component
INSERT INTO example(component, description, properties)
VALUES
(
'divider',
'An empty divider',
JSON(
'[
{
"component":"divider"
}
]'
)
),
(
'divider',
'A divider with centered text',
JSON(
'[
{
"component":"divider",
"contents":"Hello"
}
]'
)
),
(
'divider',
'A divider with text at left',
JSON(
'[
{
"component":"divider",
"contents":"Hello",
"position":"left"
}
]'
)
),
(
'divider',
'A divider with blue text and a link',
JSON(
'[
{
"component":"divider",
"contents":"SQLPage components",
"link":"/documentation.sql",
"color":"blue"
}
]'
)
),
(
'divider',
'A divider with bold, italic, and underlined text',
JSON(
'[
{
"component":"divider",
"contents":"Important notice",
"position":"left",
"color":"red",
"size":5,
"bold":true,
"italics":true,
"underline":true
}
]'
)
);
@@ -0,0 +1,76 @@
INSERT INTO component (name, description, icon, introduced_in_version)
VALUES (
'breadcrumb',
'A secondary navigation aid that helps users understand their location on a website or mobile application.',
'dots',
'0.18.0'
);
INSERT INTO parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES (
'breadcrumb',
'title',
'Hyperlink text to display.',
'TEXT',
FALSE,
FALSE
),
(
'breadcrumb',
'link',
'Link to the page to display when the link is clicked. By default, the link refers to the current page, with a ''link'' parameter set to the link''s title.',
'TEXT',
FALSE,
TRUE
),
(
'breadcrumb',
'active',
'Whether the link is active or not. Defaults to false.',
'TEXT',
FALSE,
TRUE
),
(
'breadcrumb',
'description',
'Description of the link. This is displayed when the user hovers over the link.',
'TEXT',
FALSE,
TRUE
);
-- Insert example(s) for the component
INSERT INTO example(component, description, properties)
VALUES
(
'breadcrumb',
'Basic usage of the breadcrumb component',
JSON(
'[
{"component":"breadcrumb"},
{"title":"Home","link":"/"},
{"title":"Components", "link":"/documentation.sql"},
{"title":"Breadcrumb", "link":"?component=breadcrumb"}
]'
)
),
(
'breadcrumb',
'Description of a link and selection of the current page.',
JSON(
'[
{"component":"breadcrumb"},
{"title":"Home","link":"/","active": true},
{"title":"Articles","link":"/blog.sql","description":"Stay informed with the latest news"},
{"title":"JSON in SQL","link":"/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide", "description": "Learn advanced json functions for MySQL, SQLite, PostgreSQL, and SQL Server" }
]'
)
);
@@ -0,0 +1,80 @@
DELETE FROM component WHERE name = 'card';
INSERT INTO component(name, icon, description) VALUES
('card', 'credit-card', 'A grid where each element is a small card that displays a piece of data.');
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'card', * FROM (VALUES
-- top level
('title', 'Text header at the top of the list of cards.', 'TEXT', TRUE, TRUE),
('description', 'A short paragraph displayed below the title.', 'TEXT', TRUE, TRUE),
('description_md', 'A short paragraph displayed below the title - formatted using markdown.', 'TEXT', TRUE, TRUE),
('columns', 'The number of columns in the grid of cards. This is just a hint, the grid will adjust dynamically to the user''s screen size, rendering fewer columns if needed to fit the contents. To control the size of cards individually, use the `width` row-level property instead.', 'INTEGER', TRUE, TRUE),
-- item level
('title', 'Name of the card, displayed at the top.', 'TEXT', FALSE, FALSE),
('description', 'The body of the card, where you put the main text contents of the card.
This does not support rich text formatting, only plain text.
If you want to use rich text formatting, use the `description_md` property instead.', 'TEXT', FALSE, TRUE),
('description_md', '
The body of the card, in Markdown format.
This is useful if you want to display a lot of text in the card, with many options for formatting, such as
line breaks, **bold**, *italics*, lists, #titles, [links](target.sql), ![images](photo.jpg), etc.', 'TEXT', FALSE, TRUE),
('top_image', 'The URL (absolute or relative) of an image to display at the top of the card.', 'URL', FALSE, TRUE),
('top_image_lazy', 'Whether the top image must be lazily loaded. Defaults to false, meaning eagerly loaded.', 'BOOLEAN', FALSE, TRUE),
('top_image_width', 'Specify the top image width, in pixels. Helps prevent layout shifts.', 'INTEGER', FALSE, TRUE),
('top_image_height', 'Specify the top image height, in pixels. Helps prevent layout shifts.', 'INTEGER', FALSE, TRUE),
('footer', 'Muted text to display at the bottom of the card.', 'TEXT', FALSE, TRUE),
('footer_md', 'Muted text to display at the bottom of the card, with rich text formatting in Markdown format.', 'TEXT', FALSE, TRUE),
('link', 'An URL to which the user should be taken when they click on the card.', 'URL', FALSE, TRUE),
('footer_link', 'An URL to which the user should be taken when they click on the footer.', 'URL', FALSE, TRUE),
('style', 'Inline style property to your iframe embed code. For example "background-color: #FFFFFF"', 'TEXT', FALSE, TRUE),
('icon', 'Name of an icon to display on the right side of the card.', 'ICON', FALSE, TRUE),
('color', 'The name of a color, to be displayed on the left of the card to highlight it. If the embed parameter is enabled and you don''t have a title or description, this parameter won''t apply.', 'COLOR', FALSE, TRUE),
('background_color', 'The background color of the card.', 'COLOR', FALSE, TRUE),
('active', 'Whether this item in the grid is considered "active". Active items are displayed more prominently.', 'BOOLEAN', FALSE, TRUE),
('width', 'The width of the card, between 1 (smallest) and 12 (full-width). The default width is 3, resulting in 4 cards per line.', 'INTEGER', FALSE, TRUE)
) x;
INSERT INTO parameter(component, name, description_md, type, top_level, optional) SELECT 'card', * FROM (VALUES
('embed', 'A url whose contents will be fetched and injected into the body of this card.
This can be used to inject arbitrary html content, but is especially useful for injecting
the output of other sql files rendered by SQLPage. For the latter case you can pass the
`?_sqlpage_embed` query parameter, which will skip the shell layout', 'TEXT', FALSE, TRUE),
('embed_mode', 'Set to ''iframe'' to embed the target (specified through embed property) in an iframe.
Unless this is explicitly set, the embed target is fetched and injected within the parent page. If embed_mode is set to iframe,
You can also set height and width parameters to configure the appearance and the sandbox and allow parameters to configure
security aspects of the iframe. Refer to the [MDN page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe)
for an explanation of these parameters.', 'TEXT', FALSE, TRUE)
) x;
INSERT INTO example(component, description, properties) VALUES
('card', 'A beautiful card grid with bells and whistles, showing examples of SQLPage features.',
json('[{"component":"card", "title":"Popular SQLPage features", "columns": 2},
{"title": "Download as spreadsheet", "link": "?component=csv#component", "description": "Using the CSV component, you can download your data as a spreadsheet.", "icon":"file-plus", "color": "green", "footer_md": "SQLPage can both [read](?component=form#component) and [write](?component=csv#component) **CSV** files."},
{"title": "Custom components", "link": "/custom_components.sql", "description": "If you know some HTML, you can create your own components for your application.", "icon":"code", "color": "orange", "footer_md": "You can look at the [source of the official components](https://github.com/sqlpage/SQLPage/tree/main/sqlpage/templates) for inspiration."}
]')),
('card', 'You can use cards to display a dashboard with quick access to important information. Use [markdown](https://www.markdownguide.org/basic-syntax) to format the text.',
json('[
{"component": "card", "columns": 4},
{"description_md": "**152** sales today", "active": true, "icon": "currency-euro"},
{"description_md": "**13** new users", "icon": "user-plus", "color": "green"},
{"description_md": "**2** complaints", "icon": "alert-circle", "color": "danger", "link": "?view_complaints", "background_color": "red-lt"},
{"description_md": "**1** pending support request", "icon": "mail-question", "color": "warning"}
]')),
('card', 'A gallery of images.',
json('[
{"component":"card", "title":"My favorite animals in pictures", "columns": 3},
{"title": "Lynx", "description_md": "The **lynx** is a medium-sized **wild cat** native to Northern, Central and Eastern Europe to Central Asia and Siberia, the Tibetan Plateau and the Himalayas.", "top_image": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Lynx_lynx_-_05.jpg/330px-Lynx_lynx_-_05.jpg", "top_image_width": 330, "top_image_height": 495, "top_image_lazy": true, "icon":"star" },
{"title": "Squirrel", "description_md": "The **chipmunk** is a small, striped rodent of the family Sciuridae. Chipmunks are found in North America, with the exception of the Siberian chipmunk which is found primarily in Asia.", "top_image": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/4a/American_squirrel_eating_nut%2C_13_Jun_2013.JPG/330px-American_squirrel_eating_nut%2C_13_Jun_2013.JPG", "top_image_width": 330, "top_image_height": 495, "top_image_lazy": true },
{"title": "Spider", "description_md": "The **jumping spider family** (_Salticidae_) contains more than 600 described genera and about *6000 described species*, making it the largest family of spiders with about 13% of all species.", "top_image": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Australian_orb_weaver_spinning_web.jpg/330px-Australian_orb_weaver_spinning_web.jpg", "top_image_width": 330, "top_image_height": 495, "top_image_lazy": true }
]')),
('card', 'Beautifully colored cards with variable width. The blue card (width 6) takes half the screen, whereas of the red and green cards have the default width of 3',
json('[
{"component":"card", "title":"Beautifully colored cards" },
{"title": "Red card", "color": "red", "background_color": "red-lt", "description": "Penalty! You are out!", "icon":"play-football" },
{"title": "Blue card", "color": "blue", "width": 6, "background_color": "blue-lt", "description": "The Blue Card facilitates migration of foreigners to Europe.", "icon":"currency-euro" },
{"title": "Green card", "color": "green", "background_color": "green-lt", "description": "Welcome to the United States of America !", "icon":"user-dollar" }
]')),
('card', 'Cards with remote content',
json('[
{"component":"card", "title":"Card with embedded remote content", "columns": 2},
{"title": "Embedded Chart", "embed": "/examples/chart.sql?_sqlpage_embed" },
{"title": "Embedded Video", "embed": "https://www.youtube.com/embed/mXdgmSdaXkg", "allow": "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", "embed_mode": "iframe", "height": "350" }
]'));
@@ -0,0 +1,44 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'New SQLPage, and a talk at PGConf.eu',
'SQLPage v0.18.0 is out, and there is detailed introduction to SQLPage on youtube',
'brand-youtube',
'2024-01-29',
'
[SQLPage](https://sql-page.com) is a small web server that renders your SQL queries as beautiful interactive websites. This release has seen significant new features and fixes from new contributors, which is great and show the health of the project ! If you feel something is missing or isn''t working quite right, all your contributions are always welcome.
On a side note, I [gave a talk about SQLPage last December at PGConf.eu](https://www.youtube.com/watch?v=mXdgmSdaXkg).
It is a great detailed introduction to SQLPage, and I recommend it if you want to learn more about the project.
1. **New `tracking` component for beautiful and compact status reports:** This feature adds a new way to display status reports, making them more visually appealing and concise.
1. ![screenshot](https://github.com/sqlpage/SQLPage/assets/552629/3e792953-3870-469d-a01d-898316b2ab32)
3. **New `divider` component to add a horizontal line between other components:** This simple yet useful addition allows for better separation of elements on your pages.
1. ![image](https://github.com/sqlpage/SQLPage/assets/552629/09a2cc77-3b37-401f-ab3e-441637a2c022)
5. **New `breadcrumb` component to display a breadcrumb navigation bar:** This component helps users navigate through your website''s hierarchical structure, providing a clear path back to the homepage.
1. ![image](https://github.com/sqlpage/SQLPage/assets/552629/cbf2174a-1d75-499e-9d6b-e111136dbbbc)
8. **Multi-column layouts with `embed` attribute in `card` component:** This feature enables you to create more complex and dynamic layouts within cards.
1. ![image](https://github.com/sqlpage/SQLPage/assets/552629/3f4435f0-d89b-424e-8b8a-39385a61d5ad)
6. **Customizable y-axis step size in `chart` component with `ystep` attribute:** This feature gives you more control over the chart''s appearance, especially for situations with multiple series.
7. **Updated default graph colors for better distinction:** This enhancement ensures clarity and easy identification of different data series.
10. **ID and class attributes for all components for easier styling and referencing:** This improvement simplifies custom CSS customization and inter-page element linking.
11. **Implementation of `uploaded_file_mime_type` function:** This function allows you to determine the MIME type of a uploaded file.
12. **Upgraded built-in SQLite database to version 3.45.0:** This ensures compatibility with recent SQLite features and bug fixes. See [sqlite release notes](https://www.sqlite.org/releaselog/3_45_0.html)
13. **Unicode support for built-in SQLite database:** This enables case-insensitive string comparisons and lower/upper case transformations.
5. **Improved `card` component with smaller margin below footer text:** This fix ensures consistent and visually balanced card layouts.
'
);
@@ -0,0 +1,160 @@
INSERT INTO component (name, description, icon, introduced_in_version)
VALUES (
'carousel',
'A carousel is used to display images. When used with multiple images, it will cycle through them automatically or with controls, creating a slideshow.',
'carousel-horizontal',
'0.18.3'
);
INSERT INTO parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES
(
'carousel',
'title',
'A name to display at the top of the carousel.',
'TEXT',
TRUE,
TRUE
),
(
'carousel',
'indicators',
'Style of image indicators (square or dot).',
'TEXT',
TRUE,
TRUE
),
(
'carousel',
'vertical',
'Whether to use the vertical image indicators.',
'BOOLEAN',
TRUE,
TRUE
),
(
'carousel',
'controls',
'Whether to show the control links to go previous or next item.',
'BOOLEAN',
TRUE,
TRUE
),
(
'carousel',
'width',
'Width of the component, between 1 and 12. Default is 12.',
'INTEGER',
TRUE,
TRUE
),
(
'carousel',
'auto',
'Whether to automatically cycle through the carousel items. Default is false.',
'BOOLEAN',
TRUE,
TRUE
),
(
'carousel',
'center',
'Whether to center the carousel.',
'BOOLEAN',
TRUE,
TRUE
),
(
'carousel',
'fade',
'Whether to apply the fading effect.',
'BOOLEAN',
TRUE,
TRUE
),
(
'carousel',
'delay',
'Specify the delay, in milliseconds, between two images.',
'INTEGER',
TRUE,
TRUE
),
(
'carousel',
'image',
'The URL (absolute or relative) of an image to display in the carousel.',
'URL',
FALSE,
FALSE
),
(
'carousel',
'title',
'Add caption to the slide.',
'TEXT',
FALSE,
TRUE
),
(
'carousel',
'description',
'A short paragraph.',
'TEXT',
FALSE,
TRUE
),
(
'carousel',
'description_md',
'A short paragraph formatted using markdown.',
'TEXT',
FALSE,
TRUE
),
(
'carousel',
'width',
'The width of the image, in pixels.',
'INTEGER',
FALSE,
TRUE
),
(
'carousel',
'height',
'The height of the image, in pixels.',
'INTEGER',
FALSE,
TRUE
);
-- Insert example(s) for the component
INSERT INTO example(component, description, properties)
VALUES (
'carousel',
'A basic example of carousel',
JSON(
'[
{"component":"carousel","name":"cats1","title":"Famous Database Animals"},
{"image":"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Elefantes_africanos_de_sabana_%28Loxodonta_africana%29%2C_Elephant_Sands%2C_Botsuana%2C_2018-07-28%2C_DD_114-117_PAN.jpg/2560px-Elefantes_africanos_de_sabana_%28Loxodonta_africana%29%2C_Elephant_Sands%2C_Botsuana%2C_2018-07-28%2C_DD_114-117_PAN.jpg"},
{"image":"https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Penguin_Island_panorama_with_ferry_and_dolphins_in_foreground%2C_March_2023_06.jpg/1280px-Penguin_Island_panorama_with_ferry_and_dolphins_in_foreground%2C_March_2023_06.jpg"}
]'
)
),
(
'carousel',
'An advanced example of carousel with controls',
JSON(
'[
{"component":"carousel","title":"SQL web apps","width":6, "center":true,"controls":true,"auto":true},
{"image":"/sqlpage_cover_image.webp","title":"SQLPage is modern","description":"Built by engineers who have built so many web applications the old way, they decided they just wouldn''t anymore.", "height": 512},
{"image":"/sqlpage_illustration_alien.webp","title":"SQLPage is easy", "description":"SQLPage connects to your database, then it turns your SQL queries into nice websites.", "height": 512}
]'
)
);
@@ -0,0 +1,115 @@
-- Documentation for the title component
INSERT INTO component (name, description, icon, introduced_in_version) VALUES (
'title',
'Defines HTML headings. The level 1 is used for the maximal size and the level 6 is used for the minimal size.',
'letter-case-upper',
'0.19.0'
);
INSERT INTO parameter (component,name,description,type,top_level,optional) VALUES (
'title',
'center',
'Whether to center the title.',
'BOOLEAN',
TRUE,
TRUE
),(
'title',
'contents',
'A text to display.',
'TEXT',
TRUE,
FALSE
),(
'title',
'level',
'Set the heading level (default level is 1)',
'INTEGER',
TRUE,
TRUE
);
-- Insert example(s) for the component
INSERT INTO example(component, description, properties) VALUES (
'title',
'Displays several titles with different levels.',
JSON(
'[
{"component":"title","contents":"Level 1"},
{"component":"title","contents":"Level 2","level": 2},
{"component":"title","contents":"Level 3","level": 3}
]'
)
);
INSERT INTO example(component, description, properties) VALUES (
'title',
'Displays a centered title.',
JSON(
'[
{"component":"title","contents":"Level 1","center": true}
]'
)
);
-- Documentation for the code component
INSERT INTO component (name, description, icon, introduced_in_version) VALUES (
'code',
'Displays one or many blocks of code from a programming language or formated text as XML or JSON.',
'code',
'0.19.0'
);
INSERT INTO parameter (component,name,description,type,top_level,optional) VALUES (
'code',
'title',
'Set the heading level (default level is 1)',
'TEXT',
FALSE,
TRUE
),(
'code',
'contents',
'A block of code.',
'TEXT',
FALSE,
FALSE
),(
'code',
'description',
'Description of the snipet of code.',
'TEXT',
FALSE,
TRUE
),(
'code',
'description_md',
'Rich text in the markdown format. Among others, this allows you to write bold text using **bold**, italics using *italics*, and links using [text](https://example.com).',
'TEXT',
FALSE,
TRUE
),(
'code',
'language',
'Set the programming language name.',
'TEXT',
FALSE,
TRUE
);
-- Insert example(s) for the component
INSERT INTO example(component, description, properties) VALUES (
'code',
'Displays a block of HTML code.',
JSON(
'[
{"component":"code"},
{
"title":"A HTML5 example",
"language":"html",
"description":"Heres the very minimum that an HTML document should contain, assuming it has CSS and JavaScript linked to it.",
"contents":"<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<title>title</title>\n\t\t<link rel=\"stylesheet\" href=\"style.css\">\n\t\t<script src=\"script.js\"></script>\n\t</head>\n\t<body>\n\t\t<!-- page content -->\n\t</body>\n</html>"
}
]'
)
);
@@ -0,0 +1,273 @@
-- Documentation for the RSS component
INSERT INTO component (name, description, icon, introduced_in_version) VALUES (
'rss',
'Produces a data flow in the RSS format.
Can be used to generate a podcast feed.
To use this component, you must first return an HTTP header with the "application/rss+xml" content type (see http_header component). Next, you must use the shell-empty component to avoid that SQLPage generates HTML code.',
'rss',
'0.20.0'
);
INSERT INTO parameter (component,name,description,type,top_level,optional) VALUES (
'rss',
'title',
'Defines the title of the channel.',
'TEXT',
TRUE,
FALSE
),(
'rss',
'link',
'Defines the hyperlink to the channel.',
'URL',
TRUE,
FALSE
),(
'rss',
'description',
'Describes the channel.',
'TEXT',
TRUE,
FALSE
),(
'rss',
'language',
'Defines the language of the channel, specified in the ISO 639 format. For example, "en" for English, "fr" for French.',
'TEXT',
TRUE,
TRUE
),(
'rss',
'category',
'Defines the category of the channel. The value should be a string representing the category (e.g., "News", "Technology", etc.).',
'TEXT',
TRUE,
TRUE
),(
'rss',
'explicit',
'Indicates whether the channel contains explicit content. The value can be either TRUE or FALSE.',
'BOOLEAN',
TRUE,
TRUE
),(
'rss',
'image_url',
'Provides a URL linking to the artwork for the channel.',
'URL',
TRUE,
TRUE
),(
'rss',
'author',
'Defines the group, person, or people responsible for creating the channel.',
'TEXT',
TRUE,
TRUE
),(
'rss',
'copyright',
'Provides the copyright details for the channel.',
'TEXT',
TRUE,
TRUE
),(
'rss',
'self_link',
'URL of the RSS feed.',
'URL',
TRUE,
TRUE
),(
'rss',
'funding_url',
'Specifies the donation/funding links for the channel. The content of the tag is the recommended string to be used with the link.',
'URL',
TRUE,
TRUE
),(
'rss',
'type',
'Specifies the channel as either episodic or serial. The value can be either "episodic" or "serial".',
'TEXT',
TRUE,
TRUE
),(
'rss',
'complete',
'Specifies that a channel is complete and will not post any more items in the future.',
'BOOLEAN',
TRUE,
TRUE
),(
'rss',
'locked',
'Tells podcast hosting platforms whether they are allowed to import this feed.',
'BOOLEAN',
TRUE,
TRUE
),(
'rss',
'guid',
'The globally unique identifier (GUID) for a channel. The value is a UUIDv5.',
'TEXT',
TRUE,
TRUE
),(
'rss',
'title',
'Defines the title of the feed item (episode name, blog post title, etc.).',
'TEXT',
FALSE,
FALSE
),(
'rss',
'link',
'Defines the hyperlink to the item (blog post URL, etc.).',
'URL',
FALSE,
FALSE
),(
'rss',
'description',
'Describes the item',
'TEXT',
FALSE,
FALSE
),(
'rss',
'date',
'Indicates when the item was published (RFC-822 date-time).',
'TEXT',
FALSE,
TRUE
),(
'rss',
'enclosure_url',
'For podcast episodes, provides a URL linking to the audio/video episode content, in mp3, m4a, m4v, or mp4 format.',
'URL',
FALSE,
TRUE
),(
'rss',
'enclosure_length',
'The length in bytes of the audio/video episode content.',
'INTEGER',
FALSE,
TRUE
),(
'rss',
'enclosure_type',
'The MIME media type of the audio/video episode content (e.g., "audio/mpeg", "audio/m4a", "video/m4v", "video/mp4").',
'TEXT',
FALSE,
TRUE
),(
'rss',
'guid',
'The globally unique identifier (GUID) for an item.',
'TEXT',
FALSE,
TRUE
),(
'rss',
'episode',
'The chronological number that is associated with an item.',
'INTEGER',
FALSE,
TRUE
),(
'rss',
'season',
'The chronological number associated with an item''s season.',
'INTEGER',
FALSE,
TRUE
),(
'rss',
'episode_type',
'Defines the type of content for a specific item. The value can be either "full", "trailer", or "bonus".',
'TEXT',
FALSE,
TRUE
),(
'rss',
'block',
'Prevents a specific item from appearing in podcast listening applications.',
'BOOLEAN',
FALSE,
TRUE
),(
'rss',
'explicit',
'Indicates whether the item contains explicit content. The value can be either TRUE or FALSE.',
'BOOLEAN',
FALSE,
TRUE
),(
'rss',
'image_url',
'Provides a URL linking to the artwork for the item.',
'URL',
FALSE,
TRUE
),(
'rss',
'duration',
'The duration of an item in seconds.',
'INTEGER',
FALSE,
TRUE
),(
'rss',
'transcript_url',
'A link to a transcript or closed captions file for the item.',
'URL',
FALSE,
TRUE
),(
'rss',
'transcript_type',
'The type of the transcript or closed captions file for the item (e.g., "text/plain", "text/html", "text/vtt", "application/json", "application/x-subrip").',
'TEXT',
FALSE,
TRUE
);
-- Insert example(s) for the component
INSERT INTO example (component, description)
VALUES (
'rss',
'
### An RSS channel about SQLPage latest news.
```sql
select ''http_header'' as component, ''application/rss+xml'' as content_type;
select ''shell-empty'' as component;
select
''rss'' as component,
''SQLPage blog'' as title,
''https://sql-page.com/blog.sql'' as link,
''latest news about SQLpage'' as description,
''en'' as language,
''Technology'' as category,
FALSE as explicit,
''https://sql-page.com/favicon.ico'' as image_url,
''Ophir Lojkine'' as author,
''https://github.com/sponsors/lovasoa'' as funding_url,
''episodic'' as type;
select
''Hello everyone !'' as title,
''https://sql-page.com/blog.sql?post=Come%20see%20me%20build%20twitter%20live%20on%20stage%20in%20Prague'' as link,
''If some of you european SQLPagers are around Prague this december, I will be giving a talk about SQLPage at pgconf.eu on December 14th.'' as description,
''http://127.0.0.1:8080/sqlpage_introduction_video.webm'' as enclosure_url,
123456789 as enclosure_length,
''video/webm'' as enclosure_type,
''2023-12-04'' as date;
```
Once you have your rss feed ready, you can submit it to podcast directories like
[Apple Podcasts](https://podcastsconnect.apple.com/my-podcasts),
[Spotify](https://podcasters.spotify.com/),
[Google Podcasts](https://podcastsmanager.google.com/)...
');
@@ -0,0 +1,93 @@
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'run_sql',
'0.20.0',
'login',
'Executes another SQL file and returns its result as a JSON array.
### Example
#### Include a common header in all your pages
It is common to want to run the same SQL queries at the beginning of all your pages,
to check if an user is logged in, render a header, etc.
You can create a file called `common_header.sql`,
and use the [`dynamic`](documentation.sql?component=dynamic#component) component with the `run_sql` function
to include it in all your pages.
```sql
select ''dynamic'' as component, sqlpage.run_sql(''common_header.sql'') as properties;
```
#### Factorize logic between pages
Reuse a sqlpage query in multiple pages without duplicating code by storing the results of `run_sql` to variables:
##### `reusable.sql`
```sql
select some_field from some_table;
```
##### `index.sql`
```sql
-- save the value of some_field from the first result row of reusable.sql into $my_var
set my_var = sqlpage.run_sql(''reusable.sql'')->>0->>''some_field'';
```
See [json in SQL](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide)
for help with manipulating the json array returned by `run_sql`.
#### Notes
- **recursion**: you can use `run_sql` to include a file that itself includes another file, and so on. However, be careful to avoid infinite loops. SQLPage will throw an error if the inclusion depth is superior to `max_recursion_depth` (10 by default).
- **security**: be careful when using `run_sql` to include files.
- Never use `run_sql` with a user-provided parameter.
- Never run a file uploaded by a user, or a file that is not under your control.
- Remember that users can also run the files you include with `sqlpage.run_sql(...)` directly just by loading the file in the browser.
- Make sure this does not allow users to bypass security measures you put in place such as [access control](/component.sql?component=authentication).
- If you need to include a file, but make it inaccessible to users, you can use hidden files and folders (starting with a `.`), or put files in the special `sqlpage/` folder that is not accessible to users.
- **variables**: the included file will have access to the same variables (URL parameters, POST variables, etc.)
as the calling file.
If the included file changes the value of a variable or creates a new variable, the change will not be visible in the calling file.
### Parameters
You can pass parameters to the included file, as if it had been with a URL parameter.
For instance, you can use:
```sql
sqlpage.run_sql(''included_file.sql'', json_object(''param1'', ''value1'', ''param2'', ''value2''))
```
Which will make `$param1` and `$param2` available in the included file.
[More information about building JSON objects in SQL](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide).
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'run_sql',
1,
'file',
'Path to the SQL file to execute, can be absolute, or relative to the web root (the root folder of your website sql files).
In-database files, from the sqlpage_files(path, contents, last_modified) table are supported.',
'TEXT'
),(
'run_sql',
2,
'parameters',
'Optional JSON object to pass as parameters to the included SQL file. The keys of the object will be available as variables in the included file. By default, the included file will have access to the same variables as the calling file.',
'JSON'
);
@@ -0,0 +1,77 @@
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'persist_uploaded_file',
'0.20.1',
'device-floppy',
'Persists an uploaded file to the local filesystem, and returns its path.
If the file input field is empty, the function returns NULL.
### Example
#### User profile picture
##### `upload_form.sql`
```sql
select ''form'' as component, ''persist_uploaded_file.sql'' as action;
select ''file'' as type, ''profile_picture'' as name, ''Upload your profile picture'' as label;
```
##### `persist_uploaded_file.sql`
```sql
update user
set profile_picture = sqlpage.persist_uploaded_file(''profile_picture'', ''profile_pictures'', ''jpg,jpeg,png,gif,webp'')
where id = (
select user_id from session where session_id = sqlpage.cookie(''session_id'')
);
```
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'persist_uploaded_file',
1,
'file',
'Name of the form field containing the uploaded file. The current page must be referenced in the `action` property of a `form` component that contains a file input field.',
'TEXT'
),
(
'persist_uploaded_file',
2,
'destination_folder',
'Optional. Path to the folder where the file will be saved, relative to the web root (the root folder of your website files). By default, the file will be saved in the `uploads` folder.
**Security note**: this value must be a folder name you choose yourself in your SQL code (a trusted constant). Never build it from untrusted input such as a form field, a query parameter, a request header, or anything else the visitor controls. The folder is joined directly to the web root, so a value containing `..` or an absolute path would cause the uploaded file to be written *outside* the web root, anywhere the SQLPage process can write. Keeping `destination_folder` a fixed value chosen by the application author avoids this.',
'TEXT'
),
(
'persist_uploaded_file',
3,
'allowed_extensions',
'Optional. Comma-separated list of allowed file extensions. By default: jpg,jpeg,png,gif,bmp,webp,pdf,txt,doc,docx,xls,xlsx,csv,mp3,mp4,wav,avi,mov.
Changing this may be dangerous ! If you add "sql", "svg" or "html" to the list, an attacker could execute arbitrary SQL queries on your database, or impersonate other users.',
'TEXT'
),
(
'persist_uploaded_file',
4,
'mode',
'Optional. Unix permissions to set on the file, in octal notation. By default, the file will be saved with "600" (read/write for the owner only).
Octal notation works by using three digits from 0 to 7: the first for the owner, the second for the group, and the third for others.
For example, "644" means read/write for the owner, and read-only for others.
[Learn more about numeric notation for file-system permissions on Wikipedia](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation).',
'TEXT'
);
@@ -0,0 +1,128 @@
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'fetch',
'0.20.3',
'transfer-vertical',
'Sends an HTTP request and returns the results as a string.
### Example
#### Simple GET query
In this example, we use an API call to find the latitude and longitude of a place
the user searched for, and we display it on a map.
We use the simplest form of the fetch function, that takes the URL to fetch as a string.
```sql
set url = ''https://nominatim.openstreetmap.org/search?format=json&q='' || sqlpage.url_encode($user_search)
set api_results = sqlpage.fetch($url);
select ''map'' as component;
select $user_search as title,
CAST($api_results->>0->>''lat'' AS FLOAT) as latitude,
CAST($api_results->>0->>''lon'' AS FLOAT) as longitude;
```
#### POST query with a body
In this example, we use the complex form of the function to make an
authenticated POST request, with custom request headers and a custom request body.
We use SQLite''s json functions to build the request body.
See [the list of SQL databases and their JSON functions](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide) for
more information on how to build JSON objects in your database.
```sql
set request = json_object(
''method'', ''POST'',
''url'', ''https://postman-echo.com/post'',
''headers'', json_object(
''Content-Type'', ''application/json'',
''Authorization'', ''Bearer '' || sqlpage.environment_variable(''MY_API_TOKEN'')
),
''body'', json_object(
''Hello'', ''world''
)
);
set api_results = sqlpage.fetch($request);
select ''code'' as component;
select
''API call results'' as title,
''json'' as language,
$api_results as contents;
```
#### Authenticated request using Basic Auth
Here''s how to make a request to an API that requires [HTTP Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication):
```sql
set request = json_object(
''url'', ''https://api.example.com/data'',
''username'', ''my_username'',
''password'', ''my_password''
);
set api_results = sqlpage.fetch($request);
```
> This will add the `Authorization: Basic bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQK` header to the request,
> where `bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQK` is the base64 encoding of the string `my_username:my_password`.
# JSON parameter format
The fetch function accepts either a URL string, or a JSON object with the following parameters:
- `url`: The URL to fetch. Required.
- `method`: The HTTP method to use. Defaults to `GET`.
- `headers`: A JSON object with the headers to send. Defaults to sending a User-Agent header containing the SQLPage version.
- `body`: The body of the request. If it is a JSON object, it will be sent as JSON. If it is a string, it will be sent as is. When omitted, no request body is sent.
- `timeout_ms`: The maximum time to wait for the request, in milliseconds. Defaults to 5000.
- `username`: Optional username for HTTP Basic Authentication. Introduced in version 0.33.0.
- `password`: Optional password for HTTP Basic Authentication. Only used if username is provided. Introduced in version 0.33.0.
- `response_encoding`: Optional charset to use for decoding the response body. Defaults to `utf8`, or `base64` if the response contains binary data. All [standard web encodings](https://encoding.spec.whatwg.org/#concept-encoding-get) are supported, plus `hex`, `base64`, and `base64url`. Introduced in version 0.37.0.
# Error handling and reading response headers
If the request fails, this function throws an error, that will be displayed to the user.
The response headers are not available for inspection.
## Conditional data fetching
Since v0.40, `sqlpage.fetch(null)` returns null instead of throwing an error.
This makes it easier to conditionnally query an API:
```sql
set current_field_value = (select field from my_table where id = 1);
set target_url = nullif(''http://example.com/api/field/1'', null); -- null if the field is currently null in the db
set api_value = sqlpage.fetch($target_url); -- no http request made if the field is not null in the db
update my_table set field = $api_value where id = 1 and $api_value is not null; -- update the field only if it was not present before
```
## Advanced usage
If you need to handle errors or inspect the response headers or the status code,
use [`sqlpage.fetch_with_meta`](?function=fetch_with_meta).
'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'fetch',
1,
'url',
'Either a string containing an URL to request, or a json object in the standard format of the request interface of the web fetch API.',
'TEXT'
);
@@ -0,0 +1,37 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'SQLPage website update',
'Performance, security, and new features',
'browser',
'2024-05-01',
'
Today is may day, and we are happy to announce that the SQLPage website has been updated with new contents.
## Homepage
The [homepage](/) has been updated to include prominent information about commonly asked questions,
such as the [security guarantees](/safety.sql) of SQLPage, and the [performance](/performance.sql) of SQLPage applications.
## Performance of SQLPage applications
We now have a [detailled explanation of the performance of SQLPage applications](/performance.sql) on the website.
It explains why and how SQLPage applications are often faster than equivalent applications written in other frameworks.
## Single-Sign-On
Since SQLPage v0.20.3, SQLPage can natively make requests to external HTTP APIs with [the `fetch` function](/documentation.sql#fetch),
which opens the door to many new possibilities.
An example of this is the [**SSO demo**](https://github.com/sqlpage/SQLPage/tree/main/examples/single%20sign%20on),
which demonstrates how to use SQLPage to authenticate users on a website using a third-party authentication service,
such as Google, Facebook, an enterprise identity provider using [OIDC](https://openid.net/connect/),
or an academic institution using [CAS](https://apereo.github.io/cas/).
## New architecture diagram
The README of the SQLPage repository now includes a
[clear yet detailed architecture diagram](https://github.com/sqlpage/SQLPage?tab=readme-ov-file#how-it-works).
'
);
@@ -0,0 +1,30 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'Introduction video',
'A 30-minute live presentation of SQLPage, its raison d''être, and how to use it.',
'brand-youtube',
'2024-05-14',
'
# Introduction video
## Canadians love SQLPage
The Kitchener-Waterloo Linux User Group had the pleasure of [hosting a presentation](https://kwlug.org/node/1374)
by Anton Avramov, an avid SQLPage user and community member, who gave a live demonstration of SQLPage.
## The video
The user group kindly invited me (Ophir, the initial creator and main contributor to SQLPage)
to record a video introduction to SQLPage, which I did.
The video is a 5 minute introduction to the philosophy behind SQLPage,
followed by a 25 minute live demonstration of how to create
[this todo list application](https://github.com/sqlpage/SQLPage/tree/main/examples/todo%20application)
from scratch.
Watch it on youtube:
[![video cover](https://i.ytimg.com/vi/9NJgH_-zXjY/maxresdefault.jpg)](https://www.youtube.com/watch?v=9NJgH_-zXjY)
');
@@ -0,0 +1,34 @@
INSERT INTO
sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES
(
'request_method',
'0.21.0',
'http-get',
'Returns the HTTP request method (GET, POST, etc.) used to access the page.
# HTTP request methods
HTTP request methods (also known as verbs) are used to indicate the desired action to be performed on the identified resource. The most common methods are:
- **GET**: retrieve information from the server. This is the default method used by browsers when you click on a link.
- **POST**: submit data to be processed by the server. This is the default method used by browsers when you submit a form.
- **PUT**: replace the current representation of the target resource with the request payload. Most commonly used in REST APIs.
- **DELETE**: remove the target resource.
- **PATCH**, **HEAD**, **OPTIONS**, **CONNECT**, **TRACE**: less common methods that are used in specific situations.
# Example
```sql
select ''redirect'' as component,
''/error?msg=expected+a+PUT+request'' as link,
where sqlpage.request_method() != ''PUT'';
insert into my_table (column1, column2) values (:value1, :value2);
```
'
);
@@ -0,0 +1,18 @@
create table users (
username text primary key,
password_hash text not null,
role text not null
);
-- Create example users with trivial passwords for the website's demo
insert into users (username, password_hash, role)
values
('admin', '$argon2i$v=19$m=8,t=1,p=1$YWFhYWFhYWE$ROyXNhK0utkzTA', 'admin'), -- password: admin
('user', '$argon2i$v=19$m=8,t=1,p=1$YWFhYWFhYWE$qsrWdjgl96ooYw', 'user'); -- password: user
-- (the password hashes can be generated using the `sqlpage.hash_password` function)
create table user_sessions (
session_token text primary key,
username text not null references users(username),
created_at timestamp not null default current_timestamp
);
@@ -0,0 +1,186 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'How archaeology is gradually entering the era of free software',
'A team of french archaeologists is working on the first all-digital excavation site, using SQLPage',
'skull',
'2024-07-02',
'
> This is the english translation of an article [originally published in French on linuxfr.org](https://linuxfr.org/news/comment-l-archeologie-entre-progressivement-dans-l-ere-du-logiciel-libre).
> It illustrates how SQLPage is used by non-developers
# How archaeology is gradually entering the era of free software
Archaeology has, since its beginnings, focused on cataloging, structuring and archiving data from excavations. In the field, it has long relied on creating forms, manually collecting information on paper, and hand drawing, transcribed during study phases onto digital media. It is only recently that some archaeologists have launched the movement of "all-digital" excavation. I propose to tell here the story of the digitization of archaeology, which, as you will see, relies in part on free software.
# What is an excavation site?
French archaeology is divided into two main branches: preventive archaeology, which intervenes during construction projects, and programmed archaeology, conducted on sites chosen to address research issues. Supervised by the Regional Archaeological Services of the Ministry of Culture, these activities are carried out by different organizations: public and private operators for preventive archaeology, and associations, CNRS or universities for programmed archaeology. The latter often mobilizes volunteers, especially students, offering them complementary practical training.
For the archaeologist, excavation is a tool, not an end in itself. What the archaeologist seeks is information. In essence, it''s about understanding the history of a site, its evolution, its inhabitants through the elements they left behind, whether it''s the ruins of their habitats, their crafts or their burials. This is all the more important as excavation is a destructive act, since the archaeologist dismantles his subject of study as the excavation progresses.
To be exploited, archaeological information must be organized according to well-established principles. The first key concept is the sedimentary layer (*Stratigraphic Unit* - SU), which testifies to a human action or a natural phenomenon. The study of the arrangement of these layers reveals the chronology of the site, the succession of events that took place there. These layers can be grouped into archaeological *facts*: ditches, cellars, burials, are indeed groupings of layers that define a specific element. Finally, the objects found in these layers, or *artifacts*, are cataloged and identified by their layer of origin, providing crucial chronological and cultural indications.
![mastraits site](https://github.com/sqlpage/SQLPage/assets/552629/3dbdf81e-b9d3-4268-a8e3-99e568feb695)
*The excavation site of the Necropolis of Mastraits, in Noisy-le-Grand (93).*
The actions carried out by the archaeologist throughout the site are also recorded. Indeed, the archaeologist carries out surveys, digs trenches, but also takes many photos, or drawings of everything he discovers as the site progresses. The documentation produced can be plethoric, and cataloging is essential.
This descriptive information is complemented by **spatial information**, the plan of the uncovered remains being essential for the analysis and presentation of results. The study of this plan, associated with descriptive and chronological information, highlights the major evolutions of the site or specific details. Its realization is generally entrusted to a topographer in collaboration with archaeologists.
At the end of the field phase, a phase of analysis of the collected data is carried out. This so-called post-excavation phase allows for processing all the information collected, carrying out a complete description, conducting the studies necessary for understanding the site by calling on numerous specialists: ceramologists, anthropologists, archaeozoologists, lithicists, carpologists, anthracologists, paleometallurgy specialists, etc.
This post-excavation phase initially results in the production of an operation report, the most exhaustive account possible of the site and its evolution. These reports are submitted to the Ministry of Culture, which judges their quality. They are not intended to be widely disseminated, but are normally accessible to anyone who requests them from the concerned administration. They are an important working basis for the entire scientific community.
Based on this report, the publication of articles in specialized journals allows for presenting the results of the operation more widely, sometimes according to specific themes or issues.
# Practice of archaeology: example in preventive archaeology
The use of numerous paper listings is a constant. These listings allow keeping up-to-date records of data in the form of inventory tables of layers, facts, surveys, photos, etc. Specific recording sheets are also used in many specialties of archaeology, such as funerary anthropology.
In the field, the unearthed elements are still, for a very large majority, drawn by hand, on tracing or graph paper, whether it''s a plan of remains or the numerous stratigraphic section drawings. This of course requires significant time, especially in the case of complex remains.
The use of electronic tacheometers, then differential GPS, has made it possible to do without tape measures, or grid systems, when excavating sites. Topographers, specifically trained, then began to intervene on site for the realization of general plans.
The documentary collection obtained at the end of an excavation is particularly precious. These are the only elements that will allow reconstructing the history of the site, by crossing these data with the result of the studies carried out. The fear of the disappearance of this data, or its use by others due to a remarkable discovery, is a feeling often shared within the archaeological community. The archaeologist may feel like a custodian of this information, even expressing a feeling of possession that goes completely against the idea of shared and open science. The idea that opening up data is the best way to protect it is far from obvious.
![conservation sheet, illustrating manual coloring of found skeleton parts](https://github.com/sqlpage/SQLPage/assets/552629/ca9c0f99-a520-4f2b-9826-ae49a89f844b)
> *Conservation sheet, illustrating manual coloring of found skeleton parts*
![Example of a descriptive sheet of an archaeological layer](https://gitlab.com/projet-r-d-bddsarcheo/tutos/-/raw/main/illustrations_diverses/fiche_us.svg)
> *Example, among many others, of a blank descriptive sheet of an archaeological layer*
# The beginning of digitization
It is essentially after the field phase that digital tools have been tamed by archaeologists.
In post-excavation, paper documentation is still often a fundamental documentary basis for site analysis. The irruption of computing in the mid-80s led archaeologists to transcribe this data into digital form, to facilitate its analysis and presentation. Although the software has evolved, the process is practically the same today, with digitization of documentation in many formats.
Listings can be integrated into databases (most often proprietary such as MS Access, FileMaker or 4D) or spreadsheets. Many databases have been developed internally, locally, by archaeologists themselves. Only attributive, they have gradually networked and adapted to the medium, allowing consideration of use in the field, without this being widely deployed.
![Database](https://gitlab.com/projet-r-d-bddsarcheo/tutos/-/raw/main/illustrations_diverses/exemple_bdd_fmp.png)
> *Example of a database at the turn of the 2000s*
All documentation drawn in the field is to be redrawn cleanly on digital media, in vector drawing software, very often Adobe Illustrator, sometimes Inkscape.
Plan data, surveyed by the topographer, is carried out under Autocad and was exported in .dxf or .dwg before being cleaned up under Adobe Illustrator, which is also the case for drawings made in the field.
The artifacts are entrusted to specialists who describe them, draw them, make an inventory, most often in spreadsheets. Their drawings are again scanned and cleaned up digitally.
In hindsight, we find that digital tools are mainly used as tools for cleaning up information collected in the field. Many spreadsheets are thus the strict transcription of paper tables used by archaeologists, to which some totals, averages or medians will be added. Drawings made on paper are traced in vectorization software for better readability and the scientific added values are ultimately quite limited.
This results in relatively disparate digital documentation, with the use of many proprietary tools, closed formats, and a very strong separation between spatial information and descriptive (or attributive) information.
The progressive use of databases has, however, allowed for agglomerating certain data and gathering and relating information. University work has also helped to feed reflection on the structuring of archaeological data and to train many archaeologists, allowing for the adoption of more virtuous practices.
# The all-digital movement
Until now, going fully digital in the archaeological context seemed relatively utopian. It took new technologies to appear, portable and simple-to-use supports to be put in place, networks to develop, and archaeologists to seize these new tools.
The Ramen collective (Archaeological Research in Digital Recording Modeling) was born from the exchanges and experiences of various archaeologists from the National Institute of Preventive Archaeological Research (Inrap) who grouped around the realization of [the programmed excavation of the medieval necropolis of Noisy-Le-Grand](https://archeonec.hypotheses.org/), excavation managed by the Necropolis Archaeology Association and entrusted to the scientific direction of Cyrille Le Forestier (Inrap). This programmed excavation allowed launching an experiment on the complete dematerialization of archaeological data based on photogrammetry, GIS, and a spatial database.
## General principle
While the topographer still intervenes for taking reference points, the detailed survey of remains is ensured, for this experiment, by the systematic implementation of photogrammetry. This method allows, by taking multiple photos of an object or scene, to create an accurate 3D model, and therefore exploitable a posteriori by the archaeologist in post-excavation. Photogrammetry constitutes in Noisy the only survey tool, purely and simply replacing drawing on paper. Indeed, from this 3D point cloud, it is possible to extract multiple 2D supports and add geometry or additional information to the database: burial contours, representation of the skeleton in situ, profiles, measurements, altitudes, etc.
![Photogrammetric survey of a burial](https://gitlab.com/projet-r-d-bddsarcheo/tutos/-/raw/main/illustrations_diverses/photogrammetrie3.png)
[*Photogrammetric survey of a burial*](https://sketchfab.com/3d-models/973-5d7513dd1dc941228d4a4b7b984c7af7)
Data recording is ensured by the use of a relational and spatial database whose interface is accessible in QGIS, but also via a web interface directly in the field, without going through paper inventories or listings. The web interface was created using [SQLPage](https://sql-page.com/), a web server that uses an SQL-based language for creating the graphical interface, without having to go through more complex programming languages classically used for creating web applications, such as PHP.
Of course, this approach also continues in the laboratory during the site analysis stage.
## Free software and formats
But abandoning paper support requires us to question the durability of the files and the data they contain.
Indeed, in a complete dematerialization process, the memory of the site is no longer contained on hundreds of handwritten sheets, but in digital files of which we do not know a priori if we will be able to preserve them in the long term. The impossibility of accessing this data with other software than those originally used during their creation is equivalent to their destruction. Only standard formats can address this issue, and they are particularly used by free software. For photogrammetry, the [`.ply`](https://en.wikipedia.org/wiki/PLY_(file_format)) and [`.obj`](https://en.wikipedia.org/wiki/Wavefront_.obj_file) formats, which are implemented in many software, free and proprietary, were chosen. For attributive and spatial data, it is recorded in free relational databases (Spatialite and Postgis), and easily exportable in `.sql`, which is a standardized format recognized by many databases.
Unfortunately, free software remains little used in our archaeological daily life, and proprietary software is often very well established. Free software still suffers today from preconceptions and a bad image within the archaeological community, which finds it more complicated, less pretty, less effective, etc.
However, free software has made a major incursion with the arrival of the free Geographic Information System (GIS) [QGIS](https://www.qgis.org/en/site/), which allowed installing a GIS on all the agents'' workstations of the institute and considering it as an analysis tool at the scale of an archaeological site. Through support and the implementation of an adequate training plan, many archaeologists have been trained in the use of the software within the Institute.
QGIS has truly revolutionized our practices by allowing immediate interrogation of attributive data by spatial data (what is this remains I see on the plan?) or, conversely, locating remains by their attributive data (where is burial 525?). However, it is still very common to have on one side the attributive data in spreadsheets or proprietary databases, and spatial data in QGIS, with the interrogation of both relying on joins.
Of course, QGIS also allows data analysis, the creation of thematic or chronological plans, essential supports for our reflections. We can, from these elements, create the numerous figures of the operation report, without going through vector drawing software, in plan as in section (vertical representation of stratigraphy). It allows normalizing figures through the use of styles, and, through the use of the Atlas tool, creating complete catalogs, provided that the data is rigorously structured.
![spatial analysis](https://gitlab.com/projet-r-d-bddsarcheo/tutos/-/raw/main/illustrations_diverses/ex_plan_analyse.png?ref_type=heads)
> *Example of spatial analysis in Qgis of ceramic waste distribution on a Gallic site*
In the context of the experiment on the Mastraits necropolis, while Qgis is indeed one of the pillars of the system, a few proprietary software are still used.
The processing software used for photogrammetry is proprietary. The ultimate goal is to be able to use free software, MicMac, developed by IGN, being a possible candidate. However, it still lacks a fully intuitive interface for archaeologists to appropriate the tool autonomously.
Similarly, the exciting latest developments of the Inkscape project should encourage us to turn more towards this software and systematically use .svg. The use of Scribus for DTP should also be seriously considered.
Free software and its undeniable advantages are thus slowly taking place, mainly via QGIS, in the production chain of our archaeological data. We can only hope that this place will grow. The path still seems long, but the way is free...
## Badass, spatial and attributive united
The development of the Archaeological Database of Attributive and Spatial Data (Badass) aimed to integrate, within a single database, the attributive information provided by archaeologists and the spatial information collected by the topographer. It even involves gathering, within dedicated tables, attributive and spatial information, thus ensuring data integrity.
Its principle is based on the functioning of the operational chain in archaeology, namely the identification and recording by the archaeologist of the uncovered remains, followed by the three-dimensional survey carried out by the topographer. The latter has, in the database, specific tables in which he can pour the geometry and minimal attributive data (number, type). Triggers then feed the tables filled by archaeologists with geometry, according to their identifier and type.
The database is thus the unique repository of attributive and spatial information throughout the operation, from field to post-excavation.
The format of the database was originally SpatiaLite. But the mass of documentation produced by the Mastraits necropolis led us to port it to PostGIS. Many archaeological operations, however, only require a small SpatiaLite base, which also allows the archaeologist to have control over their data file. Only a few large sites may need a PostgreSQL solution, otherwise used for the ARchaeological VIsualization CATalogue (Caviar) which is intended to host spatial and attributive data produced at the institute.
Naturally, Badass has been coupled with a QGIS project already offering default styles, but also some queries or views commonly used during an archaeological study. A QGIS extension has been developed by several students to allow automatic generation of the project and database.
Here''s the translation into idiomatic American English, keeping the original formatting:
## Entering Badass: The Bad''Mobil
The question of the system''s portability remained. QGIS is a resource-intensive software with an interface ill-suited for small screens, which are preferred for their portability in the field (phones and tablets).
Choosing to use a SpatiaLite or PostGIS database allowed us to consider a web interface from the start, which could then be used on any device. Initially, we considered developing in PHP/HTML/CSS with an Apache web server. However, this required having a web server and programming an entire interface. There were also some infrastructure questions to address: where to host it, how to finance it, and who would manage it all?
It was on LinuxFR that one of the members of the collective discovered [SQLPage](https://sql-page.com/). This open-source software, developed by [lovasoa](https://linuxfr.org/users/lovasoa), provides a very simple web server and allows for the creation of a [CRUD](https://en.wikipedia.org/wiki/CRUD) application with an interface that only requires SQL development.
SQLPage is based on an executable file which, when launched on a computer, turns it into a web server. A configuration file allows you to define the location of the database to be queried, among other things. For each web page of the interface, you write a `.sql` file to define the data to fetch or modify in the database, and the interface components to display it (tables, forms, graphs...). The interface is accessed through a web browser. If the computer is on a network, its IP address allows remote access, with an address like `http://192.168.1.5:8080`, for example. Using a VPN allows us to use the mobile phone network, eliminating the need for setting up a local network with routers, antennas, etc.
![principle](https://gitlab.com/projet-r-d-bddsarcheo/tutos/-/raw/main/illustrations_diverses/sqlpage_badass.svg)
*General operating principle*
Thus, the installation of the entire system is very simple and relies only on a file structure to be deployed on the server: the database, and a directory containing the SQLPage binary and the files making up the web pages.
By relying on the documentation (and occasionally asking questions to the software''s author), we were able to develop a very comprehensive interface on our own that meets our needs in the field. Named Bad''Mobil, the web interface provides access to all the attribute data recorded by archaeologists and now allows, thanks to the constant development of SQLPage, **to visualize spatial data**. Documentation produced during the excavation can also be consulted if the files (photos, scanned drawings, etc.) are placed in the right location in the file structure. The pages mainly consist of creation or modification forms, as well as tables listing already recorded elements. The visualization of geometry allows for spatial orientation in the field, particularly in complex excavation sites, and interaction with attribute data.
[![The BadMobil interface, with SQLPage](https://github.com/sqlpage/SQLPage/assets/552629/b421eebd-1d7a-446a-90d4-f360300453d5)](https://gitlab.com/projet-r-d-bddsarcheo/tutos/-/raw/main/illustrations_diverses/interface_badmobil.webp?ref_type=heads)
*The BadMobil interface, with SQLPage*
# Use Cases and Concrete Benefits
## First Experience at Les Mastraits
The excavation of the [Les Mastraits Necropolis](https://www.inrap.fr/la-necropole-alto-medievale-des-mastraits-noisy-le-grand-15374) was the test site for these developments. The significant amount of data collected, as well as its status as a planned excavation, allows for this kind of experimentation with much less impact than in a preventive excavation where deadlines are particularly tight.
The implementation of the SQLPage interface has allowed for the complete digitization of attribute recording and proves to be very efficient. This is a major change in our practices and will save us an enormous amount of time during data processing.
This also allows for centralizing information, working with multiple people simultaneously without waiting for traditional recording binders to become available, and guiding archaeologists through the recording process, avoiding omissions and errors. Thanks to a simplified interface, data entry can be done very intuitively without the need for extensive training.
The homogeneity of the entered data is thus better, and the possibilities for querying are much greater.
## Future Prospects
Following the development of Badass and Bad''mobil at the Les Mastraits necropolis, it seemed possible to consider its deployment in the context of preventive archaeology. While the question of the network infrastructure necessary for the operation of this solution may arise (need for stable electricity supply on remote sites in the countryside, availability of tablets, network coverage...), the benefits in terms of data homogeneity and ease of entry are very significant. A few preventive archaeology sites have thus been able to test the system, mostly on small-scale sites, benefiting from the support of collective members.
Future developments will likely focus on integrating new forms or new monitoring tools. Currently, Badass allows for collecting observations common to all archaeological sites, as well as anthropological observations due to its use within the Les Mastraits necropolis.
We could consider integrating the many specialties of archaeology, but it''s likely that we would end up with a huge machine that could be complex to maintain. We therefore remain cautious on this subject.
# Conclusion
Gradually, the use of digital tools has become widespread in archaeological professions. After the word processors and spreadsheets of the 90s (often on Mac), the first vectorized drawings digitized in Adobe Illustrator, and databases in Filemaker, Access, or 4D, digital tools are now able to be used throughout the entire data acquisition chain.
The contribution of open-source software and formats is major for this new step.
QGIS has fundamentally revolutionized archaeological practice by offering GIS access to the greatest number, allowing for the connection and manipulation of attribute and spatial data. It has paved the way for new developments and the integration of technologies previously little used by archaeology (notably the use of relational and spatial databases in SQL format).
SQLpage has allowed us to offer archaeologists a complete and simple interface to access a networked database. While its development requires certain knowledge of SQL and website functioning, its deployment and maintenance are quite manageable.
SQLPage addresses a real need in the field. For archaeologists, it simplifies their practice while responding to the growing complexity in the face of the documentary mass to be processed, and the increasing qualitative demands of deliverables.
The combination of QGIS, spatial and relational databases, and a web interface perfectly adapted to the field now fills the observed lack of an effective and reliable archaeological recording tool at the operation level. As such, Badass associated with Bad''Mobil fully meets the expectations of archaeologists who have experimented with them.
While open-source software has, in recent years, begun a timid breakthrough among many archaeological operators (some have fully adopted them), reluctance remains, whether from users or sometimes from the IT departments of public administrations, who may prefer to opt for an all-in-one service with technical support.
But the persistence of proprietary software usage is not without posing real problems regarding the sustainability of archaeological data, and archaeologists are just beginning to discover the issue. Their attachment to their data -- although it sometimes goes against the principle of open science -- should, however, encourage them to opt for formats whose durability appears certain, thereby guaranteeing access to this data in the future, regardless of the software or operating system used, if they don''t want their work to fall into oblivion...
'
);
@@ -0,0 +1,97 @@
-- Documentation for the RSS component
INSERT INTO
component (name, description, icon, introduced_in_version)
VALUES
(
'html',
'Include raw HTML in the output. For advanced users only. Use this component to create complex layouts or to include external content.
Be very careful when using this component with user-generated content, as it can lead to security vulnerabilities.
Use this component only if you are familiar with the security implications of including raw HTML, and understand the risks of cross-site scripting (XSS) attacks.',
'html',
'0.25.0'
);
INSERT INTO
parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES
(
'html',
'html',
'Raw HTML content to include in the page. This will not be sanitized or escaped. If you include content from an untrusted source, your page will be vulnerable to cross-site scripting attacks.',
'TEXT',
TRUE,
TRUE
),
(
'html',
'html',
'Raw HTML content to include in the page. This will not be sanitized or escaped. If you include content from an untrusted source, your page will be vulnerable to cross-site scripting attacks.',
'TEXT',
FALSE,
TRUE
),
(
'html',
'text',
'Text content to include in the page. This will be sanitized and escaped. Use this property to include user-generated content that should not contain HTML tags.',
'TEXT',
FALSE,
TRUE
),
(
'html',
'post_html',
'Raw HTML content to include after the text content. This will not be sanitized or escaped. If you include content from an untrusted source, your page will be vulnerable to cross-site scripting attacks.',
'TEXT',
FALSE,
TRUE
);
-- Insert example(s) for the component
INSERT INTO
example (component, description, properties)
VALUES
(
'html',
'Include a simple HTML snippet. In this example, the HTML code is hardcoded in the SQL query, so it is safe. You should never include data that may be manipulated by a user in the HTML content.
',
JSON (
'[{
"component": "html",
"html": "<h1 class=\"text-blue\">This text is safe because it is <mark>hardcoded</mark>!</h1>"
}]'
)
),
(
'html',
'Include multiple html snippets as row-level parameters. Again, be careful what you include in the HTML content. If the data comes from a user, it can be manipulated to include malicious code.',
JSON (
'[{"component":"html", "html":"<div class=\"d-flex gap-3 mb-4\" style=\"height: 40px\">"},
{"html":"<div class=\"progress h-100\"><div class=\"progress-bar progress-bar-striped progress-bar-animated\" style=\"width: 10%\">10%</div></div>"},
{"html":"<div class=\"progress h-100\"><div class=\"progress-bar progress-bar-striped progress-bar-animated bg-danger\" style=\"width: 80%\">80%</div></div>"},
{"html":"</div>"}
]'
)
),
(
'html',
'In order to include user-generated content that should be sanitized, use the `text` property instead of `html`. The `text` property will display the text as-is, without interpreting any HTML tags.',
JSON (
'
[
{"component": "html"},
{
"html": "<p>The following will be sanitized: <mark>",
"text": "<script>alert(''Failed XSS attack!'')</script>",
"post_html": "</mark>. Phew! That was close!</p>"
}
]'
)
);
@@ -0,0 +1,77 @@
INSERT INTO
sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES
(
'link',
'0.25.0',
'link',
'Returns the URL of a SQLPage file with the given parameters.
### Example
Let''s say you have a database of products, and you want the main page (`index.sql`) to link to the page of each product (`product.sql`) with the product name as a parameter.
In `index.sql`, you can use the `link` function to generate the URL of the product page for each product.
```sql
select ''list'' as component;
select
name as title,
sqlpage.link(''product'', json_object(''product_name'', name)) as link
from products;
```
In `product.sql`, you can then use `$product_name` to get the name of the product from the URL parameter:
```sql
select ''hero'' as component, $product_name as title, product_info as description
from products
where name = $product_name;
```
> You could also have manually constructed the URL with `CONCAT(''product?product_name='', name)`,
> but using `sqlpage.link` is better because it ensures that the URL is properly encoded.
> `sqlpage.link` will work even if the product name contains special characters like `&`, while `CONCAT(...)` would break the URL.
### Parameters
- `file` (TEXT): The name of the SQLPage file to link to.
- `parameters` (JSON): The parameters to pass to the linked file.
- `fragment` (TEXT): An optional fragment (hash) to append to the URL. This is useful for linking to a specific section of a page. For instance if `product.sql` contains `select ''text'' as component, ''product_description'' as id;`, you can link to the product description section with `sqlpage.link(''product.sql'', json_object(''product_name'', name), ''product_description'')`.
'
);
INSERT INTO
sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES
(
'link',
1,
'file',
'The path of the SQLPage file to link to, relative to the current file.',
'TEXT'
),
(
'link',
2,
'parameters',
'A JSON object with the parameters to pass to the linked file.',
'JSON'
),
(
'link',
3,
'fragment',
'An optional fragment (hash) to append to the URL to link to a specific section of the target page.',
'TEXT'
);
@@ -0,0 +1,65 @@
-- Insert the status_code component into the component table
INSERT INTO
component (name, description, icon)
VALUES
(
'status_code',
'Sets the HTTP response code for the current page.
This is an advanced technical component.
You typically need it when building internet-facing APIs and websites,
but you may not need it for simple internal applications.
- Indicating operation results when using [SQLPage as an API](?component=json)
- `200`: *OK*, for successful operations
- `201`: *Created*, for successful record insertion
- `404`: *Not Found*, for missing resources
- `500`: *Internal Server Error*, for failed operations
- Handling data validation errors
- `400`: *Bad Request*, for invalid data
- Enforcing access controls
- `403`: *Forbidden*, for unauthorized access
- `401`: *Unauthorized*, for unauthenticated access
- Tracking system health
- `500`: *Internal Server Error*, for failed operations
For search engine optimization:
- Use `404` for deleted content to remove outdated URLs from search engines
- For redirection from one page to another, use
- `301` (moved permanently), or
- `302` (moved temporarily)
- Use `503` during maintenance',
'error-404'
);
-- Insert the parameters for the status_code component into the parameter table
INSERT INTO
parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES
(
'status_code',
'status',
'HTTP status code (e.g., 200 OK, 401 Unauthorized, 409 Conflict)',
'INTEGER',
TRUE,
FALSE
);
INSERT INTO example (component, description)
VALUES (
'status_code',
'
Set the HTTP status code to 404, indicating that the requested resource was not found.
Useful in combination with [`404.sql` files](/your-first-sql-website/custom_urls.sql):
```sql
SELECT ''status_code'' as component, 404 as status;
```
');
@@ -0,0 +1,66 @@
-- Big Number Component Documentation
-- Component Definition
INSERT INTO component(name, icon, description, introduced_in_version) VALUES
('big_number', 'chart-area', 'A component to display key metrics or statistics with optional description, change indicator, and progress bar. Useful in dashboards.', '0.28.0');
-- Inserting parameter information for the big_number component
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'big_number', * FROM (VALUES
-- Top-level parameters (for the whole big_number list)
('columns', 'The number of columns to display the big numbers in (default is one column per item).', 'INTEGER', TRUE, TRUE),
('id', 'An optional ID to be used as an anchor for links.', 'TEXT', TRUE, TRUE),
('class', 'An optional CSS class to be added to the component for custom styling', 'TEXT', TRUE, TRUE),
-- Item-level parameters (for each big number)
('title', 'The title or label for the big number.', 'TEXT', FALSE, TRUE),
('title_link', 'A link for the Big Number title. If set, the entire title becomes clickable.', 'TEXT', FALSE, TRUE),
('title_link_new_tab', 'If true, the title link will open in a new tab/window.', 'BOOLEAN', FALSE, TRUE),
('value_link', 'A link for the Big Number value. If set, the entire value becomes clickable.', 'TEXT', FALSE, TRUE),
('value_link_new_tab', 'If true, the value link will open in a new tab/window.', 'BOOLEAN', FALSE, TRUE),
('value', 'The main value to be displayed prominently.', 'TEXT', FALSE, FALSE),
('unit', 'The unit of measurement for the value.', 'TEXT', FALSE, TRUE),
('description', 'A description or additional context for the big number.', 'TEXT', FALSE, TRUE),
('change_percent', 'The percentage change in value (e.g., 7 for 7% increase, -8 for 8% decrease).', 'INTEGER', FALSE, TRUE),
('progress_percent', 'The value of the progress (0-100).', 'INTEGER', FALSE, TRUE),
('progress_color', 'The color of the progress bar (e.g., "primary", "success", "danger").', 'TEXT', FALSE, TRUE),
('dropdown_item', 'A list of JSON objects containing links. e.g. {"label":"This week", "link":"?days=7"}', 'JSON', FALSE, TRUE),
('color', 'The color of the card', 'COLOR', FALSE, TRUE)
) x;
INSERT INTO example(component, description, properties) VALUES
('big_number', 'Big numbers with change indicators and progress bars',
json('[
{"component":"big_number"},
{
"title":"Sales",
"value":75,
"unit":"%",
"title_link": "#sales_dashboard",
"title_link_new_tab": true,
"value_link": "#sales_details",
"value_link_new_tab": false,
"description":"Conversion rate",
"change_percent": 9,
"progress_percent": 75,
"progress_color": "blue"
},
{
"title":"Revenue",
"value":"4,300",
"unit":"$",
"description":"Year on year",
"change_percent": -8
}
]'));
INSERT INTO example(component, description, properties) VALUES
('big_number', 'Big numbers with dropdowns and customized layout',
json('[
{"component":"big_number", "columns":3, "id":"colorfull_dashboard"},
{"title":"Users", "value":"1,234", "color": "red", "title_link": "#users", "title_link_new_tab": false, "value_link": "#users_details", "value_link_new_tab": true },
{"title":"Orders", "value":56, "color": "green", "title_link": "#orders", "title_link_new_tab": true },
{"title":"Revenue", "value":"9,876", "unit": "€", "color": "blue", "change_percent": -7, "dropdown_item": [
{"label":"This week", "link":"?days=7&component=big_number#colorfull_dashboard"},
{"label":"This month", "link":"?days=30&component=big_number#colorfull_dashboard"},
{"label":"This quarter", "link":"?days=90&component=big_number#colorfull_dashboard"}
]}
]'));
@@ -0,0 +1,585 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'JSON in SQL: A Comprehensive Guide',
'A comprehensive guide to working with JSON data in SQLite, PostgreSQL, MySQL, and SQL Server.',
'braces',
'2024-09-03',
'
# JSON in SQL: A Comprehensive Guide
## Introduction
JSON (JavaScript Object Notation) is a popular data format for unstructured data. It allows storing composite data types, such as arrays and objects, in a single SQL value.
Many modern applications use JSON to store and exchange data. As a result, SQL databases have incorporated JSON support to allow developers to work with structured and semi-structured data within the same database.
This guide will cover JSON operations in SQLite, PostgreSQL, MySQL, and SQL Server, focusing on querying JSON data.
SQLPage uses JSON both to pass data to the database (when a SQLPage variable contains an array), and to pass data to components (when a component has a JSON parameter).
Thus, understanding how to work with JSON in SQL will allow you to fully leverage advanced SQLPage features.
JSON supports the following data types:
- **Objects**: A mapping between keys and values (`{ "key": "value" }`). Keys must be strings, and values can be of different types.
- **Arrays**: An ordered list of values enclosed in square brackets (`[ "value1", "value2" ]`). Values can be of different types.
- **Strings**: A sequence of characters enclosed in double quotes (`"Hello, World!"`).
- **Numbers**: An integer or floating-point number (`42`, `3.14`).
- **Boolean**: A true or false value (`true`, `false`).
- **Null**: A null value (`null`).
## Sample Table
We''ll use the following sample table for our examples:
```sql
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
birthday DATE,
group_name VARCHAR(50)
);
INSERT INTO users (id, name, birthday, group_name) VALUES
(1, ''Alice'', ''1990-01-15'', ''Admin''),
(2, ''Bob'', ''1985-05-22'', ''User''),
(3, ''Charlie'', ''1992-09-30'', ''User'');
```
## SQLite
SQLite provides increasingly better JSON support since version 3.38.0.
See [the list of JSON functions in SQLite](https://www.sqlite.org/json1.html) for more details.
### Creating a JSON object
We can use the standard `json_object()` function to create a JSON object from columns in a table:
```sql
SELECT json_object(''name'', name, ''birthday'', birthday) AS user_json
FROM users;
```
| user_json |
|-----------|
| `{"name":"Alice","birthday":"1990-01-15"}` |
| `{"name":"Bob","birthday":"1985-05-22"}` |
| `{"name":"Charlie","birthday":"1992-09-30"}` |
### Creating a JSON array
```sql
SELECT json_array(name, birthday, group_name) AS user_array
FROM users;
```
| user_array |
|------------|
| `["Alice","1990-01-15","Admin"]` |
| `["Bob","1985-05-22","User"]` |
| `["Charlie","1992-09-30","User"]` |
### Aggregating multiple values into a JSON array
```sql
SELECT json_group_array(name) AS names
FROM users;
```
| names |
|-------|
| `["Alice","Bob","Charlie"]` |
### Aggregating values into a JSON object
```sql
SELECT json_group_object(name, group_name) AS name_group_map
FROM users;
```
| name_group_map |
|-------------------|
| `{"Alice":"Admin", "Bob":"User", "Charlie":"User"}` |
### Iterating over a JSON array
SQLite provides the `json_each()` table-valued function to iterate over JSON arrays. This function returns one row for each element in the JSON array.
```sql
SELECT value FROM json_each(''["Alice", "Bob", "Charlie"]'');
```
| value |
|-------|
| Alice |
| Bob |
| Charlie |
The `json_each()` function returns a table with several columns. The most commonly used are:
- `key`: The array index (0-based) for elements of a JSON array
- `value`: The value of the current element
- `type`: The type of the current element (e.g., ''text'', ''integer'', ''real'', ''true'', ''false'', ''null'')
For more complex JSON structures, you can use the `json_tree()` function, which recursively walks through the entire JSON structure.
These iteration functions can be used to check if specific values exist in a JSON array.
Here''s a practical example:
Let''s say you have a form with a [multiple-choice dropdown](documentation.sql?component=form#component) that allows selecting multiple users.
Some users might already be selected, and their IDs are stored in a JSON array passed as an URL parameter called `$selected_ids`.
You can create this dropdown using the following query:
```sql
select json_group_array(json_object(
''label'', name,
''value'', id,
''selected'', id in (select value from json_each_text($selected_ids))
)) as options
from users;
```
This query will:
1. Create a dropdown option for each user
2. Use their name as the display label
3. Use their ID as the value
4. Mark the option as selected if the user''s ID exists in the $selected_ids array
### Combining two JSON objects
SQLite provides the `json_patch()` function to combine two JSON objects. This function takes two JSON objects as arguments and returns a new JSON object that is the result of merging the two input objects.
```sql
SELECT json_patch(''{"name": "Alice"}'', ''{"birthday": "1990-01-15"}'') AS user_json;
```
| user_json |
|-----------|
| {"name": "Alice", "birthday": "1990-01-15"} |
## PostgreSQL
PostgreSQL has extensive support for JSON, including the `jsonb` type, which offers better performance and more functionality than the `json` type.
See [the list of JSON functions in PostgreSQL](https://www.postgresql.org/docs/current/functions-json.html) for more details.
### Creating a JSON object
```sql
SELECT
jsonb_build_object(
''name'', name,
''birthday'', birthday
) AS user_json
FROM users;
```
| user_json |
|-----------|
| `{"name":"Alice","birthday":"1990-01-15"}` |
| `{"name":"Bob","birthday":"1985-05-22"}` |
| `{"name":"Charlie","birthday":"1992-09-30"}` |
### Creating a JSON array
```sql
SELECT
jsonb_build_array(
name, birthday, group_name
) AS user_array
FROM users;
```
| user_array |
|------------|
| `["Alice", "1990-01-15", "Admin"]` |
| `["Bob", "1985-05-22", "User"]` |
| `["Charlie", "1992-09-30", "User"]` |
### Aggregating multiple values into a JSON array
```sql
SELECT jsonb_agg(name) AS names FROM users;
```
| names |
|-------|
| `["Alice","Bob","Charlie"]` |
### Aggregating values into a JSON object
```sql
SELECT
jsonb_object_agg(
name, birthday
) AS name_birthday_map
FROM users;
```
| name_birthday_map |
|-------------------|
| `{"Alice":"1990-01-15","Bob":"1985-05-22","Charlie":"1992-09-30"}` |
### Iterating over a JSON array
```sql
SELECT name FROM jsonb_array_elements_text(''["Alice", "Bob", "Charlie"]''::jsonb) AS name;
```
| name |
|------|
| Alice |
| Bob |
| Charlie |
You can use this function to test whether a value is present in a JSON array. For instance, to create a
[multi-value select dropdown](documentation.sql?component=form#component) with pre-selected values, you can use the following query:
```sql
SELECT jsonb_agg(jsonb_build_object(
''label'', name,
''value'', id,
''selected'', id in (SELECT value FROM jsonb_array_elements_text($selected_ids::jsonb))
)) AS options
FROM users;
```
### Iterating over a JSON object
```sql
SELECT key, value
FROM jsonb_each_text(''{"name": "Alice", "birthday": "1990-01-15"}''::jsonb);
```
| key | value |
|-----|-------|
| name | Alice |
| birthday | 1990-01-15 |
### Querying JSON data
PostgreSQL allows you to query JSON data using the `->` and `->>` operators:
```sql
SELECT name, user_data->>''age'' AS age
FROM (
SELECT name, jsonb_build_object(''age'', EXTRACT(YEAR FROM AGE(birthday))) AS user_data
FROM users
) subquery
WHERE (user_data->>''age'')::int > 30;
```
| name | age |
|------|-----|
| Bob | 38 |
### Combining two JSON objects
PostgreSQL provides the `||` operator to combine two JSON objects.
```sql
SELECT ''{"name": "Alice"}''::jsonb || ''{"birthday": "1990-01-15"}''::jsonb AS user_json;
```
| user_json |
|-----------|
| {"name": "Alice", "birthday": "1990-01-15"} |
## MySQL / MariaDB
MySQL has good support for JSON operations starting from version 5.7.
See [the list of JSON functions in MySQL](https://dev.mysql.com/doc/refman/8.0/en/json-functions.html) for more details.
### Creating a JSON object
```sql
SELECT JSON_OBJECT(''name'', name, ''birthday'', birthday) AS user_json
FROM users;
```
| user_json |
|-----------|
| `{"name":"Alice","birthday":"1990-01-15"}` |
| `{"name":"Bob","birthday":"1985-05-22"}` |
| `{"name":"Charlie","birthday":"1992-09-30"}` |
### Creating a JSON array
```sql
SELECT JSON_ARRAY(name, birthday, group_name) AS user_array
FROM users;
```
| user_array |
|------------|
| `["Alice","1990-01-15","Admin"]` |
| `["Bob","1985-05-22","User"]` |
| `["Charlie","1992-09-30","User"]` |
### Aggregating multiple values into a JSON array
```sql
SELECT JSON_ARRAYAGG(name) AS names
FROM users;
```
| names |
|-------|
| `["Alice","Bob","Charlie"]` |
### Aggregating values into a JSON object
```sql
SELECT JSON_OBJECTAGG(name, birthday) AS name_birthday_map
FROM users;
```
| name_birthday_map |
|-------------------|
| `{"Alice":"1990-01-15","Bob":"1985-05-22","Charlie":"1992-09-30"}` |
### Iterating over a JSON array
MySQL provides the JSON_TABLE() function to iterate over JSON arrays. This powerful function allows you to convert JSON data into a relational table format, making it easy to work with JSON arrays.
Here''s an example of how to use JSON_TABLE() to iterate over a JSON array:
```sql
SELECT jt.name
FROM JSON_TABLE(
''["Alice", "Bob", "Charlie"]'',
''$[*]'' COLUMNS( name VARCHAR(50) PATH ''$'' )
) AS jt;
```
| name |
|---------|
| Alice |
| Bob |
| Charlie |
In this example:
- The first argument to JSON_TABLE() is the JSON array.
- `''$[*]''` is the path expression that selects all elements of the array.
- The `COLUMNS` clause defines the structure of the output table. In our case, we want a single column named `name`:
- `name VARCHAR(50) PATH ''$''` creates a text column that contains the raw value of each array element in its entirety (`$` is the current element).
You can also use JSON_TABLE() with more complex JSON structures:
```sql
SELECT jt.*
FROM JSON_TABLE(
''[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Charlie"}]'',
''$[*]'' COLUMNS(
id INT PATH ''$.id'',
name VARCHAR(50) PATH ''$.name''
)
) AS jt;
```
| id | name |
|----|---------|
| 1 | Alice |
| 2 | Bob |
| 3 | Charlie |
This approach allows you to easily iterate over JSON arrays and access their elements in a tabular format, which can be very useful for further processing or joining with other tables in your database.
### Iterating over a JSON object
The `JSON_TABLE` function can also be used to iterate over JSON objects:
```sql
SELECT jt.*
FROM JSON_TABLE(
''{"name": "Alice", "birthday": "1990-01-15"}'',
''$.*'' COLUMNS (
value JSON PATH ''$''
)
) AS jt;
```
| value |
|-------|
| "Alice" |
| "1990-01-15" |
#### Iterating over key-value pairs
You can use the `JSON_KEYS()` function to retrieve the list of keys in a JSON object as a JSON array,
then use that array to iterate over the keys of a JSON object:
```sql
SELECT json_key, json_extract(json_str, CONCAT(''$.'', json_key)) as json_value
FROM
(select ''{"name": "Alice", "birthday": "1990-01-15"}'' as json_str) AS my_json,
JSON_TABLE(json_keys(json_str), ''$[*]'' COLUMNS (json_key JSON PATH ''$'')) AS json_keys;
```
| json_key | json_value |
|----------|------------|
| name | Alice |
| birthday | 1990-01-15 |
### Querying JSON data
MySQL allows you to query JSON data using the `->` and `->>` operators:
```sql
SELECT name, user_data->''$.age'' AS age
FROM (
SELECT name, JSON_OBJECT(''age'', YEAR(CURDATE()) - YEAR(birthday)) AS user_data
FROM users
) subquery
WHERE user_data->''$.age'' > 30;
```
| name | age |
|------|-----|
| Bob | 38 |
## Microsoft SQL Server
SQL Server has support for JSON operations starting from SQL Server 2016.
See [the list of JSON functions in SQL Server](https://learn.microsoft.com/en-us/sql/t-sql/functions/json-functions-transact-sql?view=sql-server-ver16) for more details.
# JSON in SQL: A Comprehensive Guide
[Previous sections remain unchanged]
## Microsoft SQL Server
SQL Server has support for JSON operations starting from SQL Server 2016. It provides a comprehensive set of functions for working with JSON data.
See [the list of JSON functions in SQL Server](https://learn.microsoft.com/en-us/sql/t-sql/functions/json-functions-transact-sql?view=sql-server-ver16) for more details.
### Creating a JSON object
Use the `FOR JSON PATH` clause to create a JSON object:
```sql
SELECT (SELECT name, birthday FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) AS user_json
FROM users;
```
| user_json |
|-----------|
| `{"name":"Alice","birthday":"1990-01-15"}` |
| `{"name":"Bob","birthday":"1985-05-22"}` |
| `{"name":"Charlie","birthday":"1992-09-30"}` |
Alternatively, you can use the `JSON_OBJECT` function:
```sql
SELECT JSON_OBJECT(''name'': name, ''birthday'': birthday) AS user_json
FROM users;
```
### Creating a JSON array
Use the `FOR JSON PATH` clause to create a JSON array:
```sql
SELECT (SELECT name, birthday, group_name FOR JSON PATH) AS user_array
FROM users;
```
| user_array |
|------------|
| `[{"name":"Alice","birthday":"1990-01-15","group_name":"Admin"}]` |
| `[{"name":"Bob","birthday":"1985-05-22","group_name":"User"}]` |
| `[{"name":"Charlie","birthday":"1992-09-30","group_name":"User"}]` |
You can also use the `JSON_ARRAY` function:
```sql
SELECT JSON_ARRAY(name, birthday, group_name) AS user_array
FROM users;
```
### Aggregating multiple values into a JSON array
Use the `FOR JSON PATH` clause to aggregate values into a JSON array:
```sql
SELECT (SELECT name FROM users FOR JSON PATH) AS names;
```
| names |
|-------|
| `[{"name":"Alice"},{"name":"Bob"},{"name":"Charlie"}]` |
Alternatively, use the `JSON_ARRAYAGG` function:
```sql
SELECT JSON_ARRAYAGG(name) AS names FROM users;
```
### Aggregating values into a JSON object
```sql
SELECT JSON_OBJECTAGG(name: birthday) AS name_birthday_map FROM users;
```
### Iterating over a JSON array
Use the `OPENJSON` function to iterate over JSON arrays:
```sql
SELECT value FROM OPENJSON(''["Alice", "Bob", "Charlie"]'');
```
| value |
|-------|
| Alice |
| Bob |
| Charlie |
### Iterating over a JSON object
Use `OPENJSON` to iterate over JSON objects:
```sql
SELECT *
FROM OPENJSON(''{"name": "Alice", "birthday": "1990-01-15"}'')
WITH (
name NVARCHAR(50) ''$.name'',
birthday DATE ''$.birthday''
);
```
| name | birthday |
|------|----------|
| Alice | 1990-01-15 |
### Querying JSON data
Use the `JSON_VALUE` function to extract scalar values from JSON:
```sql
SELECT JSON_VALUE(''{"age": 38}'', ''$.age'') AS age
```
| age |
|-----|
| 38 |
### Additional JSON Functions
SQL Server provides several other useful JSON functions:
- `ISJSON`: Tests whether a string contains valid JSON.
- `JSON_MODIFY`: Updates the value of a property in a JSON string.
- `JSON_PATH_EXISTS`: Tests whether a specified SQL/JSON path exists in the input JSON string.
- `JSON_QUERY`: Extracts an object or an array from a JSON string.
Example using `JSON_MODIFY`:
```sql
SELECT JSON_MODIFY(''{"name": "Alice", "age": 30}'', ''$.age'', 31) AS updated_json;
```
| updated_json |
|--------------|
| {"name": "Alice", "age": 31} |
This comprehensive guide covers the basics of working with JSON in SQLite, PostgreSQL, MySQL, and SQL Server. Each database has its own set of functions and syntax for JSON operations, but the general concepts remain similar across all platforms.
');
@@ -0,0 +1,118 @@
-- Column Component Documentation
-- Component Definition
INSERT INTO component(name, icon, description, introduced_in_version) VALUES
('columns', 'columns', 'A component to display various items in a card layout, allowing users to choose options. Useful for showcasing different features or services, or KPIs. See also the big_number component.', '0.29.0');
-- Inserting parameter information for the column component
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'columns', * FROM (VALUES
('title', 'The title or label for the item.', 'TEXT', FALSE, TRUE),
('value', 'The value associated with the item.', 'TEXT', FALSE, TRUE),
('description', 'A brief description of the item.', 'TEXT', FALSE, TRUE),
('description_md', 'A brief description of the item, formatted using markdown.', 'TEXT', FALSE, TRUE),
('item', 'A list of bullet points associated with the columns, represented either as text, or as a json object with "icon", "color", and "description" or "description_md" fields.', 'JSON', FALSE, TRUE),
('link', 'A link associated with the item.', 'TEXT', FALSE, TRUE),
('button_text', 'Text for the button.', 'TEXT', FALSE, TRUE),
('button_color', 'Optional color for the button.', 'TEXT', FALSE, TRUE),
('target', 'Optional target for the button. Set to "_blank" to open links in a new tab.', 'TEXT', FALSE, TRUE),
('value_color', 'Color for the value text.', 'TEXT', FALSE, TRUE),
('small_text', 'Optional small text to display after the value.', 'TEXT', FALSE, TRUE),
('icon', 'Optional icon to display in a ribbon.', 'ICON', FALSE, TRUE),
('icon_color', 'Color for the icon in the ribbon.', 'TEXT', FALSE, TRUE),
('size', 'Size of the column, affecting layout.', 'INTEGER', FALSE, TRUE)
) x;
INSERT INTO example(component, description, properties) VALUES
('columns', 'Pricing Plans Display',
json('[
{"component":"columns"},
{
"title":"Start Plan",
"value":"€18",
"description":"Perfect for testing and small-scale projects",
"item": [
"128MB Database",
"SQLPage hosting",
"Community support"
],
"link":"https://datapage.app",
"button_text":"Start Free Trial",
"small_text":"/month"
},
{
"title":"Pro Plan",
"value":"€40",
"icon":"rocket",
"description":"For growing projects needing enhanced features",
"item": [
{"icon":"database", "color": "blue", "description":"1GB Database"},
{"icon":"headset", "color": "green", "description":"Priority Support"},
{"icon":"world", "color": "purple", "description":"Custom Domain"}
],
"link":"https://datapage.app",
"button_text":"Start Free Trial",
"button_color":"indigo",
"value_color":"indigo",
"small_text":"/month"
},
{
"title":"Enterprise Plan",
"value":"€600",
"icon":"building-skyscraper",
"description":"For large-scale operations with custom needs",
"item": [
{"icon":"database-plus", "description_md":"**Custom Database Scaling**"},
{"icon":"shield-lock", "description_md":"**Enterprise Auth** with Single Sign-On"},
{"icon":"headset", "description_md":"**Monthly** Expert Support time"},
{"icon":"file-certificate", "description_md":"**SLA** with guaranteed uptime"}
],
"link":"mailto:contact@datapage.app",
"button_text":"Contact Us",
"small_text":"/month",
"target":"_blank"
}
]')),
('columns', 'Tech Company KPIs Display',
json('[
{"component":"columns"},
{
"title":"Monthly Active Users",
"value":"10k",
"value_color":"blue",
"size": 4,
"description":"Total active users this month, showcasing user engagement.",
"item": [
{"icon": "target", "description":"Target: 12,000"}
],
"link":"#",
"button_text":"User Activity Overview",
"button_color":"info"
},
{
"title":"Revenue",
"value":"$49k",
"value_color":"blue",
"size": 4,
"description":"Total revenue generated this month, indicating financial performance.",
"item": [
{"icon":"trending-down", "color": "red", "description":"down from $51k last month" }
],
"link":"#",
"button_text":"Financial Dashboard",
"button_color":"info"
},
{
"title":"Customer Satisfaction",
"value":"94%",
"value_color":"blue",
"size": 4,
"description":"Percentage of satisfied customers, reflecting service quality.",
"item": [
{"icon":"trending-up", "color": "green", "description":"+ 2% this month" }
],
"link":"#",
"button_text": "Open Google Ratings",
"button_color":"info"
}
]'));
@@ -0,0 +1,27 @@
INSERT INTO component(name, icon, description, introduced_in_version) VALUES
('foldable', 'chevrons-down', 'A foldable list of elements which can be expanded individually.', '0.29.0');
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'foldable', * FROM (VALUES
('id', 'ID attribute added to the container in HTML. Used for targeting through CSS or for scrolling via links. When set at the top level, applies to the entire foldable component.', 'TEXT', TRUE, TRUE),
('class', 'CSS class(es) to add to the foldable container. When set at the top level, applies to the entire foldable component.', 'TEXT', TRUE, TRUE),
('id', 'ID attribute added to individual foldable items. Used for targeting through CSS or for scrolling via links.', 'TEXT', FALSE, TRUE),
('class', 'CSS class(es) to add to individual foldable items.', 'TEXT', FALSE, TRUE),
('title', 'Title of the foldable item, displayed on the button.', 'TEXT', FALSE, TRUE),
('description', 'Plain text description of the item, displayed when expanded.', 'TEXT', FALSE, TRUE),
('description_md', 'Markdown description of the item, displayed when expanded.', 'TEXT', FALSE, TRUE),
('expanded', 'If set to TRUE, the foldable item starts in an expanded state. Defaults FALSE', 'BOOLEAN', FALSE, TRUE)
) x;
INSERT INTO example(component, description, properties) VALUES
('foldable', 'A single foldable paragraph of text', json('[
{"component":"foldable"},
{"title":"The foldable component", "description": "This is a simple foldable component. It can be used to show and hide content. It is a list of items, each with a title and a description. The description is displayed when the item is expanded."},
]'));
INSERT INTO example(component, description, properties) VALUES
('foldable', 'A SQLPage-themed foldable list with Markdown', json('[
{"component":"foldable"},
{"title":"Quick Prototyping", "description_md": "Build a functional web app prototype in minutes using just SQL queries:\n\n- Rapid development\n- Ideal for MVPs\n- Great for internal tools\n\nLearn more about [quick prototyping](/your-first-sql-website/).", "expanded": true},
{"title":"Data Visualization", "description_md": "Quickly transform your database into useful insights:\n\n1. **Charts**: Line, bar, pie\n2. **KPIs**: Appealing visualizations of key metrics\n3. **Maps**: Geospatial data\n\nAs simple as:\n\n```sql\nSELECT ''chart'' as component;\nSELECT date as x, revenue as y FROM sales;\n```"},
{"title":"Don''t stare, interact!", "description_md": "SQLPage is not just a passive *Business Intelligence* tool. With SQLPage, you can act upon user input:\n\n- *User input collection*: Building a form is just as easy as building a chart.\n- *Data validation*: Write your own validation rules in SQL.\n- *Database updates*: `INSERT` and `UPDATE` are first-class citizens.\n- *File uploads*: Upload `CSV` and other files, store and display them the way you want.\n\n> Let users interact with your data, not just look at it!"}
]'));
@@ -0,0 +1,28 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'Come and see us in Athens !',
'PGConf 2024 is coming to Athens, Greece. And we will be there to talk about SQLPage & Archeology !',
'microphone-2',
'2024-10-15',
'
# PostgreSQL & SQLPage at PGConf 2024
We have been invited to give a talk at PGConf 2024 in Athens, Greece.
Last year, we gave a general introduction to SQLPage at PGConf.eu 2023 in Prague.
This year, we will focus on a more specific use case:
using SQLPage to build a power-tool for archaeologists to explore and understand archaeological sites,
radically changing the way archaeologists work, and allowing them to
drastically reduce the time spent on data entry and management.
There will be two presenters:
- **Ophir Lojkine**, the creator and maintainer of SQLPage, will present the project and its capabilities.
- **Thomas Guillemard**, an archaeologist from France''s National Institute for Preventive Archaeological Research, will present the project''s use case and its benefits.
Come and see us in Athens !
https://www.postgresql.eu/events/pgconfeu2024/schedule/session/5707-unearthing-the-past-with-postgresql-how-open-source-is-revolutionizing-digital-archaeology/
');
@@ -0,0 +1,141 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'How I built a 360° performance monitoring tool with SQL Queries',
'Alexis built a performance monitoring tool with SQLPage for a 100M€/year company',
'shirt',
'2025-01-25',
'
# How I Built And Deployed An Exhaustive Performance Monitoring Tool For a 100M€/year Company Using SQL Queries Only
### What is SQLPage ?
> [SQLPage](http://sql-page.com) allows anyone with SQL skills to build and deploy digital tools (websites, data applications, dashboards, user forms…) using only **SQL queries**. Official website: [https://sql-page.com/](https://sql-page.com/)
SQLPage eliminates the need to learn server languages, HTML, CSS, JavaScript, or front-end frameworks, and instead uses SQL to generate modern UI layouts populated with database query results. You get native SQL interactions with the database, without all the other layers that typically get in the way.
The execution of the project is straightforward: simply run a single executable without any installation dependencies. Everything from authentication to security, and even HTTPS termination is automated. The code required to complete most real-world development tasks is minimal and seamless.
Finally, its open source with an MIT license.
### Why SQLPage became a game-changer for me, as a Head of Data
As a Head of Data at a mid-size company, I understand the challenge of juggling multiple tools — often expensive and proprietary — alongside a variety of dashboards. Building an **all-in-one**, **user-friendly**, **mobile-compatible** platform for data monitoring and visualization that serves everyone, from C-level executives to store managers, is no small feat.
The struggle intensifies when teams are small and lack coding skills or experience with diverse tech stacks. A typical data flow in a digital-native company involves several teams, specialized skills, and costly tools:
![](https://cdn-images-1.medium.com/max/800/1*1IoXc8-07rqXO3yvKC13nQ.png)
*Typical Data Flow of digital native companies.
SQLPage changes this by allowing data professionals to use the same language — SQL — across the entire process, from building data pipelines to creating fully functional digital tools. Data analysts, scientists, business analysts, DBAs, and IT teams already have the expertise to craft their own custom data applications from the ground up.
### Building an all-in one monitoring tool using SQL-queries only
[![youtube](https://github.com/user-attachments/assets/1afe36d7-9deb-40fc-a174-7a869348500b)](https://www.youtube.com/embed/R-5Pej8Sw18?si=qgxacwip2Mm-0wC7)
*Excerpt from a series of videos explaining how to build and deploy your first digital tool with SQLPage* ([https://www.youtube.com/@SQLPage](https://www.youtube.com/@SQLPage)).
I am using SQLPage to build a 360° Performance tool for my company, integrating data from multiple sources — Revenue, traffic, marketing investments, live performance monitoring, financial targets, images of top sold products, Google Analytics for the online traffic… — .
#### With SQLPage, I can:
- **Centralize** all company data in one tool for visualizations, year-over-year comparisons, financial targets, and more.
- **Provide tailored insights**: A store owner can instantly access last years performance and top-selling products, while the e-commerce director can track conversion rate history. SQLPages pre-built components offer limitless possibilities for displaying results.
- **Perform CRUD operations**: Unlike traditional BI tools, SQLPage not only displays data but also allows users to interact with it — inputting data, such as comments or updates, directly through the interface. This capability to both display and collect data is a significant enhancement over traditional BI tools, which typically do not support data input.
- **Ensure a single source of truth**: By connecting directly to the database, SQLPage avoids discrepancies between dashboards, ensuring all teams work with consistent and accurate data.
Here are some pages I built using only SQL queries, allowing everyone in the company to instantly access any level of information, from the fiscal year 2024 revenue trends to the top-selling products in Marseille in October 2022.
![](https://cdn-images-1.medium.com/max/800/1*MkORbAC7oGEG-8I1mthu6A.png)
*Performance of different channels vs last year and best sellers.
![](https://cdn-images-1.medium.com/max/800/1*_3-g1om_p9ghXhdcw0zHmw.png)
*Examples of views built with SQLPage to provide a 360° tool for the company.
### How Does It Work ?
The process in SQLPage follows a simple pattern:
> 1) Select a component
> 2) Write a query to populate the selected component with data
You can find the full list of components: [https://sql-page.com/documentation.sql](https://sql-page.com/documentation.sql)
Heres an example of a parameterized SQL query that uses the “chart” component, along with the query to feed data into it:
```sql
-- Chart Component
SELECT ''chart'' AS component,
CONCAT(''Daily Revenue from '', $start_date_comparison, '' to '', $end_date_comparison) AS title,
''area'' AS type,
''indigo'' AS color,
5 AS marker,
0 AS ymin;
-- Chart Data Query
SELECT DATE(business_date) AS x,
ROUND(SUM(value), 2) AS y
FROM data_example
WHERE DATE(business_date) BETWEEN $start_date_comparison AND $end_date_comparison
AND variable_name = ''CA''
GROUP BY DATE(business_date)
ORDER BY x ASC;
-- NB: The variables $start_date_comparison and $end_date_comparison are
-- defined dynamically in the SQL script
```
And the result:
![Example of a SQL-generated page using the “graph” and the “table” components](https://cdn-images-1.medium.com/max/800/1*mtgJNP7DSOmMnq0iu6dDcg.png)
*Example of a SQL-generated page using the components “graphs” and “tables”.*
Thats it! Each component comes with customizable parameters, allowing you to tailor the display. As shown in the screenshot, links are clickable, enabling users to add data, such as leaving a comment for a specific date.
The ability to perform CRUD operations and interact directly with databases is a game changer compared to traditional BI tools. You can try it yourself by clicking “add” in the column “COMMENT THIS YEAR” [https://demo-test.datapage.app/lets_see_some_graphs.sql](https://demo-test.datapage.app/lets_see_some_graphs.sql)
### What About GenAI ?
I couldnt write an article about data in 2024 without mentioning GenAI. The great news is that SQLPage, relying solely on SQL queries, is naturally GenAI-friendly. In fact, I rarely write SQL queries myself anymore — I let GenAI handle that. My workflow in SQLPage now becomes:
> 1) Select a component
> 2) Ask a GenAI tool to write the query I need
![](https://cdn-images-1.medium.com/max/800/1*mg45EO7XCVPNiuIQ_5Xg0Q.png)
*Example of Generated SQL to display a specific format of numbers.*
### How to Host Your SQLPage Application
Once my app was ready, I could have chosen to host it myself on any server for a few euros a month, but I opted for SQLPages official hosting service, DataPage ([https://datapage.app/](https://beta.datapage.app/)), which is fully managed and very convenient. My app was hosted at _domainname.datapage.app._ The service includes a Postgres database, allowing you to either store your data on the server or connect directly to your existing database (Microsoft SQL Server, SQLite, Postgres, MySQL, etc).
### What Difficulties Can Be Encountered With SQLPage
While SQLPage simplifies the process of building digital tools, it does come with some challenges.
As applications grow in complexity, so do the SQL queries required to power them, which can result in long and intricate scripts. Additionally, to fully leverage SQLPage, you need to understand how its components work, especially if user input is involved. Developers should be comfortable with creating tables in a database, writing `INSERT` queries, and managing data effectively. Without a solid grasp of these fundamentals, building more advanced apps can become a bit overwhelming.
### Conclusion
With SQLPage, any company with a database and one employee who knows how to query it has the tools and workforce to build and deploy virtually any digital tool.
In this article, I focused on creating an enhanced Business Intelligence tool, but SQLPages versatility goes far beyond that. It is being used to build a planning tool for lumberjacks in Finland, a monitoring app for a South African transport and logistics company, by archaeologists to input excavation data in the field…
What all these projects have in common is that they were built by a single person, using nothing but SQL queries. If youre ready to streamline your processes and build powerful tools with ease, SQLPage is worth exploring further.
### Useful links
- 🏡Official website [https://sql-page.com](https://sql-page.com/)
- 🔰Quick start (written by [Nick Antonaccio](https://medium.com/u/b6a791990395)): [https://learnsqlpage.com/sqlpage_quickstart.html](https://learnsqlpage.com/sqlpage_quickstart.html)
- 📹Youtube tutorial videos on SQLPage channel: [https://www.youtube.com/@SQLPage/playlists](https://www.youtube.com/@SQLPage/playlists)
- 🤓github: [https://github.com/sqlpage/SQLPage/](https://github.com/sqlpage/SQLPage/)
- ☁️Host your applications: [https://datapage.app](https://datapage.app)
');
@@ -0,0 +1,135 @@
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'request_body',
'0.33.0',
'http-post',
'Returns the raw request body as a string.
A client (like a web browser, mobile app, or another server) can send information to your server in the request body.
This function allows you to read that information in your SQL code,
in order to create or update a resource in your database for instance.
The request body is commonly used when building **REST APIs** (machines-to-machines interfaces)
that receive data from the client.
This is especially useful in:
- `POST` and `PUT` requests for creating or updating resources in your database
- Any API endpoint that needs to receive complex data
### Example: Building a REST API
Here''s an example of building an API endpoint that receives a json object,
and inserts it into a database.
#### `api/create_user.sql`
```sql
-- Get the raw JSON body
set user_data = sqlpage.request_body();
-- Insert the user into database
with parsed_data as (
select
json_extract($user_data, ''$.name'') as name,
json_extract($user_data, ''$.email'') as email
)
insert into users (name, email)
select name, email from parsed_data;
-- Return success response
select ''json'' as component,
json_object(
''status'', ''success'',
''message'', ''User created successfully''
) as contents;
```
### Testing the API
You can test this API using curl:
```bash
curl -X POST http://localhost:8080/api/create_user \
-H "Content-Type: application/json" \
-d ''{"name": "John", "email": "john@example.com"}''
```
## Special cases
### NULL
This function returns NULL if:
- There is no request body
- The request content type is `application/x-www-form-urlencoded` or `multipart/form-data`
(in these cases, use [`sqlpage.variables(''post'')`](?function=variables) instead)
### Binary data
If the request body is not valid text encoded in UTF-8,
invalid characters are replaced with the Unicode replacement character `` (U+FFFD).
If you need to handle binary data,
use [`sqlpage.request_body_base64()`](?function=request_body_base64) instead.
'
);
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'request_body_base64',
'0.33.0',
'photo-up',
'Returns the raw request body encoded in base64. This is useful when receiving binary data or when you need to handle non-text content in your API endpoints.
### What is Base64?
Base64 is a way to encode binary data (like images or files) into text that can be safely stored and transmitted. This function automatically converts the incoming request body into this format.
### Example: Handling Binary Data in an API
This example shows how to receive and process an image uploaded directly in the request body:
```sql
-- Assuming this is api/upload_image.sql
-- Client would send a POST request with the raw image data
-- Get the base64-encoded image data
set image_data = sqlpage.request_body_base64();
-- Store the image data in the database
insert into images (data, uploaded_at)
values ($image_data, current_timestamp);
-- Return success response
select ''json'' as component,
json_object(
''status'', ''success'',
''message'', ''Image uploaded successfully''
) as contents;
```
You can test this API using curl:
```bash
curl -X POST http://localhost:8080/api/upload_image.sql \
-H "Content-Type: application/octet-stream" \
--data-binary "@/path/to/image.jpg"
```
This is particularly useful when:
- Working with binary data (images, files, etc.)
- The request body contains non-UTF8 characters
- You need to pass the raw body to another system that expects base64
> Note: Like [`sqlpage.request_body()`](?function=request_body), this function returns NULL if:
> - There is no request body
> - The request content type is `application/x-www-form-urlencoded` or `multipart/form-data`
> (in these cases, use [`sqlpage.variables(''post'')`](?function=variables) instead)
'
);
@@ -0,0 +1,39 @@
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'headers',
'0.33.0',
'circle-dotted-letter-h',
'Returns all HTTP request headers as a JSON object.
### Example
The following displays all HTTP request headers in a list,
using SQLite''s `json_each()` function.
```sql
select ''list'' as component;
select key as title, value as description
from json_each(sqlpage.headers()); -- json_each() is SQLite only
```
If not on SQLite, use your [database''s JSON function](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide).
### Details
The function returns a JSON object where:
- Keys are lowercase header names
- Values are the corresponding header values
- If no headers are present, returns an empty JSON object `{}`
This is useful when you need to:
- Debug HTTP requests
- Access multiple headers at once
If you only need access to a single known header, use [`sqlpage.header(name)`](?function=header) instead.
');
@@ -0,0 +1,46 @@
INSERT INTO
sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES
(
'client_ip',
'0.33.0',
'network',
'Returns the IP address of the client making the HTTP request.
### Example
```sql
insert into connection_log (client_ip) values (sqlpage.client_ip());
```
### Details
The function returns:
- The IP address of the client as a string
- `null` if the client IP cannot be determined (e.g., when serving through a Unix socket)
### ⚠️ Important Notes for Production Use
When [running behind a reverse proxy](/your-first-sql-website/nginx.sql) (e.g., Nginx, Apache, Cloudflare):
- This function will return the IP address of the reverse proxy, not the actual client
- To get the real client IP, use [`sqlpage.header`](?function=header): `sqlpage.header(''x-forwarded-for'')` or `sqlpage.header(''x-real-ip'')`
- The exact header name depends on your reverse proxy configuration
Example with reverse proxy:
```sql
-- Choose the appropriate header based on your setup
select coalesce(
sqlpage.header(''x-forwarded-for''),
sqlpage.header(''x-real-ip''),
sqlpage.client_ip()
) as real_client_ip;
```
For security-critical applications, ensure your reverse proxy is properly configured to set and validate these headers.
'
);
@@ -0,0 +1,86 @@
INSERT INTO sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES (
'fetch_with_meta',
'0.34.0',
'transfer-vertical',
'Sends an HTTP request and returns detailed metadata about the response, including status code, headers, and body.
This function is similar to [`fetch`](?function=fetch), but returns a JSON object containing detailed information about the response.
The returned object has the following structure:
```json
{
"status": 200,
"headers": {
"content-type": "text/html",
"content-length": "1234"
},
"body": "a string, or a json object, depending on the content type",
"error": "error message if any"
}
```
If the request fails or encounters an error (e.g., network issues, invalid UTF-8 response), instead of throwing an error,
the function returns a JSON object with an "error" field containing the error message.
### Example: Basic Usage
```sql
-- Make a request and get detailed response information
set response = sqlpage.fetch_with_meta(''https://pokeapi.co/api/v2/pokemon/ditto'');
-- redirect the user to an error page if the request failed
select ''redirect'' as component, ''error.sql'' as url
where
json_extract($response, ''$.error'') is not null
or json_extract($response, ''$.status'') != 200;
-- Extract data from the response json body
select ''card'' as component;
select
json_extract($response, ''$.body.name'') as title,
json_extract($response, ''$.body.abilities[0].ability.name'') as description
from $response;
```
### Example: Advanced Request with Authentication
```sql
set request = json_object(
''method'', ''POST'',
''url'', ''https://sqlpage.free.beeceptor.com'',
''headers'', json_object(
''Content-Type'', ''application/json'',
''Authorization'', ''Bearer '' || sqlpage.environment_variable(''API_TOKEN'')
),
''body'', json_object(
''key'', ''value''
)
);
set response = sqlpage.fetch_with_meta($request);
-- Check response content type
select ''debug'' as component, $response as response;
```
The function accepts the same parameters as the [`fetch` function](?function=fetch).'
);
INSERT INTO sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES (
'fetch_with_meta',
1,
'url',
'Either a string containing an URL to request, or a json object in the standard format of the request interface of the web fetch API.',
'TEXT'
);
@@ -0,0 +1,4 @@
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'text', * FROM (VALUES
('unsafe_contents_md','Markdown format with html blocks. Use this only with trusted content. See the html-blocks section of the Commonmark spec for additional info.', 'TEXT', TRUE, TRUE),
('unsafe_contents_md','Markdown format with html blocks. Use this only with trusted content. See the html-blocks section of the Commonmark spec for additional info.', 'TEXT', FALSE, TRUE)
);
@@ -0,0 +1,52 @@
INSERT INTO component(name, icon, description, introduced_in_version) VALUES
('empty_state', 'info-circle', 'Displays a large placeholder message to communicate a single information to the user and invite them to take action.
Typically includes a title, an optional icon/image, descriptive text (rich text formatting and images supported via Markdown), and a call-to-action button.
Ideal for first-use screens, empty data sets, "no results" pages, or error messages.', '0.35.0');
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'empty_state', * FROM (VALUES
('title','Description of the empty state.','TEXT',TRUE,FALSE),
('header','Text displayed on the top of the empty state.','TEXT',TRUE,TRUE),
('icon','Name of an icon to be displayed on the top of the empty state.','ICON',TRUE,TRUE),
('image','The URL (absolute or relative) of an image to display at the top of the empty state.','URL',TRUE,TRUE),
('description','A short text displayed below the title.','TEXT',TRUE,TRUE),
('link_text','The text displayed on the button.','TEXT',TRUE,FALSE),
('link_icon','Name of an icon to be displayed on the left side of the button.','ICON',TRUE,FALSE),
('link','The URL to which the button should navigate when clicked.','URL',TRUE,FALSE),
('class','Class attribute added to the container in HTML. It can be used to apply custom styling to this item through css.','TEXT',TRUE,TRUE),
('id','ID attribute added to the container in HTML. It can be used to target this item through css or for scrolling to this item through links (use "#id" in link url).','TEXT',TRUE,TRUE)
) x;
INSERT INTO example(component, description, properties) VALUES
('empty_state', '
This example shows how to create a 404-style "Not Found" empty state with
- a prominent header displaying "404",
- a helpful description suggesting to adjust search parameters, and
- a "Search again" button with a search icon that links back to the search page.
',
json('[{
"component": "empty_state",
"title": "No results found",
"header": "404",
"description": "Try adjusting your search or filter to find what you''re looking for.",
"link_text": "Search again",
"link_icon": "search",
"link": "#not-found",
"id": "not-found"
}]')),
('empty_state', '
It''s possible to use an icon or an image to illustrate the problem.
',
json('[{
"component": "empty_state",
"title": "A critical problem has occurred",
"icon": "mood-wrrr",
"description_md": "SQLPage can do a lot of things, but this is not one of them.
Please restart your browser and **cross your fingers**.",
"link_text": "Close and restart",
"link_icon": "rotate-clockwise",
"link": "#"
}]'));
@@ -0,0 +1,294 @@
INSERT INTO
sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES
(
'user_info_token',
'0.35.0',
'key',
'# Accessing information about the current user, when logged in with SSO
This function can be used only when you have [configured Single Sign-On with an OIDC provider](/sso).
## The ID Token
When a user logs in through OIDC, your application receives an [identity token](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) from the identity provider.
This token contains information about the user, such as their name and email address.
The `sqlpage.user_info_token()` function lets you access the entire contents of the ID token, as a JSON object.
You can then use [your database''s JSON functions](/blog.sql?post=JSON+in+SQL%3A+A+Comprehensive+Guide) to process that JSON.
If you need to access a specific claim, it is easier and more performant to use the
[`sqlpage.user_info()`](?function=user_info) function instead.
### Example: Displaying User Information
```sql
select ''list'' as component;
select key as title, value as description
from json_each(sqlpage.user_info_token());
```
This sqlite-specific example will show all the information available about the current user, such as:
- `sub`: A unique identifier for the user
- `name`: The user''s full name
- `email`: The user''s email address
- `picture`: A URL to the user''s profile picture
### Security Notes
- The ID token is automatically verified by SQLPage to ensure it hasn''t been tampered with.
- The token is only available to authenticated users: if no user is logged in or sso is not configured, this function returns NULL
- If some information is not available in the token, you have to configure it on your OIDC provider, SQLPage can''t do anything about it.
- The token is stored in a signed http-only cookie named `sqlpage_auth`. You can use [the cookie component](/component.sql?component=cookie) to delete it, and the user will be redirected to the login page on the next page load.
'
);
INSERT INTO
sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES
(
'user_info',
'0.34.0',
'user',
'# Accessing Specific User Information
The `sqlpage.user_info` function is a convenient way to access specific pieces of information about the currently logged-in user.
When you [configure Single Sign-On](/sso), your OIDC provider will issue an [ID token](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) for the user,
which contains *claims*, with information about the user.
Calling `sqlpage.user_info(claim_name)` lets you access these claims directly from SQL.
## How to Use
The function takes one parameter: the name of the *claim* (the piece of information you want to retrieve).
For example, to display a personalized welcome message, with the user''s name, you can use:
```sql
select ''text'' as component;
select ''Welcome, '' || sqlpage.user_info(''name'') || ''!'' as title;
```
## Available Information
The exact information available depends on your identity provider (the service you chose to authenticate with),
its configuration, and the scopes you requested.
Use [`sqlpage.user_info_token()`](?function=user_info_token) to see all the information available in the ID token of the current user.
Here are some commonly available fields:
### Basic Information
- `name`: The user''s full name (usually first and last name separated by a space)
- `email`: The user''s email address (*warning*: there is no guarantee that the user currently controls this email address. Use the `sub` claim for database references instead.)
- `picture`: URL to the user''s profile picture
### User Identifiers
- `sub`: A unique identifier for the user (use this to uniquely identify the user in your database)
- `preferred_username`: The username the user prefers to use
### Name Components
- `given_name`: The user''s first name
- `family_name`: The user''s last name
## Examples
### Personalized Welcome Message
```sql
select ''text'' as component,
''Welcome back, **'' || sqlpage.user_info(''given_name'') || ''**!'' as contents_md;
```
### User Profile Card
```sql
select ''card'' as component;
select
sqlpage.user_info(''name'') as title,
sqlpage.user_info(''email'') as description,
sqlpage.user_info(''picture'') as image;
```
### Conditional Content Based on custom claims
Some identity providers let you add custom claims to the ID token.
This lets you customize the behavior of your application based on arbitrary user attributes,
such as the user''s role.
```sql
-- show everything to admins, only public items to others
select ''list'' as component;
select title from my_items
where is_public or sqlpage.user_info(''role'') = ''admin''
```
## Security Best Practices
> ⚠️ **Important**: Always use the `sub` claim to identify users in your database, not their email address.
> The `sub` claim is guaranteed to be unique and stable for each user, while email addresses can change.
> In most providers, receiving an id token with a given email does not guarantee that the user currently controls that email.
```sql
-- Store the user''s ID in your database
insert into user_preferences (user_id, theme)
values (sqlpage.user_info(''sub''), ''dark'');
```
## Troubleshooting
If you''re not getting the information you expect:
1. Check that OIDC is properly configured in your `sqlpage.json`
2. Verify that you requested the right scopes in your OIDC configuration
3. Try using `sqlpage.user_info_token()` to see all available information
4. Check your OIDC provider''s documentation for the exact claim names they use
Remember: If the user is not logged in or the requested information is not available, this function returns NULL.
'
);
INSERT INTO
sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES
(
'user_info',
1,
'claim',
'The name of the user information to retrieve. Common values include ''name'', ''email'', ''picture'', ''sub'', ''preferred_username'', ''given_name'', and ''family_name''. The exact values available depend on your OIDC provider and configuration.',
'TEXT'
);
INSERT INTO
sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES
(
'oidc_logout_url',
'0.41.0',
'logout',
'# Secure OIDC Logout
The `sqlpage.oidc_logout_url` function generates a secure logout URL for users authenticated via [OIDC Single Sign-On](/sso).
When a user visits this URL, SQLPage will:
1. Remove the authentication cookie
2. Redirect the user to the OIDC provider''s logout endpoint (if available)
3. Finally redirect back to the specified `redirect_uri`
## Security Features
This function provides protection against **Cross-Site Request Forgery (CSRF)** attacks:
- The generated URL contains a cryptographically signed token
- The token includes a timestamp and expires after 10 minutes
- The token is signed using your OIDC client secret
- Only relative URLs (starting with `/`) are allowed as redirect targets
This means that malicious websites cannot trick your users into logging out by simply including an image or link to your logout URL.
## How to Use
```sql
select ''button'' as component;
select
''Logout'' as title,
sqlpage.oidc_logout_url(''/'') as link,
''logout'' as icon,
''red'' as outline;
```
This creates a logout button that, when clicked:
1. Logs the user out of your SQLPage application
2. Logs the user out of the OIDC provider (if the provider supports [RP-Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html))
3. Redirects the user back to your homepage (`/`)
## Examples
### Logout Button in Navigation
```sql
select ''shell'' as component,
''My App'' as title,
json_array(
json_object(
''title'', ''Logout'',
''link'', sqlpage.oidc_logout_url(''/''),
''icon'', ''logout''
)
) as menu_item;
```
### Logout with Return to Current Page
```sql
select ''button'' as component;
select
''Sign Out'' as title,
sqlpage.oidc_logout_url(sqlpage.path()) as link;
```
### Conditional Logout Link
```sql
select ''button'' as component
where sqlpage.user_info(''sub'') is not null;
select
''Logout '' || sqlpage.user_info(''name'') as title,
sqlpage.oidc_logout_url(''/'') as link
where sqlpage.user_info(''sub'') is not null;
```
## Requirements
- OIDC must be [configured](/sso) in your `sqlpage.json`
- If OIDC is not configured, this function returns NULL
- The `redirect_uri` must be a relative path starting with `/`
## Provider Support
The logout behavior depends on your OIDC provider:
| Provider | Full Logout Support |
|----------|-------------------|
| Keycloak | ✅ Yes |
| Auth0 | ✅ Yes |
| Google | ❌ No (local logout only) |
| Azure AD | ✅ Yes |
| Okta | ✅ Yes |
When the provider doesn''t support RP-Initiated Logout, SQLPage will still remove the local authentication cookie and redirect to your specified URI.
'
);
INSERT INTO
sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES
(
'oidc_logout_url',
1,
'redirect_uri',
'The relative URL path where the user should be redirected after logout. Must start with `/`. Defaults to `/` if not provided.',
'TEXT'
);
@@ -0,0 +1,104 @@
create table
example_cards as
select
'Advanced Authentication' as title,
'user-authentication' as folder, -- Has to exactly match the folder name in the /examples/ directory
'postgres' as db_engine,
'Build a secure user authentication system with login, signup, and in-database session management.' as description
union all
select
'Authenticated CRUD',
'CRUD - Authentication',
'sqlite',
'Complete Create-Read-Update-Delete operations with user authentication.'
union all
select
'Image Gallery',
'image gallery with user uploads',
'sqlite',
'Create an image gallery with user uploads and session management.'
union all
select
'Developer UI',
'SQLPage developer user interface',
'postgres',
'A web-based interface for managing SQLPage files and database tables.'
union all
select
'Corporate Game',
'corporate-conundrum',
'sqlite',
'An interactive multiplayer board game with real-time updates.'
union all
select
'Roundest Pokemon',
'roundest_pokemon_rating',
'sqlite',
'Demo app with a distinct non-default design, using custom HTML templates for everything.'
union all
select
'Todo Application',
'todo application (PostgreSQL)',
'postgres',
'A full-featured todo list application with PostgreSQL backend.'
union all
select
'MySQL & JSON',
'mysql json handling',
'mysql',
'Learn advanced JSON manipulation in MySQL to build advanced SQLPage applications.'
union all
select
'Apache Web Server',
'web servers - apache',
'mysql',
'Use an existing Apache httpd Web Server to expose your SQLPage application.'
union all
select
'Sending Emails',
'sending emails',
'sqlite',
'Use the fetch function to send emails (or interact with any other HTTP API).'
union all
select
'Simple Website',
'simple-website-example',
'sqlite',
'Basic website example with navigation and data management.'
union all
select
'Geographic App',
'PostGIS - using sqlpage with geographic data',
'postgres',
'Use SQLPage to create and manage geodata.'
union all
select
'Multi-step form',
'forms-with-multiple-steps',
'sqlite',
'Guide to the implementation of forms that spread over multiple pages.'
union all
select
'Custom HTML & JS',
'custom form component',
'mysql',
'Building a custom form component with a dynamic widget using HTML and javascript.'
union all
select
'Splitwise Clone',
'splitwise',
'sqlite',
'An expense tracker app to split expenses with your friends, with nice debt charts.'
union all
select
'Advanced Forms with MS SQL Server',
'microsoft sql server advanced forms',
'sql server',
'Forms with multi-value dropdowns, using SQL Server and its JSON functions.'
union all
select
'Rich Text Editor',
'rich-text-editor',
'sqlite',
'A rich text editor with bold, italic, lists, images, and more. It posts its contents as Markdown.'
;
@@ -0,0 +1,84 @@
INSERT INTO component(name, icon, description, introduced_in_version) VALUES
('modal', 'app-window', '
Defines the a temporary popup box displayed on top of a webpages content.
Useful for displaying additional information, help, or collect data from users.
Modals are closed by default, and can be opened by clicking on a button or link targeting their ID.', '0.36.0');
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'modal', * FROM (VALUES
('title','Description of the modal box.','TEXT',TRUE,FALSE),
('close','The text to display in the Close button.','TEXT',TRUE,TRUE),
('contents','A paragraph of text to display, without any formatting, without having to make additional queries.','TEXT',FALSE,TRUE),
('contents_md','Rich text in the markdown format. Among others, this allows you to write bold text using **bold**, italics using *italics*, and links using [text](https://example.com).','TEXT',FALSE,TRUE),
('scrollable','Create a scrollable modal that allows scroll the modal body.','BOOLEAN',TRUE,TRUE),
('class','Class attribute added to the container in HTML. It can be used to apply custom styling to this item through css.','TEXT',TRUE,TRUE),
('id','ID attribute added to the container in HTML. It can be used to target this item through css or for displaying this item.','TEXT',TRUE,FALSE),
('large','Indicates that the modal box has an increased width.','BOOLEAN',TRUE,TRUE),
('small','Indicates that the modal box has a reduced width.','BOOLEAN',TRUE,TRUE),
('embed','Embed remote content in an iframe.','TEXT',TRUE,TRUE),
('embed_mode','Use "iframe" to display embedded content within an iframe.','TEXT',TRUE,TRUE),
('height','Height of the embedded content.','INTEGER',TRUE,TRUE),
('allow','For embedded content, this attribute specifies the features or permissions that can be used.','TEXT',TRUE,TRUE),
('sandbox','For embedded content, this attribute specifies the security restrictions on the loaded content.','TEXT',TRUE,TRUE),
('style','Applies CSS styles to the embedded content.','TEXT',TRUE,TRUE)
) x;
INSERT INTO example(component, description, properties) VALUES
('modal',
'This example shows how to create a modal box that displays a paragraph of text. The modal window is opened with the help of a button.',
json('[
{"component": "modal","id": "my_modal","title": "A modal box","close": "Close"},
{"contents":"I''m a modal window, and I allow you to display additional information or help for the user."},
{"component": "button"},
{"title":"Open a simple modal","link":"#my_modal"}
]')
),
('modal',
'Example of modal form content',
json('[
{
"component":"modal",
"id":"my_embed_form_modal",
"title":"Embeded form content",
"large":true,
"embed":"/examples/form.sql?_sqlpage_embed"
},
{"component": "button"},
{"title":"Open a modal with a form","link":"#my_embed_form_modal"}
]')
),
('modal',
'A popup modal that contains a chart generated by a separate SQL file. The modal is triggered by links inside a datagrid.',
json('[
{
"component":"modal",
"id":"my_embed_chart_modal",
"title":"Embeded chart content",
"close":"Close",
"embed":"/examples/chart.sql?_sqlpage_embed"
},
{"component": "datagrid"},
{"title":"Chart", "color":"blue", "description":"Revenue", "link":"#my_embed_chart_modal"},
{"title":"Form", "color":"green", "description":"Fill info", "link":"#my_embed_form_modal"},
]')
),
('modal',
'Example of modal video content',
json('[
{
"component":"modal",
"id":"my_embed_video_modal",
"title":"Embeded video content",
"close":"Close",
"embed":"https://www.youtube.com/embed/mXdgmSdaXkg",
"allow":"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
"embed_mode":"iframe",
"height":"350"
},
{"component": "text", "contents_md": "Open a [modal with a video](#my_embed_video_modal)"}
]')
);
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'button', * FROM (VALUES
('modal','Display the modal window corresponding to the specified ID.','TEXT',FALSE,TRUE)
) x;
@@ -0,0 +1,63 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'File-based routing in SQLPage',
'Understanding how SQLPage maps URLs to files and handles errors',
'route',
'2025-07-28',
'
SQLPage uses a simple file-based routing system that maps URLs directly to SQL files in your project directory.
No complex configuration is needed. Just create files and they become accessible endpoints.
This guide explains how SQLPage resolves URLs, handles different file types, and manages 404 errors so you can structure your application effectively.
## How SQLPage Routes Requests
### 1. Site Prefix Handling
If you''ve configured a [`site_prefix`](/your-first-sql-website/nginx) in your settings,
SQLPage will redirect all requests that do not start with the prefix to `/<site_prefix>`.
### 2. Path Resolution Priority
**Directory requests (paths ending with `/`)**: SQLPage looks for an `index.sql` file in that directory and executes it if found.
**Direct SQL file requests (`.sql` extension)**: SQLPage executes the requested SQL file if it exists.
**Static asset requests (other extensions)**: SQLPage serves files like CSS, JavaScript, images, or any other static content directly.
**Clean URL requests (no extension)**: SQLPage first tries to find a matching `.sql` file. If that doesn''t exist but there''s an `index.sql` file in a directory with the same name, it redirects to the directory path with a trailing slash.
### Error Handling
When, after applying each of the rules above in order, SQLPage can''t find a requested file,
it walks up your directory structure looking for [custom `404.sql` files](/your-first-sql-website/custom_urls).
## Dynamic Routing with SQLPage
SQLPage''s file-based routing becomes powerful when combined with strategic use of 404.sql files to handle dynamic URLs. Here''s how to build APIs and pages with dynamic parameters:
### Product Catalog with Dynamic IDs
**Goal**: Handle URLs like `/products/123`, `/products/abc`, `/products/new-laptop`
**Setup**:
```text
products/
├── index.sql # Lists all products (/products/)
├── 404.sql # Handles /products/<product-id>
└── categories.sql # Product categories (/products/categories)
```
**How it works**:
- `/products/` → Executes `products/index.sql` (product listing)
- `/products/123` → No `123.sql` file exists, so executes `products/404.sql`
- `/products/laptop` → No `laptop.sql` file exists, so executes `products/404.sql`
**In `products/404.sql`**:
```sql
set product_id = substr(sqlpage.path(), 1+length(''/products/''));
```
'
);
@@ -0,0 +1,138 @@
-- Insert the download component into the component table
INSERT INTO
component (name, description, icon, introduced_in_version)
VALUES
(
'download',
'
The *download* component lets a page immediately return a file to the visitor.
Instead of showing a web page, it sends the file''s bytes as the whole response,
so it should be used **at the very top of your SQL page** (before the shell or any other page contents).
It is an error to use this component after another component that would display content.
How it works in simple terms:
- You provide the file content using a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs).
A data URL is just a text string that contains both the file type and the actual data.
- Optionally, you provide a "filename" so the browser shows a proper Save As name.
If you do not provide a filename, many browsers will try to display the file inline (for example images or JSON), depending on the content type.
- You link to the page that uses the download component from another page, using the [button](/components?component=button) component for example.
What is a data URL?
- It looks like this: `data:[content-type][;base64],DATA`
- Examples:
- Plain text (URL-encoded): `data:text/plain,Hello%20world`
- JSON (URL-encoded): `data:application/json,%7B%22message%22%3A%22Hi%22%7D`
- Binary data (Base64): `data:application/octet-stream;base64,SGVsbG8h`
Tips:
- Use URL encoding when you have textual data. You can use [`sqlpage.url_encode(source_text)`](/functions?function=url_encode) to encode the data.
- Use Base64 when you have binary data (images, PDFs, or content that may include special characters).
- Use [`sqlpage.read_file_as_data_url(file_path)`](/functions?function=read_file_as_data_url) to read a file from the server and return it as a data URL.
> Keep in mind that large files are better served from disk or object storage. Data URLs are best for small to medium files.
There is a big performance penalty for loading large files as data URLs, so it is not recommended.
',
'download',
'0.37.0'
);
-- Insert the parameters for the download component into the parameter table
INSERT INTO
parameter (
component,
name,
description,
type,
top_level,
optional
)
VALUES
(
'download',
'data_url',
'The file content to send, written as a data URL (for example: data:text/plain,Hello%20world or data:application/octet-stream;base64,SGVsbG8h). The part before the comma declares the content type and whether the data is base64-encoded. The part after the comma is the actual data.',
'TEXT',
TRUE,
FALSE
),
(
'download',
'filename',
'The suggested name of the file to save (for example: report.csv). When set, the browser will download the file as an attachment with this name. When omitted, many browsers may try to display the file inline depending on its content type.',
'TEXT',
TRUE,
TRUE
);
-- Insert usage examples of the download component into the example table
INSERT INTO
example (component, description)
VALUES
(
'download',
'
## Simple plain text file
Download a small text file. The content is URL-encoded (spaces become %20).
```sql
select
''download'' as component,
''data:text/plain,Hello%20SQLPage%20world!'' as data_url,
''hello.txt'' as filename;
```
'
),
(
'download',
'
## Download a PDF file from the server
Download a PDF file with the proper content type so PDF readers recognize it.
Uses [`sqlpage.read_file_as_data_url(file_path)`](/functions?function=read_file_as_data_url) to read the file from the server.
```sql
select
''download'' as component,
''report.pdf'' as filename,
sqlpage.read_file_as_data_url(''report.pdf'') as data_url;
```
'
),
(
'download',
'
## Serve an image stored as a BLOB in the database
### Automatically detect the mime type
If you have a table with a column `content` that contains a BLOB
(depending on the database, the type may be named `BYTEA`, `BLOB`, `VARBINARY`, or `IMAGE`),
you can just return its contents directly, and SQLPage will automatically detect the mime type,
and convert it to a data URL.
```sql
select
''download'' as component,
content as data_url
from document
where id = $doc_id;
```
### Customize the mime type
In PostgreSQL, you can use the [encode(bytes, format)](https://www.postgresql.org/docs/current/functions-binarystring.html#FUNCTION-ENCODE) function to encode the file content as Base64,
and manually create your own data URL.
```sql
select
''download'' as component,
''data:'' || doc.mime_type || '';base64,'' || encode(doc.content::bytea, ''base64'') as data_url
from document as doc
where doc.id = $doc_id;
```
- In Microsoft SQL Server, you can use the [BASE64_ENCODE(bytes)](https://learn.microsoft.com/en-us/sql/t-sql/functions/base64-encode-transact-sql) function to encode the file content as Base64.
- In MySQL and MariaDB, you can use the [TO_BASE64(str)](https://mariadb.com/docs/server/reference/sql-functions/string-functions/to_base64) function.
'
);
@@ -0,0 +1,61 @@
INSERT INTO component(name, icon, introduced_in_version, description) VALUES
('log', 'logs', '0.37.1', 'A component that writes messages to the server logs.
When a page runs, it prints your message to the terminal/console (standard error).
Use it to track what happens and troubleshoot issues.
### Where do the messages appear?
- Running from a terminal (Linux, macOS, or Windows PowerShell/Command Prompt): they show up in the window.
- Docker: run `docker logs <container_name>`.
- Linux service (systemd): run `journalctl -u sqlpage`.
- This component''s output is written to [standard error (stderr)](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)). SQLPage request access logs are separate and are written to standard output (stdout).
');
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'log', * FROM (VALUES
-- top level
('message', 'The text to write to the server logs. It is printed when the page runs.', 'TEXT', TRUE, FALSE),
('level', 'How important the message is. One of ''trace'', ''debug'', ''info'' (default), ''warn'', ''error''. Not case-sensitive. Controls the level shown in the logs.', 'TEXT', TRUE, TRUE)
) x;
INSERT INTO example(component, description) VALUES
('log', '
### Record a simple message
This writes "Hello, World!" to the server logs.
```sql
SELECT ''log'' as component, ''Hello, World!'' as message;
```
Example output:
```text
[2025-09-13T22:30:14.722Z INFO sqlpage::log from "x.sql" statement 1] Hello, World!
```
### Set the importance (level)
Choose how important the message is.
```sql
SELECT ''log'' as component, ''error'' as level, ''This is an error message'' as message;
```
Example output:
```text
[2025-09-13T22:30:14.722Z ERROR sqlpage::log from "x.sql" statement 2] This is an error message
```
### Log dynamic information
Include variables like a username.
```sql
set username = ''user''
select ''log'' as component,
''403 - failed for '' || coalesce($username, ''None'') as message,
''error'' as level;
```
')
@@ -0,0 +1,137 @@
-- HMAC function documentation and examples
INSERT INTO
sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES
(
'hmac',
'0.38.0',
'shield-lock',
'Creates a unique "signature" for some data using a secret key.
This signature proves that the data hasn''t been tampered with and comes from someone who knows the secret.
### What is HMAC used for?
[**HMAC**](https://en.wikipedia.org/wiki/HMAC) (Hash-based Message Authentication Code) is commonly used to:
- **Verify webhooks**: Use HMAC to ensure only a given external service can call a given endpoint in your application.
The service signs their request with a secret key, and you verify the signature before processing the data they sent you.
Used for instance by [Stripe](https://docs.stripe.com/webhooks?verify=verify-manually), and [Shopify](https://shopify.dev/docs/apps/build/webhooks/subscribe/https#step-2-validate-the-origin-of-your-webhook-to-ensure-its-coming-from-shopify).
- **Secure API requests**: Prove that an API request comes from an authorized source
- **Generate secure tokens**: Create temporary access codes for downloads or password resets
- **Protect data**: Ensure data hasn''t been modified during transmission
### How to use it
The `sqlpage.hmac` function takes three inputs:
1. **Your data** - The text you want to sign (like a message or request body)
2. **Your secret key** - A password only you know (keep this safe!)
3. **Algorithm** (optional) - The hash algorithm and output format:
- `sha256` (default) - SHA-256 with hexadecimal output
- `sha256-base64` - SHA-256 with base64 output
- `sha512` - SHA-512 with hexadecimal output
- `sha512-base64` - SHA-512 with base64 output
It returns a signature string. If someone changes even one letter in your data, the signature will be completely different.
### Example: Verify a Webhooks signature
When Shopify sends you a webhook (like when someone places an order), it includes a signature. Here''s how to verify it''s really from Shopify.
This supposes you store the secret key in an [environment variable](https://en.wikipedia.org/wiki/Environment_variable) named `WEBHOOK_SECRET`.
```sql
SET body = sqlpage.request_body();
SET secret = sqlpage.environment_variable(''WEBHOOK_SECRET'');
SET expected_signature = sqlpage.hmac($body, $secret, ''sha256'');
SET actual_signature = sqlpage.header(''X-Webhook-Signature'');
-- redirect to an error page and stop execution if the signature does not match
SELECT
''redirect'' as component,
''/error.sql?err=bad_webhook_signature'' as link
WHERE $actual_signature != $expected_signature OR $actual_signature IS NULL;
-- If we reach here, the signature is valid - process the order
INSERT INTO orders (order_data) VALUES ($body);
SELECT ''json'' as component, ''jsonlines'' as type;
SELECT ''success'' as status;
```
### Example: Time-limited links
You can create links that will be valid only for a limited time by including a signature in them.
Let''s say we have a `download.sql` page we want to link to,
but we don''t want it to be accessible to anyone who can find the link.
Sign `file_id|expires_at` with a secret. Accept only if not expired and the signature matches.
#### Generate a signed link
```sql
SET expires_at = datetime(''now'', ''+1 hour'');
SET token = sqlpage.hmac(
$file_id || ''|'' || $expires_at,
sqlpage.environment_variable(''DOWNLOAD_SECRET''),
''sha256''
);
SELECT ''/download.sql?file_id='' || $file_id || ''&expires_at='' || $expires_at || ''&token='' || $token AS link;
```
#### Verify the signed link
```sql
SET expected = sqlpage.hmac(
$file_id || ''|'' || $expires_at,
sqlpage.environment_variable(''DOWNLOAD_SECRET''),
''sha256''
);
SELECT ''redirect'' AS component, ''/error.sql?err=expired'' AS link
WHERE $expected != $token OR $token IS NULL OR $expires_at < datetime(''now'');
-- serve the file
```
### Important Security Notes
- **Keep your secret key safe**: If your secret leaks, anyone can forge signatures and access protected pages
- **The signature is case-sensitive**: Even a single wrong letter means the signature won''t match
- **NULL handling**: Always use `IS DISTINCT FROM`, not `=` to check for hmac matches.
- `SELECT ''redirect'' as component WHERE sqlpage.hmac(...) != $signature` will not redirect if `$signature` is NULL (the signature is absent).
- `SELECT ''redirect'' as component WHERE sqlpage.hmac(...) IS DISTINCT FROM $signature` checks for both NULL and non-NULL values (but is not available in all SQL dialects).
- `SELECT ''redirect'' as component WHERE sqlpage.hmac(...) != $signature OR $signature IS NULL` is the most portable solution.
'
);
INSERT INTO
sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES
(
'hmac',
1,
'data',
'The input data to compute the HMAC for. Can be any text string. Cannot be NULL.',
'TEXT'
),
(
'hmac',
2,
'key',
'The secret key used to compute the HMAC. Should be kept confidential. Cannot be NULL.',
'TEXT'
),
(
'hmac',
3,
'algorithm',
'The hash algorithm and output format. Optional, defaults to `sha256` (hex output). Supported values: `sha256`, `sha256-base64`, `sha512`, `sha512-base64`. Defaults to `sha256`.',
'TEXT'
);
@@ -0,0 +1,74 @@
INSERT INTO component(name, icon, description, introduced_in_version) VALUES
('login', 'password-user', '
The login component is an authentication form with numerous customization options.
It offers the main functionalities for this type of form.
The user can enter their username and password.
There are many optional attributes such as the use of icons on input fields, the insertion of a link to a page to reset the password, an option for the application to maintain the user''s identity via a cookie.
It is also possible to set the title of the form, display the company logo, or customize the appearance of the form submission button.
This component should be used in conjunction with other components such as [authentication](component.sql?component=authentication) and [cookie](component.sql?component=cookie).
It does not implement any logic and simply collects the username and password to pass them to the code responsible for authentication.
A few things to know :
- The form uses the POST method to transmit information to the destination page,
- The user''s username and password are entered into fields with the names `username` and `password`,
- To obtain the values of username and password, you must use the variables `:username` and `:password`,
- When you set the `remember_me_text` property, the variable `:remember` becomes available after form submission to check if the user checked the "remember me" checkbox.
', '0.39.0');
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'login', * FROM (VALUES
('title','Title of the authentication form.','TEXT',TRUE,TRUE),
('enctype','Form data encoding.','TEXT',TRUE,TRUE),
('action','An optional link to a target page that will handle the results of the form. ','TEXT',TRUE,TRUE),
('error_message','An error message to display above the form, typically shown after a failed login attempt.','TEXT',TRUE,TRUE),
('error_message_md','A markdown error message to display above the form, typically shown after a failed login attempt.','TEXT',TRUE,TRUE),
('username','Label and placeholder for the user account identifier text field.','TEXT',TRUE,FALSE),
('password','Label and placeholder for the password field.','TEXT',TRUE,FALSE),
('username_icon','Icon to display on the left side of the input field, on the same line.','ICON',TRUE,TRUE),
('password_icon','Icon to display on the left side of the input field, on the same line.','ICON',TRUE,TRUE),
('image','The URL of an centered image displayed before the title.','URL',TRUE,TRUE),
('forgot_password_text','A text for the link allowing the user to reset their password. If the text is empty, the link is not displayed.','TEXT',TRUE,TRUE),
('forgot_password_link','The link to the page allowing the user to reset their password.','TEXT',TRUE,TRUE),
('remember_me_text','A text for the option allowing the user to request the preservation of their identity. If the text is empty, the option is not displayed.','TEXT',TRUE,TRUE),
('footer','A text placed at the bottom of the authentication form. If both footer and footer_md are specified, footer takes precedence.','TEXT',TRUE,TRUE),
('footer_md','A markdown text placed at the bottom of the authentication form. Useful for creating links to other pages (creating a new account, contacting technical support, etc.).','TEXT',TRUE,TRUE),
('validate','The text to display in the button at the bottom of the form that submits the values.','TEXT',TRUE,TRUE),
('validate_color','The color of the button at the bottom of the form that submits the values. Omit this property to use the default color.','COLOR',TRUE,TRUE),
('validate_shape','The shape of the validation button.','TEXT',TRUE,TRUE),
('validate_outline','A color to outline the validation button.','COLOR',TRUE,TRUE),
('validate_size','The size of the validation button.','TEXT',TRUE,TRUE)
) x;
-- Insert example(s) for the component
INSERT INTO example(component, description, properties)
VALUES (
'login',
'Using the main options of the login component
When the user clicks the "Sign in" button, the form is submitted to the `/examples/show_variables.sql` page.
There, you will have access to the variables:
- `:username`: the username entered by the user
- `:password`: the password entered by the user
- `:remember`: the string "on" if the checkbox was checked, or NULL if it was not checked
',
JSON(
'[
{
"component": "login",
"action": "/examples/show_variables",
"image": "../assets/icon.webp",
"title": "Please login to your account",
"username": "Username",
"password": "Password",
"username_icon": "user",
"password_icon": "lock",
"forgot_password_text": "Forgot your password?",
"forgot_password_link": "reset_password.sql",
"remember_me_text": "Remember me",
"footer_md": "Don''t have an account? [Register here](register.sql)",
"validate": "Sign in"
}
]'
)
),
('login', 'Most basic login form', JSON('[{"component": "login"}]'));
@@ -0,0 +1,284 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'Performance Guide',
'Concrete advice on how to make your SQLPage webapp fast',
'bolt',
'2025-10-31',
'
# Performance Guide
SQLPage is [optimized](/performance)
to allow you to create web pages that feel snappy.
This guide contains advice on how to ensure your users never wait
behind a blank screen waiting for your pages to load.
A lot of the advice here is not specific to SQLPage, but applies
to making SQL queries fast in general.
If you are already comfortable with SQL performance optimization, feel free to jump right to
the second part of the quide: *SQLPage-specific advice*.
## Make your queries fast
The best way to ensure your SQLPage webapp is fast is to ensure your
database is well managed and your SQL queries are well written.
We''ll go over the most common database performance pitfalls so that you know how to avoid them.
### Choose the right database schema
#### Normalize (but not too much)
Your database schema should be [normalized](https://en.wikipedia.org/wiki/Database_normalization):
one piece of information should be stored in only one place in the database.
This is a good practice that will not only make your queries faster,
but also make it impossible to store incoherent data.
You should use meaningful natural [primary keys](https://en.wikipedia.org/wiki/Primary_key) for your tables
and resort to surrogate keys (such as auto-incremented integer ids) only when the data is not naturally keyed.
Relationships between tables should be explicitly represented by [foreign keys](https://en.wikipedia.org/wiki/Foreign_key).
```sql
-- Products table, naturally keyed by catalog_number
CREATE TABLE product (
catalog_number VARCHAR(20) PRIMARY KEY,
name TEXT NOT NULL,
price DECIMAL(10,2) NOT NULL
);
-- Sales table: natural key = (sale_date, store_id, transaction_number)
-- composite primary key used since no single natural attribute alone uniquely identifies a sale
CREATE TABLE sale (
sale_date DATE NOT NULL,
store_id VARCHAR(10) NOT NULL,
transaction_number INT NOT NULL,
product_catalog_number VARCHAR(20) NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
PRIMARY KEY (sale_date, store_id, transaction_number),
FOREIGN KEY (product_catalog_number) REFERENCES product(catalog_number),
FOREIGN KEY (store_id) REFERENCES store(store_id)
);
```
Always use foreign keys instead of trying to store redundant data such as store names in the sales table.
This way, when you need to display the list of stores in your application, you don''t have to
run a slow `select distinct store from sales`, that would have to go through your millions of sales
(*even if you have an index on the store column*), you just query the tiny `stores` table directly.
You also need to use the right [data types](https://en.wikipedia.org/wiki/Data_type) for your columns,
otherwise you will waste a lot of space and time converting data at query time.
See [postgreSQL data types](https://www.postgresql.org/docs/current/datatype.html),
[MySQL data types](https://dev.mysql.com/doc/refman/8.0/en/data-types.html),
[Microsoft SQL Server data types](https://learn.microsoft.com/en-us/sql/t-sql/data-types/data-types-transact-sql?view=sql-server-ver16),
[SQLite data types](https://www.sqlite.org/datatype3.html).
[Denormalization](https://en.wikipedia.org/wiki/Denormalization) can be introduced
only after you have already normalized your data, and is often not required at all.
### Use views
Querying normalized views can be cumbersome.
`select store_name, sum(paid_eur) from sale group by store_name`
is more readable than
```sql
select store.name, sum(sale.paid_eur)
from sales
inner join stores on sale.store_id = store.store_id
group by store_name
```
To work around that, you can create views that contain
useful table joins so that you do not have to duplicate them in all your queries:
```sql
create view enriched_sales as
select sales.sales_eur, sales.client_id, store.store_name
from sales
inner join store
```
#### Materialized views
Some analytical queries just have to compute aggregated statistics over large quantities of data.
For instance, you might want to compute the total sales per store, or the total sales per product.
These queries are slow to compute when there are many rows, and you might not want to run them on every request.
You can use [materialized views](https://en.wikipedia.org/wiki/Materialized_view) to cache the results of these queries.
Materialized views are views that are stored as regular tables in the database.
Depending on the database, you might have to refresh the materialized view manually.
You can either refresh the view manually from inside your sql pages when you detect they are outdated,
or write an external script to refresh the view periodically.
```sql
create materialized view total_sales_per_store as
select store_name, sum(sales_eur) as total_sales
from sales
group by store_name;
```
### Use database indices
When a query on a large table uses non-primary column in a `WHERE`, `GROUP BY`, `ORDER BY`, or `JOIN`,
you should create an [index](https://en.wikipedia.org/wiki/Database_index) on that column.
When multiple columns are used in the query, you should create a composite index on those columns.
When creating a composite index, the order of the columns is important.
The most frequently used columns should be first.
```sql
create index idx_sales_store_date on sale (store_id, sale_date); -- useful for queries that filter by "store" or by "store and date"
create index idx_sales_product_date on sale (product_id, sale_date);
create index idx_sales_store_product_date on sale (store_id, product_id, sale_date);
```
Indexes are updated automatically when the table is modified.
They slow down the insertion and deletion of rows in the table,
but speed up the retrieval of rows in queries that use the indexed columns.
### Query performance debugging
When a query is slow, you can use the `EXPLAIN` keyword to see how the database will execute the query.
Just add `EXPLAIN` before the query you want to analyze.
On PostgreSQL, you can use a tool like [explain.dalibo.com](https://explain.dalibo.com/) to visualize the query plan.
What to look for:
- Are indexes used? You should see references to the indices you created.
- Are full table scans used? Large tables should never be scanned.
- Are expensive operations used? Such as sorting, hashing, bitmap index scans, etc.
- Are operations happening in the order you expected them to? Filtering large tables should come first.
### Vacuum your database regularly
On PostgreSQL, you can use the [`VACUUM`](https://www.postgresql.org/docs/current/sql-vacuum.html) command to garbage-collect and analyze a database.
On MySQL, you can use the [`OPTIMIZE TABLE`](https://dev.mysql.com/doc/refman/8.0/en/optimize-table.html) command to reorganize it on disk and make it faster.
On Microsoft SQL Server, you can use the [`DBCC DBREINDEX`](https://learn.microsoft.com/en-us/sql/t-sql/database-console-commands/dbcc-dbreindex-transact-sql?view=sql-server-ver17) command to rebuild the indexes.
On SQLite, you can use the [`VACUUM`](https://www.sqlite.org/lang_vacuum.html) command to garbage-collect and analyze the database.
### Use the right database engine
If the amount of data you are working with is very large, does not change frequently, and you need to run complex queries on it,
you could use a specialized analytical database such as [ClickHouse](https://clickhouse.com/) or [DuckDB](https://duckdb.org/).
Such databases can be used with SQLPage by using their [ODBC](https://en.wikipedia.org/wiki/Open_Database_Connectivity) drivers.
### Database-specific performance recommendations
- [PostgreSQL "Performance Tips"](https://www.postgresql.org/docs/current/performance-tips.html)
- [MySQL optimization guide](https://dev.mysql.com/doc/refman/8.0/en/optimization.html)
- [Microsoft SQL Server "Monitor and Tune for Performance"](https://learn.microsoft.com/en-us/sql/relational-databases/performance/monitor-and-tune-for-performance?view=sql-server-ver17)
- [SQLite query optimizer overview](https://www.sqlite.org/optoverview.html)
## SQLPage-specific advice
The best way to make your SQLPage webapp fast is to make your queries fast.
Sometimes, you just don''t have control over the database, and have to run slow queries.
This section will help you minimize the impact to your users.
### Order matters
SQLPage executes the queries in your `.sql` files in order.
It does not start executing a query before the previous one has returned all its results.
So, if you have to execute a slow query, put it as far down in the page as possible.
#### No heavy computation before the shell
Every user-facing page in a SQLPage site has a [shell](/components?component=shell).
The first queries in any sql file (all the ones that come before the [shell](/components?component=shell))
are executed before any data has been sent to the user''s browser.
During that time, the user will see a blank screen.
So, ensure your shell comes as early as possible, and does not require any heavy computation.
If you can make your shell entirely static (independent of the database), do so,
and it will be rendered before SQLPage even finishes acquiring a database connection.
#### Set variables just above their first usage
For the reasons explained above, you should avoid defining all variables at the top of your sql file.
Instead, define them just above their first usage.
### Avoid recomputing the same data multiple times
Often, a single page will require the same pieces of data in multiple places.
In this case, avoid recomputing it on every use inside the page.
#### Reusing a single database record
When that data is small, store it in a sqlpage variable as JSON and then
extract the data you need using [json operations](/blog.sql?post=JSON%20in%20SQL%3A%20A%20Comprehensive%20Guide).
```sql
set product = (
select json_object(''name'', name, ''price'', price) -- in postgres, you can simply use row_to_json(product)
from products where id = $product_id
);
select ''alert'' as component, ''Product'' as title, $product->>''name'' as description;
```
#### Reusing a large query result set
You may have a page that lets the user filter a large dataset by many different criteria,
and then displays multiple charts and tables based on the filtered data.
In this case, store the filtered data in a temporary table and then reuse it in multiple places.
```sql
drop table if exists filtered_products;
create temporary table filtered_products as
select * from products where
($category is null or category = $category) and
($manufacturer is null or manufacturer = $manufacturer);
select ''alert'' as component, count(*) || '' products'' as title
from filtered_products;
select ''list'' as component;
select name as title from filtered_products;
```
### Reduce the number of queries
Each query you execute has an overhead of at least the time it takes to send a packet back and forth
between SQLPage and the database.
When it''s possible, combine multiple queries into a single one, possibly using
[`UNION ALL`](https://en.wikipedia.org/wiki/Set_operations_(SQL)#UNION_operator).
```sql
select ''big_number'' as component;
with stats as (
select count(*) as total, avg(price) as average_price from filtered_products
)
select ''count'' as title, stats.total as value from stats
union all
select ''average price'' as title, stats.average_price as value from stats;
```
### Lazy loading
Use the [card](/component?component=card) and [modal](/component?component=modal) components
with the `embed` attribute to load data lazily.
Lazy loaded content is not sent to the user''s browser when the page initially loads,
so it does not block the initial rendering of the page and provides a better experience for
data that might be slow to load.
### Database connections
SQLPage uses connection pooling: it keeps multiple database connections opened,
and reuses them for consecutive requests. When it does not receive requests for a long time,
it closes idle connection. When it receives many requests, it opens new connection,
but never more than the value specified by `max_database_pool_connections` in its
[configuration](https://github.com/sqlpage/SQLPage/blob/main/configuration.md).
You can increase the value of that parameter if your website has many concurrent users and your
database is configured to allow opening many simultaneous connections.
### SQLPage performance debugging
When `environment` is set to `development` in its [configuration](https://github.com/sqlpage/SQLPage/blob/main/configuration.md),
SQLPage will include precise measurement of the time it spends in each of the steps it has to go through before starting to send data
back to the user''s browser. You can visualize that performance data in your browser''s network inspector.
You can set the `RUST_LOG` environment variable to `sqlpage=debug` to make SQLPage
print detailed messages associated with precise timing for everything it does.
');
@@ -0,0 +1,252 @@
INSERT INTO component(name, icon, description, introduced_in_version) VALUES
('pagination', 'sailboat-2', '
Navigation links to go to the first, previous, next, or last page of a dataset.
Useful when data is divided into pages, each containing a fixed number of rows.
This component only handles the display of pagination.
**Your sql queries are responsible for filtering data** based on the page number passed as a URL parameter.
This component is typically used in conjunction with a [table](?component=table),
[list](?component=list), or [card](?component=card) component.
The pagination component displays navigation buttons (first, previous, next, last) customizable with text or icons.
For large numbers of pages, an offset can limit the visible page links.
A minimal example of a SQL query that uses the pagination would be:
```sql
select ''table'' as component;
select * from my_table limit 100 offset $offset;
select ''pagination'' as component;
with recursive pages as (
select 0 as offset
union all
select offset + 100 from pages
where offset + 100 < (select count(*) from my_table)
)
select
(offset/100+1) as contents,
sqlpage.link(sqlpage.path(), json_object(''offset'', offset)) as link,
offset = coalesce(cast($offset as integer), 0) as active
from pages;
```
For more advanced usage, the [pagination guide](blog.sql?post=How+to+use+the+pagination+component) provides a complete tutorial.
', '0.40.0');
INSERT INTO parameter(component, name, description, type, top_level, optional) SELECT 'pagination', * FROM (VALUES
-- Top-level parameters
('first_link','A target URL to which the user should be directed to get to the first page. If none, the link is not displayed.','URL',TRUE,TRUE),
('previous_link','A target URL to which the user should be directed to get to the previous page. If none, the link is not displayed.','URL',TRUE,TRUE),
('next_link','A target URL to which the user should be directed to get to the next page. If none, the link is not displayed.','URL',TRUE,TRUE),
('last_link','A target URL to which the user should be directed to get to the last page. If none, the link is not displayed.','URL',TRUE,TRUE),
('first_title','The text displayed on the button to go to the first page.','TEXT',TRUE,TRUE),
('previous_title','The text displayed on the button to go to the previous page.','TEXT',TRUE,TRUE),
('next_title','The text displayed on the button to go to the next page.','TEXT',TRUE,TRUE),
('last_title','The text displayed on the button to go to the last page.','TEXT',TRUE,TRUE),
('first_disabled','disables the button to go to the first page.','BOOLEAN',TRUE,TRUE),
('previous_disabled','disables the button to go to the previous page.','BOOLEAN',TRUE,TRUE),
('next_disabled','Disables the button to go to the next page.','BOOLEAN',TRUE,TRUE),
('last_disabled','disables the button to go to the last page.','BOOLEAN',TRUE,TRUE),
('outline','Whether to use outline version of the pagination.','BOOLEAN',TRUE,TRUE),
('circle','Whether to use circle version of the pagination.','BOOLEAN',TRUE,TRUE),
-- Item-level parameters (for each page)
('contents','Page number.','INTEGER',FALSE,FALSE),
('link','A target URL to which the user should be redirected to view the requested page of data.','URL',FALSE,TRUE),
('offset','Whether to use offset to show only a few pages at a time. Usefull if the count of pages is too large. Defaults to false','BOOLEAN',FALSE,TRUE),
('active','Whether the link is active or not. Defaults to false.','BOOLEAN',FALSE,TRUE)
) x;
-- Insert example(s) for the component
INSERT INTO example(component, description, properties)
VALUES (
'pagination',
'This is an extremely simple example of a pagination component that displays only the page numbers, with the first page being the current page.',
JSON(
'[
{
"component": "pagination"
},
{
"contents": 1,
"link": "?component=pagination&page=1",
"active": true
},
{
"contents": 2,
"link": "?component=pagination&page=2"
},
{
"contents": 3,
"link": "?component=pagination&page=3"
}
]'
)
),
(
'pagination',
'The ouline style adds a rectangular border to each navigation link.',
JSON(
'[
{
"component": "pagination",
"outline": true
},
{
"contents": 1,
"link": "?component=pagination&page=1",
"active": true
},
{
"contents": 2,
"link": "?component=pagination&page=2"
},
{
"contents": 3,
"link": "?component=pagination&page=3"
}
]'
)
),
(
'pagination',
'The circle style adds a circular border to each navigation link.',
JSON(
'[
{
"component": "pagination",
"circle": true
},
{
"contents": 1,
"link": "?component=pagination&page=1",
"active": true
},
{
"contents": 2,
"link": "?component=pagination&page=2"
},
{
"contents": 3,
"link": "?component=pagination&page=3"
}
]'
)
),
(
'pagination',
'The following example implements navigation links that can be enabled or disabled as needed. Since a navigation link does not appear if no link is assigned to it, you must always assign a link to display it as disabled.',
JSON(
'[
{
"component": "pagination",
"first_link": "?component=pagination",
"first_disabled": true,
"previous_link": "?component=pagination",
"previous_disabled": true,
"next_link": "#?page=2",
"last_link": "#?page=3"
},
{
"contents": 1,
"link": "?component=pagination&page=1",
"active": true
},
{
"contents": 2,
"link": "?component=pagination&page=2"
},
{
"contents": 3,
"link": "?component=pagination&page=3"
}
]'
)
),
(
'pagination',
'Instead of using icons, you can apply text to the navigation links.',
JSON(
'[
{
"component": "pagination",
"first_title": "First",
"last_title": "Last",
"previous_title": "Previous",
"next_title": "Next",
"first_link": "?component=pagination",
"first_disabled": true,
"previous_link": "?component=pagination",
"previous_disabled": true,
"next_link": "#?page=2",
"last_link": "#?page=3"
},
{
"contents": 1,
"link": "?component=pagination&page=1",
"active": true
},
{
"contents": 2,
"link": "?component=pagination&page=2"
},
{
"contents": 3,
"link": "?component=pagination&page=3"
}
]'
)
),
(
'pagination',
'If you have a large number of pages to display, you can use an offset to represent a group of pages.',
JSON(
'[
{
"component": "pagination",
"first_link": "#?page=1",
"previous_link": "#?page=3",
"next_link": "#?page=4",
"last_link": "#?page=99"
},
{
"contents": 1,
"link": "?component=pagination&page=1"
},
{
"contents": 2,
"link": "?component=pagination&page=2"
},
{
"contents": 3,
"link": "?component=pagination&page=3"
},
{
"contents": 4,
"link": "?component=pagination&page=4",
"active": true
},
{
"contents": 5,
"link": "?component=pagination&page=5"
},
{
"contents": 6,
"link": "?component=pagination&page=6"
},
{
"offset": true
},
{
"contents": 99,
"link": "?component=pagination&page=99"
},
]'
)
);
@@ -0,0 +1,163 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'How to use the pagination component',
'A tutorial for using the pagination component',
'sailboat-2',
'2025-11-10',
'
# How to use the pagination component
To display a large number of records from a database, it is often practical to split these data into pages. The user can thus navigate from one page to another, as well as directly to the first or last page. With SQLPage, it is possible to perform these operations using the pagination component.
This component offers many options, and I recommend consulting its documentation before proceeding with the rest of this tutorial.
Of course, this component only handles its display and does not implement any logic for data processing or state changes. In this tutorial, we will implement a complete example of using the pagination component with a SQLite database, but the code should work without modification (or with very little modification) with any relational database management system (RDBMS).
> This article serves as a tutorial on the pagination component, rather than an advanced guide on paginated data retrieval from a database. The document employs a straightforward approach using the LIMIT and OFFSET instructions. This approach is interesting only for datasets that are big enough not to be realistically loadable on a single webpage, yet small enough for being queryable with OFFSET...LIMIT.
## Initialization
We first need to define two constants that indicate the maximum number of rows per page and the maximum number of pages that the component should display.
```
SET MAX_RECORD_PER_PAGE = 10;
SET MAX_PAGES = 10;
```
Now, we need to know the number of rows present in the table to be displayed. We can then calculate the number of pages required.
```
SET records_count = (SELECT COUNT(*) FROM album);
SET pages_count = (CAST($records_count AS INTEGER) / CAST($MAX_RECORD_PER_PAGE AS INTEGER));
```
It is possible that the number of rows in the table is greater than the estimated number of pages multiplied by the number of rows per page. In this case, it is necessary to add an additional page.
```
SET pages_count = (
CASE
WHEN MOD(CAST($records_count AS INTEGER),CAST($MAX_RECORD_PER_PAGE AS INTEGER)) = 0 THEN $pages_count
ELSE (CAST($pages_count AS INTEGER) + 1)
END
);
```
We will need to transmit the page number to be displayed in the URL using the `page` parameter. We do the same for the number of the first page (`idx_page`) appearing at the left end of the pagination component.
![Meaning of URL parameters](blog/pagination.png)
If the page number or index is not present in the URL, the value of 1 is applied by default.
```
SET page = COALESCE($page,1);
SET idx_page = COALESCE($idx_page,1);
```
## Read the data
We can now read and display the data based on the active page. To do this, we simply use a table component.
```
SELECT
''table'' as component
SELECT
user_id AS id,
last_name AS "Last name",
first_name AS "First name"
FROM
users
LIMIT CAST($MAX_RECORD_PER_PAGE AS INTEGER)
OFFSET (CAST($page AS INTEGER) - 1) * CAST($MAX_RECORD_PER_PAGE AS INTEGER);
```
The SQL LIMIT clause allows us to not read more rows than the maximum allowed for a page. With the SQL OFFSET clause, we specify from which row the data is selected.
On each HTML page load, the table content will be updated based on the `page` and `idx_page` variables, whose values will be extracted from the URL
## Set up the pagination component
Now, we need to set up the parameters that will be included in the URL for the buttons to navigate to the previous or next page.
If the user wants to view the previous page and the current page is not the first one, the value of the `page` variable is decremented. The same applies to `idx_page`, which is decremented if its value does not correspond to the first page.
```
SET previous_parameters = (
CASE
WHEN CAST($page AS INTEGER) > 1 THEN
json_object(
''page'', (CAST($page AS INTEGER) - 1),
''idx_page'', (CASE
WHEN CAST($idx_page AS INTEGER) > 1 THEN (CAST($idx_page AS INTEGER) - 1)
ELSE $idx_page
END)
)
ELSE json_object() END
);
```
The logic is quite similar for the URL to view the next page. First, it is necessary to verify that the user is not already on the last page. Then, the `page` variable can be incremented and the `idx_page` variable updated.
```
SET next_parameters = (
CASE
WHEN CAST($page AS INTEGER) < CAST($pages_count AS INTEGER) THEN
json_object(
''page'', (CAST($page AS INTEGER) + 1),
''idx_page'', (CASE
WHEN CAST($idx_page AS INTEGER) < (CAST($pages_count AS INTEGER) - CAST($MAX_PAGES AS INTEGER) + 1) THEN (CAST($idx_page AS INTEGER) + 1)
ELSE $idx_page
END)
)
ELSE json_object() END
);
```
We can now add the pagination component, which is placed below the table displaying the data. All the logic for managing the buttons is entirely handled in SQL:
- the buttons to access the first or last page,
- the buttons to view the previous or next page,
- the enabling or disabling of these buttons based on the context.
```
SELECT
''pagination'' AS component,
(CAST($page AS INTEGER) = 1) AS first_disabled,
(CAST($page AS INTEGER) = 1) AS previous_disabled,
(CAST($page AS INTEGER) = CAST($pages_count AS INTEGER)) AS next_disabled,
(CAST($page AS INTEGER) = CAST($pages_count AS INTEGER)) AS last_disabled,
sqlpage.link(sqlpage.path(), json_object(''page'', 1, ''idx_page'', 1)) as first_link,
sqlpage.link(sqlpage.path(), $previous_parameters) AS previous_link,
sqlpage.link(sqlpage.path(), $next_parameters) AS next_link,
sqlpage.link(
sqlpage.path(),
json_object(''page'', $pages_count, ''idx_page'', (
CASE
WHEN (CAST($pages_count AS INTEGER) <= CAST($MAX_PAGES AS INTEGER)) THEN 1
ELSE (CAST($pages_count AS INTEGER) - CAST($MAX_PAGES AS INTEGER) + 1)
END)
)
) AS last_link,
TRUE AS outline;
```
The final step is to generate the page numbers based on the number of pages and the index of the first page displayed to the left of the component. To do this, we use a recursive CTE query.
```
WITH RECURSIVE page_numbers AS (
SELECT $idx_page AS number
UNION ALL
SELECT number + 1
FROM page_numbers
LIMIT CAST($MAX_PAGES AS INTEGER)
)
SELECT
number AS contents,
sqlpage.link(sqlpage.path(), json_object(''page'', number, ''idx_page'', $idx_page)) as link,
(number = CAST($page AS INTEGER)) AS active
FROM page_numbers;
```
If the added page matches the content of the `page` variable, the `active` option is set to `TRUE` so that the user knows it is the current page.
');
@@ -0,0 +1,60 @@
INSERT INTO
sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES
(
'set_variable',
'0.40.0',
'variable',
'Returns a URL that is the same as the current page''s URL, but with a variable set to a new value.
This function is useful when you want to create a link that changes a parameter on the current page, while preserving other parameters.
It is equivalent to `sqlpage.link(sqlpage.path(), json_patch(sqlpage.variables(''get''), json_object(name, value)))`.
### Example
Let''s say you have a list of products, and you want to filter them by category. You can use `sqlpage.set_variable` to create links that change the category filter, without losing other potential filters (like a search query or a sort order).
```sql
select ''button'' as component, ''sm'' as size, ''center'' as justify;
select
category as title,
sqlpage.set_variable(''category'', category) as link,
case when $category = category then ''primary'' else ''secondary'' end as color
from categories;
```
### Parameters
- `name` (TEXT): The name of the variable to set.
- `value` (TEXT): The value to set the variable to. If `NULL` is passed, the variable is removed from the URL.
'
);
INSERT INTO
sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES
(
'set_variable',
1,
'name',
'The name of the variable to set.',
'TEXT'
),
(
'set_variable',
2,
'value',
'The value to set the variable to.',
'TEXT'
);
@@ -0,0 +1,175 @@
INSERT INTO blog_posts (title, description, icon, created_at, content)
VALUES
(
'Tracing SQLPage with OpenTelemetry and Grafana',
'How to inspect requests, SQL queries, and database wait time with distributed tracing',
'route-2',
'2026-03-09',
'
# Tracing SQLPage with OpenTelemetry and Grafana
When a page is slow, a log line telling you that the request took 1.8 seconds is only the start of the investigation. What you usually want to know next is where that time went:
- Did the request wait for a database connection?
- Which SQL file was executed?
- Which query took the longest?
- Did the delay start in SQLPage, in the reverse proxy, or in the database?
SQLPage now supports [OpenTelemetry](https://opentelemetry.io/), the standard way to emit distributed traces. Combined with Grafana, Tempo, Loki, and an OpenTelemetry collector, this gives you a detailed timeline of each request and lets you jump directly from logs to traces.
If you want a ready-to-run demo, see the [OpenTelemetry + Grafana example](https://github.com/sqlpage/SQLPage/tree/main/examples/telemetry), which this article is based on.
## What tracing gives you
With tracing enabled, one HTTP request becomes a tree of timed operations called spans. In a typical SQLPage app, you will see something like:
```text
[nginx] GET /todos
└─ [sqlpage] GET /todos
└─ [sqlpage] SQL website/todos.sql
├─ db.pool.acquire
└─ db.query
```
This is immediately useful for:
- Debugging slow pages by seeing exactly which query consumed the time
- Detecting connection pool pressure by measuring time spent in `db.pool.acquire`
- Following one request end-to-end from the reverse proxy to SQLPage to PostgreSQL
Tracing is especially helpful in SQLPage because one request often maps cleanly to one SQL file. That makes traces easy to interpret even when you are not used to application performance tooling.
## The easiest way to try it
The simplest way to explore tracing is to run
[the example shipped with SQLPage](https://github.com/sqlpage/SQLPage/tree/main/examples/telemetry).
After downloading or cloning the example, run this from the example''s folder:
```bash
docker compose up --build
```
That stack starts:
- nginx as the reverse proxy
- SQLPage
- PostgreSQL
- an OpenTelemetry collector
- Grafana Tempo for traces
- Grafana Loki for logs
- Promtail for log shipping
- Grafana for visualization
Then:
1. Open [http://localhost](http://localhost) and use the sample todo application.
2. Open [http://localhost:3000](http://localhost:3000) to access Grafana.
3. Inspect recent traces and logs from the default dashboard.
4. Open a trace to see the full span waterfall for a single request.
This setup is useful both as a demo and as a reference architecture for production deployments.
## Enabling tracing in SQLPage
Tracing is built into SQLPage. There is no plugin to install and no SQLPage-specific tracing configuration file to write.
You only need to set standard OpenTelemetry environment variables before starting SQLPage:
```bash
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
export OTEL_SERVICE_NAME="sqlpage"
sqlpage
```
The `OTEL_EXPORTER_OTLP_ENDPOINT` variable tells SQLPage where to send traces.
The `OTEL_SERVICE_NAME` variable controls how the service appears in your tracing backend.
If `OTEL_EXPORTER_OTLP_ENDPOINT` is not set, SQLPage falls back to normal logging and tracing stays disabled.
## What you will see in the trace
The most useful spans emitted by SQLPage are:
- The HTTP request span, with attributes such as method, path, status code, and user agent
- The SQL file execution span, showing which `.sql` file handled the request
- The `db.pool.acquire` span, showing time spent waiting for a database connection
- The `db.query` span, containing the SQL statement and database system
In practice, that means you can answer questions like:
- Is the page slow because the SQL itself is slow?
- Is the request queued because the connection pool is exhausted?
- Is the delay happening before SQLPage even receives the request?
This is much more actionable than a single request duration number.
## Logs and traces together
Tracing is even more useful when logs and traces are connected.
In the example stack, SQLPage writes request access logs to stdout and diagnostic logs to stderr. The OpenTelemetry Collector forwards both streams to Loki, and Grafana lets you move from a log line to the matching trace using the trace id. This makes it possible to start from an error log and immediately inspect the full request timeline.
That workflow is often the difference between guessing and knowing.
## PostgreSQL correlation
SQLPage also propagates trace context to PostgreSQL through the connection `application_name`.
This makes it possible to correlate live PostgreSQL activity or database logs with the trace that triggered it.
For example, inspecting `pg_stat_activity` can show which trace is attached to a running query:
```sql
SELECT application_name, query, state
FROM pg_stat_activity;
```
If you also include `%a` in PostgreSQL''s `log_line_prefix`, your database logs can contain the same trace context.
## A practical debugging example
Suppose users report that a page occasionally becomes slow under load.
With tracing enabled, you might see that:
- the HTTP span is long
- the SQL file execution span is also long
- the `db.query` span is short
- but `db.pool.acquire` takes several hundred milliseconds
That immediately tells you the database query itself is not the problem. The real issue is contention on the connection pool. You can then increase `max_database_pool_connections`, reduce concurrent load, or review long-running requests that keep connections busy.
Without tracing, this kind of diagnosis usually requires guesswork.
## Deployment options
The example uses Grafana Tempo and Loki, but SQLPage is not tied to a single backend. Because it emits standard OTLP traces, you can also send data to:
- Jaeger
- Grafana Cloud
- Datadog
- Honeycomb
- New Relic
- Axiom
In small setups, SQLPage can often send traces directly to the backend. In larger deployments, placing an OpenTelemetry collector in the middle is usually better because it centralizes routing, batching, and authentication.
## When to enable tracing
Tracing is particularly valuable when:
- you are running SQLPage behind a reverse proxy
- several SQL files participate in user-facing workflows
- you want to understand production latency, not just local development behavior
- you need a shared debugging tool for developers and operators
If your application is already important enough to monitor, it is important enough to trace.
## Conclusion
SQLPage already makes the application logic easy to inspect because it lives in SQL files. Tracing extends that visibility to runtime behavior.
By enabling OpenTelemetry and connecting SQLPage to Grafana, you can see not just that a request was slow, but why it was slow, where the time was spent, and which query or resource caused the delay.
For a complete working setup, start with the [OpenTelemetry + Grafana example](https://github.com/sqlpage/SQLPage/tree/main/examples/telemetry) and adapt it to your own deployment.
'
);
@@ -0,0 +1,103 @@
INSERT INTO
sqlpage_functions (
"name",
"introduced_in_version",
"icon",
"description_md"
)
VALUES
(
'regex_match',
'0.43.0',
'regex',
'Matches a text value against a regular expression and returns the capture groups as a JSON object.
If the text matches the pattern, the result contains one entry for each capture group that matched:
- key `0` contains the full match
- named groups like `(?<name>...)` use their name as the JSON key
- unnamed groups like `( ... )` use their numeric index as a string
If the text does not match, this function returns `NULL`.
### Example: custom routing from `404.sql`
This function is especially useful in a custom [`404.sql` page](/your-first-sql-website/custom_urls.sql),
where you want to turn a dynamic URL into variables your SQL can use.
For example, suppose you want `/categories/{category}/post/{id}` URLs such as `/categories/sql/post/42`,
but there is no physical `categories/sql/post/42.sql` file on disk.
You can put a `categories/404.sql` file in your project and extract the dynamic parts from the URL:
#### `categories/404.sql`
```sql
set route = sqlpage.regex_match(
''/categories/(?<category>\w+)/post/(?<id>\d+)'',
sqlpage.path()
);
select ''redirect'' as component, ''/404'' as link
where $route is null;
select ''text'' as component;
select
''Category: '' || ($route->>''category'') || '' | Post id: '' || ($route->>''id'') as contents;
```
If the current path is `/categories/sql/post/42`, `sqlpage.regex_match()` returns:
```json
{"0":"/categories/sql/post/42","category":"sql","id":"42"}
```
You can then use those extracted values to query your database:
```sql
select title, body
from posts
where category = $route->>''category''
and id = cast($route->>''id'' as integer);
```
### Details
- Quick regex reminder:
- `\w+` matches one or more "word" characters
- `\d+` matches one or more digits
- `(?<name>...)` creates a named capture group
- Some databases, such as MySQL and MariaDB, treat backslashes specially inside SQL strings.
In those databases, you may need to write `\\w` and `\\d`, or use portable character classes such as `[A-Za-z0-9_]` and `[0-9]` instead.
- In SQLite, PostgreSQL, and some other databases, you can read fields from the returned JSON with `->` and `->>`
- On databases that do not support that syntax, use their JSON extraction function instead, such as `json_extract($route, ''$.category'')`
- For the full regular expression syntax supported by SQLPage, see the Rust `regex` crate documentation:
[regex syntax reference](https://docs.rs/regex/latest/regex/#syntax)
- If the input text is `NULL`, the function returns `NULL`
- If an optional capture group does not match, that key is omitted from the JSON object
- If the regular expression is invalid, SQLPage returns an error
The returned JSON can then be processed with your database''s JSON functions.
'
);
INSERT INTO
sqlpage_function_parameters (
"function",
"index",
"name",
"description_md",
"type"
)
VALUES
(
'regex_match',
1,
'pattern',
'The regular expression pattern to match against the input text. Named capture groups such as `(?<name>...)` are supported.',
'TEXT'
),
(
'regex_match',
2,
'text',
'The text to match against the regular expression. Returns `NULL` when this argument is `NULL`.',
'TEXT'
);
@@ -0,0 +1,32 @@
CREATE VIRTUAL TABLE documentation_fts USING fts5(
component_name,
component_description,
parameter_name,
parameter_description,
blog_title,
blog_description,
function_name,
function_description,
function_parameter_name,
function_parameter_description,
component_example_description,
component_example_json
);
INSERT INTO documentation_fts(component_name, component_description)
SELECT name, description FROM component;
INSERT INTO documentation_fts(component_name, parameter_name, parameter_description)
SELECT component, name, description FROM parameter;
INSERT INTO documentation_fts(blog_title, blog_description)
SELECT title, description FROM blog_posts;
INSERT INTO documentation_fts(function_name, function_description)
SELECT name, description_md FROM sqlpage_functions;
INSERT INTO documentation_fts(function_name, function_parameter_name, function_parameter_description)
SELECT function, name, description_md FROM sqlpage_function_parameters;
INSERT INTO documentation_fts(component_name, component_example_description, component_example_json)
SELECT component, description, properties FROM example;
@@ -0,0 +1,59 @@
INSERT INTO parameter(component, top_level, name, description, type, optional)
SELECT *, 'id', 'id attribute added to the container in HTML. It can be used to target this item through css or for scrolling to this item through links (use "#id" in link url).', 'TEXT', TRUE
FROM (VALUES
('alert', TRUE),
('breadcrumb', TRUE),
('chart', TRUE),
('code', TRUE),
('csv', TRUE),
('datagrid', TRUE),
('datagrid', FALSE),
('hero', TRUE),
('list', TRUE),
('list', FALSE),
('map', TRUE),
('tab', FALSE),
('table', TRUE),
('timeline', TRUE),
('timeline', FALSE),
('title', TRUE),
('tracking', TRUE),
('text', TRUE),
('carousel', TRUE),
('login', TRUE),
('pagination', TRUE)
);
INSERT INTO parameter(component, top_level, name, description, type, optional)
SELECT *, 'id', 'id attribute injected as an anchor in HTML. It can be used for scrolling to this item through links (use "#id" in link url). Added in v0.18.0.', 'TEXT', TRUE
FROM (VALUES
('steps', TRUE)
);
INSERT INTO parameter(component, top_level, name, description, type, optional)
SELECT *, 'class', 'class attribute added to the container in HTML. It can be used to apply custom styling to this item through css. Added in v0.18.0.', 'TEXT', TRUE
FROM (VALUES
('alert', TRUE),
('breadcrumb', TRUE),
('button', TRUE),
('card', FALSE),
('chart', TRUE),
('code', TRUE),
('csv', TRUE),
('datagrid', TRUE),
('divider', TRUE),
('form', TRUE),
('list', TRUE),
('list', FALSE),
('map', TRUE),
('tab', FALSE),
('table', TRUE),
('timeline', TRUE),
('timeline', FALSE),
('title', TRUE),
('tracking', TRUE),
('carousel', TRUE),
('login', TRUE),
('pagination', TRUE)
);