Files
Mimante/Mimante/Data/Sqlite.cs
T
Alby96andClaude Fable 5.1 7993ca44a0 Database SQLite al posto dei dossier, gestore del rischio, decisioni in shadow, rete neurale, bandit e simulatore
Le aste non si salvano più in un file JSON per asta più tre archivi da tenere allineati:
un solo database SQLite (winsqlite3.dll di Windows via P/Invoke, nessun pacchetto) con
prodotti, aste, puntate, interrogazioni coalescenti, reset con profondità del ciclo,
nostre puntate, decisioni del motore, misure di rete, puntatori, sessioni e contabilità.
Scrittura in coda su un thread dedicato, letture in WAL. Per scelta dell'utente si parte
da zero: il database si riempie man mano che si osservano le aste; i vecchi dossier
restano leggibili per un'importazione futura ma non si scrivono più. Storico, schede,
apprendimento, rigiocata ed esportazioni leggono tutti da lì.

Il documento di progetto (Modifiche.txt) tradotto in C# senza librerie:
- RiskManager: kill-switch (file KILL_SWITCH, anche dalla barra), HALT persistente per
  stop-loss e drawdown, tetto del giorno, aste in gioco insieme, contabilità in euro.
- Theory: valore atteso con fee, spedizione, valore reale per prodotto (nuova colonna
  nella scheda Prodotti) e copertura «Compralo Ora»; null-model; sopravvivenza empirica e
  Kaplan-Meier dai reset; arrivi di Poisson.
- Shadow per asta: in Osserva il cecchino arriva allo stesso istante, registra cosa
  avrebbe fatto e non punta; alla chiusura ogni decisione riceve l'esito, e ShadowReport
  confronta le policy sugli stessi istanti con intervallo di confidenza sul ROI.
- NeuralNet (MLP, Adam) come sfidante del logistico, scelto dal Brier prequenziale;
  ThompsonBandit come seconda policy che impara dagli esiti delle decisioni;
  PennyAuctionEnv con avversari tarati sul database e QLearningAgent conservativo;
  SimulationLab per il confronto fra policy; esportazione CSV delle decisioni con
  reason_detail. Tutto nella scheda Apprendimento.

Ml/LEGGIMI.md riscritto con le avvertenze su termini d'uso, quadro legale e realtà
economica, lo schema del database e lo stato delle milestone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 11:00:49 +02:00

