diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/PostgresDialect.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/PostgresDialect.cs
new file mode 100644
index 00000000..eaa5a9b6
--- /dev/null
+++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Dialect/PostgresDialect.cs
@@ -0,0 +1,251 @@
+using System.Data.Common;
+using System.Globalization;
+using Npgsql;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Sql;
+
+///
+/// PostgreSQL dialect (design §2), over the fully managed Npgsql ADO.NET provider — no native
+/// client install anywhere. Double-quote identifier quoting, information_schema catalog SQL, an
+/// end-of-statement LIMIT 1, and a type map over the type names
+/// information_schema.columns.data_type actually reports.
+/// Stateless and immutable, so a single instance is safely shared by the driver's poll reader and
+/// the AdminUI browse session — the same contract SqlServerDialect holds to.
+///
+public sealed class PostgresDialect : ISqlDialect
+{
+ ///
+ /// PostgreSQL's identifier ceiling — NAMEDATALEN - 1, 63 by default. Enforced because the
+ /// server silently truncates anything longer, so a 70-character authored name would resolve to
+ /// a different object than it spells; rejecting it surfaces the config mistake instead.
+ ///
+ internal const int MaxIdentifierLength = 63;
+
+ ///
+ public SqlProvider Provider => SqlProvider.Postgres;
+
+ ///
+ public DbProviderFactory Factory => NpgsqlFactory.Instance;
+
+ ///
+ /// Npgsql markers are @-prefixed, exactly as SQL Server's are — ParameterMarker("k0")
+ /// is "@k0". (Npgsql also accepts :, but this dialect commits to one spelling so the
+ /// catalog SQL below and the planner's markers can never drift apart.)
+ ///
+ /// The bare base name.
+ /// The @-prefixed marker.
+ public string ParameterMarker(string baseName) => "@" + baseName;
+
+ ///
+ /// Identical to : NpgsqlParameter.ParameterName is matched against
+ /// the marker in the statement, and Npgsql strips the prefix itself either way. Spelling it out keeps
+ /// the parameter collection readable in a log and matches what the SQL text says.
+ ///
+ /// The bare base name.
+ /// The @-prefixed bind name.
+ public string ParameterBindName(string baseName) => "@" + baseName;
+
+ ///
+ /// No-op. NpgsqlCommand binds by name out of the box, so there is nothing Postgres needs set on
+ /// a fresh command — the member exists for Oracle's BindByName (see
+ /// ). It deliberately does not cast
+ /// , so a substituted or instrumented factory's command passes through
+ /// untouched rather than throwing.
+ ///
+ /// The command to configure; untouched here.
+ public void ConfigureCommand(DbCommand command)
+ {
+ // Intentionally empty: see the summary.
+ }
+
+ ///
+ public string LivenessSql => "SELECT 1";
+
+ ///
+ /// Empty — Postgres spells its row limit at the end of the statement, in
+ /// . The mirror image of T-SQL's TOP 1, which is exactly why
+ /// the seam carries both ends.
+ ///
+ 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";
+
+ ///
+ /// The schemas holding relations this connection can actually see, system schemas excluded.
+ ///
+ ///
+ /// Sourced from information_schema.tables, not information_schema.schemata
+ /// — and that is a deliberate correctness choice, not a copy of the SQL Server dialect's shape.
+ /// Postgres' schemata view lists only schemas the current role owns, so a service
+ /// account granted USAGE + SELECT on someone else's schema — the normal way a read-only
+ /// poller is provisioned — would see an empty schema list and every tag would be rejected. Deriving
+ /// the list from the relations the principal can see answers the question the catalog gate is really
+ /// asking: "which schemas hold something I may read?"
+ /// What counts as a system schema. pg_catalog and information_schema are
+ /// named explicitly (design §2). pg_toast and the per-backend pg_temp_N /
+ /// pg_toast_temp_N namespaces are excluded by prefix as well: they hold storage internals and
+ /// session-local temp objects, can never contain an authored poll target, and a pg_temp_N in
+ /// particular names a different thing per session — offering it in the browse tree would present a
+ /// schema that vanishes. The ESCAPE clause makes the _ literal rather than the
+ /// single-character wildcard LIKE would otherwise treat it as.
+ /// Aliased "TABLE_SCHEMA" in double quotes — see .
+ ///
+ public string ListSchemasSql =>
+ "SELECT DISTINCT table_schema AS \"TABLE_SCHEMA\" FROM information_schema.tables " +
+ "WHERE table_schema NOT IN ('pg_catalog', 'information_schema') " +
+ "AND table_schema NOT LIKE 'pg\\_toast%' ESCAPE '\\' " +
+ "AND table_schema NOT LIKE 'pg\\_temp%' ESCAPE '\\' " +
+ "ORDER BY \"TABLE_SCHEMA\"";
+
+ ///
+ /// current_schema() is the first schema on the connection's search_path — the one an
+ /// unqualified object name resolves in, which is precisely the question the catalog gate asks.
+ ///
+ public string DefaultSchemaSql => "SELECT current_schema()";
+
+ ///
+ /// Tables + views in one schema, matching SqlServerDialect.ListTablesSql column for column
+ /// (TABLE_NAME, TABLE_TYPE, ordered by name) so both consumers read the same shape.
+ ///
+ ///
+ /// The aliases are quoted, and that is load-bearing. SqlCatalogLoader and
+ /// SqlBrowseSession both read every catalog column by name
+ /// (DbDataReader.GetOrdinal("TABLE_NAME")), while Postgres folds an unquoted output
+ /// column name to lower case. Double-quoting each alias pins the exact upper-case spelling those
+ /// consumers ask for rather than relying on a provider's case-insensitive GetOrdinal
+ /// fallback.
+ /// The @schema marker is written literally here because
+ /// spells it identically; the two agree by construction on this dialect
+ /// (they diverge only on ODP.NET), and a test pins that they do.
+ ///
+ public string ListTablesSql =>
+ "SELECT table_name AS \"TABLE_NAME\", table_type AS \"TABLE_TYPE\" " +
+ "FROM information_schema.tables " +
+ "WHERE table_schema = @schema ORDER BY \"TABLE_NAME\"";
+
+ ///
+ /// Columns of one table in ordinal order — the same three columns
+ /// SqlServerDialect.ListColumnsSql projects (COLUMN_NAME, DATA_TYPE,
+ /// IS_NULLABLE), quoted for the reason gives.
+ ///
+ public string ListColumnsSql =>
+ "SELECT column_name AS \"COLUMN_NAME\", data_type AS \"DATA_TYPE\", " +
+ "is_nullable AS \"IS_NULLABLE\" FROM information_schema.columns " +
+ "WHERE table_schema = @schema AND table_name = @table ORDER BY ordinal_position";
+
+ ///
+ /// Double-quotes one identifier part, doubling any embedded " — the SQL-standard escape rule
+ /// Postgres follows, and the only metacharacter that could terminate a quoted identifier early.
+ /// Backslashes, semicolons and comment markers are inert inside a quoted identifier and pass through
+ /// unchanged.
+ ///
+ ///
+ /// Rejects (all as , mirroring
+ /// SqlServerDialect.QuoteIdentifier so a test written against one dialect fails the same way
+ /// against the other): null / empty / all-whitespace; longer than
+ /// ; any control character (which covers embedded NUL and the C0/C1
+ /// ranges that can truncate or corrupt a logged/audited statement); and any Unicode
+ /// character — bidi overrides, zero-width spaces, soft hyphen,
+ /// BOM. Those last cannot escape the quotes, but they spoof the rendered text of a log line
+ /// or an AdminUI label while comparing byte-different from the real catalog name (the Trojan-Source
+ /// class, CVE-2021-42574).
+ /// NUL deserves its own note here: Postgres rejects a NUL byte anywhere in a query string
+ /// outright, so passing one through would fail the whole statement rather than just the one
+ /// identifier.
+ /// The rejection messages deliberately do not echo the offending value — it is untrusted
+ /// input and this exception's text reaches the driver log.
+ /// This is a backstop, not the only defence. Design §8.1's catalog gate
+ /// (SqlCatalogGate) resolves an authored table/column against the live catalog at Initialize
+ /// and substitutes the catalog's own spelling — which on Postgres is the lower-cased one an
+ /// unquoted CREATE TABLE produced — so on the poll path the value arriving here is a string
+ /// read back out of the catalog.
+ ///
+ /// The bare, unquoted identifier part.
+ /// The double-quoted identifier.
+ /// The value cannot be a valid PostgreSQL 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));
+
+ if (ident.Length > MaxIdentifierLength)
+ throw new ArgumentException(
+ $"A PostgreSQL identifier may not exceed {MaxIdentifierLength} characters (got {ident.Length}).",
+ 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));
+
+ // Cf is a *separate* category from Cc, so char.IsControl misses every bidi override and
+ // zero-width character. They are inert inside the quotes but spoof rendered log/UI text.
+ if (CharUnicodeInfo.GetUnicodeCategory(ch) == UnicodeCategory.Format)
+ throw new ArgumentException(
+ "A SQL identifier may not contain Unicode format characters "
+ + "(bidi overrides, zero-width spaces, soft hyphen, BOM).", nameof(ident));
+ }
+
+ return string.Concat("\"", ident.Replace("\"", "\"\"", StringComparison.Ordinal), "\"");
+ }
+
+ ///
+ /// Folds an information_schema.columns.data_type family name onto a
+ /// per design §3.7.
+ ///
+ ///
+ /// The input is the canonical, spelled-out name Postgres reports —
+ /// double precision, character varying, timestamp without time zone — never the
+ /// float8/varchar/timestamptz aliases and never a length
+ /// (character varying(50) is reported as character varying). The internal aliases are
+ /// accepted anyway, so a caller reading types from pg_catalog instead maps identically.
+ /// Precision caveat: numeric / decimal / money collapse to
+ /// , as they do on SQL Server. A Postgres numeric is
+ /// arbitrary-precision, so a high-scale column loses low-order digits; authoring an explicit
+ /// "type": "String" on the tag preserves the exact textual value when that matters.
+ /// Fallback: an unrecognised family maps to and never
+ /// throws — a schema browse must render a table containing an exotic column (json,
+ /// jsonb, inet, tsvector, an enum or composite reported as
+ /// USER-DEFINED, an array reported as ARRAY) rather than crash on it. Null/blank input
+ /// takes the same fallback, and so does bytea — matching how SQL Server's varbinary
+ /// falls back rather than pretending binary is a scalar.
+ /// Deliberate non-mappings. Postgres bit / bit varying are bit
+ /// strings, not booleans — mapping bit to the way
+ /// T-SQL's bit demands would silently coerce a multi-bit value, so they fall back.
+ /// time / time with time zone are times of day rather than points in time and take the
+ /// same fallback as T-SQL's time; interval is a duration, likewise.
+ /// uuid maps to , matching how SQL Server's
+ /// uniqueidentifier is handled.
+ ///
+ /// The bare SQL type family name; matched case-insensitively.
+ /// 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" => DriverDataType.Boolean,
+ "smallint" or "int2" => DriverDataType.Int16,
+ "integer" or "int" or "int4" => DriverDataType.Int32,
+ "bigint" or "int8" => DriverDataType.Int64,
+ "real" or "float4" => DriverDataType.Float32,
+ "double precision" or "float8"
+ or "numeric" or "decimal" or "money" => DriverDataType.Float64,
+ "text" or "character varying" or "varchar" or "character" or "char" or "bpchar"
+ or "name" or "uuid" => DriverDataType.String,
+ "date"
+ or "timestamp without time zone" or "timestamp with time zone"
+ or "timestamp" or "timestamptz" => DriverDataType.DateTime,
+ _ => DriverDataType.String,
+ };
+ }
+}
diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/PostgresDialectTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/PostgresDialectTests.cs
new file mode 100644
index 00000000..0ca35ec1
--- /dev/null
+++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests/PostgresDialectTests.cs
@@ -0,0 +1,229 @@
+using Microsoft.Data.Sqlite;
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
+
+namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests;
+
+///
+/// Golden tests for , mirroring rule for
+/// rule: the same injection boundary (values bound, identifiers quoted), the same rejection discipline,
+/// and the exact catalog SQL. Where Postgres deliberately differs from T-SQL — double-quote escaping, a
+/// 63-character identifier ceiling, the end-of-statement LIMIT 1, and bit being a bit string
+/// rather than a boolean — the divergence is pinned here rather than left to reading.
+///
+public class PostgresDialectTests
+{
+ [Fact]
+ public void QuoteIdentifier_doubleQuotesAndEscapesEmbeddedQuotes()
+ {
+ var d = new PostgresDialect();
+ d.QuoteIdentifier("Speed").ShouldBe("\"Speed\"");
+ // " doubled, per the SQL-standard rule Postgres follows.
+ d.QuoteIdentifier("a\"b").ShouldBe("\"a\"\"b\"");
+ }
+
+ [Fact]
+ public void QuoteIdentifier_rejectsControlCharsAndNul()
+ => Should.Throw(() => new PostgresDialect().QuoteIdentifier("x\0y"));
+
+ // NUL alone would still pass against a `Contains('\0')` regression, so pin the whole Cc category. The
+ // code points are spelled numerically so the offending characters never appear raw in this source file.
+ [Theory]
+ [InlineData(0x0001)] // C0 SOH
+ [InlineData(0x001B)] // C0 ESC
+ [InlineData(0x007F)] // DEL
+ [InlineData(0x0085)] // C1 NEL
+ [InlineData(0x0009)] // TAB
+ [InlineData(0x000A)] // LF
+ public void QuoteIdentifier_rejectsEveryControlCharacter_notJustNul(int codePoint)
+ => Should.Throw(
+ () => new PostgresDialect().QuoteIdentifier("x" + (char)codePoint + "y"));
+
+ // Unicode Format (Cf) is a distinct category from Control (Cc): char.IsControl returns false for all of
+ // these. They cannot escape the quotes, but they spoof rendered log/AdminUI text while comparing
+ // byte-different from the real catalog name (Trojan Source, CVE-2021-42574).
+ [Theory]
+ [InlineData(0x200B)] // zero-width space
+ [InlineData(0x202E)] // right-to-left override
+ [InlineData(0x2066)] // left-to-right isolate
+ [InlineData(0xFEFF)] // BOM / zero-width no-break space
+ [InlineData(0x00AD)] // soft hyphen
+ public void QuoteIdentifier_rejectsUnicodeFormatCharacters(int codePoint)
+ => Should.Throw(
+ () => new PostgresDialect().QuoteIdentifier("Sp" + (char)codePoint + "eed"));
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("\t")]
+ public void QuoteIdentifier_rejectsNullEmptyAndWhitespace(string? ident)
+ => Should.Throw(() => new PostgresDialect().QuoteIdentifier(ident!));
+
+ [Fact]
+ public void QuoteIdentifier_escapesAnIdentifierThatIsNothingButAQuote()
+ => new PostgresDialect().QuoteIdentifier("\"").ShouldBe("\"\"\"\""); // the 1-char name "
+
+ // NAMEDATALEN - 1: a longer string cannot name a real Postgres object (the server truncates it), so it
+ // is a config mistake rather than something to quote and send.
+ [Fact]
+ public void QuoteIdentifier_acceptsExactly63Chars_andRejects64()
+ {
+ var d = new PostgresDialect();
+ d.QuoteIdentifier(new string('a', 63)).ShouldBe("\"" + new string('a', 63) + "\"");
+ Should.Throw(() => d.QuoteIdentifier(new string('a', 64)));
+ }
+
+ [Fact]
+ public void QuoteIdentifier_neutralisesAClassicInjectionPayload()
+ {
+ // A hostile "column name" cannot escape the quotes: the only metacharacter that could is ", and it
+ // is doubled. The result is a single (nonexistent) identifier, never executable SQL.
+ new PostgresDialect().QuoteIdentifier("x\" ; DROP TABLE users --")
+ .ShouldBe("\"x\"\" ; DROP TABLE users --\"");
+ }
+
+ [Theory]
+ [InlineData("boolean", DriverDataType.Boolean)]
+ [InlineData("smallint", DriverDataType.Int16)]
+ [InlineData("integer", DriverDataType.Int32)]
+ [InlineData("bigint", DriverDataType.Int64)]
+ [InlineData("real", DriverDataType.Float32)]
+ [InlineData("double precision", DriverDataType.Float64)]
+ [InlineData("numeric", DriverDataType.Float64)]
+ [InlineData("text", DriverDataType.String)]
+ [InlineData("character varying", DriverDataType.String)]
+ [InlineData("character", DriverDataType.String)]
+ [InlineData("uuid", DriverDataType.String)]
+ [InlineData("timestamp without time zone", DriverDataType.DateTime)]
+ [InlineData("timestamp with time zone", DriverDataType.DateTime)]
+ [InlineData("date", DriverDataType.DateTime)]
+ public void MapColumnType_mapsFamilies(string sql, DriverDataType expected)
+ => new PostgresDialect().MapColumnType(sql).ShouldBe(expected);
+
+ [Theory]
+ [InlineData("BOOLEAN", DriverDataType.Boolean)]
+ [InlineData("Double Precision", DriverDataType.Float64)]
+ public void MapColumnType_isCaseInsensitive(string sql, DriverDataType expected)
+ => new PostgresDialect().MapColumnType(sql).ShouldBe(expected);
+
+ [Theory]
+ [InlineData("bytea")] // binary, exactly as SQL Server's varbinary falls back
+ [InlineData("bit")] // a Postgres BIT STRING, deliberately NOT a boolean
+ [InlineData("bit varying")]
+ [InlineData("time without time zone")] // a time of day, not a point in time (mirrors T-SQL `time`)
+ [InlineData("interval")]
+ [InlineData("json")]
+ [InlineData("ARRAY")]
+ [InlineData("USER-DEFINED")]
+ [InlineData("")]
+ [InlineData(null)]
+ public void MapColumnType_unknownFamilyFallsBackToString_andNeverThrows(string? sql)
+ => new PostgresDialect().MapColumnType(sql!).ShouldBe(DriverDataType.String);
+
+ [Fact]
+ public void CatalogSql_isInformationSchemaAndParameterized()
+ {
+ var d = new PostgresDialect();
+ d.LivenessSql.ShouldBe("SELECT 1");
+ d.DefaultSchemaSql.ShouldBe("SELECT current_schema()");
+ d.ListTablesSql.ShouldContain("information_schema.tables");
+ d.ListColumnsSql.ShouldContain("information_schema.columns");
+ d.ListTablesSql.ShouldContain("@schema"); // never string-interpolated
+ d.ListColumnsSql.ShouldContain("@schema");
+ d.ListColumnsSql.ShouldContain("@table");
+ }
+
+ // The markers baked into the catalog SQL text and the ones ParameterMarker hands the loader must be the
+ // same string, or the loader binds a name the statement never mentions.
+ [Fact]
+ public void CatalogSql_markersAgreeWithParameterMarker()
+ {
+ var d = new PostgresDialect();
+ d.ListTablesSql.ShouldContain(d.ParameterMarker("schema"));
+ d.ListColumnsSql.ShouldContain(d.ParameterMarker("schema"));
+ d.ListColumnsSql.ShouldContain(d.ParameterMarker("table"));
+ }
+
+ [Fact]
+ public void ListSchemasSql_excludesTheSystemSchemas()
+ {
+ var sql = new PostgresDialect().ListSchemasSql;
+ sql.ShouldContain("'pg_catalog'");
+ sql.ShouldContain("'information_schema'");
+
+ // The prefix families are matched with LIKE, where a bare _ is the single-character wildcard — so
+ // they carry an explicit escape. Asserting the escaped spelling is what makes this test able to fail
+ // if someone "tidies" the backslash away and silently widens the exclusion.
+ sql.ShouldContain("'pg\\_toast%' ESCAPE '\\'");
+ sql.ShouldContain("'pg\\_temp%' ESCAPE '\\'");
+ }
+
+ // The loader and the browse session read every catalog column BY ALIAS (DbDataReader.GetOrdinal), and
+ // Postgres folds an unquoted output-column name to lower case. The aliases are therefore quoted, and
+ // this pins the exact ones the consumers ask for.
+ [Fact]
+ public void CatalogSql_projectsTheQuotedUpperCaseAliasesTheConsumersReadByName()
+ {
+ var d = new PostgresDialect();
+ d.ListSchemasSql.ShouldContain("\"TABLE_SCHEMA\"");
+ d.ListTablesSql.ShouldContain("\"TABLE_NAME\"");
+ d.ListTablesSql.ShouldContain("\"TABLE_TYPE\"");
+ d.ListColumnsSql.ShouldContain("\"COLUMN_NAME\"");
+ d.ListColumnsSql.ShouldContain("\"DATA_TYPE\"");
+ d.ListColumnsSql.ShouldContain("\"IS_NULLABLE\"");
+ }
+
+ [Fact]
+ public void SingleRowLimit_isLimit1_inTheSuffixPosition_withItsOwnLeadingSpace()
+ {
+ var d = new PostgresDialect();
+
+ // Postgres spells the limit at the END of the statement, the mirror image of T-SQL's TOP 1. The
+ // planner appends the suffix straight onto the finished statement, so the space belongs to it.
+ d.SingleRowLimitPrefix.ShouldBe("");
+ d.SingleRowLimitSuffix.ShouldBe(" LIMIT 1");
+ string.Concat("SELECT ", d.SingleRowLimitPrefix, "\"a\"", d.SingleRowLimitSuffix)
+ .ShouldBe("SELECT \"a\" LIMIT 1");
+ }
+
+ [Fact]
+ public void Dialect_identifiesItselfAsPostgres_andExposesTheAbstractFactory()
+ {
+ var d = new PostgresDialect();
+ d.Provider.ShouldBe(SqlProvider.Postgres);
+ d.Factory.ShouldBeSameAs(Npgsql.NpgsqlFactory.Instance);
+ // The seam must not leak Npgsql into its signature.
+ typeof(ISqlDialect).GetProperty(nameof(ISqlDialect.Factory))!
+ .PropertyType.ShouldBe(typeof(System.Data.Common.DbProviderFactory));
+ }
+
+ [Theory]
+ [InlineData("k0")]
+ [InlineData("w")]
+ [InlineData("schema")]
+ public void ParameterMarkerAndBindName_areBothAtPrefixed(string baseName)
+ {
+ var d = new PostgresDialect();
+ d.ParameterMarker(baseName).ShouldBe("@" + baseName);
+ d.ParameterBindName(baseName).ShouldBe("@" + baseName);
+ }
+
+ // Npgsql binds by name out of the box, so ConfigureCommand has nothing to do — and it must tolerate a
+ // command that is not an NpgsqlCommand at all (the driver can be pointed at a substituted factory).
+ [Fact]
+ public void ConfigureCommand_leavesAForeignCommandUntouched()
+ {
+ using var command = new SqliteCommand("SELECT 1");
+ command.Parameters.AddWithValue("@k0", 7);
+
+ Should.NotThrow(() => new PostgresDialect().ConfigureCommand(command));
+
+ command.CommandText.ShouldBe("SELECT 1");
+ command.Parameters.Count.ShouldBe(1);
+ command.Parameters[0].ParameterName.ShouldBe("@k0");
+ command.Parameters[0].Value.ShouldBe(7);
+ }
+}