diff --git a/.github/workflows/SideBySide/config-ssl.json b/.github/workflows/SideBySide/config-ssl.json index 79fa0ef22..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", + "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 4b6d5cb8c..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=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", "ManagedService": true } } diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 73ea2f35d..29476f662 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -1,11 +1,19 @@ name: SingleStore .NET Connector -on: [push] +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: [master] + tags: + - 'v*' + workflow_call: + workflow_dispatch: env: - DOTNET_VERSION: 9.0.303 - TARGET_FRAMEWORK: net9.0 - CONNECTOR_VERSION: 1.3.0 + DOTNET_VERSION: 10.0.x + 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 +59,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 +85,16 @@ 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 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-9.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..6621f4f4f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,14 +2,16 @@ true - preview + 14.0 true - $(NoWarn);1591;CA1708;CA1835;CA2215;CA5397;NU5105;SYSLIB0039 + + true + $(NoWarn);1573;1591;1712;CA1708;CA1835;CA2215;CA5397;NU5105;SYSLIB0039 + $(MSBuildThisFileDirectory)artifacts true low all - true diff --git a/Directory.Packages.props b/Directory.Packages.props index a01542f93..f579e9895 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,28 +8,29 @@ - + + - - + + - - + - + - + + 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/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/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..ad10061d3 --- /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 SingleStoreConnector. 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 ceb6dae8d..2315aabbe 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,12 @@ + + + + - + 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/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.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`. 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/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..de3eb9114 100644 --- a/src/SingleStoreConnector/Authentication/IAuthenticationPlugin.cs +++ b/src/SingleStoreConnector/Authentication/IAuthenticationPlugin.cs @@ -21,10 +21,10 @@ public interface IAuthenticationPlugin byte[] CreateResponse(string password, ReadOnlySpan authenticationData); } - /// /// is an extension to that returns a hash of the client's password. /// +[Obsolete("Use IAuthenticationPlugin3 instead.")] public interface IAuthenticationPlugin2 : IAuthenticationPlugin { /// @@ -37,3 +37,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/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/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/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/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/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..4ea5ae3a9 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); @@ -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..4c8e224cf 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 } } @@ -694,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); } } @@ -743,8 +785,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 +834,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 +891,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 +938,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 +959,7 @@ private async Task SendEncryptedPasswordAsync( RSAParameters rsaParameters; try { - rsaParameters = Utility.GetRsaParameters(rsaPublicKey); + rsaParameters = Utility.GetRsaParameters(Encoding.ASCII.GetString(rsaPublicKey)); } catch (Exception ex) { @@ -923,13 +986,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 +1001,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); @@ -972,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) { @@ -1243,6 +1306,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 +1735,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 +1873,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 +2012,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 +2209,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 +2225,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..13c257c8c 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; @@ -58,6 +61,7 @@ public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDict { commandListPosition.CommandIndex++; commandListPosition.PreparedStatementIndex = 0; + commandListPosition.PreparedStatements = null; } } return true; @@ -214,8 +218,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; 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..27abe20d2 100644 --- a/src/SingleStoreConnector/Core/TypeMapper.cs +++ b/src/SingleStoreConnector/Core/TypeMapper.cs @@ -110,6 +110,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 +179,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; } 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/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/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..a7b079ca1 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,30 @@ 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[]) || + (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 SingleStoreBulkCopy."); + } + Log.AddingDefaultColumnMapping(m_logger, i, destinationColumn); columnMappings.Add(new(i, destinationColumn)); } @@ -295,7 +263,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 +473,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..fcb2e8e7a 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; } @@ -749,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) @@ -1102,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; @@ -1151,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); @@ -1219,6 +1203,8 @@ private async ValueTask CreateSessionAsync(ConnectionPool? pool, internal IPEndPoint? SessionEndPoint => m_session!.IPEndPoint; + internal SingleStoreDataSource? SingleStoreDataSource => m_dataSource; + internal void SetState(ConnectionState newState) { if (m_connectionState != newState) 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/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..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; @@ -677,7 +677,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/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) 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"); 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/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..59aa2f122 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 SingleStoreDataSourceBuilder(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 == "SingleStoreConnector", + Sample = (ref ActivityCreationOptions options) => + options.TraceId == parentActivity.TraceId ? ActivitySamplingResult.AllData : ActivitySamplingResult.None, + ActivityStopped = x => activity = x, + }; + ActivitySource.AddActivityListener(listener); + + using (var command = new SingleStoreCommand("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..5e85d5d67 100644 --- a/tests/SideBySide/BatchTests.cs +++ b/tests/SideBySide/BatchTests.cs @@ -182,8 +182,8 @@ 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 +343,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..a4c6e436d 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); } } @@ -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 } @@ -649,11 +649,129 @@ public async Task BulkCopyNullDataReader() var bulkCopy = new SingleStoreBulkCopy(connection); await Assert.ThrowsAsync(async () => await bulkCopy.WriteToServerAsync(default(DbDataReader))); } + + [Fact] + public async Task BulkCopyGeographyAsync() + { + using var connection = new SingleStoreConnection(GetLocalConnectionString()); + await connection.OpenAsync(); + + var dataTable = new DataTable() + { + Columns = + { + new DataColumn("geo_data", typeof(SingleStoreGeography)), + }, + Rows = + { + 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 NOT NULL AUTO_INCREMENT PRIMARY KEY, + point_data GEOGRAPHYPOINT NOT NULL +);", connection)) + { + await cmd.ExecuteNonQueryAsync(); + } + + var bc = new SingleStoreBulkCopy(connection) + { + DestinationTableName = "bulk_load_data_table", + ColumnMappings = + { + new() + { + SourceOrdinal = 0, + DestinationColumn = "point_data", + }, + }, + }; + + 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(); 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..79e499263 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); } } @@ -469,7 +470,7 @@ public void BulkLoadMemoryStreamInvalidOperation() #if !BASELINE Assert.Throws(() => bl.Load()); #else - Assert.Throws(() => bl.Load(memoryStream)); + Assert.Throws(() => bl.Load(memoryStream)); #endif } @@ -673,7 +674,6 @@ public void BulkCopyDataTableWithTimeOnly() } #endif - public static IEnumerable GetBulkCopyData() => new object[][] { @@ -1066,7 +1066,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 +1120,7 @@ public void BulkCopyColumnMappingsInvalidSourceOrdinal() new object[] { 1 }, new object[] { 2 }, new object[] { 3 }, - } + }, }; Assert.Throws(() => bulkCopy.WriteToServer(dataTable)); @@ -1157,12 +1157,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 SingleStoreBulkCopy.", exception.Message); + } + [Fact] public void BulkCopyDoesNotInsertAllRows() { @@ -1174,7 +1234,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 +1249,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 +1370,124 @@ public void BulkCopyDataTableConflictOption(SingleStoreBulkLoaderConflictOption using (var cmd = new SingleStoreCommand("select b from bulk_load_data_table;", connection)) Assert.Equal(expected, cmd.ExecuteScalar()); } + + [Fact] + public void BulkCopyGeography() + { + using var connection = new SingleStoreConnection(GetLocalConnectionString()); + connection.Open(); + + var dataTable = new DataTable() + { + Columns = + { + new DataColumn("geo_data", typeof(SingleStoreGeography)), + }, + Rows = + { + 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)) + { + cmd.ExecuteNonQuery(); + } + + var bc = new SingleStoreBulkCopy(connection) + { + DestinationTableName = "bulk_load_data_table", + ColumnMappings = + { + new() + { + SourceOrdinal = 0, + DestinationColumn = "geo_data", + }, + }, + }; + + 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; @@ -1321,6 +1499,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..0987d6312 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,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 } @@ -120,7 +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,7 +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); } @@ -197,7 +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 @@ -354,8 +352,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,7 +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 } } @@ -376,7 +374,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 +388,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,7 +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 } } @@ -454,9 +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 } @@ -525,7 +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); } @@ -669,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 { @@ -682,7 +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 } } @@ -709,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 { @@ -721,12 +715,13 @@ 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 } } Assert.False(await reader.NextResultAsync()); } +#endif #endif private static CancellationToken GetCanceledToken() @@ -736,15 +731,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..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 @@ -24,7 +20,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..399ed839d 100644 --- a/tests/SideBySide/CommandTimeoutTests.cs +++ b/tests/SideBySide/CommandTimeoutTests.cs @@ -66,7 +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); @@ -92,7 +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); @@ -133,7 +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)] @@ -161,7 +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); @@ -191,7 +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); @@ -239,7 +239,6 @@ public async Task CommandTimeoutResetsOnReadAsync() Assert.Equal(ConnectionState.Open, m_connection.State); } - [Fact] public void TransactionCommandTimeoutWithSleepSync() { @@ -261,7 +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); @@ -293,6 +292,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..62668898a 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,7 +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")] @@ -152,7 +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 @@ -191,7 +188,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 +432,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 +445,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 +453,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 +492,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 +562,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..9211f250e 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++) @@ -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)); @@ -337,7 +338,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" })] @@ -444,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()); } @@ -565,7 +566,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 +723,7 @@ insert into date_time_kind(d, dt0, dt6) values(?, ?, ?)", connection) new() { Value = dateTimeIn }, new() { Value = dateTimeIn }, new() { Value = dateTimeIn }, - } + }, }; if (success) { @@ -749,11 +750,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 +1035,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 +1055,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 +1094,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); @@ -1181,7 +1178,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(); } @@ -1242,7 +1239,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 +1350,7 @@ 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("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 +1378,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 +1412,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 +1569,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) { @@ -1910,4 +1910,3 @@ private static int[] SplitAndParse(object obj) private SingleStoreConnectionStringBuilder CreateConnectionStringBuilder() => new(AppConfig.ConnectionString); } - diff --git a/tests/SideBySide/DataTypesFixture.cs b/tests/SideBySide/DataTypesFixture.cs index 32928f614..c6f17bd09 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,7 @@ insert into datatypes_json_core (value) ('{""a"": ""b""}'); "); } + 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..28fd3509d 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()); } @@ -460,40 +460,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 +513,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 +532,7 @@ enum SingleStoreColor Green, Blue, Indigo, - Violet + Violet, } [Fact] @@ -548,7 +547,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 +586,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/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/QueryTests.cs b/tests/SideBySide/QueryTests.cs index d2a74a562..ebb03baf3 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,13 @@ public void ServerDoesNotSendMariaDbCacheMetadataOrQueryAttributes() "Server should not send QueryAttributes capability flag."); } - class BoolTest + 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 +1697,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..973491506 100644 --- a/tests/SideBySide/ServerFeatures.cs +++ b/tests/SideBySide/ServerFeatures.cs @@ -40,4 +40,9 @@ 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, } 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/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..720df75e1 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)] @@ -737,6 +747,65 @@ public void SprocNameSpecialCharacters(string sprocName) } } +#if !BASELINE + [Theory] + [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 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)) + { + setup.ExecuteNonQuery(); + } + + 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 + private static string NormalizeSpaces(string input) { input = input.Replace('\r', ' '); @@ -758,5 +827,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..cbd60c633 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,5 @@ 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.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 9dae639b0..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.15, )", - "resolved": "8.0.15", - "contentHash": "wMf2N7fJ846aKd73R5gqvtbyqu89/LywlWCtMyXUqKYc9DR3s9kUgNrLIsT9KeRwyinGFJDtRbiib0M4YBX6ZA==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "yadTZIkStCVsG8nGwvfroSfBApPsgjQbodQyaIfp53dgayE0qhZpywixiCB6lx57JYQ+KVg1m1AFLrj54pxpZg==" }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.15, )", - "resolved": "8.0.15", - "contentHash": "s4eXlcRGyHeCgFUGQnhq0e/SCHBPp0jOHgMqZg3fQ2OCHJSm1aOUhI6RFWuVIcEb9ig2WgI2kWukk8wu72EbUQ==" + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA==" }, "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": { @@ -63,18 +77,34 @@ } } }, - "net9.0": { + "net10.0/linux-x64": { + "Microsoft.DotNet.ILCompiler": { + "type": "Direct", + "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": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "G85txKEuQ8s64BG9Pk3TbmE+cDgKReepnIPNfC1lNks4u2v5SkQhjCuuSAep+H2xtgFpYU7w9LFOF+vVtJZguA==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "VkChfi7e/mf6peVZaJqgP3taLHKuYGyEc1C0T1YsKJCGMe1pGlzJCp5WMbBkHtZoct7K8ksCLPncmzBA7Mrb+w==" }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "xUdlUxiFwXhTYhB4VxKg/IA0+jlZXJPo70LYuMryWbJHdonIpZjw+7DO2B0pWwpXIOs6MlH5WVXPEtfrGEcVZA==" + "requested": "[8.0.25, )", + "resolved": "8.0.25", + "contentHash": "sqX4nmBft05ivqKvUT4nxaN8rT3apCLt9SWFkfRrQPwra1zPwFknQAw1lleuMCKOCLvVmOWwrC2iPSm9RiXZUg==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", @@ -86,6 +116,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 +135,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": { @@ -124,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/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..45da9d555 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(Skip = "Resetting connection is not supported in SingleStore")] + 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/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 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/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/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/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/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/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'")] 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(); """); 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