using System.Data;
using Microsoft.Data.Sqlite;
namespace ErsatzTV.Infrastructure.Sqlite.Data;
///
/// ersatztv#668. SQLite's built-in lower()/upper() fold ASCII ONLY — lower('Édith')
/// returns 'Édith' unchanged — so a facet value whose prefix carries an uppercase non-ASCII
/// character can never be matched by the prefix predicate the facet-value endpoint emits. Registering a
/// managed scalar gives that one query a Unicode-correct fold. Wired to
/// at startup.
///
public static class SqliteUnicodeFunctions
{
///
/// SQL name of the invariant-uppercase fold. The facet-value handler interpolates this constant into
/// its SQL, so the two cannot drift apart.
///
public const string UpperInvariantFunction = "etv_upper";
///
/// Registers on when it is a
/// SQLite connection, and does nothing otherwise. Idempotent — a repeat registration replaces the
/// previous delegate with an identical one — so the single call site may call it unconditionally.
///
/// The property this fold has to satisfy is ONE-SIDED: the SQL stage may over-match freely,
/// because the endpoint applies an exact filter
/// in memory afterwards, but it must never UNDER-match — no later stage can reintroduce a row SQL
/// never returned. satisfies it because
/// OrdinalIgnoreCase equality is a strict SUBSET of invariant-uppercase equality, so
/// folding both sides with it yields a superset of the final filter's matches.
///
///
/// Do not restate that as "OrdinalIgnoreCase IS invariant-uppercase-then-ordinal" — it is
/// not, and the difference is measurable: char.ToUpperInvariant('ſ') (U+017F) is 'S',
/// yet "ſweet".StartsWith("S", OrdinalIgnoreCase) is false. That gap is precisely
/// the harmless direction — SQL returns the row, the in-memory filter drops it. The containment,
/// not any identity of the two foldings, is what makes this safe.
///
///
/// Registration is per-connection and therefore done at the one call site that uses the function,
/// not through an EF connection interceptor: Dapper opens a closed connection itself, and a direct
/// ADO open does not raise EF's interceptors — so an interceptor-based seam would silently miss
/// exactly the query that needs it.
///
///
public static void Register(IDbConnection connection)
{
if (connection is SqliteConnection sqlite)
{
sqlite.CreateFunction(
UpperInvariantFunction,
(string? value) => value?.ToUpperInvariant(),
isDeterministic: true);
}
}
}