* feat(database) :: stop casting variables to TEXT on PostgreSQL, MySQL and SQL Server
SQLPage binds every variable as a string. The generated CAST(? AS TEXT)
forced the database to type the parameter as text, which is only needed
where parameter type inference is unpredictable (SQLite, ODBC). On the
natively supported databases the cast was redundant, and on SQL Server it
was harmful: the parameter is bound as NVARCHAR(MAX), and casting it to a
narrow VARCHAR mangled non-ASCII values before comparing them to nvarchar
columns. Generated SQL is now cleaner, e.g. WHERE id = $1 instead of
WHERE id = CAST($1 AS TEXT).
SQLite and ODBC-backed databases (Oracle, DuckDB, Snowflake, Generic)
keep the cast to preserve their comparison semantics.
Verified with the full test suite against SQLite, PostgreSQL, MySQL and
SQL Server, including new fixtures for integer-column comparisons,
numeric-literal comparisons, and unicode nvarchar comparisons on SQL
Server.
* fix(odbc) :: keep the text cast around variables on ODBC connections
The ODBC job in CI failed because the cast was removed based on the
database name behind the driver: PostgreSQL reached through psqlodbc no
longer received CAST(? AS TEXT), and the driver could not determine the
type of context-free parameters such as in 'WHERE ? <> ? OR ? IS NULL',
failing with 'could not determine data type of parameter'.
The cast decision now keys off the connection kind: native PostgreSQL,
MySQL and SQL Server connections keep no cast, while every ODBC
connection keeps the previous per-database cast, since ODBC drivers
provide no parameter type information.
Adds a fixture comparing variables without any surrounding type context,
which exercises exactly this scenario on every database.
* refine: drop the text cast on MySQL, SQL Server and DuckDB behind ODBC too
ODBC connections were conservatively keeping the cast for every database.
Testing against real ODBC drivers shows the cast is needed only where the
parameter type cannot be determined without it:
- psqlodbc -> PostgreSQL: needed. Without it, context-free parameters fail
with 'could not determine data type of parameter' (the earlier CI failure).
- sqliteodbc -> SQLite: needed. Without it, '? = 1' compares text against an
integer and silently returns false, like on native SQLite.
- duckdb-odbc: not needed. The full test suite passes without the cast, as
DuckDB defaults untyped parameters to VARCHAR.
The cast is therefore dropped for MySQL, SQL Server and DuckDB behind ODBC,
mirroring their native behavior (MySQL and SQL Server convert the bound
string at execution time, which also fixes the unicode mangling for SQL
Server reached through ODBC), and kept for PostgreSQL, SQLite, Oracle,
Snowflake and unknown databases.
Verified with the full test suite on native SQLite, PostgreSQL, MySQL and
SQL Server, and through ODBC on PostgreSQL, SQLite and DuckDB. The only
ODBC failure is a pre-existing database-filesystem timestamp test that also
fails on main.
* docs(changelog): make variable cast entry concise and user-oriented
The previous entry described internal CAST(? AS TEXT) generation
and load-bearing type affinity details. Rephrase for users:
focus on the visible fix (MSSQL nvarchar Unicode mangling)
and the general simplification (no unnecessary text cast
where the database infers the type).
* docs(changelog): add issue references for variable cast fix
Fixes: #516 (CONTAINS with MSSQL variable fails due to CAST), #1154
(LIMIT/OFFSET with variables fails on MySQL/MariaDB).
See: #1317 (per-database logic still scattered, this is a step
toward the SqlDialect abstraction).
* test: simplify variable cast tests and add focused repros for #516 and #1154
- Replace verbose pattern-matching in sql.rs with helpers sql_for/
odbc_sql_for and table-driven asserts; keep coverage but drop
ceremony and duplicated error messages.
- Keep limit and mssql tests as one-liners checking the generated
SQL string.
- Trim .sql fixtures to minimal scaffold and add GH issue links as
comments. New fixtures:
* variable_limit_offset (MySQL, fixes#1154) — LIMIT/OFFSET with SET
variables must not be wrapped in CAST.
* variable_mssql_contains (MSSQL, fixes#516) — EXEC sp_executesql
with a variable must not be wrapped in CAST.
- Simplify existing variable fixtures and add issue links, keep them
short and readable.
* test: make variable fixtures self-contained
Remove 'same root cause' and inaccurate GH links that referenced
other files. Each fixture now describes its own invariant without
assuming reader context from another file.
* fix: mssql test escaping and improve changelog
- Fix variable_mssql_contains test: avoid nested single quotes in
sp_executesql string that caused 'Incorrect syntax near It' on
CI. Use parameterised expected value instead of embedding
'It works !' inside the inner N'...' string.
- Improve CHANGELOG: one main bullet about removing CAST with
subpoints for PostgreSQL/MySQL/DuckDB, SQL Server nvarchar/
CONTAINS/EXEC, MySQL LIMIT/OFFSET, and retained cast on SQLite/
ODBC. Move fixes:/see: to PR description.
* Refactor SQL variable cast tests to use typed assertions
* clippy
* Move database-specific SQL tests into per-database subdirectories
Restructure tests so files that only work on a single database engine
are
organized under `database-specific/<engine>/` instead of using long
`_no...`
suffixes. Add a dedicated test that runs these files only when the
current
database matches, and simplify the generic test runner by extracting
shared
execution logic.
* rename mssql variable fixture to match its sp_executesql repro
The file reproduces issue #516 (CONTAINS rejects CAST expressions), but
the query itself uses sp_executesql, which has the same restriction
without needing a full-text index. Rename the fixture to reflect that
and add a comment explaining why CONTAINS is not used directly.
Renders Markdown to HTML and sends as multipart/alternative
with raw Markdown as the plain-text body. When body_md is
provided, body becomes optional. body_md cannot be combined
with body_html.
Rename the misspelled `stmp_*` configuration options to `smtp_*`
and add a new `smtp_tls_mode` option (`starttls`, `tls`, `none`)
to control encryption when connecting to the SMTP server. Reject
credentials in plaintext mode.
Change `sqlpage.send_mail` to return its JSON argument unchanged
on success and update the example to use a local Mailpit SMTP
server via Docker Compose.
### Motivation
- Provide a built-in `sqlpage.send_mail(...)` SQL function so pages can send plain-text emails from SQL code.
- Allow the SMTP server to be configured via an environment / configuration option so the function can target a deployable SMTP endpoint.
### Description
- Added a new function implementation at `src/webserver/database/sqlpage_functions/functions/send_mail.rs` implementing `sqlpage.send_mail(json)` which accepts a JSON object with required `recipient`, `subject`, and `body` and optional `sender` and `reply_to`, sends the message and returns `sent` on success.
- Registered the function in the SQLPage function registry by adding `send_mail` to `src/webserver/database/sqlpage_functions/functions.rs`.
- Added a configuration option `stmp_host: Option<String>` to `AppConfig` in `src/app_config.rs`, with `parse_stmp_host`/`validate_stmp_host` helpers that accept either `host` or `host:port` and default to port 25 when none is provided; validation is run from `AppConfig::validate`.
- Added `lettre` to `Cargo.toml` and updated `Cargo.lock` to enable SMTP sending, and added official-site documentation and a migration at `examples/official-site/sqlpage/migrations/75_send_mail.sql` describing usage and parameters.
- Documented the `stmp_host` option in `configuration.md`.
### Testing
- Ran `cargo fmt --all`, which completed successfully.
- Ran `git diff --check` which reported no immediate style errors.
- Attempted `cargo clippy --all-targets --all-features -- -D warnings`, but it was blocked by a toolchain/build issue (a dependency `libsqlite3-sys` build script uses the unstable `cfg_select` feature) and did not complete.
- Attempted `cargo test`, but it was similarly blocked by the same `libsqlite3-sys` build-script error and did not complete.
SQLPage functions that are the whole value of a selected column now
execute after the database query, once per returned row. If the query
returns no rows, the function is not called. This ensures deterministic
behavior and prevents expensive or side-effectful operations from
running unnecessarily.
Document that the folder/destination_folder argument of
sqlpage.persist_uploaded_file must be chosen by the app author and never
derived from untrusted request data. It is joined directly to the web
root, so a value containing '..' or an absolute path would write the
uploaded file outside the web root. Docs-only clarification of existing
intended behavior; no logic change.