75 lines
4.8 KiB
Markdown
75 lines
4.8 KiB
Markdown
---
|
||
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.
|