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

4.8 KiB
Raw Blame History

status, date, applyTo, supersededBy
status date applyTo supersededBy
Accepted 2026-09-16
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/**

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.