From 9a383c9cd5ac9b72579ccc8fa649e848d007bed6 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Tue, 14 Oct 2025 23:35:13 +0200 Subject: [PATCH 01/30] Update README.md to clarify ODBC support and driver linking details - Added information about SQLPage's support for ODBC connections. - Clarified that Linux and MacOS binaries include a built-in statically linked ODBC driver manager (unixODBC). - Removed redundant text regarding ODBC support for specific databases. --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c25c8867..b4c99c33 100644 --- a/README.md +++ b/README.md @@ -186,15 +186,12 @@ An alternative for Mac OS users is to use [SQLPage's homebrew package](https://f ### ODBC Setup +SQLPage supports ODBC connections to connect to databases that don't have native drivers. You can skip this section if you want to use one of the built-in database drivers (SQLite, PostgreSQL, MySQL, Microsoft SQL Server). -SQLPage supports ODBC connections to connect to databases that don't have native drivers, such as Oracle, Snowflake, BigQuery, IBM DB2, and many others. - -On Linux, SQLPage supports both dynamic and static ODBC linking. The Docker image uses the system `unixODBC` (dynamic). -Linux and MacOS release binaries are built with a statically linked unixODBC. +Linux and MacOS release binaries conatain a built-in statically linked ODBC driver manager (unixODBC). You still need to install or provide the database-specific ODBC driver for the database you want to connect to. - #### Install your ODBC database driver - [DuckDB](https://duckdb.org/docs/stable/clients/odbc/overview.html) - [Snowflake](https://docs.snowflake.com/en/developer-guide/odbc/odbc) From ac4126b2af2b381f7ccace5727c0bb079e7beb25 Mon Sep 17 00:00:00 2001 From: Spencer Hansen Date: Tue, 14 Oct 2025 17:59:56 -0700 Subject: [PATCH 02/30] #Added support for action buttons in the table component template. * A default edit and delete button can be included by specifying an "edit_url" or "delete_url" * Custom Column based action buttons can be added in the table header using json object array in the custom_actions column. * Custom action buttons can be defined on the row level by specifying _sqlpage_actions. --- .../sqlpage/migrations/01_documentation.sql | 168 +++++++++++++++++- sqlpage/templates/table.handlebars | 45 +++++ 2 files changed, 212 insertions(+), 1 deletion(-) diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql index ee2b8b51..c9630fbf 100644 --- a/examples/official-site/sqlpage/migrations/01_documentation.sql +++ b/examples/official-site/sqlpage/migrations/01_documentation.sql @@ -810,11 +810,15 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('money', 'Name of a numeric column whose values should be displayed as currency amounts, in the currency defined by the `currency` property. This argument can be repeated multiple times.', 'TEXT', TRUE, TRUE), ('currency', 'The ISO 4217 currency code (e.g., USD, EUR, GBP, etc.) to use when formatting monetary values.', 'TEXT', TRUE, TRUE), ('number_format_digits', 'Maximum number of decimal digits to display for numeric values.', 'INTEGER', TRUE, TRUE), + ('edit_url', 'If set, an edit button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the edit button will take the user to that URL.', 'TEXT', TRUE, TRUE), + ('delete_url', 'If set, a delete button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the delete button will take the user to that URL.', 'TEXT', TRUE, TRUE), + ('custom_actions', 'If set, a column of custom action buttons will be added to each row. The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button).', 'JSON', TRUE, TRUE), -- row level ('_sqlpage_css_class', 'For advanced users. Sets a css class on the table row. Added in v0.8.0.', 'TEXT', FALSE, TRUE), ('_sqlpage_color', 'Sets the background color of the row. Added in v0.8.0.', 'COLOR', FALSE, TRUE), ('_sqlpage_footer', 'Sets this row as the table footer. It is recommended that this parameter is applied to the last row. Added in v0.34.0.', 'BOOLEAN', FALSE, TRUE), - ('_sqlpage_id', 'Sets the id of the html tabler row element. Allows you to make links targeting a specific row in a table.', 'TEXT', FALSE, TRUE) + ('_sqlpage_id', 'Sets the id of the html tabler row element. Allows you to make links targeting a specific row in a table.', 'TEXT', FALSE, TRUE), + ('_sqlpage_actions', 'Sets custom action buttons for this specific row in addition to any defined at the table level, The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button).', 'JSON', FALSE, TRUE) ) x; INSERT INTO example(component, description, properties) VALUES @@ -994,9 +998,171 @@ GROUP BY This will generate a table with the stores in the first column, and the items in the following columns, with the quantity sold in each store for each item. ', NULL + ), + ( + 'table', +'# Using row based custom actions in a table + +The table has a column of buttons, each button defined by the `_sqlpage_actions` column at the table level, and by the `_sqlpage_actions` property at the row level. + +```sql + SELECT + name, vendor, product_number, facility_name, + lot_number, status, date_of_expiration, + --Use the unique identifier of the row as the _sqlpage_id property + standard_id AS _sqlpage_id, + --Build an array of objects, each object defining a button with the following properties: name, icon, link, tooltip + json_array(--SQLite specific, refer to your database documentation for the equivalent JSON functions + --The {id} placeholder in the link property will be replaced by the value of the _sqlpage_id property for that row. + json_object(''name'', ''history'', ''tooltip'', ''View Standard History'', ''link'', ''./history.sql?standard_id={id}'', ''icon'', ''history''), + json_object(''name'', ''view_coa'', ''tooltip'', ''View Certificate of Analysis'', ''link'', c_of_a_path, ''icon'', ''file-type-pdf''), + json_object(''name'', ''edit'', ''tooltip'', ''Edit Standard'', ''link'', ''./update.sql?id={id}'', ''icon'', ''pencil''), + --We want different actions based on the status of the standard, so we use a CASE statement to build the appropriate action + CASE + WHEN status = ''Available'' THEN json_object( + ''name'',''Action'', + ''tooltip'',''Set In Use'', + ''link'',''./actions/set_in_use.sql?standard_id='' || standard_id, + ''icon'',''caret-right'' + ) + WHEN status = ''In Use'' THEN json_object( + ''name'',''Action'', + ''tooltip'',''Retire Standard'', + ''link'',''./actions/retire.sql?standard_id='' || standard_id, + ''icon'',''test-pipe-off'' + ) + WHEN status = ''Retired'' THEN json_object( + ''name'',''Action'', + ''tooltip'',''Discard Standard'', + ''link'',''./actions/discard.sql?standard_id='' || standard_id, + ''icon'',''flask-off'' + ) + -- Include an action with no link or icon as a placeholder to keep the buttons aligned and make sure the header is correct. + WHEN status = ''Discarded'' THEN json_object(''name'',''Action'') + + ELSE json_object(''name'',''Action'') + END + ) + AS _sqlpage_actions + FROM standard; + + ``` + + + ' + , + json('[ + { + "component": "table" + }, + { + "name": "CalStd", + "vendor": "PharmaCo", + "product_number": "P1234", + "facility_name": "A Plant", + "lot_number": "T23523", + "status": "Available", + "date_of_expiration": "2026-10-13", + "_sqlpage_id": 32, + "_sqlpage_actions": [ + { + "name": "history", + "tooltip": "View Standard History", + "link": "./history.sql?standard_id={id}", + "icon": "history" + }, + { + "name": "view_coa", + "tooltip": "View Certificate of Analysis", + "link": "/c_of_a\\2025-09-30_22h01m21s_B69baKoz.pdf", + "icon": "file-type-pdf" + }, + { + "name": "edit", + "tooltip": "Edit Standard", + "link": "./update.sql?id={id}", + "icon": "pencil" + }, + { + "name": "Action", + "tooltip": "Set In Use", + "link": "./actions/set_in_use.sql?standard_id=32", + "icon": "caret-right" + } + ] + }, + { + "name": "CalStd", + "vendor": "PharmaCo", + "product_number": "P1234", + "facility_name": "A Plant", + "lot_number": "T2352", + "status": "In Use", + "date_of_expiration": "2026-10-14", + "_sqlpage_id": 33, + "_sqlpage_actions": [ + { + "name": "history", + "tooltip": "View Standard History", + "link": "./history.sql?standard_id={id}", + "icon": "history" + }, + { + "name": "view_coa", + "tooltip": "View Certificate of Analysis", + "link": "/c_of_a\\2025-09-30_22h05m13s_cP7gqMyi.pdf", + "icon": "file-type-pdf" + }, + { + "name": "edit", + "tooltip": "Edit Standard", + "link": "./update.sql?id={id}", + "icon": "pencil" + }, + { + "name": "Action", + "tooltip": "Retire Standard", + "link": "./actions/retire.sql?standard_id=33", + "icon": "test-pipe-off" + } + ] + }, + { + "name": "CalStd", + "vendor": "PharmaCo", + "product_number": "P1234", + "facility_name": "A Plant", + "lot_number": "A123", + "status": "Discarded", + "date_of_expiration": "2026-09-30", + "_sqlpage_id": 31, + "_sqlpage_actions": [ + { + "name": "history", + "tooltip": "View Standard History", + "link": "./history.sql?standard_id={id}", + "icon": "history" + }, + { + "name": "view_coa", + "tooltip": "View Certificate of Analysis", + "link": "#", + "icon": "file-type-pdf" + }, + { + "name": "edit", + "tooltip": "Edit Standard", + "link": "./update.sql?id={id}", + "icon": "pencil" + }, + null + ] + } +]') ); + INSERT INTO component(name, icon, description) VALUES ('csv', 'download', 'Lets the user download data as a CSV file. Each column from the items in the component will map to a column in the resulting CSV. diff --git a/sqlpage/templates/table.handlebars b/sqlpage/templates/table.handlebars index 90958dfe..639f9341 100644 --- a/sqlpage/templates/table.handlebars +++ b/sqlpage/templates/table.handlebars @@ -3,6 +3,7 @@ {{#if (or search initial_search_value)}}
{{/if}} {{/each}} + {{#if ../edit_url}}Edit{{/if}} + {{#if ../delete_url}}Delete{{/if}} + {{#if ../custom_actions}} + {{#each ../custom_actions}} + {{this.name}} + {{/each}} + {{/if}} + {{#if _sqlpage_actions}} + {{#each _sqlpage_actions}} + {{this.name}} + {{/each}} + {{/if}} {{#delay}}{{/delay}} @@ -78,6 +91,38 @@ {{/if~}} {{~/each~}} + {{#if ../edit_url}} + + + {{~icon_img 'edit'~}} + + + {{/if}} + {{#if ../delete_url}} + + + {{~icon_img 'trash'~}} + + + {{/if}} + {{#if ../custom_actions}} + {{#each ../custom_actions}} + + {{!Title property sets the tooltip text}} + {{~icon_img this.icon~}} + + + {{/each}} + {{/if}} + {{#if _sqlpage_actions}} + {{#each _sqlpage_actions}} + + + {{~icon_img this.icon~}} + + + {{/each}} + {{/if}} {{!~ After this has been rendered, if this was a footer, we need to reopen a new From be67d3e4ae990f7a2efb5b7b8676153391b5c528 Mon Sep 17 00:00:00 2001 From: Spencer Hansen Date: Tue, 14 Oct 2025 19:17:56 -0700 Subject: [PATCH 03/30] Modified new table example. --- .../sqlpage/migrations/01_documentation.sql | 135 ++++++------------ 1 file changed, 45 insertions(+), 90 deletions(-) diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql index c9630fbf..b9fae3d9 100644 --- a/examples/official-site/sqlpage/migrations/01_documentation.sql +++ b/examples/official-site/sqlpage/migrations/01_documentation.sql @@ -1001,88 +1001,66 @@ This will generate a table with the stores in the first column, and the items in ), ( 'table', -'# Using row based custom actions in a table +'## Using Action Buttons in a table. -The table has a column of buttons, each button defined by the `_sqlpage_actions` column at the table level, and by the `_sqlpage_actions` property at the row level. +### Preset Actions: `edit_url` & `delete_url` +Since edit and delete are common actions, the `table` component has dedicated `edit_url` and `delete_url` properties to add buttons for these actions. +The value of these properties should be a URL, containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. -```sql - SELECT - name, vendor, product_number, facility_name, - lot_number, status, date_of_expiration, - --Use the unique identifier of the row as the _sqlpage_id property - standard_id AS _sqlpage_id, - --Build an array of objects, each object defining a button with the following properties: name, icon, link, tooltip - json_array(--SQLite specific, refer to your database documentation for the equivalent JSON functions - --The {id} placeholder in the link property will be replaced by the value of the _sqlpage_id property for that row. - json_object(''name'', ''history'', ''tooltip'', ''View Standard History'', ''link'', ''./history.sql?standard_id={id}'', ''icon'', ''history''), - json_object(''name'', ''view_coa'', ''tooltip'', ''View Certificate of Analysis'', ''link'', c_of_a_path, ''icon'', ''file-type-pdf''), - json_object(''name'', ''edit'', ''tooltip'', ''Edit Standard'', ''link'', ''./update.sql?id={id}'', ''icon'', ''pencil''), - --We want different actions based on the status of the standard, so we use a CASE statement to build the appropriate action - CASE - WHEN status = ''Available'' THEN json_object( - ''name'',''Action'', - ''tooltip'',''Set In Use'', - ''link'',''./actions/set_in_use.sql?standard_id='' || standard_id, - ''icon'',''caret-right'' - ) - WHEN status = ''In Use'' THEN json_object( - ''name'',''Action'', - ''tooltip'',''Retire Standard'', - ''link'',''./actions/retire.sql?standard_id='' || standard_id, - ''icon'',''test-pipe-off'' - ) - WHEN status = ''Retired'' THEN json_object( - ''name'',''Action'', - ''tooltip'',''Discard Standard'', - ''link'',''./actions/discard.sql?standard_id='' || standard_id, - ''icon'',''flask-off'' - ) - -- Include an action with no link or icon as a placeholder to keep the buttons aligned and make sure the header is correct. - WHEN status = ''Discarded'' THEN json_object(''name'',''Action'') - - ELSE json_object(''name'',''Action'') - END - ) - AS _sqlpage_actions - FROM standard; - - ``` +### Column with fixed action buttons +You may want to add custom action buttons to your table rows, for instance to view details, download a file, or perform a custom operation. +For this, the `table` component has a `custom_actions` property that lets you define a column of buttons, each button defined by a name, an icon, a link, and an optional tooltip. - ' +### Column with variable action buttons + +The `table` component also supports the row level `_sqlpage_actions` column in your data table. +This is helpful if you want a more complex logic, for instance to disable a button on some rows, or to change the link or icon based on the row data. + +> WARNING! +> If the number of array items in `_sqlpage_actions` is not consistent across all rows, the table may not render correctly. +> You can leave blank spaces by including an object with only the `name` property. + +The table has a column of buttons, each button defined by the `_sqlpage_actions` column at the table level, and by the `_sqlpage_actions` property at the row level. +### `custom_actions` & `_sqlpage_actions` JSON properties. +Each button is defined by the following properties: +* `name`: sets the column header and the tooltip if no tooltip is provided, +* `tooltip`: text to display when hovering over the button, +* `link`: the URL to navigate to when the button is clicked, possibly containing the {id} placeholder that will be replaced by the value of the `_sqlpage_id` property for that row, +* `icon`: the tabler icon name or image link to display on the button + +### Example using all of the above +' , json('[ { - "component": "table" + "component": "table", + "edit_url": "./update.sql?id={id}", + "delete_url": "./delete.sql?id={id}", + "custom_actions": [ + { + "name": "history", + "tooltip": "View Standard History", + "link": "./history.sql?standard_id={id}", + "icon": "history" + } + ] }, { "name": "CalStd", "vendor": "PharmaCo", "product_number": "P1234", - "facility_name": "A Plant", "lot_number": "T23523", "status": "Available", "date_of_expiration": "2026-10-13", "_sqlpage_id": 32, "_sqlpage_actions": [ - { - "name": "history", - "tooltip": "View Standard History", - "link": "./history.sql?standard_id={id}", - "icon": "history" - }, { "name": "view_coa", "tooltip": "View Certificate of Analysis", - "link": "/c_of_a\\2025-09-30_22h01m21s_B69baKoz.pdf", + "link": "/c_of_a/2025-09-30_22h01m21s_B69baKoz.pdf", "icon": "file-type-pdf" }, - { - "name": "edit", - "tooltip": "Edit Standard", - "link": "./update.sql?id={id}", - "icon": "pencil" - }, { "name": "Action", "tooltip": "Set In Use", @@ -1095,30 +1073,17 @@ The table has a column of buttons, each button defined by the `_sqlpage_actions` "name": "CalStd", "vendor": "PharmaCo", "product_number": "P1234", - "facility_name": "A Plant", "lot_number": "T2352", "status": "In Use", "date_of_expiration": "2026-10-14", "_sqlpage_id": 33, "_sqlpage_actions": [ - { - "name": "history", - "tooltip": "View Standard History", - "link": "./history.sql?standard_id={id}", - "icon": "history" - }, { "name": "view_coa", "tooltip": "View Certificate of Analysis", - "link": "/c_of_a\\2025-09-30_22h05m13s_cP7gqMyi.pdf", + "link": "/c_of_a/2025-09-30_22h05m13s_cP7gqMyi.pdf", "icon": "file-type-pdf" }, - { - "name": "edit", - "tooltip": "Edit Standard", - "link": "./update.sql?id={id}", - "icon": "pencil" - }, { "name": "Action", "tooltip": "Retire Standard", @@ -1131,35 +1096,25 @@ The table has a column of buttons, each button defined by the `_sqlpage_actions` "name": "CalStd", "vendor": "PharmaCo", "product_number": "P1234", - "facility_name": "A Plant", "lot_number": "A123", "status": "Discarded", "date_of_expiration": "2026-09-30", "_sqlpage_id": 31, "_sqlpage_actions": [ - { - "name": "history", - "tooltip": "View Standard History", - "link": "./history.sql?standard_id={id}", - "icon": "history" - }, { "name": "view_coa", "tooltip": "View Certificate of Analysis", - "link": "#", + "link": "025-09-30_22h01m21s_B439baKoz.pdf", "icon": "file-type-pdf" }, { - "name": "edit", - "tooltip": "Edit Standard", - "link": "./update.sql?id={id}", - "icon": "pencil" - }, - null + "name": "Action" + } ] } -]') - ); +]' +) +); From 3654d4272bdd1b7ff0d559327b498f026fda3361 Mon Sep 17 00:00:00 2001 From: Spencer Hansen Date: Tue, 14 Oct 2025 19:43:02 -0700 Subject: [PATCH 04/30] Updated class settings in table.handlebars so that action buttons are centered, and _col_{name} is included in new rows. --- sqlpage/templates/table.handlebars | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sqlpage/templates/table.handlebars b/sqlpage/templates/table.handlebars index 639f9341..bf1cd3e4 100644 --- a/sqlpage/templates/table.handlebars +++ b/sqlpage/templates/table.handlebars @@ -53,16 +53,16 @@ {{/if}} {{/each}} - {{#if ../edit_url}}Edit{{/if}} - {{#if ../delete_url}}Delete{{/if}} + {{#if ../edit_url}}Edit{{/if}} + {{#if ../delete_url}}Delete{{/if}} {{#if ../custom_actions}} {{#each ../custom_actions}} - {{this.name}} + {{this.name}} {{/each}} {{/if}} {{#if _sqlpage_actions}} {{#each _sqlpage_actions}} - {{this.name}} + {{this.name}} {{/each}} {{/if}} @@ -92,14 +92,14 @@ {{/if~}} {{~/each~}} {{#if ../edit_url}} - + {{~icon_img 'edit'~}} {{/if}} {{#if ../delete_url}} - + {{~icon_img 'trash'~}} @@ -107,8 +107,8 @@ {{/if}} {{#if ../custom_actions}} {{#each ../custom_actions}} - - {{!Title property sets the tooltip text}} + + {{!Title property sets the tooltip text}} {{~icon_img this.icon~}} @@ -116,8 +116,8 @@ {{/if}} {{#if _sqlpage_actions}} {{#each _sqlpage_actions}} - - + + {{~icon_img this.icon~}} From 83be28bd60d5a94940c5357b9e74628ad7bb13a8 Mon Sep 17 00:00:00 2001 From: Spencer Hansen Date: Tue, 14 Oct 2025 20:23:19 -0700 Subject: [PATCH 05/30] Eliminate clippy redundant else error from src/webserver/http.rs cargo clippy Checking sqlpage v0.38.0 error: redundant else block --> src\webserver\http.rs:493:6 | 493 | } else { | ______^ 494 | | if let Some(domain) = &config.https_domain { 495 | | let mut listen_on_https = listen_on; 496 | | listen_on_https.set_port(443); ... | 511 | | } | |_____^ | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#redundant_else note: the lint level is defined here --> src\lib.rs:1:9 | 1 | #![deny(clippy::pedantic)] | ^^^^^^^^^^^^^^^^ = note: `#[deny(clippy::redundant_else)]` implied by `#[deny(clippy::pedantic)]` help: remove the `else` block and move the contents out | 493 ~ } 494 + if let Some(domain) = &config.https_domain { 495 + let mut listen_on_https = listen_on; 496 + listen_on_https.set_port(443); 497 + log::debug!("Will start HTTPS server on {listen_on_https}"); 498 + let config = make_auto_rustls_config(domain, config); 499 + server = server 500 + .bind_rustls_0_23(listen_on_https, config) 501 + .map_err(|e| bind_error(e, listen_on_https))?; 502 + } else if listen_on.port() == 443 { 503 + bail!("Please specify a value for https_domain in the configuration file. This is required when using HTTPS (port 443)"); 504 + } 505 + if listen_on.port() != 443 { 506 + log::debug!("Will start HTTP server on {listen_on}"); 507 + server = server 508 + .bind(listen_on) 509 + .map_err(|e| bind_error(e, listen_on))?; 510 + } | error: could not compile `sqlpage` (lib) due to 1 previous error --- src/webserver/http.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/webserver/http.rs b/src/webserver/http.rs index 88c9966e..6f163a90 100644 --- a/src/webserver/http.rs +++ b/src/webserver/http.rs @@ -490,24 +490,23 @@ pub async fn run_server(config: &AppConfig, state: AppState) -> anyhow::Result<( } #[cfg(not(target_family = "unix"))] anyhow::bail!("Unix sockets are not supported on your operating system. Use listen_on instead of unix_socket."); - } else { - if let Some(domain) = &config.https_domain { - let mut listen_on_https = listen_on; - listen_on_https.set_port(443); - log::debug!("Will start HTTPS server on {listen_on_https}"); - let config = make_auto_rustls_config(domain, config); - server = server - .bind_rustls_0_23(listen_on_https, config) - .map_err(|e| bind_error(e, listen_on_https))?; - } else if listen_on.port() == 443 { - bail!("Please specify a value for https_domain in the configuration file. This is required when using HTTPS (port 443)"); - } - if listen_on.port() != 443 { - log::debug!("Will start HTTP server on {listen_on}"); - server = server - .bind(listen_on) - .map_err(|e| bind_error(e, listen_on))?; - } + } + if let Some(domain) = &config.https_domain { + let mut listen_on_https = listen_on; + listen_on_https.set_port(443); + log::debug!("Will start HTTPS server on {listen_on_https}"); + let config = make_auto_rustls_config(domain, config); + server = server + .bind_rustls_0_23(listen_on_https, config) + .map_err(|e| bind_error(e, listen_on_https))?; + } else if listen_on.port() == 443 { + bail!("Please specify a value for https_domain in the configuration file. This is required when using HTTPS (port 443)"); + } + if listen_on.port() != 443 { + log::debug!("Will start HTTP server on {listen_on}"); + server = server + .bind(listen_on) + .map_err(|e| bind_error(e, listen_on))?; } log_welcome_message(config); From 81d2dd43e3ae85178e4dc0e65a87998174684065 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Thu, 16 Oct 2025 10:18:17 +0200 Subject: [PATCH 06/30] update deps --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ae80844..39a06518 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -899,9 +899,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -3823,9 +3823,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", From cc3faba20f1b2809ceb7fee31b3bc3bc781866df Mon Sep 17 00:00:00 2001 From: lovasoa Date: Fri, 17 Oct 2025 21:45:05 +0200 Subject: [PATCH 07/30] fix https://github.com/sqlpage/SQLPage/issues/1055 --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 2 +- src/webserver/database/sql.rs | 8 +++++++- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39a06518..f33ffd2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1233,9 +1233,9 @@ dependencies = [ [[package]] name = "csv-core" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] @@ -2685,14 +2685,14 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3782,9 +3782,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" dependencies = [ "aws-lc-rs", "log", @@ -4219,7 +4219,7 @@ dependencies = [ [[package]] name = "sqlpage" -version = "0.38.0" +version = "0.38.1" dependencies = [ "actix-multipart", "actix-rt", diff --git a/Cargo.toml b/Cargo.toml index c7248abd..373c1078 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sqlpage" -version = "0.38.0" +version = "0.38.1" edition = "2021" description = "Build data user interfaces entirely in SQL. A web server that takes .sql files and formats the query result using pre-made configurable professional-looking components." keywords = ["web", "sql", "framework"] diff --git a/src/webserver/database/sql.rs b/src/webserver/database/sql.rs index b624adcf..6403adf7 100644 --- a/src/webserver/database/sql.rs +++ b/src/webserver/database/sql.rs @@ -923,7 +923,13 @@ impl VisitorMut for ParameterExtractor { Expr::Cast { kind: kind @ CastKind::DoubleColon, .. - } if self.db_info.database_type != SupportedDatabase::Postgres => { + } if ![ + SupportedDatabase::Postgres, + SupportedDatabase::Snowflake, + SupportedDatabase::Generic, + ] + .contains(&self.db_info.database_type) => + { log::warn!("Casting with '::' is not supported on your database. \ For backwards compatibility with older SQLPage versions, we will transform it to CAST(... AS ...)."); *kind = CastKind::Cast; From 0184bb9b9a3738f73d27bb45ff0842d5f189c8d8 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Fri, 17 Oct 2025 23:42:33 +0200 Subject: [PATCH 08/30] Enhance routing to support SQL execution for non-SQL file extensions - Updated routing logic to execute SQL files when a request is made for paths with extensions other than .sql. - Added tests to verify that SQL files are executed correctly for paths with non-SQL extensions. - Updated CHANGELOG to reflect this new feature in version 0.38.1. --- CHANGELOG.md | 3 +++ src/webserver/routing.rs | 39 ++++++++++++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66bdae84..eada1c2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # CHANGELOG.md +## v0.38.1 + - Ability to execute sql for URL paths with another extension. If you create sitemap.xml.sql, it will be executed for example.com/sitemap.xml + ## v0.38.0 - Added support for the Open Database Connectivity (ODBC) standard. - This makes SQLPage compatible with many new databases, including: diff --git a/src/webserver/routing.rs b/src/webserver/routing.rs index 9d79e475..1f506c43 100644 --- a/src/webserver/routing.rs +++ b/src/webserver/routing.rs @@ -22,7 +22,8 @@ //! //! #### Paths with other extensions (assets): //! - If the file exists: **Serve** the static file -//! - If not found: Look for custom 404 handlers (see Error Handling below) +//! - If not found but `{path}.sql` exists: **Execute** the SQL file +//! - If neither found: Look for custom 404 handlers (see Error Handling below) //! //! #### Paths without extension: //! - First, try to find `{path}.sql` and **Execute** if found @@ -66,6 +67,13 @@ //! - If favicon.ico exists: Serve favicon.ico //! - Else if 404.sql exists: Execute 404.sql //! - Else: Default 404 +//! +//! Request: GET /api/data.json +//! - If api/data.json exists: Serve api/data.json +//! - Else if api/data.json.sql exists: Execute api/data.json.sql +//! - Else if api/404.sql exists: Execute api/404.sql +//! - Else if 404.sql exists: Execute 404.sql +//! - Else: Default 404 //! ``` use crate::filesystem::FileSystem; @@ -140,12 +148,13 @@ where C: RoutingConfig, { let result = match check_path(path_and_query, config) { - Ok(path) => match path.extension() { + Ok(path) => match path.extension().and_then(|e| e.to_str()) { + Some(SQL_EXTENSION) => find_file_or_not_found(&path, SQL_EXTENSION, store).await?, + Some(extension) => match find_file(&path, extension, store).await? { + Some(action) => action, + None => calculate_route_without_extension(path_and_query, path, store).await?, + }, None => calculate_route_without_extension(path_and_query, path, store).await?, - Some(extension) => { - let ext = extension.to_str().unwrap_or_default(); - find_file_or_not_found(&path, ext, store).await? - } }, Err(action) => action, }; @@ -189,7 +198,7 @@ where path.push(INDEX); find_file_or_not_found(&path, SQL_EXTENSION, store).await } else { - let path_with_ext = path.with_extension(SQL_EXTENSION); + let path_with_ext = PathBuf::from(format!("{}.{SQL_EXTENSION}", path.display())); match find_file_or_not_found(&path_with_ext, SQL_EXTENSION, store).await? { Execute(x) => Ok(Execute(x)), other_action => { @@ -340,6 +349,22 @@ mod tests { assert_eq!(expected, actual); } + + #[tokio::test] + async fn path_with_non_sql_extension_executes_sql_file() { + let actual = do_route("/abc.def", File("abc.def.sql"), None).await; + let expected = execute("abc.def.sql"); + + assert_eq!(expected, actual); + } + + #[tokio::test] + async fn path_with_non_sql_extension_and_site_prefix_executes_sql_file() { + let actual = do_route("/prefix/abc.def", File("abc.def.sql"), Some("/prefix/")).await; + let expected = execute("abc.def.sql"); + + assert_eq!(expected, actual); + } } mod custom_not_found { From c5c4a4fcd5d96bbb9dcd58af9890a75d90b1963d Mon Sep 17 00:00:00 2001 From: lovasoa Date: Fri, 17 Oct 2025 23:43:20 +0200 Subject: [PATCH 09/30] add llms.txt closes https://github.com/sqlpage/SQLPage/issues/980 --- examples/official-site/llms.txt.sql | 93 +++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 examples/official-site/llms.txt.sql diff --git a/examples/official-site/llms.txt.sql b/examples/official-site/llms.txt.sql new file mode 100644 index 00000000..95cd85ef --- /dev/null +++ b/examples/official-site/llms.txt.sql @@ -0,0 +1,93 @@ +select + 'http_header' as component, + 'text/markdown; charset=utf-8' as "Content-Type", + 'inline; filename="llms.txt"' as "Content-Disposition"; + +select + 'shell-empty' as component, + '# SQLPage + +> SQLPage is a SQL-only web application framework. It lets you build entire websites and web applications using nothing but SQL queries. Write `.sql` files, and SQLPage executes them, maps results to UI components (handlebars templates), and streams HTML to the browser. + +SQLPage is designed for developers who are comfortable with SQL but want to avoid the complexity of traditional web frameworks. It works with SQLite, PostgreSQL, MySQL, and Microsoft SQL Server, and through ODBC with any other database that has an ODBC driver installed. + +Key features: +- No backend code needed: Your SQL files are your backend +- Component-based UI: Built-in components for forms, tables, charts, maps, and more +- Database-first: Every HTTP request triggers a sequence of SQL queries from a .sql file, the results are rendered with built-in or custom components, defined as .handlebars files in the sqlpage/templates folder. +- Simple deployment: Single binary with no runtime dependencies +- Secure by default: Parameterized queries prevent SQL injection + +## Getting Started + +- [Introduction to SQLPage: installation, guiding principles, and a first example](/your-first-sql-website/tutorial.md): Complete beginner tutorial covering setup, database connections, forms, and deployment + +## Core Documentation + +- [Components reference](/documentation.sql): List of all ' || ( + select + count(*) + from + component + ) || ' built-in UI components with parameters and examples +- [Functions reference](/functions.sql): SQLPage built-in functions for handling requests, encoding data, and more +- [Configuration guide](https://github.com/sqlpage/SQLPage/blob/main/configuration.md): Complete list of configuration options in sqlpage.json + +## Components + +' || ( + select + group_concat ( + '### [' || name || '](/component.sql?component=' || name || ') + +' || description || ' + +', + '' + ) + from + component + order by + name + ) || ' + +## Functions + +' || ( + select + group_concat ( + '### [sqlpage.' || name || '()](/functions.sql?function=' || name || ') +' || replace ( + replace ( + description_md, + char(10) || '#', + char(10) || '###' + ), + ' ', + ' ' + ), + char(10) + ) + from + sqlpage_functions + order by + name + ) || ' + +## Examples + +- [Authentication example](https://github.com/sqlpage/SQLPage/tree/main/examples/user-authentication): Complete user registration and login system +- [CRUD application](https://github.com/sqlpage/SQLPage/tree/main/examples/CRUD%20-%20Authentication): Create, read, update, delete with authentication +- [Image gallery](https://github.com/sqlpage/SQLPage/tree/main/examples/image%20gallery%20with%20user%20uploads): File upload and image display +- [Todo application](https://github.com/sqlpage/SQLPage/tree/main/examples/todo%20application): Simple CRUD app +- [Master-detail forms](https://github.com/sqlpage/SQLPage/tree/main/examples/master-detail-forms): Working with related data +- [Charts example](https://github.com/sqlpage/SQLPage/tree/main/examples/plots%20tables%20and%20forms): Data visualization + +## Optional + +- [Custom components guide](/custom_components.sql): Create your own handlebars components +- [Safety and security](/safety.sql): Understanding SQL injection prevention +- [Docker deployment](https://github.com/sqlpage/SQLPage#with-docker): Running SQLPage in containers +- [Systemd service](https://github.com/sqlpage/SQLPage/blob/main/sqlpage.service): Production deployment setup +- [Repository structure](https://github.com/sqlpage/SQLPage/blob/main/CONTRIBUTING.md): Project organization and contribution guide +' as html; \ No newline at end of file From b577695eee172f3276c2c24dee0ac0de66d5912c Mon Sep 17 00:00:00 2001 From: lovasoa Date: Fri, 17 Oct 2025 23:51:28 +0200 Subject: [PATCH 10/30] Enhance llms.txt.sql to include detailed parameter documentation - Updated the SQL file to improve the output format for components, adding sections for top-level and row-level parameters. - Each parameter now includes its type, requirement status, and description for better clarity in the generated documentation. --- examples/official-site/llms.txt.sql | 60 +++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/examples/official-site/llms.txt.sql b/examples/official-site/llms.txt.sql index 95cd85ef..a1f5cda5 100644 --- a/examples/official-site/llms.txt.sql +++ b/examples/official-site/llms.txt.sql @@ -38,17 +38,69 @@ Key features: ' || ( select group_concat ( - '### [' || name || '](/component.sql?component=' || name || ') + '### [' || c.name || '](/component.sql?component=' || c.name || ') -' || description || ' +' || c.description || ' + +' || ( + select + case when exists ( + select + 1 + from + parameter + where + component = c.name + and top_level + ) then '#### Top-level parameters + +' || group_concat ( + '- `' || name || '` (' || type || ')' || case when not optional then ' **REQUIRED**' else '' end || ': ' || description, + char(10) + ) + else + '' + end + from + parameter + where + component = c.name + and top_level + ) || ' + +' || ( + select + case when exists ( + select + 1 + from + parameter + where + component = c.name + and not top_level + ) then '#### Row-level parameters + +' || group_concat ( + '- `' || name || '` (' || type || ')' || case when not optional then ' **REQUIRED**' else '' end || ': ' || description, + char(10) + ) + else + '' + end + from + parameter + where + component = c.name + and not top_level + ) || ' ', '' ) from - component + component c order by - name + c.name ) || ' ## Functions From f1375615f4457b871b94bd213bd9542949d655ab Mon Sep 17 00:00:00 2001 From: lovasoa Date: Mon, 20 Oct 2025 18:27:25 +0200 Subject: [PATCH 11/30] always display line info in errors https://github.com/sqlpage/SQLPage/issues/1057 --- CHANGELOG.md | 1 + Cargo.lock | 82 ++++++++++---------- src/webserver/database/error_highlighting.rs | 42 +++++----- src/webserver/database/sql.rs | 31 ++++++-- 4 files changed, 91 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eada1c2e..1cd2975a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## v0.38.1 - Ability to execute sql for URL paths with another extension. If you create sitemap.xml.sql, it will be executed for example.com/sitemap.xml + - Display source line info in errors even when the database does not return a precise error position. In this case, the entire problematic SQL statement is referenced. ## v0.38.0 - Added support for the Open Database Connectivity (ODBC) standard. diff --git a/Cargo.lock b/Cargo.lock index f33ffd2f..3d36bc87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "futures-core", "futures-sink", @@ -31,7 +31,7 @@ dependencies = [ "actix-tls", "actix-utils", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.10.0", "brotli 8.0.2", "bytes", "bytestring", @@ -325,7 +325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.9.4", + "bitflags 2.10.0", "cc", "cesu8", "jni", @@ -699,9 +699,9 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bigdecimal" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" +checksum = "560f42649de9fa436b73517378a147ec21f6c997a546581df4b4b31677828934" dependencies = [ "autocfg", "libm", @@ -718,7 +718,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -740,11 +740,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -862,7 +862,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "log", "polling", "rustix 0.38.44", @@ -1984,7 +1984,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.11.4", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -2326,9 +2326,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", @@ -2559,7 +2559,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "redox_syscall 0.5.18", ] @@ -2701,7 +2701,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys", @@ -2815,9 +2815,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -2825,9 +2825,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2876,7 +2876,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "libc", "objc2", @@ -2892,7 +2892,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2", "objc2-core-location", @@ -2916,7 +2916,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2", "objc2-foundation", @@ -2958,7 +2958,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "dispatch", "libc", @@ -2983,7 +2983,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2", "objc2-foundation", @@ -2995,7 +2995,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2", "objc2-foundation", @@ -3018,7 +3018,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2", "objc2-cloud-kit", @@ -3050,7 +3050,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "objc2", "objc2-core-location", @@ -3589,7 +3589,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -3695,7 +3695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ "base64 0.21.7", - "bitflags 2.9.4", + "bitflags 2.10.0", "serde", "serde_derive", ] @@ -3760,7 +3760,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3773,7 +3773,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -3943,7 +3943,7 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4024,7 +4024,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "itoa", "memchr", "ryu", @@ -4083,7 +4083,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.12.0", "schemars 0.9.0", "schemars 1.0.4", "serde_core", @@ -4298,7 +4298,7 @@ dependencies = [ "atoi", "base64 0.22.1", "bigdecimal", - "bitflags 2.9.4", + "bitflags 2.10.0", "byteorder", "bytes", "chrono", @@ -4320,7 +4320,7 @@ dependencies = [ "hex", "hkdf", "hmac", - "indexmap 2.11.4", + "indexmap 2.12.0", "itoa", "libc", "libsqlite3-sys", @@ -4425,9 +4425,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" dependencies = [ "proc-macro2", "quote", @@ -4654,7 +4654,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "toml_datetime", "toml_parser", "winnow", @@ -5337,7 +5337,7 @@ checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732" dependencies = [ "android-activity", "atomic-waker", - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "calloop", "cfg_aliases", @@ -5413,7 +5413,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dlib", "log", "once_cell", diff --git a/src/webserver/database/error_highlighting.rs b/src/webserver/database/error_highlighting.rs index 4c1c3ef2..7c9a01eb 100644 --- a/src/webserver/database/error_highlighting.rs +++ b/src/webserver/database/error_highlighting.rs @@ -27,41 +27,47 @@ impl std::fmt::Display for NiceDatabaseError { )?; if let sqlx::error::Error::Database(db_err) = &self.db_err { let Some(mut offset) = db_err.offset() else { - return write!(f, "{}", self.query); + write!(f, "{}", self.query)?; + self.show_position_info(f)?; + return Ok(()); }; for line in self.query.lines() { if offset > line.len() { offset -= line.len() + 1; } else { highlight_line_offset(f, line, offset); - if let Some(query_position) = self.query_position { - let start_line = query_position.start.line; - let end_line = query_position.end.line; - if start_line == end_line { - write!(f, "{}: line {}", self.source_file.display(), start_line)?; - } else { - write!( - f, - "{}: lines {} to {}", - self.source_file.display(), - start_line, - end_line - )?; - } - } + self.show_position_info(f)?; break; } } Ok(()) } else { - write!(f, "{}", self.query) + write!(f, "{}", self.query)?; + self.show_position_info(f)?; + Ok(()) } } } +impl NiceDatabaseError { + fn show_position_info(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "\n{}", self.source_file.display())?; + let _: () = if let Some(query_position) = self.query_position { + let start_line = query_position.start.line; + let end_line = query_position.end.line; + if start_line == end_line { + write!(f, ": line {start_line}")?; + } else { + write!(f, ": lines {start_line} to {end_line}")?; + } + }; + Ok(()) + } +} + impl std::error::Error for NiceDatabaseError {} -/// Display a database error with a highlighted line and character offset. +/// Display a database error without any position information #[must_use] pub fn display_db_error( source_file: &Path, diff --git a/src/webserver/database/sql.rs b/src/webserver/database/sql.rs index 6403adf7..eefedbc0 100644 --- a/src/webserver/database/sql.rs +++ b/src/webserver/database/sql.rs @@ -20,8 +20,9 @@ use sqlparser::dialect::{ }; use sqlparser::parser::{Parser, ParserError}; use sqlparser::tokenizer::Token::{self, SemiColon, EOF}; -use sqlparser::tokenizer::{TokenWithSpan, Tokenizer}; +use sqlparser::tokenizer::{Location, Span, TokenWithSpan, Tokenizer}; use sqlx::any::AnyKind; +use std::fmt::Write; use std::ops::ControlFlow; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -241,11 +242,29 @@ fn extract_query_start(stmt: &impl Spanned) -> SourceSpan { } fn syntax_error(err: ParserError, parser: &Parser, sql: &str) -> ParsedStatement { - let location = parser.peek_token_no_skip().span; - ParsedStatement::Error(anyhow::Error::from(err).context(format!( - "Parsing failed: SQLPage couldn't understand the SQL file. Please check for syntax errors:\n\n{}", - quote_source_with_highlight(sql, location.start.line, location.start.column) - ))) + let Span { + start: Location { + line: start_line, + column: start_column, + }, + end: Location { line: end_line, .. }, + } = parser.peek_token_no_skip().span; + + let mut msg = String::from( + "Parsing failed: SQLPage couldn't understand the SQL file. Please check for syntax errors on ", + ); + if start_line == end_line { + write!(&mut msg, "line {start_line}:").unwrap(); + } else { + write!(&mut msg, "lines {start_line} to {end_line}:").unwrap(); + } + write!( + &mut msg, + "\n{}", + quote_source_with_highlight(sql, start_line, start_column) + ) + .unwrap(); + ParsedStatement::Error(anyhow::Error::from(err).context(msg)) } fn dialect_for_db(dbms: SupportedDatabase) -> Box { From 6729e10a01fc442eb981490440c564c50497f755 Mon Sep 17 00:00:00 2001 From: Spencer Hansen Date: Mon, 20 Oct 2025 09:31:36 -0700 Subject: [PATCH 12/30] Added sqlpage version number information to the new components. --- .../official-site/sqlpage/migrations/01_documentation.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql index b9fae3d9..410dfe2f 100644 --- a/examples/official-site/sqlpage/migrations/01_documentation.sql +++ b/examples/official-site/sqlpage/migrations/01_documentation.sql @@ -810,15 +810,15 @@ INSERT INTO parameter(component, name, description, type, top_level, optional) S ('money', 'Name of a numeric column whose values should be displayed as currency amounts, in the currency defined by the `currency` property. This argument can be repeated multiple times.', 'TEXT', TRUE, TRUE), ('currency', 'The ISO 4217 currency code (e.g., USD, EUR, GBP, etc.) to use when formatting monetary values.', 'TEXT', TRUE, TRUE), ('number_format_digits', 'Maximum number of decimal digits to display for numeric values.', 'INTEGER', TRUE, TRUE), - ('edit_url', 'If set, an edit button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the edit button will take the user to that URL.', 'TEXT', TRUE, TRUE), - ('delete_url', 'If set, a delete button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the delete button will take the user to that URL.', 'TEXT', TRUE, TRUE), - ('custom_actions', 'If set, a column of custom action buttons will be added to each row. The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button).', 'JSON', TRUE, TRUE), + ('edit_url', 'If set, an edit button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the edit button will take the user to that URL. Added in v0.39.0', 'TEXT', TRUE, TRUE), + ('delete_url', 'If set, a delete button will be added to each row. The value of this property should be a URL, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row. Clicking the delete button will take the user to that URL. Added in v0.39.0', 'TEXT', TRUE, TRUE), + ('custom_actions', 'If set, a column of custom action buttons will be added to each row. The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button). Added in v0.39.0', 'JSON', TRUE, TRUE), -- row level ('_sqlpage_css_class', 'For advanced users. Sets a css class on the table row. Added in v0.8.0.', 'TEXT', FALSE, TRUE), ('_sqlpage_color', 'Sets the background color of the row. Added in v0.8.0.', 'COLOR', FALSE, TRUE), ('_sqlpage_footer', 'Sets this row as the table footer. It is recommended that this parameter is applied to the last row. Added in v0.34.0.', 'BOOLEAN', FALSE, TRUE), ('_sqlpage_id', 'Sets the id of the html tabler row element. Allows you to make links targeting a specific row in a table.', 'TEXT', FALSE, TRUE), - ('_sqlpage_actions', 'Sets custom action buttons for this specific row in addition to any defined at the table level, The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button).', 'JSON', FALSE, TRUE) + ('_sqlpage_actions', 'Sets custom action buttons for this specific row in addition to any defined at the table level, The value of this property should be a JSON array of objects, each object defining a button with the following properties: `name` (the text to display on the button), `icon` (the tabler icon name or image link to display on the button), `link` (the URL to navigate to when the button is clicked, possibly containing the `{id}` placeholder that will be replaced by the value of the `_sqlpage_id` property for that row), and `tooltip` (optional text to display when hovering over the button). Added in v0.39.0', 'JSON', FALSE, TRUE) ) x; INSERT INTO example(component, description, properties) VALUES From 5d9df45231ab3a68e847826fcee1c30763b320ca Mon Sep 17 00:00:00 2001 From: Spencer Hansen Date: Mon, 20 Oct 2025 09:53:10 -0700 Subject: [PATCH 13/30] Undo changes to http.rs since it is not part of the feature. --- src/webserver/http.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src/webserver/http.rs b/src/webserver/http.rs index 88c9966e..6f163a90 100644 --- a/src/webserver/http.rs +++ b/src/webserver/http.rs @@ -490,24 +490,23 @@ pub async fn run_server(config: &AppConfig, state: AppState) -> anyhow::Result<( } #[cfg(not(target_family = "unix"))] anyhow::bail!("Unix sockets are not supported on your operating system. Use listen_on instead of unix_socket."); - } else { - if let Some(domain) = &config.https_domain { - let mut listen_on_https = listen_on; - listen_on_https.set_port(443); - log::debug!("Will start HTTPS server on {listen_on_https}"); - let config = make_auto_rustls_config(domain, config); - server = server - .bind_rustls_0_23(listen_on_https, config) - .map_err(|e| bind_error(e, listen_on_https))?; - } else if listen_on.port() == 443 { - bail!("Please specify a value for https_domain in the configuration file. This is required when using HTTPS (port 443)"); - } - if listen_on.port() != 443 { - log::debug!("Will start HTTP server on {listen_on}"); - server = server - .bind(listen_on) - .map_err(|e| bind_error(e, listen_on))?; - } + } + if let Some(domain) = &config.https_domain { + let mut listen_on_https = listen_on; + listen_on_https.set_port(443); + log::debug!("Will start HTTPS server on {listen_on_https}"); + let config = make_auto_rustls_config(domain, config); + server = server + .bind_rustls_0_23(listen_on_https, config) + .map_err(|e| bind_error(e, listen_on_https))?; + } else if listen_on.port() == 443 { + bail!("Please specify a value for https_domain in the configuration file. This is required when using HTTPS (port 443)"); + } + if listen_on.port() != 443 { + log::debug!("Will start HTTP server on {listen_on}"); + server = server + .bind(listen_on) + .map_err(|e| bind_error(e, listen_on))?; } log_welcome_message(config); From c371b23af2252dba0e8f8ffbe06601d535f26bc5 Mon Sep 17 00:00:00 2001 From: Spencer Hansen Date: Mon, 20 Oct 2025 10:20:02 -0700 Subject: [PATCH 14/30] Undo changes to http.rs --- src/webserver/http.rs | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/webserver/http.rs b/src/webserver/http.rs index 6f163a90..88c9966e 100644 --- a/src/webserver/http.rs +++ b/src/webserver/http.rs @@ -490,23 +490,24 @@ pub async fn run_server(config: &AppConfig, state: AppState) -> anyhow::Result<( } #[cfg(not(target_family = "unix"))] anyhow::bail!("Unix sockets are not supported on your operating system. Use listen_on instead of unix_socket."); - } - if let Some(domain) = &config.https_domain { - let mut listen_on_https = listen_on; - listen_on_https.set_port(443); - log::debug!("Will start HTTPS server on {listen_on_https}"); - let config = make_auto_rustls_config(domain, config); - server = server - .bind_rustls_0_23(listen_on_https, config) - .map_err(|e| bind_error(e, listen_on_https))?; - } else if listen_on.port() == 443 { - bail!("Please specify a value for https_domain in the configuration file. This is required when using HTTPS (port 443)"); - } - if listen_on.port() != 443 { - log::debug!("Will start HTTP server on {listen_on}"); - server = server - .bind(listen_on) - .map_err(|e| bind_error(e, listen_on))?; + } else { + if let Some(domain) = &config.https_domain { + let mut listen_on_https = listen_on; + listen_on_https.set_port(443); + log::debug!("Will start HTTPS server on {listen_on_https}"); + let config = make_auto_rustls_config(domain, config); + server = server + .bind_rustls_0_23(listen_on_https, config) + .map_err(|e| bind_error(e, listen_on_https))?; + } else if listen_on.port() == 443 { + bail!("Please specify a value for https_domain in the configuration file. This is required when using HTTPS (port 443)"); + } + if listen_on.port() != 443 { + log::debug!("Will start HTTP server on {listen_on}"); + server = server + .bind(listen_on) + .map_err(|e| bind_error(e, listen_on))?; + } } log_welcome_message(config); From 32d6863982bfa90f953fc0f13ec706e6d4848b31 Mon Sep 17 00:00:00 2001 From: Spencer Hansen Date: Mon, 20 Oct 2025 15:48:07 -0700 Subject: [PATCH 15/30] Added checks for active status to dropdown items in shell.handlebars. --- sqlpage/templates/shell.handlebars | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlpage/templates/shell.handlebars b/sqlpage/templates/shell.handlebars index 35f31b91..0e1c7a8e 100644 --- a/sqlpage/templates/shell.handlebars +++ b/sqlpage/templates/shell.handlebars @@ -95,7 +95,7 @@ {{~#with (parse_json this)}} {{#if (or (or this.title this.icon) this.image)}}