using System.Data.Common;
using Microsoft.Data.Sqlite;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests;
///
/// A test-only over SQLite, so the poll reader and the schema browser can
/// be exercised against a real — real parameter binding, real type coercion,
/// real NULLs — with no SQL Server and no network. No product project references SQLite.
/// It is also the seam's falsifiability control: it is the only non-SQL-Server
/// in the tree, so it is what proves the planner emits dialect SQL
/// rather than T-SQL with a quoting function attached. A SQLite statement built with T-SQL's
/// TOP 1 does not parse, and the fixture tests fail loudly if that regresses.
/// Kept public and self-contained on purpose: Driver.Sql.Browser.Tests links this file
/// (<Compile Include="..\..\Driver.Sql.Tests\SqliteDialect.cs" Link="SqliteDialect.cs" />)
/// rather than referencing this project, so it must not depend on anything else defined here.
///
public sealed class SqliteDialect : ISqlDialect
{
///
/// The only schema name SQLite's main database answers to. SQLite has no schema namespace — the
/// catalog queries accept this one value so the browser's schema level has something real to expand.
///
public const string MainSchema = "main";
///
/// Reports .
/// Why not a Sqlite member: is a shipped product
/// contract, and a test-only dialect must not widen it — every consumer's exhaustive switch would gain
/// a case that can never occur in production.
/// Why of the existing members: it is the enum's generic,
/// not-yet-constructed member, so nothing branches on it today. Crucially it is not
/// — any future T-SQL-only code path that keys off
/// stays switched off against this dialect and fails visibly here rather than
/// silently applying T-SQL rules to SQLite. Claiming SqlServer would make this dialect a rubber stamp
/// for exactly the assumptions it exists to catch.
///
public SqlProvider Provider => SqlProvider.Odbc;
///
public DbProviderFactory Factory => SqliteFactory.Instance;
///
public string LivenessSql => "SELECT 1";
///
/// SQLite spells the row limit at the end of the statement, so the after-SELECT
/// position is empty — the mirror image of SqlServerDialect, which is the point of having both
/// ends on the seam.
///
public string SingleRowLimitPrefix => string.Empty;
///
/// " LIMIT 1", leading space included so it appends straight onto the finished statement
/// (… ORDER BY "sample_ts" DESC LIMIT 1).
///
public string SingleRowLimitSuffix => " LIMIT 1";
///
/// SQLite has no schema namespace, so the schema level is the single literal
/// — enough to give the browser a real root to expand.
///
public string ListSchemasSql => $"SELECT '{MainSchema}' AS TABLE_SCHEMA";
///
/// Tables + views from sqlite_schema (the modern name for sqlite_master), with the
/// internal sqlite_* objects filtered out and the type folded onto the
/// INFORMATION_SCHEMA.TABLES vocabulary the browser already speaks. @schema is bound and
/// honoured — anything but legitimately lists nothing.
///
public string ListTablesSql =>
"SELECT name AS TABLE_NAME, " +
"CASE type WHEN 'view' THEN 'VIEW' ELSE 'BASE TABLE' END AS TABLE_TYPE " +
"FROM sqlite_schema " +
"WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\' " +
$"AND @schema = '{MainSchema}' ORDER BY name";
///
/// Columns via the pragma_table_info table-valued function rather than the bare
/// PRAGMA table_info(x) statement, because only the function form lets the table name be a
/// bound parameter — a bare PRAGMA takes its argument as text, which would put an authored name
/// back into a command string and reopen the injection boundary this seam exists to close.
/// DATA_TYPE is the column's declared type (SQLite stores affinity, not a type).
///
public string ListColumnsSql =>
"SELECT name AS COLUMN_NAME, type AS DATA_TYPE, " +
"CASE \"notnull\" WHEN 1 THEN 'NO' ELSE 'YES' END AS IS_NULLABLE " +
"FROM pragma_table_info(@table) " +
$"WHERE @schema = '{MainSchema}' ORDER BY cid";
///
/// Double-quotes one identifier part, doubling any embedded " — SQLite's escape rule, and the
/// only metacharacter that could close a quoted identifier early.
///
///
/// Mirrors SqlServerDialect.QuoteIdentifier's rejections (null / empty / all-whitespace /
/// control characters) so a test written against one dialect fails the same way against the other.
/// SQLite imposes no identifier length ceiling, so there is no length rule here.
///
/// The bare, unquoted identifier part.
/// The double-quote-quoted identifier.
/// The value cannot be a valid SQLite identifier.
public string QuoteIdentifier(string ident)
{
if (string.IsNullOrWhiteSpace(ident))
throw new ArgumentException(
"A SQL identifier must be a non-empty, non-whitespace name.", nameof(ident));
foreach (var ch in ident)
{
if (char.IsControl(ch))
throw new ArgumentException(
"A SQL identifier may not contain control characters (including NUL).", nameof(ident));
}
return string.Concat("\"", ident.Replace("\"", "\"\"", StringComparison.Ordinal), "\"");
}
///
/// Folds a SQLite declared column type onto a . SQLite is
/// dynamically typed and a column's declared type is advisory, which is exactly why the fixture
/// declares every seeded column explicitly.
///
///
/// Never throws, mirroring SqlServerDialect: an unrecognised (or blank, or
/// parameterised like VARCHAR(50)) declaration falls back to
/// so a browse over an oddly-declared table still renders.
/// Deliberate divergence from SQL Server: REAL maps to
/// here. SQLite's REAL is an 8-byte IEEE double, where T-SQL's
/// real is 4-byte — mapping it to Float32 would narrow every value the fixture returns.
///
/// The declared type as pragma_table_info reports it; case-insensitive.
/// The mapped driver data type, or when unrecognised.
public DriverDataType MapColumnType(string sqlDataType)
{
if (string.IsNullOrWhiteSpace(sqlDataType)) return DriverDataType.String;
return sqlDataType.Trim().ToLowerInvariant() switch
{
"boolean" or "bool" or "bit" => DriverDataType.Boolean,
"tinyint" or "smallint" or "int2" => DriverDataType.Int16,
"int" or "integer" or "mediumint" or "int4" => DriverDataType.Int32,
"bigint" or "int8" => DriverDataType.Int64,
// SQLite's REAL is a double — deliberately NOT Float32 (see remarks).
"real" or "double" or "double precision" or "float"
or "numeric" or "decimal" => DriverDataType.Float64,
"text" or "clob" or "char" or "varchar" or "nchar" or "nvarchar" => DriverDataType.String,
"date" or "datetime" or "timestamp" => DriverDataType.DateTime,
_ => DriverDataType.String,
};
}
}