diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7eb315db..47625c5d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -54,7 +54,7 @@ jobs:
db_url: "mssql://root:Password123!@127.0.0.1/sqlpage"
- database: odbc
container: postgres
- db_url: "Driver=/usr/lib/x86_64-linux-gnu/odbc/psqlodbcw.so;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!"
+ db_url: "Driver=PostgreSQL Unicode;Server=127.0.0.1;Port=5432;Database=sqlpage;UID=root;PWD=Password123!"
setup_odbc: true
steps:
- uses: actions/checkout@v4
@@ -74,6 +74,8 @@ jobs:
env:
DATABASE_URL: ${{ matrix.db_url }}
RUST_BACKTRACE: 1
+ MALLOC_CHECK_: 3
+ MALLOC_PERTURB_: 10
windows_test:
runs-on: windows-latest
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66bdae84..1dce2696 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,22 @@
# CHANGELOG.md
+## v0.39.1 (unreleased)
+ - More precise server timing tracking to debug performance issues
+ - Fix missing server timing header in some cases
+ - Implement nice error messages for some header-related errors such as invalid header values.
+
+## v0.39.0 (2025-10-28)
+ - 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.
+ - The shell with a vertical sidebar can now have "active" elements, just like the horizontal header bar.
+ - New `edit_url`, `delete_url`, and `custom_actions` properties in the [table](https://sql-page.com/component.sql?component=table) component to easily add nice icon buttons to a table.
+ - SQLPage now sets the [`Server-Timing` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing) in development. So when you have a page that loads slowly, you can open your browser's network inspector, click on the slow request, then open the timing tab to understand where it's spending its time.
+ -
+ - Fixed a memory corruption issue in the builtin odbc driver manager
+ - ODBC: fix using globally installed system drivers by their name in debian-based linux distributions.
+ - New [login](https://sql-page.com/component.sql?component=table) component.
+
+
## v0.38.0
- Added support for the Open Database Connectivity (ODBC) standard.
- This makes SQLPage compatible with many new databases, including:
diff --git a/Cargo.lock b/Cargo.lock
index 6ae80844..96f46222 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,14 +31,14 @@ dependencies = [
"actix-tls",
"actix-utils",
"base64 0.22.1",
- "bitflags 2.9.4",
+ "bitflags 2.10.0",
"brotli 8.0.2",
"bytes",
"bytestring",
"derive_more 2.0.1",
"encoding_rs",
"flate2",
- "foldhash",
+ "foldhash 0.1.5",
"futures-core",
"h2",
"http 0.2.12",
@@ -214,7 +214,7 @@ dependencies = [
"cookie",
"derive_more 2.0.1",
"encoding_rs",
- "foldhash",
+ "foldhash 0.1.5",
"futures-core",
"futures-util",
"impl-more",
@@ -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",
@@ -872,9 +872,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.41"
+version = "1.2.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7"
+checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -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"
@@ -936,9 +936,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.49"
+version = "4.5.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f"
+checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
dependencies = [
"clap_builder",
"clap_derive",
@@ -946,9 +946,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.49"
+version = "4.5.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730"
+checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
dependencies = [
"anstream",
"anstyle",
@@ -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",
]
@@ -1382,9 +1382,9 @@ dependencies = [
[[package]]
name = "deranged"
-version = "0.5.4"
+version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071"
+checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
"serde_core",
@@ -1722,9 +1722,9 @@ checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
[[package]]
name = "flate2"
-version = "1.1.4"
+version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9"
+checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -1753,6 +1753,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -1984,7 +1990,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
- "indexmap 2.11.4",
+ "indexmap 2.12.0",
"slab",
"tokio",
"tokio-util",
@@ -2018,10 +2024,6 @@ name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
-dependencies = [
- "ahash",
- "allocator-api2",
-]
[[package]]
name = "hashbrown"
@@ -2029,7 +2031,7 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
- "foldhash",
+ "foldhash 0.1.5",
]
[[package]]
@@ -2037,6 +2039,11 @@ name = "hashbrown"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.2.0",
+]
[[package]]
name = "hashlink"
@@ -2326,9 +2333,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",
@@ -2338,9 +2345,9 @@ dependencies = [
[[package]]
name = "is_terminal_polyfill"
-version = "1.70.1"
+version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
@@ -2424,9 +2431,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.81"
+version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
+checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -2515,9 +2522,9 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libflate"
-version = "2.1.0"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e"
+checksum = "249fa21ba2b59e8cbd69e722f5b31e1b466db96c937ae3de23e8b99ead0d1383"
dependencies = [
"adler32",
"core2",
@@ -2528,12 +2535,12 @@ dependencies = [
[[package]]
name = "libflate_lz77"
-version = "2.1.0"
+version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d"
+checksum = "a599cb10a9cd92b1300debcef28da8f70b935ec937f44fcd1b70a7c986a11c5c"
dependencies = [
"core2",
- "hashbrown 0.14.5",
+ "hashbrown 0.16.0",
"rle-decode-fast",
]
@@ -2559,7 +2566,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",
]
@@ -2685,14 +2692,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]]
@@ -2701,7 +2708,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 +2822,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 +2832,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 +2883,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 +2899,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 +2923,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 +2965,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 +2990,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 +3002,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 +3025,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 +3057,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",
@@ -3073,8 +3080,9 @@ dependencies = [
[[package]]
name = "odbc-sys"
-version = "0.27.3"
-source = "git+https://github.com/sqlpage/odbc-sys?branch=no-autotools#ae3e15446bb2c5c191f05e7c6affc37dfd6fcabe"
+version = "0.27.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1896e52e97c2f0cf997cc627380f1af1ecb3f6c29ce6175047cd38adaadb46f5"
dependencies = [
"unix-odbc",
]
@@ -3096,9 +3104,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
-version = "1.70.1"
+version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "openidconnect"
@@ -3474,9 +3482,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.101"
+version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
@@ -3589,7 +3597,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 +3703,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 +3768,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 +3781,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",
@@ -3782,9 +3790,9 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.23.32"
+version = "0.23.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40"
+checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
dependencies = [
"aws-lc-rs",
"log",
@@ -3823,9 +3831,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",
@@ -3844,9 +3852,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
-version = "1.12.0"
+version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
dependencies = [
"zeroize",
]
@@ -3943,7 +3951,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 +4032,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",
@@ -4075,15 +4083,15 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.15.0"
+version = "3.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5"
+checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04"
dependencies = [
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
- "indexmap 2.11.4",
+ "indexmap 2.12.0",
"schemars 0.9.0",
"schemars 1.0.4",
"serde_core",
@@ -4094,9 +4102,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.15.0"
+version = "3.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27"
+checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955"
dependencies = [
"darling 0.21.3",
"proc-macro2",
@@ -4219,7 +4227,7 @@ dependencies = [
[[package]]
name = "sqlpage"
-version = "0.38.0"
+version = "0.39.0"
dependencies = [
"actix-multipart",
"actix-rt",
@@ -4298,7 +4306,7 @@ dependencies = [
"atoi",
"base64 0.22.1",
"bigdecimal",
- "bitflags 2.9.4",
+ "bitflags 2.10.0",
"byteorder",
"bytes",
"chrono",
@@ -4320,7 +4328,7 @@ dependencies = [
"hex",
"hkdf",
"hmac",
- "indexmap 2.11.4",
+ "indexmap 2.12.0",
"itoa",
"libc",
"libsqlite3-sys",
@@ -4425,9 +4433,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
-version = "2.0.106"
+version = "2.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
dependencies = [
"proc-macro2",
"quote",
@@ -4654,7 +4662,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",
@@ -4772,9 +4780,9 @@ checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580"
[[package]]
name = "unicode-ident"
-version = "1.0.19"
+version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
+checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
[[package]]
name = "unicode-normalization"
@@ -4805,8 +4813,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unix-odbc"
-version = "0.1.2"
-source = "git+https://github.com/sqlpage/odbc-sys?branch=no-autotools#ae3e15446bb2c5c191f05e7c6affc37dfd6fcabe"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8579f2e2aaba57c09f10990cf9ab50eef8c0155820ed8a72d962c1c05af4a8a"
dependencies = [
"cc",
]
@@ -4905,9 +4914,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
-version = "0.2.104"
+version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
+checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
dependencies = [
"cfg-if",
"once_cell",
@@ -4916,25 +4925,11 @@ dependencies = [
"wasm-bindgen-shared",
]
-[[package]]
-name = "wasm-bindgen-backend"
-version = "0.2.104"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
-dependencies = [
- "bumpalo",
- "log",
- "proc-macro2",
- "quote",
- "syn",
- "wasm-bindgen-shared",
-]
-
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.54"
+version = "0.4.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c"
+checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
dependencies = [
"cfg-if",
"js-sys",
@@ -4945,9 +4940,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.104"
+version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
+checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4955,31 +4950,31 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.104"
+version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
+checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
dependencies = [
+ "bumpalo",
"proc-macro2",
"quote",
"syn",
- "wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.104"
+version = "0.2.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
+checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
-version = "0.3.81"
+version = "0.3.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120"
+checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -5337,7 +5332,7 @@ checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732"
dependencies = [
"android-activity",
"atomic-waker",
- "bitflags 2.9.4",
+ "bitflags 2.10.0",
"block2",
"calloop",
"cfg_aliases",
@@ -5413,7 +5408,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/Cargo.toml b/Cargo.toml
index c7248abd..465464e9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "sqlpage"
-version = "0.38.0"
+version = "0.39.0"
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"]
@@ -79,7 +79,7 @@ clap = { version = "4.5.17", features = ["derive"] }
tokio-util = "0.7.12"
openidconnect = { version = "4.0.0", default-features = false }
encoding_rs = "0.8.35"
-odbc-sys = { version = "0.27.1", optional = true }
+odbc-sys = { version = "0.27.4", optional = true }
[features]
@@ -87,10 +87,6 @@ default = []
odbc-static = ["odbc-sys", "odbc-sys/vendored-unix-odbc"]
lambda-web = ["dep:lambda-web", "odbc-static"]
-
-[patch.crates-io]
-odbc-sys = { git = "https://github.com/sqlpage/odbc-sys", branch = "no-autotools" }
-
[build-dependencies]
awc = { version = "3", features = ["rustls-0_23-webpki-roots"] }
rustls = "0.23"
diff --git a/README.md b/README.md
index c25c8867..d1692f5d 100644
--- a/README.md
+++ b/README.md
@@ -164,12 +164,12 @@ To run on a server, you can use [the docker image](https://hub.docker.com/r/lova
custom components, and migrations
(see [configuration.md](./configuration.md)) to `/etc/sqlpage` in the container.
- For instance, you can use:
- - `docker run -it --name sqlpage -p 8080:8080 --volume "$(pwd)/source:/var/www" --volume "$(pwd)/configuration:/etc/sqlpage:ro" --rm sqlpage/SQLPage`
+ - `docker run -it --name sqlpage -p 80:8080 --volume "$(pwd)/source:/var/www" --volume "$(pwd)/configuration:/etc/sqlpage:ro" --rm lovasoa/sqlpage`
- And place your website in a folder named `source` and your `sqlpage.json` in a folder named `configuration`.
- If you want to build your own docker image, taking the raw sqlpage image as a base is not recommended, since it is extremely stripped down and probably won't contain the dependencies you need. Instead, you can take debian as a base and simply copy the sqlpage binary from the official image to your own image:
- ```Dockerfile
FROM debian:stable-slim
- COPY --from=sqlpage/SQLPage:main /usr/local/bin/sqlpage /usr/local/bin/sqlpage
+ COPY --from=lovasoa/sqlpage:main /usr/local/bin/sqlpage /usr/local/bin/sqlpage
```
We provide compiled binaries only for the x86_64 architecture, but provide docker images for other architectures, including arm64 and armv7. If you want to run SQLPage on a Raspberry Pi or
@@ -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)
@@ -362,4 +359,4 @@ Check out our [Contributing Guide](./CONTRIBUTING.md) for detailed instructions
Our windows binaries are digitally signed, so they should be recognized as safe by Windows.
Free code signing provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/). [Contributors](https://github.com/sqlpage/SQLPage/graphs/contributors), [Owners](https://github.com/orgs/sqlpage/people?query=role%3Aowner).
-This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it
\ No newline at end of file
+This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it
diff --git a/build.rs b/build.rs
index a39ea959..ed14c612 100644
--- a/build.rs
+++ b/build.rs
@@ -29,6 +29,7 @@ async fn main() {
] {
h.await.unwrap();
}
+ set_odbc_rpath();
}
fn make_client() -> awc::Client {
@@ -171,3 +172,14 @@ fn make_url_path(url: &str) -> PathBuf {
);
sqlpage_artefacts.join(filename)
}
+
+/// On debian-based linux distributions, odbc drivers are installed in /usr/lib/-linux-gnu/odbc
+/// which is not in the default library search path.
+fn set_odbc_rpath() {
+ if cfg!(all(target_os = "linux", feature = "odbc-static")) {
+ println!(
+ "cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/{}-linux-gnu/odbc",
+ std::env::var("TARGET").unwrap().split('-').next().unwrap()
+ );
+ }
+}
diff --git a/examples/official-site/examples/authentication/create_session_token.sql b/examples/official-site/examples/authentication/create_session_token.sql
index 645fedc4..8ea8cd19 100644
--- a/examples/official-site/examples/authentication/create_session_token.sql
+++ b/examples/official-site/examples/authentication/create_session_token.sql
@@ -4,12 +4,12 @@ delete from user_sessions where created_at < datetime('now', '-1 day');
-- check that the
SELECT 'authentication' AS component,
'login.sql?failed' AS link, -- redirect to the login page on error
- (SELECT password_hash FROM users WHERE username = :Username) AS password_hash, -- this is a hash of the password 'admin'
- :Password AS password; -- this is the password that the user sent through our form in 'index.sql'
+ (SELECT password_hash FROM users WHERE username = :username) AS password_hash, -- this is a hash of the password 'admin'
+ :password AS password; -- this is the password that the user sent through our form in 'index.sql'
-- if we haven't been redirected, then the password is correct
-- create a new session
-insert into user_sessions (session_token, username) values (sqlpage.random_string(32), :Username)
+insert into user_sessions (session_token, username) values (sqlpage.random_string(32), :username)
returning 'cookie' as component, 'session_token' as name, session_token as value;
-- redirect to the authentication example home page
diff --git a/examples/official-site/examples/authentication/login.sql b/examples/official-site/examples/authentication/login.sql
index b634de30..b0a39f5b 100644
--- a/examples/official-site/examples/authentication/login.sql
+++ b/examples/official-site/examples/authentication/login.sql
@@ -1,10 +1,15 @@
select 'dynamic' as component, properties FROM example WHERE component = 'shell' LIMIT 1;
-select 'form' as component, 'Authentication' as title, 'Log in' as validate, 'create_session_token.sql' as action;
-select 'Username' as name, 'user' as prefix_icon, 'admin' as placeholder;
-select 'Password' as name, 'lock' as prefix_icon, 'admin' as placeholder, 'password' as type;
-
-select 'alert' as component, 'danger' as color, 'Invalid username or password' as title where $failed is not null;
+select
+ 'login' as component,
+ 'create_session_token.sql' as action,
+ '/assets/icon.webp' as image,
+ 'Demo Login Form' as title,
+ 'Username' as username,
+ 'Password' as password,
+ case when $failed is not null then 'Invalid username or password. In this demo, you can log in with admin / admin.' end as error_message,
+ 'In this demo, the username is "admin" and the password is "admin".' as footer_md,
+ 'Log in' as validate;
select 'text' as component, '
@@ -12,7 +17,7 @@ select 'text' as component, '
This is a simple example of an authentication form.
It uses
- - the [`form`](/documentation.sql?component=form#component) component to create a login form
+ - the [`login`](/documentation.sql?component=login#component) component to create a login form
- the [`authentication`](/documentation.sql?component=authentication#component) component to check the user password
- the [`cookie`](/documentation.sql?component=cookie#component) component to store a unique session token in the user browser
- the [`redirect`](/documentation.sql?component=redirect#component) component to redirect the user to the login page if they are not logged in
diff --git a/examples/official-site/extensions-to-sql.md b/examples/official-site/extensions-to-sql.md
index 81ee0902..88fa4067 100644
--- a/examples/official-site/extensions-to-sql.md
+++ b/examples/official-site/extensions-to-sql.md
@@ -1,257 +1,262 @@
-# Extensions to SQL
+## How SQLPage runs your SQL
-SQLPage makes some special treatment before executing your SQL queries.
+SQLPage reads your SQL file and runs one statement at a time. For each statement, it
-When executing your SQL file, SQLPage executes each query one at a time.
-It doesn't send the whole file as-is to the database engine.
+- decides whether to:
+ - handle it inside SQLPage, or
+ - prepare it as a (potentially slightly modified) sql statement on the database.
+- extracts values from the request to pass them as prepared statements parameters
+- runs [`sqlpage.*` functions](/functions)
+- passes the database results to components
-## Performance
+This page explains every step of the process,
+with examples and details about differences between how SQLPage understands SQL and how your database does.
-See the [performance page](/performance.sql) for details on the optimizations
-made to run your queries as fast as possible.
+## What runs where
-## Variables
+### Handled locally by SQLPage
-SQL doesn't have its own mechanism for variables.
-SQLPage implements variables in the following way:
+- Static simple selects (a tiny, fast subset of SELECT)
+- Simple variable assignments that use only literals or variables
+ - All sqlpage functions
+
-### POST parameters
+### Sent to your database
-When sending a POST request, most often by sending a form with the
-[form component](/component.sql?component=form), the form data is made
-available as variables prefixed by a colon.
+Everything else: joins, subqueries, arithmetic, database functions, `SELECT @@VERSION`, `CURRENT_TIMESTAMP`, `SELECT *`, expressions, `FROM`, `WHERE`, `GROUP BY`, `ORDER BY`, `LIMIT`/`FETCH`, `WITH`, `DISTINCT`, etc.
-So when this form is sent:
+### Mixed statements using `sqlpage.*` functions
-`form.sql`
-```sql
-SELECT
- 'form' AS component,
- 'POST' AS method, -- form defaults to using the HTTP POST method
- 'result.sql' AS action;
+[`sqlpage.*` functions](/functions.sql) are executed by SQLPage; your database never sees them. They can run:
-SELECT
- 'age' AS name,
- 'How old are you?' AS label,
- 'number' AS type;
-```
+- Before the query, when used as values inside conditions or parameters.
+- After the query, when used as top-level selected columns (applied per row).
+
+Examples are shown below.
+
+## Static simple selects
+
+A *static simple select* is a very restricted `SELECT` that SQLPage can execute entirely by itself. This avoids back and forths between SQLPage and the database for trivial queries.
+
+To be static and simple, a statement must satisfy all of the following:
+
+- No `FROM`, `WHERE`, `GROUP BY`, `HAVING`, `ORDER BY`, `LIMIT`/`FETCH`, `WITH`, `DISTINCT`, `TOP`, windowing, locks, or other clauses.
+- Each selected item is of the form `value AS alias`.
+- Each `value` is either:
+ - a literal (single-quoted string, number, boolean, or `NULL`), or
+ - a variable (like `$name`, `:message`)
+
+That’s it. If any part is more complex, it is not a static simple select and will be sent to the database.
-It will make a request to this page:
+#### Examples that ARE static (executed by SQLPage)
-`result.sql`
```sql
-SELECT
- 'text' AS component,
- 'You are ' || :age || ' years old!' AS contents;
+SELECT 'text' AS component, 'Hello' AS contents;
+SELECT 'text' AS component, $name AS contents;
```
-`:age` will be substituted by the actual value of the POST parameter.
+#### Examples that are NOT static (sent to the database)
-### URL parameters
+```sql
+-- Has string concatenation
+select 'from' as component, 'handle_form.sql?id=' || $id as action;
+
+-- Has WHERE
+select 'text' as component, $alert_message as contents where $should_alert;
+
+-- Uses database functions or expressions
+SELECT 1 + 1 AS two;
+SELECT CURRENT_TIMESTAMP AS now;
+SELECT @@VERSION AS version; -- SQL Server variables
+-- Uses a subquery
+SELECT (select 1) AS one;
+```
-Likewise, URL parameters are available as variables prefixed by a dollar sign.
+## Variables
+
+SQLPage communicates information about incoming HTTP requests to your SQL code through prepared statement variables.
+You can use
+ - `$var` to reference a GET variable (an URL parameter),
+ - `:var` to reference a POST variable (a value filled by an user in a form field),
+ - `set var = ...` to set the value of `$var`.
-> URL parameters are often called GET parameters because they can originate
-> from a form with 'GET' as the method.
+### POST parameters
-So the previous example can be reworked to handle URL parameters:
+Form fields sent with POST are available as `:name`.
-`result.sql`
```sql
SELECT
- 'text' AS component,
- 'You are ' || $age || ' years old!' AS contents;
+ 'form' AS component,
+ 'POST' AS method,
+ 'result.sql' AS action;
+
+SELECT 'age' AS name, 'How old are you?' AS label, 'number' AS type;
```
-By querying this page with this URL: `/request.sql?age=42`
-we would get `You are 42 years old!` as a response.
+```sql
+-- result.sql
+SELECT 'text' AS component, 'You are ' || :age || ' years old!' AS contents;
+```
-### The `SET` command
+### URL parameters
-SQLPage overrides the behavior of `SET` statements in SQL to store variables in SQLPage itself instead of running the statement on the database.
+Query-string parameters are available as `$name`.
```sql
-SET coalesced_post_id = COALESCE($post_id, 0);
+SELECT 'text' AS component, 'You are ' || $age || ' years old!' AS contents;
+-- /result.sql?age=42 → You are 42 years old!
```
-`SET` statements are transformed into `SELECT` queries, and their result is stored in a `$`-variable:
+When a URL parameter is not set, its value is `NULL`.
-```sql
-SELECT COALESCE($post_id, 0);
-```
+### The SET command
-We can override a previous `$`-variable:
+`SET` stores a value in SQLPage (not in the database). Only strings and `NULL` are stored.
```sql
+-- Give a default value to a variable
SET post_id = COALESCE($post_id, 0);
```
-### Limitations
+- If the right-hand side is purely literals/variables, SQLPage computes it directly. See the section about *static simple select* above.
+- If it needs the database (for example, calls a database function), SQLPage runs an internal `SELECT` to compute it and stores the first column of the first row of results.
-`$`-variables and `:`-variables are stored by SQLPage, not in the database.
+Only a single textual value (**string or `NULL`**) is stored.
+`set id = 1` will store the string `'1'`, not the number `1`.
-They can only store a string, or null.
+On databases with a strict type system, such as PostgreSQL, if you need a number, you will need to cast your variables: `select * from post where id = $id::int`.
-As such, they're not designed to store table-valued results.
-They will only store the first value of the first column:
+Complex structures can be stored as json strings.
-```sql
-CREATE TABLE t(a, b);
-INSERT INTO t(a, b) VALUES (1, 2), (3, 4);
+For larger temporary results, prefer temporary tables on your database; do not send them to SQLPage at all.
-SET var = (SELECT * FROM t);
+## `sqlpage.*` functions
--- now $var contains '1'
-```
+Functions under the `sqlpage.` prefix run in SQLPage. See the [functions page](/functions.sql).
-Temporary table-valued results can be stored in two ways.
+They can run:
-## Storing large datasets in the database with temporary tables
+### Before sending the query (as input values)
+
+Used inside conditions or parameters, the function is evaluated first and its result is passed to the database.
-This is the most efficient method to store large values.
```sql
--- Database connections are reused and temporary tables are stored at the
--- connection level, so we make sure the table doesn't exist already
-DROP TABLE IF EXISTS my_temp_table;
-CREATE TEMPORARY TABLE my_temp_table AS
-SELECT a, b
-FROM my_stored_table ...
-
--- Insert data from direct values
-INSERT INTO my_temp_table(a, b)
-VALUES (1, 2), (3, 4);
+SELECT *
+FROM blog
+WHERE slug = sqlpage.path();
```
-## Storing rich structured data in memory using JSON
-
-This can be more convenient, but should only be used for small values, because data
-is copied from the database into SQLPage memory, and to the database again at each use.
+### After receiving results (as top-level selected columns)
-You can use the [JSON functions from your database](/blog.sql?post=JSON+in+SQL%3A+A+Comprehensive+Guide).
+Used as top-level selected columns, the query is rewritten to first fetch the raw column, and the function is applied per row in SQLPage.
-Here are some examples with SQLite:
```sql
--- CREATE TABLE my_table(a, b);
--- INSERT INTO my_table(a, b)
--- VALUES (1, 2), (3, 4);
-
-SET my_json = (
- SELECT json_group_array(a)
- FROM my_table
-);
--- [1, 3]
-
-SET my_json = json_array(1, 2, 3);
--- [1, 2, 3]
+SELECT sqlpage.read_file_as_text(file_path) AS contents
+FROM blog_posts;
```
-## Functions
-
-Functions starting with `sqlpage.` are executed by SQLPage, not by your database engine.
-See the [functions page](/functions.sql) for more details.
+## Performance
-They're either executed before or after the query is run in the database.
+See the [performance page](/performance.sql) for details. In short:
-### Executing functions *before* sending a query to the database
+- Statements sent to the database are prepared and cached.
+- Variables and pre-computed values are bound as parameters.
+- This keeps queries fast and repeatable.
-When they don't process results coming from the database:
+## Working with larger temporary results
-```sql
-SELECT * FROM blog WHERE slug = sqlpage.path()
-```
+### Temporary tables in your database
-`sqlpage.path()` will get replaced by the result of the function.
+When you reuse the same values multiple times in your page,
+store them in a temporary table.
-### Executing functions *after* receiving results from the database
+```sql
+DROP TABLE IF EXISTS filtered_posts;
+CREATE TEMPORARY TABLE filtered_posts AS
+SELECT * FROM posts where category = $category;
-When they process results coming from the database:
+select 'alert' as component, count(*) || 'results' as title
+from filtered_posts;
-```sql
-SELECT sqlpage.read_file_as_text(blog_post_file) AS title
-FROM blog;
+select 'list' as component;
+select name from filtered_posts;
```
-The query executed will be:
+### Small JSON values in variables
+
+Useful for small datasets that you want to keep in memory.
+See the [guide on JSON in SQL](/blog.sql?post=JSON+in+SQL%3A+A+Comprehensive+Guide).
```sql
-SELECT blog_post_file AS title FROM blog;
+set product = (
+ select json_object('name', name, 'price', price)
+ from products where id = $product_id
+);
```
-Then `sqlpage.read_file_as_text()` will be called on each row.
-
-## Implementation details of variables and functions
+## CSV imports
-All queries run by SQLPage in the database are first prepared, then executed.
+When you write a compatible `COPY ... FROM 'field'` statement and upload a file with the matching form field name, SQLPage orchestrates the import:
-Statements are prepared and cached the first time they're encountered by SQLPage.
-Then those cached prepared statements are executed at each run, with parameter substitution.
+- PostgreSQL: the file is streamed directly to the database using `COPY FROM STDIN`; the database performs the import.
+- Other databases: SQLPage reads the CSV and inserts rows using a prepared `INSERT` statement. Options like delimiter, quote, header, escape, and a custom `NULL` string are supported. With a header row, column names are matched by name; otherwise, the order is used.
-All variables and function results are cast as text, to let the
-database query optimizer know only strings (or nulls) will be passed.
-
-Examples:
+Example:
```sql
--- Source query
-SELECT * FROM blog WHERE slug = sqlpage.path();
-
--- Prepared statement (SQLite syntax)
-SELECT * FROM blog WHERE slug = CAST(?1 AS TEXT)
+COPY my_table (col1, col2)
+FROM 'my_csv'
+(DELIMITER ';', HEADER);
```
-```sql
--- Source query
-SET post_id = COALESCE($post_id, 0);
-
--- Prepared statement (SQLite syntax)
-SELECT COALESCE(CAST(?1 AS TEXT), 0)
-```
+The uploaded file should be provided in a form field with `'file' as type, 'my_csv' as name`.
-# Data types
+## Data types
-Each database has its own rich set of data types.
-The data modal in SQLPage itself is simpler, mainly composed of text strings and json objects.
+Each database has its own usually large set of data types.
+SQLPage itself has a much more rudimentary type system.
### From the user to SQLPage
-Form fields and URL parameters may contain arrays. These are converted to JSON strings before processing.
+Form fields and URL parameters in HTTP are fundamentally untyped.
+They are just sequences of bytes. SQLPage requires them to be valid utf8 strings.
-For instance, Loading `users.sql?user[]=Tim&user[]=Tom` will result in a single variable `$user` with the textual value `["Tim", "Tom"]`.
+SQLPage follows the convention that when a parameter name ends with `[]`, it represents an array.
+Arrays in SQLPage are represented as JSON strings.
+
+Example: In `users.sql?user[]=Tim&user[]=Tom`, `$user` becomes `'["Tim", "Tom"]'` (a JSON string exploitable with your database's builtin json functions).
### From SQLPage to the database
-SQLPage sends only text strings (`VARCHAR`) and `NULL`s to the database, since these are the only possible variable and function return values.
+SQLPage sends only strings (`TEXT` or `VARCHAR`) and `NULL`s as parameters.
### From the database to SQLPage
-Each row of data returned by a SQL query is converted to a JSON object before being passed to components.
+Each row returned by the database becomes a JSON object
+before its passed to components:
-- Each column becomes a key in the json object. If a row has two columns of the same name, they become an array in the json object.
-- Each value is converted to the closest JSON value
- - all number types map to json numbers, booleans to booleans, and `NULL` to `null`,
- - all text types map to json strings
- - date and time types map to json strings containing ISO datetime values
- - binary values (BLOBs) map to json strings containing [data URLs](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data)
+- Each column is a key. Duplicate column names turn into arrays.
+- Numbers, booleans, text, and `NULL` map naturally.
+- Dates/times become ISO strings.
+- Binary data (BLOBs) becomes a data URL (with mime type auto-detection).
#### Example
-The following PostgreSQL query:
-
```sql
-select
- 1 as one,
- 'x' as my_array, 'y' as my_array,
- now() as today,
- ''::bytea as my_image;
+SELECT
+ 1 AS one,
+ 'x' AS my_array, 'y' AS my_array,
+ now() AS today,
+ ''::bytea AS my_image;
```
-will result in the following JSON object being passed to components for rendering
+Produces something like:
```json
{
- "one" : 1,
- "my_array" : ["x","y"],
- "today":"2025-08-30T06:40:13.894918+00:00",
- "my_image":"data:image/svg+xml;base64,PHN2Zz48L3N2Zz4="
+ "one": 1,
+ "my_array": ["x", "y"],
+ "today": "2025-08-30T06:40:13.894918+00:00",
+ "my_image": "data:image/svg+xml;base64,PHN2Zz48L3N2Zz4="
}
```
\ No newline at end of file
diff --git a/examples/official-site/llms.txt.sql b/examples/official-site/llms.txt.sql
new file mode 100644
index 00000000..a1f5cda5
--- /dev/null
+++ b/examples/official-site/llms.txt.sql
@@ -0,0 +1,145 @@
+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 (
+ '### [' || c.name || '](/component.sql?component=' || c.name || ')
+
+' || 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 c
+ order by
+ c.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
diff --git a/examples/official-site/sqlpage/migrations/01_documentation.sql b/examples/official-site/sqlpage/migrations/01_documentation.sql
index ee2b8b51..dabd6d76 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. 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_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). Added in v0.39.0', 'JSON', FALSE, TRUE)
) x;
INSERT INTO example(component, description, properties) VALUES
@@ -994,7 +998,124 @@ 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 Action Buttons in a table.
+
+### 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.
+
+### 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` top-level 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",
+ "edit_url": "/examples/show_variables.sql?action=edit&update_id={id}",
+ "delete_url": "/examples/show_variables.sql?action=delete&delete_id={id}",
+ "custom_actions": [
+ {
+ "name": "history",
+ "tooltip": "View Standard History",
+ "link": "/examples/show_variables.sql?action=history&standard_id={id}",
+ "icon": "history"
+ }
+ ]
+ },
+ {
+ "name": "CalStd",
+ "vendor": "PharmaCo",
+ "Product": "P1234",
+ "lot number": "T23523",
+ "status": "Available",
+ "expires on": "2026-10-13",
+ "_sqlpage_id": 32,
+ "_sqlpage_actions": [
+ {
+ "name": "View PDF",
+ "tooltip": "View Presentation",
+ "link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf",
+ "icon": "file-type-pdf"
+ },
+ {
+ "name": "Action",
+ "tooltip": "Set In Use",
+ "link": "/examples/show_variables.sql?action=set_in_use&standard_id=32",
+ "icon": "caret-right"
+ }
+ ]
+ },
+ {
+ "name": "CalStd",
+ "vendor": "PharmaCo",
+ "Product": "P1234",
+ "lot number": "T2352",
+ "status": "In Use",
+ "expires on": "2026-10-14",
+ "_sqlpage_id": 33,
+ "_sqlpage_actions": [
+ {
+ "name": "View PDF",
+ "tooltip": "View Presentation",
+ "link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf",
+ "icon": "file-type-pdf"
+ },
+ {
+ "name": "Action",
+ "tooltip": "Retire Standard",
+ "link": "/examples/show_variables.sql?action=retire&standard_id=33",
+ "icon": "test-pipe-off"
+ }
+ ]
+ },
+ {
+ "name": "CalStd",
+ "vendor": "PharmaCo",
+ "Product": "P1234",
+ "lot number": "A123",
+ "status": "Discarded",
+ "expires on": "2026-09-30",
+ "_sqlpage_id": 31,
+ "_sqlpage_actions": [
+ {
+ "name": "View PDF",
+ "tooltip": "View Presentation",
+ "link": "https://sql-page.com/pgconf/2024-sqlpage-badass.pdf",
+ "icon": "file-type-pdf"
+ },
+ {
+ "name": "Action"
+ }
+ ]
+ }
+]'
+)
+);
+
INSERT INTO component(name, icon, description) VALUES
diff --git a/examples/official-site/sqlpage/migrations/07_authentication.sql b/examples/official-site/sqlpage/migrations/07_authentication.sql
index fd342fc6..c759dee8 100644
--- a/examples/official-site/sqlpage/migrations/07_authentication.sql
+++ b/examples/official-site/sqlpage/migrations/07_authentication.sql
@@ -14,7 +14,7 @@ you have two main options:
- does not require any external service
- gives you fine-grained control over
- which pages and actions are protected
- - the look of the login form
+ - the look of the [login form](?component=login)
- the duration of the session
- the permissions of each user
2. [**Single sign-on**](/sso)
@@ -128,12 +128,10 @@ Then, in all the pages that require authentication, you check if the cookie is p
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`:
+Create a login form in a file called `login.sql` that uses the [login component](?component=login):
```sql
-select ''form'' as component, ''Authentication'' as title, ''Log in'' as validate, ''create_session_token.sql'' as action;
-select ''Username'' as name, ''admin'' as placeholder;
-select ''Password'' as name, ''admin'' as placeholder, ''password'' as type;
+select ''login'' as component;
```
And then, in `create_session_token.sql` :
diff --git a/examples/official-site/sqlpage/migrations/68_login.sql b/examples/official-site/sqlpage/migrations/68_login.sql
new file mode 100644
index 00000000..cce32fae
--- /dev/null
+++ b/examples/official-site/sqlpage/migrations/68_login.sql
@@ -0,0 +1,66 @@
+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`,
+- To know if the user wants their identity to be remembered, you must read the value of the variable `:remember`.
+', '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),
+ ('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.','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',
+ JSON(
+ '[
+ {
+ "component": "login",
+ "action": "login.sql",
+ "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"}]'));
diff --git a/examples/official-site/sqlpage/migrations/99_shared_id_class_attributes.sql b/examples/official-site/sqlpage/migrations/99_shared_id_class_attributes.sql
index 47e66576..df054541 100644
--- a/examples/official-site/sqlpage/migrations/99_shared_id_class_attributes.sql
+++ b/examples/official-site/sqlpage/migrations/99_shared_id_class_attributes.sql
@@ -18,7 +18,8 @@ FROM (VALUES
('title', TRUE),
('tracking', TRUE),
('text', TRUE),
- ('carousel', TRUE)
+ ('carousel', TRUE),
+ ('login', TRUE)
);
INSERT INTO parameter(component, top_level, name, description, type, optional)
@@ -49,6 +50,7 @@ FROM (VALUES
('timeline', FALSE),
('title', TRUE),
('tracking', TRUE),
- ('carousel', TRUE)
+ ('carousel', TRUE),
+ ('login', TRUE)
);
diff --git a/examples/user-authentication/docker-compose.yml b/examples/user-authentication/docker-compose.yml
index 5108ac1e..96b78c0e 100644
--- a/examples/user-authentication/docker-compose.yml
+++ b/examples/user-authentication/docker-compose.yml
@@ -1,6 +1,8 @@
services:
web:
image: lovasoa/sqlpage:main # main is cutting edge, use sqlpage/SQLPage:latest for the latest stable version
+ build:
+ context: "../.."
ports:
- "8080:8080"
volumes:
diff --git a/examples/user-authentication/signin.sql b/examples/user-authentication/signin.sql
index 4057e44d..bab0e883 100644
--- a/examples/user-authentication/signin.sql
+++ b/examples/user-authentication/signin.sql
@@ -1,14 +1,9 @@
-SELECT 'form' AS component,
+SELECT 'login' AS component,
+ 'login.sql' AS action,
'Sign in' AS title,
- 'Sign in' AS validate,
- 'login.sql' AS action;
-
-SELECT 'username' AS name;
-SELECT 'password' AS name, 'password' AS type;
-
-SELECT 'alert' as component,
- 'Sorry' as title,
- 'We could not authenticate you. Please log in or [create an account](signup.sql).' as description_md,
- 'alert-circle' as icon,
- 'red' as color
-WHERE $error IS NOT NULL;
\ No newline at end of file
+ 'Username' AS username,
+ 'Password' AS password,
+ 'user' AS username_icon,
+ 'lock' AS password_icon,
+ case when $error is not null then 'We could not authenticate you. Please log in or [create an account](signup.sql).' end as error_message_md,
+ 'Sign in' AS validate;
\ No newline at end of file
diff --git a/sqlpage/templates/login.handlebars b/sqlpage/templates/login.handlebars
new file mode 100644
index 00000000..46014bfc
--- /dev/null
+++ b/sqlpage/templates/login.handlebars
@@ -0,0 +1,82 @@
+