From 186e8ab4cd94fc3701f738add9554ec3b83a4e5e Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Mon, 30 Mar 2026 15:20:36 +0300 Subject: [PATCH 01/21] CI config changes (version, test triggers) + .props --- .github/workflows/SideBySide/config-ssl.json | 2 +- .github/workflows/SideBySide/config.json | 4 +- .github/workflows/config.yml | 37 ++++--- .github/workflows/fill_test_config.py | 2 +- Directory.Build.props | 3 +- Directory.Packages.props | 13 +-- LICENSE | 2 +- SingleStoreConnector.slnx | 17 ++++ SingleStoreNETConnector.sln | 96 ------------------- .../packages.lock.json | 52 +++++++--- 10 files changed, 92 insertions(+), 136 deletions(-) create mode 100644 SingleStoreConnector.slnx delete mode 100644 SingleStoreNETConnector.sln diff --git a/.github/workflows/SideBySide/config-ssl.json b/.github/workflows/SideBySide/config-ssl.json index 79fa0ef22..9f0c10097 100644 --- a/.github/workflows/SideBySide/config-ssl.json +++ b/.github/workflows/SideBySide/config-ssl.json @@ -1,7 +1,7 @@ { "Data": { "ConnectionString": "server=SINGLESTORE_HOST;user id=SQL_USER_NAME;password=SQL_USER_PASSWORD;port=3306;database=singlestoretest;sslmode=Required;certificate file=../../../../.ci/server/certs/ssl-client.pfx;", - "UnsupportedFeatures": "CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin,UnixDomainSocket,Sha256Password,GlobalLog,Redirection,CancelSleepSuccessfully,TlsFingerprintValidation", + "UnsupportedFeatures": "CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin,UnixDomainSocket,Sha256Password,GlobalLog,Redirection,CancelSleepSuccessfully,TlsFingerprintValidation,Vector", "CertificatesPath": "../../../../.ci/server/certs" } } diff --git a/.github/workflows/SideBySide/config.json b/.github/workflows/SideBySide/config.json index 4b6d5cb8c..d30d9f890 100644 --- a/.github/workflows/SideBySide/config.json +++ b/.github/workflows/SideBySide/config.json @@ -1,7 +1,7 @@ { "Data": { - "ConnectionString": "server=SINGLESTORE_HOST;user id=SQL_USER_NAME;password=SQL_USER_PASSWORD;port=3306;database=singlestoretest;sslmode=None", - "UnsupportedFeatures": "CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin,UnixDomainSocket,Sha256Password,GlobalLog,Redirection,CancelSleepSuccessfully,TlsFingerprintValidation", + "ConnectionString": "server=SINGLESTORE_HOST;user id=SQL_USER_NAME;password=SQL_USER_PASSWORD;port=3306;database=singlestoretest;sslmode=disabled;", + "UnsupportedFeatures": "CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin,UnixDomainSocket,Sha256Password,GlobalLog,Redirection,CancelSleepSuccessfully,TlsFingerprintValidation,ParsecAuthentication,Vector", "ManagedService": true } } diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 73ea2f35d..8529f6c76 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -1,11 +1,17 @@ name: SingleStore .NET Connector -on: [push] +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: [main] + workflow_call: + workflow_dispatch: env: - DOTNET_VERSION: 9.0.303 - TARGET_FRAMEWORK: net9.0 - CONNECTOR_VERSION: 1.3.0 + DOTNET_VERSION: 10.0.5 + TARGET_FRAMEWORK: net10.0 + CONNECTOR_VERSION: 1.4.0 LICENSE_KEY: ${{ secrets.LICENSE_KEY }} SQL_USER_PASSWORD: ${{ secrets.SQL_USER_PASSWORD }} S2MS_API_KEY: ${{ secrets.S2MS_API_KEY }} @@ -51,13 +57,13 @@ jobs: test-ubuntu: name: ${{ matrix.name }} - runs-on: ubuntu-22.04 needs: build-matrix + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: ${{ fromJson(needs.build-matrix.outputs.matrix) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Remove unnecessary pre-installed toolchains for free disk spaces run: | @@ -77,18 +83,18 @@ jobs: df -h - name: Set up .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y apt-transport-https + sudo apt-get install -y apt-transport-https sudo apt-get install -y mariadb-client-core-10.6 sudo apt-get install -y mariadb-client-10.6 sudo apt-get update - sudo apt-get install -y dotnet-sdk-9.0 + sudo apt-get install -y dotnet-sdk-10.0 dotnet --info - name: Start SingleStore Cluster @@ -138,18 +144,19 @@ jobs: dotnet test -f ${{ env.TARGET_FRAMEWORK }} -c Release --no-build cd ../../ + test-windows: runs-on: windows-latest strategy: fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install Python dependencies run: pip install singlestoredb - name: Install .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} @@ -161,7 +168,7 @@ jobs: - name: Fill test config run: python .github\workflows\fill_test_config.py - + - name: Export full connection string shell: bash run: echo "CONNECTION_STRING=$(< $HOME/CONNECTION_STRING)" >> $GITHUB_ENV @@ -189,10 +196,10 @@ jobs: if: startsWith(github.ref, 'refs/tags/') runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} @@ -206,7 +213,7 @@ jobs: run: dotnet pack -c Release --output net_connector -p:PackageVersion=${{ env.CONNECTOR_VERSION }} - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: net_connector path: net_connector/ diff --git a/.github/workflows/fill_test_config.py b/.github/workflows/fill_test_config.py index 5b11e3272..8a822e7f4 100644 --- a/.github/workflows/fill_test_config.py +++ b/.github/workflows/fill_test_config.py @@ -2,7 +2,7 @@ import os from s2ms_cluster import WORKSPACE_ENDPOINT_FILE -NET_FRAMEWORKS = ["net462", "net472", "net6.0", "net7.0", "net8.0", "net9.0"] +NET_FRAMEWORKS = ["net462", "net472", "net6.0", "net7.0", "net8.0", "net10.0"] if __name__ == "__main__": diff --git a/Directory.Build.props b/Directory.Build.props index ff778ea9a..1fed69cf4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,14 +2,13 @@ true - preview + 14.0 true $(NoWarn);1591;CA1708;CA1835;CA2215;CA5397;NU5105;SYSLIB0039 $(MSBuildThisFileDirectory)artifacts true low all - true diff --git a/Directory.Packages.props b/Directory.Packages.props index a01542f93..21f04bb41 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,27 +9,28 @@ + - - + + - - + - + - + + diff --git a/LICENSE b/LICENSE index a160f10d7..48c547baf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License Copyright (c) 2016-2021 Bradley Grainger -Copyright (c) 2022-2025 SingleStore, Inc. +Copyright (c) 2022-2026 SingleStore, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/SingleStoreConnector.slnx b/SingleStoreConnector.slnx new file mode 100644 index 000000000..7fa8f1875 --- /dev/null +++ b/SingleStoreConnector.slnx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/SingleStoreNETConnector.sln b/SingleStoreNETConnector.sln deleted file mode 100644 index 816a48433..000000000 --- a/SingleStoreNETConnector.sln +++ /dev/null @@ -1,96 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2010 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SingleStoreConnector", "src\SingleStoreConnector\SingleStoreConnector.csproj", "{F82378AF-274E-4FBA-8E45-27126D607B85}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SideBySide", "tests\SideBySide\SideBySide.csproj", "{407C2AC9-0CCA-4D6B-8698-362976D97730}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Conformance.Tests", "tests\Conformance.Tests\Conformance.Tests.csproj", "{CC0DA702-43E8-471E-9320-F36685C540A1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SingleStoreConnector.Logging.log4net", "src\SingleStoreConnector.Logging.log4net\SingleStoreConnector.Logging.log4net.csproj", "{A15647B8-FA3F-4536-BF4E-4F93F2FBFC75}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SingleStoreConnector.Logging.Microsoft.Extensions.Logging", "src\SingleStoreConnector.Logging.Microsoft.Extensions.Logging\SingleStoreConnector.Logging.Microsoft.Extensions.Logging.csproj", "{6A3F1732-F874-463E-9BB8-21690E7B8ED0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SingleStoreConnector.Logging.Serilog", "src\SingleStoreConnector.Logging.Serilog\SingleStoreConnector.Logging.Serilog.csproj", "{38806B85-6526-4A81-9905-45D3411B0AE2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SingleStoreConnector.Logging.NLog", "src\SingleStoreConnector.Logging.NLog\SingleStoreConnector.Logging.NLog.csproj", "{92015BEE-563A-4595-9243-0510D2B8767F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SingleStoreConnector.Authentication.Ed25519", "src\SingleStoreConnector.Authentication.Ed25519\SingleStoreConnector.Authentication.Ed25519.csproj", "{5DB4FA2E-910B-47CE-B467-F6852104D567}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SingleStoreConnector.Tests", "tests\SingleStoreConnector.Tests\SingleStoreConnector.Tests.csproj", "{7A89FC1F-4DD2-4D84-A489-57BE246A1FB8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SchemaCollectionGenerator", "tools\SchemaCollectionGenerator\SchemaCollectionGenerator.csproj", "{1FFFFB30-013B-44DD-B016-7266CDC14A87}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SingleStoreConnector.NativeAot.Tests", "tests\SingleStoreConnector.NativeAot.Tests\SingleStoreConnector.NativeAot.Tests.csproj", "{55C8FCF9-D492-497E-96A5-FEE307481EEA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SingleStoreConnector.DependencyInjection.Tests", "tests\SingleStoreConnector.DependencyInjection.Tests\SingleStoreConnector.DependencyInjection.Tests.csproj", "{69351125-C517-4FBE-A390-7A91630F170F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SingleStoreConnector.DependencyInjection", "src\SingleStoreConnector.DependencyInjection\SingleStoreConnector.DependencyInjection.csproj", "{9A904FE0-2558-4088-B56F-B4025A91AEEB}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {F82378AF-274E-4FBA-8E45-27126D607B85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F82378AF-274E-4FBA-8E45-27126D607B85}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F82378AF-274E-4FBA-8E45-27126D607B85}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F82378AF-274E-4FBA-8E45-27126D607B85}.Release|Any CPU.Build.0 = Release|Any CPU - {407C2AC9-0CCA-4D6B-8698-362976D97730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {407C2AC9-0CCA-4D6B-8698-362976D97730}.Debug|Any CPU.Build.0 = Debug|Any CPU - {407C2AC9-0CCA-4D6B-8698-362976D97730}.Release|Any CPU.ActiveCfg = Release|Any CPU - {407C2AC9-0CCA-4D6B-8698-362976D97730}.Release|Any CPU.Build.0 = Release|Any CPU - {CC0DA702-43E8-471E-9320-F36685C540A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CC0DA702-43E8-471E-9320-F36685C540A1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CC0DA702-43E8-471E-9320-F36685C540A1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CC0DA702-43E8-471E-9320-F36685C540A1}.Release|Any CPU.Build.0 = Release|Any CPU - {A15647B8-FA3F-4536-BF4E-4F93F2FBFC75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A15647B8-FA3F-4536-BF4E-4F93F2FBFC75}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A15647B8-FA3F-4536-BF4E-4F93F2FBFC75}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A15647B8-FA3F-4536-BF4E-4F93F2FBFC75}.Release|Any CPU.Build.0 = Release|Any CPU - {6A3F1732-F874-463E-9BB8-21690E7B8ED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6A3F1732-F874-463E-9BB8-21690E7B8ED0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6A3F1732-F874-463E-9BB8-21690E7B8ED0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6A3F1732-F874-463E-9BB8-21690E7B8ED0}.Release|Any CPU.Build.0 = Release|Any CPU - {38806B85-6526-4A81-9905-45D3411B0AE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38806B85-6526-4A81-9905-45D3411B0AE2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {38806B85-6526-4A81-9905-45D3411B0AE2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38806B85-6526-4A81-9905-45D3411B0AE2}.Release|Any CPU.Build.0 = Release|Any CPU - {92015BEE-563A-4595-9243-0510D2B8767F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {92015BEE-563A-4595-9243-0510D2B8767F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {92015BEE-563A-4595-9243-0510D2B8767F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {92015BEE-563A-4595-9243-0510D2B8767F}.Release|Any CPU.Build.0 = Release|Any CPU - {5DB4FA2E-910B-47CE-B467-F6852104D567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5DB4FA2E-910B-47CE-B467-F6852104D567}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5DB4FA2E-910B-47CE-B467-F6852104D567}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5DB4FA2E-910B-47CE-B467-F6852104D567}.Release|Any CPU.Build.0 = Release|Any CPU - {7A89FC1F-4DD2-4D84-A489-57BE246A1FB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7A89FC1F-4DD2-4D84-A489-57BE246A1FB8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7A89FC1F-4DD2-4D84-A489-57BE246A1FB8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7A89FC1F-4DD2-4D84-A489-57BE246A1FB8}.Release|Any CPU.Build.0 = Release|Any CPU - {1FFFFB30-013B-44DD-B016-7266CDC14A87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1FFFFB30-013B-44DD-B016-7266CDC14A87}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1FFFFB30-013B-44DD-B016-7266CDC14A87}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1FFFFB30-013B-44DD-B016-7266CDC14A87}.Release|Any CPU.Build.0 = Release|Any CPU - {55C8FCF9-D492-497E-96A5-FEE307481EEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {55C8FCF9-D492-497E-96A5-FEE307481EEA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {55C8FCF9-D492-497E-96A5-FEE307481EEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {55C8FCF9-D492-497E-96A5-FEE307481EEA}.Release|Any CPU.Build.0 = Release|Any CPU - {69351125-C517-4FBE-A390-7A91630F170F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {69351125-C517-4FBE-A390-7A91630F170F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {69351125-C517-4FBE-A390-7A91630F170F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {69351125-C517-4FBE-A390-7A91630F170F}.Release|Any CPU.Build.0 = Release|Any CPU - {9A904FE0-2558-4088-B56F-B4025A91AEEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9A904FE0-2558-4088-B56F-B4025A91AEEB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9A904FE0-2558-4088-B56F-B4025A91AEEB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9A904FE0-2558-4088-B56F-B4025A91AEEB}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {4784EA55-5DE0-404F-8BEC-E14C202F01FA} - EndGlobalSection -EndGlobal diff --git a/tests/SingleStoreConnector.NativeAot.Tests/packages.lock.json b/tests/SingleStoreConnector.NativeAot.Tests/packages.lock.json index 9dae639b0..3075979e3 100644 --- a/tests/SingleStoreConnector.NativeAot.Tests/packages.lock.json +++ b/tests/SingleStoreConnector.NativeAot.Tests/packages.lock.json @@ -4,15 +4,15 @@ "net8.0": { "Microsoft.DotNet.ILCompiler": { "type": "Direct", - "requested": "[8.0.15, )", - "resolved": "8.0.15", - "contentHash": "wMf2N7fJ846aKd73R5gqvtbyqu89/LywlWCtMyXUqKYc9DR3s9kUgNrLIsT9KeRwyinGFJDtRbiib0M4YBX6ZA==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "VkChfi7e/mf6peVZaJqgP3taLHKuYGyEc1C0T1YsKJCGMe1pGlzJCp5WMbBkHtZoct7K8ksCLPncmzBA7Mrb+w==" }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.15, )", - "resolved": "8.0.15", - "contentHash": "s4eXlcRGyHeCgFUGQnhq0e/SCHBPp0jOHgMqZg3fQ2OCHJSm1aOUhI6RFWuVIcEb9ig2WgI2kWukk8wu72EbUQ==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", @@ -24,6 +24,15 @@ "Microsoft.SourceLink.Common": "8.0.0" } }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", "resolved": "8.0.0", @@ -34,6 +43,11 @@ "resolved": "8.0.0", "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + }, "singlestoreconnector": { "type": "Project", "dependencies": { @@ -66,15 +80,15 @@ "net9.0": { "Microsoft.DotNet.ILCompiler": { "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "G85txKEuQ8s64BG9Pk3TbmE+cDgKReepnIPNfC1lNks4u2v5SkQhjCuuSAep+H2xtgFpYU7w9LFOF+vVtJZguA==" + "requested": "[9.0.14, )", + "resolved": "9.0.14", + "contentHash": "lhAsN6MyG7tSo4qPZNHdGKqSFjU0re4vqoJdjRu6MFYxNbroAUzrEc7lTLm5Dh/H5mqucdAs4ADJHZII4GxgGQ==" }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "xUdlUxiFwXhTYhB4VxKg/IA0+jlZXJPo70LYuMryWbJHdonIpZjw+7DO2B0pWwpXIOs6MlH5WVXPEtfrGEcVZA==" + "requested": "[9.0.14, )", + "resolved": "9.0.14", + "contentHash": "+MeWjj5sGq6Oj/l0E9RPMgXDyCIPxczzCbGuvuVTZFEGiy2S/atsfoAoKUnkEin/GeGpN+HenCzRmiQKSc99eQ==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", @@ -86,6 +100,15 @@ "Microsoft.SourceLink.Common": "8.0.0" } }, + "StyleCop.Analyzers": { + "type": "Direct", + "requested": "[1.2.0-beta.556, )", + "resolved": "1.2.0-beta.556", + "contentHash": "llRPgmA1fhC0I0QyFLEcjvtM2239QzKr/tcnbsjArLMJxJlu0AA5G7Fft0OI30pHF3MW63Gf4aSSsjc5m82J1Q==", + "dependencies": { + "StyleCop.Analyzers.Unstable": "1.2.0.556" + } + }, "Microsoft.Build.Tasks.Git": { "type": "Transitive", "resolved": "8.0.0", @@ -96,6 +119,11 @@ "resolved": "8.0.0", "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" }, + "StyleCop.Analyzers.Unstable": { + "type": "Transitive", + "resolved": "1.2.0.556", + "contentHash": "zvn9Mqs/ox/83cpYPignI8hJEM2A93s2HkHs8HYMOAQW0PkampyoErAiIyKxgTLqbbad29HX/shv/6LGSjPJNQ==" + }, "singlestoreconnector": { "type": "Project", "dependencies": { From cc2a684a921562189dacba164006c6ddf166f150 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Tue, 31 Mar 2026 13:36:14 +0300 Subject: [PATCH 02/21] update .csproj files --- src/Directory.Build.props | 7 --- ...oreConnector.Authentication.Ed25519.csproj | 21 +++++-- ...eStoreConnector.DependencyInjection.csproj | 2 +- ...ogging.Microsoft.Extensions.Logging.csproj | 2 +- .../SingleStoreConnector.Logging.NLog.csproj | 2 +- ...ingleStoreConnector.Logging.Serilog.csproj | 2 +- ...ingleStoreConnector.Logging.log4net.csproj | 2 +- .../SingleStoreConnector.csproj | 4 +- .../Conformance.Tests.csproj | 3 +- tests/SideBySide/SideBySide.csproj | 9 ++- ...Connector.DependencyInjection.Tests.csproj | 2 +- ...ingleStoreConnector.NativeAot.Tests.csproj | 2 +- .../packages.lock.json | 60 ++++++++++++++----- .../SingleStoreConnector.Tests.csproj | 4 +- .../SchemaCollectionGenerator.csproj | 2 +- 15 files changed, 78 insertions(+), 46 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cefebf31e..f1a56faf0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -20,13 +20,6 @@ true - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - diff --git a/src/SingleStoreConnector.Authentication.Ed25519/SingleStoreConnector.Authentication.Ed25519.csproj b/src/SingleStoreConnector.Authentication.Ed25519/SingleStoreConnector.Authentication.Ed25519.csproj index ceb6dae8d..ff32bf2c2 100644 --- a/src/SingleStoreConnector.Authentication.Ed25519/SingleStoreConnector.Authentication.Ed25519.csproj +++ b/src/SingleStoreConnector.Authentication.Ed25519/SingleStoreConnector.Authentication.Ed25519.csproj @@ -1,15 +1,16 @@ - net462;netstandard2.0 + net462;net472;netstandard2.0;netstandard2.1;net6.0 SingleStoreConnector Ed25519 Authentication Plugin - Implements the client_ed25519 authentication plugin for SingleStore. + Implements the client_ed25519 and parsec authentication plugins for SingleStore. Copyright 2019–2021 Bradley Grainger - Copyright 2022-2025 SingleStore Inc. + Copyright 2022-2026 SingleStore Inc. Bradley Grainger README.md - singlestore;singlestoreconnector;authentication;ed25519 - SA1001;SA1002;SA1005;SA1011;SA1012;SA1021;SA1025;SA1106;SA1107;SA1111;SA1119;SA1121;SA1300;SA1307;SA1312;SA1401;SA1413;SA1501;SA1505;SA1507;SA1508;SA1512;SA1518;SA1601 + singlestore;singlestoreconnector;authentication;ed25519;parsec + +CA1305;CA1507;CA1802;CA2208;CS0649;IDE0049;SA1001;SA1002;SA1005;SA1011;SA1012;SA1021;SA1025;SA1028;SA1106;SA1107;SA1111;SA1119;SA1121;SA1124;SA1137;SA1214;SA1300;SA1307;SA1309;SA1312;SA1313;SA1401;SA1413;SA1501;SA1505;SA1507;SA1508;SA1509;SA1512;SA1515;SA1518;SA1520;SA1601 SingleStoreConnector.Authentication.Ed25519 @@ -17,8 +18,16 @@ + + + + + + + + - + diff --git a/src/SingleStoreConnector.DependencyInjection/SingleStoreConnector.DependencyInjection.csproj b/src/SingleStoreConnector.DependencyInjection/SingleStoreConnector.DependencyInjection.csproj index 74aa1ed7b..85f1372b4 100644 --- a/src/SingleStoreConnector.DependencyInjection/SingleStoreConnector.DependencyInjection.csproj +++ b/src/SingleStoreConnector.DependencyInjection/SingleStoreConnector.DependencyInjection.csproj @@ -8,7 +8,7 @@ SingleStoreConnector Dependency Injection Helpers Provides extension methods for integrating SingleStoreConnector with .NET dependency injection. Copyright 2022 Bradley Grainger - Copyright 2022-2025 SingleStore Inc. + Copyright 2022-2026 SingleStore Inc. Bradley Grainger README.md singlestoreconnector;dependency injection;iservicecollection diff --git a/src/SingleStoreConnector.Logging.Microsoft.Extensions.Logging/SingleStoreConnector.Logging.Microsoft.Extensions.Logging.csproj b/src/SingleStoreConnector.Logging.Microsoft.Extensions.Logging/SingleStoreConnector.Logging.Microsoft.Extensions.Logging.csproj index aeb9145ac..20f845f68 100644 --- a/src/SingleStoreConnector.Logging.Microsoft.Extensions.Logging/SingleStoreConnector.Logging.Microsoft.Extensions.Logging.csproj +++ b/src/SingleStoreConnector.Logging.Microsoft.Extensions.Logging/SingleStoreConnector.Logging.Microsoft.Extensions.Logging.csproj @@ -5,7 +5,7 @@ SingleStoreConnector Logging Adapter for Microsoft.Extensions.Logging Writes SingleStoreConnector logging output to Microsoft.Extensions.Logging with one line of code. Copyright 2017–2021 Bradley Grainger - Copyright 2022-2025 SingleStore Inc. + Copyright 2022-2026 SingleStore Inc. Bradley Grainger README.md singlestore;singlestoreconnector;async;ado.net;database;netcore;logging diff --git a/src/SingleStoreConnector.Logging.NLog/SingleStoreConnector.Logging.NLog.csproj b/src/SingleStoreConnector.Logging.NLog/SingleStoreConnector.Logging.NLog.csproj index ee53e9584..b067d6378 100644 --- a/src/SingleStoreConnector.Logging.NLog/SingleStoreConnector.Logging.NLog.csproj +++ b/src/SingleStoreConnector.Logging.NLog/SingleStoreConnector.Logging.NLog.csproj @@ -5,7 +5,7 @@ SingleStoreConnector Logging Adapter for NLog Writes lightly-structured SingleStoreConnector logging output to NLog. Copyright 2018–2021 Bradley Grainger - Copyright 2022-2025 SingleStore Inc. + Copyright 2022-2026 SingleStore Inc. Rolf Kristensen README.md singlestore;singlestoreconnector;async;ado.net;database;netcore;NLog;logging diff --git a/src/SingleStoreConnector.Logging.Serilog/SingleStoreConnector.Logging.Serilog.csproj b/src/SingleStoreConnector.Logging.Serilog/SingleStoreConnector.Logging.Serilog.csproj index a4e573015..cfc58a54f 100644 --- a/src/SingleStoreConnector.Logging.Serilog/SingleStoreConnector.Logging.Serilog.csproj +++ b/src/SingleStoreConnector.Logging.Serilog/SingleStoreConnector.Logging.Serilog.csproj @@ -5,7 +5,7 @@ SingleStoreConnector Logging Adapter for Serilog Writes lightly-structured SingleStoreConnector logging output to Serilog. Copyright 2017–2021 Bradley Grainger - Copyright 2022-2025 SingleStore Inc. + Copyright 2022-2026 SingleStore Inc. Marc Lewandowski README.md singlestore;singlestoreconnector;async;ado.net;database;netcore;serilog;logging diff --git a/src/SingleStoreConnector.Logging.log4net/SingleStoreConnector.Logging.log4net.csproj b/src/SingleStoreConnector.Logging.log4net/SingleStoreConnector.Logging.log4net.csproj index 7fc139a96..a5a091dc8 100644 --- a/src/SingleStoreConnector.Logging.log4net/SingleStoreConnector.Logging.log4net.csproj +++ b/src/SingleStoreConnector.Logging.log4net/SingleStoreConnector.Logging.log4net.csproj @@ -5,7 +5,7 @@ SingleStoreConnector Logging Adapter for log4net Writes SingleStoreConnector logging output to log4net with one line of code. Copyright 2017–2021 Bradley Grainger - Copyright 2022-2025 SingleStore Inc. + Copyright 2022-2026 SingleStore Inc. Bradley Grainger README.md singlestore;singlestoreconnector;async;ado.net;database;netcore;log4net;logging diff --git a/src/SingleStoreConnector/SingleStoreConnector.csproj b/src/SingleStoreConnector/SingleStoreConnector.csproj index 64bc461c0..5d796a70f 100644 --- a/src/SingleStoreConnector/SingleStoreConnector.csproj +++ b/src/SingleStoreConnector/SingleStoreConnector.csproj @@ -1,10 +1,10 @@ - net462;net471;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0 + net462;net471;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0;net10.0 A truly async SingleStore ADO.NET provider. Copyright 2016–2021 Bradley Grainger - Copyright 2022-2025 SingleStore Inc. + Copyright 2022-2026 SingleStore Inc. SingleStore Inc. SingleStoreConnector SingleStoreConnector diff --git a/tests/Conformance.Tests/Conformance.Tests.csproj b/tests/Conformance.Tests/Conformance.Tests.csproj index ca4f8001c..2e53564af 100644 --- a/tests/Conformance.Tests/Conformance.Tests.csproj +++ b/tests/Conformance.Tests/Conformance.Tests.csproj @@ -1,13 +1,12 @@  - net9.0 + net10.0 0.1.0 true true ..\..\SingleStoreConnector.snk true - 11.0 diff --git a/tests/SideBySide/SideBySide.csproj b/tests/SideBySide/SideBySide.csproj index d5c0a908e..861c2b8e6 100644 --- a/tests/SideBySide/SideBySide.csproj +++ b/tests/SideBySide/SideBySide.csproj @@ -1,12 +1,12 @@  - net462;net481;net6.0;net8.0;net9.0 + net462;net481;net6.0;net8.0;net10.0 false - net9.0 + net10.0 BASELINE $(NoWarn);MSB3246 @@ -17,15 +17,15 @@ true ..\..\SingleStoreConnector.snk true + true true SideBySide SideBySide true true 64 - 11.0 enable - $(NoWarn);xUnit1030 + $(NoWarn);SA0001;SA1021;SA1133;xUnit1030 @@ -41,7 +41,6 @@ - diff --git a/tests/SingleStoreConnector.DependencyInjection.Tests/SingleStoreConnector.DependencyInjection.Tests.csproj b/tests/SingleStoreConnector.DependencyInjection.Tests/SingleStoreConnector.DependencyInjection.Tests.csproj index 82b767a95..c364bea5b 100644 --- a/tests/SingleStoreConnector.DependencyInjection.Tests/SingleStoreConnector.DependencyInjection.Tests.csproj +++ b/tests/SingleStoreConnector.DependencyInjection.Tests/SingleStoreConnector.DependencyInjection.Tests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 true true ..\..\SingleStoreConnector.snk diff --git a/tests/SingleStoreConnector.NativeAot.Tests/SingleStoreConnector.NativeAot.Tests.csproj b/tests/SingleStoreConnector.NativeAot.Tests/SingleStoreConnector.NativeAot.Tests.csproj index b84356939..2cbf4d232 100644 --- a/tests/SingleStoreConnector.NativeAot.Tests/SingleStoreConnector.NativeAot.Tests.csproj +++ b/tests/SingleStoreConnector.NativeAot.Tests/SingleStoreConnector.NativeAot.Tests.csproj @@ -2,7 +2,7 @@ Exe - net8.0;net9.0 + net8.0;net10.0 enable enable true diff --git a/tests/SingleStoreConnector.NativeAot.Tests/packages.lock.json b/tests/SingleStoreConnector.NativeAot.Tests/packages.lock.json index 3075979e3..22d501bef 100644 --- a/tests/SingleStoreConnector.NativeAot.Tests/packages.lock.json +++ b/tests/SingleStoreConnector.NativeAot.Tests/packages.lock.json @@ -1,18 +1,18 @@ { "version": 2, "dependencies": { - "net8.0": { + "net10.0": { "Microsoft.DotNet.ILCompiler": { "type": "Direct", - "requested": "[8.0.25, )", - "resolved": "8.0.25", - "contentHash": "VkChfi7e/mf6peVZaJqgP3taLHKuYGyEc1C0T1YsKJCGMe1pGlzJCp5WMbBkHtZoct7K8ksCLPncmzBA7Mrb+w==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "yadTZIkStCVsG8nGwvfroSfBApPsgjQbodQyaIfp53dgayE0qhZpywixiCB6lx57JYQ+KVg1m1AFLrj54pxpZg==" }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.25, )", - "resolved": "8.0.25", - "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", @@ -77,18 +77,34 @@ } } }, - "net9.0": { + "net10.0/linux-x64": { "Microsoft.DotNet.ILCompiler": { "type": "Direct", - "requested": "[9.0.14, )", - "resolved": "9.0.14", - "contentHash": "lhAsN6MyG7tSo4qPZNHdGKqSFjU0re4vqoJdjRu6MFYxNbroAUzrEc7lTLm5Dh/H5mqucdAs4ADJHZII4GxgGQ==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "yadTZIkStCVsG8nGwvfroSfBApPsgjQbodQyaIfp53dgayE0qhZpywixiCB6lx57JYQ+KVg1m1AFLrj54pxpZg==", + "dependencies": { + "runtime.linux-x64.Microsoft.DotNet.ILCompiler": "10.0.5" + } + }, + "runtime.linux-x64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "QYAggijHfk8FpLrPVhaESEOCYGWzEjA9JLapzg6XMFK5TGmwQ2HIqEprFv28MwvB5GdPGaZVLXE5icnkgLUfSA==" + } + }, + "net8.0": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "VkChfi7e/mf6peVZaJqgP3taLHKuYGyEc1C0T1YsKJCGMe1pGlzJCp5WMbBkHtZoct7K8ksCLPncmzBA7Mrb+w==" }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.14, )", - "resolved": "9.0.14", - "contentHash": "+MeWjj5sGq6Oj/l0E9RPMgXDyCIPxczzCbGuvuVTZFEGiy2S/atsfoAoKUnkEin/GeGpN+HenCzRmiQKSc99eQ==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", @@ -152,6 +168,22 @@ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" } } + }, + "net8.0/linux-x64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "VkChfi7e/mf6peVZaJqgP3taLHKuYGyEc1C0T1YsKJCGMe1pGlzJCp5WMbBkHtZoct7K8ksCLPncmzBA7Mrb+w==", + "dependencies": { + "runtime.linux-x64.Microsoft.DotNet.ILCompiler": "8.0.25" + } + }, + "runtime.linux-x64.Microsoft.DotNet.ILCompiler": { + "type": "Transitive", + "resolved": "8.0.25", + "contentHash": "QjZ3EEvEkNoQkr8ByKabhYz8pp80zVRx9otEZeg2reVdX57btV1Qn5yUAjCfYsK/RNxWA5zn3rqlG3OiS17Yiw==" + } } } } \ No newline at end of file diff --git a/tests/SingleStoreConnector.Tests/SingleStoreConnector.Tests.csproj b/tests/SingleStoreConnector.Tests/SingleStoreConnector.Tests.csproj index 283bacbe1..da7e6ed5b 100644 --- a/tests/SingleStoreConnector.Tests/SingleStoreConnector.Tests.csproj +++ b/tests/SingleStoreConnector.Tests/SingleStoreConnector.Tests.csproj @@ -1,7 +1,7 @@ - net481;net9.0 + net481;net8.0;net10.0 @@ -15,7 +15,7 @@ true ..\..\SingleStoreConnector.snk true - 12.0 + $(NoWarn);SA1021 diff --git a/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.csproj b/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.csproj index 85c1ed799..cfbfc836c 100644 --- a/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.csproj +++ b/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.csproj @@ -2,7 +2,7 @@ Exe - net9.0 + net10.0 enable enable true From e34c6e8168bc21253feb7b0f4f9af46eaa71ed98 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Tue, 31 Mar 2026 14:23:49 +0300 Subject: [PATCH 03/21] update SingleStoreConnector.Authentication.Ed25519 --- .../Chaos.NaCl/CryptoBytes.cs | 37 +++++++ .../Chaos.NaCl/Ed25519.cs | 50 +++++++++ .../Internal/Ed25519Ref10/GroupElement.cs | 99 +++++++++-------- .../Chaos.NaCl/Internal/Ed25519Ref10/base2.cs | 50 +++++++++ .../Internal/Ed25519Ref10/keypair.cs | 33 ++++++ .../Chaos.NaCl/Internal/Ed25519Ref10/sign.cs | 50 +++++++++ .../Internal/Ed25519Ref10/sqrtm1.cs | 9 ++ .../CompatibilitySuppressions.xml | 8 ++ .../Ed25519AuthenticationPlugin.cs | 31 +++++- .../ParsecAuthenticationPlugin.cs | 104 ++++++++++++++++++ ...oreConnector.Authentication.Ed25519.csproj | 4 - .../docs/README.md | 10 +- .../docs/README.md | 2 +- 13 files changed, 432 insertions(+), 55 deletions(-) create mode 100644 src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs create mode 100644 src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs create mode 100644 src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs create mode 100644 src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs create mode 100644 src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs create mode 100644 src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs create mode 100644 src/SingleStoreConnector.Authentication.Ed25519/CompatibilitySuppressions.xml create mode 100644 src/SingleStoreConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs diff --git a/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs new file mode 100644 index 000000000..1cdef9558 --- /dev/null +++ b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs @@ -0,0 +1,37 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Chaos.NaCl +{ + internal static class CryptoBytes + { + public static void Wipe(byte[] data) + { + if (data == null) + throw new ArgumentNullException("data"); + InternalWipe(data, 0, data.Length); + } + + // Secure wiping is hard + // * the GC can move around and copy memory + // Perhaps this can be avoided by using unmanaged memory or by fixing the position of the array in memory + // * Swap files and error dumps can contain secret information + // It seems possible to lock memory in RAM, no idea about error dumps + // * Compiler could optimize out the wiping if it knows that data won't be read back + // I hope this is enough, suppressing inlining + // but perhaps `RtlSecureZeroMemory` is needed + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void InternalWipe(byte[] data, int offset, int count) + { + Array.Clear(data, offset, count); + } + + // shallow wipe of structs + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void InternalWipe(ref T data) + where T : struct + { + data = default(T); + } + } +} diff --git a/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs new file mode 100644 index 000000000..f3a0b7011 --- /dev/null +++ b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs @@ -0,0 +1,50 @@ +using System; +using System.Security.Cryptography; +using Chaos.NaCl.Internal.Ed25519Ref10; + +namespace Chaos.NaCl +{ + internal static class Ed25519 + { + public static readonly int PublicKeySizeInBytes = 32; + public static readonly int SignatureSizeInBytes = 64; + public static readonly int ExpandedPrivateKeySizeInBytes = 32 * 2; + public static readonly int PrivateKeySeedSizeInBytes = 32; + public static readonly int SharedKeySizeInBytes = 32; + + public static void Sign(ArraySegment signature, ArraySegment message, ArraySegment expandedPrivateKey) + { + if (signature.Array == null) + throw new ArgumentNullException("signature.Array"); + if (signature.Count != SignatureSizeInBytes) + throw new ArgumentException("signature.Count"); + if (expandedPrivateKey.Array == null) + throw new ArgumentNullException("expandedPrivateKey.Array"); + if (expandedPrivateKey.Count != ExpandedPrivateKeySizeInBytes) + throw new ArgumentException("expandedPrivateKey.Count"); + if (message.Array == null) + throw new ArgumentNullException("message.Array"); + Ed25519Operations.crypto_sign2(signature.Array, signature.Offset, message.Array, message.Offset, message.Count, expandedPrivateKey.Array, expandedPrivateKey.Offset); + } + + public static byte[] Sign(byte[] message, byte[] expandedPrivateKey) + { + var signature = new byte[SignatureSizeInBytes]; + Sign(new ArraySegment(signature), new ArraySegment(message), new ArraySegment(expandedPrivateKey)); + return signature; + } + + public static void KeyPairFromSeed(out byte[] publicKey, out byte[] expandedPrivateKey, byte[] privateKeySeed) + { + if (privateKeySeed == null) + throw new ArgumentNullException("privateKeySeed"); + if (privateKeySeed.Length != PrivateKeySeedSizeInBytes) + throw new ArgumentException("privateKeySeed"); + var pk = new byte[PublicKeySizeInBytes]; + var sk = new byte[ExpandedPrivateKeySizeInBytes]; + Ed25519Operations.crypto_sign_keypair(pk, 0, sk, 0, privateKeySeed, 0); + publicKey = pk; + expandedPrivateKey = sk; + } + } +} diff --git a/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs index 9ae034b6e..692a3cc8b 100644 --- a/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs +++ b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs @@ -1,52 +1,63 @@ -namespace Chaos.NaCl.Internal.Ed25519Ref10; +using System; -/* -ge means group element. - -Here the group is the set of pairs (x,y) of field elements (see fe.h) -satisfying -x^2 + y^2 = 1 + d x^2y^2 -where d = -121665/121666. +namespace Chaos.NaCl.Internal.Ed25519Ref10 +{ + /* + ge means group element. -Representations: - ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z - ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT - ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T - ge_precomp (Duif): (y+x,y-x,2dxy) -*/ + Here the group is the set of pairs (x,y) of field elements (see fe.h) + satisfying -x^2 + y^2 = 1 + d x^2y^2 + where d = -121665/121666. -internal struct GroupElementP2 -{ - public FieldElement X; - public FieldElement Y; - public FieldElement Z; -} ; + Representations: + ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z + ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT + ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T + ge_precomp (Duif): (y+x,y-x,2dxy) + */ -internal struct GroupElementP3 -{ - public FieldElement X; - public FieldElement Y; - public FieldElement Z; - public FieldElement T; -} ; + internal struct GroupElementP2 + { + public FieldElement X; + public FieldElement Y; + public FieldElement Z; + } ; -internal struct GroupElementP1P1 -{ - public FieldElement X; - public FieldElement Y; - public FieldElement Z; - public FieldElement T; -} ; + internal struct GroupElementP3 + { + public FieldElement X; + public FieldElement Y; + public FieldElement Z; + public FieldElement T; + } ; -internal struct GroupElementPreComp -{ - public FieldElement yplusx; - public FieldElement yminusx; - public FieldElement xy2d; + internal struct GroupElementP1P1 + { + public FieldElement X; + public FieldElement Y; + public FieldElement Z; + public FieldElement T; + } ; - public GroupElementPreComp(FieldElement yplusx, FieldElement yminusx, FieldElement xy2d) + internal struct GroupElementPreComp + { + public FieldElement yplusx; + public FieldElement yminusx; + public FieldElement xy2d; + + public GroupElementPreComp(FieldElement yplusx, FieldElement yminusx, FieldElement xy2d) + { + this.yplusx = yplusx; + this.yminusx = yminusx; + this.xy2d = xy2d; + } + } ; + + internal struct GroupElementCached { - this.yplusx = yplusx; - this.yminusx = yminusx; - this.xy2d = xy2d; - } -} ; + public FieldElement YplusX; + public FieldElement YminusX; + public FieldElement Z; + public FieldElement T2d; + } ; +} diff --git a/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs new file mode 100644 index 000000000..0f5a69a45 --- /dev/null +++ b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs @@ -0,0 +1,50 @@ +using System; + +namespace Chaos.NaCl.Internal.Ed25519Ref10 +{ + internal static partial class LookupTables + { + internal static readonly GroupElementPreComp[] Base2 = new GroupElementPreComp[]{ + new GroupElementPreComp( + new FieldElement( 25967493,-14356035,29566456,3660896,-12694345,4014787,27544626,-11754271,-6079156,2047605 ), + new FieldElement( -12545711,934262,-2722910,3049990,-727428,9406986,12720692,5043384,19500929,-15469378 ), + new FieldElement( -8738181,4489570,9688441,-14785194,10184609,-12363380,29287919,11864899,-24514362,-4438546 ) + ), + new GroupElementPreComp( + new FieldElement( 15636291,-9688557,24204773,-7912398,616977,-16685262,27787600,-14772189,28944400,-1550024 ), + new FieldElement( 16568933,4717097,-11556148,-1102322,15682896,-11807043,16354577,-11775962,7689662,11199574 ), + new FieldElement( 30464156,-5976125,-11779434,-15670865,23220365,15915852,7512774,10017326,-17749093,-9920357 ) + ), + new GroupElementPreComp( + new FieldElement( 10861363,11473154,27284546,1981175,-30064349,12577861,32867885,14515107,-15438304,10819380 ), + new FieldElement( 4708026,6336745,20377586,9066809,-11272109,6594696,-25653668,12483688,-12668491,5581306 ), + new FieldElement( 19563160,16186464,-29386857,4097519,10237984,-4348115,28542350,13850243,-23678021,-15815942 ) + ), + new GroupElementPreComp( + new FieldElement( 5153746,9909285,1723747,-2777874,30523605,5516873,19480852,5230134,-23952439,-15175766 ), + new FieldElement( -30269007,-3463509,7665486,10083793,28475525,1649722,20654025,16520125,30598449,7715701 ), + new FieldElement( 28881845,14381568,9657904,3680757,-20181635,7843316,-31400660,1370708,29794553,-1409300 ) + ), + new GroupElementPreComp( + new FieldElement( -22518993,-6692182,14201702,-8745502,-23510406,8844726,18474211,-1361450,-13062696,13821877 ), + new FieldElement( -6455177,-7839871,3374702,-4740862,-27098617,-10571707,31655028,-7212327,18853322,-14220951 ), + new FieldElement( 4566830,-12963868,-28974889,-12240689,-7602672,-2830569,-8514358,-10431137,2207753,-3209784 ) + ), + new GroupElementPreComp( + new FieldElement( -25154831,-4185821,29681144,7868801,-6854661,-9423865,-12437364,-663000,-31111463,-16132436 ), + new FieldElement( 25576264,-2703214,7349804,-11814844,16472782,9300885,3844789,15725684,171356,6466918 ), + new FieldElement( 23103977,13316479,9739013,-16149481,817875,-15038942,8965339,-14088058,-30714912,16193877 ) + ), + new GroupElementPreComp( + new FieldElement( -33521811,3180713,-2394130,14003687,-16903474,-16270840,17238398,4729455,-18074513,9256800 ), + new FieldElement( -25182317,-4174131,32336398,5036987,-21236817,11360617,22616405,9761698,-19827198,630305 ), + new FieldElement( -13720693,2639453,-24237460,-7406481,9494427,-5774029,-6554551,-15960994,-2449256,-14291300 ) + ), + new GroupElementPreComp( + new FieldElement( -3151181,-5046075,9282714,6866145,-31907062,-863023,-18940575,15033784,25105118,-7894876 ), + new FieldElement( -24326370,15950226,-31801215,-14592823,-11662737,-5090925,1573892,-2625887,2198790,-15804619 ), + new FieldElement( -3099351,10324967,-2241613,7453183,-5446979,-2735503,-13812022,-16236442,-32461234,-12290683 ) + ) + }; + } +} diff --git a/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs new file mode 100644 index 000000000..6779f94a4 --- /dev/null +++ b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs @@ -0,0 +1,33 @@ +using System; +using System.Security.Cryptography; + +namespace Chaos.NaCl.Internal.Ed25519Ref10 +{ + internal static partial class Ed25519Operations + { + public static void crypto_sign_keypair(byte[] pk, int pkoffset, byte[] sk, int skoffset, byte[] seed, int seedoffset) + { + GroupElementP3 A; + int i; + + Array.Copy(seed, seedoffset, sk, skoffset, 32); +#if NET5_0_OR_GREATER + byte[] h = SHA512.HashData(sk.AsSpan(skoffset, 32)); +#else + using var hash = SHA512.Create(); + byte[] h = hash.ComputeHash(sk, skoffset, 32); +#endif + ScalarOperations.sc_clamp(h, 0); + + GroupOperations.ge_scalarmult_base(out A, h, 0); + GroupOperations.ge_p3_tobytes(pk, pkoffset, ref A); + + for (i = 0; i < 32; ++i) sk[skoffset + 32 + i] = pk[pkoffset + i]; +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + CryptographicOperations.ZeroMemory(h); +#else + CryptoBytes.Wipe(h); +#endif + } + } +} diff --git a/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs new file mode 100644 index 000000000..a56625557 --- /dev/null +++ b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs @@ -0,0 +1,50 @@ +using System; +using System.Security.Cryptography; + +namespace Chaos.NaCl.Internal.Ed25519Ref10 +{ + internal static partial class Ed25519Operations + { + public static void crypto_sign2( + byte[] sig, int sigoffset, + byte[] m, int moffset, int mlen, + byte[] sk, int skoffset) + { + byte[] az; + byte[] r; + byte[] hram; + GroupElementP3 R; + using (var hasher = SHA512.Create()) + { + az = hasher.ComputeHash(sk, skoffset, 32); + ScalarOperations.sc_clamp(az, 0); + + hasher.Initialize(); + hasher.TransformBlock(az, 32, 32, null, 0); + hasher.TransformFinalBlock(m, moffset, mlen); + r = hasher.Hash; + + ScalarOperations.sc_reduce(r); + GroupOperations.ge_scalarmult_base(out R, r, 0); + GroupOperations.ge_p3_tobytes(sig, sigoffset, ref R); + + hasher.Initialize(); + hasher.TransformBlock(sig, sigoffset, 32, null, 0); + hasher.TransformBlock(sk, skoffset + 32, 32, null, 0); + hasher.TransformFinalBlock(m, moffset, mlen); + hram = hasher.Hash; + + ScalarOperations.sc_reduce(hram); + var s = new byte[32];//todo: remove allocation + Array.Copy(sig, sigoffset + 32, s, 0, 32); + ScalarOperations.sc_muladd(s, hram, az, r); + Array.Copy(s, 0, sig, sigoffset + 32, 32); +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + CryptographicOperations.ZeroMemory(s); +#else + CryptoBytes.Wipe(s); +#endif + } + } + } +} diff --git a/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs new file mode 100644 index 000000000..dfa325fe0 --- /dev/null +++ b/src/SingleStoreConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs @@ -0,0 +1,9 @@ +using System; + +namespace Chaos.NaCl.Internal.Ed25519Ref10 +{ + internal static partial class LookupTables + { + internal static FieldElement sqrtm1 = new FieldElement(-32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482); + } +} diff --git a/src/SingleStoreConnector.Authentication.Ed25519/CompatibilitySuppressions.xml b/src/SingleStoreConnector.Authentication.Ed25519/CompatibilitySuppressions.xml new file mode 100644 index 000000000..0aa1c2f25 --- /dev/null +++ b/src/SingleStoreConnector.Authentication.Ed25519/CompatibilitySuppressions.xml @@ -0,0 +1,8 @@ + + + + + PKV006 + .NETFramework,Version=v4.5 + + diff --git a/src/SingleStoreConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs b/src/SingleStoreConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs index 79d3ccec6..87a6f11bd 100644 --- a/src/SingleStoreConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs +++ b/src/SingleStoreConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs @@ -10,7 +10,9 @@ namespace SingleStoreConnector.Authentication.Ed25519; /// Provides an implementation of the client_ed25519 authentication plugin for MariaDB. /// /// See Authentication Plugin - ed25519. -public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin2 +#pragma warning disable CS0618 // Type or member is obsolete +public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin3, IAuthenticationPlugin2 +#pragma warning restore CS0618 // Type or member is obsolete { /// /// Registers the Ed25519 authentication plugin with SingleStoreConnector. You must call this method once before @@ -32,7 +34,7 @@ public static void Install() /// public byte[] CreateResponse(string password, ReadOnlySpan authenticationData) { - CreateResponseAndHash(password, authenticationData, out _, out var authenticationResponse); + CreateResponseAndPasswordHash(password, authenticationData, out var authenticationResponse, out _); return authenticationResponse; } @@ -41,11 +43,20 @@ public byte[] CreateResponse(string password, ReadOnlySpan authenticationD /// public byte[] CreatePasswordHash(string password, ReadOnlySpan authenticationData) { - CreateResponseAndHash(password, authenticationData, out var passwordHash, out _); + CreateResponseAndPasswordHash(password, authenticationData, out _, out var passwordHash); return passwordHash; } - private static void CreateResponseAndHash(string password, ReadOnlySpan authenticationData, out byte[] passwordHash, out byte[] authenticationResponse) + /// + /// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification). + /// + /// The client's password. + /// The authentication data supplied by the server; this is the auth method data + /// from the Authentication + /// Method Switch Request Packet. + /// The authentication response. + /// The authentication-method-specific hash of the client's password. + public void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash) { // Java reference: https://github.com/MariaDB/mariadb-connector-j/blob/master/src/main/java/org/mariadb/jdbc/internal/com/send/authentication/Ed25519PasswordPlugin.java // C reference: https://github.com/MariaDB/server/blob/592fe954ef82be1bc08b29a8e54f7729eb1e1343/plugin/auth_ed25519/ref10/sign.c#L7 @@ -77,8 +88,12 @@ private static void CreateResponseAndHash(string password, ReadOnlySpan au az[31] |= 64; */ +#if NET5_0_OR_GREATER + byte[] az = SHA512.HashData(passwordBytes); +#else using var sha512 = SHA512.Create(); byte[] az = sha512.ComputeHash(passwordBytes); +#endif ScalarOperations.sc_clamp(az, 0); /*** Java @@ -104,7 +119,11 @@ private static void CreateResponseAndHash(string password, ReadOnlySpan au byte[] sm = new byte[64 + authenticationData.Length]; authenticationData.CopyTo(sm.AsSpan().Slice(64)); Buffer.BlockCopy(az, 32, sm, 32, 32); +#if NET5_0_OR_GREATER + byte[] nonce = SHA512.HashData(sm.AsSpan(32, authenticationData.Length + 32)); +#else byte[] nonce = sha512.ComputeHash(sm, 32, authenticationData.Length + 32); +#endif /*** Java ScalarOps scalar = new ScalarOps(); @@ -162,7 +181,11 @@ private static void CreateResponseAndHash(string password, ReadOnlySpan au return 0; */ +#if NET5_0_OR_GREATER + var hram = SHA512.HashData(sm); +#else var hram = sha512.ComputeHash(sm); +#endif ScalarOperations.sc_reduce(hram); var temp = new byte[32]; ScalarOperations.sc_muladd(temp, hram, az, nonce); diff --git a/src/SingleStoreConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs b/src/SingleStoreConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs new file mode 100644 index 000000000..0b07ea589 --- /dev/null +++ b/src/SingleStoreConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs @@ -0,0 +1,104 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using System.Threading; + +namespace SingleStoreConnector.Authentication.Ed25519; + +/// +/// Provides an implementation of the Parsec authentication plugin for MariaDB. +/// +public sealed class ParsecAuthenticationPlugin : IAuthenticationPlugin3 +{ + /// + /// Registers the Parsec authentication plugin with MySqlConnector. You must call this method once before + /// opening a connection that uses Parsec authentication. + /// + public static void Install() + { + if (Interlocked.CompareExchange(ref s_isInstalled, 1, 0) == 0) + AuthenticationPlugins.Register(new ParsecAuthenticationPlugin()); + } + + /// + /// Gets the authentication plugin name. + /// + public string Name => "parsec"; + + /// + /// Creates the authentication response. + /// + public byte[] CreateResponse(string password, ReadOnlySpan authenticationData) + { + CreateResponseAndPasswordHash(password, authenticationData, out var response, out _); + return response; + } + + /// + /// Creates the authentication response. + /// + public void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash) + { + // first 32 bytes are server scramble + var serverScramble = authenticationData.Slice(0, 32); + + // generate client scramble +#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + Span clientScramble = stackalloc byte[32]; + RandomNumberGenerator.Fill(clientScramble); +#else + var clientScramble = new byte[32]; + using var randomNumberGenerator = RandomNumberGenerator.Create(); + randomNumberGenerator.GetBytes(clientScramble); +#endif + + // parse extended salt from remaining authentication data and verify format + var extendedSalt = authenticationData.Slice(32); + if (extendedSalt[0] != (byte) 'P') + throw new ArgumentException("Invalid extended salt", nameof(authenticationData)); + if (extendedSalt[1] is not (>= 0 and <= 3)) + throw new ArgumentException("Invalid iteration count", nameof(authenticationData)); + + var iterationCount = 1024 << extendedSalt[1]; + var salt = extendedSalt.Slice(2); + + // derive private key using PBKDF2-SHA512 + byte[] privateKeySeed; +#if NET6_0_OR_GREATER + privateKeySeed = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterationCount, HashAlgorithmName.SHA512, 32); +#elif NET472_OR_GREATER || NETSTANDARD2_1_OR_GREATER + using (var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt.ToArray(), iterationCount, HashAlgorithmName.SHA512)) + privateKeySeed = pbkdf2.GetBytes(32); +#else + privateKeySeed = Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2( + password, salt.ToArray(), Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivationPrf.HMACSHA512, + iterationCount, numBytesRequested: 32); +#endif + Chaos.NaCl.Ed25519.KeyPairFromSeed(out var publicKey, out var privateKey, privateKeySeed); + + // generate Ed25519 keypair and sign concatenated scrambles + var message = new byte[serverScramble.Length + clientScramble.Length]; + serverScramble.CopyTo(message); + clientScramble.CopyTo(message.AsSpan(serverScramble.Length)); + + var signature = Chaos.NaCl.Ed25519.Sign(message, privateKey); + +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER + CryptographicOperations.ZeroMemory(privateKey); +#endif + + // return client scramble followed by signature + authenticationResponse = new byte[clientScramble.Length + signature.Length]; + clientScramble.CopyTo(authenticationResponse.AsSpan()); + signature.CopyTo(authenticationResponse.AsSpan(clientScramble.Length)); + + // "password hash" for parsec is the extended salt followed by the public key + passwordHash = [(byte) 'P', (byte) iterationCount, .. salt, .. publicKey]; + } + + private ParsecAuthenticationPlugin() + { + } + + private static int s_isInstalled; +} diff --git a/src/SingleStoreConnector.Authentication.Ed25519/SingleStoreConnector.Authentication.Ed25519.csproj b/src/SingleStoreConnector.Authentication.Ed25519/SingleStoreConnector.Authentication.Ed25519.csproj index ff32bf2c2..2315aabbe 100644 --- a/src/SingleStoreConnector.Authentication.Ed25519/SingleStoreConnector.Authentication.Ed25519.csproj +++ b/src/SingleStoreConnector.Authentication.Ed25519/SingleStoreConnector.Authentication.Ed25519.csproj @@ -22,10 +22,6 @@ - - - - diff --git a/src/SingleStoreConnector.Authentication.Ed25519/docs/README.md b/src/SingleStoreConnector.Authentication.Ed25519/docs/README.md index 0dd5a9af7..b2d794296 100644 --- a/src/SingleStoreConnector.Authentication.Ed25519/docs/README.md +++ b/src/SingleStoreConnector.Authentication.Ed25519/docs/README.md @@ -1,7 +1,13 @@ ## About -This package implements the `client_ed25519` [authentication plugin for MariaDB](https://mariadb.com/kb/en/authentication-plugin-ed25519/). +This package implements the following authentication plugins for MariaDB: + +* [`client_ed25519`](https://mariadb.com/kb/en/authentication-plugin-ed25519/). +* [PARSEC](https://mariadb.com/kb/en/authentication-plugin-parsec/) ## How to Use -Call `Ed25519AuthenticationPlugin.Install()` from your application startup code to enable it. +Call either of the following methods from your application startup code to enable the corresponding authentication plugin: + +* `Ed25519AuthenticationPlugin.Install()` +* `ParsecAuthenticationPlugin.Install()` diff --git a/src/SingleStoreConnector.DependencyInjection/docs/README.md b/src/SingleStoreConnector.DependencyInjection/docs/README.md index 18e04b073..8dbac802a 100644 --- a/src/SingleStoreConnector.DependencyInjection/docs/README.md +++ b/src/SingleStoreConnector.DependencyInjection/docs/README.md @@ -51,7 +51,7 @@ builder.Services.AddSingleStoreDataSource("Server=server;User ID=test;Password=t ## Keyed Services -Use the `AddKeyedSingleStoreDataSource` method to register a `SingleStoreDataSource` as a [keyed service](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-9.0#keyed-services). +Use the `AddKeyedSingleStoreDataSource` method to register a `SingleStoreDataSource` as a [keyed service](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8/runtime#keyed-di-services). This is useful if you have multiple connection strings or need to connect to multiple databases. If the service key is a string, it will automatically be used as the `SingleStoreDataSource` name; to customize this, call the `AddKeyedSingleStoreDataSource(object?, string, Action)` overload and call `SingleStoreDataSourceBuilder.UseName`. From af4cb81ecca4e24d37929be34938f480ee87dd09 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Tue, 31 Mar 2026 15:19:41 +0300 Subject: [PATCH 04/21] grab block of updates for Authentication, ColumnReaders and Core folders --- .../Authentication/AuthenticationPlugins.cs | 10 -- .../Authentication/IAuthenticationPlugin.cs | 19 +++ .../ColumnReaders/ColumnReader.cs | 3 + .../ColumnReaders/VectorColumnReader.cs | 37 +++++ .../Core/CachedParameter.cs | 4 +- .../Core/CachedProcedure.cs | 17 +- .../Core/ColumnTypeMetadata.cs | 20 ++- .../Core/CommandExecutor.cs | 2 + .../Core/ConcatenatedCommandPayloadCreator.cs | 3 + .../Core/ICommandPayloadCreator.cs | 10 ++ src/SingleStoreConnector/Core/ResultSet.cs | 8 +- src/SingleStoreConnector/Core/Row.cs | 31 +--- .../Core/SchemaProvider.cs | 2 - .../Core/SchemaProvider.g.cs | 5 - .../Core/ServerSession.cs | 147 ++++++++++++------ .../Core/SingleCommandPayloadCreator.cs | 97 +++++++++++- 16 files changed, 310 insertions(+), 105 deletions(-) create mode 100644 src/SingleStoreConnector/ColumnReaders/VectorColumnReader.cs diff --git a/src/SingleStoreConnector/Authentication/AuthenticationPlugins.cs b/src/SingleStoreConnector/Authentication/AuthenticationPlugins.cs index 7386deb40..4b2e30697 100644 --- a/src/SingleStoreConnector/Authentication/AuthenticationPlugins.cs +++ b/src/SingleStoreConnector/Authentication/AuthenticationPlugins.cs @@ -13,18 +13,8 @@ public static class AuthenticationPlugins /// The authentication plugin. public static void Register(IAuthenticationPlugin plugin) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(plugin); -#else - if (plugin is null) - throw new ArgumentNullException(nameof(plugin)); -#endif -#if NET8_0_OR_GREATER ArgumentException.ThrowIfNullOrEmpty(plugin.Name); -#else - if (string.IsNullOrEmpty(plugin.Name)) - throw new ArgumentException("Invalid plugin name.", nameof(plugin)); -#endif lock (s_lock) s_plugins.Add(plugin.Name, plugin); } diff --git a/src/SingleStoreConnector/Authentication/IAuthenticationPlugin.cs b/src/SingleStoreConnector/Authentication/IAuthenticationPlugin.cs index 2be902d31..411525893 100644 --- a/src/SingleStoreConnector/Authentication/IAuthenticationPlugin.cs +++ b/src/SingleStoreConnector/Authentication/IAuthenticationPlugin.cs @@ -25,6 +25,7 @@ public interface IAuthenticationPlugin /// /// is an extension to that returns a hash of the client's password. /// +[Obsolete("Use IAuthenticationPlugin3 instead.")] public interface IAuthenticationPlugin2 : IAuthenticationPlugin { /// @@ -37,3 +38,21 @@ public interface IAuthenticationPlugin2 : IAuthenticationPlugin /// The authentication-method-specific hash of the client's password. byte[] CreatePasswordHash(string password, ReadOnlySpan authenticationData); } + +/// +/// is an extension to that also returns a hash of the client's password. +/// +/// If an authentication plugin supports this interface, the base method will not be called. +public interface IAuthenticationPlugin3 : IAuthenticationPlugin +{ + /// + /// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification). + /// + /// The client's password. + /// The authentication data supplied by the server; this is the auth method data + /// from the Authentication + /// Method Switch Request Packet. + /// The authentication response. + /// The authentication-method-specific hash of the client's password. + void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash); +} diff --git a/src/SingleStoreConnector/ColumnReaders/ColumnReader.cs b/src/SingleStoreConnector/ColumnReaders/ColumnReader.cs index e0bca6a64..2eaed4797 100644 --- a/src/SingleStoreConnector/ColumnReaders/ColumnReader.cs +++ b/src/SingleStoreConnector/ColumnReaders/ColumnReader.cs @@ -120,6 +120,9 @@ public static ColumnReader Create(bool isBinary, ColumnDefinitionPayload columnD case ColumnType.Null: return NullColumnReader.Instance; + case ColumnType.Vector: + return VectorColumnReader.Instance; + default: throw new NotImplementedException($"Reading {columnDefinition.ColumnType} not implemented"); } diff --git a/src/SingleStoreConnector/ColumnReaders/VectorColumnReader.cs b/src/SingleStoreConnector/ColumnReaders/VectorColumnReader.cs new file mode 100644 index 000000000..77166649f --- /dev/null +++ b/src/SingleStoreConnector/ColumnReaders/VectorColumnReader.cs @@ -0,0 +1,37 @@ +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using SingleStoreConnector.Protocol.Payloads; + +namespace SingleStoreConnector.ColumnReaders; + +internal sealed class VectorColumnReader : ColumnReader +{ + public static VectorColumnReader Instance { get; } = new(); + + public override object ReadValue(ReadOnlySpan data, ColumnDefinitionPayload columnDefinition) + { + if (BitConverter.IsLittleEndian) + { + return new ReadOnlyMemory(MemoryMarshal.Cast(data).ToArray()); + } + else + { + var floats = new float[data.Length / 4]; + +#if !NET5_0_OR_GREATER + var bytes = data.ToArray(); +#endif + for (var i = 0; i < floats.Length; i++) + { +#if NET5_0_OR_GREATER + floats[i] = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(i * 4)); +#else + Array.Reverse(bytes, i * 4, 4); + floats[i] = BitConverter.ToSingle(bytes, i * 4); +#endif + } + + return new ReadOnlyMemory(floats); + } + } +} diff --git a/src/SingleStoreConnector/Core/CachedParameter.cs b/src/SingleStoreConnector/Core/CachedParameter.cs index afb2f8b98..adbbc55d9 100644 --- a/src/SingleStoreConnector/Core/CachedParameter.cs +++ b/src/SingleStoreConnector/Core/CachedParameter.cs @@ -2,7 +2,7 @@ namespace SingleStoreConnector.Core; internal sealed class CachedParameter { - public CachedParameter(int ordinalPosition, string? mode, string name, string dataType, bool unsigned, int length) + public CachedParameter(int ordinalPosition, string? mode, string name, string dataType, bool unsigned, int length, SingleStoreGuidFormat guidFormat) { Position = ordinalPosition; if (Position == 0) @@ -14,7 +14,7 @@ public CachedParameter(int ordinalPosition, string? mode, string name, string da else if (string.Equals(mode, "out", StringComparison.OrdinalIgnoreCase)) Direction = ParameterDirection.Output; Name = name; - SingleStoreDbType = TypeMapper.Instance.GetSingleStoreDbType(dataType, unsigned, length); + SingleStoreDbType = TypeMapper.Instance.GetSingleStoreDbType(dataType, unsigned, length, guidFormat); Length = length; } diff --git a/src/SingleStoreConnector/Core/CachedProcedure.cs b/src/SingleStoreConnector/Core/CachedProcedure.cs index 6d761d86e..1314565f2 100644 --- a/src/SingleStoreConnector/Core/CachedProcedure.cs +++ b/src/SingleStoreConnector/Core/CachedProcedure.cs @@ -47,7 +47,8 @@ FROM information_schema.parameters !reader.IsDBNull(2) ? reader.GetString(2) : "", dataType, unsigned, - length + length, + connection.GuidFormat )); } } @@ -88,6 +89,10 @@ internal SingleStoreParameterCollection AlignParamsWithDb(SingleStoreParameterCo if (!alignParam.HasSetDbType) alignParam.SingleStoreDbType = cachedParam.SingleStoreDbType; + // for a GUID column, pass along the length so the out parameter can be cast to the right size + if (alignParam.SingleStoreDbType == SingleStoreDbType.Guid && cachedParam.Direction is ParameterDirection.Output or ParameterDirection.InputOutput) + alignParam.Size = cachedParam.Length; + // cached parameters are ordered by ordinal position alignedParams.Add(alignParam); } @@ -95,7 +100,7 @@ internal SingleStoreParameterCollection AlignParamsWithDb(SingleStoreParameterCo return alignedParams; } - internal static List ParseParameters(string parametersSql) + internal static List ParseParameters(string parametersSql, SingleStoreGuidFormat guidFormat) { // strip comments parametersSql = s_cStyleComments.Replace(parametersSql, ""); @@ -140,7 +145,7 @@ internal static List ParseParameters(string parametersSql) var name = parts.Groups[1].Success ? parts.Groups[1].Value.Replace("``", "`") : parts.Groups[2].Value; var dataType = ParseDataType(parts.Groups[3].Value, out var unsigned, out var length); - cachedParameters.Add(CreateCachedParameter(i + 1, direction, name, dataType, unsigned, length, originalString)); + cachedParameters.Add(CreateCachedParameter(i + 1, direction, name, dataType, unsigned, length, guidFormat, originalString)); } return cachedParameters; @@ -178,11 +183,11 @@ internal static string ParseDataType(string sql, out bool unsigned, out int leng return type ?? list[0]; } - private static CachedParameter CreateCachedParameter(int ordinal, string? direction, string name, string dataType, bool unsigned, int length, string originalSql) + private static CachedParameter CreateCachedParameter(int ordinal, string? direction, string name, string dataType, bool unsigned, int length, SingleStoreGuidFormat guidFormat, string originalSql) { try { - return new CachedParameter(ordinal, direction, name, dataType, unsigned, length); + return new CachedParameter(ordinal, direction, name, dataType, unsigned, length, guidFormat); } catch (NullReferenceException ex) { @@ -215,7 +220,7 @@ private static CachedParameter CreateCachedParameter(int ordinal, string? direct private static readonly Regex s_singleLineComments = new(@"(^|\s)--.*?$", RegexOptions.Multiline); private static readonly Regex s_multipleSpaces = new(@"\s+"); private static readonly Regex s_numericTypes = new(@"(DECIMAL|DEC|FIXED|NUMERIC|FLOAT|DOUBLE PRECISION|DOUBLE|REAL)\s*\([0-9]+(,\s*[0-9]+)\)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - private static readonly Regex s_enumOrSet = new(@"(ENUM|SET)\s*\([^)]+\)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + private static readonly Regex s_enumOrSet = new(@"(ENUM|SET)\s*\([^)]+\)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); private static readonly Regex s_parameterName = new(@"^(?:`((?:[\u0001-\u005F\u0061-\uFFFF]+|``)+)`|([A-Za-z0-9$_\u0080-\uFFFF]+)) (.*)$"); private static readonly Regex s_characterSet = new(" (CHARSET|CHARACTER SET) [A-Za-z0-9_]+", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); private static readonly Regex s_collate = new(" (COLLATE) [A-Za-z0-9_]+", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); diff --git a/src/SingleStoreConnector/Core/ColumnTypeMetadata.cs b/src/SingleStoreConnector/Core/ColumnTypeMetadata.cs index 423caad97..2ebbe3c5c 100644 --- a/src/SingleStoreConnector/Core/ColumnTypeMetadata.cs +++ b/src/SingleStoreConnector/Core/ColumnTypeMetadata.cs @@ -1,8 +1,21 @@ +using System.Runtime.CompilerServices; + namespace SingleStoreConnector.Core; -internal sealed class ColumnTypeMetadata(string dataTypeName, DbTypeMapping dbTypeMapping, SingleStoreDbType mySqlDbType, bool isUnsigned = false, bool binary = false, int length = 0, string? simpleDataTypeName = null, string? createFormat = null, long columnSize = 0) +internal sealed class ColumnTypeMetadata(string dataTypeName, DbTypeMapping dbTypeMapping, SingleStoreDbType mySqlDbType, bool isUnsigned = false, bool binary = false, int length = 0, string? simpleDataTypeName = null, string? createFormat = null, long columnSize = 0, SingleStoreGuidFormat guidFormat = SingleStoreGuidFormat.Default) { - public static string CreateLookupKey(string columnTypeName, bool isUnsigned, int length) => $"{columnTypeName}|{(isUnsigned ? "u" : "s")}|{length}"; + public static string CreateLookupKey(string columnTypeName, bool isUnsigned, int length, SingleStoreGuidFormat guidFormat) => + $"{columnTypeName}|{(isUnsigned ? "u" : "s")}|{length}|{GetGuidFormatLookupKey(guidFormat)}"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string GetGuidFormatLookupKey(SingleStoreGuidFormat guidFormat) => + guidFormat switch + { + SingleStoreGuidFormat.Char36 => "c36", + SingleStoreGuidFormat.Char32 => "c32", + SingleStoreGuidFormat.Binary16 or SingleStoreGuidFormat.TimeSwapBinary16 or SingleStoreGuidFormat.LittleEndianBinary16 => "b16", + _ => "def", + }; public string DataTypeName { get; } = dataTypeName; public string SimpleDataTypeName { get; } = simpleDataTypeName ?? dataTypeName; @@ -13,6 +26,7 @@ internal sealed class ColumnTypeMetadata(string dataTypeName, DbTypeMapping dbTy public long ColumnSize { get; } = columnSize; public bool IsUnsigned { get; } = isUnsigned; public int Length { get; } = length; + public SingleStoreGuidFormat GuidFormat { get; } = guidFormat; - public string CreateLookupKey() => CreateLookupKey(DataTypeName, IsUnsigned, Length); + public string CreateLookupKey() => CreateLookupKey(DataTypeName, IsUnsigned, Length, GuidFormat); } diff --git a/src/SingleStoreConnector/Core/CommandExecutor.cs b/src/SingleStoreConnector/Core/CommandExecutor.cs index a8418f4c4..b80eeda9a 100644 --- a/src/SingleStoreConnector/Core/CommandExecutor.cs +++ b/src/SingleStoreConnector/Core/CommandExecutor.cs @@ -39,6 +39,8 @@ public static async ValueTask ExecuteReaderAsync(CommandL } } + await payloadCreator.SendCommandPrologueAsync(connection, commandListPosition, ioBehavior, cancellationToken).ConfigureAwait(false); + var writer = new ByteBufferWriter(); //// cachedProcedures will be non-null if there is a stored procedure, which is also the only time it will be read if (!payloadCreator.WriteQueryCommand(ref commandListPosition, cachedProcedures!, writer, false)) diff --git a/src/SingleStoreConnector/Core/ConcatenatedCommandPayloadCreator.cs b/src/SingleStoreConnector/Core/ConcatenatedCommandPayloadCreator.cs index e628e1064..1c704798a 100644 --- a/src/SingleStoreConnector/Core/ConcatenatedCommandPayloadCreator.cs +++ b/src/SingleStoreConnector/Core/ConcatenatedCommandPayloadCreator.cs @@ -8,6 +8,9 @@ internal sealed class ConcatenatedCommandPayloadCreator : ICommandPayloadCreator { public static ICommandPayloadCreator Instance { get; } = new ConcatenatedCommandPayloadCreator(); + public ValueTask SendCommandPrologueAsync(SingleStoreConnection connection, CommandListPosition commandListPosition, IOBehavior ioBehavior, CancellationToken cancellationToken) => + default; + public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDictionary cachedProcedures, ByteBufferWriter writer, bool appendSemicolon) { if (commandListPosition.CommandIndex == commandListPosition.CommandCount) diff --git a/src/SingleStoreConnector/Core/ICommandPayloadCreator.cs b/src/SingleStoreConnector/Core/ICommandPayloadCreator.cs index 43ec66a15..9958016b0 100644 --- a/src/SingleStoreConnector/Core/ICommandPayloadCreator.cs +++ b/src/SingleStoreConnector/Core/ICommandPayloadCreator.cs @@ -7,6 +7,16 @@ namespace SingleStoreConnector.Core; /// internal interface ICommandPayloadCreator { + /// + /// Sends any data that is required to be sent to the server before the current command in the command list. + /// + /// The to which the data will be written. + /// The giving the current command and current prepared statement. + /// The IO behavior. + /// A cancellation token to cancel the asynchronous operation. + /// A representing the asynchronous operation or a completed if no data needed to be sent. + ValueTask SendCommandPrologueAsync(SingleStoreConnection connection, CommandListPosition commandListPosition, IOBehavior ioBehavior, CancellationToken cancellationToken); + /// /// Writes the payload for an "execute query" command to . /// diff --git a/src/SingleStoreConnector/Core/ResultSet.cs b/src/SingleStoreConnector/Core/ResultSet.cs index 419474560..d21f72f13 100644 --- a/src/SingleStoreConnector/Core/ResultSet.cs +++ b/src/SingleStoreConnector/Core/ResultSet.cs @@ -167,7 +167,7 @@ public async Task ReadResultSetHeaderAsync(IOBehavior ioBehavior) ContainsCommandParameters = true; WarningCount = 0; State = ResultSetState.ReadResultSetHeader; - if (DataReader.Activity is { IsAllDataRequested: true }) + if (DataReader.Activity is { IsAllDataRequested: true } && (Command?.Connection!.SingleStoreDataSource?.TracingOptions.EnableResultSetHeaderEvent ?? SingleStoreConnectorTracingOptions.Default.EnableResultSetHeaderEvent)) DataReader.Activity.AddEvent(new ActivityEvent("read-result-set-header")); break; } @@ -332,12 +332,8 @@ public bool HasRows public int GetOrdinal(string name) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(name); -#else - if (name is null) - throw new ArgumentNullException(nameof(name)); -#endif + if (!HasResultSet) throw new InvalidOperationException("There is no current result set."); diff --git a/src/SingleStoreConnector/Core/Row.cs b/src/SingleStoreConnector/Core/Row.cs index 77c102979..431c48794 100644 --- a/src/SingleStoreConnector/Core/Row.cs +++ b/src/SingleStoreConnector/Core/Row.cs @@ -77,13 +77,8 @@ public void SetData(ReadOnlyMemory data) public object GetValue(int ordinal) { -#if NET8_0_OR_GREATER ArgumentOutOfRangeException.ThrowIfNegative(ordinal); ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(ordinal, ResultSet.ColumnDefinitions!.Length); -#else - if (ordinal < 0 || ordinal >= ResultSet.ColumnDefinitions!.Length) - throw new ArgumentOutOfRangeException(nameof(ordinal), $"value must be between 0 and {ResultSet.ColumnDefinitions!.Length - 1}"); -#endif if (m_dataOffsetLengths[ordinal].Offset == -1) return DBNull.Value; @@ -235,8 +230,8 @@ public short GetInt16(int ordinal) public int GetInt32(int ordinal) { - if (ordinal < 0 || ordinal >= ResultSet.ColumnDefinitions!.Length) - throw new ArgumentOutOfRangeException(nameof(ordinal), $"value must be between 0 and {ResultSet.ColumnDefinitions!.Length - 1}"); + ArgumentOutOfRangeException.ThrowIfNegative(ordinal); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(ordinal, ResultSet.ColumnDefinitions!.Length); if (m_dataOffsetLengths[ordinal].Offset == -1) throw new InvalidCastException("Can't convert NULL to Int32"); @@ -431,12 +426,8 @@ public SingleStoreDecimal GetSingleStoreDecimal(int ordinal) public int GetValues(object[] values) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(values); -#else - if (values is null) - throw new ArgumentNullException(nameof(values)); -#endif + int count = Math.Min(values.Length, ResultSet.ColumnDefinitions!.Length); for (int i = 0; i < count; i++) values[i] = GetValue(i); @@ -461,7 +452,7 @@ private void CheckBinaryColumn(int ordinal) var columnType = column.ColumnType; if ((column.ColumnFlags & ColumnFlags.Binary) == 0 || (columnType != ColumnType.String && columnType != ColumnType.VarString && columnType != ColumnType.TinyBlob && - columnType != ColumnType.Blob && columnType != ColumnType.MediumBlob && columnType != ColumnType.LongBlob)) + columnType != ColumnType.Blob && columnType != ColumnType.MediumBlob && columnType != ColumnType.LongBlob && columnType != ColumnType.Vector)) { throw new InvalidCastException($"Can't convert {columnType} to bytes."); } @@ -469,24 +460,12 @@ private void CheckBinaryColumn(int ordinal) private static void CheckBufferArguments(long dataOffset, T[] buffer, int bufferOffset, int length) { -#if NET8_0_OR_GREATER ArgumentOutOfRangeException.ThrowIfNegative(dataOffset); ArgumentOutOfRangeException.ThrowIfGreaterThan(dataOffset, int.MaxValue); ArgumentOutOfRangeException.ThrowIfNegative(length); ArgumentOutOfRangeException.ThrowIfNegative(bufferOffset); ArgumentOutOfRangeException.ThrowIfGreaterThan(bufferOffset, buffer.Length); -#else - if (dataOffset < 0) - throw new ArgumentOutOfRangeException(nameof(dataOffset), dataOffset, nameof(dataOffset) + " must be non-negative"); - if (dataOffset > int.MaxValue) - throw new ArgumentOutOfRangeException(nameof(dataOffset), dataOffset, nameof(dataOffset) + " must be a 32-bit integer"); - if (length < 0) - throw new ArgumentOutOfRangeException(nameof(length), length, nameof(length) + " must be non-negative"); - if (bufferOffset < 0) - throw new ArgumentOutOfRangeException(nameof(bufferOffset), bufferOffset, nameof(bufferOffset) + " must be non-negative"); - if (bufferOffset > buffer.Length) - throw new ArgumentOutOfRangeException(nameof(bufferOffset), bufferOffset, nameof(bufferOffset) + " must be within the buffer"); -#endif + if (checked(bufferOffset + length) > buffer.Length) throw new ArgumentException(nameof(bufferOffset) + " + " + nameof(length) + " cannot exceed " + nameof(buffer) + "." + nameof(buffer.Length), nameof(length)); } diff --git a/src/SingleStoreConnector/Core/SchemaProvider.cs b/src/SingleStoreConnector/Core/SchemaProvider.cs index 2dd9bdc8b..978a3362d 100644 --- a/src/SingleStoreConnector/Core/SchemaProvider.cs +++ b/src/SingleStoreConnector/Core/SchemaProvider.cs @@ -387,9 +387,7 @@ private async Task FillDataTableAsync(IOBehavior ioBehavior, DataTable dataTable { await FillDataTableAsync(ioBehavior, dataTable, command => { -#pragma warning disable CA2100 command.CommandText = "SELECT " + string.Join(", ", dataTable.Columns.Cast().Select(static x => x!.ColumnName)) + " FROM INFORMATION_SCHEMA." + tableName; -#pragma warning restore CA2100 if (columns is { Count: > 0 }) { command.CommandText += " WHERE " + string.Join(" AND ", columns.Select(static x => $@"{x.Key} = @{x.Key}")); diff --git a/src/SingleStoreConnector/Core/SchemaProvider.g.cs b/src/SingleStoreConnector/Core/SchemaProvider.g.cs index be1098c55..c3771f09f 100644 --- a/src/SingleStoreConnector/Core/SchemaProvider.g.cs +++ b/src/SingleStoreConnector/Core/SchemaProvider.g.cs @@ -9,12 +9,7 @@ internal sealed partial class SchemaProvider { public async ValueTask GetSchemaAsync(IOBehavior ioBehavior, string collectionName, string?[]? restrictionValues, CancellationToken cancellationToken) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(collectionName); -#else - if (collectionName is null) - throw new ArgumentNullException(nameof(collectionName)); -#endif var dataTable = new DataTable(); if (string.Equals(collectionName, "MetaDataCollections", StringComparison.OrdinalIgnoreCase)) diff --git a/src/SingleStoreConnector/Core/ServerSession.cs b/src/SingleStoreConnector/Core/ServerSession.cs index 79435489a..251d77919 100644 --- a/src/SingleStoreConnector/Core/ServerSession.cs +++ b/src/SingleStoreConnector/Core/ServerSession.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Buffers.Text; using System.ComponentModel; using System.Diagnostics; @@ -83,6 +84,11 @@ public ValueTask ReturnToPoolAsync(IOBehavior ioBehavior, SingleStoreConnection? LastReturnedTimestamp = Stopwatch.GetTimestamp(); if (Pool is null) return default; + + // if the payload cache is too large, discard it rather than keeping it alive in the pool; see https://github.com/mysql-net/MySqlConnector/issues/1587 + if (m_payloadCache.Array?.Length > ProtocolUtility.MaxPacketSize + 1) + m_payloadCache.ArraySegment = default; + MetricsReporter.RecordUseTime(Pool, Utility.GetElapsedSeconds(LastLeasedTimestamp, LastReturnedTimestamp)); LastLeasedTimestamp = 0; return Pool.ReturnAsync(ioBehavior, this); @@ -431,13 +437,13 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella ServerCapabilities = initialHandshake.ProtocolCapabilities & ~(ProtocolCapabilities.MariaDbCacheMetadata | ProtocolCapabilities.QueryAttributes); // if PluginAuth is supported, then use the specified auth plugin; else, fall back to protocol capabilities to determine the auth type to use - m_currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! : + var currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! : (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SecureConnection) == 0 ? "mysql_old_password" : "mysql_native_password"; - Log.ServerSentAuthPluginName(m_logger, Id, m_currentAuthenticationMethod); - if (m_currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password") + Log.ServerSentAuthPluginName(m_logger, Id, currentAuthenticationMethod); + if (currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password") { - Log.UnsupportedAuthenticationMethod(m_logger, Id, m_currentAuthenticationMethod); + Log.UnsupportedAuthenticationMethod(m_logger, Id, currentAuthenticationMethod); throw new NotSupportedException($"Authentication method '{initialHandshake.AuthPluginName}' is not supported."); } @@ -484,7 +490,16 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella cs.ConnectionAttributes = CreateConnectionAttributes(cs.ApplicationName, cs.ConnAttrsExtra); var password = GetPassword(cs, connection); - using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, password, m_useCompression, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null)) + + // send a caching_sha2_password response if the server advertised support in the initial handshake + var useCachingSha2 = initialHandshake.AuthPluginName == "caching_sha2_password"; + byte[] authenticationResponse; + if (useCachingSha2) + authenticationResponse = AuthenticationUtility.CreateScrambleResponse(Utility.TrimZeroByte(initialHandshake.AuthPluginData.AsSpan()), password); + else + AuthenticationUtility.CreateResponseAndPasswordHash(password, initialHandshake.AuthPluginData, out authenticationResponse, out m_passwordHash); + + using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, authenticationResponse, m_useCompression, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null)) await SendReplyAsync(handshakeResponsePayload, ioBehavior, cancellationToken).ConfigureAwait(false); payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); @@ -493,6 +508,27 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella { payload = await SwitchAuthenticationAsync(cs, password, payload, ioBehavior, cancellationToken).ConfigureAwait(false); } + + // check if caching_sha2_password response was sent + if (useCachingSha2 && payload.HeaderByte == CachingSha2ServerResponsePayload.Signature) + { + // process a successful response, or fall back to sending the password if the server doesn't have it + var cachingSha2ServerResponsePayload = CachingSha2ServerResponsePayload.Create(payload.Span); + if (cachingSha2ServerResponsePayload.Succeeded) + { + payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + } + else if (!m_isSecureConnection && password.Length != 0) + { + var publicKey = await GetRsaPublicKeyAsync(currentAuthenticationMethod, cs, ioBehavior, cancellationToken).ConfigureAwait(false); + payload = await SendEncryptedPasswordAsync(AuthPluginData, publicKey, password, ioBehavior, cancellationToken).ConfigureAwait(false); + } + else + { + payload = await SendClearPasswordAsync(password, ioBehavior, cancellationToken).ConfigureAwait(false); + } + } + var ok = OkPayload.Create(payload.Span, this); if (m_sslPolicyErrors != SslPolicyErrors.None) @@ -514,7 +550,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella // there is no shared secret that can be used to validate the certificate Log.CertificateErrorNoPassword(m_logger, Id, m_sslPolicyErrors); } - else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20), password)) + else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20))) { Log.CertificateErrorValidThumbprint(m_logger, Id, m_sslPolicyErrors); ignoreCertificateError = true; @@ -566,36 +602,20 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella /// /// The validation hash received from the server. /// The auth plugin data from the initial handshake. - /// The user's password. /// true if the validation hash matches the locally-computed value; otherwise, false. - private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan challenge, string password) + private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan challenge) { // expect 0x01 followed by 64 hex characters giving a SHA2 hash if (validationHash?.Length != 65 || validationHash[0] != 1) return false; - byte[]? passwordHashResult = null; - switch (m_currentAuthenticationMethod) - { - case "mysql_native_password": - passwordHashResult = AuthenticationUtility.HashPassword([], password, onlyHashPassword: true); - break; - - case "client_ed25519": - AuthenticationPlugins.TryGetPlugin(m_currentAuthenticationMethod, out var ed25519Plugin); - if (ed25519Plugin is IAuthenticationPlugin2 plugin2) - passwordHashResult = plugin2.CreatePasswordHash(password, challenge); - break; - } - if (passwordHashResult is null) + // the authentication plugin must have provided a password hash (via IAuthenticationPlugin3) that we saved for future use + if (m_passwordHash is null) return false; - Span combined = stackalloc byte[32 + challenge.Length + passwordHashResult.Length]; - passwordHashResult.CopyTo(combined); - challenge.CopyTo(combined[passwordHashResult.Length..]); - m_remoteCertificateSha2Thumbprint!.CopyTo(combined[(passwordHashResult.Length + challenge.Length)..]); - + // hash password hash || scramble || certificate thumbprint Span hashBytes = stackalloc byte[32]; + Span combined = [.. m_passwordHash, .. challenge, .. m_remoteCertificateSha2Thumbprint!]; #if NET5_0_OR_GREATER SHA256.TryHashData(combined, hashBytes, out _); #else @@ -608,6 +628,9 @@ private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan chal static bool TryConvertFromHexString(ReadOnlySpan hexChars, Span data) { +#if NET10_0_OR_GREATER + return Convert.FromHexString(hexChars, data, out _, out _) == OperationStatus.Done; +#else ReadOnlySpan hexDigits = "0123456789ABCDEFabcdef"u8; for (var i = 0; i < hexChars.Length; i += 2) { @@ -622,6 +645,7 @@ static bool TryConvertFromHexString(ReadOnlySpan hexChars, Span data data[i / 2] = (byte) ((high << 4) | low); } return true; +#endif } } @@ -743,8 +767,8 @@ public async Task TryResetConnectionAsync(ConnectionSettings cs, SingleSto DatabaseOverride = null; } var password = GetPassword(cs, connection); - var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData!, password); - using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, hashedPassword, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null)) + AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash); + using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, nativeResponse, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null)) await SendAsync(changeUserPayload, ioBehavior, cancellationToken).ConfigureAwait(false); payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); if (payload.HeaderByte == AuthenticationMethodSwitchRequestPayload.Signature) @@ -792,18 +816,17 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs, // if the server didn't support the hashed password; rehash with the new challenge var switchRequest = AuthenticationMethodSwitchRequestPayload.Create(payload.Span); Log.SwitchingToAuthenticationMethod(m_logger, Id, switchRequest.Name); - m_currentAuthenticationMethod = switchRequest.Name; switch (switchRequest.Name) { case "mysql_native_password": AuthPluginData = switchRequest.Data; - var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData, password); - payload = new(hashedPassword); + AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash); + payload = new(nativeResponse); await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); case "mysql_clear_password": - if (!m_isSecureConnection) + if (!m_isSecureConnection && !m_isLoopbackConnection) { Log.NeedsSecureConnection(m_logger, Id, switchRequest.Name); throw new SingleStoreException(SingleStoreErrorCode.UnableToConnectToHost, $"Authentication method '{switchRequest.Name}' requires a secure connection."); @@ -850,9 +873,31 @@ private async Task SwitchAuthenticationAsync(ConnectionSettings cs, throw new NotSupportedException("'MySQL Server is requesting the insecure pre-4.1 auth mechanism (mysql_old_password). The user password must be upgraded; see https://dev.mysql.com/doc/refman/5.7/en/account-upgrades.html."); case "client_ed25519": - if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin)) + if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin) || ed25519Plugin is not IAuthenticationPlugin3 ed25519Plugin3) throw new NotSupportedException("You must install the SingleStoreConnector.Authentication.Ed25519 package and call Ed25519AuthenticationPlugin.Install to use client_ed25519 authentication."); - payload = new(ed25519Plugin.CreateResponse(password, switchRequest.Data)); + ed25519Plugin3.CreateResponseAndPasswordHash(password, switchRequest.Data, out var ed25519Response, out m_passwordHash); + payload = new(ed25519Response); + await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); + return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + + case "parsec": + if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin) || parsecPlugin is not IAuthenticationPlugin3 parsecPlugin3) + throw new NotSupportedException("You must install the SingleStoreConnector.Authentication.Ed25519 package and call ParsecAuthenticationPlugin.Install to use parsec authentication."); + payload = new([]); + await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); + payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + var extendedSalt = payload.Span; + + // MariaDB 11.8.4 sends an extra 0x01 byte at the beginning of ext-salt: https://github.com/mysql-net/MySqlConnector/issues/1606 + if (extendedSalt.Length > 2 && extendedSalt[0] == 1 && extendedSalt[1] == 'P') + extendedSalt = extendedSalt[1..]; + + Span combinedData = stackalloc byte[switchRequest.Data.Length + extendedSalt.Length]; + switchRequest.Data.CopyTo(combinedData); + extendedSalt.CopyTo(combinedData.Slice(switchRequest.Data.Length)); + + parsecPlugin3.CreateResponseAndPasswordHash(password, combinedData, out var parsecResponse, out m_passwordHash); + payload = new(parsecResponse); await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); @@ -875,7 +920,7 @@ private async Task SendClearPasswordAsync(string password, IOBehavi private async Task SendEncryptedPasswordAsync( byte[] switchRequestData, - string rsaPublicKey, + byte[] rsaPublicKey, string password, IOBehavior ioBehavior, CancellationToken cancellationToken) @@ -896,7 +941,7 @@ private async Task SendEncryptedPasswordAsync( RSAParameters rsaParameters; try { - rsaParameters = Utility.GetRsaParameters(rsaPublicKey); + rsaParameters = Utility.GetRsaParameters(Encoding.ASCII.GetString(rsaPublicKey)); } catch (Exception ex) { @@ -923,13 +968,13 @@ private async Task SendEncryptedPasswordAsync( return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); } - private async Task GetRsaPublicKeyAsync(string switchRequestName, ConnectionSettings cs, IOBehavior ioBehavior, CancellationToken cancellationToken) + private async Task GetRsaPublicKeyAsync(string switchRequestName, ConnectionSettings cs, IOBehavior ioBehavior, CancellationToken cancellationToken) { if (cs.ServerRsaPublicKeyFile.Length != 0) { try { - return File.ReadAllText(cs.ServerRsaPublicKeyFile); + return File.ReadAllBytes(cs.ServerRsaPublicKeyFile); } catch (IOException ex) { @@ -938,14 +983,14 @@ private async Task GetRsaPublicKeyAsync(string switchRequestName, Connec } } - if (cs.AllowPublicKeyRetrieval) + if (cs.AllowPublicKeyRetrieval|| m_isLoopbackConnection) { // request the RSA public key var payloadContent = switchRequestName == "caching_sha2_password" ? (byte) 0x02 : (byte) 0x01; await SendReplyAsync(new PayloadData([payloadContent]), ioBehavior, cancellationToken).ConfigureAwait(false); var payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); var publicKeyPayload = AuthenticationMoreDataPayload.Create(payload.Span); - return Encoding.ASCII.GetString(publicKeyPayload.Data); + return publicKeyPayload.Data; } Log.CouldNotUseAuthenticationMethodForRsa(m_logger, Id, switchRequestName); @@ -1243,6 +1288,7 @@ private async Task OpenTcpSocketAsync(ConnectionSettings cs, ILoadBalancer m_socket.NoDelay = true; m_stream = m_tcpClient.GetStream(); m_socket.SetKeepAlive(cs.Keepalive); + m_isLoopbackConnection = IPAddress.IsLoopback(ipAddress); } catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested) { @@ -1671,7 +1717,8 @@ await sslStream.AuthenticateAsClientAsync(clientAuthenticationOptions.TargetHost } catch (Exception ex) { - Log.CouldNotInitializeTlsConnection(m_logger, ex, Id); + if (ex is not OperationCanceledException) + Log.CouldNotInitializeTlsConnection(m_logger, ex, Id); sslStream.Dispose(); ShutdownSocket(); HostName = ""; @@ -1808,6 +1855,13 @@ private bool ShouldGetRealServerDetails(ConnectionSettings cs) HostName.EndsWith(".mysql.database.chinacloudapi.cn", StringComparison.OrdinalIgnoreCase); } + // detect AWS RDS Proxy, if hostname like .proxy-..rds.amazonaws.com + if (HostName.EndsWith(".rds.amazonaws.com", StringComparison.OrdinalIgnoreCase) && + HostName.AsSpan().Contains(".proxy-".AsSpan(), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; } @@ -1940,7 +1994,7 @@ internal void SetFailed(Exception exception) lock (m_lock) m_state = State.Failed; if (OwningConnection is not null && OwningConnection.TryGetTarget(out var connection)) - connection.SetState(ConnectionState.Closed); + connection.SetState(ConnectionState.Broken); } private void VerifyState(State state) @@ -2137,7 +2191,11 @@ protected override void OnStatementBegin(int index) private static readonly PayloadData s_selectConnectionIdVersionWithAttributesPayload = QueryPayload.Create(true, "SELECT CONNECTION_ID(), VERSION(), @@memsql_version, @@aggregator_id;"u8); private readonly ILogger m_logger; +#if NET9_0_OR_GREATER + private readonly Lock m_lock; +#else private readonly object m_lock; +#endif private readonly ArraySegmentHolder m_payloadCache; private readonly ActivityTagsCollection m_activityTags; private State m_state; @@ -2149,12 +2207,13 @@ protected override void OnStatementBegin(int index) private IPayloadHandler? m_payloadHandler; private bool m_useCompression; private bool m_isSecureConnection; + private bool m_isLoopbackConnection; private bool m_supportsConnectionAttributes; private bool m_supportsPipelining; private CharacterSet m_characterSet; private PayloadData m_setNamesPayload; private Dictionary? m_preparedStatements; - private string? m_currentAuthenticationMethod; + private byte[]? m_passwordHash; private byte[]? m_remoteCertificateSha2Thumbprint; private SslPolicyErrors m_sslPolicyErrors; } diff --git a/src/SingleStoreConnector/Core/SingleCommandPayloadCreator.cs b/src/SingleStoreConnector/Core/SingleCommandPayloadCreator.cs index 00463f8bc..a1c17579b 100644 --- a/src/SingleStoreConnector/Core/SingleCommandPayloadCreator.cs +++ b/src/SingleStoreConnector/Core/SingleCommandPayloadCreator.cs @@ -1,3 +1,6 @@ +using System.Buffers; +using System.Buffers.Binary; +using System.Runtime.InteropServices; using SingleStoreConnector.Logging; using SingleStoreConnector.Protocol; using SingleStoreConnector.Protocol.Serialization; @@ -12,6 +15,81 @@ internal sealed class SingleCommandPayloadCreator : ICommandPayloadCreator // with this as the first column name, the result set will be treated as 'out' parameters for the previous command. public static string OutParameterSentinelColumnName => "\uE001\b\x0B"; + public async ValueTask SendCommandPrologueAsync(SingleStoreConnection connection, CommandListPosition commandListPosition, IOBehavior ioBehavior, CancellationToken cancellationToken) + { + // get the current command and check for prepared statements + var command = commandListPosition.CommandAt(commandListPosition.CommandIndex); + var preparedStatements = commandListPosition.PreparedStatements ?? command.TryGetPreparedStatements(); + if (preparedStatements is not null) + { + // get the current prepared statement; WriteQueryCommand will advance this + var preparedStatement = preparedStatements.Statements[commandListPosition.PreparedStatementIndex]; + if (preparedStatement.Parameters is { } parameters) + { + byte[]? buffer = null; + try + { + // check each parameter + for (var i = 0; i < parameters.Length; i++) + { + // look up this parameter in the command's parameter collection and check if it is a Stream + // NOTE: full parameter checks will be performed (and throw any necessary exceptions) in WritePreparedStatement + var parameterName = preparedStatement.Statement.NormalizedParameterNames![i]; + var parameterIndex = parameterName is not null ? (command.RawParameters?.UnsafeIndexOf(parameterName) ?? -1) : preparedStatement.Statement.ParameterIndexes[i]; + if (command.RawParameters is { } rawParameters && parameterIndex >= 0 && parameterIndex < rawParameters.Count && rawParameters[parameterIndex] is { Value: Stream stream and not MemoryStream }) + { + // seven-byte COM_STMT_SEND_LONG_DATA header: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_stmt_send_long_data.html + const int packetHeaderLength = 7; + + // send almost-full packets, but don't send exactly ProtocolUtility.MaxPacketSize bytes in one payload (as that's ambiguous to whether another packet follows). + const int maxDataSize = 16_000_000; + int totalBytesRead; + while (true) + { + buffer ??= ArrayPool.Shared.Rent(packetHeaderLength + maxDataSize); + buffer[0] = (byte) CommandKind.StatementSendLongData; + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(1), preparedStatement.StatementId); + BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(5), (ushort) i); + + // keep reading from the stream until we've filled the buffer to send +#if NET7_0_OR_GREATER + if (ioBehavior == IOBehavior.Synchronous) + totalBytesRead = stream.ReadAtLeast(buffer.AsSpan(packetHeaderLength, maxDataSize), maxDataSize, throwOnEndOfStream: false); + else + totalBytesRead = await stream.ReadAtLeastAsync(buffer.AsMemory(packetHeaderLength, maxDataSize), maxDataSize, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false); +#else + totalBytesRead = 0; + int bytesRead; + do + { + var sizeToRead = maxDataSize - totalBytesRead; + if (ioBehavior == IOBehavior.Synchronous) + bytesRead = stream.Read(buffer, packetHeaderLength + totalBytesRead, sizeToRead); + else + bytesRead = await stream.ReadAsync(buffer, packetHeaderLength + totalBytesRead, sizeToRead, cancellationToken).ConfigureAwait(false); + totalBytesRead += bytesRead; + } while (bytesRead > 0 && totalBytesRead < maxDataSize); +#endif + + if (totalBytesRead == 0) + break; + + // send StatementSendLongData; MySQL Server will keep appending the sent data to the parameter value + using var payload = new PayloadData(buffer.AsMemory(0, packetHeaderLength + totalBytesRead), isPooled: false); + await connection.Session.SendAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); + } + } + } + } + finally + { + if (buffer is not null) + ArrayPool.Shared.Return(buffer); + } + } + } + } + public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDictionary cachedProcedures, ByteBufferWriter writer, bool appendSemicolon) { if (commandListPosition.CommandIndex == commandListPosition.CommandCount) @@ -58,6 +136,7 @@ public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDict { commandListPosition.CommandIndex++; commandListPosition.PreparedStatementIndex = 0; + commandListPosition.PreparedStatements = null; } } return true; @@ -214,8 +293,24 @@ private static bool WriteStoredProcedure(ISingleStoreCommand command, IDictionar break; case ParameterDirection.Output: outParameters.Add(param); - outParameterNames.Add(outName); argParameterNames.Add(outName); + + // special handling for GUIDs to ensure that the result set has a type and length that will be autodetected as a GUID + switch (param.SingleStoreDbType, param.Size) + { + case (SingleStoreDbType.Guid, 16): + outParameterNames.Add($"CAST({outName} AS BINARY(16))"); + break; + case (SingleStoreDbType.Guid, 32): + outParameterNames.Add($"CAST({outName} AS CHAR(32))"); + break; + case (SingleStoreDbType.Guid, 36): + outParameterNames.Add($"CAST({outName} AS CHAR(36))"); + break; + default: + outParameterNames.Add(outName); + break; + } break; case ParameterDirection.ReturnValue: returnParameter = param; From b9968b98a3f4c31ff4591e9500e83f30a9100ada Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Wed, 1 Apr 2026 19:07:49 +0300 Subject: [PATCH 05/21] grab another block of changes --- .../Core/ServerSession.cs | 30 ++++-- src/SingleStoreConnector/Core/SqlParser.cs | 6 +- src/SingleStoreConnector/Core/TypeMapper.cs | 26 ++++- .../Core/XaEnlistedTransaction.cs | 15 ++- .../ExceptionExtensions.cs | 98 +++++++++++++++++++ src/SingleStoreConnector/Logging/EventIds.cs | 2 + src/SingleStoreConnector/Logging/Log.cs | 14 ++- .../SingleStoreAttributeCollection.cs | 17 +--- src/SingleStoreConnector/SingleStoreBatch.cs | 7 +- .../SingleStoreBulkCopy.cs | 64 ++++-------- .../SingleStoreBulkLoader.cs | 4 +- .../SingleStoreCommand.cs | 32 +++--- .../SingleStoreCommandBuilder.cs | 5 - .../SingleStoreConnection.cs | 9 +- 14 files changed, 207 insertions(+), 122 deletions(-) create mode 100644 src/SingleStoreConnector/ExceptionExtensions.cs diff --git a/src/SingleStoreConnector/Core/ServerSession.cs b/src/SingleStoreConnector/Core/ServerSession.cs index 251d77919..c2383f4f7 100644 --- a/src/SingleStoreConnector/Core/ServerSession.cs +++ b/src/SingleStoreConnector/Core/ServerSession.cs @@ -718,22 +718,40 @@ public static async ValueTask ConnectAndRedirectAsync(ILogger con return session; } + private async Task SendAndVerifyOkAsync( + Func sendAsync, + IOBehavior ioBehavior, + CancellationToken cancellationToken) + { + await sendAsync().ConfigureAwait(false); + + var response = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); + OkPayload.Verify(response.Span, this); + } + public async Task ResetConnectionAsync(IOBehavior ioBehavior, string targetDatabase = "", CancellationToken cancellationToken = default) { if (S2ServerVersion.Version.CompareTo(S2Versions.SupportsResetConnection) < 0) throw new InvalidOperationException("Resetting connection is not supported in SingleStore " + S2ServerVersion.OriginalString); Log.ResettingConnection(m_logger, Id); - await SendAsync(ResetConnectionPayload.Instance, ioBehavior, cancellationToken).ConfigureAwait(false); - var payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); - OkPayload.Verify(payload.Span, this); + + await SendAndVerifyOkAsync( + () => SendAsync(ResetConnectionPayload.Instance, ioBehavior, cancellationToken), + ioBehavior, + cancellationToken).ConfigureAwait(false); if (targetDatabase.Length > 0) { var useDb = $"USE {targetDatabase}"; - await SendAsync(QueryPayload.Create(SupportsQueryAttributes, Encoding.ASCII.GetBytes(useDb)), ioBehavior, cancellationToken).ConfigureAwait(false); - payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); - OkPayload.Verify(payload.Span, this); + + await SendAndVerifyOkAsync( + () => SendAsync( + QueryPayload.Create(SupportsQueryAttributes, Encoding.ASCII.GetBytes(useDb)), + ioBehavior, + cancellationToken), + ioBehavior, + cancellationToken).ConfigureAwait(false); } } diff --git a/src/SingleStoreConnector/Core/SqlParser.cs b/src/SingleStoreConnector/Core/SqlParser.cs index ef85cc967..81f34d2ee 100644 --- a/src/SingleStoreConnector/Core/SqlParser.cs +++ b/src/SingleStoreConnector/Core/SqlParser.cs @@ -8,12 +8,8 @@ internal abstract class SqlParser(StatementPreparer preparer) public void Parse(string sql) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(sql); -#else - if (sql is null) - throw new ArgumentNullException(nameof(sql)); -#endif + OnBeforeParse(sql); int parameterStartIndex = -1; diff --git a/src/SingleStoreConnector/Core/TypeMapper.cs b/src/SingleStoreConnector/Core/TypeMapper.cs index 59af72101..d90e3f058 100644 --- a/src/SingleStoreConnector/Core/TypeMapper.cs +++ b/src/SingleStoreConnector/Core/TypeMapper.cs @@ -55,6 +55,10 @@ private TypeMapper() AddColumnTypeMetadata(new("DOUBLE", typeDouble, SingleStoreDbType.Double)); AddColumnTypeMetadata(new("FLOAT", typeFloat, SingleStoreDbType.Float)); + // vector + var typeFloatReadOnlyMemory = AddDbTypeMapping(new(typeof(ReadOnlyMemory), [DbType.Object])); + AddColumnTypeMetadata(new("VECTOR", typeFloatReadOnlyMemory, SingleStoreDbType.Vector, binary: true, simpleDataTypeName: "VECTOR", createFormat: "VECTOR({0})")); + // string var typeFixedString = AddDbTypeMapping(new(typeof(string), [DbType.StringFixedLength, DbType.AnsiStringFixedLength], convert: Convert.ToString!)); var typeString = AddDbTypeMapping(new(typeof(string), [DbType.String, DbType.AnsiString, DbType.Xml], convert: Convert.ToString!)); @@ -110,6 +114,9 @@ private TypeMapper() #endif var typeGuid = AddDbTypeMapping(new(typeof(Guid), [DbType.Guid], convert: convertGuid)); AddColumnTypeMetadata(new("CHAR", typeGuid, SingleStoreDbType.Guid, length: 36, simpleDataTypeName: "CHAR(36)", createFormat: "CHAR(36)")); + AddColumnTypeMetadata(new("CHAR", typeGuid, SingleStoreDbType.Guid, length: 32, guidFormat: SingleStoreGuidFormat.Char32)); + AddColumnTypeMetadata(new("CHAR", typeGuid, SingleStoreDbType.Guid, length: 36, guidFormat: SingleStoreGuidFormat.Char36)); + AddColumnTypeMetadata(new("BINARY", typeGuid, SingleStoreDbType.Guid, binary: true, length: 16, guidFormat: SingleStoreGuidFormat.Binary16)); // null var typeNull = AddDbTypeMapping(new(typeof(object), [DbType.Object])); @@ -176,15 +183,20 @@ private void AddColumnTypeMetadata(ColumnTypeMetadata columnTypeMetadata) public DbTypeMapping? GetDbTypeMapping(string columnTypeName, bool unsigned = false, int length = 0) { - return GetColumnTypeMetadata(columnTypeName, unsigned, length)?.DbTypeMapping; + return GetColumnTypeMetadata(columnTypeName, unsigned, length, SingleStoreGuidFormat.Default)?.DbTypeMapping; } - public SingleStoreDbType GetSingleStoreDbType(string typeName, bool unsigned, int length) => GetColumnTypeMetadata(typeName, unsigned, length)!.SingleStoreDbType; + public SingleStoreDbType GetSingleStoreDbType(string typeName, bool unsigned, int length, SingleStoreGuidFormat guidFormat) => + GetColumnTypeMetadata(typeName, unsigned, length, guidFormat)!.SingleStoreDbType; - private ColumnTypeMetadata? GetColumnTypeMetadata(string columnTypeName, bool unsigned, int length) + private ColumnTypeMetadata? GetColumnTypeMetadata(string columnTypeName, bool unsigned, int length, SingleStoreGuidFormat guidFormat) { - if (!m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, length), out var columnTypeMetadata) && length != 0) - m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, 0), out columnTypeMetadata); + if (m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, length, guidFormat), out var columnTypeMetadata)) + return columnTypeMetadata; + if (guidFormat != SingleStoreGuidFormat.Default && m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, length, SingleStoreGuidFormat.Default), out columnTypeMetadata)) + return columnTypeMetadata; + if (length != 0) + m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, 0, SingleStoreGuidFormat.Default), out columnTypeMetadata); return columnTypeMetadata; } @@ -308,6 +320,9 @@ public static SingleStoreDbType ConvertToSingleStoreDbType(ColumnDefinitionPaylo case ColumnType.Set: return SingleStoreDbType.Set; + case ColumnType.Vector: + return SingleStoreDbType.Vector; + default: throw new NotImplementedException($"ConvertToSingleStoreDbType for {columnDefinition.ColumnType} is not implemented"); } @@ -345,6 +360,7 @@ public static ushort ConvertToColumnTypeAndFlags(SingleStoreDbType dbType, Singl SingleStoreDbType.Geography => ColumnType.Geography, SingleStoreDbType.GeographyPoint => ColumnType.GeographyPoint, SingleStoreDbType.Null => ColumnType.Null, + SingleStoreDbType.Vector => ColumnType.Vector, _ => throw new NotImplementedException($"ConvertToColumnTypeAndFlags for {dbType} is not implemented"), }; return (ushort) ((byte) columnType | (isUnsigned ? 0x8000 : 0)); diff --git a/src/SingleStoreConnector/Core/XaEnlistedTransaction.cs b/src/SingleStoreConnector/Core/XaEnlistedTransaction.cs index 015d51796..1ed68429b 100644 --- a/src/SingleStoreConnector/Core/XaEnlistedTransaction.cs +++ b/src/SingleStoreConnector/Core/XaEnlistedTransaction.cs @@ -30,16 +30,25 @@ protected override void OnCommit(Enlistment enlistment) protected override void OnRollback(Enlistment enlistment) { - try + if (!IsPrepared) { - if (!IsPrepared) + try + { ExecuteXaCommand("END"); + } + catch (SingleStoreException ex) when (ex.ErrorCode is SingleStoreErrorCode.XARBDeadlock || (ex.ErrorCode is SingleStoreErrorCode.XAERRemoveFail && ex.Message.Contains("ROLLBACK ONLY"))) + { + // ignore deadlock notification AND any unprepared end failure when XAERRemoveFail is returned telling us the XA state is ROLLBACK ONLY. + } + } + try + { ExecuteXaCommand("ROLLBACK"); } catch (SingleStoreException ex) when (ex.ErrorCode is SingleStoreErrorCode.XARBDeadlock) { - // ignore deadlock when rolling back + // ignore deadlock notification when rolling back. } } diff --git a/src/SingleStoreConnector/ExceptionExtensions.cs b/src/SingleStoreConnector/ExceptionExtensions.cs new file mode 100644 index 000000000..b6f94cd92 --- /dev/null +++ b/src/SingleStoreConnector/ExceptionExtensions.cs @@ -0,0 +1,98 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace SingleStoreConnector; + +internal static class ExceptionExtensions +{ +#if !NET8_0_OR_GREATER + // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/ArgumentException.cs + extension(ArgumentException) + { + /// Throws an exception if is null or empty. + /// The string argument to validate as non-null and non-empty. + /// The name of the parameter with which corresponds. + /// is null. + /// is empty. + public static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (argument is null) + throw new ArgumentNullException(paramName); + if (argument.Length == 0) + throw new ArgumentException("The string must not be empty.", paramName); + } + } +#endif + +#if !NET6_0_OR_GREATER + // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs + extension(ArgumentNullException) + { + /// Throws an if is null. + /// The reference type argument to validate as non-null. + /// The name of the parameter with which corresponds. + /// From https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs. + public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression("argument")] string? paramName = null) + { + if (argument is null) + throw new ArgumentNullException(paramName); + } + } +#endif + +#if !NET8_0_OR_GREATER + extension (ArgumentOutOfRangeException) + { + /// Throws an if is negative. + /// The argument to validate as non-negative. + /// The name of the parameter with which corresponds. + public static void ThrowIfNegative(int value, [CallerArgumentExpression("value")] string? paramName = null) + { + if (value < 0) + throw new ArgumentOutOfRangeException(paramName, "The value must not be negative."); + } + + /// Throws an if is negative. + /// The argument to validate as non-negative. + /// The name of the parameter with which corresponds. + public static void ThrowIfNegative(long value, [CallerArgumentExpression("value")] string? paramName = null) + { + if (value < 0L) + throw new ArgumentOutOfRangeException(paramName, "The value must not be negative."); + } + + /// Throws an if is greater than . + /// The argument to validate as less or equal than . + /// The value to compare with . + /// The name of the parameter with which corresponds. + public static void ThrowIfGreaterThan(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) + where T : IComparable + { + if (value.CompareTo(other) > 0) + throw new ArgumentOutOfRangeException(paramName, $"The value must be less than or equal to {other}."); + } + + /// Throws an if is greater than or equal . + /// The argument to validate as less than . + /// The value to compare with . + /// The name of the parameter with which corresponds. + public static void ThrowIfGreaterThanOrEqual(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) + where T : IComparable + { + if (value.CompareTo(other) >= 0) + throw new ArgumentOutOfRangeException(paramName, $"The value must be less than {other}."); + } + + /// Throws an if is less than or equal . + /// The argument to validate as greater than than . + /// The value to compare with . + /// The name of the parameter with which corresponds. + public static void ThrowIfLessThanOrEqual(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) + where T : IComparable + { + if (value.CompareTo(other) <= 0) + throw new ArgumentOutOfRangeException(paramName, $"The value must be greater than {other}."); + } + } +#endif +} diff --git a/src/SingleStoreConnector/Logging/EventIds.cs b/src/SingleStoreConnector/Logging/EventIds.cs index 414b8f517..29fb2a86c 100644 --- a/src/SingleStoreConnector/Logging/EventIds.cs +++ b/src/SingleStoreConnector/Logging/EventIds.cs @@ -25,6 +25,8 @@ internal static class EventIds public const int ExpectedToReadMoreBytes = 2010; public const int ExpectedSessionState1 = 2011; public const int ExpectedSessionState6 = 2016; + public const int ResettingConnectionFailed = 2017; + public const int ResetConnection = 2018; // Session connecting events, 2100-2199 public const int ConnectingFailed = 2100; diff --git a/src/SingleStoreConnector/Logging/Log.cs b/src/SingleStoreConnector/Logging/Log.cs index 8fbf8b367..bd0e66124 100644 --- a/src/SingleStoreConnector/Logging/Log.cs +++ b/src/SingleStoreConnector/Logging/Log.cs @@ -28,6 +28,12 @@ internal static partial class Log [LoggerMessage(EventIds.ResettingConnection, LogLevel.Debug, "Session {SessionId} resetting connection")] public static partial void ResettingConnection(ILogger logger, string sessionId); + [LoggerMessage(EventIds.ResettingConnectionFailed, LogLevel.Warning, "Session {SessionId} failed to reset connection: {Message}")] + public static partial void ResettingConnectionFailed(ILogger logger, string sessionId, string message); + + [LoggerMessage(EventIds.ResetConnection, LogLevel.Debug, "Session {SessionId} reset connection")] + public static partial void ResetConnection(ILogger logger, string sessionId); + [LoggerMessage(EventIds.ReturningToPool, LogLevel.Trace, "Session {SessionId} returning to pool {PoolId}")] public static partial void ReturningToPool(ILogger logger, string sessionId, int poolId); @@ -166,10 +172,10 @@ internal static partial class Log #if NETCOREAPP3_0_OR_GREATER [LoggerMessage(EventIds.ConnectedTlsBasic, LogLevel.Debug, "Session {SessionId} connected TLS using {SslProtocol} and {NegotiatedCipherSuite}")] public static partial void ConnectedTlsBasic(ILogger logger, string sessionId, SslProtocols sslProtocol, TlsCipherSuite negotiatedCipherSuite); -#endif - +#else [LoggerMessage(EventIds.ConnectedTlsDetailed, LogLevel.Debug, "Session {SessionId} connected TLS using {SslProtocol}, {CipherAlgorithm}, {HashAlgorithm}, {KeyExchangeAlgorithm}, {KeyExchangeStrength}")] public static partial void ConnectedTlsDetailed(ILogger logger, string sessionId, SslProtocols sslProtocol, CipherAlgorithmType cipherAlgorithm, HashAlgorithmType hashAlgorithm, ExchangeAlgorithmType keyExchangeAlgorithm, int keyExchangeStrength); +#endif [LoggerMessage(EventIds.CouldNotInitializeTlsConnection, LogLevel.Error, "Session {SessionId} couldn't initialize TLS connection")] public static partial void CouldNotInitializeTlsConnection(ILogger logger, Exception exception, string sessionId); @@ -216,10 +222,10 @@ internal static partial class Log #if NETCOREAPP3_0_OR_GREATER [LoggerMessage(EventIds.ConnectedTlsBasicPreliminary, LogLevel.Debug, "Session {SessionId} provisionally connected TLS with error {SslPolicyErrors} using {SslProtocol} and {NegotiatedCipherSuite}")] public static partial void ConnectedTlsBasicPreliminary(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors, SslProtocols sslProtocol, TlsCipherSuite negotiatedCipherSuite); -#endif - +#else [LoggerMessage(EventIds.ConnectedTlsDetailedPreliminary, LogLevel.Debug, "Session {SessionId} provisionally connected TLS with error {SslPolicyErrors} using {SslProtocol}, {CipherAlgorithm}, {HashAlgorithm}, {KeyExchangeAlgorithm}, {KeyExchangeStrength}")] public static partial void ConnectedTlsDetailedPreliminary(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors, SslProtocols sslProtocol, CipherAlgorithmType cipherAlgorithm, HashAlgorithmType hashAlgorithm, ExchangeAlgorithmType keyExchangeAlgorithm, int keyExchangeStrength); +#endif [LoggerMessage(EventIds.CertificateErrorUnixSocket, LogLevel.Trace, "Session {SessionId} ignoring remote certificate error {SslPolicyErrors} due to Unix socket connection")] public static partial void CertificateErrorUnixSocket(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors); diff --git a/src/SingleStoreConnector/SingleStoreAttributeCollection.cs b/src/SingleStoreConnector/SingleStoreAttributeCollection.cs index 332a8916d..dbb8774d8 100644 --- a/src/SingleStoreConnector/SingleStoreAttributeCollection.cs +++ b/src/SingleStoreConnector/SingleStoreAttributeCollection.cs @@ -19,18 +19,9 @@ public sealed class SingleStoreAttributeCollection : IEnumerableThe attribute name must not be empty, and must not already exist in the collection. public void Add(SingleStoreAttribute attribute) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(attribute); -#else - if (attribute is null) - throw new ArgumentNullException(nameof(attribute)); -#endif -#if NET8_0_OR_GREATER ArgumentException.ThrowIfNullOrEmpty(attribute.AttributeName); -#else - if (string.IsNullOrEmpty(attribute.AttributeName)) - throw new ArgumentException("Attribute name must not be empty", nameof(attribute)); -#endif + foreach (var existingAttribute in m_attributes) { if (existingAttribute.AttributeName == attribute.AttributeName) @@ -46,12 +37,8 @@ public void Add(SingleStoreAttribute attribute) /// The attribute value. public void SetAttribute(string attributeName, object? value) { -#if NET8_0_OR_GREATER ArgumentException.ThrowIfNullOrEmpty(attributeName); -#else - if (string.IsNullOrEmpty(attributeName)) - throw new ArgumentException("Attribute name must not be empty", nameof(attributeName)); -#endif + for (var i = 0; i < m_attributes.Count; i++) { if (m_attributes[i].AttributeName == attributeName) diff --git a/src/SingleStoreConnector/SingleStoreBatch.cs b/src/SingleStoreConnector/SingleStoreBatch.cs index 8f0c4cb79..c51add71e 100644 --- a/src/SingleStoreConnector/SingleStoreBatch.cs +++ b/src/SingleStoreConnector/SingleStoreBatch.cs @@ -14,7 +14,7 @@ namespace SingleStoreConnector; /// individually. /// Example usage: /// -/// using var connection = new SingleStoreConnection("...connection string..."); +/// await using var connection = new SingleStoreConnection("...connection string..."); /// await connection.OpenAsync(); /// /// using var batch = new SingleStoreBatch(connection) @@ -199,10 +199,10 @@ public Task ExecuteNonQueryAsync(CancellationToken cancellationToken = defa #endif int Timeout { - get => m_timeout; + get; set { - m_timeout = value; + field = value; ((ICancellableCommand) this).EffectiveCommandTimeout = null; } } @@ -412,7 +412,6 @@ private bool IsPrepared private readonly int m_commandId; private bool m_isDisposed; - private int m_timeout; private Action? m_cancelAction; private Action? m_cancelForCommandTimeoutAction; private uint m_cancelTimerId; diff --git a/src/SingleStoreConnector/SingleStoreBulkCopy.cs b/src/SingleStoreConnector/SingleStoreBulkCopy.cs index 36fe934df..696f3652e 100644 --- a/src/SingleStoreConnector/SingleStoreBulkCopy.cs +++ b/src/SingleStoreConnector/SingleStoreBulkCopy.cs @@ -27,7 +27,7 @@ namespace SingleStoreConnector; /// var dataTable = GetDataTableFromExternalSource(); /// /// // open the connection -/// using var connection = new SingleStoreConnection("...;AllowLoadLocalInfile=True"); +/// await using var connection = new SingleStoreConnection("...;AllowLoadLocalInfile=True"); /// await connection.OpenAsync(); /// /// // bulk copy the data @@ -52,12 +52,7 @@ public sealed class SingleStoreBulkCopy /// (Optional) The to use. public SingleStoreBulkCopy(SingleStoreConnection connection, SingleStoreTransaction? transaction = null) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(connection); -#else - if (connection is null) - throw new ArgumentNullException(nameof(connection)); -#endif m_connection = connection; m_transaction = transaction; m_logger = m_connection.LoggingConfiguration.BulkCopyLogger; @@ -117,12 +112,7 @@ public SingleStoreBulkCopy(SingleStoreConnection connection, SingleStoreTransact /// A with the result of the bulk copy operation. public SingleStoreBulkCopyResult WriteToServer(DataTable dataTable) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(dataTable); -#else - if (dataTable is null) - throw new ArgumentNullException(nameof(dataTable)); -#endif m_valuesEnumerator = DataRowsValuesEnumerator.Create(dataTable); #pragma warning disable CA2012 // Safe because method completes synchronously return WriteToServerAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult(); @@ -138,12 +128,7 @@ public SingleStoreBulkCopyResult WriteToServer(DataTable dataTable) /// A with the result of the bulk copy operation. public async ValueTask WriteToServerAsync(DataTable dataTable, CancellationToken cancellationToken = default) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(dataTable); -#else - if (dataTable is null) - throw new ArgumentNullException(nameof(dataTable)); -#endif m_valuesEnumerator = DataRowsValuesEnumerator.Create(dataTable); return await WriteToServerAsync(IOBehavior.Asynchronous, cancellationToken).ConfigureAwait(false); } @@ -158,12 +143,7 @@ public async ValueTask WriteToServerAsync(DataTable d /// A with the result of the bulk copy operation. public SingleStoreBulkCopyResult WriteToServer(IEnumerable dataRows, int columnCount) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(dataRows); -#else - if (dataRows is null) - throw new ArgumentNullException(nameof(dataRows)); -#endif m_valuesEnumerator = new DataRowsValuesEnumerator(dataRows, columnCount); #pragma warning disable CA2012 // Safe because method completes synchronously return WriteToServerAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult(); @@ -181,12 +161,7 @@ public SingleStoreBulkCopyResult WriteToServer(IEnumerable dataRows, in /// A with the result of the bulk copy operation. public async ValueTask WriteToServerAsync(IEnumerable dataRows, int columnCount, CancellationToken cancellationToken = default) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(dataRows); -#else - if (dataRows is null) - throw new ArgumentNullException(nameof(dataRows)); -#endif m_valuesEnumerator = new DataRowsValuesEnumerator(dataRows, columnCount); return await WriteToServerAsync(IOBehavior.Asynchronous, cancellationToken).ConfigureAwait(false); } @@ -199,12 +174,7 @@ public async ValueTask WriteToServerAsync(IEnumerable /// A with the result of the bulk copy operation. public SingleStoreBulkCopyResult WriteToServer(IDataReader dataReader) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(dataReader); -#else - if (dataReader is null) - throw new ArgumentNullException(nameof(dataReader)); -#endif m_valuesEnumerator = DataReaderValuesEnumerator.Create(dataReader); #pragma warning disable CA2012 // Safe because method completes synchronously return WriteToServerAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult(); @@ -220,12 +190,7 @@ public SingleStoreBulkCopyResult WriteToServer(IDataReader dataReader) /// A with the result of the bulk copy operation. public async ValueTask WriteToServerAsync(IDataReader dataReader, CancellationToken cancellationToken = default) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(dataReader); -#else - if (dataReader is null) - throw new ArgumentNullException(nameof(dataReader)); -#endif m_valuesEnumerator = DataReaderValuesEnumerator.Create(dataReader); return await WriteToServerAsync(IOBehavior.Asynchronous, cancellationToken).ConfigureAwait(false); } @@ -266,27 +231,31 @@ private async ValueTask WriteToServerAsync(IOBehavior using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.SchemaOnly, ioBehavior, cancellationToken).ConfigureAwait(false)) { var schema = reader.GetColumnSchema(); - for (var i = 0; i < Math.Min(m_valuesEnumerator!.FieldCount, schema.Count); i++) + for (var i = 0; i < schema.Count; i++) { var destinationColumn = reader.GetName(i); - if (schema[i].DataTypeName == "BIT") + var dataTypeName = schema[i].DataTypeName; + if (dataTypeName == "BIT") { AddColumnMapping(m_logger, columnMappings, addDefaultMappings, i, destinationColumn, $"@`temporary_column_dotnet_connector_col{i}`", $"%COL% = CAST(%VAR% AS UNSIGNED)"); } - else if (schema[i].DataTypeName == "YEAR") - { - // the current code can't distinguish between 0 = 0000 and 0 = 2000 - throw new NotSupportedException("'YEAR' columns are not supported by SingleStoreBulkLoader."); - } else { var type = schema[i].DataType; - if (type == typeof(byte[]) || (type == typeof(Guid) && (m_connection.GuidFormat is SingleStoreGuidFormat.Binary16 or SingleStoreGuidFormat.LittleEndianBinary16 or SingleStoreGuidFormat.TimeSwapBinary16))) + if (type == typeof(byte[]) || + dataTypeName == "VECTOR" || + (type == typeof(Guid) && (m_connection.GuidFormat is SingleStoreGuidFormat.Binary16 or SingleStoreGuidFormat.LittleEndianBinary16 or SingleStoreGuidFormat.TimeSwapBinary16))) { AddColumnMapping(m_logger, columnMappings, addDefaultMappings, i, destinationColumn, $"@`temporary_column_dotnet_connector_col{i}`", $"%COL% = UNHEX(%VAR%)"); } else if (addDefaultMappings) { + if (schema[i].DataTypeName == "YEAR") + { + // the current code can't distinguish between 0 = 0000 and 0 = 2000 + throw new NotSupportedException("'YEAR' columns are not supported by MySqlBulkCopy."); + } + Log.AddingDefaultColumnMapping(m_logger, i, destinationColumn); columnMappings.Add(new(i, destinationColumn)); } @@ -295,7 +264,7 @@ private async ValueTask WriteToServerAsync(IOBehavior } // set columns and expressions from the column mappings - for (var i = 0; i < m_valuesEnumerator.FieldCount; i++) + for (var i = 0; i < m_valuesEnumerator!.FieldCount; i++) { var columnMapping = columnMappings.FirstOrDefault(x => x.SourceOrdinal == i); if (columnMapping is null) @@ -505,13 +474,16 @@ static bool WriteValue(SingleStoreConnection connection, object value, ref int i { return Utf8Formatter.TryFormat(decimalValue, output, out bytesWritten); } - else if (value is byte[] or ReadOnlyMemory or Memory or ArraySegment) + else if (value is byte[] or ReadOnlyMemory or Memory or ArraySegment or float[] or ReadOnlyMemory or Memory) { var inputSpan = value switch { byte[] byteArray => byteArray.AsSpan(), ArraySegment arraySegment => arraySegment.AsSpan(), Memory memory => memory.Span, + float[] floatArray => SingleStoreParameter.ConvertFloatsToBytes(floatArray.AsSpan()), + Memory memory => SingleStoreParameter.ConvertFloatsToBytes(memory.Span), + ReadOnlyMemory memory => SingleStoreParameter.ConvertFloatsToBytes(memory.Span), _ => ((ReadOnlyMemory) value).Span, }; diff --git a/src/SingleStoreConnector/SingleStoreBulkLoader.cs b/src/SingleStoreConnector/SingleStoreBulkLoader.cs index 5945fe39c..8bd222a53 100644 --- a/src/SingleStoreConnector/SingleStoreBulkLoader.cs +++ b/src/SingleStoreConnector/SingleStoreBulkLoader.cs @@ -9,7 +9,7 @@ namespace SingleStoreConnector; /// lets you efficiently load a SingleStore Server Table with data from a CSV or TSV file or . /// Example code: /// -/// using var connection = new SingleStoreConnection("...;AllowLoadLocalInfile=True"); +/// await using var connection = new SingleStoreConnection("...;AllowLoadLocalInfile=True"); /// await connection.OpenAsync(); /// var bulkLoader = new SingleStoreBulkLoader(connection) /// { @@ -183,7 +183,7 @@ internal async ValueTask LoadAsync(IOBehavior ioBehavior, CancellationToken else { if (!Local) - throw new InvalidOperationException("Local must be true to use SourceStream, SourceDataTable, or SourceDataReader."); + throw new InvalidOperationException("Local must be true to use SourceStream."); FileName = GenerateSourceFileName(); AddSource(FileName, Source!); diff --git a/src/SingleStoreConnector/SingleStoreCommand.cs b/src/SingleStoreConnector/SingleStoreCommand.cs index 9c5e63875..7072237c9 100644 --- a/src/SingleStoreConnector/SingleStoreCommand.cs +++ b/src/SingleStoreConnector/SingleStoreCommand.cs @@ -59,7 +59,7 @@ public SingleStoreCommand(string? commandText, SingleStoreConnection? connection { GC.SuppressFinalize(this); m_commandId = ICancellableCommandExtensions.GetNextId(); - m_commandText = commandText ?? ""; + CommandText = commandText ?? ""; Connection = connection; Transaction = transaction; CommandType = CommandType.Text; @@ -71,7 +71,7 @@ private SingleStoreCommand(SingleStoreCommand other) GC.SuppressFinalize(this); m_commandTimeout = other.m_commandTimeout; ((ICancellableCommand) this).EffectiveCommandTimeout = null; - m_commandType = other.m_commandType; + CommandType = other.CommandType; DesignTimeVisible = other.DesignTimeVisible; UpdatedRowSource = other.UpdatedRowSource; m_parameterCollection = other.CloneRawParameters(); @@ -200,12 +200,12 @@ private bool NeedsPrepare(out Exception? exception) [AllowNull] public override string CommandText { - get => m_commandText; + get; set { - if (m_connection?.ActiveCommandId == m_commandId) + if (Connection?.ActiveCommandId == m_commandId) throw new InvalidOperationException("Cannot set SingleStoreCommand.CommandText when there is an open DataReader for this command; it must be closed first."); - m_commandText = value ?? ""; + field = value ?? ""; } } @@ -215,12 +215,12 @@ public override string CommandText public new SingleStoreConnection? Connection { - get => m_connection; + get; set { - if (m_connection?.ActiveCommandId == m_commandId) + if (field?.ActiveCommandId == m_commandId) throw new InvalidOperationException("Cannot set SingleStoreCommand.Connection when there is an open DataReader for this command; it must be closed first."); - m_connection = value; + field = value; } } @@ -230,12 +230,7 @@ public override int CommandTimeout get => Math.Min(m_commandTimeout ?? Connection?.DefaultCommandTimeout ?? 0, int.MaxValue / 1000); set { -#if NET8_0_OR_GREATER ArgumentOutOfRangeException.ThrowIfNegative(value); -#else - if (value < 0) - throw new ArgumentOutOfRangeException(nameof(value), "CommandTimeout must be greater than or equal to zero."); -#endif m_commandTimeout = value; ((ICancellableCommand) this).EffectiveCommandTimeout = null; } @@ -244,12 +239,12 @@ public override int CommandTimeout /// public override CommandType CommandType { - get => m_commandType; + get; set { if (value is not CommandType.Text and not CommandType.StoredProcedure) throw new ArgumentException("CommandType must be Text or StoredProcedure.", nameof(value)); - m_commandType = value; + field = value; } } @@ -454,8 +449,8 @@ private bool IsValid([NotNullWhen(false)] out Exception? exception) return exception is null; } - PreparedStatements? ISingleStoreCommand.TryGetPreparedStatements() => CommandType == CommandType.Text && !string.IsNullOrWhiteSpace(CommandText) && m_connection is not null && - m_connection.State == ConnectionState.Open ? m_connection.Session.TryGetPreparedStatement(CommandText!) : null; + PreparedStatements? ISingleStoreCommand.TryGetPreparedStatements() => CommandType == CommandType.Text && !string.IsNullOrWhiteSpace(CommandText) && Connection is not null && + Connection.State == ConnectionState.Open ? Connection.Session.TryGetPreparedStatement(CommandText!) : null; CommandBehavior ISingleStoreCommand.CommandBehavior => m_commandBehavior; SingleStoreParameterCollection? ISingleStoreCommand.OutParameters { get; set; } @@ -464,12 +459,9 @@ private bool IsValid([NotNullWhen(false)] out Exception? exception) private readonly int m_commandId; private bool m_isDisposed; - private SingleStoreConnection? m_connection; - private string m_commandText; private SingleStoreParameterCollection? m_parameterCollection; private SingleStoreAttributeCollection? m_attributeCollection; private int? m_commandTimeout; - private CommandType m_commandType; private CommandBehavior m_commandBehavior; private Action? m_cancelAction; private Action? m_cancelForCommandTimeoutAction; diff --git a/src/SingleStoreConnector/SingleStoreCommandBuilder.cs b/src/SingleStoreConnector/SingleStoreCommandBuilder.cs index 6a96df703..1feb9b280 100644 --- a/src/SingleStoreConnector/SingleStoreCommandBuilder.cs +++ b/src/SingleStoreConnector/SingleStoreCommandBuilder.cs @@ -12,12 +12,7 @@ public sealed class SingleStoreCommandBuilder : DbCommandBuilder private static async Task DeriveParametersAsync(IOBehavior ioBehavior, SingleStoreCommand command, CancellationToken cancellationToken) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(command); -#else - if (command is null) - throw new ArgumentNullException(nameof(command)); -#endif if (command.CommandType != CommandType.StoredProcedure) throw new ArgumentException($"SingleStoreCommand.CommandType must be StoredProcedure not {command.CommandType}", nameof(command)); if (string.IsNullOrWhiteSpace(command.CommandText)) diff --git a/src/SingleStoreConnector/SingleStoreConnection.cs b/src/SingleStoreConnector/SingleStoreConnection.cs index 0e11c4d5a..b457d6ce2 100644 --- a/src/SingleStoreConnector/SingleStoreConnection.cs +++ b/src/SingleStoreConnector/SingleStoreConnection.cs @@ -450,12 +450,7 @@ internal void UnenlistTransaction() private void TakeSessionFrom(SingleStoreConnection other) { #if DEBUG -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(other); -#else - if (other is null) - throw new ArgumentNullException(nameof(other)); -#endif + ArgumentNullException.ThrowIfNull(other); if (m_session is not null) throw new InvalidOperationException("This connection must not have a session"); if (other.m_session is null) @@ -537,7 +532,7 @@ private async ValueTask PingAsync(IOBehavior ioBehavior, CancellationToken { } - SetState(ConnectionState.Closed); + SetState(ConnectionState.Broken); return false; } From 4fcc2083af33cfed5304da332b80c5b3baa4fb0c Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Thu, 2 Apr 2026 13:58:09 +0300 Subject: [PATCH 06/21] grab Protocol changes --- .../Core/ServerSession.cs | 2 +- .../Protocol/ColumnType.cs | 3 +- .../Protocol/CommandKind.cs | 1 + .../Payloads/HandshakeResponse41Payload.cs | 7 +- .../Serialization/AuthenticationUtility.cs | 27 ++--- .../Protocol/Serialization/ByteArrayReader.cs | 16 +-- .../Serialization/StandardPayloadHandler.cs | 5 - .../SingleStoreConnection.cs | 16 +-- .../SingleStoreConnectorTracingOptions.cs | 11 ++ ...ngleStoreConnectorTracingOptionsBuilder.cs | 25 +++++ .../SingleStoreDataReader.cs | 6 +- .../SingleStoreDataSource.cs | 6 +- .../SingleStoreDataSourceBuilder.cs | 20 ++++ .../SingleStoreDbColumn.cs | 1 + src/SingleStoreConnector/SingleStoreDbType.cs | 1 + src/SingleStoreConnector/SingleStoreHelper.cs | 5 - .../SingleStoreParameter.cs | 104 +++++++++++++++--- .../SingleStoreParameterCollection.cs | 15 --- 18 files changed, 184 insertions(+), 87 deletions(-) create mode 100644 src/SingleStoreConnector/SingleStoreConnectorTracingOptions.cs create mode 100644 src/SingleStoreConnector/SingleStoreConnectorTracingOptionsBuilder.cs diff --git a/src/SingleStoreConnector/Core/ServerSession.cs b/src/SingleStoreConnector/Core/ServerSession.cs index c2383f4f7..f6df44448 100644 --- a/src/SingleStoreConnector/Core/ServerSession.cs +++ b/src/SingleStoreConnector/Core/ServerSession.cs @@ -719,7 +719,7 @@ public static async ValueTask ConnectAndRedirectAsync(ILogger con } private async Task SendAndVerifyOkAsync( - Func sendAsync, + Func sendAsync, IOBehavior ioBehavior, CancellationToken cancellationToken) { diff --git a/src/SingleStoreConnector/Protocol/ColumnType.cs b/src/SingleStoreConnector/Protocol/ColumnType.cs index 0c033c822..15205f6d8 100644 --- a/src/SingleStoreConnector/Protocol/ColumnType.cs +++ b/src/SingleStoreConnector/Protocol/ColumnType.cs @@ -24,7 +24,8 @@ internal enum ColumnType Bit = 16, Timestamp2 = 17, DateTime2 = 18, - GeographyPoint = 0xF2, + Vector = 242, + GeographyPoint = 0xF3, Geography = 0xF4, Json = 0xF5, NewDecimal = 0xF6, diff --git a/src/SingleStoreConnector/Protocol/CommandKind.cs b/src/SingleStoreConnector/Protocol/CommandKind.cs index 510045d48..1edf7414f 100644 --- a/src/SingleStoreConnector/Protocol/CommandKind.cs +++ b/src/SingleStoreConnector/Protocol/CommandKind.cs @@ -9,5 +9,6 @@ internal enum CommandKind ChangeUser = 17, StatementPrepare = 22, StatementExecute = 23, + StatementSendLongData = 24, ResetConnection = 31, } diff --git a/src/SingleStoreConnector/Protocol/Payloads/HandshakeResponse41Payload.cs b/src/SingleStoreConnector/Protocol/Payloads/HandshakeResponse41Payload.cs index 55507bdf9..4c1cc5f7c 100644 --- a/src/SingleStoreConnector/Protocol/Payloads/HandshakeResponse41Payload.cs +++ b/src/SingleStoreConnector/Protocol/Payloads/HandshakeResponse41Payload.cs @@ -1,5 +1,6 @@ using SingleStoreConnector.Core; using SingleStoreConnector.Protocol.Serialization; +using SingleStoreConnector.Utilities; namespace SingleStoreConnector.Protocol.Payloads; @@ -56,12 +57,12 @@ private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities s public static PayloadData CreateWithSsl(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, bool useCompression, CharacterSet characterSet) => CreateCapabilitiesPayload(serverCapabilities, cs, useCompression, characterSet, ProtocolCapabilities.Ssl).ToPayloadData(); - public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, string password, bool useCompression, CharacterSet characterSet, byte[]? connectionAttributes) + public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, byte[] authenticationResponse, bool useCompression, CharacterSet characterSet, byte[]? connectionAttributes) { // TODO: verify server capabilities var writer = CreateCapabilitiesPayload(handshake.ProtocolCapabilities, cs, useCompression, characterSet); writer.WriteNullTerminatedString(cs.UserID); - var authenticationResponse = AuthenticationUtility.CreateAuthenticationResponse(handshake.AuthPluginData, password); + writer.Write((byte) authenticationResponse.Length); writer.Write(authenticationResponse); @@ -69,7 +70,7 @@ public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSe writer.WriteNullTerminatedString(cs.Database); if ((handshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0) - writer.Write("mysql_native_password\0"u8); + writer.Write(handshake.AuthPluginName == "caching_sha2_password" ? "caching_sha2_password\0"u8 : "mysql_native_password\0"u8); if (connectionAttributes is not null) writer.Write(connectionAttributes); diff --git a/src/SingleStoreConnector/Protocol/Serialization/AuthenticationUtility.cs b/src/SingleStoreConnector/Protocol/Serialization/AuthenticationUtility.cs index fb1dd8e4f..f4b7eb834 100644 --- a/src/SingleStoreConnector/Protocol/Serialization/AuthenticationUtility.cs +++ b/src/SingleStoreConnector/Protocol/Serialization/AuthenticationUtility.cs @@ -24,24 +24,26 @@ public static byte[] GetNullTerminatedPasswordBytes(string password) return passwordBytes; } - public static byte[] CreateAuthenticationResponse(ReadOnlySpan challenge, string password) => - string.IsNullOrEmpty(password) ? [] : HashPassword(challenge, password, onlyHashPassword: false); - /// /// Hashes a password with the "Secure Password Authentication" method. /// - /// The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake). /// The password to hash. - /// If true, is ignored and only the twice-hashed password - /// is returned, instead of performing the full "secure password authentication" algorithm that XORs the hashed password against - /// a hash derived from the challenge. - /// A 20-byte password hash. + /// The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake). + /// The authentication response. + /// The twice-hashed password. /// See Secure Password Authentication. #if NET5_0_OR_GREATER [SkipLocalsInit] #endif - public static byte[] HashPassword(ReadOnlySpan challenge, string password, bool onlyHashPassword) + public static void CreateResponseAndPasswordHash(string password, ReadOnlySpan authenticationData, out byte[] authenticationResponse, out byte[] passwordHash) { + if (string.IsNullOrEmpty(password)) + { + authenticationResponse = []; + passwordHash = []; + return; + } + #if !NET5_0_OR_GREATER using var sha1 = SHA1.Create(); #endif @@ -58,10 +60,9 @@ public static byte[] HashPassword(ReadOnlySpan challenge, string password, sha1.TryComputeHash(passwordBytes, hashedPassword, out _); sha1.TryComputeHash(hashedPassword, combined[20..], out _); #endif - if (onlyHashPassword) - return combined[20..].ToArray(); + passwordHash = combined[20..].ToArray(); - challenge[..20].CopyTo(combined); + authenticationData[..20].CopyTo(combined); Span xorBytes = stackalloc byte[20]; #if NET5_0_OR_GREATER SHA1.TryHashData(combined, xorBytes, out _); @@ -71,7 +72,7 @@ public static byte[] HashPassword(ReadOnlySpan challenge, string password, for (var i = 0; i < hashedPassword.Length; i++) hashedPassword[i] ^= xorBytes[i]; - return hashedPassword.ToArray(); + authenticationResponse = hashedPassword.ToArray(); } public static byte[] CreateScrambleResponse(ReadOnlySpan nonce, string password) => diff --git a/src/SingleStoreConnector/Protocol/Serialization/ByteArrayReader.cs b/src/SingleStoreConnector/Protocol/Serialization/ByteArrayReader.cs index 9db9a3631..89607e8c7 100644 --- a/src/SingleStoreConnector/Protocol/Serialization/ByteArrayReader.cs +++ b/src/SingleStoreConnector/Protocol/Serialization/ByteArrayReader.cs @@ -16,13 +16,9 @@ public int Offset readonly get => m_offset; set { -#if NET8_0_OR_GREATER ArgumentOutOfRangeException.ThrowIfNegative(value); ArgumentOutOfRangeException.ThrowIfGreaterThan(value, m_maxOffset); -#else - if (value < 0 || value > m_maxOffset) - throw new ArgumentOutOfRangeException(nameof(value), $"value must be between 0 and {m_maxOffset:d}"); -#endif + m_offset = value; } } @@ -73,13 +69,8 @@ public uint ReadUInt32() public uint ReadFixedLengthUInt32(int length) { -#if NET8_0_OR_GREATER ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(length, 0); ArgumentOutOfRangeException.ThrowIfGreaterThan(length, 4); -#else - if (length is <= 0 or > 4) - throw new ArgumentOutOfRangeException(nameof(length)); -#endif VerifyRead(length); uint result = 0; for (var i = 0; i < length; i++) @@ -90,13 +81,8 @@ public uint ReadFixedLengthUInt32(int length) public ulong ReadFixedLengthUInt64(int length) { -#if NET8_0_OR_GREATER ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(length, 0); ArgumentOutOfRangeException.ThrowIfGreaterThan(length, 8); -#else - if (length is <= 0 or > 8) - throw new ArgumentOutOfRangeException(nameof(length)); -#endif VerifyRead(length); ulong result = 0; for (var i = 0; i < length; i++) diff --git a/src/SingleStoreConnector/Protocol/Serialization/StandardPayloadHandler.cs b/src/SingleStoreConnector/Protocol/Serialization/StandardPayloadHandler.cs index 790238c2b..ecbceec6c 100644 --- a/src/SingleStoreConnector/Protocol/Serialization/StandardPayloadHandler.cs +++ b/src/SingleStoreConnector/Protocol/Serialization/StandardPayloadHandler.cs @@ -37,12 +37,7 @@ public IByteHandler ByteHandler set { var oldByteHandler = m_byteHandler; -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(value); -#else - if (value is null) - throw new ArgumentNullException(nameof(value)); -#endif m_byteHandler = value; oldByteHandler?.Dispose(); m_bufferedByteReader = new(); diff --git a/src/SingleStoreConnector/SingleStoreConnection.cs b/src/SingleStoreConnector/SingleStoreConnection.cs index b457d6ce2..134a600be 100644 --- a/src/SingleStoreConnector/SingleStoreConnection.cs +++ b/src/SingleStoreConnector/SingleStoreConnection.cs @@ -744,12 +744,7 @@ public static Task ClearAllPoolsAsync(CancellationToken cancellationToken = defa private static async Task ClearPoolAsync(SingleStoreConnection connection, IOBehavior ioBehavior, CancellationToken cancellationToken) { -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(connection); -#else - if (connection is null) - throw new ArgumentNullException(nameof(connection)); -#endif + ArgumentNullException.ThrowIfNull(connection); var pool = ConnectionPool.GetPool(connection.m_connectionString, null, createIfNotFound: false); if (pool is not null) @@ -1097,12 +1092,7 @@ internal void Cancel(ICancellableCommand command, int commandId, bool isCancel) internal void SetActiveReader(SingleStoreDataReader dataReader) { -#if NET6_0_OR_GREATER - ArgumentNullException.ThrowIfNull(dataReader); -#else - if (dataReader is null) - throw new ArgumentNullException(nameof(dataReader)); -#endif + ArgumentNullException.ThrowIfNull(dataReader); if (m_activeReader is not null) throw new InvalidOperationException("Can't replace active reader."); m_activeReader = dataReader; @@ -1214,6 +1204,8 @@ private async ValueTask CreateSessionAsync(ConnectionPool? pool, internal IPEndPoint? SessionEndPoint => m_session!.IPEndPoint; + internal SingleStoreDataSource? MySqlDataSource => m_dataSource; + internal void SetState(ConnectionState newState) { if (m_connectionState != newState) diff --git a/src/SingleStoreConnector/SingleStoreConnectorTracingOptions.cs b/src/SingleStoreConnector/SingleStoreConnectorTracingOptions.cs new file mode 100644 index 000000000..dfa69856d --- /dev/null +++ b/src/SingleStoreConnector/SingleStoreConnectorTracingOptions.cs @@ -0,0 +1,11 @@ +namespace SingleStoreConnector; + +internal sealed class SingleStoreConnectorTracingOptions +{ + public bool EnableResultSetHeaderEvent { get; set; } + + public static SingleStoreConnectorTracingOptions Default { get; } = new() + { + EnableResultSetHeaderEvent = true, + }; +} diff --git a/src/SingleStoreConnector/SingleStoreConnectorTracingOptionsBuilder.cs b/src/SingleStoreConnector/SingleStoreConnectorTracingOptionsBuilder.cs new file mode 100644 index 000000000..750822912 --- /dev/null +++ b/src/SingleStoreConnector/SingleStoreConnectorTracingOptionsBuilder.cs @@ -0,0 +1,25 @@ +namespace SingleStoreConnector; + +/// +/// provides an API for configuring OpenTelemetry tracing options. +/// +public sealed class SingleStoreConnectorTracingOptionsBuilder +{ + /// + /// Gets or sets a value indicating whether to enable the "time-to-first-read" event. + /// Default is true to preserve existing behavior. + /// + public SingleStoreConnectorTracingOptionsBuilder EnableResultSetHeaderEvent(bool enable = true) + { + m_enableResultSetHeaderEvent = enable; + return this; + } + + internal SingleStoreConnectorTracingOptions Build() => + new() + { + EnableResultSetHeaderEvent = m_enableResultSetHeaderEvent, + }; + + private bool m_enableResultSetHeaderEvent = SingleStoreConnectorTracingOptions.Default.EnableResultSetHeaderEvent; +} diff --git a/src/SingleStoreConnector/SingleStoreDataReader.cs b/src/SingleStoreConnector/SingleStoreDataReader.cs index 3a5def519..252262efc 100644 --- a/src/SingleStoreConnector/SingleStoreDataReader.cs +++ b/src/SingleStoreConnector/SingleStoreDataReader.cs @@ -74,8 +74,10 @@ internal async Task NextResultAsync(IOBehavior ioBehavior, CancellationTok Command = m_commandListPosition.CommandAt(m_commandListPosition.CommandIndex); using (Command.CancellableCommand.RegisterCancel(cancellationToken)) { + await m_payloadCreator!.SendCommandPrologueAsync(Command.Connection!, m_commandListPosition, ioBehavior, cancellationToken).ConfigureAwait(false); + var writer = new ByteBufferWriter(); - if (!Command.Connection!.Session.IsCancelingQuery && m_payloadCreator!.WriteQueryCommand(ref m_commandListPosition, m_cachedProcedures!, writer, false)) + if (!Command.Connection!.Session.IsCancelingQuery && m_payloadCreator.WriteQueryCommand(ref m_commandListPosition, m_cachedProcedures!, writer, false)) { using var payload = writer.ToPayloadData(); await Command.Connection.Session.SendAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); @@ -677,7 +679,7 @@ private static async Task ReadOutParametersAsync(ISingleStoreCommand command, Re if (param.HasSetDbType && !row.IsDBNull(columnIndex)) { var dbTypeMapping = TypeMapper.Instance.GetDbTypeMapping(param.DbType); - if (dbTypeMapping is not null) + if (dbTypeMapping is not null && param.DbType is not DbType.Object) { param.Value = dbTypeMapping.DoConversion(row.GetValue(columnIndex)); continue; diff --git a/src/SingleStoreConnector/SingleStoreDataSource.cs b/src/SingleStoreConnector/SingleStoreDataSource.cs index 61287487c..4954f0123 100644 --- a/src/SingleStoreConnector/SingleStoreDataSource.cs +++ b/src/SingleStoreConnector/SingleStoreDataSource.cs @@ -19,12 +19,13 @@ public sealed class SingleStoreDataSource : DbDataSource /// Thrown if is null. public SingleStoreDataSource(string connectionString) : this(connectionString ?? throw new ArgumentNullException(nameof(connectionString)), - SingleStoreConnectorLoggingConfiguration.NullConfiguration, null, null, null, null, default, default, default) + SingleStoreConnectorLoggingConfiguration.NullConfiguration, null, null, null, null, null, default, default, default) { } internal SingleStoreDataSource(string connectionString, SingleStoreConnectorLoggingConfiguration loggingConfiguration, + SingleStoreConnectorTracingOptions? tracingOptions, string? name, Func? clientCertificatesCallback, RemoteCertificateValidationCallback? remoteCertificateValidationCallback, @@ -35,6 +36,7 @@ internal SingleStoreDataSource(string connectionString, { m_connectionString = connectionString; LoggingConfiguration = loggingConfiguration; + TracingOptions = tracingOptions ?? SingleStoreConnectorTracingOptions.Default; Name = name; m_clientCertificatesCallback = clientCertificatesCallback; m_remoteCertificateValidationCallback = remoteCertificateValidationCallback; @@ -208,6 +210,8 @@ private async Task RefreshPassword() internal SingleStoreConnectorLoggingConfiguration LoggingConfiguration { get; } + internal SingleStoreConnectorTracingOptions TracingOptions { get; } + internal string? Name { get; } private string ProvidePasswordFromField(SingleStoreProvidePasswordContext context) => m_password!; diff --git a/src/SingleStoreConnector/SingleStoreDataSourceBuilder.cs b/src/SingleStoreConnector/SingleStoreDataSourceBuilder.cs index e50cefd18..133546068 100644 --- a/src/SingleStoreConnector/SingleStoreDataSourceBuilder.cs +++ b/src/SingleStoreConnector/SingleStoreDataSourceBuilder.cs @@ -20,6 +20,18 @@ public SingleStoreDataSourceBuilder(string? connectionString = null) ConnectionStringBuilder = new(connectionString ?? ""); } + /// + /// Configures OpenTelemetry tracing options. + /// + /// This builder, so that method calls can be chained. + public SingleStoreDataSourceBuilder ConfigureTracing(Action configureAction) + { + ArgumentNullException.ThrowIfNull(configureAction); + m_tracingOptionsBuilderCallbacks ??= []; + m_tracingOptionsBuilderCallbacks.Add(configureAction); + return this; + } + /// /// Sets the that will be used for logging. /// @@ -106,8 +118,15 @@ public SingleStoreDataSourceBuilder UseConnectionOpenedCallback(SingleStoreConne public SingleStoreDataSource Build() { var loggingConfiguration = m_loggerFactory is null ? SingleStoreConnectorLoggingConfiguration.NullConfiguration : new(m_loggerFactory); + + var tracingOptionsBuilder = new SingleStoreConnectorTracingOptionsBuilder(); + foreach (var callback in m_tracingOptionsBuilderCallbacks ?? (IEnumerable>) []) + callback.Invoke(tracingOptionsBuilder); + var tracingOptions = tracingOptionsBuilder.Build(); + return new(ConnectionStringBuilder.ConnectionString, loggingConfiguration, + tracingOptions, m_name, m_clientCertificatesCallback, m_remoteCertificateValidationCallback, @@ -131,4 +150,5 @@ public SingleStoreDataSource Build() private TimeSpan m_periodicPasswordProviderSuccessRefreshInterval; private TimeSpan m_periodicPasswordProviderFailureRefreshInterval; private SingleStoreConnectionOpenedCallback? m_connectionOpenedCallback; + private List>? m_tracingOptionsBuilderCallbacks; } diff --git a/src/SingleStoreConnector/SingleStoreDbColumn.cs b/src/SingleStoreConnector/SingleStoreDbColumn.cs index 6f9b9ccf7..f475de825 100644 --- a/src/SingleStoreConnector/SingleStoreDbColumn.cs +++ b/src/SingleStoreConnector/SingleStoreDbColumn.cs @@ -25,6 +25,7 @@ internal SingleStoreDbColumn(int ordinal, ColumnDefinitionPayload column, bool a } else { + // TODO: VECTOR COLUMNSIZE HANDLING if (mySqlDbType == SingleStoreDbType.JSON || mySqlDbType == SingleStoreDbType.LongBlob) ColumnSize = int.MaxValue; else diff --git a/src/SingleStoreConnector/SingleStoreDbType.cs b/src/SingleStoreConnector/SingleStoreDbType.cs index 0375eba9f..68240f60d 100644 --- a/src/SingleStoreConnector/SingleStoreDbType.cs +++ b/src/SingleStoreConnector/SingleStoreDbType.cs @@ -38,6 +38,7 @@ public enum SingleStoreDbType String, Geography, GeographyPoint, + Vector = 242, UByte = 501, UInt16, UInt32, diff --git a/src/SingleStoreConnector/SingleStoreHelper.cs b/src/SingleStoreConnector/SingleStoreHelper.cs index 3fd6f8871..4d7738f84 100644 --- a/src/SingleStoreConnector/SingleStoreHelper.cs +++ b/src/SingleStoreConnector/SingleStoreHelper.cs @@ -12,12 +12,7 @@ public sealed class SingleStoreHelper /// public static string EscapeString(string value) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(value); -#else - if (value is null) - throw new ArgumentNullException(nameof(value)); -#endif StringBuilder? sb = null; int last = -1; diff --git a/src/SingleStoreConnector/SingleStoreParameter.cs b/src/SingleStoreConnector/SingleStoreParameter.cs index 72825341a..a32f1c483 100644 --- a/src/SingleStoreConnector/SingleStoreParameter.cs +++ b/src/SingleStoreConnector/SingleStoreParameter.cs @@ -1,8 +1,11 @@ +using System.Buffers.Binary; using System.Buffers.Text; +using System.Data.Common; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Numerics; +using System.Runtime.InteropServices; using System.Text; #if NET8_0_OR_GREATER using System.Text.Unicode; @@ -26,7 +29,7 @@ public SingleStoreParameter(string? name, object? value) m_name = name ?? ""; NormalizedParameterName = NormalizeParameterName(m_name); Value = value; - m_sourceColumn = ""; + SourceColumn = ""; SourceVersion = DataRowVersion.Current; } @@ -46,7 +49,7 @@ public SingleStoreParameter(string name, SingleStoreDbType mySqlDbType, int size NormalizedParameterName = NormalizeParameterName(m_name); SingleStoreDbType = mySqlDbType; Size = size; - m_sourceColumn = sourceColumn ?? ""; + SourceColumn = sourceColumn ?? ""; SourceVersion = DataRowVersion.Current; } @@ -72,6 +75,7 @@ public override DbType DbType } } + [DbProviderSpecificTypeProperty(true)] public SingleStoreDbType SingleStoreDbType { get => m_mySqlDbType; @@ -119,8 +123,8 @@ public override string ParameterName [AllowNull] public override string SourceColumn { - get => m_sourceColumn; - set => m_sourceColumn = value ?? ""; + get; + set => field = value ?? ""; } public override bool SourceColumnNullMapping { get; set; } @@ -171,7 +175,7 @@ private SingleStoreParameter(SingleStoreParameter other) m_value = other.m_value; Precision = other.Precision; Scale = other.Scale; - m_sourceColumn = other.m_sourceColumn; + SourceColumn = other.SourceColumn; SourceColumnNullMapping = other.SourceColumnNullMapping; SourceVersion = other.SourceVersion; } @@ -179,12 +183,7 @@ private SingleStoreParameter(SingleStoreParameter other) private SingleStoreParameter(SingleStoreParameter other, string parameterName) : this(other) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(parameterName); -#else - if (parameterName is null) - throw new ArgumentNullException(nameof(parameterName)); -#endif ParameterName = parameterName; } @@ -289,7 +288,7 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions { writer.WriteString(ulongValue); } - else if (Value is byte[] or ReadOnlyMemory or Memory or ArraySegment or MemoryStream) + else if (Value is byte[] or ReadOnlyMemory or Memory or ArraySegment or MemoryStream or float[] or ReadOnlyMemory or Memory) { var inputSpan = Value switch { @@ -297,6 +296,9 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions ArraySegment arraySegment => arraySegment.AsSpan(), Memory memory => memory.Span, MemoryStream memoryStream => memoryStream.TryGetBuffer(out var streamBuffer) ? streamBuffer.AsSpan() : memoryStream.ToArray().AsSpan(), + float[] floatArray => ConvertFloatsToBytes(floatArray.AsSpan()), + Memory memory => ConvertFloatsToBytes(memory.Span), + ReadOnlyMemory memory => ConvertFloatsToBytes(memory.Span), _ => ((ReadOnlyMemory) Value).Span, }; @@ -340,6 +342,10 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions writer.Write('\"' + geographyValue + '\"'); } + else if (Value is Stream) + { + throw new NotSupportedException($"Parameter type {Value.GetType().Name} can only be used after calling SingleStoreCommand.Prepare."); + } else if (Value is bool boolValue) { writer.Write(boolValue ? "true"u8 : "false"u8); @@ -743,13 +749,58 @@ private void AppendBinary(ByteBufferWriter writer, object value, StatementPrepar writer.WriteLengthEncodedInteger(unchecked((ulong) streamBuffer.Count)); writer.Write(streamBuffer); } + else if (value is Stream) + { + // do nothing; this will be sent via CommandKind.StatementSendLongData + } else if (value is float floatValue) { - writer.Write(BitConverter.GetBytes(floatValue)); +#if NET5_0_OR_GREATER + Span bytes = stackalloc byte[4]; + BinaryPrimitives.WriteSingleLittleEndian(bytes, floatValue); + writer.Write(bytes); +#else + // convert float to bytes with correct endianness (MySQL uses little-endian) + var bytes = BitConverter.GetBytes(floatValue); + if (!BitConverter.IsLittleEndian) + Array.Reverse(bytes); + writer.Write(bytes); +#endif } else if (value is double doubleValue) { - writer.Write(unchecked((ulong) BitConverter.DoubleToInt64Bits(doubleValue))); +#if NET5_0_OR_GREATER + Span bytes = stackalloc byte[8]; + BinaryPrimitives.WriteDoubleLittleEndian(bytes, doubleValue); + writer.Write(bytes); +#else + if (BitConverter.IsLittleEndian) + { + writer.Write(unchecked((ulong) BitConverter.DoubleToInt64Bits(doubleValue))); + } + else + { + // convert double to bytes with correct endianness (MySQL uses little-endian) + var bytes = BitConverter.GetBytes(doubleValue); + Array.Reverse(bytes); + writer.Write(bytes); + } +#endif + } + else if (value is float[] floatArrayValue) + { + writer.WriteLengthEncodedInteger(unchecked((ulong) floatArrayValue.Length * 4)); + writer.Write(ConvertFloatsToBytes(floatArrayValue.AsSpan())); + } + else if (value is Memory floatMemory) + { + writer.WriteLengthEncodedInteger(unchecked((ulong) floatMemory.Length * 4)); + writer.Write(ConvertFloatsToBytes(floatMemory.Span)); + } + else if (value is ReadOnlyMemory floatReadOnlyMemory) + { + writer.WriteLengthEncodedInteger(unchecked((ulong) floatReadOnlyMemory.Length * 4)); + writer.Write(ConvertFloatsToBytes(floatReadOnlyMemory.Span)); } else if (value is decimal decimalValue) { @@ -968,12 +1019,37 @@ private static void WriteTime(ByteBufferWriter writer, TimeSpan timeSpan) } } + internal static ReadOnlySpan ConvertFloatsToBytes(ReadOnlySpan floats) + { + if (BitConverter.IsLittleEndian) + { + return MemoryMarshal.AsBytes(floats); + } + else + { + // for big-endian platforms, we need to convert each float individually + var bytes = new byte[floats.Length * 4]; + + for (var i = 0; i < floats.Length; i++) + { +#if NET5_0_OR_GREATER + BinaryPrimitives.WriteSingleLittleEndian(bytes.AsSpan(i * 4), floats[i]); +#else + var floatBytes = BitConverter.GetBytes(floats[i]); + Array.Reverse(floatBytes); + floatBytes.CopyTo(bytes, i * 4); +#endif + } + + return bytes; + } + } + private static ReadOnlySpan BinaryBytes => "_binary'"u8; private DbType m_dbType; private SingleStoreDbType m_mySqlDbType; private string m_name; private ParameterDirection? m_direction; - private string m_sourceColumn; private object? m_value; } diff --git a/src/SingleStoreConnector/SingleStoreParameterCollection.cs b/src/SingleStoreConnector/SingleStoreParameterCollection.cs index e5f7eb78a..9bbded24e 100644 --- a/src/SingleStoreConnector/SingleStoreParameterCollection.cs +++ b/src/SingleStoreConnector/SingleStoreParameterCollection.cs @@ -25,24 +25,14 @@ public SingleStoreParameter Add(string parameterName, DbType dbType) public override int Add(object value) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(value); -#else - if (value is null) - throw new ArgumentNullException(nameof(value)); -#endif AddParameter((SingleStoreParameter) value, m_parameters.Count); return m_parameters.Count - 1; } public SingleStoreParameter Add(SingleStoreParameter parameter) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(parameter); -#else - if (parameter is null) - throw new ArgumentNullException(nameof(parameter)); -#endif AddParameter(parameter, m_parameters.Count); return parameter; } @@ -135,12 +125,7 @@ public override void RemoveAt(int index) protected override void SetParameter(int index, DbParameter value) { -#if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(value); -#else - if (value is null) - throw new ArgumentNullException(nameof(value)); -#endif var newParameter = (SingleStoreParameter) value; var oldParameter = m_parameters[index]; if (oldParameter.NormalizedParameterName is not null) From af6dd579b9c9208a74dfe186466a08fd8c072d05 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Thu, 2 Apr 2026 14:08:18 +0300 Subject: [PATCH 07/21] grab the last block of changes for the connector --- src/SingleStoreConnector/Utilities/Adler32.cs | 2 - .../Utilities/CallerAttributes.cs | 10 ++++ .../Utilities/TimerQueue.cs | 5 -- src/SingleStoreConnector/Utilities/Utility.cs | 53 ++++++++++++++++++- src/SingleStoreConnector/docs/README.md | 6 +-- 5 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 src/SingleStoreConnector/Utilities/CallerAttributes.cs diff --git a/src/SingleStoreConnector/Utilities/Adler32.cs b/src/SingleStoreConnector/Utilities/Adler32.cs index dcba6cc29..747e6c3fa 100644 --- a/src/SingleStoreConnector/Utilities/Adler32.cs +++ b/src/SingleStoreConnector/Utilities/Adler32.cs @@ -9,8 +9,6 @@ using System.Runtime.Intrinsics.X86; #endif -#pragma warning disable IDE0007 // Use implicit type - namespace SingleStoreConnector.Utilities; /// diff --git a/src/SingleStoreConnector/Utilities/CallerAttributes.cs b/src/SingleStoreConnector/Utilities/CallerAttributes.cs new file mode 100644 index 000000000..c550fb87b --- /dev/null +++ b/src/SingleStoreConnector/Utilities/CallerAttributes.cs @@ -0,0 +1,10 @@ +#if !NET6_0_OR_GREATER +namespace System.Runtime.CompilerServices; + +// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CallerArgumentExpressionAttribute.cs +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class CallerArgumentExpressionAttribute(string parameterName) : Attribute +{ + public string ParameterName { get; } = parameterName; +} +#endif diff --git a/src/SingleStoreConnector/Utilities/TimerQueue.cs b/src/SingleStoreConnector/Utilities/TimerQueue.cs index 887a0e9d8..8d0f7f290 100644 --- a/src/SingleStoreConnector/Utilities/TimerQueue.cs +++ b/src/SingleStoreConnector/Utilities/TimerQueue.cs @@ -14,12 +14,7 @@ internal sealed class TimerQueue /// A timer ID that can be passed to to cancel the timer. public uint Add(int delay, Action action) { -#if NET8_0_OR_GREATER ArgumentOutOfRangeException.ThrowIfNegative(delay); -#else - if (delay < 0) - throw new ArgumentOutOfRangeException(nameof(delay), $"delay must not be negative: {delay}"); -#endif var current = Environment.TickCount; lock (m_lock) diff --git a/src/SingleStoreConnector/Utilities/Utility.cs b/src/SingleStoreConnector/Utilities/Utility.cs index abb2ef90e..e7f16555b 100644 --- a/src/SingleStoreConnector/Utilities/Utility.cs +++ b/src/SingleStoreConnector/Utilities/Utility.cs @@ -88,6 +88,44 @@ public static unsafe int GetByteCount(this Encoder encoder, ReadOnlySpan c } #endif +#if NET5_0_OR_GREATER + /// + /// Loads a RSA key from PEM bytes. + /// + public static void LoadRsaParameters(byte[] key, RSA rsa) + { +#if NET10_0_OR_GREATER + if (!PemEncoding.TryFindUtf8(key, out var pemFields)) + throw new FormatException("Unrecognized PEM data: " + Encoding.ASCII.GetString(key.AsSpan(0, Math.Min(key.Length, 80)))); + var isPrivate = key.AsSpan()[pemFields.Label].SequenceEqual("RSA PRIVATE KEY"u8); + + var keyBytes = key.AsSpan()[pemFields.Base64Data]; + var bufferLength = keyBytes.Length / 4 * 3; + byte[]? buffer = null; + Span bufferBytes = bufferLength > 1024 ? + (Span) (buffer = ArrayPool.Shared.Rent(bufferLength)) : + stackalloc byte[bufferLength]; + try + { + if (Base64.DecodeFromUtf8(keyBytes, bufferBytes, out _, out var bytesWritten) != OperationStatus.Done) + throw new FormatException("The input is not a valid Base-64 string."); + if (isPrivate) + rsa.ImportRSAPrivateKey(bufferBytes[..bytesWritten], out var _); + else + rsa.ImportSubjectPublicKeyInfo(bufferBytes[..bytesWritten], out var _); + } + finally + { + if (buffer is not null) + ArrayPool.Shared.Return(buffer); + } +#else + LoadRsaParameters(Encoding.ASCII.GetString(key), rsa); +#endif + } +#endif + +#if !NET10_0_OR_GREATER /// /// Loads a RSA key from a PEM string. /// @@ -97,6 +135,11 @@ public static void LoadRsaParameters(string key, RSA rsa) public static RSAParameters GetRsaParameters(string key) #endif { +#if NET5_0_OR_GREATER + if (!PemEncoding.TryFind(key, out var pemFields)) + throw new FormatException(string.Concat("Unrecognized PEM data: ", key.AsSpan(0, Math.Min(key.Length, 80)))); + var isPrivate = key.AsSpan()[pemFields.Label].SequenceEqual("RSA PRIVATE KEY"); +#else const string beginRsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----"; const string endRsaPrivateKey = "-----END RSA PRIVATE KEY-----"; const string beginPublicKey = "-----BEGIN PUBLIC KEY-----"; @@ -135,9 +178,14 @@ public static RSAParameters GetRsaParameters(string key) #else throw new FormatException($"Missing expected '{pemFooter}' PEM footer: " + key[Math.Max(key.Length - 80, 0)..]); #endif +#endif #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER +#if NET5_0_OR_GREATER + var keyChars = key.AsSpan()[pemFields.Base64Data]; +#else var keyChars = key.AsSpan()[keyStartIndex..keyEndIndex]; +#endif var bufferLength = keyChars.Length / 4 * 3; byte[]? buffer = null; Span bufferBytes = bufferLength > 1024 ? @@ -166,6 +214,7 @@ public static RSAParameters GetRsaParameters(string key) return GetRsaParameters(System.Convert.FromBase64String(key), isPrivate); #endif } +#endif #if !NET5_0_OR_GREATER // Derived from: https://stackoverflow.com/a/32243171/, https://stackoverflow.com/a/26978561/, http://luca.ntop.org/Teaching/Appunti/asn1.html @@ -351,7 +400,7 @@ public static bool TryParseRedirectionHeader(string redirectUrl, string initialU var uri = new Uri(redirectUrl); host = uri.Host; if (string.IsNullOrEmpty(host)) return false; - if (host.StartsWith('[') && host.EndsWith("]", StringComparison.InvariantCulture)) host = host.Substring(1, host.Length - 2); + if (host.StartsWith('[') && host.EndsWith("]", StringComparison.Ordinal)) host = host.Substring(1, host.Length - 2); port = uri.Port; user = Uri.UnescapeDataString(uri.UserInfo.Split(':')[0]); @@ -361,7 +410,7 @@ public static bool TryParseRedirectionHeader(string redirectUrl, string initialU var q = uri.Query.Substring(1); foreach (var token in q.Split('&')) { - if (token.StartsWith("user=", StringComparison.InvariantCulture)) + if (token.StartsWith("user=", StringComparison.Ordinal)) { user = Uri.UnescapeDataString(token.Substring(5)); } diff --git a/src/SingleStoreConnector/docs/README.md b/src/SingleStoreConnector/docs/README.md index 17fcd8738..f978cfcbd 100644 --- a/src/SingleStoreConnector/docs/README.md +++ b/src/SingleStoreConnector/docs/README.md @@ -15,16 +15,16 @@ var builder = new SingleStoreConnectionStringBuilder }; // open a connection asynchronously -using var connection = new SingleStoreConnection(builder.ConnectionString); +await using var connection = new SingleStoreConnection(builder.ConnectionString); await connection.OpenAsync(); // create a DB command and set the SQL statement with parameters -using var command = connection.CreateCommand(); +await using var command = connection.CreateCommand(); command.CommandText = @"SELECT * FROM orders WHERE order_id = @OrderId;"; command.Parameters.AddWithValue("@OrderId", orderId); // execute the command and read the results -using var reader = await command.ExecuteReaderAsync(); +await using var reader = await command.ExecuteReaderAsync(); while (reader.Read()) { var id = reader.GetInt32("order_id"); From c75f384941cfda3584bb7feba0b93281aaf009e3 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Thu, 2 Apr 2026 15:15:33 +0300 Subject: [PATCH 08/21] grab first block of changes for the tests --- .../ConnectionStringBuilderTests.cs | 1 - tests/Conformance.Tests/DataReaderTests.cs | 4 +- .../GetValueConversionTests.cs | 2 - tests/Conformance.Tests/TransactionTests.cs | 28 ++- tests/SideBySide/ActivityTests.cs | 42 ++++ tests/SideBySide/BatchTests.cs | 5 +- tests/SideBySide/BulkLoaderAsync.cs | 48 ++++- tests/SideBySide/BulkLoaderSync.cs | 185 +++++++++++++++++- tests/SideBySide/CancelTests.cs | 34 ++-- tests/SideBySide/CharacterSetTests.cs | 2 +- tests/SideBySide/ChunkStream.cs | 130 ++++++++++++ tests/SideBySide/ClientFactoryTests.cs | 1 - tests/SideBySide/CommandBuilderTests.cs | 8 +- tests/SideBySide/CommandTests.cs | 6 +- tests/SideBySide/CommandTimeoutTests.cs | 11 +- tests/SideBySide/ConnectAsync.cs | 31 ++- tests/SideBySide/ConnectSync.cs | 10 +- tests/SideBySide/DataAdapterTests.cs | 5 +- tests/SideBySide/DataTypes.cs | 104 +++++++--- 19 files changed, 564 insertions(+), 93 deletions(-) create mode 100644 tests/SideBySide/ChunkStream.cs diff --git a/tests/Conformance.Tests/ConnectionStringBuilderTests.cs b/tests/Conformance.Tests/ConnectionStringBuilderTests.cs index 51b026177..5aaf6ef2a 100644 --- a/tests/Conformance.Tests/ConnectionStringBuilderTests.cs +++ b/tests/Conformance.Tests/ConnectionStringBuilderTests.cs @@ -9,4 +9,3 @@ public ConnectionStringBuilderTests(DbFactoryFixture fixture) { } } - diff --git a/tests/Conformance.Tests/DataReaderTests.cs b/tests/Conformance.Tests/DataReaderTests.cs index 41bcbdad3..4618d5f35 100644 --- a/tests/Conformance.Tests/DataReaderTests.cs +++ b/tests/Conformance.Tests/DataReaderTests.cs @@ -11,5 +11,7 @@ public DataReaderTests(SelectValueFixture fixture) } [Fact(Skip = "Deliberately throws InvalidCastException")] - public override void GetTextReader_returns_empty_for_null_String() { } + public override void GetTextReader_returns_empty_for_null_String() + { + } } diff --git a/tests/Conformance.Tests/GetValueConversionTests.cs b/tests/Conformance.Tests/GetValueConversionTests.cs index 71bed9f96..0b68c9106 100644 --- a/tests/Conformance.Tests/GetValueConversionTests.cs +++ b/tests/Conformance.Tests/GetValueConversionTests.cs @@ -21,7 +21,6 @@ public GetValueConversionTests(SelectValueFixture fixture) public override void GetValue_for_Boolean() => TestGetValue(DbType.Boolean, ValueKind.One, (sbyte)1); public override void GetFieldType_for_Boolean() => TestGetFieldType(DbType.Boolean, ValueKind.One, typeof(sbyte)); - // GetBoolean allows conversions from any integral type and decimal for backwards compatibility public override void GetBoolean_throws_for_maximum_Byte() => TestGetValue(DbType.Byte, ValueKind.Maximum, x => x.GetBoolean(0), true); public override void GetBoolean_throws_for_maximum_Byte_with_GetFieldValue() => TestGetValue(DbType.Byte, ValueKind.Maximum, x => x.GetFieldValue(0), true); @@ -280,7 +279,6 @@ public GetValueConversionTests(SelectValueFixture fixture) public override void GetByte_throws_for_zero_Decimal_with_GetFieldValue() => TestGetValue(DbType.Decimal, ValueKind.Zero, x => x.GetFieldValue(0), (byte) 0); public override Task GetByte_throws_for_zero_Decimal_with_GetFieldValueAsync() => TestGetValueAsync(DbType.Decimal, ValueKind.Zero, x => x.GetFieldValueAsync(0), (byte) 0); - // the minimum date permitted by MySQL is 1000-01-01; override the minimum value for DateTime tests public override void GetDateTime_for_minimum_Date() => TestGetValue(DbType.Date, ValueKind.Minimum, x => x.GetDateTime(0), new DateTime(1000, 1, 1)); public override void GetDateTime_for_minimum_Date_with_GetFieldValue() => TestGetValue(DbType.Date, ValueKind.Minimum, x => x.GetFieldValue(0), new DateTime(1000, 1, 1)); diff --git a/tests/Conformance.Tests/TransactionTests.cs b/tests/Conformance.Tests/TransactionTests.cs index 4a2617b57..0382bfd62 100644 --- a/tests/Conformance.Tests/TransactionTests.cs +++ b/tests/Conformance.Tests/TransactionTests.cs @@ -11,23 +11,37 @@ public TransactionTests(DbFactoryFixture fixture) } [Fact(Skip = "Deliberately throws System.NotSupportedException : IsolationLevel.Serializable is not supported.")] - public override void BeginTransaction_works() {} + public override void BeginTransaction_works() + { + } [Fact(Skip = "Deliberately throws System.NotSupportedException : IsolationLevel.Serializable is not supported.")] - public override void Commit_transaction_clears_Connection() {} + public override void Commit_transaction_clears_Connection() + { + } [Fact(Skip = "Deliberately throws System.NotSupportedException : IsolationLevel.Serializable is not supported.")] - public override void Commit_transaction_then_Rollback_throws() {} + public override void Commit_transaction_then_Rollback_throws() + { + } [Fact(Skip = "Deliberately throws System.NotSupportedException : IsolationLevel.Serializable is not supported.")] - public override void Commit_transaction_twice_throws() {} + public override void Commit_transaction_twice_throws() + { + } [Fact(Skip = "Deliberately throws System.NotSupportedException : IsolationLevel.Serializable is not supported.")] - public override void Rollback_transaction_clears_Connection() {} + public override void Rollback_transaction_clears_Connection() + { + } [Fact(Skip = "Deliberately throws System.NotSupportedException : IsolationLevel.Serializable is not supported.")] - public override void Rollback_transaction_then_Commit_throws() {} + public override void Rollback_transaction_then_Commit_throws() + { + } [Fact(Skip = "Deliberately throws System.NotSupportedException : IsolationLevel.Serializable is not supported.")] - public override void Rollback_transaction_twice_throws() {} + public override void Rollback_transaction_twice_throws() + { + } } diff --git a/tests/SideBySide/ActivityTests.cs b/tests/SideBySide/ActivityTests.cs index 3d3557c20..ccbd78beb 100644 --- a/tests/SideBySide/ActivityTests.cs +++ b/tests/SideBySide/ActivityTests.cs @@ -140,6 +140,48 @@ public void SelectTags() AssertTag(activity.Tags, "db.statement", "SELECT 1;"); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ReadResultSetHeaderEvent(bool enableEvent) + { + var dataSourceBuilder = new MySqlDataSourceBuilder(AppConfig.ConnectionString) + .ConfigureTracing(o => o.EnableResultSetHeaderEvent(enableEvent)); + using var dataSource = dataSourceBuilder.Build(); + using var connection = dataSource.OpenConnection(); + + using var parentActivity = new Activity(nameof(ReadResultSetHeaderEvent)); + parentActivity.Start(); + + Activity activity = null; + using var listener = new ActivityListener + { + ShouldListenTo = x => x.Name == "MySqlConnector", + Sample = (ref ActivityCreationOptions options) => + options.TraceId == parentActivity.TraceId ? ActivitySamplingResult.AllData : ActivitySamplingResult.None, + ActivityStopped = x => activity = x, + }; + ActivitySource.AddActivityListener(listener); + + using (var command = new MySqlCommand("SELECT 1;", connection)) + { + command.ExecuteScalar(); + } + + Assert.NotNull(activity); + Assert.Equal(ActivityKind.Client, activity.Kind); + Assert.Equal("Execute", activity.OperationName); + if (enableEvent) + { + var activityEvent = Assert.Single(activity.Events); + Assert.Equal("read-result-set-header", activityEvent.Name); + } + else + { + Assert.Empty(activity.Events); + } + } + private void AssertTags(IEnumerable> tags, SingleStoreConnectionStringBuilder csb) { AssertTag(tags, "db.system", "mysql"); diff --git a/tests/SideBySide/BatchTests.cs b/tests/SideBySide/BatchTests.cs index 557a73824..7adcdfc87 100644 --- a/tests/SideBySide/BatchTests.cs +++ b/tests/SideBySide/BatchTests.cs @@ -182,8 +182,9 @@ public void IgnoreCommandTransactionIgnoresDifferentTransaction() [InlineData(";")] [InlineData(";\n")] [InlineData("; -- ")] - // [InlineData(" -- ")] TODO: uncomment if DB-53659 is done [InlineData(" # ")] + + // [InlineData(" -- ")] TODO: uncomment if DB-53659 is done public void ExecuteBatch(string suffix) { using var connection = new SingleStoreConnection(AppConfig.ConnectionString); @@ -343,7 +344,7 @@ public void PrepareNeedsCommandsWithText() private static string GetIgnoreCommandTransactionConnectionString() => new SingleStoreConnectionStringBuilder(AppConfig.ConnectionString) { - IgnoreCommandTransaction = true + IgnoreCommandTransaction = true, }.ConnectionString; } #endif diff --git a/tests/SideBySide/BulkLoaderAsync.cs b/tests/SideBySide/BulkLoaderAsync.cs index 007face24..d6d3097aa 100644 --- a/tests/SideBySide/BulkLoaderAsync.cs +++ b/tests/SideBySide/BulkLoaderAsync.cs @@ -161,7 +161,7 @@ public async Task BulkLoadLocalCsvFileNotFound() await connection.OpenAsync(); SingleStoreBulkLoader bl = new SingleStoreBulkLoader(connection) { - Timeout = 3, //Set a short timeout for this test because the file not found exception takes a long time otherwise, the timeout does not change the result + Timeout = 3, // Set a short timeout for this test because the file not found exception takes a long time otherwise, the timeout does not change the result FileName = AppConfig.SingleStoreBulkLoaderLocalCsvFile + "-junk", TableName = m_testTable, CharacterSet = "UTF8", @@ -198,7 +198,7 @@ public async Task BulkLoadLocalCsvFileNotFound() } catch (Exception exception) { - //We know that the exception is not a SingleStoreException, just use the assertion to fail the test + // We know that the exception is not a SingleStoreException, just use the assertion to fail the test Assert.IsType(exception); } } @@ -649,11 +649,51 @@ public async Task BulkCopyNullDataReader() var bulkCopy = new SingleStoreBulkCopy(connection); await Assert.ThrowsAsync(async () => await bulkCopy.WriteToServerAsync(default(DbDataReader))); } + + // TODO: reimplement for SingleStore Geography types + /*[Fact] + public async Task BulkCopyGeometryAsync() + { + var dataTable = new DataTable() + { + Columns = + { + new DataColumn("geo_data", typeof(SingleStoreGeometry)), + }, + Rows = + { + new object[] { SingleStoreGeometry.FromWkb(0, [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 63, 0, 0, 0, 0, 0, 0, 240, 63]) }, + }, + }; + + using var connection = new SingleStoreConnection(GetLocalConnectionString()); + await connection.OpenAsync(); + using (var cmd = new SingleStoreCommand(@"drop table if exists bulk_load_data_table; +create table bulk_load_data_table(id BIGINT UNIQUE NOT NULL AUTO_INCREMENT, geo_data GEOMETRY NOT NULL);", connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + var bc = new SingleStoreBulkCopy(connection) + { + DestinationTableName = "bulk_load_data_table", + ColumnMappings = + { + new() + { + SourceOrdinal = 0, + DestinationColumn = "geo_data", + }, + }, + }; + + await bc.WriteToServerAsync(dataTable); + }*/ #endif private static string GetConnectionString() => BulkLoaderSync.GetConnectionString(); private static string GetLocalConnectionString() => BulkLoaderSync.GetLocalConnectionString(); - readonly string m_testTable; - readonly byte[] m_memoryStreamBytes; + private readonly string m_testTable; + private readonly byte[] m_memoryStreamBytes; } diff --git a/tests/SideBySide/BulkLoaderSync.cs b/tests/SideBySide/BulkLoaderSync.cs index 7f6c2cd80..835730627 100644 --- a/tests/SideBySide/BulkLoaderSync.cs +++ b/tests/SideBySide/BulkLoaderSync.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using SingleStoreConnector.Core; using Xunit.Sdk; @@ -180,7 +181,7 @@ public void BulkLoadLocalCsvFileNotFound() connection.Open(); SingleStoreBulkLoader bl = new SingleStoreBulkLoader(connection) { - Timeout = 3, //Set a short timeout for this test because the file not found exception takes a long time otherwise, the timeout does not change the result + Timeout = 3, // Set a short timeout for this test because the file not found exception takes a long time otherwise, the timeout does not change the result FileName = AppConfig.SingleStoreBulkLoaderLocalCsvFile + "-junk", TableName = m_testTable, CharacterSet = "UTF8", @@ -217,7 +218,7 @@ public void BulkLoadLocalCsvFileNotFound() } catch (Exception exception) { - //We know that the exception is not a SingleStoreException, just use the assertion to fail the test + // We know that the exception is not a SingleStoreException, just use the assertion to fail the test Assert.IsType(exception); } } @@ -589,6 +590,71 @@ public void BulkCopyDataTableWithSingleStoreDecimal() } } + [SkippableTheory(ServerFeatures.Vector)] + [InlineData("byte[]")] + [InlineData("float[]")] + public void BulkCopyDataTableWithVector(string dataType) + { + var dataTable = new DataTable() + { + Columns = + { + new DataColumn("id", typeof(int)), + new DataColumn("data", +#pragma warning disable SA1118 // Parameter should not span multiple lines + dataType switch + { + "byte[]" => typeof(byte[]), + "float[]" => typeof(float[]), + _ => throw new ArgumentOutOfRangeException(nameof(dataType)), + }), +#pragma warning restore SA1118 // Parameter should not span multiple lines + }, + Rows = + { + new object[] { 1, GetDataRowValue([0, 0, 0], dataType) }, + new object[] { 2, GetDataRowValue([1f, 2f, 3f], dataType) }, + }, + }; + + using var connection = new SingleStoreConnection(GetLocalConnectionString()); + connection.Open(); + using (var cmd = new SingleStoreCommand(@"drop table if exists bulk_load_data_table; +create table bulk_load_data_table(a int, b vector(3));", connection)) + { + cmd.ExecuteNonQuery(); + } + + var bulkCopy = new SingleStoreBulkCopy(connection) + { + DestinationTableName = "bulk_load_data_table", + }; + var result = bulkCopy.WriteToServer(dataTable); + Assert.Equal(2, result.RowsInserted); + Assert.Empty(result.Warnings); + + using (var cmd = new SingleStoreCommand(@"select b from bulk_load_data_table order by a;", connection)) + { + using var reader = cmd.ExecuteReader(); + Assert.True(reader.Read()); + Assert.Equal(new float[3], GetFloatArray(reader, 0)); + Assert.True(reader.Read()); + Assert.Equal([1f, 2f, 3f], GetFloatArray(reader, 0)); + Assert.False(reader.Read()); + } + + static object GetDataRowValue(float[] data, string dataType) => + dataType == "byte[]" ? MemoryMarshal.Cast(data).ToArray() : data; + + static float[] GetFloatArray(SingleStoreDataReader reader, int ordinal) => + reader.GetValue(ordinal) switch + { + ReadOnlyMemory romf => romf.ToArray(), + byte[] b => MemoryMarshal.Cast(b).ToArray(), + { } x => throw new NotSupportedException(x.GetType().Name), + }; + } + #if NET6_0_OR_GREATER [Fact] public void BulkCopyDataTableWithDateOnly() @@ -673,7 +739,6 @@ public void BulkCopyDataTableWithTimeOnly() } #endif - public static IEnumerable GetBulkCopyData() => new object[][] { @@ -1066,7 +1131,7 @@ public void BulkCopyColumnMappings() new object[] { 1, 100, "a", "A", new byte[] { 0x33, 0x30 } }, new object[] { 2, 200, "bb", "BB", new byte[] { 0x33, 0x31 } }, new object[] { 3, 300, "ccc", "CCC", new byte[] { 0x33, 0x32 } }, - } + }, }; var result = bulkCopy.WriteToServer(dataTable); @@ -1120,7 +1185,7 @@ public void BulkCopyColumnMappingsInvalidSourceOrdinal() new object[] { 1 }, new object[] { 2 }, new object[] { 3 }, - } + }, }; Assert.Throws(() => bulkCopy.WriteToServer(dataTable)); @@ -1157,12 +1222,72 @@ public void BulkCopyColumnMappingsInvalidDestinationColumn() new object[] { 1 }, new object[] { 2 }, new object[] { 3 }, - } + }, }; Assert.Throws(() => bulkCopy.WriteToServer(dataTable)); } + [Fact] + public void BulkCopyToTableWithYear() + { + using var connection = new SingleStoreConnection(GetLocalConnectionString()); + connection.Open(); + + using var cmd = connection.CreateCommand(); + cmd.CommandText = """ + DROP TABLE IF EXISTS bulk_copy_year; + CREATE TABLE bulk_copy_year(int_value int NULL, year_value year NULL) + """; + cmd.ExecuteNonQuery(); + + var bulkCopy = new SingleStoreBulkCopy(connection) + { + DestinationTableName = "bulk_copy_year", + ColumnMappings = + { + new SingleStoreBulkCopyColumnMapping(0, "int_value", null), + }, + }; + + var dt = new DataTable(); + dt.Columns.Add("numbers"); + dt.Rows.Add(1); + dt.Rows.Add(2); + + var result = bulkCopy.WriteToServer(dt); + Assert.Equal(2, result.RowsInserted); + Assert.Empty(result.Warnings); + } + + [Fact] + public void BulkCopyToTableWithYearNotSupported() + { + using var connection = new SingleStoreConnection(GetLocalConnectionString()); + connection.Open(); + + using var cmd = connection.CreateCommand(); + cmd.CommandText = """ + DROP TABLE IF EXISTS bulk_copy_year; + CREATE TABLE bulk_copy_year(int_value int NULL, year_value year NULL) + """; + cmd.ExecuteNonQuery(); + + var bulkCopy = new SingleStoreBulkCopy(connection) + { + DestinationTableName = "bulk_copy_year", + }; + + var dt = new DataTable(); + dt.Columns.Add("numbers"); + dt.Columns.Add("year"); + dt.Rows.Add(1, 2000); + dt.Rows.Add(2, 2001); + + var exception = Assert.Throws(() => bulkCopy.WriteToServer(dt)); + Assert.Equal("'YEAR' columns are not supported by MySqlBulkCopy.", exception.Message); + } + [Fact] public void BulkCopyDoesNotInsertAllRows() { @@ -1174,7 +1299,7 @@ public void BulkCopyDoesNotInsertAllRows() var bcp = new SingleStoreBulkCopy(connection) { - DestinationTableName = "bulk_copy_duplicate_pk" + DestinationTableName = "bulk_copy_duplicate_pk", }; var dataTable = new DataTable() @@ -1189,7 +1314,7 @@ public void BulkCopyDoesNotInsertAllRows() new object[] { 1, "a" }, new object[] { 1, "b" }, new object[] { 3, "c" }, - } + }, }; var ex = Assert.Throws(() => bcp.WriteToServer(dataTable)); @@ -1310,6 +1435,46 @@ public void BulkCopyDataTableConflictOption(SingleStoreBulkLoaderConflictOption using (var cmd = new SingleStoreCommand("select b from bulk_load_data_table;", connection)) Assert.Equal(expected, cmd.ExecuteScalar()); } + + // TODO: reimplement for SingleStore Geography types + /*[Fact] + public void BulkCopyGeometry() + { + var dataTable = new DataTable() + { + Columns = + { + new DataColumn("geo_data", typeof(MySqlGeometry)), + }, + Rows = + { + new object[] { MySqlGeometry.FromWkb(0, [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 63, 0, 0, 0, 0, 0, 0, 240, 63]) }, + }, + }; + + using var connection = new MySqlConnection(GetLocalConnectionString()); + connection.Open(); + using (var cmd = new MySqlCommand(@"drop table if exists bulk_load_data_table; +create table bulk_load_data_table(id BIGINT UNIQUE NOT NULL AUTO_INCREMENT, geo_data GEOMETRY NOT NULL);", connection)) + { + cmd.ExecuteNonQuery(); + } + + var bc = new MySqlBulkCopy(connection) + { + DestinationTableName = "bulk_load_data_table", + ColumnMappings = + { + new() + { + SourceOrdinal = 0, + DestinationColumn = "geo_data", + }, + }, + }; + + bc.WriteToServer(dataTable); + }*/ #endif internal static string GetConnectionString() => AppConfig.ConnectionString; @@ -1321,6 +1486,6 @@ internal static string GetLocalConnectionString() return csb.ConnectionString; } - readonly string m_testTable; - readonly byte[] m_memoryStreamBytes; + private readonly string m_testTable; + private readonly byte[] m_memoryStreamBytes; } diff --git a/tests/SideBySide/CancelTests.cs b/tests/SideBySide/CancelTests.cs index 3faf8e2f4..ad4e32710 100644 --- a/tests/SideBySide/CancelTests.cs +++ b/tests/SideBySide/CancelTests.cs @@ -44,7 +44,6 @@ public void CancelCommand() #pragma warning restore xUnit1031 // Do not use blocking task operations in test method } - [SkippableFact(ServerFeatures.Timeout)] public void CancelReaderAsynchronously() { @@ -100,8 +99,8 @@ public async Task CancelCommandWithPasswordCallback() var stopwatch = Stopwatch.StartNew(); await TestUtilities.AssertExecuteScalarReturnsOneOrIsCanceledAsync(command); - // Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + // Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. task.Wait(); // shouldn't throw } @@ -120,6 +119,7 @@ public async Task CancelCommandCancellationTokenWithPasswordCallback() using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); var stopwatch = Stopwatch.StartNew(); await TestUtilities.AssertExecuteScalarReturnsOneOrIsCanceledAsync(command, cts.Token); + // Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } #endif @@ -143,6 +143,7 @@ public void CancelCommandBeforeRead() Assert.Equal((int) SingleStoreErrorCode.QueryInterrupted, ex.Number); } Assert.False(reader.NextResult()); + // TestUtilities.AssertDuration(stopwatch, 0, 1000); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. Assert.InRange(rows, 0, 10000000); } @@ -197,6 +198,7 @@ public void DapperQueryMultiple() stopwatch = Stopwatch.StartNew(); } stopwatch.Stop(); + // TestUtilities.AssertDuration(stopwatch, 0, 1000); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } @@ -354,8 +356,8 @@ public async Task CancelSlowQueryWithTokenAfterExecuteReader() // the call to ExecuteReader should block until the token is cancelled var stopwatch = Stopwatch.StartNew(); using var reader = await cmd.ExecuteReaderAsync(cts.Token); - // TestUtilities.AssertDuration(stopwatch, 450, 3000); commented out due to flakiness — execution can complete too quickly depending on system/load. + // TestUtilities.AssertDuration(stopwatch, 450, 3000); commented out due to flakiness — execution can complete too quickly depending on system/load. var rows = 0; try { @@ -367,6 +369,7 @@ public async Task CancelSlowQueryWithTokenAfterExecuteReader() catch (OperationCanceledException ex) { Assert.Equal(cts.Token, ex.CancellationToken); + // Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows } } @@ -376,7 +379,7 @@ public async Task CancelSlowQueryWithTokenAfterNextResult() { using var cmd = new SingleStoreCommand("SELECT 1; " + c_slowQuery, m_database.Connection) { - CommandTimeout = 0 + CommandTimeout = 0, }; using var reader = await cmd.ExecuteReaderAsync(); @@ -390,8 +393,8 @@ public async Task CancelSlowQueryWithTokenAfterNextResult() // the call to NextResult should block until the token is cancelled var stopwatch = Stopwatch.StartNew(); Assert.True(await reader.NextResultAsync(cts.Token)); - // TestUtilities.AssertDuration(stopwatch, 450, 1500); commented out due to flakiness — execution can complete too quickly depending on system/load. + // TestUtilities.AssertDuration(stopwatch, 450, 1500); commented out due to flakiness — execution can complete too quickly depending on system/load. int rows = 0; try { @@ -402,6 +405,7 @@ public async Task CancelSlowQueryWithTokenAfterNextResult() catch (OperationCanceledException ex) { Assert.Equal(cts.Token, ex.CancellationToken); + // Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows } } @@ -456,7 +460,6 @@ public void CancelBatchCommand() Assert.Equal("Query execution was interrupted", ex.Message); // Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. - task.Wait(); // shouldn't throw } @@ -525,6 +528,7 @@ public void CancelBatchBeforeRead() Assert.Equal(SingleStoreErrorCode.QueryInterrupted, ex.ErrorCode); } Assert.False(reader.NextResult()); + // TestUtilities.AssertDuration(stopwatch, 0, 1000); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. Assert.InRange(rows, 0, 10000000); } @@ -669,8 +673,8 @@ public async Task CancelSlowQueryBatchWithTokenAfterExecuteReader() // the call to ExecuteReader should block until the token is cancelled var stopwatch = Stopwatch.StartNew(); using var reader = await batch.ExecuteReaderAsync(cts.Token); - // TestUtilities.AssertDuration(stopwatch, 450, 3000); commented out due to flakiness — execution can complete too quickly depending on system/load. + // TestUtilities.AssertDuration(stopwatch, 450, 3000); commented out due to flakiness — execution can complete too quickly depending on system/load. var rows = 0; try { @@ -682,6 +686,7 @@ public async Task CancelSlowQueryBatchWithTokenAfterExecuteReader() catch (OperationCanceledException ex) { Assert.Equal(cts.Token, ex.CancellationToken); + // Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows } } @@ -709,8 +714,8 @@ public async Task CancelSlowQueryBatchWithTokenAfterNextResult() // the call to NextResult should block until the token is cancelled var stopwatch = Stopwatch.StartNew(); Assert.True(await reader.NextResultAsync(cts.Token)); - // TestUtilities.AssertDuration(stopwatch, 450, 1500); commented out due to flakiness — execution can complete too quickly depending on system/load. + // TestUtilities.AssertDuration(stopwatch, 450, 1500); commented out due to flakiness — execution can complete too quickly depending on system/load. int rows = 0; try { @@ -721,12 +726,14 @@ public async Task CancelSlowQueryBatchWithTokenAfterNextResult() catch (OperationCanceledException ex) { Assert.Equal(cts.Token, ex.CancellationToken); + // Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows } } Assert.False(await reader.NextResultAsync()); } +#endif #endif private static CancellationToken GetCanceledToken() @@ -736,15 +743,14 @@ private static CancellationToken GetCanceledToken() return cts.Token; } - static readonly CancellationToken s_canceledToken = GetCanceledToken(); -#endif - // returns billions of rows - const string c_hugeQuery = @"select * from integers a join integers b join integers c join integers d join integers e join integers f join integers g join integers h;"; + private const string c_hugeQuery = @"select * from integers a join integers b join integers c join integers d join integers e join integers f join integers g join integers h;"; // takes a long time to return any rows - const string c_slowQuery = @"select * from integers a join integers b join integers c join integers d join integers e join integers f join integers g join integers h + private const string c_slowQuery = @"select * from integers a join integers b join integers c join integers d join integers e join integers f join integers g join integers h where sqrt(a.value) + sqrt(b.value) + sqrt(c.value) + sqrt(d.value) + sqrt(e.value) + sqrt(f.value) + sqrt(g.value) + sqrt(h.value) = 20;"; - readonly DatabaseFixture m_database; + private static readonly CancellationToken s_canceledToken = GetCanceledToken(); + + private readonly DatabaseFixture m_database; } diff --git a/tests/SideBySide/CharacterSetTests.cs b/tests/SideBySide/CharacterSetTests.cs index 17dbbdb36..cd290f9f1 100644 --- a/tests/SideBySide/CharacterSetTests.cs +++ b/tests/SideBySide/CharacterSetTests.cs @@ -105,5 +105,5 @@ public void CollationConnection(bool reopenConnection) Assert.Equal(expected, collation); } - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; } diff --git a/tests/SideBySide/ChunkStream.cs b/tests/SideBySide/ChunkStream.cs new file mode 100644 index 000000000..3fd4fda23 --- /dev/null +++ b/tests/SideBySide/ChunkStream.cs @@ -0,0 +1,130 @@ +namespace SideBySide; + +internal sealed class ChunkStream : Stream +{ + public ChunkStream(byte[] data, int chunkLength) + { + if (data is null) + throw new ArgumentNullException(nameof(data)); + if (chunkLength <= 0) + throw new ArgumentOutOfRangeException(nameof(chunkLength)); + + m_data = data; + m_chunkLength = chunkLength; + m_position = 0; + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => m_data.Length; + public override long Position + { + get => m_position; + set => throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (buffer is null) + throw new ArgumentNullException(nameof(buffer)); + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException(nameof(offset)); + if (count < 0 || offset + count > buffer.Length) + throw new ArgumentOutOfRangeException(nameof(count)); + + return Read(buffer.AsSpan(offset, count)); + } + + public +#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER + override +#endif + int Read(Span buffer) + { + if (m_position >= m_data.Length) + return 0; + + // Read at most chunkLength bytes + var bytesToRead = Math.Min(buffer.Length, Math.Min(m_chunkLength, m_data.Length - m_position)); + + // Copy data from the actual data array + m_data.AsSpan(m_position, bytesToRead).CopyTo(buffer); + + m_position += bytesToRead; + return bytesToRead; + } + + public override int ReadByte() + { + Span buffer = stackalloc byte[1]; + var bytesRead = Read(buffer); + return bytesRead == 0 ? -1 : buffer[0]; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (buffer is null) + throw new ArgumentNullException(nameof(buffer)); + if (offset < 0 || offset > buffer.Length) + throw new ArgumentOutOfRangeException(nameof(offset)); + if (count < 0 || offset + count > buffer.Length) + throw new ArgumentOutOfRangeException(nameof(count)); + + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + try + { + return Task.FromResult(Read(buffer.AsSpan(offset, count))); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + public +#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER + override +#endif + ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + return new(Task.FromCanceled(cancellationToken)); + + try + { + return new(Read(buffer.Span)); + } + catch (Exception ex) + { + return new(Task.FromException(ex)); + } + } + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + + public override void WriteByte(byte value) => + throw new NotSupportedException(); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + throw new NotSupportedException(); + + public override void SetLength(long value) => + throw new NotSupportedException(); + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + + public override void Flush() => + throw new NotSupportedException(); + + public override Task FlushAsync(CancellationToken cancellationToken) => + throw new NotSupportedException(); + + private readonly byte[] m_data; + private readonly int m_chunkLength; + private int m_position; +} diff --git a/tests/SideBySide/ClientFactoryTests.cs b/tests/SideBySide/ClientFactoryTests.cs index 48ed0e7fb..cacc33d68 100644 --- a/tests/SideBySide/ClientFactoryTests.cs +++ b/tests/SideBySide/ClientFactoryTests.cs @@ -24,7 +24,6 @@ public void CreateConnectionStringBuilder() Assert.IsType(SingleStoreConnectorFactory.Instance.CreateConnectionStringBuilder()); } - [Fact] public void CreateParameter() { diff --git a/tests/SideBySide/CommandBuilderTests.cs b/tests/SideBySide/CommandBuilderTests.cs index 57af01f81..f74f910ec 100644 --- a/tests/SideBySide/CommandBuilderTests.cs +++ b/tests/SideBySide/CommandBuilderTests.cs @@ -136,11 +136,11 @@ value varchar(100) [Theory] [InlineData("test", "`test`")] [InlineData("te`st", "`te``st`")] - [InlineData("`test`", "```test```" #if BASELINE - , Skip = "Doesn't quote leading quotes" + [InlineData("`test`", "```test```", Skip = "Doesn't quote leading quotes")] +#else + [InlineData("`test`", "```test```")] #endif - )] public void QuoteIdentifier(string input, string expected) { var cb = new SingleStoreCommandBuilder(); @@ -158,5 +158,5 @@ public void UnquoteIdentifier(string input, string expected) Assert.Equal(expected, cb.UnquoteIdentifier(input)); } - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; } diff --git a/tests/SideBySide/CommandTests.cs b/tests/SideBySide/CommandTests.cs index cda46d853..ef2ed8304 100644 --- a/tests/SideBySide/CommandTests.cs +++ b/tests/SideBySide/CommandTests.cs @@ -267,7 +267,7 @@ public void IgnoreCommandTransactionIgnoresNull() { using var connection = new SingleStoreConnection(GetIgnoreCommandTransactionConnectionString()); connection.Open(); - using var _ = connection.BeginTransaction(); + using var ignoredTransaction = connection.BeginTransaction(); using var command = connection.CreateCommand(); command.CommandText = "SELECT 1;"; TestUtilities.AssertIsOne(command.ExecuteScalar()); @@ -469,10 +469,10 @@ private static string GetIgnoreCommandTransactionConnectionString() #else return new SingleStoreConnectionStringBuilder(AppConfig.ConnectionString) { - IgnoreCommandTransaction = true + IgnoreCommandTransaction = true, }.ConnectionString; #endif } - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; } diff --git a/tests/SideBySide/CommandTimeoutTests.cs b/tests/SideBySide/CommandTimeoutTests.cs index 191e7c869..ccd63331d 100644 --- a/tests/SideBySide/CommandTimeoutTests.cs +++ b/tests/SideBySide/CommandTimeoutTests.cs @@ -66,6 +66,7 @@ public void CommandTimeoutWithSleepSync() } #endif sw.Stop(); + // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } @@ -92,6 +93,7 @@ public async Task CommandTimeoutWithSleepAsync() } #endif sw.Stop(); + // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 700); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } @@ -133,6 +135,7 @@ create procedure sleep_sproc(seconds INT) as } #endif sw.Stop(); + // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } @@ -161,6 +164,7 @@ public void MultipleCommandTimeoutWithSleepSync() #endif sw.Stop(); + // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } @@ -191,6 +195,7 @@ public async Task MultipleCommandTimeoutWithSleepAsync() #endif sw.Stop(); + // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 550); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } @@ -239,7 +244,6 @@ public async Task CommandTimeoutResetsOnReadAsync() Assert.Equal(ConnectionState.Open, m_connection.State); } - [Fact] public void TransactionCommandTimeoutWithSleepSync() { @@ -261,6 +265,7 @@ public void TransactionCommandTimeoutWithSleepSync() } #endif sw.Stop(); + // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } @@ -293,6 +298,6 @@ public async Task TransactionCommandTimeoutWithSleepAsync() Assert.Equal(connectionState, m_connection.State); } - readonly DatabaseFixture m_database; - readonly SingleStoreConnection m_connection; + private readonly DatabaseFixture m_database; + private readonly SingleStoreConnection m_connection; } diff --git a/tests/SideBySide/ConnectAsync.cs b/tests/SideBySide/ConnectAsync.cs index f1152a010..a03132e59 100644 --- a/tests/SideBySide/ConnectAsync.cs +++ b/tests/SideBySide/ConnectAsync.cs @@ -1,7 +1,4 @@ using System.Security.Authentication; -#if !BASELINE -using SingleStoreConnector.Authentication.Ed25519; -#endif namespace SideBySide; @@ -128,6 +125,7 @@ public async Task ConnectTimeoutAsync() var ex = await Assert.ThrowsAsync(connection.OpenAsync); stopwatch.Stop(); Assert.Equal((int) SingleStoreErrorCode.UnableToConnectToHost, ex.Number); + // TestUtilities.AssertDuration(stopwatch, 1900, 1500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } @@ -152,6 +150,7 @@ public async Task ConnectTimeoutAsyncCancellationToken() stopwatch.Stop(); Assert.Equal(TaskStatus.Canceled, task.Status); Assert.Equal(cts.Token, ex.CancellationToken); + // TestUtilities.AssertDuration(stopwatch, 1900, 1500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } @@ -191,7 +190,11 @@ public async Task UsePasswordProviderPasswordTakesPrecedence() await SingleStoreConnection.ClearPoolAsync(connection); var wasCalled = false; - connection.ProvidePasswordCallback = _ => { wasCalled = true; return password; }; + connection.ProvidePasswordCallback = _ => + { + wasCalled = true; + return password; + }; await connection.OpenAsync(); Assert.False(wasCalled); @@ -431,7 +434,7 @@ public async Task CachingSha2WithoutSecureConnection() [SkippableFact(ServerFeatures.Ed25519)] public async Task Ed25519Authentication() { - Ed25519AuthenticationPlugin.Install(); + SingleStoreConnector.Authentication.Ed25519.Ed25519AuthenticationPlugin.Install(); var csb = AppConfig.CreateConnectionStringBuilder(); csb.UserID = "ed25519user"; @@ -444,7 +447,7 @@ public async Task Ed25519Authentication() [SkippableFact(ServerFeatures.Ed25519)] public async Task MultiAuthentication() { - Ed25519AuthenticationPlugin.Install(); + SingleStoreConnector.Authentication.Ed25519.Ed25519AuthenticationPlugin.Install(); var csb = AppConfig.CreateConnectionStringBuilder(); csb.UserID = "multiAuthUser"; csb.Password = "secret"; @@ -452,6 +455,18 @@ public async Task MultiAuthentication() using var connection = new SingleStoreConnection(csb.ConnectionString); await connection.OpenAsync(); } + + [SkippableFact(ServerFeatures.ParsecAuthentication)] + public async Task Parsec() + { + SingleStoreConnector.Authentication.Ed25519.ParsecAuthenticationPlugin.Install(); + var csb = AppConfig.CreateConnectionStringBuilder(); + csb.UserID = "parsec-user"; + csb.Password = "P@rs3c-Pa55"; + csb.Database = null; + using var connection = new SingleStoreConnection(csb.ConnectionString); + await connection.OpenAsync(); + } #endif // To create a MariaDB GSSAPI user for a current user @@ -479,6 +494,7 @@ public async Task GoodServerSPN() { var csb = AppConfig.CreateGSSAPIConnectionStringBuilder(); string serverSPN; + // Use server's variable gssapi_principal_name as SPN using (var connection = new SingleStoreConnection(csb.ConnectionString)) { @@ -548,6 +564,5 @@ public async Task DisposeAsyncRaisesDisposed() Assert.Equal(1, disposedCount); } - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; } - diff --git a/tests/SideBySide/ConnectSync.cs b/tests/SideBySide/ConnectSync.cs index 695884b81..54dfae485 100644 --- a/tests/SideBySide/ConnectSync.cs +++ b/tests/SideBySide/ConnectSync.cs @@ -98,7 +98,7 @@ public void NonExistentPipe() PipeName = "nonexistingpipe", ConnectionProtocol = SingleStoreConnectionProtocol.NamedPipe, Server = ".", - ConnectionTimeout = 1 + ConnectionTimeout = 1, }; var sw = Stopwatch.StartNew(); @@ -265,7 +265,11 @@ public void UsePasswordProviderPasswordTakesPrecedence() SingleStoreConnection.ClearPool(connection); var wasCalled = false; - connection.ProvidePasswordCallback = _ => { wasCalled = true; return password; }; + connection.ProvidePasswordCallback = _ => + { + wasCalled = true; + return password; + }; connection.Open(); Assert.False(wasCalled); @@ -604,5 +608,5 @@ public void DisposeRaisesDisposed() Assert.Equal(1, disposedCount); } - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; } diff --git a/tests/SideBySide/DataAdapterTests.cs b/tests/SideBySide/DataAdapterTests.cs index 45cace751..b8b819940 100644 --- a/tests/SideBySide/DataAdapterTests.cs +++ b/tests/SideBySide/DataAdapterTests.cs @@ -187,10 +187,9 @@ public void BatchUpdate() da.Update(ds); } - Assert.Equal(new List{ "two", "three", "four" }, m_connection.Query("SELECT text_value FROM data_adapter ORDER BY int_value")); + Assert.Equal(new List { "two", "three", "four" }, m_connection.Query("SELECT text_value FROM data_adapter ORDER BY int_value")); } - [Fact] public void BatchInsert() { @@ -326,5 +325,5 @@ public void ConvertBatchToCommandParameters() } #endif - readonly SingleStoreConnection m_connection; + private readonly SingleStoreConnection m_connection; } diff --git a/tests/SideBySide/DataTypes.cs b/tests/SideBySide/DataTypes.cs index 35b3a34a6..78064537a 100644 --- a/tests/SideBySide/DataTypes.cs +++ b/tests/SideBySide/DataTypes.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Runtime.InteropServices; using SingleStoreConnector.Core; #if BASELINE using MySql.Data.Types; @@ -9,15 +10,14 @@ // However, DbDataReader.GetString etc. are documented as throwing InvalidCastException: https://msdn.microsoft.com/en-us/library/system.data.common.dbdatareader.getstring.aspx // Additionally, that is what DbDataReader.GetFieldValue throws. For consistency, we prefer InvalidCastException. #if BASELINE -using GetValueWhenNullException = System.Data.SqlTypes.SqlNullValueException; -using GetGuidWhenNullException = MySql.Data.MySqlClient.SingleStoreException; using GetBytesWhenNullException = System.NullReferenceException; using GetGeographyWhenNullException = System.Exception; #else -using GetValueWhenNullException = System.InvalidCastException; -using GetGuidWhenNullException = System.InvalidCastException; using GetBytesWhenNullException = System.InvalidCastException; using GetGeographyWhenNullException = System.InvalidCastException; +using GetGuidWhenNullException = System.InvalidCastException; +using GetStreamWhenNullException = System.InvalidCastException; +using GetValueWhenNullException = System.InvalidCastException; #endif namespace SideBySide; @@ -217,7 +217,7 @@ public void QueryTinyIntSbyte(string column, string dataTypeName, object[] expec DoQuery("bools", column, dataTypeName, expected, reader => reader.GetSByte(0), baselineCoercedNullValue: default(sbyte), connection: connection); } - [Theory()] + [Theory] [InlineData("TinyInt1U", "TINYINT", new object[] { null, (byte) 0, (byte) 1, (byte) 0, (byte) 1, (byte) 255, (byte) 123 })] public void QueryTinyInt1Unsigned(string column, string dataTypeName, object[] expected) { @@ -313,8 +313,8 @@ public void QueryDouble(string column, string dataTypeName, object[] expected) [Theory] [InlineData("SmallDecimal", new object[] { null, "0", "-999.99", "-0.01", "999.99", "0.01" })] [InlineData("MediumDecimal", new object[] { null, "0", "-999999999999.99999999", "-0.00000001", "999999999999.99999999", "0.00000001" })] - // value exceeds the range of a decimal and cannot be deserialized - // [InlineData("BigDecimal", new object[] { null, "0", "-99999999999999999999.999999999999999999999999999999", "-0.000000000000000000000000000001", "99999999999999999999.999999999999999999999999999999", "0.000000000000000000000000000001" })] + //// value exceeds the range of a decimal and cannot be deserialized + //// [InlineData("BigDecimal", new object[] { null, "0", "-99999999999999999999.999999999999999999999999999999", "-0.000000000000000000000000000001", "99999999999999999999.999999999999999999999999999999", "0.000000000000000000000000000001" })] public void QueryDecimal(string column, object[] expected) { for (int i = 0; i < expected.Length; i++) @@ -337,7 +337,7 @@ public void QueryString(string column, string[] expected) }, getFieldValueType: typeof(TextReader)); #endif } - const string c_251ByteString = "This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating \"this field is null\". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data."; + private const string c_251ByteString = "This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating \"this field is null\". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data."; [Theory] [InlineData("guid", "CHAR(36)", new object[] { null, "00000000-0000-0000-0000-000000000000", "00000000-0000-0000-c000-000000000046", "fd24a0e8-c3f2-4821-a456-35da2dc4bb8f", "6A0E0A40-6228-11D3-A996-0050041896C8" })] @@ -565,7 +565,7 @@ insert into guid_format(c36, c32, b16, tsb16, leb16, t, b) values( new() { Value = isLittleEndianBinary16 ? guid : guidAsLittleEndianBinary16 }, new() { Value = guidAsChar32 }, new() { Value = isBinary16 ? guidAsBinary16 : isTimeSwapBinary16 ? guidAsTimeSwapBinary16 : guidAsLittleEndianBinary16 }, - } + }, }; cmd.ExecuteNonQuery(); cmd.CommandText = "select c36, c32, b16, tsb16, leb16, t, b from guid_format;"; @@ -722,7 +722,7 @@ insert into date_time_kind(d, dt0, dt6) values(?, ?, ?)", connection) new() { Value = dateTimeIn }, new() { Value = dateTimeIn }, new() { Value = dateTimeIn }, - } + }, }; if (success) { @@ -749,11 +749,7 @@ insert into date_time_kind(d, dt0, dt6) values(?, ?, ?)", connection) [InlineData("`Time`", "TIME", new object[] { null, "-838 -59 -59", "838 59 59", "0 0 0", "0 14 3 4 567890" })] public void QueryTime(string column, string dataTypeName, object[] expected) { - DoQuery("times", column, dataTypeName, ConvertToTimeSpan(expected), reader => reader.GetTimeSpan(0) -#if BASELINE // https://bugs.mysql.com/bug.php?id=103801 - , omitWherePrepareTest: true -#endif - ); + DoQuery("times", column, dataTypeName, ConvertToTimeSpan(expected), reader => reader.GetTimeSpan(0)); } #if NET6_0_OR_GREATER && !BASELINE @@ -1038,8 +1034,8 @@ public void ReadVarCharAsDate(string value, bool prepare, string expectedValue) } [Theory] - // [InlineData("Geography", "GEOGRAPHY", "POINT(1.00000000 1.00000000)")] those two are failing bc we use string to represent geospatial types => SS returns POINT(0.99999998 1.00000003) - // [InlineData("Point", "POINT", "POINT(1.00000000 1.00000000)")] + //// [InlineData("Geography", "GEOGRAPHY", "POINT(1.00000000 1.00000000)")] those two are failing bc we use string to represent geospatial types => SS returns POINT(0.99999998 1.00000003) + //// [InlineData("Point", "POINT", "POINT(1.00000000 1.00000000)")] [InlineData("LineString", "GEOGRAPHY", "LINESTRING(0.00000000 0.00000000, 1.00000000 1.00000000, 2.00000000 2.00000000)")] [InlineData("Polygon", "GEOGRAPHY", "POLYGON((0.00000000 0.00000000, 1.00000000 0.00000000, 1.00000000 1.00000000, 0.00000000 1.00000000, 0.00000000 0.00000000))")] public void QueryGeography(string columnName, string dataTypeName, string expectedGeography) @@ -1058,7 +1054,7 @@ public void QueryGeography(string columnName, string dataTypeName, string expect var geographyData = new string[] { null, - expectedGeography + expectedGeography, }; DoQuery("geography", columnName, dataTypeName, geographyData, reader => reader.GetString(0), connection: connectionWithParam); @@ -1097,7 +1093,7 @@ private static object CreateGeography(string data) private static object CreateGeographyPoint(string data) { - if(data is null) + if (data is null) return null; return new SingleStoreGeographyPoint(data); @@ -1140,6 +1136,11 @@ private static object CreateGeographyPoint(string data) [InlineData("Int64", "datatypes_integers", SingleStoreDbType.Int64, 20, typeof(long), "N", 0, 0)] [InlineData("UInt64", "datatypes_integers", SingleStoreDbType.UInt64, 20, typeof(ulong), "N", 0, 0)] [InlineData("value", "datatypes_json_core", SingleStoreDbType.JSON, int.MaxValue, typeof(string), "LN", 0, 0)] +#if BASELINE + [InlineData("value", "datatypes_vector", SingleStoreDbType.Vector, 12, typeof(byte[]), "N", 0, 31)] +#else + [InlineData("value", "datatypes_vector", SingleStoreDbType.Vector, 3, typeof(ReadOnlyMemory), "N", 0, 31)] +#endif [InlineData("Single", "datatypes_reals", SingleStoreDbType.Float, 12, typeof(float), "N", 0, 31)] [InlineData("Double", "datatypes_reals", SingleStoreDbType.Double, 22, typeof(double), "N", 0, 31)] [InlineData("SmallDecimal", "datatypes_reals", SingleStoreDbType.NewDecimal, 7, typeof(decimal), "N", 5, 2)] @@ -1181,7 +1182,7 @@ public void GetSchemaTable(string column, string table, SingleStoreDbType mySqlD } DoGetSchemaTable(column, table, mySqlDbType, columnSize, dataType, flags, precision, scale, connection: connectionWithParam); - if(connectionWithParam != null) + if (connectionWithParam != null) connectionWithParam.Close(); } @@ -1204,6 +1205,17 @@ private void DoGetSchemaTable(string column, string table, SingleStoreDbType myS { if (table == "datatypes_json_core" && !AppConfig.SupportsJson) return; + if (table == "datatypes_vector" && !AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Vector)) + return; + + // adjust for databases that don't have a dedicated on-the-wire type for VECTOR(n) + if (mySqlDbType == SingleStoreDbType.Vector && !AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType)) + { + mySqlDbType = SingleStoreDbType.VarBinary; + columnSize *= 4; + dataType = typeof(byte[]); + scale = 0; + } connection = connection ?? Connection; var isAutoIncrement = flags.IndexOf('A') != -1; @@ -1242,7 +1254,9 @@ private void DoGetSchemaTable(string column, string table, SingleStoreDbType myS { } else + { Assert.Equal(columnSize, schema["ColumnSize"]); + } #endif Assert.Equal(isLong, schema["IsLong"]); Assert.Equal(isAutoIncrement, schema["IsAutoIncrement"]); @@ -1351,6 +1365,8 @@ public void GetSchemaTableAfterNextResult() [InlineData("utf8bin", "datatypes_strings", SingleStoreDbType.VarChar, "VARCHAR", 300, typeof(string), "N", -1, 0)] [InlineData("guid", "datatypes_strings", SingleStoreDbType.Guid, "CHAR(36)", 36, typeof(Guid), "N", -1, 0)] [InlineData("guidbin", "datatypes_strings", SingleStoreDbType.Guid, "CHAR(36)", 36, typeof(Guid), "N", -1, 0)] + [InlineData("nonguid_utf8", "datatypes_strings", SingleStoreDbType.VarChar, "VARCHAR", 36, typeof(string), "N", -1, 0)] + [InlineData("nonguid_latin1", "datatypes_strings", SingleStoreDbType.VarChar, "VARCHAR", 36, typeof(string), "N", -1, 0)] [InlineData("Date", "datatypes_times", SingleStoreDbType.Date, "DATE", 10, typeof(DateTime), "N", -1, 0)] [InlineData("DateTime", "datatypes_times", SingleStoreDbType.DateTime, "DATETIME", 26, typeof(DateTime), "N", -1, 6)] [InlineData("Timestamp", "datatypes_times", SingleStoreDbType.Timestamp, "TIMESTAMP", 26, typeof(DateTime), "N", -1, 6)] @@ -1378,7 +1394,7 @@ public void GetColumnSchema(string column, string table, SingleStoreDbType mySql } DoGetColumnSchema(column, table, mySqlDbType, dataTypeName, columnSize, dataType, flags, precision, scale, connection: connectionWithParam); - if(connectionWithParam != null) + if (connectionWithParam != null) connectionWithParam.Close(); } @@ -1412,9 +1428,9 @@ private void DoGetColumnSchema(string column, string table, SingleStoreDbType my // the condition below accounts for wrong charset reported in SingleStore 7.5 and 7.6 // when dealing with utf8mb4 data - if ( !( (column == "utf8" || column == "utf8bin") && - connection.Session.S2ServerVersion.Version.CompareTo(new Version(7,5,0)) > 0 && - connection.Session.S2ServerVersion.Version.CompareTo(new Version(7,8,0)) < 0 )) + if (!((column == "utf8" || column == "utf8bin") && + connection.Session.S2ServerVersion.Version.CompareTo(new Version(7, 5, 0)) > 0 && + connection.Session.S2ServerVersion.Version.CompareTo(new Version(7, 8, 0)) < 0)) { // TODO: PLAT-6085: remove this if if (column != "Single" && column != "Double") @@ -1569,7 +1585,7 @@ public void StoredProcedureParameter(string column, string table, string dataTyp [InlineData("DateTime", "datatypes_times", "DATETIME(6)")] [InlineData("Timestamp", "datatypes_times", "TIMESTAMP(6)")] [InlineData("Time", "datatypes_times", "TIME(6)")] - // [InlineData("Year", "datatypes_times", "YEAR")] + //// [InlineData("Year", "datatypes_times", "YEAR")] [InlineData("value", "datatypes_json_core", "JSON")] public void BulkCopyDataReader(string column, string table, string dataTypeName) { @@ -1661,6 +1677,43 @@ public void QueryJson(string column, string[] expected) DoQuery("json_core", column, dataTypeName, expected, reader => reader.GetString(0), omitWhereTest: true); } + [SkippableTheory(ServerFeatures.Vector)] + [InlineData("value", new[] { null, "0,0,0", "1,1,1", "1,2,3", "-1,-1,-1" })] + public void QueryVector(string column, string[] expected) + { + var hasVectorType = AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType); + string dataTypeName = hasVectorType ? "VECTOR" : "BLOB"; + DoQuery("vector", column, dataTypeName, + expected.Select(x => +#if !MYSQL_DATA + hasVectorType ? (GetFloatArray(x) is float[] a ? (object) new ReadOnlyMemory(a) : null) : GetByteArray(x)) +#else + // Connector/NET returns the float array as a byte[] + GetByteArray(x)) +#endif + .ToArray(), +#if !MYSQL_DATA + x => hasVectorType ? (ReadOnlyMemory) x.GetValue(0) : (byte[]) x.GetValue(0), +#else + // NOTE: Connector/NET returns 'null' for NULL so simulate an exception for the tests + x => x.IsDBNull(0) ? throw new GetValueWhenNullException() : x.GetValue(0), +#endif + assertEqual: (l, r) => + { + if (l is ReadOnlyMemory roml) + l = roml.ToArray(); + if (r is ReadOnlyMemory romr) + r = romr.ToArray(); + Assert.Equal(l, r); + }, + omitWhereTest: true); + + static float[] GetFloatArray(string value) => value?.Split(',').Select(x => float.Parse(x, CultureInfo.InvariantCulture)).ToArray(); + + static byte[] GetByteArray(string value) => + GetFloatArray(value) is { } floats ? MemoryMarshal.AsBytes(floats).ToArray() : null; + } + [SkippableTheory(Baseline = "https://bugs.mysql.com/bug.php?id=97067")] [InlineData(false, "MIN", 0)] [InlineData(false, "MAX", uint.MaxValue)] @@ -1910,4 +1963,3 @@ private static int[] SplitAndParse(object obj) private SingleStoreConnectionStringBuilder CreateConnectionStringBuilder() => new(AppConfig.ConnectionString); } - From b3575810db9baeb82770efdcf774a1b39ab11c41 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Fri, 3 Apr 2026 13:56:42 +0300 Subject: [PATCH 09/21] grab the last block of changes for tests --- tests/SideBySide/DataTypes.cs | 3 +- tests/SideBySide/DataTypesFixture.cs | 38 +++- tests/SideBySide/DatabaseFixture.cs | 4 +- tests/SideBySide/InsertTests.cs | 162 +++++++++++++++--- tests/SideBySide/LoadDataInfileAsync.cs | 6 +- tests/SideBySide/LoadDataInfileSync.cs | 6 +- tests/SideBySide/ParameterCollection.cs | 4 +- tests/SideBySide/QueryTests.cs | 76 +++++++- tests/SideBySide/RedirectionTests.cs | 139 +++++++-------- tests/SideBySide/SchemaProviderTests.cs | 6 +- tests/SideBySide/ServerFeatures.cs | 15 ++ .../SideBySide/SingleStoreDataSourceTests.cs | 2 +- tests/SideBySide/SslTests.cs | 16 +- tests/SideBySide/StoredProcedureTests.cs | 129 ++++++++++++-- tests/SideBySide/TestUtilities.cs | 4 +- tests/SideBySide/Transaction.cs | 4 +- tests/SideBySide/TransactionScopeTests.cs | 16 +- tests/SideBySide/UpdateTests.cs | 2 +- .../DependencyInjectionTests.cs | 2 +- .../ByteBufferWriterTests.cs | 2 +- .../CachedProcedureTests.cs | 110 ++++++------ .../CancellationTests.cs | 29 +++- .../ConnectionTests.cs | 76 +++++++- tests/SingleStoreConnector.Tests/DummyEnum.cs | 2 +- .../FakeSingleStoreServer.cs | 28 ++- .../FakeSingleStoreServerConnection.cs | 4 +- .../Metrics/IConnectionCreator.cs | 5 +- .../Metrics/MetricsTestsBase.cs | 3 +- ...SingleStoreConnectionStringBuilderTests.cs | 6 +- .../SingleStoreDateTimeTests.cs | 18 +- ...toreParameterCollectionNameToIndexTests.cs | 2 +- .../SingleStoreParameterCollectionTests.cs | 2 +- .../SingleStoreParameterTests.cs | 6 +- .../TypeMapperTests.cs | 14 +- .../UtilityTests.cs | 36 ++-- .../SchemaCollectionGenerator.cs | 5 - 36 files changed, 711 insertions(+), 271 deletions(-) diff --git a/tests/SideBySide/DataTypes.cs b/tests/SideBySide/DataTypes.cs index 78064537a..38d1e92fd 100644 --- a/tests/SideBySide/DataTypes.cs +++ b/tests/SideBySide/DataTypes.cs @@ -326,6 +326,7 @@ public void QueryDecimal(string column, object[] expected) [Theory] [InlineData("utf8", new[] { null, "", "ASCII", "Ũńıċōđĕ", c_251ByteString })] [InlineData("utf8bin", new[] { null, "", "ASCII", "Ũńıċōđĕ", c_251ByteString })] + [InlineData("nonguid_utf8", new[] { null, "", "ASCII", "Ũńıċōđĕ", "This string has 36 characters in it." })] public void QueryString(string column, string[] expected) { DoQuery("strings", column, "VARCHAR", expected, reader => reader.GetString(0)); @@ -1366,7 +1367,6 @@ public void GetSchemaTableAfterNextResult() [InlineData("guid", "datatypes_strings", SingleStoreDbType.Guid, "CHAR(36)", 36, typeof(Guid), "N", -1, 0)] [InlineData("guidbin", "datatypes_strings", SingleStoreDbType.Guid, "CHAR(36)", 36, typeof(Guid), "N", -1, 0)] [InlineData("nonguid_utf8", "datatypes_strings", SingleStoreDbType.VarChar, "VARCHAR", 36, typeof(string), "N", -1, 0)] - [InlineData("nonguid_latin1", "datatypes_strings", SingleStoreDbType.VarChar, "VARCHAR", 36, typeof(string), "N", -1, 0)] [InlineData("Date", "datatypes_times", SingleStoreDbType.Date, "DATE", 10, typeof(DateTime), "N", -1, 0)] [InlineData("DateTime", "datatypes_times", SingleStoreDbType.DateTime, "DATETIME", 26, typeof(DateTime), "N", -1, 6)] [InlineData("Timestamp", "datatypes_times", SingleStoreDbType.Timestamp, "TIMESTAMP", 26, typeof(DateTime), "N", -1, 6)] @@ -1491,6 +1491,7 @@ private void DoGetColumnSchema(string column, string table, SingleStoreDbType my [InlineData("MediumDecimal", "datatypes_reals", "DECIMAL(28,8)", typeof(decimal), 3, null)] [InlineData("BigDecimal", "datatypes_reals", "DECIMAL(50,30)", typeof(decimal), 3, null)] [InlineData("utf8", "datatypes_strings", "VARCHAR(300)", typeof(string), 3, "ASCII")] + [InlineData("nonguid_utf8", "datatypes_strings", SingleStoreDbType.VarChar, "VARCHAR(36)", typeof(string), 3, "ASCII")] [InlineData("Date", "datatypes_times", "DATE", typeof(DateTime), 2, null)] [InlineData("DateTime", "datatypes_times", "DATETIME", typeof(DateTime), 2, null)] [InlineData("Timestamp", "datatypes_times", "TIMESTAMP", typeof(DateTime), 2, null)] diff --git a/tests/SideBySide/DataTypesFixture.cs b/tests/SideBySide/DataTypesFixture.cs index 32928f614..89ca9f817 100644 --- a/tests/SideBySide/DataTypesFixture.cs +++ b/tests/SideBySide/DataTypesFixture.cs @@ -121,21 +121,22 @@ latin1 varchar(300) character set 'utf8' null, latin1bin varchar(300) character set utf8 collate utf8_bin null, cp1251 varchar(300) character set 'utf8' null, guid char(36) null, - guidbin char(36) binary null + guidbin char(36) binary null, + nonguid_utf8 varchar(36) character set 'utf8mb4' null ); -insert into datatypes_strings(utf8, utf8bin, latin1, latin1bin, cp1251, guid, guidbin) +insert into datatypes_strings(utf8, utf8bin, latin1, latin1bin, cp1251, guid, guidbin, nonguid_utf8) values - (null, null, null, null, null, null, null), - ('', '', '', '', '', '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000'), - ('ASCII', 'ASCII', 'ASCII', 'ASCII', 'ASCII', '00000000-0000-0000-c000-000000000046', '00000000-0000-0000-c000-000000000046'), - ('Ũńıċōđĕ', 'Ũńıċōđĕ', 'Lãtïñ', 'Lãtïñ', 'АБВГабвг', 'fd24a0e8-c3f2-4821-a456-35da2dc4bb8f', 'fd24a0e8-c3f2-4821-a456-35da2dc4bb8f'), + (null, null, null, null, null, null, null, null), + ('', '', '', '', '', '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000', ''), + ('ASCII', 'ASCII', 'ASCII', 'ASCII', 'ASCII', '00000000-0000-0000-c000-000000000046', '00000000-0000-0000-c000-000000000046', 'ASCII'), + ('Ũńıċōđĕ', 'Ũńıċōđĕ', 'Lãtïñ', 'Lãtïñ', 'АБВГабвг', 'fd24a0e8-c3f2-4821-a456-35da2dc4bb8f', 'fd24a0e8-c3f2-4821-a456-35da2dc4bb8f', 'Ũńıċōđĕ'), ('This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating ""this field is null"". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.', 'This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating ""this field is null"". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.', 'This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating ""this field is null"". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.', 'This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating ""this field is null"". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.', 'This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating ""this field is null"". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.', - '6a0e0a40-6228-11d3-a996-0050041896c8', '6a0e0a40-6228-11d3-a996-0050041896c8'); + '6a0e0a40-6228-11d3-a996-0050041896c8', '6a0e0a40-6228-11d3-a996-0050041896c8', 'This string has 36 characters in it.'); drop table if exists datatypes_blobs; create table datatypes_blobs( @@ -236,6 +237,29 @@ insert into datatypes_json_core (value) ('{""a"": ""b""}'); "); } + + if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Vector)) + { + /* create a helper function for MariaDB 11.7+ + if (Connection.ServerVersion.StartsWith("11.8.", StringComparison.Ordinal)) + Connection.Execute("create function if not exists STRING_TO_VECTOR(s text) returns vector(3) deterministic return Vec_FromText(s);");*/ + + Connection.Execute(""" + drop table if exists datatypes_vector; + create table datatypes_vector ( + rowid bigint not null primary key auto_increment, + value vector(3) null + ); + insert into datatypes_vector (value) + values + (null), + (STRING_TO_VECTOR('[0, 0, 0]')), + (STRING_TO_VECTOR('[1, 1, 1]')), + (STRING_TO_VECTOR('[1, 2, 3]')), + (STRING_TO_VECTOR('[-1, -1, -1]')); + """); + } + Connection.Close(); } } diff --git a/tests/SideBySide/DatabaseFixture.cs b/tests/SideBySide/DatabaseFixture.cs index 98193c363..b4c49bd50 100644 --- a/tests/SideBySide/DatabaseFixture.cs +++ b/tests/SideBySide/DatabaseFixture.cs @@ -53,6 +53,6 @@ protected virtual void Dispose(bool disposing) } } - static readonly object s_lock = new(); - static bool s_isInitialized; + private static readonly object s_lock = new(); + private static bool s_isInitialized; } diff --git a/tests/SideBySide/InsertTests.cs b/tests/SideBySide/InsertTests.cs index 9cf3c1b85..2a2353c5e 100644 --- a/tests/SideBySide/InsertTests.cs +++ b/tests/SideBySide/InsertTests.cs @@ -95,7 +95,7 @@ public void InsertTime(int precision) using (var reader = command.ExecuteReader()) { Assert.True(reader.Read()); - if(precision == 0) + if (precision == 0) Assert.Equal(TimeSpan.Zero, reader.GetValue(0)); else Assert.Equal(TimeSpan.FromMilliseconds(10), reader.GetValue(0)); @@ -113,7 +113,7 @@ public void InsertDateTimeOffset() { m_database.Connection.Execute(@"drop table if exists insert_datetimeoffset; create table insert_datetimeoffset(rowid integer not null primary key auto_increment, datetimeoffset1 datetime null);"); - var value = new DateTimeOffsetValues { datetimeoffset1 = new DateTimeOffset(2017, 1, 2, 3, 4, 5, TimeSpan.FromMinutes(678)) }; + var value = new DateTimeOffsetValues { DateTimeOffset1 = new DateTimeOffset(2017, 1, 2, 3, 4, 5, TimeSpan.FromMinutes(678)) }; m_database.Connection.Open(); try @@ -124,7 +124,7 @@ public void InsertDateTimeOffset() { ParameterName = "@datetimeoffset1", DbType = DbType.DateTimeOffset, - Value = value.datetimeoffset1 + Value = value.DateTimeOffset1, }); Assert.Equal(1, cmd.ExecuteNonQuery()); } @@ -137,7 +137,7 @@ public void InsertDateTimeOffset() DateTime.SpecifyKind(datetime, DateTimeKind.Utc); - Assert.Equal(value.datetimeoffset1.Value.UtcDateTime, datetime); + Assert.Equal(value.DateTimeOffset1.Value.UtcDateTime, datetime); } [SkippableFact(Baseline = "https://bugs.mysql.com/bug.php?id=91199")] @@ -145,7 +145,7 @@ public void InsertSingleStoreDateTime() { m_database.Connection.Execute(@"drop table if exists insert_mysqldatetime; create table insert_mysqldatetime(rowid integer not null primary key auto_increment, ts timestamp(6) null);"); - var value = new DateTimeOffsetValues { datetimeoffset1 = new DateTimeOffset(2017, 1, 2, 3, 4, 5, TimeSpan.FromMinutes(678)) }; + var value = new DateTimeOffsetValues { DateTimeOffset1 = new DateTimeOffset(2017, 1, 2, 3, 4, 5, TimeSpan.FromMinutes(678)) }; m_database.Connection.Open(); try @@ -178,7 +178,7 @@ public void InsertGeography(bool prepare) using var cmd = m_database.Connection.CreateCommand(); cmd.CommandText = @"insert into insert_singlestoregeography(shape) values(@shape);"; cmd.Parameters.AddWithValue("@shape", new SingleStoreGeography("POLYGON((3 3,4 3,4 4,3 4,3 3))")); - if(prepare) + if (prepare) cmd.Prepare(); Assert.Equal(1, cmd.ExecuteNonQuery()); } @@ -326,6 +326,130 @@ public void InsertSingleStoreDecimalAsDecimal(bool prepare) var val = ((decimal) reader.GetValue(0)).ToString(CultureInfo.InvariantCulture); Assert.Equal(value, val); } + + [Theory] + [InlineData(1_000_000, 1024, true)] + [InlineData(1_000_000, 1024, false)] + [InlineData(1_000_000, int.MaxValue, true)] + [InlineData(1_000_000, int.MaxValue, false)] + [InlineData(0xff_fff8, 299593, true)] + [InlineData(0xff_fff8, 299593, false)] + [InlineData(0xff_fff8, 300000, true)] + [InlineData(0xff_fff8, 300000, false)] + [InlineData(0xff_fff8, int.MaxValue, true)] + [InlineData(0xff_fff8, int.MaxValue, false)] + [InlineData(0xff_fff9, int.MaxValue, true)] + [InlineData(0xff_fff9, int.MaxValue, false)] + [InlineData(0x1ff_fff0, 299593, true)] + [InlineData(0x1ff_fff0, 299593, false)] + [InlineData(0x1ff_fff0, 300000, true)] + [InlineData(0x1ff_fff0, 300000, false)] + [InlineData(15_999_999, int.MaxValue, true)] + [InlineData(15_999_999, int.MaxValue, false)] + [InlineData(16_000_000, int.MaxValue, true)] + [InlineData(16_000_000, int.MaxValue, false)] + [InlineData(16_000_001, int.MaxValue, true)] + [InlineData(16_000_001, int.MaxValue, false)] + [InlineData(31_999_999, 999_999, true)] + [InlineData(31_999_999, 1_000_000, false)] + [InlineData(32_000_000, 1_000_001, true)] + [InlineData(32_000_000, 1_000_002, false)] + [InlineData(32_000_001, 1_000_003, true)] + [InlineData(32_000_001, 1_000_004, false)] + public async Task SendLongData(int dataLength, int chunkLength, bool isAsync) + { + using SingleStoreConnection connection = new SingleStoreConnection(AppConfig.ConnectionString); + connection.Open(); + connection.Execute(""" + drop table if exists insert_mysql_long_data; + create table insert_mysql_long_data(rowid bigint not null primary key auto_increment, value longblob); + """); + + var random = new Random(dataLength); + var data = new byte[dataLength]; + random.NextBytes(data); + + using var chunkStream = new ChunkStream(data, chunkLength); + + using var writeCommand = new SingleStoreCommand(""" + insert into insert_mysql_long_data(value) values(@value); + select length(value) from insert_mysql_long_data order by rowid; + """, connection); + writeCommand.Parameters.AddWithValue("@value", chunkStream); + writeCommand.Prepare(); + using (var reader = isAsync ? await writeCommand.ExecuteReaderAsync().ConfigureAwait(true) : writeCommand.ExecuteReader()) + { + Assert.True(reader.Read()); + Assert.Equal(1, reader.FieldCount); + Assert.Equal(dataLength, reader.GetInt32(0)); + Assert.False(reader.Read()); + } + + using var readCommand = new SingleStoreCommand("select value from insert_mysql_long_data order by rowid;", connection); + using (var reader = readCommand.ExecuteReader()) + { + Assert.True(reader.Read()); + var readData = (byte[]) reader.GetValue(0); + Assert.True(data.AsSpan().SequenceEqual(readData)); // much faster than Assert.Equal + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task SendLongDataMultipleStatements(bool isAsync) + { + using SingleStoreConnection connection = new SingleStoreConnection(AppConfig.ConnectionString); + connection.Open(); + connection.Execute(""" + drop table if exists insert_mysql_long_data; + create table insert_mysql_long_data(rowid bigint not null primary key auto_increment, value longblob); + """); + + var data1 = new byte[1000]; + var data2 = new byte[2000]; + var data3 = new byte[3000]; + var random = new Random(1); + random.NextBytes(data1); + random.NextBytes(data2); + random.NextBytes(data3); + + using var chunkStream1 = new ChunkStream(data1, int.MaxValue); + using var chunkStream2 = new ChunkStream(data2, int.MaxValue); + using var chunkStream3 = new ChunkStream(data3, int.MaxValue); + + using var writeCommand = new SingleStoreCommand(""" + insert into insert_mysql_long_data(rowid, value) values(1, @value1); + insert into insert_mysql_long_data(rowid, value) values(2, @value2); + insert into insert_mysql_long_data(rowid, value) values(3, @value3); + """, connection); + writeCommand.Parameters.AddWithValue("@value1", chunkStream1); + writeCommand.Parameters.AddWithValue("@value2", chunkStream2); + writeCommand.Parameters.AddWithValue("@value3", chunkStream3); + writeCommand.Prepare(); + if (isAsync) + await writeCommand.ExecuteNonQueryAsync(); + else + writeCommand.ExecuteNonQuery(); + + using var readCommand = new SingleStoreCommand("select value from insert_mysql_long_data order by rowid;", connection); + using (var reader = readCommand.ExecuteReader()) + { + Assert.True(reader.Read()); + var readData = (byte[]) reader.GetValue(0); + Assert.True(data1.AsSpan().SequenceEqual(readData)); + + Assert.True(reader.Read()); + readData = (byte[]) reader.GetValue(0); + Assert.True(data2.AsSpan().SequenceEqual(readData)); + + Assert.True(reader.Read()); + readData = (byte[]) reader.GetValue(0); + Assert.True(data3.AsSpan().SequenceEqual(readData)); + + Assert.False(reader.Read()); + } + } #endif [Theory] @@ -460,40 +584,39 @@ public async Task EnumParametersAreParsedCorrectly() { m_database.Connection.Close(); } - } - enum Enum16 : short + private enum Enum16 : short { Off, On, } - enum Enum32 : int + private enum Enum32 : int { Off, On, } - enum Enum64 : long + private enum Enum64 : long { Off, On, } - class DateTimeOffsetValues + private class DateTimeOffsetValues { - public DateTimeOffset? datetimeoffset1 { get; set; } + public DateTimeOffset? DateTimeOffset1 { get; set; } } - class ColorEnumValues + private class ColorEnumValues { public string Varchar { get; set; } public string String { get; set; } public int Int { get; set; } } - class EnumValues + private class EnumValues { public Enum16? Enum16 { get; set; } public Enum32? Enum32 { get; set; } @@ -514,17 +637,17 @@ color enum('red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet') not n Assert.Equal(new[] { "blue" }, m_database.Connection.Query(@"select color from insert_mysql_enums")); } - enum SingleStoreSize + private enum SingleStoreSize { None, XSmall, Small, Medium, Large, - XLarge + XLarge, } - enum SingleStoreColor + private enum SingleStoreColor { None, Red, @@ -533,7 +656,7 @@ enum SingleStoreColor Green, Blue, Indigo, - Violet + Violet, } [Fact] @@ -548,7 +671,6 @@ value set('""one""', '""two""', '""four""', '""eight""') null Assert.Equal(new[] { "\"one\"", "\"one\",\"two\"", "\"one\",\"four\"", "\"one\",\"two\",\"four\"" }, m_database.Connection.Query(@"select value from insert_mysql_set where JSON_ARRAY_CONTAINS_STRING(concat('[', value, ']'), 'one') order by rowid")); } - #if !BASELINE [Theory] [MemberData(nameof(GetBlobs))] @@ -588,5 +710,5 @@ public static IEnumerable GetBlobs() } #endif - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; } diff --git a/tests/SideBySide/LoadDataInfileAsync.cs b/tests/SideBySide/LoadDataInfileAsync.cs index f414e61e9..6c957aa73 100644 --- a/tests/SideBySide/LoadDataInfileAsync.cs +++ b/tests/SideBySide/LoadDataInfileAsync.cs @@ -66,7 +66,7 @@ public async Task ThrowsNotSupportedExceptionForNotTrustedHostAndNotStream() await Assert.ThrowsAsync(async () => await command.ExecuteNonQueryAsync()); } - readonly DatabaseFixture m_database; - readonly string m_testTable; - readonly string m_loadDataInfileCommand; + private readonly DatabaseFixture m_database; + private readonly string m_testTable; + private readonly string m_loadDataInfileCommand; } diff --git a/tests/SideBySide/LoadDataInfileSync.cs b/tests/SideBySide/LoadDataInfileSync.cs index ecdfa87f6..477f9a7db 100644 --- a/tests/SideBySide/LoadDataInfileSync.cs +++ b/tests/SideBySide/LoadDataInfileSync.cs @@ -63,7 +63,7 @@ public void ThrowsNotSupportedExceptionForNotTrustedHostAndNotStream() m_database.Connection.Close(); } - readonly DatabaseFixture m_database; - readonly string m_testTable; - readonly string m_loadDataInfileCommand; + private readonly DatabaseFixture m_database; + private readonly string m_testTable; + private readonly string m_loadDataInfileCommand; } diff --git a/tests/SideBySide/ParameterCollection.cs b/tests/SideBySide/ParameterCollection.cs index fce278e00..26266a5bc 100644 --- a/tests/SideBySide/ParameterCollection.cs +++ b/tests/SideBySide/ParameterCollection.cs @@ -299,6 +299,6 @@ public void SetTwoParametersToSameNAme() #endif } - readonly SingleStoreCommand m_command; - readonly SingleStoreParameterCollection m_parameterCollection; + private readonly SingleStoreCommand m_command; + private readonly SingleStoreParameterCollection m_parameterCollection; } diff --git a/tests/SideBySide/QueryTests.cs b/tests/SideBySide/QueryTests.cs index d2a74a562..530a77270 100644 --- a/tests/SideBySide/QueryTests.cs +++ b/tests/SideBySide/QueryTests.cs @@ -1,3 +1,4 @@ +using System.Runtime.InteropServices; using SingleStoreConnector.Protocol; namespace SideBySide; @@ -1387,6 +1388,7 @@ END AS value Assert.True(await reader.NextResultAsync()); Assert.True(await reader.ReadAsync()); + // MySQL returns ulong, MariaDB returns decimal; GetBoolean will coerce both Assert.True(reader.GetBoolean(0)); Assert.False(await reader.ReadAsync()); @@ -1413,7 +1415,7 @@ public void GetIntForTinyInt1(bool prepare) using var reader = command.ExecuteReader(); - int[] expected = {-128, -1, 0, 1, 2, 127}; + int[] expected = { -128, -1, 0, 1, 2, 127 }; for (int i = 0; i < expected.Length; i++) { @@ -1663,7 +1665,7 @@ public void GetBytesByName() [Fact] public void ServerDoesNotSendMariaDbCacheMetadataOrQueryAttributes() { - using var connection = new SingleStoreConnection(AppConfig.ConnectionString); + using var connection = new SingleStoreConnection(AppConfig.ConnectionString); connection.Open(); var serverCapabilities = connection.Session.ServerCapabilities; @@ -1676,13 +1678,75 @@ public void ServerDoesNotSendMariaDbCacheMetadataOrQueryAttributes() "Server should not send QueryAttributes capability flag."); } - class BoolTest + [SkippableTheory(ServerFeatures.Vector)] + [InlineData(false, 0)] + [InlineData(false, 1)] + [InlineData(false, 2)] + [InlineData(true, 0)] + [InlineData(true, 1)] + [InlineData(true, 2)] + public void QueryVector(bool prepare, int dataFormat) + { + using var connection = new SingleStoreConnection(AppConfig.ConnectionString); + connection.Open(); + + connection.Execute(""" + drop table if exists test_vector; + create table test_vector(id bigint auto_increment not null primary key, vec vector(3) not null); + """); + + using var cmd = m_database.Connection.CreateCommand(); + cmd.CommandText = "INSERT INTO test_vector(vec) VALUES(@vec)"; + cmd.Parameters.Add(new SingleStoreParameter + { + ParameterName = "@vec", + SingleStoreDbType = SingleStoreDbType.Vector, + }); + + var floatArray = new[] { 1.2f, 3.4f, 5.6f }; +#if MYSQL_DATA + // Connector/NET requires the float vector to be passed as a byte array + cmd.Parameters[0].Value = MemoryMarshal.AsBytes(floatArray).ToArray(); + Assert.InRange(dataFormat, 0, 2); +#else + cmd.Parameters[0].Value = dataFormat switch + { + 0 => floatArray, + 1 => new Memory(floatArray), + 2 => new ReadOnlyMemory(floatArray), + _ => throw new NotSupportedException(), + }; +#endif + + if (prepare) + cmd.Prepare(); + cmd.ExecuteNonQuery(); + + // Select and verify the value + cmd.CommandText = "SELECT vec FROM test_vector"; + if (prepare) + cmd.Prepare(); + + using var reader = cmd.ExecuteReader(); + Assert.True(reader.Read()); + var value = reader.GetValue(0); + +#if MYSQL_DATA + var result = MemoryMarshal.Cast((byte[]) value).ToArray(); +#else + var result = AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType) ? (ReadOnlyMemory) value : + MemoryMarshal.Cast((byte[]) value).ToArray(); +#endif + Assert.Equal(floatArray, result); + } + + private class BoolTest { public int Id { get; set; } public bool? IsBold { get; set; } } - class UseReaderWithoutDisposingThreadData + private class UseReaderWithoutDisposingThreadData { public UseReaderWithoutDisposingThreadData(List exceptions, SingleStoreConnectionStringBuilder csb) { @@ -1695,10 +1759,10 @@ public UseReaderWithoutDisposingThreadData(List exceptions, SingleSto public SingleStoreConnectionStringBuilder ConnectionStringBuilder { get; } } - enum TestLongEnum : long + private enum TestLongEnum : long { Value = long.MaxValue, } - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; } diff --git a/tests/SideBySide/RedirectionTests.cs b/tests/SideBySide/RedirectionTests.cs index 98da055d2..676a56ef0 100644 --- a/tests/SideBySide/RedirectionTests.cs +++ b/tests/SideBySide/RedirectionTests.cs @@ -79,31 +79,25 @@ public void RedirectionTest() Assert.Equal(proxy.ListenPort, db.SessionEndPoint!.Port); db.Close(); } - - } finally{ - m_database.Connection.Execute( - $"set @@global.redirect_url=\"\""); + } + finally + { + m_database.Connection.Execute($"set @@global.redirect_url=\"\""); } SingleStoreConnection.ClearAllPools(); + // ensure that when required, throwing error if no redirection csb.ServerRedirectionMode = SingleStoreServerRedirectionMode.Required; using (var db = new SingleStoreConnection(csb.ConnectionString)) { - try - { - db.Open(); - Assert.Fail("must have thrown error"); - } - catch (SingleStoreException ex) - { - Assert.Equal((int) SingleStoreErrorCode.UnableToConnectToHost, ex.Number); - } + var exception = Assert.Throws(db.Open); + Assert.Equal(SingleStoreErrorCode.UnableToConnectToHost, exception.ErrorCode); } StopProxy(); } - protected void StartProxy() + protected void StartProxy() { var csb = AppConfig.CreateConnectionStringBuilder(); proxy = new ServerConfiguration( csb.Server, (int)csb.Port ); @@ -111,36 +105,38 @@ protected void StartProxy() serverThread.Start( proxy ); } - protected void StopProxy() + protected void StopProxy() { proxy.RunServer = false; proxy.ServerSocket.Close(); } - private class ServerConfiguration { - - public IPAddress RemoteAddress; - public int RemotePort; - public int ListenPort; - public Socket ServerSocket; - public ServerConfiguration(String remoteAddress, int remotePort) { + private class ServerConfiguration + { + public IPAddress RemoteAddress { get; set; } + public int RemotePort { get; set; } + public int ListenPort { get; set; } + public Socket ServerSocket { get; set; } + public ServerConfiguration(string remoteAddress, int remotePort) { var ipHostEntry = Dns.GetHostEntry(remoteAddress); RemoteAddress = ipHostEntry.AddressList[0]; - RemotePort = remotePort; - ListenPort = 0; + RemotePort = remotePort; + ListenPort = 0; } - public bool RunServer = true; + public bool RunServer { get; set; } = true; } - private static void ServerThread(Object configObj) { - ServerConfiguration config = (ServerConfiguration)configObj; - Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - - serverSocket.Bind( new IPEndPoint( IPAddress.Any, 0 ) ); - serverSocket.Listen(1); - config.ListenPort = ((IPEndPoint) serverSocket.LocalEndPoint).Port; - config.ServerSocket = serverSocket; - while( config.RunServer ) { + private static void ServerThread(object configObj) + { + ServerConfiguration config = (ServerConfiguration)configObj; + Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + + serverSocket.Bind( new IPEndPoint( IPAddress.Any, 0 ) ); + serverSocket.Listen(1); + config.ListenPort = ((IPEndPoint) serverSocket.LocalEndPoint).Port; + config.ServerSocket = serverSocket; + while (config.RunServer) + { try { Socket client = serverSocket.Accept(); @@ -151,43 +147,50 @@ private static void ServerThread(Object configObj) { { return; } - } - } + } + } - private class ClientContext { - public ServerConfiguration Config; - public Socket Client; - } + private class ClientContext + { + public ServerConfiguration Config { get; set; } + public Socket Client { get; set; } + } - private static void ClientThread(Object contextObj) { - ClientContext context = (ClientContext)contextObj; - Socket client = context.Client; - ServerConfiguration config = context.Config; - IPEndPoint remoteEndPoint = new IPEndPoint( config.RemoteAddress, config.RemotePort ); - Socket remote = new Socket( remoteEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - remote.Connect( remoteEndPoint ); - Byte[] buffer = new Byte[4096]; - for(;;) { - if (!config.RunServer) - { - remote.Close(); - client.Close(); - return; - } - if( client.Available > 0 ) { - var count = client.Receive( buffer ); - if( count == 0 ) return; - remote.Send( buffer, count, SocketFlags.None ); - } - if( remote.Available > 0 ) { - var count = remote.Receive( buffer ); - if( count == 0 ) return; - client.Send( buffer, count, SocketFlags.None ); - } - } - } + private static void ClientThread(object contextObj) + { + ClientContext context = (ClientContext) contextObj; + Socket client = context.Client; + ServerConfiguration config = context.Config; + IPEndPoint remoteEndPoint = new IPEndPoint(config.RemoteAddress, config.RemotePort); + Socket remote = new Socket(remoteEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + remote.Connect(remoteEndPoint); + var buffer = new byte[4096]; + while (true) + { + if (!config.RunServer) + { + remote.Close(); + client.Close(); + return; + } + if (client.Available > 0) + { + var count = client.Receive(buffer); + if (count == 0) + return; + remote.Send(buffer, count, SocketFlags.None); + } + if (remote.Available > 0) + { + var count = remote.Receive(buffer); + if (count == 0) + return; + client.Send(buffer, count, SocketFlags.None); + } + } + } - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; private ServerConfiguration proxy; } #endif diff --git a/tests/SideBySide/SchemaProviderTests.cs b/tests/SideBySide/SchemaProviderTests.cs index ff7976ae2..431c031b3 100644 --- a/tests/SideBySide/SchemaProviderTests.cs +++ b/tests/SideBySide/SchemaProviderTests.cs @@ -121,10 +121,10 @@ public void MetaDataCollectionsRestriction() => [InlineData("datatypes", "DataTypes")] [InlineData("Indexes")] [InlineData("IndexColumns")] - // only in 8.0 - [InlineData("KeyWords")] + //// only in 8.0 - [InlineData("KeyWords")] [InlineData("MetaDataCollections")] [InlineData("Procedures")] - // only in 8.0 - [InlineData("ResourceGroups")] + //// only in 8.0 - [InlineData("ResourceGroups")] [InlineData("Tables")] [InlineData("Triggers")] [InlineData("Views")] @@ -237,5 +237,5 @@ public void IndexColumnsWithColumnName() Assert.Equal(expected, actual); } - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; } diff --git a/tests/SideBySide/ServerFeatures.cs b/tests/SideBySide/ServerFeatures.cs index 9a3dab829..08809b522 100644 --- a/tests/SideBySide/ServerFeatures.cs +++ b/tests/SideBySide/ServerFeatures.cs @@ -40,4 +40,19 @@ public enum ServerFeatures /// Server provides hash of TLS certificate in first OK packet. /// TlsFingerprintValidation = 0x100_0000, + + /// + /// Server supports the 'parsec' authentication plugin. + /// + ParsecAuthentication = 0x200_0000, + + /// + /// Server supports the VECTOR SQL type. + /// + Vector = 0x400_0000, + + /// + /// Server has a dedicated type on the wire for VECTOR. + /// + VectorType = 0x800_0000, } diff --git a/tests/SideBySide/SingleStoreDataSourceTests.cs b/tests/SideBySide/SingleStoreDataSourceTests.cs index c53c9d0cd..b7d751d2f 100644 --- a/tests/SideBySide/SingleStoreDataSourceTests.cs +++ b/tests/SideBySide/SingleStoreDataSourceTests.cs @@ -4,7 +4,7 @@ namespace SideBySide; public class SingleStoreDataSourceTests : IClassFixture { - public SingleStoreDataSourceTests(DatabaseFixture _) + public SingleStoreDataSourceTests(DatabaseFixture ignoredFixture) { } diff --git a/tests/SideBySide/SslTests.cs b/tests/SideBySide/SslTests.cs index a2034e349..ac721521b 100644 --- a/tests/SideBySide/SslTests.cs +++ b/tests/SideBySide/SslTests.cs @@ -259,6 +259,20 @@ public async Task ConnectZeroConfigurationSslEd25519() using var connection = new SingleStoreConnection(csb.ConnectionString); await connection.OpenAsync(); } + + [SkippableFact(ServerFeatures.TlsFingerprintValidation | ServerFeatures.ParsecAuthentication)] + public async Task ConnectZeroConfigurationSslParsec() + { + SingleStoreConnector.Authentication.Ed25519.ParsecAuthenticationPlugin.Install(); + var csb = AppConfig.CreateConnectionStringBuilder(); + csb.CertificateFile = null; + csb.SslMode = SingleStoreSslMode.VerifyFull; + csb.SslCa = ""; + csb.UserID = "parsec-user"; + csb.Password = "P@rs3c-Pa55"; + using var connection = new SingleStoreConnection(csb.ConnectionString); + await connection.OpenAsync(); + } #endif [SkippableFact(ConfigSettings.RequiresSsl)] @@ -316,5 +330,5 @@ public async Task ForceTls11() } #endif - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; } diff --git a/tests/SideBySide/StoredProcedureTests.cs b/tests/SideBySide/StoredProcedureTests.cs index 6ef5cfac1..25d65e31b 100644 --- a/tests/SideBySide/StoredProcedureTests.cs +++ b/tests/SideBySide/StoredProcedureTests.cs @@ -1,3 +1,5 @@ +using System.Runtime.InteropServices; + namespace SideBySide; public class StoredProcedureTests : IClassFixture @@ -88,7 +90,7 @@ public void CallFailingFunctionInTransaction() } [SkippableTheory(ServerFeatures.StoredProcedures)] - // [InlineData("FUNCTION", false)] see PLAT-6053 + //// [InlineData("FUNCTION", false)] see PLAT-6053 [InlineData("PROCEDURE", true)] [InlineData("PROCEDURE", false)] public async Task StoredProcedureEchoException(string procedureType, bool prepare) @@ -207,8 +209,8 @@ public async Task StoredProcedureReturnsNull(bool prepare) var reader = (SingleStoreDataReader) await cmd.ExecuteReaderAsync(); var result = await reader.ReadAsync(); Assert.True(result); - Assert.Equal(DBNull.Value, reader.GetValue(0)); - Assert.Equal(DBNull.Value, reader.GetValue(1)); + Assert.Equal(DBNull.Value, reader.GetValue(0)); + Assert.Equal(DBNull.Value, reader.GetValue(1)); } [Theory] @@ -304,12 +306,20 @@ private async Task CircleAssertions(DbCommand cmd, string executorType) { var reader = (SingleStoreDataReader) await cmd.ExecuteReaderAsync(); var result = await reader.ReadAsync(); + Assert.True(result); - Assert.Equal(2 * (double) cmd.Parameters["@radius"].Value, reader.GetDouble("diameter")); - Assert.Equal(2.0 * Math.PI * (double) cmd.Parameters["@radius"].Value, reader.GetDouble("circumference")); - Assert.Equal(Math.PI * Math.Pow((double) cmd.Parameters["@radius"].Value, 2), reader.GetDouble("area")); - Assert.Equal(reader.GetDouble("area") * (double) cmd.Parameters["@height"].Value, reader.GetDouble("volume")); - } else { + + var radius = (double)cmd.Parameters["@radius"].Value; + var height = (double)cmd.Parameters["@height"].Value; + var area = reader.GetDouble("area"); + + Assert.Equal(2 * radius, reader.GetDouble("diameter")); + Assert.Equal(2.0 * Math.PI * radius, reader.GetDouble("circumference")); + Assert.Equal(Math.PI * Math.Pow(radius, 2), area); + Assert.Equal(area * height, reader.GetDouble("volume")); + } + else + { var result = await ExecuteCommandAsync(cmd, executorType); if (executorType != "NonQuery") Assert.Equal((string) cmd.Parameters["@name"].Value + "circle", result); @@ -409,7 +419,7 @@ public async Task ParameterLoop(bool prepare) ParameterName = "high", DbType = DbType.Int32, Direction = ParameterDirection.Input, - Value = 1 + Value = 1, }; while ((int) parameter.Value < 8) { @@ -546,11 +556,11 @@ public void DeriveParametersDoesNotExistThenIsCreated() #endif [InlineData("char(30)", 30)] [InlineData("varchar(50)", 50)] - // These return nonzero sizes for some versions of MySQL Server 8.0 - // [InlineData("bit", 0)] - // [InlineData("tinyint", 0)] - // [InlineData("bigint", 0)] - // [InlineData("bigint unsigned", 0)] + //// These return nonzero sizes for some versions of MySQL Server 8.0 + //// [InlineData("bit", 0)] + //// [InlineData("tinyint", 0)] + //// [InlineData("bigint", 0)] + //// [InlineData("bigint unsigned", 0)] public void DeriveParametersParameterSize(string parameterType, int expectedSize) { var csb = AppConfig.CreateConnectionStringBuilder(); @@ -575,7 +585,7 @@ public void DeriveParametersParameterSize(string parameterType, int expectedSize [InlineData("bit(1)", SingleStoreDbType.Bit)] #if BASELINE [InlineData("bool", SingleStoreDbType.Byte)] - [InlineData("tinyint(1)", MySqlDbType.Byte)] + [InlineData("tinyint(1)", SingleStoreDbType.Byte)] #else [InlineData("bool", SingleStoreDbType.Bool)] [InlineData("tinyint(1)", SingleStoreDbType.Bool)] @@ -631,6 +641,39 @@ public void PassJsonParameter() Assert.False(reader.Read()); } + [SkippableTheory(ServerFeatures.Vector | ServerFeatures.VectorType)] + [InlineData(false)] + [InlineData(true)] + public void VectorOutputParameter(bool prepare) + { + using var cmd = m_database.Connection.CreateCommand(); + cmd.CommandText = """ + DROP PROCEDURE IF EXISTS sp_vector_out; + CREATE PROCEDURE sp_vector_out (OUT vec VECTOR) + BEGIN + SELECT STRING_TO_VECTOR('[1.2, 3.4, 5.6]') INTO vec; + END; + """; + cmd.ExecuteNonQuery(); + + cmd.CommandText = "sp_vector_out"; + cmd.CommandType = CommandType.StoredProcedure; + cmd.Parameters.Add(new SingleStoreParameter + { + Direction = ParameterDirection.Output, + SingleStoreDbType = SingleStoreDbType.Vector, + ParameterName = "@vec", + }); + + if (prepare) + cmd.Prepare(); + cmd.ExecuteNonQuery(); + + var value = cmd.Parameters[0].Value; + var result = Assert.IsType(value); + Assert.Equal(new float[] { 1.2f, 3.4f, 5.6f }, MemoryMarshal.Cast(result).ToArray()); + } + private static Action AssertParameter(string name, ParameterDirection direction, SingleStoreDbType mySqlDbType) { return x => @@ -737,6 +780,60 @@ public void SprocNameSpecialCharacters(string sprocName) } } +#if !BASELINE + [Theory] + [InlineData(SingleStoreGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", false, false)] + [InlineData(SingleStoreGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", false, true)] + [InlineData(SingleStoreGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", true, false)] + [InlineData(SingleStoreGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", true, true)] + [InlineData(SingleStoreGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", false, false)] + [InlineData(SingleStoreGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", false, true)] + [InlineData(SingleStoreGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", true, false)] + [InlineData(SingleStoreGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", true, true)] + [InlineData(SingleStoreGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", false, false)] + [InlineData(SingleStoreGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", false, true)] + [InlineData(SingleStoreGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", true, false)] + [InlineData(SingleStoreGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", true, true)] + public void StoredProcedureReturnsGuid(SingleStoreGuidFormat guidFormat, string columnDefinition, string columnValue, bool setMySqlDbType, bool prepare) + { + var csb = AppConfig.CreateConnectionStringBuilder(); + csb.GuidFormat = guidFormat; + csb.Pooling = false; + using var connection = new SingleStoreConnection(csb.ConnectionString); + connection.Open(); + + using (var command = new SingleStoreCommand($""" + DROP TABLE IF EXISTS out_guid_table; + CREATE TABLE out_guid_table (id INT PRIMARY KEY AUTO_INCREMENT, guid {columnDefinition}); + INSERT INTO out_guid_table (guid) VALUES ({columnValue}); + DROP PROCEDURE IF EXISTS out_guid; + CREATE PROCEDURE out_guid + ( + OUT out_name {columnDefinition} + ) + BEGIN + SELECT guid INTO out_name FROM out_guid_table; + END; + """, connection)) + { + command.ExecuteNonQuery(); + } + + using (var command = new SingleStoreCommand("out_guid", connection)) + { + command.CommandType = CommandType.StoredProcedure; + var param = new SingleStoreParameter("out_name", null) { Direction = ParameterDirection.Output }; + if (setMySqlDbType) + param.SingleStoreDbType = SingleStoreDbType.Guid; + command.Parameters.Add(param); + command.ExecuteNonQuery(); + if (prepare) + command.Prepare(); + Assert.Equal(new Guid("BABD8384C908499C9D95C02ADA94A970"), param.Value); + } + } +#endif + private static string NormalizeSpaces(string input) { input = input.Replace('\r', ' '); @@ -758,5 +855,5 @@ private static SingleStoreConnection CreateOpenConnection() return connection; } - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; } diff --git a/tests/SideBySide/TestUtilities.cs b/tests/SideBySide/TestUtilities.cs index db828c2fc..ee92b7783 100644 --- a/tests/SideBySide/TestUtilities.cs +++ b/tests/SideBySide/TestUtilities.cs @@ -18,7 +18,7 @@ public static void AssertEqual(byte[] expected, byte[] actual) } /// - /// Verifies that is an integer ( or ) with the value 1. + /// Verifies that is an integer ( or ) with the value 1. /// public static void AssertIsOne(object value) { @@ -80,7 +80,7 @@ private static async Task AssertExecuteScalarReturnsOneOrThrowsExceptionAsync(Si else { var ex = await Assert.ThrowsAnyAsync(async () => await command.ExecuteScalarAsync(token)); - var exception = ex as SingleStoreException; + SingleStoreException exception = ex as SingleStoreException; while (exception is null && ex is not null) { ex = ex.InnerException; diff --git a/tests/SideBySide/Transaction.cs b/tests/SideBySide/Transaction.cs index af458b1dd..868b6fd4d 100644 --- a/tests/SideBySide/Transaction.cs +++ b/tests/SideBySide/Transaction.cs @@ -377,6 +377,6 @@ public async Task DisposeAsync() } #endif - readonly TransactionFixture m_database; - readonly SingleStoreConnection m_connection; + private readonly TransactionFixture m_database; + private readonly SingleStoreConnection m_connection; } diff --git a/tests/SideBySide/TransactionScopeTests.cs b/tests/SideBySide/TransactionScopeTests.cs index 991f3d89f..daf085497 100644 --- a/tests/SideBySide/TransactionScopeTests.cs +++ b/tests/SideBySide/TransactionScopeTests.cs @@ -9,13 +9,13 @@ public TransactionScopeTests(DatabaseFixture database) m_database = database; } - public static IEnumerable ConnectionStrings = new[] + public static IEnumerable ConnectionStrings { get; } = new[] { #if BASELINE new object[] { "" }, #else new object[] { "UseXaTransactions=False" }, - // new object[] { "UseXaTransactions=True" }, no XA transactions in SingleStore + //// new object[] { "UseXaTransactions=True" }, no XA transactions in SingleStore #endif }; @@ -274,7 +274,7 @@ public void UsingSequentialConnectionsInOneTransactionDoesNotDeadlock(string con var transactionOptions = new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted, - Timeout = TransactionManager.MaximumTimeout + Timeout = TransactionManager.MaximumTimeout, }; using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) { @@ -318,7 +318,7 @@ public void UsingSequentialConnectionsInOneTransactionDoesNotDeadlockWithoutComp var transactionOptions = new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted, - Timeout = TransactionManager.MaximumTimeout + Timeout = TransactionManager.MaximumTimeout, }; using (new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) { @@ -370,7 +370,7 @@ public void UsingSequentialConnectionsInOneTransactionWithoutAutoEnlistDoesNotDe { connection.Open(); connection.EnlistTransaction(transaction); - connection.Execute("update transaction_scope_test set value = @newValue where rowid = @id", new { newValue = "new value", id = command.LastInsertedId}); + connection.Execute("update transaction_scope_test set value = @newValue where rowid = @id", new { newValue = "new value", id = command.LastInsertedId }); } transaction.Commit(); @@ -458,7 +458,7 @@ public void ReusingConnectionInOneTransactionDoesNotDeadlock(string connectionSt var transactionOptions = new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted, - Timeout = TransactionManager.MaximumTimeout + Timeout = TransactionManager.MaximumTimeout, }; using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) { @@ -499,7 +499,7 @@ public void ReusingConnectionInOneTransactionDoesNotDeadlockWithoutComplete(stri var transactionOptions = new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted, - Timeout = TransactionManager.MaximumTimeout + Timeout = TransactionManager.MaximumTimeout, }; using (new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) { @@ -881,6 +881,6 @@ public void ConnectionOpenedCallbackAutoEnlistInTransaction() } #endif - DatabaseFixture m_database; + private DatabaseFixture m_database; } diff --git a/tests/SideBySide/UpdateTests.cs b/tests/SideBySide/UpdateTests.cs index 07f7173f4..f46fa4d96 100644 --- a/tests/SideBySide/UpdateTests.cs +++ b/tests/SideBySide/UpdateTests.cs @@ -161,5 +161,5 @@ public void UpdateFieldCount() } } - readonly DatabaseFixture m_database; + private readonly DatabaseFixture m_database; } diff --git a/tests/SingleStoreConnector.DependencyInjection.Tests/DependencyInjectionTests.cs b/tests/SingleStoreConnector.DependencyInjection.Tests/DependencyInjectionTests.cs index 8ea85f4f5..8c0cffaf7 100644 --- a/tests/SingleStoreConnector.DependencyInjection.Tests/DependencyInjectionTests.cs +++ b/tests/SingleStoreConnector.DependencyInjection.Tests/DependencyInjectionTests.cs @@ -215,5 +215,5 @@ public async Task KeyedDbDataSourceIsRegistered() Assert.Equal(c_connectionString, connection.ConnectionString); } - const string c_connectionString = "Server=localhost;User ID=root;Password=pass"; + private const string c_connectionString = "Server=localhost;User ID=root;Password=pass"; } diff --git a/tests/SingleStoreConnector.Tests/ByteBufferWriterTests.cs b/tests/SingleStoreConnector.Tests/ByteBufferWriterTests.cs index 0a7ca1c76..d30590e76 100644 --- a/tests/SingleStoreConnector.Tests/ByteBufferWriterTests.cs +++ b/tests/SingleStoreConnector.Tests/ByteBufferWriterTests.cs @@ -22,6 +22,6 @@ public void WriteString(int length) var input = char.ConvertFromUtf32(0xE001); for (var i = 0; i < length; i++) writer.Write(input); - Assert.Equal(expected, writer.ArraySegment); + Assert.Equal(expected, writer.ArraySegment.ToArray()); } } diff --git a/tests/SingleStoreConnector.Tests/CachedProcedureTests.cs b/tests/SingleStoreConnector.Tests/CachedProcedureTests.cs index 01e46ea6a..1430442dd 100644 --- a/tests/SingleStoreConnector.Tests/CachedProcedureTests.cs +++ b/tests/SingleStoreConnector.Tests/CachedProcedureTests.cs @@ -4,9 +4,9 @@ public class CachedProcedureTests { [Theory] [MemberData(nameof(CreateParseableParameters))] - public void ParseParameters(string sql, object[] expected) + public void ParseParameters(string sql, SingleStoreGuidFormat guidFormat, object[] expected) { - var actual = CachedProcedure.ParseParameters(sql); + var actual = CachedProcedure.ParseParameters(sql, guidFormat); Assert.Equal(expected.Length, actual.Count); for (int i = 0; i < expected.Length; i++) { @@ -25,106 +25,118 @@ public static IEnumerable CreateParseableParameters() { new object[] { - "", new object[0], + "", SingleStoreGuidFormat.Binary16, new object[0], }, [ - "/* no, parameters */", new object[0], + "/* no, parameters */", SingleStoreGuidFormat.Binary16, new object[0], ], [ - "IN test INT", new object[] + "IN test INT", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "test", "INT", false, 0), + new CachedParameter(1, "IN", "test", "INT", false, 0, SingleStoreGuidFormat.Binary16), } ], [ - "IN test INT UNSIGNED", new object[] + "IN test INT UNSIGNED", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "test", "INT", true, 0), + new CachedParameter(1, "IN", "test", "INT", true, 0, SingleStoreGuidFormat.Binary16), } ], [ - "-- IN ignored INT UNSIGNED,\r\nIN notignored INT", new object[] + "-- IN ignored INT UNSIGNED,\r\nIN notignored INT", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "notignored", "INT", false, 0), + new CachedParameter(1, "IN", "notignored", "INT", false, 0, SingleStoreGuidFormat.Binary16), } ], [ - "IN param1 INT,\r\nIN param2 INT", new object[] + "IN param1 INT,\r\nIN param2 INT", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "INT", false, 0), - new CachedParameter(2, "IN", "param2", "INT", false, 0), + new CachedParameter(1, "IN", "param1", "INT", false, 0, SingleStoreGuidFormat.Binary16), + new CachedParameter(2, "IN", "param2", "INT", false, 0, SingleStoreGuidFormat.Binary16), } ], [ - "IN /* ignored BIGINT,\r\nIN*/ param1 INT", new object[] + "IN /* ignored BIGINT,\r\nIN*/ param1 INT", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "INT", false, 0), + new CachedParameter(1, "IN", "param1", "INT", false, 0, SingleStoreGuidFormat.Binary16), } ], [ - "IN param1 INT(11)", new object[] + "IN param1 INT(11)", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "INT", false, 11), + new CachedParameter(1, "IN", "param1", "INT", false, 11, SingleStoreGuidFormat.Binary16), } ], [ - "param1 BIGINT(21) UNSIGNED ZEROFILL", new object[] + "param1 BIGINT(21) UNSIGNED ZEROFILL", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "BIGINT", true, 21), + new CachedParameter(1, "IN", "param1", "BIGINT", true, 21, SingleStoreGuidFormat.Binary16), } ], [ - "param1 VARCHAR(63)", new object[] + "param1 VARCHAR(63)", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63), + new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, SingleStoreGuidFormat.Binary16), } ], [ - "param1 VARCHAR(63) CHARSET latin1", new object[] + "param1 VARCHAR(63) CHARSET latin1", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63), + new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, SingleStoreGuidFormat.Binary16), } ], [ - "param1 VARCHAR(63) COLLATE utf8bin", new object[] + "param1 VARCHAR(63) COLLATE utf8bin", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63), + new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, SingleStoreGuidFormat.Binary16), } ], [ - "param1 VARCHAR(63) CHARACTER SET latin1 COLLATE latin1_bin", new object[] + "param1 VARCHAR(63) CHARACTER SET latin1 COLLATE latin1_bin", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63), + new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, SingleStoreGuidFormat.Binary16), } ], [ - "`par``am` INT", new object[] + "`par``am` INT", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "par`am", "INT", false, 0), + new CachedParameter(1, "IN", "par`am", "INT", false, 0, SingleStoreGuidFormat.Binary16), } ], [ - "IN input enum ('One', 'Two', 'Three')", new object[] + "IN input enum ('One', 'Two', 'Three')", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "input", "ENUM", false, 0), + new CachedParameter(1, "IN", "input", "ENUM", false, 0, SingleStoreGuidFormat.Binary16), } ], [ - "OUT param DECIMAL(10,5)", new object[] + "OUT param DECIMAL(10,5)", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "OUT", "param", "DECIMAL", false, 0), + new CachedParameter(1, "OUT", "param", "DECIMAL", false, 0, SingleStoreGuidFormat.Binary16), } ], [ - "INOUT param LONGTEXT", new object[] + "INOUT param LONGTEXT", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "INOUT", "param", "LONGTEXT", false, 0), + new CachedParameter(1, "INOUT", "param", "LONGTEXT", false, 0, SingleStoreGuidFormat.Binary16), } ], [ - "ColSet set('set1','set2','set3')", new object[] + "OUT param1 BINARY(16)", SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "ColSet", "SET", false, 0), + new CachedParameter(1, "OUT", "param1", "BINARY", false, 16, SingleStoreGuidFormat.Binary16), + } + ], + [ + "OUT param1 CHAR(36)", SingleStoreGuidFormat.Char36, new object[] + { + new CachedParameter(1, "OUT", "param1", "CHAR", false, 36, SingleStoreGuidFormat.Char36), + } + ], + [ + "ColSet set('set1','set2','set3')", SingleStoreGuidFormat.Binary16, new object[] + { + new CachedParameter(1, "IN", "ColSet", "SET", false, 0, SingleStoreGuidFormat.Binary16), } ], [ @@ -135,14 +147,14 @@ param3 DECIMAL(20,10), inout param4 VARCHAR(63) CHARSET latin1, param5 bigint(20) unsigned zerofill, out param6 bool", - new object[] + SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "DATETIME", false, 6), - new CachedParameter(2, "OUT", "param2", "INT", false, 0), - new CachedParameter(3, "IN", "param3", "DECIMAL", false, 0), - new CachedParameter(4, "INOUT", "param4", "VARCHAR", false, 63), - new CachedParameter(5, "IN", "param5", "BIGINT", true, 20), - new CachedParameter(6, "OUT", "param6", "TINYINT", false, 1), + new CachedParameter(1, "IN", "param1", "DATETIME", false, 6, SingleStoreGuidFormat.Binary16), + new CachedParameter(2, "OUT", "param2", "INT", false, 0, SingleStoreGuidFormat.Binary16), + new CachedParameter(3, "IN", "param3", "DECIMAL", false, 0, SingleStoreGuidFormat.Binary16), + new CachedParameter(4, "INOUT", "param4", "VARCHAR", false, 63, SingleStoreGuidFormat.Binary16), + new CachedParameter(5, "IN", "param5", "BIGINT", true, 20, SingleStoreGuidFormat.Binary16), + new CachedParameter(6, "OUT", "param6", "TINYINT", false, 1, SingleStoreGuidFormat.Binary16), } ], [ @@ -153,12 +165,12 @@ param3 real(20,10), -- ignored INT param4 INTEGER(3) ", - new object[] + SingleStoreGuidFormat.Binary16, new object[] { - new CachedParameter(1, "IN", "param1", "TINYINT", false, 1), - new CachedParameter(2, "IN", "param2", "VARCHAR", false, 0), - new CachedParameter(3, "IN", "param3", "DOUBLE", false, 20), - new CachedParameter(4, "IN", "param4", "INT", false, 3), + new CachedParameter(1, "IN", "param1", "TINYINT", false, 1, SingleStoreGuidFormat.Binary16), + new CachedParameter(2, "IN", "param2", "VARCHAR", false, 0, SingleStoreGuidFormat.Binary16), + new CachedParameter(3, "IN", "param3", "DOUBLE", false, 20, SingleStoreGuidFormat.Binary16), + new CachedParameter(4, "IN", "param4", "INT", false, 3, SingleStoreGuidFormat.Binary16), } ], }; diff --git a/tests/SingleStoreConnector.Tests/CancellationTests.cs b/tests/SingleStoreConnector.Tests/CancellationTests.cs index 340210a24..a2ff220d4 100644 --- a/tests/SingleStoreConnector.Tests/CancellationTests.cs +++ b/tests/SingleStoreConnector.Tests/CancellationTests.cs @@ -18,7 +18,7 @@ public CancellationTests() public void Dispose() => m_server.Stop(); - // NOTE: Multiple nested classes in order to force tests to run in parallel against each other + //// NOTE: Multiple nested classes in order to force tests to run in parallel against each other public class CancelWithCommandTimeout : CancellationTests { @@ -329,7 +329,7 @@ public void Test(int step, int method) Assert.Null(ex.InnerException); // connection is unusable - Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Equal(ConnectionState.Broken, connection.State); } } @@ -351,7 +351,7 @@ public async Task Test(int step, int method) Assert.IsType(ex.InnerException); // connection is unusable - Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Equal(ConnectionState.Broken, connection.State); } } @@ -374,7 +374,7 @@ public void Execute(int step, int method) Assert.Null(ex.InnerException); // connection is unusable - Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Equal(ConnectionState.Broken, connection.State); } [SkipCITheory] @@ -394,7 +394,7 @@ public async Task ExecuteAsync(int step, int method) Assert.IsType(ex.InnerException); // connection is unusable - Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Equal(ConnectionState.Broken, connection.State); } } @@ -425,8 +425,19 @@ public static IEnumerable GetAsyncMethodSteps() private static int ExecuteScalar(SingleStoreCommand command) => (int) command.ExecuteScalar(); private static async Task ExecuteScalarAsync(SingleStoreCommand command, CancellationToken token) => (int) await command.ExecuteScalarAsync(token); - private static int ExecuteNonQuery(SingleStoreCommand command) { command.ExecuteNonQuery(); return 0; } - private static async Task ExecuteNonQueryAsync(SingleStoreCommand command, CancellationToken token) { await command.ExecuteNonQueryAsync(token); return 0; } + + private static int ExecuteNonQuery(SingleStoreCommand command) + { + command.ExecuteNonQuery(); + return 0; + } + + private static async Task ExecuteNonQueryAsync(SingleStoreCommand command, CancellationToken token) + { + await command.ExecuteNonQueryAsync(token); + return 0; + } + private static int ExecuteReader(SingleStoreCommand command) { using var reader = command.ExecuteReader(); @@ -450,6 +461,6 @@ private static async Task ExecuteReaderAsync(SingleStoreCommand command, Ca return value.Value; } - readonly FakeSingleStoreServer m_server; - readonly SingleStoreConnectionStringBuilder m_csb; + private readonly FakeSingleStoreServer m_server; + private readonly SingleStoreConnectionStringBuilder m_csb; } diff --git a/tests/SingleStoreConnector.Tests/ConnectionTests.cs b/tests/SingleStoreConnector.Tests/ConnectionTests.cs index c179b7a05..3cdab4fc7 100644 --- a/tests/SingleStoreConnector.Tests/ConnectionTests.cs +++ b/tests/SingleStoreConnector.Tests/ConnectionTests.cs @@ -166,14 +166,14 @@ public void Ping() } [Fact] - public void PingWhenClosed() + public void PingWhenReset() { using var connection = new SingleStoreConnection(m_csb.ConnectionString); connection.Open(); Assert.Equal(ConnectionState.Open, connection.State); - m_server.Stop(); + m_server.Reset(); Assert.False(connection.Ping()); - Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Equal(ConnectionState.Broken, connection.State); } [Fact] @@ -277,6 +277,72 @@ public void ReplaceActiveReader() connection.Close(); } + [Fact] + public async Task ResetServerConnectionWhileOpen() + { + var csb = new SingleStoreConnectionStringBuilder(m_csb.ConnectionString) + { + MaximumPoolSize = 5, + ConnectionTimeout = 5, + }; + + List tasks = []; + using var barrier = new Barrier((int) csb.MaximumPoolSize); + for (var i = 0; i < csb.MaximumPoolSize - 1; i++) + { + var threadId = i; + tasks.Add(Task.Run(async () => + { + using var connection = new SingleStoreConnection(csb.ConnectionString); + await connection.OpenAsync().ConfigureAwait(false); + + barrier.SignalAndWait(); + //// wait for reset + barrier.SignalAndWait(); + + switch (threadId % 3) + { + case 0: + { + using (var command = connection.CreateCommand()) + { + command.CommandText = "SELECT 1;"; + var exception = Assert.Throws(() => command.ExecuteScalar()); + Assert.Equal("Failed to read the result set.", exception.Message); + } + break; + } + case 1: + { + // NOTE: duplicate of PingWhenReset, but included here for completeness + var ping = await connection.PingAsync().ConfigureAwait(false); + Assert.False(ping); + break; + } + case 2: + { + await Assert.ThrowsAsync(async () => await connection.ResetConnectionAsync().ConfigureAwait(false)); + break; + } + } + + Assert.Equal(ConnectionState.Broken, connection.State); + + await connection.CloseAsync().ConfigureAwait(false); + Assert.Equal(ConnectionState.Closed, connection.State); + + await connection.OpenAsync().ConfigureAwait(false); + Assert.Equal(ConnectionState.Open, connection.State); + })); + } + + barrier.SignalAndWait(); + m_server.Reset(); + barrier.SignalAndWait(); + + await Task.WhenAll(tasks); + } + private static async Task WaitForConditionAsync(T expected, Func getValue) { var sw = Stopwatch.StartNew(); @@ -285,6 +351,6 @@ private static async Task WaitForConditionAsync(T expected, Func getValue) Assert.Equal(expected, getValue()); } - readonly FakeSingleStoreServer m_server; - readonly SingleStoreConnectionStringBuilder m_csb; + private readonly FakeSingleStoreServer m_server; + private readonly SingleStoreConnectionStringBuilder m_csb; } diff --git a/tests/SingleStoreConnector.Tests/DummyEnum.cs b/tests/SingleStoreConnector.Tests/DummyEnum.cs index aad29ff07..abbe4601e 100644 --- a/tests/SingleStoreConnector.Tests/DummyEnum.cs +++ b/tests/SingleStoreConnector.Tests/DummyEnum.cs @@ -3,7 +3,7 @@ namespace SingleStoreConnector.Tests; internal enum DummyEnum { FirstValue, - SecondValue + SecondValue, } internal enum DummyByteEnum : byte diff --git a/tests/SingleStoreConnector.Tests/FakeSingleStoreServer.cs b/tests/SingleStoreConnector.Tests/FakeSingleStoreServer.cs index 07ca3459c..112130740 100644 --- a/tests/SingleStoreConnector.Tests/FakeSingleStoreServer.cs +++ b/tests/SingleStoreConnector.Tests/FakeSingleStoreServer.cs @@ -21,6 +21,22 @@ public void Start() m_tasks.Add(AcceptConnectionsAsync()); } + public void Reset() + { + m_cts.Cancel(); + try + { + Task.WaitAll(m_tasks.Skip(1).ToArray()); + } + catch (AggregateException) + { + } + m_connections.Clear(); + m_tasks.Clear(); + m_cts.Dispose(); + m_cts = new(); + } + public void Stop() { if (m_cts is not null) @@ -81,10 +97,10 @@ private async Task AcceptConnectionsAsync() } } - readonly object m_lock; - readonly TcpListener m_tcpListener; - readonly List m_connections; - readonly List m_tasks; - CancellationTokenSource m_cts; - int m_activeConnections; + private readonly object m_lock; + private readonly TcpListener m_tcpListener; + private readonly List m_connections; + private readonly List m_tasks; + private CancellationTokenSource m_cts; + private int m_activeConnections; } diff --git a/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs b/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs index 9803fdc51..3b3eabedd 100644 --- a/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs +++ b/tests/SingleStoreConnector.Tests/FakeSingleStoreServerConnection.cs @@ -312,6 +312,6 @@ private static void WriteError(BinaryWriter writer, string message = "An unknown writer.WriteRaw(message); } - readonly FakeSingleStoreServer m_server; - readonly int m_connectionId; + private readonly FakeSingleStoreServer m_server; + private readonly int m_connectionId; } diff --git a/tests/SingleStoreConnector.Tests/Metrics/IConnectionCreator.cs b/tests/SingleStoreConnector.Tests/Metrics/IConnectionCreator.cs index b8974905e..c075b04e4 100644 --- a/tests/SingleStoreConnector.Tests/Metrics/IConnectionCreator.cs +++ b/tests/SingleStoreConnector.Tests/Metrics/IConnectionCreator.cs @@ -45,7 +45,10 @@ public SingleStoreConnection OpenConnection() } public string PoolName { get; } - public void Dispose() { } + + public void Dispose() + { + } private readonly string m_connectionString; } diff --git a/tests/SingleStoreConnector.Tests/Metrics/MetricsTestsBase.cs b/tests/SingleStoreConnector.Tests/Metrics/MetricsTestsBase.cs index 6ad5a24e7..c3870910f 100644 --- a/tests/SingleStoreConnector.Tests/Metrics/MetricsTestsBase.cs +++ b/tests/SingleStoreConnector.Tests/Metrics/MetricsTestsBase.cs @@ -20,7 +20,7 @@ public MetricsTestsBase() { if (instrument.Meter.Name == "SingleStoreConnector") listener.EnableMeasurementEvents(instrument); - } + }, }; m_meterListener.SetMeasurementEventCallback(OnMeasurementRecorded); m_meterListener.SetMeasurementEventCallback(OnMeasurementRecorded); @@ -124,7 +124,6 @@ private void OnMeasurementRecorded(Instrument instrument, double measurement, Re return (poolName, state); } - private readonly Dictionary m_measurements; private readonly Dictionary> m_timeMeasurements; private readonly MeterListener m_meterListener; diff --git a/tests/SingleStoreConnector.Tests/SingleStoreConnectionStringBuilderTests.cs b/tests/SingleStoreConnector.Tests/SingleStoreConnectionStringBuilderTests.cs index 473a38683..276d61521 100644 --- a/tests/SingleStoreConnector.Tests/SingleStoreConnectionStringBuilderTests.cs +++ b/tests/SingleStoreConnector.Tests/SingleStoreConnectionStringBuilderTests.cs @@ -160,7 +160,7 @@ public void ParseConnectionString() "ssl mode=verifyca;" + "tls version=Tls12, TLS v1.3;" + "Uid=username;" + - "useaffectedrows=true" + "useaffectedrows=true", }; Assert.True(csb.AllowLoadLocalInfile); Assert.True(csb.AllowPublicKeyRetrieval); @@ -169,7 +169,7 @@ public void ParseConnectionString() Assert.False(csb.AutoEnlist); #if !BASELINE Assert.Equal(-1, csb.CancellationTimeout); - // Connector/NET treats "CertificateFile" (client certificate) and "SslCa" (server CA) as aliases + //// Connector/NET treats "CertificateFile" (client certificate) and "SslCa" (server CA) as aliases Assert.Equal("file.pfx", csb.CertificateFile); #endif Assert.Equal("Pass2345", csb.CertificatePassword); @@ -589,8 +589,6 @@ public void SpecialCharactersInPassword() Password = "foo;=bar,baz", }; Assert.Equal("Password=\"foo;=bar,baz\"", builder.ConnectionString, StringComparer.OrdinalIgnoreCase); -#if !BASELINE // https://bugs.mysql.com/bug.php?id=111797 using var connection = new SingleStoreConnection(builder.ConnectionString); -#endif } } diff --git a/tests/SingleStoreConnector.Tests/SingleStoreDateTimeTests.cs b/tests/SingleStoreConnector.Tests/SingleStoreDateTimeTests.cs index d3b469baf..e4646cfd3 100644 --- a/tests/SingleStoreConnector.Tests/SingleStoreDateTimeTests.cs +++ b/tests/SingleStoreConnector.Tests/SingleStoreDateTimeTests.cs @@ -7,7 +7,7 @@ public class SingleStoreDateTimeTests [Fact] public void NewSingleStoreDateTimeIsNotValidDateTime() { - var msdt = new SingleStoreDateTime(); + var msdt = default(SingleStoreDateTime); Assert.False(msdt.IsValidDateTime); } @@ -52,7 +52,7 @@ public void GetDateTime() [Fact] public void GetDateTimeForInvalidDate() { - var msdt = new SingleStoreDateTime(); + var msdt = default(SingleStoreDateTime); Assert.False(msdt.IsValidDateTime); Assert.Throws(() => msdt.GetDateTime()); } @@ -60,7 +60,7 @@ public void GetDateTimeForInvalidDate() [Fact] public void SetMicrosecond() { - var msdt = new SingleStoreDateTime(); + var msdt = default(SingleStoreDateTime); Assert.Equal(0, msdt.Microsecond); msdt.Microsecond = 123456; Assert.Equal(123, msdt.Millisecond); @@ -93,7 +93,7 @@ public void ChangeTypeToDateTime() [Fact] public void NotConvertibleToDateTime() { - IConvertible convertible = new SingleStoreDateTime(); + IConvertible convertible = default(SingleStoreDateTime); #if !BASELINE Assert.Throws(() => convertible.ToDateTime(CultureInfo.InvariantCulture)); #else @@ -104,7 +104,7 @@ public void NotConvertibleToDateTime() [Fact] public void NotConvertToDateTime() { - object obj = new SingleStoreDateTime(); + object obj = default(SingleStoreDateTime); #if !BASELINE Assert.Throws(() => Convert.ToDateTime(obj)); #else @@ -115,7 +115,7 @@ public void NotConvertToDateTime() [Fact] public void NotChangeTypeToDateTime() { - object obj = new SingleStoreDateTime(); + object obj = default(SingleStoreDateTime); #if !BASELINE Assert.Throws(() => Convert.ChangeType(obj, TypeCode.DateTime)); #else @@ -134,7 +134,7 @@ public void ValidDateTimeConvertibleToString() [Fact] public void InvalidDateTimeConvertibleToString() { - IConvertible convertible = new SingleStoreDateTime(); + IConvertible convertible = default(SingleStoreDateTime); Assert.Equal("0000-00-00", convertible.ToString(CultureInfo.InvariantCulture)); } #endif @@ -255,6 +255,6 @@ public void Equal() } #endif - static readonly SingleStoreDateTime s_mySqlDateTime = new(2018, 6, 9, 12, 34, 56, 123456); - static readonly DateTime s_dateTime = new DateTime(2018, 6, 9, 12, 34, 56, 123).AddTicks(4560); + private static readonly SingleStoreDateTime s_mySqlDateTime = new(2018, 6, 9, 12, 34, 56, 123456); + private static readonly DateTime s_dateTime = new DateTime(2018, 6, 9, 12, 34, 56, 123).AddTicks(4560); } diff --git a/tests/SingleStoreConnector.Tests/SingleStoreParameterCollectionNameToIndexTests.cs b/tests/SingleStoreConnector.Tests/SingleStoreParameterCollectionNameToIndexTests.cs index 2f84504d3..b016a546a 100644 --- a/tests/SingleStoreConnector.Tests/SingleStoreParameterCollectionNameToIndexTests.cs +++ b/tests/SingleStoreConnector.Tests/SingleStoreParameterCollectionNameToIndexTests.cs @@ -124,5 +124,5 @@ public void ChangeParameter() Assert.Equal(1, m_collection.NormalizedIndexOf("D")); } - readonly SingleStoreParameterCollection m_collection; + private readonly SingleStoreParameterCollection m_collection; } diff --git a/tests/SingleStoreConnector.Tests/SingleStoreParameterCollectionTests.cs b/tests/SingleStoreConnector.Tests/SingleStoreParameterCollectionTests.cs index 2e142cd34..3c172cf82 100644 --- a/tests/SingleStoreConnector.Tests/SingleStoreParameterCollectionTests.cs +++ b/tests/SingleStoreConnector.Tests/SingleStoreParameterCollectionTests.cs @@ -19,5 +19,5 @@ public SingleStoreParameterCollectionTests() [Fact] public void RemoveAtEnd() => Assert.Throws(() => m_collection.RemoveAt(0)); - readonly SingleStoreParameterCollection m_collection; + private readonly SingleStoreParameterCollection m_collection; } diff --git a/tests/SingleStoreConnector.Tests/SingleStoreParameterTests.cs b/tests/SingleStoreConnector.Tests/SingleStoreParameterTests.cs index 8eb63cbdd..4a6210c5e 100644 --- a/tests/SingleStoreConnector.Tests/SingleStoreParameterTests.cs +++ b/tests/SingleStoreConnector.Tests/SingleStoreParameterTests.cs @@ -1,9 +1,9 @@ using System; using System.Data; using System.Text; +using SingleStoreConnector; using SingleStoreConnector.Core; using SingleStoreConnector.Protocol.Serialization; -using SingleStoreConnector; using Xunit; namespace SingleStoreConnector.Tests; @@ -20,14 +20,14 @@ private string EncodeParameterToAscii(SingleStoreParameter parameter, StatementP [Fact] public void ZeroByteInBinary() { - var parameter = new SingleStoreParameter {Direction = ParameterDirection.Input, Value = new byte[]{0x54, 0x00, 0x45, 0x53, 0x54}}; + var parameter = new SingleStoreParameter { Direction = ParameterDirection.Input, Value = new byte[] { 0x54, 0x00, 0x45, 0x53, 0x54 } }; Assert.Equal(@"_binary'T\0EST'", EncodeParameterToAscii(parameter)); } [Fact] public void ZeroByteInGuid() { - var parameter = new SingleStoreParameter {Direction = ParameterDirection.Input, Value = new Guid(new byte[]{0x44, 0x49, 0x55, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})}; + var parameter = new SingleStoreParameter { Direction = ParameterDirection.Input, Value = new Guid(new byte[] { 0x44, 0x49, 0x55, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }) }; Assert.Equal(@"_binary'GUID\0\0\0\0\0\0\0\0\0\0\0\0'", EncodeParameterToAscii(parameter, StatementPreparerOptions.GuidFormatBinary16)); } } diff --git a/tests/SingleStoreConnector.Tests/TypeMapperTests.cs b/tests/SingleStoreConnector.Tests/TypeMapperTests.cs index d17f4acc8..f84397582 100644 --- a/tests/SingleStoreConnector.Tests/TypeMapperTests.cs +++ b/tests/SingleStoreConnector.Tests/TypeMapperTests.cs @@ -36,13 +36,13 @@ public void DbTypeMappingTest(Type clrType, DbType dbType) [InlineData((byte) 1, DbType.Int16, (short) 1)] [InlineData((short) 1, DbType.UInt16, (ushort) 1)] [InlineData((ushort) 1, DbType.Int32, 1)] - [InlineData(1, DbType.UInt32, (uint) 1)] - [InlineData((uint) 1, DbType.Int64, (long) 1)] - [InlineData((long) 1, DbType.UInt64, (ulong) 1)] - [InlineData((ulong) 1, DbType.String, "1")] - [InlineData((ulong) 1, DbType.StringFixedLength, "1")] - [InlineData((ulong) 1, DbType.AnsiString, "1")] - [InlineData((ulong) 1, DbType.AnsiStringFixedLength, "1")] + [InlineData(1, DbType.UInt32, 1U)] + [InlineData(1U, DbType.Int64, 1L)] + [InlineData(1L, DbType.UInt64, 1UL)] + [InlineData(1UL, DbType.String, "1")] + [InlineData(1UL, DbType.StringFixedLength, "1")] + [InlineData(1UL, DbType.AnsiString, "1")] + [InlineData(1UL, DbType.AnsiStringFixedLength, "1")] public void ConversionTest(object original, DbType dbType, object expected) { Assert.Equal(expected, TypeMapper.Instance.GetDbTypeMapping(dbType).DoConversion(original)); diff --git a/tests/SingleStoreConnector.Tests/UtilityTests.cs b/tests/SingleStoreConnector.Tests/UtilityTests.cs index 18b85b818..eceae370d 100644 --- a/tests/SingleStoreConnector.Tests/UtilityTests.cs +++ b/tests/SingleStoreConnector.Tests/UtilityTests.cs @@ -83,14 +83,14 @@ public void ParseTimeSpanFails(string input) [Theory] [InlineData("", "")] - [InlineData("pre", "")] - [InlineData("", "post")] - [InlineData("pre", "post")] + [InlineData("pre\n", "")] + [InlineData("", "\npost")] + [InlineData("pre\n", "\npost")] public void DecodePublicKey(string pre, string post) { #if NET5_0_OR_GREATER using var rsa = RSA.Create(); - Utility.LoadRsaParameters(pre + c_publicKey + post, rsa); + Utility.LoadRsaParameters(Encoding.ASCII.GetBytes(pre + c_publicKey + post), rsa); var parameters = rsa.ExportParameters(false); #else var parameters = Utility.GetRsaParameters(pre + c_publicKey + post); @@ -101,14 +101,14 @@ public void DecodePublicKey(string pre, string post) [Theory] [InlineData("", "")] - [InlineData("pre", "")] - [InlineData("", "post")] - [InlineData("pre", "post")] + [InlineData("pre\n", "")] + [InlineData("", "\npost")] + [InlineData("pre\n", "\npost")] public void DecodePrivateKey(string pre, string post) { #if NET5_0_OR_GREATER using var rsa = RSA.Create(); - Utility.LoadRsaParameters(pre + c_privateKey + post, rsa); + Utility.LoadRsaParameters(Encoding.ASCII.GetBytes(pre + c_privateKey + post), rsa); var parameters = rsa.ExportParameters(true); #else var parameters = Utility.GetRsaParameters(pre + c_privateKey + post); @@ -123,7 +123,7 @@ public void DecodePrivateKey(string pre, string post) Assert.Equal(s_iq, parameters.InverseQ); } - const string c_publicKey = @"-----BEGIN PUBLIC KEY----- + private const string c_publicKey = @"-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwmlDX62hQAQNvSJZ/HAO UjCbAiEPQquEyPpxjqDxyx1fVxL93U1au50xGk4sad4OH+GSZCChqj3kvwJhXc52 iHdBzjQbucGYlC1wLNMc1F+H89vMjEq1ZexsRDWQSrgL1I6i9Mn5NFgS563yPBpO @@ -133,7 +133,7 @@ public void DecodePrivateKey(string pre, string post) LwIDAQAB -----END PUBLIC KEY-----"; - const string c_privateKey = @"-----BEGIN RSA PRIVATE KEY----- + private const string c_privateKey = @"-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAwmlDX62hQAQNvSJZ/HAOUjCbAiEPQquEyPpxjqDxyx1fVxL9 3U1au50xGk4sad4OH+GSZCChqj3kvwJhXc52iHdBzjQbucGYlC1wLNMc1F+H89vM jEq1ZexsRDWQSrgL1I6i9Mn5NFgS563yPBpOyfYyGWCrL5w7yI+we3MCwy2q8JIO @@ -161,12 +161,12 @@ public void DecodePrivateKey(string pre, string post) joMn7CFmwV9EYjPTxwkByKs5a8AbNYJDSfNCa3KoHTFjOvVMPec90Q== -----END RSA PRIVATE KEY-----"; - static readonly byte[] s_modulus = [0xC2, 0x69, 0x43, 0x5F, 0xAD, 0xA1, 0x40, 0x04, 0x0D, 0xBD, 0x22, 0x59, 0xFC, 0x70, 0x0E, 0x52, 0x30, 0x9B, 0x02, 0x21, 0x0F, 0x42, 0xAB, 0x84, 0xC8, 0xFA, 0x71, 0x8E, 0xA0, 0xF1, 0xCB, 0x1D, 0x5F, 0x57, 0x12, 0xFD, 0xDD, 0x4D, 0x5A, 0xBB, 0x9D, 0x31, 0x1A, 0x4E, 0x2C, 0x69, 0xDE, 0x0E, 0x1F, 0xE1, 0x92, 0x64, 0x20, 0xA1, 0xAA, 0x3D, 0xE4, 0xBF, 0x02, 0x61, 0x5D, 0xCE, 0x76, 0x88, 0x77, 0x41, 0xCE, 0x34, 0x1B, 0xB9, 0xC1, 0x98, 0x94, 0x2D, 0x70, 0x2C, 0xD3, 0x1C, 0xD4, 0x5F, 0x87, 0xF3, 0xDB, 0xCC, 0x8C, 0x4A, 0xB5, 0x65, 0xEC, 0x6C, 0x44, 0x35, 0x90, 0x4A, 0xB8, 0x0B, 0xD4, 0x8E, 0xA2, 0xF4, 0xC9, 0xF9, 0x34, 0x58, 0x12, 0xE7, 0xAD, 0xF2, 0x3C, 0x1A, 0x4E, 0xC9, 0xF6, 0x32, 0x19, 0x60, 0xAB, 0x2F, 0x9C, 0x3B, 0xC8, 0x8F, 0xB0, 0x7B, 0x73, 0x02, 0xC3, 0x2D, 0xAA, 0xF0, 0x92, 0x0E, 0xC9, 0x97, 0xA0, 0xCE, 0x1E, 0xFA, 0x5B, 0xE7, 0xF7, 0x17, 0xDA, 0x45, 0x76, 0x77, 0xE3, 0x40, 0xEF, 0xAF, 0x8C, 0xFC, 0x8D, 0xBA, 0x99, 0xD3, 0x10, 0x3B, 0x58, 0x6C, 0x68, 0xC5, 0x61, 0xAA, 0x98, 0x21, 0x33, 0x67, 0x71, 0x9F, 0x1C, 0x2F, 0x52, 0xD8, 0x57, 0x0B, 0x94, 0x5D, 0x6D, 0x0F, 0x5B, 0xC6, 0xDF, 0x94, 0x47, 0x45, 0x77, 0xF0, 0x6B, 0xF1, 0xFC, 0xC2, 0x80, 0xAE, 0x08, 0x55, 0xD4, 0xA6, 0xE8, 0xE5, 0x4F, 0xCD, 0x26, 0x5A, 0xB5, 0xAC, 0x38, 0xDA, 0x37, 0x17, 0xDA, 0xD3, 0x53, 0xF1, 0xFD, 0xCF, 0xDC, 0x29, 0x40, 0xBC, 0xBD, 0x55, 0xDE, 0x3E, 0x04, 0x74, 0x21, 0xF2, 0x60, 0xDA, 0xCA, 0x26, 0xAC, 0x19, 0xEE, 0x74, 0xED, 0x8E, 0xA0, 0x41, 0x3E, 0xA3, 0x66, 0x36, 0x27, 0x11, 0xDD, 0xE3, 0x3C, 0xB2, 0xE1, 0xA3, 0x31, 0x6C, 0xA6, 0x2F]; - static readonly byte[] s_exponent = [0x01, 0x00, 0x01]; - static readonly byte[] s_d = [0x11, 0xB3, 0x61, 0xD6, 0x01, 0x4A, 0x10, 0x39, 0x0E, 0x43, 0x2D, 0x30, 0x82, 0x42, 0x75, 0x9B, 0x58, 0x1F, 0x94, 0xE7, 0x0B, 0xAB, 0xA7, 0x50, 0x34, 0xB8, 0x50, 0xC4, 0x8A, 0xB4, 0xD9, 0x28, 0x78, 0x78, 0xC6, 0x1A, 0xE5, 0x1E, 0x58, 0xE7, 0x82, 0x1D, 0x69, 0x66, 0xBA, 0xB6, 0x7C, 0xE8, 0x4E, 0x50, 0xCC, 0x72, 0x5F, 0x62, 0x38, 0xCC, 0xDF, 0xD9, 0xE0, 0x4E, 0x9B, 0x2A, 0xE4, 0x31, 0xF2, 0xA1, 0xC4, 0x33, 0x8C, 0x5E, 0xB2, 0x58, 0xC1, 0x0D, 0x0E, 0x0C, 0x96, 0xC0, 0x26, 0x39, 0xF8, 0x77, 0xE4, 0x88, 0x50, 0x31, 0xB1, 0x28, 0x71, 0x89, 0x3E, 0x00, 0x9E, 0x7D, 0x9F, 0x1A, 0x3C, 0xA5, 0x2C, 0x03, 0x1B, 0xAC, 0x9B, 0xEE, 0x74, 0xF2, 0x31, 0x2D, 0x3F, 0xF9, 0xA9, 0x38, 0xCC, 0x38, 0xAD, 0x84, 0xD6, 0xB1, 0x44, 0x09, 0x51, 0x6E, 0x7B, 0xC4, 0x11, 0xA9, 0xAB, 0xB9, 0x80, 0xD4, 0xA8, 0x87, 0xEF, 0x2C, 0x76, 0x51, 0x64, 0x3E, 0xE5, 0x37, 0x7F, 0xF1, 0x26, 0xD7, 0xC0, 0x80, 0xEB, 0x96, 0xA9, 0x1A, 0x4D, 0xB8, 0xAF, 0xC9, 0xE5, 0xA2, 0x43, 0x7A, 0x6C, 0xEB, 0x9F, 0xF3, 0x8A, 0x72, 0xDD, 0xE7, 0xD2, 0xEB, 0x60, 0xD6, 0x01, 0xDD, 0x25, 0x40, 0x8B, 0x71, 0xBA, 0x0D, 0x49, 0x55, 0x3A, 0x1D, 0x9B, 0x60, 0x71, 0x2C, 0x37, 0xAF, 0xFB, 0xD9, 0x3C, 0xEF, 0x77, 0xF1, 0x43, 0xE9, 0x7F, 0xE9, 0x2C, 0xDC, 0x76, 0xDC, 0x34, 0x48, 0xFF, 0x4F, 0xB4, 0xBE, 0x93, 0x82, 0xC3, 0x14, 0x42, 0xC6, 0xE4, 0x01, 0xF5, 0xB0, 0xDE, 0xED, 0x64, 0x82, 0xB9, 0x1C, 0x17, 0x0A, 0x28, 0x89, 0x66, 0xC2, 0x45, 0xA3, 0x4E, 0xBC, 0x61, 0xB4, 0xD7, 0xCC, 0x47, 0xDC, 0x86, 0x8E, 0x1F, 0xEE, 0x8E, 0x0D, 0x0F, 0x51, 0xB1, 0xAC, 0x62, 0xEF, 0xDC, 0x6D, 0x90, 0x81]; - static readonly byte[] s_p = [0xFA, 0x63, 0x13, 0x19, 0xF3, 0x13, 0x7A, 0xC0, 0x70, 0x67, 0xAD, 0xE8, 0x05, 0x26, 0x70, 0x68, 0x9F, 0x30, 0xFB, 0x06, 0x9E, 0x45, 0xFF, 0x38, 0xF2, 0x56, 0xA3, 0x35, 0x4F, 0xAE, 0x21, 0x20, 0x94, 0x44, 0x80, 0x42, 0x61, 0x39, 0xC3, 0xEB, 0xA9, 0x8F, 0xD2, 0x61, 0x88, 0xE9, 0x6A, 0x62, 0x42, 0xF0, 0x18, 0x55, 0xDE, 0x50, 0x73, 0x1E, 0xFA, 0x70, 0xE5, 0x25, 0x73, 0x28, 0x6F, 0x29, 0x40, 0xE0, 0x09, 0x1A, 0xB6, 0x11, 0xF4, 0xB1, 0xEC, 0x89, 0x25, 0x66, 0x7D, 0x41, 0xC7, 0x53, 0xFA, 0x3A, 0x58, 0xF0, 0xB2, 0x27, 0x73, 0xE8, 0x63, 0x49, 0x06, 0x15, 0xF1, 0x4C, 0x35, 0x9D, 0x33, 0xE5, 0x28, 0x3E, 0xBF, 0x2F, 0x9F, 0xE4, 0x33, 0xE9, 0xCE, 0x6F, 0x91, 0x18, 0xAD, 0x95, 0xD4, 0xF5, 0x12, 0xD6, 0x92, 0xBD, 0xDA, 0xDF, 0x64, 0xA8, 0xB4, 0xEC, 0x7E, 0xD8, 0x1A, 0x41]; - static readonly byte[] s_q = [0xC6, 0xC4, 0xF4, 0x19, 0x1A, 0x74, 0xFB, 0xA1, 0x07, 0x3D, 0x8A, 0x80, 0x9A, 0x6F, 0xC5, 0x60, 0xCB, 0x68, 0x32, 0x14, 0xCA, 0x9D, 0x21, 0x59, 0x7C, 0xA5, 0xAC, 0x0B, 0x68, 0x0E, 0x8C, 0xE2, 0x82, 0x77, 0x6C, 0xBC, 0xC2, 0xA6, 0xB3, 0xF2, 0x8B, 0xE4, 0xC8, 0xDF, 0xF9, 0x7B, 0x92, 0x82, 0xE6, 0x20, 0x84, 0xCB, 0xE0, 0x46, 0x44, 0x0A, 0xB4, 0xD7, 0x48, 0x43, 0x29, 0x78, 0xE7, 0xCA, 0xE4, 0x8F, 0xEC, 0x52, 0x2A, 0x6F, 0xDA, 0x58, 0x8D, 0xAA, 0xEB, 0xC0, 0x5E, 0x4F, 0xC1, 0x70, 0x9A, 0x97, 0xCE, 0xE4, 0xFF, 0x70, 0xEC, 0xA1, 0x2A, 0xED, 0xD8, 0x7A, 0xA1, 0xA5, 0x1C, 0x79, 0x2A, 0x8E, 0xBC, 0x74, 0xA3, 0x45, 0x20, 0x27, 0x14, 0xE1, 0xB9, 0x16, 0x80, 0x26, 0x81, 0xC2, 0x17, 0x26, 0xBB, 0x83, 0x04, 0x88, 0xFB, 0x0B, 0xB2, 0x06, 0x49, 0x88, 0x9A, 0xC0, 0x44, 0x6F]; - static readonly byte[] s_dp = [0xA9, 0x1C, 0xD1, 0x81, 0xED, 0x53, 0x72, 0xCD, 0x17, 0x1E, 0x6F, 0xAF, 0x0E, 0x0B, 0x70, 0x50, 0xB9, 0x74, 0x73, 0x97, 0x7C, 0xBE, 0xFA, 0x2A, 0x94, 0x43, 0x3F, 0xE8, 0x79, 0xF0, 0xBA, 0x1F, 0x2F, 0x05, 0x35, 0x3B, 0xA0, 0x3F, 0x4B, 0xBC, 0x97, 0xC1, 0xB3, 0xEE, 0x7C, 0x72, 0x6A, 0x90, 0x03, 0x75, 0xF6, 0x79, 0xC8, 0xB5, 0xBD, 0x8B, 0x66, 0xCB, 0x69, 0x79, 0x69, 0xD6, 0x1D, 0x7E, 0x57, 0x7A, 0xF9, 0xAB, 0x38, 0xDD, 0xD7, 0xCF, 0x74, 0x5B, 0x0D, 0x0F, 0xB3, 0xFA, 0x01, 0x8F, 0x0E, 0xAA, 0xE9, 0xF7, 0x67, 0xDC, 0x8D, 0xC4, 0x9F, 0x8A, 0x6A, 0xB9, 0xD7, 0x48, 0x09, 0xCE, 0x2B, 0x86, 0xD3, 0x2F, 0xCE, 0x21, 0x74, 0x74, 0xC5, 0x3B, 0x5D, 0x5A, 0xAD, 0x3D, 0x65, 0xC0, 0x95, 0x1D, 0x11, 0xA4, 0x2B, 0x68, 0x5F, 0xB4, 0x12, 0x60, 0x80, 0x12, 0x5B, 0x79, 0xCA, 0x01]; - static readonly byte[] s_dq = [0x99, 0x7F, 0xF2, 0x0E, 0x0C, 0xDB, 0x78, 0x93, 0x63, 0x97, 0x08, 0x05, 0xC6, 0xBE, 0x38, 0x4C, 0x46, 0xE9, 0x21, 0x7B, 0xE2, 0xF8, 0xB3, 0x8F, 0x7A, 0xEE, 0x2A, 0x4D, 0xE8, 0xBF, 0x0B, 0xD7, 0xC4, 0xEF, 0x5B, 0x3E, 0xEE, 0x87, 0x74, 0x82, 0x03, 0xBC, 0xDB, 0xCF, 0xF3, 0xC7, 0x95, 0x5E, 0x97, 0x3F, 0x57, 0xAE, 0x66, 0x75, 0x7D, 0x08, 0x53, 0x9D, 0xC9, 0x85, 0x6C, 0x5A, 0x9D, 0x8E, 0x97, 0x31, 0xFA, 0x4B, 0x7D, 0xD0, 0x41, 0x6B, 0x8F, 0x84, 0x06, 0x69, 0xD9, 0xAB, 0x77, 0xF4, 0x70, 0xBE, 0x79, 0x9D, 0x69, 0x13, 0x18, 0xFA, 0x61, 0xBF, 0xAA, 0x25, 0x11, 0xFE, 0x03, 0x5E, 0x23, 0x1A, 0x75, 0xB9, 0x11, 0x79, 0x49, 0x9A, 0x44, 0xCB, 0x08, 0x77, 0xB2, 0xE1, 0x89, 0xE5, 0x8C, 0xA1, 0x71, 0xBF, 0x7D, 0x5A, 0x17, 0x9D, 0x71, 0x8E, 0xDF, 0x6C, 0xAA, 0x2D, 0x7E, 0xEB]; - static readonly byte[] s_iq = [0x2F, 0x91, 0x84, 0x58, 0xAB, 0x47, 0xAE, 0xE6, 0x92, 0xBC, 0xE4, 0x13, 0xBA, 0x9C, 0xDD, 0x66, 0xFF, 0x91, 0xA8, 0x94, 0x41, 0xBF, 0xD6, 0x54, 0x1B, 0x6D, 0x92, 0xC0, 0x67, 0xC6, 0x08, 0x23, 0xA4, 0x6D, 0x24, 0xA1, 0xC1, 0xBD, 0xCB, 0xC5, 0xAF, 0x4E, 0x96, 0x39, 0x46, 0x4A, 0x44, 0xD7, 0x10, 0x8F, 0x35, 0x76, 0xF5, 0xCB, 0x86, 0xE2, 0x9A, 0xAD, 0x32, 0xCD, 0x2C, 0xC0, 0xB8, 0x4A, 0x31, 0xB0, 0xAC, 0xF8, 0xFA, 0xAB, 0xEB, 0x0E, 0x8A, 0x7D, 0x92, 0x0D, 0xB5, 0x5C, 0x8B, 0x36, 0xD6, 0xC3, 0x56, 0x8F, 0x6B, 0xC0, 0x54, 0x5C, 0x8E, 0x83, 0x27, 0xEC, 0x21, 0x66, 0xC1, 0x5F, 0x44, 0x62, 0x33, 0xD3, 0xC7, 0x09, 0x01, 0xC8, 0xAB, 0x39, 0x6B, 0xC0, 0x1B, 0x35, 0x82, 0x43, 0x49, 0xF3, 0x42, 0x6B, 0x72, 0xA8, 0x1D, 0x31, 0x63, 0x3A, 0xF5, 0x4C, 0x3D, 0xE7, 0x3D, 0xD1]; + private static readonly byte[] s_modulus = [0xC2, 0x69, 0x43, 0x5F, 0xAD, 0xA1, 0x40, 0x04, 0x0D, 0xBD, 0x22, 0x59, 0xFC, 0x70, 0x0E, 0x52, 0x30, 0x9B, 0x02, 0x21, 0x0F, 0x42, 0xAB, 0x84, 0xC8, 0xFA, 0x71, 0x8E, 0xA0, 0xF1, 0xCB, 0x1D, 0x5F, 0x57, 0x12, 0xFD, 0xDD, 0x4D, 0x5A, 0xBB, 0x9D, 0x31, 0x1A, 0x4E, 0x2C, 0x69, 0xDE, 0x0E, 0x1F, 0xE1, 0x92, 0x64, 0x20, 0xA1, 0xAA, 0x3D, 0xE4, 0xBF, 0x02, 0x61, 0x5D, 0xCE, 0x76, 0x88, 0x77, 0x41, 0xCE, 0x34, 0x1B, 0xB9, 0xC1, 0x98, 0x94, 0x2D, 0x70, 0x2C, 0xD3, 0x1C, 0xD4, 0x5F, 0x87, 0xF3, 0xDB, 0xCC, 0x8C, 0x4A, 0xB5, 0x65, 0xEC, 0x6C, 0x44, 0x35, 0x90, 0x4A, 0xB8, 0x0B, 0xD4, 0x8E, 0xA2, 0xF4, 0xC9, 0xF9, 0x34, 0x58, 0x12, 0xE7, 0xAD, 0xF2, 0x3C, 0x1A, 0x4E, 0xC9, 0xF6, 0x32, 0x19, 0x60, 0xAB, 0x2F, 0x9C, 0x3B, 0xC8, 0x8F, 0xB0, 0x7B, 0x73, 0x02, 0xC3, 0x2D, 0xAA, 0xF0, 0x92, 0x0E, 0xC9, 0x97, 0xA0, 0xCE, 0x1E, 0xFA, 0x5B, 0xE7, 0xF7, 0x17, 0xDA, 0x45, 0x76, 0x77, 0xE3, 0x40, 0xEF, 0xAF, 0x8C, 0xFC, 0x8D, 0xBA, 0x99, 0xD3, 0x10, 0x3B, 0x58, 0x6C, 0x68, 0xC5, 0x61, 0xAA, 0x98, 0x21, 0x33, 0x67, 0x71, 0x9F, 0x1C, 0x2F, 0x52, 0xD8, 0x57, 0x0B, 0x94, 0x5D, 0x6D, 0x0F, 0x5B, 0xC6, 0xDF, 0x94, 0x47, 0x45, 0x77, 0xF0, 0x6B, 0xF1, 0xFC, 0xC2, 0x80, 0xAE, 0x08, 0x55, 0xD4, 0xA6, 0xE8, 0xE5, 0x4F, 0xCD, 0x26, 0x5A, 0xB5, 0xAC, 0x38, 0xDA, 0x37, 0x17, 0xDA, 0xD3, 0x53, 0xF1, 0xFD, 0xCF, 0xDC, 0x29, 0x40, 0xBC, 0xBD, 0x55, 0xDE, 0x3E, 0x04, 0x74, 0x21, 0xF2, 0x60, 0xDA, 0xCA, 0x26, 0xAC, 0x19, 0xEE, 0x74, 0xED, 0x8E, 0xA0, 0x41, 0x3E, 0xA3, 0x66, 0x36, 0x27, 0x11, 0xDD, 0xE3, 0x3C, 0xB2, 0xE1, 0xA3, 0x31, 0x6C, 0xA6, 0x2F]; + private static readonly byte[] s_exponent = [0x01, 0x00, 0x01]; + private static readonly byte[] s_d = [0x11, 0xB3, 0x61, 0xD6, 0x01, 0x4A, 0x10, 0x39, 0x0E, 0x43, 0x2D, 0x30, 0x82, 0x42, 0x75, 0x9B, 0x58, 0x1F, 0x94, 0xE7, 0x0B, 0xAB, 0xA7, 0x50, 0x34, 0xB8, 0x50, 0xC4, 0x8A, 0xB4, 0xD9, 0x28, 0x78, 0x78, 0xC6, 0x1A, 0xE5, 0x1E, 0x58, 0xE7, 0x82, 0x1D, 0x69, 0x66, 0xBA, 0xB6, 0x7C, 0xE8, 0x4E, 0x50, 0xCC, 0x72, 0x5F, 0x62, 0x38, 0xCC, 0xDF, 0xD9, 0xE0, 0x4E, 0x9B, 0x2A, 0xE4, 0x31, 0xF2, 0xA1, 0xC4, 0x33, 0x8C, 0x5E, 0xB2, 0x58, 0xC1, 0x0D, 0x0E, 0x0C, 0x96, 0xC0, 0x26, 0x39, 0xF8, 0x77, 0xE4, 0x88, 0x50, 0x31, 0xB1, 0x28, 0x71, 0x89, 0x3E, 0x00, 0x9E, 0x7D, 0x9F, 0x1A, 0x3C, 0xA5, 0x2C, 0x03, 0x1B, 0xAC, 0x9B, 0xEE, 0x74, 0xF2, 0x31, 0x2D, 0x3F, 0xF9, 0xA9, 0x38, 0xCC, 0x38, 0xAD, 0x84, 0xD6, 0xB1, 0x44, 0x09, 0x51, 0x6E, 0x7B, 0xC4, 0x11, 0xA9, 0xAB, 0xB9, 0x80, 0xD4, 0xA8, 0x87, 0xEF, 0x2C, 0x76, 0x51, 0x64, 0x3E, 0xE5, 0x37, 0x7F, 0xF1, 0x26, 0xD7, 0xC0, 0x80, 0xEB, 0x96, 0xA9, 0x1A, 0x4D, 0xB8, 0xAF, 0xC9, 0xE5, 0xA2, 0x43, 0x7A, 0x6C, 0xEB, 0x9F, 0xF3, 0x8A, 0x72, 0xDD, 0xE7, 0xD2, 0xEB, 0x60, 0xD6, 0x01, 0xDD, 0x25, 0x40, 0x8B, 0x71, 0xBA, 0x0D, 0x49, 0x55, 0x3A, 0x1D, 0x9B, 0x60, 0x71, 0x2C, 0x37, 0xAF, 0xFB, 0xD9, 0x3C, 0xEF, 0x77, 0xF1, 0x43, 0xE9, 0x7F, 0xE9, 0x2C, 0xDC, 0x76, 0xDC, 0x34, 0x48, 0xFF, 0x4F, 0xB4, 0xBE, 0x93, 0x82, 0xC3, 0x14, 0x42, 0xC6, 0xE4, 0x01, 0xF5, 0xB0, 0xDE, 0xED, 0x64, 0x82, 0xB9, 0x1C, 0x17, 0x0A, 0x28, 0x89, 0x66, 0xC2, 0x45, 0xA3, 0x4E, 0xBC, 0x61, 0xB4, 0xD7, 0xCC, 0x47, 0xDC, 0x86, 0x8E, 0x1F, 0xEE, 0x8E, 0x0D, 0x0F, 0x51, 0xB1, 0xAC, 0x62, 0xEF, 0xDC, 0x6D, 0x90, 0x81]; + private static readonly byte[] s_p = [0xFA, 0x63, 0x13, 0x19, 0xF3, 0x13, 0x7A, 0xC0, 0x70, 0x67, 0xAD, 0xE8, 0x05, 0x26, 0x70, 0x68, 0x9F, 0x30, 0xFB, 0x06, 0x9E, 0x45, 0xFF, 0x38, 0xF2, 0x56, 0xA3, 0x35, 0x4F, 0xAE, 0x21, 0x20, 0x94, 0x44, 0x80, 0x42, 0x61, 0x39, 0xC3, 0xEB, 0xA9, 0x8F, 0xD2, 0x61, 0x88, 0xE9, 0x6A, 0x62, 0x42, 0xF0, 0x18, 0x55, 0xDE, 0x50, 0x73, 0x1E, 0xFA, 0x70, 0xE5, 0x25, 0x73, 0x28, 0x6F, 0x29, 0x40, 0xE0, 0x09, 0x1A, 0xB6, 0x11, 0xF4, 0xB1, 0xEC, 0x89, 0x25, 0x66, 0x7D, 0x41, 0xC7, 0x53, 0xFA, 0x3A, 0x58, 0xF0, 0xB2, 0x27, 0x73, 0xE8, 0x63, 0x49, 0x06, 0x15, 0xF1, 0x4C, 0x35, 0x9D, 0x33, 0xE5, 0x28, 0x3E, 0xBF, 0x2F, 0x9F, 0xE4, 0x33, 0xE9, 0xCE, 0x6F, 0x91, 0x18, 0xAD, 0x95, 0xD4, 0xF5, 0x12, 0xD6, 0x92, 0xBD, 0xDA, 0xDF, 0x64, 0xA8, 0xB4, 0xEC, 0x7E, 0xD8, 0x1A, 0x41]; + private static readonly byte[] s_q = [0xC6, 0xC4, 0xF4, 0x19, 0x1A, 0x74, 0xFB, 0xA1, 0x07, 0x3D, 0x8A, 0x80, 0x9A, 0x6F, 0xC5, 0x60, 0xCB, 0x68, 0x32, 0x14, 0xCA, 0x9D, 0x21, 0x59, 0x7C, 0xA5, 0xAC, 0x0B, 0x68, 0x0E, 0x8C, 0xE2, 0x82, 0x77, 0x6C, 0xBC, 0xC2, 0xA6, 0xB3, 0xF2, 0x8B, 0xE4, 0xC8, 0xDF, 0xF9, 0x7B, 0x92, 0x82, 0xE6, 0x20, 0x84, 0xCB, 0xE0, 0x46, 0x44, 0x0A, 0xB4, 0xD7, 0x48, 0x43, 0x29, 0x78, 0xE7, 0xCA, 0xE4, 0x8F, 0xEC, 0x52, 0x2A, 0x6F, 0xDA, 0x58, 0x8D, 0xAA, 0xEB, 0xC0, 0x5E, 0x4F, 0xC1, 0x70, 0x9A, 0x97, 0xCE, 0xE4, 0xFF, 0x70, 0xEC, 0xA1, 0x2A, 0xED, 0xD8, 0x7A, 0xA1, 0xA5, 0x1C, 0x79, 0x2A, 0x8E, 0xBC, 0x74, 0xA3, 0x45, 0x20, 0x27, 0x14, 0xE1, 0xB9, 0x16, 0x80, 0x26, 0x81, 0xC2, 0x17, 0x26, 0xBB, 0x83, 0x04, 0x88, 0xFB, 0x0B, 0xB2, 0x06, 0x49, 0x88, 0x9A, 0xC0, 0x44, 0x6F]; + private static readonly byte[] s_dp = [0xA9, 0x1C, 0xD1, 0x81, 0xED, 0x53, 0x72, 0xCD, 0x17, 0x1E, 0x6F, 0xAF, 0x0E, 0x0B, 0x70, 0x50, 0xB9, 0x74, 0x73, 0x97, 0x7C, 0xBE, 0xFA, 0x2A, 0x94, 0x43, 0x3F, 0xE8, 0x79, 0xF0, 0xBA, 0x1F, 0x2F, 0x05, 0x35, 0x3B, 0xA0, 0x3F, 0x4B, 0xBC, 0x97, 0xC1, 0xB3, 0xEE, 0x7C, 0x72, 0x6A, 0x90, 0x03, 0x75, 0xF6, 0x79, 0xC8, 0xB5, 0xBD, 0x8B, 0x66, 0xCB, 0x69, 0x79, 0x69, 0xD6, 0x1D, 0x7E, 0x57, 0x7A, 0xF9, 0xAB, 0x38, 0xDD, 0xD7, 0xCF, 0x74, 0x5B, 0x0D, 0x0F, 0xB3, 0xFA, 0x01, 0x8F, 0x0E, 0xAA, 0xE9, 0xF7, 0x67, 0xDC, 0x8D, 0xC4, 0x9F, 0x8A, 0x6A, 0xB9, 0xD7, 0x48, 0x09, 0xCE, 0x2B, 0x86, 0xD3, 0x2F, 0xCE, 0x21, 0x74, 0x74, 0xC5, 0x3B, 0x5D, 0x5A, 0xAD, 0x3D, 0x65, 0xC0, 0x95, 0x1D, 0x11, 0xA4, 0x2B, 0x68, 0x5F, 0xB4, 0x12, 0x60, 0x80, 0x12, 0x5B, 0x79, 0xCA, 0x01]; + private static readonly byte[] s_dq = [0x99, 0x7F, 0xF2, 0x0E, 0x0C, 0xDB, 0x78, 0x93, 0x63, 0x97, 0x08, 0x05, 0xC6, 0xBE, 0x38, 0x4C, 0x46, 0xE9, 0x21, 0x7B, 0xE2, 0xF8, 0xB3, 0x8F, 0x7A, 0xEE, 0x2A, 0x4D, 0xE8, 0xBF, 0x0B, 0xD7, 0xC4, 0xEF, 0x5B, 0x3E, 0xEE, 0x87, 0x74, 0x82, 0x03, 0xBC, 0xDB, 0xCF, 0xF3, 0xC7, 0x95, 0x5E, 0x97, 0x3F, 0x57, 0xAE, 0x66, 0x75, 0x7D, 0x08, 0x53, 0x9D, 0xC9, 0x85, 0x6C, 0x5A, 0x9D, 0x8E, 0x97, 0x31, 0xFA, 0x4B, 0x7D, 0xD0, 0x41, 0x6B, 0x8F, 0x84, 0x06, 0x69, 0xD9, 0xAB, 0x77, 0xF4, 0x70, 0xBE, 0x79, 0x9D, 0x69, 0x13, 0x18, 0xFA, 0x61, 0xBF, 0xAA, 0x25, 0x11, 0xFE, 0x03, 0x5E, 0x23, 0x1A, 0x75, 0xB9, 0x11, 0x79, 0x49, 0x9A, 0x44, 0xCB, 0x08, 0x77, 0xB2, 0xE1, 0x89, 0xE5, 0x8C, 0xA1, 0x71, 0xBF, 0x7D, 0x5A, 0x17, 0x9D, 0x71, 0x8E, 0xDF, 0x6C, 0xAA, 0x2D, 0x7E, 0xEB]; + private static readonly byte[] s_iq = [0x2F, 0x91, 0x84, 0x58, 0xAB, 0x47, 0xAE, 0xE6, 0x92, 0xBC, 0xE4, 0x13, 0xBA, 0x9C, 0xDD, 0x66, 0xFF, 0x91, 0xA8, 0x94, 0x41, 0xBF, 0xD6, 0x54, 0x1B, 0x6D, 0x92, 0xC0, 0x67, 0xC6, 0x08, 0x23, 0xA4, 0x6D, 0x24, 0xA1, 0xC1, 0xBD, 0xCB, 0xC5, 0xAF, 0x4E, 0x96, 0x39, 0x46, 0x4A, 0x44, 0xD7, 0x10, 0x8F, 0x35, 0x76, 0xF5, 0xCB, 0x86, 0xE2, 0x9A, 0xAD, 0x32, 0xCD, 0x2C, 0xC0, 0xB8, 0x4A, 0x31, 0xB0, 0xAC, 0xF8, 0xFA, 0xAB, 0xEB, 0x0E, 0x8A, 0x7D, 0x92, 0x0D, 0xB5, 0x5C, 0x8B, 0x36, 0xD6, 0xC3, 0x56, 0x8F, 0x6B, 0xC0, 0x54, 0x5C, 0x8E, 0x83, 0x27, 0xEC, 0x21, 0x66, 0xC1, 0x5F, 0x44, 0x62, 0x33, 0xD3, 0xC7, 0x09, 0x01, 0xC8, 0xAB, 0x39, 0x6B, 0xC0, 0x1B, 0x35, 0x82, 0x43, 0x49, 0xF3, 0x42, 0x6B, 0x72, 0xA8, 0x1D, 0x31, 0x63, 0x3A, 0xF5, 0x4C, 0x3D, 0xE7, 0x3D, 0xD1]; } diff --git a/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.cs b/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.cs index 837c5ee2b..fc8d00e2a 100644 --- a/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.cs +++ b/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.cs @@ -25,12 +25,7 @@ internal sealed partial class SchemaProvider { public async ValueTask GetSchemaAsync(IOBehavior ioBehavior, string collectionName, string?[]? restrictionValues, CancellationToken cancellationToken) { - #if NET6_0_OR_GREATER ArgumentNullException.ThrowIfNull(collectionName); - #else - if (collectionName is null) - throw new ArgumentNullException(nameof(collectionName)); - #endif var dataTable = new DataTable(); """); From dfb6ad79500371e2bc201ec713de7ce0185014e5 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Mon, 6 Apr 2026 14:03:27 +0300 Subject: [PATCH 10/21] make it build --- .../Authentication/IAuthenticationPlugin.cs | 1 - src/SingleStoreConnector/Core/ILoadBalancer.cs | 3 +-- .../SingleStoreConnection.cs | 5 ++--- tests/SideBySide/ActivityTests.cs | 6 +++--- tests/SideBySide/DataTypes.cs | 5 ++--- tests/SideBySide/ParameterTests.cs | 16 +++++++--------- tests/SideBySide/TransactionScopeTests.cs | 1 - .../Metrics/ConnectionTimeTests.cs | 4 ++-- .../StatementPreparerTests.cs | 1 - 9 files changed, 17 insertions(+), 25 deletions(-) diff --git a/src/SingleStoreConnector/Authentication/IAuthenticationPlugin.cs b/src/SingleStoreConnector/Authentication/IAuthenticationPlugin.cs index 411525893..de3eb9114 100644 --- a/src/SingleStoreConnector/Authentication/IAuthenticationPlugin.cs +++ b/src/SingleStoreConnector/Authentication/IAuthenticationPlugin.cs @@ -21,7 +21,6 @@ public interface IAuthenticationPlugin byte[] CreateResponse(string password, ReadOnlySpan authenticationData); } - /// /// is an extension to that returns a hash of the client's password. /// diff --git a/src/SingleStoreConnector/Core/ILoadBalancer.cs b/src/SingleStoreConnector/Core/ILoadBalancer.cs index fd97f9566..6a5dd0837 100644 --- a/src/SingleStoreConnector/Core/ILoadBalancer.cs +++ b/src/SingleStoreConnector/Core/ILoadBalancer.cs @@ -34,7 +34,7 @@ public IReadOnlyList LoadBalance(IReadOnlyList hosts) return shuffled; #else var shuffled = new List(hosts); - // from https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm + //// from https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm for (var i = hosts.Count - 1; i >= 1; i--) { int j; @@ -53,7 +53,6 @@ private RandomLoadBalancer() m_lock = new(); } - private readonly Random m_random; #if NET9_0_OR_GREATER private readonly Lock m_lock; diff --git a/src/SingleStoreConnector/SingleStoreConnection.cs b/src/SingleStoreConnector/SingleStoreConnection.cs index 134a600be..fcb2e8e7a 100644 --- a/src/SingleStoreConnector/SingleStoreConnection.cs +++ b/src/SingleStoreConnector/SingleStoreConnection.cs @@ -1136,8 +1136,7 @@ private async ValueTask CreateSessionAsync(ConnectionPool? pool, // (from the connection string, if non-zero), or a combination of both if (connectionSettings.ConnectionTimeout != 0) timeoutSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(Math.Max(1, - connectionSettings.ConnectionTimeoutMilliseconds - - Utility.GetElapsedMilliseconds(startingTimestamp)))); + connectionSettings.ConnectionTimeoutMilliseconds - Utility.GetElapsedMilliseconds(startingTimestamp)))); if (cancellationToken.CanBeCanceled && timeoutSource is not null) linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token); @@ -1204,7 +1203,7 @@ private async ValueTask CreateSessionAsync(ConnectionPool? pool, internal IPEndPoint? SessionEndPoint => m_session!.IPEndPoint; - internal SingleStoreDataSource? MySqlDataSource => m_dataSource; + internal SingleStoreDataSource? SingleStoreDataSource => m_dataSource; internal void SetState(ConnectionState newState) { diff --git a/tests/SideBySide/ActivityTests.cs b/tests/SideBySide/ActivityTests.cs index ccbd78beb..59aa2f122 100644 --- a/tests/SideBySide/ActivityTests.cs +++ b/tests/SideBySide/ActivityTests.cs @@ -145,7 +145,7 @@ public void SelectTags() [InlineData(false)] public void ReadResultSetHeaderEvent(bool enableEvent) { - var dataSourceBuilder = new MySqlDataSourceBuilder(AppConfig.ConnectionString) + var dataSourceBuilder = new SingleStoreDataSourceBuilder(AppConfig.ConnectionString) .ConfigureTracing(o => o.EnableResultSetHeaderEvent(enableEvent)); using var dataSource = dataSourceBuilder.Build(); using var connection = dataSource.OpenConnection(); @@ -156,14 +156,14 @@ public void ReadResultSetHeaderEvent(bool enableEvent) Activity activity = null; using var listener = new ActivityListener { - ShouldListenTo = x => x.Name == "MySqlConnector", + ShouldListenTo = x => x.Name == "SingleStoreConnector", Sample = (ref ActivityCreationOptions options) => options.TraceId == parentActivity.TraceId ? ActivitySamplingResult.AllData : ActivitySamplingResult.None, ActivityStopped = x => activity = x, }; ActivitySource.AddActivityListener(listener); - using (var command = new MySqlCommand("SELECT 1;", connection)) + using (var command = new SingleStoreCommand("SELECT 1;", connection)) { command.ExecuteScalar(); } diff --git a/tests/SideBySide/DataTypes.cs b/tests/SideBySide/DataTypes.cs index 38d1e92fd..8ae42a22f 100644 --- a/tests/SideBySide/DataTypes.cs +++ b/tests/SideBySide/DataTypes.cs @@ -445,9 +445,9 @@ public async Task QueryWithGuidParameter(bool oldGuids) Assert.Equal(oldGuids ? 0L : 1L, (await connection.QueryAsync(@"select count(*) from datatypes_strings where guid = @guid", new { guid = new Guid("fd24a0e8-c3f2-4821-a456-35da2dc4bb8f") }).ConfigureAwait(false)).SingleOrDefault()); Assert.Equal(oldGuids ? 0L : 1L, (await connection.QueryAsync(@"select count(*) from datatypes_strings where guidbin = @guid", new { guid = new Guid("fd24a0e8-c3f2-4821-a456-35da2dc4bb8f") }).ConfigureAwait(false)).SingleOrDefault()); } - catch (SingleStoreException ex) when (oldGuids && ex.Number is 1300 or 3854) // InvalidCharacterString, CannotConvertString + //// 1300 = InvalidCharacterString, 3854 = CannotConvertString. + catch (SingleStoreException ex) when (oldGuids && ex.Number is 1300 or 3854) { - // new error in MySQL 8.0.24, MariaDB 10.5 } Assert.Equal(oldGuids ? 1L : 0L, (await connection.QueryAsync(@"select count(*) from datatypes_blobs where guidbin = @guid", new { guid = new Guid(0x33221100, 0x5544, 0x7766, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF) }).ConfigureAwait(false)).SingleOrDefault()); } @@ -1491,7 +1491,6 @@ private void DoGetColumnSchema(string column, string table, SingleStoreDbType my [InlineData("MediumDecimal", "datatypes_reals", "DECIMAL(28,8)", typeof(decimal), 3, null)] [InlineData("BigDecimal", "datatypes_reals", "DECIMAL(50,30)", typeof(decimal), 3, null)] [InlineData("utf8", "datatypes_strings", "VARCHAR(300)", typeof(string), 3, "ASCII")] - [InlineData("nonguid_utf8", "datatypes_strings", SingleStoreDbType.VarChar, "VARCHAR(36)", typeof(string), 3, "ASCII")] [InlineData("Date", "datatypes_times", "DATE", typeof(DateTime), 2, null)] [InlineData("DateTime", "datatypes_times", "DATETIME", typeof(DateTime), 2, null)] [InlineData("Timestamp", "datatypes_times", "TIMESTAMP", typeof(DateTime), 2, null)] diff --git a/tests/SideBySide/ParameterTests.cs b/tests/SideBySide/ParameterTests.cs index 727635590..3579d463c 100644 --- a/tests/SideBySide/ParameterTests.cs +++ b/tests/SideBySide/ParameterTests.cs @@ -31,7 +31,7 @@ public void DbTypeToSingleStoreDbType(DbType dbType, SingleStoreDbType mySqlDbTy [InlineData(new[] { DbType.Date }, new[] { SingleStoreDbType.Date, SingleStoreDbType.Newdate })] #if !BASELINE [InlineData(new[] { DbType.Int32 }, new[] { SingleStoreDbType.Int32, SingleStoreDbType.Year })] - [InlineData(new[] { DbType.Binary }, new[] { SingleStoreDbType.Blob, SingleStoreDbType.Binary, SingleStoreDbType.TinyBlob, SingleStoreDbType.MediumBlob, SingleStoreDbType.LongBlob})] + [InlineData(new[] { DbType.Binary }, new[] { SingleStoreDbType.Blob, SingleStoreDbType.Binary, SingleStoreDbType.TinyBlob, SingleStoreDbType.MediumBlob, SingleStoreDbType.LongBlob })] [InlineData(new[] { DbType.String, DbType.AnsiString, DbType.Xml }, new[] { SingleStoreDbType.VarChar, SingleStoreDbType.VarString, SingleStoreDbType.Text, SingleStoreDbType.TinyText, SingleStoreDbType.MediumText, SingleStoreDbType.LongText, SingleStoreDbType.JSON, SingleStoreDbType.Enum, SingleStoreDbType.Set, SingleStoreDbType.Geography, SingleStoreDbType.GeographyPoint })] [InlineData(new[] { DbType.Decimal, DbType.Currency }, new[] { SingleStoreDbType.NewDecimal, SingleStoreDbType.Decimal })] @@ -333,7 +333,6 @@ public void SetValueToByteArrayInfersType() Assert.Equal(SingleStoreDbType.Blob, parameter.SingleStoreDbType); } - [Fact] public void SetValueDoesNotInferType() { @@ -457,13 +456,12 @@ PRIMARY KEY (`Id`) INSERT INTO zeroByteEscaping VALUES(1, BINARY('\012\0\0')); "); - using (var command = new SingleStoreCommand(@"CREATE TABLE zeroByteEscapingCTAS as SELECT * FROM zeroByteEscaping WHERE Content=@content", connection)) - { - command.Parameters.AddWithValue("@content", new byte[] {0x00, 0x31, 0x32, 0x00, 0x00}); - Assert.False(command.IsPrepared); - command.ExecuteNonQuery(); - } - + using (var command = new SingleStoreCommand(@"CREATE TABLE zeroByteEscapingCTAS as SELECT * FROM zeroByteEscaping WHERE Content=@content", connection)) + { + command.Parameters.AddWithValue("@content", new byte[] { 0x00, 0x31, 0x32, 0x00, 0x00 }); + Assert.False(command.IsPrepared); + command.ExecuteNonQuery(); + } using (var command = new SingleStoreCommand(@"SELECT COUNT(*) FROM `zeroByteEscapingCTAS` WHERE BINARY(`Content`) = 0x0031320000", connection)) { diff --git a/tests/SideBySide/TransactionScopeTests.cs b/tests/SideBySide/TransactionScopeTests.cs index daf085497..cbd60c633 100644 --- a/tests/SideBySide/TransactionScopeTests.cs +++ b/tests/SideBySide/TransactionScopeTests.cs @@ -883,4 +883,3 @@ public void ConnectionOpenedCallbackAutoEnlistInTransaction() private DatabaseFixture m_database; } - diff --git a/tests/SingleStoreConnector.Tests/Metrics/ConnectionTimeTests.cs b/tests/SingleStoreConnector.Tests/Metrics/ConnectionTimeTests.cs index e4eb6a02a..ae803986b 100644 --- a/tests/SingleStoreConnector.Tests/Metrics/ConnectionTimeTests.cs +++ b/tests/SingleStoreConnector.Tests/Metrics/ConnectionTimeTests.cs @@ -11,7 +11,7 @@ public async Task ConnectionTime() await connection.OpenAsync(); var measurements = GetAndClearMeasurements("db.client.connections.create_time"); var time = Assert.Single(measurements); - // adjusted the highest value for S2MS + //// adjusted the highest value for S2MS Assert.InRange(time, 0, 1300); } @@ -26,7 +26,7 @@ public async Task ConnectionTimeWithDelay() await connection.OpenAsync(); var measurements = GetAndClearMeasurements("db.client.connections.create_time"); var time = Assert.Single(measurements); - // adjusted the highest value for S2MS + //// adjusted the highest value for S2MS Assert.InRange(time, 1000, 2300); } diff --git a/tests/SingleStoreConnector.Tests/StatementPreparerTests.cs b/tests/SingleStoreConnector.Tests/StatementPreparerTests.cs index 9a543ff96..a5b34e126 100644 --- a/tests/SingleStoreConnector.Tests/StatementPreparerTests.cs +++ b/tests/SingleStoreConnector.Tests/StatementPreparerTests.cs @@ -209,7 +209,6 @@ [new MemoryStream([0, 0x41, 0x42, 0x27, 0x61, 0x5c, 0x62, 0x63], 1, 6, false, tr [new StringBuilder("\"AB\\ab'"), @"'""AB\\ab'''"], }; - [Theory] [InlineData(StatementPreparerOptions.GuidFormatChar36, "'61626364-6566-6768-696a-6b6c6d6e6f70'")] [InlineData(StatementPreparerOptions.GuidFormatChar32, "'6162636465666768696a6b6c6d6e6f70'")] From 18c172b956b9977333a72f73eb74b10364c28562 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Mon, 6 Apr 2026 14:10:32 +0300 Subject: [PATCH 11/21] fix dotnet version in CI --- .github/workflows/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 8529f6c76..ffb5385d0 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: env: - DOTNET_VERSION: 10.0.5 + DOTNET_VERSION: 10.0.x TARGET_FRAMEWORK: net10.0 CONNECTOR_VERSION: 1.4.0 LICENSE_KEY: ${{ secrets.LICENSE_KEY }} From 77d534020797b0ef30949388c7797084f37e31b0 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Mon, 6 Apr 2026 14:29:05 +0300 Subject: [PATCH 12/21] update config.yml --- .github/workflows/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index ffb5385d0..cd7ee699f 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -5,6 +5,8 @@ on: types: [opened, synchronize, reopened] push: branches: [main] + tags: + - 'v*' workflow_call: workflow_dispatch: @@ -90,11 +92,9 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y apt-transport-https - sudo apt-get install -y mariadb-client-core-10.6 - sudo apt-get install -y mariadb-client-10.6 + sudo apt-get install -y mariadb-client-core + sudo apt-get install -y mariadb-client sudo apt-get update - sudo apt-get install -y dotnet-sdk-10.0 dotnet --info - name: Start SingleStore Cluster From 5ef8f819622c7f6007abd73d29391468eb4d96dd Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Mon, 6 Apr 2026 14:44:32 +0300 Subject: [PATCH 13/21] update Directory.Build.props --- Directory.Build.props | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 1fed69cf4..cbf2f0e6d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,10 @@ true 14.0 true - $(NoWarn);1591;CA1708;CA1835;CA2215;CA5397;NU5105;SYSLIB0039 + + true + $(NoWarn);15731591;CA1708;CA1835;CA2215;CA5397;NU5105;SYSLIB0039 + $(MSBuildThisFileDirectory)artifacts true low From 45e9cbd1c70bc20987ef8e94310b346bb79567ac Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Mon, 6 Apr 2026 14:57:55 +0300 Subject: [PATCH 14/21] fix typo --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index cbf2f0e6d..6621f4f4f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ true true - $(NoWarn);15731591;CA1708;CA1835;CA2215;CA5397;NU5105;SYSLIB0039 + $(NoWarn);1573;1591;1712;CA1708;CA1835;CA2215;CA5397;NU5105;SYSLIB0039 $(MSBuildThisFileDirectory)artifacts true From a007803d1f5d0e8cc3d6144a50588e211c666349 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Mon, 6 Apr 2026 15:37:56 +0300 Subject: [PATCH 15/21] fix SingleStoreConnector.Tests and resolve Vector fixture creation --- tests/SideBySide/DataTypesFixture.cs | 28 +++++++++---------- .../ConnectionTests.cs | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/SideBySide/DataTypesFixture.cs b/tests/SideBySide/DataTypesFixture.cs index 89ca9f817..583742a58 100644 --- a/tests/SideBySide/DataTypesFixture.cs +++ b/tests/SideBySide/DataTypesFixture.cs @@ -240,23 +240,21 @@ insert into datatypes_json_core (value) if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Vector)) { - /* create a helper function for MariaDB 11.7+ - if (Connection.ServerVersion.StartsWith("11.8.", StringComparison.Ordinal)) - Connection.Execute("create function if not exists STRING_TO_VECTOR(s text) returns vector(3) deterministic return Vec_FromText(s);");*/ - Connection.Execute(""" - drop table if exists datatypes_vector; - create table datatypes_vector ( - rowid bigint not null primary key auto_increment, - value vector(3) null + DROP TABLE IF EXISTS datatypes_vector; + + CREATE TABLE datatypes_vector ( + rowid BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, + value VECTOR(3) NULL ); - insert into datatypes_vector (value) - values - (null), - (STRING_TO_VECTOR('[0, 0, 0]')), - (STRING_TO_VECTOR('[1, 1, 1]')), - (STRING_TO_VECTOR('[1, 2, 3]')), - (STRING_TO_VECTOR('[-1, -1, -1]')); + + INSERT INTO datatypes_vector (value) + VALUES + (NULL), + ('[0, 0, 0]'), + ('[1, 1, 1]'), + ('[1, 2, 3]'), + ('[-1, -1, -1]'); """); } diff --git a/tests/SingleStoreConnector.Tests/ConnectionTests.cs b/tests/SingleStoreConnector.Tests/ConnectionTests.cs index 3cdab4fc7..45da9d555 100644 --- a/tests/SingleStoreConnector.Tests/ConnectionTests.cs +++ b/tests/SingleStoreConnector.Tests/ConnectionTests.cs @@ -277,7 +277,7 @@ public void ReplaceActiveReader() connection.Close(); } - [Fact] + [Fact(Skip = "Resetting connection is not supported in SingleStore")] public async Task ResetServerConnectionWhileOpen() { var csb = new SingleStoreConnectionStringBuilder(m_csb.ConnectionString) From 501f031ef4f517f65125d27f80e2c7805f8979c3 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Wed, 22 Apr 2026 15:41:19 +0300 Subject: [PATCH 16/21] get rid of the specific MySQL protocol mechanism (COM_STMT_SEND_LONG_DATA) --- .../Core/CommandExecutor.cs | 2 - .../Core/ConcatenatedCommandPayloadCreator.cs | 3 - .../Core/ICommandPayloadCreator.cs | 10 -- .../Core/SingleCommandPayloadCreator.cs | 75 ----------- .../Protocol/CommandKind.cs | 1 - .../SingleStoreDataReader.cs | 4 +- tests/SideBySide/InsertTests.cs | 124 ------------------ tests/SideBySide/StoredProcedureTests.cs | 83 ++++++------ 8 files changed, 45 insertions(+), 257 deletions(-) diff --git a/src/SingleStoreConnector/Core/CommandExecutor.cs b/src/SingleStoreConnector/Core/CommandExecutor.cs index b80eeda9a..a8418f4c4 100644 --- a/src/SingleStoreConnector/Core/CommandExecutor.cs +++ b/src/SingleStoreConnector/Core/CommandExecutor.cs @@ -39,8 +39,6 @@ public static async ValueTask ExecuteReaderAsync(CommandL } } - await payloadCreator.SendCommandPrologueAsync(connection, commandListPosition, ioBehavior, cancellationToken).ConfigureAwait(false); - var writer = new ByteBufferWriter(); //// cachedProcedures will be non-null if there is a stored procedure, which is also the only time it will be read if (!payloadCreator.WriteQueryCommand(ref commandListPosition, cachedProcedures!, writer, false)) diff --git a/src/SingleStoreConnector/Core/ConcatenatedCommandPayloadCreator.cs b/src/SingleStoreConnector/Core/ConcatenatedCommandPayloadCreator.cs index 1c704798a..e628e1064 100644 --- a/src/SingleStoreConnector/Core/ConcatenatedCommandPayloadCreator.cs +++ b/src/SingleStoreConnector/Core/ConcatenatedCommandPayloadCreator.cs @@ -8,9 +8,6 @@ internal sealed class ConcatenatedCommandPayloadCreator : ICommandPayloadCreator { public static ICommandPayloadCreator Instance { get; } = new ConcatenatedCommandPayloadCreator(); - public ValueTask SendCommandPrologueAsync(SingleStoreConnection connection, CommandListPosition commandListPosition, IOBehavior ioBehavior, CancellationToken cancellationToken) => - default; - public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDictionary cachedProcedures, ByteBufferWriter writer, bool appendSemicolon) { if (commandListPosition.CommandIndex == commandListPosition.CommandCount) diff --git a/src/SingleStoreConnector/Core/ICommandPayloadCreator.cs b/src/SingleStoreConnector/Core/ICommandPayloadCreator.cs index 9958016b0..43ec66a15 100644 --- a/src/SingleStoreConnector/Core/ICommandPayloadCreator.cs +++ b/src/SingleStoreConnector/Core/ICommandPayloadCreator.cs @@ -7,16 +7,6 @@ namespace SingleStoreConnector.Core; /// internal interface ICommandPayloadCreator { - /// - /// Sends any data that is required to be sent to the server before the current command in the command list. - /// - /// The to which the data will be written. - /// The giving the current command and current prepared statement. - /// The IO behavior. - /// A cancellation token to cancel the asynchronous operation. - /// A representing the asynchronous operation or a completed if no data needed to be sent. - ValueTask SendCommandPrologueAsync(SingleStoreConnection connection, CommandListPosition commandListPosition, IOBehavior ioBehavior, CancellationToken cancellationToken); - /// /// Writes the payload for an "execute query" command to . /// diff --git a/src/SingleStoreConnector/Core/SingleCommandPayloadCreator.cs b/src/SingleStoreConnector/Core/SingleCommandPayloadCreator.cs index a1c17579b..13c257c8c 100644 --- a/src/SingleStoreConnector/Core/SingleCommandPayloadCreator.cs +++ b/src/SingleStoreConnector/Core/SingleCommandPayloadCreator.cs @@ -15,81 +15,6 @@ internal sealed class SingleCommandPayloadCreator : ICommandPayloadCreator // with this as the first column name, the result set will be treated as 'out' parameters for the previous command. public static string OutParameterSentinelColumnName => "\uE001\b\x0B"; - public async ValueTask SendCommandPrologueAsync(SingleStoreConnection connection, CommandListPosition commandListPosition, IOBehavior ioBehavior, CancellationToken cancellationToken) - { - // get the current command and check for prepared statements - var command = commandListPosition.CommandAt(commandListPosition.CommandIndex); - var preparedStatements = commandListPosition.PreparedStatements ?? command.TryGetPreparedStatements(); - if (preparedStatements is not null) - { - // get the current prepared statement; WriteQueryCommand will advance this - var preparedStatement = preparedStatements.Statements[commandListPosition.PreparedStatementIndex]; - if (preparedStatement.Parameters is { } parameters) - { - byte[]? buffer = null; - try - { - // check each parameter - for (var i = 0; i < parameters.Length; i++) - { - // look up this parameter in the command's parameter collection and check if it is a Stream - // NOTE: full parameter checks will be performed (and throw any necessary exceptions) in WritePreparedStatement - var parameterName = preparedStatement.Statement.NormalizedParameterNames![i]; - var parameterIndex = parameterName is not null ? (command.RawParameters?.UnsafeIndexOf(parameterName) ?? -1) : preparedStatement.Statement.ParameterIndexes[i]; - if (command.RawParameters is { } rawParameters && parameterIndex >= 0 && parameterIndex < rawParameters.Count && rawParameters[parameterIndex] is { Value: Stream stream and not MemoryStream }) - { - // seven-byte COM_STMT_SEND_LONG_DATA header: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_stmt_send_long_data.html - const int packetHeaderLength = 7; - - // send almost-full packets, but don't send exactly ProtocolUtility.MaxPacketSize bytes in one payload (as that's ambiguous to whether another packet follows). - const int maxDataSize = 16_000_000; - int totalBytesRead; - while (true) - { - buffer ??= ArrayPool.Shared.Rent(packetHeaderLength + maxDataSize); - buffer[0] = (byte) CommandKind.StatementSendLongData; - BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(1), preparedStatement.StatementId); - BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(5), (ushort) i); - - // keep reading from the stream until we've filled the buffer to send -#if NET7_0_OR_GREATER - if (ioBehavior == IOBehavior.Synchronous) - totalBytesRead = stream.ReadAtLeast(buffer.AsSpan(packetHeaderLength, maxDataSize), maxDataSize, throwOnEndOfStream: false); - else - totalBytesRead = await stream.ReadAtLeastAsync(buffer.AsMemory(packetHeaderLength, maxDataSize), maxDataSize, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false); -#else - totalBytesRead = 0; - int bytesRead; - do - { - var sizeToRead = maxDataSize - totalBytesRead; - if (ioBehavior == IOBehavior.Synchronous) - bytesRead = stream.Read(buffer, packetHeaderLength + totalBytesRead, sizeToRead); - else - bytesRead = await stream.ReadAsync(buffer, packetHeaderLength + totalBytesRead, sizeToRead, cancellationToken).ConfigureAwait(false); - totalBytesRead += bytesRead; - } while (bytesRead > 0 && totalBytesRead < maxDataSize); -#endif - - if (totalBytesRead == 0) - break; - - // send StatementSendLongData; MySQL Server will keep appending the sent data to the parameter value - using var payload = new PayloadData(buffer.AsMemory(0, packetHeaderLength + totalBytesRead), isPooled: false); - await connection.Session.SendAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); - } - } - } - } - finally - { - if (buffer is not null) - ArrayPool.Shared.Return(buffer); - } - } - } - } - public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDictionary cachedProcedures, ByteBufferWriter writer, bool appendSemicolon) { if (commandListPosition.CommandIndex == commandListPosition.CommandCount) diff --git a/src/SingleStoreConnector/Protocol/CommandKind.cs b/src/SingleStoreConnector/Protocol/CommandKind.cs index 1edf7414f..510045d48 100644 --- a/src/SingleStoreConnector/Protocol/CommandKind.cs +++ b/src/SingleStoreConnector/Protocol/CommandKind.cs @@ -9,6 +9,5 @@ internal enum CommandKind ChangeUser = 17, StatementPrepare = 22, StatementExecute = 23, - StatementSendLongData = 24, ResetConnection = 31, } diff --git a/src/SingleStoreConnector/SingleStoreDataReader.cs b/src/SingleStoreConnector/SingleStoreDataReader.cs index 252262efc..5ac369926 100644 --- a/src/SingleStoreConnector/SingleStoreDataReader.cs +++ b/src/SingleStoreConnector/SingleStoreDataReader.cs @@ -74,10 +74,8 @@ internal async Task NextResultAsync(IOBehavior ioBehavior, CancellationTok Command = m_commandListPosition.CommandAt(m_commandListPosition.CommandIndex); using (Command.CancellableCommand.RegisterCancel(cancellationToken)) { - await m_payloadCreator!.SendCommandPrologueAsync(Command.Connection!, m_commandListPosition, ioBehavior, cancellationToken).ConfigureAwait(false); - var writer = new ByteBufferWriter(); - if (!Command.Connection!.Session.IsCancelingQuery && m_payloadCreator.WriteQueryCommand(ref m_commandListPosition, m_cachedProcedures!, writer, false)) + if (!Command.Connection!.Session.IsCancelingQuery && m_payloadCreator!.WriteQueryCommand(ref m_commandListPosition, m_cachedProcedures!, writer, false)) { using var payload = writer.ToPayloadData(); await Command.Connection.Session.SendAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false); diff --git a/tests/SideBySide/InsertTests.cs b/tests/SideBySide/InsertTests.cs index 2a2353c5e..28fd3509d 100644 --- a/tests/SideBySide/InsertTests.cs +++ b/tests/SideBySide/InsertTests.cs @@ -326,130 +326,6 @@ public void InsertSingleStoreDecimalAsDecimal(bool prepare) var val = ((decimal) reader.GetValue(0)).ToString(CultureInfo.InvariantCulture); Assert.Equal(value, val); } - - [Theory] - [InlineData(1_000_000, 1024, true)] - [InlineData(1_000_000, 1024, false)] - [InlineData(1_000_000, int.MaxValue, true)] - [InlineData(1_000_000, int.MaxValue, false)] - [InlineData(0xff_fff8, 299593, true)] - [InlineData(0xff_fff8, 299593, false)] - [InlineData(0xff_fff8, 300000, true)] - [InlineData(0xff_fff8, 300000, false)] - [InlineData(0xff_fff8, int.MaxValue, true)] - [InlineData(0xff_fff8, int.MaxValue, false)] - [InlineData(0xff_fff9, int.MaxValue, true)] - [InlineData(0xff_fff9, int.MaxValue, false)] - [InlineData(0x1ff_fff0, 299593, true)] - [InlineData(0x1ff_fff0, 299593, false)] - [InlineData(0x1ff_fff0, 300000, true)] - [InlineData(0x1ff_fff0, 300000, false)] - [InlineData(15_999_999, int.MaxValue, true)] - [InlineData(15_999_999, int.MaxValue, false)] - [InlineData(16_000_000, int.MaxValue, true)] - [InlineData(16_000_000, int.MaxValue, false)] - [InlineData(16_000_001, int.MaxValue, true)] - [InlineData(16_000_001, int.MaxValue, false)] - [InlineData(31_999_999, 999_999, true)] - [InlineData(31_999_999, 1_000_000, false)] - [InlineData(32_000_000, 1_000_001, true)] - [InlineData(32_000_000, 1_000_002, false)] - [InlineData(32_000_001, 1_000_003, true)] - [InlineData(32_000_001, 1_000_004, false)] - public async Task SendLongData(int dataLength, int chunkLength, bool isAsync) - { - using SingleStoreConnection connection = new SingleStoreConnection(AppConfig.ConnectionString); - connection.Open(); - connection.Execute(""" - drop table if exists insert_mysql_long_data; - create table insert_mysql_long_data(rowid bigint not null primary key auto_increment, value longblob); - """); - - var random = new Random(dataLength); - var data = new byte[dataLength]; - random.NextBytes(data); - - using var chunkStream = new ChunkStream(data, chunkLength); - - using var writeCommand = new SingleStoreCommand(""" - insert into insert_mysql_long_data(value) values(@value); - select length(value) from insert_mysql_long_data order by rowid; - """, connection); - writeCommand.Parameters.AddWithValue("@value", chunkStream); - writeCommand.Prepare(); - using (var reader = isAsync ? await writeCommand.ExecuteReaderAsync().ConfigureAwait(true) : writeCommand.ExecuteReader()) - { - Assert.True(reader.Read()); - Assert.Equal(1, reader.FieldCount); - Assert.Equal(dataLength, reader.GetInt32(0)); - Assert.False(reader.Read()); - } - - using var readCommand = new SingleStoreCommand("select value from insert_mysql_long_data order by rowid;", connection); - using (var reader = readCommand.ExecuteReader()) - { - Assert.True(reader.Read()); - var readData = (byte[]) reader.GetValue(0); - Assert.True(data.AsSpan().SequenceEqual(readData)); // much faster than Assert.Equal - } - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task SendLongDataMultipleStatements(bool isAsync) - { - using SingleStoreConnection connection = new SingleStoreConnection(AppConfig.ConnectionString); - connection.Open(); - connection.Execute(""" - drop table if exists insert_mysql_long_data; - create table insert_mysql_long_data(rowid bigint not null primary key auto_increment, value longblob); - """); - - var data1 = new byte[1000]; - var data2 = new byte[2000]; - var data3 = new byte[3000]; - var random = new Random(1); - random.NextBytes(data1); - random.NextBytes(data2); - random.NextBytes(data3); - - using var chunkStream1 = new ChunkStream(data1, int.MaxValue); - using var chunkStream2 = new ChunkStream(data2, int.MaxValue); - using var chunkStream3 = new ChunkStream(data3, int.MaxValue); - - using var writeCommand = new SingleStoreCommand(""" - insert into insert_mysql_long_data(rowid, value) values(1, @value1); - insert into insert_mysql_long_data(rowid, value) values(2, @value2); - insert into insert_mysql_long_data(rowid, value) values(3, @value3); - """, connection); - writeCommand.Parameters.AddWithValue("@value1", chunkStream1); - writeCommand.Parameters.AddWithValue("@value2", chunkStream2); - writeCommand.Parameters.AddWithValue("@value3", chunkStream3); - writeCommand.Prepare(); - if (isAsync) - await writeCommand.ExecuteNonQueryAsync(); - else - writeCommand.ExecuteNonQuery(); - - using var readCommand = new SingleStoreCommand("select value from insert_mysql_long_data order by rowid;", connection); - using (var reader = readCommand.ExecuteReader()) - { - Assert.True(reader.Read()); - var readData = (byte[]) reader.GetValue(0); - Assert.True(data1.AsSpan().SequenceEqual(readData)); - - Assert.True(reader.Read()); - readData = (byte[]) reader.GetValue(0); - Assert.True(data2.AsSpan().SequenceEqual(readData)); - - Assert.True(reader.Read()); - readData = (byte[]) reader.GetValue(0); - Assert.True(data3.AsSpan().SequenceEqual(readData)); - - Assert.False(reader.Read()); - } - } #endif [Theory] diff --git a/tests/SideBySide/StoredProcedureTests.cs b/tests/SideBySide/StoredProcedureTests.cs index 25d65e31b..7f5fec0e2 100644 --- a/tests/SideBySide/StoredProcedureTests.cs +++ b/tests/SideBySide/StoredProcedureTests.cs @@ -782,55 +782,60 @@ public void SprocNameSpecialCharacters(string sprocName) #if !BASELINE [Theory] - [InlineData(SingleStoreGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", false, false)] - [InlineData(SingleStoreGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", false, true)] - [InlineData(SingleStoreGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", true, false)] - [InlineData(SingleStoreGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", true, true)] - [InlineData(SingleStoreGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", false, false)] - [InlineData(SingleStoreGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", false, true)] - [InlineData(SingleStoreGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", true, false)] - [InlineData(SingleStoreGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", true, true)] - [InlineData(SingleStoreGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", false, false)] - [InlineData(SingleStoreGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", false, true)] - [InlineData(SingleStoreGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", true, false)] - [InlineData(SingleStoreGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", true, true)] - public void StoredProcedureReturnsGuid(SingleStoreGuidFormat guidFormat, string columnDefinition, string columnValue, bool setMySqlDbType, bool prepare) + [InlineData(SingleStoreGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", false)] + [InlineData(SingleStoreGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", true)] + [InlineData(SingleStoreGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", false)] + [InlineData(SingleStoreGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", true)] + [InlineData(SingleStoreGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", false)] + [InlineData(SingleStoreGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", true)] + public void StoredProcedureReturnsGuid(SingleStoreGuidFormat guidFormat, string columnDefinition, string columnValue, bool prepare) { + if (prepare) + { + return; // SingleStore does not currently support preparing ECHO proc() + } + var csb = AppConfig.CreateConnectionStringBuilder(); csb.GuidFormat = guidFormat; csb.Pooling = false; + using var connection = new SingleStoreConnection(csb.ConnectionString); connection.Open(); - using (var command = new SingleStoreCommand($""" - DROP TABLE IF EXISTS out_guid_table; - CREATE TABLE out_guid_table (id INT PRIMARY KEY AUTO_INCREMENT, guid {columnDefinition}); - INSERT INTO out_guid_table (guid) VALUES ({columnValue}); - DROP PROCEDURE IF EXISTS out_guid; - CREATE PROCEDURE out_guid - ( - OUT out_name {columnDefinition} - ) - BEGIN - SELECT guid INTO out_name FROM out_guid_table; - END; - """, connection)) + using (var setup = new SingleStoreCommand($""" + DROP TABLE IF EXISTS out_guid_table; + CREATE TABLE out_guid_table + ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + guid {columnDefinition} + ); + INSERT INTO out_guid_table (guid) VALUES ({columnValue}); + + DROP PROCEDURE IF EXISTS out_guid; + CREATE PROCEDURE out_guid() RETURNS {columnDefinition} AS + DECLARE g {columnDefinition}; + BEGIN + SELECT guid INTO g + FROM out_guid_table + LIMIT 1; + + RETURN g; + END; + """, connection)) { - command.ExecuteNonQuery(); + setup.ExecuteNonQuery(); } - using (var command = new SingleStoreCommand("out_guid", connection)) - { - command.CommandType = CommandType.StoredProcedure; - var param = new SingleStoreParameter("out_name", null) { Direction = ParameterDirection.Output }; - if (setMySqlDbType) - param.SingleStoreDbType = SingleStoreDbType.Guid; - command.Parameters.Add(param); - command.ExecuteNonQuery(); - if (prepare) - command.Prepare(); - Assert.Equal(new Guid("BABD8384C908499C9D95C02ADA94A970"), param.Value); - } + using var command = new SingleStoreCommand("ECHO out_guid()", connection); + + if (prepare) + command.Prepare(); + + var value = command.ExecuteScalar(); + + Assert.Equal( + new Guid("BABD8384-C908-499C-9D95-C02ADA94A970"), + Assert.IsType(value)); } #endif From 78aee2ef188f56ad346981424c12c8a616bc46d0 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Wed, 22 Apr 2026 15:49:50 +0300 Subject: [PATCH 17/21] resolve issue with log4net package --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 21f04bb41..f579e9895 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - + From 85899abf663c0fdca377695dc68660e1d79b710e Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Thu, 23 Apr 2026 21:28:12 +0300 Subject: [PATCH 18/21] minor fixes --- .github/workflows/SideBySide/config-ssl.json | 2 +- .github/workflows/config.yml | 2 +- tests/SideBySide/BatchTests.cs | 3 +- tests/SideBySide/CancelTests.cs | 36 +++++++------------- tests/SideBySide/CommandTimeoutTests.cs | 18 ++++------ tests/SideBySide/ConnectAsync.cs | 6 ++-- 6 files changed, 23 insertions(+), 44 deletions(-) diff --git a/.github/workflows/SideBySide/config-ssl.json b/.github/workflows/SideBySide/config-ssl.json index 9f0c10097..5c1424c02 100644 --- a/.github/workflows/SideBySide/config-ssl.json +++ b/.github/workflows/SideBySide/config-ssl.json @@ -1,7 +1,7 @@ { "Data": { "ConnectionString": "server=SINGLESTORE_HOST;user id=SQL_USER_NAME;password=SQL_USER_PASSWORD;port=3306;database=singlestoretest;sslmode=Required;certificate file=../../../../.ci/server/certs/ssl-client.pfx;", - "UnsupportedFeatures": "CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin,UnixDomainSocket,Sha256Password,GlobalLog,Redirection,CancelSleepSuccessfully,TlsFingerprintValidation,Vector", + "UnsupportedFeatures": "CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin,UnixDomainSocket,Sha256Password,GlobalLog,Redirection,CancelSleepSuccessfully,TlsFingerprintValidation,ParsecAuthentication,Vector", "CertificatesPath": "../../../../.ci/server/certs" } } diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index cd7ee699f..29476f662 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -4,7 +4,7 @@ on: pull_request: types: [opened, synchronize, reopened] push: - branches: [main] + branches: [master] tags: - 'v*' workflow_call: diff --git a/tests/SideBySide/BatchTests.cs b/tests/SideBySide/BatchTests.cs index 7adcdfc87..5e85d5d67 100644 --- a/tests/SideBySide/BatchTests.cs +++ b/tests/SideBySide/BatchTests.cs @@ -183,8 +183,7 @@ public void IgnoreCommandTransactionIgnoresDifferentTransaction() [InlineData(";\n")] [InlineData("; -- ")] [InlineData(" # ")] - - // [InlineData(" -- ")] TODO: uncomment if DB-53659 is done + //// [InlineData(" -- ")] TODO: uncomment if DB-53659 is done public void ExecuteBatch(string suffix) { using var connection = new SingleStoreConnection(AppConfig.ConnectionString); diff --git a/tests/SideBySide/CancelTests.cs b/tests/SideBySide/CancelTests.cs index ad4e32710..0987d6312 100644 --- a/tests/SideBySide/CancelTests.cs +++ b/tests/SideBySide/CancelTests.cs @@ -99,8 +99,7 @@ public async Task CancelCommandWithPasswordCallback() var stopwatch = Stopwatch.StartNew(); await TestUtilities.AssertExecuteScalarReturnsOneOrIsCanceledAsync(command); - - // Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. task.Wait(); // shouldn't throw } @@ -119,8 +118,7 @@ public async Task CancelCommandCancellationTokenWithPasswordCallback() using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); var stopwatch = Stopwatch.StartNew(); await TestUtilities.AssertExecuteScalarReturnsOneOrIsCanceledAsync(command, cts.Token); - - // Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } #endif @@ -143,8 +141,7 @@ public void CancelCommandBeforeRead() Assert.Equal((int) SingleStoreErrorCode.QueryInterrupted, ex.Number); } Assert.False(reader.NextResult()); - - // TestUtilities.AssertDuration(stopwatch, 0, 1000); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// TestUtilities.AssertDuration(stopwatch, 0, 1000); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. Assert.InRange(rows, 0, 10000000); } @@ -198,8 +195,7 @@ public void DapperQueryMultiple() stopwatch = Stopwatch.StartNew(); } stopwatch.Stop(); - - // TestUtilities.AssertDuration(stopwatch, 0, 1000); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// TestUtilities.AssertDuration(stopwatch, 0, 1000); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } #if !BASELINE @@ -369,8 +365,7 @@ public async Task CancelSlowQueryWithTokenAfterExecuteReader() catch (OperationCanceledException ex) { Assert.Equal(cts.Token, ex.CancellationToken); - - // Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows + //// Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows } } @@ -405,8 +400,7 @@ public async Task CancelSlowQueryWithTokenAfterNextResult() catch (OperationCanceledException ex) { Assert.Equal(cts.Token, ex.CancellationToken); - - // Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows + //// Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows } } @@ -458,8 +452,7 @@ public void CancelBatchCommand() var stopwatch = Stopwatch.StartNew(); var ex = Assert.Throws(() => batch.ExecuteScalar()); Assert.Equal("Query execution was interrupted", ex.Message); - - // Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. task.Wait(); // shouldn't throw } @@ -528,8 +521,7 @@ public void CancelBatchBeforeRead() Assert.Equal(SingleStoreErrorCode.QueryInterrupted, ex.ErrorCode); } Assert.False(reader.NextResult()); - - // TestUtilities.AssertDuration(stopwatch, 0, 1000); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// TestUtilities.AssertDuration(stopwatch, 0, 1000); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. Assert.InRange(rows, 0, 10000000); } @@ -673,8 +665,7 @@ public async Task CancelSlowQueryBatchWithTokenAfterExecuteReader() // the call to ExecuteReader should block until the token is cancelled var stopwatch = Stopwatch.StartNew(); using var reader = await batch.ExecuteReaderAsync(cts.Token); - - // TestUtilities.AssertDuration(stopwatch, 450, 3000); commented out due to flakiness — execution can complete too quickly depending on system/load. + //// TestUtilities.AssertDuration(stopwatch, 450, 3000); commented out due to flakiness — execution can complete too quickly depending on system/load. var rows = 0; try { @@ -686,8 +677,7 @@ public async Task CancelSlowQueryBatchWithTokenAfterExecuteReader() catch (OperationCanceledException ex) { Assert.Equal(cts.Token, ex.CancellationToken); - - // Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows + //// Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows } } @@ -714,8 +704,7 @@ public async Task CancelSlowQueryBatchWithTokenAfterNextResult() // the call to NextResult should block until the token is cancelled var stopwatch = Stopwatch.StartNew(); Assert.True(await reader.NextResultAsync(cts.Token)); - - // TestUtilities.AssertDuration(stopwatch, 450, 1500); commented out due to flakiness — execution can complete too quickly depending on system/load. + //// TestUtilities.AssertDuration(stopwatch, 450, 1500); commented out due to flakiness — execution can complete too quickly depending on system/load. int rows = 0; try { @@ -726,8 +715,7 @@ public async Task CancelSlowQueryBatchWithTokenAfterNextResult() catch (OperationCanceledException ex) { Assert.Equal(cts.Token, ex.CancellationToken); - - // Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows + //// Assert.InRange(rows, 0, 100); commented out due to flakiness — we can't guarantee that it won't read more rows } } diff --git a/tests/SideBySide/CommandTimeoutTests.cs b/tests/SideBySide/CommandTimeoutTests.cs index ccd63331d..399ed839d 100644 --- a/tests/SideBySide/CommandTimeoutTests.cs +++ b/tests/SideBySide/CommandTimeoutTests.cs @@ -66,8 +66,7 @@ public void CommandTimeoutWithSleepSync() } #endif sw.Stop(); - - // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } Assert.Equal(connectionState, m_connection.State); @@ -93,8 +92,7 @@ public async Task CommandTimeoutWithSleepAsync() } #endif sw.Stop(); - - // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 700); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 700); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } Assert.Equal(connectionState, m_connection.State); @@ -135,8 +133,7 @@ create procedure sleep_sproc(seconds INT) as } #endif sw.Stop(); - - // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } [SkippableFact(ServerFeatures.Timeout)] @@ -164,8 +161,7 @@ public void MultipleCommandTimeoutWithSleepSync() #endif sw.Stop(); - - // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } Assert.Equal(connectionState, m_connection.State); @@ -195,8 +191,7 @@ public async Task MultipleCommandTimeoutWithSleepAsync() #endif sw.Stop(); - - // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 550); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 550); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } Assert.Equal(connectionState, m_connection.State); @@ -265,8 +260,7 @@ public void TransactionCommandTimeoutWithSleepSync() } #endif sw.Stop(); - - // TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } Assert.Equal(connectionState, m_connection.State); diff --git a/tests/SideBySide/ConnectAsync.cs b/tests/SideBySide/ConnectAsync.cs index a03132e59..62668898a 100644 --- a/tests/SideBySide/ConnectAsync.cs +++ b/tests/SideBySide/ConnectAsync.cs @@ -125,8 +125,7 @@ public async Task ConnectTimeoutAsync() var ex = await Assert.ThrowsAsync(connection.OpenAsync); stopwatch.Stop(); Assert.Equal((int) SingleStoreErrorCode.UnableToConnectToHost, ex.Number); - - // TestUtilities.AssertDuration(stopwatch, 1900, 1500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// TestUtilities.AssertDuration(stopwatch, 1900, 1500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } [SkippableFact(ServerFeatures.Timeout, Baseline = "https://bugs.mysql.com/bug.php?id=94760")] @@ -150,8 +149,7 @@ public async Task ConnectTimeoutAsyncCancellationToken() stopwatch.Stop(); Assert.Equal(TaskStatus.Canceled, task.Status); Assert.Equal(cts.Token, ex.CancellationToken); - - // TestUtilities.AssertDuration(stopwatch, 1900, 1500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. + //// TestUtilities.AssertDuration(stopwatch, 1900, 1500); commented out due to flakiness — execution can complete too quickly/slow depending on system/load. } #if !BASELINE From 7c5528543874e80ceecaf6a554230d75f717e8f0 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Fri, 24 Apr 2026 14:49:40 +0300 Subject: [PATCH 19/21] implement bulk loading tests for Geography types --- tests/SideBySide/BulkLoaderAsync.cs | 96 +++++++++++++++++++++++--- tests/SideBySide/BulkLoaderSync.cs | 102 ++++++++++++++++++++++++---- 2 files changed, 177 insertions(+), 21 deletions(-) diff --git a/tests/SideBySide/BulkLoaderAsync.cs b/tests/SideBySide/BulkLoaderAsync.cs index d6d3097aa..80f221e3b 100644 --- a/tests/SideBySide/BulkLoaderAsync.cs +++ b/tests/SideBySide/BulkLoaderAsync.cs @@ -650,26 +650,86 @@ public async Task BulkCopyNullDataReader() await Assert.ThrowsAsync(async () => await bulkCopy.WriteToServerAsync(default(DbDataReader))); } - // TODO: reimplement for SingleStore Geography types - /*[Fact] - public async Task BulkCopyGeometryAsync() + [Fact] + public async Task BulkCopyGeographyAsync() { + using var connection = new SingleStoreConnection(GetLocalConnectionString()); + await connection.OpenAsync(); + var dataTable = new DataTable() { Columns = { - new DataColumn("geo_data", typeof(SingleStoreGeometry)), + new DataColumn("geo_data", typeof(SingleStoreGeography)), }, Rows = { - new object[] { SingleStoreGeometry.FromWkb(0, [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 63, 0, 0, 0, 0, 0, 0, 240, 63]) }, + new object[] { new SingleStoreGeography("LINESTRING(0.00000000 0.00000000, 1.00000000 1.00000000, 2.00000000 2.00000000)") }, + new object[] { new SingleStoreGeography("POLYGON((0.00000000 0.00000000, 1.00000000 0.00000000, 1.00000000 1.00000000, 0.00000000 1.00000000, 0.00000000 0.00000000))") }, }, }; + using (var cmd = new SingleStoreCommand(@"drop table if exists bulk_load_data_table; +create rowstore table bulk_load_data_table( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + geo_data GEOGRAPHY NOT NULL +);", connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + var bc = new SingleStoreBulkCopy(connection) + { + DestinationTableName = "bulk_load_data_table", + ColumnMappings = + { + new() + { + SourceOrdinal = 0, + DestinationColumn = "geo_data", + }, + }, + }; + + var result = await bc.WriteToServerAsync(dataTable); + Assert.Equal(2, result.RowsInserted); + Assert.Empty(result.Warnings); + + using (var cmd = new SingleStoreCommand("select geo_data from bulk_load_data_table order by id;", connection)) + using (var reader = await cmd.ExecuteReaderAsync()) + { + Assert.True(await reader.ReadAsync()); + Assert.Equal("LINESTRING(0.00000000 0.00000000, 1.00000000 1.00000000, 2.00000000 2.00000000)", reader.GetString(0)); + Assert.True(await reader.ReadAsync()); + Assert.Equal("POLYGON((0.00000000 0.00000000, 1.00000000 0.00000000, 1.00000000 1.00000000, 0.00000000 1.00000000, 0.00000000 0.00000000))", reader.GetString(0)); + Assert.False(await reader.ReadAsync()); + } + } + + [Fact] + public async Task BulkCopyGeographyPointAsync() + { using var connection = new SingleStoreConnection(GetLocalConnectionString()); await connection.OpenAsync(); + + var dataTable = new DataTable() + { + Columns = + { + new DataColumn("point_data", typeof(SingleStoreGeographyPoint)), + }, + Rows = + { + new object[] { new SingleStoreGeographyPoint("POINT(0.00000000 0.00000000)") }, + new object[] { new SingleStoreGeographyPoint("POINT(1.00000000 1.00000000)") }, + }, + }; + using (var cmd = new SingleStoreCommand(@"drop table if exists bulk_load_data_table; -create table bulk_load_data_table(id BIGINT UNIQUE NOT NULL AUTO_INCREMENT, geo_data GEOMETRY NOT NULL);", connection)) +create table bulk_load_data_table( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + point_data GEOGRAPHYPOINT NOT NULL +);", connection)) { await cmd.ExecuteNonQueryAsync(); } @@ -682,13 +742,31 @@ public async Task BulkCopyGeometryAsync() new() { SourceOrdinal = 0, - DestinationColumn = "geo_data", + DestinationColumn = "point_data", }, }, }; - await bc.WriteToServerAsync(dataTable); - }*/ + var result = await bc.WriteToServerAsync(dataTable); + Assert.Equal(2, result.RowsInserted); + Assert.Empty(result.Warnings); + + using (var cmd = new SingleStoreCommand( + "select GEOGRAPHY_LONGITUDE(point_data), GEOGRAPHY_LATITUDE(point_data) from bulk_load_data_table order by id;", + connection)) + using (var reader = await cmd.ExecuteReaderAsync()) + { + Assert.True(await reader.ReadAsync()); + Assert.InRange(reader.GetDouble(0), -1e-6, 1e-6); + Assert.InRange(reader.GetDouble(1), -1e-6, 1e-6); + + Assert.True(await reader.ReadAsync()); + Assert.InRange(reader.GetDouble(0), 1.0 - 1e-6, 1.0 + 1e-6); + Assert.InRange(reader.GetDouble(1), 1.0 - 1e-6, 1.0 + 1e-6); + + Assert.False(await reader.ReadAsync()); + } + } #endif private static string GetConnectionString() => BulkLoaderSync.GetConnectionString(); diff --git a/tests/SideBySide/BulkLoaderSync.cs b/tests/SideBySide/BulkLoaderSync.cs index 835730627..71aeadaf6 100644 --- a/tests/SideBySide/BulkLoaderSync.cs +++ b/tests/SideBySide/BulkLoaderSync.cs @@ -1436,31 +1436,35 @@ public void BulkCopyDataTableConflictOption(SingleStoreBulkLoaderConflictOption Assert.Equal(expected, cmd.ExecuteScalar()); } - // TODO: reimplement for SingleStore Geography types - /*[Fact] - public void BulkCopyGeometry() + [Fact] + public void BulkCopyGeography() { + using var connection = new SingleStoreConnection(GetLocalConnectionString()); + connection.Open(); + var dataTable = new DataTable() { Columns = { - new DataColumn("geo_data", typeof(MySqlGeometry)), + new DataColumn("geo_data", typeof(SingleStoreGeography)), }, Rows = { - new object[] { MySqlGeometry.FromWkb(0, [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 63, 0, 0, 0, 0, 0, 0, 240, 63]) }, + new object[] { new SingleStoreGeography("LINESTRING(0.00000000 0.00000000, 1.00000000 1.00000000, 2.00000000 2.00000000)") }, + new object[] { new SingleStoreGeography("POLYGON((0.00000000 0.00000000, 1.00000000 0.00000000, 1.00000000 1.00000000, 0.00000000 1.00000000, 0.00000000 0.00000000))") }, }, }; - using var connection = new MySqlConnection(GetLocalConnectionString()); - connection.Open(); - using (var cmd = new MySqlCommand(@"drop table if exists bulk_load_data_table; -create table bulk_load_data_table(id BIGINT UNIQUE NOT NULL AUTO_INCREMENT, geo_data GEOMETRY NOT NULL);", connection)) + using (var cmd = new SingleStoreCommand(@"drop table if exists bulk_load_data_table; +create rowstore table bulk_load_data_table( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + geo_data GEOGRAPHY NOT NULL +);", connection)) { cmd.ExecuteNonQuery(); } - var bc = new MySqlBulkCopy(connection) + var bc = new SingleStoreBulkCopy(connection) { DestinationTableName = "bulk_load_data_table", ColumnMappings = @@ -1473,8 +1477,82 @@ public void BulkCopyGeometry() }, }; - bc.WriteToServer(dataTable); - }*/ + var result = bc.WriteToServer(dataTable); + Assert.Equal(2, result.RowsInserted); + Assert.Empty(result.Warnings); + + using (var cmd = new SingleStoreCommand("select geo_data from bulk_load_data_table order by id;", connection)) + using (var reader = cmd.ExecuteReader()) + { + Assert.True(reader.Read()); + Assert.Equal("LINESTRING(0.00000000 0.00000000, 1.00000000 1.00000000, 2.00000000 2.00000000)", reader.GetString(0)); + Assert.True(reader.Read()); + Assert.Equal("POLYGON((0.00000000 0.00000000, 1.00000000 0.00000000, 1.00000000 1.00000000, 0.00000000 1.00000000, 0.00000000 0.00000000))", reader.GetString(0)); + Assert.False(reader.Read()); + } + } + + [Fact] + public void BulkCopyGeographyPoint() + { + using var connection = new SingleStoreConnection(GetLocalConnectionString()); + connection.Open(); + + var dataTable = new DataTable() + { + Columns = + { + new DataColumn("point_data", typeof(SingleStoreGeographyPoint)), + }, + Rows = + { + new object[] { new SingleStoreGeographyPoint("POINT(0.00000000 0.00000000)") }, + new object[] { new SingleStoreGeographyPoint("POINT(1.00000000 1.00000000)") }, + }, + }; + + using (var cmd = new SingleStoreCommand(@"drop table if exists bulk_load_data_table; +create table bulk_load_data_table( + id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + point_data GEOGRAPHYPOINT NOT NULL +);", connection)) + { + cmd.ExecuteNonQuery(); + } + + var bc = new SingleStoreBulkCopy(connection) + { + DestinationTableName = "bulk_load_data_table", + ColumnMappings = + { + new() + { + SourceOrdinal = 0, + DestinationColumn = "point_data", + }, + }, + }; + + var result = bc.WriteToServer(dataTable); + Assert.Equal(2, result.RowsInserted); + Assert.Empty(result.Warnings); + + using (var cmd = new SingleStoreCommand( + "select GEOGRAPHY_LONGITUDE(point_data), GEOGRAPHY_LATITUDE(point_data) from bulk_load_data_table order by id;", + connection)) + using (var reader = cmd.ExecuteReader()) + { + Assert.True(reader.Read()); + Assert.InRange(reader.GetDouble(0), -1e-6, 1e-6); + Assert.InRange(reader.GetDouble(1), -1e-6, 1e-6); + + Assert.True(reader.Read()); + Assert.InRange(reader.GetDouble(0), 1.0 - 1e-6, 1.0 + 1e-6); + Assert.InRange(reader.GetDouble(1), 1.0 - 1e-6, 1.0 + 1e-6); + + Assert.False(reader.Read()); + } + } #endif internal static string GetConnectionString() => AppConfig.ConnectionString; From 712308ae268ddb70c35719b1a7e58dd05427de01 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Fri, 24 Apr 2026 15:35:32 +0300 Subject: [PATCH 20/21] get rid of Vector type support (need to reimplement it) --- .../ColumnReaders/ColumnReader.cs | 3 - .../ColumnReaders/VectorColumnReader.cs | 37 ----------- src/SingleStoreConnector/Core/Row.cs | 2 +- src/SingleStoreConnector/Core/TypeMapper.cs | 8 --- .../Protocol/ColumnType.cs | 1 - .../SingleStoreBulkCopy.cs | 1 - .../SingleStoreDbColumn.cs | 1 - src/SingleStoreConnector/SingleStoreDbType.cs | 1 - tests/SideBySide/BulkLoaderSync.cs | 65 ------------------- tests/SideBySide/DataTypes.cs | 53 --------------- tests/SideBySide/DataTypesFixture.cs | 20 ------ tests/SideBySide/QueryTests.cs | 62 ------------------ tests/SideBySide/ServerFeatures.cs | 10 --- tests/SideBySide/StoredProcedureTests.cs | 33 ---------- 14 files changed, 1 insertion(+), 296 deletions(-) delete mode 100644 src/SingleStoreConnector/ColumnReaders/VectorColumnReader.cs diff --git a/src/SingleStoreConnector/ColumnReaders/ColumnReader.cs b/src/SingleStoreConnector/ColumnReaders/ColumnReader.cs index 2eaed4797..e0bca6a64 100644 --- a/src/SingleStoreConnector/ColumnReaders/ColumnReader.cs +++ b/src/SingleStoreConnector/ColumnReaders/ColumnReader.cs @@ -120,9 +120,6 @@ public static ColumnReader Create(bool isBinary, ColumnDefinitionPayload columnD case ColumnType.Null: return NullColumnReader.Instance; - case ColumnType.Vector: - return VectorColumnReader.Instance; - default: throw new NotImplementedException($"Reading {columnDefinition.ColumnType} not implemented"); } diff --git a/src/SingleStoreConnector/ColumnReaders/VectorColumnReader.cs b/src/SingleStoreConnector/ColumnReaders/VectorColumnReader.cs deleted file mode 100644 index 77166649f..000000000 --- a/src/SingleStoreConnector/ColumnReaders/VectorColumnReader.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Buffers.Binary; -using System.Runtime.InteropServices; -using SingleStoreConnector.Protocol.Payloads; - -namespace SingleStoreConnector.ColumnReaders; - -internal sealed class VectorColumnReader : ColumnReader -{ - public static VectorColumnReader Instance { get; } = new(); - - public override object ReadValue(ReadOnlySpan data, ColumnDefinitionPayload columnDefinition) - { - if (BitConverter.IsLittleEndian) - { - return new ReadOnlyMemory(MemoryMarshal.Cast(data).ToArray()); - } - else - { - var floats = new float[data.Length / 4]; - -#if !NET5_0_OR_GREATER - var bytes = data.ToArray(); -#endif - for (var i = 0; i < floats.Length; i++) - { -#if NET5_0_OR_GREATER - floats[i] = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(i * 4)); -#else - Array.Reverse(bytes, i * 4, 4); - floats[i] = BitConverter.ToSingle(bytes, i * 4); -#endif - } - - return new ReadOnlyMemory(floats); - } - } -} diff --git a/src/SingleStoreConnector/Core/Row.cs b/src/SingleStoreConnector/Core/Row.cs index 431c48794..4ea5ae3a9 100644 --- a/src/SingleStoreConnector/Core/Row.cs +++ b/src/SingleStoreConnector/Core/Row.cs @@ -452,7 +452,7 @@ private void CheckBinaryColumn(int ordinal) var columnType = column.ColumnType; if ((column.ColumnFlags & ColumnFlags.Binary) == 0 || (columnType != ColumnType.String && columnType != ColumnType.VarString && columnType != ColumnType.TinyBlob && - columnType != ColumnType.Blob && columnType != ColumnType.MediumBlob && columnType != ColumnType.LongBlob && columnType != ColumnType.Vector)) + columnType != ColumnType.Blob && columnType != ColumnType.MediumBlob && columnType != ColumnType.LongBlob)) { throw new InvalidCastException($"Can't convert {columnType} to bytes."); } diff --git a/src/SingleStoreConnector/Core/TypeMapper.cs b/src/SingleStoreConnector/Core/TypeMapper.cs index d90e3f058..27abe20d2 100644 --- a/src/SingleStoreConnector/Core/TypeMapper.cs +++ b/src/SingleStoreConnector/Core/TypeMapper.cs @@ -55,10 +55,6 @@ private TypeMapper() AddColumnTypeMetadata(new("DOUBLE", typeDouble, SingleStoreDbType.Double)); AddColumnTypeMetadata(new("FLOAT", typeFloat, SingleStoreDbType.Float)); - // vector - var typeFloatReadOnlyMemory = AddDbTypeMapping(new(typeof(ReadOnlyMemory), [DbType.Object])); - AddColumnTypeMetadata(new("VECTOR", typeFloatReadOnlyMemory, SingleStoreDbType.Vector, binary: true, simpleDataTypeName: "VECTOR", createFormat: "VECTOR({0})")); - // string var typeFixedString = AddDbTypeMapping(new(typeof(string), [DbType.StringFixedLength, DbType.AnsiStringFixedLength], convert: Convert.ToString!)); var typeString = AddDbTypeMapping(new(typeof(string), [DbType.String, DbType.AnsiString, DbType.Xml], convert: Convert.ToString!)); @@ -320,9 +316,6 @@ public static SingleStoreDbType ConvertToSingleStoreDbType(ColumnDefinitionPaylo case ColumnType.Set: return SingleStoreDbType.Set; - case ColumnType.Vector: - return SingleStoreDbType.Vector; - default: throw new NotImplementedException($"ConvertToSingleStoreDbType for {columnDefinition.ColumnType} is not implemented"); } @@ -360,7 +353,6 @@ public static ushort ConvertToColumnTypeAndFlags(SingleStoreDbType dbType, Singl SingleStoreDbType.Geography => ColumnType.Geography, SingleStoreDbType.GeographyPoint => ColumnType.GeographyPoint, SingleStoreDbType.Null => ColumnType.Null, - SingleStoreDbType.Vector => ColumnType.Vector, _ => throw new NotImplementedException($"ConvertToColumnTypeAndFlags for {dbType} is not implemented"), }; return (ushort) ((byte) columnType | (isUnsigned ? 0x8000 : 0)); diff --git a/src/SingleStoreConnector/Protocol/ColumnType.cs b/src/SingleStoreConnector/Protocol/ColumnType.cs index 15205f6d8..94694986b 100644 --- a/src/SingleStoreConnector/Protocol/ColumnType.cs +++ b/src/SingleStoreConnector/Protocol/ColumnType.cs @@ -24,7 +24,6 @@ internal enum ColumnType Bit = 16, Timestamp2 = 17, DateTime2 = 18, - Vector = 242, GeographyPoint = 0xF3, Geography = 0xF4, Json = 0xF5, diff --git a/src/SingleStoreConnector/SingleStoreBulkCopy.cs b/src/SingleStoreConnector/SingleStoreBulkCopy.cs index 696f3652e..e69d1c9a0 100644 --- a/src/SingleStoreConnector/SingleStoreBulkCopy.cs +++ b/src/SingleStoreConnector/SingleStoreBulkCopy.cs @@ -243,7 +243,6 @@ private async ValueTask WriteToServerAsync(IOBehavior { var type = schema[i].DataType; if (type == typeof(byte[]) || - dataTypeName == "VECTOR" || (type == typeof(Guid) && (m_connection.GuidFormat is SingleStoreGuidFormat.Binary16 or SingleStoreGuidFormat.LittleEndianBinary16 or SingleStoreGuidFormat.TimeSwapBinary16))) { AddColumnMapping(m_logger, columnMappings, addDefaultMappings, i, destinationColumn, $"@`temporary_column_dotnet_connector_col{i}`", $"%COL% = UNHEX(%VAR%)"); diff --git a/src/SingleStoreConnector/SingleStoreDbColumn.cs b/src/SingleStoreConnector/SingleStoreDbColumn.cs index f475de825..6f9b9ccf7 100644 --- a/src/SingleStoreConnector/SingleStoreDbColumn.cs +++ b/src/SingleStoreConnector/SingleStoreDbColumn.cs @@ -25,7 +25,6 @@ internal SingleStoreDbColumn(int ordinal, ColumnDefinitionPayload column, bool a } else { - // TODO: VECTOR COLUMNSIZE HANDLING if (mySqlDbType == SingleStoreDbType.JSON || mySqlDbType == SingleStoreDbType.LongBlob) ColumnSize = int.MaxValue; else diff --git a/src/SingleStoreConnector/SingleStoreDbType.cs b/src/SingleStoreConnector/SingleStoreDbType.cs index 68240f60d..0375eba9f 100644 --- a/src/SingleStoreConnector/SingleStoreDbType.cs +++ b/src/SingleStoreConnector/SingleStoreDbType.cs @@ -38,7 +38,6 @@ public enum SingleStoreDbType String, Geography, GeographyPoint, - Vector = 242, UByte = 501, UInt16, UInt32, diff --git a/tests/SideBySide/BulkLoaderSync.cs b/tests/SideBySide/BulkLoaderSync.cs index 71aeadaf6..b5cad0a51 100644 --- a/tests/SideBySide/BulkLoaderSync.cs +++ b/tests/SideBySide/BulkLoaderSync.cs @@ -590,71 +590,6 @@ public void BulkCopyDataTableWithSingleStoreDecimal() } } - [SkippableTheory(ServerFeatures.Vector)] - [InlineData("byte[]")] - [InlineData("float[]")] - public void BulkCopyDataTableWithVector(string dataType) - { - var dataTable = new DataTable() - { - Columns = - { - new DataColumn("id", typeof(int)), - new DataColumn("data", -#pragma warning disable SA1118 // Parameter should not span multiple lines - dataType switch - { - "byte[]" => typeof(byte[]), - "float[]" => typeof(float[]), - _ => throw new ArgumentOutOfRangeException(nameof(dataType)), - }), -#pragma warning restore SA1118 // Parameter should not span multiple lines - }, - Rows = - { - new object[] { 1, GetDataRowValue([0, 0, 0], dataType) }, - new object[] { 2, GetDataRowValue([1f, 2f, 3f], dataType) }, - }, - }; - - using var connection = new SingleStoreConnection(GetLocalConnectionString()); - connection.Open(); - using (var cmd = new SingleStoreCommand(@"drop table if exists bulk_load_data_table; -create table bulk_load_data_table(a int, b vector(3));", connection)) - { - cmd.ExecuteNonQuery(); - } - - var bulkCopy = new SingleStoreBulkCopy(connection) - { - DestinationTableName = "bulk_load_data_table", - }; - var result = bulkCopy.WriteToServer(dataTable); - Assert.Equal(2, result.RowsInserted); - Assert.Empty(result.Warnings); - - using (var cmd = new SingleStoreCommand(@"select b from bulk_load_data_table order by a;", connection)) - { - using var reader = cmd.ExecuteReader(); - Assert.True(reader.Read()); - Assert.Equal(new float[3], GetFloatArray(reader, 0)); - Assert.True(reader.Read()); - Assert.Equal([1f, 2f, 3f], GetFloatArray(reader, 0)); - Assert.False(reader.Read()); - } - - static object GetDataRowValue(float[] data, string dataType) => - dataType == "byte[]" ? MemoryMarshal.Cast(data).ToArray() : data; - - static float[] GetFloatArray(SingleStoreDataReader reader, int ordinal) => - reader.GetValue(ordinal) switch - { - ReadOnlyMemory romf => romf.ToArray(), - byte[] b => MemoryMarshal.Cast(b).ToArray(), - { } x => throw new NotSupportedException(x.GetType().Name), - }; - } - #if NET6_0_OR_GREATER [Fact] public void BulkCopyDataTableWithDateOnly() diff --git a/tests/SideBySide/DataTypes.cs b/tests/SideBySide/DataTypes.cs index 8ae42a22f..9211f250e 100644 --- a/tests/SideBySide/DataTypes.cs +++ b/tests/SideBySide/DataTypes.cs @@ -1137,11 +1137,6 @@ private static object CreateGeographyPoint(string data) [InlineData("Int64", "datatypes_integers", SingleStoreDbType.Int64, 20, typeof(long), "N", 0, 0)] [InlineData("UInt64", "datatypes_integers", SingleStoreDbType.UInt64, 20, typeof(ulong), "N", 0, 0)] [InlineData("value", "datatypes_json_core", SingleStoreDbType.JSON, int.MaxValue, typeof(string), "LN", 0, 0)] -#if BASELINE - [InlineData("value", "datatypes_vector", SingleStoreDbType.Vector, 12, typeof(byte[]), "N", 0, 31)] -#else - [InlineData("value", "datatypes_vector", SingleStoreDbType.Vector, 3, typeof(ReadOnlyMemory), "N", 0, 31)] -#endif [InlineData("Single", "datatypes_reals", SingleStoreDbType.Float, 12, typeof(float), "N", 0, 31)] [InlineData("Double", "datatypes_reals", SingleStoreDbType.Double, 22, typeof(double), "N", 0, 31)] [InlineData("SmallDecimal", "datatypes_reals", SingleStoreDbType.NewDecimal, 7, typeof(decimal), "N", 5, 2)] @@ -1206,17 +1201,6 @@ private void DoGetSchemaTable(string column, string table, SingleStoreDbType myS { if (table == "datatypes_json_core" && !AppConfig.SupportsJson) return; - if (table == "datatypes_vector" && !AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Vector)) - return; - - // adjust for databases that don't have a dedicated on-the-wire type for VECTOR(n) - if (mySqlDbType == SingleStoreDbType.Vector && !AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType)) - { - mySqlDbType = SingleStoreDbType.VarBinary; - columnSize *= 4; - dataType = typeof(byte[]); - scale = 0; - } connection = connection ?? Connection; var isAutoIncrement = flags.IndexOf('A') != -1; @@ -1677,43 +1661,6 @@ public void QueryJson(string column, string[] expected) DoQuery("json_core", column, dataTypeName, expected, reader => reader.GetString(0), omitWhereTest: true); } - [SkippableTheory(ServerFeatures.Vector)] - [InlineData("value", new[] { null, "0,0,0", "1,1,1", "1,2,3", "-1,-1,-1" })] - public void QueryVector(string column, string[] expected) - { - var hasVectorType = AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType); - string dataTypeName = hasVectorType ? "VECTOR" : "BLOB"; - DoQuery("vector", column, dataTypeName, - expected.Select(x => -#if !MYSQL_DATA - hasVectorType ? (GetFloatArray(x) is float[] a ? (object) new ReadOnlyMemory(a) : null) : GetByteArray(x)) -#else - // Connector/NET returns the float array as a byte[] - GetByteArray(x)) -#endif - .ToArray(), -#if !MYSQL_DATA - x => hasVectorType ? (ReadOnlyMemory) x.GetValue(0) : (byte[]) x.GetValue(0), -#else - // NOTE: Connector/NET returns 'null' for NULL so simulate an exception for the tests - x => x.IsDBNull(0) ? throw new GetValueWhenNullException() : x.GetValue(0), -#endif - assertEqual: (l, r) => - { - if (l is ReadOnlyMemory roml) - l = roml.ToArray(); - if (r is ReadOnlyMemory romr) - r = romr.ToArray(); - Assert.Equal(l, r); - }, - omitWhereTest: true); - - static float[] GetFloatArray(string value) => value?.Split(',').Select(x => float.Parse(x, CultureInfo.InvariantCulture)).ToArray(); - - static byte[] GetByteArray(string value) => - GetFloatArray(value) is { } floats ? MemoryMarshal.AsBytes(floats).ToArray() : null; - } - [SkippableTheory(Baseline = "https://bugs.mysql.com/bug.php?id=97067")] [InlineData(false, "MIN", 0)] [InlineData(false, "MAX", uint.MaxValue)] diff --git a/tests/SideBySide/DataTypesFixture.cs b/tests/SideBySide/DataTypesFixture.cs index 583742a58..c6f17bd09 100644 --- a/tests/SideBySide/DataTypesFixture.cs +++ b/tests/SideBySide/DataTypesFixture.cs @@ -238,26 +238,6 @@ insert into datatypes_json_core (value) "); } - if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Vector)) - { - Connection.Execute(""" - DROP TABLE IF EXISTS datatypes_vector; - - CREATE TABLE datatypes_vector ( - rowid BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, - value VECTOR(3) NULL - ); - - INSERT INTO datatypes_vector (value) - VALUES - (NULL), - ('[0, 0, 0]'), - ('[1, 1, 1]'), - ('[1, 2, 3]'), - ('[-1, -1, -1]'); - """); - } - Connection.Close(); } } diff --git a/tests/SideBySide/QueryTests.cs b/tests/SideBySide/QueryTests.cs index 530a77270..ebb03baf3 100644 --- a/tests/SideBySide/QueryTests.cs +++ b/tests/SideBySide/QueryTests.cs @@ -1678,68 +1678,6 @@ public void ServerDoesNotSendMariaDbCacheMetadataOrQueryAttributes() "Server should not send QueryAttributes capability flag."); } - [SkippableTheory(ServerFeatures.Vector)] - [InlineData(false, 0)] - [InlineData(false, 1)] - [InlineData(false, 2)] - [InlineData(true, 0)] - [InlineData(true, 1)] - [InlineData(true, 2)] - public void QueryVector(bool prepare, int dataFormat) - { - using var connection = new SingleStoreConnection(AppConfig.ConnectionString); - connection.Open(); - - connection.Execute(""" - drop table if exists test_vector; - create table test_vector(id bigint auto_increment not null primary key, vec vector(3) not null); - """); - - using var cmd = m_database.Connection.CreateCommand(); - cmd.CommandText = "INSERT INTO test_vector(vec) VALUES(@vec)"; - cmd.Parameters.Add(new SingleStoreParameter - { - ParameterName = "@vec", - SingleStoreDbType = SingleStoreDbType.Vector, - }); - - var floatArray = new[] { 1.2f, 3.4f, 5.6f }; -#if MYSQL_DATA - // Connector/NET requires the float vector to be passed as a byte array - cmd.Parameters[0].Value = MemoryMarshal.AsBytes(floatArray).ToArray(); - Assert.InRange(dataFormat, 0, 2); -#else - cmd.Parameters[0].Value = dataFormat switch - { - 0 => floatArray, - 1 => new Memory(floatArray), - 2 => new ReadOnlyMemory(floatArray), - _ => throw new NotSupportedException(), - }; -#endif - - if (prepare) - cmd.Prepare(); - cmd.ExecuteNonQuery(); - - // Select and verify the value - cmd.CommandText = "SELECT vec FROM test_vector"; - if (prepare) - cmd.Prepare(); - - using var reader = cmd.ExecuteReader(); - Assert.True(reader.Read()); - var value = reader.GetValue(0); - -#if MYSQL_DATA - var result = MemoryMarshal.Cast((byte[]) value).ToArray(); -#else - var result = AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType) ? (ReadOnlyMemory) value : - MemoryMarshal.Cast((byte[]) value).ToArray(); -#endif - Assert.Equal(floatArray, result); - } - private class BoolTest { public int Id { get; set; } diff --git a/tests/SideBySide/ServerFeatures.cs b/tests/SideBySide/ServerFeatures.cs index 08809b522..973491506 100644 --- a/tests/SideBySide/ServerFeatures.cs +++ b/tests/SideBySide/ServerFeatures.cs @@ -45,14 +45,4 @@ public enum ServerFeatures /// Server supports the 'parsec' authentication plugin. /// ParsecAuthentication = 0x200_0000, - - /// - /// Server supports the VECTOR SQL type. - /// - Vector = 0x400_0000, - - /// - /// Server has a dedicated type on the wire for VECTOR. - /// - VectorType = 0x800_0000, } diff --git a/tests/SideBySide/StoredProcedureTests.cs b/tests/SideBySide/StoredProcedureTests.cs index 7f5fec0e2..720df75e1 100644 --- a/tests/SideBySide/StoredProcedureTests.cs +++ b/tests/SideBySide/StoredProcedureTests.cs @@ -641,39 +641,6 @@ public void PassJsonParameter() Assert.False(reader.Read()); } - [SkippableTheory(ServerFeatures.Vector | ServerFeatures.VectorType)] - [InlineData(false)] - [InlineData(true)] - public void VectorOutputParameter(bool prepare) - { - using var cmd = m_database.Connection.CreateCommand(); - cmd.CommandText = """ - DROP PROCEDURE IF EXISTS sp_vector_out; - CREATE PROCEDURE sp_vector_out (OUT vec VECTOR) - BEGIN - SELECT STRING_TO_VECTOR('[1.2, 3.4, 5.6]') INTO vec; - END; - """; - cmd.ExecuteNonQuery(); - - cmd.CommandText = "sp_vector_out"; - cmd.CommandType = CommandType.StoredProcedure; - cmd.Parameters.Add(new SingleStoreParameter - { - Direction = ParameterDirection.Output, - SingleStoreDbType = SingleStoreDbType.Vector, - ParameterName = "@vec", - }); - - if (prepare) - cmd.Prepare(); - cmd.ExecuteNonQuery(); - - var value = cmd.Parameters[0].Value; - var result = Assert.IsType(value); - Assert.Equal(new float[] { 1.2f, 3.4f, 5.6f }, MemoryMarshal.Cast(result).ToArray()); - } - private static Action AssertParameter(string name, ParameterDirection direction, SingleStoreDbType mySqlDbType) { return x => From 082c9a47f897828c7b710ce39b1050cb67439b47 Mon Sep 17 00:00:00 2001 From: Olha Kramarenko Date: Fri, 24 Apr 2026 15:51:52 +0300 Subject: [PATCH 21/21] resolve small issues --- .github/workflows/SideBySide/config-ssl.json | 2 +- .github/workflows/SideBySide/config.json | 2 +- .../ParsecAuthenticationPlugin.cs | 2 +- .../ColumnReaders/TextDateTimeColumnReader.cs | 2 +- src/SingleStoreConnector/Core/ConnectionPool.cs | 4 ++-- src/SingleStoreConnector/Core/ServerSession.cs | 2 +- src/SingleStoreConnector/Protocol/ColumnType.cs | 2 +- src/SingleStoreConnector/SingleStoreBulkCopy.cs | 2 +- src/SingleStoreConnector/SingleStoreDataReader.cs | 2 +- tests/SideBySide/BulkLoaderAsync.cs | 2 +- tests/SideBySide/BulkLoaderSync.cs | 4 ++-- tests/SideBySide/ClientFactoryTests.cs | 4 ---- tests/SingleStoreConnector.Tests/DbProviderFactoryTests.cs | 4 ---- 13 files changed, 13 insertions(+), 21 deletions(-) diff --git a/.github/workflows/SideBySide/config-ssl.json b/.github/workflows/SideBySide/config-ssl.json index 5c1424c02..81650eaba 100644 --- a/.github/workflows/SideBySide/config-ssl.json +++ b/.github/workflows/SideBySide/config-ssl.json @@ -1,7 +1,7 @@ { "Data": { "ConnectionString": "server=SINGLESTORE_HOST;user id=SQL_USER_NAME;password=SQL_USER_PASSWORD;port=3306;database=singlestoretest;sslmode=Required;certificate file=../../../../.ci/server/certs/ssl-client.pfx;", - "UnsupportedFeatures": "CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin,UnixDomainSocket,Sha256Password,GlobalLog,Redirection,CancelSleepSuccessfully,TlsFingerprintValidation,ParsecAuthentication,Vector", + "UnsupportedFeatures": "CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin,UnixDomainSocket,Sha256Password,GlobalLog,Redirection,CancelSleepSuccessfully,TlsFingerprintValidation,ParsecAuthentication", "CertificatesPath": "../../../../.ci/server/certs" } } diff --git a/.github/workflows/SideBySide/config.json b/.github/workflows/SideBySide/config.json index d30d9f890..972386fee 100644 --- a/.github/workflows/SideBySide/config.json +++ b/.github/workflows/SideBySide/config.json @@ -1,7 +1,7 @@ { "Data": { "ConnectionString": "server=SINGLESTORE_HOST;user id=SQL_USER_NAME;password=SQL_USER_PASSWORD;port=3306;database=singlestoretest;sslmode=disabled;", - "UnsupportedFeatures": "CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin,UnixDomainSocket,Sha256Password,GlobalLog,Redirection,CancelSleepSuccessfully,TlsFingerprintValidation,ParsecAuthentication,Vector", + "UnsupportedFeatures": "CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin,UnixDomainSocket,Sha256Password,GlobalLog,Redirection,CancelSleepSuccessfully,TlsFingerprintValidation,ParsecAuthentication", "ManagedService": true } } diff --git a/src/SingleStoreConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs b/src/SingleStoreConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs index 0b07ea589..ad10061d3 100644 --- a/src/SingleStoreConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs +++ b/src/SingleStoreConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs @@ -11,7 +11,7 @@ namespace SingleStoreConnector.Authentication.Ed25519; public sealed class ParsecAuthenticationPlugin : IAuthenticationPlugin3 { /// - /// Registers the Parsec authentication plugin with MySqlConnector. You must call this method once before + /// Registers the Parsec authentication plugin with SingleStoreConnector. You must call this method once before /// opening a connection that uses Parsec authentication. /// public static void Install() diff --git a/src/SingleStoreConnector/ColumnReaders/TextDateTimeColumnReader.cs b/src/SingleStoreConnector/ColumnReaders/TextDateTimeColumnReader.cs index 39b748ea1..92345d284 100644 --- a/src/SingleStoreConnector/ColumnReaders/TextDateTimeColumnReader.cs +++ b/src/SingleStoreConnector/ColumnReaders/TextDateTimeColumnReader.cs @@ -81,7 +81,7 @@ public static object ParseDateTime(ReadOnlySpan data, bool convertZeroDate try { - return allowZeroDateTime ? (ticks % 10 == 0 ? (object) new SingleStoreDateTime(year, month, day, hour, minute, second, ticks / 10) : throw new NotSupportedException("MySqlDateTime does not support sub-microsecond precision")) : + return allowZeroDateTime ? (ticks % 10 == 0 ? (object) new SingleStoreDateTime(year, month, day, hour, minute, second, ticks / 10) : throw new NotSupportedException("SingleStoreDateTime does not support sub-microsecond precision")) : new DateTime(year, month, day, hour, minute, second, dateTimeKind).AddTicks(ticks); } catch (Exception ex) diff --git a/src/SingleStoreConnector/Core/ConnectionPool.cs b/src/SingleStoreConnector/Core/ConnectionPool.cs index db4e0ee13..c3e0d34e3 100644 --- a/src/SingleStoreConnector/Core/ConnectionPool.cs +++ b/src/SingleStoreConnector/Core/ConnectionPool.cs @@ -295,11 +295,11 @@ private async Task RecoverLeakedSessionsAsync(IOBehavior ioBehavior) foreach (var (session, connection) in recoveredSessions) { - // bypass SingleStoreConnection.Dispose(Async), because it's a dummy MySqlConnection that's not set up + // bypass SingleStoreConnection.Dispose(Async), because it's a dummy SingleStoreConnection that's not set up // properly, and simply return the session to the pool directly await session.ReturnToPoolAsync(ioBehavior, null).ConfigureAwait(false); - // be explicit about keeping the associated MySqlConnection alive until the session has been returned + // be explicit about keeping the associated SingleStoreConnection alive until the session has been returned GC.KeepAlive(connection); } } diff --git a/src/SingleStoreConnector/Core/ServerSession.cs b/src/SingleStoreConnector/Core/ServerSession.cs index f6df44448..4c8e224cf 100644 --- a/src/SingleStoreConnector/Core/ServerSession.cs +++ b/src/SingleStoreConnector/Core/ServerSession.cs @@ -1035,7 +1035,7 @@ public async ValueTask TryPingAsync(bool logInfo, IOBehavior ioBehavior, C } catch (SingleStoreException ex) when (ex.ErrorCode == SingleStoreErrorCode.ClientInteractionTimeout) { - Log.PingFailed(m_logger, ex, Id, "ClientInteractionTimeout MySqlException"); + Log.PingFailed(m_logger, ex, Id, "ClientInteractionTimeout SingleStoreException"); } catch (SocketException ex) { diff --git a/src/SingleStoreConnector/Protocol/ColumnType.cs b/src/SingleStoreConnector/Protocol/ColumnType.cs index 94694986b..0c033c822 100644 --- a/src/SingleStoreConnector/Protocol/ColumnType.cs +++ b/src/SingleStoreConnector/Protocol/ColumnType.cs @@ -24,7 +24,7 @@ internal enum ColumnType Bit = 16, Timestamp2 = 17, DateTime2 = 18, - GeographyPoint = 0xF3, + GeographyPoint = 0xF2, Geography = 0xF4, Json = 0xF5, NewDecimal = 0xF6, diff --git a/src/SingleStoreConnector/SingleStoreBulkCopy.cs b/src/SingleStoreConnector/SingleStoreBulkCopy.cs index e69d1c9a0..a7b079ca1 100644 --- a/src/SingleStoreConnector/SingleStoreBulkCopy.cs +++ b/src/SingleStoreConnector/SingleStoreBulkCopy.cs @@ -252,7 +252,7 @@ private async ValueTask WriteToServerAsync(IOBehavior if (schema[i].DataTypeName == "YEAR") { // the current code can't distinguish between 0 = 0000 and 0 = 2000 - throw new NotSupportedException("'YEAR' columns are not supported by MySqlBulkCopy."); + throw new NotSupportedException("'YEAR' columns are not supported by SingleStoreBulkCopy."); } Log.AddingDefaultColumnMapping(m_logger, i, destinationColumn); diff --git a/src/SingleStoreConnector/SingleStoreDataReader.cs b/src/SingleStoreConnector/SingleStoreDataReader.cs index 5ac369926..92ed68a5a 100644 --- a/src/SingleStoreConnector/SingleStoreDataReader.cs +++ b/src/SingleStoreConnector/SingleStoreDataReader.cs @@ -651,7 +651,7 @@ internal async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancel if ((m_behavior & CommandBehavior.CloseConnection) != 0) await connection.CloseAsync(ioBehavior).ConfigureAwait(false); - // clear fields (so that MySqlConnection can be GCed if the user doesn't hold a reference to it) + // clear fields (so that SingleStoreConnection can be GCed if the user doesn't hold a reference to it) Command = null; m_commandListPosition = default; m_payloadCreator = null; diff --git a/tests/SideBySide/BulkLoaderAsync.cs b/tests/SideBySide/BulkLoaderAsync.cs index 80f221e3b..a4c6e436d 100644 --- a/tests/SideBySide/BulkLoaderAsync.cs +++ b/tests/SideBySide/BulkLoaderAsync.cs @@ -341,7 +341,7 @@ public async Task BulkLoadFileStreamInvalidOperation() #if !BASELINE await Assert.ThrowsAsync(async () => { var rowCount = await bl.LoadAsync(); }); #else - await Assert.ThrowsAsync(async () => { var rowCount = await bl.LoadAsync(fileStream); }); + await Assert.ThrowsAsync(async () => { var rowCount = await bl.LoadAsync(fileStream); }); #endif } diff --git a/tests/SideBySide/BulkLoaderSync.cs b/tests/SideBySide/BulkLoaderSync.cs index b5cad0a51..79e499263 100644 --- a/tests/SideBySide/BulkLoaderSync.cs +++ b/tests/SideBySide/BulkLoaderSync.cs @@ -470,7 +470,7 @@ public void BulkLoadMemoryStreamInvalidOperation() #if !BASELINE Assert.Throws(() => bl.Load()); #else - Assert.Throws(() => bl.Load(memoryStream)); + Assert.Throws(() => bl.Load(memoryStream)); #endif } @@ -1220,7 +1220,7 @@ CREATE TABLE bulk_copy_year(int_value int NULL, year_value year NULL) dt.Rows.Add(2, 2001); var exception = Assert.Throws(() => bulkCopy.WriteToServer(dt)); - Assert.Equal("'YEAR' columns are not supported by MySqlBulkCopy.", exception.Message); + Assert.Equal("'YEAR' columns are not supported by SingleStoreBulkCopy.", exception.Message); } [Fact] diff --git a/tests/SideBySide/ClientFactoryTests.cs b/tests/SideBySide/ClientFactoryTests.cs index cacc33d68..955cd23c8 100644 --- a/tests/SideBySide/ClientFactoryTests.cs +++ b/tests/SideBySide/ClientFactoryTests.cs @@ -1,7 +1,3 @@ -#if BASELINE -using SingleStoreConnectorFactory = MySql.Data.MySqlClient.MySqlClientFactory; -#endif - namespace SideBySide; public class ClientFactoryTests diff --git a/tests/SingleStoreConnector.Tests/DbProviderFactoryTests.cs b/tests/SingleStoreConnector.Tests/DbProviderFactoryTests.cs index 0ccca30d2..1e5990212 100644 --- a/tests/SingleStoreConnector.Tests/DbProviderFactoryTests.cs +++ b/tests/SingleStoreConnector.Tests/DbProviderFactoryTests.cs @@ -1,7 +1,3 @@ -#if BASELINE -using SingleStoreConnectorFactory = MySql.Data.MySqlClient.MySqlClientFactory; -#endif - namespace SingleStoreConnector.Tests; public class DbProviderFactoryTests