63372e757a
SqlProvider.MySql is now a constructed backend instead of a reserved name. - MySqlDialect (Driver.Sql.Dialect): MySqlConnector 2.6.2 (MIT, fully managed, no vulnerable packages), backtick quoting with MySQL's 64-char / no-trailing- space rules, information_schema catalog SQL with the database as the schema (system databases excluded, aliases pinned), trailing LIMIT, a type map that also reads the reader-side spellings (INT UNSIGNED -> UInt32, BOOL -> Boolean). - SqlDialects: the one provider -> dialect map. The factory, the Test Connect probe and the browse picker each carried their own switch (and drifted once); they now all read this, and their refusals list the supported/reserved sets from it. - AdminUI driver form offers MySql. - Tests: MySqlDialectTests (golden quoting/catalog/type map/planner SQL); the provider-gate tests in factory, probe, browser and form now treat MySql as supported and Odbc as the reserved control. MySqlReadTests + Docker/mysql (throwaway mysql:8.4, SELECT-only login): liveness, KV/wide-row/topByTimestamp, INT keys vs authored text, case folding against case-sensitive table names, catalog rejection, browse walk, factory+probe, named query with a bound parameter, and a writing stored function whose effect the rolled-back transaction discards. (lmxopcua#753) Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
162 lines
6.8 KiB
C#
162 lines
6.8 KiB
C#
using Microsoft.Data.Sqlite;
|
|
using MySqlConnector;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Tests;
|
|
|
|
/// <summary>
|
|
/// Golden tests for <see cref="MySqlDialect"/> (lmxopcua#753), mirroring <see cref="PostgresDialectTests"/>
|
|
/// rule for rule: the injection boundary (values bound, identifiers backtick-quoted), the rejection
|
|
/// discipline, the exact catalog SQL, the end-of-statement <c>LIMIT</c>, and the type map — including the
|
|
/// reader-side spellings (<c>INT UNSIGNED</c>, <c>BOOL</c>) that <c>information_schema</c> never reports.
|
|
/// </summary>
|
|
public class MySqlDialectTests
|
|
{
|
|
[Fact]
|
|
public void QuoteIdentifier_backtickQuotesAndDoublesAnEmbeddedBacktick()
|
|
{
|
|
var d = new MySqlDialect();
|
|
d.QuoteIdentifier("Speed").ShouldBe("`Speed`");
|
|
d.QuoteIdentifier("a`b").ShouldBe("`a``b`");
|
|
}
|
|
|
|
[Fact]
|
|
public void QuoteIdentifier_neutralisesAClassicInjectionPayload()
|
|
=> new MySqlDialect().QuoteIdentifier("x` ; DROP TABLE users --")
|
|
.ShouldBe("`x`` ; DROP TABLE users --`");
|
|
|
|
[Theory]
|
|
[InlineData(0x0000)] // NUL — MySQL forbids it in identifiers
|
|
[InlineData(0x0001)]
|
|
[InlineData(0x007F)]
|
|
[InlineData(0x000A)]
|
|
public void QuoteIdentifier_rejectsControlCharacters(int codePoint)
|
|
=> Should.Throw<ArgumentException>(() => new MySqlDialect().QuoteIdentifier("a" + (char)codePoint + "b"));
|
|
|
|
[Theory]
|
|
[InlineData(0x200B)]
|
|
[InlineData(0x202E)]
|
|
[InlineData(0xFEFF)]
|
|
public void QuoteIdentifier_rejectsUnicodeFormatCharacters(int codePoint)
|
|
=> Should.Throw<ArgumentException>(() => new MySqlDialect().QuoteIdentifier("a" + (char)codePoint + "b"));
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("trailing ")] // MySQL refuses a name ending in a space, so it can denote no real object
|
|
public void QuoteIdentifier_rejectsBlankAndTrailingSpace(string? ident)
|
|
=> Should.Throw<ArgumentException>(() => new MySqlDialect().QuoteIdentifier(ident!));
|
|
|
|
[Fact]
|
|
public void QuoteIdentifier_acceptsExactly64Characters_andRejects65()
|
|
{
|
|
var d = new MySqlDialect();
|
|
d.QuoteIdentifier(new string('a', 64)).ShouldBe("`" + new string('a', 64) + "`");
|
|
Should.Throw<ArgumentException>(() => d.QuoteIdentifier(new string('a', 65)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("tinyint", DriverDataType.Int16)]
|
|
[InlineData("TINYINT UNSIGNED", DriverDataType.Int16)]
|
|
[InlineData("smallint", DriverDataType.Int16)]
|
|
[InlineData("SMALLINT UNSIGNED", DriverDataType.UInt16)]
|
|
[InlineData("mediumint", DriverDataType.Int32)]
|
|
[InlineData("int", DriverDataType.Int32)]
|
|
[InlineData("INT UNSIGNED", DriverDataType.UInt32)]
|
|
[InlineData("int(10) unsigned zerofill", DriverDataType.UInt32)]
|
|
[InlineData("bigint", DriverDataType.Int64)]
|
|
[InlineData("BIGINT UNSIGNED", DriverDataType.UInt64)]
|
|
[InlineData("BOOL", DriverDataType.Boolean)]
|
|
[InlineData("float", DriverDataType.Float32)]
|
|
[InlineData("double", DriverDataType.Float64)]
|
|
[InlineData("decimal", DriverDataType.Float64)]
|
|
[InlineData("varchar", DriverDataType.String)]
|
|
[InlineData("longtext", DriverDataType.String)]
|
|
[InlineData("enum", DriverDataType.String)]
|
|
[InlineData("datetime", DriverDataType.DateTime)]
|
|
[InlineData("timestamp", DriverDataType.DateTime)]
|
|
[InlineData("date", DriverDataType.DateTime)]
|
|
public void MapColumnType_mapsFamilies(string sql, DriverDataType expected)
|
|
=> new MySqlDialect().MapColumnType(sql).ShouldBe(expected);
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData("json")]
|
|
[InlineData("blob")]
|
|
[InlineData("bit")] // a bit string, deliberately NOT a boolean (as on Postgres)
|
|
[InlineData("time")] // a time of day, not a point in time
|
|
[InlineData("geometry")]
|
|
public void MapColumnType_unknownFamilyFallsBackToString_andNeverThrows(string? sql)
|
|
=> new MySqlDialect().MapColumnType(sql!).ShouldBe(DriverDataType.String);
|
|
|
|
[Fact]
|
|
public void CatalogSql_isInformationSchema_parameterized_andAliasesTheColumnsTheConsumersRead()
|
|
{
|
|
var d = new MySqlDialect();
|
|
d.ListSchemasSql.ShouldContain("information_schema.TABLES");
|
|
d.ListSchemasSql.ShouldContain("AS TABLE_SCHEMA");
|
|
foreach (var system in new[] { "'mysql'", "'information_schema'", "'performance_schema'", "'sys'" })
|
|
d.ListSchemasSql.ShouldContain(system);
|
|
d.DefaultSchemaSql.ShouldBe("SELECT DATABASE()");
|
|
|
|
d.ListTablesSql.ShouldContain("AS TABLE_NAME");
|
|
d.ListTablesSql.ShouldContain(d.ParameterMarker("schema"));
|
|
d.ListColumnsSql.ShouldContain("AS COLUMN_NAME");
|
|
d.ListColumnsSql.ShouldContain(d.ParameterMarker("schema"));
|
|
d.ListColumnsSql.ShouldContain(d.ParameterMarker("table"));
|
|
// schema is bound before table — the order the catalog loader binds in (and what a positional
|
|
// provider would need).
|
|
d.ListColumnsSql.IndexOf(d.ParameterMarker("schema"), StringComparison.Ordinal)
|
|
.ShouldBeLessThan(d.ListColumnsSql.IndexOf(d.ParameterMarker("table"), StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void RowLimits_areLimitN_inTheSuffixPosition()
|
|
{
|
|
var d = new MySqlDialect();
|
|
d.SingleRowLimitPrefix.ShouldBeEmpty();
|
|
d.SingleRowLimitSuffix.ShouldBe(" LIMIT 1");
|
|
d.RowLimitPrefix(5).ShouldBeEmpty();
|
|
d.RowLimitSuffix(5).ShouldBe(" LIMIT 5");
|
|
Should.Throw<ArgumentOutOfRangeException>(() => d.RowLimitSuffix(0));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dialect_identifiesItselfAsMySql_andExposesTheManagedFactory()
|
|
{
|
|
var d = new MySqlDialect();
|
|
d.Provider.ShouldBe(SqlProvider.MySql);
|
|
d.Factory.ShouldBeSameAs(MySqlConnectorFactory.Instance);
|
|
d.LivenessSql.ShouldBe("SELECT 1");
|
|
d.MaxBindParameters.ShouldBe(65535);
|
|
d.ParameterMarker("k0").ShouldBe("@k0");
|
|
d.ParameterBindName("k0").ShouldBe("@k0");
|
|
}
|
|
|
|
[Fact]
|
|
public void ConfigureCommandAndParameter_leaveAForeignProvidersObjectsUntouched()
|
|
{
|
|
using var command = new SqliteCommand();
|
|
new MySqlDialect().ConfigureCommand(command);
|
|
var parameter = command.CreateParameter();
|
|
((ISqlDialect)new MySqlDialect()).ConfigureParameter(parameter, "7");
|
|
parameter.Value.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void Planner_emitsBacktickQuotedSqlWithTheTrailingLimit()
|
|
{
|
|
var plan = SqlGroupPlanner.Plan(
|
|
[new SqlTagDefinition("A", SqlTagModel.WideRow, "plant.status", ColumnName: "oven_temp",
|
|
RowSelectorTopByTimestamp: "ts")],
|
|
new MySqlDialect()).ShouldHaveSingleItem();
|
|
|
|
plan.SqlText.ShouldBe("SELECT `oven_temp` FROM `plant`.`status` ORDER BY `ts` DESC LIMIT 1");
|
|
}
|
|
}
|