496 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
namespace AutoBidder.Data
{
/// <summary>
/// SQLite senza librerie: si chiama direttamente <c>winsqlite3.dll</c>, la copia di
/// SQLite che Windows 10 e 11 tengono in <c>System32</c> per i propri componenti.
///
/// <para><b>Perché così.</b> L'applicazione non deve dipendere da pacchetti esterni.
/// Un provider ADO.NET porterebbe con sé quattro assembly e una DLL nativa; qui ci
/// sono le quindici funzioni C che servono davvero, avvolte in due classi che non
/// fanno niente di più di quello che il programma usa: aprire, preparare, legare
/// parametri, scorrere righe, chiudere.</para>
///
/// <para><b>Fili.</b> Ogni connessione è usata da un thread alla volta: il database
/// scrive da un thread suo e legge sotto un lucchetto (vedi <see cref="AuctionDatabase"/>).
/// La connessione è comunque aperta in modalità serializzata, per non dipendere dalla
/// disciplina del chiamante.</para>
/// </summary>
internal static class SqliteNative
{
private const string Lib = "winsqlite3.dll";
public const int Ok = 0;
public const int Row = 100;
public const int Done = 101;
public const int Busy = 5;
public const int Locked = 6;
public const int OpenReadWrite = 0x2;
public const int OpenCreate = 0x4;
public const int OpenFullMutex = 0x10000;
public const int TypeInteger = 1;
public const int TypeFloat = 2;
public const int TypeText = 3;
public const int TypeBlob = 4;
public const int TypeNull = 5;
/// <summary>SQLITE_TRANSIENT: SQLite copia il testo legato prima che il buffer venga liberato.</summary>
public static readonly IntPtr Transient = new(-1);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_open_v2(byte[] filename, out IntPtr db, int flags, IntPtr vfs);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_close_v2(IntPtr db);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_prepare_v2(IntPtr db, byte[] sql, int nByte, out IntPtr stmt, out IntPtr tail);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_step(IntPtr stmt);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_reset(IntPtr stmt);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_clear_bindings(IntPtr stmt);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_finalize(IntPtr stmt);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_bind_int64(IntPtr stmt, int index, long value);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_bind_double(IntPtr stmt, int index, double value);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_bind_null(IntPtr stmt, int index);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_bind_text(IntPtr stmt, int index, byte[] text, int nBytes, IntPtr destructor);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_column_count(IntPtr stmt);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_column_type(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern long sqlite3_column_int64(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern double sqlite3_column_double(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr sqlite3_column_text(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_column_bytes(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr sqlite3_column_name(IntPtr stmt, int col);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr sqlite3_errmsg(IntPtr db);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern long sqlite3_last_insert_rowid(IntPtr db);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_changes(IntPtr db);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern int sqlite3_busy_timeout(IntPtr db, int ms);
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr sqlite3_libversion();
public static byte[] Utf8Z(string s)
{
var bytes = Encoding.UTF8.GetBytes(s);
var z = new byte[bytes.Length + 1];
Buffer.BlockCopy(bytes, 0, z, 0, bytes.Length);
return z;
}
public static string? Utf8(IntPtr p) => p == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(p);
}
public sealed class SqliteException : Exception
{
public int Code { get; }
public SqliteException(int code, string message) : base(message) => Code = code;
}
/// <summary>Una connessione. Da usare da un thread alla volta.</summary>
public sealed class SqliteConnection : IDisposable
{
private IntPtr _db;
private readonly Dictionary<string, SqliteStatement> _cache = new(StringComparer.Ordinal);
public string Path { get; }
public static string LibraryVersion
{
get
{
try { return SqliteNative.Utf8(SqliteNative.sqlite3_libversion()) ?? "?"; }
catch (DllNotFoundException) { return "winsqlite3.dll non trovata"; }
}
}
public SqliteConnection(string path)
{
Path = path;
var rc = SqliteNative.sqlite3_open_v2(
SqliteNative.Utf8Z(path), out _db,
SqliteNative.OpenReadWrite | SqliteNative.OpenCreate | SqliteNative.OpenFullMutex,
IntPtr.Zero);
if (rc != SqliteNative.Ok)
{
var msg = _db != IntPtr.Zero ? ErrorMessage() : $"codice {rc}";
if (_db != IntPtr.Zero) SqliteNative.sqlite3_close_v2(_db);
_db = IntPtr.Zero;
throw new SqliteException(rc, $"apertura di {path} non riuscita: {msg}");
}
// Un altro processo (o l'altra connessione) può tenere il file per qualche
// millisecondo: si aspetta invece di fallire.
SqliteNative.sqlite3_busy_timeout(_db, 5000);
}
internal IntPtr Handle => _db;
public string ErrorMessage() => SqliteNative.Utf8(SqliteNative.sqlite3_errmsg(_db)) ?? "errore sconosciuto";
public long LastInsertRowId => SqliteNative.sqlite3_last_insert_rowid(_db);
public int Changes => SqliteNative.sqlite3_changes(_db);
/// <summary>Esegue un'istruzione senza risultati (o ne scarta le righe).</summary>
public void Execute(string sql, params object?[] args)
{
using var stmt = Prepare(sql);
stmt.Bind(args);
stmt.RunToEnd();
}
/// <summary>Esegue più istruzioni separate da punto e virgola, senza parametri.</summary>
public void ExecuteScript(string script)
{
var bytes = SqliteNative.Utf8Z(script);
var offset = 0;
while (offset < bytes.Length - 1)
{
var remaining = new byte[bytes.Length - offset];
Buffer.BlockCopy(bytes, offset, remaining, 0, remaining.Length);
var rc = SqliteNative.sqlite3_prepare_v2(_db, remaining, -1, out var stmt, out var tail);
if (rc != SqliteNative.Ok) throw new SqliteException(rc, ErrorMessage());
// Il puntatore alla coda restituito da SQLite riguarda una copia nativa
// del buffer di cui non abbiamo la base: si ricava quanto è stato
// consumato scandendo fino al primo ';' fuori da apici e commenti, che è
// esattamente dove SQLite si è fermato.
var consumed = IndexAfterStatement(remaining);
if (stmt != IntPtr.Zero)
{
using var s = new SqliteStatement(this, stmt, "");
s.RunToEnd();
}
offset += Math.Max(1, consumed);
}
}
/// <summary>Fino al primo ';' fuori da apici e commenti, incluso.</summary>
private static int IndexAfterStatement(byte[] bytes)
{
var inQuote = false;
var inLineComment = false;
for (var i = 0; i < bytes.Length - 1; i++)
{
var c = (char)bytes[i];
if (inLineComment) { if (c == '\n') inLineComment = false; continue; }
if (c == '\'') inQuote = !inQuote;
else if (!inQuote && c == '-' && i + 1 < bytes.Length && bytes[i + 1] == '-') inLineComment = true;
else if (!inQuote && c == ';') return i + 1;
}
return bytes.Length - 1;
}
/// <summary>Prepara un'istruzione nuova, non condivisa. Chi la chiede la libera.</summary>
public SqliteStatement Prepare(string sql)
{
var rc = SqliteNative.sqlite3_prepare_v2(_db, SqliteNative.Utf8Z(sql), -1, out var stmt, out _);
if (rc != SqliteNative.Ok || stmt == IntPtr.Zero)
throw new SqliteException(rc, $"{ErrorMessage()} — SQL: {sql}");
return new SqliteStatement(this, stmt, sql);
}
/// <summary>
/// Un'istruzione preparata e riusata: le stesse dieci frasi vengono eseguite
/// migliaia di volte, e prepararle ogni volta costerebbe più dell'esecuzione.
/// Viene azzerata prima di ogni uso.
/// </summary>
public SqliteStatement Cached(string sql)
{
if (!_cache.TryGetValue(sql, out var stmt))
{
stmt = Prepare(sql);
stmt.Shared = true;
_cache[sql] = stmt;
}
stmt.Reset();
return stmt;
}
public List<SqliteRow> Query(string sql, params object?[] args)
{
var stmt = Cached(sql);
stmt.Bind(args);
return stmt.ReadAll();
}
public void Dispose()
{
foreach (var s in _cache.Values) s.Shared = false;
foreach (var s in _cache.Values) s.Dispose();
_cache.Clear();
if (_db != IntPtr.Zero)
{
SqliteNative.sqlite3_close_v2(_db);
_db = IntPtr.Zero;
}
}
}
/// <summary>Un'istruzione preparata.</summary>
public sealed class SqliteStatement : IDisposable
{
private readonly SqliteConnection _conn;
private IntPtr _stmt;
private string[]? _columns;
public string Sql { get; }
/// <summary>Appartiene alla cache della connessione: Dispose non la libera.</summary>
internal bool Shared { get; set; }
internal SqliteStatement(SqliteConnection conn, IntPtr stmt, string sql)
{
_conn = conn;
_stmt = stmt;
Sql = sql;
}
public void Reset()
{
SqliteNative.sqlite3_reset(_stmt);
SqliteNative.sqlite3_clear_bindings(_stmt);
}
/// <summary>Lega i parametri posizionali (?1, ?2, …) nell'ordine dato.</summary>
public void Bind(object?[] args)
{
for (var i = 0; i < args.Length; i++)
{
var idx = i + 1;
var rc = args[i] switch
{
null => SqliteNative.sqlite3_bind_null(_stmt, idx),
string s => BindText(idx, s),
bool b => SqliteNative.sqlite3_bind_int64(_stmt, idx, b ? 1 : 0),
int n => SqliteNative.sqlite3_bind_int64(_stmt, idx, n),
long l => SqliteNative.sqlite3_bind_int64(_stmt, idx, l),
double d => double.IsNaN(d) || double.IsInfinity(d)
? SqliteNative.sqlite3_bind_null(_stmt, idx)
: SqliteNative.sqlite3_bind_double(_stmt, idx, d),
float f => SqliteNative.sqlite3_bind_double(_stmt, idx, f),
decimal m => SqliteNative.sqlite3_bind_double(_stmt, idx, (double)m),
DateTime dt => BindText(idx, AuctionDatabase.Iso(dt)),
DateTimeOffset dto => BindText(idx, dto.UtcDateTime.ToString("o")),
Enum e => BindText(idx, e.ToString()),
_ => BindText(idx, args[i]!.ToString() ?? "")
};
if (rc != SqliteNative.Ok)
throw new SqliteException(rc, $"{_conn.ErrorMessage()} — parametro {idx} di: {Sql}");
}
}
private int BindText(int idx, string s)
{
var bytes = Encoding.UTF8.GetBytes(s);
return SqliteNative.sqlite3_bind_text(_stmt, idx, bytes, bytes.Length, SqliteNative.Transient);
}
/// <summary>Avanza di una riga. False alla fine.</summary>
public bool Step()
{
var rc = SqliteNative.sqlite3_step(_stmt);
if (rc == SqliteNative.Row) return true;
if (rc == SqliteNative.Done) return false;
throw new SqliteException(rc, $"{_conn.ErrorMessage()} — SQL: {Sql}");
}
public void RunToEnd()
{
while (Step()) { }
}
public string[] Columns
{
get
{
if (_columns != null) return _columns;
var n = SqliteNative.sqlite3_column_count(_stmt);
_columns = new string[n];
for (var i = 0; i < n; i++)
_columns[i] = SqliteNative.Utf8(SqliteNative.sqlite3_column_name(_stmt, i)) ?? $"c{i}";
return _columns;
}
}
public object? Value(int col)
{
switch (SqliteNative.sqlite3_column_type(_stmt, col))
{
case SqliteNative.TypeInteger: return SqliteNative.sqlite3_column_int64(_stmt, col);
case SqliteNative.TypeFloat: return SqliteNative.sqlite3_column_double(_stmt, col);
case SqliteNative.TypeText:
var p = SqliteNative.sqlite3_column_text(_stmt, col);
var n = SqliteNative.sqlite3_column_bytes(_stmt, col);
return p == IntPtr.Zero ? "" : Marshal.PtrToStringUTF8(p, n);
default: return null;
}
}
public List<SqliteRow> ReadAll()
{
var rows = new List<SqliteRow>();
var columns = Columns;
var index = SqliteRow.IndexFor(columns);
while (Step())
{
var values = new object?[columns.Length];
for (var i = 0; i < values.Length; i++) values[i] = Value(i);
rows.Add(new SqliteRow(index, values));
}
return rows;
}
public void Dispose()
{
if (Shared) return;
if (_stmt != IntPtr.Zero)
{
SqliteNative.sqlite3_finalize(_stmt);
_stmt = IntPtr.Zero;
}
}
}
/// <summary>Una riga letta, con accesso per nome di colonna e conversioni tolleranti.</summary>
public sealed class SqliteRow
{
private readonly Dictionary<string, int> _index;
private readonly object?[] _values;
private static readonly Dictionary<string, Dictionary<string, int>> IndexCache = new(StringComparer.Ordinal);
internal static Dictionary<string, int> IndexFor(string[] columns)
{
var key = string.Join("|", columns);
lock (IndexCache)
{
if (!IndexCache.TryGetValue(key, out var idx))
{
idx = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < columns.Length; i++) idx[columns[i]] = i;
IndexCache[key] = idx;
}
return idx;
}
}
internal SqliteRow(Dictionary<string, int> index, object?[] values)
{
_index = index;
_values = values;
}
public object? this[string column] => _index.TryGetValue(column, out var i) ? _values[i] : null;
/// <summary>Valore per posizione: per i risultati scalari.</summary>
public object? At(int i) => i >= 0 && i < _values.Length ? _values[i] : null;
public bool Has(string column) => _index.ContainsKey(column);
public string Str(string column) => this[column]?.ToString() ?? "";
public string? StrOrNull(string column) => this[column] as string;
public long Long(string column) => this[column] switch
{
long l => l,
double d => (long)d,
string s when long.TryParse(s, out var v) => v,
_ => 0
};
public int Int(string column) => (int)Long(column);
public bool Bool(string column) => Long(column) != 0;
public double Dbl(string column) => this[column] switch
{
double d => d,
long l => l,
string s when double.TryParse(s, System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var v) => v,
_ => 0
};
public double? DblOrNull(string column) => this[column] == null ? null : Dbl(column);
public int? IntOrNull(string column) => this[column] == null ? null : Int(column);
public long? LongOrNull(string column) => this[column] == null ? null : Long(column);
/// <summary>Data in UTC, o null. Le date sono scritte in ISO 8601 UTC.</summary>
public DateTime? Date(string column)
{
var s = this[column] as string;
if (string.IsNullOrEmpty(s)) return null;
if (!DateTime.TryParse(s, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.RoundtripKind, out var d))
return null;
return d.Kind switch
{
DateTimeKind.Utc => d,
DateTimeKind.Local => d.ToUniversalTime(),
_ => DateTime.SpecifyKind(d, DateTimeKind.Utc) // scritto senza fuso: era UTC
};
}
}
}