diff --git a/docs/drivers/Sql.md b/docs/drivers/Sql.md index 9d32457f..a3988fb0 100644 --- a/docs/drivers/Sql.md +++ b/docs/drivers/Sql.md @@ -12,9 +12,20 @@ An ordinary SQL database — typically a MES / LIMS / historian staging table th already writes to. There is no protocol here and nothing to "connect" to a machine: the driver runs `SELECT`s on a timer and turns cells into tag values. -**v1 constructs SQL Server only** (`Microsoft.Data.SqlClient`). `SqlProvider` also declares -`Postgres`, `MySql`, `Odbc` and `Oracle`, but those are **reserved names, not working backends** — -authoring one fails the driver rather than silently falling back. +**Three providers construct**: `SqlServer` (`Microsoft.Data.SqlClient`), `Postgres` +(`Npgsql`) and `Oracle` (ODP.NET managed — **Oracle 12.2 or later**, the floor the +`FETCH FIRST` paging needs). `SqlProvider` also declares `MySql` and `Odbc`, but those remain +**reserved names, not working backends** — authoring one fails the driver rather than silently +falling back. + +Two provider-specific things are worth knowing before you author against them: + +- **Identifier case folds, and the catalog gate absorbs it.** Postgres lower-cases and Oracle + upper-cases unquoted identifiers. Every authored identifier is resolved to the catalog's own + spelling, so an authored `widgets` binds to Oracle's `WIDGETS` rather than failing. +- **Oracle `NUMBER` infers as `Float64`.** Column metadata does not carry precision/scale to the + type map, so an integer-shaped `NUMBER` still infers double. Use the per-tag `type` override + (see [Optional per-tag fields](#optional-per-tag-fields)) when the OPC UA type matters. **Read-only, structurally.** `SqlDriver` does not implement `IWritable`, so no configuration can produce a write. The `allowWrites` config field exists and is **inert**; the factory *warns* when it @@ -94,7 +105,7 @@ ignoring it later. | Key | Default | Notes | |---|---|---| -| `provider` | `SqlServer` | The only provider v1 constructs. | +| `provider` | `SqlServer` | `SqlServer`, `Postgres` or `Oracle`. `MySql` / `Odbc` are reserved and rejected. | | `connectionStringRef` | — | **A name, never a connection string.** See below. | | `defaultPollInterval` | `00:00:05` | Applied to any group without its own interval. | | `operationTimeout` | `00:00:15` | Client-side wall-clock deadline; a breach surfaces `BadTimeout`. | @@ -132,7 +143,9 @@ The two classes of authored string are handled differently, and deliberately so: concatenated into command text. - **Identifier-bearing** fields (`table`, `keyColumn`, `valueColumn`, `timestampColumn`, `columnName`, `whereColumn`, `topByTimestamp`) cannot be parameterised in SQL. They are validated - against **`INFORMATION_SCHEMA`** and dialect-quoted before they reach a command. + against the live catalog — **`INFORMATION_SCHEMA`** on SQL Server and Postgres, the **`ALL_*`** + views (`ALL_USERS` / `ALL_TABLES` ∪ `ALL_VIEWS` / `ALL_TAB_COLUMNS`) on Oracle — and + dialect-quoted before they reach a command. The parser deliberately does **not** sanitise either kind — sanitising a value would corrupt legitimate data, and sanitising an identifier would mask an authoring error the catalog check reports @@ -152,6 +165,10 @@ can commit columns as tags. A committed leaf carries **schema, table and column* cannot address a SQL tag, and an earlier version that emitted only the column produced tags whose typed editor opened empty. +Browse works against all three constructed providers; the catalog query is the dialect's own +(`INFORMATION_SCHEMA` on SQL Server and Postgres — with `pg_catalog` / `information_schema` +themselves excluded — and the `ALL_*` views on Oracle). + ## Authoring in the AdminUI - **Driver config** — `/raw` → driver node → *Configure*. Sql is a **single-connection** driver: the diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/SqlDriverForm.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/SqlDriverForm.razor index 744aee9f..9eb5efed 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/SqlDriverForm.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Forms/SqlDriverForm.razor @@ -18,10 +18,13 @@
- @* v1 constructs only SqlServer; the other SqlProvider members are reserved (P2). *@ + @* SqlServer, Postgres and Oracle construct a dialect; MySql/Odbc stay reserved + SqlProvider members and are deliberately not offered (authoring one fails the driver). *@ + + -
Only Microsoft SQL Server is supported in v1.
+
SQL Server, PostgreSQL and Oracle supported; MySql/Odbc reserved.
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/SqlDriverFormContractTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/SqlDriverFormContractTests.cs index 8bc28e88..492ce79c 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/SqlDriverFormContractTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/SqlDriverFormContractTests.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using Shouldly; using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Forms; using ZB.MOM.WW.OtOpcUa.Driver.Sql; using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts; @@ -86,4 +87,47 @@ public sealed class SqlDriverFormContractTests json.ShouldContain("\"SqlServer\""); JsonSerializer.Deserialize(json, FormOptions)!.Provider.ShouldBe(SqlProvider.SqlServer); } + + // ---- the multi-provider dropdown: every offered option survives the form model's own round-trip ---- + + /// + /// The dropdown offers SqlServer + Postgres + Oracle, so each must survive + /// FormModel.ToDto() → JSON → FormModel.FromDto() as a NAME string. The razor markup + /// itself is not reachable (no bUnit), so this pins the model half of the change: a provider the + /// dropdown can select but the form model cannot carry back would silently reset to SqlServer on + /// the next modal open. + /// + [Theory] + [InlineData(SqlProvider.SqlServer, "SqlServer")] + [InlineData(SqlProvider.Postgres, "Postgres")] + [InlineData(SqlProvider.Oracle, "Oracle")] + public void Every_dropdown_provider_round_trips_through_the_form_model(SqlProvider provider, string name) + { + var form = new SqlDriverForm.FormModel { Provider = provider, ConnectionStringRef = "MesStaging" }; + + var json = JsonSerializer.Serialize(form.ToDto(), FormOptions); + + json.ShouldContain($"\"provider\":\"{name}\""); + json.ShouldNotContain($"\"provider\":{(int)provider}"); // never an ordinal — that faults the factory read + var reloaded = SqlDriverForm.FormModel.FromDto( + JsonSerializer.Deserialize(json, FormOptions)!); + reloaded.Provider.ShouldBe(provider); + reloaded.ConnectionStringRef.ShouldBe("MesStaging"); + } + + /// + /// MySql and Odbc stay reserved-and-rejected (design §2), so the dropdown deliberately does not offer + /// them. Nothing in the form model blocks the value — this records the intent so a later "the enum has + /// five members, the dropdown should too" edit has to argue with a test. + /// + [Theory] + [InlineData(SqlProvider.MySql)] + [InlineData(SqlProvider.Odbc)] + public void Reserved_providers_are_not_offered_but_still_serialize_by_name(SqlProvider reserved) + { + var json = JsonSerializer.Serialize( + new SqlDriverForm.FormModel { Provider = reserved, ConnectionStringRef = "R" }.ToDto(), FormOptions); + + json.ShouldContain($"\"provider\":\"{reserved}\""); + } }