189 lines
13 KiB
C#
189 lines
13 KiB
C#
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 three things that differ
|
|
/// per backend — <b>identifier quoting</b>, the <b>metadata-catalog SQL</b>, and <b>row-limit syntax</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 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><b>Quoting is the backstop, not the only defence.</b> Design §8.1's catalog gate is
|
|
/// implemented: <c>SqlCatalogGate</c> resolves every authored table/column against the live
|
|
/// catalog at Initialize and <b>replaces it with the catalog's own spelling</b>, so an identifier
|
|
/// reaching <see cref="QuoteIdentifier"/> on the poll path is a string this driver read back out of
|
|
/// <see cref="ListSchemasSql"/> / <see cref="ListTablesSql"/> / <see cref="ListColumnsSql"/> — not
|
|
/// operator input. An identifier that resolves to nothing rejects its tag (which then publishes
|
|
/// <c>BadNodeIdUnknown</c>) rather than being quoted into a query against a nonexistent object.</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. One implementation ships per constructed
|
|
/// <see cref="SqlProvider"/> member — <see cref="SqlProvider.SqlServer"/>,
|
|
/// <see cref="SqlProvider.Postgres"/> and <see cref="SqlProvider.Oracle"/>.
|
|
/// </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>
|
|
/// The token that stands for a bound parameter <b>in the command text</b> — <c>"@k0"</c> for
|
|
/// SqlClient/Npgsql, <c>":p_k0"</c> for ODP.NET — given the caller's bare base name (<c>"k0"</c>,
|
|
/// <c>"w"</c>, <c>"schema"</c>).
|
|
/// <para><b>Why the marker and the bind name are two members.</b> They are the same string on every
|
|
/// provider but Oracle, where the SQL text carries <c>:p_schema</c> while
|
|
/// <see cref="DbParameter.ParameterName"/> is the bare <c>schema</c>. Modelling one and deriving the
|
|
/// other means a call site guessing at a convention it does not own — the exact defect this seam
|
|
/// exists to prevent, and one that surfaces as a runtime bind failure (or, worse under
|
|
/// bind-by-position, as a <em>silently mis-bound value</em>) rather than a compile error.</para>
|
|
/// <para>The base name is caller-owned and never operator input: the poll planner mints
|
|
/// <c>k0…kN</c>/<c>w</c>, the catalog path uses <c>schema</c>/<c>table</c>. Authored <em>values</em>
|
|
/// ride the parameter; authored <em>identifiers</em> go through <see cref="QuoteIdentifier"/>. Nothing
|
|
/// else reaches a command text.</para>
|
|
/// </summary>
|
|
/// <param name="baseName">The bare base name, without any provider prefix.</param>
|
|
/// <returns>The marker to embed in the SQL text.</returns>
|
|
string ParameterMarker(string baseName);
|
|
|
|
/// <summary>
|
|
/// The value to assign to <see cref="DbParameter.ParameterName"/> for the same
|
|
/// <paramref name="baseName"/> — <c>"@k0"</c> for SqlClient-style providers (which expect the marker
|
|
/// verbatim), the bare <c>"k0"</c> for ODP.NET-style ones (whose documented convention is a
|
|
/// prefix-less bind name).
|
|
/// <para>Always paired with <see cref="ParameterMarker"/> over the <em>same</em> base name: a plan
|
|
/// carries both lists and every binding site <b>copies</b> them index-for-index rather than
|
|
/// re-deriving either from the other.</para>
|
|
/// </summary>
|
|
/// <param name="baseName">The bare base name, without any provider prefix.</param>
|
|
/// <returns>The parameter name to bind under.</returns>
|
|
string ParameterBindName(string baseName);
|
|
|
|
/// <summary>
|
|
/// Applies provider-specific setup to a freshly created <see cref="DbCommand"/>, <b>before</b> its
|
|
/// parameters are added or it is executed. A no-op for providers that need none.
|
|
/// <para><b>Oracle uses this to set <c>OracleCommand.BindByName = true</c></b>, and that call is the
|
|
/// sharpest correctness edge on this seam: ODP.NET binds <em>by position</em> by default, so a
|
|
/// multi-parameter statement (the planner's <c>IN (:p_k0, :p_k1, …)</c> list, the catalog path's
|
|
/// <c>schema</c> + <c>table</c> pair) would bind values to the wrong markers whenever the parameter
|
|
/// order and the marker order differ — publishing one tag's value onto another tag's node with no
|
|
/// error anywhere. Every command that carries parameters — the poll path, the catalog path, the
|
|
/// browse path — therefore calls this immediately after <c>CreateCommand()</c> and <b>before</b> the
|
|
/// first parameter is added. (Bind mode cannot affect a parameterless statement, so the liveness
|
|
/// ones are indifferent: <c>SqlDriverProbe</c> calls it anyway for uniformity,
|
|
/// <c>SqlDriver.PingAsync</c> does not.)</para>
|
|
/// <para>Implementations must tolerate <em>any</em> <see cref="DbCommand"/> they are handed: the
|
|
/// driver can be pointed at an instrumented or substituted factory, so a dialect that casts to its own
|
|
/// provider type must do so defensively rather than throwing.</para>
|
|
/// </summary>
|
|
/// <param name="command">The command to configure.</param>
|
|
void ConfigureCommand(DbCommand command);
|
|
|
|
/// <summary>Cheapest statement that proves the connection is alive (<c>SELECT 1</c>; Oracle needs <c>FROM DUAL</c>).</summary>
|
|
string LivenessSql { get; }
|
|
|
|
/// <summary>
|
|
/// The fragment that goes <b>immediately after <c>SELECT</c></b> to limit a statement to one row —
|
|
/// T-SQL's <c>"TOP 1 "</c>. Empty for a dialect that spells the limit at the end of the statement (see
|
|
/// <see cref="SingleRowLimitSuffix"/>).
|
|
/// <para><b>Include the trailing space</b> when non-empty: the planner concatenates
|
|
/// <c>"SELECT " + SingleRowLimitPrefix + columns</c> with no separator of its own, so an empty fragment
|
|
/// must leave the statement byte-identical to an unlimited one.</para>
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para><b>Why a prefix/suffix pair rather than one <c>ApplyRowLimit(sql, n)</c> method.</b> The two
|
|
/// spellings sit at opposite ends of the statement — SQL Server writes
|
|
/// <c>SELECT TOP 1 … ORDER BY … DESC</c>, while SQLite/Postgres/MySQL write
|
|
/// <c>SELECT … ORDER BY … DESC LIMIT 1</c>. A single method taking the SELECT list could only serve the
|
|
/// prefix position; a single method taking the whole statement would move statement assembly out of the
|
|
/// planner and into every dialect. Two fragments keep the planner the sole author of statement shape
|
|
/// and let each dialect fill in the end it uses.</para>
|
|
/// <para><b>Why "single row" rather than a row count.</b> The only limit v1 emits is the wide-row
|
|
/// <c>topByTimestamp</c> selector's newest-row pick. Modelling an arbitrary <c>n</c> would be untested
|
|
/// surface, and <c>FETCH FIRST</c>/<c>ROWNUM</c> dialects need more than a substituted number anyway.
|
|
/// Widen this to a method when a feature actually needs <c>n > 1</c>.</para>
|
|
/// </remarks>
|
|
string SingleRowLimitPrefix { get; }
|
|
|
|
/// <summary>
|
|
/// The fragment appended to the <b>very end</b> of a statement to limit it to one row —
|
|
/// <c>" LIMIT 1"</c> for SQLite/Postgres/MySQL. Empty for a dialect that limits after <c>SELECT</c>
|
|
/// (see <see cref="SingleRowLimitPrefix"/>).
|
|
/// <para><b>Include the leading space</b> when non-empty, for the same reason the prefix carries a
|
|
/// trailing one: the planner appends it directly to the finished statement.</para>
|
|
/// <para>Exactly one of the two fragments is non-empty in every dialect modelled so far, but the planner
|
|
/// emits <b>both</b> unconditionally — a dialect needing both ends (or neither) is expressible without
|
|
/// touching the call site.</para>
|
|
/// </summary>
|
|
string SingleRowLimitSuffix { get; }
|
|
|
|
/// <summary>Catalog query listing schemas — the browser's <c>RootAsync</c> level. Takes no parameters.</summary>
|
|
string ListSchemasSql { get; }
|
|
|
|
/// <summary>
|
|
/// Scalar query returning the schema an <b>unqualified</b> object name resolves to for the connecting
|
|
/// principal — T-SQL's <c>SELECT SCHEMA_NAME()</c>. Takes no parameters.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>Needed by <c>SqlCatalogGate</c>: a tag authored as <c>TagValues</c> rather than
|
|
/// <c>dbo.TagValues</c> has to be looked up in <em>some</em> schema, and guessing <c>dbo</c> would be a
|
|
/// silent lie on any estate that maps its service accounts to their own default schema. Asking the
|
|
/// server is the only answer that matches how the poll query itself will resolve the name.</para>
|
|
/// <para>It is a <em>query</em> rather than a name because the answer is per-connection, not per-dialect
|
|
/// — the same dialect resolves differently for two different logins.</para>
|
|
/// </remarks>
|
|
string DefaultSchemaSql { get; }
|
|
|
|
/// <summary>
|
|
/// Catalog query listing tables + views in one schema — the browser's schema-expand level. Carries one
|
|
/// parameter under the base name <c>schema</c>, spelled with this dialect's own
|
|
/// <see cref="ParameterMarker"/> (<c>@schema</c> on SqlClient, <c>:p_schema</c> on ODP.NET); callers bind
|
|
/// it under <see cref="ParameterBindName"/> of the same base name.
|
|
/// </summary>
|
|
string ListTablesSql { get; }
|
|
|
|
/// <summary>
|
|
/// Catalog query listing columns of one table — the browser's table-expand / attributes level. Carries
|
|
/// parameters under the base names <c>schema</c> and <c>table</c>, spelled and bound exactly as
|
|
/// <see cref="ListTablesSql"/> describes.
|
|
/// </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);
|
|
}
|