Merge branch 'main' into login

This commit is contained in:
lovasoa
2025-10-27 22:50:56 +01:00
22 changed files with 676 additions and 185 deletions
+3 -1
View File
@@ -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
+9 -1
View File
@@ -1,7 +1,15 @@
# CHANGELOG.md
## v0.38.1
## v0.39.0
- 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.
- <img width="1250" height="1263" alt="image" src="https://github.com/user-attachments/assets/6781a31f-e342-4e8c-8506-bc47049ce313" />
- 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.
## v0.38.0
- Added support for the Open Database Connectivity (ODBC) standard.
Generated
+108 -113
View File
@@ -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",
@@ -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",
@@ -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",
]
@@ -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.33"
version = "0.23.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c"
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
dependencies = [
"aws-lc-rs",
"log",
@@ -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.1"
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",
+2 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "sqlpage"
version = "0.38.1"
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"
+3 -3
View File
@@ -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
@@ -359,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
This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it
+12
View File
@@ -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/<target>-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()
);
}
}
@@ -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
+2 -2
View File
@@ -95,7 +95,7 @@
{{~#with (parse_json this)}}
{{#if (or (or this.title this.icon) this.image)}}
<li class="nav-item{{#if this.submenu}} dropdown{{/if}}{{#if this.active}} active{{/if}}">
<a class="nav-link {{#if this.submenu}}dropdown-toggle{{/if}}" href="{{#if this.link}}{{this.link}}{{else}}#{{/if}}"
<a class="nav-link {{#if this.active}}active {{/if}}{{#if this.submenu}}dropdown-toggle{{/if}}" href="{{#if this.link}}{{this.link}}{{else}}#{{/if}}"
{{~#if this.submenu}}
data-bs-toggle="dropdown"
{{!-- commented out until this is fixed: https://github.com/tabler/tabler/issues/2485
@@ -125,7 +125,7 @@
<div class="dropdown-menu dropdown-menu-end" data-bs-popper="static">
{{~#each this.submenu~}}
{{#if (or (or this.title this.icon) this.image)}}
<a class="dropdown-item" href="{{this.link}}" {{#if this.target}}target="{{this.target}}"{{/if}}>
<a class="dropdown-item{{#if this.active}} active{{/if}}" href="{{this.link}}" {{#if this.target}}target="{{this.target}}"{{/if}}>
{{~#if this.image~}}
<span {{~#if this.title}} class="me-1"{{/if}}>
{{~#if (eq ../this.size 'sm')}}
+45
View File
@@ -3,6 +3,7 @@
{{#if (or search initial_search_value)}}
<div class="p-3">
<input
id="{{component_index}}-search"
type="search"
class="form-control form-control-rounded fs-6 search"
placeholder="{{default search_placeholder 'Search…'}}"
@@ -52,6 +53,18 @@
</th>
{{/if}}
{{/each}}
{{#if ../edit_url}}<th class="_col_edit text-center">Edit</th>{{/if}}
{{#if ../delete_url}}<th class="_col_delete text-center">Delete</th>{{/if}}
{{#if ../custom_actions}}
{{#each ../custom_actions}}
<th class="_col_custom text-center">{{this.name}}</th>
{{/each}}
{{/if}}
{{#if _sqlpage_actions}}
{{#each _sqlpage_actions}}
<th class="_col_action text-center">{{this.name}}</th>
{{/each}}
{{/if}}
</tr>
</thead>
<tbody class="table-tbody list">{{#delay}}</tbody>{{/delay}}
@@ -78,6 +91,38 @@
</td>
{{/if~}}
{{~/each~}}
{{#if ../edit_url}}
<td class="align-middle _col_edit">
<a href="{{replace ../edit_url '{id}' _sqlpage_id}}" class="align-middle link-secondary _col_edit" data-action="edit" title="Edit">
{{~icon_img 'edit'~}}
</a>
</td>
{{/if}}
{{#if ../delete_url}}
<td class="align-middle _col_delete text-center">
<a href="{{replace ../delete_url '{id}' _sqlpage_id}}" class="align-middle link-secondary _col_delete" data-action="delete" title="Delete">
{{~icon_img 'trash'~}}
</a>
</td>
{{/if}}
{{#if ../custom_actions}}
{{#each ../custom_actions}}
<td class="align-middle _col_{{this.name}} text-center">
<a href="{{replace this.link '{id}' ../_sqlpage_id}}" class="align-middle link-secondary _col_{{this.name}}" data-action="{{this.name}}" title="{{default this.tooltip this.name}}">{{!Title property sets the tooltip text}}
{{~icon_img this.icon~}}
</a>
</td>
{{/each}}
{{/if}}
{{#if _sqlpage_actions}}
{{#each _sqlpage_actions}}
<td class="align-middle _col_{{this.name}} text-center">
<a href="{{replace this.link '{id}' ../_sqlpage_id}}" class="align-middle link-secondary _col_{{this.name}}" data-action="{{this.name}}" title="{{default this.tooltip this.name}}">
{{~icon_img this.icon~}}
</a>
</td>
{{/each}}
{{/if}}
</tr>
{{!~
After this <tr> has been rendered, if this was a footer, we need to reopen a new <tbody>
+10 -2
View File
@@ -80,7 +80,7 @@ pub enum PageContext {
/// Handles the first SQL statements, before the headers have been sent to
pub struct HeaderContext {
app_state: Arc<AppState>,
request_context: RequestContext,
pub request_context: RequestContext,
pub writer: ResponseWriter,
response: HttpResponseBuilder,
has_status: bool,
@@ -368,7 +368,14 @@ impl HeaderContext {
Ok(PageContext::Header(self))
}
async fn start_body(self, data: JsonValue) -> anyhow::Result<PageContext> {
fn add_server_timing_header(&mut self) {
if let Some(header_value) = self.request_context.server_timing.header_value() {
self.response.insert_header(("Server-Timing", header_value));
}
}
async fn start_body(mut self, data: JsonValue) -> anyhow::Result<PageContext> {
self.add_server_timing_header();
let html_renderer =
HtmlRenderContext::new(self.app_state, self.request_context, self.writer, data)
.await
@@ -382,6 +389,7 @@ impl HeaderContext {
}
pub fn close(mut self) -> HttpResponse {
self.add_server_timing_header();
self.response.finish()
}
}
+24 -18
View File
@@ -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,
+5 -3
View File
@@ -52,13 +52,13 @@ pub fn stream_query_results_with_conn<'a>(
for res in &sql_file.statements {
match res {
ParsedStatement::CsvImport(csv_import) => {
let connection = take_connection(&request.app_state.db, db_connection).await?;
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
log::debug!("Executing CSV import: {csv_import:?}");
run_csv_import(connection, csv_import, request).await.with_context(|| format!("Failed to import the CSV file {:?} into the table {:?}", csv_import.uploaded_file, csv_import.table_name))?;
},
ParsedStatement::StmtWithParams(stmt) => {
let query = bind_parameters(stmt, request, db_connection).await?;
let connection = take_connection(&request.app_state.db, db_connection).await?;
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
log::trace!("Executing query {:?}", query.sql);
let mut stream = connection.fetch_many(query);
let mut error = None;
@@ -192,7 +192,7 @@ async fn execute_set_variable_query<'a>(
source_file: &Path,
) -> anyhow::Result<()> {
let query = bind_parameters(statement, request, db_connection).await?;
let connection = take_connection(&request.app_state.db, db_connection).await?;
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
log::debug!(
"Executing query to set the {variable:?} variable: {:?}",
query.sql
@@ -276,6 +276,7 @@ fn vars_and_name<'a, 'b>(
async fn take_connection<'a>(
db: &'a Database,
conn: &'a mut DbConn,
request: &RequestInfo,
) -> anyhow::Result<&'a mut PoolConnection<sqlx::Any>> {
if let Some(c) = conn {
return Ok(c);
@@ -283,6 +284,7 @@ async fn take_connection<'a>(
match db.connection.acquire().await {
Ok(c) => {
log::debug!("Acquired a database connection");
request.server_timing.record("db_conn");
*conn = Some(c);
Ok(conn.as_mut().unwrap())
}
+25 -6
View File
@@ -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<dyn Dialect> {
+17 -4
View File
@@ -7,6 +7,7 @@ use crate::webserver::content_security_policy::ContentSecurityPolicy;
use crate::webserver::database::execute_queries::stop_at_first_error;
use crate::webserver::database::{execute_queries::stream_query_results_with_conn, DbItem};
use crate::webserver::http_request_info::extract_request_info;
use crate::webserver::server_timing::ServerTiming;
use crate::webserver::ErrorWithStatus;
use crate::{AppConfig, AppState, ParsedSqlFile, DEFAULT_404_FILE};
use actix_web::dev::{fn_service, ServiceFactory, ServiceRequest};
@@ -46,6 +47,7 @@ pub struct RequestContext {
pub is_embedded: bool,
pub source_path: PathBuf,
pub content_security_policy: ContentSecurityPolicy,
pub server_timing: Arc<ServerTiming>,
}
async fn stream_response(stream: impl Stream<Item = DbItem>, mut renderer: AnyRenderBodyContext) {
@@ -106,7 +108,10 @@ async fn build_response_header_and_stream<S: Stream<Item = DbItem>>(
let mut stream = Box::pin(database_entries);
while let Some(item) = stream.next().await {
let page_context = match item {
DbItem::Row(data) => head_context.handle_row(data).await?,
DbItem::Row(data) => {
head_context.request_context.server_timing.record("row");
head_context.handle_row(data).await?
}
DbItem::FinishedQuery => {
log::debug!("finished query");
continue;
@@ -163,18 +168,21 @@ enum ResponseWithWriter<S> {
async fn render_sql(
srv_req: &mut ServiceRequest,
sql_file: Arc<ParsedSqlFile>,
server_timing: ServerTiming,
) -> actix_web::Result<HttpResponse> {
let app_state = srv_req
.app_data::<web::Data<AppState>>()
.ok_or_else(|| ErrorInternalServerError("no state"))?
.clone() // Cheap reference count increase
.clone()
.into_inner();
let mut req_param = extract_request_info(srv_req, Arc::clone(&app_state))
let mut req_param = extract_request_info(srv_req, Arc::clone(&app_state), server_timing)
.await
.map_err(|e| anyhow_err_to_actix(e, &app_state))?;
log::debug!("Received a request with the following parameters: {req_param:?}");
req_param.server_timing.record("parse_req");
let (resp_send, resp_recv) = tokio::sync::oneshot::channel::<HttpResponse>();
let source_path: PathBuf = sql_file.source_path.clone();
actix_web::rt::spawn(async move {
@@ -182,6 +190,7 @@ async fn render_sql(
is_embedded: req_param.get_variables.contains_key("_sqlpage_embed"),
source_path,
content_security_policy: ContentSecurityPolicy::with_random_nonce(),
server_timing: Arc::clone(&req_param.server_timing),
};
let mut conn = None;
let database_entries_stream =
@@ -275,13 +284,17 @@ async fn process_sql_request(
sql_path: PathBuf,
) -> actix_web::Result<HttpResponse> {
let app_state: &web::Data<AppState> = req.app_data().expect("app_state");
let server_timing = ServerTiming::for_env(app_state.config.environment);
let sql_file = app_state
.sql_file_cache
.get_with_privilege(app_state, &sql_path, false)
.await
.with_context(|| format!("Unable to read SQL file \"{}\"", sql_path.display()))
.map_err(|e| anyhow_err_to_actix(e, app_state))?;
render_sql(req, sql_file).await
server_timing.record("sql_file");
render_sql(req, sql_file, server_timing).await
}
async fn serve_file(
+12 -4
View File
@@ -1,3 +1,4 @@
use crate::webserver::server_timing::ServerTiming;
use crate::AppState;
use actix_multipart::form::bytes::Bytes;
use actix_multipart::form::tempfile::TempFile;
@@ -42,6 +43,7 @@ pub struct RequestInfo {
pub clone_depth: u8,
pub raw_body: Option<Vec<u8>>,
pub oidc_claims: Option<OidcClaims>,
pub server_timing: Arc<super::server_timing::ServerTiming>,
}
impl RequestInfo {
@@ -62,6 +64,7 @@ impl RequestInfo {
clone_depth: self.clone_depth + 1,
raw_body: self.raw_body.clone(),
oidc_claims: self.oidc_claims.clone(),
server_timing: Arc::clone(&self.server_timing),
}
}
}
@@ -78,6 +81,7 @@ impl Clone for RequestInfo {
pub(crate) async fn extract_request_info(
req: &mut ServiceRequest,
app_state: Arc<AppState>,
server_timing: ServerTiming,
) -> anyhow::Result<RequestInfo> {
let (http_req, payload) = req.parts_mut();
let method = http_req.method().clone();
@@ -123,6 +127,7 @@ pub(crate) async fn extract_request_info(
clone_depth: 0,
raw_body,
oidc_claims,
server_timing: Arc::new(server_timing),
})
}
@@ -275,7 +280,7 @@ async fn is_file_field_empty(
mod test {
use super::super::http::SingleOrVec;
use super::*;
use crate::app_config::AppConfig;
use crate::{app_config::AppConfig, webserver::server_timing::ServerTiming};
use actix_web::{http::header::ContentType, test::TestRequest};
#[actix_web::test]
@@ -284,7 +289,8 @@ mod test {
serde_json::from_str::<AppConfig>(r#"{"listen_on": "localhost:1234"}"#).unwrap();
let mut service_request = TestRequest::default().to_srv_request();
let app_data = Arc::new(AppState::init(&config).await.unwrap());
let request_info = extract_request_info(&mut service_request, app_data)
let server_timing = ServerTiming::default();
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
.await
.unwrap();
assert_eq!(request_info.post_variables.len(), 0);
@@ -302,7 +308,8 @@ mod test {
.set_payload("my_array[]=3&my_array[]=Hello%20World&repeated=1&repeated=2")
.to_srv_request();
let app_data = Arc::new(AppState::init(&config).await.unwrap());
let request_info = extract_request_info(&mut service_request, app_data)
let server_timing = ServerTiming::default();
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
.await
.unwrap();
assert_eq!(
@@ -351,7 +358,8 @@ mod test {
)
.to_srv_request();
let app_data = Arc::new(AppState::init(&config).await.unwrap());
let request_info = extract_request_info(&mut service_request, app_data)
let server_timing = ServerTiming::enabled(false);
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
.await
.unwrap();
assert_eq!(
+1
View File
@@ -38,6 +38,7 @@ pub mod http_client;
pub mod http_request_info;
mod https;
pub mod request_variables;
pub mod server_timing;
pub use database::Database;
pub use error_with_status::ErrorWithStatus;
+70
View File
@@ -0,0 +1,70 @@
use std::fmt::Write;
use std::sync::Mutex;
use std::time::Instant;
use crate::app_config::DevOrProd;
#[derive(Debug)]
pub struct ServerTiming {
enabled: bool,
created_at: Instant,
events: Mutex<Vec<PerfEvent>>,
}
#[derive(Debug)]
struct PerfEvent {
time: Instant,
name: &'static str,
}
impl Default for ServerTiming {
fn default() -> Self {
Self {
enabled: false,
created_at: Instant::now(),
events: Mutex::new(Vec::new()),
}
}
}
impl ServerTiming {
#[must_use]
pub fn enabled(enabled: bool) -> Self {
Self {
enabled,
..Default::default()
}
}
#[must_use]
pub fn for_env(env: DevOrProd) -> Self {
Self::enabled(!env.is_prod())
}
pub fn record(&self, name: &'static str) {
if self.enabled {
self.events.lock().unwrap().push(PerfEvent {
time: Instant::now(),
name,
});
}
}
pub fn header_value(&self) -> Option<String> {
if !self.enabled {
return None;
}
let evts = self.events.lock().unwrap();
let mut s = String::with_capacity(evts.len() * 16);
let mut last = self.created_at;
for (i, PerfEvent { name, time }) in evts.iter().enumerate() {
if i > 0 {
s.push_str(", ");
}
let millis = time.saturating_duration_since(last).as_millis();
write!(&mut s, "{name};dur={millis}").ok()?;
last = *time;
}
Some(s)
}
}
+84
View File
@@ -213,3 +213,87 @@ test("modal", async ({ page }) => {
await modal.getByRole("button", { label: "Close" }).first().click();
await expect(modal).not.toBeVisible();
});
test("table action buttons - edit_url and delete_url", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=table`);
const tableSection = page.locator(".table-responsive", {
has: page.getByRole("cell", { name: "PharmaCo" }),
});
const editButton = tableSection.getByTitle("Edit").first();
await expect(editButton).toBeVisible();
await expect(editButton).toHaveAttribute("href", /action=edit&update_id=\d+/);
const deleteButton = tableSection.getByTitle("Delete").first();
await expect(deleteButton).toBeVisible();
await expect(deleteButton).toHaveAttribute(
"href",
/action=delete&delete_id=\d+/,
);
});
test("table action buttons - custom_actions", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=table`);
const tableSection = page.locator(".table-responsive", {
has: page.getByRole("cell", { name: "PharmaCo" }),
});
const historyButton = tableSection
.getByTitle("View Standard History")
.first();
await expect(historyButton).toBeVisible();
await expect(historyButton).toHaveAttribute(
"href",
/action=history&standard_id=\d+/,
);
});
test("table action buttons - _sqlpage_actions", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=table`);
const tableSection = page.locator(".table-responsive", {
has: page.getByRole("cell", { name: "PharmaCo" }),
});
const pdfButtons = tableSection.getByTitle("View Presentation");
await expect(pdfButtons.first()).toBeVisible();
await expect(pdfButtons).toHaveCount(3);
const firstPdfButton = pdfButtons.first();
await expect(firstPdfButton).toHaveAttribute(
"href",
"https://sql-page.com/pgconf/2024-sqlpage-badass.pdf",
);
const setInUseButton = tableSection.getByTitle("Set In Use");
await expect(setInUseButton).toBeVisible();
await expect(setInUseButton).toHaveAttribute(
"href",
/action=set_in_use&standard_id=32/,
);
const retireButton = tableSection.getByTitle("Retire Standard");
await expect(retireButton).toBeVisible();
await expect(retireButton).toHaveAttribute(
"href",
/action=retire&standard_id=33/,
);
});
test("table action buttons - disabled action", async ({ page }) => {
await page.goto(`${BASE}/documentation.sql?component=table`);
const tableSection = page.locator(".table-responsive", {
has: page.getByRole("cell", { name: "PharmaCo" }),
});
const viewPresentationButtons = tableSection.getByTitle("View Presentation");
await expect(viewPresentationButtons).toHaveCount(3);
const actionColumnButtons = tableSection.locator(
"td._col_Action a[data-action='Action']",
);
await expect(actionColumnButtons).toHaveCount(3);
const emptyActionButton = actionColumnButtons.last();
await expect(emptyActionButton).toHaveAttribute("href", "null");
await expect(emptyActionButton).toHaveAttribute("title", "Action");
});
+19 -19
View File
@@ -10,17 +10,17 @@
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.45.3",
"@types/node": "^22.1.0"
"@types/node": "^24.9.1"
}
},
"node_modules/@playwright/test": {
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
"integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz",
"integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.53.1"
"playwright": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -30,13 +30,13 @@
}
},
"node_modules/@types/node": {
"version": "22.15.33",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.33.tgz",
"integrity": "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw==",
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
"undici-types": "~7.16.0"
}
},
"node_modules/fsevents": {
@@ -55,13 +55,13 @@
}
},
"node_modules/playwright": {
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
"integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz",
"integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.53.1"
"playwright-core": "1.56.1"
},
"bin": {
"playwright": "cli.js"
@@ -74,9 +74,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.53.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz",
"integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==",
"version": "1.56.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz",
"integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -87,9 +87,9 @@
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
}
+1 -1
View File
@@ -12,6 +12,6 @@
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.45.3",
"@types/node": "^22.1.0"
"@types/node": "^24.9.1"
}
}
+1
View File
@@ -5,6 +5,7 @@ mod core;
mod data_formats;
mod errors;
mod requests;
mod server_timing;
pub mod sql_test_files;
mod transactions;
mod uploads;
+100
View File
@@ -0,0 +1,100 @@
use actix_web::http::StatusCode;
use sqlpage::webserver::http::main_handler;
use crate::common::{get_request_to, make_app_data_from_config, test_config};
#[actix_web::test]
async fn test_server_timing_disabled_in_production() -> actix_web::Result<()> {
let mut config = test_config();
config.environment = sqlpage::app_config::DevOrProd::Production;
let app_data = make_app_data_from_config(config).await;
let req = crate::common::get_request_to_with_data(
"/tests/sql_test_files/it_works_simple.sql",
app_data,
)
.await?
.to_srv_request();
let resp = main_handler(req).await?;
assert_eq!(resp.status(), StatusCode::OK);
assert!(
resp.headers().get("Server-Timing").is_none(),
"Server-Timing header should not be present in production mode"
);
Ok(())
}
#[actix_web::test]
async fn test_server_timing_enabled_in_development() -> actix_web::Result<()> {
let mut config = test_config();
config.environment = sqlpage::app_config::DevOrProd::Development;
let app_data = make_app_data_from_config(config).await;
let req = crate::common::get_request_to_with_data(
"/tests/sql_test_files/it_works_sqrt.sql",
app_data,
)
.await?
.to_srv_request();
let resp = main_handler(req).await?;
assert_eq!(resp.status(), StatusCode::OK);
let server_timing_header = resp
.headers()
.get("Server-Timing")
.expect("Server-Timing header should be present in development mode");
let header_value = server_timing_header.to_str().unwrap();
assert!(
header_value.contains("sql_file;dur="),
"Should contain sql_file timing: {header_value}"
);
assert!(
header_value.contains("parse_req;dur="),
"Should contain parse_req timing: {header_value}"
);
assert!(
header_value.contains("db_conn;dur="),
"Should contain db_conn timing: {header_value}"
);
assert!(
header_value.contains("row;dur="),
"Should contain row timing: {header_value}"
);
Ok(())
}
#[actix_web::test]
async fn test_server_timing_format() -> actix_web::Result<()> {
let req = get_request_to("/tests/sql_test_files/it_works_sqrt.sql")
.await?
.to_srv_request();
let resp = main_handler(req).await?;
assert_eq!(resp.status(), StatusCode::OK);
let server_timing_header = resp.headers().get("Server-Timing").unwrap();
let header_value = server_timing_header.to_str().unwrap();
let parts: Vec<&str> = header_value.split(", ").collect();
assert!(parts.len() >= 4, "Should have at least 4 timing events");
for part in parts {
assert!(
part.contains(";dur="),
"Each part should have name;dur= format: {part}"
);
let dur_parts: Vec<&str> = part.split(";dur=").collect();
assert_eq!(dur_parts.len(), 2, "Should have name and duration: {part}");
let duration: f64 = dur_parts[1]
.parse()
.expect("Duration should be a valid number");
assert!(
duration >= 0.0,
"Duration should be non-negative: {duration}"
);
}
Ok(())
}