feat(sql): ISqlDialect + SqlServerDialect with golden catalog SQL
The dialect owns the two things ADO.NET's System.Data.Common base types cannot abstract: identifier quoting and the metadata-catalog SQL. It is the driver's SQL-injection boundary — values always bind as DbParameters, identifiers cannot, so the few that reach a command text are catalog- sourced and pass through QuoteIdentifier. QuoteIdentifier brackets one identifier part and doubles embedded ] (the only metacharacter that can terminate a bracketed identifier early); rejects null/empty/whitespace, >128 chars (T-SQL sysname ceiling), and any control character (incl. NUL) as ArgumentException, without echoing the untrusted value into the message. MapColumnType folds the design §3.7 families case-insensitively and never throws — an unrecognised family falls back to String so a browse over an exotic column still renders; timestamp/rowversion and time are deliberately NOT mapped to DateTime. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
using System.Data.Common;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Sql;
|
||||
|
||||
/// <summary>
|
||||
/// The provider seam (design §2.2). ADO.NET's <see cref="System.Data.Common"/> base types abstract
|
||||
/// connections, commands, parameters and readers; they do <b>not</b> abstract the two things that differ
|
||||
/// per backend — <b>identifier quoting</b> and the <b>metadata-catalog SQL</b>. Those live here, so no
|
||||
/// dialect assumption leaks into the reader, the poll planner, or the schema browser.
|
||||
/// <para><b>This interface is the driver's SQL-injection boundary.</b> Every runtime <em>value</em> is
|
||||
/// bound as a <see cref="DbParameter"/> and never reaches a command text. Identifiers cannot be
|
||||
/// parameterized in SQL, so the few that must appear as text are sourced only from catalog-validated
|
||||
/// (<c>INFORMATION_SCHEMA</c>) names and emitted through <see cref="QuoteIdentifier"/>. Any code path that
|
||||
/// builds SQL by concatenating an authored tag field <em>without</em> going through
|
||||
/// <see cref="QuoteIdentifier"/> is a defect (design §8.1).</para>
|
||||
/// <para>Public because <c>Driver.Sql.Browser</c> consumes it — the catalog SQL <em>is</em> the browse
|
||||
/// engine, so it is shared rather than duplicated. Implementations own their provider package;
|
||||
/// <b>no provider-specific type appears in this signature</b> (<see cref="Factory"/> is the abstract
|
||||
/// <see cref="DbProviderFactory"/>).</para>
|
||||
/// </summary>
|
||||
public interface ISqlDialect
|
||||
{
|
||||
/// <summary>Which backend this dialect speaks. v1 constructs <see cref="SqlProvider.SqlServer"/> only.</summary>
|
||||
SqlProvider Provider { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The provider's factory singleton, used to create connections/commands/parameters. Deliberately the
|
||||
/// abstract base type so consumers never bind to a concrete provider package.
|
||||
/// </summary>
|
||||
DbProviderFactory Factory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Quotes <b>one</b> identifier part so it can be safely embedded in a command text, escaping the
|
||||
/// dialect's own quote character. Rejects anything that cannot be a real catalog identifier rather
|
||||
/// than emitting it.
|
||||
/// <para><b>One part only.</b> A multi-part name such as <c>dbo.TagValues</c> must be split by the
|
||||
/// caller and each part quoted separately; passing the dotted form yields a single (nonexistent)
|
||||
/// identifier — safe, but not what the caller meant.</para>
|
||||
/// </summary>
|
||||
/// <param name="ident">The bare, unquoted identifier — as returned by the catalog, not pre-quoted.</param>
|
||||
/// <returns>The quoted identifier, ready to concatenate into a command text.</returns>
|
||||
/// <exception cref="ArgumentException">The value cannot be a valid catalog identifier.</exception>
|
||||
string QuoteIdentifier(string ident);
|
||||
|
||||
/// <summary>Cheapest statement that proves the connection is alive (<c>SELECT 1</c>; Oracle needs <c>FROM DUAL</c>).</summary>
|
||||
string LivenessSql { get; }
|
||||
|
||||
/// <summary>Catalog query listing schemas — the browser's <c>RootAsync</c> level. Takes no parameters.</summary>
|
||||
string ListSchemasSql { get; }
|
||||
|
||||
/// <summary>Catalog query listing tables + views in one schema — the browser's schema-expand level. Bind <c>@schema</c>.</summary>
|
||||
string ListTablesSql { get; }
|
||||
|
||||
/// <summary>Catalog query listing columns of one table — the browser's table-expand / attributes level. Bind <c>@schema</c> and <c>@table</c>.</summary>
|
||||
string ListColumnsSql { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Folds a catalog data-type family name (as <c>INFORMATION_SCHEMA.COLUMNS.DATA_TYPE</c> reports it —
|
||||
/// e.g. <c>nvarchar</c>, never <c>nvarchar(50)</c>) onto a <see cref="DriverDataType"/>.
|
||||
/// <para>Must <b>never throw</b>: a browse over a table holding one exotic column must still render.
|
||||
/// An unrecognised family falls back to <see cref="DriverDataType.String"/>.</para>
|
||||
/// </summary>
|
||||
/// <param name="sqlDataType">The bare SQL type family name; matched case-insensitively.</param>
|
||||
/// <returns>The mapped driver data type, or <see cref="DriverDataType.String"/> when unrecognised.</returns>
|
||||
DriverDataType MapColumnType(string sqlDataType);
|
||||
}
|
||||
Reference in New Issue
Block a user