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; /// /// The provider seam (design §2.2). ADO.NET's base types abstract /// connections, commands, parameters and readers; they do not abstract the three things that differ /// per backend — identifier quoting, the metadata-catalog SQL, and row-limit syntax. /// Those live here, so no dialect assumption leaks into the reader, the poll planner, or the schema browser. /// This interface is the driver's SQL-injection boundary. Every runtime value is /// bound as a and never reaches a command text. Identifiers cannot be /// parameterized in SQL, so the few that must appear as text are emitted through /// . Any code path that builds SQL by concatenating an authored tag field /// without going through is a defect (design §8.1). /// Quoting is the backstop, not the only defence. Design §8.1's catalog gate is /// implemented: SqlCatalogGate resolves every authored table/column against the live /// catalog at Initialize and replaces it with the catalog's own spelling, so an identifier /// reaching on the poll path is a string this driver read back out of /// / / — not /// operator input. An identifier that resolves to nothing rejects its tag (which then publishes /// BadNodeIdUnknown) rather than being quoted into a query against a nonexistent object. /// Public because Driver.Sql.Browser consumes it — the catalog SQL is the browse /// engine, so it is shared rather than duplicated. Implementations own their provider package; /// no provider-specific type appears in this signature ( is the abstract /// ). /// public interface ISqlDialect { /// /// Which backend this dialect speaks. One implementation ships per constructed /// member — , /// and . /// SqlProvider Provider { get; } /// /// 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. /// DbProviderFactory Factory { get; } /// /// Quotes one 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. /// One part only. A multi-part name such as dbo.TagValues 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. /// /// The bare, unquoted identifier — as returned by the catalog, not pre-quoted. /// The quoted identifier, ready to concatenate into a command text. /// The value cannot be a valid catalog identifier. string QuoteIdentifier(string ident); /// /// The token that stands for a bound parameter in the command text — "@k0" for /// SqlClient/Npgsql, ":p_k0" for ODP.NET — given the caller's bare base name ("k0", /// "w", "schema"). /// Why the marker and the bind name are two members. They are the same string on every /// provider but Oracle, where the SQL text carries :p_schema while /// is the bare schema. 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 silently mis-bound value) rather than a compile error. /// The base name is caller-owned and never operator input: the poll planner mints /// k0…kN/w, the catalog path uses schema/table. Authored values /// ride the parameter; authored identifiers go through . Nothing /// else reaches a command text. /// /// The bare base name, without any provider prefix. /// The marker to embed in the SQL text. string ParameterMarker(string baseName); /// /// The value to assign to for the same /// — "@k0" for SqlClient-style providers (which expect the marker /// verbatim), the bare "k0" for ODP.NET-style ones (whose documented convention is a /// prefix-less bind name). /// Always paired with over the same base name: a plan /// carries both lists and every binding site copies them index-for-index rather than /// re-deriving either from the other. /// /// The bare base name, without any provider prefix. /// The parameter name to bind under. string ParameterBindName(string baseName); /// /// Applies provider-specific setup to a freshly created , before its /// parameters are added or it is executed. A no-op for providers that need none. /// Oracle uses this to set OracleCommand.BindByName = true, and that call is the /// sharpest correctness edge on this seam: ODP.NET binds by position by default, so a /// multi-parameter statement (the planner's IN (:p_k0, :p_k1, …) list, the catalog path's /// schema + table 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 CreateCommand() and before the /// first parameter is added. (Bind mode cannot affect a parameterless statement, so the liveness /// ones are indifferent: SqlDriverProbe calls it anyway for uniformity, /// SqlDriver.PingAsync does not.) /// Implementations must tolerate any 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. /// /// The command to configure. void ConfigureCommand(DbCommand command); /// Cheapest statement that proves the connection is alive (SELECT 1; Oracle needs FROM DUAL). string LivenessSql { get; } /// /// The fragment that goes immediately after SELECT to limit a statement to one row — /// T-SQL's "TOP 1 ". Empty for a dialect that spells the limit at the end of the statement (see /// ). /// Include the trailing space when non-empty: the planner concatenates /// "SELECT " + SingleRowLimitPrefix + columns with no separator of its own, so an empty fragment /// must leave the statement byte-identical to an unlimited one. /// /// /// Why a prefix/suffix pair rather than one ApplyRowLimit(sql, n) method. The two /// spellings sit at opposite ends of the statement — SQL Server writes /// SELECT TOP 1 … ORDER BY … DESC, while SQLite/Postgres/MySQL write /// SELECT … ORDER BY … DESC LIMIT 1. 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. /// Why "single row" rather than a row count. The only limit v1 emits is the wide-row /// topByTimestamp selector's newest-row pick. Modelling an arbitrary n would be untested /// surface, and FETCH FIRST/ROWNUM dialects need more than a substituted number anyway. /// Widen this to a method when a feature actually needs n > 1. /// string SingleRowLimitPrefix { get; } /// /// The fragment appended to the very end of a statement to limit it to one row — /// " LIMIT 1" for SQLite/Postgres/MySQL. Empty for a dialect that limits after SELECT /// (see ). /// Include the leading space when non-empty, for the same reason the prefix carries a /// trailing one: the planner appends it directly to the finished statement. /// Exactly one of the two fragments is non-empty in every dialect modelled so far, but the planner /// emits both unconditionally — a dialect needing both ends (or neither) is expressible without /// touching the call site. /// string SingleRowLimitSuffix { get; } /// Catalog query listing schemas — the browser's RootAsync level. Takes no parameters. string ListSchemasSql { get; } /// /// Scalar query returning the schema an unqualified object name resolves to for the connecting /// principal — T-SQL's SELECT SCHEMA_NAME(). Takes no parameters. /// /// /// Needed by SqlCatalogGate: a tag authored as TagValues rather than /// dbo.TagValues has to be looked up in some schema, and guessing dbo 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. /// It is a query rather than a name because the answer is per-connection, not per-dialect /// — the same dialect resolves differently for two different logins. /// string DefaultSchemaSql { get; } /// /// Catalog query listing tables + views in one schema — the browser's schema-expand level. Carries one /// parameter under the base name schema, spelled with this dialect's own /// (@schema on SqlClient, :p_schema on ODP.NET); callers bind /// it under of the same base name. /// string ListTablesSql { get; } /// /// Catalog query listing columns of one table — the browser's table-expand / attributes level. Carries /// parameters under the base names schema and table, spelled and bound exactly as /// describes. /// string ListColumnsSql { get; } /// /// Folds a catalog data-type family name (as INFORMATION_SCHEMA.COLUMNS.DATA_TYPE reports it — /// e.g. nvarchar, never nvarchar(50)) onto a . /// Must never throw: a browse over a table holding one exotic column must still render. /// An unrecognised family falls back to . /// /// The bare SQL type family name; matched case-insensitively. /// The mapped driver data type, or when unrecognised. DriverDataType MapColumnType(string sqlDataType); }