docs: fill missing XML docs and strip tracking IDs from src comments
Clears CommentChecker findings in src/ (3,280 -> 209, the remainder being false positives like PLC-5/IEEE-754/M0.0) by adding missing summaries/params/returns, normalising inheritdoc usage, and removing Gitea/review tracking IDs while keeping the rationale they annotated. Comment-only: comment-stripped sources are identical to the parent commit.
This commit is contained in:
@@ -44,201 +44,177 @@ public sealed class OracleDialect : ISqlDialect
|
||||
/// </summary>
|
||||
private const string BindPrefix = "p_";
|
||||
|
||||
/// <summary>
|
||||
/// Oracle markers are <c>:</c>-prefixed <b>and namespaced</b> — <c>ParameterMarker("k0")</c> is
|
||||
/// <c>":p_k0"</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>The <c>p_</c> is load-bearing, and was found live.</b> Oracle rejects a bind variable
|
||||
/// whose name is a reserved word with <c>ORA-01745: invalid host/bind variable name</c> — and the
|
||||
/// catalog readers' own base names include <c>table</c> (<c>SqlCatalogLoader</c> /
|
||||
/// <c>SqlBrowseSession</c> bind <c>schema</c> + <c>table</c>). So an un-prefixed
|
||||
/// <see cref="ListColumnsSql"/> could not execute at all: every Oracle driver instance failed its
|
||||
/// Initialize-time catalog load and rejected every authored tag, and every AdminUI browse of a table's
|
||||
/// columns threw. Caught by <c>OracleReadTests</c> against a real database; invisible to any offline
|
||||
/// test, because the SQL is only ever parsed by a server.</para>
|
||||
/// <para>Prefixing <em>everything</em> rather than maintaining a reserved-word list is deliberate:
|
||||
/// the list is long, version-dependent, and a future base name (<c>date</c>, <c>user</c>,
|
||||
/// <c>comment</c>, <c>level</c>…) would re-open the same hole silently. A prefixed name can never be
|
||||
/// a reserved word, and the marker and the bind name are derived from the same constant, so they
|
||||
/// cannot drift apart.</para>
|
||||
/// </remarks>
|
||||
/// <param name="baseName">The bare base name.</param>
|
||||
/// <returns>The <c>:</c>-prefixed, namespaced marker.</returns>
|
||||
/// <inheritdoc />
|
||||
// Oracle markers are :-prefixed and namespaced — ParameterMarker("k0") is
|
||||
// ":p_k0".
|
||||
//
|
||||
// The p_ is load-bearing, and was found live. Oracle rejects a bind variable
|
||||
// whose name is a reserved word with ORA-01745: invalid host/bind variable name — and the
|
||||
// catalog readers' own base names include table (SqlCatalogLoader /
|
||||
// SqlBrowseSession bind schema + table). So an un-prefixed
|
||||
// ListColumnsSql could not execute at all: every Oracle driver instance failed its
|
||||
// Initialize-time catalog load and rejected every authored tag, and every AdminUI browse of a table's
|
||||
// columns threw. Caught by OracleReadTests against a real database; invisible to any offline
|
||||
// test, because the SQL is only ever parsed by a server.
|
||||
// Prefixing everything rather than maintaining a reserved-word list is deliberate:
|
||||
// the list is long, version-dependent, and a future base name (date, user,
|
||||
// comment, level…) would re-open the same hole silently. A prefixed name can never be
|
||||
// a reserved word, and the marker and the bind name are derived from the same constant, so they
|
||||
// cannot drift apart.
|
||||
public string ParameterMarker(string baseName) => ":" + BindPrefix + baseName;
|
||||
|
||||
/// <summary>
|
||||
/// The same namespaced name <b>without the <c>:</c></b> — ODP.NET's documented convention for
|
||||
/// <see cref="DbParameter.ParameterName"/>, and deliberately <em>not</em>
|
||||
/// <see cref="ParameterMarker"/>. This is the divergence that makes the two members exist at all
|
||||
/// (see <see cref="ISqlDialect.ParameterBindName"/>).
|
||||
/// </summary>
|
||||
/// <param name="baseName">The bare base name.</param>
|
||||
/// <returns>The colon-less bind name.</returns>
|
||||
/// <inheritdoc />
|
||||
// The same namespaced name without the : — ODP.NET's documented convention for
|
||||
// DbParameter.ParameterName, and deliberately not
|
||||
// ParameterMarker. This is the divergence that makes the two members exist at all
|
||||
// (see ISqlDialect.ParameterBindName).
|
||||
public string ParameterBindName(string baseName) => BindPrefix + baseName;
|
||||
|
||||
/// <summary>
|
||||
/// Sets <c>OracleCommand.BindByName = true</c>. <b>This is the sharpest correctness edge in the
|
||||
/// driver's provider support.</b>
|
||||
/// <para>ODP.NET binds <em>by position</em> by default: parameter 0 fills the first marker in the
|
||||
/// statement, parameter 1 the second, and so on, <em>regardless of the names</em>. Every statement this
|
||||
/// driver builds with more than one marker would then be a coin flip — the planner's
|
||||
/// <c>IN (:p_k0, :p_k1, …)</c> key list, and <see cref="ListColumnsSql"/>'s <c>schema</c> + <c>table</c>
|
||||
/// pair — and the failure mode is not an exception but a <em>successful query over the wrong rows</em>,
|
||||
/// publishing one tag's value onto another tag's node. Worse still,
|
||||
/// <see cref="ListTablesSql"/> names <c>:p_schema</c> <b>twice</b> (once per branch of its
|
||||
/// <c>UNION ALL</c>) and is bound once: under positional binding that statement cannot even execute,
|
||||
/// because the second occurrence has no parameter to take.</para>
|
||||
/// <para>Must be called while the parameter collection is still empty — which is exactly what every
|
||||
/// call site does (immediately after <c>CreateCommand()</c>, before the first bind).</para>
|
||||
/// <para><b>A foreign command passes through untouched rather than throwing.</b> The dialect can be
|
||||
/// handed a <see cref="DbCommand"/> from another provider — an instrumented or substituted factory in
|
||||
/// a shared test harness is the realistic case — and an <c>InvalidCastException</c> there would fail a
|
||||
/// poll for a reason that has nothing to do with the database. There is nothing to configure on a
|
||||
/// non-ODP.NET command anyway: every other provider this repo speaks binds by name already.</para>
|
||||
/// </summary>
|
||||
/// <param name="command">The command to configure; only an <see cref="OracleCommand"/> is touched.</param>
|
||||
/// <inheritdoc />
|
||||
// Sets OracleCommand.BindByName = true. This is the sharpest correctness edge in the
|
||||
// driver's provider support.
|
||||
// ODP.NET binds by position by default: parameter 0 fills the first marker in the
|
||||
// statement, parameter 1 the second, and so on, regardless of the names. Every statement this
|
||||
// driver builds with more than one marker would then be a coin flip — the planner's
|
||||
// IN (:p_k0, :p_k1, …) key list, and ListColumnsSql's schema + table
|
||||
// pair — and the failure mode is not an exception but a successful query over the wrong rows,
|
||||
// publishing one tag's value onto another tag's node. Worse still,
|
||||
// ListTablesSql names :p_schema twice (once per branch of its
|
||||
// UNION ALL) and is bound once: under positional binding that statement cannot even execute,
|
||||
// because the second occurrence has no parameter to take.
|
||||
// Must be called while the parameter collection is still empty — which is exactly what every
|
||||
// call site does (immediately after CreateCommand(), before the first bind).
|
||||
// A foreign command passes through untouched rather than throwing. The dialect can be
|
||||
// handed a DbCommand from another provider — an instrumented or substituted factory in
|
||||
// a shared test harness is the realistic case — and an InvalidCastException there would fail a
|
||||
// poll for a reason that has nothing to do with the database. There is nothing to configure on a
|
||||
// non-ODP.NET command anyway: every other provider this repo speaks binds by name already.
|
||||
public void ConfigureCommand(DbCommand command)
|
||||
{
|
||||
if (command is OracleCommand oracle) oracle.BindByName = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 1000 — <b>the tightest of the three ceilings, and the one that actually binds.</b> Oracle allows
|
||||
/// 64k bind variables per statement, but an <c>IN</c> <em>expression list</em> is limited to 1000
|
||||
/// entries (<c>ORA-01795</c>), and the key-value model's <c>IN (:p_k0, …)</c> list is the only
|
||||
/// variable-length parameter list this driver emits. The seam therefore reports the limit the caller
|
||||
/// will hit rather than the larger one it never reaches — a dialect that reported 64000 here would be
|
||||
/// technically accurate and would still produce a statement the server refuses.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
// 1000 — the tightest of the three ceilings, and the one that actually binds. Oracle allows
|
||||
// 64k bind variables per statement, but an IN expression list is limited to 1000
|
||||
// entries (ORA-01795), and the key-value model's IN (:p_k0, …) list is the only
|
||||
// variable-length parameter list this driver emits. The seam therefore reports the limit the caller
|
||||
// will hit rather than the larger one it never reaches — a dialect that reported 64000 here would be
|
||||
// technically accurate and would still produce a statement the server refuses.
|
||||
public int MaxBindParameters => 1000;
|
||||
|
||||
/// <summary>Oracle has no bare <c>SELECT</c>; every projection needs a table, and <c>DUAL</c> is the one-row one.</summary>
|
||||
/// <inheritdoc />
|
||||
// Oracle has no bare SELECT; every projection needs a table, and DUAL is the one-row one.
|
||||
public string LivenessSql => "SELECT 1 FROM DUAL";
|
||||
|
||||
/// <summary>
|
||||
/// Empty — Oracle 12c+ spells the row limit at the <em>end</em> of the statement (see
|
||||
/// <see cref="SingleRowLimitSuffix"/>). Anything here would land between <c>SELECT</c> and the column
|
||||
/// list, where Oracle has no limit syntax at all.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
// Empty — Oracle 12c+ spells the row limit at the end of the statement (see
|
||||
// SingleRowLimitSuffix). Anything here would land between SELECT and the column
|
||||
// list, where Oracle has no limit syntax at all.
|
||||
public string SingleRowLimitPrefix => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The ANSI row-limiting clause Oracle 12.1 added, leading space included so it appends directly to a
|
||||
/// finished statement.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Deliberately <b>not</b> the older <c>WHERE ROWNUM = 1</c>: <c>ROWNUM</c> is assigned <em>before</em>
|
||||
/// <c>ORDER BY</c> runs, so it would take an arbitrary row and then sort it — silently returning the
|
||||
/// wrong row for the <c>topByTimestamp</c> selector, whose entire purpose is "the newest one". Making
|
||||
/// that correct pre-12c requires wrapping the ordered query in a subselect, i.e. authorship of the
|
||||
/// statement shape, which this seam deliberately leaves with the planner.
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
// The ANSI row-limiting clause Oracle 12.1 added, leading space included so it appends directly to a
|
||||
// finished statement.
|
||||
//
|
||||
// Deliberately not the older WHERE ROWNUM = 1: ROWNUM is assigned before
|
||||
// ORDER BY runs, so it would take an arbitrary row and then sort it — silently returning the
|
||||
// wrong row for the topByTimestamp selector, whose entire purpose is "the newest one". Making
|
||||
// that correct pre-12c requires wrapping the ordered query in a subselect, i.e. authorship of the
|
||||
// statement shape, which this seam deliberately leaves with the planner.
|
||||
public string SingleRowLimitSuffix => " FETCH FIRST 1 ROWS ONLY";
|
||||
|
||||
/// <summary>
|
||||
/// Schemas visible to the connecting principal, from <c>ALL_USERS</c> (Oracle equates a schema with a
|
||||
/// user), aliased to the <c>TABLE_SCHEMA</c> column name the catalog readers project by.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Broader than the SQL Server dialect's <c>DISTINCT TABLE_SCHEMA FROM
|
||||
/// INFORMATION_SCHEMA.TABLES</c>, which lists only schemas that own an object: <c>ALL_USERS</c>
|
||||
/// includes empty ones. Harmless for the <b>catalog gate</b> — the list is used to resolve an authored
|
||||
/// schema name to the catalog's own spelling, and a schema holding no tables simply resolves and then
|
||||
/// matches no relation.</para>
|
||||
/// <para>⚠️ <b>It is NOT harmless for the browse tree, and that is a known, accepted trade rather than
|
||||
/// an oversight (Driver.Sql.Dialect-003).</b> This query has two consumers, and only the gate wants the
|
||||
/// broad list: <c>SqlBrowseSession.RootAsync</c> renders every row as an expandable schema folder, so
|
||||
/// on a real Oracle estate the operator is offered every database <em>user</em> — hundreds of them,
|
||||
/// nearly all empty, with nothing in the tree distinguishing the ones that hold a relation until each
|
||||
/// is expanded.</para>
|
||||
/// <para><b>The narrower query is
|
||||
/// <c>SELECT DISTINCT owner FROM ALL_TABLES UNION SELECT DISTINCT owner FROM ALL_VIEWS</c></b>, which
|
||||
/// would match what the other two dialects list and what the browse tree wants. It is deliberately not
|
||||
/// taken here because it also <em>narrows the gate</em>: a tag naming an empty schema today resolves and
|
||||
/// then fails at the table level, and afterwards would fail at the schema level with a different
|
||||
/// message — a behaviour change on the poll path that can only be validated against a real Oracle
|
||||
/// server (<c>OracleReadTests</c>), never offline, for the same reason the <c>p_</c> bind-prefix defect
|
||||
/// was invisible until it ran. Make the swap when an Oracle estate is available to gate it on, and
|
||||
/// change both consumers' expectations in the same commit.</para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
// Schemas visible to the connecting principal, from ALL_USERS (Oracle equates a schema with a
|
||||
// user), aliased to the TABLE_SCHEMA column name the catalog readers project by.
|
||||
//
|
||||
// Broader than the SQL Server dialect's DISTINCT TABLE_SCHEMA FROM
|
||||
// INFORMATION_SCHEMA.TABLES, which lists only schemas that own an object: ALL_USERS
|
||||
// includes empty ones. Harmless for the catalog gate — the list is used to resolve an authored
|
||||
// schema name to the catalog's own spelling, and a schema holding no tables simply resolves and then
|
||||
// matches no relation.
|
||||
// ⚠️ It is NOT harmless for the browse tree, and that is a known, accepted trade rather than
|
||||
// an oversight. This query has two consumers, and only the gate wants the
|
||||
// broad list: SqlBrowseSession.RootAsync renders every row as an expandable schema folder, so
|
||||
// on a real Oracle estate the operator is offered every database user — hundreds of them,
|
||||
// nearly all empty, with nothing in the tree distinguishing the ones that hold a relation until each
|
||||
// is expanded.
|
||||
// The narrower query is
|
||||
// SELECT DISTINCT owner FROM ALL_TABLES UNION SELECT DISTINCT owner FROM ALL_VIEWS, which
|
||||
// would match what the other two dialects list and what the browse tree wants. It is deliberately not
|
||||
// taken here because it also narrows the gate: a tag naming an empty schema today resolves and
|
||||
// then fails at the table level, and afterwards would fail at the schema level with a different
|
||||
// message — a behaviour change on the poll path that can only be validated against a real Oracle
|
||||
// server (OracleReadTests), never offline, for the same reason the p_ bind-prefix defect
|
||||
// was invisible until it ran. Make the swap when an Oracle estate is available to gate it on, and
|
||||
// change both consumers' expectations in the same commit.
|
||||
public string ListSchemasSql =>
|
||||
"SELECT username AS TABLE_SCHEMA FROM ALL_USERS ORDER BY username";
|
||||
|
||||
/// <summary>
|
||||
/// <c>sys_context('userenv','current_schema')</c> is the schema an unqualified name resolves in for
|
||||
/// this session — which is what the catalog gate asks, and it correctly follows an
|
||||
/// <c>ALTER SESSION SET CURRENT_SCHEMA</c> rather than reporting the login name.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
// sys_context('userenv','current_schema') is the schema an unqualified name resolves in for
|
||||
// this session — which is what the catalog gate asks, and it correctly follows an
|
||||
// ALTER SESSION SET CURRENT_SCHEMA rather than reporting the login name.
|
||||
public string DefaultSchemaSql => "SELECT sys_context('userenv','current_schema') FROM dual";
|
||||
|
||||
/// <summary>
|
||||
/// Tables and views in one schema, unioned because Oracle splits them across two catalog views where
|
||||
/// <c>INFORMATION_SCHEMA.TABLES</c> carries both. The synthesised <c>TABLE_TYPE</c> literals are the
|
||||
/// <c>INFORMATION_SCHEMA</c> spellings the browser compares against to mark a view node.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>UNION ALL</c> rather than <c>UNION</c>: the two sources are disjoint by construction (a name
|
||||
/// cannot be both a table and a view in one schema), so de-duplicating would only buy a sort.
|
||||
/// <para>Note this statement names <c>:p_schema</c> twice and is bound <b>once</b> — see
|
||||
/// <see cref="ConfigureCommand"/>.</para>
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
// Tables and views in one schema, unioned because Oracle splits them across two catalog views where
|
||||
// INFORMATION_SCHEMA.TABLES carries both. The synthesised TABLE_TYPE literals are the
|
||||
// INFORMATION_SCHEMA spellings the browser compares against to mark a view node.
|
||||
//
|
||||
// UNION ALL rather than UNION: the two sources are disjoint by construction (a name
|
||||
// cannot be both a table and a view in one schema), so de-duplicating would only buy a sort.
|
||||
// Note this statement names :p_schema twice and is bound once — see
|
||||
// ConfigureCommand.
|
||||
public string ListTablesSql =>
|
||||
"SELECT table_name AS TABLE_NAME, 'BASE TABLE' AS TABLE_TYPE FROM ALL_TABLES WHERE owner = :p_schema " +
|
||||
"UNION ALL " +
|
||||
"SELECT view_name AS TABLE_NAME, 'VIEW' AS TABLE_TYPE FROM ALL_VIEWS WHERE owner = :p_schema " +
|
||||
"ORDER BY TABLE_NAME";
|
||||
|
||||
/// <summary>
|
||||
/// Columns of one relation, from <c>ALL_TAB_COLUMNS</c> — which covers views as well as tables, so it
|
||||
/// serves both halves of <see cref="ListTablesSql"/>. <c>column_id</c> is Oracle's
|
||||
/// <c>ORDINAL_POSITION</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>ALL_TAB_COLUMNS.NULLABLE</c> is <c>Y</c>/<c>N</c> where <c>INFORMATION_SCHEMA.IS_NULLABLE</c> is
|
||||
/// <c>YES</c>/<c>NO</c>, so it is translated rather than merely aliased: a column that carries a
|
||||
/// shared name across dialects has to carry shared <em>values</em> too, or the first consumer to read
|
||||
/// it gets a different answer per provider.
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
// Columns of one relation, from ALL_TAB_COLUMNS — which covers views as well as tables, so it
|
||||
// serves both halves of ListTablesSql. column_id is Oracle's
|
||||
// ORDINAL_POSITION.
|
||||
//
|
||||
// ALL_TAB_COLUMNS.NULLABLE is Y/N where INFORMATION_SCHEMA.IS_NULLABLE is
|
||||
// YES/NO, so it is translated rather than merely aliased: a column that carries a
|
||||
// shared name across dialects has to carry shared values too, or the first consumer to read
|
||||
// it gets a different answer per provider.
|
||||
public string ListColumnsSql =>
|
||||
"SELECT column_name AS COLUMN_NAME, data_type AS DATA_TYPE, " +
|
||||
"CASE nullable WHEN 'Y' THEN 'YES' ELSE 'NO' END AS IS_NULLABLE " +
|
||||
"FROM ALL_TAB_COLUMNS WHERE owner = :p_schema AND table_name = :p_table ORDER BY column_id";
|
||||
|
||||
/// <summary>
|
||||
/// Double-quotes one identifier part, doubling any embedded <c>"</c> — the Oracle escape rule, and the
|
||||
/// only metacharacter that could terminate a quoted identifier early. Everything else (quotes of the
|
||||
/// other kind, semicolons, comment markers) is inert inside the quotes and passes through unchanged.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Case matters once an identifier is quoted.</b> Oracle upper-cases unquoted identifiers but
|
||||
/// takes a quoted one literally, so <c>"speed"</c> and <c>"SPEED"</c> are different columns. This is
|
||||
/// safe here only because the catalog gate has already substituted the catalog's own spelling
|
||||
/// (<c>ALL_TAB_COLUMNS.column_name</c>, i.e. what the server actually stored) before anything reaches
|
||||
/// this method — an operator who authored <c>speed</c> against an Oracle <c>SPEED</c> column resolves
|
||||
/// to <c>SPEED</c> at Initialize, and it is <c>SPEED</c> that gets quoted.</para>
|
||||
/// <para><b>The length ceiling is measured in UTF-8 bytes, not characters.</b> Oracle's 128 bounds the
|
||||
/// name in the <em>database character set</em> (<c>AL32UTF8</c> on any modern estate), so counting
|
||||
/// UTF-16 code units would accept a 65-character Cyrillic name — 130 bytes — and let it fail at the
|
||||
/// server with <c>ORA-00972</c> rather than here, where the message can name the tag.
|
||||
/// <c>SqlServerDialect</c> deliberately does <b>not</b> do this: T-SQL's <c>sysname</c> is
|
||||
/// <c>nvarchar(128)</c>, a genuine 128-character limit.</para>
|
||||
/// <para><b>Rejects</b> (all as <see cref="ArgumentException"/>): null / empty / all-whitespace; longer
|
||||
/// than <see cref="MaxIdentifierLength"/> <b>bytes</b>; any control character (which covers embedded NUL, and the
|
||||
/// C0/C1 ranges that can truncate or corrupt a logged/audited statement); and any Unicode
|
||||
/// <see cref="UnicodeCategory.Format"/> character — bidi overrides, zero-width spaces, soft hyphen,
|
||||
/// BOM. Those last cannot escape the quotes, but they spoof the <em>rendered</em> 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) — the same log/audit-integrity concern that motivates the control-character rule.</para>
|
||||
/// <para>The rejection messages deliberately do <b>not</b> echo the offending value — it is untrusted
|
||||
/// input and this exception's text reaches the driver log.</para>
|
||||
/// <para><b>This is a backstop, not the only defence</b> — see
|
||||
/// <see cref="ISqlDialect.QuoteIdentifier"/> on the catalog gate. The rules below still run, and must
|
||||
/// hold for any future caller that has no catalog to check against.</para>
|
||||
/// </remarks>
|
||||
/// <param name="ident">The bare, unquoted identifier part.</param>
|
||||
/// <returns>The double-quoted identifier.</returns>
|
||||
/// <exception cref="ArgumentException">The value cannot be a valid Oracle identifier.</exception>
|
||||
/// <inheritdoc />
|
||||
// Double-quotes one identifier part, doubling any embedded " — the Oracle escape rule, and the
|
||||
// only metacharacter that could terminate a quoted identifier early. Everything else (quotes of the
|
||||
// other kind, semicolons, comment markers) is inert inside the quotes and passes through unchanged.
|
||||
//
|
||||
// Case matters once an identifier is quoted. Oracle upper-cases unquoted identifiers but
|
||||
// takes a quoted one literally, so "speed" and "SPEED" are different columns. This is
|
||||
// safe here only because the catalog gate has already substituted the catalog's own spelling
|
||||
// (ALL_TAB_COLUMNS.column_name, i.e. what the server actually stored) before anything reaches
|
||||
// this method — an operator who authored speed against an Oracle SPEED column resolves
|
||||
// to SPEED at Initialize, and it is SPEED that gets quoted.
|
||||
// The length ceiling is measured in UTF-8 bytes, not characters. Oracle's 128 bounds the
|
||||
// name in the database character set (AL32UTF8 on any modern estate), so counting
|
||||
// UTF-16 code units would accept a 65-character Cyrillic name — 130 bytes — and let it fail at the
|
||||
// server with ORA-00972 rather than here, where the message can name the tag.
|
||||
// SqlServerDialect deliberately does not do this: T-SQL's sysname is
|
||||
// nvarchar(128), a genuine 128-character limit.
|
||||
// Rejects (all as ArgumentException): null / empty / all-whitespace; longer
|
||||
// than MaxIdentifierLength bytes; any control character (which covers embedded NUL, and the
|
||||
// C0/C1 ranges that can truncate or corrupt a logged/audited statement); and any Unicode
|
||||
// UnicodeCategory.Format 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) — the same log/audit-integrity concern that motivates the control-character rule.
|
||||
// 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 — see
|
||||
// ISqlDialect.QuoteIdentifier on the catalog gate. The rules below still run, and must
|
||||
// hold for any future caller that has no catalog to check against.
|
||||
public string QuoteIdentifier(string ident)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ident))
|
||||
@@ -271,38 +247,34 @@ public sealed class OracleDialect : ISqlDialect
|
||||
return string.Concat("\"", ident.Replace("\"", "\"\"", StringComparison.Ordinal), "\"");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Folds an <c>ALL_TAB_COLUMNS.DATA_TYPE</c> family name onto a <see cref="DriverDataType"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b><c>NUMBER</c> maps to <see cref="DriverDataType.Float64"/>, and that is a deliberate
|
||||
/// under-specification.</b> Oracle has one numeric type: an integer column, a currency column and a
|
||||
/// scientific measurement are all <c>NUMBER</c>, differing only in the precision/scale this method
|
||||
/// never sees — its whole input is the family name. Choosing an integral driver type would truncate
|
||||
/// every scaled value, so the widest numeric type wins. Two consequences worth knowing: a
|
||||
/// <c>NUMBER(38)</c> beyond 2^53 loses low-order digits in a <c>double</c>, and an integer-valued
|
||||
/// column surfaces as a floating-point OPC UA node. The documented escape hatch for both is authoring
|
||||
/// an explicit <c>"type"</c> on the tag (<c>Int64</c> for a true integer key, <c>String</c> to keep an
|
||||
/// exact high-precision decimal), which overrides this inference.</para>
|
||||
/// <para><b><c>TIMESTAMP</c> is matched by prefix</b>, unlike every other entry. Oracle reports the
|
||||
/// fractional-seconds precision and the time-zone qualifier <em>inside</em> the family name —
|
||||
/// <c>TIMESTAMP(6)</c>, <c>TIMESTAMP(6) WITH TIME ZONE</c>, <c>TIMESTAMP(6) WITH LOCAL TIME ZONE</c> —
|
||||
/// where <c>INFORMATION_SCHEMA</c> would report a bare <c>timestamp</c>. An exact-match table would
|
||||
/// send all of them to the String fallback.</para>
|
||||
/// <para><b>Fallback:</b> an unrecognised family maps to <see cref="DriverDataType.String"/> and never
|
||||
/// throws — a schema browse must render a table containing an exotic column (<c>RAW</c>,
|
||||
/// <c>LONG RAW</c>, <c>BLOB</c>, <c>BFILE</c>, <c>ROWID</c>, <c>XMLTYPE</c>, an object type) rather
|
||||
/// than crash on it. Null/blank input takes the same fallback. <c>RAW</c>/<c>BLOB</c> land here for
|
||||
/// the same reason SQL Server's <c>varbinary</c> does: there is no binary driver type.</para>
|
||||
/// <para><b>Deliberate non-mapping:</b> the <c>INTERVAL</c> types are durations, not points in time,
|
||||
/// and take the String fallback rather than being mistaken for a
|
||||
/// <see cref="DriverDataType.DateTime"/> — the same call the SQL Server dialect makes for
|
||||
/// <c>time</c>.</para>
|
||||
/// <para>Oracle's <c>DATE</c> carries a time-of-day component (it is not a date-only type), so
|
||||
/// <see cref="DriverDataType.DateTime"/> is the faithful mapping rather than a widening.</para>
|
||||
/// </remarks>
|
||||
/// <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>
|
||||
/// <inheritdoc />
|
||||
// Folds an ALL_TAB_COLUMNS.DATA_TYPE family name onto a DriverDataType.
|
||||
//
|
||||
// NUMBER maps to DriverDataType.Float64, and that is a deliberate
|
||||
// under-specification. Oracle has one numeric type: an integer column, a currency column and a
|
||||
// scientific measurement are all NUMBER, differing only in the precision/scale this method
|
||||
// never sees — its whole input is the family name. Choosing an integral driver type would truncate
|
||||
// every scaled value, so the widest numeric type wins. Two consequences worth knowing: a
|
||||
// NUMBER(38) beyond 2^53 loses low-order digits in a double, and an integer-valued
|
||||
// column surfaces as a floating-point OPC UA node. The documented escape hatch for both is authoring
|
||||
// an explicit "type" on the tag (Int64 for a true integer key, String to keep an
|
||||
// exact high-precision decimal), which overrides this inference.
|
||||
// TIMESTAMP is matched by prefix, unlike every other entry. Oracle reports the
|
||||
// fractional-seconds precision and the time-zone qualifier inside the family name —
|
||||
// TIMESTAMP(6), TIMESTAMP(6) WITH TIME ZONE, TIMESTAMP(6) WITH LOCAL TIME ZONE —
|
||||
// where INFORMATION_SCHEMA would report a bare timestamp. An exact-match table would
|
||||
// send all of them to the String fallback.
|
||||
// Fallback: an unrecognised family maps to DriverDataType.String and never
|
||||
// throws — a schema browse must render a table containing an exotic column (RAW,
|
||||
// LONG RAW, BLOB, BFILE, ROWID, XMLTYPE, an object type) rather
|
||||
// than crash on it. Null/blank input takes the same fallback. RAW/BLOB land here for
|
||||
// the same reason SQL Server's varbinary does: there is no binary driver type.
|
||||
// Deliberate non-mapping: the INTERVAL types are durations, not points in time,
|
||||
// and take the String fallback rather than being mistaken for a
|
||||
// DriverDataType.DateTime — the same call the SQL Server dialect makes for
|
||||
// time.
|
||||
// Oracle's DATE carries a time-of-day component (it is not a date-only type), so
|
||||
// DriverDataType.DateTime is the faithful mapping rather than a widening.
|
||||
public DriverDataType MapColumnType(string sqlDataType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sqlDataType)) return DriverDataType.String;
|
||||
|
||||
Reference in New Issue
Block a user