Files
Fuchs_Intranet/Fuchs/Docs/Decisions/0026-dashboard-widget-sql-set-based-and-emaillog-config-contract.md
2026-09-16 22:31:27 +02:00

75 lines
4.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: Accepted
date: 2026-09-16
applyTo:
- "Fuchs/Services/FuchsWidgetService.cs"
- "Fuchs/Services/ProcessWebComService.cs"
- "Fuchs_Database/dbo/Functions/fds__fn_unpaidInvoices.sql"
- "Fuchs_Database/dbo/Functions/fds__fn_InvoicePaymentAmount*.sql"
- "Fuchs_Database/dbo/Functions/fds_wgd_*.sql"
- "Fuchs_Database/dbo/Functions/fds_wdg_*.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__logEmail.sql"
- "Fuchs_Database/Scripts/**"
supersededBy: ""
---
# 0026 — Dashboard widget SQL is set-based (no per-row scalar UDFs); `fds__emaillog.config` is a JSON contract
## Context
Two dashboard widgets were failing in the DB log (`fds__debuglog`, 2026-09-16):
1. **`wdg_invoices_overdue_sum`** timed out after the widget command timeout of 90 s. Its SQL called
`[dbo].[fds__fn_InvoicePaymentAmount](Id)` once per row of `fds__fn_unpaidInvoices()`. That scalar
UDF declares a table variable, so SQL Server 2019 cannot inline it (`sys.sql_modules.is_inlineable = 0`)
and re-executes the multi-statement TVF `fds__fn_bankingtransactions` — a full pass over
~11 k banking rows plus a `STRING_SPLIT` cross-apply over the settings heap — for **every** invoice:
≈0.5 s × 187 unpaid invoices ≈ 100 s measured. `wdg_unbez_akonto` had the same pattern (≈5 s,
hidden only by `daycache = 1`). The same total computed set-based (one TVF pass, `GROUP BY InvId`,
joined to the invoices) takes < 0.5 s and returns the identical figure.
2. **`wgd_lastEmails`** raised *JSON text is not properly formatted*. `fds_wgd_lastEmails` parses
`fds__emaillog.config` with `OPENJSON`/`JSON_QUERY`, but `ProcessWebComService` — the replacement
for the legacy mailer — wrote `config = ''` on every path. One such row was enough to error the
whole widget, and the audit trail had lost recipient/subject/attachment names for every email sent
through the new service.
Widget definitions are **data** (`fis_widgets.sql`), not schema, so there is no compile-time check
that a widget's SQL is cheap or that the columns it parses exist.
## Decision
- **Widget SQL and the functions backing it are set-based.** Per-invoice payment totals come from the
inline TVF `[dbo].[fds__fn_InvoicePaymentAmounts]()` (one banking pass, grouped by `InvId`) or from
the `[PaymentAmount]`/`[OpenAmount]` columns `fds__fn_unpaidInvoices()` now exposes. The scalar
`fds__fn_InvoicePaymentAmount(_full)` remains for single-invoice callers (invoice/reminder
preparation, per-row `paymentstatus = 'p'` cells in the invoice lists) but must not be called
per row from a widget or any other query that iterates a whole invoice set.
- **`fds__emaillog.config` is a JSON contract**, written by `ProcessWebComService.BuildAuditConfig`
in the legacy mailer's shape — `{subject, from, to[], cc[], bcc[], attachments[]}` with addresses
as `"Name" <email>` and attachments as file names only (never the body, never attachment bytes) —
on **every** audit path (success, API error, exception, invalid address, service disabled). SQL
consumers of the column must still tolerate non-JSON text (`IIF(ISJSON(x)=1, x, N'{}')` before
parsing), because `config` is `NOT NULL` and older rows/other writers may hold `''`.
- **Widget-data migrations live in `Fuchs_Database/Scripts/`** as idempotent, dated T-SQL files
(`<None Include>` in the sqlproj, not part of the schema build) and are run once per environment
after the schema deploy they depend on.
## Consequences
- `wdg_invoices_overdue_sum` 101.9 s → 0.45 s, `wdg_unbez_akonto` 4.7 s → 0.4 s, `wgd_lastEmails`
error → 15 ms (dev, 2026-09-16). The widget command timeout stays at 90 s; it is a guard, not a
budget.
- Adding a scalar UDF call inside a widget's `SELECT`/`WHERE` over `fds__invoices` or the
`fds__fn_unpaidInvoices()` set is a regression of this decision. Prefer exposing a set-based column
on the TVF, as done for `PaymentAmount`.
- `ProcessWebComServiceTests` assert the config contract on every audit path; changing the shape
requires updating `fds_wgd_lastEmails` in the same change.
- The `Scripts/2026-09-16_fis_widgets_set_based_payment_amounts.sql` migration must be run on
Production after the schema deploy (it is a no-op once applied).
## Alternatives considered
- **Raise the widget `CommandTimeout`** — masks an O(N) re-scan; the cost grows with every unpaid
invoice and every banking upload, and it would still block a dashboard load for minutes.
- **Convert `fds__fn_InvoicePaymentAmount` into an inlineable scalar UDF** — impossible while it
needs a TVP (`fds__tt__idlist_vchar`) to call the banking TVF; rewriting the banking TVF as an
inline TVF is a larger change to a function shared with the banking-assignment UI.
- **Fix the bad `fds__emaillog` row and leave the widget function strict** — the next `''` row (any
writer, any environment) would take the widget down again; tolerance belongs in the reader.