f5c30ed0ef
v2-ci / unit-tests (push) Has been cancelled
Item 2: a tag's optional `write` block picks the statement the driver builds from its own catalog-validated identifiers (no authored SQL; values always bound): - Update (default, unchanged back-compat UPDATE ... WHERE key; 0 rows = BadNoData) - Insert (key+value[+stamp]; append for a WideRow topByTimestamp tag) - Upsert: SQL Server MERGE WITH (HOLDLOCK), PostgreSQL INSERT ... ON CONFLICT, MySQL ON DUPLICATE KEY UPDATE (1-or-2 rows), Oracle MERGE from DUAL - Procedure: named stored procedure resolved against the routine catalog by the catalog gate (INFORMATION_SCHEMA.ROUTINES / ALL_PROCEDURES), value/key/timestamp params by name (MySQL positional) - stampTimestamp sets the tag's timestamp column to the write time (UTC) Odbc serves Update/Insert only (shared SqlWriteSupport table). Constraint failures map to BadEntryExists (unique) / BadOutOfRange (other integrity, data exception); everything else, permission denied included, stays BadCommunicationError. WriteIdempotent stays false. The parser refuses malformed blocks (strict keys/enums/param charset); DraftValidator emits blocking SqlWriteBlockInvalid / SqlWriteModeUnsupported from the driver's own parse; the AdminUI editor surfaces the same message. Items 1 + 4 (docs/drivers/Sql.md): ODBC stays the ANSI SQL:2008 profile (native providers for SQL Server/MySQL, DB2 unsupported) with an ops note on driver manager + vendor driver on every Odbc and AdminUI host; read-only drivers require a SELECT-only login, writable drivers a login limited to their write targets. Tests: SqlWriteModeTests (parse, every dialect's SQL, gate, status map, SQLite end-to-end), DraftValidatorSqlWriteBlockTests, AdminUI round-trip; live SqlWriteModesLiveTests on SQL Server, PostgreSQL, MySQL, Oracle (seeds gain write_targets + set_target) and an Odbc write leg. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
68 lines
3.4 KiB
C#
68 lines
3.4 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|
using ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Tests;
|
|
|
|
/// <summary>
|
|
/// lmxopcua#781 item 2: a Sql tag's <c>write</c> block is checked at deploy by the driver's own parser
|
|
/// (<c>SqlWriteBlockInvalid</c>), and its mode against the instance's provider (<c>SqlWriteModeUnsupported</c>).
|
|
/// Both always block.
|
|
/// </summary>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class DraftValidatorSqlWriteBlockTests
|
|
{
|
|
private static string Kv(string write) =>
|
|
"""{"model":"KeyValue","table":"t","keyColumn":"k","keyValue":"a","valueColumn":"v","writable":true,"write":""" + write + "}";
|
|
|
|
private static DraftSnapshot Draft(string driverConfig, string tagConfig, string driverType = "Sql") => new()
|
|
{
|
|
GenerationId = 1, ClusterId = "c",
|
|
DriverInstances = [new DriverInstance
|
|
{ DriverInstanceId = "di", ClusterId = "c", Name = "sql", DriverType = driverType, DriverConfig = driverConfig }],
|
|
Devices = [new Device { DeviceId = "dev", DriverInstanceId = "di", Name = "d", DeviceConfig = "{}" }],
|
|
Tags = [new Tag { TagId = "t1", DeviceId = "dev", Name = "t", DataType = "Float64",
|
|
AccessLevel = TagAccessLevel.ReadWrite, TagConfig = tagConfig }],
|
|
};
|
|
|
|
[Theory]
|
|
[InlineData("""{"mode":"Merge"}""", "'write.mode'")]
|
|
[InlineData("""{"mode":"Update","oops":1}""", "unknown key 'oops'")]
|
|
[InlineData("""{"mode":"Procedure"}""", "needs 'write.procedure'")]
|
|
[InlineData("""{"stampTimestamp":true}""", "no timestamp column")]
|
|
public void A_malformed_write_block_is_a_blocking_deploy_error_with_the_parsers_reason(string write, string reason)
|
|
=> DraftValidator.Validate(Draft("{}", Kv(write))).ShouldContain(e =>
|
|
e.Code == SqlTagDeployChecks.WriteBlockInvalidCode && e.Context == "t1" && e.Message.Contains(reason));
|
|
|
|
[Theory]
|
|
[InlineData("""{"mode":"Update"}""")]
|
|
[InlineData("""{"mode":"Insert"}""")]
|
|
[InlineData("""{"mode":"Upsert"}""")]
|
|
[InlineData("""{"mode":"Procedure","procedure":"dbo.SetSp","keyParam":"key"}""")]
|
|
public void Positive_control_valid_blocks_on_SqlServer_are_quiet(string write)
|
|
=> DraftValidator.Validate(Draft("""{"provider":"SqlServer"}""", Kv(write)))
|
|
.ShouldNotContain(e => e.Code.StartsWith("SqlWrite", StringComparison.Ordinal));
|
|
|
|
[Theory]
|
|
[InlineData("""{"mode":"Upsert"}""", true)]
|
|
[InlineData("""{"mode":"Procedure","procedure":"p"}""", true)]
|
|
[InlineData("""{"mode":"Insert"}""", false)]
|
|
[InlineData("""{"mode":"Update"}""", false)]
|
|
public void Odbc_serves_update_and_insert_only(string write, bool refused)
|
|
=> DraftValidator.Validate(Draft("""{"provider":"odbc"}""", Kv(write)))
|
|
.Any(e => e.Code == SqlTagDeployChecks.WriteModeUnsupportedCode && e.Context == "t1")
|
|
.ShouldBe(refused);
|
|
|
|
[Fact]
|
|
public void An_absent_provider_is_SqlServer_so_upsert_is_accepted()
|
|
=> DraftValidator.Validate(Draft("{}", Kv("""{"mode":"Upsert"}""")))
|
|
.ShouldNotContain(e => e.Code == SqlTagDeployChecks.WriteModeUnsupportedCode);
|
|
|
|
[Fact]
|
|
public void Tags_of_other_drivers_are_not_checked()
|
|
=> DraftValidator.Validate(Draft("{}", Kv("""{"mode":"Merge"}"""), driverType: "Modbus"))
|
|
.ShouldNotContain(e => e.Code == SqlTagDeployChecks.WriteBlockInvalidCode);
|
|
}
|