fix(sql): Oracle bind names must not be reserved words (ORA-01745)

Found by the new OracleReadTests against a real Oracle 23 Free: EVERY Oracle
driver instance failed its Initialize-time catalog load, and every AdminUI browse
of a table's columns threw, with

    ORA-01745: invalid host/bind variable name

Oracle rejects a bind variable named after a reserved word, and the catalog
readers bind a parameter whose base name is `table` (SqlCatalogLoader /
SqlBrowseSession bind `schema` + `table`), so `ListColumnsSql` could not execute
at all. The provider was structurally non-functional.

OracleDialect now namespaces every marker/bind name through one `p_` constant
(`:p_k0` / `p_k0`), which a reserved word can never collide with, and its catalog
SQL is respelled to match. Prefixing everything rather than carrying a
reserved-word list is deliberate: the list is long and version-dependent, and a
future base name (`date`, `user`, `comment`, `level`…) would silently re-open the
same hole. Marker and bind name are derived from the same constant, so they
cannot drift apart.

No offline test could have caught this — only a server parses the SQL — so the
regression test asserts the invariant that made it possible (a bind name is never
the bare base name), with `table` called out explicitly.

Live evidence: 6/6 OracleReadTests red with ORA-01745 before, 6/6 green after,
against gvenzl/oracle-free:23-slim.
This commit is contained in:
Joseph Doherty
2026-08-05 07:15:23 -04:00
parent 429ead837f
commit ec90b7c416
2 changed files with 72 additions and 19 deletions
@@ -57,15 +57,45 @@ public class OracleDialectTests
}
[Fact]
public void ParameterMarker_isColonPrefixed_andTheBindNameIsBare()
public void ParameterMarker_isColonPrefixed_andTheBindNameIsTheSameNameWithoutTheColon()
{
var d = new OracleDialect();
// The whole reason ISqlDialect models these as two members: on ODP.NET they are different strings.
d.ParameterMarker("k0").ShouldBe(":k0");
d.ParameterBindName("k0").ShouldBe("k0");
d.ParameterMarker("schema").ShouldBe(":schema");
d.ParameterBindName("schema").ShouldBe("schema");
d.ParameterMarker("k0").ShouldBe(":p_k0");
d.ParameterBindName("k0").ShouldBe("p_k0");
d.ParameterMarker("schema").ShouldBe(":p_schema");
d.ParameterBindName("schema").ShouldBe("p_schema");
}
/// <summary>
/// No bind name may be spelled as its bare base name — the <c>p_</c> namespace is what stops one
/// colliding with an Oracle reserved word.
/// </summary>
/// <remarks>
/// <b>This is a regression test for a live-found, provider-fatal defect.</b> The catalog readers bind
/// a parameter whose base name is <c>table</c>, and Oracle rejects a bind variable named after a
/// reserved word outright: <c>ORA-01745: invalid host/bind variable name</c>. With the un-prefixed
/// names this dialect first shipped with, <c>ListColumnsSql</c> could not execute at all — so 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. It was invisible to every offline test (only a
/// server parses the SQL) and was caught by <c>OracleReadTests</c> against a real database.
/// <para><c>table</c> is asserted explicitly because it is the one base name in use today that is
/// actually reserved; the others guard the general rule.</para>
/// </remarks>
[Theory]
[InlineData("table")]
[InlineData("schema")]
[InlineData("k0")]
public void ParameterBindName_neverSurfacesTheBareBaseName_soItCannotBeAReservedWord(string baseName)
{
var d = new OracleDialect();
d.ParameterBindName(baseName).ShouldNotBe(baseName);
d.ParameterMarker(baseName).ShouldNotBe(":" + baseName);
// The two must stay derived from one another, or a statement's marker and its bind drift apart and
// ODP.NET silently leaves the parameter unbound.
d.ParameterMarker(baseName).ShouldBe(":" + d.ParameterBindName(baseName));
}
// ---- identity ----